From 5e6043aeb3b56953239bd80d49aca5fd94dcf1fa Mon Sep 17 00:00:00 2001 From: FlightControl Date: Fri, 24 Jun 2016 12:52:52 +0200 Subject: [PATCH] Progress reworking object model. --- Moose Development/Moose/Airbase.lua | 12 +- Moose Development/Moose/Controllable.lua | 13 +- Moose Development/Moose/Detection.lua | 281 +- Moose Development/Moose/FAC.lua | 123 +- Moose Development/Moose/Group.lua | 49 +- Moose Development/Moose/Identifiable.lua | 205 + Moose Development/Moose/Moose.lua | 4 + Moose Development/Moose/Object.lua | 213 + Moose Development/Moose/Positionable.lua | 213 + Moose Development/Moose/Static.lua | 11 +- Moose Development/Moose/Unit.lua | 389 +- .../l10n/DEFAULT/Moose.lua | 23234 +--------------- Moose Mission Setup/Moose.lua | 23234 +--------------- .../Moose_Test_FAC/Moose_Test_FAC.lua | 4 +- .../Moose_Test_FAC/Moose_Test_FAC.miz | Bin 183859 -> 45326 bytes .../Moose_Test_SPAWN/MOOSE_Test_SPAWN.miz | Bin 191907 -> 53365 bytes ...evelopment - Part 4 - Wrapper Classes.pptx | Bin 0 -> 738565 bytes 17 files changed, 1022 insertions(+), 46963 deletions(-) create mode 100644 Moose Development/Moose/Identifiable.lua create mode 100644 Moose Development/Moose/Object.lua create mode 100644 Moose Development/Moose/Positionable.lua create mode 100644 Moose Training/Presentations/DCS World - MOOSE - Development - Part 4 - Wrapper Classes.pptx diff --git a/Moose Development/Moose/Airbase.lua b/Moose Development/Moose/Airbase.lua index 87ccf2ba9..15c4fc7d5 100644 --- a/Moose Development/Moose/Airbase.lua +++ b/Moose Development/Moose/Airbase.lua @@ -2,8 +2,8 @@ -- -- === -- --- 1) @{Airbase#AIRBASE} class, extends @{Base#BASE} --- ================================================= +-- 1) @{Airbase#AIRBASE} class, extends @{Positionable#POSITIONABLE} +-- ================================================================= -- The @{AIRBASE} class is a wrapper class to handle the DCS Airbase objects: -- -- * Support all DCS Airbase APIs. @@ -49,7 +49,7 @@ --- The AIRBASE class -- @type AIRBASE --- @extends Base#BASE +-- @extends Positionable#POSITIONABLE AIRBASE = { ClassName="AIRBASE", CategoryName = { @@ -63,13 +63,11 @@ AIRBASE = { --- Create a new AIRBASE from DCSAirbase. -- @param #AIRBASE self --- @param DCSAirbase#Airbase DCSAirbase --- @param Database#DATABASE Database +-- @param #string AirbaseName The name of the airbase. -- @return Airbase#AIRBASE function AIRBASE:Register( AirbaseName ) - local self = BASE:Inherit( self, BASE:New() ) - self:F2( AirbaseName ) + local self = BASE:Inherit( self, POSITIONABLE:New( AirbaseName ) ) self.AirbaseName = AirbaseName return self end diff --git a/Moose Development/Moose/Controllable.lua b/Moose Development/Moose/Controllable.lua index f83217f12..bbab284be 100644 --- a/Moose Development/Moose/Controllable.lua +++ b/Moose Development/Moose/Controllable.lua @@ -1,6 +1,6 @@ --- This module contains the CONTROLLABLE class. -- --- 1) @{Controllable#CONTROLLABLE} class, extends @{Base#BASE} +-- 1) @{Controllable#CONTROLLABLE} class, extends @{Positionable#POSITIONABLE} -- =========================================================== -- The @{Controllable#CONTROLLABLE} class is a wrapper class to handle the DCS Controllable objects: -- @@ -127,28 +127,21 @@ --- The CONTROLLABLE class -- @type CONTROLLABLE --- @extends Base#BASE +-- @extends Positionable#POSITIONABLE -- @field DCSControllable#Controllable DCSControllable The DCS controllable class. -- @field #string ControllableName The name of the controllable. CONTROLLABLE = { ClassName = "CONTROLLABLE", ControllableName = "", - ControllableID = 0, - Controller = nil, - DCSControllable = nil, WayPointFunctions = {}, } ---- A DCSControllable --- @type DCSControllable --- @field id_ The ID of the controllable in DCS - --- Create a new CONTROLLABLE from a DCSControllable -- @param #CONTROLLABLE self -- @param DCSControllable#Controllable ControllableName The DCS Controllable name -- @return #CONTROLLABLE self function CONTROLLABLE:New( ControllableName ) - local self = BASE:Inherit( self, BASE:New() ) + local self = BASE:Inherit( self, POSITIONABLE:New( ControllableName ) ) self:F2( ControllableName ) self.ControllableName = ControllableName return self diff --git a/Moose Development/Moose/Detection.lua b/Moose Development/Moose/Detection.lua index 3c8f1c08f..7e1e82b69 100644 --- a/Moose Development/Moose/Detection.lua +++ b/Moose Development/Moose/Detection.lua @@ -7,17 +7,17 @@ -- The @{Detection#DETECTION_BASE} class defines the core functions to administer detected objects. -- Detected objects are grouped in SETS of UNITS. -- --- 1.1) DETECTION constructor: --- ---------------------------- +-- 1.1) DETECTION_BASE constructor: +-- -------------------------------- -- * @{Detection#DETECTION.New}(): Create a new DETECTION object. -- --- 1.2) DETECTION initialization: --- ------------------------------ --- By default, detection will return detected units with all the methods available. --- However, you can specify which units it found with specific detection methods. --- If you use one of the below functions, the detection will work with the detection method specified. +-- 1.2) DETECTION_BASE initialization: +-- ----------------------------------- +-- By default, detection will return detected objects with all the methods available. +-- However, you can ask how the objects were found with specific detection methods. +-- If you use one of the below methods, the detection will work with the detection method specified. -- You can specify to apply multiple detection methods. --- Use the following functions to report the units it detected using the methods Visual, Optical, Radar, IRST, RWR, DLINK: +-- Use the following functions to report the objects it detected using the methods Visual, Optical, Radar, IRST, RWR, DLINK: -- -- * @{Detection#DETECTION.InitDetectVisual}(): Detected using Visual. -- * @{Detection#DETECTION.InitDetectOptical}(): Detected using Optical. @@ -26,6 +26,17 @@ -- * @{Detection#DETECTION.InitDetectRWR}(): Detected using RWR. -- * @{Detection#DETECTION.InitDetectDLINK}(): Detected using DLINK. -- +-- 1.3) Obtain objects detected by DETECTION_BASE: +-- ----------------------------------------------- +-- DETECTION_BASE builds @{Set}s of objects detected. These @{Set#SET_BASE}s can be retrieved using the method @{Detection#DETECTION_BASE.GetDetectedSets}(). +-- The method will return a list (table) of @{Set#SET_BASE} objects. +-- +-- 2) @{Detection#DETECTION_UNITGROUPS} class, extends @{Detection#DETECTION_BASE} +-- =============================================================================== +-- The @{Detection#DETECTION_UNITGROUPS} class will detect units within the battle zone for a FAC group, +-- and will build a list (table) of @{Set#SET_UNIT}s containing the @{Unit#UNIT}s detected. +-- +-- -- === -- -- @module Detection @@ -39,29 +50,31 @@ -- @field Group#GROUP FACGroup The GROUP in the Forward Air Controller role. -- @field DCSTypes#Distance DetectionRange The range till which targets are accepted to be detected. -- @field DCSTypes#Distance DetectionZoneRange The range till which targets are grouped upon the first detected target. --- @field #DETECTION_BASE.DetectedUnitSets DetectedUnitSets A list of @{Set#SET_UNIT}s containing the units in each set that were detected within a DetectedZoneRange. --- @field #DETECTION_BASE.DetectedUnitZones DetectedUnitZones A list of @{Zone#ZONE_UNIT}s containing the zones of the reference detected units. +-- @field #DETECTION_BASE.DetectedSets DetectedSets A list of @{Set#SET_BASE}s containing the objects in each set that were detected within a DetectedZoneRange. +-- @field #DETECTION_BASE.DetectedZones DetectedZones A list of @{Zone#ZONE_BASE}s containing the zones of the reference detected objects. -- @extends Set#SET_BASE DETECTION_BASE = { ClassName = "DETECTION_BASE", - DetectedUnitSets = {}, - DetectedUnitZones = {}, - DetectedUnits = {}, + DetectedSets = {}, + DetectedObjects = {}, FACGroup = nil, DetectionRange = nil, DetectionZoneRange = nil, } ---- @type DETECTION_BASE.DetectedUnitSets --- @list +--- @type DETECTION_BASE.DetectedSets +-- @list ---- @type DETECTION_BASE.DetectedUnitZones --- @list +--- @type DETECTION_BASE.DetectedZones +-- @list --- DETECTION constructor. -- @param #DETECTION_BASE self +-- @param Group#GROUP FACGroup The GROUP in the Forward Air Controller role. +-- @param DCSTypes#Distance DetectionRange The range till which targets are accepted to be detected. +-- @param DCSTypes#Distance DetectionZoneRange The range till which targets are grouped upon the first detected target. -- @return #DETECTION_BASE self function DETECTION_BASE:New( FACGroup, DetectionRange, DetectionZoneRange ) @@ -152,45 +165,56 @@ function DETECTION_BASE:GetFACGroup() return self.FACGroup end ---- Get the detected @{Set#SET_UNIT}s. +--- Get the detected @{Set#SET_BASE}s. -- @param #DETECTION_BASE self --- @return #DETECTION_BASE.DetectedUnitSets DetectedUnitSets -function DETECTION_BASE:GetDetectionUnitSets() +-- @return #DETECTION_BASE.DetectedSets DetectedSets +function DETECTION_BASE:GetDetectionSets() - local DetectionUnitSets = self.DetectedUnitSets - return DetectionUnitSets + local DetectionSets = self.DetectedSets + return DetectionSets end ---- Get the amount of SETs with detected units. +--- Get the amount of SETs with detected objects. -- @param #DETECTION_BASE self -- @return #number Count -function DETECTION_BASE:GetDetectionUnitSetCount() +function DETECTION_BASE:GetDetectionSetCount() - local DetectionUnitSetCount = #self.DetectedUnitSets - return DetectionUnitSetCount + local DetectionSetCount = #self.DetectedSets + return DetectionSetCount end ---- Get a SET of detected units using a given numeric index. +--- Get a SET of detected objects using a given numeric index. -- @param #DETECTION_BASE self -- @param #number Index --- @return Set#SET_UNIT -function DETECTION_BASE:GetDetectionUnitSet( Index ) +-- @return Set#SET_BASE +function DETECTION_BASE:GetDetectionSet( Index ) - local DetectionUnitSet = self.DetectedUnitSets[Index] - if DetectionUnitSet then - return DetectionUnitSet + local DetectionSet = self.DetectedSets[Index] + if DetectionSet then + return DetectionSet end return nil end ---- Form @{Set}s of detected @{Unit#UNIT}s in an array of @{Set#SET_UNIT}s. +--- Make a DetectionSet table. This function will be overridden in the derived clsses. +-- @param #DETECTION_BASE self +-- @return #DETECTION_BASE self +function DETECTION_BASE:CreateDetectionSets() + self:F2() + + self:E( "Error, in DETECTION_BASE class..." ) + +end + +--- Form @{Set}s of detected @{Unit#UNIT}s in an array of @{Set#SET_BASE}s. -- @param #DETECTION_BASE self function DETECTION_BASE:_DetectionScheduler( SchedulerName ) self:F2( { SchedulerName } ) - self.DetectedUnitSets = {} - self.DetectedUnitZones = {} + self.DetectedObjects = {} + self.DetectedSets = {} + self.DetectedZones = {} if self.FACGroup:IsAlive() then local FACGroupName = self.FACGroup:GetName() @@ -205,98 +229,149 @@ function DETECTION_BASE:_DetectionScheduler( SchedulerName ) ) for FACDetectedTargetID, FACDetectedTarget in pairs( FACDetectedTargets ) do - local FACObject = FACDetectedTarget.object + local FACObject = FACDetectedTarget.object -- DCSObject#Object self:T2( FACObject ) if FACObject and FACObject:isExist() and FACObject.id_ < 50000000 then - local FACDetectedUnit = UNIT:Find( FACObject ) - local FACDetectedUnitName = FACDetectedUnit:GetName() + local FACDetectedObjectName = FACObject:getName() - local FACDetectedUnitPositionVec3 = FACDetectedUnit:GetPointVec3() + local FACDetectedObjectPositionVec3 = FACObject:getPoint() local FACGroupPositionVec3 = self.FACGroup:GetPointVec3() - local Distance = ( ( FACDetectedUnitPositionVec3.x - FACGroupPositionVec3.x )^2 + - ( FACDetectedUnitPositionVec3.y - FACGroupPositionVec3.y )^2 + - ( FACDetectedUnitPositionVec3.z - FACGroupPositionVec3.z )^2 + + local Distance = ( ( FACDetectedObjectPositionVec3.x - FACGroupPositionVec3.x )^2 + + ( FACDetectedObjectPositionVec3.y - FACGroupPositionVec3.y )^2 + + ( FACDetectedObjectPositionVec3.z - FACGroupPositionVec3.z )^2 ) ^ 0.5 / 1000 - self:T( { FACGroupName, FACDetectedUnitName, Distance } ) + self:T( { FACGroupName, FACDetectedObjectName, Distance } ) if Distance <= self.DetectionRange then - if not self.DetectedUnits[FACDetectedUnitName] then - self.DetectedUnits[FACDetectedUnitName] = {} + if not self.DetectedObjects[FACDetectedObjectName] then + self.DetectedObjects[FACDetectedObjectName] = {} end - self.DetectedUnits[FACDetectedUnitName].DetectedUnit = UNIT:FindByName( FACDetectedUnitName ) - self.DetectedUnits[FACDetectedUnitName].Visible = FACDetectedTarget.visible - self.DetectedUnits[FACDetectedUnitName].Type = FACDetectedTarget.type - self.DetectedUnits[FACDetectedUnitName].Distance = FACDetectedTarget.distance + self.DetectedObjects[FACDetectedObjectName].Name = FACDetectedObjectName + self.DetectedObjects[FACDetectedObjectName].Visible = FACDetectedTarget.visible + self.DetectedObjects[FACDetectedObjectName].Type = FACDetectedTarget.type + self.DetectedObjects[FACDetectedObjectName].Distance = FACDetectedTarget.distance else -- if beyond the DetectionRange then nullify... - if self.DetectedUnits[FACDetectedUnitName] then - self.DetectedUnits[FACDetectedUnitName] = nil + if self.DetectedObjects[FACDetectedObjectName] then + self.DetectedObjects[FACDetectedObjectName] = nil end end end end + + self:T2( self.DetectedObjects ) - -- okay, now we have a list of detected unit names ... + -- okay, now we have a list of detected object names ... -- Sort the table based on distance ... - self:T( { "Sorting DetectedUnits table:", self.DetectedUnits } ) - table.sort( self.DetectedUnits, function( a, b ) return a.Distance < b.Distance end ) - self:T( { "Sorted Targets Table:", self.DetectedUnits } ) + self:T( { "Sorting DetectedObjects table:", self.DetectedObjects } ) + table.sort( self.DetectedObjects, function( a, b ) return a.Distance < b.Distance end ) + self:T( { "Sorted Targets Table:", self.DetectedObjects } ) - -- Now group the DetectedUnits table into SET_UNITs, evaluating the DetectionZoneRange. + -- Now group the DetectedObjects table into SET_BASEs, evaluating the DetectionZoneRange. - if self.DetectedUnits then - for DetectedUnitName, DetectedUnitData in pairs( self.DetectedUnits ) do - local DetectedUnit = DetectedUnitData.DetectedUnit -- Unit#UNIT - if DetectedUnit and DetectedUnit:IsAlive() then - self:T( DetectedUnit:GetName() ) - if #self.DetectedUnitSets == 0 then - self:T( { "Adding Unit Set #", 1 } ) - self.DetectedUnitZones[1] = ZONE_UNIT:New( DetectedUnitName, DetectedUnit, self.DetectionZoneRange ) - self.DetectedUnitSets[1] = SET_UNIT:New() - self.DetectedUnitSets[1]:AddUnit( DetectedUnit ) - else - local AddedToSet = false - for DetectedZoneIndex = 1, #self.DetectedUnitZones do - self:T( "Detected Unit Set #" .. DetectedZoneIndex ) - local DetectedUnitSet = self.DetectedUnitSets[DetectedZoneIndex] -- Set#SET_UNIT - DetectedUnitSet:Flush() - local DetectedZone = self.DetectedUnitZones[DetectedZoneIndex] -- Zone#ZONE_UNIT - if DetectedUnit:IsInZone( DetectedZone ) then - self:T( "Adding to Unit Set #" .. DetectedZoneIndex ) - DetectedUnitSet:AddUnit( DetectedUnit ) - AddedToSet = true - end - end - if AddedToSet == false then - local DetectedZoneIndex = #self.DetectedUnitZones + 1 - self:T( "Adding new zone #" .. DetectedZoneIndex ) - self.DetectedUnitZones[DetectedZoneIndex] = ZONE_UNIT:New( DetectedUnitName, DetectedUnit, self.DetectionZoneRange ) - self.DetectedUnitSets[DetectedZoneIndex] = SET_UNIT:New() - self.DetectedUnitSets[DetectedZoneIndex]:AddUnit( DetectedUnit ) - end - end - end - end + if self.DetectedObjects then + self:CreateDetectionSets() end - -- Now all the tests should have been build, now make some smoke and flares... - - for DetectedZoneIndex = 1, #self.DetectedUnitZones do - local DetectedUnitSet = self.DetectedUnitSets[DetectedZoneIndex] -- Set#SET_UNIT - local DetectedZone = self.DetectedUnitZones[DetectedZoneIndex] -- Zone#ZONE_UNIT - self:T( "Detected Set #" .. DetectedZoneIndex ) - DetectedUnitSet:ForEachUnit( - --- @param Unit#UNIT DetectedUnit - function( DetectedUnit ) - self:T( DetectedUnit:GetName() ) - DetectedUnit:FlareRed() + + end +end + + + +--- DETECTION_UNITGROUPS class +-- @type DETECTION_UNITGROUPS +-- @field #DETECTION_UNITGROUPS.DetectedSets DetectedSets A list of @{Set#SET_UNIT}s containing the units in each set that were detected within a DetectedZoneRange. +-- @field #DETECTION_UNITGROUPS.DetectedZones DetectedZones A list of @{Zone#ZONE_UNIT}s containing the zones of the reference detected units. +-- @extends Set#SET_BASE +DETECTION_UNITGROUPS = { + ClassName = "DETECTION_UNITGROUPS", + DetectedZones = {}, +} + +--- @type DETECTION_UNITGROUPS.DetectedSets +-- @list + + +--- @type DETECTION_UNITGROUPS.DetectedZones +-- @list + + +--- DETECTION_UNITGROUPS constructor. +-- @param #DETECTION_UNITGROUPS self +-- @field Group#GROUP FACGroup The GROUP in the Forward Air Controller role. +-- @field DCSTypes#Distance DetectionRange The range till which targets are accepted to be detected. +-- @field DCSTypes#Distance DetectionZoneRange The range till which targets are grouped upon the first detected target. +-- @return #DETECTION_UNITGROUPS self +function DETECTION_UNITGROUPS:New( FACGroup, DetectionRange, DetectionZoneRange ) + + -- Inherits from DETECTION_BASE + local self = BASE:Inherit( self, DETECTION_BASE:New( FACGroup, DetectionRange, DetectionZoneRange ) ) + + return self +end + + +--- Make a DetectionSet table. This function will be overridden in the derived clsses. +-- @param #DETECTION_UNITGROUPS self +-- @return #DETECTION_UNITGROUPS self +function DETECTION_UNITGROUPS:CreateDetectionSets() + self:F2() + + for DetectedUnitName, DetectedUnitData in pairs( self.DetectedObjects ) do + local DetectedUnit = UNIT:FindByName( DetectedUnitData.Name ) -- Unit#UNIT + if DetectedUnit and DetectedUnit:IsAlive() then + self:T( DetectedUnit:GetName() ) + if #self.DetectedSets == 0 then + self:T( { "Adding Unit Set #", 1 } ) + self.DetectedZones[1] = ZONE_UNIT:New( DetectedUnitName, DetectedUnit, self.DetectionZoneRange ) + self.DetectedSets[1] = SET_BASE:New() + self.DetectedSets[1]:AddUnit( DetectedUnit ) + else + local AddedToSet = false + for DetectedZoneIndex = 1, #self.DetectedZones do + self:T( "Detected Unit Set #" .. DetectedZoneIndex ) + local DetectedUnitSet = self.DetectedSets[DetectedZoneIndex] -- Set#SET_BASE + DetectedUnitSet:Flush() + local DetectedZone = self.DetectedZones[DetectedZoneIndex] -- Zone#ZONE_UNIT + if DetectedUnit:IsInZone( DetectedZone ) then + self:T( "Adding to Unit Set #" .. DetectedZoneIndex ) + DetectedUnitSet:AddUnit( DetectedUnit ) + AddedToSet = true + end end - ) - DetectedZone:FlareZone( POINT_VEC3.SmokeColor.White, 30, math.random( 0,90 ) ) + if AddedToSet == false then + local DetectedZoneIndex = #self.DetectedZones + 1 + self:T( "Adding new zone #" .. DetectedZoneIndex ) + self.DetectedZones[DetectedZoneIndex] = ZONE_UNIT:New( DetectedUnitName, DetectedUnit, self.DetectionZoneRange ) + self.DetectedSets[DetectedZoneIndex] = SET_BASE:New() + self.DetectedSets[DetectedZoneIndex]:AddUnit( DetectedUnit ) + end + end end end -end \ No newline at end of file + + -- Now all the tests should have been build, now make some smoke and flares... + + for DetectedZoneIndex = 1, #self.DetectedZones do + local DetectedUnitSet = self.DetectedSets[DetectedZoneIndex] -- Set#SET_BASE + local DetectedZone = self.DetectedZones[DetectedZoneIndex] -- Zone#ZONE_UNIT + self:T( "Detected Set #" .. DetectedZoneIndex ) + DetectedUnitSet:ForEachUnit( + --- @param Unit#UNIT DetectedUnit + function( DetectedUnit ) + self:T( DetectedUnit:GetName() ) + DetectedUnit:FlareRed() + end + ) + DetectedZone:FlareZone( POINT_VEC3.SmokeColor.White, 30, math.random( 0,90 ) ) + end + +end + + diff --git a/Moose Development/Moose/FAC.lua b/Moose Development/Moose/FAC.lua index f276c4e33..1f101f30b 100644 --- a/Moose Development/Moose/FAC.lua +++ b/Moose Development/Moose/FAC.lua @@ -11,17 +11,31 @@ -- -- Detected objects are grouped in SETS of UNITS. -- --- 1.1) FAC constructor: +-- 1.1) FAC_BASE constructor: -- ---------------------------- --- * @{Fac#FAC.New}(): Create a new FAC object. +-- * @{Fac#FAC_BASE.New}(): Create a new FAC_BASE instance. -- --- 1.2) FAC initialization: --- ------------------------------ +-- 1.2) FAC_BASE reporting: +-- ------------------------ +-- Derived FAC_BASE classes will reports detected units using the method @{Fac#FAC_BASE.ReportDetected}(). This method implements polymorphic behaviour. +-- The time interval in seconds of the reporting can be changed using the methods @{Fac#FAC_BASE.SetReportInterval}(). +-- Reporting can be started and stopped using the methods @{Fac#FAC_BASE.StartReporting}() and @{Fac#FAC_BASE.StopReporting}() respectively. +-- If an ad-hoc report is requested, use the method @{Fac#FAC_BASE#ReportNow}(). -- -- === -- +-- 2) @{Fac#FAC_REPORTING} class, extends @{Fac#FAC_BASE} +-- ====================================================== +-- The @{Fac#FAC_REPORTING} class implements detected units reporting. Reporting can be controlled using the reporting methods available in the @{Fac#FAC_BASE} class. +-- +-- 2.1) FAC_REPORTING constructor: +-- ------------------------------- +-- * @{Fac#FAC_REPORTING.New}(): Create a new FAC_REPORTING instance. +-- +-- === +-- -- @module Fac --- @author Mechanic : Concept & Testing +-- @author Mechanic, Prof_Hilactic, FlightControl : Concept & Testing -- @author FlightControl : Design & Programming @@ -55,6 +69,17 @@ function FAC_BASE:New( ClientSet, Detection ) return self end +--- Reports the detected items to the @{Set#SET_CLIENT}. +-- @param #FAC_BASE self +-- @param Set#SET_BASE DetectedSets The detected Sets created by the @{Detection#DETECTION_BASE} object. +-- @return #FAC_BASE self +function FAC_BASE:ReportDetected( DetectedSets ) + self:F2() + + + +end + --- Report the detected @{Unit#UNIT}s detected within the @{DetectION#DETECTION_BASE} object to the @{Set#SET_CLIENT}s. -- @param #FAC_BASE self @@ -65,33 +90,71 @@ function FAC_BASE:_FacScheduler( SchedulerName ) --- @param Client#CLIENT Client function( Client ) if Client:IsAlive() then - local DetectedUnitSets = self.Detection:GetDetectionUnitSets() - local DetectedMsg = { } - for DetectedUnitSetID, DetectedUnitSet in pairs( DetectedUnitSets ) do - local UnitSet = DetectedUnitSet -- Set#SET_UNIT - local MT = {} -- Message Text - local UnitTypes = {} - for DetectedUnitID, DetectedUnitData in pairs( UnitSet:GetSet() ) do - local DetectedUnit = DetectedUnitData -- Unit#UNIT - local UnitType = DetectedUnit:GetTypeName() - if not UnitTypes[UnitType] then - UnitTypes[UnitType] = 1 - else - UnitTypes[UnitType] = UnitTypes[UnitType] + 1 - end - end - for UnitTypeID, UnitType in pairs( UnitTypes ) do - MT[#MT+1] = UnitType .. " of " .. UnitTypeID - end - local MessageText = table.concat( MT, ", " ) - DetectedMsg[#DetectedMsg+1] = " - Group #" .. DetectedUnitSetID .. ": " .. MessageText - end - local FACGroup = self.Detection:GetFACGroup() - FACGroup:MessageToClient( "Reporting detected target groups:\n" .. table.concat( DetectedMsg, "\n" ), 12, Client ) + local DetectedSets = self.Detection:GetDetectionSets() + return self:ReportDetected( Client, DetectedSets ) end - return true end ) return true -end \ No newline at end of file +end + +-- FAC_REPORTING + +--- FAC_REPORTING class. +-- @type FAC_REPORTING +-- @field Set#SET_CLIENT ClientSet The clients to which the FAC will report to. +-- @field Detection#DETECTION_BASE Detection The DETECTION_BASE object that is used to report the detected objects. +-- @extends Set#SET_BASE +FAC_REPORTING = { + ClassName = "FAC_REPORTING", +} + +--- FAC_REPORTING constructor. +-- @param #FAC_REPORTING self +-- @param Set#SET_CLIENT ClientSet +-- @param Detection#DETECTION_BASE Detection +-- @return #FAC_REPORTING self +function FAC_REPORTING:New( ClientSet, Detection ) + + -- Inherits from FAC_BASE + local self = BASE:Inherit( self, FAC_BASE:New( ClientSet, Detection ) ) + + return self +end + +--- Reports the detected items to the @{Set#SET_CLIENT}. +-- @param #FAC_REPORTING self +-- @param Client#CLIENT Client The @{Client} object to where the report needs to go. +-- @param Set#SET_BASE DetectedSets The detected Sets created by the @{Detection#DETECTION_BASE} object. +-- @return #boolean Return true if you want the reporting to continue... false will cancel the reporting loop. +function FAC_REPORTING:ReportDetected( Client, DetectedSets ) + self:F2( Client ) + DetectedSets:Flush() + + local DetectedMsg = {} + for DetectedUnitSetID, DetectedUnitSet in pairs( DetectedSets ) do + local UnitSet = DetectedUnitSet -- Set#SET_UNIT + local MT = {} -- Message Text + local UnitTypes = {} + for DetectedUnitID, DetectedUnitData in pairs( UnitSet:GetSet() ) do + local DetectedUnit = DetectedUnitData -- Unit#UNIT + local UnitType = DetectedUnit:GetTypeName() + if not UnitTypes[UnitType] then + UnitTypes[UnitType] = 1 + else + UnitTypes[UnitType] = UnitTypes[UnitType] + 1 + end + end + for UnitTypeID, UnitType in pairs( UnitTypes ) do + MT[#MT+1] = UnitType .. " of " .. UnitTypeID + end + local MessageText = table.concat( MT, ", " ) + DetectedMsg[#DetectedMsg+1] = " - Group #" .. DetectedUnitSetID .. ": " .. MessageText + end + local FACGroup = self.Detection:GetFACGroup() + FACGroup:MessageToClient( "Reporting detected target groups:\n" .. table.concat( DetectedMsg, "\n" ), 12, Client ) + + return true +end + diff --git a/Moose Development/Moose/Group.lua b/Moose Development/Moose/Group.lua index f1a146a74..5792cad84 100644 --- a/Moose Development/Moose/Group.lua +++ b/Moose Development/Moose/Group.lua @@ -150,21 +150,11 @@ --- The GROUP class -- @type GROUP -- @extends Controllable#CONTROLLABLE --- @field DCSGroup#Group DCSGroup The DCS group class. -- @field #string GroupName The name of the group. GROUP = { ClassName = "GROUP", - GroupName = "", - GroupID = 0, - Controller = nil, - DCSGroup = nil, - WayPointFunctions = {}, } ---- A DCSGroup --- @type DCSGroup --- @field id_ The ID of the group in DCS - --- Create a new GROUP from a DCSGroup -- @param #GROUP self -- @param DCSGroup#Group GroupName The DCS Group name @@ -327,40 +317,6 @@ function GROUP:GetCountry() return nil end ---- Returns the name of the DCS Group. --- @param #GROUP self --- @return #string The DCS Group name. -function GROUP:GetName() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupName = DCSGroup:getName() - self:T3( GroupName ) - return GroupName - end - - return nil -end - ---- Returns the DCS Group identifier. --- @param #GROUP self --- @return #number The identifier of the DCS Group. -function GROUP:GetID() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupID = DCSGroup:getID() - self:T3( GroupID ) - return GroupID - 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 @@ -517,11 +473,14 @@ function GROUP:GetCallsign() end --- Returns the current point (Vec2 vector) of the first DCS Unit in the DCS Group. +-- @param #GROUP self -- @return DCSTypes#Vec2 Current Vec2 point of the first DCS Unit of the DCS Group. function GROUP:GetPointVec2() self:F2( self.GroupName ) - local GroupPointVec2 = self:GetUnit(1):GetPointVec2() + local UnitPoint = self:GetUnit(1) + UnitPoint:GetPointVec2() + local GroupPointVec2 = UnitPoint:GetPointVec2() self:T3( GroupPointVec2 ) return GroupPointVec2 end diff --git a/Moose Development/Moose/Identifiable.lua b/Moose Development/Moose/Identifiable.lua new file mode 100644 index 000000000..82ca95e15 --- /dev/null +++ b/Moose Development/Moose/Identifiable.lua @@ -0,0 +1,205 @@ +--- This module contains the IDENTIFIABLE class. +-- +-- 1) @{Identifiable#IDENTIFIABLE} class, extends @{Object#OBJECT} +-- =============================================================== +-- The @{Identifiable#IDENTIFIABLE} class is a wrapper class to handle the DCS Identifiable objects: +-- +-- * Support all DCS Identifiable APIs. +-- * Enhance with Identifiable specific APIs not in the DCS Identifiable API set. +-- * Manage the "state" of the DCS Identifiable. +-- +-- 1.1) IDENTIFIABLE constructor: +-- ------------------------------ +-- The IDENTIFIABLE class provides the following functions to construct a IDENTIFIABLE instance: +-- +-- * @{Identifiable#IDENTIFIABLE.New}(): Create a IDENTIFIABLE instance. +-- +-- 1.2) IDENTIFIABLE methods: +-- -------------------------- +-- The following methods can be used to identify an identifiable object: +-- +-- * @{Identifiable#IDENTIFIABLE.GetName}(): Returns the name of the Identifiable. +-- * @{Identifiable#IDENTIFIABLE.IsAlive}(): Returns if the Identifiable is alive. +-- * @{Identifiable#IDENTIFIABLE.GetTypeName}(): Returns the type name of the Identifiable. +-- * @{Identifiable#IDENTIFIABLE.GetCoalition}(): Returns the coalition of the Identifiable. +-- * @{Identifiable#IDENTIFIABLE.GetCountry}(): Returns the country of the Identifiable. +-- * @{Identifiable#IDENTIFIABLE.GetDesc}(): Returns the descriptor structure of the Identifiable. +-- +-- +-- === +-- +-- @module Identifiable +-- @author FlightControl + +--- The IDENTIFIABLE class +-- @type IDENTIFIABLE +-- @extends Object#OBJECT +-- @field #string IdentifiableName The name of the identifiable. +IDENTIFIABLE = { + ClassName = "IDENTIFIABLE", + IdentifiableName = "", +} + +local _CategoryName = { + [Unit.Category.AIRPLANE] = "Airplane", + [Unit.Category.HELICOPTER] = "Helicoper", + [Unit.Category.GROUND_UNIT] = "Ground Identifiable", + [Unit.Category.SHIP] = "Ship", + [Unit.Category.STRUCTURE] = "Structure", + } + +--- Create a new IDENTIFIABLE from a DCSIdentifiable +-- @param #IDENTIFIABLE self +-- @param DCSIdentifiable#Identifiable IdentifiableName The DCS Identifiable name +-- @return #IDENTIFIABLE self +function IDENTIFIABLE:New( IdentifiableName ) + local self = BASE:Inherit( self, BASE:New() ) + self:F2( IdentifiableName ) + self.IdentifiableName = IdentifiableName + return self +end + +--- Returns if the Identifiable is alive. +-- @param Identifiable#IDENTIFIABLE self +-- @return #boolean true if Identifiable is alive. +-- @return #nil The DCS Identifiable is not existing or alive. +function IDENTIFIABLE:IsAlive() + self:F2( self.IdentifiableName ) + + local DCSIdentifiable = self:GetDCSObject() + + if DCSIdentifiable then + local IdentifiableIsAlive = DCSIdentifiable:isExist() + return IdentifiableIsAlive + end + + return false +end + + + + +--- Returns DCS Identifiable object name. +-- The function provides access to non-activated objects too. +-- @param Identifiable#IDENTIFIABLE self +-- @return #string The name of the DCS Identifiable. +-- @return #nil The DCS Identifiable is not existing or alive. +function IDENTIFIABLE:GetName() + self:F2( self.IdentifiableName ) + + local DCSIdentifiable = self:GetDCSObject() + + if DCSIdentifiable then + local IdentifiableName = self.IdentifiableName + return IdentifiableName + end + + self:E( self.ClassName .. " " .. self.IdentifiableName .. " not found!" ) + return nil +end + + +--- Returns the type name of the DCS Identifiable. +-- @param Identifiable#IDENTIFIABLE self +-- @return #string The type name of the DCS Identifiable. +-- @return #nil The DCS Identifiable is not existing or alive. +function IDENTIFIABLE:GetTypeName() + self:F2( self.IdentifiableName ) + + local DCSIdentifiable = self:GetDCSObject() + + if DCSIdentifiable then + local IdentifiableTypeName = DCSIdentifiable:getTypeName() + self:T3( IdentifiableTypeName ) + return IdentifiableTypeName + end + + self:E( self.ClassName .. " " .. self.IdentifiableName .. " not found!" ) + return nil +end + + + + +--- Returns the DCS Identifiable category name as defined within the DCS Identifiable Descriptor. +-- @param Identifiable#IDENTIFIABLE self +-- @return #string The DCS Identifiable Category Name +function IDENTIFIABLE:GetCategoryName() + local DCSIdentifiable = self:GetDCSObject() + + if DCSIdentifiable then + local IdentifiableCategoryName = _CategoryName[ self:GetDesc().category ] + return IdentifiableCategoryName + end + + self:E( self.ClassName .. " " .. self.IdentifiableName .. " not found!" ) + return nil +end + +--- Returns coalition of the Identifiable. +-- @param Identifiable#IDENTIFIABLE self +-- @return DCSCoalitionObject#coalition.side The side of the coalition. +-- @return #nil The DCS Identifiable is not existing or alive. +function IDENTIFIABLE:GetCoalition() + self:F2( self.IdentifiableName ) + + local DCSIdentifiable = self:GetDCSObject() + + if DCSIdentifiable then + local IdentifiableCoalition = DCSIdentifiable:getCoalition() + self:T3( IdentifiableCoalition ) + return IdentifiableCoalition + end + + self:E( self.ClassName .. " " .. self.IdentifiableName .. " not found!" ) + return nil +end + +--- Returns country of the Identifiable. +-- @param Identifiable#IDENTIFIABLE self +-- @return DCScountry#country.id The country identifier. +-- @return #nil The DCS Identifiable is not existing or alive. +function IDENTIFIABLE:GetCountry() + self:F2( self.IdentifiableName ) + + local DCSIdentifiable = self:GetDCSObject() + + if DCSIdentifiable then + local IdentifiableCountry = DCSIdentifiable:getCountry() + self:T3( IdentifiableCountry ) + return IdentifiableCountry + end + + self:E( self.ClassName .. " " .. self.IdentifiableName .. " not found!" ) + return nil +end + + + +--- Returns Identifiable descriptor. Descriptor type depends on Identifiable category. +-- @param Identifiable#IDENTIFIABLE self +-- @return DCSIdentifiable#Identifiable.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() + + if DCSIdentifiable then + local IdentifiableDesc = DCSIdentifiable:getDesc() + self:T2( IdentifiableDesc ) + return IdentifiableDesc + end + + self:E( self.ClassName .. " " .. self.IdentifiableName .. " not found!" ) + return nil +end + + + + + + + + + diff --git a/Moose Development/Moose/Moose.lua b/Moose Development/Moose/Moose.lua index 29e369675..9f81e35ae 100644 --- a/Moose Development/Moose/Moose.lua +++ b/Moose Development/Moose/Moose.lua @@ -2,6 +2,10 @@ Include.File( "Routines" ) Include.File( "Base" ) +Include.File( "Object" ) +Include.File( "Identifiable" ) +Include.File( "Positionable" ) +Include.File( "Controllable" ) Include.File( "Scheduler" ) Include.File( "Event" ) Include.File( "Menu" ) diff --git a/Moose Development/Moose/Object.lua b/Moose Development/Moose/Object.lua new file mode 100644 index 000000000..05b8e2855 --- /dev/null +++ b/Moose Development/Moose/Object.lua @@ -0,0 +1,213 @@ +--- This module contains the OBJECT class. +-- +-- 1) @{Object#OBJECT} class, extends @{Base#BASE} +-- =========================================================== +-- The @{Object#OBJECT} class is a wrapper class to handle the DCS Object objects: +-- +-- * Support all DCS Object APIs. +-- * Enhance with Object specific APIs not in the DCS Object API set. +-- * Manage the "state" of the DCS Object. +-- +-- 1.1) OBJECT constructor: +-- ------------------------------ +-- The OBJECT class provides the following functions to construct a OBJECT instance: +-- +-- * @{Object#OBJECT.New}(): Create a OBJECT instance. +-- +-- 1.2) OBJECT methods: +-- -------------------------- +-- The following methods can be used to identify an Object object: +-- +-- * @{Object#OBJECT.GetID}(): Returns the ID of the Object object. +-- +-- === +-- +-- @module Object +-- @author FlightControl + +--- The OBJECT class +-- @type OBJECT +-- @extends Base#BASE +-- @field #string ObjectName The name of the Object. +OBJECT = { + ClassName = "OBJECT", + ObjectName = "", +} + + +--- A DCSObject +-- @type DCSObject +-- @field id_ The ID of the controllable in DCS + +--- Create a new OBJECT from a DCSObject +-- @param #OBJECT self +-- @param DCSObject#Object ObjectName The Object name +-- @return #OBJECT self +function OBJECT:New( ObjectName ) + local self = BASE:Inherit( self, BASE:New() ) + self:F2( ObjectName ) + self.ObjectName = ObjectName + return self +end + + +--- Returns if the Object is alive. +-- @param Object#OBJECT self +-- @return #boolean true if Object is alive. +-- @return #nil The DCS Object is not existing or alive. +function OBJECT:IsAlive() + self:F2( self.ObjectName ) + + local DCSObject = self:GetDCSObject() + + if DCSObject then + local ObjectIsAlive = DCSObject:isExist() + return ObjectIsAlive + end + + return false +end + + + + +--- Returns DCS Object object name. +-- The function provides access to non-activated objects too. +-- @param Object#OBJECT self +-- @return #string The name of the DCS Object. +-- @return #nil The DCS Object is not existing or alive. +function OBJECT:GetName() + self:F2( self.ObjectName ) + + local DCSObject = self:GetDCSObject() + + if DCSObject then + local ObjectName = self.ObjectName + return ObjectName + end + + self:E( self.ClassName .. " " .. self.ObjectName .. " not found!" ) + return nil +end + + +--- Returns the type name of the DCS Object. +-- @param Object#OBJECT self +-- @return #string The type name of the DCS Object. +-- @return #nil The DCS Object is not existing or alive. +function OBJECT:GetTypeName() + self:F2( self.ObjectName ) + + local DCSObject = self:GetDCSObject() + + if DCSObject then + local ObjectTypeName = DCSObject:getTypeName() + self:T3( ObjectTypeName ) + return ObjectTypeName + end + + self:E( self.ClassName .. " " .. self.ObjectName .. " not found!" ) + return nil +end + +--- Returns the Object's callsign - the localized string. +-- @param Object#OBJECT self +-- @return #string The Callsign of the Object. +-- @return #nil The DCS Object is not existing or alive. +function OBJECT:GetCallSign() + self:F2( self.ObjectName ) + + local DCSObject = self:GetDCSObject() + + if DCSObject then + local ObjectCallSign = DCSObject:getCallsign() + return ObjectCallSign + end + + self:E( self.ClassName .. " " .. self.ObjectName .. " not found!" ) + return nil +end + + +--- Returns the DCS Object category name as defined within the DCS Object Descriptor. +-- @param Object#OBJECT self +-- @return #string The DCS Object Category Name +function OBJECT:GetCategoryName() + local DCSObject = self:GetDCSObject() + + if DCSObject then + local ObjectCategoryName = _CategoryName[ self:GetDesc().category ] + return ObjectCategoryName + end + + self:E( self.ClassName .. " " .. self.ObjectName .. " not found!" ) + return nil +end + +--- Returns coalition of the Object. +-- @param Object#OBJECT self +-- @return DCSCoalitionObject#coalition.side The side of the coalition. +-- @return #nil The DCS Object is not existing or alive. +function OBJECT:GetCoalition() + self:F2( self.ObjectName ) + + local DCSObject = self:GetDCSObject() + + if DCSObject then + local ObjectCoalition = DCSObject:getCoalition() + self:T3( ObjectCoalition ) + return ObjectCoalition + end + + self:E( self.ClassName .. " " .. self.ObjectName .. " not found!" ) + return nil +end + +--- Returns country of the Object. +-- @param Object#OBJECT self +-- @return DCScountry#country.id The country identifier. +-- @return #nil The DCS Object is not existing or alive. +function OBJECT:GetCountry() + self:F2( self.ObjectName ) + + local DCSObject = self:GetDCSObject() + + if DCSObject then + local ObjectCountry = DCSObject:getCountry() + self:T3( ObjectCountry ) + return ObjectCountry + end + + self:E( self.ClassName .. " " .. self.ObjectName .. " not found!" ) + return nil +end + + + +--- Returns Object descriptor. Descriptor type depends on Object category. +-- @param Object#OBJECT self +-- @return DCSObject#Object.Desc The Object descriptor. +-- @return #nil The DCS Object is not existing or alive. +function OBJECT:GetDesc() + self:F2( self.ObjectName ) + + local DCSObject = self:GetDCSObject() + + if DCSObject then + local ObjectDesc = DCSObject:getDesc() + self:T2( ObjectDesc ) + return ObjectDesc + end + + self:E( self.ClassName .. " " .. self.ObjectName .. " not found!" ) + return nil +end + + + + + + + + + diff --git a/Moose Development/Moose/Positionable.lua b/Moose Development/Moose/Positionable.lua new file mode 100644 index 000000000..82c824957 --- /dev/null +++ b/Moose Development/Moose/Positionable.lua @@ -0,0 +1,213 @@ +--- This module contains the POSITIONABLE class. +-- +-- 1) @{Positionable#POSITIONABLE} class, extends @{Identifiable#IDENTIFIABLE} +-- =========================================================== +-- The @{Positionable#POSITIONABLE} class is a wrapper class to handle the DCS Positionable objects: +-- +-- * Support all DCS Positionable APIs. +-- * Enhance with Positionable specific APIs not in the DCS Positionable API set. +-- * Manage the "state" of the DCS Positionable. +-- +-- 1.1) POSITIONABLE constructor: +-- ------------------------------ +-- The POSITIONABLE class provides the following functions to construct a POSITIONABLE instance: +-- +-- * @{Positionable#POSITIONABLE.New}(): Create a POSITIONABLE instance. +-- +-- 1.2) POSITIONABLE methods: +-- -------------------------- +-- The following methods can be used to identify an measurable object: +-- +-- * @{Positionable#POSITIONABLE.GetID}(): Returns the ID of the measurable object. +-- * @{Positionable#POSITIONABLE.GetName}(): Returns the name of the measurable object. +-- +-- === +-- +-- @module Positionable +-- @author FlightControl + +--- The POSITIONABLE class +-- @type POSITIONABLE +-- @extends Identifiable#IDENTIFIABLE +-- @field #string PositionableName The name of the measurable. +POSITIONABLE = { + ClassName = "POSITIONABLE", + PositionableName = "", +} + +--- A DCSPositionable +-- @type DCSPositionable +-- @field id_ The ID of the controllable in DCS + +--- Create a new POSITIONABLE from a DCSPositionable +-- @param #POSITIONABLE self +-- @param DCSPositionable#Positionable PositionableName The DCS Positionable name +-- @return #POSITIONABLE self +function POSITIONABLE:New( PositionableName ) + local self = BASE:Inherit( self, IDENTIFIABLE:New( PositionableName ) ) + + return self +end + +--- Returns the @{DCSTypes#Position3} position vectors indicating the point and direction vectors in 3D of the DCS Positionable within the mission. +-- @param Positionable#POSITIONABLE self +-- @return DCSTypes#Position The 3D position vectors of the DCS Positionable. +-- @return #nil The DCS Positionable is not existing or alive. +function POSITIONABLE:GetPositionVec3() + self:F2( self.PositionableName ) + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + local PositionablePosition = DCSPositionable:getPosition() + self:T3( PositionablePosition ) + return PositionablePosition + end + + return nil +end + +--- Returns the @{DCSTypes#Vec2} vector indicating the point in 2D of the DCS Positionable within the mission. +-- @param Positionable#POSITIONABLE self +-- @return DCSTypes#Vec2 The 2D point vector of the DCS Positionable. +-- @return #nil The DCS Positionable is not existing or alive. +function POSITIONABLE:GetPointVec2() + self:F2( self.PositionableName ) + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + local PositionablePointVec3 = DCSPositionable:getPosition().p + + local PositionablePointVec2 = {} + PositionablePointVec2.x = PositionablePointVec3.x + PositionablePointVec2.y = PositionablePointVec3.z + + self:T2( PositionablePointVec2 ) + return PositionablePointVec2 + end + + return nil +end + + +--- Returns the @{DCSTypes#Vec3} vector indicating the point in 3D of the DCS Positionable within the mission. +-- @param Positionable#POSITIONABLE self +-- @return DCSTypes#Vec3 The 3D point vector of the DCS Positionable. +-- @return #nil The DCS Positionable is not existing or alive. +function POSITIONABLE:GetPointVec3() + self:F2( self.PositionableName ) + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + local PositionablePointVec3 = DCSPositionable:getPosition().p + self:T3( PositionablePointVec3 ) + return PositionablePointVec3 + end + + return nil +end + +--- Returns the altitude of the DCS Positionable. +-- @param Positionable#POSITIONABLE self +-- @return DCSTypes#Distance The altitude of the DCS Positionable. +-- @return #nil The DCS Positionable is not existing or alive. +function POSITIONABLE:GetAltitude() + self:F2() + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + local PositionablePointVec3 = DCSPositionable:getPoint() --DCSTypes#Vec3 + return PositionablePointVec3.y + end + + return nil +end + +--- Returns if the Positionable is located above a runway. +-- @param Positionable#POSITIONABLE self +-- @return #boolean true if Positionable is above a runway. +-- @return #nil The DCS Positionable is not existing or alive. +function POSITIONABLE:IsAboveRunway() + self:F2( self.PositionableName ) + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + + local PointVec2 = self:GetPointVec2() + local SurfaceType = land.getSurfaceType( PointVec2 ) + local IsAboveRunway = SurfaceType == land.SurfaceType.RUNWAY + + self:T2( IsAboveRunway ) + return IsAboveRunway + end + + return nil +end + + + +--- Returns the DCS Positionable heading. +-- @param Positionable#POSITIONABLE self +-- @return #number The DCS Positionable heading +function POSITIONABLE:GetHeading() + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + + local PositionablePosition = DCSPositionable:getPosition() + if PositionablePosition then + local PositionableHeading = math.atan2( PositionablePosition.x.z, PositionablePosition.x.x ) + if PositionableHeading < 0 then + PositionableHeading = PositionableHeading + 2 * math.pi + end + self:T2( PositionableHeading ) + return PositionableHeading + end + end + + return nil +end + + +--- Returns true if the DCS Positionable is in the air. +-- @param Positionable#POSITIONABLE self +-- @return #boolean true if in the air. +-- @return #nil The DCS Positionable is not existing or alive. +function POSITIONABLE:InAir() + self:F2( self.PositionableName ) + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + local PositionableInAir = DCSPositionable:inAir() + self:T3( PositionableInAir ) + return PositionableInAir + end + + return nil +end + +--- Returns the DCS Positionable velocity vector. +-- @param Positionable#POSITIONABLE self +-- @return DCSTypes#Vec3 The velocity vector +-- @return #nil The DCS Positionable is not existing or alive. +function POSITIONABLE:GetVelocity() + self:F2( self.PositionableName ) + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + local PositionableVelocityVec3 = DCSPositionable:getVelocity() + self:T3( PositionableVelocityVec3 ) + return PositionableVelocityVec3 + end + + return nil +end + + + diff --git a/Moose Development/Moose/Static.lua b/Moose Development/Moose/Static.lua index 76e385b57..6833cf646 100644 --- a/Moose Development/Moose/Static.lua +++ b/Moose Development/Moose/Static.lua @@ -1,7 +1,7 @@ --- This module contains the STATIC class. -- --- 1) @{Static#STATIC} class, extends @{Unit#UNIT} --- =============================================== +-- 1) @{Static#STATIC} class, extends @{Positionable#POSITIONABLE} +-- =============================================================== -- 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: @@ -38,7 +38,7 @@ --- The STATIC class -- @type STATIC --- @extends Unit#UNIT +-- @extends Positionable#POSITIONABLE STATIC = { ClassName = "STATIC", } @@ -62,10 +62,7 @@ function STATIC:FindByName( StaticName ) end function STATIC:Register( StaticName ) - local self = BASE:Inherit( self, UNIT:Register( StaticName ) ) - - self:F( StaticName ) - + local self = BASE:Inherit( self, POSITIONABLE:New( StaticName ) ) return self end diff --git a/Moose Development/Moose/Unit.lua b/Moose Development/Moose/Unit.lua index 9907418f7..32b4d7bb2 100644 --- a/Moose Development/Moose/Unit.lua +++ b/Moose Development/Moose/Unit.lua @@ -79,13 +79,6 @@ -- @field #UNIT.SmokeColor SmokeColor UNIT = { ClassName="UNIT", - CategoryName = { - [Unit.Category.AIRPLANE] = "Airplane", - [Unit.Category.HELICOPTER] = "Helicoper", - [Unit.Category.GROUND_UNIT] = "Ground Unit", - [Unit.Category.SHIP] = "Ship", - [Unit.Category.STRUCTURE] = "Structure", - }, FlareColor = { Green = trigger.flareColor.Green, Red = trigger.flareColor.Red, @@ -120,13 +113,10 @@ UNIT = { --- Create a new UNIT from DCSUnit. -- @param #UNIT self --- @param DCSUnit#Unit DCSUnit --- @param Database#DATABASE Database +-- @param #string UnitName The name of the DCS unit. -- @return Unit#UNIT function UNIT:Register( UnitName ) - - local self = BASE:Inherit( self, CONTROLLABLE:New() ) - self:F2( UnitName ) + local self = BASE:Inherit( self, CONTROLLABLE:New( UnitName ) ) self.UnitName = UnitName return self end @@ -168,78 +158,8 @@ function UNIT:GetDCSObject() return nil end ---- Returns coalition of the Unit. --- @param Unit#UNIT self --- @return DCSCoalitionObject#coalition.side The side of the coalition. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetCoalition() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitCoalition = DCSUnit:getCoalition() - self:T3( UnitCoalition ) - return UnitCoalition - end - - return nil -end - ---- Returns country of the Unit. --- @param Unit#UNIT self --- @return DCScountry#country.id The country identifier. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetCountry() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitCountry = DCSUnit:getCountry() - self:T3( UnitCountry ) - return UnitCountry - end - - return nil -end - - ---- Returns DCS Unit object name. --- The function provides access to non-activated units too. --- @param Unit#UNIT self --- @return #string The name of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetName() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitName = self.UnitName - return UnitName - end - - return nil -end ---- Returns if the unit is alive. --- @param Unit#UNIT self --- @return #boolean true if Unit is alive. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:IsAlive() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitIsAlive = DCSUnit:isExist() - return UnitIsAlive - end - - return false -end --- Returns if the unit is activated. -- @param Unit#UNIT self @@ -259,30 +179,25 @@ function UNIT:IsActive() return nil end ---- Returns if the unit is located above a runway. +--- Returns the Unit's callsign - the localized string. -- @param Unit#UNIT self --- @return #boolean true if Unit is above a runway. +-- @return #string The Callsign of the Unit. -- @return #nil The DCS Unit is not existing or alive. -function UNIT:IsAboveRunway() +function UNIT:GetCallSign() self:F2( self.UnitName ) local DCSUnit = self:GetDCSObject() if DCSUnit then - - local PointVec2 = self:GetPointVec2() - local SurfaceType = land.getSurfaceType( PointVec2 ) - local IsAboveRunway = SurfaceType == land.SurfaceType.RUNWAY - - self:T2( IsAboveRunway ) - return IsAboveRunway + local UnitCallSign = DCSUnit:getCallsign() + return UnitCallSign end - + + self:E( self.ClassName .. " " .. self.UnitName .. " not found!" ) return nil end - --- Returns name of the player that control the unit or nil if the unit is controlled by A.I. -- @param Unit#UNIT self -- @return #string Player Name @@ -304,23 +219,6 @@ function UNIT:GetPlayerName() return nil end ---- Returns the unit's unique identifier. --- @param Unit#UNIT self --- @return DCSUnit#Unit.ID Unit ID --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetID() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitID = DCSUnit:getID() - return UnitID - end - - return nil -end - --- Returns the unit's number in the group. -- The number is the same number the unit has in ME. -- It may not be changed during the mission. @@ -359,69 +257,23 @@ function UNIT:GetGroup() end ---- Returns the unit's callsign - the localized string. +-- Need to add here functions to check if radar is on and which object etc. + +--- Returns the prefix name of the DCS Unit. A prefix name is a part of the name before a '#'-sign. +-- DCS Units spawned with the @{SPAWN} class contain a '#'-sign to indicate the end of the (base) DCS Unit name. +-- The spawn sequence number and unit number are contained within the name after the '#' sign. -- @param Unit#UNIT self --- @return #string The Callsign of the Unit. +-- @return #string The name of the DCS Unit. -- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetCallSign() - self:F2( self.UnitName ) +function UNIT:GetPrefix() + self:F2( self.UnitName ) local DCSUnit = self:GetDCSObject() - + if DCSUnit then - local UnitCallSign = DCSUnit:getCallsign() - return UnitCallSign - end - - return nil -end - ---- Returns the unit's health. Dead units has health <= 1.0. --- @param Unit#UNIT self --- @return #number The Unit's health value. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetLife() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitLife = DCSUnit:getLife() - return UnitLife - end - - return nil -end - ---- Returns the Unit's initial health. --- @param Unit#UNIT self --- @return #number The Unit's initial health value. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetLife0() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitLife0 = DCSUnit:getLife0() - return UnitLife0 - end - - return nil -end - ---- Returns relative amount of fuel (from 0.0 to 1.0) the unit has in its internal tanks. If there are additional fuel tanks the value may be greater than 1.0. --- @param Unit#UNIT self --- @return #number The relative amount of fuel (from 0.0 to 1.0). --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetFuel() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitFuel = DCSUnit:getFuel() - return UnitFuel + local UnitPrefix = string.match( self.UnitName, ".*#" ):sub( 1, -2 ) + self:T3( UnitPrefix ) + return UnitPrefix end return nil @@ -485,63 +337,52 @@ function UNIT:GetRadar() return nil, nil end --- Need to add here functions to check if radar is on and which object etc. - ---- Returns unit descriptor. Descriptor type depends on unit category. +--- Returns relative amount of fuel (from 0.0 to 1.0) the unit has in its internal tanks. If there are additional fuel tanks the value may be greater than 1.0. -- @param Unit#UNIT self --- @return DCSUnit#Unit.Desc The Unit descriptor. +-- @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:GetDesc() +function UNIT:GetFuel() self:F2( self.UnitName ) local DCSUnit = self:GetDCSObject() if DCSUnit then - local UnitDesc = DCSUnit:getDesc() - self:T2( UnitDesc ) - return UnitDesc + local UnitFuel = DCSUnit:getFuel() + return UnitFuel end - self:E( "Unit " .. self.UnitName .. "not found!" ) return nil end - ---- Returns the type name of the DCS Unit. +--- Returns the unit's health. Dead units has health <= 1.0. -- @param Unit#UNIT self --- @return #string The type name of the DCS Unit. +-- @return #number The Unit's health value. -- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetTypeName() - self:F2( self.UnitName ) - +function UNIT:GetLife() + self:F2( self.UnitName ) + local DCSUnit = self:GetDCSObject() if DCSUnit then - local UnitTypeName = DCSUnit:getTypeName() - self:T3( UnitTypeName ) - return UnitTypeName + local UnitLife = DCSUnit:getLife() + return UnitLife end - - return nil + + return nil end - - ---- Returns the prefix name of the DCS Unit. A prefix name is a part of the name before a '#'-sign. --- DCS Units spawned with the @{SPAWN} class contain a '#'-sign to indicate the end of the (base) DCS Unit name. --- The spawn sequence number and unit number are contained within the name after the '#' sign. +--- Returns the Unit's initial health. -- @param Unit#UNIT self --- @return #string The name of the DCS Unit. +-- @return #number The Unit's initial health value. -- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetPrefix() - self:F2( self.UnitName ) +function UNIT:GetLife0() + self:F2( self.UnitName ) local DCSUnit = self:GetDCSObject() - + if DCSUnit then - local UnitPrefix = string.match( self.UnitName, ".*#" ):sub( 1, -2 ) - self:T3( UnitPrefix ) - return UnitPrefix + local UnitLife0 = DCSUnit:getLife0() + return UnitLife0 end return nil @@ -549,83 +390,6 @@ end ---- Returns the @{DCSTypes#Vec2} vector indicating the point in 2D of the DCS Unit within the mission. --- @param Unit#UNIT self --- @return DCSTypes#Vec2 The 2D point vector of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetPointVec2() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitPointVec3 = DCSUnit:getPosition().p - - local UnitPointVec2 = {} - UnitPointVec2.x = UnitPointVec3.x - UnitPointVec2.y = UnitPointVec3.z - - self:T2( UnitPointVec2 ) - return UnitPointVec2 - end - - return nil -end - - ---- Returns the @{DCSTypes#Vec3} vector indicating the point in 3D of the DCS Unit within the mission. --- @param Unit#UNIT self --- @return DCSTypes#Vec3 The 3D point vector of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetPointVec3() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitPointVec3 = DCSUnit:getPosition().p - self:T3( UnitPointVec3 ) - return UnitPointVec3 - end - - return nil -end - ---- Returns the @{DCSTypes#Position3} position vectors indicating the point and direction vectors in 3D of the DCS Unit within the mission. --- @param Unit#UNIT self --- @return DCSTypes#Position The 3D position vectors of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetPositionVec3() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitPosition = DCSUnit:getPosition() - self:T3( UnitPosition ) - return UnitPosition - end - - return nil -end - ---- Returns the DCS Unit velocity vector. --- @param Unit#UNIT self --- @return DCSTypes#Vec3 The velocity vector --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetVelocity() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitVelocityVec3 = DCSUnit:getVelocity() - self:T3( UnitVelocityVec3 ) - return UnitVelocityVec3 - end - - return nil -end -- Is functions @@ -663,40 +427,6 @@ function UNIT:IsNotInZone( Zone ) end end ---- Returns true if the DCS Unit is in the air. --- @param Unit#UNIT self --- @return #boolean true if in the air. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:InAir() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitInAir = DCSUnit:inAir() - self:T3( UnitInAir ) - return UnitInAir - end - - return nil -end - ---- Returns the altitude of the DCS Unit. --- @param Unit#UNIT self --- @return DCSTypes#Distance The altitude of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetAltitude() - self:F2() - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitPointVec3 = DCSUnit:getPoint() --DCSTypes#Vec3 - return UnitPointVec3.y - end - - return nil -end --- Returns true if there is an **other** DCS Unit within a radius of the current 2D point of the DCS Unit. -- @param Unit#UNIT self @@ -725,41 +455,6 @@ function UNIT:OtherUnitInRadius( AwaitUnit, Radius ) return nil end ---- Returns the DCS Unit category name as defined within the DCS Unit Descriptor. --- @param Unit#UNIT self --- @return #string The DCS Unit Category Name -function UNIT:GetCategoryName() - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitCategoryName = self.CategoryName[ self:GetDesc().category ] - return UnitCategoryName - end - - return nil -end - ---- Returns the DCS Unit heading. --- @param Unit#UNIT self --- @return #number The DCS Unit heading -function UNIT:GetHeading() - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - - local UnitPosition = DCSUnit:getPosition() - if UnitPosition then - local UnitHeading = math.atan2( UnitPosition.x.z, UnitPosition.x.x ) - if UnitHeading < 0 then - UnitHeading = UnitHeading + 2 * math.pi - end - self:T2( UnitHeading ) - return UnitHeading - end - end - - return nil -end --- Signal a flare at the position of the UNIT. diff --git a/Moose Mission Setup/Moose Mission Update/l10n/DEFAULT/Moose.lua b/Moose Mission Setup/Moose Mission Update/l10n/DEFAULT/Moose.lua index 923532602..518c25c8a 100644 --- a/Moose Mission Setup/Moose Mission Update/l10n/DEFAULT/Moose.lua +++ b/Moose Mission Setup/Moose Mission Update/l10n/DEFAULT/Moose.lua @@ -1,23223 +1,45 @@ -env.info( '*** MOOSE STATIC INCLUDE START *** ' ) -env.info( 'Moose Generation Timestamp: 20160623_1441' ) +env.info( '*** MOOSE DYNAMIC INCLUDE START *** ' ) +env.info( 'Moose Generation Timestamp: 20160624_1231' ) + local base = _G Include = {} -Include.Files = {} + +Include.Path = function() + local str = debug.getinfo(2, "S").source + return str:match("(.*/)"):sub(1,-2):gsub("\\","/") +end + Include.File = function( IncludeFile ) -end - ---- Various routines --- @module routines --- @author Flightcontrol - -env.setErrorMessageBoxEnabled(false) - ---- Extract of MIST functions. --- @author Grimes - -routines = {} - - --- don't change these -routines.majorVersion = 3 -routines.minorVersion = 3 -routines.build = 22 - ------------------------------------------------------------------------------------------------------------------ - ----------------------------------------------------------------------------------------------- --- Utils- conversion, Lua utils, etc. -routines.utils = {} - ---from http://lua-users.org/wiki/CopyTable -routines.utils.deepCopy = function(object) - local lookup_table = {} - local function _copy(object) - if type(object) ~= "table" then - return object - elseif lookup_table[object] then - return lookup_table[object] - end - local new_table = {} - lookup_table[object] = new_table - for index, value in pairs(object) do - new_table[_copy(index)] = _copy(value) - end - return setmetatable(new_table, getmetatable(object)) - end - local objectreturn = _copy(object) - return objectreturn -end - - --- porting in Slmod's serialize_slmod2 -routines.utils.oneLineSerialize = function(tbl) -- serialization of a table all on a single line, no comments, made to replace old get_table_string function - - lookup_table = {} - - local function _Serialize( tbl ) - - if type(tbl) == 'table' then --function only works for tables! - - if lookup_table[tbl] then - return lookup_table[object] - end - - local tbl_str = {} - - lookup_table[tbl] = tbl_str - - tbl_str[#tbl_str + 1] = '{' - - for ind,val in pairs(tbl) do -- serialize its fields - local ind_str = {} - if type(ind) == "number" then - ind_str[#ind_str + 1] = '[' - ind_str[#ind_str + 1] = tostring(ind) - ind_str[#ind_str + 1] = ']=' - else --must be a string - ind_str[#ind_str + 1] = '[' - ind_str[#ind_str + 1] = routines.utils.basicSerialize(ind) - ind_str[#ind_str + 1] = ']=' - end - - local val_str = {} - if ((type(val) == 'number') or (type(val) == 'boolean')) then - val_str[#val_str + 1] = tostring(val) - val_str[#val_str + 1] = ',' - tbl_str[#tbl_str + 1] = table.concat(ind_str) - tbl_str[#tbl_str + 1] = table.concat(val_str) - elseif type(val) == 'string' then - val_str[#val_str + 1] = routines.utils.basicSerialize(val) - val_str[#val_str + 1] = ',' - tbl_str[#tbl_str + 1] = table.concat(ind_str) - tbl_str[#tbl_str + 1] = table.concat(val_str) - elseif type(val) == 'nil' then -- won't ever happen, right? - val_str[#val_str + 1] = 'nil,' - tbl_str[#tbl_str + 1] = table.concat(ind_str) - tbl_str[#tbl_str + 1] = table.concat(val_str) - elseif type(val) == 'table' then - if ind == "__index" then - -- tbl_str[#tbl_str + 1] = "__index" - -- tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it - else - - val_str[#val_str + 1] = _Serialize(val) - val_str[#val_str + 1] = ',' --I think this is right, I just added it - tbl_str[#tbl_str + 1] = table.concat(ind_str) - tbl_str[#tbl_str + 1] = table.concat(val_str) - end - elseif type(val) == 'function' then - -- tbl_str[#tbl_str + 1] = "function " .. tostring(ind) - -- tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it - else --- env.info('unable to serialize value type ' .. routines.utils.basicSerialize(type(val)) .. ' at index ' .. tostring(ind)) --- env.info( debug.traceback() ) - end - - end - tbl_str[#tbl_str + 1] = '}' - return table.concat(tbl_str) - else - return tostring(tbl) - end - end - - local objectreturn = _Serialize(tbl) - return objectreturn -end - ---porting in Slmod's "safestring" basic serialize -routines.utils.basicSerialize = function(s) - if s == nil then - return "\"\"" - else - if ((type(s) == 'number') or (type(s) == 'boolean') or (type(s) == 'function') or (type(s) == 'table') or (type(s) == 'userdata') ) then - return tostring(s) - elseif type(s) == 'string' then - s = string.format('%q', s) - return s - end - end -end - - -routines.utils.toDegree = function(angle) - return angle*180/math.pi -end - -routines.utils.toRadian = function(angle) - return angle*math.pi/180 -end - -routines.utils.metersToNM = function(meters) - return meters/1852 -end - -routines.utils.metersToFeet = function(meters) - return meters/0.3048 -end - -routines.utils.NMToMeters = function(NM) - return NM*1852 -end - -routines.utils.feetToMeters = function(feet) - return feet*0.3048 -end - -routines.utils.mpsToKnots = function(mps) - return mps*3600/1852 -end - -routines.utils.mpsToKmph = function(mps) - return mps*3.6 -end - -routines.utils.knotsToMps = function(knots) - return knots*1852/3600 -end - -routines.utils.kmphToMps = function(kmph) - return kmph/3.6 -end - -function routines.utils.makeVec2(Vec3) - if Vec3.z then - return {x = Vec3.x, y = Vec3.z} - else - return {x = Vec3.x, y = Vec3.y} -- it was actually already vec2. - end -end - -function routines.utils.makeVec3(Vec2, y) - if not Vec2.z then - if not y then - y = 0 - end - return {x = Vec2.x, y = y, z = Vec2.y} - else - return {x = Vec2.x, y = Vec2.y, z = Vec2.z} -- it was already Vec3, actually. - end -end - -function routines.utils.makeVec3GL(Vec2, offset) - local adj = offset or 0 - - if not Vec2.z then - return {x = Vec2.x, y = (land.getHeight(Vec2) + adj), z = Vec2.y} - else - return {x = Vec2.x, y = (land.getHeight({x = Vec2.x, y = Vec2.z}) + adj), z = Vec2.z} - end -end - -routines.utils.zoneToVec3 = function(zone) - local new = {} - if type(zone) == 'table' and zone.point then - new.x = zone.point.x - new.y = zone.point.y - new.z = zone.point.z - return new - elseif type(zone) == 'string' then - zone = trigger.misc.getZone(zone) - if zone then - new.x = zone.point.x - new.y = zone.point.y - new.z = zone.point.z - return new - end - end -end - --- gets heading-error corrected direction from point along vector vec. -function routines.utils.getDir(vec, point) - local dir = math.atan2(vec.z, vec.x) - dir = dir + routines.getNorthCorrection(point) - if dir < 0 then - dir = dir + 2*math.pi -- put dir in range of 0 to 2*pi - end - return dir -end - --- gets distance in meters between two points (2 dimensional) -function routines.utils.get2DDist(point1, point2) - point1 = routines.utils.makeVec3(point1) - point2 = routines.utils.makeVec3(point2) - return routines.vec.mag({x = point1.x - point2.x, y = 0, z = point1.z - point2.z}) -end - --- gets distance in meters between two points (3 dimensional) -function routines.utils.get3DDist(point1, point2) - return routines.vec.mag({x = point1.x - point2.x, y = point1.y - point2.y, z = point1.z - point2.z}) -end - - - --- From http://lua-users.org/wiki/SimpleRound --- use negative idp for rounding ahead of decimal place, positive for rounding after decimal place -routines.utils.round = function(num, idp) - local mult = 10^(idp or 0) - return math.floor(num * mult + 0.5) / mult -end - --- porting in Slmod's dostring -routines.utils.dostring = function(s) - local f, err = loadstring(s) - if f then - return true, f() - else - return false, err - end -end - - ---3D Vector manipulation -routines.vec = {} - -routines.vec.add = function(vec1, vec2) - return {x = vec1.x + vec2.x, y = vec1.y + vec2.y, z = vec1.z + vec2.z} -end - -routines.vec.sub = function(vec1, vec2) - return {x = vec1.x - vec2.x, y = vec1.y - vec2.y, z = vec1.z - vec2.z} -end - -routines.vec.scalarMult = function(vec, mult) - return {x = vec.x*mult, y = vec.y*mult, z = vec.z*mult} -end - -routines.vec.scalar_mult = routines.vec.scalarMult - -routines.vec.dp = function(vec1, vec2) - return vec1.x*vec2.x + vec1.y*vec2.y + vec1.z*vec2.z -end - -routines.vec.cp = function(vec1, vec2) - return { x = vec1.y*vec2.z - vec1.z*vec2.y, y = vec1.z*vec2.x - vec1.x*vec2.z, z = vec1.x*vec2.y - vec1.y*vec2.x} -end - -routines.vec.mag = function(vec) - return (vec.x^2 + vec.y^2 + vec.z^2)^0.5 -end - -routines.vec.getUnitVec = function(vec) - local mag = routines.vec.mag(vec) - return { x = vec.x/mag, y = vec.y/mag, z = vec.z/mag } -end - -routines.vec.rotateVec2 = function(vec2, theta) - return { x = vec2.x*math.cos(theta) - vec2.y*math.sin(theta), y = vec2.x*math.sin(theta) + vec2.y*math.cos(theta)} -end ---------------------------------------------------------------------------------------------------------------------------- - - - - --- acc- the accuracy of each easting/northing. 0, 1, 2, 3, 4, or 5. -routines.tostringMGRS = function(MGRS, acc) - if acc == 0 then - return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph - else - return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph .. ' ' .. string.format('%0' .. acc .. 'd', routines.utils.round(MGRS.Easting/(10^(5-acc)), 0)) - .. ' ' .. string.format('%0' .. acc .. 'd', routines.utils.round(MGRS.Northing/(10^(5-acc)), 0)) - end -end - ---[[acc: -in DM: decimal point of minutes. -In DMS: decimal point of seconds. -position after the decimal of the least significant digit: -So: -42.32 - acc of 2. -]] -routines.tostringLL = function(lat, lon, acc, DMS) - - local latHemi, lonHemi - if lat > 0 then - latHemi = 'N' - else - latHemi = 'S' - end - - if lon > 0 then - lonHemi = 'E' - else - lonHemi = 'W' - end - - lat = math.abs(lat) - lon = math.abs(lon) - - local latDeg = math.floor(lat) - local latMin = (lat - latDeg)*60 - - local lonDeg = math.floor(lon) - local lonMin = (lon - lonDeg)*60 - - if DMS then -- degrees, minutes, and seconds. - local oldLatMin = latMin - latMin = math.floor(latMin) - local latSec = routines.utils.round((oldLatMin - latMin)*60, acc) - - local oldLonMin = lonMin - lonMin = math.floor(lonMin) - local lonSec = routines.utils.round((oldLonMin - lonMin)*60, acc) - - if latSec == 60 then - latSec = 0 - latMin = latMin + 1 - end - - if lonSec == 60 then - lonSec = 0 - lonMin = lonMin + 1 - end - - local secFrmtStr -- create the formatting string for the seconds place - if acc <= 0 then -- no decimal place. - secFrmtStr = '%02d' - else - local width = 3 + acc -- 01.310 - that's a width of 6, for example. - secFrmtStr = '%0' .. width .. '.' .. acc .. 'f' - end - - return string.format('%02d', latDeg) .. ' ' .. string.format('%02d', latMin) .. '\' ' .. string.format(secFrmtStr, latSec) .. '"' .. latHemi .. ' ' - .. string.format('%02d', lonDeg) .. ' ' .. string.format('%02d', lonMin) .. '\' ' .. string.format(secFrmtStr, lonSec) .. '"' .. lonHemi - - else -- degrees, decimal minutes. - latMin = routines.utils.round(latMin, acc) - lonMin = routines.utils.round(lonMin, acc) - - if latMin == 60 then - latMin = 0 - latDeg = latDeg + 1 - end - - if lonMin == 60 then - lonMin = 0 - lonDeg = lonDeg + 1 - end - - local minFrmtStr -- create the formatting string for the minutes place - if acc <= 0 then -- no decimal place. - minFrmtStr = '%02d' - else - local width = 3 + acc -- 01.310 - that's a width of 6, for example. - minFrmtStr = '%0' .. width .. '.' .. acc .. 'f' - end - - return string.format('%02d', latDeg) .. ' ' .. string.format(minFrmtStr, latMin) .. '\'' .. latHemi .. ' ' - .. string.format('%02d', lonDeg) .. ' ' .. string.format(minFrmtStr, lonMin) .. '\'' .. lonHemi - - end -end - ---[[ required: az - radian - required: dist - meters - optional: alt - meters (set to false or nil if you don't want to use it). - optional: metric - set true to get dist and alt in km and m. - precision will always be nearest degree and NM or km.]] -routines.tostringBR = function(az, dist, alt, metric) - az = routines.utils.round(routines.utils.toDegree(az), 0) - - if metric then - dist = routines.utils.round(dist/1000, 2) - else - dist = routines.utils.round(routines.utils.metersToNM(dist), 2) - end - - local s = string.format('%03d', az) .. ' for ' .. dist - - if alt then - if metric then - s = s .. ' at ' .. routines.utils.round(alt, 0) - else - s = s .. ' at ' .. routines.utils.round(routines.utils.metersToFeet(alt), 0) - end - end - return s -end - -routines.getNorthCorrection = function(point) --gets the correction needed for true north - if not point.z then --Vec2; convert to Vec3 - point.z = point.y - point.y = 0 - end - local lat, lon = coord.LOtoLL(point) - local north_posit = coord.LLtoLO(lat + 1, lon) - return math.atan2(north_posit.z - point.z, north_posit.x - point.x) -end - - --- the main area -do - -- THE MAIN FUNCTION -- Accessed 100 times/sec. - routines.main = function() - timer.scheduleFunction(routines.main, {}, timer.getTime() + 2) --reschedule first in case of Lua error - ---------------------------------------------------------------------------------------------------------- - --area to add new stuff in - - routines.do_scheduled_functions() - end -- end of routines.main - - timer.scheduleFunction(routines.main, {}, timer.getTime() + 2) - -end - - -do - local idNum = 0 - - --Simplified event handler - routines.addEventHandler = function(f) --id is optional! - local handler = {} - idNum = idNum + 1 - handler.id = idNum - handler.f = f - handler.onEvent = function(self, event) - self.f(event) - end - world.addEventHandler(handler) - end - - routines.removeEventHandler = function(id) - for key, handler in pairs(world.eventHandlers) do - if handler.id and handler.id == id then - world.eventHandlers[key] = nil - return true - end - end - return false - end -end - --- need to return a Vec3 or Vec2? -function routines.getRandPointInCircle(point, radius, innerRadius) - local theta = 2*math.pi*math.random() - local rad = math.random() + math.random() - if rad > 1 then - rad = 2 - rad - end - - local radMult - if innerRadius and innerRadius <= radius then - radMult = (radius - innerRadius)*rad + innerRadius - else - radMult = radius*rad - end - - if not point.z then --might as well work with vec2/3 - point.z = point.y - end - - local rndCoord - if radius > 0 then - rndCoord = {x = math.cos(theta)*radMult + point.x, y = math.sin(theta)*radMult + point.z} - else - rndCoord = {x = point.x, y = point.z} - end - return rndCoord -end - -routines.goRoute = function(group, path) - local misTask = { - id = 'Mission', - params = { - route = { - points = routines.utils.deepCopy(path), - }, - }, - } - if type(group) == 'string' then - group = Group.getByName(group) - end - local groupCon = group:getController() - if groupCon then - groupCon:setTask(misTask) - return true - end - - Controller.setTask(groupCon, misTask) - return false -end - - --- Useful atomic functions from mist, ported. - -routines.ground = {} -routines.fixedWing = {} -routines.heli = {} - -routines.ground.buildWP = function(point, overRideForm, overRideSpeed) - - local wp = {} - wp.x = point.x - - if point.z then - wp.y = point.z - else - wp.y = point.y - end - local form, speed - - if point.speed and not overRideSpeed then - wp.speed = point.speed - elseif type(overRideSpeed) == 'number' then - wp.speed = overRideSpeed - else - wp.speed = routines.utils.kmphToMps(20) - end - - if point.form and not overRideForm then - form = point.form - else - form = overRideForm - end - - if not form then - wp.action = 'Cone' - else - form = string.lower(form) - if form == 'off_road' or form == 'off road' then - wp.action = 'Off Road' - elseif form == 'on_road' or form == 'on road' then - wp.action = 'On Road' - elseif form == 'rank' or form == 'line_abrest' or form == 'line abrest' or form == 'lineabrest'then - wp.action = 'Rank' - elseif form == 'cone' then - wp.action = 'Cone' - elseif form == 'diamond' then - wp.action = 'Diamond' - elseif form == 'vee' then - wp.action = 'Vee' - elseif form == 'echelon_left' or form == 'echelon left' or form == 'echelonl' then - wp.action = 'EchelonL' - elseif form == 'echelon_right' or form == 'echelon right' or form == 'echelonr' then - wp.action = 'EchelonR' - else - wp.action = 'Cone' -- if nothing matched - end - end - - wp.type = 'Turning Point' - - return wp - -end - -routines.fixedWing.buildWP = function(point, WPtype, speed, alt, altType) - - local wp = {} - wp.x = point.x - - if point.z then - wp.y = point.z - else - wp.y = point.y - end - - if alt and type(alt) == 'number' then - wp.alt = alt - else - wp.alt = 2000 - end - - if altType then - altType = string.lower(altType) - if altType == 'radio' or 'agl' then - wp.alt_type = 'RADIO' - elseif altType == 'baro' or 'asl' then - wp.alt_type = 'BARO' - end - else - wp.alt_type = 'RADIO' - end - - if point.speed then - speed = point.speed - end - - if point.type then - WPtype = point.type - end - - if not speed then - wp.speed = routines.utils.kmphToMps(500) - else - wp.speed = speed - end - - if not WPtype then - wp.action = 'Turning Point' - else - WPtype = string.lower(WPtype) - if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then - wp.action = 'Fly Over Point' - elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then - wp.action = 'Turning Point' - else - wp.action = 'Turning Point' - end - end - - wp.type = 'Turning Point' - return wp -end - -routines.heli.buildWP = function(point, WPtype, speed, alt, altType) - - local wp = {} - wp.x = point.x - - if point.z then - wp.y = point.z - else - wp.y = point.y - end - - if alt and type(alt) == 'number' then - wp.alt = alt - else - wp.alt = 500 - end - - if altType then - altType = string.lower(altType) - if altType == 'radio' or 'agl' then - wp.alt_type = 'RADIO' - elseif altType == 'baro' or 'asl' then - wp.alt_type = 'BARO' - end - else - wp.alt_type = 'RADIO' - end - - if point.speed then - speed = point.speed - end - - if point.type then - WPtype = point.type - end - - if not speed then - wp.speed = routines.utils.kmphToMps(200) - else - wp.speed = speed - end - - if not WPtype then - wp.action = 'Turning Point' - else - WPtype = string.lower(WPtype) - if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then - wp.action = 'Fly Over Point' - elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then - wp.action = 'Turning Point' - else - wp.action = 'Turning Point' - end - end - - wp.type = 'Turning Point' - return wp -end - -routines.groupToRandomPoint = function(vars) - local group = vars.group --Required - local point = vars.point --required - local radius = vars.radius or 0 - local innerRadius = vars.innerRadius - local form = vars.form or 'Cone' - local heading = vars.heading or math.random()*2*math.pi - local headingDegrees = vars.headingDegrees - local speed = vars.speed or routines.utils.kmphToMps(20) - - - local useRoads - if not vars.disableRoads then - useRoads = true - else - useRoads = false - end - - local path = {} - - if headingDegrees then - heading = headingDegrees*math.pi/180 - end - - if heading >= 2*math.pi then - heading = heading - 2*math.pi - end - - local rndCoord = routines.getRandPointInCircle(point, radius, innerRadius) - - local offset = {} - local posStart = routines.getLeadPos(group) - - offset.x = routines.utils.round(math.sin(heading - (math.pi/2)) * 50 + rndCoord.x, 3) - offset.z = routines.utils.round(math.cos(heading + (math.pi/2)) * 50 + rndCoord.y, 3) - path[#path + 1] = routines.ground.buildWP(posStart, form, speed) - - - if useRoads == true and ((point.x - posStart.x)^2 + (point.z - posStart.z)^2)^0.5 > radius * 1.3 then - path[#path + 1] = routines.ground.buildWP({['x'] = posStart.x + 11, ['z'] = posStart.z + 11}, 'off_road', speed) - path[#path + 1] = routines.ground.buildWP(posStart, 'on_road', speed) - path[#path + 1] = routines.ground.buildWP(offset, 'on_road', speed) - else - path[#path + 1] = routines.ground.buildWP({['x'] = posStart.x + 25, ['z'] = posStart.z + 25}, form, speed) - end - - path[#path + 1] = routines.ground.buildWP(offset, form, speed) - path[#path + 1] = routines.ground.buildWP(rndCoord, form, speed) - - routines.goRoute(group, path) - - return -end - -routines.groupRandomDistSelf = function(gpData, dist, form, heading, speed) - local pos = routines.getLeadPos(gpData) - local fakeZone = {} - fakeZone.radius = dist or math.random(300, 1000) - fakeZone.point = {x = pos.x, y, pos.y, z = pos.z} - routines.groupToRandomZone(gpData, fakeZone, form, heading, speed) - - return -end - -routines.groupToRandomZone = function(gpData, zone, form, heading, speed) - if type(gpData) == 'string' then - gpData = Group.getByName(gpData) - end - - if type(zone) == 'string' then - zone = trigger.misc.getZone(zone) - elseif type(zone) == 'table' and not zone.radius then - zone = trigger.misc.getZone(zone[math.random(1, #zone)]) - end - - if speed then - speed = routines.utils.kmphToMps(speed) - end - - local vars = {} - vars.group = gpData - vars.radius = zone.radius - vars.form = form - vars.headingDegrees = heading - vars.speed = speed - vars.point = routines.utils.zoneToVec3(zone) - - routines.groupToRandomPoint(vars) - - return -end - -routines.isTerrainValid = function(coord, terrainTypes) -- vec2/3 and enum or table of acceptable terrain types - if coord.z then - coord.y = coord.z - end - local typeConverted = {} - - if type(terrainTypes) == 'string' then -- if its a string it does this check - for constId, constData in pairs(land.SurfaceType) do - if string.lower(constId) == string.lower(terrainTypes) or string.lower(constData) == string.lower(terrainTypes) then - table.insert(typeConverted, constId) - end - end - elseif type(terrainTypes) == 'table' then -- if its a table it does this check - for typeId, typeData in pairs(terrainTypes) do - for constId, constData in pairs(land.SurfaceType) do - if string.lower(constId) == string.lower(typeData) or string.lower(constData) == string.lower(typeId) then - table.insert(typeConverted, constId) - end - end - end - end - for validIndex, validData in pairs(typeConverted) do - if land.getSurfaceType(coord) == land.SurfaceType[validData] then - return true - end - end - return false -end - -routines.groupToPoint = function(gpData, point, form, heading, speed, useRoads) - if type(point) == 'string' then - point = trigger.misc.getZone(point) - end - if speed then - speed = routines.utils.kmphToMps(speed) - end - - local vars = {} - vars.group = gpData - vars.form = form - vars.headingDegrees = heading - vars.speed = speed - vars.disableRoads = useRoads - vars.point = routines.utils.zoneToVec3(point) - routines.groupToRandomPoint(vars) - - return -end - - -routines.getLeadPos = function(group) - if type(group) == 'string' then -- group name - group = Group.getByName(group) - end - - local units = group:getUnits() - - local leader = units[1] - if not leader then -- SHOULD be good, but if there is a bug, this code future-proofs it then. - local lowestInd = math.huge - for ind, unit in pairs(units) do - if ind < lowestInd then - lowestInd = ind - leader = unit - end - end - end - if leader and Unit.isExist(leader) then -- maybe a little too paranoid now... - return leader:getPosition().p - end -end - ---[[ vars for routines.getMGRSString: -vars.units - table of unit names (NOT unitNameTable- maybe this should change). -vars.acc - integer between 0 and 5, inclusive -]] -routines.getMGRSString = function(vars) - local units = vars.units - local acc = vars.acc or 5 - local avgPos = routines.getAvgPos(units) - if avgPos then - return routines.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(avgPos)), acc) - end -end - ---[[ vars for routines.getLLString -vars.units - table of unit names (NOT unitNameTable- maybe this should change). -vars.acc - integer, number of numbers after decimal place -vars.DMS - if true, output in degrees, minutes, seconds. Otherwise, output in degrees, minutes. - - -]] -routines.getLLString = function(vars) - local units = vars.units - local acc = vars.acc or 3 - local DMS = vars.DMS - local avgPos = routines.getAvgPos(units) - if avgPos then - local lat, lon = coord.LOtoLL(avgPos) - return routines.tostringLL(lat, lon, acc, DMS) - end -end - ---[[ -vars.zone - table of a zone name. -vars.ref - vec3 ref point, maybe overload for vec2 as well? -vars.alt - boolean, if used, includes altitude in string -vars.metric - boolean, gives distance in km instead of NM. -]] -routines.getBRStringZone = function(vars) - local zone = trigger.misc.getZone( vars.zone ) - local ref = routines.utils.makeVec3(vars.ref, 0) -- turn it into Vec3 if it is not already. - local alt = vars.alt - local metric = vars.metric - if zone then - local vec = {x = zone.point.x - ref.x, y = zone.point.y - ref.y, z = zone.point.z - ref.z} - local dir = routines.utils.getDir(vec, ref) - local dist = routines.utils.get2DDist(zone.point, ref) - if alt then - alt = zone.y - end - return routines.tostringBR(dir, dist, alt, metric) - else - env.info( 'routines.getBRStringZone: error: zone is nil' ) - end -end - ---[[ -vars.units- table of unit names (NOT unitNameTable- maybe this should change). -vars.ref - vec3 ref point, maybe overload for vec2 as well? -vars.alt - boolean, if used, includes altitude in string -vars.metric - boolean, gives distance in km instead of NM. -]] -routines.getBRString = function(vars) - local units = vars.units - local ref = routines.utils.makeVec3(vars.ref, 0) -- turn it into Vec3 if it is not already. - local alt = vars.alt - local metric = vars.metric - local avgPos = routines.getAvgPos(units) - if avgPos then - local vec = {x = avgPos.x - ref.x, y = avgPos.y - ref.y, z = avgPos.z - ref.z} - local dir = routines.utils.getDir(vec, ref) - local dist = routines.utils.get2DDist(avgPos, ref) - if alt then - alt = avgPos.y - end - return routines.tostringBR(dir, dist, alt, metric) - end -end - - --- Returns the Vec3 coordinates of the average position of the concentration of units most in the heading direction. ---[[ vars for routines.getLeadingPos: -vars.units - table of unit names -vars.heading - direction -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees -]] -routines.getLeadingPos = function(vars) - local units = vars.units - local heading = vars.heading - local radius = vars.radius - if vars.headingDegrees then - heading = routines.utils.toRadian(vars.headingDegrees) - end - - local unitPosTbl = {} - for i = 1, #units do - local unit = Unit.getByName(units[i]) - if unit and unit:isExist() then - unitPosTbl[#unitPosTbl + 1] = unit:getPosition().p - end - end - if #unitPosTbl > 0 then -- one more more units found. - -- first, find the unit most in the heading direction - local maxPos = -math.huge - - local maxPosInd -- maxPos - the furthest in direction defined by heading; maxPosInd = - for i = 1, #unitPosTbl do - local rotatedVec2 = routines.vec.rotateVec2(routines.utils.makeVec2(unitPosTbl[i]), heading) - if (not maxPos) or maxPos < rotatedVec2.x then - maxPos = rotatedVec2.x - maxPosInd = i - end - end - - --now, get all the units around this unit... - local avgPos - if radius then - local maxUnitPos = unitPosTbl[maxPosInd] - local avgx, avgy, avgz, totNum = 0, 0, 0, 0 - for i = 1, #unitPosTbl do - if routines.utils.get2DDist(maxUnitPos, unitPosTbl[i]) <= radius then - avgx = avgx + unitPosTbl[i].x - avgy = avgy + unitPosTbl[i].y - avgz = avgz + unitPosTbl[i].z - totNum = totNum + 1 - end - end - avgPos = { x = avgx/totNum, y = avgy/totNum, z = avgz/totNum} - else - avgPos = unitPosTbl[maxPosInd] - end - - return avgPos - end -end - - ---[[ vars for routines.getLeadingMGRSString: -vars.units - table of unit names -vars.heading - direction -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees -vars.acc - number, 0 to 5. -]] -routines.getLeadingMGRSString = function(vars) - local pos = routines.getLeadingPos(vars) - if pos then - local acc = vars.acc or 5 - return routines.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(pos)), acc) - end -end - ---[[ vars for routines.getLeadingLLString: -vars.units - table of unit names -vars.heading - direction, number -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees -vars.acc - number of digits after decimal point (can be negative) -vars.DMS - boolean, true if you want DMS. -]] -routines.getLeadingLLString = function(vars) - local pos = routines.getLeadingPos(vars) - if pos then - local acc = vars.acc or 3 - local DMS = vars.DMS - local lat, lon = coord.LOtoLL(pos) - return routines.tostringLL(lat, lon, acc, DMS) - end -end - - - ---[[ vars for routines.getLeadingBRString: -vars.units - table of unit names -vars.heading - direction, number -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees -vars.metric - boolean, if true, use km instead of NM. -vars.alt - boolean, if true, include altitude. -vars.ref - vec3/vec2 reference point. -]] -routines.getLeadingBRString = function(vars) - local pos = routines.getLeadingPos(vars) - if pos then - local ref = vars.ref - local alt = vars.alt - local metric = vars.metric - - local vec = {x = pos.x - ref.x, y = pos.y - ref.y, z = pos.z - ref.z} - local dir = routines.utils.getDir(vec, ref) - local dist = routines.utils.get2DDist(pos, ref) - if alt then - alt = pos.y - end - return routines.tostringBR(dir, dist, alt, metric) - end -end - ---[[ vars for routines.message.add - vars.text = 'Hello World' - vars.displayTime = 20 - vars.msgFor = {coa = {'red'}, countries = {'Ukraine', 'Georgia'}, unitTypes = {'A-10C'}} - -]] - ---[[ vars for routines.msgMGRS -vars.units - table of unit names (NOT unitNameTable- maybe this should change). -vars.acc - integer between 0 and 5, inclusive -vars.text - text in the message -vars.displayTime - self explanatory -vars.msgFor - scope -]] -routines.msgMGRS = function(vars) - local units = vars.units - local acc = vars.acc - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getMGRSString{units = units, acc = acc} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } -end - ---[[ vars for routines.msgLL -vars.units - table of unit names (NOT unitNameTable- maybe this should change) (Yes). -vars.acc - integer, number of numbers after decimal place -vars.DMS - if true, output in degrees, minutes, seconds. Otherwise, output in degrees, minutes. -vars.text - text in the message -vars.displayTime - self explanatory -vars.msgFor - scope -]] -routines.msgLL = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local acc = vars.acc - local DMS = vars.DMS - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getLLString{units = units, acc = acc, DMS = DMS} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } - -end - - ---[[ -vars.units- table of unit names (NOT unitNameTable- maybe this should change). -vars.ref - vec3 ref point, maybe overload for vec2 as well? -vars.alt - boolean, if used, includes altitude in string -vars.metric - boolean, gives distance in km instead of NM. -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] -routines.msgBR = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local ref = vars.ref -- vec2/vec3 will be handled in routines.getBRString - local alt = vars.alt - local metric = vars.metric - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getBRString{units = units, ref = ref, alt = alt, metric = metric} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } - -end - - --------------------------------------------------------------------------------------------- --- basically, just sub-types of routines.msgBR... saves folks the work of getting the ref point. ---[[ -vars.units- table of unit names (NOT unitNameTable- maybe this should change). -vars.ref - string red, blue -vars.alt - boolean, if used, includes altitude in string -vars.metric - boolean, gives distance in km instead of NM. -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] -routines.msgBullseye = function(vars) - if string.lower(vars.ref) == 'red' then - vars.ref = routines.DBs.missionData.bullseye.red - routines.msgBR(vars) - elseif string.lower(vars.ref) == 'blue' then - vars.ref = routines.DBs.missionData.bullseye.blue - routines.msgBR(vars) - end -end - ---[[ -vars.units- table of unit names (NOT unitNameTable- maybe this should change). -vars.ref - unit name of reference point -vars.alt - boolean, if used, includes altitude in string -vars.metric - boolean, gives distance in km instead of NM. -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] - -routines.msgBRA = function(vars) - if Unit.getByName(vars.ref) then - vars.ref = Unit.getByName(vars.ref):getPosition().p - if not vars.alt then - vars.alt = true - end - routines.msgBR(vars) - end -end --------------------------------------------------------------------------------------------- - ---[[ vars for routines.msgLeadingMGRS: -vars.units - table of unit names -vars.heading - direction -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees (optional) -vars.acc - number, 0 to 5. -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] -routines.msgLeadingMGRS = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local heading = vars.heading - local radius = vars.radius - local headingDegrees = vars.headingDegrees - local acc = vars.acc - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getLeadingMGRSString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } - - -end ---[[ vars for routines.msgLeadingLL: -vars.units - table of unit names -vars.heading - direction, number -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees (optional) -vars.acc - number of digits after decimal point (can be negative) -vars.DMS - boolean, true if you want DMS. (optional) -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] -routines.msgLeadingLL = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local heading = vars.heading - local radius = vars.radius - local headingDegrees = vars.headingDegrees - local acc = vars.acc - local DMS = vars.DMS - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getLeadingLLString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc, DMS = DMS} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } - -end - ---[[ -vars.units - table of unit names -vars.heading - direction, number -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees (optional) -vars.metric - boolean, if true, use km instead of NM. (optional) -vars.alt - boolean, if true, include altitude. (optional) -vars.ref - vec3/vec2 reference point. -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] -routines.msgLeadingBR = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local heading = vars.heading - local radius = vars.radius - local headingDegrees = vars.headingDegrees - local metric = vars.metric - local alt = vars.alt - local ref = vars.ref -- vec2/vec3 will be handled in routines.getBRString - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getLeadingBRString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, metric = metric, alt = alt, ref = ref} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } -end - - -function spairs(t, order) - -- collect the keys - local keys = {} - for k in pairs(t) do keys[#keys+1] = k end - - -- if order function given, sort by it by passing the table and keys a, b, - -- otherwise just sort the keys - if order then - table.sort(keys, function(a,b) return order(t, a, b) end) - else - table.sort(keys) - end - - -- return the iterator function - local i = 0 - return function() - i = i + 1 - if keys[i] then - return keys[i], t[keys[i]] - end - end -end - - -function routines.IsPartOfGroupInZones( CargoGroup, LandingZones ) ---trace.f() - - local CurrentZoneID = nil - - if CargoGroup then - local CargoUnits = CargoGroup:getUnits() - for CargoUnitID, CargoUnit in pairs( CargoUnits ) do - if CargoUnit and CargoUnit:getLife() >= 1.0 then - CurrentZoneID = routines.IsUnitInZones( CargoUnit, LandingZones ) - if CurrentZoneID then - break - end - end - end - end - ---trace.r( "", "", { CurrentZoneID } ) - return CurrentZoneID -end - - - -function routines.IsUnitInZones( TransportUnit, LandingZones ) ---trace.f("", "routines.IsUnitInZones" ) - - local TransportZoneResult = nil - local TransportZonePos = nil - local TransportZone = nil - - -- fill-up some local variables to support further calculations to determine location of units within the zone. - if TransportUnit then - local TransportUnitPos = TransportUnit:getPosition().p - if type( LandingZones ) == "table" then - for LandingZoneID, LandingZoneName in pairs( LandingZones ) do - TransportZone = trigger.misc.getZone( LandingZoneName ) - if TransportZone then - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then - TransportZoneResult = LandingZoneID - break - end - end - end - else - TransportZone = trigger.misc.getZone( LandingZones ) - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then - TransportZoneResult = 1 - end - end - if TransportZoneResult then - --trace.i( "routines", "TransportZone:" .. TransportZoneResult ) - else - --trace.i( "routines", "TransportZone:nil logic" ) - end - return TransportZoneResult - else - --trace.i( "routines", "TransportZone:nil hard" ) - return nil - end -end - -function routines.IsUnitNearZonesRadius( TransportUnit, LandingZones, ZoneRadius ) ---trace.f("", "routines.IsUnitInZones" ) - - local TransportZoneResult = nil - local TransportZonePos = nil - local TransportZone = nil - - -- fill-up some local variables to support further calculations to determine location of units within the zone. - if TransportUnit then - local TransportUnitPos = TransportUnit:getPosition().p - if type( LandingZones ) == "table" then - for LandingZoneID, LandingZoneName in pairs( LandingZones ) do - TransportZone = trigger.misc.getZone( LandingZoneName ) - if TransportZone then - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= ZoneRadius ) then - TransportZoneResult = LandingZoneID - break - end - end - end - else - TransportZone = trigger.misc.getZone( LandingZones ) - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= ZoneRadius ) then - TransportZoneResult = 1 - end - end - if TransportZoneResult then - --trace.i( "routines", "TransportZone:" .. TransportZoneResult ) - else - --trace.i( "routines", "TransportZone:nil logic" ) - end - return TransportZoneResult - else - --trace.i( "routines", "TransportZone:nil hard" ) - return nil - end -end - - -function routines.IsStaticInZones( TransportStatic, LandingZones ) ---trace.f() - - local TransportZoneResult = nil - local TransportZonePos = nil - local TransportZone = nil - - -- fill-up some local variables to support further calculations to determine location of units within the zone. - local TransportStaticPos = TransportStatic:getPosition().p - if type( LandingZones ) == "table" then - for LandingZoneID, LandingZoneName in pairs( LandingZones ) do - TransportZone = trigger.misc.getZone( LandingZoneName ) - if TransportZone then - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportStaticPos.x - TransportZonePos.x)^2 + (TransportStaticPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then - TransportZoneResult = LandingZoneID - break - end - end - end - else - TransportZone = trigger.misc.getZone( LandingZones ) - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportStaticPos.x - TransportZonePos.x)^2 + (TransportStaticPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then - TransportZoneResult = 1 - end - end - ---trace.r( "", "", { TransportZoneResult } ) - return TransportZoneResult -end - - -function routines.IsUnitInRadius( CargoUnit, ReferencePosition, Radius ) ---trace.f() - - local Valid = true - - -- fill-up some local variables to support further calculations to determine location of units within the zone. - local CargoPos = CargoUnit:getPosition().p - local ReferenceP = ReferencePosition.p - - if (((CargoPos.x - ReferenceP.x)^2 + (CargoPos.z - ReferenceP.z)^2)^0.5 <= Radius) then - else - Valid = false - end - - return Valid -end - -function routines.IsPartOfGroupInRadius( CargoGroup, ReferencePosition, Radius ) ---trace.f() - - local Valid = true - - Valid = routines.ValidateGroup( CargoGroup, "CargoGroup", Valid ) - - -- fill-up some local variables to support further calculations to determine location of units within the zone - local CargoUnits = CargoGroup:getUnits() - for CargoUnitId, CargoUnit in pairs( CargoUnits ) do - local CargoUnitPos = CargoUnit:getPosition().p --- env.info( 'routines.IsPartOfGroupInRadius: CargoUnitPos.x = ' .. CargoUnitPos.x .. ' CargoUnitPos.z = ' .. CargoUnitPos.z ) - local ReferenceP = ReferencePosition.p --- env.info( 'routines.IsPartOfGroupInRadius: ReferenceGroupPos.x = ' .. ReferenceGroupPos.x .. ' ReferenceGroupPos.z = ' .. ReferenceGroupPos.z ) - - if ((( CargoUnitPos.x - ReferenceP.x)^2 + (CargoUnitPos.z - ReferenceP.z)^2)^0.5 <= Radius) then - else - Valid = false - break - end - end - - return Valid -end - - -function routines.ValidateString( Variable, VariableName, Valid ) ---trace.f() - - if type( Variable ) == "string" then - if Variable == "" then - error( "routines.ValidateString: error: " .. VariableName .. " must be filled out!" ) - Valid = false - end - else - error( "routines.ValidateString: error: " .. VariableName .. " is not a string." ) - Valid = false - end - ---trace.r( "", "", { Valid } ) - return Valid -end - -function routines.ValidateNumber( Variable, VariableName, Valid ) ---trace.f() - - if type( Variable ) == "number" then - else - error( "routines.ValidateNumber: error: " .. VariableName .. " is not a number." ) - Valid = false - end - ---trace.r( "", "", { Valid } ) - return Valid - -end - -function routines.ValidateGroup( Variable, VariableName, Valid ) ---trace.f() - - if Variable == nil then - error( "routines.ValidateGroup: error: " .. VariableName .. " is a nil value!" ) - Valid = false - end - ---trace.r( "", "", { Valid } ) - return Valid -end - -function routines.ValidateZone( LandingZones, VariableName, Valid ) ---trace.f() - - if LandingZones == nil then - error( "routines.ValidateGroup: error: " .. VariableName .. " is a nil value!" ) - Valid = false - end - - if type( LandingZones ) == "table" then - for LandingZoneID, LandingZoneName in pairs( LandingZones ) do - if trigger.misc.getZone( LandingZoneName ) == nil then - error( "routines.ValidateGroup: error: Zone " .. LandingZoneName .. " does not exist!" ) - Valid = false - break - end - end - else - if trigger.misc.getZone( LandingZones ) == nil then - error( "routines.ValidateGroup: error: Zone " .. LandingZones .. " does not exist!" ) - Valid = false - end - end - ---trace.r( "", "", { Valid } ) - return Valid -end - -function routines.ValidateEnumeration( Variable, VariableName, Enum, Valid ) ---trace.f() - - local ValidVariable = false - - for EnumId, EnumData in pairs( Enum ) do - if Variable == EnumData then - ValidVariable = true - break - end - end - - if ValidVariable then - else - error( 'TransportValidateEnum: " .. VariableName .. " is not a valid type.' .. Variable ) - Valid = false - end - ---trace.r( "", "", { Valid } ) - return Valid -end - -function routines.getGroupRoute(groupIdent, task) -- same as getGroupPoints but returns speed and formation type along with vec2 of point} - -- refactor to search by groupId and allow groupId and groupName as inputs - local gpId = groupIdent - if type(groupIdent) == 'string' and not tonumber(groupIdent) then - gpId = _DATABASE.Templates.Groups[groupIdent].groupId - end - - for coa_name, coa_data in pairs(env.mission.coalition) do - if (coa_name == 'red' or coa_name == 'blue') and type(coa_data) == 'table' then - if coa_data.country then --there is a country table - for cntry_id, cntry_data in pairs(coa_data.country) do - for obj_type_name, obj_type_data in pairs(cntry_data) do - if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" then -- only these types have points - if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! - for group_num, group_data in pairs(obj_type_data.group) do - if group_data and group_data.groupId == gpId then -- this is the group we are looking for - if group_data.route and group_data.route.points and #group_data.route.points > 0 then - local points = {} - - for point_num, point in pairs(group_data.route.points) do - local routeData = {} - if not point.point then - routeData.x = point.x - routeData.y = point.y - else - routeData.point = point.point --it's possible that the ME could move to the point = Vec2 notation. - end - routeData.form = point.action - routeData.speed = point.speed - routeData.alt = point.alt - routeData.alt_type = point.alt_type - routeData.airdromeId = point.airdromeId - routeData.helipadId = point.helipadId - routeData.type = point.type - routeData.action = point.action - if task then - routeData.task = point.task - end - points[point_num] = routeData - end - - return points - end - return - end --if group_data and group_data.name and group_data.name == 'groupname' - end --for group_num, group_data in pairs(obj_type_data.group) do - end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then - end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then - end --for obj_type_name, obj_type_data in pairs(cntry_data) do - end --for cntry_id, cntry_data in pairs(coa_data.country) do - end --if coa_data.country then --there is a country table - end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then - end --for coa_name, coa_data in pairs(mission.coalition) do -end - -routines.ground.patrolRoute = function(vars) - - - local tempRoute = {} - local useRoute = {} - local gpData = vars.gpData - if type(gpData) == 'string' then - gpData = Group.getByName(gpData) - end - - local useGroupRoute - if not vars.useGroupRoute then - useGroupRoute = vars.gpData - else - useGroupRoute = vars.useGroupRoute - end - local routeProvided = false - if not vars.route then - if useGroupRoute then - tempRoute = routines.getGroupRoute(useGroupRoute) - end - else - useRoute = vars.route - local posStart = routines.getLeadPos(gpData) - useRoute[1] = routines.ground.buildWP(posStart, useRoute[1].action, useRoute[1].speed) - routeProvided = true - end - - - local overRideSpeed = vars.speed or 'default' - local pType = vars.pType - local offRoadForm = vars.offRoadForm or 'default' - local onRoadForm = vars.onRoadForm or 'default' - - if routeProvided == false and #tempRoute > 0 then - local posStart = routines.getLeadPos(gpData) - - - useRoute[#useRoute + 1] = routines.ground.buildWP(posStart, offRoadForm, overRideSpeed) - for i = 1, #tempRoute do - local tempForm = tempRoute[i].action - local tempSpeed = tempRoute[i].speed - - if offRoadForm == 'default' then - tempForm = tempRoute[i].action - end - if onRoadForm == 'default' then - onRoadForm = 'On Road' - end - if (string.lower(tempRoute[i].action) == 'on road' or string.lower(tempRoute[i].action) == 'onroad' or string.lower(tempRoute[i].action) == 'on_road') then - tempForm = onRoadForm + if not Include.Files[ IncludeFile ] then + Include.Files[IncludeFile] = IncludeFile + env.info( "Include:" .. IncludeFile .. " from " .. Include.ProgramPath ) + local f = assert( base.loadfile( Include.ProgramPath .. IncludeFile .. ".lua" ) ) + if f == nil then + env.info( "Include:" .. IncludeFile .. " from " .. Include.MissionPath ) + local f = assert( base.loadfile( Include.MissionPath .. IncludeFile .. ".lua" ) ) + if f == nil then + error ("Could not load MOOSE file " .. IncludeFile .. ".lua" ) else - tempForm = offRoadForm - end - - if type(overRideSpeed) == 'number' then - tempSpeed = overRideSpeed - end - - - useRoute[#useRoute + 1] = routines.ground.buildWP(tempRoute[i], tempForm, tempSpeed) - end - - if pType and string.lower(pType) == 'doubleback' then - local curRoute = routines.utils.deepCopy(useRoute) - for i = #curRoute, 2, -1 do - useRoute[#useRoute + 1] = routines.ground.buildWP(curRoute[i], curRoute[i].action, curRoute[i].speed) - end - end - - useRoute[1].action = useRoute[#useRoute].action -- make it so the first WP matches the last WP - end - - local cTask3 = {} - local newPatrol = {} - newPatrol.route = useRoute - newPatrol.gpData = gpData:getName() - cTask3[#cTask3 + 1] = 'routines.ground.patrolRoute(' - cTask3[#cTask3 + 1] = routines.utils.oneLineSerialize(newPatrol) - cTask3[#cTask3 + 1] = ')' - cTask3 = table.concat(cTask3) - local tempTask = { - id = 'WrappedAction', - params = { - action = { - id = 'Script', - params = { - command = cTask3, - - }, - }, - }, - } - - - useRoute[#useRoute].task = tempTask - routines.goRoute(gpData, useRoute) - - return -end - -routines.ground.patrol = function(gpData, pType, form, speed) - local vars = {} - - if type(gpData) == 'table' and gpData:getName() then - gpData = gpData:getName() - end - - vars.useGroupRoute = gpData - vars.gpData = gpData - vars.pType = pType - vars.offRoadForm = form - vars.speed = speed - - routines.ground.patrolRoute(vars) - - return -end - -function routines.GetUnitHeight( CheckUnit ) ---trace.f( "routines" ) - - local UnitPoint = CheckUnit:getPoint() - local UnitPosition = { x = UnitPoint.x, y = UnitPoint.z } - local UnitHeight = UnitPoint.y - - local LandHeight = land.getHeight( UnitPosition ) - - --env.info(( 'CarrierHeight: LandHeight = ' .. LandHeight .. ' CarrierHeight = ' .. CarrierHeight )) - - --trace.f( "routines", "Unit Height = " .. UnitHeight - LandHeight ) - - return UnitHeight - LandHeight - -end - - - -Su34Status = { status = {} } -boardMsgRed = { statusMsg = "" } -boardMsgAll = { timeMsg = "" } -SpawnSettings = {} -Su34MenuPath = {} -Su34Menus = 0 - - -function Su34AttackCarlVinson(groupName) ---trace.menu("", "Su34AttackCarlVinson") - local groupSu34 = Group.getByName( groupName ) - local controllerSu34 = groupSu34.getController(groupSu34) - local groupCarlVinson = Group.getByName("US Carl Vinson #001") - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE ) - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) - if groupCarlVinson ~= nil then - controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupCarlVinson:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = true}}) - end - Su34Status.status[groupName] = 1 - MessageToRed( string.format('%s: ',groupName) .. 'Attacking carrier Carl Vinson. ', 10, 'RedStatus' .. groupName ) -end - -function Su34AttackWest(groupName) ---trace.f("","Su34AttackWest") - local groupSu34 = Group.getByName( groupName ) - local controllerSu34 = groupSu34.getController(groupSu34) - local groupShipWest1 = Group.getByName("US Ship West #001") - local groupShipWest2 = Group.getByName("US Ship West #002") - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE ) - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) - if groupShipWest1 ~= nil then - controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipWest1:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = true}}) - end - if groupShipWest2 ~= nil then - controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipWest2:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = true}}) - end - Su34Status.status[groupName] = 2 - MessageToRed( string.format('%s: ',groupName) .. 'Attacking invading ships in the west. ', 10, 'RedStatus' .. groupName ) -end - -function Su34AttackNorth(groupName) ---trace.menu("","Su34AttackNorth") - local groupSu34 = Group.getByName( groupName ) - local controllerSu34 = groupSu34.getController(groupSu34) - local groupShipNorth1 = Group.getByName("US Ship North #001") - local groupShipNorth2 = Group.getByName("US Ship North #002") - local groupShipNorth3 = Group.getByName("US Ship North #003") - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE ) - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) - if groupShipNorth1 ~= nil then - controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipNorth1:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = false}}) - end - if groupShipNorth2 ~= nil then - controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipNorth2:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = false}}) - end - if groupShipNorth3 ~= nil then - controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipNorth3:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = false}}) - end - Su34Status.status[groupName] = 3 - MessageToRed( string.format('%s: ',groupName) .. 'Attacking invading ships in the north. ', 10, 'RedStatus' .. groupName ) -end - -function Su34Orbit(groupName) ---trace.menu("","Su34Orbit") - local groupSu34 = Group.getByName( groupName ) - local controllerSu34 = groupSu34:getController() - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD ) - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) - controllerSu34:pushTask( {id = 'ControlledTask', params = { task = { id = 'Orbit', params = { pattern = AI.Task.OrbitPattern.RACE_TRACK } }, stopCondition = { duration = 600 } } } ) - Su34Status.status[groupName] = 4 - MessageToRed( string.format('%s: ',groupName) .. 'In orbit and awaiting further instructions. ', 10, 'RedStatus' .. groupName ) -end - -function Su34TakeOff(groupName) ---trace.menu("","Su34TakeOff") - local groupSu34 = Group.getByName( groupName ) - local controllerSu34 = groupSu34:getController() - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD ) - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.BYPASS_AND_ESCAPE ) - Su34Status.status[groupName] = 8 - MessageToRed( string.format('%s: ',groupName) .. 'Take-Off. ', 10, 'RedStatus' .. groupName ) -end - -function Su34Hold(groupName) ---trace.menu("","Su34Hold") - local groupSu34 = Group.getByName( groupName ) - local controllerSu34 = groupSu34:getController() - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD ) - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.BYPASS_AND_ESCAPE ) - Su34Status.status[groupName] = 5 - MessageToRed( string.format('%s: ',groupName) .. 'Holding Weapons. ', 10, 'RedStatus' .. groupName ) -end - -function Su34RTB(groupName) ---trace.menu("","Su34RTB") - Su34Status.status[groupName] = 6 - MessageToRed( string.format('%s: ',groupName) .. 'Return to Krasnodar. ', 10, 'RedStatus' .. groupName ) -end - -function Su34Destroyed(groupName) ---trace.menu("","Su34Destroyed") - Su34Status.status[groupName] = 7 - MessageToRed( string.format('%s: ',groupName) .. 'Destroyed. ', 30, 'RedStatus' .. groupName ) -end - -function GroupAlive( groupName ) ---trace.menu("","GroupAlive") - local groupTest = Group.getByName( groupName ) - - local groupExists = false - - if groupTest then - groupExists = groupTest:isExist() - end - - --trace.r( "", "", { groupExists } ) - return groupExists -end - -function Su34IsDead() ---trace.f() - -end - -function Su34OverviewStatus() ---trace.menu("","Su34OverviewStatus") - local msg = "" - local currentStatus = 0 - local Exists = false - - for groupName, currentStatus in pairs(Su34Status.status) do - - env.info(('Su34 Overview Status: GroupName = ' .. groupName )) - Alive = GroupAlive( groupName ) - - if Alive then - if currentStatus == 1 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Attacking carrier Carl Vinson. " - elseif currentStatus == 2 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Attacking supporting ships in the west. " - elseif currentStatus == 3 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Attacking invading ships in the north. " - elseif currentStatus == 4 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "In orbit and awaiting further instructions. " - elseif currentStatus == 5 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Holding Weapons. " - elseif currentStatus == 6 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Return to Krasnodar. " - elseif currentStatus == 7 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Destroyed. " - elseif currentStatus == 8 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Take-Off. " + env.info( "Include:" .. IncludeFile .. " loaded from " .. Include.MissionPath ) + return f() end else - if currentStatus == 7 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Destroyed. " - else - Su34Destroyed(groupName) - end - end - end - - boardMsgRed.statusMsg = msg -end - - -function UpdateBoardMsg() ---trace.f() - Su34OverviewStatus() - MessageToRed( boardMsgRed.statusMsg, 15, 'RedStatus' ) -end - -function MusicReset( flg ) ---trace.f() - trigger.action.setUserFlag(95,flg) -end - -function PlaneActivate(groupNameFormat, flg) ---trace.f() - local groupName = groupNameFormat .. string.format("#%03d", trigger.misc.getUserFlag(flg)) - --trigger.action.outText(groupName,10) - trigger.action.activateGroup(Group.getByName(groupName)) -end - -function Su34Menu(groupName) ---trace.f() - - --env.info(( 'Su34Menu(' .. groupName .. ')' )) - local groupSu34 = Group.getByName( groupName ) - - if Su34Status.status[groupName] == 1 or - Su34Status.status[groupName] == 2 or - Su34Status.status[groupName] == 3 or - Su34Status.status[groupName] == 4 or - Su34Status.status[groupName] == 5 then - if Su34MenuPath[groupName] == nil then - if planeMenuPath == nil then - planeMenuPath = missionCommands.addSubMenuForCoalition( - coalition.side.RED, - "SU-34 anti-ship flights", - nil - ) - end - Su34MenuPath[groupName] = missionCommands.addSubMenuForCoalition( - coalition.side.RED, - "Flight " .. groupName, - planeMenuPath - ) - - missionCommands.addCommandForCoalition( - coalition.side.RED, - "Attack carrier Carl Vinson", - Su34MenuPath[groupName], - Su34AttackCarlVinson, - groupName - ) - - missionCommands.addCommandForCoalition( - coalition.side.RED, - "Attack ships in the west", - Su34MenuPath[groupName], - Su34AttackWest, - groupName - ) - - missionCommands.addCommandForCoalition( - coalition.side.RED, - "Attack ships in the north", - Su34MenuPath[groupName], - Su34AttackNorth, - groupName - ) - - missionCommands.addCommandForCoalition( - coalition.side.RED, - "Hold position and await instructions", - Su34MenuPath[groupName], - Su34Orbit, - groupName - ) - - missionCommands.addCommandForCoalition( - coalition.side.RED, - "Report status", - Su34MenuPath[groupName], - Su34OverviewStatus - ) - end - else - if Su34MenuPath[groupName] then - missionCommands.removeItemForCoalition(coalition.side.RED, Su34MenuPath[groupName]) + env.info( "Include:" .. IncludeFile .. " loaded from " .. Include.ProgramPath ) + return f() end end end ---- Obsolete function, but kept to rework in framework. +Include.ProgramPath = "Scripts/Moose/" +Include.MissionPath = Include.Path() -function ChooseInfantry ( TeleportPrefixTable, TeleportMax ) ---trace.f("Spawn") - --env.info(( 'ChooseInfantry: ' )) +env.info( "Include.ProgramPath = " .. Include.ProgramPath) +env.info( "Include.MissionPath = " .. Include.MissionPath) - TeleportPrefixTableCount = #TeleportPrefixTable - TeleportPrefixTableIndex = math.random( 1, TeleportPrefixTableCount ) +Include.Files = {} - --env.info(( 'ChooseInfantry: TeleportPrefixTableIndex = ' .. TeleportPrefixTableIndex .. ' TeleportPrefixTableCount = ' .. TeleportPrefixTableCount .. ' TeleportMax = ' .. TeleportMax )) - - local TeleportFound = false - local TeleportLoop = true - local Index = TeleportPrefixTableIndex - local TeleportPrefix = '' - - while TeleportLoop do - TeleportPrefix = TeleportPrefixTable[Index] - if SpawnSettings[TeleportPrefix] then - if SpawnSettings[TeleportPrefix]['SpawnCount'] - 1 < TeleportMax then - SpawnSettings[TeleportPrefix]['SpawnCount'] = SpawnSettings[TeleportPrefix]['SpawnCount'] + 1 - TeleportFound = true - else - TeleportFound = false - end - else - SpawnSettings[TeleportPrefix] = {} - SpawnSettings[TeleportPrefix]['SpawnCount'] = 0 - TeleportFound = true - end - if TeleportFound then - TeleportLoop = false - else - if Index < TeleportPrefixTableCount then - Index = Index + 1 - else - TeleportLoop = false - end - end - --env.info(( 'ChooseInfantry: Loop 1 - TeleportPrefix = ' .. TeleportPrefix .. ' Index = ' .. Index )) - end - - if TeleportFound == false then - TeleportLoop = true - Index = 1 - while TeleportLoop do - TeleportPrefix = TeleportPrefixTable[Index] - if SpawnSettings[TeleportPrefix] then - if SpawnSettings[TeleportPrefix]['SpawnCount'] - 1 < TeleportMax then - SpawnSettings[TeleportPrefix]['SpawnCount'] = SpawnSettings[TeleportPrefix]['SpawnCount'] + 1 - TeleportFound = true - else - TeleportFound = false - end - else - SpawnSettings[TeleportPrefix] = {} - SpawnSettings[TeleportPrefix]['SpawnCount'] = 0 - TeleportFound = true - end - if TeleportFound then - TeleportLoop = false - else - if Index < TeleportPrefixTableIndex then - Index = Index + 1 - else - TeleportLoop = false - end - end - --env.info(( 'ChooseInfantry: Loop 2 - TeleportPrefix = ' .. TeleportPrefix .. ' Index = ' .. Index )) - end - end - - local TeleportGroupName = '' - if TeleportFound == true then - TeleportGroupName = TeleportPrefix .. string.format("#%03d", SpawnSettings[TeleportPrefix]['SpawnCount'] ) - else - TeleportGroupName = '' - end - - --env.info(('ChooseInfantry: TeleportGroupName = ' .. TeleportGroupName )) - --env.info(('ChooseInfantry: return')) - - return TeleportGroupName -end - -SpawnedInfantry = 0 - -function LandCarrier ( CarrierGroup, LandingZonePrefix ) ---trace.f() - --env.info(( 'LandCarrier: ' )) - --env.info(( 'LandCarrier: CarrierGroup = ' .. CarrierGroup:getName() )) - --env.info(( 'LandCarrier: LandingZone = ' .. LandingZonePrefix )) - - local controllerGroup = CarrierGroup:getController() - - local LandingZone = trigger.misc.getZone(LandingZonePrefix) - local LandingZonePos = {} - LandingZonePos.x = LandingZone.point.x + math.random(LandingZone.radius * -1, LandingZone.radius) - LandingZonePos.y = LandingZone.point.z + math.random(LandingZone.radius * -1, LandingZone.radius) - - controllerGroup:pushTask( { id = 'Land', params = { point = LandingZonePos, durationFlag = true, duration = 10 } } ) - - --env.info(( 'LandCarrier: end' )) -end - -EscortCount = 0 -function EscortCarrier ( CarrierGroup, EscortPrefix, EscortLastWayPoint, EscortEngagementDistanceMax, EscortTargetTypes ) ---trace.f() - --env.info(( 'EscortCarrier: ' )) - --env.info(( 'EscortCarrier: CarrierGroup = ' .. CarrierGroup:getName() )) - --env.info(( 'EscortCarrier: EscortPrefix = ' .. EscortPrefix )) - - local CarrierName = CarrierGroup:getName() - - local EscortMission = {} - local CarrierMission = {} - - local EscortMission = SpawnMissionGroup( EscortPrefix ) - local CarrierMission = SpawnMissionGroup( CarrierGroup:getName() ) - - if EscortMission ~= nil and CarrierMission ~= nil then - - EscortCount = EscortCount + 1 - EscortMissionName = string.format( EscortPrefix .. '#Escort %s', CarrierName ) - EscortMission.name = EscortMissionName - EscortMission.groupId = nil - EscortMission.lateActivation = false - EscortMission.taskSelected = false - - local EscortUnits = #EscortMission.units - for u = 1, EscortUnits do - EscortMission.units[u].name = string.format( EscortPrefix .. '#Escort %s %02d', CarrierName, u ) - EscortMission.units[u].unitId = nil - end - - - EscortMission.route.points[1].task = { id = "ComboTask", - params = - { - tasks = - { - [1] = - { - enabled = true, - auto = false, - id = "Escort", - number = 1, - params = - { - lastWptIndexFlagChangedManually = false, - groupId = CarrierGroup:getID(), - lastWptIndex = nil, - lastWptIndexFlag = false, - engagementDistMax = EscortEngagementDistanceMax, - targetTypes = EscortTargetTypes, - pos = - { - y = 20, - x = 20, - z = 0, - } -- end of ["pos"] - } -- end of ["params"] - } -- end of [1] - } -- end of ["tasks"] - } -- end of ["params"] - } -- end of ["task"] - - SpawnGroupAdd( EscortPrefix, EscortMission ) - - end -end - -function SendMessageToCarrier( CarrierGroup, CarrierMessage ) ---trace.f() - - if CarrierGroup ~= nil then - MessageToGroup( CarrierGroup, CarrierMessage, 30, 'Carrier/' .. CarrierGroup:getName() ) - end - -end - -function MessageToGroup( MsgGroup, MsgText, MsgTime, MsgName ) ---trace.f() - - if type(MsgGroup) == 'string' then - --env.info( 'MessageToGroup: Converted MsgGroup string "' .. MsgGroup .. '" into a Group structure.' ) - MsgGroup = Group.getByName( MsgGroup ) - end - - if MsgGroup ~= nil then - local MsgTable = {} - MsgTable.text = MsgText - MsgTable.displayTime = MsgTime - MsgTable.msgFor = { units = { MsgGroup:getUnits()[1]:getName() } } - MsgTable.name = MsgName - --routines.message.add( MsgTable ) - --env.info(('MessageToGroup: Message sent to ' .. MsgGroup:getUnits()[1]:getName() .. ' -> ' .. MsgText )) - end -end - -function MessageToUnit( UnitName, MsgText, MsgTime, MsgName ) ---trace.f() - - if UnitName ~= nil then - local MsgTable = {} - MsgTable.text = MsgText - MsgTable.displayTime = MsgTime - MsgTable.msgFor = { units = { UnitName } } - MsgTable.name = MsgName - --routines.message.add( MsgTable ) - end -end - -function MessageToAll( MsgText, MsgTime, MsgName ) ---trace.f() - - MESSAGE:New( MsgText, MsgTime, "Message" ):ToCoalition( coalition.side.RED ):ToCoalition( coalition.side.BLUE ) -end - -function MessageToRed( MsgText, MsgTime, MsgName ) ---trace.f() - - MESSAGE:New( MsgText, MsgTime, "To Red Coalition" ):ToCoalition( coalition.side.RED ) -end - -function MessageToBlue( MsgText, MsgTime, MsgName ) ---trace.f() - - MESSAGE:New( MsgText, MsgTime, "To Blue Coalition" ):ToCoalition( coalition.side.RED ) -end - -function getCarrierHeight( CarrierGroup ) ---trace.f() - - if CarrierGroup ~= nil then - if table.getn(CarrierGroup:getUnits()) == 1 then - local CarrierUnit = CarrierGroup:getUnits()[1] - local CurrentPoint = CarrierUnit:getPoint() - - local CurrentPosition = { x = CurrentPoint.x, y = CurrentPoint.z } - local CarrierHeight = CurrentPoint.y - - local LandHeight = land.getHeight( CurrentPosition ) - - --env.info(( 'CarrierHeight: LandHeight = ' .. LandHeight .. ' CarrierHeight = ' .. CarrierHeight )) - - return CarrierHeight - LandHeight - else - return 999999 - end - else - return 999999 - end - -end - -function GetUnitHeight( CheckUnit ) ---trace.f() - - local UnitPoint = CheckUnit:getPoint() - local UnitPosition = { x = CurrentPoint.x, y = CurrentPoint.z } - local UnitHeight = CurrentPoint.y - - local LandHeight = land.getHeight( CurrentPosition ) - - --env.info(( 'CarrierHeight: LandHeight = ' .. LandHeight .. ' CarrierHeight = ' .. CarrierHeight )) - - return UnitHeight - LandHeight - -end - - -_MusicTable = {} -_MusicTable.Files = {} -_MusicTable.Queue = {} -_MusicTable.FileCnt = 0 - - -function MusicRegister( SndRef, SndFile, SndTime ) ---trace.f() - - env.info(( 'MusicRegister: SndRef = ' .. SndRef )) - env.info(( 'MusicRegister: SndFile = ' .. SndFile )) - env.info(( 'MusicRegister: SndTime = ' .. SndTime )) - - - _MusicTable.FileCnt = _MusicTable.FileCnt + 1 - - _MusicTable.Files[_MusicTable.FileCnt] = {} - _MusicTable.Files[_MusicTable.FileCnt].Ref = SndRef - _MusicTable.Files[_MusicTable.FileCnt].File = SndFile - _MusicTable.Files[_MusicTable.FileCnt].Time = SndTime - - if not _MusicTable.Function then - _MusicTable.Function = routines.scheduleFunction( MusicScheduler, { }, timer.getTime() + 10, 10) - end - -end - -function MusicToPlayer( SndRef, PlayerName, SndContinue ) ---trace.f() - - --env.info(( 'MusicToPlayer: SndRef = ' .. SndRef )) - - local PlayerUnits = AlivePlayerUnits() - for PlayerUnitIdx, PlayerUnit in pairs(PlayerUnits) do - local PlayerUnitName = PlayerUnit:getPlayerName() - --env.info(( 'MusicToPlayer: PlayerUnitName = ' .. PlayerUnitName )) - if PlayerName == PlayerUnitName then - PlayerGroup = PlayerUnit:getGroup() - if PlayerGroup then - --env.info(( 'MusicToPlayer: PlayerGroup = ' .. PlayerGroup:getName() )) - MusicToGroup( SndRef, PlayerGroup, SndContinue ) - end - break - end - end - - --env.info(( 'MusicToPlayer: end' )) - -end - -function MusicToGroup( SndRef, SndGroup, SndContinue ) ---trace.f() - - --env.info(( 'MusicToGroup: SndRef = ' .. SndRef )) - - if SndGroup ~= nil then - if _MusicTable and _MusicTable.FileCnt > 0 then - if SndGroup:isExist() then - if MusicCanStart(SndGroup:getUnit(1):getPlayerName()) then - --env.info(( 'MusicToGroup: OK for Sound.' )) - local SndIdx = 0 - if SndRef == '' then - --env.info(( 'MusicToGroup: SndRef as empty. Queueing at random.' )) - SndIdx = math.random( 1, _MusicTable.FileCnt ) - else - for SndIdx = 1, _MusicTable.FileCnt do - if _MusicTable.Files[SndIdx].Ref == SndRef then - break - end - end - end - --env.info(( 'MusicToGroup: SndIdx = ' .. SndIdx )) - --env.info(( 'MusicToGroup: Queueing Music ' .. _MusicTable.Files[SndIdx].File .. ' for Group ' .. SndGroup:getID() )) - trigger.action.outSoundForGroup( SndGroup:getID(), _MusicTable.Files[SndIdx].File ) - MessageToGroup( SndGroup, 'Playing ' .. _MusicTable.Files[SndIdx].File, 15, 'Music-' .. SndGroup:getUnit(1):getPlayerName() ) - - local SndQueueRef = SndGroup:getUnit(1):getPlayerName() - if _MusicTable.Queue[SndQueueRef] == nil then - _MusicTable.Queue[SndQueueRef] = {} - end - _MusicTable.Queue[SndQueueRef].Start = timer.getTime() - _MusicTable.Queue[SndQueueRef].PlayerName = SndGroup:getUnit(1):getPlayerName() - _MusicTable.Queue[SndQueueRef].Group = SndGroup - _MusicTable.Queue[SndQueueRef].ID = SndGroup:getID() - _MusicTable.Queue[SndQueueRef].Ref = SndIdx - _MusicTable.Queue[SndQueueRef].Continue = SndContinue - _MusicTable.Queue[SndQueueRef].Type = Group - end - end - end - end -end - -function MusicCanStart(PlayerName) ---trace.f() - - --env.info(( 'MusicCanStart:' )) - - local MusicOut = false - - if _MusicTable['Queue'] ~= nil and _MusicTable.FileCnt > 0 then - --env.info(( 'MusicCanStart: PlayerName = ' .. PlayerName )) - local PlayerFound = false - local MusicStart = 0 - local MusicTime = 0 - for SndQueueIdx, SndQueue in pairs( _MusicTable.Queue ) do - if SndQueue.PlayerName == PlayerName then - PlayerFound = true - MusicStart = SndQueue.Start - MusicTime = _MusicTable.Files[SndQueue.Ref].Time - break - end - end - if PlayerFound then - --env.info(( 'MusicCanStart: MusicStart = ' .. MusicStart )) - --env.info(( 'MusicCanStart: MusicTime = ' .. MusicTime )) - --env.info(( 'MusicCanStart: timer.getTime() = ' .. timer.getTime() )) - - if MusicStart + MusicTime <= timer.getTime() then - MusicOut = true - end - else - MusicOut = true - end - end - - if MusicOut then - --env.info(( 'MusicCanStart: true' )) - else - --env.info(( 'MusicCanStart: false' )) - end - - return MusicOut -end - -function MusicScheduler() ---trace.scheduled("", "MusicScheduler") - - --env.info(( 'MusicScheduler:' )) - if _MusicTable['Queue'] ~= nil and _MusicTable.FileCnt > 0 then - --env.info(( 'MusicScheduler: Walking Sound Queue.')) - for SndQueueIdx, SndQueue in pairs( _MusicTable.Queue ) do - if SndQueue.Continue then - if MusicCanStart(SndQueue.PlayerName) then - --env.info(('MusicScheduler: MusicToGroup')) - MusicToPlayer( '', SndQueue.PlayerName, true ) - end - end - end - end - -end - - -env.info(( 'Init: Scripts Loaded v1.1' )) - ---- This module contains the BASE class. --- --- 1) @{#BASE} class --- ================= --- The @{#BASE} class is the super class for all the classes defined within MOOSE. --- --- It handles: --- --- * The construction and inheritance of child classes. --- * The tracing of objects during mission execution within the **DCS.log** file, under the **"Saved Games\DCS\Logs"** folder. --- --- Note: Normally you would not use the BASE class unless you are extending the MOOSE framework with new classes. --- --- 1.1) BASE constructor --- --------------------- --- Any class derived from BASE, must use the @{Base#BASE.New) constructor within the @{Base#BASE.Inherit) method. --- See an example at the @{Base#BASE.New} method how this is done. --- --- 1.2) BASE Trace functionality --- ----------------------------- --- The BASE class contains trace methods to trace progress within a mission execution of a certain object. --- Note that these trace methods are inherited by each MOOSE class interiting BASE. --- As such, each object created from derived class from BASE can use the tracing functions to trace its execution. --- --- 1.2.1) Tracing functions --- ------------------------ --- There are basically 3 types of tracing methods available within BASE: --- --- * @{#BASE.F}: Trace the beginning of a function and its given parameters. An F is indicated at column 44 in the DCS.log file. --- * @{#BASE.T}: 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}: Trace an exception within a function giving optional variables or parameters. An E is indicated at column 44 in the DCS.log file. An exception will always be traced. --- --- 1.2.2) Tracing levels --- --------------------- --- There are 3 tracing levels within MOOSE. --- These tracing levels were defined to avoid bulks of tracing to be generated by lots of objects. --- --- As such, the F and T methods have additional variants to trace level 2 and 3 respectively: --- --- * @{#BASE.F2}: Trace the beginning of a function and its given parameters with tracing level 2. --- * @{#BASE.F3}: Trace the beginning of a function and its given parameters with tracing level 3. --- * @{#BASE.T2}: Trace further logic within a function giving optional variables or parameters with tracing level 2. --- * @{#BASE.T3}: Trace further logic within a function giving optional variables or parameters with tracing level 3. --- --- 1.3) BASE Inheritance support --- =========================== --- The following methods are available to support inheritance: --- --- * @{#BASE.Inherit}: Inherits from a class. --- * @{#BASE.Inherited}: Returns the parent class from the class. --- --- Future --- ====== --- Further methods may be added to BASE whenever there is a need to make "overall" functions available within MOOSE. --- --- ==== --- --- @module Base --- @author FlightControl - - - -local _TraceOnOff = true -local _TraceLevel = 1 -local _TraceAll = false -local _TraceClass = {} -local _TraceClassMethod = {} - -local _ClassID = 0 - ---- The BASE Class --- @type BASE --- @field ClassName The name of the class. --- @field ClassID The ID number of the class. --- @field ClassNameAndID The name of the class concatenated with the ID number of the class. -BASE = { - ClassName = "BASE", - ClassID = 0, - Events = {}, - States = {} -} - ---- The Formation Class --- @type FORMATION --- @field Cone A cone formation. -FORMATION = { - Cone = "Cone" -} - - - ---- The base constructor. This is the top top class of all classed defined within the MOOSE. --- Any new class needs to be derived from this class for proper inheritance. --- @param #BASE self --- @return #BASE The new instance of the BASE class. --- @usage --- -- This declares the constructor of the class TASK, inheriting from BASE. --- --- TASK constructor --- -- @param #TASK self --- -- @param Parameter The parameter of the New constructor. --- -- @return #TASK self --- function TASK:New( Parameter ) --- --- local self = BASE:Inherit( self, BASE:New() ) --- --- self.Variable = Parameter --- --- return self --- end --- @todo need to investigate if the deepCopy is really needed... Don't think so. -function BASE:New() - local self = routines.utils.deepCopy( self ) -- Create a new self instance - local MetaTable = {} - setmetatable( self, MetaTable ) - self.__index = self - _ClassID = _ClassID + 1 - self.ClassID = _ClassID - self.ClassNameAndID = string.format( '%s#%09d', self.ClassName, self.ClassID ) - return self -end - ---- This is the worker method to inherit from a parent class. --- @param #BASE self --- @param Child is the Child class that inherits. --- @param #BASE Parent is the Parent class that the Child inherits from. --- @return #BASE Child -function BASE:Inherit( Child, Parent ) - local Child = routines.utils.deepCopy( Child ) - --local Parent = routines.utils.deepCopy( Parent ) - --local Parent = Parent - if Child ~= nil then - setmetatable( Child, Parent ) - Child.__index = Child - end - --Child.ClassName = Child.ClassName .. '.' .. Child.ClassID - self:T( 'Inherited from ' .. Parent.ClassName ) - return Child -end - ---- This is the worker method to retrieve the Parent class. --- @param #BASE self --- @param #BASE Child is the Child class from which the Parent class needs to be retrieved. --- @return #BASE -function BASE:Inherited( Child ) - local Parent = getmetatable( Child ) --- env.info('Inherited class of ' .. Child.ClassName .. ' is ' .. Parent.ClassName ) - return Parent -end - ---- Get the ClassName + ClassID of the class instance. --- The ClassName + ClassID is formatted as '%s#%09d'. --- @param #BASE self --- @return #string The ClassName + ClassID of the class instance. -function BASE:GetClassNameAndID() - return self.ClassNameAndID -end - ---- Get the ClassName of the class instance. --- @param #BASE self --- @return #string The ClassName of the class instance. -function BASE:GetClassName() - return self.ClassName -end - ---- Get the ClassID of the class instance. --- @param #BASE self --- @return #string The ClassID of the class instance. -function BASE:GetClassID() - return self.ClassID -end - ---- Set a new listener for the class. --- @param self --- @param DCSTypes#Event Event --- @param #function EventFunction --- @return #BASE -function BASE:AddEvent( Event, EventFunction ) - self:F( Event ) - - self.Events[#self.Events+1] = {} - self.Events[#self.Events].Event = Event - self.Events[#self.Events].EventFunction = EventFunction - self.Events[#self.Events].EventEnabled = false - - return self -end - ---- Returns the event dispatcher --- @param #BASE self --- @return Event#EVENT -function BASE:Event() - - return _EVENTDISPATCHER -end - - - - - ---- Enable the event listeners for the class. --- @param #BASE self --- @return #BASE -function BASE:EnableEvents() - self:F( #self.Events ) - - for EventID, Event in pairs( self.Events ) do - Event.Self = self - Event.EventEnabled = true - end - self.Events.Handler = world.addEventHandler( self ) - - return self -end - - ---- Disable the event listeners for the class. --- @param #BASE self --- @return #BASE -function BASE:DisableEvents() - self:F() - - world.removeEventHandler( self ) - for EventID, Event in pairs( self.Events ) do - Event.Self = nil - Event.EventEnabled = false - end - - return self -end - - -local BaseEventCodes = { - "S_EVENT_SHOT", - "S_EVENT_HIT", - "S_EVENT_TAKEOFF", - "S_EVENT_LAND", - "S_EVENT_CRASH", - "S_EVENT_EJECTION", - "S_EVENT_REFUELING", - "S_EVENT_DEAD", - "S_EVENT_PILOT_DEAD", - "S_EVENT_BASE_CAPTURED", - "S_EVENT_MISSION_START", - "S_EVENT_MISSION_END", - "S_EVENT_TOOK_CONTROL", - "S_EVENT_REFUELING_STOP", - "S_EVENT_BIRTH", - "S_EVENT_HUMAN_FAILURE", - "S_EVENT_ENGINE_STARTUP", - "S_EVENT_ENGINE_SHUTDOWN", - "S_EVENT_PLAYER_ENTER_UNIT", - "S_EVENT_PLAYER_LEAVE_UNIT", - "S_EVENT_PLAYER_COMMENT", - "S_EVENT_SHOOTING_START", - "S_EVENT_SHOOTING_END", - "S_EVENT_MAX", -} - ---onEvent( {[1]="S_EVENT_BIRTH",[2]={["subPlace"]=5,["time"]=0,["initiator"]={["id_"]=16884480,},["place"]={["id_"]=5000040,},["id"]=15,["IniUnitName"]="US F-15C@RAMP-Air Support Mountains#001-01",},} --- Event = { --- id = enum world.event, --- time = Time, --- initiator = Unit, --- target = Unit, --- place = Unit, --- subPlace = enum world.BirthPlace, --- weapon = Weapon --- } - ---- Creation of a Birth Event. --- @param #BASE self --- @param DCSTypes#Time EventTime The time stamp of the event. --- @param DCSObject#Object Initiator The initiating object of the event. --- @param #string IniUnitName The initiating unit name. --- @param place --- @param subplace -function BASE:CreateEventBirth( EventTime, Initiator, IniUnitName, place, subplace ) - self:F( { EventTime, Initiator, IniUnitName, place, subplace } ) - - local Event = { - id = world.event.S_EVENT_BIRTH, - time = EventTime, - initiator = Initiator, - IniUnitName = IniUnitName, - place = place, - subplace = subplace - } - - world.onEvent( Event ) -end - ---- Creation of a Crash Event. --- @param #BASE self --- @param DCSTypes#Time EventTime The time stamp of the event. --- @param DCSObject#Object Initiator The initiating object of the event. -function BASE:CreateEventCrash( EventTime, Initiator ) - self:F( { EventTime, Initiator } ) - - local Event = { - id = world.event.S_EVENT_CRASH, - time = EventTime, - initiator = Initiator, - } - - world.onEvent( Event ) -end - --- TODO: Complete DCSTypes#Event structure. ---- The main event handling function... This function captures all events generated for the class. --- @param #BASE self --- @param DCSTypes#Event event -function BASE:onEvent(event) - --self:F( { BaseEventCodes[event.id], event } ) - - if self then - for EventID, EventObject in pairs( self.Events ) do - if EventObject.EventEnabled then - --env.info( 'onEvent Table EventObject.Self = ' .. tostring(EventObject.Self) ) - --env.info( 'onEvent event.id = ' .. tostring(event.id) ) - --env.info( 'onEvent EventObject.Event = ' .. tostring(EventObject.Event) ) - if event.id == EventObject.Event then - if self == EventObject.Self then - if event.initiator and event.initiator:isExist() then - event.IniUnitName = event.initiator:getName() - end - if event.target and event.target:isExist() then - event.TgtUnitName = event.target:getName() - end - --self:T( { BaseEventCodes[event.id], event } ) - --EventObject.EventFunction( self, event ) - end - end - end - end - end -end - -function BASE:SetState( Object, StateName, State ) - - local ClassNameAndID = Object:GetClassNameAndID() - - if not self.States[ClassNameAndID] then - self.States[ClassNameAndID] = {} - end - self.States[ClassNameAndID][StateName] = State - self:F2( { ClassNameAndID, StateName, State } ) - - return self.States[ClassNameAndID][StateName] -end - -function BASE:GetState( Object, StateName ) - - local ClassNameAndID = Object:GetClassNameAndID() - - if self.States[ClassNameAndID] then - local State = self.States[ClassNameAndID][StateName] - self:F2( { ClassNameAndID, StateName, State } ) - return State - end - - return nil -end - -function BASE:ClearState( Object, StateName ) - - local ClassNameAndID = Object:GetClassNameAndID() - if self.States[ClassNameAndID] then - self.States[ClassNameAndID][StateName] = nil - end -end - --- Trace section - --- Log a trace (only shown when trace is on) --- TODO: Make trace function using variable parameters. - ---- Set trace on or off --- Note that when trace is off, no debug statement is performed, increasing performance! --- When Moose is loaded statically, (as one file), tracing is switched off by default. --- So tracing must be switched on manually in your mission if you are using Moose statically. --- When moose is loading dynamically (for moose class development), tracing is switched on by default. --- @param BASE self --- @param #boolean TraceOnOff Switch the tracing on or off. --- @usage --- -- Switch the tracing On --- BASE:TraceOn( true ) --- --- -- Switch the tracing Off --- BASE:TraceOn( false ) -function BASE:TraceOnOff( TraceOnOff ) - _TraceOnOff = TraceOnOff -end - ---- Set trace level --- @param #BASE self --- @param #number Level -function BASE:TraceLevel( Level ) - _TraceLevel = Level - self:E( "Tracing level " .. Level ) -end - ---- Trace all methods in MOOSE --- @param #BASE self --- @param #boolean TraceAll true = trace all methods in MOOSE. -function BASE:TraceAll( TraceAll ) - - _TraceAll = TraceAll - - if _TraceAll then - self:E( "Tracing all methods in MOOSE " ) - else - self:E( "Switched off tracing all methods in MOOSE" ) - end -end - ---- Set tracing for a class --- @param #BASE self --- @param #string Class -function BASE:TraceClass( Class ) - _TraceClass[Class] = true - _TraceClassMethod[Class] = {} - self:E( "Tracing class " .. Class ) -end - ---- Set tracing for a specific method of class --- @param #BASE self --- @param #string Class --- @param #string Method -function BASE:TraceClassMethod( Class, Method ) - if not _TraceClassMethod[Class] then - _TraceClassMethod[Class] = {} - _TraceClassMethod[Class].Method = {} - end - _TraceClassMethod[Class].Method[Method] = true - self:E( "Tracing method " .. Method .. " of class " .. Class ) -end - ---- Trace a function call. This function is private. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:_F( Arguments, DebugInfoCurrentParam, DebugInfoFromParam ) - - if ( _TraceAll == true ) or ( _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName] ) then - - local DebugInfoCurrent = DebugInfoCurrentParam and DebugInfoCurrentParam or debug.getinfo( 2, "nl" ) - local DebugInfoFrom = DebugInfoFromParam and DebugInfoFromParam or debug.getinfo( 3, "l" ) - - local Function = "function" - if DebugInfoCurrent.name then - Function = DebugInfoCurrent.name - end - - if _TraceAll == true or _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName].Method[Function] then - local LineCurrent = 0 - if DebugInfoCurrent.currentline then - LineCurrent = DebugInfoCurrent.currentline - end - local LineFrom = 0 - if DebugInfoFrom then - LineFrom = DebugInfoFrom.currentline - end - env.info( string.format( "%6d(%6d)/%1s:%20s%05d.%s(%s)" , LineCurrent, LineFrom, "F", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) - end - end -end - ---- Trace a function call. Must be at the beginning of the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:F( Arguments ) - - if _TraceOnOff then - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - if _TraceLevel >= 1 then - self:_F( Arguments, DebugInfoCurrent, DebugInfoFrom ) - end - end -end - - ---- Trace a function call level 2. Must be at the beginning of the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:F2( Arguments ) - - if _TraceOnOff then - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - if _TraceLevel >= 2 then - self:_F( Arguments, DebugInfoCurrent, DebugInfoFrom ) - end - end -end - ---- Trace a function call level 3. Must be at the beginning of the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:F3( Arguments ) - - if _TraceOnOff then - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - if _TraceLevel >= 3 then - self:_F( Arguments, DebugInfoCurrent, DebugInfoFrom ) - end - end -end - ---- Trace a function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:_T( Arguments, DebugInfoCurrentParam, DebugInfoFromParam ) - - if ( _TraceAll == true ) or ( _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName] ) then - - local DebugInfoCurrent = DebugInfoCurrentParam and DebugInfoCurrentParam or debug.getinfo( 2, "nl" ) - local DebugInfoFrom = DebugInfoFromParam and DebugInfoFromParam or debug.getinfo( 3, "l" ) - - local Function = "function" - if DebugInfoCurrent.name then - Function = DebugInfoCurrent.name - end - - if _TraceAll == true or _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName].Method[Function] then - local LineCurrent = 0 - if DebugInfoCurrent.currentline then - LineCurrent = DebugInfoCurrent.currentline - end - local LineFrom = 0 - if DebugInfoFrom then - LineFrom = DebugInfoFrom.currentline - end - env.info( string.format( "%6d(%6d)/%1s:%20s%05d.%s" , LineCurrent, LineFrom, "T", self.ClassName, self.ClassID, routines.utils.oneLineSerialize( Arguments ) ) ) - end - end -end - ---- Trace a function logic level 1. Can be anywhere within the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:T( Arguments ) - - if _TraceOnOff then - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - if _TraceLevel >= 1 then - self:_T( Arguments, DebugInfoCurrent, DebugInfoFrom ) - end - end -end - - ---- Trace a function logic level 2. Can be anywhere within the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:T2( Arguments ) - - if _TraceOnOff then - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - if _TraceLevel >= 2 then - self:_T( Arguments, DebugInfoCurrent, DebugInfoFrom ) - end - end -end - ---- Trace a function logic level 3. Can be anywhere within the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:T3( Arguments ) - - if _TraceOnOff then - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - if _TraceLevel >= 3 then - self:_T( Arguments, DebugInfoCurrent, DebugInfoFrom ) - end - end -end - ---- Log an exception which will be traced always. Can be anywhere within the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:E( Arguments ) - - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - local Function = "function" - if DebugInfoCurrent.name then - Function = DebugInfoCurrent.name - end - - local LineCurrent = DebugInfoCurrent.currentline - local LineFrom = -1 - if DebugInfoFrom then - LineFrom = DebugInfoFrom.currentline - end - - env.info( string.format( "%6d(%6d)/%1s:%20s%05d.%s(%s)" , LineCurrent, LineFrom, "E", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) -end - - - ---- This module contains the SCHEDULER class. --- --- 1) @{Scheduler#SCHEDULER} class, extends @{Base#BASE} --- ===================================================== --- The @{Scheduler#SCHEDULER} class models time events calling given event handling functions. --- --- 1.1) SCHEDULER constructor --- -------------------------- --- The SCHEDULER class is quite easy to use: --- --- * @{Scheduler#SCHEDULER.New}: Setup a new scheduler and start it with the specified parameters. --- --- 1.2) SCHEDULER timer stop and start --- ----------------------------------- --- The SCHEDULER can be stopped and restarted with the following methods: --- --- * @{Scheduler#SCHEDULER.Start}: (Re-)Start the scheduler. --- * @{Scheduler#SCHEDULER.Stop}: Stop the scheduler. --- --- @module Scheduler --- @author FlightControl - - ---- The SCHEDULER class --- @type SCHEDULER --- @field #number ScheduleID the ID of the scheduler. --- @extends Base#BASE -SCHEDULER = { - ClassName = "SCHEDULER", -} - ---- SCHEDULER constructor. --- @param #SCHEDULER self --- @param #table TimeEventObject Specified for which Moose object the timer is setup. If a value of nil is provided, a scheduler will be setup without an object reference. --- @param #function TimeEventFunction The event function to be called when a timer event occurs. The event function needs to accept the parameters specified in TimeEventFunctionArguments. --- @param #table TimeEventFunctionArguments Optional arguments that can be given as part of scheduler. The arguments need to be given as a table { param1, param 2, ... }. --- @param #number StartSeconds Specifies the amount of seconds that will be waited before the scheduling is started, and the event function is called. --- @param #number RepeatSecondsInterval Specifies the interval in seconds when the scheduler will call the event function. --- @param #number RandomizationFactor Specifies a randomization factor between 0 and 1 to randomize the RepeatSecondsInterval. --- @param #number StopSeconds Specifies the amount of seconds when the scheduler will be stopped. --- @return #SCHEDULER self -function SCHEDULER:New( TimeEventObject, TimeEventFunction, TimeEventFunctionArguments, StartSeconds, RepeatSecondsInterval, RandomizationFactor, StopSeconds ) - local self = BASE:Inherit( self, BASE:New() ) - self:F2( { TimeEventObject, TimeEventFunction, TimeEventFunctionArguments, StartSeconds, RepeatSecondsInterval, RandomizationFactor, StopSeconds } ) - - self.TimeEventObject = TimeEventObject - self.TimeEventFunction = TimeEventFunction - self.TimeEventFunctionArguments = TimeEventFunctionArguments - self.StartSeconds = StartSeconds - self.Repeat = false - - if RepeatSecondsInterval then - self.RepeatSecondsInterval = RepeatSecondsInterval - else - self.RepeatSecondsInterval = 0 - end - - if RandomizationFactor then - self.RandomizationFactor = RandomizationFactor - else - self.RandomizationFactor = 0 - end - - if StopSeconds then - self.StopSeconds = StopSeconds - end - - - self.StartTime = timer.getTime() - - self:Start() - - return self -end - ---- (Re-)Starts the scheduler. --- @param #SCHEDULER self --- @return #SCHEDULER self -function SCHEDULER:Start() - self:F2( self.TimeEventObject ) - - if self.RepeatSecondsInterval ~= 0 then - self.Repeat = true - end - self.ScheduleID = timer.scheduleFunction( self._Scheduler, self, timer.getTime() + self.StartSeconds + .01 ) - - return self -end - ---- Stops the scheduler. --- @param #SCHEDULER self --- @return #SCHEDULER self -function SCHEDULER:Stop() - self:F2( self.TimeEventObject ) - - self.Repeat = false - if self.ScheduleID then - timer.removeFunction( self.ScheduleID ) - end - self.ScheduleID = nil - - return self -end - --- Private Functions - ---- @param #SCHEDULER self -function SCHEDULER:_Scheduler() - self:F2( self.TimeEventFunctionArguments ) - - local ErrorHandler = function( errmsg ) - - env.info( "Error in SCHEDULER function:" .. errmsg ) - env.info( debug.traceback() ) - - return errmsg - end - - local Status, Result - if self.TimeEventObject then - Status, Result = xpcall( function() return self.TimeEventFunction( self.TimeEventObject, unpack( self.TimeEventFunctionArguments ) ) end, ErrorHandler ) - else - Status, Result = xpcall( function() return self.TimeEventFunction( unpack( self.TimeEventFunctionArguments ) ) end, ErrorHandler ) - end - - self:T( { self.TimeEventFunctionArguments, Status, Result, self.StartTime, self.RepeatSecondsInterval, self.RandomizationFactor, self.StopSeconds } ) - - if Status and ( ( Result == nil ) or ( Result and Result ~= false ) ) then - if self.Repeat and ( not self.StopSeconds or ( self.StopSeconds and timer.getTime() <= self.StartTime + self.StopSeconds ) ) then - local ScheduleTime = - timer.getTime() + - self.RepeatSecondsInterval + - math.random( - - ( self.RandomizationFactor * self.RepeatSecondsInterval / 2 ), - ( self.RandomizationFactor * self.RepeatSecondsInterval / 2 ) - ) + - 0.01 - self:T( { self.TimeEventFunctionArguments, "Repeat:", timer.getTime(), ScheduleTime } ) - return ScheduleTime -- returns the next time the function needs to be called. - else - timer.removeFunction( self.ScheduleID ) - self.ScheduleID = nil - end - else - timer.removeFunction( self.ScheduleID ) - self.ScheduleID = nil - end - - return nil -end - - - - - - - - - - - - - - - - ---- The EVENT class models an efficient event handling process between other classes and its units, weapons. --- @module Event --- @author FlightControl - ---- The EVENT structure --- @type EVENT --- @field #EVENT.Events Events -EVENT = { - ClassName = "EVENT", - ClassID = 0, -} - -local _EVENTCODES = { - "S_EVENT_SHOT", - "S_EVENT_HIT", - "S_EVENT_TAKEOFF", - "S_EVENT_LAND", - "S_EVENT_CRASH", - "S_EVENT_EJECTION", - "S_EVENT_REFUELING", - "S_EVENT_DEAD", - "S_EVENT_PILOT_DEAD", - "S_EVENT_BASE_CAPTURED", - "S_EVENT_MISSION_START", - "S_EVENT_MISSION_END", - "S_EVENT_TOOK_CONTROL", - "S_EVENT_REFUELING_STOP", - "S_EVENT_BIRTH", - "S_EVENT_HUMAN_FAILURE", - "S_EVENT_ENGINE_STARTUP", - "S_EVENT_ENGINE_SHUTDOWN", - "S_EVENT_PLAYER_ENTER_UNIT", - "S_EVENT_PLAYER_LEAVE_UNIT", - "S_EVENT_PLAYER_COMMENT", - "S_EVENT_SHOOTING_START", - "S_EVENT_SHOOTING_END", - "S_EVENT_MAX", -} - ---- The Event structure --- @type EVENTDATA --- @field id --- @field initiator --- @field target --- @field weapon --- @field IniDCSUnit --- @field IniDCSUnitName --- @field Unit#UNIT IniUnit --- @field #string IniUnitName --- @field IniDCSGroup --- @field IniDCSGroupName --- @field TgtDCSUnit --- @field TgtDCSUnitName --- @field Unit#UNIT TgtUnit --- @field #string TgtUnitName --- @field TgtDCSGroup --- @field TgtDCSGroupName --- @field Weapon --- @field WeaponName --- @field WeaponTgtDCSUnit - ---- The Events structure --- @type EVENT.Events --- @field #number IniUnit - -function EVENT:New() - local self = BASE:Inherit( self, BASE:New() ) - self:F2() - self.EventHandler = world.addEventHandler( self ) - return self -end - -function EVENT:EventText( EventID ) - - local EventText = _EVENTCODES[EventID] - - return EventText -end - - ---- Initializes the Events structure for the event --- @param #EVENT self --- @param DCSWorld#world.event EventID --- @param #string EventClass --- @return #EVENT.Events -function EVENT:Init( EventID, EventClass ) - self:F3( { _EVENTCODES[EventID], EventClass } ) - if not self.Events[EventID] then - self.Events[EventID] = {} - end - if not self.Events[EventID][EventClass] then - self.Events[EventID][EventClass] = {} - end - return self.Events[EventID][EventClass] -end - - ---- Create an OnDead event handler for a group --- @param #EVENT self --- @param #table EventTemplate --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param EventSelf The self instance of the class for which the event is. --- @param #function OnEventFunction --- @return #EVENT -function EVENT:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, OnEventFunction ) - self:F2( EventTemplate.name ) - - for EventUnitID, EventUnit in pairs( EventTemplate.units ) do - OnEventFunction( self, EventUnit.name, EventFunction, EventSelf ) - end - return self -end - ---- Set a new listener for an S_EVENT_X event independent from a unit or a weapon. --- @param #EVENT self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @param EventID --- @return #EVENT -function EVENT:OnEventGeneric( EventFunction, EventSelf, EventID ) - self:F2( { EventID } ) - - local Event = self:Init( EventID, EventSelf:GetClassNameAndID() ) - Event.EventFunction = EventFunction - Event.EventSelf = EventSelf - return self -end - - ---- Set a new listener for an S_EVENT_X event --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @param EventID --- @return #EVENT -function EVENT:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, EventID ) - self:F2( EventDCSUnitName ) - - local Event = self:Init( EventID, EventSelf:GetClassNameAndID() ) - if not Event.IniUnit then - Event.IniUnit = {} - end - Event.IniUnit[EventDCSUnitName] = {} - Event.IniUnit[EventDCSUnitName].EventFunction = EventFunction - Event.IniUnit[EventDCSUnitName].EventSelf = EventSelf - return self -end - - ---- Create an OnBirth event handler for a group --- @param #EVENT self --- @param Group#GROUP EventGroup --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnBirthForTemplate( EventTemplate, EventFunction, EventSelf ) - self:F2( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnBirthForUnit ) - - return self -end - ---- Set a new listener for an S_EVENT_BIRTH event, and registers the unit born. --- @param #EVENT self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf --- @return #EVENT -function EVENT:OnBirth( EventFunction, EventSelf ) - self:F2() - - self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_BIRTH ) - - return self -end - ---- Set a new listener for an S_EVENT_BIRTH event. --- @param #EVENT self --- @param #string EventDCSUnitName The id of the unit for the event to be handled. --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf --- @return #EVENT -function EVENT:OnBirthForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_BIRTH ) - - return self -end - ---- Create an OnCrash event handler for a group --- @param #EVENT self --- @param Group#GROUP EventGroup --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnCrashForTemplate( EventTemplate, EventFunction, EventSelf ) - self:F2( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnCrashForUnit ) - - return self -end - ---- Set a new listener for an S_EVENT_CRASH event. --- @param #EVENT self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf --- @return #EVENT -function EVENT:OnCrash( EventFunction, EventSelf ) - self:F2() - - self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_CRASH ) - - return self -end - ---- Set a new listener for an S_EVENT_CRASH event. --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnCrashForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_CRASH ) - - return self -end - ---- Create an OnDead event handler for a group --- @param #EVENT self --- @param Group#GROUP EventGroup --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnDeadForTemplate( EventTemplate, EventFunction, EventSelf ) - self:F2( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnDeadForUnit ) - - return self -end - ---- Set a new listener for an S_EVENT_DEAD event. --- @param #EVENT self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf --- @return #EVENT -function EVENT:OnDead( EventFunction, EventSelf ) - self:F2() - - self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_DEAD ) - - return self -end - - ---- Set a new listener for an S_EVENT_DEAD event. --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnDeadForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_DEAD ) - - return self -end - ---- Set a new listener for an S_EVENT_PILOT_DEAD event. --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnPilotDeadForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_PILOT_DEAD ) - - return self -end - ---- Create an OnDead event handler for a group --- @param #EVENT self --- @param #table EventTemplate --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnLandForTemplate( EventTemplate, EventFunction, EventSelf ) - self:F2( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnLandForUnit ) - - return self -end - ---- Set a new listener for an S_EVENT_LAND event. --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnLandForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_LAND ) - - return self -end - ---- Create an OnDead event handler for a group --- @param #EVENT self --- @param #table EventTemplate --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnTakeOffForTemplate( EventTemplate, EventFunction, EventSelf ) - self:F2( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnTakeOffForUnit ) - - return self -end - ---- Set a new listener for an S_EVENT_TAKEOFF event. --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnTakeOffForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_TAKEOFF ) - - return self -end - ---- Create an OnDead event handler for a group --- @param #EVENT self --- @param #table EventTemplate --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnEngineShutDownForTemplate( EventTemplate, EventFunction, EventSelf ) - self:F2( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnEngineShutDownForUnit ) - - return self -end - ---- Set a new listener for an S_EVENT_ENGINE_SHUTDOWN event. --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnEngineShutDownForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_ENGINE_SHUTDOWN ) - - return self -end - ---- Set a new listener for an S_EVENT_ENGINE_STARTUP event. --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnEngineStartUpForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_ENGINE_STARTUP ) - - return self -end - ---- Set a new listener for an S_EVENT_SHOT event. --- @param #EVENT self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnShot( EventFunction, EventSelf ) - self:F2() - - self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_SHOT ) - - return self -end - ---- Set a new listener for an S_EVENT_SHOT event for a unit. --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnShotForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_SHOT ) - - return self -end - ---- Set a new listener for an S_EVENT_HIT event. --- @param #EVENT self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnHit( EventFunction, EventSelf ) - self:F2() - - self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_HIT ) - - return self -end - ---- Set a new listener for an S_EVENT_HIT event. --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnHitForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_HIT ) - - return self -end - ---- Set a new listener for an S_EVENT_PLAYER_ENTER_UNIT event. --- @param #EVENT self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnPlayerEnterUnit( EventFunction, EventSelf ) - self:F2() - - self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_PLAYER_ENTER_UNIT ) - - return self -end - ---- Set a new listener for an S_EVENT_PLAYER_LEAVE_UNIT event. --- @param #EVENT self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnPlayerLeaveUnit( EventFunction, EventSelf ) - self:F2() - - self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_PLAYER_LEAVE_UNIT ) - - return self -end - - - -function EVENT:onEvent( Event ) - self:F2( { _EVENTCODES[Event.id], Event } ) - - if self and self.Events and self.Events[Event.id] then - if Event.initiator and Event.initiator:getCategory() == Object.Category.UNIT then - Event.IniDCSUnit = Event.initiator - Event.IniDCSGroup = Event.IniDCSUnit:getGroup() - Event.IniDCSUnitName = Event.IniDCSUnit:getName() - Event.IniUnitName = Event.IniDCSUnitName - Event.IniUnit = UNIT:FindByName( Event.IniDCSUnitName ) - Event.IniDCSGroupName = "" - if Event.IniDCSGroup and Event.IniDCSGroup:isExist() then - Event.IniDCSGroupName = Event.IniDCSGroup:getName() - end - end - if Event.target then - if Event.target and Event.target:getCategory() == Object.Category.UNIT then - Event.TgtDCSUnit = Event.target - Event.TgtDCSGroup = Event.TgtDCSUnit:getGroup() - Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() - Event.TgtUnitName = Event.TgtDCSUnitName - Event.TgtUnit = UNIT:FindByName( Event.TgtDCSUnitName ) - Event.TgtDCSGroupName = "" - if Event.TgtDCSGroup and Event.TgtDCSGroup:isExist() then - Event.TgtDCSGroupName = Event.TgtDCSGroup:getName() - end - end - end - if Event.weapon then - Event.Weapon = Event.weapon - Event.WeaponName = Event.Weapon:getTypeName() - --Event.WeaponTgtDCSUnit = Event.Weapon:getTarget() - end - self:E( { _EVENTCODES[Event.id], Event } ) - for ClassName, EventData in pairs( self.Events[Event.id] ) do - if Event.IniDCSUnitName and EventData.IniUnit and EventData.IniUnit[Event.IniDCSUnitName] then - self:E( { "Calling event function for class ", ClassName, " unit ", Event.IniDCSUnitName } ) - EventData.IniUnit[Event.IniDCSUnitName].EventFunction( EventData.IniUnit[Event.IniDCSUnitName].EventSelf, Event ) - else - if Event.IniDCSUnit and not EventData.IniUnit then - self:E( { "Calling event function for class ", ClassName } ) - EventData.EventFunction( EventData.EventSelf, Event ) - end - end - end - end -end - ---- Encapsulation of DCS World Menu system in a set of MENU classes. --- @module Menu - ---- The MENU class --- @type MENU --- @extends Base#BASE -MENU = { - ClassName = "MENU", - MenuPath = nil, - MenuText = "", - MenuParentPath = nil -} - ---- -function MENU:New( MenuText, MenuParentPath ) - - -- Arrange meta tables - local Child = BASE:Inherit( self, BASE:New() ) - - Child.MenuPath = nil - Child.MenuText = MenuText - Child.MenuParentPath = MenuParentPath - return Child -end - ---- The COMMANDMENU class --- @type COMMANDMENU --- @extends Menu#MENU -COMMANDMENU = { - ClassName = "COMMANDMENU", - CommandMenuFunction = nil, - CommandMenuArgument = nil -} - -function COMMANDMENU:New( MenuText, ParentMenu, CommandMenuFunction, CommandMenuArgument ) - - -- Arrange meta tables - - local MenuParentPath = nil - if ParentMenu ~= nil then - MenuParentPath = ParentMenu.MenuPath - end - - local Child = BASE:Inherit( self, MENU:New( MenuText, MenuParentPath ) ) - - Child.MenuPath = missionCommands.addCommand( MenuText, MenuParentPath, CommandMenuFunction, CommandMenuArgument ) - Child.CommandMenuFunction = CommandMenuFunction - Child.CommandMenuArgument = CommandMenuArgument - return Child -end - ---- The SUBMENU class --- @type SUBMENU --- @extends Menu#MENU -SUBMENU = { - ClassName = "SUBMENU" -} - -function SUBMENU:New( MenuText, ParentMenu ) - - -- Arrange meta tables - local MenuParentPath = nil - if ParentMenu ~= nil then - MenuParentPath = ParentMenu.MenuPath - end - - local Child = BASE:Inherit( self, MENU:New( MenuText, MenuParentPath ) ) - - Child.MenuPath = missionCommands.addSubMenu( MenuText, MenuParentPath ) - return Child -end - --- This local variable is used to cache the menus registered under clients. --- Menus don't dissapear when clients are destroyed and restarted. --- So every menu for a client created must be tracked so that program logic accidentally does not create --- the same menus twice during initialization logic. --- These menu classes are handling this logic with this variable. -local _MENUCLIENTS = {} - ---- The MENU_CLIENT class --- @type MENU_CLIENT --- @extends Menu#MENU -MENU_CLIENT = { - ClassName = "MENU_CLIENT" -} - ---- Creates a new menu item for a group --- @param self --- @param Client#CLIENT MenuClient The Client owning the menu. --- @param #string MenuText The text for the menu. --- @param #table ParentMenu The parent menu. --- @return #MENU_CLIENT self -function MENU_CLIENT:New( MenuClient, MenuText, ParentMenu ) - - -- Arrange meta tables - local MenuParentPath = {} - if ParentMenu ~= nil then - MenuParentPath = ParentMenu.MenuPath - end - - local self = BASE:Inherit( self, MENU:New( MenuText, MenuParentPath ) ) - self:F( { MenuClient, MenuText, ParentMenu } ) - - self.MenuClient = MenuClient - self.MenuClientGroupID = MenuClient:GetClientGroupID() - self.MenuParentPath = MenuParentPath - self.MenuText = MenuText - self.ParentMenu = ParentMenu - - self.Menus = {} - - if not _MENUCLIENTS[self.MenuClientGroupID] then - _MENUCLIENTS[self.MenuClientGroupID] = {} - end - - local MenuPath = _MENUCLIENTS[self.MenuClientGroupID] - - self:T( { MenuClient:GetClientGroupName(), MenuPath[table.concat(MenuParentPath)], MenuParentPath, MenuText } ) - - local MenuPathID = table.concat(MenuParentPath) .. "/" .. MenuText - if MenuPath[MenuPathID] then - missionCommands.removeItemForGroup( self.MenuClient:GetClientGroupID(), MenuPath[MenuPathID] ) - end - - self.MenuPath = missionCommands.addSubMenuForGroup( self.MenuClient:GetClientGroupID(), MenuText, MenuParentPath ) - MenuPath[MenuPathID] = self.MenuPath - - self:T( { MenuClient:GetClientGroupName(), self.MenuPath } ) - - if ParentMenu and ParentMenu.Menus then - ParentMenu.Menus[self.MenuPath] = self - end - return self -end - ---- Removes the sub menus recursively of this MENU_CLIENT. --- @param #MENU_CLIENT self --- @return #MENU_CLIENT self -function MENU_CLIENT:RemoveSubMenus() - self:F( self.MenuPath ) - - for MenuID, Menu in pairs( self.Menus ) do - Menu:Remove() - end - -end - ---- Removes the sub menus recursively of this MENU_CLIENT. --- @param #MENU_CLIENT self --- @return #MENU_CLIENT self -function MENU_CLIENT:Remove() - self:F( self.MenuPath ) - - self:RemoveSubMenus() - - if not _MENUCLIENTS[self.MenuClientGroupID] then - _MENUCLIENTS[self.MenuClientGroupID] = {} - end - - local MenuPath = _MENUCLIENTS[self.MenuClientGroupID] - - if MenuPath[table.concat(self.MenuParentPath) .. "/" .. self.MenuText] then - MenuPath[table.concat(self.MenuParentPath) .. "/" .. self.MenuText] = nil - end - - missionCommands.removeItemForGroup( self.MenuClient:GetClientGroupID(), self.MenuPath ) - self.ParentMenu.Menus[self.MenuPath] = nil - return nil -end - - ---- The MENU_CLIENT_COMMAND class --- @type MENU_CLIENT_COMMAND --- @extends Menu#MENU -MENU_CLIENT_COMMAND = { - ClassName = "MENU_CLIENT_COMMAND" -} - ---- Creates a new radio command item for a group --- @param self --- @param Client#CLIENT MenuClient The Client owning the menu. --- @param MenuText The text for the menu. --- @param ParentMenu The parent menu. --- @param CommandMenuFunction A function that is called when the menu key is pressed. --- @param CommandMenuArgument An argument for the function. --- @return Menu#MENU_CLIENT_COMMAND self -function MENU_CLIENT_COMMAND:New( MenuClient, MenuText, ParentMenu, CommandMenuFunction, CommandMenuArgument ) - - -- Arrange meta tables - - local MenuParentPath = {} - if ParentMenu ~= nil then - MenuParentPath = ParentMenu.MenuPath - end - - local self = BASE:Inherit( self, MENU:New( MenuText, MenuParentPath ) ) - - self.MenuClient = MenuClient - self.MenuClientGroupID = MenuClient:GetClientGroupID() - self.MenuParentPath = MenuParentPath - self.MenuText = MenuText - self.ParentMenu = ParentMenu - - if not _MENUCLIENTS[self.MenuClientGroupID] then - _MENUCLIENTS[self.MenuClientGroupID] = {} - end - - local MenuPath = _MENUCLIENTS[self.MenuClientGroupID] - - self:T( { MenuClient:GetClientGroupName(), MenuPath[table.concat(MenuParentPath)], MenuParentPath, MenuText, CommandMenuFunction, CommandMenuArgument } ) - - local MenuPathID = table.concat(MenuParentPath) .. "/" .. MenuText - if MenuPath[MenuPathID] then - missionCommands.removeItemForGroup( self.MenuClient:GetClientGroupID(), MenuPath[MenuPathID] ) - end - - self.MenuPath = missionCommands.addCommandForGroup( self.MenuClient:GetClientGroupID(), MenuText, MenuParentPath, CommandMenuFunction, CommandMenuArgument ) - MenuPath[MenuPathID] = self.MenuPath - - self.CommandMenuFunction = CommandMenuFunction - self.CommandMenuArgument = CommandMenuArgument - - ParentMenu.Menus[self.MenuPath] = self - - return self -end - -function MENU_CLIENT_COMMAND:Remove() - self:F( self.MenuPath ) - - if not _MENUCLIENTS[self.MenuClientGroupID] then - _MENUCLIENTS[self.MenuClientGroupID] = {} - end - - local MenuPath = _MENUCLIENTS[self.MenuClientGroupID] - - if MenuPath[table.concat(self.MenuParentPath) .. "/" .. self.MenuText] then - MenuPath[table.concat(self.MenuParentPath) .. "/" .. self.MenuText] = nil - end - - missionCommands.removeItemForGroup( self.MenuClient:GetClientGroupID(), self.MenuPath ) - self.ParentMenu.Menus[self.MenuPath] = nil - return nil -end - - ---- The MENU_COALITION class --- @type MENU_COALITION --- @extends Menu#MENU -MENU_COALITION = { - ClassName = "MENU_COALITION" -} - ---- Creates a new coalition menu item --- @param #MENU_COALITION self --- @param DCSCoalition#coalition.side MenuCoalition The coalition owning the menu. --- @param #string MenuText The text for the menu. --- @param #table ParentMenu The parent menu. --- @return #MENU_COALITION self -function MENU_COALITION:New( MenuCoalition, MenuText, ParentMenu ) - - -- Arrange meta tables - local MenuParentPath = {} - if ParentMenu ~= nil then - MenuParentPath = ParentMenu.MenuPath - end - - local self = BASE:Inherit( self, MENU:New( MenuText, MenuParentPath ) ) - self:F( { MenuCoalition, MenuText, ParentMenu } ) - - self.MenuCoalition = MenuCoalition - self.MenuParentPath = MenuParentPath - self.MenuText = MenuText - self.ParentMenu = ParentMenu - - self.Menus = {} - - self:T( { MenuParentPath, MenuText } ) - - self.MenuPath = missionCommands.addSubMenuForCoalition( self.MenuCoalition, MenuText, MenuParentPath ) - - self:T( { self.MenuPath } ) - - if ParentMenu and ParentMenu.Menus then - ParentMenu.Menus[self.MenuPath] = self - end - return self -end - ---- Removes the sub menus recursively of this MENU_COALITION. --- @param #MENU_COALITION self --- @return #MENU_COALITION self -function MENU_COALITION:RemoveSubMenus() - self:F( self.MenuPath ) - - for MenuID, Menu in pairs( self.Menus ) do - Menu:Remove() - end - -end - ---- Removes the sub menus recursively of this MENU_COALITION. --- @param #MENU_COALITION self --- @return #MENU_COALITION self -function MENU_COALITION:Remove() - self:F( self.MenuPath ) - - self:RemoveSubMenus() - missionCommands.removeItemForCoalition( self.MenuCoalition, self.MenuPath ) - self.ParentMenu.Menus[self.MenuPath] = nil - - return nil -end - - ---- The MENU_COALITION_COMMAND class --- @type MENU_COALITION_COMMAND --- @extends Menu#MENU -MENU_COALITION_COMMAND = { - ClassName = "MENU_COALITION_COMMAND" -} - ---- Creates a new radio command item for a group --- @param #MENU_COALITION_COMMAND self --- @param DCSCoalition#coalition.side MenuCoalition The coalition owning the menu. --- @param MenuText The text for the menu. --- @param ParentMenu The parent menu. --- @param CommandMenuFunction A function that is called when the menu key is pressed. --- @param CommandMenuArgument An argument for the function. --- @return #MENU_COALITION_COMMAND self -function MENU_COALITION_COMMAND:New( MenuCoalition, MenuText, ParentMenu, CommandMenuFunction, CommandMenuArgument ) - - -- Arrange meta tables - - local MenuParentPath = {} - if ParentMenu ~= nil then - MenuParentPath = ParentMenu.MenuPath - end - - local self = BASE:Inherit( self, MENU:New( MenuText, MenuParentPath ) ) - - self.MenuCoalition = MenuCoalition - self.MenuParentPath = MenuParentPath - self.MenuText = MenuText - self.ParentMenu = ParentMenu - - self:T( { MenuParentPath, MenuText, CommandMenuFunction, CommandMenuArgument } ) - - self.MenuPath = missionCommands.addCommandForCoalition( self.MenuCoalition, MenuText, MenuParentPath, CommandMenuFunction, CommandMenuArgument ) - - self.CommandMenuFunction = CommandMenuFunction - self.CommandMenuArgument = CommandMenuArgument - - ParentMenu.Menus[self.MenuPath] = self - - return self -end - ---- Removes a radio command item for a coalition --- @param #MENU_COALITION_COMMAND self --- @return #MENU_COALITION_COMMAND self -function MENU_COALITION_COMMAND:Remove() - self:F( self.MenuPath ) - - missionCommands.removeItemForCoalition( self.MenuCoalition, self.MenuPath ) - self.ParentMenu.Menus[self.MenuPath] = nil - return nil -end ---- This module contains the CONTROLLABLE class. --- --- 1) @{Controllable#CONTROLLABLE} class, extends @{Base#BASE} --- =========================================================== --- The @{Controllable#CONTROLLABLE} class is a wrapper class to handle the DCS Controllable objects: --- --- * Support all DCS Controllable APIs. --- * Enhance with Controllable specific APIs not in the DCS Controllable API set. --- * Handle local Controllable Controller. --- * Manage the "state" of the DCS Controllable. --- --- 1.1) CONTROLLABLE constructor --- ----------------------------- --- The CONTROLLABLE class provides the following functions to construct a CONTROLLABLE instance: --- --- * @{#CONTROLLABLE.New}(): Create a CONTROLLABLE instance. --- --- 1.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. --- 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. --- --- ### 1.2.1) Assigned task methods --- --- Assigned task methods make the controllable execute the task where the location of the (possible) targets of the task are known before being detected. --- This is different from the EnRoute tasks, where the targets of the task need to be detected before the task can be executed. --- --- Find below a list of the **assigned task** methods: --- --- * @{#CONTROLLABLE.TaskAttackControllable}: (AIR) Attack a Controllable. --- * @{#CONTROLLABLE.TaskAttackMapObject}: (AIR) Attacking the map object (building, structure, e.t.c). --- * @{#CONTROLLABLE.TaskAttackUnit}: (AIR) Attack the Unit. --- * @{#CONTROLLABLE.TaskBombing}: (AIR) Delivering weapon at the point on the ground. --- * @{#CONTROLLABLE.TaskBombingRunway}: (AIR) Delivering weapon on the runway. --- * @{#CONTROLLABLE.TaskEmbarking}: (AIR) Move the controllable to a Vec2 Point, wait for a defined duration and embark a controllable. --- * @{#CONTROLLABLE.TaskEmbarkToTransport}: (GROUND) Embark to a Transport landed at a location. --- * @{#CONTROLLABLE.TaskEscort}: (AIR) Escort another airborne controllable. --- * @{#CONTROLLABLE.TaskFAC_AttackControllable}: (AIR + GROUND) The task makes the controllable/unit a FAC and orders the FAC to control the target (enemy ground controllable) destruction. --- * @{#CONTROLLABLE.TaskFireAtPoint}: (GROUND) Fire at a VEC2 point until ammunition is finished. --- * @{#CONTROLLABLE.TaskFollow}: (AIR) Following another airborne controllable. --- * @{#CONTROLLABLE.TaskHold}: (GROUND) Hold ground controllable from moving. --- * @{#CONTROLLABLE.TaskHoldPosition}: (AIR) Hold position at the current position of the first unit of the controllable. --- * @{#CONTROLLABLE.TaskLand}: (AIR HELICOPTER) Landing at the ground. For helicopters only. --- * @{#CONTROLLABLE.TaskLandAtZone}: (AIR) Land the controllable at a @{Zone#ZONE_RADIUS). --- * @{#CONTROLLABLE.TaskOrbitCircle}: (AIR) Orbit at the current position of the first unit of the controllable at a specified alititude. --- * @{#CONTROLLABLE.TaskOrbitCircleAtVec2}: (AIR) Orbit at a specified position at a specified alititude during a specified duration with a specified speed. --- * @{#CONTROLLABLE.TaskRefueling}: (AIR) Refueling from the nearest tanker. No parameters. --- * @{#CONTROLLABLE.TaskRoute}: (AIR + GROUND) Return a Misson task to follow a given route defined by Points. --- * @{#CONTROLLABLE.TaskRouteToVec2}: (AIR + GROUND) Make the Controllable move to a given point. --- * @{#CONTROLLABLE.TaskRouteToVec3}: (AIR + GROUND) Make the Controllable move to a given point. --- * @{#CONTROLLABLE.TaskRouteToZone}: (AIR + GROUND) Route the controllable to a given zone. --- * @{#CONTROLLABLE.TaskReturnToBase}: (AIR) Route the controllable to an airbase. --- --- ### 1.2.2) EnRoute task methods --- --- EnRoute tasks require the targets of the task need to be detected by the controllable (using its sensors) before the task can be executed: --- --- * @{#CONTROLLABLE.EnRouteTaskAWACS}: (AIR) Aircraft will act as an AWACS for friendly units (will provide them with information about contacts). No parameters. --- * @{#CONTROLLABLE.EnRouteTaskEngageControllable}: (AIR) Engaging a controllable. The task does not assign the target controllable to the unit/controllable to attack now; it just allows the unit/controllable to engage the target controllable as well as other assigned targets. --- * @{#CONTROLLABLE.EnRouteTaskEngageTargets}: (AIR) Engaging targets of defined types. --- * @{#CONTROLLABLE.EnRouteTaskEWR}: (AIR) Attack the Unit. --- * @{#CONTROLLABLE.EnRouteTaskFAC}: (AIR + GROUND) The task makes the controllable/unit a FAC and lets the FAC to choose a targets (enemy ground controllable) around as well as other assigned targets. --- * @{#CONTROLLABLE.EnRouteTaskFAC_EngageControllable}: (AIR + GROUND) The task makes the controllable/unit a FAC and lets the FAC to choose the target (enemy ground controllable) as well as other assigned targets. --- * @{#CONTROLLABLE.EnRouteTaskTanker}: (AIR) Aircraft will act as a tanker for friendly units. No parameters. --- --- ### 1.2.3) Preparation task methods --- --- There are certain task methods that allow to tailor the task behaviour: --- --- * @{#CONTROLLABLE.TaskWrappedAction}: Return a WrappedAction Task taking a Command. --- * @{#CONTROLLABLE.TaskCombo}: Return a Combo Task taking an array of Tasks. --- * @{#CONTROLLABLE.TaskCondition}: Return a condition section for a controlled task. --- * @{#CONTROLLABLE.TaskControlled}: Return a Controlled Task taking a Task and a TaskCondition. --- --- ### 1.2.4) 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. --- --- 1.3) CONTROLLABLE 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. --- --- 1.4) CONTROLLABLE Option methods --- ------------------------- --- Controllable **Option methods** change the behaviour of the Controllable while being alive. --- --- ### 1.4.1) Rule of Engagement: --- --- * @{#CONTROLLABLE.OptionROEWeaponFree} --- * @{#CONTROLLABLE.OptionROEOpenFire} --- * @{#CONTROLLABLE.OptionROEReturnFire} --- * @{#CONTROLLABLE.OptionROEEvadeFire} --- --- To check whether an ROE option is valid for a specific controllable, use: --- --- * @{#CONTROLLABLE.OptionROEWeaponFreePossible} --- * @{#CONTROLLABLE.OptionROEOpenFirePossible} --- * @{#CONTROLLABLE.OptionROEReturnFirePossible} --- * @{#CONTROLLABLE.OptionROEEvadeFirePossible} --- --- ### 1.4.2) Rule on thread: --- --- * @{#CONTROLLABLE.OptionROTNoReaction} --- * @{#CONTROLLABLE.OptionROTPassiveDefense} --- * @{#CONTROLLABLE.OptionROTEvadeFire} --- * @{#CONTROLLABLE.OptionROTVertical} --- --- To test whether an ROT option is valid for a specific controllable, use: --- --- * @{#CONTROLLABLE.OptionROTNoReactionPossible} --- * @{#CONTROLLABLE.OptionROTPassiveDefensePossible} --- * @{#CONTROLLABLE.OptionROTEvadeFirePossible} --- * @{#CONTROLLABLE.OptionROTVerticalPossible} --- --- === --- --- @module Controllable --- @author FlightControl - ---- The CONTROLLABLE class --- @type CONTROLLABLE --- @extends Base#BASE --- @field DCSControllable#Controllable DCSControllable The DCS controllable class. --- @field #string ControllableName The name of the controllable. -CONTROLLABLE = { - ClassName = "CONTROLLABLE", - ControllableName = "", - ControllableID = 0, - Controller = nil, - DCSControllable = nil, - WayPointFunctions = {}, -} - ---- A DCSControllable --- @type DCSControllable --- @field id_ The ID of the controllable in DCS - ---- Create a new CONTROLLABLE from a DCSControllable --- @param #CONTROLLABLE self --- @param DCSControllable#Controllable ControllableName The DCS Controllable name --- @return #CONTROLLABLE self -function CONTROLLABLE:New( ControllableName ) - local self = BASE:Inherit( self, BASE:New() ) - self:F2( ControllableName ) - self.ControllableName = ControllableName - return self -end - --- DCS Controllable methods support. - ---- Get the controller for the CONTROLLABLE. --- @param #CONTROLLABLE self --- @return DCSController#Controller -function CONTROLLABLE:_GetController() - self:F2( { self.ControllableName } ) - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - local ControllableController = DCSControllable:getController() - self:T3( ControllableController ) - return ControllableController - end - - return nil -end - - - --- Tasks - ---- Popping current Task from the controllable. --- @param #CONTROLLABLE self --- @return Controllable#CONTROLLABLE self -function CONTROLLABLE:PopCurrentTask() - self:F2() - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - local Controller = self:_GetController() - Controller:popTask() - return self - end - - return nil -end - ---- Pushing Task on the queue from the controllable. --- @param #CONTROLLABLE self --- @return Controllable#CONTROLLABLE self -function CONTROLLABLE:PushTask( DCSTask, WaitTime ) - self:F2() - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - local Controller = self:_GetController() - - -- 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 ) - - if WaitTime then - --routines.scheduleFunction( Controller.pushTask, { Controller, DCSTask }, timer.getTime() + WaitTime ) - SCHEDULER:New( Controller, Controller.pushTask, { DCSTask }, WaitTime ) - else - Controller:pushTask( DCSTask ) - end - - return self - end - - return nil -end - ---- Clearing the Task Queue and Setting the Task on the queue from the controllable. --- @param #CONTROLLABLE self --- @return Controllable#CONTROLLABLE self -function CONTROLLABLE:SetTask( DCSTask, WaitTime ) - self:F2( { DCSTask } ) - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - - local Controller = self:_GetController() - - -- When a controllable SPAWNs, it takes about a second to get the controllable in the simulator. Setting tasks to unspawned controllables provides unexpected results. - -- Therefore we schedule the functions to set the mission and options for the Controllable. - -- Controller.setTask( Controller, DCSTask ) - - if not WaitTime then - WaitTime = 1 - end - --routines.scheduleFunction( Controller.setTask, { Controller, DCSTask }, timer.getTime() + WaitTime ) - SCHEDULER:New( Controller, Controller.setTask, { DCSTask }, WaitTime ) - - return self - end - - return nil -end - - ---- Return a condition section for a controlled task. --- @param #CONTROLLABLE self --- @param DCSTime#Time time --- @param #string userFlag --- @param #boolean userFlagValue --- @param #string condition --- @param DCSTime#Time duration --- @param #number lastWayPoint --- return DCSTask#Task -function CONTROLLABLE:TaskCondition( time, userFlag, userFlagValue, condition, duration, lastWayPoint ) - self:F2( { time, userFlag, userFlagValue, condition, duration, lastWayPoint } ) - - local DCSStopCondition = {} - DCSStopCondition.time = time - DCSStopCondition.userFlag = userFlag - DCSStopCondition.userFlagValue = userFlagValue - DCSStopCondition.condition = condition - DCSStopCondition.duration = duration - DCSStopCondition.lastWayPoint = lastWayPoint - - self:T3( { DCSStopCondition } ) - return DCSStopCondition -end - ---- Return a Controlled Task taking a Task and a TaskCondition. --- @param #CONTROLLABLE self --- @param DCSTask#Task DCSTask --- @param #DCSStopCondition DCSStopCondition --- @return DCSTask#Task -function CONTROLLABLE:TaskControlled( DCSTask, DCSStopCondition ) - self:F2( { DCSTask, DCSStopCondition } ) - - local DCSTaskControlled - - DCSTaskControlled = { - id = 'ControlledTask', - params = { - task = DCSTask, - stopCondition = DCSStopCondition - } - } - - self:T3( { DCSTaskControlled } ) - return DCSTaskControlled -end - ---- Return a Combo Task taking an array of Tasks. --- @param #CONTROLLABLE self --- @param DCSTask#TaskArray DCSTasks Array of @{DCSTask#Task} --- @return DCSTask#Task -function CONTROLLABLE:TaskCombo( DCSTasks ) - self:F2( { DCSTasks } ) - - local DCSTaskCombo - - DCSTaskCombo = { - id = 'ComboTask', - params = { - tasks = DCSTasks - } - } - - self:T3( { DCSTaskCombo } ) - return DCSTaskCombo -end - ---- Return a WrappedAction Task taking a Command. --- @param #CONTROLLABLE self --- @param DCSCommand#Command DCSCommand --- @return DCSTask#Task -function CONTROLLABLE:TaskWrappedAction( DCSCommand, Index ) - self:F2( { DCSCommand } ) - - local DCSTaskWrappedAction - - DCSTaskWrappedAction = { - id = "WrappedAction", - enabled = true, - number = Index, - auto = false, - params = { - action = DCSCommand, - }, - } - - self:T3( { DCSTaskWrappedAction } ) - return DCSTaskWrappedAction -end - ---- Executes a command action --- @param #CONTROLLABLE self --- @param DCSCommand#Command DCSCommand --- @return #CONTROLLABLE self -function CONTROLLABLE:SetCommand( DCSCommand ) - self:F2( DCSCommand ) - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - local Controller = self:_GetController() - Controller:setCommand( DCSCommand ) - return self - end - - return nil -end - ---- Perform a switch waypoint command --- @param #CONTROLLABLE self --- @param #number FromWayPoint --- @param #number ToWayPoint --- @return DCSTask#Task -function CONTROLLABLE:CommandSwitchWayPoint( FromWayPoint, ToWayPoint, Index ) - self:F2( { FromWayPoint, ToWayPoint, Index } ) - - local CommandSwitchWayPoint = { - id = 'SwitchWaypoint', - params = { - fromWaypointIndex = FromWayPoint, - goToWaypointIndex = ToWayPoint, - }, - } - - self:T3( { CommandSwitchWayPoint } ) - return CommandSwitchWayPoint -end - ---- Perform stop route command --- @param #CONTROLLABLE self --- @param #boolean StopRoute --- @return DCSTask#Task -function CONTROLLABLE:CommandStopRoute( StopRoute, Index ) - self:F2( { StopRoute, Index } ) - - local CommandStopRoute = { - id = 'StopRoute', - params = { - value = StopRoute, - }, - } - - self:T3( { CommandStopRoute } ) - return CommandStopRoute -end - - --- TASKS FOR AIR CONTROLLABLES - - ---- (AIR) Attack a Controllable. --- @param #CONTROLLABLE self --- @param Controllable#CONTROLLABLE AttackControllable 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 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 #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 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 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 #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 "AttackControllable" and "AttackUnit" tasks. If the flag is false then attackQty is a desired attack quantity for "Bombing" and "BombingRunway" tasks. --- @return DCSTask#Task The DCS task structure. -function CONTROLLABLE:TaskAttackControllable( AttackControllable, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit ) - self:F2( { self.ControllableName, AttackControllable, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit } ) - - -- AttackControllable = { - -- id = 'AttackControllable', - -- params = { - -- controllableId = Controllable.ID, - -- weaponType = number, - -- expend = enum AI.Task.WeaponExpend, - -- attackQty = number, - -- directionEnabled = boolean, - -- direction = Azimuth, - -- altitudeEnabled = boolean, - -- altitude = Distance, - -- attackQtyLimit = boolean, - -- } - -- } - - local DirectionEnabled = nil - if Direction then - DirectionEnabled = true - end - - local AltitudeEnabled = nil - if Altitude then - AltitudeEnabled = true - end - - local DCSTask - DCSTask = { id = 'AttackControllable', - params = { - controllableId = AttackControllable:GetID(), - weaponType = WeaponType, - expend = WeaponExpend, - attackQty = AttackQty, - directionEnabled = DirectionEnabled, - direction = Direction, - altitudeEnabled = AltitudeEnabled, - altitude = Altitude, - attackQtyLimit = AttackQtyLimit, - }, - }, - - self:T3( { DCSTask } ) - return DCSTask -end - - ---- (AIR) Attack the Unit. --- @param #CONTROLLABLE self --- @param Unit#UNIT AttackUnit The unit. --- @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 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 #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 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 #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 "AttackControllable" and "AttackUnit" tasks. If the flag is false then attackQty is a desired attack quantity for "Bombing" and "BombingRunway" tasks. --- @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 DCSTask#Task The DCS task structure. -function CONTROLLABLE:TaskAttackUnit( AttackUnit, WeaponType, WeaponExpend, AttackQty, Direction, AttackQtyLimit, ControllableAttack ) - self:F2( { self.ControllableName, AttackUnit, WeaponType, WeaponExpend, AttackQty, Direction, AttackQtyLimit, ControllableAttack } ) - - -- AttackUnit = { - -- id = 'AttackUnit', - -- params = { - -- unitId = Unit.ID, - -- weaponType = number, - -- expend = enum AI.Task.WeaponExpend - -- attackQty = number, - -- direction = Azimuth, - -- attackQtyLimit = boolean, - -- controllableAttack = boolean, - -- } - -- } - - local DCSTask - DCSTask = { id = 'AttackUnit', - params = { - unitId = AttackUnit:GetID(), - weaponType = WeaponType, - expend = WeaponExpend, - attackQty = AttackQty, - direction = Direction, - attackQtyLimit = AttackQtyLimit, - controllableAttack = ControllableAttack, - }, - }, - - self:T3( { DCSTask } ) - return DCSTask -end - - ---- (AIR) Delivering weapon at the point on the ground. --- @param #CONTROLLABLE self --- @param DCSTypes#Vec2 PointVec2 2D-coordinates of the point to deliver weapon at. --- @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 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 #number AttackQty (optional) Desired quantity of passes. The parameter is not the same in AttackControllable and AttackUnit tasks. --- @param 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 #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 DCSTask#Task The DCS task structure. -function CONTROLLABLE:TaskBombing( PointVec2, WeaponType, WeaponExpend, AttackQty, Direction, ControllableAttack ) - self:F2( { self.ControllableName, PointVec2, WeaponType, WeaponExpend, AttackQty, Direction, ControllableAttack } ) - --- Bombing = { --- id = 'Bombing', --- params = { --- point = Vec2, --- weaponType = number, --- expend = enum AI.Task.WeaponExpend, --- attackQty = number, --- direction = Azimuth, --- controllableAttack = boolean, --- } --- } - - local DCSTask - DCSTask = { id = 'Bombing', - params = { - point = PointVec2, - weaponType = WeaponType, - expend = WeaponExpend, - attackQty = AttackQty, - direction = Direction, - controllableAttack = ControllableAttack, - }, - }, - - self:T3( { DCSTask } ) - return DCSTask -end - ---- (AIR) Orbit at a specified position at a specified alititude during a specified duration with a specified speed. --- @param #CONTROLLABLE self --- @param 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. --- @return #CONTROLLABLE self -function CONTROLLABLE:TaskOrbitCircleAtVec2( Point, Altitude, Speed ) - self:F2( { self.ControllableName, Point, Altitude, Speed } ) - - -- pattern = enum AI.Task.OribtPattern, - -- point = Vec2, - -- point2 = Vec2, - -- speed = Distance, - -- altitude = Distance - - local LandHeight = land.getHeight( Point ) - - self:T3( { LandHeight } ) - - local DCSTask = { id = 'Orbit', - params = { pattern = AI.Task.OrbitPattern.CIRCLE, - point = Point, - speed = Speed, - altitude = Altitude + LandHeight - } - } - - - -- local AITask = { id = 'ControlledTask', - -- params = { task = { id = 'Orbit', - -- params = { pattern = AI.Task.OrbitPattern.CIRCLE, - -- point = Point, - -- speed = Speed, - -- altitude = Altitude + LandHeight - -- } - -- }, - -- stopCondition = { duration = Duration - -- } - -- } - -- } - -- ) - - return DCSTask -end - ---- (AIR) Orbit at 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. --- @return #CONTROLLABLE self -function CONTROLLABLE:TaskOrbitCircle( Altitude, Speed ) - self:F2( { self.ControllableName, Altitude, Speed } ) - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - local ControllablePoint = self:GetPointVec2() - return self:TaskOrbitCircleAtVec2( ControllablePoint, Altitude, Speed ) - end - - return nil -end - - - ---- (AIR) Hold position at the current position of the first unit of the controllable. --- @param #CONTROLLABLE self --- @param #number Duration The maximum duration in seconds to hold the position. --- @return #CONTROLLABLE self -function CONTROLLABLE:TaskHoldPosition() - self:F2( { self.ControllableName } ) - - return self:TaskOrbitCircle( 30, 10 ) -end - - - - ---- (AIR) Attacking the map object (building, structure, e.t.c). --- @param #CONTROLLABLE self --- @param DCSTypes#Vec2 PointVec2 2D-coordinates of the point the map object is closest to. The distance between the point and the map object must not be greater than 2000 meters. Object id is not used here because Mission Editor doesn't support map object identificators. --- @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 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 #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 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 #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 DCSTask#Task The DCS task structure. -function CONTROLLABLE:TaskAttackMapObject( PointVec2, WeaponType, WeaponExpend, AttackQty, Direction, ControllableAttack ) - self:F2( { self.ControllableName, PointVec2, WeaponType, WeaponExpend, AttackQty, Direction, ControllableAttack } ) - --- AttackMapObject = { --- id = 'AttackMapObject', --- params = { --- point = Vec2, --- weaponType = number, --- expend = enum AI.Task.WeaponExpend, --- attackQty = number, --- direction = Azimuth, --- controllableAttack = boolean, --- } --- } - - local DCSTask - DCSTask = { id = 'AttackMapObject', - params = { - point = PointVec2, - weaponType = WeaponType, - expend = WeaponExpend, - attackQty = AttackQty, - direction = Direction, - controllableAttack = ControllableAttack, - }, - }, - - self:T3( { DCSTask } ) - return DCSTask -end - - ---- (AIR) Delivering weapon on the runway. --- @param #CONTROLLABLE self --- @param 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 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 #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 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 #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 DCSTask#Task The DCS task structure. -function CONTROLLABLE:TaskBombingRunway( Airbase, WeaponType, WeaponExpend, AttackQty, Direction, ControllableAttack ) - self:F2( { self.ControllableName, Airbase, WeaponType, WeaponExpend, AttackQty, Direction, ControllableAttack } ) - --- BombingRunway = { --- id = 'BombingRunway', --- params = { --- runwayId = AirdromeId, --- weaponType = number, --- expend = enum AI.Task.WeaponExpend, --- attackQty = number, --- direction = Azimuth, --- controllableAttack = boolean, --- } --- } - - local DCSTask - DCSTask = { id = 'BombingRunway', - params = { - point = Airbase:GetID(), - weaponType = WeaponType, - expend = WeaponExpend, - attackQty = AttackQty, - direction = Direction, - controllableAttack = ControllableAttack, - }, - }, - - self:T3( { DCSTask } ) - return DCSTask -end - - ---- (AIR) Refueling from the nearest tanker. No parameters. --- @param #CONTROLLABLE self --- @return DCSTask#Task The DCS task structure. -function CONTROLLABLE:TaskRefueling() - self:F2( { self.ControllableName } ) - --- Refueling = { --- id = 'Refueling', --- params = {} --- } - - local DCSTask - DCSTask = { id = 'Refueling', - params = { - }, - }, - - self:T3( { DCSTask } ) - return DCSTask -end - - ---- (AIR HELICOPTER) Landing at the ground. For helicopters only. --- @param #CONTROLLABLE self --- @param DCSTypes#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 ) - self:F2( { self.ControllableName, Point, Duration } ) - --- Land = { --- id= 'Land', --- params = { --- point = Vec2, --- durationFlag = boolean, --- duration = Time --- } --- } - - local DCSTask - if Duration and Duration > 0 then - DCSTask = { id = 'Land', - params = { - point = Point, - durationFlag = true, - duration = Duration, - }, - } - else - DCSTask = { id = 'Land', - params = { - point = Point, - durationFlag = false, - }, - } - end - - self:T3( DCSTask ) - return DCSTask -end - ---- (AIR) Land the controllable at a @{Zone#ZONE_RADIUS). --- @param #CONTROLLABLE self --- @param Zone#ZONE Zone The zone where to land. --- @param #number Duration The duration in seconds to stay on the ground. --- @return #CONTROLLABLE self -function CONTROLLABLE:TaskLandAtZone( Zone, Duration, RandomPoint ) - self:F2( { self.ControllableName, Zone, Duration, RandomPoint } ) - - local Point - if RandomPoint then - Point = Zone:GetRandomVec2() - else - Point = Zone:GetPointVec2() - end - - local DCSTask = self:TaskLandAtVec2( Point, Duration ) - - self:T3( DCSTask ) - return DCSTask -end - - - ---- (AIR) Following another airborne controllable. --- The unit / controllable will follow lead unit of another controllable, wingmens of both controllables will continue following their leaders. --- If another controllable is on land the unit / controllable will orbit around. --- @param #CONTROLLABLE self --- @param Controllable#CONTROLLABLE FollowControllable The controllable to be followed. --- @param DCSTypes#Vec3 PointVec3 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 DCSTask#Task The DCS task structure. -function CONTROLLABLE:TaskFollow( FollowControllable, PointVec3, LastWaypointIndex ) - self:F2( { self.ControllableName, FollowControllable, PointVec3, LastWaypointIndex } ) - --- Follow = { --- id = 'Follow', --- params = { --- controllableId = Controllable.ID, --- pos = Vec3, --- lastWptIndexFlag = boolean, --- lastWptIndex = number --- } --- } - - local LastWaypointIndexFlag = nil - if LastWaypointIndex then - LastWaypointIndexFlag = true - end - - local DCSTask - DCSTask = { id = 'Follow', - params = { - controllableId = FollowControllable:GetID(), - pos = PointVec3, - lastWptIndexFlag = LastWaypointIndexFlag, - lastWptIndex = LastWaypointIndex, - }, - }, - - self:T3( { DCSTask } ) - return DCSTask -end - - ---- (AIR) Escort another airborne controllable. --- The unit / controllable will follow lead unit of another controllable, wingmens of both controllables will continue following their leaders. --- The unit / controllable will also protect that controllable from threats of specified types. --- @param #CONTROLLABLE self --- @param Controllable#CONTROLLABLE EscortControllable The controllable to be escorted. --- @param DCSTypes#Vec3 PointVec3 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 DCSTypes#AttributeNameArray TargetTypes Array of AttributeName that is contains threat categories allowed to engage. --- @return DCSTask#Task The DCS task structure. -function CONTROLLABLE:TaskEscort( FollowControllable, PointVec3, LastWaypointIndex, EngagementDistance, TargetTypes ) - self:F2( { self.ControllableName, FollowControllable, PointVec3, LastWaypointIndex, EngagementDistance, TargetTypes } ) - --- Escort = { --- id = 'Escort', --- params = { --- controllableId = Controllable.ID, --- pos = Vec3, --- lastWptIndexFlag = boolean, --- lastWptIndex = number, --- engagementDistMax = Distance, --- targetTypes = array of AttributeName, --- } --- } - - local LastWaypointIndexFlag = nil - if LastWaypointIndex then - LastWaypointIndexFlag = true - end - - local DCSTask - DCSTask = { id = 'Follow', - params = { - controllableId = FollowControllable:GetID(), - pos = PointVec3, - lastWptIndexFlag = LastWaypointIndexFlag, - lastWptIndex = LastWaypointIndex, - engagementDistMax = EngagementDistance, - targetTypes = TargetTypes, - }, - }, - - self:T3( { DCSTask } ) - return DCSTask -end - - --- GROUND TASKS - ---- (GROUND) Fire at a VEC2 point until ammunition is finished. --- @param #CONTROLLABLE self --- @param DCSTypes#Vec2 PointVec2 The point to fire at. --- @param DCSTypes#Distance Radius The radius of the zone to deploy the fire at. --- @return DCSTask#Task The DCS task structure. -function CONTROLLABLE:TaskFireAtPoint( PointVec2, Radius ) - self:F2( { self.ControllableName, PointVec2, Radius } ) - - -- FireAtPoint = { - -- id = 'FireAtPoint', - -- params = { - -- point = Vec2, - -- radius = Distance, - -- } - -- } - - local DCSTask - DCSTask = { id = 'FireAtPoint', - params = { - point = PointVec2, - radius = Radius, - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - ---- (GROUND) Hold ground controllable from moving. --- @param #CONTROLLABLE self --- @return DCSTask#Task The DCS task structure. -function CONTROLLABLE:TaskHold() - self:F2( { self.ControllableName } ) - --- Hold = { --- id = 'Hold', --- params = { --- } --- } - - local DCSTask - DCSTask = { id = 'Hold', - params = { - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - --- TASKS FOR AIRBORNE AND GROUND UNITS/CONTROLLABLES - ---- (AIR + GROUND) The task makes the controllable/unit a FAC and orders the FAC to control the target (enemy ground controllable) destruction. --- The killer is player-controlled allied CAS-aircraft that is in contact with the FAC. --- If the task is assigned to the controllable lead unit will be a FAC. --- @param #CONTROLLABLE self --- @param Controllable#CONTROLLABLE AttackControllable 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 DCSTypes#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 DCSTask#Task The DCS task structure. -function CONTROLLABLE:TaskFAC_AttackControllable( AttackControllable, WeaponType, Designation, Datalink ) - self:F2( { self.ControllableName, AttackControllable, WeaponType, Designation, Datalink } ) - --- FAC_AttackControllable = { --- id = 'FAC_AttackControllable', --- params = { --- controllableId = Controllable.ID, --- weaponType = number, --- designation = enum AI.Task.Designation, --- datalink = boolean --- } --- } - - local DCSTask - DCSTask = { id = 'FAC_AttackControllable', - params = { - controllableId = AttackControllable:GetID(), - weaponType = WeaponType, - designation = Designation, - datalink = Datalink, - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - --- EN-ROUTE TASKS FOR AIRBORNE CONTROLLABLES - ---- (AIR) Engaging targets of defined types. --- @param #CONTROLLABLE self --- @param 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 DCSTypes#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 DCSTask#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskEngageTargets( Distance, TargetTypes, Priority ) - self:F2( { self.ControllableName, Distance, TargetTypes, Priority } ) - --- EngageTargets ={ --- id = 'EngageTargets', --- params = { --- maxDist = Distance, --- targetTypes = array of AttributeName, --- priority = number --- } --- } - - local DCSTask - DCSTask = { id = 'EngageTargets', - params = { - maxDist = Distance, - targetTypes = TargetTypes, - priority = Priority - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - - ---- (AIR) Engaging a targets of defined types at circle-shaped zone. --- @param #CONTROLLABLE self --- @param DCSTypes#Vec2 PointVec2 2D-coordinates of the zone. --- @param DCSTypes#Distance Radius Radius of the zone. --- @param DCSTypes#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 DCSTask#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskEngageTargets( PointVec2, Radius, TargetTypes, Priority ) - self:F2( { self.ControllableName, PointVec2, Radius, TargetTypes, Priority } ) - --- EngageTargetsInZone = { --- id = 'EngageTargetsInZone', --- params = { --- point = Vec2, --- zoneRadius = Distance, --- targetTypes = array of AttributeName, --- priority = number --- } --- } - - local DCSTask - DCSTask = { id = 'EngageTargetsInZone', - params = { - point = PointVec2, - zoneRadius = Radius, - targetTypes = TargetTypes, - priority = Priority - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - ---- (AIR) Engaging a controllable. The task does not assign the target controllable to the unit/controllable to attack now; it just allows the unit/controllable to engage the target controllable as well as other assigned targets. --- @param #CONTROLLABLE self --- @param Controllable#CONTROLLABLE AttackControllable 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 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 #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 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 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 #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 "AttackControllable" and "AttackUnit" tasks. If the flag is false then attackQty is a desired attack quantity for "Bombing" and "BombingRunway" tasks. --- @return DCSTask#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskEngageControllable( AttackControllable, Priority, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit ) - self:F2( { self.ControllableName, AttackControllable, Priority, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit } ) - - -- EngageControllable = { - -- id = 'EngageControllable ', - -- params = { - -- controllableId = Controllable.ID, - -- weaponType = number, - -- expend = enum AI.Task.WeaponExpend, - -- attackQty = number, - -- directionEnabled = boolean, - -- direction = Azimuth, - -- altitudeEnabled = boolean, - -- altitude = Distance, - -- attackQtyLimit = boolean, - -- priority = number, - -- } - -- } - - local DirectionEnabled = nil - if Direction then - DirectionEnabled = true - end - - local AltitudeEnabled = nil - if Altitude then - AltitudeEnabled = true - end - - local DCSTask - DCSTask = { id = 'EngageControllable', - params = { - controllableId = AttackControllable:GetID(), - weaponType = WeaponType, - expend = WeaponExpend, - attackQty = AttackQty, - directionEnabled = DirectionEnabled, - direction = Direction, - altitudeEnabled = AltitudeEnabled, - altitude = Altitude, - attackQtyLimit = AttackQtyLimit, - priority = Priority, - }, - }, - - self:T3( { DCSTask } ) - return DCSTask -end - - ---- (AIR) Attack the Unit. --- @param #CONTROLLABLE self --- @param Unit#UNIT AttackUnit The UNIT. --- @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 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 #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 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 #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 "AttackControllable" and "AttackUnit" tasks. If the flag is false then attackQty is a desired attack quantity for "Bombing" and "BombingRunway" tasks. --- @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 DCSTask#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskEngageUnit( AttackUnit, Priority, WeaponType, WeaponExpend, AttackQty, Direction, AttackQtyLimit, ControllableAttack ) - self:F2( { self.ControllableName, AttackUnit, Priority, WeaponType, WeaponExpend, AttackQty, Direction, AttackQtyLimit, ControllableAttack } ) - - -- EngageUnit = { - -- id = 'EngageUnit', - -- params = { - -- unitId = Unit.ID, - -- weaponType = number, - -- expend = enum AI.Task.WeaponExpend - -- attackQty = number, - -- direction = Azimuth, - -- attackQtyLimit = boolean, - -- controllableAttack = boolean, - -- priority = number, - -- } - -- } - - local DCSTask - DCSTask = { id = 'EngageUnit', - params = { - unitId = AttackUnit:GetID(), - weaponType = WeaponType, - expend = WeaponExpend, - attackQty = AttackQty, - direction = Direction, - attackQtyLimit = AttackQtyLimit, - controllableAttack = ControllableAttack, - priority = Priority, - }, - }, - - self:T3( { DCSTask } ) - return DCSTask -end - - - ---- (AIR) Aircraft will act as an AWACS for friendly units (will provide them with information about contacts). No parameters. --- @param #CONTROLLABLE self --- @return DCSTask#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskAWACS( ) - self:F2( { self.ControllableName } ) - --- AWACS = { --- id = 'AWACS', --- params = { --- } --- } - - local DCSTask - DCSTask = { id = 'AWACS', - params = { - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - ---- (AIR) Aircraft will act as a tanker for friendly units. No parameters. --- @param #CONTROLLABLE self --- @return DCSTask#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskTanker( ) - self:F2( { self.ControllableName } ) - --- Tanker = { --- id = 'Tanker', --- params = { --- } --- } - - local DCSTask - DCSTask = { id = 'Tanker', - params = { - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - --- En-route tasks for ground units/controllables - ---- (GROUND) Ground unit (EW-radar) will act as an EWR for friendly units (will provide them with information about contacts). No parameters. --- @param #CONTROLLABLE self --- @return DCSTask#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskEWR( ) - self:F2( { self.ControllableName } ) - --- EWR = { --- id = 'EWR', --- params = { --- } --- } - - local DCSTask - DCSTask = { id = 'EWR', - params = { - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - --- En-route tasks for airborne and ground units/controllables - ---- (AIR + GROUND) The task makes the controllable/unit a FAC and lets the FAC to choose the target (enemy ground controllable) as well as other assigned targets. --- The killer is player-controlled allied CAS-aircraft that is in contact with the FAC. --- If the task is assigned to the controllable lead unit will be a FAC. --- @param #CONTROLLABLE self --- @param Controllable#CONTROLLABLE AttackControllable 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 DCSTypes#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 DCSTask#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskFAC_EngageControllable( AttackControllable, Priority, WeaponType, Designation, Datalink ) - self:F2( { self.ControllableName, AttackControllable, WeaponType, Priority, Designation, Datalink } ) - --- FAC_EngageControllable = { --- id = 'FAC_EngageControllable', --- params = { --- controllableId = Controllable.ID, --- weaponType = number, --- designation = enum AI.Task.Designation, --- datalink = boolean, --- priority = number, --- } --- } - - local DCSTask - DCSTask = { id = 'FAC_EngageControllable', - params = { - controllableId = AttackControllable:GetID(), - weaponType = WeaponType, - designation = Designation, - datalink = Datalink, - priority = Priority, - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - ---- (AIR + GROUND) The task makes the controllable/unit a FAC and lets the FAC to choose a targets (enemy ground controllable) around as well as other assigned targets. --- 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 DCSTypes#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 DCSTask#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskFAC( Radius, Priority ) - self:F2( { self.ControllableName, Radius, Priority } ) - --- FAC = { --- id = 'FAC', --- params = { --- radius = Distance, --- priority = number --- } --- } - - local DCSTask - DCSTask = { id = 'FAC', - params = { - radius = Radius, - priority = Priority - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - - - ---- (AIR) Move the controllable to a Vec2 Point, wait for a defined duration and embark a controllable. --- @param #CONTROLLABLE self --- @param DCSTypes#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 DCSTask#Task The DCS task structure -function CONTROLLABLE:TaskEmbarking( Point, Duration, EmbarkingControllable ) - self:F2( { self.ControllableName, Point, Duration, EmbarkingControllable.DCSControllable } ) - - local DCSTask - DCSTask = { id = 'Embarking', - params = { x = Point.x, - y = Point.y, - duration = Duration, - controllablesForEmbarking = { EmbarkingControllable.ControllableID }, - durationFlag = true, - distributionFlag = false, - distribution = {}, - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - ---- (GROUND) Embark to a Transport landed at a location. - ---- Move to a defined Vec2 Point, and embark to a controllable when arrived within a defined Radius. --- @param #CONTROLLABLE self --- @param DCSTypes#Vec2 Point The point where to wait. --- @param #number Radius The radius of the embarking zone around the Point. --- @return DCSTask#Task The DCS task structure. -function CONTROLLABLE:TaskEmbarkToTransport( Point, Radius ) - self:F2( { self.ControllableName, Point, Radius } ) - - local DCSTask --DCSTask#Task - DCSTask = { id = 'EmbarkToTransport', - params = { x = Point.x, - y = Point.y, - zoneRadius = Radius, - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - - ---- (AIR + GROUND) Return a mission task from a mission template. --- @param #CONTROLLABLE self --- @param #table TaskMission A table containing the mission task. --- @return DCSTask#Task -function CONTROLLABLE:TaskMission( TaskMission ) - self:F2( Points ) - - local DCSTask - DCSTask = { id = 'Mission', params = { TaskMission, }, } - - self:T3( { DCSTask } ) - return DCSTask -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 DCSTask#Task -function CONTROLLABLE:TaskRoute( Points ) - self:F2( Points ) - - local DCSTask - DCSTask = { id = 'Mission', params = { route = { points = Points, }, }, } - - self:T3( { DCSTask } ) - return DCSTask -end - ---- (AIR + GROUND) Make the Controllable move to fly to a given point. --- @param #CONTROLLABLE self --- @param DCSTypes#Vec3 Point The destination point in Vec3 format. --- @param #number Speed The speed to travel. --- @return #CONTROLLABLE self -function CONTROLLABLE:TaskRouteToVec2( Point, Speed ) - self:F2( { Point, Speed } ) - - local ControllablePoint = self:GetUnit( 1 ):GetPointVec2() - - 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 DCSTypes#Vec3 Point The destination point in Vec3 format. --- @param #number Speed The speed to travel. --- @return #CONTROLLABLE self -function CONTROLLABLE:TaskRouteToVec3( Point, Speed ) - self:F2( { Point, Speed } ) - - local ControllablePoint = self:GetUnit( 1 ):GetPointVec3() - - local PointFrom = {} - PointFrom.x = ControllablePoint.x - PointFrom.y = ControllablePoint.z - PointFrom.alt = ControllablePoint.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 GoPoints A table of Route Points. --- @return #CONTROLLABLE self -function CONTROLLABLE:Route( GoPoints ) - self:F2( GoPoints ) - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - local Points = routines.utils.deepCopy( GoPoints ) - local MissionTask = { id = 'Mission', params = { route = { points = Points, }, }, } - local Controller = self:_GetController() - --Controller.setTask( Controller, MissionTask ) - --routines.scheduleFunction( Controller.setTask, { Controller, MissionTask}, timer.getTime() + 1 ) - SCHEDULER:New( Controller, Controller.setTask, { MissionTask }, 1 ) - return self - end - - return nil -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 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:GetPointVec2() - - local PointFrom = {} - PointFrom.x = ControllablePoint.x - PointFrom.y = ControllablePoint.y - PointFrom.type = "Turning Point" - PointFrom.action = "Cone" - PointFrom.speed = 20 / 1.6 - - - local PointTo = {} - local ZonePoint - - if Randomize then - ZonePoint = Zone:GetRandomVec2() - else - ZonePoint = Zone:GetPointVec2() - 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 / 1.6 - end - - local Points = { PointFrom, PointTo } - - self:T3( Points ) - - self:Route( Points ) - - return self - end - - return nil -end - ---- (AIR) Return the Controllable to an @{Airbase#AIRBASE} --- A speed can be given in km/h. --- A given formation can be given. --- @param #CONTROLLABLE self --- @param Airbase#AIRBASE ReturnAirbase The @{Airbase#AIRBASE} to return to. --- @param #number Speed (optional) The speed. --- @return #string The route -function CONTROLLABLE:RouteReturnToAirbase( ReturnAirbase, Speed ) - self:F2( { ReturnAirbase, Speed } ) - --- Example --- [4] = --- { --- ["alt"] = 45, --- ["type"] = "Land", --- ["action"] = "Landing", --- ["alt_type"] = "BARO", --- ["formation_template"] = "", --- ["properties"] = --- { --- ["vnav"] = 1, --- ["scale"] = 0, --- ["angle"] = 0, --- ["vangle"] = 0, --- ["steer"] = 2, --- }, -- end of ["properties"] --- ["ETA"] = 527.81058817743, --- ["airdromeId"] = 12, --- ["y"] = 243127.2973737, --- ["x"] = -5406.2803440839, --- ["name"] = "DictKey_WptName_53", --- ["speed"] = 138.88888888889, --- ["ETA_locked"] = false, --- ["task"] = --- { --- ["id"] = "ComboTask", --- ["params"] = --- { --- ["tasks"] = --- { --- }, -- end of ["tasks"] --- }, -- end of ["params"] --- }, -- end of ["task"] --- ["speed_locked"] = true, --- }, -- end of [4] - - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - - local ControllablePoint = self:GetPointVec2() - local ControllableVelocity = self:GetMaxVelocity() - - local PointFrom = {} - PointFrom.x = ControllablePoint.x - PointFrom.y = ControllablePoint.y - PointFrom.type = "Turning Point" - PointFrom.action = "Turning Point" - PointFrom.speed = ControllableVelocity - - - local PointTo = {} - local AirbasePoint = ReturnAirbase:GetPointVec2() - - PointTo.x = AirbasePoint.x - PointTo.y = AirbasePoint.y - PointTo.type = "Land" - PointTo.action = "Landing" - PointTo.airdromeId = ReturnAirbase:GetID()-- Airdrome ID - self:T(PointTo.airdromeId) - --PointTo.alt = 0 - - local Points = { PointFrom, PointTo } - - self:T3( Points ) - - local Route = { points = Points, } - - return Route - end - - return nil -end - --- Commands - ---- Do Script command --- @param #CONTROLLABLE self --- @param #string DoScript --- @return #DCSCommand -function CONTROLLABLE:CommandDoScript( DoScript ) - - local DCSDoScript = { - id = "Script", - params = { - command = DoScript, - }, - } - - self:T3( DCSDoScript ) - return DCSDoScript -end - - ---- Return the mission template of the controllable. --- @param #CONTROLLABLE self --- @return #table The MissionTemplate --- TODO: Rework the method how to retrieve a template ... -function CONTROLLABLE:GetTaskMission() - self:F2( self.ControllableName ) - - return routines.utils.deepCopy( _DATABASE.Templates.Controllables[self.ControllableName].Template ) -end - ---- Return the mission route of the controllable. --- @param #CONTROLLABLE self --- @return #table The mission route defined by points. -function CONTROLLABLE:GetTaskRoute() - self:F2( self.ControllableName ) - - return routines.utils.deepCopy( _DATABASE.Templates.Controllables[self.ControllableName].Template.route.points ) -end - ---- Return the route of a controllable by using the @{Database#DATABASE} class. --- @param #CONTROLLABLE self --- @param #number Begin The route point from where the copy will start. The base route point is 0. --- @param #number End The route point where the copy will end. The End point is the last point - the End point. The last point has base 0. --- @param #boolean Randomize Randomization of the route, when true. --- @param #number Radius When randomization is on, the randomization is within the radius. -function CONTROLLABLE:CopyRoute( Begin, End, Randomize, Radius ) - self:F2( { Begin, End } ) - - local Points = {} - - -- Could be a Spawned Controllable - local ControllableName = string.match( self:GetName(), ".*#" ) - if ControllableName then - ControllableName = ControllableName:sub( 1, -2 ) - else - ControllableName = self:GetName() - end - - self:T3( { ControllableName } ) - - local Template = _DATABASE.Templates.Controllables[ControllableName].Template - - if Template then - if not Begin then - Begin = 0 - end - if not End then - End = 0 - end - - for TPointID = Begin + 1, #Template.route.points - End do - if Template.route.points[TPointID] then - Points[#Points+1] = routines.utils.deepCopy( Template.route.points[TPointID] ) - if Randomize then - if not Radius then - Radius = 500 - end - Points[#Points].x = Points[#Points].x + math.random( Radius * -1, Radius ) - Points[#Points].y = Points[#Points].y + math.random( Radius * -1, Radius ) - end - end - end - return Points - else - error( "Template not found for Controllable : " .. ControllableName ) - end - - return nil -end - - ---- Return the detected targets of the controllable. --- The optional parametes specify the detection methods that can be applied. --- If no detection method is given, the detection will use all the available methods by default. --- @param Controllable#CONTROLLABLE self --- @param #boolean DetectVisual (optional) --- @param #boolean DetectOptical (optional) --- @param #boolean DetectRadar (optional) --- @param #boolean DetectIRST (optional) --- @param #boolean DetectRWR (optional) --- @param #boolean DetectDLINK (optional) --- @return #table DetectedTargets -function CONTROLLABLE:GetDetectedTargets( DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK ) - self:F2( self.ControllableName ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local DetectionVisual = ( DetectVisual and DetectVisual == true ) and Controller.Detection.VISUAL or nil - local DetectionOptical = ( DetectOptical and DetectOptical == true ) and Controller.Detection.OPTICAL or nil - local DetectionRadar = ( DetectRadar and DetectRadar == true ) and Controller.Detection.RADAR or nil - local DetectionIRST = ( DetectIRST and DetectIRST == true ) and Controller.Detection.IRST or nil - local DetectionRWR = ( DetectRWR and DetectRWR == true ) and Controller.Detection.RWR or nil - local DetectionDLINK = ( DetectDLINK and DetectDLINK == true ) and Controller.Detection.DLINK or nil - - - return self:_GetController():getDetectedTargets( DetectionVisual, DetectionOptical, DetectionRadar, DetectionIRST, DetectionRWR, DetectionDLINK ) - end - - return nil -end - -function CONTROLLABLE:IsTargetDetected( DCSObject ) - self:F2( self.ControllableName ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - - local TargetIsDetected, TargetIsVisible, TargetLastTime, TargetKnowType, TargetKnowDistance, TargetLastPos, TargetLastVelocity - = self:_GetController().isTargetDetected( self:_GetController(), DCSObject, - Controller.Detection.VISUAL, - Controller.Detection.OPTIC, - Controller.Detection.RADAR, - Controller.Detection.IRST, - Controller.Detection.RWR, - Controller.Detection.DLINK - ) - return TargetIsDetected, TargetIsVisible, TargetLastTime, TargetKnowType, TargetKnowDistance, TargetLastPos, TargetLastVelocity - end - - return nil -end - --- Options - ---- Can the CONTROLLABLE hold their weapons? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROEHoldFirePossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() or self:IsGround() or self:IsShip() then - return true - end - - return false - end - - return nil -end - ---- Holding weapons. --- @param Controllable#CONTROLLABLE self --- @return Controllable#CONTROLLABLE self -function CONTROLLABLE:OptionROEHoldFire() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD ) - elseif self:IsGround() then - Controller:setOption( AI.Option.Ground.id.ROE, AI.Option.Ground.val.ROE.WEAPON_HOLD ) - elseif self:IsShip() then - Controller:setOption( AI.Option.Naval.id.ROE, AI.Option.Naval.val.ROE.WEAPON_HOLD ) - end - - return self - end - - return nil -end - ---- Can the CONTROLLABLE attack returning on enemy fire? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROEReturnFirePossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() or self:IsGround() or self:IsShip() then - return true - end - - return false - end - - return nil -end - ---- Return fire. --- @param #CONTROLLABLE self --- @return #CONTROLLABLE self -function CONTROLLABLE:OptionROEReturnFire() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.RETURN_FIRE ) - elseif self:IsGround() then - Controller:setOption( AI.Option.Ground.id.ROE, AI.Option.Ground.val.ROE.RETURN_FIRE ) - elseif self:IsShip() then - Controller:setOption( AI.Option.Naval.id.ROE, AI.Option.Naval.val.ROE.RETURN_FIRE ) - end - - return self - end - - return nil -end - ---- Can the CONTROLLABLE attack designated targets? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROEOpenFirePossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() or self:IsGround() or self:IsShip() then - return true - end - - return false - end - - return nil -end - ---- Openfire. --- @param #CONTROLLABLE self --- @return #CONTROLLABLE self -function CONTROLLABLE:OptionROEOpenFire() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE ) - elseif self:IsGround() then - Controller:setOption( AI.Option.Ground.id.ROE, AI.Option.Ground.val.ROE.OPEN_FIRE ) - elseif self:IsShip() then - Controller:setOption( AI.Option.Naval.id.ROE, AI.Option.Naval.val.ROE.OPEN_FIRE ) - end - - return self - end - - return nil -end - ---- Can the CONTROLLABLE attack targets of opportunity? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROEWeaponFreePossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() then - return true - end - - return false - end - - return nil -end - ---- Weapon free. --- @param #CONTROLLABLE self --- @return #CONTROLLABLE self -function CONTROLLABLE:OptionROEWeaponFree() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_FREE ) - end - - return self - end - - return nil -end - ---- Can the CONTROLLABLE ignore enemy fire? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROTNoReactionPossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() then - return true - end - - return false - end - - return nil -end - - ---- No evasion on enemy threats. --- @param #CONTROLLABLE self --- @return #CONTROLLABLE self -function CONTROLLABLE:OptionROTNoReaction() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.NO_REACTION ) - end - - return self - end - - return nil -end - ---- Can the CONTROLLABLE evade using passive defenses? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROTPassiveDefensePossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() then - return true - end - - return false - end - - return nil -end - ---- Evasion passive defense. --- @param #CONTROLLABLE self --- @return #CONTROLLABLE self -function CONTROLLABLE:OptionROTPassiveDefense() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.PASSIVE_DEFENCE ) - end - - return self - end - - return nil -end - ---- Can the CONTROLLABLE evade on enemy fire? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROTEvadeFirePossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() then - return true - end - - return false - end - - return nil -end - - ---- Evade on fire. --- @param #CONTROLLABLE self --- @return #CONTROLLABLE self -function CONTROLLABLE:OptionROTEvadeFire() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) - end - - return self - end - - return nil -end - ---- Can the CONTROLLABLE evade on fire using vertical manoeuvres? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROTVerticalPossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() then - return true - end - - return false - end - - return nil -end - - ---- Evade on fire using vertical manoeuvres. --- @param #CONTROLLABLE self --- @return #CONTROLLABLE self -function CONTROLLABLE:OptionROTVertical() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.BYPASS_AND_ESCAPE ) - end - - return self - end - - return nil -end - ---- 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 @{Controllable@CONTROLLABLE:WayPointExecute) to start the execution of the new mission plan. --- Note that when WayPointInitialize is called, the Mission of the controllable is RESTARTED! --- @param #CONTROLLABLE self --- @param #table WayPoints If WayPoints is given, then use the route. --- @return #CONTROLLABLE -function CONTROLLABLE:WayPointInitialize( WayPoints ) - - if WayPoints then - self.WayPoints = WayPoints - else - self.WayPoints = self:GetTaskRoute() - end - - return self -end - - ---- Registers a waypoint function that will be executed when the controllable moves over the WayPoint. --- @param #CONTROLLABLE self --- @param #number WayPoint The waypoint number. Note that the start waypoint on the route is WayPoint 1! --- @param #number WayPointIndex When defining multiple WayPoint functions for one WayPoint, use WayPointIndex to set the sequence of actions. --- @param #function WayPointFunction The waypoint function to be called when the controllable moves over the waypoint. The waypoint function takes variable parameters. --- @return #CONTROLLABLE -function CONTROLLABLE:WayPointFunction( WayPoint, WayPointIndex, WayPointFunction, ... ) - self:F2( { WayPoint, WayPointIndex, WayPointFunction } ) - - table.insert( self.WayPoints[WayPoint].task.params.tasks, WayPointIndex ) - self.WayPoints[WayPoint].task.params.tasks[WayPointIndex] = self:TaskFunction( WayPoint, WayPointIndex, WayPointFunction, arg ) - return self -end - - -function CONTROLLABLE:TaskFunction( WayPoint, WayPointIndex, FunctionString, FunctionArguments ) - self:F2( { WayPoint, WayPointIndex, FunctionString, FunctionArguments } ) - - local DCSTask - - local DCSScript = {} - DCSScript[#DCSScript+1] = "local MissionControllable = CONTROLLABLE:Find( ... ) " - - if FunctionArguments and #FunctionArguments > 0 then - DCSScript[#DCSScript+1] = FunctionString .. "( MissionControllable, " .. table.concat( FunctionArguments, "," ) .. ")" - else - DCSScript[#DCSScript+1] = FunctionString .. "( MissionControllable )" - end - - DCSTask = self:TaskWrappedAction( - self:CommandDoScript( - table.concat( DCSScript ) - ), WayPointIndex - ) - - self:T3( DCSTask ) - - return DCSTask - -end - ---- Executes the WayPoint plan. --- The function gets a WayPoint parameter, that you can use to restart the mission at a specific WayPoint. --- Note that when the WayPoint parameter is used, the new start mission waypoint of the controllable will be 1! --- @param #CONTROLLABLE self --- @param #number WayPoint The WayPoint from where to execute the mission. --- @param #number WaitTime The amount seconds to wait before initiating the mission. --- @return #CONTROLLABLE -function CONTROLLABLE:WayPointExecute( WayPoint, WaitTime ) - - if not WayPoint then - WayPoint = 1 - end - - -- When starting the mission from a certain point, the TaskPoints need to be deleted before the given WayPoint. - for TaskPointID = 1, WayPoint - 1 do - table.remove( self.WayPoints, 1 ) - end - - self:T3( self.WayPoints ) - - self:SetTask( self:TaskRoute( self.WayPoints ), WaitTime ) - - return self -end - - ---- This module contains the GROUP class. --- --- 1) @{Group#GROUP} class, extends @{Controllable#CONTROLLABLE} --- ============================================================= --- The @{Group#GROUP} class is a wrapper class to handle the DCS Group objects: --- --- * Support all DCS Group APIs. --- * Enhance with Group specific APIs not in the DCS Group API set. --- * Handle local Group Controller. --- * Manage the "state" of the DCS Group. --- --- **IMPORTANT: ONE SHOULD NEVER SANATIZE these GROUP OBJECT REFERENCES! (make the GROUP object references nil).** --- --- 1.1) GROUP reference methods --- ----------------------- --- 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. --- --- 1.2) GROUP task methods --- ----------------------- --- Several group 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 @{Group#GROUP.PushTask} or @{Group#SetTask} method to assign the task to the GROUP. --- Tasks are specific for the category of the GROUP, more specific, for AIR, GROUND or AIR and GROUND. --- Each task description where applicable indicates for which group category the task is valid. --- There are 2 main subdivisions of tasks: Assigned tasks and EnRoute tasks. --- --- ### 1.2.1) Assigned task methods --- --- Assigned task methods make the group execute the task where the location of the (possible) targets of the task are known before being detected. --- This is different from the EnRoute tasks, where the targets of the task need to be detected before the task can be executed. --- --- Find below a list of the **assigned task** methods: --- --- * @{#GROUP.TaskAttackGroup}: (AIR) Attack a Group. --- * @{#GROUP.TaskAttackMapObject}: (AIR) Attacking the map object (building, structure, e.t.c). --- * @{#GROUP.TaskAttackUnit}: (AIR) Attack the Unit. --- * @{#GROUP.TaskBombing}: (AIR) Delivering weapon at the point on the ground. --- * @{#GROUP.TaskBombingRunway}: (AIR) Delivering weapon on the runway. --- * @{#GROUP.TaskEmbarking}: (AIR) Move the group to a Vec2 Point, wait for a defined duration and embark a group. --- * @{#GROUP.TaskEmbarkToTransport}: (GROUND) Embark to a Transport landed at a location. --- * @{#GROUP.TaskEscort}: (AIR) Escort another airborne group. --- * @{#GROUP.TaskFAC_AttackGroup}: (AIR + GROUND) The task makes the group/unit a FAC and orders the FAC to control the target (enemy ground group) destruction. --- * @{#GROUP.TaskFireAtPoint}: (GROUND) Fire at a VEC2 point until ammunition is finished. --- * @{#GROUP.TaskFollow}: (AIR) Following another airborne group. --- * @{#GROUP.TaskHold}: (GROUND) Hold ground group from moving. --- * @{#GROUP.TaskHoldPosition}: (AIR) Hold position at the current position of the first unit of the group. --- * @{#GROUP.TaskLand}: (AIR HELICOPTER) Landing at the ground. For helicopters only. --- * @{#GROUP.TaskLandAtZone}: (AIR) Land the group at a @{Zone#ZONE_RADIUS). --- * @{#GROUP.TaskOrbitCircle}: (AIR) Orbit at the current position of the first unit of the group at a specified alititude. --- * @{#GROUP.TaskOrbitCircleAtVec2}: (AIR) Orbit at a specified position at a specified alititude during a specified duration with a specified speed. --- * @{#GROUP.TaskRefueling}: (AIR) Refueling from the nearest tanker. No parameters. --- * @{#GROUP.TaskRoute}: (AIR + GROUND) Return a Misson task to follow a given route defined by Points. --- * @{#GROUP.TaskRouteToVec2}: (AIR + GROUND) Make the Group move to a given point. --- * @{#GROUP.TaskRouteToVec3}: (AIR + GROUND) Make the Group move to a given point. --- * @{#GROUP.TaskRouteToZone}: (AIR + GROUND) Route the group to a given zone. --- * @{#GROUP.TaskReturnToBase}: (AIR) Route the group to an airbase. --- --- ### 1.2.2) EnRoute task methods --- --- EnRoute tasks require the targets of the task need to be detected by the group (using its sensors) before the task can be executed: --- --- * @{#GROUP.EnRouteTaskAWACS}: (AIR) Aircraft will act as an AWACS for friendly units (will provide them with information about contacts). No parameters. --- * @{#GROUP.EnRouteTaskEngageGroup}: (AIR) Engaging a group. The task does not assign the target group to the unit/group to attack now; it just allows the unit/group to engage the target group as well as other assigned targets. --- * @{#GROUP.EnRouteTaskEngageTargets}: (AIR) Engaging targets of defined types. --- * @{#GROUP.EnRouteTaskEWR}: (AIR) Attack the Unit. --- * @{#GROUP.EnRouteTaskFAC}: (AIR + GROUND) The task makes the group/unit a FAC and lets the FAC to choose a targets (enemy ground group) around as well as other assigned targets. --- * @{#GROUP.EnRouteTaskFAC_EngageGroup}: (AIR + GROUND) The task makes the group/unit a FAC and lets the FAC to choose the target (enemy ground group) as well as other assigned targets. --- * @{#GROUP.EnRouteTaskTanker}: (AIR) Aircraft will act as a tanker for friendly units. No parameters. --- --- ### 1.2.3) Preparation task methods --- --- There are certain task methods that allow to tailor the task behaviour: --- --- * @{#GROUP.TaskWrappedAction}: Return a WrappedAction Task taking a Command. --- * @{#GROUP.TaskCombo}: Return a Combo Task taking an array of Tasks. --- * @{#GROUP.TaskCondition}: Return a condition section for a controlled task. --- * @{#GROUP.TaskControlled}: Return a Controlled Task taking a Task and a TaskCondition. --- --- ### 1.2.4) 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: --- --- * @{#GROUP.TaskMission}: (AIR + GROUND) Return a mission task from a mission template. --- --- 1.3) GROUP Command methods --- -------------------------- --- Group **command methods** prepare the execution of commands using the @{#GROUP.SetCommand} method: --- --- * @{#GROUP.CommandDoScript}: Do Script command. --- * @{#GROUP.CommandSwitchWayPoint}: Perform a switch waypoint command. --- --- 1.4) GROUP Option methods --- ------------------------- --- Group **Option methods** change the behaviour of the Group while being alive. --- --- ### 1.4.1) Rule of Engagement: --- --- * @{#GROUP.OptionROEWeaponFree} --- * @{#GROUP.OptionROEOpenFire} --- * @{#GROUP.OptionROEReturnFire} --- * @{#GROUP.OptionROEEvadeFire} --- --- To check whether an ROE option is valid for a specific group, use: --- --- * @{#GROUP.OptionROEWeaponFreePossible} --- * @{#GROUP.OptionROEOpenFirePossible} --- * @{#GROUP.OptionROEReturnFirePossible} --- * @{#GROUP.OptionROEEvadeFirePossible} --- --- ### 1.4.2) Rule on thread: --- --- * @{#GROUP.OptionROTNoReaction} --- * @{#GROUP.OptionROTPassiveDefense} --- * @{#GROUP.OptionROTEvadeFire} --- * @{#GROUP.OptionROTVertical} --- --- To test whether an ROT option is valid for a specific group, use: --- --- * @{#GROUP.OptionROTNoReactionPossible} --- * @{#GROUP.OptionROTPassiveDefensePossible} --- * @{#GROUP.OptionROTEvadeFirePossible} --- * @{#GROUP.OptionROTVerticalPossible} --- --- 1.5) GROUP Zone validation methods --- ---------------------------------- --- The group can be validated whether it is completely, partly or not within a @{Zone}. --- Use the following Zone validation methods on the group: --- --- * @{#GROUP.IsCompletelyInZone}: Returns true if all units of the group are within a @{Zone}. --- * @{#GROUP.IsPartlyInZone}: Returns true if some units of the group are within a @{Zone}. --- * @{#GROUP.IsNotInZone}: Returns true if none of the group units of the group are within a @{Zone}. --- --- The zone can be of any @{Zone} class derived from @{Zone#ZONE_BASE}. So, these methods are polymorphic to the zones tested on. --- --- @module Group --- @author FlightControl - ---- The GROUP class --- @type GROUP --- @extends Controllable#CONTROLLABLE --- @field DCSGroup#Group DCSGroup The DCS group class. --- @field #string GroupName The name of the group. -GROUP = { - ClassName = "GROUP", - GroupName = "", - GroupID = 0, - Controller = nil, - DCSGroup = nil, - WayPointFunctions = {}, -} - ---- A DCSGroup --- @type DCSGroup --- @field id_ The ID of the group in DCS - ---- Create a new GROUP from a DCSGroup --- @param #GROUP self --- @param DCSGroup#Group GroupName The DCS Group name --- @return #GROUP self -function GROUP:Register( GroupName ) - local self = BASE:Inherit( self, CONTROLLABLE:New( GroupName ) ) - self:F2( GroupName ) - self.GroupName = GroupName - return self -end - --- Reference methods. - ---- Find the GROUP wrapper class instance using the DCS Group. --- @param #GROUP self --- @param DCSGroup#Group DCSGroup The DCS Group. --- @return #GROUP The GROUP. -function GROUP:Find( DCSGroup ) - - local GroupName = DCSGroup:getName() -- Group#GROUP - local GroupFound = _DATABASE:FindGroup( GroupName ) - GroupFound:E( { GroupName, GroupFound:GetClassNameAndID() } ) - return GroupFound -end - ---- Find the created GROUP using the DCS Group Name. --- @param #GROUP self --- @param #string GroupName The DCS Group Name. --- @return #GROUP The GROUP. -function GROUP:FindByName( GroupName ) - - local GroupFound = _DATABASE:FindGroup( GroupName ) - return GroupFound -end - --- DCS Group methods support. - ---- Returns the DCS Group. --- @param #GROUP self --- @return DCSGroup#Group The DCS Group. -function GROUP:GetDCSObject() - local DCSGroup = Group.getByName( self.GroupName ) - - if DCSGroup then - return DCSGroup - end - - return nil -end - - ---- Returns if the DCS Group is alive. --- When the group exists at run-time, this method will return true, otherwise false. --- @param #GROUP self --- @return #boolean true if the DCS Group is alive. -function GROUP:IsAlive() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupIsAlive = DCSGroup:isExist() - self:T3( GroupIsAlive ) - return GroupIsAlive - end - - 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. --- @param #GROUP self -function GROUP:Destroy() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - for Index, UnitData in pairs( DCSGroup:getUnits() ) do - self:CreateEventCrash( timer.getTime(), UnitData ) - end - DCSGroup:destroy() - DCSGroup = nil - end - - return nil -end - ---- Returns category of the DCS Group. --- @param #GROUP self --- @return DCSGroup#Group.Category The category ID -function GROUP:GetCategory() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - if DCSGroup then - local GroupCategory = DCSGroup:getCategory() - self:T3( GroupCategory ) - return GroupCategory - end - - return nil -end - ---- Returns the category name of the DCS Group. --- @param #GROUP self --- @return #string Category name = Helicopter, Airplane, Ground Unit, Ship -function GROUP:GetCategoryName() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - if DCSGroup then - local CategoryNames = { - [Group.Category.AIRPLANE] = "Airplane", - [Group.Category.HELICOPTER] = "Helicopter", - [Group.Category.GROUND] = "Ground Unit", - [Group.Category.SHIP] = "Ship", - } - local GroupCategory = DCSGroup:getCategory() - self:T3( GroupCategory ) - - return CategoryNames[GroupCategory] - end - - return nil -end - - ---- Returns the coalition of the DCS Group. --- @param #GROUP self --- @return DCSCoalitionObject#coalition.side The coalition side of the DCS Group. -function GROUP:GetCoalition() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - if DCSGroup then - local GroupCoalition = DCSGroup:getCoalition() - self:T3( GroupCoalition ) - return GroupCoalition - end - - return nil -end - ---- Returns the country of the DCS Group. --- @param #GROUP self --- @return DCScountry#country.id The country identifier. --- @return #nil The DCS Group is not existing or alive. -function GROUP:GetCountry() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - if DCSGroup then - local GroupCountry = DCSGroup:getUnit(1):getCountry() - self:T3( GroupCountry ) - return GroupCountry - end - - return nil -end - ---- Returns the name of the DCS Group. --- @param #GROUP self --- @return #string The DCS Group name. -function GROUP:GetName() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupName = DCSGroup:getName() - self:T3( GroupName ) - return GroupName - end - - return nil -end - ---- Returns the DCS Group identifier. --- @param #GROUP self --- @return #number The identifier of the DCS Group. -function GROUP:GetID() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupID = DCSGroup:getID() - self:T3( GroupID ) - return GroupID - 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 Unit#UNIT The UNIT wrapper class. -function GROUP:GetUnit( UnitNumber ) - self:F2( { self.GroupName, UnitNumber } ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local UnitFound = UNIT:Find( DCSGroup:getUnit( UnitNumber ) ) - self:T3( UnitFound.UnitName ) - self:T2( UnitFound ) - return UnitFound - end - - return nil -end - ---- Returns the DCS Unit with number UnitNumber. --- If the underlying DCS Unit does not exist, the method will return nil. . --- @param #GROUP self --- @param #number UnitNumber The number of the DCS Unit to be returned. --- @return DCSUnit#Unit The DCS Unit. -function GROUP:GetDCSUnit( UnitNumber ) - self:F2( { self.GroupName, UnitNumber } ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local DCSUnitFound = DCSGroup:getUnit( UnitNumber ) - self:T3( DCSUnitFound ) - return DCSUnitFound - end - - return nil -end - ---- Returns current size of the DCS Group. --- If some of the DCS Units of the DCS Group are destroyed the size of the DCS Group is changed. --- @param #GROUP self --- @return #number The DCS Group size. -function GROUP:GetSize() - self:F2( { self.GroupName } ) - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupSize = DCSGroup:getSize() - self:T3( GroupSize ) - return GroupSize - 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 } ) - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupInitialSize = DCSGroup:getInitialSize() - self:T3( GroupInitialSize ) - return GroupInitialSize - end - - return nil -end - ---- Returns the UNITs wrappers of the DCS Units of the DCS Group. --- @param #GROUP self --- @return #table The UNITs wrappers. -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 the DCS Units of the DCS Group. --- @param #GROUP self --- @return #table The DCS Units. -function GROUP:GetDCSUnits() - self:F2( { self.GroupName } ) - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local DCSUnits = DCSGroup:getUnits() - self:T3( DCSUnits ) - return DCSUnits - end - - return nil -end - - ---- Activates a GROUP. --- @param #GROUP self -function GROUP:Activate() - self:F2( { self.GroupName } ) - trigger.action.activateGroup( self:GetDCSObject() ) - return self:GetDCSObject() -end - - ---- Gets the type name of the group. --- @param #GROUP self --- @return #string The type name of the group. -function GROUP:GetTypeName() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupTypeName = DCSGroup:getUnit(1):getTypeName() - self:T3( GroupTypeName ) - return( GroupTypeName ) - end - - return nil -end - ---- Gets the CallSign of the first DCS Unit of the DCS Group. --- @param #GROUP self --- @return #string The CallSign of the first DCS Unit of the DCS Group. -function GROUP:GetCallsign() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupCallSign = DCSGroup:getUnit(1):getCallsign() - self:T3( GroupCallSign ) - return GroupCallSign - end - - return nil -end - ---- Returns the current point (Vec2 vector) of the first DCS Unit in the DCS Group. --- @return DCSTypes#Vec2 Current Vec2 point of the first DCS Unit of the DCS Group. -function GROUP:GetPointVec2() - self:F2( self.GroupName ) - - local GroupPointVec2 = self:GetUnit(1):GetPointVec2() - self:T3( GroupPointVec2 ) - return GroupPointVec2 -end - ---- Returns the current point (Vec3 vector) of the first DCS Unit in the DCS Group. --- @return DCSTypes#Vec3 Current Vec3 point of the first DCS Unit of the DCS Group. -function GROUP:GetPointVec3() - self:F2( self.GroupName ) - - local GroupPointVec3 = self:GetUnit(1):GetPointVec3() - self:T3( GroupPointVec3 ) - return GroupPointVec3 -end - - - --- Is Zone Functions - ---- Returns true if all units of the group are within a @{Zone}. --- @param #GROUP self --- @param Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the Group is completely within the @{Zone#ZONE_BASE} -function GROUP:IsCompletelyInZone( Zone ) - self:F2( { self.GroupName, Zone } ) - - for UnitID, UnitData in pairs( self:GetUnits() ) do - local Unit = UnitData -- Unit#UNIT - if Zone:IsPointVec3InZone( Unit:GetPointVec3() ) then - else - return false - end - end - - return true -end - ---- Returns true if some units of the group are within a @{Zone}. --- @param #GROUP self --- @param Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the Group is completely within the @{Zone#ZONE_BASE} -function GROUP:IsPartlyInZone( Zone ) - self:F2( { self.GroupName, Zone } ) - - for UnitID, UnitData in pairs( self:GetUnits() ) do - local Unit = UnitData -- Unit#UNIT - if Zone:IsPointVec3InZone( Unit:GetPointVec3() ) then - return true - end - end - - return false -end - ---- Returns true if none of the group units of the group are within a @{Zone}. --- @param #GROUP self --- @param Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the Group is completely within the @{Zone#ZONE_BASE} -function GROUP:IsNotInZone( Zone ) - self:F2( { self.GroupName, Zone } ) - - for UnitID, UnitData in pairs( self:GetUnits() ) do - local Unit = UnitData -- Unit#UNIT - if Zone:IsPointVec3InZone( Unit:GetPointVec3() ) then - return false - end - end - - return true -end - ---- Returns if the group is of an air category. --- If the group is a helicopter or a plane, then this method will return true, otherwise false. --- @param #GROUP self --- @return #boolean Air category evaluation result. -function GROUP:IsAir() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local IsAirResult = DCSGroup:getCategory() == Group.Category.AIRPLANE or DCSGroup:getCategory() == Group.Category.HELICOPTER - self:T3( IsAirResult ) - return IsAirResult - end - - return nil -end - ---- Returns if the DCS Group contains Helicopters. --- @param #GROUP self --- @return #boolean true if DCS Group contains Helicopters. -function GROUP:IsHelicopter() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupCategory = DCSGroup:getCategory() - self:T2( GroupCategory ) - return GroupCategory == Group.Category.HELICOPTER - end - - return nil -end - ---- Returns if the DCS Group contains AirPlanes. --- @param #GROUP self --- @return #boolean true if DCS Group contains AirPlanes. -function GROUP:IsAirPlane() - self:F2() - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupCategory = DCSGroup:getCategory() - self:T2( GroupCategory ) - return GroupCategory == Group.Category.AIRPLANE - end - - return nil -end - ---- Returns if the DCS Group contains Ground troops. --- @param #GROUP self --- @return #boolean true if DCS Group contains Ground troops. -function GROUP:IsGround() - self:F2() - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupCategory = DCSGroup:getCategory() - self:T2( GroupCategory ) - return GroupCategory == Group.Category.GROUND - end - - return nil -end - ---- Returns if the DCS Group contains Ships. --- @param #GROUP self --- @return #boolean true if DCS Group contains Ships. -function GROUP:IsShip() - self:F2() - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupCategory = DCSGroup:getCategory() - self:T2( GroupCategory ) - return GroupCategory == Group.Category.SHIP - end - - return nil -end - ---- Returns if all units of the group are on the ground or landed. --- If all units of this group are on the ground, this function will return true, otherwise false. --- @param #GROUP self --- @return #boolean All units on the ground result. -function GROUP:AllOnGround() - self:F2() - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local AllOnGroundResult = true - - for Index, UnitData in pairs( DCSGroup:getUnits() ) do - if UnitData:inAir() then - AllOnGroundResult = false - end - end - - self:T3( AllOnGroundResult ) - return AllOnGroundResult - end - - return nil -end - ---- Returns the current maximum velocity of the group. --- Each unit within the group gets evaluated, and the maximum velocity (= the unit which is going the fastest) is returned. --- @param #GROUP self --- @return #number Maximum velocity found. -function GROUP:GetMaxVelocity() - self:F2() - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local MaxVelocity = 0 - - for Index, UnitData in pairs( DCSGroup:getUnits() ) do - - local Velocity = UnitData:getVelocity() - local VelocityTotal = math.abs( Velocity.x ) + math.abs( Velocity.y ) + math.abs( Velocity.z ) - - if VelocityTotal < MaxVelocity then - MaxVelocity = VelocityTotal - end - end - - return MaxVelocity - end - - return nil -end - ---- Returns the current minimum height of the group. --- Each unit within the group gets evaluated, and the minimum height (= the unit which is the lowest elevated) is returned. --- @param #GROUP self --- @return #number Minimum height found. -function GROUP:GetMinHeight() - self:F2() - -end - ---- Returns the current maximum height of the group. --- Each unit within the group gets evaluated, and the maximum height (= the unit which is the highest elevated) is returned. --- @param #GROUP self --- @return #number Maximum height found. -function GROUP:GetMaxHeight() - self:F2() - -end - ---- @param Group#GROUP self -function GROUP:Respawn( Template ) - - local Vec3 = self:GetPointVec3() - --Template.x = Vec3.x - --Template.y = Vec3.z - Template.x = nil - Template.y = nil - - self:E( #Template.units ) - for UnitID, UnitData in pairs( self:GetUnits() ) do - local GroupUnit = UnitData -- Unit#UNIT - self:E( GroupUnit:GetName() ) - if GroupUnit:IsAlive() then - local GroupUnitVec3 = GroupUnit:GetPointVec3() - local GroupUnitHeading = GroupUnit:GetHeading() - Template.units[UnitID].alt = GroupUnitVec3.y - Template.units[UnitID].x = GroupUnitVec3.x - Template.units[UnitID].y = GroupUnitVec3.z - Template.units[UnitID].heading = GroupUnitHeading - self:E( { UnitID, Template.units[UnitID], Template.units[UnitID] } ) - end - end - - _DATABASE:Spawn( Template ) - -end - -function GROUP:GetTemplate() - - return _DATABASE.Templates.Groups[self:GetName()].Template - -end - ---- Return the mission template of the group. --- @param #GROUP self --- @return #table The MissionTemplate -function GROUP:GetTaskMission() - self:F2( self.GroupName ) - - return routines.utils.deepCopy( _DATABASE.Templates.Groups[self.GroupName].Template ) -end - ---- Return the mission route of the group. --- @param #GROUP self --- @return #table The mission route defined by points. -function GROUP:GetTaskRoute() - self:F2( self.GroupName ) - - return routines.utils.deepCopy( _DATABASE.Templates.Groups[self.GroupName].Template.route.points ) -end - ---- Return the route of a group by using the @{Database#DATABASE} class. --- @param #GROUP self --- @param #number Begin The route point from where the copy will start. The base route point is 0. --- @param #number End The route point where the copy will end. The End point is the last point - the End point. The last point has base 0. --- @param #boolean Randomize Randomization of the route, when true. --- @param #number Radius When randomization is on, the randomization is within the radius. -function GROUP:CopyRoute( Begin, End, Randomize, Radius ) - self:F2( { Begin, End } ) - - local Points = {} - - -- Could be a Spawned Group - local GroupName = string.match( self:GetName(), ".*#" ) - if GroupName then - GroupName = GroupName:sub( 1, -2 ) - else - GroupName = self:GetName() - end - - self:T3( { GroupName } ) - - local Template = _DATABASE.Templates.Groups[GroupName].Template - - if Template then - if not Begin then - Begin = 0 - end - if not End then - End = 0 - end - - for TPointID = Begin + 1, #Template.route.points - End do - if Template.route.points[TPointID] then - Points[#Points+1] = routines.utils.deepCopy( Template.route.points[TPointID] ) - if Randomize then - if not Radius then - Radius = 500 - end - Points[#Points].x = Points[#Points].x + math.random( Radius * -1, Radius ) - Points[#Points].y = Points[#Points].y + math.random( Radius * -1, Radius ) - end - end - end - return Points - else - error( "Template not found for Group : " .. GroupName ) - end - - return nil -end - - --- Message APIs - ---- Returns a message for a coalition or a client. --- @param #GROUP self --- @param #string Message The message text --- @param DCSTypes#Duration Duration The duration of the message. --- @return Message#MESSAGE -function GROUP:Message( Message, Duration ) - self:F2( { Message, Duration } ) - - local DCSGroup = self:GetDCSObject() - if DCSGroup then - return MESSAGE:New( Message, Duration, self:GetCallsign() .. " (" .. self:GetTypeName() .. ")" ) - end - - return nil -end - ---- Send a message to all coalitions. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #GROUP self --- @param #string Message The message text --- @param DCSTypes#Duration Duration The duration of the message. -function GROUP:MessageToAll( Message, Duration ) - self:F2( { Message, Duration } ) - - local DCSGroup = self:GetDCSObject() - if DCSGroup then - self:Message( Message, Duration ):ToAll() - end - - return nil -end - ---- Send a message to the red coalition. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #GROUP self --- @param #string Message The message text --- @param DCSTYpes#Duration Duration The duration of the message. -function GROUP:MessageToRed( Message, Duration ) - self:F2( { Message, Duration } ) - - local DCSGroup = self:GetDCSObject() - if DCSGroup then - self:Message( Message, Duration ):ToRed() - end - - return nil -end - ---- Send a message to the blue coalition. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #GROUP self --- @param #string Message The message text --- @param DCSTypes#Duration Duration The duration of the message. -function GROUP:MessageToBlue( Message, Duration ) - self:F2( { Message, Duration } ) - - local DCSGroup = self:GetDCSObject() - if DCSGroup then - self:Message( Message, Duration ):ToBlue() - end - - return nil -end - ---- Send a message to a client. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #GROUP self --- @param #string Message The message text --- @param DCSTypes#Duration Duration The duration of the message. --- @param Client#CLIENT Client The client object receiving the message. -function GROUP:MessageToClient( Message, Duration, Client ) - self:F2( { Message, Duration } ) - - local DCSGroup = self:GetDCSObject() - if DCSGroup then - self:Message( Message, Duration ):ToClient( Client ) - end - - return nil -end ---- This module contains the UNIT class. --- --- 1) @{Unit#UNIT} class, extends @{Controllable#CONTROLLABLE} --- =========================================================== --- The @{Unit#UNIT} class is a wrapper class to handle the DCS Unit objects: --- --- * Support all DCS Unit APIs. --- * Enhance with Unit specific APIs not in the DCS Unit API set. --- * Handle local Unit Controller. --- * Manage the "state" of the DCS Unit. --- --- --- 1.1) UNIT reference methods --- ---------------------- --- For each DCS Unit object alive within a running mission, a UNIT wrapper object (instance) will be created within the _@{DATABASE} object. --- This is done at the beginning of the mission (when the mission starts), and dynamically when new DCS Unit objects are spawned (using the @{SPAWN} class). --- --- The UNIT class **does not contain a :New()** method, rather it provides **:Find()** methods to retrieve the object reference --- using the DCS Unit or the DCS UnitName. --- --- Another thing to know is that UNIT objects do not "contain" the DCS Unit object. --- The UNIT methods will reference the DCS Unit object by name when it is needed during API execution. --- If the DCS Unit object does not exist or is nil, the UNIT methods will return nil and log an exception in the DCS.log file. --- --- The UNIT class provides the following functions to retrieve quickly the relevant UNIT instance: --- --- * @{#UNIT.Find}(): Find a UNIT instance from the _DATABASE object using a DCS Unit object. --- * @{#UNIT.FindByName}(): Find a UNIT instance from the _DATABASE object using a DCS Unit name. --- --- IMPORTANT: ONE SHOULD NEVER SANATIZE these UNIT OBJECT REFERENCES! (make the UNIT object references nil). --- --- 1.2) DCS UNIT APIs --- ------------------ --- The DCS Unit APIs are used extensively within MOOSE. The UNIT class has for each DCS Unit API a corresponding method. --- To be able to distinguish easily in your code the difference between a UNIT API call and a DCS Unit API call, --- the first letter of the method is also capitalized. So, by example, the DCS Unit method @{DCSUnit#Unit.getName}() --- is implemented in the UNIT class as @{#UNIT.GetName}(). --- --- 1.3) Smoke, Flare Units --- ----------------------- --- The UNIT class provides methods to smoke or flare units easily. --- The @{#UNIT.SmokeBlue}(), @{#UNIT.SmokeGreen}(),@{#UNIT.SmokeOrange}(), @{#UNIT.SmokeRed}(), @{#UNIT.SmokeRed}() methods --- will smoke the unit in the corresponding color. Note that smoking a unit is done at the current position of the DCS Unit. --- When the DCS Unit moves for whatever reason, the smoking will still continue! --- The @{#UNIT.FlareGreen}(), @{#UNIT.FlareRed}(), @{#UNIT.FlareWhite}(), @{#UNIT.FlareYellow}() --- methods will fire off a flare in the air with the corresponding color. Note that a flare is a one-off shot and its effect is of very short duration. --- --- 1.4) Location Position, Point --- ----------------------------- --- The UNIT class provides methods to obtain the current point or position of the DCS Unit. --- The @{#UNIT.GetPointVec2}(), @{#UNIT.GetPointVec3}() will obtain the current **location** of the DCS Unit in a Vec2 (2D) or a **point** in a Vec3 (3D) vector respectively. --- If you want to obtain the complete **3D position** including oriëntation and direction vectors, consult the @{#UNIT.GetPositionVec3}() method respectively. --- --- 1.5) Test if alive --- ------------------ --- The @{#UNIT.IsAlive}(), @{#UNIT.IsActive}() methods determines if the DCS Unit is alive, meaning, it is existing and active. --- --- 1.6) Test for proximity --- ----------------------- --- The UNIT class contains methods to test the location or proximity against zones or other objects. --- --- ### 1.6.1) Zones --- 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}. --- --- ### 1.6.2) Units --- Test if another DCS Unit is within a given radius of the current DCS Unit, use the @{#UNIT.OtherUnitInRadius}() method. --- --- @module Unit --- @author FlightControl - - - - - ---- The UNIT class --- @type UNIT --- @extends Controllable#CONTROLLABLE --- @field #UNIT.FlareColor FlareColor --- @field #UNIT.SmokeColor SmokeColor -UNIT = { - ClassName="UNIT", - CategoryName = { - [Unit.Category.AIRPLANE] = "Airplane", - [Unit.Category.HELICOPTER] = "Helicoper", - [Unit.Category.GROUND_UNIT] = "Ground Unit", - [Unit.Category.SHIP] = "Ship", - [Unit.Category.STRUCTURE] = "Structure", - }, - FlareColor = { - Green = trigger.flareColor.Green, - Red = trigger.flareColor.Red, - White = trigger.flareColor.White, - Yellow = trigger.flareColor.Yellow - }, - SmokeColor = { - Green = trigger.smokeColor.Green, - Red = trigger.smokeColor.Red, - White = trigger.smokeColor.White, - Orange = trigger.smokeColor.Orange, - Blue = trigger.smokeColor.Blue - }, - } - ---- FlareColor --- @type UNIT.FlareColor --- @field Green --- @field Red --- @field White --- @field Yellow - ---- SmokeColor --- @type UNIT.SmokeColor --- @field Green --- @field Red --- @field White --- @field Orange --- @field Blue - --- Registration. - ---- Create a new UNIT from DCSUnit. --- @param #UNIT self --- @param DCSUnit#Unit DCSUnit --- @param Database#DATABASE Database --- @return Unit#UNIT -function UNIT:Register( UnitName ) - - local self = BASE:Inherit( self, CONTROLLABLE:New() ) - self:F2( UnitName ) - self.UnitName = UnitName - return self -end - --- Reference methods. - ---- Finds a UNIT from the _DATABASE using a DCSUnit object. --- @param #UNIT self --- @param DCSUnit#Unit DCSUnit An existing DCS Unit object reference. --- @return Unit#UNIT self -function UNIT:Find( DCSUnit ) - - local UnitName = DCSUnit:getName() - local UnitFound = _DATABASE:FindUnit( UnitName ) - return UnitFound -end - ---- Find a UNIT in the _DATABASE using the name of an existing DCS Unit. --- @param #UNIT self --- @param #string UnitName The Unit Name. --- @return Unit#UNIT self -function UNIT:FindByName( UnitName ) - - local UnitFound = _DATABASE:FindUnit( UnitName ) - return UnitFound -end - - ---- @param #UNIT self --- @return DCSUnit#Unit -function UNIT:GetDCSObject() - - local DCSUnit = Unit.getByName( self.UnitName ) - - if DCSUnit then - return DCSUnit - end - - return nil -end - ---- Returns coalition of the Unit. --- @param Unit#UNIT self --- @return DCSCoalitionObject#coalition.side The side of the coalition. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetCoalition() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitCoalition = DCSUnit:getCoalition() - self:T3( UnitCoalition ) - return UnitCoalition - end - - return nil -end - ---- Returns country of the Unit. --- @param Unit#UNIT self --- @return DCScountry#country.id The country identifier. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetCountry() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitCountry = DCSUnit:getCountry() - self:T3( UnitCountry ) - return UnitCountry - end - - return nil -end - - ---- Returns DCS Unit object name. --- The function provides access to non-activated units too. --- @param Unit#UNIT self --- @return #string The name of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetName() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitName = self.UnitName - return UnitName - end - - return nil -end - - ---- Returns if the unit is alive. --- @param Unit#UNIT self --- @return #boolean true if Unit is alive. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:IsAlive() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitIsAlive = DCSUnit:isExist() - return UnitIsAlive - end - - return false -end - ---- Returns if the unit is activated. --- @param Unit#UNIT self --- @return #boolean true if Unit is activated. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:IsActive() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - - local UnitIsActive = DCSUnit:isActive() - return UnitIsActive - end - - return nil -end - ---- Returns if the unit is located above a runway. --- @param Unit#UNIT self --- @return #boolean true if Unit is above a runway. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:IsAboveRunway() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - - local PointVec2 = self:GetPointVec2() - local SurfaceType = land.getSurfaceType( PointVec2 ) - local IsAboveRunway = SurfaceType == land.SurfaceType.RUNWAY - - self:T2( IsAboveRunway ) - return IsAboveRunway - end - - return nil -end - - - ---- Returns name of the player that control the unit or nil if the unit is controlled by A.I. --- @param Unit#UNIT self --- @return #string Player Name --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetPlayerName() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - - local PlayerName = DCSUnit:getPlayerName() - if PlayerName == nil then - PlayerName = "" - end - return PlayerName - end - - return nil -end - ---- Returns the unit's unique identifier. --- @param Unit#UNIT self --- @return DCSUnit#Unit.ID Unit ID --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetID() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitID = DCSUnit:getID() - return UnitID - end - - return nil -end - ---- Returns the unit's number in the group. --- The number is the same number the unit has in ME. --- It may not be changed during the mission. --- If any unit in the group is destroyed, the numbers of another units will not be changed. --- @param Unit#UNIT self --- @return #number The Unit number. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetNumber() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitNumber = DCSUnit:getNumber() - return UnitNumber - end - - return nil -end - ---- Returns the unit's group if it exist and nil otherwise. --- @param Unit#UNIT self --- @return Group#GROUP The Group of the Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetGroup() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitGroup = GROUP:Find( DCSUnit:getGroup() ) - return UnitGroup - end - - return nil -end - - ---- Returns the unit's callsign - the localized string. --- @param Unit#UNIT self --- @return #string The Callsign of the Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetCallSign() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitCallSign = DCSUnit:getCallsign() - return UnitCallSign - end - - return nil -end - ---- Returns the unit's health. Dead units has health <= 1.0. --- @param Unit#UNIT self --- @return #number The Unit's health value. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetLife() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitLife = DCSUnit:getLife() - return UnitLife - end - - return nil -end - ---- Returns the Unit's initial health. --- @param Unit#UNIT self --- @return #number The Unit's initial health value. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetLife0() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitLife0 = DCSUnit:getLife0() - return UnitLife0 - end - - return nil -end - ---- Returns relative amount of fuel (from 0.0 to 1.0) the unit has in its internal tanks. If there are additional fuel tanks the value may be greater than 1.0. --- @param Unit#UNIT self --- @return #number The relative amount of fuel (from 0.0 to 1.0). --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetFuel() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitFuel = DCSUnit:getFuel() - return UnitFuel - end - - return nil -end - ---- Returns the Unit's ammunition. --- @param Unit#UNIT self --- @return DCSUnit#Unit.Ammo --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetAmmo() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitAmmo = DCSUnit:getAmmo() - return UnitAmmo - end - - return nil -end - ---- Returns the unit sensors. --- @param Unit#UNIT self --- @return DCSUnit#Unit.Sensors --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetSensors() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitSensors = DCSUnit:getSensors() - return UnitSensors - end - - return nil -end - --- Need to add here a function per sensortype --- unit:hasSensors(Unit.SensorType.RADAR, Unit.RadarType.AS) - ---- Returns two values: --- --- * First value indicates if at least one of the unit's radar(s) is on. --- * Second value is the object of the radar's interest. Not nil only if at least one radar of the unit is tracking a target. --- @param Unit#UNIT self --- @return #boolean Indicates if at least one of the unit's radar(s) is on. --- @return DCSObject#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 ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitRadarOn, UnitRadarObject = DCSUnit:getRadar() - return UnitRadarOn, UnitRadarObject - end - - return nil, nil -end - --- Need to add here functions to check if radar is on and which object etc. - ---- Returns unit descriptor. Descriptor type depends on unit category. --- @param Unit#UNIT self --- @return DCSUnit#Unit.Desc The Unit descriptor. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetDesc() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitDesc = DCSUnit:getDesc() - self:T2( UnitDesc ) - return UnitDesc - end - - self:E( "Unit " .. self.UnitName .. "not found!" ) - return nil -end - - ---- Returns the type name of the DCS Unit. --- @param Unit#UNIT self --- @return #string The type name of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetTypeName() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitTypeName = DCSUnit:getTypeName() - self:T3( UnitTypeName ) - return UnitTypeName - end - - return nil -end - - - ---- Returns the prefix name of the DCS Unit. A prefix name is a part of the name before a '#'-sign. --- DCS Units spawned with the @{SPAWN} class contain a '#'-sign to indicate the end of the (base) DCS Unit name. --- The spawn sequence number and unit number are contained within the name after the '#' sign. --- @param Unit#UNIT self --- @return #string The name of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetPrefix() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitPrefix = string.match( self.UnitName, ".*#" ):sub( 1, -2 ) - self:T3( UnitPrefix ) - return UnitPrefix - end - - return nil -end - - - ---- Returns the @{DCSTypes#Vec2} vector indicating the point in 2D of the DCS Unit within the mission. --- @param Unit#UNIT self --- @return DCSTypes#Vec2 The 2D point vector of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetPointVec2() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitPointVec3 = DCSUnit:getPosition().p - - local UnitPointVec2 = {} - UnitPointVec2.x = UnitPointVec3.x - UnitPointVec2.y = UnitPointVec3.z - - self:T2( UnitPointVec2 ) - return UnitPointVec2 - end - - return nil -end - - ---- Returns the @{DCSTypes#Vec3} vector indicating the point in 3D of the DCS Unit within the mission. --- @param Unit#UNIT self --- @return DCSTypes#Vec3 The 3D point vector of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetPointVec3() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitPointVec3 = DCSUnit:getPosition().p - self:T3( UnitPointVec3 ) - return UnitPointVec3 - end - - return nil -end - ---- Returns the @{DCSTypes#Position3} position vectors indicating the point and direction vectors in 3D of the DCS Unit within the mission. --- @param Unit#UNIT self --- @return DCSTypes#Position The 3D position vectors of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetPositionVec3() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitPosition = DCSUnit:getPosition() - self:T3( UnitPosition ) - return UnitPosition - end - - return nil -end - ---- Returns the DCS Unit velocity vector. --- @param Unit#UNIT self --- @return DCSTypes#Vec3 The velocity vector --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetVelocity() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitVelocityVec3 = DCSUnit:getVelocity() - self:T3( UnitVelocityVec3 ) - return UnitVelocityVec3 - end - - return nil -end - --- Is functions - ---- Returns true if the unit is within a @{Zone}. --- @param #UNIT self --- @param Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the unit is within the @{Zone#ZONE_BASE} -function UNIT:IsInZone( Zone ) - self:F2( { self.UnitName, Zone } ) - - if self:IsAlive() then - local IsInZone = Zone:IsPointVec3InZone( self:GetPointVec3() ) - - self:T( { IsInZone } ) - return IsInZone - else - return false - end -end - ---- Returns true if the unit is not within a @{Zone}. --- @param #UNIT self --- @param Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the unit is not within the @{Zone#ZONE_BASE} -function UNIT:IsNotInZone( Zone ) - self:F2( { self.UnitName, Zone } ) - - if self:IsAlive() then - local IsInZone = not Zone:IsPointVec3InZone( self:GetPointVec3() ) - - self:T( { IsInZone } ) - return IsInZone - else - return false - end -end - ---- Returns true if the DCS Unit is in the air. --- @param Unit#UNIT self --- @return #boolean true if in the air. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:InAir() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitInAir = DCSUnit:inAir() - self:T3( UnitInAir ) - return UnitInAir - end - - return nil -end - ---- Returns the altitude of the DCS Unit. --- @param Unit#UNIT self --- @return DCSTypes#Distance The altitude of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetAltitude() - self:F2() - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitPointVec3 = DCSUnit:getPoint() --DCSTypes#Vec3 - return UnitPointVec3.y - end - - return nil -end - ---- Returns true if there is an **other** DCS Unit within a radius of the current 2D point of the DCS Unit. --- @param Unit#UNIT self --- @param Unit#UNIT AwaitUnit The other UNIT wrapper object. --- @param Radius The radius in meters with the DCS Unit in the centre. --- @return true If the other DCS Unit is within the radius of the 2D point of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:OtherUnitInRadius( AwaitUnit, Radius ) - self:F2( { self.UnitName, AwaitUnit.UnitName, Radius } ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitPos = self:GetPointVec3() - local AwaitUnitPos = AwaitUnit:GetPointVec3() - - if (((UnitPos.x - AwaitUnitPos.x)^2 + (UnitPos.z - AwaitUnitPos.z)^2)^0.5 <= Radius) then - self:T3( "true" ) - return true - else - self:T3( "false" ) - return false - end - end - - return nil -end - ---- Returns the DCS Unit category name as defined within the DCS Unit Descriptor. --- @param Unit#UNIT self --- @return #string The DCS Unit Category Name -function UNIT:GetCategoryName() - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitCategoryName = self.CategoryName[ self:GetDesc().category ] - return UnitCategoryName - end - - return nil -end - ---- Returns the DCS Unit heading. --- @param Unit#UNIT self --- @return #number The DCS Unit heading -function UNIT:GetHeading() - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - - local UnitPosition = DCSUnit:getPosition() - if UnitPosition then - local UnitHeading = math.atan2( UnitPosition.x.z, UnitPosition.x.x ) - if UnitHeading < 0 then - UnitHeading = UnitHeading + 2 * math.pi - end - self:T2( UnitHeading ) - return UnitHeading - end - end - - return nil -end - - ---- Signal a flare at the position of the UNIT. --- @param #UNIT self -function UNIT:Flare( FlareColor ) - self:F2() - trigger.action.signalFlare( self:GetPointVec3(), FlareColor , 0 ) -end - ---- Signal a white flare at the position of the UNIT. --- @param #UNIT self -function UNIT:FlareWhite() - self:F2() - trigger.action.signalFlare( self:GetPointVec3(), trigger.flareColor.White , 0 ) -end - ---- Signal a yellow flare at the position of the UNIT. --- @param #UNIT self -function UNIT:FlareYellow() - self:F2() - trigger.action.signalFlare( self:GetPointVec3(), trigger.flareColor.Yellow , 0 ) -end - ---- Signal a green flare at the position of the UNIT. --- @param #UNIT self -function UNIT:FlareGreen() - self:F2() - trigger.action.signalFlare( self:GetPointVec3(), trigger.flareColor.Green , 0 ) -end - ---- Signal a red flare at the position of the UNIT. --- @param #UNIT self -function UNIT:FlareRed() - self:F2() - trigger.action.signalFlare( self:GetPointVec3(), trigger.flareColor.Red, 0 ) -end - ---- Smoke the UNIT. --- @param #UNIT self -function UNIT:Smoke( SmokeColor ) - self:F2() - trigger.action.smoke( self:GetPointVec3(), SmokeColor ) -end - ---- Smoke the UNIT Green. --- @param #UNIT self -function UNIT:SmokeGreen() - self:F2() - trigger.action.smoke( self:GetPointVec3(), trigger.smokeColor.Green ) -end - ---- Smoke the UNIT Red. --- @param #UNIT self -function UNIT:SmokeRed() - self:F2() - trigger.action.smoke( self:GetPointVec3(), trigger.smokeColor.Red ) -end - ---- Smoke the UNIT White. --- @param #UNIT self -function UNIT:SmokeWhite() - self:F2() - trigger.action.smoke( self:GetPointVec3(), trigger.smokeColor.White ) -end - ---- Smoke the UNIT Orange. --- @param #UNIT self -function UNIT:SmokeOrange() - self:F2() - trigger.action.smoke( self:GetPointVec3(), trigger.smokeColor.Orange ) -end - ---- Smoke the UNIT Blue. --- @param #UNIT self -function UNIT:SmokeBlue() - self:F2() - trigger.action.smoke( self:GetPointVec3(), trigger.smokeColor.Blue ) -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 UnitDescriptor = self.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 - ---- This module contains the ZONE classes, inherited from @{Zone#ZONE_BASE}. --- There are essentially two core functions that zones accomodate: --- --- * Test if an object is within the zone boundaries. --- * Provide the zone behaviour. Some zones are static, while others are moveable. --- --- The object classes are using the zone classes to test the zone boundaries, which can take various forms: --- --- * Test if completely within the zone. --- * Test if partly within the zone (for @{Group#GROUP} objects). --- * Test if not in the zone. --- * Distance to the nearest intersecting point of the zone. --- * Distance to the center of the zone. --- * ... --- --- Each of these ZONE classes have a zone name, and specific parameters defining the zone type: --- --- * @{Zone#ZONE_BASE}: The ZONE_BASE class defining the base for all other zone classes. --- * @{Zone#ZONE_RADIUS}: The ZONE_RADIUS class defined by a zone name, a location and a radius. --- * @{Zone#ZONE}: The ZONE class, defined by the zone name as defined within the Mission Editor. --- * @{Zone#ZONE_UNIT}: The ZONE_UNIT class defined by a zone around a @{Unit#UNIT} with a radius. --- * @{Zone#ZONE_POLYGON}: The ZONE_POLYGON class defined by a sequence of @{Group#GROUP} waypoints within the Mission Editor, forming a polygon. --- --- Each zone implements two polymorphic functions defined in @{Zone#ZONE_BASE}: --- --- * @{#ZONE_BASE.IsPointVec2InZone}: Returns if a location is within the zone. --- * @{#ZONE_BASE.IsPointVec3InZone}: Returns if a point is within the zone. --- --- === --- --- 1) @{Zone#ZONE_BASE} class, extends @{Base#BASE} --- ================================================ --- The ZONE_BASE class defining the base for all other zone classes. --- --- === --- --- 2) @{Zone#ZONE_RADIUS} class, extends @{Zone#ZONE_BASE} --- ======================================================= --- The ZONE_RADIUS class defined by a zone name, a location and a radius. --- --- === --- --- 3) @{Zone#ZONE} class, extends @{Zone#ZONE_RADIUS} --- ========================================== --- The ZONE class, defined by the zone name as defined within the Mission Editor. --- --- === --- --- 4) @{Zone#ZONE_UNIT} class, extends @{Zone#ZONE_RADIUS} --- ======================================================= --- The ZONE_UNIT class defined by a zone around a @{Unit#UNIT} with a radius. --- --- === --- --- 5) @{Zone#ZONE_POLYGON} class, extends @{Zone#ZONE_BASE} --- ======================================================== --- The ZONE_POLYGON class defined by a sequence of @{Group#GROUP} waypoints within the Mission Editor, forming a polygon. --- --- === --- --- @module Zone --- @author FlightControl - - - - - - - - - ---- The ZONE_BASE class --- @type ZONE_BASE --- @field #string ZoneName Name of the zone. --- @extends Base#BASE -ZONE_BASE = { - ClassName = "ZONE_BASE", - } - - ---- The ZONE_BASE.BoundingSquare --- @type ZONE_BASE.BoundingSquare --- @field DCSTypes#Distance x1 The lower x coordinate (left down) --- @field DCSTypes#Distance y1 The lower y coordinate (left down) --- @field DCSTypes#Distance x2 The higher x coordinate (right up) --- @field DCSTypes#Distance y2 The higher y coordinate (right up) - - ---- ZONE_BASE constructor --- @param #ZONE_BASE self --- @param #string ZoneName Name of the zone. --- @return #ZONE_BASE self -function ZONE_BASE:New( ZoneName ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( ZoneName ) - - self.ZoneName = ZoneName - - return self -end - ---- Returns if a location is within the zone. --- @param #ZONE_BASE self --- @param DCSTypes#Vec2 PointVec2 The location to test. --- @return #boolean true if the location is within the zone. -function ZONE_BASE:IsPointVec2InZone( PointVec2 ) - self:F2( PointVec2 ) - - return false -end - ---- Returns if a point is within the zone. --- @param #ZONE_BASE self --- @param DCSTypes#Vec3 PointVec3 The point to test. --- @return #boolean true if the point is within the zone. -function ZONE_BASE:IsPointVec3InZone( PointVec3 ) - self:F2( PointVec3 ) - - local InZone = self:IsPointVec2InZone( { x = PointVec3.x, y = PointVec3.z } ) - - return InZone -end - ---- Define a random @{DCSTypes#Vec2} within the zone. --- @param #ZONE_BASE self --- @return DCSTypes#Vec2 The Vec2 coordinates. -function ZONE_BASE:GetRandomVec2() - return { x = 0, y = 0 } -end - ---- Get the bounding square the zone. --- @param #ZONE_BASE self --- @return #ZONE_BASE.BoundingSquare The bounding square. -function ZONE_BASE:GetBoundingSquare() - return { x1 = 0, y1 = 0, x2 = 0, y2 = 0 } -end - - ---- Smokes the zone boundaries in a color. --- @param #ZONE_BASE self --- @param SmokeColor The smoke color. -function ZONE_BASE:SmokeZone( SmokeColor ) - self:F2( SmokeColor ) - -end - - ---- The ZONE_RADIUS class, defined by a zone name, a location and a radius. --- @type ZONE_RADIUS --- @field DCSTypes#Vec2 PointVec2 The current location of the zone. --- @field DCSTypes#Distance Radius The radius of the zone. --- @extends Zone#ZONE_BASE -ZONE_RADIUS = { - ClassName="ZONE_RADIUS", - } - ---- Constructor of ZONE_RADIUS, taking the zone name, the zone location and a radius. --- @param #ZONE_RADIUS self --- @param #string ZoneName Name of the zone. --- @param DCSTypes#Vec2 PointVec2 The location of the zone. --- @param DCSTypes#Distance Radius The radius of the zone. --- @return #ZONE_RADIUS self -function ZONE_RADIUS:New( ZoneName, PointVec2, Radius ) - local self = BASE:Inherit( self, ZONE_BASE:New( ZoneName ) ) - self:F( { ZoneName, PointVec2, Radius } ) - - self.Radius = Radius - self.PointVec2 = PointVec2 - - return self -end - ---- Smokes the zone boundaries in a color. --- @param #ZONE_RADIUS self --- @param #POINT_VEC3.SmokeColor SmokeColor The smoke color. --- @param #number Points (optional) The amount of points in the circle. --- @return #ZONE_RADIUS self -function ZONE_RADIUS:SmokeZone( SmokeColor, Points ) - self:F2( SmokeColor ) - - local Point = {} - local PointVec2 = self:GetPointVec2() - - Points = Points and Points or 360 - - local Angle - local RadialBase = math.pi*2 - - for Angle = 0, 360, 360 / Points do - local Radial = Angle * RadialBase / 360 - Point.x = PointVec2.x + math.cos( Radial ) * self:GetRadius() - Point.y = PointVec2.y + math.sin( Radial ) * self:GetRadius() - POINT_VEC2:New( Point.x, Point.y ):Smoke( SmokeColor ) - end - - return self -end - - ---- Flares the zone boundaries in a color. --- @param #ZONE_RADIUS self --- @param #POINT_VEC3.FlareColor FlareColor The flare color. --- @param #number Points (optional) The amount of points in the circle. --- @param DCSTypes#Azimuth Azimuth (optional) Azimuth The azimuth of the flare. --- @return #ZONE_RADIUS self -function ZONE_RADIUS:FlareZone( FlareColor, Points, Azimuth ) - self:F2( { FlareColor, Azimuth } ) - - local Point = {} - local PointVec2 = self:GetPointVec2() - - Points = Points and Points or 360 - - local Angle - local RadialBase = math.pi*2 - - for Angle = 0, 360, 360 / Points do - local Radial = Angle * RadialBase / 360 - Point.x = PointVec2.x + math.cos( Radial ) * self:GetRadius() - Point.y = PointVec2.y + math.sin( Radial ) * self:GetRadius() - POINT_VEC2:New( Point.x, Point.y ):Flare( FlareColor, Azimuth ) - end - - return self -end - ---- Returns the radius of the zone. --- @param #ZONE_RADIUS self --- @return DCSTypes#Distance The radius of the zone. -function ZONE_RADIUS:GetRadius() - self:F2( self.ZoneName ) - - self:T2( { self.Radius } ) - - return self.Radius -end - ---- Sets the radius of the zone. --- @param #ZONE_RADIUS self --- @param DCSTypes#Distance Radius The radius of the zone. --- @return DCSTypes#Distance The radius of the zone. -function ZONE_RADIUS:SetRadius( Radius ) - self:F2( self.ZoneName ) - - self.Radius = Radius - self:T2( { self.Radius } ) - - return self.Radius -end - ---- Returns the location of the zone. --- @param #ZONE_RADIUS self --- @return DCSTypes#Vec2 The location of the zone. -function ZONE_RADIUS:GetPointVec2() - self:F2( self.ZoneName ) - - self:T2( { self.PointVec2 } ) - - return self.PointVec2 -end - ---- Sets the location of the zone. --- @param #ZONE_RADIUS self --- @param DCSTypes#Vec2 PointVec2 The new location of the zone. --- @return DCSTypes#Vec2 The new location of the zone. -function ZONE_RADIUS:SetPointVec2( PointVec2 ) - self:F2( self.ZoneName ) - - self.PointVec2 = PointVec2 - - self:T2( { self.PointVec2 } ) - - return self.PointVec2 -end - ---- Returns the point of the zone. --- @param #ZONE_RADIUS self --- @param DCSTypes#Distance Height The height to add to the land height where the center of the zone is located. --- @return DCSTypes#Vec3 The point of the zone. -function ZONE_RADIUS:GetPointVec3( Height ) - self:F2( self.ZoneName ) - - local PointVec2 = self:GetPointVec2() - - local PointVec3 = { x = PointVec2.x, y = land.getHeight( self:GetPointVec2() ) + Height, z = PointVec2.y } - - self:T2( { PointVec3 } ) - - return PointVec3 -end - - ---- Returns if a location is within the zone. --- @param #ZONE_RADIUS self --- @param DCSTypes#Vec2 PointVec2 The location to test. --- @return #boolean true if the location is within the zone. -function ZONE_RADIUS:IsPointVec2InZone( PointVec2 ) - self:F2( PointVec2 ) - - local ZonePointVec2 = self:GetPointVec2() - - if (( PointVec2.x - ZonePointVec2.x )^2 + ( PointVec2.y - ZonePointVec2.y ) ^2 ) ^ 0.5 <= self:GetRadius() then - return true - end - - return false -end - ---- Returns if a point is within the zone. --- @param #ZONE_RADIUS self --- @param DCSTypes#Vec3 PointVec3 The point to test. --- @return #boolean true if the point is within the zone. -function ZONE_RADIUS:IsPointVec3InZone( PointVec3 ) - self:F2( PointVec3 ) - - local InZone = self:IsPointVec2InZone( { x = PointVec3.x, y = PointVec3.z } ) - - return InZone -end - ---- Returns a random location within the zone. --- @param #ZONE_RADIUS self --- @return DCSTypes#Vec2 The random location within the zone. -function ZONE_RADIUS:GetRandomVec2() - self:F( self.ZoneName ) - - local Point = {} - local PointVec2 = self:GetPointVec2() - - local angle = math.random() * math.pi*2; - Point.x = PointVec2.x + math.cos( angle ) * math.random() * self:GetRadius(); - Point.y = PointVec2.y + math.sin( angle ) * math.random() * self:GetRadius(); - - self:T( { Point } ) - - return Point -end - - - ---- The ZONE class, defined by the zone name as defined within the Mission Editor. The location and the radius are automatically collected from the mission settings. --- @type ZONE --- @extends Zone#ZONE_RADIUS -ZONE = { - ClassName="ZONE", - } - - ---- Constructor of ZONE, taking the zone name. --- @param #ZONE self --- @param #string ZoneName The name of the zone as defined within the mission editor. --- @return #ZONE -function ZONE:New( ZoneName ) - - local Zone = trigger.misc.getZone( ZoneName ) - - if not Zone then - error( "Zone " .. ZoneName .. " does not exist." ) - return nil - end - - local self = BASE:Inherit( self, ZONE_RADIUS:New( ZoneName, { x = Zone.point.x, y = Zone.point.z }, Zone.radius ) ) - self:F( ZoneName ) - - self.Zone = Zone - - return self -end - - ---- The ZONE_UNIT class defined by a zone around a @{Unit#UNIT} with a radius. --- @type ZONE_UNIT --- @field Unit#UNIT ZoneUNIT --- @extends Zone#ZONE_RADIUS -ZONE_UNIT = { - ClassName="ZONE_UNIT", - } - ---- Constructor to create a ZONE_UNIT instance, taking the zone name, a zone unit and a radius. --- @param #ZONE_UNIT self --- @param #string ZoneName Name of the zone. --- @param Unit#UNIT ZoneUNIT The unit as the center of the zone. --- @param DCSTypes#Distance Radius The radius of the zone. --- @return #ZONE_UNIT self -function ZONE_UNIT:New( ZoneName, ZoneUNIT, Radius ) - local self = BASE:Inherit( self, ZONE_RADIUS:New( ZoneName, ZoneUNIT:GetPointVec2(), Radius ) ) - self:F( { ZoneName, ZoneUNIT:GetPointVec2(), Radius } ) - - self.ZoneUNIT = ZoneUNIT - - return self -end - - ---- Returns the current location of the @{Unit#UNIT}. --- @param #ZONE_UNIT self --- @return DCSTypes#Vec2 The location of the zone based on the @{Unit#UNIT}location. -function ZONE_UNIT:GetPointVec2() - self:F( self.ZoneName ) - - local ZonePointVec2 = self.ZoneUNIT:GetPointVec2() - - self:T( { ZonePointVec2 } ) - - return ZonePointVec2 -end - --- Polygons - ---- The ZONE_POLYGON_BASE class defined by an array of @{DCSTypes#Vec2}, forming a polygon. --- @type ZONE_POLYGON_BASE --- @field #ZONE_POLYGON_BASE.ListVec2 Polygon The polygon defined by an array of @{DCSTypes#Vec2}. --- @extends Zone#ZONE_BASE -ZONE_POLYGON_BASE = { - ClassName="ZONE_POLYGON_BASE", - } - ---- A points array. --- @type ZONE_POLYGON_BASE.ListVec2 --- @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. --- @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.. --- @return #ZONE_POLYGON_BASE self -function ZONE_POLYGON_BASE:New( ZoneName, PointsArray ) - local self = BASE:Inherit( self, ZONE_BASE:New( ZoneName ) ) - self:F( { ZoneName, PointsArray } ) - - local i = 0 - - self.Polygon = {} - - for i = 1, #PointsArray do - self.Polygon[i] = {} - self.Polygon[i].x = PointsArray[i].x - self.Polygon[i].y = PointsArray[i].y - end - - return self -end - ---- Flush polygon coordinates as a table in DCS.log. --- @param #ZONE_POLYGON_BASE self --- @return #ZONE_POLYGON_BASE self -function ZONE_POLYGON_BASE:Flush() - self:F2() - - self:E( { Polygon = self.ZoneName, Coordinates = self.Polygon } ) - - return self -end - - ---- Smokes the zone boundaries in a color. --- @param #ZONE_POLYGON_BASE self --- @param #POINT_VEC3.SmokeColor SmokeColor The smoke color. --- @return #ZONE_POLYGON_BASE self -function ZONE_POLYGON_BASE:SmokeZone( SmokeColor ) - self:F2( SmokeColor ) - - local i - local j - local Segments = 10 - - i = 1 - j = #self.Polygon - - while i <= #self.Polygon do - self:T( { i, j, self.Polygon[i], self.Polygon[j] } ) - - local DeltaX = self.Polygon[j].x - self.Polygon[i].x - local DeltaY = self.Polygon[j].y - self.Polygon[i].y - - for Segment = 0, Segments do -- We divide each line in 5 segments and smoke a point on the line. - local PointX = self.Polygon[i].x + ( Segment * DeltaX / Segments ) - local PointY = self.Polygon[i].y + ( Segment * DeltaY / Segments ) - POINT_VEC2:New( PointX, PointY ):Smoke( SmokeColor ) - end - j = i - i = i + 1 - end - - return self -end - - - - ---- Returns if a location is within the zone. --- @param #ZONE_POLYGON_BASE self --- @param DCSTypes#Vec2 PointVec2 The location to test. --- @return #boolean true if the location is within the zone. -function ZONE_POLYGON_BASE:IsPointVec2InZone( PointVec2 ) - self:F2( PointVec2 ) - - local i - local j - local c = false - - i = 1 - j = #self.Polygon - - while i < #self.Polygon do - j = i - i = i + 1 - self:T( { i, j, self.Polygon[i], self.Polygon[j] } ) - if ( ( ( self.Polygon[i].y > PointVec2.y ) ~= ( self.Polygon[j].y > PointVec2.y ) ) and - ( PointVec2.x < ( self.Polygon[j].x - self.Polygon[i].x ) * ( PointVec2.y - self.Polygon[i].y ) / ( self.Polygon[j].y - self.Polygon[i].y ) + self.Polygon[i].x ) - ) then - c = not c - end - self:T2( { "c = ", c } ) - end - - self:T( { "c = ", c } ) - return c -end - ---- Define a random @{DCSTypes#Vec2} within the zone. --- @param #ZONE_POLYGON_BASE self --- @return DCSTypes#Vec2 The Vec2 coordinate. -function ZONE_POLYGON_BASE:GetRandomVec2() - self:F2() - - --- It is a bit tricky to find a random point within a polygon. Right now i am doing it the dirty and inefficient way... - local Vec2Found = false - local Vec2 - local BS = self:GetBoundingSquare() - - self:T2( BS ) - - while Vec2Found == false do - Vec2 = { x = math.random( BS.x1, BS.x2 ), y = math.random( BS.y1, BS.y2 ) } - self:T2( Vec2 ) - if self:IsPointVec2InZone( Vec2 ) then - Vec2Found = true - end - end - - self:T2( Vec2 ) - - return Vec2 -end - ---- Get the bounding square the zone. --- @param #ZONE_POLYGON_BASE self --- @return #ZONE_POLYGON_BASE.BoundingSquare The bounding square. -function ZONE_POLYGON_BASE:GetBoundingSquare() - - local x1 = self.Polygon[1].x - local y1 = self.Polygon[1].y - local x2 = self.Polygon[1].x - local y2 = self.Polygon[1].y - - for i = 2, #self.Polygon do - self:T2( { self.Polygon[i], x1, y1, x2, y2 } ) - x1 = ( x1 > self.Polygon[i].x ) and self.Polygon[i].x or x1 - x2 = ( x2 < self.Polygon[i].x ) and self.Polygon[i].x or x2 - y1 = ( y1 > self.Polygon[i].y ) and self.Polygon[i].y or y1 - y2 = ( y2 < self.Polygon[i].y ) and self.Polygon[i].y or y2 - - end - - return { x1 = x1, y1 = y1, x2 = x2, y2 = y2 } -end - - - - - ---- The ZONE_POLYGON class defined by a sequence of @{Group#GROUP} waypoints within the Mission Editor, forming a polygon. --- @type ZONE_POLYGON --- @extends Zone#ZONE_POLYGON_BASE -ZONE_POLYGON = { - ClassName="ZONE_POLYGON", - } - ---- 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. --- @param #ZONE_POLYGON self --- @param #string ZoneName Name of the zone. --- @param Group#GROUP ZoneGroup The GROUP waypoints as defined within the Mission Editor define the polygon shape. --- @return #ZONE_POLYGON self -function ZONE_POLYGON:New( ZoneName, ZoneGroup ) - - local GroupPoints = ZoneGroup:GetTaskRoute() - - local self = BASE:Inherit( self, ZONE_POLYGON_BASE:New( ZoneName, GroupPoints ) ) - self:F( { ZoneName, ZoneGroup, self.Polygon } ) - - return self -end - ---- This module contains the CLIENT class. --- --- 1) @{Client#CLIENT} class, extends @{Unit#UNIT} --- =============================================== --- 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#CLIENT} class is a wrapper class to handle the DCS Unit objects that have the skillset defined as __Client__ or __Player__: --- --- * Wraps the DCS Unit objects with skill level set to Player or Client. --- * Support all DCS Unit APIs. --- * Enhance with Unit specific APIs not in the DCS Group API set. --- * When player joins Unit, execute alive init logic. --- * Handles messages to players. --- * Manage the "state" of the DCS Unit. --- --- Clients are being used by the @{MISSION} class to follow players and register their successes. --- --- 1.1) CLIENT reference methods --- ----------------------------- --- For each DCS Unit having skill level Player or Client, a CLIENT wrapper object (instance) will be created within the _@{DATABASE} object. --- This is done at the beginning of the mission (when the mission starts). --- --- The CLIENT class does not contain a :New() method, rather it provides :Find() methods to retrieve the object reference --- using the DCS Unit or the DCS UnitName. --- --- Another thing to know is that CLIENT objects do not "contain" the DCS Unit object. --- The CLIENT methods will reference the DCS Unit object by name when it is needed during API execution. --- If the DCS Unit object does not exist or is nil, the CLIENT methods will return nil and log an exception in the DCS.log file. --- --- The CLIENT class provides the following functions to retrieve quickly the relevant CLIENT instance: --- --- * @{#CLIENT.Find}(): Find a CLIENT instance from the _DATABASE object using a DCS Unit object. --- * @{#CLIENT.FindByName}(): Find a CLIENT instance from the _DATABASE object using a DCS Unit name. --- --- IMPORTANT: ONE SHOULD NEVER SANATIZE these CLIENT OBJECT REFERENCES! (make the CLIENT object references nil). --- --- @module Client --- @author FlightControl - ---- The CLIENT class --- @type CLIENT --- @extends Unit#UNIT -CLIENT = { - ONBOARDSIDE = { - NONE = 0, - LEFT = 1, - RIGHT = 2, - BACK = 3, - FRONT = 4 - }, - ClassName = "CLIENT", - ClientName = nil, - ClientAlive = false, - ClientTransport = false, - ClientBriefingShown = false, - _Menus = {}, - _Tasks = {}, - Messages = { - } -} - - ---- Finds a CLIENT from the _DATABASE using the relevant DCS Unit. --- @param #CLIENT self --- @param #string ClientName Name of the DCS **Unit** as defined within the Mission Editor. --- @param #string ClientBriefing Text that describes the briefing of the mission when a Player logs into the Client. --- @return #CLIENT --- @usage --- -- Create new Clients. --- local Mission = MISSIONSCHEDULER.AddMission( 'Russia Transport Troops SA-6', 'Operational', 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', 'Russia' ) --- Mission:AddGoal( DeploySA6TroopsGoal ) --- --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 1' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 3' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 2' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 4' ):Transport() ) -function CLIENT:Find( DCSUnit ) - local ClientName = DCSUnit:getName() - local ClientFound = _DATABASE:FindClient( ClientName ) - - if ClientFound then - ClientFound:F( ClientName ) - return ClientFound - end - - error( "CLIENT not found for: " .. ClientName ) -end - - ---- Finds a CLIENT from the _DATABASE using the relevant Client Unit Name. --- As an optional parameter, a briefing text can be given also. --- @param #CLIENT self --- @param #string ClientName Name of the DCS **Unit** as defined within the Mission Editor. --- @param #string ClientBriefing Text that describes the briefing of the mission when a Player logs into the Client. --- @return #CLIENT --- @usage --- -- Create new Clients. --- local Mission = MISSIONSCHEDULER.AddMission( 'Russia Transport Troops SA-6', 'Operational', 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', 'Russia' ) --- Mission:AddGoal( DeploySA6TroopsGoal ) --- --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 1' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 3' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 2' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 4' ):Transport() ) -function CLIENT:FindByName( ClientName, ClientBriefing ) - local ClientFound = _DATABASE:FindClient( ClientName ) - - if ClientFound then - ClientFound:F( { ClientName, ClientBriefing } ) - ClientFound:AddBriefing( ClientBriefing ) - ClientFound.MessageSwitch = true - - return ClientFound - end - - error( "CLIENT not found for: " .. ClientName ) -end - -function CLIENT:Register( ClientName ) - local self = BASE:Inherit( self, UNIT:Register( ClientName ) ) - - self:F( ClientName ) - self.ClientName = ClientName - self.MessageSwitch = true - self.ClientAlive2 = false - - --self.AliveCheckScheduler = routines.scheduleFunction( self._AliveCheckScheduler, { self }, timer.getTime() + 1, 5 ) - self.AliveCheckScheduler = SCHEDULER:New( self, self._AliveCheckScheduler, { "Client Alive " .. ClientName }, 1, 5 ) - - self:E( self ) - return self -end - - ---- Transport defines that the Client is a Transport. Transports show cargo. --- @param #CLIENT self --- @return #CLIENT -function CLIENT:Transport() - self:F() - - self.ClientTransport = true - return self -end - ---- AddBriefing adds a briefing to a CLIENT when a player joins a mission. --- @param #CLIENT self --- @param #string ClientBriefing is the text defining the Mission briefing. --- @return #CLIENT self -function CLIENT:AddBriefing( ClientBriefing ) - self:F( ClientBriefing ) - self.ClientBriefing = ClientBriefing - self.ClientBriefingShown = false - - return self -end - ---- Show the briefing of a CLIENT. --- @param #CLIENT self --- @return #CLIENT self -function CLIENT:ShowBriefing() - self:F( { self.ClientName, self.ClientBriefingShown } ) - - if not self.ClientBriefingShown then - self.ClientBriefingShown = true - local Briefing = "" - if self.ClientBriefing then - Briefing = Briefing .. self.ClientBriefing - end - Briefing = Briefing .. " Press [LEFT ALT]+[B] to view the complete mission briefing." - self:Message( Briefing, 60, "Briefing" ) - end - - return self -end - ---- Show the mission briefing of a MISSION to the CLIENT. --- @param #CLIENT self --- @param #string MissionBriefing --- @return #CLIENT self -function CLIENT:ShowMissionBriefing( MissionBriefing ) - self:F( { self.ClientName } ) - - if MissionBriefing then - self:Message( MissionBriefing, 60, "Mission Briefing" ) - end - - return self -end - - - ---- Resets a CLIENT. --- @param #CLIENT self --- @param #string ClientName Name of the Group as defined within the Mission Editor. The Group must have a Unit with the type Client. -function CLIENT:Reset( ClientName ) - self:F() - self._Menus = {} -end - --- Is Functions - ---- Checks if the CLIENT is a multi-seated UNIT. --- @param #CLIENT self --- @return #boolean true if multi-seated. -function CLIENT:IsMultiSeated() - self:F( self.ClientName ) - - local ClientMultiSeatedTypes = { - ["Mi-8MT"] = "Mi-8MT", - ["UH-1H"] = "UH-1H", - ["P-51B"] = "P-51B" - } - - if self:IsAlive() then - local ClientTypeName = self:GetClientGroupUnit():GetTypeName() - if ClientMultiSeatedTypes[ClientTypeName] then - return true - end - end - - return false -end - ---- Checks for a client alive event and calls a function on a continuous basis. --- @param #CLIENT self --- @param #function CallBack Function. --- @return #CLIENT -function CLIENT:Alive( CallBackFunction, ... ) - self:F() - - self.ClientCallBack = CallBackFunction - self.ClientParameters = arg - - return self -end - ---- @param #CLIENT self -function CLIENT:_AliveCheckScheduler( SchedulerName ) - self:F( { SchedulerName, self.ClientName, self.ClientAlive2, self.ClientBriefingShown, self.ClientCallBack } ) - - if self:IsAlive() then - if self.ClientAlive2 == false then - self:ShowBriefing() - if self.ClientCallBack then - self:T("Calling Callback function") - self.ClientCallBack( self, unpack( self.ClientParameters ) ) - end - self.ClientAlive2 = true - end - else - if self.ClientAlive2 == true then - self.ClientAlive2 = false - end - end - - return true -end - ---- Return the DCSGroup of a Client. --- This function is modified to deal with a couple of bugs in DCS 1.5.3 --- @param #CLIENT self --- @return DCSGroup#Group -function CLIENT:GetDCSGroup() - self:F3() - --- local ClientData = Group.getByName( self.ClientName ) --- if ClientData and ClientData:isExist() then --- self:T( self.ClientName .. " : group found!" ) --- return ClientData --- else --- return nil --- end - - local ClientUnit = Unit.getByName( self.ClientName ) - - local CoalitionsData = { AlivePlayersRed = coalition.getPlayers( coalition.side.RED ), AlivePlayersBlue = coalition.getPlayers( coalition.side.BLUE ) } - for CoalitionId, CoalitionData in pairs( CoalitionsData ) do - self:T3( { "CoalitionData:", CoalitionData } ) - for UnitId, UnitData in pairs( CoalitionData ) do - self:T3( { "UnitData:", UnitData } ) - if UnitData and UnitData:isExist() then - - --self:E(self.ClientName) - if ClientUnit then - local ClientGroup = ClientUnit:getGroup() - if ClientGroup then - self:T3( "ClientGroup = " .. self.ClientName ) - if ClientGroup:isExist() and UnitData:getGroup():isExist() then - if ClientGroup:getID() == UnitData:getGroup():getID() then - self:T3( "Normal logic" ) - self:T3( self.ClientName .. " : group found!" ) - self.ClientGroupID = ClientGroup:getID() - self.ClientGroupName = ClientGroup:getName() - return ClientGroup - end - else - -- Now we need to resolve the bugs in DCS 1.5 ... - -- Consult the database for the units of the Client Group. (ClientGroup:getUnits() returns nil) - self:T3( "Bug 1.5 logic" ) - local ClientGroupTemplate = _DATABASE.Templates.Units[self.ClientName].GroupTemplate - self.ClientGroupID = ClientGroupTemplate.groupId - self.ClientGroupName = _DATABASE.Templates.Units[self.ClientName].GroupName - self:T3( self.ClientName .. " : group found in bug 1.5 resolvement logic!" ) - return ClientGroup - end - -- else - -- error( "Client " .. self.ClientName .. " not found!" ) - end - else - --self:E( { "Client not found!", self.ClientName } ) - end - end - end - end - - -- For non player clients - if ClientUnit then - local ClientGroup = ClientUnit:getGroup() - if ClientGroup then - self:T3( "ClientGroup = " .. self.ClientName ) - if ClientGroup:isExist() then - self:T3( "Normal logic" ) - self:T3( self.ClientName .. " : group found!" ) - return ClientGroup - end - end - end - - self.ClientGroupID = nil - self.ClientGroupUnit = nil - - return nil -end - - --- TODO: Check DCSTypes#Group.ID ---- Get the group ID of the client. --- @param #CLIENT self --- @return DCSTypes#Group.ID -function CLIENT:GetClientGroupID() - - local ClientGroup = self:GetDCSGroup() - - --self:E( self.ClientGroupID ) -- Determined in GetDCSGroup() - return self.ClientGroupID -end - - ---- Get the name of the group of the client. --- @param #CLIENT self --- @return #string -function CLIENT:GetClientGroupName() - - local ClientGroup = self:GetDCSGroup() - - self:T( self.ClientGroupName ) -- Determined in GetDCSGroup() - return self.ClientGroupName -end - ---- Returns the UNIT of the CLIENT. --- @param #CLIENT self --- @return Unit#UNIT -function CLIENT:GetClientGroupUnit() - self:F2() - - local ClientDCSUnit = Unit.getByName( self.ClientName ) - - self:T( self.ClientDCSUnit ) - if ClientDCSUnit and ClientDCSUnit:isExist() then - local ClientUnit = _DATABASE:FindUnit( self.ClientName ) - self:T2( ClientUnit ) - return ClientUnit - end -end - ---- Returns the DCSUnit of the CLIENT. --- @param #CLIENT self --- @return DCSTypes#Unit -function CLIENT:GetClientGroupDCSUnit() - self:F2() - - local ClientDCSUnit = Unit.getByName( self.ClientName ) - - if ClientDCSUnit and ClientDCSUnit:isExist() then - self:T2( ClientDCSUnit ) - return ClientDCSUnit - end -end - - ---- Evaluates if the CLIENT is a transport. --- @param #CLIENT self --- @return #boolean true is a transport. -function CLIENT:IsTransport() - self:F() - return self.ClientTransport -end - ---- Shows the @{Cargo#CARGO} contained within the CLIENT to the player as a message. --- The @{Cargo#CARGO} is shown using the @{Message#MESSAGE} distribution system. --- @param #CLIENT self -function CLIENT:ShowCargo() - self:F() - - local CargoMsg = "" - - for CargoName, Cargo in pairs( CARGOS ) do - if self == Cargo:IsLoadedInClient() then - CargoMsg = CargoMsg .. Cargo.CargoName .. " Type:" .. Cargo.CargoType .. " Weight: " .. Cargo.CargoWeight .. "\n" - end - end - - if CargoMsg == "" then - CargoMsg = "empty" - end - - self:Message( CargoMsg, 15, "Co-Pilot: Cargo Status", 30 ) - -end - --- 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. --- @param #CLIENT self --- @param #string Message is the text describing the message. --- @param #number MessageDuration is the duration in seconds that the Message should be displayed. --- @param #string MessageCategory is the category of the message (the title). --- @param #number MessageInterval is the interval in seconds between the display of the @{Message#MESSAGE} when the CLIENT is in the air. --- @param #string MessageID is the identifier of the message when displayed with intervals. -function CLIENT:Message( Message, MessageDuration, MessageCategory, MessageInterval, MessageID ) - self:F( { Message, MessageDuration, MessageCategory, MessageInterval } ) - - if not self.MenuMessages then - if self:GetClientGroupID() then - self.MenuMessages = MENU_CLIENT:New( self, 'Messages' ) - self.MenuRouteMessageOn = MENU_CLIENT_COMMAND:New( self, 'Messages On', self.MenuMessages, CLIENT.SwitchMessages, { self, true } ) - self.MenuRouteMessageOff = MENU_CLIENT_COMMAND:New( self,'Messages Off', self.MenuMessages, CLIENT.SwitchMessages, { self, false } ) - end - end - - if self.MessageSwitch == true then - if MessageCategory == nil then - MessageCategory = "Messages" - end - if MessageID ~= nil then - if self.Messages[MessageID] == nil then - self.Messages[MessageID] = {} - self.Messages[MessageID].MessageId = MessageID - self.Messages[MessageID].MessageTime = timer.getTime() - self.Messages[MessageID].MessageDuration = MessageDuration - if MessageInterval == nil then - self.Messages[MessageID].MessageInterval = 600 - else - self.Messages[MessageID].MessageInterval = MessageInterval - end - MESSAGE:New( Message, MessageDuration, MessageCategory ):ToClient( self ) - else - if self:GetClientGroupDCSUnit() and not self:GetClientGroupDCSUnit():inAir() then - if timer.getTime() - self.Messages[MessageID].MessageTime >= self.Messages[MessageID].MessageDuration + 10 then - MESSAGE:New( Message, MessageDuration , MessageCategory):ToClient( self ) - self.Messages[MessageID].MessageTime = timer.getTime() - end - else - if timer.getTime() - self.Messages[MessageID].MessageTime >= self.Messages[MessageID].MessageDuration + self.Messages[MessageID].MessageInterval then - MESSAGE:New( Message, MessageDuration, MessageCategory ):ToClient( self ) - self.Messages[MessageID].MessageTime = timer.getTime() - end - end - end - else - MESSAGE:New( Message, MessageDuration, MessageCategory ):ToClient( self ) - end - end -end ---- This module contains the STATIC class. --- --- 1) @{Static#STATIC} class, extends @{Unit#UNIT} --- =============================================== --- 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: --- --- * Wraps the DCS Static objects. --- * Support all DCS Static APIs. --- * Enhance with Static specific APIs not in the DCS API set. --- --- 1.1) STATIC reference methods --- ----------------------------- --- For each DCS Static will have a STATIC wrapper object (instance) within the _@{DATABASE} object. --- This is done at the beginning of the mission (when the mission starts). --- --- The STATIC class does not contain a :New() method, rather it provides :Find() methods to retrieve the object reference --- using the Static Name. --- --- Another thing to know is that STATIC objects do not "contain" the DCS Static object. --- The STATIc methods will reference the DCS Static object by name when it is needed during API execution. --- If the DCS Static object does not exist or is nil, the STATIC methods will return nil and log an exception in the DCS.log file. --- --- The STATIc class provides the following functions to retrieve quickly the relevant STATIC instance: --- --- * @{#STATIC.FindByName}(): Find a STATIC instance from the _DATABASE object using a DCS Static name. --- --- IMPORTANT: ONE SHOULD NEVER SANATIZE these STATIC OBJECT REFERENCES! (make the STATIC object references nil). --- --- @module Static --- @author FlightControl - - - - - - ---- The STATIC class --- @type STATIC --- @extends Unit#UNIT -STATIC = { - ClassName = "STATIC", -} - - ---- Finds a STATIC from the _DATABASE using the relevant Static Name. --- As an optional parameter, a briefing text can be given also. --- @param #STATIC self --- @param #string StaticName Name of the DCS **Static** as defined within the Mission Editor. --- @return #STATIC -function STATIC:FindByName( StaticName ) - local StaticFound = _DATABASE:FindStatic( StaticName ) - - if StaticFound then - StaticFound:F( { StaticName } ) - - return StaticFound - end - - error( "STATIC not found for: " .. StaticName ) -end - -function STATIC:Register( StaticName ) - local self = BASE:Inherit( self, UNIT:Register( StaticName ) ) - - self:F( StaticName ) - - return self -end - - -function STATIC:GetDCSUnit() - local DCSStatic = StaticObject.getByName( self.UnitName ) - - if DCSStatic then - return DCSStatic - end - - return nil -end ---- This module contains the AIRBASE classes. --- --- === --- --- 1) @{Airbase#AIRBASE} class, extends @{Base#BASE} --- ================================================= --- The @{AIRBASE} class is a 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. --- --- --- 1.1) AIRBASE reference methods --- ------------------------------ --- For each DCS Airbase object alive within a running mission, a AIRBASE wrapper object (instance) will be created within the _@{DATABASE} object. --- This is done at the beginning of the mission (when the mission starts). --- --- The AIRBASE class **does not contain a :New()** method, rather it provides **:Find()** methods to retrieve the object reference --- using the DCS Airbase or the DCS AirbaseName. --- --- Another thing to know is that AIRBASE objects do not "contain" the DCS Airbase object. --- The AIRBASE methods will reference the DCS Airbase object by name when it is needed during API execution. --- If the DCS Airbase object does not exist or is nil, the AIRBASE methods will return nil and log an exception in the DCS.log file. --- --- The AIRBASE class provides the following functions to retrieve quickly the relevant AIRBASE instance: --- --- * @{#AIRBASE.Find}(): Find a AIRBASE instance from the _DATABASE object using a DCS Airbase object. --- * @{#AIRBASE.FindByName}(): Find a AIRBASE instance from the _DATABASE object using a DCS Airbase name. --- --- IMPORTANT: ONE SHOULD NEVER SANATIZE these AIRBASE OBJECT REFERENCES! (make the AIRBASE object references nil). --- --- 1.2) DCS AIRBASE APIs --- --------------------- --- The DCS Airbase APIs are used extensively within MOOSE. The AIRBASE class has for each DCS Airbase API a corresponding method. --- To be able to distinguish easily in your code the difference between a AIRBASE API call and a DCS Airbase API call, --- the first letter of the method is also capitalized. So, by example, the DCS Airbase method @{DCSAirbase#Airbase.getName}() --- is implemented in the AIRBASE class as @{#AIRBASE.GetName}(). --- --- More functions will be added --- ---------------------------- --- During the MOOSE development, more functions will be added. --- --- @module Airbase --- @author FlightControl - - - - - ---- The AIRBASE class --- @type AIRBASE --- @extends Base#BASE -AIRBASE = { - ClassName="AIRBASE", - CategoryName = { - [Airbase.Category.AIRDROME] = "Airdrome", - [Airbase.Category.HELIPAD] = "Helipad", - [Airbase.Category.SHIP] = "Ship", - }, - } - --- Registration. - ---- Create a new AIRBASE from DCSAirbase. --- @param #AIRBASE self --- @param DCSAirbase#Airbase DCSAirbase --- @param Database#DATABASE Database --- @return Airbase#AIRBASE -function AIRBASE:Register( AirbaseName ) - - local self = BASE:Inherit( self, BASE:New() ) - self:F2( AirbaseName ) - self.AirbaseName = AirbaseName - return self -end - --- Reference methods. - ---- Finds a AIRBASE from the _DATABASE using a DCSAirbase object. --- @param #AIRBASE self --- @param DCSAirbase#Airbase DCSAirbase An existing DCS Airbase object reference. --- @return Airbase#AIRBASE self -function AIRBASE:Find( DCSAirbase ) - - local AirbaseName = DCSAirbase:getName() - local AirbaseFound = _DATABASE:FindAirbase( AirbaseName ) - return AirbaseFound -end - ---- Find a AIRBASE in the _DATABASE using the name of an existing DCS Airbase. --- @param #AIRBASE self --- @param #string AirbaseName The Airbase Name. --- @return Airbase#AIRBASE self -function AIRBASE:FindByName( AirbaseName ) - - local AirbaseFound = _DATABASE:FindAirbase( AirbaseName ) - return AirbaseFound -end - -function AIRBASE:GetDCSAirbase() - local DCSAirbase = Airbase.getByName( self.AirbaseName ) - - if DCSAirbase then - return DCSAirbase - end - - return nil -end - ---- Returns coalition of the Airbase. --- @param Airbase#AIRBASE self --- @return DCSCoalitionObject#coalition.side The side of the coalition. --- @return #nil The DCS Airbase is not existing or alive. -function AIRBASE:GetCoalition() - self:F2( self.AirbaseName ) - - local DCSAirbase = self:GetDCSAirbase() - - if DCSAirbase then - local AirbaseCoalition = DCSAirbase:getCoalition() - self:T3( AirbaseCoalition ) - return AirbaseCoalition - end - - return nil -end - ---- Returns country of the Airbase. --- @param Airbase#AIRBASE self --- @return DCScountry#country.id The country identifier. --- @return #nil The DCS Airbase is not existing or alive. -function AIRBASE:GetCountry() - self:F2( self.AirbaseName ) - - local DCSAirbase = self:GetDCSAirbase() - - if DCSAirbase then - local AirbaseCountry = DCSAirbase:getCountry() - self:T3( AirbaseCountry ) - return AirbaseCountry - end - - return nil -end - - ---- Returns DCS Airbase object name. --- The function provides access to non-activated units too. --- @param Airbase#AIRBASE self --- @return #string The name of the DCS Airbase. --- @return #nil The DCS Airbase is not existing or alive. -function AIRBASE:GetName() - self:F2( self.AirbaseName ) - - local DCSAirbase = self:GetDCSAirbase() - - if DCSAirbase then - local AirbaseName = self.AirbaseName - return AirbaseName - end - - return nil -end - - ---- Returns if the airbase is alive. --- @param Airbase#AIRBASE self --- @return #boolean true if Airbase is alive. --- @return #nil The DCS Airbase is not existing or alive. -function AIRBASE:IsAlive() - self:F2( self.AirbaseName ) - - local DCSAirbase = self:GetDCSAirbase() - - if DCSAirbase then - local AirbaseIsAlive = DCSAirbase:isExist() - return AirbaseIsAlive - end - - return false -end - ---- Returns the unit's unique identifier. --- @param Airbase#AIRBASE self --- @return DCSAirbase#Airbase.ID Airbase ID --- @return #nil The DCS Airbase is not existing or alive. -function AIRBASE:GetID() - self:F2( self.AirbaseName ) - - local DCSAirbase = self:GetDCSAirbase() - - if DCSAirbase then - local AirbaseID = DCSAirbase:getID() - return AirbaseID - end - - return nil -end - ---- Returns the Airbase's callsign - the localized string. --- @param Airbase#AIRBASE self --- @return #string The Callsign of the Airbase. --- @return #nil The DCS Airbase is not existing or alive. -function AIRBASE:GetCallSign() - self:F2( self.AirbaseName ) - - local DCSAirbase = self:GetDCSAirbase() - - if DCSAirbase then - local AirbaseCallSign = DCSAirbase:getCallsign() - return AirbaseCallSign - end - - return nil -end - - - ---- Returns unit descriptor. Descriptor type depends on unit category. --- @param Airbase#AIRBASE self --- @return DCSAirbase#Airbase.Desc The Airbase descriptor. --- @return #nil The DCS Airbase is not existing or alive. -function AIRBASE:GetDesc() - self:F2( self.AirbaseName ) - - local DCSAirbase = self:GetDCSAirbase() - - if DCSAirbase then - local AirbaseDesc = DCSAirbase:getDesc() - return AirbaseDesc - end - - return nil -end - - ---- Returns the type name of the DCS Airbase. --- @param Airbase#AIRBASE self --- @return #string The type name of the DCS Airbase. --- @return #nil The DCS Airbase is not existing or alive. -function AIRBASE:GetTypeName() - self:F2( self.AirbaseName ) - - local DCSAirbase = self:GetDCSAirbase() - - if DCSAirbase then - local AirbaseTypeName = DCSAirbase:getTypeName() - self:T3( AirbaseTypeName ) - return AirbaseTypeName - end - - return nil -end - - ---- Returns the @{DCSTypes#Vec2} vector indicating the point in 2D of the DCS Airbase within the mission. --- @param Airbase#AIRBASE self --- @return DCSTypes#Vec2 The 2D point vector of the DCS Airbase. --- @return #nil The DCS Airbase is not existing or alive. -function AIRBASE:GetPointVec2() - self:F2( self.AirbaseName ) - - local DCSAirbase = self:GetDCSAirbase() - - if DCSAirbase then - local AirbasePointVec3 = DCSAirbase:getPosition().p - - local AirbasePointVec2 = {} - AirbasePointVec2.x = AirbasePointVec3.x - AirbasePointVec2.y = AirbasePointVec3.z - - self:T3( AirbasePointVec2 ) - return AirbasePointVec2 - end - - return nil -end - ---- Returns the DCS Airbase category as defined within the DCS Airbase Descriptor. --- @param Airbase#AIRBASE self --- @return DCSAirbase#Airbase.Category The DCS Airbase Category -function AIRBASE:GetCategory() - local DCSAirbase = self:GetDCSAirbase() - - if DCSAirbase then - local AirbaseCategory = self:GetDesc().category - return AirbaseCategory - end - - return nil -end - - ---- Returns the DCS Airbase category name as defined within the DCS Airbase Descriptor. --- @param Airbase#AIRBASE self --- @return #string The DCS Airbase Category Name -function AIRBASE:GetCategoryName() - local DCSAirbase = self:GetDCSAirbase() - - if DCSAirbase then - local AirbaseCategoryName = self.CategoryName[ self:GetDesc().category ] - return AirbaseCategoryName - end - - return nil -end - - ---- This module contains the DATABASE class, managing the database of mission objects. --- --- ==== --- --- 1) @{Database#DATABASE} class, extends @{Base#BASE} --- =================================================== --- Mission designers can use the DATABASE class to refer to: --- --- * UNITS --- * GROUPS --- * CLIENTS --- * AIRPORTS --- * PLAYERSJOINED --- * PLAYERS --- --- On top, for internal MOOSE administration purposes, the DATBASE administers the Unit and Group TEMPLATES as defined within the Mission Editor. --- --- Moose will automatically create one instance of the DATABASE class into the **global** object _DATABASE. --- Moose refers to _DATABASE within the framework extensively, but you can also refer to the _DATABASE object within your missions if required. --- --- 1.1) DATABASE iterators --- ----------------------- --- You can iterate the database with the available iterator methods. --- The iterator methods will walk the DATABASE set, and call for each element within the set a function that you provide. --- The following iterator methods are currently available within the DATABASE: --- --- * @{#DATABASE.ForEachUnit}: Calls a function for each @{UNIT} it finds within the DATABASE. --- * @{#DATABASE.ForEachGroup}: Calls a function for each @{GROUP} it finds within the DATABASE. --- * @{#DATABASE.ForEachPlayer}: Calls a function for each alive player it finds within the DATABASE. --- * @{#DATABASE.ForEachPlayerJoined}: Calls a function for each joined player it finds within the DATABASE. --- * @{#DATABASE.ForEachClient}: Calls a function for each @{CLIENT} it finds within the DATABASE. --- * @{#DATABASE.ForEachClientAlive}: Calls a function for each alive @{CLIENT} it finds within the DATABASE. --- --- === --- --- @module Database --- @author FlightControl - ---- DATABASE class --- @type DATABASE --- @extends Base#BASE -DATABASE = { - ClassName = "DATABASE", - Templates = { - Units = {}, - Groups = {}, - ClientsByName = {}, - ClientsByID = {}, - }, - UNITS = {}, - STATICS = {}, - GROUPS = {}, - PLAYERS = {}, - PLAYERSJOINED = {}, - CLIENTS = {}, - AIRBASES = {}, - NavPoints = {}, -} - -local _DATABASECoalition = - { - [1] = "Red", - [2] = "Blue", - } - -local _DATABASECategory = - { - ["plane"] = Unit.Category.AIRPLANE, - ["helicopter"] = Unit.Category.HELICOPTER, - ["vehicle"] = Unit.Category.GROUND_UNIT, - ["ship"] = Unit.Category.SHIP, - ["static"] = Unit.Category.STRUCTURE, - } - - ---- Creates a new DATABASE object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. --- @param #DATABASE self --- @return #DATABASE --- @usage --- -- Define a new DATABASE Object. This DBObject will contain a reference to all Group and Unit Templates defined within the ME and the DCSRTE. --- DBObject = DATABASE:New() -function DATABASE:New() - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) - - _EVENTDISPATCHER:OnBirth( self._EventOnBirth, self ) - _EVENTDISPATCHER:OnDead( self._EventOnDeadOrCrash, self ) - _EVENTDISPATCHER:OnCrash( self._EventOnDeadOrCrash, self ) - - - -- Follow alive players and clients - _EVENTDISPATCHER:OnPlayerEnterUnit( self._EventOnPlayerEnterUnit, self ) - _EVENTDISPATCHER:OnPlayerLeaveUnit( self._EventOnPlayerLeaveUnit, self ) - - self:_RegisterTemplates() - self:_RegisterGroupsAndUnits() - self:_RegisterClients() - self:_RegisterStatics() - self:_RegisterPlayers() - self:_RegisterAirbases() - - return self -end - ---- Finds a Unit based on the Unit Name. --- @param #DATABASE self --- @param #string UnitName --- @return Unit#UNIT The found Unit. -function DATABASE:FindUnit( UnitName ) - - local UnitFound = self.UNITS[UnitName] - return UnitFound -end - - ---- Adds a Unit based on the Unit Name in the DATABASE. --- @param #DATABASE self -function DATABASE:AddUnit( DCSUnitName ) - - if not self.UNITS[DCSUnitName] then - self.UNITS[DCSUnitName] = UNIT:Register( DCSUnitName ) - end - - return self.UNITS[DCSUnitName] -end - - ---- Deletes a Unit from the DATABASE based on the Unit Name. --- @param #DATABASE self -function DATABASE:DeleteUnit( DCSUnitName ) - - --self.UNITS[DCSUnitName] = nil -end - ---- Adds a Static based on the Static Name in the DATABASE. --- @param #DATABASE self -function DATABASE:AddStatic( DCSStaticName ) - - if not self.STATICS[DCSStaticName] then - self.STATICS[DCSStaticName] = STATIC:Register( DCSStaticName ) - end -end - - ---- Deletes a Static from the DATABASE based on the Static Name. --- @param #DATABASE self -function DATABASE:DeleteStatic( DCSStaticName ) - - --self.STATICS[DCSStaticName] = nil -end - ---- Finds a STATIC based on the StaticName. --- @param #DATABASE self --- @param #string StaticName --- @return Static#STATIC The found STATIC. -function DATABASE:FindStatic( StaticName ) - - local StaticFound = self.STATICS[StaticName] - return StaticFound -end - ---- Adds a Airbase based on the Airbase Name in the DATABASE. --- @param #DATABASE self -function DATABASE:AddAirbase( DCSAirbaseName ) - - if not self.AIRBASES[DCSAirbaseName] then - self.AIRBASES[DCSAirbaseName] = AIRBASE:Register( DCSAirbaseName ) - end -end - - ---- Deletes a Airbase from the DATABASE based on the Airbase Name. --- @param #DATABASE self -function DATABASE:DeleteAirbase( DCSAirbaseName ) - - --self.AIRBASES[DCSAirbaseName] = nil -end - ---- Finds a AIRBASE based on the AirbaseName. --- @param #DATABASE self --- @param #string AirbaseName --- @return Airbase#AIRBASE The found AIRBASE. -function DATABASE:FindAirbase( AirbaseName ) - - local AirbaseFound = self.AIRBASES[AirbaseName] - return AirbaseFound -end - - ---- Finds a CLIENT based on the ClientName. --- @param #DATABASE self --- @param #string ClientName --- @return Client#CLIENT The found CLIENT. -function DATABASE:FindClient( ClientName ) - - local ClientFound = self.CLIENTS[ClientName] - return ClientFound -end - - ---- Adds a CLIENT based on the ClientName in the DATABASE. --- @param #DATABASE self -function DATABASE:AddClient( ClientName ) - - if not self.CLIENTS[ClientName] then - self.CLIENTS[ClientName] = CLIENT:Register( ClientName ) - end - - return self.CLIENTS[ClientName] -end - - ---- Finds a GROUP based on the GroupName. --- @param #DATABASE self --- @param #string GroupName --- @return Group#GROUP The found GROUP. -function DATABASE:FindGroup( GroupName ) - - local GroupFound = self.GROUPS[GroupName] - return GroupFound -end - - ---- Adds a GROUP based on the GroupName in the DATABASE. --- @param #DATABASE self -function DATABASE:AddGroup( GroupName ) - - if not self.GROUPS[GroupName] then - self.GROUPS[GroupName] = GROUP:Register( GroupName ) - end - - return self.GROUPS[GroupName] -end - ---- Adds a player based on the Player Name in the DATABASE. --- @param #DATABASE self -function DATABASE:AddPlayer( UnitName, PlayerName ) - - if PlayerName then - self:E( { "Add player for unit:", UnitName, PlayerName } ) - self.PLAYERS[PlayerName] = self:FindUnit( UnitName ) - self.PLAYERSJOINED[PlayerName] = PlayerName - end -end - ---- Deletes a player from the DATABASE based on the Player Name. --- @param #DATABASE self -function DATABASE:DeletePlayer( PlayerName ) - - if PlayerName then - self:E( { "Clean player:", PlayerName } ) - self.PLAYERS[PlayerName] = nil - end -end - - ---- Instantiate new Groups within the DCSRTE. --- This method expects EXACTLY the same structure as a structure within the ME, and needs 2 additional fields defined: --- SpawnCountryID, SpawnCategoryID --- This method is used by the SPAWN class. --- @param #DATABASE self --- @param #table SpawnTemplate --- @return #DATABASE self -function DATABASE:Spawn( SpawnTemplate ) - self:F2( SpawnTemplate.name ) - - self:T2( { SpawnTemplate.SpawnCountryID, SpawnTemplate.SpawnCategoryID } ) - - -- Copy the spawn variables of the template in temporary storage, nullify, and restore the spawn variables. - local SpawnCoalitionID = SpawnTemplate.SpawnCoalitionID - local SpawnCountryID = SpawnTemplate.SpawnCountryID - local SpawnCategoryID = SpawnTemplate.SpawnCategoryID - - -- Nullify - SpawnTemplate.SpawnCoalitionID = nil - SpawnTemplate.SpawnCountryID = nil - SpawnTemplate.SpawnCategoryID = nil - - self:_RegisterTemplate( SpawnTemplate, SpawnCoalitionID, SpawnCategoryID, SpawnCountryID ) - - self:T3( SpawnTemplate ) - coalition.addGroup( SpawnCountryID, SpawnCategoryID, SpawnTemplate ) - - -- Restore - SpawnTemplate.SpawnCoalitionID = SpawnCoalitionID - SpawnTemplate.SpawnCountryID = SpawnCountryID - SpawnTemplate.SpawnCategoryID = SpawnCategoryID - - local SpawnGroup = self:AddGroup( SpawnTemplate.name ) - return SpawnGroup -end - ---- Set a status to a Group within the Database, this to check crossing events for example. -function DATABASE:SetStatusGroup( GroupName, Status ) - self:F2( Status ) - - self.Templates.Groups[GroupName].Status = Status -end - ---- Get a status to a Group within the Database, this to check crossing events for example. -function DATABASE:GetStatusGroup( GroupName ) - self:F2( Status ) - - if self.Templates.Groups[GroupName] then - return self.Templates.Groups[GroupName].Status - else - return "" - end -end - ---- Private method that registers new Group Templates within the DATABASE Object. --- @param #DATABASE self --- @param #table GroupTemplate --- @return #DATABASE self -function DATABASE:_RegisterTemplate( GroupTemplate, CoalitionID, CategoryID, CountryID ) - - local GroupTemplateName = env.getValueDictByKey(GroupTemplate.name) - - local TraceTable = {} - - if not self.Templates.Groups[GroupTemplateName] then - self.Templates.Groups[GroupTemplateName] = {} - self.Templates.Groups[GroupTemplateName].Status = nil - end - - -- Delete the spans from the route, it is not needed and takes memory. - if GroupTemplate.route and GroupTemplate.route.spans then - GroupTemplate.route.spans = nil - end - - self.Templates.Groups[GroupTemplateName].GroupName = GroupTemplateName - self.Templates.Groups[GroupTemplateName].Template = GroupTemplate - self.Templates.Groups[GroupTemplateName].groupId = GroupTemplate.groupId - self.Templates.Groups[GroupTemplateName].UnitCount = #GroupTemplate.units - self.Templates.Groups[GroupTemplateName].Units = GroupTemplate.units - self.Templates.Groups[GroupTemplateName].CategoryID = CategoryID - self.Templates.Groups[GroupTemplateName].CoalitionID = CoalitionID - 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" - - for unit_num, UnitTemplate in pairs( GroupTemplate.units ) do - - local UnitTemplateName = env.getValueDictByKey(UnitTemplate.name) - self.Templates.Units[UnitTemplateName] = {} - self.Templates.Units[UnitTemplateName].UnitName = UnitTemplateName - self.Templates.Units[UnitTemplateName].Template = UnitTemplate - self.Templates.Units[UnitTemplateName].GroupName = GroupTemplateName - self.Templates.Units[UnitTemplateName].GroupTemplate = GroupTemplate - self.Templates.Units[UnitTemplateName].GroupId = GroupTemplate.groupId - self.Templates.Units[UnitTemplateName].CategoryID = CategoryID - self.Templates.Units[UnitTemplateName].CoalitionID = CoalitionID - self.Templates.Units[UnitTemplateName].CountryID = CountryID - - if UnitTemplate.skill and (UnitTemplate.skill == "Client" or UnitTemplate.skill == "Player") then - self.Templates.ClientsByName[UnitTemplateName] = UnitTemplate - self.Templates.ClientsByName[UnitTemplateName].CategoryID = CategoryID - self.Templates.ClientsByName[UnitTemplateName].CoalitionID = CoalitionID - self.Templates.ClientsByName[UnitTemplateName].CountryID = CountryID - self.Templates.ClientsByID[UnitTemplate.unitId] = UnitTemplate - end - - TraceTable[#TraceTable+1] = self.Templates.Units[UnitTemplateName].UnitName - end - - self:E( TraceTable ) -end - -function DATABASE:GetCoalitionFromClientTemplate( ClientName ) - return self.Templates.ClientsByName[ClientName].CoalitionID -end - -function DATABASE:GetCategoryFromClientTemplate( ClientName ) - return self.Templates.ClientsByName[ClientName].CategoryID -end - -function DATABASE:GetCountryFromClientTemplate( ClientName ) - return self.Templates.ClientsByName[ClientName].CountryID -end - ---- Airbase - -function DATABASE:GetCoalitionFromAirbase( AirbaseName ) - return self.AIRBASES[AirbaseName]:GetCoalition() -end - -function DATABASE:GetCategoryFromAirbase( AirbaseName ) - return self.AIRBASES[AirbaseName]:GetCategory() -end - - - ---- Private method that registers all alive players in the mission. --- @param #DATABASE self --- @return #DATABASE self -function DATABASE:_RegisterPlayers() - - local CoalitionsData = { AlivePlayersRed = coalition.getPlayers( coalition.side.RED ), AlivePlayersBlue = coalition.getPlayers( coalition.side.BLUE ) } - for CoalitionId, CoalitionData in pairs( CoalitionsData ) do - for UnitId, UnitData in pairs( CoalitionData ) do - self:T3( { "UnitData:", UnitData } ) - if UnitData and UnitData:isExist() then - local UnitName = UnitData:getName() - local PlayerName = UnitData:getPlayerName() - if not self.PLAYERS[PlayerName] then - self:E( { "Add player for unit:", UnitName, PlayerName } ) - self:AddPlayer( UnitName, PlayerName ) - end - end - end - end - - return self -end - - ---- Private method that registers all Groups and Units within in the mission. --- @param #DATABASE self --- @return #DATABASE self -function DATABASE:_RegisterGroupsAndUnits() - - local CoalitionsData = { GroupsRed = coalition.getGroups( coalition.side.RED ), GroupsBlue = coalition.getGroups( coalition.side.BLUE ) } - 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:AddGroup( DCSGroupName ) - - for DCSUnitId, DCSUnit in pairs( DCSGroup:getUnits() ) do - - local DCSUnitName = DCSUnit:getName() - self:E( { "Register Unit:", DCSUnitName } ) - self:AddUnit( DCSUnitName ) - end - else - self:E( { "Group does not exist: ", DCSGroup } ) - end - - end - end - - return self -end - ---- Private method that registers all Units of skill Client or Player within in the mission. --- @param #DATABASE self --- @return #DATABASE self -function DATABASE:_RegisterClients() - - for ClientName, ClientTemplate in pairs( self.Templates.ClientsByName ) do - self:E( { "Register Client:", ClientName } ) - self:AddClient( ClientName ) - end - - return self -end - ---- @param #DATABASE self -function DATABASE:_RegisterStatics() - - local CoalitionsData = { GroupsRed = coalition.getStaticObjects( coalition.side.RED ), GroupsBlue = coalition.getStaticObjects( coalition.side.BLUE ) } - 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:AddStatic( DCSStaticName ) - else - self:E( { "Static does not exist: ", DCSStatic } ) - end - end - end - - return self -end - ---- @param #DATABASE self -function DATABASE:_RegisterAirbases() - - local CoalitionsData = { AirbasesRed = coalition.getAirbases( coalition.side.RED ), AirbasesBlue = coalition.getAirbases( coalition.side.BLUE ), AirbasesNeutral = coalition.getAirbases( coalition.side.NEUTRAL ) } - for CoalitionId, CoalitionData in pairs( CoalitionsData ) do - for DCSAirbaseId, DCSAirbase in pairs( CoalitionData ) do - - local DCSAirbaseName = DCSAirbase:getName() - - self:E( { "Register Airbase:", DCSAirbaseName } ) - self:AddAirbase( DCSAirbaseName ) - end - end - - return self -end - - ---- Events - ---- Handles the OnBirth event for the alive units set. --- @param #DATABASE self --- @param Event#EVENTDATA Event -function DATABASE:_EventOnBirth( Event ) - self:F2( { Event } ) - - if Event.IniDCSUnit then - self:AddUnit( Event.IniDCSUnitName ) - self:AddGroup( Event.IniDCSGroupName ) - self:_EventOnPlayerEnterUnit( Event ) - end -end - - ---- Handles the OnDead or OnCrash event for alive units set. --- @param #DATABASE self --- @param Event#EVENTDATA Event -function DATABASE:_EventOnDeadOrCrash( Event ) - self:F2( { Event } ) - - if Event.IniDCSUnit then - if self.UNITS[Event.IniDCSUnitName] then - self:DeleteUnit( Event.IniDCSUnitName ) - -- add logic to correctly remove a group once all units are destroyed... - end - end -end - - ---- Handles the OnPlayerEnterUnit event to fill the active players table (with the unit filter applied). --- @param #DATABASE self --- @param Event#EVENTDATA Event -function DATABASE:_EventOnPlayerEnterUnit( Event ) - self:F2( { Event } ) - - if Event.IniUnit then - local PlayerName = Event.IniUnit:GetPlayerName() - if not self.PLAYERS[PlayerName] then - self:AddPlayer( Event.IniUnitName, PlayerName ) - end - end -end - - ---- Handles the OnPlayerLeaveUnit event to clean the active players table. --- @param #DATABASE self --- @param Event#EVENTDATA Event -function DATABASE:_EventOnPlayerLeaveUnit( Event ) - self:F2( { Event } ) - - if Event.IniUnit then - local PlayerName = Event.IniUnit:GetPlayerName() - if self.PLAYERS[PlayerName] then - self:DeletePlayer( PlayerName ) - end - end -end - ---- Iterators - ---- Iterate the DATABASE and call an iterator function for the given set, providing the Object for each element within the set and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called when there is an alive player in the database. --- @return #DATABASE self -function DATABASE:ForEach( IteratorFunction, FinalizeFunction, arg, Set ) - self:F2( arg ) - - local function CoRoutine() - local Count = 0 - for ObjectID, Object in pairs( Set ) do - self:T2( Object ) - IteratorFunction( Object, unpack( arg ) ) - Count = Count + 1 --- if Count % 100 == 0 then --- coroutine.yield( false ) --- end - end - return true - end - --- local co = coroutine.create( CoRoutine ) - local co = CoRoutine - - local function Schedule() - --- local status, res = coroutine.resume( co ) - local status, res = co() - self:T3( { status, res } ) - - if status == false then - error( res ) - end - if res == false then - return true -- resume next time the loop - end - if FinalizeFunction then - FinalizeFunction( unpack( arg ) ) - end - return false - end - - local Scheduler = SCHEDULER:New( self, Schedule, {}, 0.001, 0.001, 0 ) - - return self -end - - ---- Iterate the DATABASE and call an iterator function for each **alive** UNIT, providing the UNIT and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the database. The function needs to accept a UNIT parameter. --- @return #DATABASE self -function DATABASE:ForEachUnit( IteratorFunction, FinalizeFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, FinalizeFunction, arg, self.UNITS ) - - return self -end - ---- Iterate the DATABASE and call an iterator function for each **alive** GROUP, providing the GROUP and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the database. The function needs to accept a GROUP parameter. --- @return #DATABASE self -function DATABASE:ForEachGroup( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.GROUPS ) - - return self -end - - ---- Iterate the DATABASE and call an iterator function for each **ALIVE** player, providing the player name and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called when there is an player in the database. The function needs to accept the player name. --- @return #DATABASE self -function DATABASE:ForEachPlayer( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.PLAYERS ) - - return self -end - - ---- Iterate the DATABASE and call an iterator function for each player who has joined the mission, providing the Unit of the player and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called when there is was a player in the database. The function needs to accept a UNIT parameter. --- @return #DATABASE self -function DATABASE:ForEachPlayerJoined( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.PLAYERSJOINED ) - - return self -end - ---- Iterate the DATABASE and call an iterator function for each CLIENT, providing the CLIENT to the function and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called when there is an alive player in the database. The function needs to accept a CLIENT parameter. --- @return #DATABASE self -function DATABASE:ForEachClient( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.CLIENTS ) - - return self -end - - -function DATABASE:_RegisterTemplates() - self:F2() - - self.Navpoints = {} - self.UNITS = {} - --Build routines.db.units and self.Navpoints - for CoalitionName, coa_data in pairs(env.mission.coalition) do - - if (CoalitionName == 'red' or CoalitionName == 'blue') and type(coa_data) == 'table' then - --self.Units[coa_name] = {} - - ---------------------------------------------- - -- build nav points DB - self.Navpoints[CoalitionName] = {} - if coa_data.nav_points then --navpoints - for nav_ind, nav_data in pairs(coa_data.nav_points) do - - if type(nav_data) == 'table' then - self.Navpoints[CoalitionName][nav_ind] = routines.utils.deepCopy(nav_data) - - self.Navpoints[CoalitionName][nav_ind]['name'] = nav_data.callsignStr -- name is a little bit more self-explanatory. - self.Navpoints[CoalitionName][nav_ind]['point'] = {} -- point is used by SSE, support it. - self.Navpoints[CoalitionName][nav_ind]['point']['x'] = nav_data.x - self.Navpoints[CoalitionName][nav_ind]['point']['y'] = 0 - self.Navpoints[CoalitionName][nav_ind]['point']['z'] = nav_data.y - end - end - end - ------------------------------------------------- - if coa_data.country then --there is a country table - for cntry_id, cntry_data in pairs(coa_data.country) do - - local CountryName = string.upper(cntry_data.name) - --self.Units[coa_name][countryName] = {} - --self.Units[coa_name][countryName]["countryId"] = cntry_data.id - - if type(cntry_data) == 'table' then --just making sure - - for obj_type_name, obj_type_data in pairs(cntry_data) do - - if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then --should be an unncessary check - - local CategoryName = obj_type_name - - if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! - - --self.Units[coa_name][countryName][category] = {} - - for group_num, GroupTemplate in pairs(obj_type_data.group) do - - if GroupTemplate and GroupTemplate.units and type(GroupTemplate.units) == 'table' then --making sure again- this is a valid group - self:_RegisterTemplate( - GroupTemplate, - coalition.side[string.upper(CoalitionName)], - _DATABASECategory[string.lower(CategoryName)], - country.id[string.upper(CountryName)] - ) - end --if GroupTemplate and GroupTemplate.units then - end --for group_num, GroupTemplate in pairs(obj_type_data.group) do - end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then - end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then - end --for obj_type_name, obj_type_data in pairs(cntry_data) do - end --if type(cntry_data) == 'table' then - end --for cntry_id, cntry_data in pairs(coa_data.country) do - end --if coa_data.country then --there is a country table - end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then - end --for coa_name, coa_data in pairs(mission.coalition) do - - return self -end - - - - ---- This module contains the SET classes. --- --- === --- --- 1) @{Set#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). --- --- === --- --- 2) @{Set#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. --- --- 2.1) SET_GROUP construction method: --- ----------------------------------- --- Create a new SET_GROUP object with the @{#SET_GROUP.New} method: --- --- * @{#SET_GROUP.New}: Creates a new SET_GROUP object. --- --- 2.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. --- --- 2.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). --- --- 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}. --- --- 2.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. --- --- ==== --- --- 3) @{Set#SET_UNIT} class, extends @{Set#SET_BASE} --- =================================================== --- Mission designers can use the @{Set#SET_UNIT} class to build sets of units belonging to certain: --- --- * Coalitions --- * Categories --- * Countries --- * Unit types --- * Starting with certain prefix strings. --- --- 3.1) SET_UNIT construction method: --- ---------------------------------- --- 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: --- ----------------------------------------- --- UNITs can be added and removed using the @{Set#SET_UNIT.AddUnitsByName} and @{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: --- ------------------------------ --- You can set filter criteria to define the set of units within the SET_UNIT. --- Filter criteria are defined by: --- --- * @{#SET_UNIT.FilterCoalitions}: Builds the SET_UNIT with the units belonging to the coalition(s). --- * @{#SET_UNIT.FilterCategories}: Builds the SET_UNIT with the units belonging to the category(ies). --- * @{#SET_UNIT.FilterTypes}: Builds the SET_UNIT with the units belonging to the unit type(s). --- * @{#SET_UNIT.FilterCountries}: Builds the SET_UNIT with the units belonging to the country(ies). --- * @{#SET_UNIT.FilterPrefixes}: Builds the SET_UNIT with the units starting with the same prefix string(s). --- --- 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. --- --- 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}. --- --- 3.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. --- --- Planned iterators methods in development are (so these are not yet available): --- --- * @{#SET_UNIT.ForEachUnitInUnit}: Calls a function for each unit contained within the SET_UNIT. --- * @{#SET_UNIT.ForEachUnitCompletelyInZone}: Iterate and call an iterator function for each **alive** UNIT presence completely in a @{Zone}, providing the UNIT and optional parameters to the called function. --- * @{#SET_UNIT.ForEachUnitNotInZone}: Iterate and call an iterator function for each **alive** UNIT presence not in a @{Zone}, providing the UNIT and optional parameters to the called function. --- --- === --- --- 4) @{Set#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 construction method: --- ---------------------------------- --- 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. --- --- ==== --- --- 5) @{Set#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 construction --- ----------------------------- --- 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. --- --- ==== --- --- @module Set --- @author FlightControl - - ---- SET_BASE class --- @type SET_BASE --- @extends Base#BASE -SET_BASE = { - ClassName = "SET_BASE", - Set = {}, -} - ---- 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() ) - - self.Database = Database - - self.YieldInterval = 10 - self.TimeInterval = 0.001 - - return self -end - ---- Finds an @{Base#BASE} object based on the object Name. --- @param #SET_BASE self --- @param #string ObjectName --- @return 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 - ---- Adds a @{Base#BASE} object in the @{Set#SET_BASE}, using the Object Name as the index. --- @param #SET_BASE self --- @param #string ObjectName --- @param Base#BASE Object --- @return Base#BASE The added BASE Object. -function SET_BASE:Add( ObjectName, Object ) - - self.Set[ObjectName] = Object -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 ) - - self.Set[ObjectName] = nil -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 - - - ---- 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 - - _EVENTDISPATCHER:OnBirth( self._EventOnBirth, self ) - _EVENTDISPATCHER:OnDead( self._EventOnDeadOrCrash, self ) - _EVENTDISPATCHER:OnCrash( self._EventOnDeadOrCrash, self ) - - -- Follow alive players and clients --- _EVENTDISPATCHER:OnPlayerEnterUnit( self._EventOnPlayerEnterUnit, self ) --- _EVENTDISPATCHER:OnPlayerLeaveUnit( self._EventOnPlayerLeaveUnit, self ) - - - return self -end - ---- Iterate the SET_BASE while identifying the nearest object from a @{Point#POINT_VEC2}. --- @param #SET_BASE self --- @param Point#POINT_VEC2 PointVec2 A @{Point#POINT_VEC2} object from where to evaluate the closest object in the set. --- @return 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:GetPointVec2() ) - else - local Distance = PointVec2:DistanceFromVec2( ObjectData:GetPointVec2() ) - 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 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 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 Event#EVENTDATA Event -function SET_BASE:_EventOnDeadOrCrash( Event ) - self:F2( { Event } ) - - if Event.IniDCSUnit then - local ObjectName, Object = self:FindInDatabase( Event ) - if ObjectName and Object 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 Event#EVENTDATA Event ---function SET_BASE:_EventOnPlayerEnterUnit( Event ) --- self:F3( { Event } ) --- --- if Event.IniDCSUnit then --- if self:IsIncludeObject( Event.IniDCSUnit ) then --- if not self.PlayersAlive[Event.IniDCSUnitName] then --- self:E( { "Add player for unit:", Event.IniDCSUnitName, Event.IniDCSUnit:getPlayerName() } ) --- self.PlayersAlive[Event.IniDCSUnitName] = Event.IniDCSUnit:getPlayerName() --- self.ClientsAlive[Event.IniDCSUnitName] = _DATABASE.Clients[ Event.IniDCSUnitName ] --- end --- end --- end ---end --- ------ Handles the OnPlayerLeaveUnit event to clean the active players table. ----- @param #SET_BASE self ----- @param Event#EVENTDATA Event ---function SET_BASE:_EventOnPlayerLeaveUnit( Event ) --- self:F3( { Event } ) --- --- if Event.IniDCSUnit then --- if self:IsIncludeObject( Event.IniDCSUnit ) then --- if self.PlayersAlive[Event.IniDCSUnitName] then --- self:E( { "Cleaning player for unit:", Event.IniDCSUnitName, Event.IniDCSUnit:getPlayerName() } ) --- self.PlayersAlive[Event.IniDCSUnitName] = nil --- self.ClientsAlive[Event.IniDCSUnitName] = nil --- 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 ) - - local function CoRoutine() - local Count = 0 - for ObjectID, Object in pairs( Set ) do - self:T2( 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 - 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 - - local Scheduler = SCHEDULER:New( self, Schedule, {}, self.TimeInterval, self.TimeInterval, 0 ) - - 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 - ---- Flushes the current SET_BASE contents in the log ... (for debug reasons). --- @param #SET_BASE self --- @return #string A string with the names of the objects. -function SET_BASE:Flush() - self:F3() - - local ObjectNames = "" - for ObjectName, Object in pairs( self.Set ) do - ObjectNames = ObjectNames .. ObjectName .. ", " - end - self:T( { "Objects in Set:", ObjectNames } ) - - return ObjectNames -end - --- SET_GROUP - ---- SET_GROUP class --- @type SET_GROUP --- @extends Set#SET_BASE -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_UNIT, - 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 - ---- Add GROUP(s) to SET_GROUP. --- @param 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 Set#SET_GROUP self --- @param 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 ) - end - - return self -end - - - - ---- Finds a Group based on the Group Name. --- @param #SET_GROUP self --- @param #string GroupName --- @return Group#GROUP The found Group. -function SET_GROUP:FindGroup( GroupName ) - - local GroupFound = self.Set[GroupName] - return GroupFound -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 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 - - 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_GROUP self --- @param Event#EVENTDATA Event --- @return #string The name of the GROUP --- @return #table The GROUP -function SET_GROUP:AddInDatabase( Event ) - self:F3( { Event } ) - - if not self.Database[Event.IniDCSGroupName] then - self.Database[Event.IniDCSGroupName] = GROUP:Register( Event.IniDCSGroupName ) - self:T3( self.Database[Event.IniDCSGroupName] ) - 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 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 **alive** GROUP, 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.Set ) - - 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 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.Set, - --- @param Zone#ZONE_BASE ZoneObject - -- @param 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 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.Set, - --- @param Zone#ZONE_BASE ZoneObject - -- @param 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 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.Set, - --- @param Zone#ZONE_BASE ZoneObject - -- @param 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 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 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 - end - end - MooseGroupInclude = MooseGroupInclude and MooseGroupCoalition - 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 - end - end - MooseGroupInclude = MooseGroupInclude and MooseGroupCategory - 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 - 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, 1 ) then - MooseGroupPrefix = true - end - end - MooseGroupInclude = MooseGroupInclude and MooseGroupPrefix - end - - self:T2( MooseGroupInclude ) - return MooseGroupInclude -end - ---- SET_UNIT class --- @type SET_UNIT --- @extends Set#SET_BASE -SET_UNIT = { - ClassName = "SET_UNIT", - Units = {}, - Filter = { - Coalitions = nil, - Categories = nil, - Types = nil, - Countries = nil, - UnitPrefixes = nil, - }, - FilterMeta = { - Coalitions = { - red = coalition.side.RED, - blue = coalition.side.BLUE, - neutral = coalition.side.NEUTRAL, - }, - Categories = { - plane = Unit.Category.AIRPLANE, - helicopter = Unit.Category.HELICOPTER, - ground = Unit.Category.GROUND_UNIT, - ship = Unit.Category.SHIP, - structure = Unit.Category.STRUCTURE, - }, - }, -} - - ---- Creates a new SET_UNIT object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. --- @param #SET_UNIT self --- @return #SET_UNIT --- @usage --- -- Define a new SET_UNIT Object. This DBObject will contain a reference to all alive Units. --- DBObject = SET_UNIT:New() -function SET_UNIT:New() - - -- Inherits from BASE - local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.UNITS ) ) - - _EVENTDISPATCHER:OnBirth( self._EventOnBirth, self ) - _EVENTDISPATCHER:OnDead( self._EventOnDeadOrCrash, self ) - _EVENTDISPATCHER:OnCrash( self._EventOnDeadOrCrash, self ) - - return self -end - ---- Add UNIT(s) to SET_UNIT. --- @param #SET_UNIT self --- @param #string AddUnit A single UNIT. --- @return #SET_UNIT self -function SET_UNIT:AddUnit( AddUnit ) - self:F2( AddUnit:GetName() ) - - self:Add( AddUnit:GetName(), AddUnit ) - - return self -end - - ---- Add UNIT(s) to SET_UNIT. --- @param #SET_UNIT self --- @param #string AddUnitNames A single name or an array of UNIT names. --- @return #SET_UNIT self -function SET_UNIT:AddUnitsByName( AddUnitNames ) - - local AddUnitNamesArray = ( type( AddUnitNames ) == "table" ) and AddUnitNames or { AddUnitNames } - - self:T( AddUnitNamesArray ) - for AddUnitID, AddUnitName in pairs( AddUnitNamesArray ) do - self:Add( AddUnitName, UNIT:FindByName( AddUnitName ) ) - end - - return self -end - ---- Remove UNIT(s) from SET_UNIT. --- @param Set#SET_UNIT self --- @param Unit#UNIT RemoveUnitNames A single name or an array of UNIT names. --- @return self -function SET_UNIT:RemoveUnitsByName( RemoveUnitNames ) - - local RemoveUnitNamesArray = ( type( RemoveUnitNames ) == "table" ) and RemoveUnitNames or { RemoveUnitNames } - - for RemoveUnitID, RemoveUnitName in pairs( RemoveUnitNamesArray ) do - self:Remove( RemoveUnitName.UnitName ) - end - - return self -end - - ---- Finds a Unit based on the Unit Name. --- @param #SET_UNIT self --- @param #string UnitName --- @return Unit#UNIT The found Unit. -function SET_UNIT:FindUnit( UnitName ) - - local UnitFound = self.Set[UnitName] - return UnitFound -end - - - ---- Builds a set of units of coalitions. --- Possible current coalitions are red, blue and neutral. --- @param #SET_UNIT self --- @param #string Coalitions Can take the following values: "red", "blue", "neutral". --- @return #SET_UNIT self -function SET_UNIT:FilterCoalitions( Coalitions ) - if not self.Filter.Coalitions then - self.Filter.Coalitions = {} - end - if type( Coalitions ) ~= "table" then - Coalitions = { Coalitions } - end - for CoalitionID, Coalition in pairs( Coalitions ) do - self.Filter.Coalitions[Coalition] = Coalition - end - return self -end - - ---- Builds a set of units out of categories. --- Possible current categories are plane, helicopter, ground, ship. --- @param #SET_UNIT self --- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship". --- @return #SET_UNIT self -function SET_UNIT:FilterCategories( Categories ) - if not self.Filter.Categories then - self.Filter.Categories = {} - end - if type( Categories ) ~= "table" then - Categories = { Categories } - end - for CategoryID, Category in pairs( Categories ) do - self.Filter.Categories[Category] = Category - end - return self -end - - ---- Builds a set of units of defined unit types. --- Possible current types are those types known within DCS world. --- @param #SET_UNIT self --- @param #string Types Can take those type strings known within DCS world. --- @return #SET_UNIT self -function SET_UNIT:FilterTypes( Types ) - if not self.Filter.Types then - self.Filter.Types = {} - end - if type( Types ) ~= "table" then - Types = { Types } - end - for TypeID, Type in pairs( Types ) do - self.Filter.Types[Type] = Type - end - return self -end - - ---- Builds a set of units of defined countries. --- Possible current countries are those known within DCS world. --- @param #SET_UNIT self --- @param #string Countries Can take those country strings known within DCS world. --- @return #SET_UNIT self -function SET_UNIT:FilterCountries( Countries ) - if not self.Filter.Countries then - self.Filter.Countries = {} - end - if type( Countries ) ~= "table" then - Countries = { Countries } - end - for CountryID, Country in pairs( Countries ) do - self.Filter.Countries[Country] = Country - end - return self -end - - ---- Builds a set of units of defined unit prefixes. --- All the units starting with the given prefixes will be included within the set. --- @param #SET_UNIT self --- @param #string Prefixes The prefix of which the unit name starts with. --- @return #SET_UNIT self -function SET_UNIT:FilterPrefixes( Prefixes ) - if not self.Filter.UnitPrefixes then - self.Filter.UnitPrefixes = {} - end - if type( Prefixes ) ~= "table" then - Prefixes = { Prefixes } - end - for PrefixID, Prefix in pairs( Prefixes ) do - self.Filter.UnitPrefixes[Prefix] = Prefix - end - return self -end - - - - ---- Starts the filtering. --- @param #SET_UNIT self --- @return #SET_UNIT self -function SET_UNIT:FilterStart() - - if _DATABASE then - self:_FilterStart() - end - - return self -end - ---- Handles the Database to check on an event (birth) that the Object was added in the Database. --- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! --- @param #SET_UNIT self --- @param Event#EVENTDATA Event --- @return #string The name of the UNIT --- @return #table The UNIT -function SET_UNIT:AddInDatabase( Event ) - self:F3( { Event } ) - - if not self.Database[Event.IniDCSUnitName] then - self.Database[Event.IniDCSUnitName] = UNIT:Register( Event.IniDCSUnitName ) - self:T3( self.Database[Event.IniDCSUnitName] ) - end - - return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] -end - ---- Handles the Database to check on any event that Object exists in the Database. --- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! --- @param #SET_UNIT self --- @param Event#EVENTDATA Event --- @return #string The name of the UNIT --- @return #table The UNIT -function SET_UNIT:FindInDatabase( Event ) - self:F3( { Event } ) - - self:E( { Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] } ) - return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] -end - ---- Iterate the SET_UNIT and call an interator function for each **alive** UNIT, providing the UNIT and optional parameters. --- @param #SET_UNIT self --- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the SET_UNIT. The function needs to accept a UNIT parameter. --- @return #SET_UNIT self -function SET_UNIT:ForEachUnit( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set ) - - return self -end - ---- Iterate the SET_UNIT and call an iterator function for each **alive** UNIT presence completely in a @{Zone}, providing the UNIT and optional parameters to the called function. --- @param #SET_UNIT self --- @param Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the SET_UNIT. The function needs to accept a UNIT parameter. --- @return #SET_UNIT self -function SET_UNIT:ForEachUnitCompletelyInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set, - --- @param Zone#ZONE_BASE ZoneObject - -- @param Unit#UNIT UnitObject - function( ZoneObject, UnitObject ) - if UnitObject:IsCompletelyInZone( ZoneObject ) then - return true - else - return false - end - end, { ZoneObject } ) - - return self -end - ---- Iterate the SET_UNIT and call an iterator function for each **alive** UNIT presence not in a @{Zone}, providing the UNIT and optional parameters to the called function. --- @param #SET_UNIT self --- @param Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the SET_UNIT. The function needs to accept a UNIT parameter. --- @return #SET_UNIT self -function SET_UNIT:ForEachUnitNotInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set, - --- @param Zone#ZONE_BASE ZoneObject - -- @param Unit#UNIT UnitObject - function( ZoneObject, UnitObject ) - if UnitObject:IsNotInZone( ZoneObject ) then - return true - else - return false - end - end, { ZoneObject } ) - - return self -end - - - ------ Iterate the SET_UNIT and call an interator function for each **alive** player, providing the Unit of the player and optional parameters. ----- @param #SET_UNIT self ----- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_UNIT. The function needs to accept a UNIT parameter. ----- @return #SET_UNIT self ---function SET_UNIT:ForEachPlayer( IteratorFunction, ... ) --- self:F2( arg ) --- --- self:ForEach( IteratorFunction, arg, self.PlayersAlive ) --- --- return self ---end --- --- ------ Iterate the SET_UNIT and call an interator function for each client, providing the Client to the function and optional parameters. ----- @param #SET_UNIT self ----- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_UNIT. The function needs to accept a CLIENT parameter. ----- @return #SET_UNIT self ---function SET_UNIT:ForEachClient( IteratorFunction, ... ) --- self:F2( arg ) --- --- self:ForEach( IteratorFunction, arg, self.Clients ) --- --- return self ---end - - ---- --- @param #SET_UNIT self --- @param Unit#UNIT MUnit --- @return #SET_UNIT self -function SET_UNIT:IsIncludeObject( MUnit ) - self:F2( MUnit ) - local MUnitInclude = true - - if self.Filter.Coalitions then - local MUnitCoalition = false - for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do - self:T3( { "Coalition:", MUnit:GetCoalition(), self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) - if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == MUnit:GetCoalition() then - MUnitCoalition = true - end - end - MUnitInclude = MUnitInclude and MUnitCoalition - end - - if self.Filter.Categories then - local MUnitCategory = false - for CategoryID, CategoryName in pairs( self.Filter.Categories ) do - self:T3( { "Category:", MUnit:GetDesc().category, self.FilterMeta.Categories[CategoryName], CategoryName } ) - if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == MUnit:GetDesc().category then - MUnitCategory = true - end - end - MUnitInclude = MUnitInclude and MUnitCategory - end - - if self.Filter.Types then - local MUnitType = false - for TypeID, TypeName in pairs( self.Filter.Types ) do - self:T3( { "Type:", MUnit:GetTypeName(), TypeName } ) - if TypeName == MUnit:GetTypeName() then - MUnitType = true - end - end - MUnitInclude = MUnitInclude and MUnitType - end - - if self.Filter.Countries then - local MUnitCountry = false - for CountryID, CountryName in pairs( self.Filter.Countries ) do - self:T3( { "Country:", MUnit:GetCountry(), CountryName } ) - if country.id[CountryName] == MUnit:GetCountry() then - MUnitCountry = true - end - end - MUnitInclude = MUnitInclude and MUnitCountry - end - - if self.Filter.UnitPrefixes then - local MUnitPrefix = false - for UnitPrefixId, UnitPrefix in pairs( self.Filter.UnitPrefixes ) do - self:T3( { "Prefix:", string.find( MUnit:GetName(), UnitPrefix, 1 ), UnitPrefix } ) - if string.find( MUnit:GetName(), UnitPrefix, 1 ) then - MUnitPrefix = true - end - end - MUnitInclude = MUnitInclude and MUnitPrefix - end - - self:T2( MUnitInclude ) - return MUnitInclude -end - - ---- SET_CLIENT - ---- SET_CLIENT class --- @type SET_CLIENT --- @extends Set#SET_BASE -SET_CLIENT = { - ClassName = "SET_CLIENT", - Clients = {}, - Filter = { - Coalitions = nil, - Categories = nil, - Types = nil, - Countries = nil, - ClientPrefixes = nil, - }, - FilterMeta = { - Coalitions = { - red = coalition.side.RED, - blue = coalition.side.BLUE, - neutral = coalition.side.NEUTRAL, - }, - Categories = { - plane = Unit.Category.AIRPLANE, - helicopter = Unit.Category.HELICOPTER, - ground = Unit.Category.GROUND_UNIT, - ship = Unit.Category.SHIP, - structure = Unit.Category.STRUCTURE, - }, - }, -} - - ---- Creates a new SET_CLIENT object, building a set of clients belonging to a coalitions, categories, countries, types or with defined prefix names. --- @param #SET_CLIENT self --- @return #SET_CLIENT --- @usage --- -- Define a new SET_CLIENT Object. This DBObject will contain a reference to all Clients. --- DBObject = SET_CLIENT:New() -function SET_CLIENT:New() - -- Inherits from BASE - local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.CLIENTS ) ) - - return self -end - ---- Add CLIENT(s) to SET_CLIENT. --- @param Set#SET_CLIENT self --- @param #string AddClientNames A single name or an array of CLIENT names. --- @return self -function SET_CLIENT:AddClientsByName( AddClientNames ) - - local AddClientNamesArray = ( type( AddClientNames ) == "table" ) and AddClientNames or { AddClientNames } - - for AddClientID, AddClientName in pairs( AddClientNamesArray ) do - self:Add( AddClientName, CLIENT:FindByName( AddClientName ) ) - end - - return self -end - ---- Remove CLIENT(s) from SET_CLIENT. --- @param Set#SET_CLIENT self --- @param Client#CLIENT RemoveClientNames A single name or an array of CLIENT names. --- @return self -function SET_CLIENT:RemoveClientsByName( RemoveClientNames ) - - local RemoveClientNamesArray = ( type( RemoveClientNames ) == "table" ) and RemoveClientNames or { RemoveClientNames } - - for RemoveClientID, RemoveClientName in pairs( RemoveClientNamesArray ) do - self:Remove( RemoveClientName.ClientName ) - end - - return self -end - - ---- Finds a Client based on the Client Name. --- @param #SET_CLIENT self --- @param #string ClientName --- @return 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() - 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 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 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.Set ) - - 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 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.Set, - --- @param Zone#ZONE_BASE ZoneObject - -- @param 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 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.Set, - --- @param Zone#ZONE_BASE ZoneObject - -- @param 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 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 -end - ---- SET_AIRBASE - ---- SET_AIRBASE class --- @type SET_AIRBASE --- @extends Set#SET_BASE -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 AIRBASEs to SET_AIRBASE. --- @param 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 Set#SET_AIRBASE self --- @param 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 ) - end - - return self -end - - ---- Finds a Airbase based on the Airbase Name. --- @param #SET_AIRBASE self --- @param #string AirbaseName --- @return 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() - 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 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 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.Set ) - - return self -end - ---- Iterate the SET_AIRBASE while identifying the nearest @{Airbase#AIRBASE} from a @{Point#POINT_VEC2}. --- @param #SET_AIRBASE self --- @param Point#POINT_VEC2 PointVec2 A @{Point#POINT_VEC2} object from where to evaluate the closest @{Airbase#AIRBASE}. --- @return Airbase#AIRBASE The closest @{Airbase#AIRBASE}. -function SET_AIRBASE:FindNearestAirbaseFromPointVec2( PointVec2 ) - self:F2( PointVec2 ) - - local NearestAirbase = self:FindNearestObjectFromPointVec2( PointVec2 ) - return NearestAirbase -end - - - ---- --- @param #SET_AIRBASE self --- @param 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 ---- This module contains the POINT classes. --- --- 1) @{Point#POINT_VEC3} class, extends @{Base#BASE} --- =============================================== --- The @{Point#POINT_VEC3} class defines a 3D point in the simulator. --- --- 1.1) POINT_VEC3 constructor --- --------------------------- --- --- A new POINT instance can be created with: --- --- * @{#POINT_VEC3.New}(): a 3D point. --- --- 2) @{Point#POINT_VEC2} class, extends @{Point#POINT_VEC3} --- ========================================================= --- 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. --- --- 2.1) POINT_VEC2 constructor --- --------------------------- --- --- A new POINT instance can be created with: --- --- * @{#POINT_VEC2.New}(): a 2D point. --- --- @module Point --- @author FlightControl - ---- The POINT_VEC3 class --- @type POINT_VEC3 --- @extends Base#BASE --- @field #POINT_VEC3.SmokeColor SmokeColor --- @field #POINT_VEC3.FlareColor FlareColor --- @field #POINT_VEC3.RoutePointAltType RoutePointAltType --- @field #POINT_VEC3.RoutePointType RoutePointType --- @field #POINT_VEC3.RoutePointAction RoutePointAction -POINT_VEC3 = { - ClassName = "POINT_VEC3", - SmokeColor = { - Green = trigger.smokeColor.Green, - Red = trigger.smokeColor.Red, - White = trigger.smokeColor.White, - Orange = trigger.smokeColor.Orange, - Blue = trigger.smokeColor.Blue - }, - FlareColor = { - Green = trigger.flareColor.Green, - Red = trigger.flareColor.Red, - White = trigger.flareColor.White, - Yellow = trigger.flareColor.Yellow - }, - RoutePointAltType = { - BARO = "BARO", - }, - RoutePointType = { - TurningPoint = "Turning Point", - }, - RoutePointAction = { - TurningPoint = "Turning Point", - }, -} - - ---- SmokeColor --- @type POINT_VEC3.SmokeColor --- @field Green --- @field Red --- @field White --- @field Orange --- @field Blue - - - ---- FlareColor --- @type POINT_VEC3.FlareColor --- @field Green --- @field Red --- @field White --- @field Yellow - - - ---- RoutePoint AltTypes --- @type POINT_VEC3.RoutePointAltType --- @field BARO "BARO" - - - ---- RoutePoint Types --- @type POINT_VEC3.RoutePointType --- @field TurningPoint "Turning Point" - - - ---- RoutePoint Actions --- @type POINT_VEC3.RoutePointAction --- @field TurningPoint "Turning Point" - - - --- Constructor. - ---- Create a new POINT_VEC3 object. --- @param #POINT_VEC3 self --- @param DCSTypes#Distance x The x coordinate of the Vec3 point, pointing to the North. --- @param DCSTypes#Distance y The y coordinate of the Vec3 point, pointing Upwards. --- @param DCSTypes#Distance z The z coordinate of the Vec3 point, pointing to the Right. --- @return Point#POINT_VEC3 self -function POINT_VEC3:New( x, y, z ) - - local self = BASE:Inherit( self, BASE:New() ) - self.PointVec3 = { x = x, y = y, z = z } - self:F2( self.PointVec3 ) - return self -end - - ---- Build an air type route point. --- @param #POINT_VEC3 self --- @param #POINT_VEC3.RoutePointAltType AltType The altitude type. --- @param #POINT_VEC3.RoutePointType Type The route point type. --- @param #POINT_VEC3.RoutePointAction Action The route point action. --- @param DCSTypes#Speed Speed Airspeed in km/h. --- @param #boolean SpeedLocked true means the speed is locked. --- @return #table The route point. -function POINT_VEC3:RoutePointAir( AltType, Type, Action, Speed, SpeedLocked ) - self:F2( { AltType, Type, Action, Speed, SpeedLocked } ) - - local RoutePoint = {} - RoutePoint.x = self.PointVec3.x - RoutePoint.y = self.PointVec3.z - RoutePoint.alt = self.PointVec3.y - RoutePoint.alt_type = AltType - - RoutePoint.type = Type - RoutePoint.action = Action - - RoutePoint.speed = Speed / 3.6 - RoutePoint.speed_locked = true - --- ["task"] = --- { --- ["id"] = "ComboTask", --- ["params"] = --- { --- ["tasks"] = --- { --- }, -- end of ["tasks"] --- }, -- end of ["params"] --- }, -- end of ["task"] - - - RoutePoint.task = {} - RoutePoint.task.id = "ComboTask" - RoutePoint.task.params = {} - RoutePoint.task.params.tasks = {} - - - return RoutePoint -end - - ---- Smokes the point in a color. --- @param #POINT_VEC3 self --- @param Point#POINT_VEC3.SmokeColor SmokeColor -function POINT_VEC3:Smoke( SmokeColor ) - self:F2( { SmokeColor, self.PointVec3 } ) - trigger.action.smoke( self.PointVec3, SmokeColor ) -end - ---- Smoke the POINT_VEC3 Green. --- @param #POINT_VEC3 self -function POINT_VEC3:SmokeGreen() - self:F2() - self:Smoke( POINT_VEC3.SmokeColor.Green ) -end - ---- Smoke the POINT_VEC3 Red. --- @param #POINT_VEC3 self -function POINT_VEC3:SmokeRed() - self:F2() - self:Smoke( POINT_VEC3.SmokeColor.Red ) -end - ---- Smoke the POINT_VEC3 White. --- @param #POINT_VEC3 self -function POINT_VEC3:SmokeWhite() - self:F2() - self:Smoke( POINT_VEC3.SmokeColor.White ) -end - ---- Smoke the POINT_VEC3 Orange. --- @param #POINT_VEC3 self -function POINT_VEC3:SmokeOrange() - self:F2() - self:Smoke( POINT_VEC3.SmokeColor.Orange ) -end - ---- Smoke the POINT_VEC3 Blue. --- @param #POINT_VEC3 self -function POINT_VEC3:SmokeBlue() - self:F2() - self:Smoke( POINT_VEC3.SmokeColor.Blue ) -end - ---- Flares the point in a color. --- @param #POINT_VEC3 self --- @param Point#POINT_VEC3.FlareColor --- @param DCSTypes#Azimuth (optional) Azimuth The azimuth of the flare direction. The default azimuth is 0. -function POINT_VEC3:Flare( FlareColor, Azimuth ) - self:F2( { FlareColor, self.PointVec3 } ) - trigger.action.signalFlare( self.PointVec3, FlareColor, Azimuth and Azimuth or 0 ) -end - ---- Flare the POINT_VEC3 White. --- @param #POINT_VEC3 self --- @param DCSTypes#Azimuth (optional) Azimuth The azimuth of the flare direction. The default azimuth is 0. -function POINT_VEC3:FlareWhite( Azimuth ) - self:F2( Azimuth ) - self:Flare( POINT_VEC3.FlareColor.White, Azimuth ) -end - ---- Flare the POINT_VEC3 Yellow. --- @param #POINT_VEC3 self --- @param DCSTypes#Azimuth (optional) Azimuth The azimuth of the flare direction. The default azimuth is 0. -function POINT_VEC3:FlareYellow( Azimuth ) - self:F2( Azimuth ) - self:Flare( POINT_VEC3.FlareColor.Yellow, Azimuth ) -end - ---- Flare the POINT_VEC3 Green. --- @param #POINT_VEC3 self --- @param DCSTypes#Azimuth (optional) Azimuth The azimuth of the flare direction. The default azimuth is 0. -function POINT_VEC3:FlareGreen( Azimuth ) - self:F2( Azimuth ) - self:Flare( POINT_VEC3.FlareColor.Green, Azimuth ) -end - ---- Flare the POINT_VEC3 Red. --- @param #POINT_VEC3 self -function POINT_VEC3:FlareRed( Azimuth ) - self:F2( Azimuth ) - self:Flare( POINT_VEC3.FlareColor.Red, Azimuth ) -end - - ---- The POINT_VEC2 class --- @type POINT_VEC2 --- @field DCSTypes#Vec2 PointVec2 --- @extends Point#POINT_VEC3 -POINT_VEC2 = { - ClassName = "POINT_VEC2", - } - ---- Create a new POINT_VEC2 object. --- @param #POINT_VEC2 self --- @param DCSTypes#Distance x The x coordinate of the Vec3 point, pointing to the North. --- @param DCSTypes#Distance y The y coordinate of the Vec3 point, pointing to the Right. --- @param 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. --- @return Point#POINT_VEC2 -function POINT_VEC2:New( x, y, LandHeightAdd ) - - local LandHeight = land.getHeight( { ["x"] = x, ["y"] = y } ) - if LandHeightAdd then - LandHeight = LandHeight + LandHeightAdd - end - - local self = BASE:Inherit( self, POINT_VEC3:New( x, LandHeight, y ) ) - self:F2( { x, y, LandHeightAdd } ) - - self.PointVec2 = { x = x, y = y } - - return self -end - ---- Calculate the distance from a reference @{Point#POINT_VEC2}. --- @param #POINT_VEC2 self --- @param #POINT_VEC2 PointVec2Reference The reference @{Point#POINT_VEC2}. --- @return DCSTypes#Distance The distance from the reference @{Point#POINT_VEC2} in meters. -function POINT_VEC2:DistanceFromPointVec2( PointVec2Reference ) - self:F2( PointVec2Reference ) - - local Distance = ( ( PointVec2Reference.PointVec2.x - self.PointVec2.x ) ^ 2 + ( PointVec2Reference.PointVec2.y - self.PointVec2.y ) ^2 ) ^0.5 - - self:T2( Distance ) - return Distance -end - ---- Calculate the distance from a reference @{DCSTypes#Vec2}. --- @param #POINT_VEC2 self --- @param DCSTypes#Vec2 Vec2Reference The reference @{DCSTypes#Vec2}. --- @return DCSTypes#Distance The distance from the reference @{DCSTypes#Vec2} in meters. -function POINT_VEC2:DistanceFromVec2( Vec2Reference ) - self:F2( Vec2Reference ) - - local Distance = ( ( Vec2Reference.x - self.PointVec2.x ) ^ 2 + ( Vec2Reference.y - self.PointVec2.y ) ^2 ) ^0.5 - - self:T2( Distance ) - return Distance -end - - ---- The main include file for the MOOSE system. - -Include.File( "Routines" ) -Include.File( "Base" ) -Include.File( "Scheduler" ) -Include.File( "Event" ) -Include.File( "Menu" ) -Include.File( "Controllable" ) -Include.File( "Group" ) -Include.File( "Unit" ) -Include.File( "Zone" ) -Include.File( "Client" ) -Include.File( "Static" ) -Include.File( "Airbase" ) -Include.File( "Database" ) -Include.File( "Set" ) -Include.File( "Point" ) Include.File( "Moose" ) -Include.File( "Scoring" ) -Include.File( "Cargo" ) -Include.File( "Message" ) -Include.File( "Stage" ) -Include.File( "Task" ) -Include.File( "GoHomeTask" ) -Include.File( "DestroyBaseTask" ) -Include.File( "DestroyGroupsTask" ) -Include.File( "DestroyRadarsTask" ) -Include.File( "DestroyUnitTypesTask" ) -Include.File( "PickupTask" ) -Include.File( "DeployTask" ) -Include.File( "NoTask" ) -Include.File( "RouteTask" ) -Include.File( "Mission" ) -Include.File( "CleanUp" ) -Include.File( "Spawn" ) -Include.File( "Movement" ) -Include.File( "Sead" ) -Include.File( "Escort" ) -Include.File( "MissileTrainer" ) -Include.File( "PatrolZone" ) -Include.File( "AIBalancer" ) -Include.File( "AirbasePolice" ) -Include.File( "Detection" ) -Include.File( "FAC" ) --- The order of the declarations is important here. Don't touch it. - ---- Declare the event dispatcher based on the EVENT class -_EVENTDISPATCHER = EVENT:New() -- #EVENT - ---- Declare the main database object, which is used internally by the MOOSE classes. -_DATABASE = DATABASE:New() -- Database#DATABASE - ---- Scoring system for MOOSE. --- This scoring class calculates the hits and kills that players make within a simulation session. --- Scoring is calculated using a defined algorithm. --- With a small change in MissionScripting.lua, the scoring can also be logged in a CSV file, that can then be uploaded --- to a database or a BI tool to publish the scoring results to the player community. --- @module Scoring --- @author FlightControl - - ---- The Scoring class --- @type SCORING --- @field Players A collection of the current players that have joined the game. --- @extends Base#BASE -SCORING = { - ClassName = "SCORING", - ClassID = 0, - Players = {}, -} - -local _SCORINGCoalition = - { - [1] = "Red", - [2] = "Blue", - } - -local _SCORINGCategory = - { - [Unit.Category.AIRPLANE] = "Plane", - [Unit.Category.HELICOPTER] = "Helicopter", - [Unit.Category.GROUND_UNIT] = "Vehicle", - [Unit.Category.SHIP] = "Ship", - [Unit.Category.STRUCTURE] = "Structure", - } - ---- Creates a new SCORING object to administer the scoring achieved by players. --- @param #SCORING self --- @param #string GameName The name of the game. This name is also logged in the CSV score file. --- @return #SCORING self --- @usage --- -- Define a new scoring object for the mission Gori Valley. --- ScoringObject = SCORING:New( "Gori Valley" ) -function SCORING:New( GameName ) - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) - - if GameName then - self.GameName = GameName - else - error( "A game name must be given to register the scoring results" ) - end - - - _EVENTDISPATCHER:OnDead( self._EventOnDeadOrCrash, self ) - _EVENTDISPATCHER:OnCrash( self._EventOnDeadOrCrash, self ) - _EVENTDISPATCHER:OnHit( self._EventOnHit, self ) - - --self.SchedulerId = routines.scheduleFunction( SCORING._FollowPlayersScheduled, { self }, 0, 5 ) - self.SchedulerId = SCHEDULER:New( self, self._FollowPlayersScheduled, {}, 0, 5 ) - - self:ScoreMenu() - - return self - -end - ---- Creates a score radio menu. Can be accessed using Radio -> F10. --- @param #SCORING self --- @return #SCORING self -function SCORING:ScoreMenu() - self.Menu = SUBMENU:New( 'Scoring' ) - self.AllScoresMenu = COMMANDMENU:New( 'Score All Active Players', self.Menu, SCORING.ReportScoreAll, self ) - --- = COMMANDMENU:New('Your Current Score', ReportScore, SCORING.ReportScorePlayer, self ) - return self -end - ---- Follows new players entering Clients within the DCSRTE. --- TODO: Need to see if i can catch this also with an event. It will eliminate the schedule ... -function SCORING:_FollowPlayersScheduled() - self:F3( "_FollowPlayersScheduled" ) - - local ClientUnit = 0 - local CoalitionsData = { AlivePlayersRed = coalition.getPlayers(coalition.side.RED), AlivePlayersBlue = coalition.getPlayers(coalition.side.BLUE) } - local unitId - local unitData - local AlivePlayerUnits = {} - - for CoalitionId, CoalitionData in pairs( CoalitionsData ) do - self:T3( { "_FollowPlayersScheduled", CoalitionData } ) - for UnitId, UnitData in pairs( CoalitionData ) do - self:_AddPlayerFromUnit( UnitData ) - end - end - - return true -end - - ---- Track DEAD or CRASH events for the scoring. --- @param #SCORING self --- @param Event#EVENTDATA Event -function SCORING:_EventOnDeadOrCrash( Event ) - self:F( { Event } ) - - local TargetUnit = nil - local TargetGroup = nil - local TargetUnitName = "" - local TargetGroupName = "" - local TargetPlayerName = "" - local TargetCoalition = nil - local TargetCategory = nil - local TargetType = nil - local TargetUnitCoalition = nil - local TargetUnitCategory = nil - local TargetUnitType = nil - - if Event.IniDCSUnit then - - TargetUnit = Event.IniDCSUnit - TargetUnitName = Event.IniDCSUnitName - TargetGroup = Event.IniDCSGroup - TargetGroupName = Event.IniDCSGroupName - TargetPlayerName = TargetUnit:getPlayerName() - - TargetCoalition = TargetUnit:getCoalition() - --TargetCategory = TargetUnit:getCategory() - TargetCategory = TargetUnit:getDesc().category -- Workaround - TargetType = TargetUnit:getTypeName() - - TargetUnitCoalition = _SCORINGCoalition[TargetCoalition] - TargetUnitCategory = _SCORINGCategory[TargetCategory] - TargetUnitType = TargetType - - self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType } ) - end - - for PlayerName, PlayerData in pairs( self.Players ) do - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Something got killed" ) - - -- Some variables - local InitUnitName = PlayerData.UnitName - local InitUnitType = PlayerData.UnitType - local InitCoalition = PlayerData.UnitCoalition - local InitCategory = PlayerData.UnitCategory - local InitUnitCoalition = _SCORINGCoalition[InitCoalition] - local InitUnitCategory = _SCORINGCategory[InitCategory] - - self:T( { InitUnitName, InitUnitType, InitUnitCoalition, InitCoalition, InitUnitCategory, InitCategory } ) - - -- What is he hitting? - if TargetCategory then - if PlayerData and PlayerData.Hit and PlayerData.Hit[TargetCategory] and PlayerData.Hit[TargetCategory][TargetUnitName] then -- Was there a hit for this unit for this player before registered??? - if not PlayerData.Kill[TargetCategory] then - PlayerData.Kill[TargetCategory] = {} - end - if not PlayerData.Kill[TargetCategory][TargetType] then - PlayerData.Kill[TargetCategory][TargetType] = {} - PlayerData.Kill[TargetCategory][TargetType].Score = 0 - PlayerData.Kill[TargetCategory][TargetType].ScoreKill = 0 - PlayerData.Kill[TargetCategory][TargetType].Penalty = 0 - PlayerData.Kill[TargetCategory][TargetType].PenaltyKill = 0 - end - - if InitCoalition == TargetCoalition then - PlayerData.Penalty = PlayerData.Penalty + 25 - PlayerData.Kill[TargetCategory][TargetType].Penalty = PlayerData.Kill[TargetCategory][TargetType].Penalty + 25 - PlayerData.Kill[TargetCategory][TargetType].PenaltyKill = PlayerData.Kill[TargetCategory][TargetType].PenaltyKill + 1 - MESSAGE:New( "Player '" .. PlayerName .. "' killed a friendly " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. - PlayerData.Kill[TargetCategory][TargetType].PenaltyKill .. " times. Penalty: -" .. PlayerData.Kill[TargetCategory][TargetType].Penalty .. - ". Score Total:" .. PlayerData.Score - PlayerData.Penalty, - 5 ):ToAll() - self:ScoreCSV( PlayerName, "KILL_PENALTY", 1, -125, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - else - PlayerData.Score = PlayerData.Score + 10 - PlayerData.Kill[TargetCategory][TargetType].Score = PlayerData.Kill[TargetCategory][TargetType].Score + 10 - PlayerData.Kill[TargetCategory][TargetType].ScoreKill = PlayerData.Kill[TargetCategory][TargetType].ScoreKill + 1 - MESSAGE:New( "Player '" .. PlayerName .. "' killed an enemy " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. - PlayerData.Kill[TargetCategory][TargetType].ScoreKill .. " times. Score: " .. PlayerData.Kill[TargetCategory][TargetType].Score .. - ". Score Total:" .. PlayerData.Score - PlayerData.Penalty, - 5 ):ToAll() - self:ScoreCSV( PlayerName, "KILL_SCORE", 1, 10, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - end - end - end - end - end -end - - - ---- Add a new player entering a Unit. -function SCORING:_AddPlayerFromUnit( UnitData ) - self:F( UnitData ) - - if UnitData and UnitData:isExist() then - local UnitName = UnitData:getName() - local PlayerName = UnitData:getPlayerName() - local UnitDesc = UnitData:getDesc() - local UnitCategory = UnitDesc.category - local UnitCoalition = UnitData:getCoalition() - local UnitTypeName = UnitData:getTypeName() - - self:T( { PlayerName, UnitName, UnitCategory, UnitCoalition, UnitTypeName } ) - - if self.Players[PlayerName] == nil then -- I believe this is the place where a Player gets a life in a mission when he enters a unit ... - self.Players[PlayerName] = {} - self.Players[PlayerName].Hit = {} - self.Players[PlayerName].Kill = {} - self.Players[PlayerName].Mission = {} - - -- for CategoryID, CategoryName in pairs( SCORINGCategory ) do - -- self.Players[PlayerName].Hit[CategoryID] = {} - -- self.Players[PlayerName].Kill[CategoryID] = {} - -- end - self.Players[PlayerName].HitPlayers = {} - self.Players[PlayerName].HitUnits = {} - self.Players[PlayerName].Score = 0 - self.Players[PlayerName].Penalty = 0 - self.Players[PlayerName].PenaltyCoalition = 0 - self.Players[PlayerName].PenaltyWarning = 0 - end - - if not self.Players[PlayerName].UnitCoalition then - self.Players[PlayerName].UnitCoalition = UnitCoalition - else - if self.Players[PlayerName].UnitCoalition ~= UnitCoalition then - self.Players[PlayerName].Penalty = self.Players[PlayerName].Penalty + 50 - self.Players[PlayerName].PenaltyCoalition = self.Players[PlayerName].PenaltyCoalition + 1 - MESSAGE:New( "Player '" .. PlayerName .. "' changed coalition from " .. _SCORINGCoalition[self.Players[PlayerName].UnitCoalition] .. " to " .. _SCORINGCoalition[UnitCoalition] .. - "(changed " .. self.Players[PlayerName].PenaltyCoalition .. " times the coalition). 50 Penalty points added.", - 2 - ):ToAll() - self:ScoreCSV( PlayerName, "COALITION_PENALTY", 1, -50, self.Players[PlayerName].UnitName, _SCORINGCoalition[self.Players[PlayerName].UnitCoalition], _SCORINGCategory[self.Players[PlayerName].UnitCategory], self.Players[PlayerName].UnitType, - UnitName, _SCORINGCoalition[UnitCoalition], _SCORINGCategory[UnitCategory], UnitData:getTypeName() ) - end - end - self.Players[PlayerName].UnitName = UnitName - self.Players[PlayerName].UnitCoalition = UnitCoalition - self.Players[PlayerName].UnitCategory = UnitCategory - self.Players[PlayerName].UnitType = UnitTypeName - - if self.Players[PlayerName].Penalty > 100 then - if self.Players[PlayerName].PenaltyWarning < 1 then - MESSAGE:New( "Player '" .. PlayerName .. "': WARNING! If you continue to commit FRATRICIDE and have a PENALTY score higher than 150, you will be COURT MARTIALED and DISMISSED from this mission! \nYour total penalty is: " .. self.Players[PlayerName].Penalty, - 30 - ):ToAll() - self.Players[PlayerName].PenaltyWarning = self.Players[PlayerName].PenaltyWarning + 1 - end - end - - if self.Players[PlayerName].Penalty > 150 then - ClientGroup = GROUP:NewFromDCSUnit( UnitData ) - ClientGroup:Destroy() - MESSAGE:New( "Player '" .. PlayerName .. "' committed FRATRICIDE, he will be COURT MARTIALED and is DISMISSED from this mission!", - 10 - ):ToAll() - end - - end -end - - ---- Registers Scores the players completing a Mission Task. -function SCORING:_AddMissionTaskScore( PlayerUnit, MissionName, Score ) - self:F( { PlayerUnit, MissionName, Score } ) - - local PlayerName = PlayerUnit:getPlayerName() - - if not self.Players[PlayerName].Mission[MissionName] then - self.Players[PlayerName].Mission[MissionName] = {} - self.Players[PlayerName].Mission[MissionName].ScoreTask = 0 - self.Players[PlayerName].Mission[MissionName].ScoreMission = 0 - end - - self:T( PlayerName ) - self:T( self.Players[PlayerName].Mission[MissionName] ) - - self.Players[PlayerName].Score = self.Players[PlayerName].Score + Score - self.Players[PlayerName].Mission[MissionName].ScoreTask = self.Players[PlayerName].Mission[MissionName].ScoreTask + Score - - MESSAGE:New( "Player '" .. PlayerName .. "' has finished another Task in Mission '" .. MissionName .. "'. " .. - Score .. " Score points added.", - 20 ):ToAll() - - self:ScoreCSV( PlayerName, "TASK_" .. MissionName:gsub( ' ', '_' ), 1, Score, PlayerUnit:getName() ) -end - - ---- Registers Mission Scores for possible multiple players that contributed in the Mission. -function SCORING:_AddMissionScore( MissionName, Score ) - self:F( { MissionName, Score } ) - - for PlayerName, PlayerData in pairs( self.Players ) do - - if PlayerData.Mission[MissionName] then - PlayerData.Score = PlayerData.Score + Score - PlayerData.Mission[MissionName].ScoreMission = PlayerData.Mission[MissionName].ScoreMission + Score - MESSAGE:New( "Player '" .. PlayerName .. "' has finished Mission '" .. MissionName .. "'. " .. - Score .. " Score points added.", - 20 ):ToAll() - self:ScoreCSV( PlayerName, "MISSION_" .. MissionName:gsub( ' ', '_' ), 1, Score ) - end - end -end - ---- Handles the OnHit event for the scoring. --- @param #SCORING self --- @param Event#EVENTDATA Event -function SCORING:_EventOnHit( Event ) - self:F( { Event } ) - - local InitUnit = nil - local InitUnitName = "" - local InitGroup = nil - local InitGroupName = "" - local InitPlayerName = nil - - local InitCoalition = nil - local InitCategory = nil - local InitType = nil - local InitUnitCoalition = nil - local InitUnitCategory = nil - local InitUnitType = nil - - local TargetUnit = nil - local TargetUnitName = "" - local TargetGroup = nil - local TargetGroupName = "" - local TargetPlayerName = "" - - local TargetCoalition = nil - local TargetCategory = nil - local TargetType = nil - local TargetUnitCoalition = nil - local TargetUnitCategory = nil - local TargetUnitType = nil - - if Event.IniDCSUnit then - - InitUnit = Event.IniDCSUnit - InitUnitName = Event.IniDCSUnitName - InitGroup = Event.IniDCSGroup - InitGroupName = Event.IniDCSGroupName - InitPlayerName = InitUnit:getPlayerName() - - InitCoalition = InitUnit:getCoalition() - --TODO: Workaround Client DCS Bug - --InitCategory = InitUnit:getCategory() - InitCategory = InitUnit:getDesc().category - InitType = InitUnit:getTypeName() - - InitUnitCoalition = _SCORINGCoalition[InitCoalition] - InitUnitCategory = _SCORINGCategory[InitCategory] - InitUnitType = InitType - - self:T( { InitUnitName, InitGroupName, InitPlayerName, InitCoalition, InitCategory, InitType , InitUnitCoalition, InitUnitCategory, InitUnitType } ) - end - - - if Event.TgtDCSUnit then - - TargetUnit = Event.TgtDCSUnit - TargetUnitName = Event.TgtDCSUnitName - TargetGroup = Event.TgtDCSGroup - TargetGroupName = Event.TgtDCSGroupName - TargetPlayerName = TargetUnit:getPlayerName() - - TargetCoalition = TargetUnit:getCoalition() - --TODO: Workaround Client DCS Bug - --TargetCategory = TargetUnit:getCategory() - TargetCategory = TargetUnit:getDesc().category - TargetType = TargetUnit:getTypeName() - - TargetUnitCoalition = _SCORINGCoalition[TargetCoalition] - TargetUnitCategory = _SCORINGCategory[TargetCategory] - TargetUnitType = TargetType - - self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType, TargetUnitCoalition, TargetUnitCategory, TargetUnitType } ) - end - - if InitPlayerName ~= nil then -- It is a player that is hitting something - self:_AddPlayerFromUnit( InitUnit ) - if self.Players[InitPlayerName] then -- This should normally not happen, but i'll test it anyway. - if TargetPlayerName ~= nil then -- It is a player hitting another player ... - self:_AddPlayerFromUnit( TargetUnit ) - self.Players[InitPlayerName].HitPlayers = self.Players[InitPlayerName].HitPlayers + 1 - end - - self:T( "Hitting Something" ) - -- What is he hitting? - if TargetCategory then - if not self.Players[InitPlayerName].Hit[TargetCategory] then - self.Players[InitPlayerName].Hit[TargetCategory] = {} - end - if not self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName] then - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName] = {} - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score = 0 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty = 0 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit = 0 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit = 0 - end - local Score = 0 - if InitCoalition == TargetCoalition then - self.Players[InitPlayerName].Penalty = self.Players[InitPlayerName].Penalty + 10 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty + 10 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit + 1 - MESSAGE:New( "Player '" .. InitPlayerName .. "' hit a friendly " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit .. " times. Penalty: -" .. self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty .. - ". Score Total:" .. self.Players[InitPlayerName].Score - self.Players[InitPlayerName].Penalty, - 2 - ):ToAll() - self:ScoreCSV( InitPlayerName, "HIT_PENALTY", 1, -25, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - else - self.Players[InitPlayerName].Score = self.Players[InitPlayerName].Score + 10 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score + 1 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit + 1 - MESSAGE:New( "Player '" .. InitPlayerName .. "' hit a target " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit .. " times. Score: " .. self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score .. - ". Score Total:" .. self.Players[InitPlayerName].Score - self.Players[InitPlayerName].Penalty, - 2 - ):ToAll() - self:ScoreCSV( InitPlayerName, "HIT_SCORE", 1, 1, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - end - end - end - elseif InitPlayerName == nil then -- It is an AI hitting a player??? - - end -end - - -function SCORING:ReportScoreAll() - - env.info( "Hello World " ) - - local ScoreMessage = "" - local PlayerMessage = "" - - self:T( "Score Report" ) - - for PlayerName, PlayerData in pairs( self.Players ) do - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) - - -- Some variables - local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] - local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local PlayerScore = 0 - local PlayerPenalty = 0 - - ScoreMessage = ":\n" - - local ScoreMessageHits = "" - - for CategoryID, CategoryName in pairs( _SCORINGCategory ) do - self:T( CategoryName ) - if PlayerData.Hit[CategoryID] then - local Score = 0 - local ScoreHit = 0 - local Penalty = 0 - local PenaltyHit = 0 - self:T( "Hit scores exist for player " .. PlayerName ) - for UnitName, UnitData in pairs( PlayerData.Hit[CategoryID] ) do - Score = Score + UnitData.Score - ScoreHit = ScoreHit + UnitData.ScoreHit - Penalty = Penalty + UnitData.Penalty - PenaltyHit = UnitData.PenaltyHit - end - local ScoreMessageHit = string.format( "%s:%d ", CategoryName, Score - Penalty ) - self:T( ScoreMessageHit ) - ScoreMessageHits = ScoreMessageHits .. ScoreMessageHit - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageHits ~= "" then - ScoreMessage = ScoreMessage .. " Hits: " .. ScoreMessageHits .. "\n" - end - - local ScoreMessageKills = "" - for CategoryID, CategoryName in pairs( _SCORINGCategory ) do - self:T( "Kill scores exist for player " .. PlayerName ) - if PlayerData.Kill[CategoryID] then - local Score = 0 - local ScoreKill = 0 - local Penalty = 0 - local PenaltyKill = 0 - - for UnitName, UnitData in pairs( PlayerData.Kill[CategoryID] ) do - Score = Score + UnitData.Score - ScoreKill = ScoreKill + UnitData.ScoreKill - Penalty = Penalty + UnitData.Penalty - PenaltyKill = PenaltyKill + UnitData.PenaltyKill - end - - local ScoreMessageKill = string.format( " %s:%d ", CategoryName, Score - Penalty ) - self:T( ScoreMessageKill ) - ScoreMessageKills = ScoreMessageKills .. ScoreMessageKill - - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageKills = ScoreMessageKills .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageKills ~= "" then - ScoreMessage = ScoreMessage .. " Kills: " .. ScoreMessageKills .. "\n" - end - - local ScoreMessageCoalitionChangePenalties = "" - if PlayerData.PenaltyCoalition ~= 0 then - ScoreMessageCoalitionChangePenalties = ScoreMessageCoalitionChangePenalties .. string.format( " -%d (%d changed)", PlayerData.Penalty, PlayerData.PenaltyCoalition ) - PlayerPenalty = PlayerPenalty + PlayerData.Penalty - end - if ScoreMessageCoalitionChangePenalties ~= "" then - ScoreMessage = ScoreMessage .. " Coalition Penalties: " .. ScoreMessageCoalitionChangePenalties .. "\n" - end - - local ScoreMessageMission = "" - local ScoreMission = 0 - local ScoreTask = 0 - for MissionName, MissionData in pairs( PlayerData.Mission ) do - ScoreMission = ScoreMission + MissionData.ScoreMission - ScoreTask = ScoreTask + MissionData.ScoreTask - ScoreMessageMission = ScoreMessageMission .. "'" .. MissionName .. "'; " - end - PlayerScore = PlayerScore + ScoreMission + ScoreTask - - if ScoreMessageMission ~= "" then - ScoreMessage = ScoreMessage .. " Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ")\n" - end - - PlayerMessage = PlayerMessage .. string.format( "Player '%s' Score:%d (%d Score -%d Penalties)%s", PlayerName, PlayerScore - PlayerPenalty, PlayerScore, PlayerPenalty, ScoreMessage ) - end - end - MESSAGE:New( PlayerMessage, 30, "Player Scores" ):ToAll() -end - - -function SCORING:ReportScorePlayer() - - env.info( "Hello World " ) - - local ScoreMessage = "" - local PlayerMessage = "" - - self:T( "Score Report" ) - - for PlayerName, PlayerData in pairs( self.Players ) do - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) - - -- Some variables - local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] - local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local PlayerScore = 0 - local PlayerPenalty = 0 - - ScoreMessage = "" - - local ScoreMessageHits = "" - - for CategoryID, CategoryName in pairs( _SCORINGCategory ) do - self:T( CategoryName ) - if PlayerData.Hit[CategoryID] then - local Score = 0 - local ScoreHit = 0 - local Penalty = 0 - local PenaltyHit = 0 - self:T( "Hit scores exist for player " .. PlayerName ) - for UnitName, UnitData in pairs( PlayerData.Hit[CategoryID] ) do - Score = Score + UnitData.Score - ScoreHit = ScoreHit + UnitData.ScoreHit - Penalty = Penalty + UnitData.Penalty - PenaltyHit = UnitData.PenaltyHit - end - local ScoreMessageHit = string.format( "\n %s = %d score(%d;-%d) hits(#%d;#-%d)", CategoryName, Score - Penalty, Score, Penalty, ScoreHit, PenaltyHit ) - self:T( ScoreMessageHit ) - ScoreMessageHits = ScoreMessageHits .. ScoreMessageHit - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageHits ~= "" then - ScoreMessage = ScoreMessage .. "\n Hits: " .. ScoreMessageHits .. " " - end - - local ScoreMessageKills = "" - for CategoryID, CategoryName in pairs( _SCORINGCategory ) do - self:T( "Kill scores exist for player " .. PlayerName ) - if PlayerData.Kill[CategoryID] then - local Score = 0 - local ScoreKill = 0 - local Penalty = 0 - local PenaltyKill = 0 - - for UnitName, UnitData in pairs( PlayerData.Kill[CategoryID] ) do - Score = Score + UnitData.Score - ScoreKill = ScoreKill + UnitData.ScoreKill - Penalty = Penalty + UnitData.Penalty - PenaltyKill = PenaltyKill + UnitData.PenaltyKill - end - - local ScoreMessageKill = string.format( "\n %s = %d score(%d;-%d) hits(#%d;#-%d)", CategoryName, Score - Penalty, Score, Penalty, ScoreKill, PenaltyKill ) - self:T( ScoreMessageKill ) - ScoreMessageKills = ScoreMessageKills .. ScoreMessageKill - - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageKills = ScoreMessageKills .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageKills ~= "" then - ScoreMessage = ScoreMessage .. "\n Kills: " .. ScoreMessageKills .. " " - end - - local ScoreMessageCoalitionChangePenalties = "" - if PlayerData.PenaltyCoalition ~= 0 then - ScoreMessageCoalitionChangePenalties = ScoreMessageCoalitionChangePenalties .. string.format( " -%d (%d changed)", PlayerData.Penalty, PlayerData.PenaltyCoalition ) - PlayerPenalty = PlayerPenalty + PlayerData.Penalty - end - if ScoreMessageCoalitionChangePenalties ~= "" then - ScoreMessage = ScoreMessage .. "\n Coalition: " .. ScoreMessageCoalitionChangePenalties .. " " - end - - local ScoreMessageMission = "" - local ScoreMission = 0 - local ScoreTask = 0 - for MissionName, MissionData in pairs( PlayerData.Mission ) do - ScoreMission = ScoreMission + MissionData.ScoreMission - ScoreTask = ScoreTask + MissionData.ScoreTask - ScoreMessageMission = ScoreMessageMission .. "'" .. MissionName .. "'; " - end - PlayerScore = PlayerScore + ScoreMission + ScoreTask - - if ScoreMessageMission ~= "" then - ScoreMessage = ScoreMessage .. "\n Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ") " - end - - PlayerMessage = PlayerMessage .. string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties ):%s", PlayerName, PlayerScore - PlayerPenalty, PlayerScore, PlayerPenalty, ScoreMessage ) - end - end - MESSAGE:New( PlayerMessage, 30, "Player Scores" ):ToAll() - -end - - -function SCORING:SecondsToClock(sSeconds) - local nSeconds = sSeconds - if nSeconds == 0 then - --return nil; - return "00:00:00"; - else - nHours = string.format("%02.f", math.floor(nSeconds/3600)); - nMins = string.format("%02.f", math.floor(nSeconds/60 - (nHours*60))); - nSecs = string.format("%02.f", math.floor(nSeconds - nHours*3600 - nMins *60)); - return nHours..":"..nMins..":"..nSecs - end -end - ---- Opens a score CSV file to log the scores. --- @param #SCORING self --- @param #string ScoringCSV --- @return #SCORING self --- @usage --- -- Open a new CSV file to log the scores of the game Gori Valley. Let the name of the CSV file begin with "Player Scores". --- ScoringObject = SCORING:New( "Gori Valley" ) --- ScoringObject:OpenCSV( "Player Scores" ) -function SCORING:OpenCSV( ScoringCSV ) - self:F( ScoringCSV ) - - if lfs and io and os then - if ScoringCSV then - self.ScoringCSV = ScoringCSV - local fdir = lfs.writedir() .. [[Logs\]] .. self.ScoringCSV .. " " .. os.date( "%Y-%m-%d %H-%M-%S" ) .. ".csv" - - self.CSVFile, self.err = io.open( fdir, "w+" ) - if not self.CSVFile then - error( "Error: Cannot open CSV file in " .. lfs.writedir() ) - end - - self.CSVFile:write( '"GameName","RunTime","Time","PlayerName","ScoreType","PlayerUnitCoaltion","PlayerUnitCategory","PlayerUnitType","PlayerUnitName","TargetUnitCoalition","TargetUnitCategory","TargetUnitType","TargetUnitName","Times","Score"\n' ) - - self.RunTime = os.date("%y-%m-%d_%H-%M-%S") - else - error( "A string containing the CSV file name must be given." ) - end - else - self:E( "The MissionScripting.lua file has not been changed to allow lfs, io and os modules to be used..." ) - end - return self -end - - ---- Registers a score for a player. --- @param #SCORING self --- @param #string PlayerName The name of the player. --- @param #string ScoreType The type of the score. --- @param #string ScoreTimes The amount of scores achieved. --- @param #string ScoreAmount The score given. --- @param #string PlayerUnitName The unit name of the player. --- @param #string PlayerUnitCoalition The coalition of the player unit. --- @param #string PlayerUnitCategory The category of the player unit. --- @param #string PlayerUnitType The type of the player unit. --- @param #string TargetUnitName The name of the target unit. --- @param #string TargetUnitCoalition The coalition of the target unit. --- @param #string TargetUnitCategory The category of the target unit. --- @param #string TargetUnitType The type of the target unit. --- @return #SCORING self -function SCORING:ScoreCSV( PlayerName, ScoreType, ScoreTimes, ScoreAmount, PlayerUnitName, PlayerUnitCoalition, PlayerUnitCategory, PlayerUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - --write statistic information to file - local ScoreTime = self:SecondsToClock( timer.getTime() ) - PlayerName = PlayerName:gsub( '"', '_' ) - - if PlayerUnitName and PlayerUnitName ~= '' then - local PlayerUnit = Unit.getByName( PlayerUnitName ) - - if PlayerUnit then - if not PlayerUnitCategory then - --PlayerUnitCategory = SCORINGCategory[PlayerUnit:getCategory()] - PlayerUnitCategory = _SCORINGCategory[PlayerUnit:getDesc().category] - end - - if not PlayerUnitCoalition then - PlayerUnitCoalition = _SCORINGCoalition[PlayerUnit:getCoalition()] - end - - if not PlayerUnitType then - PlayerUnitType = PlayerUnit:getTypeName() - end - else - PlayerUnitName = '' - PlayerUnitCategory = '' - PlayerUnitCoalition = '' - PlayerUnitType = '' - end - else - PlayerUnitName = '' - PlayerUnitCategory = '' - PlayerUnitCoalition = '' - PlayerUnitType = '' - end - - if not TargetUnitCoalition then - TargetUnitCoalition = '' - end - - if not TargetUnitCategory then - TargetUnitCategory = '' - end - - if not TargetUnitType then - TargetUnitType = '' - end - - if not TargetUnitName then - TargetUnitName = '' - end - - if lfs and io and os then - self.CSVFile:write( - '"' .. self.GameName .. '"' .. ',' .. - '"' .. self.RunTime .. '"' .. ',' .. - '' .. ScoreTime .. '' .. ',' .. - '"' .. PlayerName .. '"' .. ',' .. - '"' .. ScoreType .. '"' .. ',' .. - '"' .. PlayerUnitCoalition .. '"' .. ',' .. - '"' .. PlayerUnitCategory .. '"' .. ',' .. - '"' .. PlayerUnitType .. '"' .. ',' .. - '"' .. PlayerUnitName .. '"' .. ',' .. - '"' .. TargetUnitCoalition .. '"' .. ',' .. - '"' .. TargetUnitCategory .. '"' .. ',' .. - '"' .. TargetUnitType .. '"' .. ',' .. - '"' .. TargetUnitName .. '"' .. ',' .. - '' .. ScoreTimes .. '' .. ',' .. - '' .. ScoreAmount - ) - - self.CSVFile:write( "\n" ) - end -end - - -function SCORING:CloseCSV() - if lfs and io and os then - self.CSVFile:close() - end -end - ---- CARGO Classes --- @module CARGO - - - - - - - ---- Clients are those Groups defined within the Mission Editor that have the skillset defined as "Client" or "Player". --- These clients are defined within the Mission Orchestration Framework (MOF) - -CARGOS = {} - - -CARGO_ZONE = { - ClassName="CARGO_ZONE", - CargoZoneName = '', - CargoHostUnitName = '', - SIGNAL = { - TYPE = { - SMOKE = { ID = 1, TEXT = "smoke" }, - FLARE = { ID = 2, TEXT = "flare" } - }, - COLOR = { - GREEN = { ID = 1, TRIGGERCOLOR = trigger.smokeColor.Green, TEXT = "A green" }, - RED = { ID = 2, TRIGGERCOLOR = trigger.smokeColor.Red, TEXT = "A red" }, - WHITE = { ID = 3, TRIGGERCOLOR = trigger.smokeColor.White, TEXT = "A white" }, - ORANGE = { ID = 4, TRIGGERCOLOR = trigger.smokeColor.Orange, TEXT = "An orange" }, - BLUE = { ID = 5, TRIGGERCOLOR = trigger.smokeColor.Blue, TEXT = "A blue" }, - YELLOW = { ID = 6, TRIGGERCOLOR = trigger.flareColor.Yellow, TEXT = "A yellow" } - } - } -} - ---- Creates a new zone where cargo can be collected or deployed. --- The zone functionality is useful to smoke or indicate routes for cargo pickups or deployments. --- Provide the zone name as declared in the mission file into the CargoZoneName in the :New method. --- An optional parameter is the CargoHostName, which is a Group declared with Late Activation switched on in the mission file. --- The CargoHostName is the "host" of the cargo zone: --- --- * It will smoke the zone position when a client is approaching the zone. --- * Depending on the cargo type, it will assist in the delivery of the cargo by driving to and from the client. --- --- @param #CARGO_ZONE self --- @param #string CargoZoneName The name of the zone as declared within the mission editor. --- @param #string CargoHostName The name of the Group "hosting" the zone. The Group MUST NOT be a static, and must be a "mobile" unit. -function CARGO_ZONE:New( CargoZoneName, CargoHostName ) local self = BASE:Inherit( self, ZONE:New( CargoZoneName ) ) - self:F( { CargoZoneName, CargoHostName } ) - - self.CargoZoneName = CargoZoneName - self.SignalHeight = 2 - --self.CargoZone = trigger.misc.getZone( CargoZoneName ) - - - if CargoHostName then - self.CargoHostName = CargoHostName - end - - self:T( self.CargoZoneName ) - - return self -end - -function CARGO_ZONE:Spawn() - self:F( self.CargoHostName ) - - if self.CargoHostName then -- Only spawn a host in the zone when there is one given as a parameter in the New function. - if self.CargoHostSpawn then - local CargoHostGroup = self.CargoHostSpawn:GetGroupFromIndex() - if CargoHostGroup and CargoHostGroup:IsAlive() then - else - self.CargoHostSpawn:ReSpawn( 1 ) - end - else - self:T( "Initialize CargoHostSpawn" ) - self.CargoHostSpawn = SPAWN:New( self.CargoHostName ):Limit( 1, 1 ) - self.CargoHostSpawn:ReSpawn( 1 ) - end - end - - return self -end - -function CARGO_ZONE:GetHostUnit() - self:F( self ) - - if self.CargoHostName then - - -- A Host has been given, signal the host - local CargoHostGroup = self.CargoHostSpawn:GetGroupFromIndex() - local CargoHostUnit - if CargoHostGroup and CargoHostGroup:IsAlive() then - CargoHostUnit = CargoHostGroup:GetUnit(1) - else - CargoHostUnit = StaticObject.getByName( self.CargoHostName ) - end - - return CargoHostUnit - end - - return nil -end - -function CARGO_ZONE:ReportCargosToClient( Client, CargoType ) - self:F() - - local SignalUnit = self:GetHostUnit() - - if SignalUnit then - - local SignalUnitTypeName = SignalUnit:getTypeName() - - local HostMessage = "" - - local IsCargo = false - for CargoID, Cargo in pairs( CARGOS ) do - if Cargo.CargoType == Task.CargoType then - if Cargo:IsStatusNone() then - HostMessage = HostMessage .. " - " .. Cargo.CargoName .. " - " .. Cargo.CargoType .. " (" .. Cargo.Weight .. "kg)" .. "\n" - IsCargo = true - end - end - end - - if not IsCargo then - HostMessage = "No Cargo Available." - end - - Client:Message( HostMessage, 20, SignalUnitTypeName .. ": Reporting Cargo", 10 ) - end -end - - -function CARGO_ZONE:Signal() - self:F() - - local Signalled = false - - if self.SignalType then - - if self.CargoHostName then - - -- A Host has been given, signal the host - - local SignalUnit = self:GetHostUnit() - - if SignalUnit then - - self:T( 'Signalling Unit' ) - local SignalVehiclePos = SignalUnit:GetPointVec3() - SignalVehiclePos.y = SignalVehiclePos.y + 2 - - if self.SignalType.ID == CARGO_ZONE.SIGNAL.TYPE.SMOKE.ID then - - trigger.action.smoke( SignalVehiclePos, self.SignalColor.TRIGGERCOLOR ) - Signalled = true - - elseif self.SignalType.ID == CARGO_ZONE.SIGNAL.TYPE.FLARE.ID then - - trigger.action.signalFlare( SignalVehiclePos, self.SignalColor.TRIGGERCOLOR , 0 ) - Signalled = false - - end - end - - else - - local ZonePointVec3 = self:GetPointVec3( self.SignalHeight ) -- Get the zone position + the landheight + 2 meters - - if self.SignalType.ID == CARGO_ZONE.SIGNAL.TYPE.SMOKE.ID then - - trigger.action.smoke( ZonePointVec3, self.SignalColor.TRIGGERCOLOR ) - Signalled = true - - elseif self.SignalType.ID == CARGO_ZONE.SIGNAL.TYPE.FLARE.ID then - trigger.action.signalFlare( ZonePointVec3, self.SignalColor.TRIGGERCOLOR, 0 ) - Signalled = false - - end - end - end - - return Signalled - -end - -function CARGO_ZONE:WhiteSmoke( SignalHeight ) - self:F() - - self.SignalType = CARGO_ZONE.SIGNAL.TYPE.SMOKE - self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.WHITE - - if SignalHeight then - self.SignalHeight = SignalHeight - end - - return self -end - -function CARGO_ZONE:BlueSmoke( SignalHeight ) - self:F() - - self.SignalType = CARGO_ZONE.SIGNAL.TYPE.SMOKE - self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.BLUE - - if SignalHeight then - self.SignalHeight = SignalHeight - end - - return self -end - -function CARGO_ZONE:RedSmoke( SignalHeight ) - self:F() - - self.SignalType = CARGO_ZONE.SIGNAL.TYPE.SMOKE - self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.RED - - if SignalHeight then - self.SignalHeight = SignalHeight - end - - return self -end - -function CARGO_ZONE:OrangeSmoke( SignalHeight ) - self:F() - - self.SignalType = CARGO_ZONE.SIGNAL.TYPE.SMOKE - self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.ORANGE - - if SignalHeight then - self.SignalHeight = SignalHeight - end - - return self -end - -function CARGO_ZONE:GreenSmoke( SignalHeight ) - self:F() - - self.SignalType = CARGO_ZONE.SIGNAL.TYPE.SMOKE - self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.GREEN - - if SignalHeight then - self.SignalHeight = SignalHeight - end - - return self -end - - -function CARGO_ZONE:WhiteFlare( SignalHeight ) - self:F() - - self.SignalType = CARGO_ZONE.SIGNAL.TYPE.FLARE - self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.WHITE - - if SignalHeight then - self.SignalHeight = SignalHeight - end - - return self -end - -function CARGO_ZONE:RedFlare( SignalHeight ) - self:F() - - self.SignalType = CARGO_ZONE.SIGNAL.TYPE.FLARE - self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.RED - - if SignalHeight then - self.SignalHeight = SignalHeight - end - - return self -end - -function CARGO_ZONE:GreenFlare( SignalHeight ) - self:F() - - self.SignalType = CARGO_ZONE.SIGNAL.TYPE.FLARE - self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.GREEN - - if SignalHeight then - self.SignalHeight = SignalHeight - end - - return self -end - -function CARGO_ZONE:YellowFlare( SignalHeight ) - self:F() - - self.SignalType = CARGO_ZONE.SIGNAL.TYPE.FLARE - self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.YELLOW - - if SignalHeight then - self.SignalHeight = SignalHeight - end - - return self -end - - -function CARGO_ZONE:GetCargoHostUnit() - self:F( self ) - - if self.CargoHostSpawn then - local CargoHostGroup = self.CargoHostSpawn:GetGroupFromIndex(1) - if CargoHostGroup and CargoHostGroup:IsAlive() then - local CargoHostUnit = CargoHostGroup:GetUnit(1) - if CargoHostUnit and CargoHostUnit:IsAlive() then - return CargoHostUnit - end - end - end - - return nil -end - -function CARGO_ZONE:GetCargoZoneName() - self:F() - - return self.CargoZoneName -end - -CARGO = { - ClassName = "CARGO", - STATUS = { - NONE = 0, - LOADED = 1, - UNLOADED = 2, - LOADING = 3 - }, - CargoClient = nil -} - ---- Add Cargo to the mission... Cargo functionality needs to be reworked a bit, so this is still under construction. I need to make a CARGO Class... -function CARGO:New( CargoType, CargoName, CargoWeight ) local self = BASE:Inherit( self, BASE:New() ) - self:F( { CargoType, CargoName, CargoWeight } ) - - - self.CargoType = CargoType - self.CargoName = CargoName - self.CargoWeight = CargoWeight - - self:StatusNone() - - return self -end - -function CARGO:Spawn( Client ) - self:F() - - return self - -end - -function CARGO:IsNear( Client, LandingZone ) - self:F() - - local Near = true - - return Near - -end - - -function CARGO:IsLoadingToClient() - self:F() - - if self:IsStatusLoading() then - return self.CargoClient - end - - return nil - -end - - -function CARGO:IsLoadedInClient() - self:F() - - if self:IsStatusLoaded() then - return self.CargoClient - end - - return nil - -end - - -function CARGO:UnLoad( Client, TargetZoneName ) - self:F() - - self:StatusUnLoaded() - - return self -end - -function CARGO:OnBoard( Client, LandingZone ) - self:F() - - local Valid = true - - self.CargoClient = Client - local ClientUnit = Client:GetClientGroupDCSUnit() - - return Valid -end - -function CARGO:OnBoarded( Client, LandingZone ) - self:F() - - local OnBoarded = true - - return OnBoarded -end - -function CARGO:Load( Client ) - self:F() - - self:StatusLoaded( Client ) - - return self -end - -function CARGO:IsLandingRequired() - self:F() - return true -end - -function CARGO:IsSlingLoad() - self:F() - return false -end - - -function CARGO:StatusNone() - self:F() - - self.CargoClient = nil - self.CargoStatus = CARGO.STATUS.NONE - - return self -end - -function CARGO:StatusLoading( Client ) - self:F() - - self.CargoClient = Client - self.CargoStatus = CARGO.STATUS.LOADING - self:T( "Cargo " .. self.CargoName .. " loading to Client: " .. self.CargoClient:GetClientGroupName() ) - - return self -end - -function CARGO:StatusLoaded( Client ) - self:F() - - self.CargoClient = Client - self.CargoStatus = CARGO.STATUS.LOADED - self:T( "Cargo " .. self.CargoName .. " loaded in Client: " .. self.CargoClient:GetClientGroupName() ) - - return self -end - -function CARGO:StatusUnLoaded() - self:F() - - self.CargoClient = nil - self.CargoStatus = CARGO.STATUS.UNLOADED - - return self -end - - -function CARGO:IsStatusNone() - self:F() - - return self.CargoStatus == CARGO.STATUS.NONE -end - -function CARGO:IsStatusLoading() - self:F() - - return self.CargoStatus == CARGO.STATUS.LOADING -end - -function CARGO:IsStatusLoaded() - self:F() - - return self.CargoStatus == CARGO.STATUS.LOADED -end - -function CARGO:IsStatusUnLoaded() - self:F() - - return self.CargoStatus == CARGO.STATUS.UNLOADED -end - - -CARGO_GROUP = { - ClassName = "CARGO_GROUP" -} - - -function CARGO_GROUP:New( CargoType, CargoName, CargoWeight, CargoGroupTemplate, CargoZone ) local self = BASE:Inherit( self, CARGO:New( CargoType, CargoName, CargoWeight ) ) - self:F( { CargoType, CargoName, CargoWeight, CargoGroupTemplate, CargoZone } ) - - self.CargoSpawn = SPAWN:NewWithAlias( CargoGroupTemplate, CargoName ) - self.CargoZone = CargoZone - - CARGOS[self.CargoName] = self - - return self - -end - -function CARGO_GROUP:Spawn( Client ) - self:F( { Client } ) - - local SpawnCargo = true - - if self:IsStatusNone() then - local CargoGroup = Group.getByName( self.CargoName ) - if CargoGroup and CargoGroup:isExist() then - SpawnCargo = false - end - - elseif self:IsStatusLoading() then - - local Client = self:IsLoadingToClient() - if Client and Client:GetDCSGroup() then - SpawnCargo = false - else - local CargoGroup = Group.getByName( self.CargoName ) - if CargoGroup and CargoGroup:isExist() then - SpawnCargo = false - end - end - - elseif self:IsStatusLoaded() then - - local ClientLoaded = self:IsLoadedInClient() - -- Now test if another Client is alive (not this one), and it has the CARGO, then this cargo does not need to be initialized and spawned. - if ClientLoaded and ClientLoaded ~= Client then - local ClientGroup = Client:GetDCSGroup() - if ClientLoaded:GetClientGroupDCSUnit() and ClientLoaded:GetClientGroupDCSUnit():isExist() then - SpawnCargo = false - else - self:StatusNone() - end - else - -- Same Client, but now in initialize, so set back the status to None. - self:StatusNone() - end - - elseif self:IsStatusUnLoaded() then - - SpawnCargo = false - - end - - if SpawnCargo then - if self.CargoZone:GetCargoHostUnit() then - --- ReSpawn the Cargo from the CargoHost - self.CargoGroupName = self.CargoSpawn:SpawnFromUnit( self.CargoZone:GetCargoHostUnit(), 60, 30, 1 ):GetName() - else - --- ReSpawn the Cargo in the CargoZone without a host ... - self:T( self.CargoZone ) - self.CargoGroupName = self.CargoSpawn:SpawnInZone( self.CargoZone, true, 1 ):GetName() - end - self:StatusNone() - end - - self:T( { self.CargoGroupName, CARGOS[self.CargoName].CargoGroupName } ) - - return self -end - -function CARGO_GROUP:IsNear( Client, LandingZone ) - self:F() - - local Near = false - - if self.CargoGroupName then - local CargoGroup = Group.getByName( self.CargoGroupName ) - if routines.IsPartOfGroupInRadius( CargoGroup, Client:GetPositionVec3(), 250 ) then - Near = true - end - end - - return Near - -end - - -function CARGO_GROUP:OnBoard( Client, LandingZone, OnBoardSide ) - self:F() - - local Valid = true - - local ClientUnit = Client:GetClientGroupDCSUnit() - - local CarrierPos = ClientUnit:getPoint() - local CarrierPosMove = ClientUnit:getPoint() - local CarrierPosOnBoard = ClientUnit:getPoint() - - local CargoGroup = Group.getByName( self.CargoGroupName ) - - local CargoUnit = CargoGroup:getUnit(1) - local CargoPos = CargoUnit:getPoint() - - self.CargoInAir = CargoUnit: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 - - local Points = {} - - self:T( 'CargoPos x = ' .. CargoPos.x .. " z = " .. CargoPos.z ) - self:T( 'CarrierPosMove x = ' .. CarrierPosMove.x .. " z = " .. CarrierPosMove.z ) - - Points[#Points+1] = routines.ground.buildWP( CargoPos, "Cone", 10 ) - - self:T( 'Points[1] x = ' .. Points[1].x .. " y = " .. Points[1].y ) - - if OnBoardSide == nil then - OnBoardSide = CLIENT.ONBOARDSIDE.NONE - end - - if OnBoardSide == CLIENT.ONBOARDSIDE.LEFT then - - self:T( "TransportCargoOnBoard: Onboarding LEFT" ) - CarrierPosMove.z = CarrierPosMove.z - 25 - CarrierPosOnBoard.z = CarrierPosOnBoard.z - 5 - Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) - - elseif OnBoardSide == CLIENT.ONBOARDSIDE.RIGHT then - - self:T( "TransportCargoOnBoard: Onboarding RIGHT" ) - CarrierPosMove.z = CarrierPosMove.z + 25 - CarrierPosOnBoard.z = CarrierPosOnBoard.z + 5 - Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) - - elseif OnBoardSide == CLIENT.ONBOARDSIDE.BACK then - - self:T( "TransportCargoOnBoard: Onboarding BACK" ) - CarrierPosMove.x = CarrierPosMove.x - 25 - CarrierPosOnBoard.x = CarrierPosOnBoard.x - 5 - Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) - - elseif OnBoardSide == CLIENT.ONBOARDSIDE.FRONT then - - self:T( "TransportCargoOnBoard: Onboarding FRONT" ) - CarrierPosMove.x = CarrierPosMove.x + 25 - CarrierPosOnBoard.x = CarrierPosOnBoard.x + 5 - Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) - - elseif OnBoardSide == CLIENT.ONBOARDSIDE.NONE then - - self:T( "TransportCargoOnBoard: Onboarding CENTRAL" ) - Points[#Points+1] = routines.ground.buildWP( CarrierPos, "Cone", 10 ) - - end - self:T( "TransportCargoOnBoard: Routing " .. self.CargoGroupName ) - - --routines.scheduleFunction( routines.goRoute, { self.CargoGroupName, Points}, timer.getTime() + 4 ) - SCHEDULER:New( self, routines.goRoute, { self.CargoGroupName, Points}, 4 ) - end - - self:StatusLoading( Client ) - - return Valid - -end - - -function CARGO_GROUP:OnBoarded( Client, LandingZone ) - self:F() - - local OnBoarded = false - - local CargoGroup = Group.getByName( self.CargoGroupName ) - - if not self.CargoInAir then - if routines.IsPartOfGroupInRadius( CargoGroup, Client:GetPositionVec3(), 25 ) then - CargoGroup:destroy() - self:StatusLoaded( Client ) - OnBoarded = true - end - else - CargoGroup:destroy() - self:StatusLoaded( Client ) - OnBoarded = true - end - - return OnBoarded -end - - -function CARGO_GROUP:UnLoad( Client, TargetZoneName ) - self:F() - - self:T( 'self.CargoName = ' .. self.CargoName ) - - local CargoGroup = self.CargoSpawn:SpawnFromUnit( Client:GetClientGroupUnit(), 60, 30 ) - - self.CargoGroupName = CargoGroup:GetName() - self:T( 'self.CargoGroupName = ' .. self.CargoGroupName ) - - CargoGroup:TaskRouteToZone( ZONE:New( TargetZoneName ), true ) - - self:StatusUnLoaded() - - return self -end - - -CARGO_PACKAGE = { - ClassName = "CARGO_PACKAGE" -} - - -function CARGO_PACKAGE:New( CargoType, CargoName, CargoWeight, CargoClient ) local self = BASE:Inherit( self, CARGO:New( CargoType, CargoName, CargoWeight ) ) - self:F( { CargoType, CargoName, CargoWeight, CargoClient } ) - - self.CargoClient = CargoClient - - CARGOS[self.CargoName] = self - - return self - -end - - -function CARGO_PACKAGE:Spawn( Client ) - self:F( { self, Client } ) - - -- this needs to be checked thoroughly - - local CargoClientGroup = self.CargoClient:GetDCSGroup() - if not CargoClientGroup then - if not self.CargoClientSpawn then - self.CargoClientSpawn = SPAWN:New( self.CargoClient:GetClientGroupName() ):Limit( 1, 1 ) - end - self.CargoClientSpawn:ReSpawn( 1 ) - end - - local SpawnCargo = true - - if self:IsStatusNone() then - - elseif self:IsStatusLoading() or self:IsStatusLoaded() then - - local CargoClientLoaded = self:IsLoadedInClient() - if CargoClientLoaded and CargoClientLoaded:GetDCSGroup() then - SpawnCargo = false - end - - elseif self:IsStatusUnLoaded() then - - SpawnCargo = false - - else - - end - - if SpawnCargo then - self:StatusLoaded( self.CargoClient ) - end - - return self -end - - -function CARGO_PACKAGE:IsNear( Client, LandingZone ) - self:F() - - local Near = false - - if self.CargoClient and self.CargoClient:GetDCSGroup() then - self:T( self.CargoClient.ClientName ) - self:T( 'Client Exists.' ) - - if routines.IsUnitInRadius( self.CargoClient:GetClientGroupDCSUnit(), Client:GetPositionVec3(), 150 ) then - Near = true - end - end - - return Near - -end - - -function CARGO_PACKAGE:OnBoard( Client, LandingZone, OnBoardSide ) - self:F() - - local Valid = true - - local ClientUnit = Client:GetClientGroupDCSUnit() - - local CarrierPos = ClientUnit:getPoint() - local CarrierPosMove = ClientUnit:getPoint() - local CarrierPosOnBoard = ClientUnit:getPoint() - local CarrierPosMoveAway = ClientUnit:getPoint() - - local CargoHostGroup = self.CargoClient:GetDCSGroup() - local CargoHostName = self.CargoClient:GetDCSGroup():getName() - - local CargoHostUnits = CargoHostGroup:getUnits() - local CargoPos = CargoHostUnits[1]:getPoint() - - local Points = {} - - self:T( 'CargoPos x = ' .. CargoPos.x .. " z = " .. CargoPos.z ) - self:T( 'CarrierPosMove x = ' .. CarrierPosMove.x .. " z = " .. CarrierPosMove.z ) - - Points[#Points+1] = routines.ground.buildWP( CargoPos, "Cone", 10 ) - - self:T( 'Points[1] x = ' .. Points[1].x .. " y = " .. Points[1].y ) - - if OnBoardSide == nil then - OnBoardSide = CLIENT.ONBOARDSIDE.NONE - end - - if OnBoardSide == CLIENT.ONBOARDSIDE.LEFT then - - self:T( "TransportCargoOnBoard: Onboarding LEFT" ) - CarrierPosMove.z = CarrierPosMove.z - 25 - CarrierPosOnBoard.z = CarrierPosOnBoard.z - 5 - CarrierPosMoveAway.z = CarrierPosMoveAway.z - 20 - Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosMoveAway, "Cone", 10 ) - - elseif OnBoardSide == CLIENT.ONBOARDSIDE.RIGHT then - - self:T( "TransportCargoOnBoard: Onboarding RIGHT" ) - CarrierPosMove.z = CarrierPosMove.z + 25 - CarrierPosOnBoard.z = CarrierPosOnBoard.z + 5 - CarrierPosMoveAway.z = CarrierPosMoveAway.z + 20 - Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosMoveAway, "Cone", 10 ) - - elseif OnBoardSide == CLIENT.ONBOARDSIDE.BACK then - - self:T( "TransportCargoOnBoard: Onboarding BACK" ) - CarrierPosMove.x = CarrierPosMove.x - 25 - CarrierPosOnBoard.x = CarrierPosOnBoard.x - 5 - CarrierPosMoveAway.x = CarrierPosMoveAway.x - 20 - Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosMoveAway, "Cone", 10 ) - - elseif OnBoardSide == CLIENT.ONBOARDSIDE.FRONT then - - self:T( "TransportCargoOnBoard: Onboarding FRONT" ) - CarrierPosMove.x = CarrierPosMove.x + 25 - CarrierPosOnBoard.x = CarrierPosOnBoard.x + 5 - CarrierPosMoveAway.x = CarrierPosMoveAway.x + 20 - Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosMoveAway, "Cone", 10 ) - - elseif OnBoardSide == CLIENT.ONBOARDSIDE.NONE then - - self:T( "TransportCargoOnBoard: Onboarding FRONT" ) - CarrierPosMove.x = CarrierPosMove.x + 25 - CarrierPosOnBoard.x = CarrierPosOnBoard.x + 5 - CarrierPosMoveAway.x = CarrierPosMoveAway.x + 20 - Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosMoveAway, "Cone", 10 ) - - end - self:T( "Routing " .. CargoHostName ) - - --routines.scheduleFunction( routines.goRoute, { CargoHostName, Points}, timer.getTime() + 4 ) - SCHEDULER:New( self, routines.goRoute, { CargoHostName, Points }, 4 ) - - return Valid - -end - - -function CARGO_PACKAGE:OnBoarded( Client, LandingZone ) - self:F() - - local OnBoarded = false - - if self.CargoClient and self.CargoClient:GetDCSGroup() then - if routines.IsUnitInRadius( self.CargoClient:GetClientGroupDCSUnit(), self.CargoClient:GetPositionVec3(), 10 ) then - - -- Switch Cargo from self.CargoClient to Client ... Each cargo can have only one client. So assigning the new client for the cargo is enough. - self:StatusLoaded( Client ) - - -- All done, onboarded the Cargo to the new Client. - OnBoarded = true - end - end - - return OnBoarded -end - - -function CARGO_PACKAGE:UnLoad( Client, TargetZoneName ) - self:F() - - self:T( 'self.CargoName = ' .. self.CargoName ) - --self:T( 'self.CargoHostName = ' .. self.CargoHostName ) - - --self.CargoSpawn:FromCarrier( Client:GetDCSGroup(), TargetZoneName, self.CargoHostName ) - self:StatusUnLoaded() - - return Cargo -end - - -CARGO_SLINGLOAD = { - ClassName = "CARGO_SLINGLOAD" -} - - -function CARGO_SLINGLOAD:New( CargoType, CargoName, CargoWeight, CargoZone, CargoHostName, CargoCountryID ) - local self = BASE:Inherit( self, CARGO:New( CargoType, CargoName, CargoWeight ) ) - self:F( { CargoType, CargoName, CargoWeight, CargoZone, CargoHostName, CargoCountryID } ) - - self.CargoHostName = CargoHostName - - -- Cargo will be initialized around the CargoZone position. - self.CargoZone = CargoZone - - self.CargoCount = 0 - self.CargoStaticName = string.format( "%s#%03d", self.CargoName, self.CargoCount ) - - -- The country ID needs to be correctly set. - self.CargoCountryID = CargoCountryID - - CARGOS[self.CargoName] = self - - return self - -end - - -function CARGO_SLINGLOAD:IsLandingRequired() - self:F() - return false -end - - -function CARGO_SLINGLOAD:IsSlingLoad() - self:F() - return true -end - - -function CARGO_SLINGLOAD:Spawn( Client ) - self:F( { self, Client } ) - - local Zone = trigger.misc.getZone( self.CargoZone ) - - local ZonePos = {} - ZonePos.x = Zone.point.x + math.random( Zone.radius / 2 * -1, Zone.radius / 2 ) - ZonePos.y = Zone.point.z + math.random( Zone.radius / 2 * -1, Zone.radius / 2 ) - - self:T( "Cargo Location = " .. ZonePos.x .. ", " .. ZonePos.y ) - - --[[ - -- This does not work in 1.5.2. - CargoStatic = StaticObject.getByName( self.CargoName ) - if CargoStatic then - CargoStatic:destroy() - end - --]] - - CargoStatic = StaticObject.getByName( self.CargoStaticName ) - - if CargoStatic and CargoStatic:isExist() then - CargoStatic:destroy() - end - - -- I need to make every time a new cargo due to bugs in 1.5.2. - - self.CargoCount = self.CargoCount + 1 - self.CargoStaticName = string.format( "%s#%03d", self.CargoName, self.CargoCount ) - - local CargoTemplate = { - ["category"] = "Cargo", - ["shape_name"] = "ab-212_cargo", - ["type"] = "Cargo1", - ["x"] = ZonePos.x, - ["y"] = ZonePos.y, - ["mass"] = self.CargoWeight, - ["name"] = self.CargoStaticName, - ["canCargo"] = true, - ["heading"] = 0, - } - - coalition.addStaticObject( self.CargoCountryID, CargoTemplate ) - --- end - - return self -end - - -function CARGO_SLINGLOAD:IsNear( Client, LandingZone ) - self:F() - - local Near = false - - return Near -end - - -function CARGO_SLINGLOAD:IsInLandingZone( Client, LandingZone ) - self:F() - - local Near = false - - local CargoStaticUnit = StaticObject.getByName( self.CargoName ) - if CargoStaticUnit then - if routines.IsStaticInZones( CargoStaticUnit, LandingZone ) then - Near = true - end - end - - return Near -end - - -function CARGO_SLINGLOAD:OnBoard( Client, LandingZone, OnBoardSide ) - self:F() - - local Valid = true - - - return Valid -end - - -function CARGO_SLINGLOAD:OnBoarded( Client, LandingZone ) - self:F() - - local OnBoarded = false - - local CargoStaticUnit = StaticObject.getByName( self.CargoName ) - if CargoStaticUnit then - if not routines.IsStaticInZones( CargoStaticUnit, LandingZone ) then - OnBoarded = true - end - end - - return OnBoarded -end - - -function CARGO_SLINGLOAD:UnLoad( Client, TargetZoneName ) - self:F() - - self:T( 'self.CargoName = ' .. self.CargoName ) - self:T( 'self.CargoGroupName = ' .. self.CargoGroupName ) - - self:StatusUnLoaded() - - return Cargo -end ---- This module contains the MESSAGE class. --- --- 1) @{Message#MESSAGE} class, extends @{Base#BASE} --- ================================================= --- Message System to display Messages to Clients, Coalitions or All. --- Messages are shown on the display panel for an amount of seconds, and will then disappear. --- Messages can contain a category which is indicating the category of the message. --- --- 1.1) MESSAGE construction methods --- --------------------------------- --- Messages are created with @{Message#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. --- --- 1.2) Send messages with MESSAGE To methods --- ------------------------------------------ --- Messages are sent to: --- --- * Clients with @{Message#MESSAGE.ToClient}. --- * Coalitions with @{Message#MESSAGE.ToCoalition}. --- * All Players with @{Message#MESSAGE.ToAll}. --- --- @module Message --- @author FlightControl - ---- The MESSAGE class --- @type MESSAGE --- @extends Base#BASE -MESSAGE = { - ClassName = "MESSAGE", - MessageCategory = 0, - MessageID = 0, -} - - ---- Creates a new MESSAGE object. Note that these MESSAGE objects are not yet displayed on the display panel. You must use the functions @{ToClient} or @{ToCoalition} or @{ToAll} to send these Messages to the respective recipients. --- @param self --- @param #string MessageText is the text of the Message. --- @param #number MessageDuration is a number in seconds of how long the MESSAGE should be shown on the display panel. --- @param #string MessageCategory (optional) is a string expressing the "category" of the Message. The category will be shown as the first text in the message followed by a ": ". --- @return #MESSAGE --- @usage --- -- Create a series of new Messages. --- -- MessageAll is meant to be sent to all players, for 25 seconds, and is classified as "Score". --- -- MessageRED is meant to be sent to the RED players only, for 10 seconds, and is classified as "End of Mission", with ID "Win". --- -- MessageClient1 is meant to be sent to a Client, for 25 seconds, and is classified as "Score", with ID "Score". --- -- MessageClient1 is meant to be sent to a Client, for 25 seconds, and is classified as "Score", with ID "Score". --- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", 25, "End of Mission" ) --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", 25, "Penalty" ) --- MessageClient1 = MESSAGE:New( "Congratulations, you've just hit a target", 25, "Score" ) --- MessageClient2 = MESSAGE:New( "Congratulations, you've just killed a target", 25, "Score") -function MESSAGE:New( MessageText, MessageDuration, MessageCategory ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( { MessageText, MessageDuration, MessageCategory } ) - - -- When no MessageCategory is given, we don't show it as a title... - if MessageCategory and MessageCategory ~= "" then - self.MessageCategory = MessageCategory .. ": " - else - self.MessageCategory = "" - end - - self.MessageDuration = MessageDuration - self.MessageTime = timer.getTime() - self.MessageText = MessageText - - self.MessageSent = false - self.MessageGroup = false - self.MessageCoalition = false - - 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 Client#CLIENT Client is the Group of the Client. --- @return #MESSAGE --- @usage --- -- Send the 2 messages created with the @{New} method to the Client Group. --- -- Note that the Message of MessageClient2 is overwriting the Message of MessageClient1. --- ClientGroup = Group.getByName( "ClientGroup" ) --- --- MessageClient1 = MESSAGE:New( "Congratulations, you've just hit a target", "Score", 25, "Score" ):ToClient( ClientGroup ) --- MessageClient2 = MESSAGE:New( "Congratulations, you've just killed a target", "Score", 25, "Score" ):ToClient( ClientGroup ) --- or --- MESSAGE:New( "Congratulations, you've just hit a target", "Score", 25, "Score" ):ToClient( ClientGroup ) --- MESSAGE:New( "Congratulations, you've just killed a target", "Score", 25, "Score" ):ToClient( ClientGroup ) --- or --- MessageClient1 = MESSAGE:New( "Congratulations, you've just hit a target", "Score", 25, "Score" ) --- MessageClient2 = MESSAGE:New( "Congratulations, you've just killed a target", "Score", 25, "Score" ) --- MessageClient1:ToClient( ClientGroup ) --- MessageClient2:ToClient( ClientGroup ) -function MESSAGE:ToClient( Client ) - self:F( Client ) - - if Client and Client:GetClientGroupID() 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 ) - end - - return self -end - ---- Sends a MESSAGE to the Blue coalition. --- @param #MESSAGE self --- @return #MESSAGE --- @usage --- -- Send a message created with the @{New} method to the BLUE coalition. --- MessageBLUE = MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToBlue() --- or --- MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToBlue() --- or --- MessageBLUE = MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ) --- MessageBLUE:ToBlue() -function MESSAGE:ToBlue() - self:F() - - self:ToCoalition( coalition.side.BLUE ) - - return self -end - ---- Sends a MESSAGE to the Red Coalition. --- @param #MESSAGE self --- @return #MESSAGE --- @usage --- -- Send a message created with the @{New} method to the RED coalition. --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToRed() --- or --- MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToRed() --- or --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ) --- MessageRED:ToRed() -function MESSAGE:ToRed( ) - self:F() - - self:ToCoalition( coalition.side.RED ) - - return self -end - ---- Sends a MESSAGE to a Coalition. --- @param #MESSAGE self --- @param CoalitionSide needs to be filled out by the defined structure of the standard scripting engine @{coalition.side}. --- @return #MESSAGE --- @usage --- -- Send a message created with the @{New} method to the RED coalition. --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToCoalition( coalition.side.RED ) --- or --- MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToCoalition( coalition.side.RED ) --- or --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ) --- MessageRED:ToCoalition( coalition.side.RED ) -function MESSAGE:ToCoalition( CoalitionSide ) - self:F( CoalitionSide ) - - if CoalitionSide then - self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration ) - trigger.action.outTextForCoalition( CoalitionSide, self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration ) - end - - return self -end - ---- Sends a MESSAGE to all players. --- @param #MESSAGE self --- @return #MESSAGE --- @usage --- -- Send a message created to all players. --- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ):ToAll() --- or --- MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ):ToAll() --- or --- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ) --- MessageAll:ToAll() -function MESSAGE:ToAll() - self:F() - - self:ToCoalition( coalition.side.RED ) - self:ToCoalition( coalition.side.BLUE ) - - return self -end - - - ------ The MESSAGEQUEUE class ----- @type MESSAGEQUEUE ---MESSAGEQUEUE = { --- ClientGroups = {}, --- CoalitionSides = {} ---} --- ---function MESSAGEQUEUE:New( RefreshInterval ) --- local self = BASE:Inherit( self, BASE:New() ) --- self:F( { RefreshInterval } ) --- --- self.RefreshInterval = RefreshInterval --- --- --self.DisplayFunction = routines.scheduleFunction( self._DisplayMessages, { self }, 0, RefreshInterval ) --- self.DisplayFunction = SCHEDULER:New( self, self._DisplayMessages, {}, 0, RefreshInterval ) --- --- return self ---end --- ------ This function is called automatically by the MESSAGEQUEUE scheduler. ---function MESSAGEQUEUE:_DisplayMessages() --- --- -- First we display all messages that a coalition needs to receive... Also those who are not in a client (CA module clients...). --- for CoalitionSideID, CoalitionSideData in pairs( self.CoalitionSides ) do --- for MessageID, MessageData in pairs( CoalitionSideData.Messages ) do --- if MessageData.MessageSent == false then --- --trigger.action.outTextForCoalition( CoalitionSideID, MessageData.MessageCategory .. '\n' .. MessageData.MessageText:gsub("\n$",""):gsub("\n$",""), MessageData.MessageDuration ) --- MessageData.MessageSent = true --- end --- local MessageTimeLeft = ( MessageData.MessageTime + MessageData.MessageDuration ) - timer.getTime() --- if MessageTimeLeft <= 0 then --- MessageData = nil --- end --- end --- end --- --- -- Then we send the messages for each individual client, but also to be included are those Coalition messages for the Clients who belong to a coalition. --- -- Because the Client messages will overwrite the Coalition messages (for that Client). --- for ClientGroupName, ClientGroupData in pairs( self.ClientGroups ) do --- for MessageID, MessageData in pairs( ClientGroupData.Messages ) do --- if MessageData.MessageGroup == false then --- trigger.action.outTextForGroup( Group.getByName(ClientGroupName):getID(), MessageData.MessageCategory .. '\n' .. MessageData.MessageText:gsub("\n$",""):gsub("\n$",""), MessageData.MessageDuration ) --- MessageData.MessageGroup = true --- end --- local MessageTimeLeft = ( MessageData.MessageTime + MessageData.MessageDuration ) - timer.getTime() --- if MessageTimeLeft <= 0 then --- MessageData = nil --- end --- end --- --- -- Now check if the Client also has messages that belong to the Coalition of the Client... --- for CoalitionSideID, CoalitionSideData in pairs( self.CoalitionSides ) do --- for MessageID, MessageData in pairs( CoalitionSideData.Messages ) do --- local CoalitionGroup = Group.getByName( ClientGroupName ) --- if CoalitionGroup and CoalitionGroup:getCoalition() == CoalitionSideID then --- if MessageData.MessageCoalition == false then --- trigger.action.outTextForGroup( Group.getByName(ClientGroupName):getID(), MessageData.MessageCategory .. '\n' .. MessageData.MessageText:gsub("\n$",""):gsub("\n$",""), MessageData.MessageDuration ) --- MessageData.MessageCoalition = true --- end --- end --- local MessageTimeLeft = ( MessageData.MessageTime + MessageData.MessageDuration ) - timer.getTime() --- if MessageTimeLeft <= 0 then --- MessageData = nil --- end --- end --- end --- end --- --- return true ---end --- ------ The _MessageQueue object is created when the MESSAGE class module is loaded. -----_MessageQueue = MESSAGEQUEUE:New( 0.5 ) --- ---- Stages within a @{TASK} within a @{MISSION}. All of the STAGE functionality is considered internally administered and not to be used by any Mission designer. --- @module STAGE --- @author Flightcontrol - - - - - - - ---- The STAGE class --- @type -STAGE = { - ClassName = "STAGE", - MSG = { ID = "None", TIME = 10 }, - FREQUENCY = { NONE = 0, ONCE = 1, REPEAT = -1 }, - - Name = "NoStage", - StageType = '', - WaitTime = 1, - Frequency = 1, - MessageCount = 0, - MessageInterval = 15, - MessageShown = {}, - MessageShow = false, - MessageFlash = false -} - - -function STAGE:New() - local self = BASE:Inherit( self, BASE:New() ) - self:F() - return self -end - -function STAGE:Execute( Mission, Client, Task ) - - local Valid = true - - return Valid -end - -function STAGE:Executing( Mission, Client, Task ) - -end - -function STAGE:Validate( Mission, Client, Task ) - local Valid = true - - return Valid -end - - -STAGEBRIEF = { - ClassName = "BRIEF", - MSG = { ID = "Brief", TIME = 1 }, - Name = "Brief", - StageBriefingTime = 0, - StageBriefingDuration = 1 -} - -function STAGEBRIEF:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - return self -end - ---- Execute --- @param #STAGEBRIEF self --- @param Mission#MISSION Mission --- @param Client#CLIENT Client --- @param Task#TASK Task --- @return #boolean -function STAGEBRIEF:Execute( Mission, Client, Task ) - local Valid = BASE:Inherited(self):Execute( Mission, Client, Task ) - self:F() - Client:ShowMissionBriefing( Mission.MissionBriefing ) - self.StageBriefingTime = timer.getTime() - return Valid -end - -function STAGEBRIEF:Validate( Mission, Client, Task ) - local Valid = STAGE:Validate( Mission, Client, Task ) - self:T() - - if timer.getTime() - self.StageBriefingTime <= self.StageBriefingDuration then - return 0 - else - self.StageBriefingTime = timer.getTime() - return 1 - end - -end - - -STAGESTART = { - ClassName = "START", - MSG = { ID = "Start", TIME = 1 }, - Name = "Start", - StageStartTime = 0, - StageStartDuration = 1 -} - -function STAGESTART:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - return self -end - -function STAGESTART:Execute( Mission, Client, Task ) - self:F() - local Valid = BASE:Inherited(self):Execute( Mission, Client, Task ) - if Task.TaskBriefing then - Client:Message( Task.TaskBriefing, 30, "Command" ) - else - Client:Message( 'Task ' .. Task.TaskNumber .. '.', 30, "Command" ) - end - self.StageStartTime = timer.getTime() - return Valid -end - -function STAGESTART:Validate( Mission, Client, Task ) - self:F() - local Valid = STAGE:Validate( Mission, Client, Task ) - - if timer.getTime() - self.StageStartTime <= self.StageStartDuration then - return 0 - else - self.StageStartTime = timer.getTime() - return 1 - end - - return 1 - -end - -STAGE_CARGO_LOAD = { - ClassName = "STAGE_CARGO_LOAD" -} - -function STAGE_CARGO_LOAD:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - return self -end - -function STAGE_CARGO_LOAD:Execute( Mission, Client, Task ) - self:F() - local Valid = BASE:Inherited(self):Execute( Mission, Client, Task ) - - for LoadCargoID, LoadCargo in pairs( Task.Cargos.LoadCargos ) do - LoadCargo:Load( Client ) - end - - if Mission.MissionReportFlash and Client:IsTransport() then - Client:ShowCargo() - end - - return Valid -end - -function STAGE_CARGO_LOAD:Validate( Mission, Client, Task ) - self:F() - local Valid = STAGE:Validate( Mission, Client, Task ) - - return 1 -end - - -STAGE_CARGO_INIT = { - ClassName = "STAGE_CARGO_INIT" -} - -function STAGE_CARGO_INIT:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - return self -end - -function STAGE_CARGO_INIT:Execute( Mission, Client, Task ) - self:F() - local Valid = BASE:Inherited(self):Execute( Mission, Client, Task ) - - for InitLandingZoneID, InitLandingZone in pairs( Task.LandingZones.LandingZones ) do - self:T( InitLandingZone ) - InitLandingZone:Spawn() - end - - - self:T( Task.Cargos.InitCargos ) - for InitCargoID, InitCargoData in pairs( Task.Cargos.InitCargos ) do - self:T( { InitCargoData } ) - InitCargoData:Spawn( Client ) - end - - return Valid -end - - -function STAGE_CARGO_INIT:Validate( Mission, Client, Task ) - self:F() - local Valid = STAGE:Validate( Mission, Client, Task ) - - return 1 -end - - - -STAGEROUTE = { - ClassName = "STAGEROUTE", - MSG = { ID = "Route", TIME = 5 }, - Frequency = STAGE.FREQUENCY.REPEAT, - Name = "Route" -} - -function STAGEROUTE:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - self.MessageSwitch = true - return self -end - - ---- Execute the routing. --- @param #STAGEROUTE self --- @param Mission#MISSION Mission --- @param Client#CLIENT Client --- @param Task#TASK Task -function STAGEROUTE:Execute( Mission, Client, Task ) - self:F() - local Valid = BASE:Inherited(self):Execute( Mission, Client, Task ) - - local RouteMessage = "Fly to: " - self:T( Task.LandingZones ) - for LandingZoneID, LandingZoneName in pairs( Task.LandingZones.LandingZoneNames ) do - RouteMessage = RouteMessage .. "\n " .. LandingZoneName .. ' at ' .. routines.getBRStringZone( { zone = LandingZoneName, ref = Client:GetClientGroupDCSUnit():getPoint(), true, true } ) .. ' km.' - end - - if Client:IsMultiSeated() then - Client:Message( RouteMessage, self.MSG.TIME, "Co-Pilot", 20, "Route" ) - else - Client:Message( RouteMessage, self.MSG.TIME, "Command", 20, "Route" ) - end - - - if Mission.MissionReportFlash and Client:IsTransport() then - Client:ShowCargo() - end - - return Valid -end - -function STAGEROUTE:Validate( Mission, Client, Task ) - self:F() - local Valid = STAGE:Validate( Mission, Client, Task ) - - -- check if the Client is in the landing zone - self:T( Task.LandingZones.LandingZoneNames ) - Task.CurrentLandingZoneName = routines.IsUnitNearZonesRadius( Client:GetClientGroupDCSUnit(), Task.LandingZones.LandingZoneNames, 500 ) - - if Task.CurrentLandingZoneName then - - Task.CurrentLandingZone = Task.LandingZones.LandingZones[Task.CurrentLandingZoneName].CargoZone - Task.CurrentCargoZone = Task.LandingZones.LandingZones[Task.CurrentLandingZoneName] - - if Task.CurrentCargoZone then - if not Task.Signalled then - Task.Signalled = Task.CurrentCargoZone:Signal() - end - end - - self:T( 1 ) - return 1 - end - - self:T( 0 ) - return 0 -end - - - -STAGELANDING = { - ClassName = "STAGELANDING", - MSG = { ID = "Landing", TIME = 10 }, - Name = "Landing", - Signalled = false -} - -function STAGELANDING:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - return self -end - ---- Execute the landing coordination. --- @param #STAGELANDING self --- @param Mission#MISSION Mission --- @param Client#CLIENT Client --- @param Task#TASK Task -function STAGELANDING:Execute( Mission, Client, Task ) - self:F() - - if Client:IsMultiSeated() then - Client:Message( "We have arrived at the landing zone.", self.MSG.TIME, "Co-Pilot" ) - else - Client:Message( "You have arrived at the landing zone.", self.MSG.TIME, "Command" ) - end - - Task.HostUnit = Task.CurrentCargoZone:GetHostUnit() - - self:T( { Task.HostUnit } ) - - if Task.HostUnit then - - Task.HostUnitName = Task.HostUnit:GetPrefix() - Task.HostUnitTypeName = Task.HostUnit:GetTypeName() - - local HostMessage = "" - Task.CargoNames = "" - - local IsFirst = true - - for CargoID, Cargo in pairs( CARGOS ) do - if Cargo.CargoType == Task.CargoType then - - if Cargo:IsLandingRequired() then - self:T( "Task for cargo " .. Cargo.CargoType .. " requires landing.") - Task.IsLandingRequired = true - end - - if Cargo:IsSlingLoad() then - self:T( "Task for cargo " .. Cargo.CargoType .. " is a slingload.") - Task.IsSlingLoad = true - end - - if IsFirst then - IsFirst = false - Task.CargoNames = Task.CargoNames .. Cargo.CargoName .. "( " .. Cargo.CargoWeight .. " )" - else - Task.CargoNames = Task.CargoNames .. "; " .. Cargo.CargoName .. "( " .. Cargo.CargoWeight .. " )" - end - end - end - - if Task.IsLandingRequired then - HostMessage = "Land the helicopter to " .. Task.TEXT[1] .. " " .. Task.CargoNames .. "." - else - HostMessage = "Use the Radio menu and F6 to find the cargo, then fly or land near the cargo and " .. Task.TEXT[1] .. " " .. Task.CargoNames .. "." - end - - local Host = "Command" - if Task.HostUnitName then - Host = Task.HostUnitName .. " (" .. Task.HostUnitTypeName .. ")" - else - if Client:IsMultiSeated() then - Host = "Co-Pilot" - end - end - - Client:Message( HostMessage, self.MSG.TIME, Host ) - - end -end - -function STAGELANDING:Validate( Mission, Client, Task ) - self:F() - - Task.CurrentLandingZoneName = routines.IsUnitNearZonesRadius( Client:GetClientGroupDCSUnit(), Task.LandingZones.LandingZoneNames, 500 ) - if Task.CurrentLandingZoneName then - - -- Client is in de landing zone. - self:T( Task.CurrentLandingZoneName ) - - Task.CurrentLandingZone = Task.LandingZones.LandingZones[Task.CurrentLandingZoneName].CargoZone - Task.CurrentCargoZone = Task.LandingZones.LandingZones[Task.CurrentLandingZoneName] - - if Task.CurrentCargoZone then - if not Task.Signalled then - Task.Signalled = Task.CurrentCargoZone:Signal() - end - end - else - if Task.CurrentLandingZone then - Task.CurrentLandingZone = nil - end - if Task.CurrentCargoZone then - Task.CurrentCargoZone = nil - end - Task.Signalled = false - Task:RemoveCargoMenus( Client ) - self:T( -1 ) - return -1 - end - - - local DCSUnitVelocityVec3 = Client:GetClientGroupDCSUnit():getVelocity() - local DCSUnitVelocity = ( DCSUnitVelocityVec3.x ^2 + DCSUnitVelocityVec3.y ^2 + DCSUnitVelocityVec3.z ^2 ) ^ 0.5 - - local DCSUnitPointVec3 = Client:GetClientGroupDCSUnit():getPoint() - local LandHeight = land.getHeight( { x = DCSUnitPointVec3.x, y = DCSUnitPointVec3.z } ) - local DCSUnitHeight = DCSUnitPointVec3.y - LandHeight - - self:T( { Task.IsLandingRequired, Client:GetClientGroupDCSUnit():inAir() } ) - if Task.IsLandingRequired and not Client:GetClientGroupDCSUnit():inAir() then - self:T( 1 ) - Task.IsInAirTestRequired = true - return 1 - end - - self:T( { DCSUnitVelocity, DCSUnitHeight, LandHeight, Task.CurrentCargoZone.SignalHeight } ) - if Task.IsLandingRequired and DCSUnitVelocity <= 0.05 and DCSUnitHeight <= Task.CurrentCargoZone.SignalHeight then - self:T( 1 ) - Task.IsInAirTestRequired = false - return 1 - end - - self:T( 0 ) - return 0 -end - -STAGELANDED = { - ClassName = "STAGELANDED", - MSG = { ID = "Land", TIME = 10 }, - Name = "Landed", - MenusAdded = false -} - -function STAGELANDED:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - return self -end - -function STAGELANDED:Execute( Mission, Client, Task ) - self:F() - - if Task.IsLandingRequired then - - local Host = "Command" - if Task.HostUnitName then - Host = Task.HostUnitName .. " (" .. Task.HostUnitTypeName .. ")" - else - if Client:IsMultiSeated() then - Host = "Co-Pilot" - end - end - - Client:Message( 'You have landed within the landing zone. Use the radio menu (F10) to ' .. Task.TEXT[1] .. ' the ' .. Task.CargoType .. '.', - self.MSG.TIME, Host ) - - if not self.MenusAdded then - Task.Cargo = nil - Task:RemoveCargoMenus( Client ) - Task:AddCargoMenus( Client, CARGOS, 250 ) - end - end -end - - - -function STAGELANDED:Validate( Mission, Client, Task ) - self:F() - - if not routines.IsUnitNearZonesRadius( Client:GetClientGroupDCSUnit(), Task.CurrentLandingZoneName, 500 ) then - self:T( "Client is not anymore in the landing zone, go back to stage Route, and remove cargo menus." ) - Task.Signalled = false - Task:RemoveCargoMenus( Client ) - self:T( -2 ) - return -2 - end - - local DCSUnitVelocityVec3 = Client:GetClientGroupDCSUnit():getVelocity() - local DCSUnitVelocity = ( DCSUnitVelocityVec3.x ^2 + DCSUnitVelocityVec3.y ^2 + DCSUnitVelocityVec3.z ^2 ) ^ 0.5 - - local DCSUnitPointVec3 = Client:GetClientGroupDCSUnit():getPoint() - local LandHeight = land.getHeight( { x = DCSUnitPointVec3.x, y = DCSUnitPointVec3.z } ) - local DCSUnitHeight = DCSUnitPointVec3.y - LandHeight - - self:T( { Task.IsLandingRequired, Client:GetClientGroupDCSUnit():inAir() } ) - if Task.IsLandingRequired and Task.IsInAirTestRequired == true and Client:GetClientGroupDCSUnit():inAir() then - self:T( "Client went back in the air. Go back to stage Landing." ) - self:T( -1 ) - return -1 - end - - self:T( { DCSUnitVelocity, DCSUnitHeight, LandHeight, Task.CurrentCargoZone.SignalHeight } ) - if Task.IsLandingRequired and Task.IsInAirTestRequired == false and DCSUnitVelocity >= 2 and DCSUnitHeight >= Task.CurrentCargoZone.SignalHeight then - self:T( "It seems the Client went back in the air and over the boundary limits. Go back to stage Landing." ) - self:T( -1 ) - return -1 - end - - -- Wait until cargo is selected from the menu. - if Task.IsLandingRequired then - if not Task.Cargo then - self:T( 0 ) - return 0 - end - end - - self:T( 1 ) - return 1 -end - -STAGEUNLOAD = { - ClassName = "STAGEUNLOAD", - MSG = { ID = "Unload", TIME = 10 }, - Name = "Unload" -} - -function STAGEUNLOAD:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - return self -end - ---- Coordinate UnLoading --- @param #STAGEUNLOAD self --- @param Mission#MISSION Mission --- @param Client#CLIENT Client --- @param Task#TASK Task -function STAGEUNLOAD:Execute( Mission, Client, Task ) - self:F() - - if Client:IsMultiSeated() then - Client:Message( 'The ' .. Task.CargoType .. ' are being ' .. Task.TEXT[2] .. ' within the landing zone. Wait until the helicopter is ' .. Task.TEXT[3] .. '.', - "Co-Pilot" ) - else - Client:Message( 'You are unloading the ' .. Task.CargoType .. ' ' .. Task.TEXT[2] .. ' within the landing zone. Wait until the helicopter is ' .. Task.TEXT[3] .. '.', - "Command" ) - end - Task:RemoveCargoMenus( Client ) -end - -function STAGEUNLOAD:Executing( Mission, Client, Task ) - self:F() - env.info( 'STAGEUNLOAD:Executing() Task.Cargo.CargoName = ' .. Task.Cargo.CargoName ) - - local TargetZoneName - - if Task.TargetZoneName then - TargetZoneName = Task.TargetZoneName - else - TargetZoneName = Task.CurrentLandingZoneName - end - - if Task.Cargo:UnLoad( Client, TargetZoneName ) then - Task.ExecuteStage = _TransportExecuteStage.SUCCESS - if Mission.MissionReportFlash then - Client:ShowCargo() - end - end -end - ---- Validate UnLoading --- @param #STAGEUNLOAD self --- @param Mission#MISSION Mission --- @param Client#CLIENT Client --- @param Task#TASK Task -function STAGEUNLOAD:Validate( Mission, Client, Task ) - self:F() - env.info( 'STAGEUNLOAD:Validate()' ) - - if routines.IsUnitNearZonesRadius( Client:GetClientGroupDCSUnit(), Task.CurrentLandingZoneName, 500 ) then - else - Task.ExecuteStage = _TransportExecuteStage.FAILED - Task:RemoveCargoMenus( Client ) - if Client:IsMultiSeated() then - Client:Message( 'The ' .. Task.CargoType .. " haven't been successfully " .. Task.TEXT[3] .. ' within the landing zone. Task and mission has failed.', - _TransportStageMsgTime.DONE, "Co-Pilot" ) - else - Client:Message( 'The ' .. Task.CargoType .. " haven't been successfully " .. Task.TEXT[3] .. ' within the landing zone. Task and mission has failed.', - _TransportStageMsgTime.DONE, "Command" ) - end - return 1 - end - - if not Client:GetClientGroupDCSUnit():inAir() then - else - Task.ExecuteStage = _TransportExecuteStage.FAILED - Task:RemoveCargoMenus( Client ) - if Client:IsMultiSeated() then - Client:Message( 'The ' .. Task.CargoType .. " haven't been successfully " .. Task.TEXT[3] .. ' within the landing zone. Task and mission has failed.', - _TransportStageMsgTime.DONE, "Co-Pilot" ) - else - Client:Message( 'The ' .. Task.CargoType .. " haven't been successfully " .. Task.TEXT[3] .. ' within the landing zone. Task and mission has failed.', - _TransportStageMsgTime.DONE, "Command" ) - end - return 1 - end - - if Task.ExecuteStage == _TransportExecuteStage.SUCCESS then - if Client:IsMultiSeated() then - Client:Message( 'The ' .. Task.CargoType .. ' have been sucessfully ' .. Task.TEXT[3] .. ' within the landing zone.', _TransportStageMsgTime.DONE, "Co-Pilot" ) - else - Client:Message( 'The ' .. Task.CargoType .. ' have been sucessfully ' .. Task.TEXT[3] .. ' within the landing zone.', _TransportStageMsgTime.DONE, "Command" ) - end - Task:RemoveCargoMenus( Client ) - Task.MissionTask:AddGoalCompletion( Task.MissionTask.GoalVerb, Task.CargoName, 1 ) -- We set the cargo as one more goal completed in the mission. - return 1 - end - - return 1 -end - -STAGELOAD = { - ClassName = "STAGELOAD", - MSG = { ID = "Load", TIME = 10 }, - Name = "Load" -} - -function STAGELOAD:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - return self -end - -function STAGELOAD:Execute( Mission, Client, Task ) - self:F() - - if not Task.IsSlingLoad then - - local Host = "Command" - if Task.HostUnitName then - Host = Task.HostUnitName .. " (" .. Task.HostUnitTypeName .. ")" - else - if Client:IsMultiSeated() then - Host = "Co-Pilot" - end - end - - Client:Message( 'The ' .. Task.CargoType .. ' are being ' .. Task.TEXT[2] .. ' within the landing zone. Wait until the helicopter is ' .. Task.TEXT[3] .. '.', - _TransportStageMsgTime.EXECUTING, Host ) - - -- Route the cargo to the Carrier - - Task.Cargo:OnBoard( Client, Task.CurrentCargoZone, Task.OnBoardSide ) - Task.ExecuteStage = _TransportExecuteStage.EXECUTING - else - Task.ExecuteStage = _TransportExecuteStage.EXECUTING - end -end - -function STAGELOAD:Executing( Mission, Client, Task ) - self:F() - - -- If the Cargo is ready to be loaded, load it into the Client. - - local Host = "Command" - if Task.HostUnitName then - Host = Task.HostUnitName .. " (" .. Task.HostUnitTypeName .. ")" - else - if Client:IsMultiSeated() then - Host = "Co-Pilot" - end - end - - if not Task.IsSlingLoad then - self:T( Task.Cargo.CargoName) - - if Task.Cargo:OnBoarded( Client, Task.CurrentCargoZone ) then - - -- Load the Cargo onto the Client - Task.Cargo:Load( Client ) - - -- Message to the pilot that cargo has been loaded. - Client:Message( "The cargo " .. Task.Cargo.CargoName .. " has been loaded in our helicopter.", - 20, Host ) - Task.ExecuteStage = _TransportExecuteStage.SUCCESS - - Client:ShowCargo() - end - else - Client:Message( "Hook the " .. Task.CargoNames .. " onto the helicopter " .. Task.TEXT[3] .. " within the landing zone.", - _TransportStageMsgTime.EXECUTING, Host ) - for CargoID, Cargo in pairs( CARGOS ) do - self:T( "Cargo.CargoName = " .. Cargo.CargoName ) - - if Cargo:IsSlingLoad() then - local CargoStatic = StaticObject.getByName( Cargo.CargoStaticName ) - if CargoStatic then - self:T( "Cargo is found in the DCS simulator.") - local CargoStaticPosition = CargoStatic:getPosition().p - self:T( "Cargo Position x = " .. CargoStaticPosition.x .. ", y = " .. CargoStaticPosition.y .. ", z = " .. CargoStaticPosition.z ) - local CargoStaticHeight = routines.GetUnitHeight( CargoStatic ) - if CargoStaticHeight > 5 then - self:T( "Cargo is airborne.") - Cargo:StatusLoaded() - Task.Cargo = Cargo - Client:Message( 'The Cargo has been successfully hooked onto the helicopter and is now being sling loaded. Fly outside the landing zone.', - self.MSG.TIME, Host ) - Task.ExecuteStage = _TransportExecuteStage.SUCCESS - break - end - else - self:T( "Cargo not found in the DCS simulator." ) - end - end - end - end - -end - -function STAGELOAD:Validate( Mission, Client, Task ) - self:F() - - self:T( "Task.CurrentLandingZoneName = " .. Task.CurrentLandingZoneName ) - - local Host = "Command" - if Task.HostUnitName then - Host = Task.HostUnitName .. " (" .. Task.HostUnitTypeName .. ")" - else - if Client:IsMultiSeated() then - Host = "Co-Pilot" - end - end - - if not Task.IsSlingLoad then - if not routines.IsUnitNearZonesRadius( Client:GetClientGroupDCSUnit(), Task.CurrentLandingZoneName, 500 ) then - Task:RemoveCargoMenus( Client ) - Task.ExecuteStage = _TransportExecuteStage.FAILED - Task.CargoName = nil - Client:Message( "The " .. Task.CargoType .. " loading has been aborted. You flew outside the pick-up zone while loading. ", - self.MSG.TIME, Host ) - self:T( -1 ) - return -1 - end - - local DCSUnitVelocityVec3 = Client:GetClientGroupDCSUnit():getVelocity() - local DCSUnitVelocity = ( DCSUnitVelocityVec3.x ^2 + DCSUnitVelocityVec3.y ^2 + DCSUnitVelocityVec3.z ^2 ) ^ 0.5 - - local DCSUnitPointVec3 = Client:GetClientGroupDCSUnit():getPoint() - local LandHeight = land.getHeight( { x = DCSUnitPointVec3.x, y = DCSUnitPointVec3.z } ) - local DCSUnitHeight = DCSUnitPointVec3.y - LandHeight - - self:T( { Task.IsLandingRequired, Client:GetClientGroupDCSUnit():inAir() } ) - if Task.IsLandingRequired and Task.IsInAirTestRequired == true and Client:GetClientGroupDCSUnit():inAir() then - Task:RemoveCargoMenus( Client ) - Task.ExecuteStage = _TransportExecuteStage.FAILED - Task.CargoName = nil - Client:Message( "The " .. Task.CargoType .. " loading has been aborted. Re-start the " .. Task.TEXT[3] .. " process. Don't fly outside the pick-up zone.", - self.MSG.TIME, Host ) - self:T( -1 ) - return -1 - end - - self:T( { DCSUnitVelocity, DCSUnitHeight, LandHeight, Task.CurrentCargoZone.SignalHeight } ) - if Task.IsLandingRequired and Task.IsInAirTestRequired == false and DCSUnitVelocity >= 2 and DCSUnitHeight >= Task.CurrentCargoZone.SignalHeight then - Task:RemoveCargoMenus( Client ) - Task.ExecuteStage = _TransportExecuteStage.FAILED - Task.CargoName = nil - Client:Message( "The " .. Task.CargoType .. " loading has been aborted. Re-start the " .. Task.TEXT[3] .. " process. Don't fly outside the pick-up zone.", - self.MSG.TIME, Host ) - self:T( -1 ) - return -1 - end - - if Task.ExecuteStage == _TransportExecuteStage.SUCCESS then - Task:RemoveCargoMenus( Client ) - Client:Message( "Good Job. The " .. Task.CargoType .. " has been sucessfully " .. Task.TEXT[3] .. " within the landing zone.", - self.MSG.TIME, Host ) - Task.MissionTask:AddGoalCompletion( Task.MissionTask.GoalVerb, Task.CargoName, 1 ) - self:T( 1 ) - return 1 - end - - else - if Task.ExecuteStage == _TransportExecuteStage.SUCCESS then - CargoStatic = StaticObject.getByName( Task.Cargo.CargoStaticName ) - if CargoStatic and not routines.IsStaticInZones( CargoStatic, Task.CurrentLandingZoneName ) then - Client:Message( "Good Job. The " .. Task.CargoType .. " has been sucessfully " .. Task.TEXT[3] .. " and flown outside of the landing zone.", - self.MSG.TIME, Host ) - Task.MissionTask:AddGoalCompletion( Task.MissionTask.GoalVerb, Task.Cargo.CargoName, 1 ) - self:T( 1 ) - return 1 - end - end - - end - - - self:T( 0 ) - return 0 -end - - -STAGEDONE = { - ClassName = "STAGEDONE", - MSG = { ID = "Done", TIME = 10 }, - Name = "Done" -} - -function STAGEDONE:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'AI' - return self -end - -function STAGEDONE:Execute( Mission, Client, Task ) - self:F() - -end - -function STAGEDONE:Validate( Mission, Client, Task ) - self:F() - - Task:Done() - - return 0 -end - -STAGEARRIVE = { - ClassName = "STAGEARRIVE", - MSG = { ID = "Arrive", TIME = 10 }, - Name = "Arrive" -} - -function STAGEARRIVE:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - return self -end - - ---- Execute Arrival --- @param #STAGEARRIVE self --- @param Mission#MISSION Mission --- @param Client#CLIENT Client --- @param Task#TASK Task -function STAGEARRIVE:Execute( Mission, Client, Task ) - self:F() - - if Client:IsMultiSeated() then - Client:Message( 'We have arrived at ' .. Task.CurrentLandingZoneName .. ".", self.MSG.TIME, "Co-Pilot" ) - else - Client:Message( 'We have arrived at ' .. Task.CurrentLandingZoneName .. ".", self.MSG.TIME, "Command" ) - end - -end - -function STAGEARRIVE:Validate( Mission, Client, Task ) - self:F() - - Task.CurrentLandingZoneID = routines.IsUnitInZones( Client:GetClientGroupDCSUnit(), Task.LandingZones ) - if ( Task.CurrentLandingZoneID ) then - else - return -1 - end - - return 1 -end - -STAGEGROUPSDESTROYED = { - ClassName = "STAGEGROUPSDESTROYED", - DestroyGroupSize = -1, - Frequency = STAGE.FREQUENCY.REPEAT, - MSG = { ID = "DestroyGroup", TIME = 10 }, - Name = "GroupsDestroyed" -} - -function STAGEGROUPSDESTROYED:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'AI' - return self -end - ---function STAGEGROUPSDESTROYED:Execute( Mission, Client, Task ) --- --- Client:Message( 'Task: Still ' .. DestroyGroupSize .. " of " .. Task.DestroyGroupCount .. " " .. Task.DestroyGroupType .. " to be destroyed!", self.MSG.TIME, Mission.Name .. "/Stage" ) --- ---end - -function STAGEGROUPSDESTROYED:Validate( Mission, Client, Task ) - self:F() - - if Task.MissionTask:IsGoalReached() then - return 1 - else - return 0 - end -end - -function STAGEGROUPSDESTROYED:Execute( Mission, Client, Task ) - self:F() - self:T( { Task.ClassName, Task.Destroyed } ) - --env.info( 'Event Table Task = ' .. tostring(Task) ) - -end - - - - - - - - - - - - - ---[[ - _TransportStage: Defines the different stages of which of transport missions can be in. This table is internal and is used to control the sequence of messages, actions and flow. - - - _TransportStage.START - - _TransportStage.ROUTE - - _TransportStage.LAND - - _TransportStage.EXECUTE - - _TransportStage.DONE - - _TransportStage.REMOVE ---]] -_TransportStage = { - HOLD = "HOLD", - START = "START", - ROUTE = "ROUTE", - LANDING = "LANDING", - LANDED = "LANDED", - EXECUTING = "EXECUTING", - LOAD = "LOAD", - UNLOAD = "UNLOAD", - DONE = "DONE", - NEXT = "NEXT" -} - -_TransportStageMsgTime = { - HOLD = 10, - START = 60, - ROUTE = 5, - LANDING = 10, - LANDED = 30, - EXECUTING = 30, - LOAD = 30, - UNLOAD = 30, - DONE = 30, - NEXT = 0 -} - -_TransportStageTime = { - HOLD = 10, - START = 5, - ROUTE = 5, - LANDING = 1, - LANDED = 1, - EXECUTING = 5, - LOAD = 5, - UNLOAD = 5, - DONE = 1, - NEXT = 0 -} - -_TransportStageAction = { - REPEAT = -1, - NONE = 0, - ONCE = 1 -} ---- The TASK Classes define major end-to-end activities within a MISSION. The TASK Class is the Master Class to orchestrate these activities. From this class, many concrete TASK classes are inherited. --- @module TASK - - - - - - - ---- The TASK class --- @type TASK --- @extends Base#BASE -TASK = { - - -- Defines the different signal types with a Task. - SIGNAL = { - COLOR = { - RED = { ID = 1, COLOR = trigger.smokeColor.Red, TEXT = "A red" }, - GREEN = { ID = 2, COLOR = trigger.smokeColor.Green, TEXT = "A green" }, - BLUE = { ID = 3, COLOR = trigger.smokeColor.Blue, TEXT = "A blue" }, - WHITE = { ID = 4, COLOR = trigger.smokeColor.White, TEXT = "A white" }, - ORANGE = { ID = 5, COLOR = trigger.smokeColor.Orange, TEXT = "An orange" } - }, - TYPE = { - SMOKE = { ID = 1, TEXT = "smoke" }, - FLARE = { ID = 2, TEXT = "flare" } - } - }, - ClassName = "TASK", - Mission = {}, -- Owning mission of the Task - Name = '', - Stages = {}, - Stage = {}, - Cargos = { - InitCargos = {}, - LoadCargos = {} - }, - LandingZones = { - LandingZoneNames = {}, - LandingZones = {} - }, - ActiveStage = 0, - TaskDone = false, - TaskFailed = false, - GoalTasks = {} -} - ---- Instantiates a new TASK Base. Should never be used. Interface Class. --- @return TASK -function TASK:New() - local self = BASE:Inherit( self, BASE:New() ) - self:F() - - -- assign Task default values during construction - self.TaskBriefing = "Task: No Task." - self.Time = timer.getTime() - self.ExecuteStage = _TransportExecuteStage.NONE - - return self -end - -function TASK:SetStage( StageSequenceIncrement ) - self:F( { StageSequenceIncrement } ) - - local Valid = false - if StageSequenceIncrement ~= 0 then - self.ActiveStage = self.ActiveStage + StageSequenceIncrement - if 1 <= self.ActiveStage and self.ActiveStage <= #self.Stages then - self.Stage = self.Stages[self.ActiveStage] - self:T( { self.Stage.Name } ) - self.Frequency = self.Stage.Frequency - Valid = true - else - Valid = false - env.info( "TASK:SetStage() self.ActiveStage is smaller or larger than self.Stages array. self.ActiveStage = " .. self.ActiveStage ) - end - end - self.Time = timer.getTime() - return Valid -end - -function TASK:Init() - self:F() - self.ActiveStage = 0 - self:SetStage(1) - self.TaskDone = false - self.TaskFailed = false -end - - ---- Get progress of a TASK. --- @return string GoalsText -function TASK:GetGoalProgress() - self:F2() - - local GoalsText = "" - for GoalVerb, GoalVerbData in pairs( self.GoalTasks ) do - local Goals = self:GetGoalCompletion( GoalVerb ) - if Goals and Goals ~= "" then - Goals = '(' .. Goals .. ')' - else - Goals = '( - )' - end - GoalsText = GoalsText .. GoalVerb .. ': ' .. self:GetGoalCount(GoalVerb) .. ' goals ' .. Goals .. ' of ' .. self:GetGoalTotal(GoalVerb) .. ' goals completed (' .. self:GetGoalPercentage(GoalVerb) .. '%); ' - end - - if GoalsText == "" then - GoalsText = "( - )" - end - - return GoalsText -end - ---- Show progress of a TASK. --- @param MISSION Mission Group structure describing the Mission. --- @param CLIENT Client Group structure describing the Client. -function TASK:ShowGoalProgress( Mission, Client ) - self:F2() - - local GoalsText = "" - for GoalVerb, GoalVerbData in pairs( self.GoalTasks ) do - if Mission:IsCompleted() then - else - local Goals = self:GetGoalCompletion( GoalVerb ) - if Goals and Goals ~= "" then - else - Goals = "-" - end - GoalsText = GoalsText .. self:GetGoalProgress() - end - end - - if Mission.MissionReportFlash or Mission.MissionReportShow then - Client:Message( GoalsText, 10, "Mission Command: Task Status", 30, "Task status" ) - end -end - ---- Sets a TASK to status Done. -function TASK:Done() - self:F2() - self.TaskDone = true -end - ---- Returns if a TASK is done. --- @return bool -function TASK:IsDone() - self:F2( self.TaskDone ) - return self.TaskDone -end - ---- Sets a TASK to status failed. -function TASK:Failed() - self:F() - self.TaskFailed = true -end - ---- Returns if a TASk has failed. --- @return bool -function TASK:IsFailed() - self:F2( self.TaskFailed ) - return self.TaskFailed -end - -function TASK:Reset( Mission, Client ) - self:F2() - self.ExecuteStage = _TransportExecuteStage.NONE -end - ---- Returns the Goals of a TASK --- @return @table Goals -function TASK:GetGoals() - return self.GoalTasks -end - ---- Returns if a TASK has Goal(s). --- @param #TASK self --- @param #string GoalVerb is the name of the Goal of the TASK. --- @return bool -function TASK:Goal( GoalVerb ) - self:F2( { GoalVerb } ) - if not GoalVerb then - GoalVerb = self.GoalVerb - end - self:T2( {self.GoalTasks[GoalVerb] } ) - if self.GoalTasks[GoalVerb] and self.GoalTasks[GoalVerb].GoalTotal > 0 then - return true - else - return false - end -end - ---- Sets the total Goals to be achieved of the Goal Name --- @param number GoalTotal is the number of times the GoalVerb needs to be achieved. --- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. -function TASK:SetGoalTotal( GoalTotal, GoalVerb ) - self:F2( { GoalTotal, GoalVerb } ) - - if not GoalVerb then - GoalVerb = self.GoalVerb - end - self.GoalTasks[GoalVerb] = {} - self.GoalTasks[GoalVerb].Goals = {} - self.GoalTasks[GoalVerb].GoalTotal = GoalTotal - self.GoalTasks[GoalVerb].GoalCount = 0 - return self -end - ---- Gets the total of Goals to be achieved within the TASK of the GoalVerb. --- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. -function TASK:GetGoalTotal( GoalVerb ) - self:F2( { GoalVerb } ) - if not GoalVerb then - GoalVerb = self.GoalVerb - end - if self:Goal( GoalVerb ) then - return self.GoalTasks[GoalVerb].GoalTotal - else - return 0 - end -end - ---- Sets the total of Goals currently achieved within the TASK of the GoalVerb. --- @param number GoalCount is the total number of Goals achieved within the TASK. --- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. --- @return TASK -function TASK:SetGoalCount( GoalCount, GoalVerb ) - self:F2() - if not GoalVerb then - GoalVerb = self.GoalVerb - end - if self:Goal( GoalVerb) then - self.GoalTasks[GoalVerb].GoalCount = GoalCount - end - return self -end - ---- Increments the total of Goals currently achieved within the TASK of the GoalVerb, with the given GoalCountIncrease. --- @param number GoalCountIncrease is the number of new Goals achieved within the TASK. --- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. --- @return TASK -function TASK:IncreaseGoalCount( GoalCountIncrease, GoalVerb ) - self:F2( { GoalCountIncrease, GoalVerb } ) - if not GoalVerb then - GoalVerb = self.GoalVerb - end - if self:Goal( GoalVerb) then - self.GoalTasks[GoalVerb].GoalCount = self.GoalTasks[GoalVerb].GoalCount + GoalCountIncrease - end - return self -end - ---- Gets the total of Goals currently achieved within the TASK of the GoalVerb. --- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. --- @return TASK -function TASK:GetGoalCount( GoalVerb ) - self:F2() - if not GoalVerb then - GoalVerb = self.GoalVerb - end - if self:Goal( GoalVerb ) then - return self.GoalTasks[GoalVerb].GoalCount - else - return 0 - end -end - ---- Gets the percentage of Goals currently achieved within the TASK of the GoalVerb. --- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. --- @return TASK -function TASK:GetGoalPercentage( GoalVerb ) - self:F2() - if not GoalVerb then - GoalVerb = self.GoalVerb - end - if self:Goal( GoalVerb ) then - return math.floor( self:GetGoalCount( GoalVerb ) / self:GetGoalTotal( GoalVerb ) * 100 + .5 ) - else - return 100 - end -end - ---- Returns if all the Goals of the TASK were achieved. --- @return bool -function TASK:IsGoalReached() - self:F2() - - local GoalReached = true - - for GoalVerb, Goals in pairs( self.GoalTasks ) do - self:T2( { "GoalVerb", GoalVerb } ) - if self:Goal( GoalVerb ) then - local GoalToDo = self:GetGoalTotal( GoalVerb ) - self:GetGoalCount( GoalVerb ) - self:T2( "GoalToDo = " .. GoalToDo ) - if GoalToDo <= 0 then - else - GoalReached = false - break - end - else - break - end - end - - self:T( { GoalReached, self.GoalTasks } ) - return GoalReached -end - ---- Adds an Additional Goal for the TASK to be achieved. --- @param string GoalVerb is the name of the Goal of the TASK. --- @param string GoalTask is a text describing the Goal of the TASK to be achieved. --- @param number GoalIncrease is a number by which the Goal achievement is increasing. -function TASK:AddGoalCompletion( GoalVerb, GoalTask, GoalIncrease ) - self:F2( { GoalVerb, GoalTask, GoalIncrease } ) - - if self:Goal( GoalVerb ) then - self.GoalTasks[GoalVerb].Goals[#self.GoalTasks[GoalVerb].Goals+1] = GoalTask - self.GoalTasks[GoalVerb].GoalCount = self.GoalTasks[GoalVerb].GoalCount + GoalIncrease - end - return self -end - ---- Returns if the additional Goal for the TASK was completed. --- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. --- @return string Goals -function TASK:GetGoalCompletion( GoalVerb ) - self:F2( { GoalVerb } ) - - if self:Goal( GoalVerb ) then - local Goals = "" - for GoalID, GoalName in pairs( self.GoalTasks[GoalVerb].Goals ) do Goals = Goals .. GoalName .. " + " end - return Goals:gsub(" + $", ""), self.GoalTasks[GoalVerb].GoalCount - end -end - -function TASK.MenuAction( Parameter ) - Parameter.ReferenceTask.ExecuteStage = _TransportExecuteStage.EXECUTING - Parameter.ReferenceTask.Cargo = Parameter.CargoTask -end - -function TASK:StageExecute() - self:F() - - local Execute = false - - if self.Frequency == STAGE.FREQUENCY.REPEAT then - Execute = true - elseif self.Frequency == STAGE.FREQUENCY.NONE then - Execute = false - elseif self.Frequency >= 0 then - Execute = true - self.Frequency = self.Frequency - 1 - end - - return Execute - -end - ---- Work function to set signal events within a TASK. -function TASK:AddSignal( SignalUnitNames, SignalType, SignalColor, SignalHeight ) - self:F() - - local Valid = true - - if Valid then - if type( SignalUnitNames ) == "table" then - self.LandingZoneSignalUnitNames = SignalUnitNames - else - self.LandingZoneSignalUnitNames = { SignalUnitNames } - end - self.LandingZoneSignalType = SignalType - self.LandingZoneSignalColor = SignalColor - self.Signalled = false - if SignalHeight ~= nil then - self.LandingZoneSignalHeight = SignalHeight - else - self.LandingZoneSignalHeight = 0 - end - - if self.TaskBriefing then - self.TaskBriefing = self.TaskBriefing .. " " .. SignalColor.TEXT .. " " .. SignalType.TEXT .. " will be fired when entering the landing zone." - end - end - - return Valid -end - ---- When the CLIENT is approaching the landing zone, a RED SMOKE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddSmokeRed( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.SMOKE, TASK.SIGNAL.COLOR.RED, SignalHeight ) -end - ---- When the CLIENT is approaching the landing zone, a GREEN SMOKE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddSmokeGreen( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.SMOKE, TASK.SIGNAL.COLOR.GREEN, SignalHeight ) -end - ---- When the CLIENT is approaching the landing zone, a BLUE SMOKE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddSmokeBlue( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.SMOKE, TASK.SIGNAL.COLOR.BLUE, SignalHeight ) -end - ---- When the CLIENT is approaching the landing zone, a WHITE SMOKE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddSmokeWhite( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.SMOKE, TASK.SIGNAL.COLOR.WHITE, SignalHeight ) -end - ---- When the CLIENT is approaching the landing zone, an ORANGE SMOKE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddSmokeOrange( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.SMOKE, TASK.SIGNAL.COLOR.ORANGE, SignalHeight ) -end - ---- When the CLIENT is approaching the landing zone, a RED FLARE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddFlareRed( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.FLARE, TASK.SIGNAL.COLOR.RED, SignalHeight ) -end - ---- When the CLIENT is approaching the landing zone, a GREEN FLARE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddFlareGreen( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.FLARE, TASK.SIGNAL.COLOR.GREEN, SignalHeight ) -end - ---- When the CLIENT is approaching the landing zone, a BLUE FLARE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddFlareBlue( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.FLARE, TASK.SIGNAL.COLOR.BLUE, SignalHeight ) -end - ---- When the CLIENT is approaching the landing zone, a WHITE FLARE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddFlareWhite( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.FLARE, TASK.SIGNAL.COLOR.WHITE, SignalHeight ) -end - ---- When the CLIENT is approaching the landing zone, an ORANGE FLARE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddFlareOrange( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.FLARE, TASK.SIGNAL.COLOR.ORANGE, SignalHeight ) -end ---- A GOHOMETASK orchestrates the travel back to the home base, which is a specific zone defined within the ME. --- @module GOHOMETASK - ---- The GOHOMETASK class --- @type -GOHOMETASK = { - ClassName = "GOHOMETASK", -} - ---- Creates a new GOHOMETASK. --- @param table{string,...}|string LandingZones Table of Landing Zone names where Home(s) are located. --- @return GOHOMETASK -function GOHOMETASK:New( LandingZones ) - local self = BASE:Inherit( self, TASK:New() ) - self:F( { LandingZones } ) - local Valid = true - - Valid = routines.ValidateZone( LandingZones, "LandingZones", Valid ) - - if Valid then - self.Name = 'Fly Home' - self.TaskBriefing = "Task: Fly back to your home base. Your co-pilot will provide you with the directions (required flight angle in degrees) and the distance (in km) to your home base." - if type( LandingZones ) == "table" then - self.LandingZones = LandingZones - else - self.LandingZones = { LandingZones } - end - self.Stages = { STAGEBRIEF:New(), STAGESTART:New(), STAGEROUTE:New(), STAGEARRIVE:New(), STAGEDONE:New() } - self.SetStage( self, 1 ) - end - - return self -end ---- A DESTROYBASETASK will monitor the destruction of Groups and Units. This is a BASE class, other classes are derived from this class. --- @module DESTROYBASETASK --- @see DESTROYGROUPSTASK --- @see DESTROYUNITTYPESTASK --- @see DESTROY_RADARS_TASK - - - ---- The DESTROYBASETASK class --- @type DESTROYBASETASK -DESTROYBASETASK = { - ClassName = "DESTROYBASETASK", - Destroyed = 0, - GoalVerb = "Destroy", - DestroyPercentage = 100, -} - ---- Creates a new DESTROYBASETASK. --- @param #DESTROYBASETASK self --- @param #string DestroyGroupType Text describing the group to be destroyed. f.e. "Radar Installations", "Ships", "Vehicles", "Command Centers". --- @param #string DestroyUnitType Text describing the unit types to be destroyed. f.e. "SA-6", "Row Boats", "Tanks", "Tents". --- @param #list<#string> DestroyGroupPrefixes Table of Prefixes of the Groups to be destroyed before task is completed. --- @param #number DestroyPercentage defines the %-tage that needs to be destroyed to achieve mission success. eg. If in the Group there are 10 units, then a value of 75 would require 8 units to be destroyed from the Group to complete the @{TASK}. --- @return DESTROYBASETASK -function DESTROYBASETASK:New( DestroyGroupType, DestroyUnitType, DestroyGroupPrefixes, DestroyPercentage ) - local self = BASE:Inherit( self, TASK:New() ) - self:F() - - self.Name = 'Destroy' - self.Destroyed = 0 - self.DestroyGroupPrefixes = DestroyGroupPrefixes - self.DestroyGroupType = DestroyGroupType - self.DestroyUnitType = DestroyUnitType - if DestroyPercentage then - self.DestroyPercentage = DestroyPercentage - end - self.TaskBriefing = "Task: Destroy " .. DestroyGroupType .. "." - self.Stages = { STAGEBRIEF:New(), STAGESTART:New(), STAGEGROUPSDESTROYED:New(), STAGEDONE:New() } - self.SetStage( self, 1 ) - - return self -end - ---- Handle the S_EVENT_DEAD events to validate the destruction of units for the task monitoring. --- @param #DESTROYBASETASK self --- @param Event#EVENTDATA Event structure of MOOSE. -function DESTROYBASETASK:EventDead( Event ) - self:F( { Event } ) - - if Event.IniDCSUnit then - local DestroyUnit = Event.IniDCSUnit - local DestroyUnitName = Event.IniDCSUnitName - local DestroyGroup = Event.IniDCSGroup - local DestroyGroupName = Event.IniDCSGroupName - - --TODO: I need to fix here if 2 groups in the mission have a similar name with GroupPrefix equal, then i should differentiate for which group the goal was reached! - --I may need to test if for the goalverb that group goal was reached or something. Need to think about it a bit more ... - local UnitsDestroyed = 0 - for DestroyGroupPrefixID, DestroyGroupPrefix in pairs( self.DestroyGroupPrefixes ) do - self:T( DestroyGroupPrefix ) - if string.find( DestroyGroupName, DestroyGroupPrefix, 1, true ) then - self:T( BASE:Inherited(self).ClassName ) - UnitsDestroyed = self:ReportGoalProgress( DestroyGroup, DestroyUnit ) - self:T( UnitsDestroyed ) - end - end - - self:T( { UnitsDestroyed } ) - self:IncreaseGoalCount( UnitsDestroyed, self.GoalVerb ) - end - -end - ---- Validate task completeness of DESTROYBASETASK. --- @param DestroyGroup Group structure describing the group to be evaluated. --- @param DestroyUnit Unit structure describing the Unit to be evaluated. -function DESTROYBASETASK:ReportGoalProgress( DestroyGroup, DestroyUnit ) - self:F() - - return 0 -end ---- DESTROYGROUPSTASK --- @module DESTROYGROUPSTASK - - - ---- The DESTROYGROUPSTASK class --- @type -DESTROYGROUPSTASK = { - ClassName = "DESTROYGROUPSTASK", - GoalVerb = "Destroy Groups", -} - ---- Creates a new DESTROYGROUPSTASK. --- @param #DESTROYGROUPSTASK self --- @param #string DestroyGroupType String describing the group to be destroyed. --- @param #string DestroyUnitType String describing the unit to be destroyed. --- @param #list<#string> DestroyGroupNames Table of string containing the name of the groups to be destroyed before task is completed. --- @param #number DestroyPercentage defines the %-tage that needs to be destroyed to achieve mission success. eg. If in the Group there are 10 units, then a value of 75 would require 8 units to be destroyed from the Group to complete the @{TASK}. ----@return DESTROYGROUPSTASK -function DESTROYGROUPSTASK:New( DestroyGroupType, DestroyUnitType, DestroyGroupNames, DestroyPercentage ) - local self = BASE:Inherit( self, DESTROYBASETASK:New( DestroyGroupType, DestroyUnitType, DestroyGroupNames, DestroyPercentage ) ) - self:F() - - self.Name = 'Destroy Groups' - self.GoalVerb = "Destroy " .. DestroyGroupType - - _EVENTDISPATCHER:OnDead( self.EventDead , self ) - _EVENTDISPATCHER:OnCrash( self.EventDead , self ) - - return self -end - ---- Report Goal Progress. --- @param #DESTROYGROUPSTASK self --- @param DCSGroup#Group DestroyGroup Group structure describing the group to be evaluated. --- @param DCSUnit#Unit DestroyUnit Unit structure describing the Unit to be evaluated. --- @return #number The DestroyCount reflecting the amount of units destroyed within the group. -function DESTROYGROUPSTASK:ReportGoalProgress( DestroyGroup, DestroyUnit ) - self:F( { DestroyGroup, DestroyUnit, self.DestroyPercentage } ) - - local DestroyGroupSize = DestroyGroup:getSize() - 1 -- When a DEAD event occurs, the getSize is still one larger than the destroyed unit. - local DestroyGroupInitialSize = DestroyGroup:getInitialSize() - self:T( { DestroyGroupSize, DestroyGroupInitialSize - ( DestroyGroupInitialSize * self.DestroyPercentage / 100 ) } ) - - local DestroyCount = 0 - if DestroyGroup then - if DestroyGroupSize <= DestroyGroupInitialSize - ( DestroyGroupInitialSize * self.DestroyPercentage / 100 ) then - DestroyCount = 1 - end - else - DestroyCount = 1 - end - - self:T( DestroyCount ) - - return DestroyCount -end ---- Task class to destroy radar installations. --- @module DESTROYRADARSTASK - - - ---- The DESTROYRADARS class --- @type -DESTROYRADARSTASK = { - ClassName = "DESTROYRADARSTASK", - GoalVerb = "Destroy Radars" -} - ---- Creates a new DESTROYRADARSTASK. --- @param table{string,...} DestroyGroupNames Table of string containing the group names of which the radars are be destroyed. --- @return DESTROYRADARSTASK -function DESTROYRADARSTASK:New( DestroyGroupNames ) - local self = BASE:Inherit( self, DESTROYGROUPSTASK:New( 'radar installations', 'radars', DestroyGroupNames ) ) - self:F() - - self.Name = 'Destroy Radars' - - _EVENTDISPATCHER:OnDead( self.EventDead , self ) - - return self -end - ---- Report Goal Progress. --- @param Group DestroyGroup Group structure describing the group to be evaluated. --- @param Unit DestroyUnit Unit structure describing the Unit to be evaluated. -function DESTROYRADARSTASK:ReportGoalProgress( DestroyGroup, DestroyUnit ) - self:F( { DestroyGroup, DestroyUnit } ) - - local DestroyCount = 0 - if DestroyUnit and DestroyUnit:hasSensors( Unit.SensorType.RADAR, Unit.RadarType.AS ) then - if DestroyUnit and DestroyUnit:getLife() <= 1.0 then - self:T( 'Destroyed a radar' ) - DestroyCount = 1 - end - end - return DestroyCount -end ---- Set TASK to destroy certain unit types. --- @module DESTROYUNITTYPESTASK - - - ---- The DESTROYUNITTYPESTASK class --- @type -DESTROYUNITTYPESTASK = { - ClassName = "DESTROYUNITTYPESTASK", - GoalVerb = "Destroy", -} - ---- Creates a new DESTROYUNITTYPESTASK. --- @param string DestroyGroupType String describing the group to be destroyed. f.e. "Radar Installations", "Fleet", "Batallion", "Command Centers". --- @param string DestroyUnitType String describing the unit to be destroyed. f.e. "radars", "ships", "tanks", "centers". --- @param table{string,...} DestroyGroupNames Table of string containing the group names of which the radars are be destroyed. --- @param string DestroyUnitTypes Table of string containing the type names of the units to achieve mission success. --- @return DESTROYUNITTYPESTASK -function DESTROYUNITTYPESTASK:New( DestroyGroupType, DestroyUnitType, DestroyGroupNames, DestroyUnitTypes ) - local self = BASE:Inherit( self, DESTROYBASETASK:New( DestroyGroupType, DestroyUnitType, DestroyGroupNames ) ) - self:F( { DestroyGroupType, DestroyUnitType, DestroyGroupNames, DestroyUnitTypes } ) - - if type(DestroyUnitTypes) == 'table' then - self.DestroyUnitTypes = DestroyUnitTypes - else - self.DestroyUnitTypes = { DestroyUnitTypes } - end - - self.Name = 'Destroy Unit Types' - self.GoalVerb = "Destroy " .. DestroyGroupType - - _EVENTDISPATCHER:OnDead( self.EventDead , self ) - - return self -end - ---- Report Goal Progress. --- @param Group DestroyGroup Group structure describing the group to be evaluated. --- @param Unit DestroyUnit Unit structure describing the Unit to be evaluated. -function DESTROYUNITTYPESTASK:ReportGoalProgress( DestroyGroup, DestroyUnit ) - self:F( { DestroyGroup, DestroyUnit } ) - - local DestroyCount = 0 - for UnitTypeID, UnitType in pairs( self.DestroyUnitTypes ) do - if DestroyUnit and DestroyUnit:getTypeName() == UnitType then - if DestroyUnit and DestroyUnit:getLife() <= 1.0 then - DestroyCount = DestroyCount + 1 - end - end - end - return DestroyCount -end ---- A PICKUPTASK orchestrates the loading of CARGO at a specific landing zone. --- @module PICKUPTASK --- @parent TASK - ---- The PICKUPTASK class --- @type -PICKUPTASK = { - ClassName = "PICKUPTASK", - TEXT = { "Pick-Up", "picked-up", "loaded" }, - GoalVerb = "Pick-Up" -} - ---- Creates a new PICKUPTASK. --- @param table{string,...}|string LandingZones Table of Zone names where Cargo is to be loaded. --- @param CARGO_TYPE CargoType Type of the Cargo. The type must be of the following Enumeration:.. --- @param number OnBoardSide Reflects from which side the cargo Group will be on-boarded on the Carrier. -function PICKUPTASK:New( CargoType, OnBoardSide ) - local self = BASE:Inherit( self, TASK:New() ) - self:F() - - -- self holds the inherited instance of the PICKUPTASK Class to the BASE class. - - local Valid = true - - if Valid then - self.Name = 'Pickup Cargo' - self.TaskBriefing = "Task: Fly to the indicated landing zones and pickup " .. CargoType .. ". Your co-pilot will provide you with the directions (required flight angle in degrees) and the distance (in km) to the pickup zone." - self.CargoType = CargoType - self.GoalVerb = CargoType .. " " .. self.GoalVerb - self.OnBoardSide = OnBoardSide - self.IsLandingRequired = true -- required to decide whether the client needs to land or not - self.IsSlingLoad = false -- Indicates whether the cargo is a sling load cargo - self.Stages = { STAGE_CARGO_INIT:New(), STAGE_CARGO_LOAD:New(), STAGEBRIEF:New(), STAGESTART:New(), STAGEROUTE:New(), STAGELANDING:New(), STAGELANDED:New(), STAGELOAD:New(), STAGEDONE:New() } - self.SetStage( self, 1 ) - end - - return self -end - -function PICKUPTASK:FromZone( LandingZone ) - self:F() - - self.LandingZones.LandingZoneNames[LandingZone.CargoZoneName] = LandingZone.CargoZoneName - self.LandingZones.LandingZones[LandingZone.CargoZoneName] = LandingZone - - return self -end - -function PICKUPTASK:InitCargo( InitCargos ) - self:F( { InitCargos } ) - - if type( InitCargos ) == "table" then - self.Cargos.InitCargos = InitCargos - else - self.Cargos.InitCargos = { InitCargos } - end - - return self -end - -function PICKUPTASK:LoadCargo( LoadCargos ) - self:F( { LoadCargos } ) - - if type( LoadCargos ) == "table" then - self.Cargos.LoadCargos = LoadCargos - else - self.Cargos.LoadCargos = { LoadCargos } - end - - return self -end - -function PICKUPTASK:AddCargoMenus( Client, Cargos, TransportRadius ) - self:F() - - for CargoID, Cargo in pairs( Cargos ) do - - self:T( { Cargo.ClassName, Cargo.CargoName, Cargo.CargoType, Cargo:IsStatusNone(), Cargo:IsStatusLoaded(), Cargo:IsStatusLoading(), Cargo:IsStatusUnLoaded() } ) - - -- If the Cargo has no status, allow the menu option. - if Cargo:IsStatusNone() or ( Cargo:IsStatusLoading() and Client == Cargo:IsLoadingToClient() ) then - - local MenuAdd = false - if Cargo:IsNear( Client, self.CurrentCargoZone ) then - MenuAdd = true - end - - if MenuAdd then - if Client._Menus[Cargo.CargoType] == nil then - Client._Menus[Cargo.CargoType] = {} - end - - if not Client._Menus[Cargo.CargoType].PickupMenu then - Client._Menus[Cargo.CargoType].PickupMenu = missionCommands.addSubMenuForGroup( - Client:GetClientGroupID(), - self.TEXT[1] .. " " .. Cargo.CargoType, - nil - ) - self:T( 'Added PickupMenu: ' .. self.TEXT[1] .. " " .. Cargo.CargoType ) - end - - if Client._Menus[Cargo.CargoType].PickupSubMenus == nil then - Client._Menus[Cargo.CargoType].PickupSubMenus = {} - end - - Client._Menus[Cargo.CargoType].PickupSubMenus[ #Client._Menus[Cargo.CargoType].PickupSubMenus + 1 ] = missionCommands.addCommandForGroup( - Client:GetClientGroupID(), - Cargo.CargoName .. " ( " .. Cargo.CargoWeight .. "kg )", - Client._Menus[Cargo.CargoType].PickupMenu, - self.MenuAction, - { ReferenceTask = self, CargoTask = Cargo } - ) - self:T( 'Added PickupSubMenu' .. Cargo.CargoType .. ":" .. Cargo.CargoName .. " ( " .. Cargo.CargoWeight .. "kg )" ) - end - end - end - -end - -function PICKUPTASK:RemoveCargoMenus( Client ) - self:F() - - for MenuID, MenuData in pairs( Client._Menus ) do - for SubMenuID, SubMenuData in pairs( MenuData.PickupSubMenus ) do - missionCommands.removeItemForGroup( Client:GetClientGroupID(), SubMenuData ) - self:T( "Removed PickupSubMenu " ) - SubMenuData = nil - end - if MenuData.PickupMenu then - missionCommands.removeItemForGroup( Client:GetClientGroupID(), MenuData.PickupMenu ) - self:T( "Removed PickupMenu " ) - MenuData.PickupMenu = nil - end - end - - for CargoID, Cargo in pairs( CARGOS ) do - self:T( { Cargo.ClassName, Cargo.CargoName, Cargo.CargoType, Cargo:IsStatusNone(), Cargo:IsStatusLoaded(), Cargo:IsStatusLoading(), Cargo:IsStatusUnLoaded() } ) - if Cargo:IsStatusLoading() and Client == Cargo:IsLoadingToClient() then - Cargo:StatusNone() - end - end - -end - - - -function PICKUPTASK:HasFailed( ClientDead ) - self:F() - - local TaskHasFailed = self.TaskFailed - return TaskHasFailed -end - ---- A DEPLOYTASK orchestrates the deployment of CARGO within a specific landing zone. --- @module DEPLOYTASK - - - ---- A DeployTask --- @type DEPLOYTASK -DEPLOYTASK = { - ClassName = "DEPLOYTASK", - TEXT = { "Deploy", "deployed", "unloaded" }, - GoalVerb = "Deployment" -} - - ---- Creates a new DEPLOYTASK object, which models the sequence of STAGEs to unload a cargo. --- @function [parent=#DEPLOYTASK] New --- @param #string CargoType Type of the Cargo. --- @return #DEPLOYTASK The created DeployTask -function DEPLOYTASK:New( CargoType ) - local self = BASE:Inherit( self, TASK:New() ) - self:F() - - local Valid = true - - if Valid then - self.Name = 'Deploy Cargo' - self.TaskBriefing = "Fly to one of the indicated landing zones and deploy " .. CargoType .. ". Your co-pilot will provide you with the directions (required flight angle in degrees) and the distance (in km) to the deployment zone." - self.CargoType = CargoType - self.GoalVerb = CargoType .. " " .. self.GoalVerb - self.Stages = { STAGE_CARGO_INIT:New(), STAGE_CARGO_LOAD:New(), STAGEBRIEF:New(), STAGESTART:New(), STAGEROUTE:New(), STAGELANDING:New(), STAGELANDED:New(), STAGEUNLOAD:New(), STAGEDONE:New() } - self.SetStage( self, 1 ) - end - - return self -end - -function DEPLOYTASK:ToZone( LandingZone ) - self:F() - - self.LandingZones.LandingZoneNames[LandingZone.CargoZoneName] = LandingZone.CargoZoneName - self.LandingZones.LandingZones[LandingZone.CargoZoneName] = LandingZone - - return self -end - - -function DEPLOYTASK:InitCargo( InitCargos ) - self:F( { InitCargos } ) - - if type( InitCargos ) == "table" then - self.Cargos.InitCargos = InitCargos - else - self.Cargos.InitCargos = { InitCargos } - end - - return self -end - - -function DEPLOYTASK:LoadCargo( LoadCargos ) - self:F( { LoadCargos } ) - - if type( LoadCargos ) == "table" then - self.Cargos.LoadCargos = LoadCargos - else - self.Cargos.LoadCargos = { LoadCargos } - end - - return self -end - - ---- When the cargo is unloaded, it will move to the target zone name. --- @param string TargetZoneName Name of the Zone to where the Cargo should move after unloading. -function DEPLOYTASK:SetCargoTargetZoneName( TargetZoneName ) - self:F() - - local Valid = true - - Valid = routines.ValidateString( TargetZoneName, "TargetZoneName", Valid ) - - if Valid then - self.TargetZoneName = TargetZoneName - end - - return Valid - -end - -function DEPLOYTASK:AddCargoMenus( Client, Cargos, TransportRadius ) - self:F() - - local ClientGroupID = Client:GetClientGroupID() - - self:T( ClientGroupID ) - - for CargoID, Cargo in pairs( Cargos ) do - - self:T( { Cargo.ClassName, Cargo.CargoName, Cargo.CargoType, Cargo.CargoWeight } ) - - if Cargo:IsStatusLoaded() and Client == Cargo:IsLoadedInClient() then - - if Client._Menus[Cargo.CargoType] == nil then - Client._Menus[Cargo.CargoType] = {} - end - - if not Client._Menus[Cargo.CargoType].DeployMenu then - Client._Menus[Cargo.CargoType].DeployMenu = missionCommands.addSubMenuForGroup( - ClientGroupID, - self.TEXT[1] .. " " .. Cargo.CargoType, - nil - ) - self:T( 'Added DeployMenu ' .. self.TEXT[1] ) - end - - if Client._Menus[Cargo.CargoType].DeploySubMenus == nil then - Client._Menus[Cargo.CargoType].DeploySubMenus = {} - end - - if Client._Menus[Cargo.CargoType].DeployMenu == nil then - self:T( 'deploymenu is nil' ) - end - - Client._Menus[Cargo.CargoType].DeploySubMenus[ #Client._Menus[Cargo.CargoType].DeploySubMenus + 1 ] = missionCommands.addCommandForGroup( - ClientGroupID, - Cargo.CargoName .. " ( " .. Cargo.CargoWeight .. "kg )", - Client._Menus[Cargo.CargoType].DeployMenu, - self.MenuAction, - { ReferenceTask = self, CargoTask = Cargo } - ) - self:T( 'Added DeploySubMenu ' .. Cargo.CargoType .. ":" .. Cargo.CargoName .. " ( " .. Cargo.CargoWeight .. "kg )" ) - end - end - -end - -function DEPLOYTASK:RemoveCargoMenus( Client ) - self:F() - - local ClientGroupID = Client:GetClientGroupID() - self:T( ClientGroupID ) - - for MenuID, MenuData in pairs( Client._Menus ) do - if MenuData.DeploySubMenus ~= nil then - for SubMenuID, SubMenuData in pairs( MenuData.DeploySubMenus ) do - missionCommands.removeItemForGroup( ClientGroupID, SubMenuData ) - self:T( "Removed DeploySubMenu " ) - SubMenuData = nil - end - end - if MenuData.DeployMenu then - missionCommands.removeItemForGroup( ClientGroupID, MenuData.DeployMenu ) - self:T( "Removed DeployMenu " ) - MenuData.DeployMenu = nil - end - end - -end ---- A NOTASK is a dummy activity... But it will show a Mission Briefing... --- @module NOTASK - ---- The NOTASK class --- @type -NOTASK = { - ClassName = "NOTASK", -} - ---- Creates a new NOTASK. -function NOTASK:New() - local self = BASE:Inherit( self, TASK:New() ) - self:F() - - local Valid = true - - if Valid then - self.Name = 'Nothing' - self.TaskBriefing = "Task: Execute your mission." - self.Stages = { STAGEBRIEF:New(), STAGESTART:New(), STAGEDONE:New() } - self.SetStage( self, 1 ) - end - - return self -end ---- A ROUTETASK orchestrates the travel to a specific zone defined within the ME. --- @module ROUTETASK - ---- The ROUTETASK class --- @type -ROUTETASK = { - ClassName = "ROUTETASK", - GoalVerb = "Route", -} - ---- Creates a new ROUTETASK. --- @param table{sring,...}|string LandingZones Table of Zone Names where the target is located. --- @param string TaskBriefing (optional) Defines a text describing the briefing of the task. --- @return ROUTETASK -function ROUTETASK:New( LandingZones, TaskBriefing ) - local self = BASE:Inherit( self, TASK:New() ) - self:F( { LandingZones, TaskBriefing } ) - - local Valid = true - - Valid = routines.ValidateZone( LandingZones, "LandingZones", Valid ) - - if Valid then - self.Name = 'Route To Zone' - if TaskBriefing then - self.TaskBriefing = TaskBriefing .. " Your co-pilot will provide you with the directions (required flight angle in degrees) and the distance (in km) to the target objective." - else - self.TaskBriefing = "Task: Fly to specified zone(s). Your co-pilot will provide you with the directions (required flight angle in degrees) and the distance (in km) to the target objective." - end - if type( LandingZones ) == "table" then - self.LandingZones = LandingZones - else - self.LandingZones = { LandingZones } - end - self.Stages = { STAGEBRIEF:New(), STAGESTART:New(), STAGEROUTE:New(), STAGEARRIVE:New(), STAGEDONE:New() } - self.SetStage( self, 1 ) - end - - return self -end - ---- A MISSION is the main owner of a Mission orchestration within MOOSE . The Mission framework orchestrates @{CLIENT}s, @{TASK}s, @{STAGE}s etc. --- A @{CLIENT} needs to be registered within the @{MISSION} through the function @{AddClient}. A @{TASK} needs to be registered within the @{MISSION} through the function @{AddTask}. --- @module Mission - ---- The MISSION class --- @type MISSION --- @extends Base#BASE --- @field #MISSION.Clients _Clients --- @field #string MissionBriefing -MISSION = { - ClassName = "MISSION", - Name = "", - MissionStatus = "PENDING", - _Clients = {}, - _Tasks = {}, - _ActiveTasks = {}, - GoalFunction = nil, - MissionReportTrigger = 0, - MissionProgressTrigger = 0, - MissionReportShow = false, - MissionReportFlash = false, - MissionTimeInterval = 0, - MissionCoalition = "", - SUCCESS = 1, - FAILED = 2, - REPEAT = 3, - _GoalTasks = {} -} - ---- @type MISSION.Clients --- @list - -function MISSION:Meta() - - local self = BASE:Inherit( self, BASE:New() ) - self:F() - - return self -end - ---- This is the main MISSION declaration method. Each Mission is like the master or a Mission orchestration between, Clients, Tasks, Stages etc. --- @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 string 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"... --- @return MISSION --- @usage --- -- Declare a few missions. --- local Mission = MISSIONSCHEDULER.AddMission( 'Russia Transport Troops SA-6', 'Operational', 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', 'Russia' ) --- local Mission = MISSIONSCHEDULER.AddMission( 'Patriots', 'Primary', 'Our intelligence reports that 3 Patriot SAM defense batteries are located near Ruisi, Kvarhiti and Gori.', 'Russia' ) --- local Mission = MISSIONSCHEDULER.AddMission( 'Package Delivery', 'Operational', 'In order to be in full control of the situation, we need you to deliver a very important package at a secret location. Fly undetected through the NATO defenses and deliver the secret package. The secret agent is located at waypoint 4.', 'Russia' ) --- local Mission = MISSIONSCHEDULER.AddMission( 'Rescue General', 'Tactical', 'Our intelligence has received a remote signal behind Gori. We believe it is a very important Russian General that was captured by Georgia. Go out there and rescue him! Ensure you stay out of the battle zone, keep south. Waypoint 4 is the location of our Russian General.', 'Russia' ) --- local Mission = MISSIONSCHEDULER.AddMission( 'NATO Transport Troops', 'Operational', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.', 'NATO' ) --- local Mission = MISSIONSCHEDULER.AddMission( 'SA-6 SAMs', 'Primary', 'Our intelligence reports that 3 SA-6 SAM defense batteries are located near Didmukha, Khetagurov and Berula. Eliminate the Russian SAMs.', 'NATO' ) --- local Mission = MISSIONSCHEDULER.AddMission( 'NATO Sling Load', 'Operational', 'Fly to the cargo pickup zone at Dzegvi or Kaspi, and sling the cargo to Soganlug airbase.', 'NATO' ) --- local Mission = MISSIONSCHEDULER.AddMission( 'Rescue secret agent', 'Tactical', 'In order to be in full control of the situation, we need you to rescue a secret agent from the woods behind enemy lines. Avoid the Russian defenses and rescue the agent. Keep south until Khasuri, and keep your eyes open for any SAM presence. The agent is located at waypoint 4 on your kneeboard.', 'NATO' ) -function MISSION:New( MissionName, MissionPriority, MissionBriefing, MissionCoalition ) - - self = MISSION:Meta() - self:T({ MissionName, MissionPriority, MissionBriefing, MissionCoalition }) - - local Valid = true - - Valid = routines.ValidateString( MissionName, "MissionName", Valid ) - Valid = routines.ValidateString( MissionPriority, "MissionPriority", Valid ) - Valid = routines.ValidateString( MissionBriefing, "MissionBriefing", Valid ) - Valid = routines.ValidateString( MissionCoalition, "MissionCoalition", Valid ) - - if Valid then - self.Name = MissionName - self.MissionPriority = MissionPriority - self.MissionBriefing = MissionBriefing - self.MissionCoalition = MissionCoalition - end - - return self -end - ---- Returns if a Mission has completed. --- @return bool -function MISSION:IsCompleted() - self:F() - return self.MissionStatus == "ACCOMPLISHED" -end - ---- Set a Mission to completed. -function MISSION:Completed() - self:F() - self.MissionStatus = "ACCOMPLISHED" - self:StatusToClients() -end - ---- Returns if a Mission is ongoing. --- treturn bool -function MISSION:IsOngoing() - self:F() - return self.MissionStatus == "ONGOING" -end - ---- Set a Mission to ongoing. -function MISSION:Ongoing() - self:F() - self.MissionStatus = "ONGOING" - --self:StatusToClients() -end - ---- Returns if a Mission is pending. --- treturn bool -function MISSION:IsPending() - self:F() - return self.MissionStatus == "PENDING" -end - ---- Set a Mission to pending. -function MISSION:Pending() - self:F() - self.MissionStatus = "PENDING" - self:StatusToClients() -end - ---- Returns if a Mission has failed. --- treturn bool -function MISSION:IsFailed() - self:F() - return self.MissionStatus == "FAILED" -end - ---- Set a Mission to failed. -function MISSION:Failed() - self:F() - self.MissionStatus = "FAILED" - self:StatusToClients() -end - ---- Send the status of the MISSION to all Clients. -function MISSION:StatusToClients() - self:F() - if self.MissionReportFlash then - for ClientID, Client in pairs( self._Clients ) do - Client:Message( self.MissionCoalition .. ' "' .. self.Name .. '": ' .. self.MissionStatus .. '! ( ' .. self.MissionPriority .. ' mission ) ', 10, "Mission Command: Mission Status") - end - end -end - ---- Handles the reporting. After certain time intervals, a MISSION report MESSAGE will be shown to All Players. -function MISSION:ReportTrigger() - self:F() - - if self.MissionReportShow == true then - self.MissionReportShow = false - return true - else - if self.MissionReportFlash == true then - if timer.getTime() >= self.MissionReportTrigger then - self.MissionReportTrigger = timer.getTime() + self.MissionTimeInterval - return true - else - return false - end - else - return false - end - end -end - ---- Report the status of all MISSIONs to all active Clients. -function MISSION:ReportToAll() - self:F() - - local AlivePlayers = '' - for ClientID, Client in pairs( self._Clients ) do - if Client:GetDCSGroup() then - if Client:GetClientGroupDCSUnit() then - if Client:GetClientGroupDCSUnit():getLife() > 0.0 then - if AlivePlayers == '' then - AlivePlayers = ' Players: ' .. Client:GetClientGroupDCSUnit():getPlayerName() - else - AlivePlayers = AlivePlayers .. ' / ' .. Client:GetClientGroupDCSUnit():getPlayerName() - end - end - end - end - end - local Tasks = self:GetTasks() - local TaskText = "" - for TaskID, TaskData in pairs( Tasks ) do - TaskText = TaskText .. " - Task " .. TaskID .. ": " .. TaskData.Name .. ": " .. TaskData:GetGoalProgress() .. "\n" - end - MESSAGE:New( self.MissionCoalition .. ' "' .. self.Name .. '": ' .. self.MissionStatus .. ' ( ' .. self.MissionPriority .. ' mission )' .. AlivePlayers .. "\n" .. TaskText:gsub("\n$",""), 10, "Mission Command: Mission Report" ):ToAll() -end - - ---- Add a goal function to a MISSION. Goal functions are called when a @{TASK} within a mission has been completed. --- @param function GoalFunction is the function defined by the mission designer to evaluate whether a certain goal has been reached after a @{TASK} finishes within the @{MISSION}. A GoalFunction must accept 2 parameters: Mission, Client, which contains the current MISSION object and the current CLIENT object respectively. --- @usage --- PatriotActivation = { --- { "US SAM Patriot Zerti", false }, --- { "US SAM Patriot Zegduleti", false }, --- { "US SAM Patriot Gvleti", false } --- } --- --- function DeployPatriotTroopsGoal( Mission, Client ) --- --- --- -- Check if the cargo is all deployed for mission success. --- for CargoID, CargoData in pairs( Mission._Cargos ) do --- if Group.getByName( CargoData.CargoGroupName ) then --- CargoGroup = Group.getByName( CargoData.CargoGroupName ) --- if CargoGroup then --- -- Check if the cargo is ready to activate --- CurrentLandingZoneID = routines.IsUnitInZones( CargoGroup:getUnits()[1], Mission:GetTask( 2 ).LandingZones ) -- The second task is the Deploytask to measure mission success upon --- if CurrentLandingZoneID then --- if PatriotActivation[CurrentLandingZoneID][2] == false then --- -- Now check if this is a new Mission Task to be completed... --- trigger.action.setGroupAIOn( Group.getByName( PatriotActivation[CurrentLandingZoneID][1] ) ) --- PatriotActivation[CurrentLandingZoneID][2] = true --- MessageToBlue( "Mission Command: Message to all airborne units! The " .. PatriotActivation[CurrentLandingZoneID][1] .. " is armed. Our air defenses are now stronger.", 60, "BLUE/PatriotDefense" ) --- MessageToRed( "Mission Command: Our satellite systems are detecting additional NATO air defenses. To all airborne units: Take care!!!", 60, "RED/PatriotDefense" ) --- Mission:GetTask( 2 ):AddGoalCompletion( "Patriots activated", PatriotActivation[CurrentLandingZoneID][1], 1 ) -- Register Patriot activation as part of mission goal. --- end --- end --- end --- end --- end --- end --- --- local Mission = MISSIONSCHEDULER.AddMission( 'NATO Transport Troops', 'Operational', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.', 'NATO' ) --- Mission:AddGoalFunction( DeployPatriotTroopsGoal ) -function MISSION:AddGoalFunction( GoalFunction ) - self:F() - self.GoalFunction = GoalFunction -end - ---- Register a new @{CLIENT} to participate within the mission. --- @param CLIENT Client is the @{CLIENT} object. The object must have been instantiated with @{CLIENT:New}. --- @return CLIENT --- @usage --- Add a number of Client objects to the Mission. --- Mission:AddClient( CLIENT:FindByName( 'US UH-1H*HOT-Deploy Troops 1', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'US UH-1H*RAMP-Deploy Troops 3', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'US UH-1H*HOT-Deploy Troops 2', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'US UH-1H*RAMP-Deploy Troops 4', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.' ):Transport() ) -function MISSION:AddClient( Client ) - self:F( { Client } ) - - local Valid = true - - if Valid then - self._Clients[Client.ClientName] = Client - end - - return Client -end - ---- Find a @{CLIENT} object within the @{MISSION} by its ClientName. --- @param CLIENT ClientName is a string defining the Client Group as defined within the ME. --- @return CLIENT --- @usage --- -- Seach for Client "Bomber" within the Mission. --- local BomberClient = Mission:FindClient( "Bomber" ) -function MISSION:FindClient( ClientName ) - self:F( { self._Clients[ClientName] } ) - return self._Clients[ClientName] -end - - ---- Register a @{TASK} to be completed within the @{MISSION}. Note that there can be multiple @{TASK}s registered to be completed. Each TASK can be set a certain Goal. The MISSION will not be completed until all Goals are reached. --- @param TASK Task is the @{TASK} object. The object must have been instantiated with @{TASK:New} or any of its inherited @{TASK}s. --- @param number TaskNumber is the sequence number of the TASK within the MISSION. This number does have to be chronological. --- @return TASK --- @usage --- -- Define a few tasks for the Mission. --- PickupZones = { "NATO Gold Pickup Zone", "NATO Titan Pickup Zone" } --- PickupSignalUnits = { "NATO Gold Coordination Center", "NATO Titan Coordination Center" } --- --- -- Assign the Pickup Task --- local PickupTask = PICKUPTASK:New( PickupZones, CARGO_TYPE.ENGINEERS, CLIENT.ONBOARDSIDE.LEFT ) --- PickupTask:AddSmokeBlue( PickupSignalUnits ) --- PickupTask:SetGoalTotal( 3 ) --- Mission:AddTask( PickupTask, 1 ) --- --- -- Assign the Deploy Task --- local PatriotActivationZones = { "US Patriot Battery 1 Activation", "US Patriot Battery 2 Activation", "US Patriot Battery 3 Activation" } --- local PatriotActivationZonesSmokeUnits = { "US SAM Patriot - Battery 1 Control", "US SAM Patriot - Battery 2 Control", "US SAM Patriot - Battery 3 Control" } --- local DeployTask = DEPLOYTASK:New( PatriotActivationZones, CARGO_TYPE.ENGINEERS ) --- --DeployTask:SetCargoTargetZoneName( 'US Troops Attack ' .. math.random(2) ) --- DeployTask:AddSmokeBlue( PatriotActivationZonesSmokeUnits ) --- DeployTask:SetGoalTotal( 3 ) --- DeployTask:SetGoalTotal( 3, "Patriots activated" ) --- Mission:AddTask( DeployTask, 2 ) - -function MISSION:AddTask( Task, TaskNumber ) - self:F() - - self._Tasks[TaskNumber] = Task - self._Tasks[TaskNumber]:EnableEvents() - self._Tasks[TaskNumber].ID = TaskNumber - - return Task - end - ---- Get the TASK idenified by the TaskNumber from the Mission. This function is useful in GoalFunctions. --- @param number TaskNumber is the number of the @{TASK} within the @{MISSION}. --- @return TASK --- @usage --- -- Get Task 2 from the Mission. --- Task2 = Mission:GetTask( 2 ) - -function MISSION:GetTask( TaskNumber ) - self:F() - - local Valid = true - - local Task = nil - - if type(TaskNumber) ~= "number" then - Valid = false - end - - if Valid then - Task = self._Tasks[TaskNumber] - end - - return Task -end - ---- Get all the TASKs from the Mission. This function is useful in GoalFunctions. --- @return {TASK,...} Structure of TASKS with the @{TASK} number as the key. --- @usage --- -- Get Tasks from the Mission. --- Tasks = Mission:GetTasks() --- env.info( "Task 2 Completion = " .. Tasks[2]:GetGoalPercentage() .. "%" ) -function MISSION:GetTasks() - self:F() - - return self._Tasks -end - - ---[[ - _TransportExecuteStage: Defines the different stages of Transport unload/load execution. This table is internal and is used to control the validity of Transport load/unload timing. - - - _TransportExecuteStage.EXECUTING - - _TransportExecuteStage.SUCCESS - - _TransportExecuteStage.FAILED - ---]] -_TransportExecuteStage = { - NONE = 0, - EXECUTING = 1, - SUCCESS = 2, - FAILED = 3 -} - - ---- The MISSIONSCHEDULER is an OBJECT and is the main scheduler of ALL active MISSIONs registered within this scheduler. It's workings are considered internal and is automatically created when the Mission.lua file is included. --- @type MISSIONSCHEDULER --- @field #MISSIONSCHEDULER.MISSIONS Missions -MISSIONSCHEDULER = { - Missions = {}, - MissionCount = 0, - TimeIntervalCount = 0, - TimeIntervalShow = 150, - TimeSeconds = 14400, - TimeShow = 5 -} - ---- @type MISSIONSCHEDULER.MISSIONS --- @list <#MISSION> Mission - ---- This is the main MISSIONSCHEDULER Scheduler function. It is considered internal and is automatically created when the Mission.lua file is included. -function MISSIONSCHEDULER.Scheduler() - - - -- loop through the missions in the TransportTasks - for MissionName, MissionData in pairs( MISSIONSCHEDULER.Missions ) do - - local Mission = MissionData -- #MISSION - - if not Mission:IsCompleted() then - - -- This flag will monitor if for this mission, there are clients alive. If this flag is still false at the end of the loop, the mission status will be set to Pending (if not Failed or Completed). - local ClientsAlive = false - - for ClientID, ClientData in pairs( Mission._Clients ) do - - local Client = ClientData -- Client#CLIENT - - if Client:IsAlive() then - - -- There is at least one Client that is alive... So the Mission status is set to Ongoing. - ClientsAlive = true - - -- If this Client was not registered as Alive before: - -- 1. We register the Client as Alive. - -- 2. We initialize the Client Tasks and make a link to the original Mission Task. - -- 3. We initialize the Cargos. - -- 4. We flag the Mission as Ongoing. - if not Client.ClientAlive then - Client.ClientAlive = true - Client.ClientBriefingShown = false - for TaskNumber, Task in pairs( Mission._Tasks ) do - -- Note that this a deepCopy. Each client must have their own Tasks with own Stages!!! - Client._Tasks[TaskNumber] = routines.utils.deepCopy( Mission._Tasks[TaskNumber] ) - -- Each MissionTask must point to the original Mission. - Client._Tasks[TaskNumber].MissionTask = Mission._Tasks[TaskNumber] - Client._Tasks[TaskNumber].Cargos = Mission._Tasks[TaskNumber].Cargos - Client._Tasks[TaskNumber].LandingZones = Mission._Tasks[TaskNumber].LandingZones - end - - Mission:Ongoing() - end - - - -- For each Client, check for each Task the state and evolve the mission. - -- This flag will indicate if the Task of the Client is Complete. - local TaskComplete = false - - for TaskNumber, Task in pairs( Client._Tasks ) do - - if not Task.Stage then - Task:SetStage( 1 ) - end - - - local TransportTime = timer.getTime() - - if not Task:IsDone() then - - if Task:Goal() then - Task:ShowGoalProgress( Mission, Client ) - end - - --env.info( 'Scheduler: Mission = ' .. Mission.Name .. ' / Client = ' .. Client.ClientName .. ' / Task = ' .. Task.Name .. ' / Stage = ' .. Task.ActiveStage .. ' - ' .. Task.Stage.Name .. ' - ' .. Task.Stage.StageType ) - - -- Action - if Task:StageExecute() then - Task.Stage:Execute( Mission, Client, Task ) - end - - -- Wait until execution is finished - if Task.ExecuteStage == _TransportExecuteStage.EXECUTING then - Task.Stage:Executing( Mission, Client, Task ) - end - - -- Validate completion or reverse to earlier stage - if Task.Time + Task.Stage.WaitTime <= TransportTime then - Task:SetStage( Task.Stage:Validate( Mission, Client, Task ) ) - end - - if Task:IsDone() then - --env.info( 'Scheduler: Mission '.. Mission.Name .. ' Task ' .. Task.Name .. ' Stage ' .. Task.Stage.Name .. ' done. TaskComplete = ' .. string.format ( "%s", TaskComplete and "true" or "false" ) ) - TaskComplete = true -- when a task is not yet completed, a mission cannot be completed - - else - -- break only if this task is not yet done, so that future task are not yet activated. - TaskComplete = false -- when a task is not yet completed, a mission cannot be completed - --env.info( 'Scheduler: Mission "'.. Mission.Name .. '" Task "' .. Task.Name .. '" Stage "' .. Task.Stage.Name .. '" break. TaskComplete = ' .. string.format ( "%s", TaskComplete and "true" or "false" ) ) - break - end - - if TaskComplete then - - if Mission.GoalFunction ~= nil then - Mission.GoalFunction( Mission, Client ) - end - if MISSIONSCHEDULER.Scoring then - MISSIONSCHEDULER.Scoring:_AddMissionTaskScore( Client:GetClientGroupDCSUnit(), Mission.Name, 25 ) - end - --- if not Mission:IsCompleted() then --- end - end - end - end - - local MissionComplete = true - for TaskNumber, Task in pairs( Mission._Tasks ) do - if Task:Goal() then --- Task:ShowGoalProgress( Mission, Client ) - if Task:IsGoalReached() then - else - MissionComplete = false - end - else - MissionComplete = false -- If there is no goal, the mission should never be ended. The goal status will be set somewhere else. - end - end - - if MissionComplete then - Mission:Completed() - if MISSIONSCHEDULER.Scoring then - MISSIONSCHEDULER.Scoring:_AddMissionScore( Mission.Name, 100 ) - end - else - if TaskComplete then - -- Reset for new tasking of active client - Client.ClientAlive = false -- Reset the client tasks. - end - end - - - else - if Client.ClientAlive then - env.info( 'Scheduler: Client "' .. Client.ClientName .. '" is inactive.' ) - Client.ClientAlive = false - - -- This is tricky. If we sanitize Client._Tasks before sanitizing Client._Tasks[TaskNumber].MissionTask, then the original MissionTask will be sanitized, and will be lost within the garbage collector. - -- So first sanitize Client._Tasks[TaskNumber].MissionTask, after that, sanitize only the whole _Tasks structure... - --Client._Tasks[TaskNumber].MissionTask = nil - --Client._Tasks = nil - end - end - end - - -- If all Clients of this Mission are not activated, then the Mission status needs to be put back into Pending status. - -- But only if the Mission was Ongoing. In case the Mission is Completed or Failed, the Mission status may not be changed. In these cases, this will be the last run of this Mission in the Scheduler. - if ClientsAlive == false then - if Mission:IsOngoing() then - -- Mission status back to pending... - Mission:Pending() - end - end - end - - Mission:StatusToClients() - - if Mission:ReportTrigger() then - Mission:ReportToAll() - end - end - - return true -end - ---- Start the MISSIONSCHEDULER. -function MISSIONSCHEDULER.Start() - if MISSIONSCHEDULER ~= nil then - --MISSIONSCHEDULER.SchedulerId = routines.scheduleFunction( MISSIONSCHEDULER.Scheduler, { }, 0, 2 ) - MISSIONSCHEDULER.SchedulerId = SCHEDULER:New( nil, MISSIONSCHEDULER.Scheduler, { }, 0, 2 ) - end -end - ---- Stop the MISSIONSCHEDULER. -function MISSIONSCHEDULER.Stop() - if MISSIONSCHEDULER.SchedulerId then - routines.removeFunction(MISSIONSCHEDULER.SchedulerId) - MISSIONSCHEDULER.SchedulerId = nil - end -end - ---- This is the main MISSION declaration method. Each Mission is like the master or a Mission orchestration between, Clients, Tasks, Stages etc. --- @param Mission is the MISSION object instantiated by @{MISSION:New}. --- @return MISSION --- @usage --- -- Declare a mission. --- Mission = MISSION:New( 'Russia Transport Troops SA-6', --- 'Operational', --- 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', --- 'Russia' ) --- MISSIONSCHEDULER:AddMission( Mission ) -function MISSIONSCHEDULER.AddMission( Mission ) - MISSIONSCHEDULER.Missions[Mission.Name] = Mission - MISSIONSCHEDULER.MissionCount = MISSIONSCHEDULER.MissionCount + 1 - -- Add an overall AI Client for the AI tasks... This AI Client will facilitate the Events in the background for each Task. - --MissionAdd:AddClient( CLIENT:Register( 'AI' ) ) - - return Mission -end - ---- Remove a MISSION from the MISSIONSCHEDULER. --- @param MissionName is the name of the MISSION given at declaration using @{AddMission}. --- @usage --- -- Declare a mission. --- Mission = MISSION:New( 'Russia Transport Troops SA-6', --- 'Operational', --- 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', --- 'Russia' ) --- MISSIONSCHEDULER:AddMission( Mission ) --- --- -- Now remove the Mission. --- MISSIONSCHEDULER:RemoveMission( 'Russia Transport Troops SA-6' ) -function MISSIONSCHEDULER.RemoveMission( MissionName ) - MISSIONSCHEDULER.Missions[MissionName] = nil - MISSIONSCHEDULER.MissionCount = MISSIONSCHEDULER.MissionCount - 1 -end - ---- Find a MISSION within the MISSIONSCHEDULER. --- @param MissionName is the name of the MISSION given at declaration using @{AddMission}. --- @return MISSION --- @usage --- -- Declare a mission. --- Mission = MISSION:New( 'Russia Transport Troops SA-6', --- 'Operational', --- 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', --- 'Russia' ) --- MISSIONSCHEDULER:AddMission( Mission ) --- --- -- Now find the Mission. --- MissionFind = MISSIONSCHEDULER:FindMission( 'Russia Transport Troops SA-6' ) -function MISSIONSCHEDULER.FindMission( MissionName ) - return MISSIONSCHEDULER.Missions[MissionName] -end - --- Internal function used by the MISSIONSCHEDULER menu. -function MISSIONSCHEDULER.ReportMissionsShow( ) - for MissionName, Mission in pairs( MISSIONSCHEDULER.Missions ) do - Mission.MissionReportShow = true - Mission.MissionReportFlash = false - end -end - --- Internal function used by the MISSIONSCHEDULER menu. -function MISSIONSCHEDULER.ReportMissionsFlash( TimeInterval ) - local Count = 0 - for MissionName, Mission in pairs( MISSIONSCHEDULER.Missions ) do - Mission.MissionReportShow = false - Mission.MissionReportFlash = true - Mission.MissionReportTrigger = timer.getTime() + Count * TimeInterval - Mission.MissionTimeInterval = MISSIONSCHEDULER.MissionCount * TimeInterval - env.info( "TimeInterval = " .. Mission.MissionTimeInterval ) - Count = Count + 1 - end -end - --- Internal function used by the MISSIONSCHEDULER menu. -function MISSIONSCHEDULER.ReportMissionsHide( Prm ) - for MissionName, Mission in pairs( MISSIONSCHEDULER.Missions ) do - Mission.MissionReportShow = false - Mission.MissionReportFlash = false - end -end - ---- Enables a MENU option in the communications menu under F10 to control the status of the active missions. --- This function should be called only once when starting the MISSIONSCHEDULER. -function MISSIONSCHEDULER.ReportMenu() - local ReportMenu = SUBMENU:New( 'Status' ) - local ReportMenuShow = COMMANDMENU:New( 'Show Report Missions', ReportMenu, MISSIONSCHEDULER.ReportMissionsShow, 0 ) - local ReportMenuFlash = COMMANDMENU:New('Flash Report Missions', ReportMenu, MISSIONSCHEDULER.ReportMissionsFlash, 120 ) - local ReportMenuHide = COMMANDMENU:New( 'Hide Report Missions', ReportMenu, MISSIONSCHEDULER.ReportMissionsHide, 0 ) -end - ---- Show the remaining mission time. -function MISSIONSCHEDULER:TimeShow() - self.TimeIntervalCount = self.TimeIntervalCount + 1 - if self.TimeIntervalCount >= self.TimeTriggerShow then - local TimeMsg = string.format("%00d", ( self.TimeSeconds / 60 ) - ( timer.getTime() / 60 )) .. ' minutes left until mission reload.' - MESSAGE:New( TimeMsg, self.TimeShow, "Mission time" ):ToAll() - self.TimeIntervalCount = 0 - end -end - -function MISSIONSCHEDULER:Time( TimeSeconds, TimeIntervalShow, TimeShow ) - - self.TimeIntervalCount = 0 - self.TimeSeconds = TimeSeconds - self.TimeIntervalShow = TimeIntervalShow - self.TimeShow = TimeShow -end - ---- Adds a mission scoring to the game. -function MISSIONSCHEDULER:Scoring( Scoring ) - - self.Scoring = Scoring -end - ---- The CLEANUP class keeps an area clean of crashing or colliding airplanes. It also prevents airplanes from firing within this area. --- @module CleanUp --- @author Flightcontrol - - - - - - - ---- The CLEANUP class. --- @type CLEANUP --- @extends Base#BASE -CLEANUP = { - ClassName = "CLEANUP", - ZoneNames = {}, - TimeInterval = 300, - CleanUpList = {}, -} - ---- Creates the main object which is handling the cleaning of the debris within the given Zone Names. --- @param #CLEANUP self --- @param #table ZoneNames Is a table of zone names where the debris should be cleaned. Also a single string can be passed with one zone name. --- @param #number TimeInterval The interval in seconds when the clean activity takes place. The default is 300 seconds, thus every 5 minutes. --- @return #CLEANUP --- @usage --- -- Clean these Zones. --- CleanUpAirports = CLEANUP:New( { 'CLEAN Tbilisi', 'CLEAN Kutaisi' }, 150 ) --- or --- CleanUpTbilisi = CLEANUP:New( 'CLEAN Tbilisi', 150 ) --- CleanUpKutaisi = CLEANUP:New( 'CLEAN Kutaisi', 600 ) -function CLEANUP:New( ZoneNames, TimeInterval ) local self = BASE:Inherit( self, BASE:New() ) - self:F( { ZoneNames, TimeInterval } ) - - if type( ZoneNames ) == 'table' then - self.ZoneNames = ZoneNames - else - self.ZoneNames = { ZoneNames } - end - if TimeInterval then - self.TimeInterval = TimeInterval - end - - _EVENTDISPATCHER:OnBirth( self._OnEventBirth, self ) - - --self.CleanUpScheduler = routines.scheduleFunction( self._CleanUpScheduler, { self }, timer.getTime() + 1, TimeInterval ) - self.CleanUpScheduler = SCHEDULER:New( self, self._CleanUpScheduler, {}, 1, TimeInterval ) - - return self -end - - ---- Destroys a group from the simulator, but checks first if it is still existing! --- @param #CLEANUP self --- @param DCSGroup#Group GroupObject The object to be destroyed. --- @param #string CleanUpGroupName The groupname... -function CLEANUP:_DestroyGroup( GroupObject, CleanUpGroupName ) - self:F( { GroupObject, CleanUpGroupName } ) - - if GroupObject then -- and GroupObject:isExist() then - trigger.action.deactivateGroup(GroupObject) - self:T( { "GroupObject Destroyed", GroupObject } ) - end -end - ---- Destroys a @{DCSUnit#Unit} from the simulator, but checks first if it is still existing! --- @param #CLEANUP self --- @param DCSUnit#Unit CleanUpUnit The object to be destroyed. --- @param #string CleanUpUnitName The Unit name ... -function CLEANUP:_DestroyUnit( CleanUpUnit, CleanUpUnitName ) - self:F( { CleanUpUnit, CleanUpUnitName } ) - - if CleanUpUnit then - local CleanUpGroup = Unit.getGroup(CleanUpUnit) - -- TODO Client bug in 1.5.3 - if CleanUpGroup and CleanUpGroup:isExist() then - local CleanUpGroupUnits = CleanUpGroup:getUnits() - if #CleanUpGroupUnits == 1 then - local CleanUpGroupName = CleanUpGroup:getName() - --self:CreateEventCrash( timer.getTime(), CleanUpUnit ) - CleanUpGroup:destroy() - self:T( { "Destroyed Group:", CleanUpGroupName } ) - else - CleanUpUnit:destroy() - self:T( { "Destroyed Unit:", CleanUpUnitName } ) - end - self.CleanUpList[CleanUpUnitName] = nil -- Cleaning from the list - CleanUpUnit = nil - end - end -end - --- TODO check DCSTypes#Weapon ---- Destroys a missile from the simulator, but checks first if it is still existing! --- @param #CLEANUP self --- @param DCSTypes#Weapon MissileObject -function CLEANUP:_DestroyMissile( MissileObject ) - self:F( { MissileObject } ) - - if MissileObject and MissileObject:isExist() then - MissileObject:destroy() - self:T( "MissileObject Destroyed") - end -end - -function CLEANUP:_OnEventBirth( Event ) - self:F( { Event } ) - - self.CleanUpList[Event.IniDCSUnitName] = {} - self.CleanUpList[Event.IniDCSUnitName].CleanUpUnit = Event.IniDCSUnit - self.CleanUpList[Event.IniDCSUnitName].CleanUpGroup = Event.IniDCSGroup - self.CleanUpList[Event.IniDCSUnitName].CleanUpGroupName = Event.IniDCSGroupName - self.CleanUpList[Event.IniDCSUnitName].CleanUpUnitName = Event.IniDCSUnitName - - _EVENTDISPATCHER:OnEngineShutDownForUnit( Event.IniDCSUnitName, self._EventAddForCleanUp, self ) - _EVENTDISPATCHER:OnEngineStartUpForUnit( Event.IniDCSUnitName, self._EventAddForCleanUp, self ) - _EVENTDISPATCHER:OnHitForUnit( Event.IniDCSUnitName, self._EventAddForCleanUp, self ) - _EVENTDISPATCHER:OnPilotDeadForUnit( Event.IniDCSUnitName, self._EventCrash, self ) - _EVENTDISPATCHER:OnDeadForUnit( Event.IniDCSUnitName, self._EventCrash, self ) - _EVENTDISPATCHER:OnCrashForUnit( Event.IniDCSUnitName, self._EventCrash, self ) - _EVENTDISPATCHER:OnShotForUnit( Event.IniDCSUnitName, self._EventShot, self ) - - --self:AddEvent( world.event.S_EVENT_ENGINE_SHUTDOWN, self._EventAddForCleanUp ) - --self:AddEvent( world.event.S_EVENT_ENGINE_STARTUP, self._EventAddForCleanUp ) --- self:AddEvent( world.event.S_EVENT_HIT, self._EventAddForCleanUp ) -- , self._EventHitCleanUp ) --- self:AddEvent( world.event.S_EVENT_CRASH, self._EventCrash ) -- , self._EventHitCleanUp ) --- --self:AddEvent( world.event.S_EVENT_DEAD, self._EventCrash ) --- self:AddEvent( world.event.S_EVENT_SHOT, self._EventShot ) --- --- self:EnableEvents() - - -end - ---- Detects if a crash event occurs. --- Crashed units go into a CleanUpList for removal. --- @param #CLEANUP self --- @param DCSTypes#Event event -function CLEANUP:_EventCrash( Event ) - self:F( { Event } ) - - --TODO: This stuff is not working due to a DCS bug. Burning units cannot be destroyed. - -- self:T("before getGroup") - -- local _grp = Unit.getGroup(event.initiator)-- Identify the group that fired - -- self:T("after getGroup") - -- _grp:destroy() - -- self:T("after deactivateGroup") - -- event.initiator:destroy() - - self.CleanUpList[Event.IniDCSUnitName] = {} - self.CleanUpList[Event.IniDCSUnitName].CleanUpUnit = Event.IniDCSUnit - self.CleanUpList[Event.IniDCSUnitName].CleanUpGroup = Event.IniDCSGroup - self.CleanUpList[Event.IniDCSUnitName].CleanUpGroupName = Event.IniDCSGroupName - self.CleanUpList[Event.IniDCSUnitName].CleanUpUnitName = Event.IniDCSUnitName - -end - ---- Detects if a unit shoots a missile. --- If this occurs within one of the zones, then the weapon used must be destroyed. --- @param #CLEANUP self --- @param DCSTypes#Event event -function CLEANUP:_EventShot( Event ) - self:F( { Event } ) - - -- Test if the missile was fired within one of the CLEANUP.ZoneNames. - local CurrentLandingZoneID = 0 - CurrentLandingZoneID = routines.IsUnitInZones( Event.IniDCSUnit, self.ZoneNames ) - if ( CurrentLandingZoneID ) then - -- Okay, the missile was fired within the CLEANUP.ZoneNames, destroy the fired weapon. - --_SEADmissile:destroy() - --routines.scheduleFunction( CLEANUP._DestroyMissile, { self, Event.Weapon }, timer.getTime() + 0.1) - SCHEDULER:New( self, CLEANUP._DestroyMissile, { Event.Weapon }, 0.1 ) - end -end - - ---- Detects if the Unit has an S_EVENT_HIT within the given ZoneNames. If this is the case, destroy the unit. --- @param #CLEANUP self --- @param DCSTypes#Event event -function CLEANUP:_EventHitCleanUp( Event ) - self:F( { Event } ) - - if Event.IniDCSUnit then - if routines.IsUnitInZones( Event.IniDCSUnit, self.ZoneNames ) ~= nil then - self:T( { "Life: ", Event.IniDCSUnitName, ' = ', Event.IniDCSUnit:getLife(), "/", Event.IniDCSUnit:getLife0() } ) - if Event.IniDCSUnit:getLife() < Event.IniDCSUnit:getLife0() then - self:T( "CleanUp: Destroy: " .. Event.IniDCSUnitName ) - --routines.scheduleFunction( CLEANUP._DestroyUnit, { self, Event.IniDCSUnit }, timer.getTime() + 0.1) - SCHEDULER:New( self, CLEANUP._DestroyUnit, { Event.IniDCSUnit }, 0.1 ) - end - end - end - - if Event.TgtDCSUnit then - if routines.IsUnitInZones( Event.TgtDCSUnit, self.ZoneNames ) ~= nil then - self:T( { "Life: ", Event.TgtDCSUnitName, ' = ', Event.TgtDCSUnit:getLife(), "/", Event.TgtDCSUnit:getLife0() } ) - if Event.TgtDCSUnit:getLife() < Event.TgtDCSUnit:getLife0() then - self:T( "CleanUp: Destroy: " .. Event.TgtDCSUnitName ) - --routines.scheduleFunction( CLEANUP._DestroyUnit, { self, Event.TgtDCSUnit }, timer.getTime() + 0.1 ) - SCHEDULER:New( self, CLEANUP._DestroyUnit, { Event.TgtDCSUnit }, 0.1 ) - end - end - end -end - ---- Add the @{DCSUnit#Unit} to the CleanUpList for CleanUp. -function CLEANUP:_AddForCleanUp( CleanUpUnit, CleanUpUnitName ) - self:F( { CleanUpUnit, CleanUpUnitName } ) - - self.CleanUpList[CleanUpUnitName] = {} - self.CleanUpList[CleanUpUnitName].CleanUpUnit = CleanUpUnit - self.CleanUpList[CleanUpUnitName].CleanUpUnitName = CleanUpUnitName - self.CleanUpList[CleanUpUnitName].CleanUpGroup = Unit.getGroup(CleanUpUnit) - self.CleanUpList[CleanUpUnitName].CleanUpGroupName = Unit.getGroup(CleanUpUnit):getName() - self.CleanUpList[CleanUpUnitName].CleanUpTime = timer.getTime() - self.CleanUpList[CleanUpUnitName].CleanUpMoved = false - - self:T( { "CleanUp: Add to CleanUpList: ", Unit.getGroup(CleanUpUnit):getName(), CleanUpUnitName } ) - -end - ---- Detects if the Unit has an S_EVENT_ENGINE_SHUTDOWN or an S_EVENT_HIT within the given ZoneNames. If this is the case, add the Group to the CLEANUP List. --- @param #CLEANUP self --- @param DCSTypes#Event event -function CLEANUP:_EventAddForCleanUp( Event ) - - if Event.IniDCSUnit then - if self.CleanUpList[Event.IniDCSUnitName] == nil then - if routines.IsUnitInZones( Event.IniDCSUnit, self.ZoneNames ) ~= nil then - self:_AddForCleanUp( Event.IniDCSUnit, Event.IniDCSUnitName ) - end - end - end - - if Event.TgtDCSUnit then - if self.CleanUpList[Event.TgtDCSUnitName] == nil then - if routines.IsUnitInZones( Event.TgtDCSUnit, self.ZoneNames ) ~= nil then - self:_AddForCleanUp( Event.TgtDCSUnit, Event.TgtDCSUnitName ) - end - end - end - -end - -local CleanUpSurfaceTypeText = { - "LAND", - "SHALLOW_WATER", - "WATER", - "ROAD", - "RUNWAY" - } - ---- At the defined time interval, CleanUp the Groups within the CleanUpList. --- @param #CLEANUP self -function CLEANUP:_CleanUpScheduler() - self:F( { "CleanUp Scheduler" } ) - - local CleanUpCount = 0 - for CleanUpUnitName, UnitData in pairs( self.CleanUpList ) do - CleanUpCount = CleanUpCount + 1 - - self:T( { CleanUpUnitName, UnitData } ) - local CleanUpUnit = Unit.getByName(UnitData.CleanUpUnitName) - local CleanUpGroupName = UnitData.CleanUpGroupName - local CleanUpUnitName = UnitData.CleanUpUnitName - if CleanUpUnit then - self:T( { "CleanUp Scheduler", "Checking:", CleanUpUnitName } ) - if _DATABASE:GetStatusGroup( CleanUpGroupName ) ~= "ReSpawn" then - local CleanUpUnitVec3 = CleanUpUnit:getPoint() - --self:T( CleanUpUnitVec3 ) - local CleanUpUnitVec2 = {} - CleanUpUnitVec2.x = CleanUpUnitVec3.x - CleanUpUnitVec2.y = CleanUpUnitVec3.z - --self:T( CleanUpUnitVec2 ) - local CleanUpSurfaceType = land.getSurfaceType(CleanUpUnitVec2) - --self:T( CleanUpSurfaceType ) - - if CleanUpUnit and CleanUpUnit:getLife() <= CleanUpUnit:getLife0() * 0.95 then - if CleanUpSurfaceType == land.SurfaceType.RUNWAY then - if CleanUpUnit:inAir() then - local CleanUpLandHeight = land.getHeight(CleanUpUnitVec2) - local CleanUpUnitHeight = CleanUpUnitVec3.y - CleanUpLandHeight - self:T( { "CleanUp Scheduler", "Height = " .. CleanUpUnitHeight } ) - if CleanUpUnitHeight < 30 then - self:T( { "CleanUp Scheduler", "Destroy " .. CleanUpUnitName .. " because below safe height and damaged." } ) - self:_DestroyUnit(CleanUpUnit, CleanUpUnitName) - end - else - self:T( { "CleanUp Scheduler", "Destroy " .. CleanUpUnitName .. " because on runway and damaged." } ) - self:_DestroyUnit(CleanUpUnit, CleanUpUnitName) - end - end - end - -- Clean Units which are waiting for a very long time in the CleanUpZone. - if CleanUpUnit then - local CleanUpUnitVelocity = CleanUpUnit:getVelocity() - local CleanUpUnitVelocityTotal = math.abs(CleanUpUnitVelocity.x) + math.abs(CleanUpUnitVelocity.y) + math.abs(CleanUpUnitVelocity.z) - if CleanUpUnitVelocityTotal < 1 then - if UnitData.CleanUpMoved then - if UnitData.CleanUpTime + 180 <= timer.getTime() then - self:T( { "CleanUp Scheduler", "Destroy due to not moving anymore " .. CleanUpUnitName } ) - self:_DestroyUnit(CleanUpUnit, CleanUpUnitName) - end - end - else - UnitData.CleanUpTime = timer.getTime() - UnitData.CleanUpMoved = true - end - end - - else - -- Do nothing ... - self.CleanUpList[CleanUpUnitName] = nil -- Not anymore in the DCSRTE - end - else - self:T( "CleanUp: Group " .. CleanUpUnitName .. " cannot be found in DCS RTE, removing ..." ) - self.CleanUpList[CleanUpUnitName] = nil -- Not anymore in the DCSRTE - end - end - self:T(CleanUpCount) - - return true -end - ---- This module contains the SPAWN class. --- --- 1) @{Spawn#SPAWN} class, extends @{Base#BASE} --- ============================================= --- The @{#SPAWN} class allows to spawn dynamically new groups, based on pre-defined initialization settings, modifying the behaviour when groups are spawned. --- For each group to be spawned, within the mission editor, a group has to be created with the "late activation flag" set. We call this group the *"Spawn Template"* of the SPAWN object. --- A reference to this Spawn Template needs to be provided when constructing the SPAWN object, by indicating the name of the group within the mission editor in the constructor methods. --- --- Within the SPAWN object, there is an internal index that keeps track of which group from the internal group list was spawned. --- When new groups get spawned by using the SPAWN functions (see below), it will be validated whether the Limits (@{#SPAWN.Limit}) of the SPAWN object are not reached. --- When all is valid, a new group will be created by the spawning methods, and the internal index will be increased with 1. --- --- Regarding the name of new spawned groups, a _SpawnPrefix_ will be assigned for each new group created. --- If you want to have the Spawn Template name to be used as the _SpawnPrefix_ name, use the @{#SPAWN.New} constructor. --- However, when the @{#SPAWN.NewWithAlias} constructor was used, the Alias name will define the _SpawnPrefix_ name. --- Groups will follow the following naming structure when spawned at run-time: --- --- 1. Spawned groups will have the name _SpawnPrefix_#ggg, where ggg is a counter from 0 to 999. --- 2. Spawned units will have the name _SpawnPrefix_#ggg-uu, where uu is a counter from 0 to 99 for each new spawned unit belonging to the group. --- --- Some additional notes that need to be remembered: --- --- * Templates are actually groups defined within the mission editor, with the flag "Late Activation" set. As such, these groups are never used within the mission, but are used by the @{#SPAWN} module. --- * It is important to defined BEFORE you spawn new groups, a proper initialization of the SPAWN instance is done with the options you want to use. --- * When designing a mission, NEVER name groups using a "#" within the name of the group Spawn Template(s), or the SPAWN module logic won't work anymore. --- --- 1.1) SPAWN construction methods --- ------------------------------- --- Create a new SPAWN object with the @{#SPAWN.New} or the @{#SPAWN.NewWithAlias} methods: --- --- * @{#SPAWN.New}: Creates a new SPAWN object taking the name of the group that functions as the Template. --- --- 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 functions will modify this list of groups so that when a group gets spawned, ALL information is already prepared when spawning. This is done for performance reasons. --- So in principle, the group list will contain all parameters and configurations after initialization, and when groups get actually spawned, this spawning can be done quickly and efficient. --- --- 1.2) SPAWN initialization methods --- --------------------------------- --- A spawn object will behave differently based on the usage of initialization methods: --- --- * @{#SPAWN.Limit}: Limits the amount of groups that can be alive at the same time and that can be dynamically spawned. --- * @{#SPAWN.RandomizeRoute}: Randomize the routes of spawned groups. --- * @{#SPAWN.RandomizeTemplate}: Randomize the group templates so that when a new group is spawned, a random group template is selected from one of the templates defined. --- * @{#SPAWN.Uncontrolled}: Spawn plane groups uncontrolled. --- * @{#SPAWN.Array}: Make groups visible before they are actually activated, and order these groups like a batallion in an array. --- * @{#SPAWN.InitRepeat}: Re-spawn groups when they land at the home base. Similar functions are @{#SPAWN.InitRepeatOnLanding} and @{#SPAWN.InitRepeatOnEngineShutDown}. --- --- 1.3) SPAWN spawning methods --- --------------------------- --- Groups can be spawned at different times and methods: --- --- * @{#SPAWN.Spawn}: Spawn one new group based on the last spawned index. --- * @{#SPAWN.ReSpawn}: Re-spawn a group based on a given index. --- * @{#SPAWN.SpawnScheduled}: Spawn groups at scheduled but randomized intervals. You can use @{#SPAWN.SpawnScheduleStart} and @{#SPAWN.SpawnScheduleStop} to start and stop the schedule respectively. --- * @{#SPAWN.SpawnFromUnit}: Spawn a new group taking the position of a @{UNIT}. --- * @{#SPAWN.SpawnInZone}: Spawn a new group in a @{ZONE}. --- --- Note that @{#SPAWN.Spawn} and @{#SPAWN.ReSpawn} return a @{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. --- --- 1.4) SPAWN object cleaning --- -------------------------- --- Sometimes, it will occur during a mission run-time, that ground or especially air objects get damaged, and will while being damged stop their activities, while remaining alive. --- In such cases, the SPAWN object will just sit there and wait until that group gets destroyed, but most of the time it won't, --- and it may occur that no new groups are or can be spawned as limits are reached. --- To prevent this, a @{#SPAWN.CleanUp} initialization method has been defined that will silently monitor the status of each spawned group. --- Once a group has a velocity = 0, and has been waiting for a defined interval, that group will be cleaned or removed from run-time. --- There is a catch however :-) If a damaged group has returned to an airbase within the coalition, that group will not be considered as "lost"... --- In such a case, when the inactive group is cleaned, a new group will Re-spawned automatically. --- This models AI that has succesfully returned to their airbase, to restart their combat activities. --- Check the @{#SPAWN.CleanUp} for further info. --- --- --- @module Spawn --- @author FlightControl - ---- SPAWN Class --- @type SPAWN --- @extends Base#BASE --- @field ClassName --- @field #string SpawnTemplatePrefix --- @field #string SpawnAliasPrefix -SPAWN = { - ClassName = "SPAWN", - SpawnTemplatePrefix = nil, - SpawnAliasPrefix = nil, -} - - - ---- Creates the main object to spawn a GROUP defined in the DCS ME. --- @param #SPAWN self --- @param #string SpawnTemplatePrefix is the name of the Group in the ME that defines the Template. Each new group will have the name starting with SpawnTemplatePrefix. --- @return #SPAWN --- @usage --- -- NATO helicopters engaging in the battle field. --- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ) --- @usage local Plane = SPAWN:New( "Plane" ) -- Creates a new local variable that can initiate new planes with the name "Plane#ddd" using the template "Plane" as defined within the ME. -function SPAWN:New( SpawnTemplatePrefix ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( { SpawnTemplatePrefix } ) - - local TemplateGroup = Group.getByName( SpawnTemplatePrefix ) - if TemplateGroup then - self.SpawnTemplatePrefix = SpawnTemplatePrefix - self.SpawnIndex = 0 - self.SpawnCount = 0 -- The internal counter of the amount of spawning the has happened since SpawnStart. - self.AliveUnits = 0 -- Contains the counter how many units are currently alive - self.SpawnIsScheduled = false -- Reflects if the spawning for this SpawnTemplatePrefix is going to be scheduled or not. - self.SpawnTemplate = self._GetTemplate( self, SpawnTemplatePrefix ) -- Contains the template structure for a Group Spawn from the Mission Editor. Note that this group must have lateActivation always on!!! - self.Repeat = false -- Don't repeat the group from Take-Off till Landing and back Take-Off by ReSpawning. - self.UnControlled = false -- When working in UnControlled mode, all planes are Spawned in UnControlled mode before the scheduler starts. - self.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.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. - else - error( "SPAWN:New: There is no group declared in the mission editor with SpawnTemplatePrefix = '" .. SpawnTemplatePrefix .. "'" ) - end - - return self -end - ---- Creates a new SPAWN instance to create new groups based on the defined template and using a new alias for each new group. --- @param #SPAWN self --- @param #string SpawnTemplatePrefix is the name of the Group in the ME that defines the Template. --- @param #string SpawnAliasPrefix is the name that will be given to the Group at runtime. --- @return #SPAWN --- @usage --- -- NATO helicopters engaging in the battle field. --- Spawn_BE_KA50 = SPAWN:NewWithAlias( 'BE KA-50@RAMP-Ground Defense', 'Helicopter Attacking a City' ) --- @usage local PlaneWithAlias = SPAWN:NewWithAlias( "Plane", "Bomber" ) -- Creates a new local variable that can instantiate new planes with the name "Bomber#ddd" using the template "Plane" as defined within the ME. -function SPAWN:NewWithAlias( SpawnTemplatePrefix, SpawnAliasPrefix ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( { SpawnTemplatePrefix, SpawnAliasPrefix } ) - - local TemplateGroup = Group.getByName( SpawnTemplatePrefix ) - if TemplateGroup then - self.SpawnTemplatePrefix = SpawnTemplatePrefix - self.SpawnAliasPrefix = SpawnAliasPrefix - self.SpawnIndex = 0 - self.SpawnCount = 0 -- The internal counter of the amount of spawning the has happened since SpawnStart. - self.AliveUnits = 0 -- Contains the counter how many units are currently alive - self.SpawnIsScheduled = false -- Reflects if the spawning for this SpawnTemplatePrefix is going to be scheduled or not. - self.SpawnTemplate = self._GetTemplate( self, SpawnTemplatePrefix ) -- Contains the template structure for a Group Spawn from the Mission Editor. Note that this group must have lateActivation always on!!! - self.Repeat = false -- Don't repeat the group from Take-Off till Landing and back Take-Off by ReSpawning. - self.UnControlled = false -- When working in UnControlled mode, all planes are Spawned in UnControlled mode before the scheduler starts. - self.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.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. - else - error( "SPAWN:New: There is no group declared in the mission editor with SpawnTemplatePrefix = '" .. SpawnTemplatePrefix .. "'" ) - end - - 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 function should be used... --- When a @{#SPAWN.New} is executed and the limit of the amount of units alive is reached, then no new spawn will happen of the group, until some of these units of the spawn object will be destroyed. --- @param #SPAWN self --- @param #number SpawnMaxUnitsAlive The maximum amount of units that can be alive at runtime. --- @param #number SpawnMaxGroups The maximum amount of groups that can be spawned. When the limit is reached, then no more actual spawns will happen of the group. --- This parameter is useful to define a maximum amount of airplanes, ground troops, helicopters, ships etc within a supply area. --- This parameter accepts the value 0, which defines that there are no maximum group limits, but there are limits on the maximum of units that can be alive at the same time. --- @return #SPAWN self --- @usage --- -- NATO helicopters engaging in the battle field. --- -- This helicopter group consists of one Unit. So, this group will SPAWN maximum 2 groups simultaneously within the DCSRTE. --- -- There will be maximum 24 groups spawned during the whole mission lifetime. --- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):Limit( 2, 24 ) -function SPAWN:Limit( SpawnMaxUnitsAlive, SpawnMaxGroups ) - self:F( { self.SpawnTemplatePrefix, SpawnMaxUnitsAlive, SpawnMaxGroups } ) - - self.SpawnMaxUnitsAlive = SpawnMaxUnitsAlive -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time. - self.SpawnMaxGroups = SpawnMaxGroups -- The maximum amount of groups that can be spawned. - - for SpawnGroupID = 1, self.SpawnMaxGroups do - self:_InitializeSpawnGroups( SpawnGroupID ) - end - - return self -end - - ---- Randomizes the defined route of the SpawnTemplatePrefix group in the ME. This is very useful to define extra variation of the behaviour of groups. --- @param #SPAWN self --- @param #number SpawnStartPoint is the waypoint where the randomization begins. --- Note that the StartPoint = 0 equaling the point where the group is spawned. --- @param #number SpawnEndPoint is the waypoint where the randomization ends counting backwards. --- This parameter is useful to avoid randomization to end at a waypoint earlier than the last waypoint on the route. --- @param #number SpawnRadius is the radius in meters in which the randomization of the new waypoints, with the original waypoint of the original template located in the middle ... --- @return #SPAWN --- @usage --- -- NATO helicopters engaging in the battle field. --- -- The KA-50 has waypoints Start point ( =0 or SP ), 1, 2, 3, 4, End point (= 5 or DP). --- -- Waypoints 2 and 3 will only be randomized. The others will remain on their original position with each new spawn of the helicopter. --- -- The randomization of waypoint 2 and 3 will take place within a radius of 2000 meters. --- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):RandomizeRoute( 2, 2, 2000 ) -function SPAWN:RandomizeRoute( SpawnStartPoint, SpawnEndPoint, SpawnRadius ) - self:F( { self.SpawnTemplatePrefix, SpawnStartPoint, SpawnEndPoint, SpawnRadius } ) - - self.SpawnRandomizeRoute = true - self.SpawnRandomizeRouteStartPoint = SpawnStartPoint - self.SpawnRandomizeRouteEndPoint = SpawnEndPoint - self.SpawnRandomizeRouteRadius = SpawnRadius - - for GroupID = 1, self.SpawnMaxGroups do - self:_RandomizeRoute( GroupID ) - end - - return self -end - - ---- This function is rather complicated to understand. But I'll try to explain. --- This function becomes useful when you need to spawn groups with random templates of groups defined within the mission editor, --- but they will all follow the same Template route and have the same prefix name. --- In other words, this method randomizes between a defined set of groups the template to be used for each new spawn of a group. --- @param #SPAWN self --- @param #string SpawnTemplatePrefixTable A table with the names of the groups defined within the mission editor, from which one will be choosen when a new group will be spawned. --- @return #SPAWN --- @usage --- -- NATO Tank Platoons invading Gori. --- -- Choose between 13 different 'US Tank Platoon' configurations for each new SPAWN the Group to be spawned for the --- -- 'US Tank Platoon Left', 'US Tank Platoon Middle' and 'US Tank Platoon Right' SpawnTemplatePrefixes. --- -- Each new SPAWN will randomize the route, with a defined time interval of 200 seconds with 40% time variation (randomization) and --- -- with a limit set of maximum 12 Units alive simulteneously and 150 Groups to be spawned during the whole mission. --- Spawn_US_Platoon = { 'US Tank Platoon 1', 'US Tank Platoon 2', 'US Tank Platoon 3', 'US Tank Platoon 4', 'US Tank Platoon 5', --- 'US Tank Platoon 6', 'US Tank Platoon 7', 'US Tank Platoon 8', 'US Tank Platoon 9', 'US Tank Platoon 10', --- 'US Tank Platoon 11', 'US Tank Platoon 12', 'US Tank Platoon 13' } --- Spawn_US_Platoon_Left = SPAWN:New( 'US Tank Platoon Left' ):Limit( 12, 150 ):Schedule( 200, 0.4 ):RandomizeTemplate( Spawn_US_Platoon ):RandomizeRoute( 3, 3, 2000 ) --- Spawn_US_Platoon_Middle = SPAWN:New( 'US Tank Platoon Middle' ):Limit( 12, 150 ):Schedule( 200, 0.4 ):RandomizeTemplate( Spawn_US_Platoon ):RandomizeRoute( 3, 3, 2000 ) --- Spawn_US_Platoon_Right = SPAWN:New( 'US Tank Platoon Right' ):Limit( 12, 150 ):Schedule( 200, 0.4 ):RandomizeTemplate( Spawn_US_Platoon ):RandomizeRoute( 3, 3, 2000 ) -function SPAWN:RandomizeTemplate( SpawnTemplatePrefixTable ) - self:F( { self.SpawnTemplatePrefix, SpawnTemplatePrefixTable } ) - - self.SpawnTemplatePrefixTable = SpawnTemplatePrefixTable - self.SpawnRandomizeTemplate = true - - for SpawnGroupID = 1, self.SpawnMaxGroups do - self:_RandomizeTemplate( SpawnGroupID ) - end - - return self -end - - - - - ---- For planes and helicopters, when these groups go home and land on their home airbases and farps, they normally would taxi to the parking spot, shut-down their engines and wait forever until the Group is removed by the runtime environment. --- This function is used to re-spawn automatically (so no extra call is needed anymore) the same group after it has landed. --- This will enable a spawned group to be re-spawned after it lands, until it is destroyed... --- Note: When the group is respawned, it will re-spawn from the original airbase where it took off. --- So ensure that the routes for groups that respawn, always return to the original airbase, or players may get confused ... --- @param #SPAWN self --- @return #SPAWN self --- @usage --- -- RU Su-34 - AI Ship Attack --- -- Re-SPAWN the Group(s) after each landing and Engine Shut-Down automatically. --- SpawnRU_SU34 = SPAWN:New( 'TF1 RU Su-34 Krymsk@AI - Attack Ships' ):Schedule( 2, 3, 1800, 0.4 ):SpawnUncontrolled():RandomizeRoute( 1, 1, 3000 ):RepeatOnEngineShutDown() -function SPAWN:InitRepeat() - self:F( { self.SpawnTemplatePrefix, self.SpawnIndex } ) - - self.Repeat = true - self.RepeatOnEngineShutDown = false - self.RepeatOnLanding = true - - return self -end - ---- Respawn group after landing. --- @param #SPAWN self --- @return #SPAWN self -function SPAWN:InitRepeatOnLanding() - self:F( { self.SpawnTemplatePrefix } ) - - self:InitRepeat() - self.RepeatOnEngineShutDown = false - self.RepeatOnLanding = true - - return self -end - - ---- Respawn after landing when its engines have shut down. --- @param #SPAWN self --- @return #SPAWN self -function SPAWN:InitRepeatOnEngineShutDown() - self:F( { self.SpawnTemplatePrefix } ) - - self:InitRepeat() - self.RepeatOnEngineShutDown = true - self.RepeatOnLanding = false - - return self -end - - ---- CleanUp groups when they are still alive, but inactive. --- When groups are still alive and have become inactive due to damage and are unable to contribute anything, then this group will be removed at defined intervals in seconds. --- @param #SPAWN self --- @param #string SpawnCleanUpInterval The interval to check for inactive groups within seconds. --- @return #SPAWN self --- @usage Spawn_Helicopter:CleanUp( 20 ) -- CleanUp the spawning of the helicopters every 20 seconds when they become inactive. -function SPAWN:CleanUp( SpawnCleanUpInterval ) - self:F( { self.SpawnTemplatePrefix, SpawnCleanUpInterval } ) - - self.SpawnCleanUpInterval = SpawnCleanUpInterval - self.SpawnCleanUpTimeStamps = {} - --self.CleanUpFunction = routines.scheduleFunction( self._SpawnCleanUpScheduler, { self }, timer.getTime() + 1, SpawnCleanUpInterval ) - self.CleanUpScheduler = SCHEDULER:New( self, self._SpawnCleanUpScheduler, {}, 1, SpawnCleanUpInterval, 0.2 ) - return self -end - - - ---- Makes the groups visible before start (like a batallion). --- The method will take the position of the group as the first position in the array. --- @param #SPAWN self --- @param #number SpawnAngle The angle in degrees how the groups and each unit of the group will be positioned. --- @param #number SpawnWidth The amount of Groups that will be positioned on the X axis. --- @param #number SpawnDeltaX The space between each Group on the X-axis. --- @param #number SpawnDeltaY The space between each Group on the Y-axis. --- @return #SPAWN self --- @usage --- -- Define an array of Groups. --- Spawn_BE_Ground = SPAWN:New( 'BE Ground' ):Limit( 2, 24 ):Visible( 90, "Diamond", 10, 100, 50 ) -function SPAWN:Array( SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY ) - self:F( { self.SpawnTemplatePrefix, SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY } ) - - self.SpawnVisible = true -- When the first Spawn executes, all the Groups need to be made visible before start. - - local SpawnX = 0 - local SpawnY = 0 - local SpawnXIndex = 0 - local SpawnYIndex = 0 - - for SpawnGroupID = 1, self.SpawnMaxGroups do - self:T( { SpawnX, SpawnY, SpawnXIndex, SpawnYIndex } ) - - self.SpawnGroups[SpawnGroupID].Visible = true - self.SpawnGroups[SpawnGroupID].Spawned = false - - SpawnXIndex = SpawnXIndex + 1 - if SpawnWidth and SpawnWidth ~= 0 then - if SpawnXIndex >= SpawnWidth then - SpawnXIndex = 0 - SpawnYIndex = SpawnYIndex + 1 - end - end - - local SpawnRootX = self.SpawnGroups[SpawnGroupID].SpawnTemplate.x - local SpawnRootY = self.SpawnGroups[SpawnGroupID].SpawnTemplate.y - - self:_TranslateRotate( SpawnGroupID, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle ) - - self.SpawnGroups[SpawnGroupID].SpawnTemplate.lateActivation = true - self.SpawnGroups[SpawnGroupID].SpawnTemplate.visible = true - - self.SpawnGroups[SpawnGroupID].Visible = true - - _EVENTDISPATCHER:OnBirthForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnBirth, self ) - _EVENTDISPATCHER:OnCrashForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnDeadOrCrash, self ) - _EVENTDISPATCHER:OnDeadForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnDeadOrCrash, self ) - - if self.Repeat then - _EVENTDISPATCHER:OnTakeOffForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnTakeOff, self ) - _EVENTDISPATCHER:OnLandForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnLand, self ) - end - if self.RepeatOnEngineShutDown then - _EVENTDISPATCHER:OnEngineShutDownForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnEngineShutDown, self ) - end - - self.SpawnGroups[SpawnGroupID].Group = _DATABASE:Spawn( self.SpawnGroups[SpawnGroupID].SpawnTemplate ) - - SpawnX = SpawnXIndex * SpawnDeltaX - SpawnY = SpawnYIndex * SpawnDeltaY - end - - return self -end - - - ---- Will spawn a group based on the internal index. --- Note: Uses @{DATABASE} module defined in MOOSE. --- @param #SPAWN self --- @return Group#GROUP The group that was spawned. You can use this group for further actions. -function SPAWN:Spawn() - self:F( { self.SpawnTemplatePrefix, self.SpawnIndex } ) - - return self:SpawnWithIndex( self.SpawnIndex + 1 ) -end - ---- Will re-spawn a group based on a given index. --- Note: Uses @{DATABASE} module defined in MOOSE. --- @param #SPAWN self --- @param #string SpawnIndex The index of the group to be spawned. --- @return Group#GROUP The group that was spawned. You can use this group for further actions. -function SPAWN:ReSpawn( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex } ) - - if not SpawnIndex then - SpawnIndex = 1 - end - --- TODO: This logic makes DCS crash and i don't know why (yet). - local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) - if SpawnGroup then - local SpawnDCSGroup = SpawnGroup:GetDCSGroup() - if SpawnDCSGroup then - SpawnGroup:Destroy() - end - end - - return self:SpawnWithIndex( SpawnIndex ) -end - ---- Will spawn a group with a specified index number. --- Uses @{DATABASE} global object defined in MOOSE. --- @param #SPAWN self --- @return Group#GROUP The group that was spawned. You can use this group for further actions. -function SPAWN:SpawnWithIndex( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnMaxGroups } ) - - if self:_GetSpawnIndex( SpawnIndex ) then - - if self.SpawnGroups[self.SpawnIndex].Visible then - self.SpawnGroups[self.SpawnIndex].Group:Activate() - else - self:T( self.SpawnGroups[self.SpawnIndex].SpawnTemplate ) - _EVENTDISPATCHER:OnBirthForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnBirth, self ) - _EVENTDISPATCHER:OnCrashForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnDeadOrCrash, self ) - _EVENTDISPATCHER:OnDeadForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnDeadOrCrash, self ) - - if self.Repeat then - _EVENTDISPATCHER:OnTakeOffForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnTakeOff, self ) - _EVENTDISPATCHER:OnLandForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnLand, self ) - end - if self.RepeatOnEngineShutDown then - _EVENTDISPATCHER:OnEngineShutDownForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnEngineShutDown, self ) - end - - self:T( self.SpawnGroups[self.SpawnIndex].SpawnTemplate ) - - self.SpawnGroups[self.SpawnIndex].Group = _DATABASE:Spawn( self.SpawnGroups[self.SpawnIndex].SpawnTemplate ) - - -- If there is a SpawnFunction hook defined, call it. - if self.SpawnFunctionHook then - self.SpawnFunctionHook( self.SpawnGroups[self.SpawnIndex].Group, unpack( self.SpawnFunctionArguments ) ) - end - -- TODO: Need to fix this by putting an "R" in the name of the group when the group repeats. - --if self.Repeat then - -- _DATABASE:SetStatusGroup( SpawnTemplate.name, "ReSpawn" ) - --end - end - - self.SpawnGroups[self.SpawnIndex].Spawned = true - return self.SpawnGroups[self.SpawnIndex].Group - else - --self:E( { self.SpawnTemplatePrefix, "No more Groups to Spawn:", SpawnIndex, self.SpawnMaxGroups } ) - end - - return nil -end - ---- Spawns new groups at varying time intervals. --- This is useful if you want to have continuity within your missions of certain (AI) groups to be present (alive) within your missions. --- @param #SPAWN self --- @param #number SpawnTime The time interval defined in seconds between each new spawn of new groups. --- @param #number SpawnTimeVariation The variation to be applied on the defined time interval between each new spawn. --- The variation is a number between 0 and 1, representing the %-tage of variation to be applied on the time interval. --- @return #SPAWN self --- @usage --- -- NATO helicopters engaging in the battle field. --- -- The time interval is set to SPAWN new helicopters between each 600 seconds, with a time variation of 50%. --- -- The time variation in this case will be between 450 seconds and 750 seconds. --- -- This is calculated as follows: --- -- Low limit: 600 * ( 1 - 0.5 / 2 ) = 450 --- -- High limit: 600 * ( 1 + 0.5 / 2 ) = 750 --- -- Between these two values, a random amount of seconds will be choosen for each new spawn of the helicopters. --- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):Schedule( 600, 0.5 ) -function SPAWN:SpawnScheduled( SpawnTime, SpawnTimeVariation ) - self:F( { SpawnTime, SpawnTimeVariation } ) - - if SpawnTime ~= nil and SpawnTimeVariation ~= nil then - self.SpawnScheduler = SCHEDULER:New( self, self._Scheduler, {}, 1, SpawnTime, SpawnTimeVariation ) - end - - return self -end - ---- Will re-start the spawning scheduler. --- Note: This function is only required to be called when the schedule was stopped. -function SPAWN:SpawnScheduleStart() - self:F( { self.SpawnTemplatePrefix } ) - - self.SpawnScheduler:Start() -end - ---- Will stop the scheduled spawning scheduler. -function SPAWN:SpawnScheduleStop() - self:F( { self.SpawnTemplatePrefix } ) - - self.SpawnScheduler:Stop() -end - - ---- Allows to place a CallFunction hook when a new group spawns. --- The provided function 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. --- @param #SPAWN self --- @param #function SpawnFunctionHook 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 -function SPAWN:SpawnFunction( SpawnFunctionHook, ... ) - self:F( SpawnFunction ) - - self.SpawnFunctionHook = SpawnFunctionHook - self.SpawnFunctionArguments = {} - if arg then - self.SpawnFunctionArguments = arg - end - - return self -end - - - - ---- Will spawn a group from a hosting unit. This function is mostly advisable to be used if you want to simulate spawning from air units, like helicopters, which are dropping infantry into a defined Landing Zone. --- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. --- You can use the returned group to further define the route to be followed. --- @param #SPAWN self --- @param Unit#UNIT HostUnit The air or ground unit dropping or unloading the group. --- @param #number OuterRadius The outer radius in meters where the new group will be spawned. --- @param #number InnerRadius The inner radius in meters where the new group will NOT be spawned. --- @param #number SpawnIndex (Optional) The index which group to spawn within the given zone. --- @return Group#GROUP that was spawned. --- @return #nil Nothing was spawned. -function SPAWN:SpawnFromUnit( HostUnit, OuterRadius, InnerRadius, SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, HostUnit, OuterRadius, InnerRadius, SpawnIndex } ) - - if HostUnit and HostUnit:IsAlive() then -- and HostUnit:getUnit(1):inAir() == false then - - if SpawnIndex then - else - SpawnIndex = self.SpawnIndex + 1 - end - - if self:_GetSpawnIndex( SpawnIndex ) then - - local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate - - if SpawnTemplate then - - local UnitPoint = HostUnit:GetPointVec2() - - self:T( { "Current point of ", self.SpawnTemplatePrefix, UnitPoint } ) - - --for PointID, Point in pairs( SpawnTemplate.route.points ) do - --Point.x = UnitPoint.x - --Point.y = UnitPoint.y - --Point.alt = nil - --Point.alt_type = nil - --end - - SpawnTemplate.route.points[1].x = UnitPoint.x - SpawnTemplate.route.points[1].y = UnitPoint.y - - if not InnerRadius then - InnerRadius = 10 - end - - if not OuterRadius then - OuterRadius = 50 - end - - -- Apply SpawnFormation - for UnitID = 1, #SpawnTemplate.units do - if InnerRadius == 0 then - SpawnTemplate.units[UnitID].x = UnitPoint.x - SpawnTemplate.units[UnitID].y = UnitPoint.y - else - local CirclePos = routines.getRandPointInCircle( UnitPoint, OuterRadius, InnerRadius ) - SpawnTemplate.units[UnitID].x = CirclePos.x - SpawnTemplate.units[UnitID].y = CirclePos.y - end - self:T( 'SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) - end - - local SpawnPos = routines.getRandPointInCircle( UnitPoint, OuterRadius, InnerRadius ) - local Point = {} - Point.type = "Turning Point" - Point.x = SpawnPos.x - Point.y = SpawnPos.y - Point.action = "Cone" - Point.speed = 5 - - table.insert( SpawnTemplate.route.points, 2, Point ) - - return self:SpawnWithIndex( self.SpawnIndex ) - end - end - end - - return nil -end - ---- Will spawn a Group within a given @{Zone#ZONE}. --- Once the group is spawned within the zone, it will continue on its route. --- The first waypoint (where the group is spawned) is replaced with the zone coordinates. --- @param #SPAWN self --- @param Zone#ZONE Zone The zone where the group is to be spawned. --- @param #number ZoneRandomize (Optional) Set to true if you want to randomize the starting point in the zone. --- @param #number SpawnIndex (Optional) The index which group to spawn within the given zone. --- @return Group#GROUP that was spawned. --- @return #nil when nothing was spawned. -function SPAWN:SpawnInZone( Zone, ZoneRandomize, SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, Zone, ZoneRandomize, SpawnIndex } ) - - if Zone then - - if SpawnIndex then - else - SpawnIndex = self.SpawnIndex + 1 - end - - if self:_GetSpawnIndex( SpawnIndex ) then - - local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate - - if SpawnTemplate then - - local ZonePoint - - if ZoneRandomize == true then - ZonePoint = Zone:GetRandomVec2() - else - ZonePoint = Zone:GetPointVec2() - end - - SpawnTemplate.route.points[1].x = ZonePoint.x - SpawnTemplate.route.points[1].y = ZonePoint.y - - -- Apply SpawnFormation - for UnitID = 1, #SpawnTemplate.units do - local ZonePointUnit = Zone:GetRandomVec2() - SpawnTemplate.units[UnitID].x = ZonePointUnit.x - SpawnTemplate.units[UnitID].y = ZonePointUnit.y - self:T( 'SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) - end - - return self:SpawnWithIndex( self.SpawnIndex ) - end - end - end - - return nil -end - - - - ---- Will spawn a plane group in uncontrolled mode... --- This will be similar to the uncontrolled flag setting in the ME. --- @return #SPAWN self -function SPAWN:UnControlled() - self:F( { self.SpawnTemplatePrefix } ) - - self.SpawnUnControlled = true - - for SpawnGroupID = 1, self.SpawnMaxGroups do - self.SpawnGroups[SpawnGroupID].UnControlled = true - end - - return self -end - - - ---- Will return the SpawnGroupName either with with a specific count number or without any count. --- @param #SPAWN self --- @param #number SpawnIndex Is the number of the Group that is to be spawned. --- @return #string SpawnGroupName -function SPAWN:SpawnGroupName( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex } ) - - local SpawnPrefix = self.SpawnTemplatePrefix - if self.SpawnAliasPrefix then - SpawnPrefix = self.SpawnAliasPrefix - end - - if SpawnIndex then - local SpawnName = string.format( '%s#%03d', SpawnPrefix, SpawnIndex ) - self:T( SpawnName ) - return SpawnName - else - self:T( SpawnPrefix ) - return SpawnPrefix - end - -end - ---- Find the first alive group. --- @param #SPAWN self --- @param #number SpawnCursor A number holding the index from where to find the first group from. --- @return Group#GROUP, #number The group found, the new index where the group was found. --- @return #nil, #nil When no group is found, #nil is returned. -function SPAWN:GetFirstAliveGroup( SpawnCursor ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnCursor } ) - - for SpawnIndex = 1, self.SpawnCount do - local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) - if SpawnGroup and SpawnGroup:IsAlive() then - SpawnCursor = SpawnIndex - return SpawnGroup, SpawnCursor - end - end - - return nil, nil -end - - ---- Find the next alive group. --- @param #SPAWN self --- @param #number SpawnCursor A number holding the last found previous index. --- @return Group#GROUP, #number The group found, the new index where the group was found. --- @return #nil, #nil When no group is found, #nil is returned. -function SPAWN:GetNextAliveGroup( SpawnCursor ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnCursor } ) - - SpawnCursor = SpawnCursor + 1 - for SpawnIndex = SpawnCursor, self.SpawnCount do - local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) - if SpawnGroup and SpawnGroup:IsAlive() then - SpawnCursor = SpawnIndex - return SpawnGroup, SpawnCursor - end - end - - return nil, nil -end - ---- Find the last alive group during runtime. -function SPAWN:GetLastAliveGroup() - self:F( { self.SpawnTemplatePrefixself.SpawnAliasPrefix } ) - - self.SpawnIndex = self:_GetLastIndex() - for SpawnIndex = self.SpawnIndex, 1, -1 do - local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) - if SpawnGroup and SpawnGroup:IsAlive() then - self.SpawnIndex = SpawnIndex - return SpawnGroup - end - end - - self.SpawnIndex = nil - return nil -end - - - ---- Get the group from an index. --- Returns the group from the SpawnGroups list. --- If no index is given, it will return the first group in the list. --- @param #SPAWN self --- @param #number SpawnIndex The index of the group to return. --- @return Group#GROUP self -function SPAWN:GetGroupFromIndex( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnIndex } ) - - if not SpawnIndex then - SpawnIndex = 1 - end - - if self.SpawnGroups and self.SpawnGroups[SpawnIndex] then - local SpawnGroup = self.SpawnGroups[SpawnIndex].Group - return SpawnGroup - else - return nil - end -end - ---- Get the group index from a DCSUnit. --- The method will search for a #-mark, and will return the index behind the #-mark of the DCSUnit. --- It will return nil of no prefix was found. --- @param #SPAWN self --- @param DCSUnit The DCS unit to be searched. --- @return #string The prefix --- @return #nil Nothing found -function SPAWN:_GetGroupIndexFromDCSUnit( DCSUnit ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, DCSUnit } ) - - if DCSUnit and DCSUnit:getName() then - local IndexString = string.match( DCSUnit:getName(), "#.*-" ):sub( 2, -2 ) - self:T( IndexString ) - - if IndexString then - local Index = tonumber( IndexString ) - self:T( { "Index:", IndexString, Index } ) - return Index - end - end - - return nil -end - ---- Return the prefix of a DCSUnit. --- 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 DCSUnit The DCS unit to be searched. --- @return #string The prefix --- @return #nil Nothing found -function SPAWN:_GetPrefixFromDCSUnit( DCSUnit ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, DCSUnit } ) - - if DCSUnit and DCSUnit:getName() then - local SpawnPrefix = string.match( DCSUnit:getName(), ".*#" ) - if SpawnPrefix then - SpawnPrefix = SpawnPrefix:sub( 1, -2 ) - end - self:T( SpawnPrefix ) - return SpawnPrefix - end - - return nil -end - ---- Return the group within the SpawnGroups collection with input a DCSUnit. -function SPAWN:_GetGroupFromDCSUnit( DCSUnit ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, DCSUnit } ) - - if DCSUnit then - local SpawnPrefix = self:_GetPrefixFromDCSUnit( DCSUnit ) - - if self.SpawnTemplatePrefix == SpawnPrefix or ( self.SpawnAliasPrefix and self.SpawnAliasPrefix == SpawnPrefix ) then - local SpawnGroupIndex = self:_GetGroupIndexFromDCSUnit( DCSUnit ) - local SpawnGroup = self.SpawnGroups[SpawnGroupIndex].Group - self:T( SpawnGroup ) - return SpawnGroup - end - end - - return nil -end - - ---- Get the index from a given group. --- The function will search the name of the group for a #, and will return the number behind the #-mark. -function SPAWN:GetSpawnIndexFromGroup( SpawnGroup ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnGroup } ) - - local IndexString = string.match( SpawnGroup:GetName(), "#.*$" ):sub( 2 ) - local Index = tonumber( IndexString ) - - self:T( IndexString, Index ) - return Index - -end - ---- Return the last maximum index that can be used. -function SPAWN:_GetLastIndex() - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) - - return self.SpawnMaxGroups -end - ---- Initalize the SpawnGroups collection. -function SPAWN:_InitializeSpawnGroups( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnIndex } ) - - if not self.SpawnGroups[SpawnIndex] then - self.SpawnGroups[SpawnIndex] = {} - self.SpawnGroups[SpawnIndex].Visible = false - self.SpawnGroups[SpawnIndex].Spawned = false - self.SpawnGroups[SpawnIndex].UnControlled = false - self.SpawnGroups[SpawnIndex].SpawnTime = 0 - - self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix = self.SpawnTemplatePrefix - self.SpawnGroups[SpawnIndex].SpawnTemplate = self:_Prepare( self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix, SpawnIndex ) - end - - self:_RandomizeTemplate( SpawnIndex ) - self:_RandomizeRoute( SpawnIndex ) - --self:_TranslateRotate( SpawnIndex ) - - return self.SpawnGroups[SpawnIndex] -end - - - ---- Gets the CategoryID of the Group with the given SpawnPrefix -function SPAWN:_GetGroupCategoryID( SpawnPrefix ) - local TemplateGroup = Group.getByName( SpawnPrefix ) - - if TemplateGroup then - return TemplateGroup:getCategory() - else - return nil - end -end - ---- Gets the CoalitionID of the Group with the given SpawnPrefix -function SPAWN:_GetGroupCoalitionID( SpawnPrefix ) - local TemplateGroup = Group.getByName( SpawnPrefix ) - - if TemplateGroup then - return TemplateGroup:getCoalition() - else - return nil - end -end - ---- Gets the CountryID of the Group with the given SpawnPrefix -function SPAWN:_GetGroupCountryID( SpawnPrefix ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnPrefix } ) - - local TemplateGroup = Group.getByName( SpawnPrefix ) - - if TemplateGroup then - local TemplateUnits = TemplateGroup:getUnits() - return TemplateUnits[1]:getCountry() - else - return nil - end -end - ---- Gets the Group Template from the ME environment definition. --- This method used the @{DATABASE} object, which contains ALL initial and new spawned object in MOOSE. --- @param #SPAWN self --- @param #string SpawnTemplatePrefix --- @return @SPAWN self -function SPAWN:_GetTemplate( SpawnTemplatePrefix ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnTemplatePrefix } ) - - local SpawnTemplate = nil - - SpawnTemplate = routines.utils.deepCopy( _DATABASE.Templates.Groups[SpawnTemplatePrefix].Template ) - - if SpawnTemplate == nil then - error( 'No Template returned for SpawnTemplatePrefix = ' .. SpawnTemplatePrefix ) - end - - SpawnTemplate.SpawnCoalitionID = self:_GetGroupCoalitionID( SpawnTemplatePrefix ) - SpawnTemplate.SpawnCategoryID = self:_GetGroupCategoryID( SpawnTemplatePrefix ) - SpawnTemplate.SpawnCountryID = self:_GetGroupCountryID( SpawnTemplatePrefix ) - - self:T( { SpawnTemplate } ) - return SpawnTemplate -end - ---- Prepares the new Group Template. --- @param #SPAWN self --- @param #string SpawnTemplatePrefix --- @param #number SpawnIndex --- @return #SPAWN self -function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) - - local SpawnTemplate = self:_GetTemplate( SpawnTemplatePrefix ) - SpawnTemplate.name = self:SpawnGroupName( SpawnIndex ) - - SpawnTemplate.groupId = nil - --SpawnTemplate.lateActivation = false - SpawnTemplate.lateActivation = false -- TODO BUGFIX - - if SpawnTemplate.SpawnCategoryID == Group.Category.GROUND then - self:T( "For ground units, visible needs to be false..." ) - SpawnTemplate.visible = false -- TODO BUGFIX - end - - if SpawnTemplate.SpawnCategoryID == Group.Category.HELICOPTER or SpawnTemplate.SpawnCategoryID == Group.Category.AIRPLANE then - SpawnTemplate.uncontrolled = false - end - - for UnitID = 1, #SpawnTemplate.units do - SpawnTemplate.units[UnitID].name = string.format( SpawnTemplate.name .. '-%02d', UnitID ) - SpawnTemplate.units[UnitID].unitId = nil - SpawnTemplate.units[UnitID].x = SpawnTemplate.route.points[1].x - SpawnTemplate.units[UnitID].y = SpawnTemplate.route.points[1].y - end - - self:T( { "Template:", SpawnTemplate } ) - return SpawnTemplate - -end - ---- Private method randomizing the routes. --- @param #SPAWN self --- @param #number SpawnIndex The index of the group to be spawned. --- @return #SPAWN -function SPAWN:_RandomizeRoute( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeRoute, self.SpawnRandomizeRouteStartPoint, self.SpawnRandomizeRouteEndPoint, self.SpawnRandomizeRouteRadius } ) - - if self.SpawnRandomizeRoute then - local SpawnTemplate = self.SpawnGroups[SpawnIndex].SpawnTemplate - local RouteCount = #SpawnTemplate.route.points - - for t = self.SpawnRandomizeRouteStartPoint + 1, ( RouteCount - self.SpawnRandomizeRouteEndPoint ) do - SpawnTemplate.route.points[t].x = SpawnTemplate.route.points[t].x + math.random( self.SpawnRandomizeRouteRadius * -1, self.SpawnRandomizeRouteRadius ) - SpawnTemplate.route.points[t].y = SpawnTemplate.route.points[t].y + math.random( self.SpawnRandomizeRouteRadius * -1, self.SpawnRandomizeRouteRadius ) - -- TODO: manage altitude for airborne units ... - SpawnTemplate.route.points[t].alt = nil - --SpawnGroup.route.points[t].alt_type = nil - self:T( 'SpawnTemplate.route.points[' .. t .. '].x = ' .. SpawnTemplate.route.points[t].x .. ', SpawnTemplate.route.points[' .. t .. '].y = ' .. SpawnTemplate.route.points[t].y ) - end - end - - return self -end - ---- Private method that randomizes the template of the group. --- @param #SPAWN self --- @param #number SpawnIndex --- @return #SPAWN self -function SPAWN:_RandomizeTemplate( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeTemplate } ) - - if self.SpawnRandomizeTemplate then - self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix = self.SpawnTemplatePrefixTable[ math.random( 1, #self.SpawnTemplatePrefixTable ) ] - self.SpawnGroups[SpawnIndex].SpawnTemplate = self:_Prepare( self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix, SpawnIndex ) - self.SpawnGroups[SpawnIndex].SpawnTemplate.route = routines.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 - for UnitID = 1, #self.SpawnGroups[SpawnIndex].SpawnTemplate.units do - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].heading = self.SpawnTemplate.units[1].heading - end - end - - self:_RandomizeRoute( SpawnIndex ) - - return self -end - -function SPAWN:_TranslateRotate( SpawnIndex, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle } ) - - -- Translate - local TranslatedX = SpawnX - local TranslatedY = SpawnY - - -- Rotate - -- From Wikipedia: https://en.wikipedia.org/wiki/Rotation_matrix#Common_rotations - -- x' = x \cos \theta - y \sin \theta\ - -- y' = x \sin \theta + y \cos \theta\ - local RotatedX = - TranslatedX * math.cos( math.rad( SpawnAngle ) ) - + TranslatedY * math.sin( math.rad( SpawnAngle ) ) - local RotatedY = TranslatedX * math.sin( math.rad( SpawnAngle ) ) - + TranslatedY * math.cos( math.rad( SpawnAngle ) ) - - -- Assign - self.SpawnGroups[SpawnIndex].SpawnTemplate.x = SpawnRootX - RotatedX - self.SpawnGroups[SpawnIndex].SpawnTemplate.y = SpawnRootY + RotatedY - - - local SpawnUnitCount = table.getn( self.SpawnGroups[SpawnIndex].SpawnTemplate.units ) - for u = 1, SpawnUnitCount do - - -- Translate - local TranslatedX = SpawnX - local TranslatedY = SpawnY - 10 * ( u - 1 ) - - -- Rotate - local RotatedX = - TranslatedX * math.cos( math.rad( SpawnAngle ) ) - + TranslatedY * math.sin( math.rad( SpawnAngle ) ) - local RotatedY = TranslatedX * math.sin( math.rad( SpawnAngle ) ) - + TranslatedY * math.cos( math.rad( SpawnAngle ) ) - - -- Assign - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].x = SpawnRootX - RotatedX - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].y = SpawnRootY + RotatedY - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].heading = self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].heading + math.rad( SpawnAngle ) - end - - return self -end - ---- Get the next index of the groups to be spawned. This function is complicated, as it is used at several spaces. -function SPAWN:_GetSpawnIndex( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnMaxGroups, self.SpawnMaxUnitsAlive, self.AliveUnits, #self.SpawnTemplate.units } ) - - - if ( self.SpawnMaxGroups == 0 ) or ( SpawnIndex <= self.SpawnMaxGroups ) then - if ( self.SpawnMaxUnitsAlive == 0 ) or ( self.AliveUnits < self.SpawnMaxUnitsAlive * #self.SpawnTemplate.units ) or self.UnControlled then - if SpawnIndex and SpawnIndex >= self.SpawnCount + 1 then - self.SpawnCount = self.SpawnCount + 1 - SpawnIndex = self.SpawnCount - end - self.SpawnIndex = SpawnIndex - if not self.SpawnGroups[self.SpawnIndex] then - self:_InitializeSpawnGroups( self.SpawnIndex ) - end - else - return nil - end - else - return nil - end - - return self.SpawnIndex -end - - --- TODO Need to delete this... _DATABASE does this now ... -function SPAWN:_OnBirth( event ) - - if timer.getTime0() < timer.getAbsTime() then -- dont need to add units spawned in at the start of the mission if mist is loaded in init line - if event.initiator and event.initiator:getName() then - local EventPrefix = self:_GetPrefixFromDCSUnit( event.initiator ) - if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then - self:T( { "Birth event: " .. event.initiator:getName(), event } ) - --MessageToAll( "Mission command: unit " .. SpawnTemplatePrefix .. " spawned." , 5, EventPrefix .. '/Event') - self.AliveUnits = self.AliveUnits + 1 - self:T( "Alive Units: " .. self.AliveUnits ) - end - end - end - -end - ---- Obscolete --- @todo Need to delete this... _DATABASE does this now ... -function SPAWN:_OnDeadOrCrash( event ) - self:F( self.SpawnTemplatePrefix, event ) - - if event.initiator and event.initiator:getName() then - local EventPrefix = self:_GetPrefixFromDCSUnit( event.initiator ) - if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then - self:T( { "Dead event: " .. event.initiator:getName(), event } ) --- local DestroyedUnit = Unit.getByName( EventPrefix ) --- if DestroyedUnit and DestroyedUnit.getLife() <= 1.0 then - --MessageToAll( "Mission command: unit " .. SpawnTemplatePrefix .. " crashed." , 5, EventPrefix .. '/Event') - self.AliveUnits = self.AliveUnits - 1 - self:T( "Alive Units: " .. self.AliveUnits ) --- end - end - end -end - ---- Will detect AIR Units taking off... When the event takes place, the spawned Group is registered as airborne... --- This is needed to ensure that Re-SPAWNing only is done for landed AIR Groups. --- @todo Need to test for AIR Groups only... -function SPAWN:_OnTakeOff( event ) - self:F( self.SpawnTemplatePrefix, event ) - - if event.initiator and event.initiator:getName() then - local SpawnGroup = self:_GetGroupFromDCSUnit( event.initiator ) - if SpawnGroup then - self:T( { "TakeOff event: " .. event.initiator:getName(), event } ) - self:T( "self.Landed = false" ) - self.Landed = false - end - end -end - ---- Will detect AIR Units landing... When the event takes place, the spawned Group is registered as landed. --- This is needed to ensure that Re-SPAWNing is only done for landed AIR Groups. --- @todo Need to test for AIR Groups only... -function SPAWN:_OnLand( event ) - self:F( self.SpawnTemplatePrefix, event ) - - local SpawnUnit = event.initiator - if SpawnUnit and SpawnUnit:isExist() and Object.getCategory(SpawnUnit) == Object.Category.UNIT then - local SpawnGroup = self:_GetGroupFromDCSUnit( SpawnUnit ) - if SpawnGroup then - self:T( { "Landed event:" .. SpawnUnit:getName(), event } ) - self.Landed = true - self:T( "self.Landed = true" ) - if self.Landed and self.RepeatOnLanding then - local SpawnGroupIndex = self:GetSpawnIndexFromGroup( SpawnGroup ) - self:T( { "Landed:", "ReSpawn:", SpawnGroup:GetName(), SpawnGroupIndex } ) - self:ReSpawn( SpawnGroupIndex ) - end - end - end -end - ---- Will detect AIR Units shutting down their engines ... --- When the event takes place, and the method @{RepeatOnEngineShutDown} was called, the spawned Group will Re-SPAWN. --- But only when the Unit was registered to have landed. --- @param #SPAWN self --- @see _OnTakeOff --- @see _OnLand --- @todo Need to test for AIR Groups only... -function SPAWN:_OnEngineShutDown( event ) - self:F( self.SpawnTemplatePrefix, event ) - - local SpawnUnit = event.initiator - if SpawnUnit and SpawnUnit:isExist() and Object.getCategory(SpawnUnit) == Object.Category.UNIT then - local SpawnGroup = self:_GetGroupFromDCSUnit( SpawnUnit ) - if SpawnGroup then - self:T( { "EngineShutDown event: " .. SpawnUnit:getName(), event } ) - if self.Landed and self.RepeatOnEngineShutDown then - local SpawnGroupIndex = self:GetSpawnIndexFromGroup( SpawnGroup ) - self:T( { "EngineShutDown: ", "ReSpawn:", SpawnGroup:GetName(), SpawnGroupIndex } ) - self:ReSpawn( SpawnGroupIndex ) - end - end - end -end - ---- This function is called automatically by the Spawning scheduler. --- It is the internal worker method SPAWNing new Groups on the defined time intervals. -function SPAWN:_Scheduler() - self:F( { "_Scheduler", self.SpawnTemplatePrefix, self.SpawnAliasPrefix, self.SpawnIndex, self.SpawnMaxGroups, self.SpawnMaxUnitsAlive } ) - - -- Validate if there are still groups left in the batch... - self:Spawn() - - return true -end - -function SPAWN:_SpawnCleanUpScheduler() - self:F( { "CleanUp Scheduler:", self.SpawnTemplatePrefix } ) - - local SpawnCursor - local SpawnGroup, SpawnCursor = self:GetFirstAliveGroup( SpawnCursor ) - - self:T( { "CleanUp Scheduler:", SpawnGroup } ) - - while SpawnGroup do - - if SpawnGroup:AllOnGround() and SpawnGroup:GetMaxVelocity() < 1 then - if not self.SpawnCleanUpTimeStamps[SpawnGroup:GetName()] then - self.SpawnCleanUpTimeStamps[SpawnGroup:GetName()] = timer.getTime() - else - if self.SpawnCleanUpTimeStamps[SpawnGroup:GetName()] + self.SpawnCleanUpInterval < timer.getTime() then - self:T( { "CleanUp Scheduler:", "Cleaning:", SpawnGroup } ) - SpawnGroup:Destroy() - end - end - else - self.SpawnCleanUpTimeStamps[SpawnGroup:GetName()] = nil - end - - SpawnGroup, SpawnCursor = self:GetNextAliveGroup( SpawnCursor ) - - self:T( { "CleanUp Scheduler:", SpawnGroup } ) - - end - - return true -- Repeat - -end ---- Limit the simultaneous movement of Groups within a running Mission. --- This module is defined to improve the performance in missions, and to bring additional realism for GROUND vehicles. --- Performance: If in a DCSRTE there are a lot of moving GROUND units, then in a multi player mission, this WILL create lag if --- the main DCS execution core of your CPU is fully utilized. So, this class will limit the amount of simultaneous moving GROUND units --- on defined intervals (currently every minute). --- @module MOVEMENT - ---- the MOVEMENT class --- @type -MOVEMENT = { - ClassName = "MOVEMENT", -} - ---- Creates the main object which is handling the GROUND forces movement. --- @param table{string,...}|string MovePrefixes is a table of the Prefixes (names) of the GROUND Groups that need to be controlled by the MOVEMENT Object. --- @param number MoveMaximum is a number that defines the maximum amount of GROUND Units to be moving during one minute. --- @return MOVEMENT --- @usage --- -- Limit the amount of simultaneous moving units on the ground to prevent lag. --- Movement_US_Platoons = MOVEMENT:New( { 'US Tank Platoon Left', 'US Tank Platoon Middle', 'US Tank Platoon Right', 'US CH-47D Troops' }, 15 ) - -function MOVEMENT:New( MovePrefixes, MoveMaximum ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( { MovePrefixes, MoveMaximum } ) - - if type( MovePrefixes ) == 'table' then - self.MovePrefixes = MovePrefixes - else - self.MovePrefixes = { MovePrefixes } - end - self.MoveCount = 0 -- The internal counter of the amount of Moveing the has happened since MoveStart. - self.MoveMaximum = MoveMaximum -- Contains the Maximum amount of units that are allowed to move... - self.AliveUnits = 0 -- Contains the counter how many units are currently alive - self.MoveUnits = {} -- Reflects if the Moving for this MovePrefixes is going to be scheduled or not. - - _EVENTDISPATCHER:OnBirth( self.OnBirth, self ) - --- self:AddEvent( world.event.S_EVENT_BIRTH, self.OnBirth ) --- --- self:EnableEvents() - - self:ScheduleStart() - - return self -end - ---- Call this function to start the MOVEMENT scheduling. -function MOVEMENT:ScheduleStart() - self:F() - --self.MoveFunction = routines.scheduleFunction( self._Scheduler, { self }, timer.getTime() + 1, 120 ) - self.MoveFunction = SCHEDULER:New( self, self._Scheduler, {}, 1, 120 ) -end - ---- Call this function to stop the MOVEMENT scheduling. --- @todo need to implement it ... Forgot. -function MOVEMENT:ScheduleStop() - self:F() - -end - ---- Captures the birth events when new Units were spawned. --- @todo This method should become obsolete. The new @{DATABASE} class will handle the collection administration. -function MOVEMENT:OnBirth( Event ) - self:F( { Event } ) - - if timer.getTime0() < timer.getAbsTime() then -- dont need to add units spawned in at the start of the mission if mist is loaded in init line - if Event.IniDCSUnit then - self:T( "Birth object : " .. Event.IniDCSUnitName ) - if Event.IniDCSGroup and Event.IniDCSGroup:isExist() then - for MovePrefixID, MovePrefix in pairs( self.MovePrefixes ) do - if string.find( Event.IniDCSUnitName, MovePrefix, 1, true ) then - self.AliveUnits = self.AliveUnits + 1 - self.MoveUnits[Event.IniDCSUnitName] = Event.IniDCSGroupName - self:T( self.AliveUnits ) - end - end - end - end - _EVENTDISPATCHER:OnCrashForUnit( Event.IniDCSUnitName, self.OnDeadOrCrash, self ) - _EVENTDISPATCHER:OnDeadForUnit( Event.IniDCSUnitName, self.OnDeadOrCrash, self ) - end - -end - ---- Captures the Dead or Crash events when Units crash or are destroyed. --- @todo This method should become obsolete. The new @{DATABASE} class will handle the collection administration. -function MOVEMENT:OnDeadOrCrash( Event ) - self:F( { Event } ) - - if Event.IniDCSUnit then - self:T( "Dead object : " .. Event.IniDCSUnitName ) - for MovePrefixID, MovePrefix in pairs( self.MovePrefixes ) do - if string.find( Event.IniDCSUnitName, MovePrefix, 1, true ) then - self.AliveUnits = self.AliveUnits - 1 - self.MoveUnits[Event.IniDCSUnitName] = nil - self:T( self.AliveUnits ) - end - end - end -end - ---- This function is called automatically by the MOVEMENT scheduler. A new function is scheduled when MoveScheduled is true. -function MOVEMENT:_Scheduler() - self:F( { self.MovePrefixes, self.MoveMaximum, self.AliveUnits, self.MovementGroups } ) - - if self.AliveUnits > 0 then - local MoveProbability = ( self.MoveMaximum * 100 ) / self.AliveUnits - self:T( 'Move Probability = ' .. MoveProbability ) - - for MovementUnitName, MovementGroupName in pairs( self.MoveUnits ) do - local MovementGroup = Group.getByName( MovementGroupName ) - if MovementGroup and MovementGroup:isExist() then - local MoveOrStop = math.random( 1, 100 ) - self:T( 'MoveOrStop = ' .. MoveOrStop ) - if MoveOrStop <= MoveProbability then - self:T( 'Group continues moving = ' .. MovementGroupName ) - trigger.action.groupContinueMoving( MovementGroup ) - else - self:T( 'Group stops moving = ' .. MovementGroupName ) - trigger.action.groupStopMoving( MovementGroup ) - end - else - self.MoveUnits[MovementUnitName] = nil - end - end - end - return true -end ---- Provides defensive behaviour to a set of SAM sites within a running Mission. --- @module Sead --- @author to be searched on the forum --- @author (co) Flightcontrol (Modified and enriched with functionality) - ---- The SEAD class --- @type SEAD --- @extends Base#BASE -SEAD = { - ClassName = "SEAD", - TargetSkill = { - Average = { Evade = 50, DelayOff = { 10, 25 }, DelayOn = { 10, 30 } } , - Good = { Evade = 30, DelayOff = { 8, 20 }, DelayOn = { 20, 40 } } , - High = { Evade = 15, DelayOff = { 5, 17 }, DelayOn = { 30, 50 } } , - Excellent = { Evade = 10, DelayOff = { 3, 10 }, DelayOn = { 30, 60 } } - }, - SEADGroupPrefixes = {} -} - ---- Creates the main object which is handling defensive actions for SA sites or moving SA vehicles. --- When an anti radiation missile is fired (KH-58, KH-31P, KH-31A, KH-25MPU, HARM missiles), the SA will shut down their radars and will take evasive actions... --- Chances are big that the missile will miss. --- @param table{string,...}|string SEADGroupPrefixes which is a table of Prefixes of the SA Groups in the DCSRTE on which evasive actions need to be taken. --- @return SEAD --- @usage --- -- CCCP SEAD Defenses --- -- Defends the Russian SA installations from SEAD attacks. --- SEAD_RU_SAM_Defenses = SEAD:New( { 'RU SA-6 Kub', 'RU SA-6 Defenses', 'RU MI-26 Troops', 'RU Attack Gori' } ) -function SEAD:New( SEADGroupPrefixes ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( SEADGroupPrefixes ) - if type( SEADGroupPrefixes ) == 'table' then - for SEADGroupPrefixID, SEADGroupPrefix in pairs( SEADGroupPrefixes ) do - self.SEADGroupPrefixes[SEADGroupPrefix] = SEADGroupPrefix - end - else - self.SEADGroupNames[SEADGroupPrefixes] = SEADGroupPrefixes - end - _EVENTDISPATCHER:OnShot( self.EventShot, self ) - - return self -end - ---- Detects if an SA site was shot with an anti radiation missile. In this case, take evasive actions based on the skill level set within the ME. --- @see SEAD -function SEAD:EventShot( Event ) - self:F( { Event } ) - - local SEADUnit = Event.IniDCSUnit - local SEADUnitName = Event.IniDCSUnitName - local SEADWeapon = Event.Weapon -- Identify the weapon fired - local SEADWeaponName = Event.WeaponName -- return weapon type - --trigger.action.outText( string.format("Alerte, depart missile " ..string.format(SEADWeaponName)), 20) --debug message - -- Start of the 2nd loop - self:T( "Missile Launched = " .. SEADWeaponName ) - if SEADWeaponName == "KH-58" or SEADWeaponName == "KH-25MPU" or SEADWeaponName == "AGM-88" or SEADWeaponName == "KH-31A" or SEADWeaponName == "KH-31P" then -- Check if the missile is a SEAD - local _evade = math.random (1,100) -- random number for chance of evading action - local _targetMim = Event.Weapon:getTarget() -- Identify target - local _targetMimname = Unit.getName(_targetMim) - local _targetMimgroup = Unit.getGroup(Weapon.getTarget(SEADWeapon)) - local _targetMimgroupName = _targetMimgroup:getName() - local _targetMimcont= _targetMimgroup:getController() - local _targetskill = _DATABASE.Templates.Units[_targetMimname].Template.skill - self:T( self.SEADGroupPrefixes ) - self:T( _targetMimgroupName ) - local SEADGroupFound = false - for SEADGroupPrefixID, SEADGroupPrefix in pairs( self.SEADGroupPrefixes ) do - if string.find( _targetMimgroupName, SEADGroupPrefix, 1, true ) then - SEADGroupFound = true - self:T( 'Group Found' ) - break - end - end - if SEADGroupFound == true then - if _targetskill == "Random" then -- when skill is random, choose a skill - local Skills = { "Average", "Good", "High", "Excellent" } - _targetskill = Skills[ math.random(1,4) ] - end - self:T( _targetskill ) -- debug message for skill check - if self.TargetSkill[_targetskill] then - if (_evade > self.TargetSkill[_targetskill].Evade) then - self:T( string.format("Evading, target skill " ..string.format(_targetskill)) ) --debug message - local _targetMim = Weapon.getTarget(SEADWeapon) - local _targetMimname = Unit.getName(_targetMim) - local _targetMimgroup = Unit.getGroup(Weapon.getTarget(SEADWeapon)) - local _targetMimcont= _targetMimgroup:getController() - routines.groupRandomDistSelf(_targetMimgroup,300,'Diamond',250,20) -- move randomly - local SuppressedGroups1 = {} -- unit suppressed radar off for a random time - local function SuppressionEnd1(id) - id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN) - SuppressedGroups1[id.groupName] = nil - end - local id = { - groupName = _targetMimgroup, - ctrl = _targetMimcont - } - local delay1 = math.random(self.TargetSkill[_targetskill].DelayOff[1], self.TargetSkill[_targetskill].DelayOff[2]) - if SuppressedGroups1[id.groupName] == nil then - SuppressedGroups1[id.groupName] = { - SuppressionEndTime1 = timer.getTime() + delay1, - SuppressionEndN1 = SuppressionEndCounter1 --Store instance of SuppressionEnd() scheduled function - } - Controller.setOption(_targetMimcont, AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN) - timer.scheduleFunction(SuppressionEnd1, id, SuppressedGroups1[id.groupName].SuppressionEndTime1) --Schedule the SuppressionEnd() function - --trigger.action.outText( string.format("Radar Off " ..string.format(delay1)), 20) - end - - local SuppressedGroups = {} - local function SuppressionEnd(id) - id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.RED) - SuppressedGroups[id.groupName] = nil - end - local id = { - groupName = _targetMimgroup, - ctrl = _targetMimcont - } - local delay = math.random(self.TargetSkill[_targetskill].DelayOn[1], self.TargetSkill[_targetskill].DelayOn[2]) - if SuppressedGroups[id.groupName] == nil then - SuppressedGroups[id.groupName] = { - SuppressionEndTime = timer.getTime() + delay, - SuppressionEndN = SuppressionEndCounter --Store instance of SuppressionEnd() scheduled function - } - timer.scheduleFunction(SuppressionEnd, id, SuppressedGroups[id.groupName].SuppressionEndTime) --Schedule the SuppressionEnd() function - --trigger.action.outText( string.format("Radar On " ..string.format(delay)), 20) - end - end - end - end - end -end ---- Taking the lead of AI escorting your flight. --- --- @{#ESCORT} class --- ================ --- The @{#ESCORT} class allows you to interact with escorting AI on your flight and take the lead. --- Each escorting group can be commanded with a whole set of radio commands (radio menu in your flight, and then F10). --- --- The radio commands will vary according the category of the group. The richest set of commands are with Helicopters and AirPlanes. --- Ships and Ground troops will have a more limited set, but they can provide support through the bombing of targets designated by the other escorts. --- --- RADIO MENUs that can be created: --- ================================ --- Find a summary below of the current available commands: --- --- Navigation ...: --- --------------- --- Escort group navigation functions: --- --- * **"Join-Up and Follow at x meters":** The escort group fill follow you at about x meters, and they will follow you. --- * **"Flare":** Provides menu commands to let the escort group shoot a flare in the air in a color. --- * **"Smoke":** Provides menu commands to let the escort group smoke the air in a color. Note that smoking is only available for ground and naval troops. --- --- Hold position ...: --- ------------------ --- Escort group navigation functions: --- --- * **"At current location":** Stops the escort group and they will hover 30 meters above the ground at the position they stopped. --- * **"At client location":** Stops the escort group and they will hover 30 meters above the ground at the position they stopped. --- --- Report targets ...: --- ------------------- --- Report targets will make the escort group to report any target that it identifies within a 8km range. Any detected target can be attacked using the 4. Attack nearby targets function. (see below). --- --- * **"Report now":** Will report the current detected targets. --- * **"Report targets on":** Will make the escort group to report detected targets and will fill the "Attack nearby targets" menu list. --- * **"Report targets off":** Will stop detecting targets. --- --- Scan targets ...: --- ----------------- --- Menu items to pop-up the escort group for target scanning. After scanning, the escort group will resume with the mission or defined task. --- --- * **"Scan targets 30 seconds":** Scan 30 seconds for targets. --- * **"Scan targets 60 seconds":** Scan 60 seconds for targets. --- --- Attack targets ...: --- ------------------- --- This menu item will list all detected targets within a 15km range. Depending on the level of detection (known/unknown) and visuality, the targets type will also be listed. --- --- Request assistance from ...: --- ---------------------------- --- This menu item will list all detected targets within a 15km range, as with the menu item **Attack Targets**. --- This menu item allows to request attack support from other escorts supporting the current client group. --- eg. the function allows a player to request support from the Ship escort to attack a target identified by the Plane escort with its Tomahawk missiles. --- eg. the function allows a player to request support from other Planes escorting to bomb the unit with illumination missiles or bombs, so that the main plane escort can attack the area. --- --- ROE ...: --- -------- --- Sets the Rules of Engagement (ROE) of the escort group when in flight. --- --- * **"Hold Fire":** The escort group will hold fire. --- * **"Return Fire":** The escort group will return fire. --- * **"Open Fire":** The escort group will open fire on designated targets. --- * **"Weapon Free":** The escort group will engage with any target. --- --- Evasion ...: --- ------------ --- Will define the evasion techniques that the escort group will perform during flight or combat. --- --- * **"Fight until death":** The escort group will have no reaction to threats. --- * **"Use flares, chaff and jammers":** The escort group will use passive defense using flares and jammers. No evasive manoeuvres are executed. --- * **"Evade enemy fire":** The rescort group will evade enemy fire before firing. --- * **"Go below radar and evade fire":** The escort group will perform evasive vertical manoeuvres. --- --- Resume Mission ...: --- ------------------- --- Escort groups can have their own mission. This menu item will allow the escort group to resume their Mission from a given waypoint. --- Note that this is really fantastic, as you now have the dynamic of taking control of the escort groups, and allowing them to resume their path or mission. --- --- 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 initialization methods. --- ============================== --- The following menus are created within the RADIO MENU 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: --- --- -- First find the GROUP object and the CLIENT object. --- local EscortClient = CLIENT:FindByName( "Unit Name" ) -- The Unit Name is the name of the unit flagged with the skill Client in the mission editor. --- local EscortGroup = GROUP:FindByName( "Group Name" ) -- The Group Name is the name of the group that will escort the Escort Client. --- --- -- Now use these 2 objects to construct the new EscortPlanes object. --- EscortPlanes = ESCORT:New( EscortClient, EscortGroup, "Desert", "Welcome to the mission. You are escorted by a plane with code name 'Desert', which can be instructed through the F10 radio menu." ) --- --- --- --- @module Escort --- @author FlightControl - ---- ESCORT class --- @type ESCORT --- @extends Base#BASE --- @field Client#CLIENT EscortClient --- @field Group#GROUP EscortGroup --- @field #string EscortName --- @field #ESCORT.MODE EscortMode The mode the escort is in. --- @field Scheduler#SCHEDULER FollowScheduler The instance of the SCHEDULER class. --- @field #number FollowDistance The current follow distance. --- @field #boolean ReportTargets If true, nearby targets are reported. --- @Field DCSTypes#AI.Option.Air.val.ROE OptionROE Which ROE is set to the EscortGroup. --- @field DCSTypes#AI.Option.Air.val.REACTION_ON_THREAT OptionReactionOnThreat Which REACTION_ON_THREAT is set to the EscortGroup. --- @field Menu#MENU_CLIENT EscortMenuResumeMission -ESCORT = { - ClassName = "ESCORT", - EscortName = nil, -- The Escort Name - EscortClient = nil, - EscortGroup = nil, - EscortMode = 1, - MODE = { - FOLLOW = 1, - MISSION = 2, - }, - Targets = {}, -- The identified targets - FollowScheduler = nil, - ReportTargets = true, - OptionROE = AI.Option.Air.val.ROE.OPEN_FIRE, - OptionReactionOnThreat = AI.Option.Air.val.REACTION_ON_THREAT.ALLOW_ABORT_MISSION, - SmokeDirectionVector = false, - TaskPoints = {} -} - ---- ESCORT.Mode class --- @type ESCORT.MODE --- @field #number FOLLOW --- @field #number MISSION - ---- MENUPARAM type --- @type MENUPARAM --- @field #ESCORT ParamSelf --- @field #Distance ParamDistance --- @field #function ParamFunction --- @field #string ParamMessage - ---- ESCORT class constructor for an AI group --- @param #ESCORT self --- @param Client#CLIENT EscortClient The client escorted by the EscortGroup. --- @param Group#GROUP EscortGroup The group AI escorting the EscortClient. --- @param #string EscortName Name of the escort. --- @return #ESCORT self --- @usage --- -- Declare a new EscortPlanes object as follows: --- --- -- First find the GROUP object and the CLIENT object. --- local EscortClient = CLIENT:FindByName( "Unit Name" ) -- The Unit Name is the name of the unit flagged with the skill Client in the mission editor. --- local EscortGroup = GROUP:FindByName( "Group Name" ) -- The Group Name is the name of the group that will escort the Escort Client. --- --- -- Now use these 2 objects to construct the new EscortPlanes object. --- EscortPlanes = ESCORT:New( EscortClient, EscortGroup, "Desert", "Welcome to the mission. You are escorted by a plane with code name 'Desert', which can be instructed through the F10 radio menu." ) -function ESCORT:New( EscortClient, EscortGroup, EscortName, EscortBriefing ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( { EscortClient, EscortGroup, EscortName } ) - - self.EscortClient = EscortClient -- Client#CLIENT - self.EscortGroup = EscortGroup -- Group#GROUP - self.EscortName = EscortName - self.EscortBriefing = EscortBriefing - - -- Set EscortGroup known at EscortClient. - if not self.EscortClient._EscortGroups then - self.EscortClient._EscortGroups = {} - end - - if not self.EscortClient._EscortGroups[EscortGroup:GetName()] then - self.EscortClient._EscortGroups[EscortGroup:GetName()] = {} - self.EscortClient._EscortGroups[EscortGroup:GetName()].EscortGroup = self.EscortGroup - self.EscortClient._EscortGroups[EscortGroup:GetName()].EscortName = self.EscortName - self.EscortClient._EscortGroups[EscortGroup:GetName()].Targets = {} - end - - self.EscortMenu = MENU_CLIENT:New( self.EscortClient, self.EscortName ) - - self.EscortGroup:WayPointInitialize(1) - - self.EscortGroup:OptionROTVertical() - self.EscortGroup:OptionROEOpenFire() - - EscortGroup:MessageToClient( EscortGroup:GetCategoryName() .. " '" .. EscortName .. "' (" .. EscortGroup:GetCallsign() .. ") reporting! " .. - "We're escorting your flight. " .. - "Use the Radio Menu and F10 and use the options under + " .. EscortName .. "\n", - 60, EscortClient - ) - - self.FollowDistance = 100 - self.CT1 = 0 - self.GT1 = 0 - self.FollowScheduler = SCHEDULER:New( self, self._FollowScheduler, {}, 1, .5, .01 ) - self.EscortMode = ESCORT.MODE.MISSION - self.FollowScheduler:Stop() - - 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 #ESCORT self --- @param #boolean SmokeDirection If true, then the direction vector will be smoked. -function ESCORT:TestSmokeDirectionVector( SmokeDirection ) - self.SmokeDirectionVector = ( SmokeDirection == true ) and true or false -end - - ---- Defines the default menus --- @param #ESCORT self --- @return #ESCORT -function ESCORT:Menus() - self:F() - - self:MenuFollowAt( 100 ) - self:MenuFollowAt( 200 ) - self:MenuFollowAt( 300 ) - self:MenuFollowAt( 400 ) - - self:MenuScanForTargets( 100, 60 ) - - self:MenuHoldAtEscortPosition( 30 ) - self:MenuHoldAtLeaderPosition( 30 ) - - self:MenuFlare() - self:MenuSmoke() - - self:MenuReportTargets( 60 ) - self:MenuAssistedAttack() - self:MenuROE() - self:MenuEvasion() - self:MenuResumeMission() - - - return self -end - - - ---- Defines a menu slot to let the escort Join and Follow you at a certain distance. --- This menu will appear under **Navigation**. --- @param #ESCORT self --- @param DCSTypes#Distance Distance The distance in meters that the escort needs to follow the client. --- @return #ESCORT -function ESCORT:MenuFollowAt( Distance ) - self:F(Distance) - - if self.EscortGroup:IsAir() then - if not self.EscortMenuReportNavigation then - self.EscortMenuReportNavigation = MENU_CLIENT:New( self.EscortClient, "Navigation", self.EscortMenu ) - end - - if not self.EscortMenuJoinUpAndFollow then - self.EscortMenuJoinUpAndFollow = {} - end - - self.EscortMenuJoinUpAndFollow[#self.EscortMenuJoinUpAndFollow+1] = MENU_CLIENT_COMMAND:New( self.EscortClient, "Join-Up and Follow at " .. Distance, self.EscortMenuReportNavigation, ESCORT._JoinUpAndFollow, { ParamSelf = self, ParamDistance = Distance } ) - - self.EscortMode = ESCORT.MODE.FOLLOW - end - - return self -end - ---- Defines a menu slot to let the escort hold at their current position and stay low with a specified height during a specified time in seconds. --- This menu will appear under **Hold position**. --- @param #ESCORT self --- @param 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 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 #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed. --- @return #ESCORT --- TODO: Implement Seconds parameter. Challenge is to first develop the "continue from last activity" function. -function ESCORT:MenuHoldAtEscortPosition( Height, Seconds, MenuTextFormat ) - self:F( { Height, Seconds, MenuTextFormat } ) - - if self.EscortGroup:IsAir() then - - if not self.EscortMenuHold then - self.EscortMenuHold = MENU_CLIENT:New( self.EscortClient, "Hold position", self.EscortMenu ) - end - - if not Height then - Height = 30 - end - - if not Seconds then - Seconds = 0 - end - - local MenuText = "" - if not MenuTextFormat then - if Seconds == 0 then - MenuText = string.format( "Hold at %d meter", Height ) - else - MenuText = string.format( "Hold at %d meter for %d seconds", Height, Seconds ) - end - else - if Seconds == 0 then - MenuText = string.format( MenuTextFormat, Height ) - else - MenuText = string.format( MenuTextFormat, Height, Seconds ) - end - end - - if not self.EscortMenuHoldPosition then - self.EscortMenuHoldPosition = {} - end - - self.EscortMenuHoldPosition[#self.EscortMenuHoldPosition+1] = MENU_CLIENT_COMMAND - :New( - self.EscortClient, - MenuText, - self.EscortMenuHold, - ESCORT._HoldPosition, - { ParamSelf = self, - ParamOrbitGroup = self.EscortGroup, - ParamHeight = Height, - ParamSeconds = Seconds - } - ) - end - - return self -end - - ---- Defines a menu slot to let the escort hold at the client position and stay low with a specified height during a specified time in seconds. --- This menu will appear under **Navigation**. --- @param #ESCORT self --- @param 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 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 #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain one or two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed. --- @return #ESCORT --- TODO: Implement Seconds parameter. Challenge is to first develop the "continue from last activity" function. -function ESCORT:MenuHoldAtLeaderPosition( Height, Seconds, MenuTextFormat ) - self:F( { Height, Seconds, MenuTextFormat } ) - - if self.EscortGroup:IsAir() then - - if not self.EscortMenuHold then - self.EscortMenuHold = MENU_CLIENT:New( self.EscortClient, "Hold position", self.EscortMenu ) - end - - if not Height then - Height = 30 - end - - if not Seconds then - Seconds = 0 - end - - local MenuText = "" - if not MenuTextFormat then - if Seconds == 0 then - MenuText = string.format( "Rejoin and hold at %d meter", Height ) - else - MenuText = string.format( "Rejoin and hold at %d meter for %d seconds", Height, Seconds ) - end - else - if Seconds == 0 then - MenuText = string.format( MenuTextFormat, Height ) - else - MenuText = string.format( MenuTextFormat, Height, Seconds ) - end - end - - if not self.EscortMenuHoldAtLeaderPosition then - self.EscortMenuHoldAtLeaderPosition = {} - end - - self.EscortMenuHoldAtLeaderPosition[#self.EscortMenuHoldAtLeaderPosition+1] = MENU_CLIENT_COMMAND - :New( - self.EscortClient, - MenuText, - self.EscortMenuHold, - ESCORT._HoldPosition, - { ParamSelf = self, - ParamOrbitGroup = self.EscortClient, - ParamHeight = Height, - ParamSeconds = Seconds - } - ) - end - - return self -end - ---- Defines a menu slot to let the escort scan for targets at a certain height for a certain time in seconds. --- This menu will appear under **Scan targets**. --- @param #ESCORT self --- @param 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 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 #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain one or two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed. --- @return #ESCORT -function ESCORT:MenuScanForTargets( Height, Seconds, MenuTextFormat ) - self:F( { Height, Seconds, MenuTextFormat } ) - - if self.EscortGroup:IsAir() then - if not self.EscortMenuScan then - self.EscortMenuScan = MENU_CLIENT:New( self.EscortClient, "Scan for targets", self.EscortMenu ) - end - - if not Height then - Height = 100 - end - - if not Seconds then - Seconds = 30 - end - - local MenuText = "" - if not MenuTextFormat then - if Seconds == 0 then - MenuText = string.format( "At %d meter", Height ) - else - MenuText = string.format( "At %d meter for %d seconds", Height, Seconds ) - end - else - if Seconds == 0 then - MenuText = string.format( MenuTextFormat, Height ) - else - MenuText = string.format( MenuTextFormat, Height, Seconds ) - end - end - - if not self.EscortMenuScanForTargets then - self.EscortMenuScanForTargets = {} - end - - self.EscortMenuScanForTargets[#self.EscortMenuScanForTargets+1] = MENU_CLIENT_COMMAND - :New( - self.EscortClient, - MenuText, - self.EscortMenuScan, - ESCORT._ScanTargets, - { ParamSelf = self, - ParamScanDuration = 30 - } - ) - end - - return self -end - - - ---- Defines a menu slot to let the escort disperse a flare in a certain color. --- This menu will appear under **Navigation**. --- The flare will be fired from the first unit in the group. --- @param #ESCORT self --- @param #string MenuTextFormat Optional parameter that shows the menu option text. If no text is given, the default text will be displayed. --- @return #ESCORT -function ESCORT:MenuFlare( MenuTextFormat ) - self:F() - - if not self.EscortMenuReportNavigation then - self.EscortMenuReportNavigation = MENU_CLIENT:New( self.EscortClient, "Navigation", self.EscortMenu ) - end - - local MenuText = "" - if not MenuTextFormat then - MenuText = "Flare" - else - MenuText = MenuTextFormat - end - - if not self.EscortMenuFlare then - self.EscortMenuFlare = MENU_CLIENT:New( self.EscortClient, MenuText, self.EscortMenuReportNavigation, ESCORT._Flare, { ParamSelf = self } ) - self.EscortMenuFlareGreen = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release green flare", self.EscortMenuFlare, ESCORT._Flare, { ParamSelf = self, ParamColor = UNIT.FlareColor.Green, ParamMessage = "Released a green flare!" } ) - self.EscortMenuFlareRed = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release red flare", self.EscortMenuFlare, ESCORT._Flare, { ParamSelf = self, ParamColor = UNIT.FlareColor.Red, ParamMessage = "Released a red flare!" } ) - self.EscortMenuFlareWhite = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release white flare", self.EscortMenuFlare, ESCORT._Flare, { ParamSelf = self, ParamColor = UNIT.FlareColor.White, ParamMessage = "Released a white flare!" } ) - self.EscortMenuFlareYellow = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release yellow flare", self.EscortMenuFlare, ESCORT._Flare, { ParamSelf = self, ParamColor = UNIT.FlareColor.Yellow, ParamMessage = "Released a yellow flare!" } ) - end - - return self -end - ---- Defines a menu slot to let the escort disperse a smoke in a certain color. --- This menu will appear under **Navigation**. --- Note that smoke menu options will only be displayed for ships and ground units. Not for air units. --- The smoke will be fired from the first unit in the group. --- @param #ESCORT self --- @param #string MenuTextFormat Optional parameter that shows the menu option text. If no text is given, the default text will be displayed. --- @return #ESCORT -function ESCORT:MenuSmoke( MenuTextFormat ) - self:F() - - if not self.EscortGroup:IsAir() then - if not self.EscortMenuReportNavigation then - self.EscortMenuReportNavigation = MENU_CLIENT:New( self.EscortClient, "Navigation", self.EscortMenu ) - end - - local MenuText = "" - if not MenuTextFormat then - MenuText = "Smoke" - else - MenuText = MenuTextFormat - end - - if not self.EscortMenuSmoke then - self.EscortMenuSmoke = MENU_CLIENT:New( self.EscortClient, "Smoke", self.EscortMenuReportNavigation, ESCORT._Smoke, { ParamSelf = self } ) - self.EscortMenuSmokeGreen = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release green smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.Green, ParamMessage = "Releasing green smoke!" } ) - self.EscortMenuSmokeRed = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release red smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.Red, ParamMessage = "Releasing red smoke!" } ) - self.EscortMenuSmokeWhite = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release white smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.White, ParamMessage = "Releasing white smoke!" } ) - self.EscortMenuSmokeOrange = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release orange smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.Orange, ParamMessage = "Releasing orange smoke!" } ) - self.EscortMenuSmokeBlue = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release blue smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.Blue, ParamMessage = "Releasing blue smoke!" } ) - end - end - - return self -end - ---- Defines a menu slot to let the escort report their current detected targets with a specified time interval in seconds. --- This menu will appear under **Report targets**. --- Note that if a report targets menu is not specified, no targets will be detected by the escort, and the attack and assisted attack menus will not be displayed. --- @param #ESCORT self --- @param 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. --- @return #ESCORT -function ESCORT:MenuReportTargets( Seconds ) - self:F( { Seconds } ) - - if not self.EscortMenuReportNearbyTargets then - self.EscortMenuReportNearbyTargets = MENU_CLIENT:New( self.EscortClient, "Report targets", self.EscortMenu ) - end - - if not Seconds then - Seconds = 30 - end - - -- Report Targets - self.EscortMenuReportNearbyTargetsNow = MENU_CLIENT_COMMAND:New( self.EscortClient, "Report targets now!", self.EscortMenuReportNearbyTargets, ESCORT._ReportNearbyTargetsNow, { ParamSelf = self } ) - self.EscortMenuReportNearbyTargetsOn = MENU_CLIENT_COMMAND:New( self.EscortClient, "Report targets on", self.EscortMenuReportNearbyTargets, ESCORT._SwitchReportNearbyTargets, { ParamSelf = self, ParamReportTargets = true } ) - self.EscortMenuReportNearbyTargetsOff = MENU_CLIENT_COMMAND:New( self.EscortClient, "Report targets off", self.EscortMenuReportNearbyTargets, ESCORT._SwitchReportNearbyTargets, { ParamSelf = self, ParamReportTargets = false, } ) - - -- Attack Targets - self.EscortMenuAttackNearbyTargets = MENU_CLIENT:New( self.EscortClient, "Attack targets", self.EscortMenu ) - - - --self.ReportTargetsScheduler = routines.scheduleFunction( self._ReportTargetsScheduler, { self }, timer.getTime() + 1, Seconds ) - self.ReportTargetsScheduler = SCHEDULER:New( self, self._ReportTargetsScheduler, {}, 1, Seconds ) - - return self -end - ---- Defines a menu slot to let the escort attack its detected targets using assisted attack from another escort joined also with the client. --- This menu will appear under **Request assistance from**. --- Note that this method needs to be preceded with the method MenuReportTargets. --- @param #ESCORT self --- @return #ESCORT -function ESCORT:MenuAssistedAttack() - self:F() - - -- Request assistance from other escorts. - -- This is very useful to let f.e. an escorting ship attack a target detected by an escorting plane... - self.EscortMenuTargetAssistance = MENU_CLIENT:New( self.EscortClient, "Request assistance from", self.EscortMenu ) - - return self -end - ---- Defines a menu to let the escort set its rules of engagement. --- All rules of engagement will appear under the menu **ROE**. --- @param #ESCORT self --- @return #ESCORT -function ESCORT:MenuROE( MenuTextFormat ) - self:F( MenuTextFormat ) - - if not self.EscortMenuROE then - -- Rules of Engagement - self.EscortMenuROE = MENU_CLIENT:New( self.EscortClient, "ROE", self.EscortMenu ) - if self.EscortGroup:OptionROEHoldFirePossible() then - self.EscortMenuROEHoldFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Hold Fire", self.EscortMenuROE, ESCORT._ROE, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROEHoldFire(), ParamMessage = "Holding weapons!" } ) - end - if self.EscortGroup:OptionROEReturnFirePossible() then - self.EscortMenuROEReturnFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Return Fire", self.EscortMenuROE, ESCORT._ROE, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROEReturnFire(), ParamMessage = "Returning fire!" } ) - end - if self.EscortGroup:OptionROEOpenFirePossible() then - self.EscortMenuROEOpenFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Open Fire", self.EscortMenuROE, ESCORT._ROE, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROEOpenFire(), ParamMessage = "Opening fire on designated targets!!" } ) - end - if self.EscortGroup:OptionROEWeaponFreePossible() then - self.EscortMenuROEWeaponFree = MENU_CLIENT_COMMAND:New( self.EscortClient, "Weapon Free", self.EscortMenuROE, ESCORT._ROE, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROEWeaponFree(), ParamMessage = "Opening fire on targets of opportunity!" } ) - end - end - - return self -end - - ---- Defines a menu to let the escort set its evasion when under threat. --- All rules of engagement will appear under the menu **Evasion**. --- @param #ESCORT self --- @return #ESCORT -function ESCORT:MenuEvasion( MenuTextFormat ) - self:F( MenuTextFormat ) - - if self.EscortGroup:IsAir() then - if not self.EscortMenuEvasion then - -- Reaction to Threats - self.EscortMenuEvasion = MENU_CLIENT:New( self.EscortClient, "Evasion", self.EscortMenu ) - if self.EscortGroup:OptionROTNoReactionPossible() then - self.EscortMenuEvasionNoReaction = MENU_CLIENT_COMMAND:New( self.EscortClient, "Fight until death", self.EscortMenuEvasion, ESCORT._ROT, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROTNoReaction(), ParamMessage = "Fighting until death!" } ) - end - if self.EscortGroup:OptionROTPassiveDefensePossible() then - self.EscortMenuEvasionPassiveDefense = MENU_CLIENT_COMMAND:New( self.EscortClient, "Use flares, chaff and jammers", self.EscortMenuEvasion, ESCORT._ROT, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROTPassiveDefense(), ParamMessage = "Defending using jammers, chaff and flares!" } ) - end - if self.EscortGroup:OptionROTEvadeFirePossible() then - self.EscortMenuEvasionEvadeFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Evade enemy fire", self.EscortMenuEvasion, ESCORT._ROT, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROTEvadeFire(), ParamMessage = "Evading on enemy fire!" } ) - end - if self.EscortGroup:OptionROTVerticalPossible() then - self.EscortMenuOptionEvasionVertical = MENU_CLIENT_COMMAND:New( self.EscortClient, "Go below radar and evade fire", self.EscortMenuEvasion, ESCORT._ROT, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROTVertical(), ParamMessage = "Evading on enemy fire with vertical manoeuvres!" } ) - end - end - end - - return self -end - ---- Defines a menu to let the escort resume its mission from a waypoint on its route. --- All rules of engagement will appear under the menu **Resume mission from**. --- @param #ESCORT self --- @return #ESCORT -function ESCORT:MenuResumeMission() - self:F() - - if not self.EscortMenuResumeMission then - -- Mission Resume Menu Root - self.EscortMenuResumeMission = MENU_CLIENT:New( self.EscortClient, "Resume mission from", self.EscortMenu ) - end - - return self -end - - ---- @param #MENUPARAM MenuParam -function ESCORT._HoldPosition( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - local OrbitGroup = MenuParam.ParamOrbitGroup -- Group#GROUP - local OrbitUnit = OrbitGroup:GetUnit(1) -- Unit#UNIT - local OrbitHeight = MenuParam.ParamHeight - local OrbitSeconds = MenuParam.ParamSeconds -- Not implemented yet - - self.FollowScheduler:Stop() - - local PointFrom = {} - local GroupPoint = EscortGroup:GetUnit(1):GetPointVec3() - PointFrom = {} - PointFrom.x = GroupPoint.x - PointFrom.y = GroupPoint.z - PointFrom.speed = 250 - PointFrom.type = AI.Task.WaypointType.TURNING_POINT - PointFrom.alt = GroupPoint.y - PointFrom.alt_type = AI.Task.AltitudeType.BARO - - local OrbitPoint = OrbitUnit:GetPointVec2() - local PointTo = {} - PointTo.x = OrbitPoint.x - PointTo.y = OrbitPoint.y - PointTo.speed = 250 - PointTo.type = AI.Task.WaypointType.TURNING_POINT - PointTo.alt = OrbitHeight - PointTo.alt_type = AI.Task.AltitudeType.BARO - PointTo.task = EscortGroup:TaskOrbitCircleAtVec2( OrbitPoint, OrbitHeight, 0 ) - - local Points = { PointFrom, PointTo } - - EscortGroup:OptionROEHoldFire() - EscortGroup:OptionROTPassiveDefense() - - EscortGroup:SetTask( EscortGroup:TaskRoute( Points ) ) - EscortGroup:MessageToClient( "Orbiting at location.", 10, EscortClient ) - -end - ---- @param #MENUPARAM MenuParam -function ESCORT._JoinUpAndFollow( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - self.Distance = MenuParam.ParamDistance - - self:JoinUpAndFollow( EscortGroup, EscortClient, self.Distance ) -end - ---- JoinsUp and Follows a CLIENT. --- @param Escort#ESCORT self --- @param Group#GROUP EscortGroup --- @param Client#CLIENT EscortClient --- @param DCSTypes#Distance Distance -function ESCORT:JoinUpAndFollow( EscortGroup, EscortClient, Distance ) - self:F( { EscortGroup, EscortClient, Distance } ) - - self.FollowScheduler:Stop() - - EscortGroup:OptionROEHoldFire() - EscortGroup:OptionROTPassiveDefense() - - self.EscortMode = ESCORT.MODE.FOLLOW - - self.CT1 = 0 - self.GT1 = 0 - self.FollowScheduler:Start() - - EscortGroup:MessageToClient( "Rejoining and Following at " .. Distance .. "!", 30, EscortClient ) -end - ---- @param #MENUPARAM MenuParam -function ESCORT._Flare( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - local Color = MenuParam.ParamColor - local Message = MenuParam.ParamMessage - - EscortGroup:GetUnit(1):Flare( Color ) - EscortGroup:MessageToClient( Message, 10, EscortClient ) -end - ---- @param #MENUPARAM MenuParam -function ESCORT._Smoke( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - local Color = MenuParam.ParamColor - local Message = MenuParam.ParamMessage - - EscortGroup:GetUnit(1):Smoke( Color ) - EscortGroup:MessageToClient( Message, 10, EscortClient ) -end - - ---- @param #MENUPARAM MenuParam -function ESCORT._ReportNearbyTargetsNow( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - self:_ReportTargetsScheduler() - -end - -function ESCORT._SwitchReportNearbyTargets( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - self.ReportTargets = MenuParam.ParamReportTargets - - if self.ReportTargets then - if not self.ReportTargetsScheduler then - --self.ReportTargetsScheduler = routines.scheduleFunction( self._ReportTargetsScheduler, { self }, timer.getTime() + 1, 30 ) - self.ReportTargetsScheduler = SCHEDULER:New( self, self._ReportTargetsScheduler, {}, 1, 30 ) - end - else - routines.removeFunction( self.ReportTargetsScheduler ) - self.ReportTargetsScheduler = nil - end -end - ---- @param #MENUPARAM MenuParam -function ESCORT._ScanTargets( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - local ScanDuration = MenuParam.ParamScanDuration - - self.FollowScheduler:Stop() - - if EscortGroup:IsHelicopter() then - SCHEDULER:New( EscortGroup, EscortGroup.PushTask, - { EscortGroup:TaskControlled( - EscortGroup:TaskOrbitCircle( 200, 20 ), - EscortGroup:TaskCondition( nil, nil, nil, nil, ScanDuration, nil ) - ) - }, - 1 - ) - elseif EscortGroup:IsAirPlane() then - SCHEDULER:New( EscortGroup, EscortGroup.PushTask, - { EscortGroup:TaskControlled( - EscortGroup:TaskOrbitCircle( 1000, 500 ), - EscortGroup:TaskCondition( nil, nil, nil, nil, ScanDuration, nil ) - ) - }, - 1 - ) - end - - EscortGroup:MessageToClient( "Scanning targets for " .. ScanDuration .. " seconds.", ScanDuration, EscortClient ) - - if self.EscortMode == ESCORT.MODE.FOLLOW then - self.FollowScheduler:Start() - end - -end - ---- @param Group#GROUP EscortGroup -function _Resume( EscortGroup ) - env.info( '_Resume' ) - - local Escort = EscortGroup:GetState( EscortGroup, "Escort" ) - env.info( "EscortMode = " .. Escort.EscortMode ) - if Escort.EscortMode == ESCORT.MODE.FOLLOW then - Escort:JoinUpAndFollow( EscortGroup, Escort.EscortClient, Escort.Distance ) - end - -end - ---- @param #MENUPARAM MenuParam -function ESCORT._AttackTarget( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - - local EscortClient = self.EscortClient - local AttackUnit = MenuParam.ParamUnit -- Unit#UNIT - - self.FollowScheduler:Stop() - - self:T( AttackUnit ) - - if EscortGroup:IsAir() then - EscortGroup:OptionROEOpenFire() - EscortGroup:OptionROTPassiveDefense() - EscortGroup:SetState( EscortGroup, "Escort", self ) --- routines.scheduleFunction( --- EscortGroup.PushTask, --- { EscortGroup, --- EscortGroup:TaskCombo( --- { EscortGroup:TaskAttackUnit( AttackUnit ), --- EscortGroup:TaskFunction( 1, 2, "_Resume", {"''"} ) --- } --- ) --- }, timer.getTime() + 10 --- ) - SCHEDULER:New( EscortGroup, - EscortGroup.PushTask, - { EscortGroup:TaskCombo( - { EscortGroup:TaskAttackUnit( AttackUnit ), - EscortGroup:TaskFunction( 1, 2, "_Resume", { "''" } ) - } - ) - }, 10 - ) - else --- routines.scheduleFunction( --- EscortGroup.PushTask, --- { EscortGroup, --- EscortGroup:TaskCombo( --- { EscortGroup:TaskFireAtPoint( AttackUnit:GetPointVec2(), 50 ) --- } --- ) --- }, timer.getTime() + 10 --- ) - SCHEDULER:New( EscortGroup, - EscortGroup.PushTask, - { EscortGroup:TaskCombo( - { EscortGroup:TaskFireAtPoint( AttackUnit:GetPointVec2(), 50 ) - } - ) - }, 10 - ) - end - - EscortGroup:MessageToClient( "Engaging Designated Unit!", 10, EscortClient ) - -end - ---- @param #MENUPARAM MenuParam -function ESCORT._AssistTarget( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - local EscortGroupAttack = MenuParam.ParamEscortGroup - local AttackUnit = MenuParam.ParamUnit -- Unit#UNIT - - self.FollowScheduler:Stop() - - self:T( AttackUnit ) - - if EscortGroupAttack:IsAir() then - EscortGroupAttack:OptionROEOpenFire() - EscortGroupAttack:OptionROTVertical() --- routines.scheduleFunction( --- EscortGroupAttack.PushTask, --- { EscortGroupAttack, --- EscortGroupAttack:TaskCombo( --- { EscortGroupAttack:TaskAttackUnit( AttackUnit ), --- EscortGroupAttack:TaskOrbitCircle( 500, 350 ) --- } --- ) --- }, timer.getTime() + 10 --- ) - SCHDULER:New( EscortGroupAttack, - EscortGroupAttack.PushTask, - { EscortGroupAttack:TaskCombo( - { EscortGroupAttack:TaskAttackUnit( AttackUnit ), - EscortGroupAttack:TaskOrbitCircle( 500, 350 ) - } - ) - }, 10 - ) - else --- routines.scheduleFunction( --- EscortGroupAttack.PushTask, --- { EscortGroupAttack, --- EscortGroupAttack:TaskCombo( --- { EscortGroupAttack:TaskFireAtPoint( AttackUnit:GetPointVec2(), 50 ) --- } --- ) --- }, timer.getTime() + 10 --- ) - SCHEDULER:New( EscortGroupAttack, - EscortGroupAttack.PushTask, - { EscortGroupAttack:TaskCombo( - { EscortGroupAttack:TaskFireAtPoint( AttackUnit:GetPointVec2(), 50 ) - } - ) - }, 10 - ) - end - EscortGroupAttack:MessageToClient( "Assisting with the destroying the enemy unit!", 10, EscortClient ) - -end - ---- @param #MENUPARAM MenuParam -function ESCORT._ROE( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - local EscortROEFunction = MenuParam.ParamFunction - local EscortROEMessage = MenuParam.ParamMessage - - pcall( function() EscortROEFunction() end ) - EscortGroup:MessageToClient( EscortROEMessage, 10, EscortClient ) -end - ---- @param #MENUPARAM MenuParam -function ESCORT._ROT( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - local EscortROTFunction = MenuParam.ParamFunction - local EscortROTMessage = MenuParam.ParamMessage - - pcall( function() EscortROTFunction() end ) - EscortGroup:MessageToClient( EscortROTMessage, 10, EscortClient ) -end - ---- @param #MENUPARAM MenuParam -function ESCORT._ResumeMission( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - local WayPoint = MenuParam.ParamWayPoint - - self.FollowScheduler:Stop() - - local WayPoints = EscortGroup:GetTaskRoute() - self:T( WayPoint, WayPoints ) - - for WayPointIgnore = 1, WayPoint do - table.remove( WayPoints, 1 ) - end - - --routines.scheduleFunction( EscortGroup.SetTask, {EscortGroup, EscortGroup:TaskRoute( WayPoints ) }, timer.getTime() + 1 ) - SCHEDULER:New( EscortGroup, EscortGroup.SetTask, { EscortGroup:TaskRoute( WayPoints ) }, 1 ) - - EscortGroup:MessageToClient( "Resuming mission from waypoint " .. WayPoint .. ".", 10, EscortClient ) -end - ---- Registers the waypoints --- @param #ESCORT self --- @return #table -function ESCORT:RegisterRoute() - self:F() - - local EscortGroup = self.EscortGroup -- Group#GROUP - - local TaskPoints = EscortGroup:GetTaskRoute() - - self:T( TaskPoints ) - - return TaskPoints -end - ---- @param Escort#ESCORT self -function ESCORT:_FollowScheduler() - self:F( { self.FollowDistance } ) - - self:T( {self.EscortClient.UnitName, self.EscortGroup.GroupName } ) - if self.EscortGroup:IsAlive() and self.EscortClient:IsAlive() then - - local ClientUnit = self.EscortClient:GetClientGroupUnit() - local GroupUnit = self.EscortGroup:GetUnit( 1 ) - local FollowDistance = self.FollowDistance - - self:T( {ClientUnit.UnitName, GroupUnit.UnitName } ) - - if self.CT1 == 0 and self.GT1 == 0 then - self.CV1 = ClientUnit:GetPointVec3() - self:T( { "self.CV1", self.CV1 } ) - self.CT1 = timer.getTime() - self.GV1 = GroupUnit:GetPointVec3() - self.GT1 = timer.getTime() - else - local CT1 = self.CT1 - local CT2 = timer.getTime() - local CV1 = self.CV1 - local CV2 = ClientUnit:GetPointVec3() - self.CT1 = CT2 - self.CV1 = CV2 - - local CD = ( ( CV2.x - CV1.x )^2 + ( CV2.y - CV1.y )^2 + ( CV2.z - CV1.z )^2 ) ^ 0.5 - local CT = CT2 - CT1 - - local CS = ( 3600 / CT ) * ( CD / 1000 ) - - self:T2( { "Client:", CS, CD, CT, CV2, CV1, CT2, CT1 } ) - - local GT1 = self.GT1 - local GT2 = timer.getTime() - local GV1 = self.GV1 - local GV2 = GroupUnit:GetPointVec3() - self.GT1 = GT2 - self.GV1 = GV2 - - local GD = ( ( GV2.x - GV1.x )^2 + ( GV2.y - GV1.y )^2 + ( GV2.z - GV1.z )^2 ) ^ 0.5 - local GT = GT2 - GT1 - - local GS = ( 3600 / GT ) * ( GD / 1000 ) - - self:T2( { "Group:", GS, GD, GT, GV2, GV1, GT2, GT1 } ) - - -- Calculate the group direction vector - local GV = { x = GV2.x - CV2.x, y = GV2.y - CV2.y, z = GV2.z - CV2.z } - - -- Calculate GH2, GH2 with the same height as CV2. - local GH2 = { x = GV2.x, y = CV2.y, z = GV2.z } - - -- Calculate the angle of GV to the orthonormal plane - local alpha = math.atan2( GV.z, GV.x ) - - -- Now we calculate the intersecting vector between the circle around CV2 with radius FollowDistance and GH2. - -- From the GeoGebra model: CVI = (x(CV2) + FollowDistance cos(alpha), y(GH2) + FollowDistance sin(alpha), z(CV2)) - local CVI = { x = CV2.x + FollowDistance * math.cos(alpha), - y = GH2.y, - z = CV2.z + FollowDistance * math.sin(alpha), - } - - -- Calculate the direction vector DV of the escort group. We use CVI as the base and CV2 as the direction. - local DV = { x = CV2.x - CVI.x, y = CV2.y - CVI.y, z = CV2.z - CVI.z } - - -- We now calculate the unary direction vector DVu, so that we can multiply DVu with the speed, which is expressed in meters / s. - -- We need to calculate this vector to predict the point the escort group needs to fly to according its speed. - -- The distance of the destination point should be far enough not to have the aircraft starting to swipe left to right... - local DVu = { x = DV.x / FollowDistance, y = DV.y / FollowDistance, z = DV.z / FollowDistance } - - -- Now we can calculate the group destination vector GDV. - local GDV = { x = DVu.x * CS * 8 + CVI.x, y = CVI.y, z = DVu.z * CS * 8 + CVI.z } - - if self.SmokeDirectionVector == true then - trigger.action.smoke( GDV, trigger.smokeColor.Red ) - end - - self:T2( { "CV2:", CV2 } ) - self:T2( { "CVI:", CVI } ) - self:T2( { "GDV:", GDV } ) - - -- Measure distance between client and group - local CatchUpDistance = ( ( GDV.x - GV2.x )^2 + ( GDV.y - GV2.y )^2 + ( GDV.z - GV2.z )^2 ) ^ 0.5 - - -- The calculation of the Speed would simulate that the group would take 30 seconds to overcome - -- the requested Distance). - local Time = 10 - local CatchUpSpeed = ( CatchUpDistance - ( CS * 8.4 ) ) / Time - - local Speed = CS + CatchUpSpeed - if Speed < 0 then - Speed = 0 - end - - self:T( { "Client Speed, Escort Speed, Speed, FollowDistance, Time:", CS, GS, Speed, FollowDistance, Time } ) - - -- Now route the escort to the desired point with the desired speed. - self.EscortGroup:TaskRouteToVec3( GDV, Speed / 3.6 ) -- DCS models speed in Mps (Miles per second) - end - - return true - end - - return false -end - - ---- Report Targets Scheduler. --- @param #ESCORT self -function ESCORT:_ReportTargetsScheduler() - self:F( self.EscortGroup:GetName() ) - - if self.EscortGroup:IsAlive() and self.EscortClient:IsAlive() then - local EscortGroupName = self.EscortGroup:GetName() - local EscortTargets = self.EscortGroup:GetDetectedTargets() - - local ClientEscortTargets = self.EscortClient._EscortGroups[EscortGroupName].Targets - - local EscortTargetMessages = "" - for EscortTargetID, EscortTarget in pairs( EscortTargets ) do - local EscortObject = EscortTarget.object - self:T( EscortObject ) - if EscortObject and EscortObject:isExist() and EscortObject.id_ < 50000000 then - - local EscortTargetUnit = UNIT:Find( EscortObject ) - local EscortTargetUnitName = EscortTargetUnit:GetName() - - - - -- local EscortTargetIsDetected, - -- EscortTargetIsVisible, - -- EscortTargetLastTime, - -- EscortTargetKnowType, - -- EscortTargetKnowDistance, - -- EscortTargetLastPos, - -- EscortTargetLastVelocity - -- = self.EscortGroup:IsTargetDetected( EscortObject ) - -- - -- self:T( { EscortTargetIsDetected, - -- EscortTargetIsVisible, - -- EscortTargetLastTime, - -- EscortTargetKnowType, - -- EscortTargetKnowDistance, - -- EscortTargetLastPos, - -- EscortTargetLastVelocity } ) - - - local EscortTargetUnitPositionVec3 = EscortTargetUnit:GetPointVec3() - local EscortPositionVec3 = self.EscortGroup:GetPointVec3() - local Distance = ( ( EscortTargetUnitPositionVec3.x - EscortPositionVec3.x )^2 + - ( EscortTargetUnitPositionVec3.y - EscortPositionVec3.y )^2 + - ( EscortTargetUnitPositionVec3.z - EscortPositionVec3.z )^2 - ) ^ 0.5 / 1000 - - self:T( { self.EscortGroup:GetName(), EscortTargetUnit:GetName(), Distance, EscortTarget } ) - - if Distance <= 15 then - - if not ClientEscortTargets[EscortTargetUnitName] then - ClientEscortTargets[EscortTargetUnitName] = {} - end - ClientEscortTargets[EscortTargetUnitName].AttackUnit = EscortTargetUnit - ClientEscortTargets[EscortTargetUnitName].visible = EscortTarget.visible - ClientEscortTargets[EscortTargetUnitName].type = EscortTarget.type - ClientEscortTargets[EscortTargetUnitName].distance = EscortTarget.distance - else - if ClientEscortTargets[EscortTargetUnitName] then - ClientEscortTargets[EscortTargetUnitName] = nil - end - end - end - end - - self:T( { "Sorting Targets Table:", ClientEscortTargets } ) - table.sort( ClientEscortTargets, function( a, b ) return a.Distance < b.Distance end ) - self:T( { "Sorted Targets Table:", ClientEscortTargets } ) - - -- Remove the sub menus of the Attack menu of the Escort for the EscortGroup. - self.EscortMenuAttackNearbyTargets:RemoveSubMenus() - - if self.EscortMenuTargetAssistance then - self.EscortMenuTargetAssistance:RemoveSubMenus() - end - - --for MenuIndex = 1, #self.EscortMenuAttackTargets do - -- self:T( { "Remove Menu:", self.EscortMenuAttackTargets[MenuIndex] } ) - -- self.EscortMenuAttackTargets[MenuIndex] = self.EscortMenuAttackTargets[MenuIndex]:Remove() - --end - - - if ClientEscortTargets then - for ClientEscortTargetUnitName, ClientEscortTargetData in pairs( ClientEscortTargets ) do - - for ClientEscortGroupName, EscortGroupData in pairs( self.EscortClient._EscortGroups ) do - - if ClientEscortTargetData and ClientEscortTargetData.AttackUnit:IsAlive() then - - local EscortTargetMessage = "" - local EscortTargetCategoryName = ClientEscortTargetData.AttackUnit:GetCategoryName() - local EscortTargetCategoryType = ClientEscortTargetData.AttackUnit:GetTypeName() - if ClientEscortTargetData.type then - EscortTargetMessage = EscortTargetMessage .. EscortTargetCategoryName .. " (" .. EscortTargetCategoryType .. ") at " - else - EscortTargetMessage = EscortTargetMessage .. "Unknown target at " - end - - local EscortTargetUnitPositionVec3 = ClientEscortTargetData.AttackUnit:GetPointVec3() - local EscortPositionVec3 = self.EscortGroup:GetPointVec3() - local Distance = ( ( EscortTargetUnitPositionVec3.x - EscortPositionVec3.x )^2 + - ( EscortTargetUnitPositionVec3.y - EscortPositionVec3.y )^2 + - ( EscortTargetUnitPositionVec3.z - EscortPositionVec3.z )^2 - ) ^ 0.5 / 1000 - - self:T( { self.EscortGroup:GetName(), ClientEscortTargetData.AttackUnit:GetName(), Distance, ClientEscortTargetData.AttackUnit } ) - if ClientEscortTargetData.visible == false then - EscortTargetMessage = EscortTargetMessage .. string.format( "%.2f", Distance ) .. " estimated km" - else - EscortTargetMessage = EscortTargetMessage .. string.format( "%.2f", Distance ) .. " km" - end - - if ClientEscortTargetData.visible then - EscortTargetMessage = EscortTargetMessage .. ", visual" - end - - if ClientEscortGroupName == EscortGroupName then - - MENU_CLIENT_COMMAND:New( self.EscortClient, - EscortTargetMessage, - self.EscortMenuAttackNearbyTargets, - ESCORT._AttackTarget, - { ParamSelf = self, - ParamUnit = ClientEscortTargetData.AttackUnit - } - ) - EscortTargetMessages = EscortTargetMessages .. "\n - " .. EscortTargetMessage - else - if self.EscortMenuTargetAssistance then - local MenuTargetAssistance = MENU_CLIENT:New( self.EscortClient, EscortGroupData.EscortName, self.EscortMenuTargetAssistance ) - MENU_CLIENT_COMMAND:New( self.EscortClient, - EscortTargetMessage, - MenuTargetAssistance, - ESCORT._AssistTarget, - { ParamSelf = self, - ParamEscortGroup = EscortGroupData.EscortGroup, - ParamUnit = ClientEscortTargetData.AttackUnit - } - ) - end - end - else - ClientEscortTargetData = nil - end - end - end - - if EscortTargetMessages ~= "" and self.ReportTargets == true then - self.EscortGroup:MessageToClient( "Detected targets within 15 km range:" .. EscortTargetMessages:gsub("\n$",""), 20, self.EscortClient ) - else - self.EscortGroup:MessageToClient( "No targets detected!", 20, self.EscortClient ) - end - end - - if self.EscortMenuResumeMission then - self.EscortMenuResumeMission:RemoveSubMenus() - - -- if self.EscortMenuResumeWayPoints then - -- for MenuIndex = 1, #self.EscortMenuResumeWayPoints do - -- self:T( { "Remove Menu:", self.EscortMenuResumeWayPoints[MenuIndex] } ) - -- self.EscortMenuResumeWayPoints[MenuIndex] = self.EscortMenuResumeWayPoints[MenuIndex]:Remove() - -- end - -- end - - local TaskPoints = self:RegisterRoute() - for WayPointID, WayPoint in pairs( TaskPoints ) do - local EscortPositionVec3 = self.EscortGroup:GetPointVec3() - local Distance = ( ( WayPoint.x - EscortPositionVec3.x )^2 + - ( WayPoint.y - EscortPositionVec3.z )^2 - ) ^ 0.5 / 1000 - MENU_CLIENT_COMMAND:New( self.EscortClient, "Waypoint " .. WayPointID .. " at " .. string.format( "%.2f", Distance ).. "km", self.EscortMenuResumeMission, ESCORT._ResumeMission, { ParamSelf = self, ParamWayPoint = WayPointID } ) - end - end - - return true - end - - return false -end ---- This module contains the MISSILETRAINER class. --- --- === --- --- 1) @{MissileTrainer#MISSILETRAINER} class, extends @{Base#BASE} --- =============================================================== --- The @{#MISSILETRAINER} class uses the DCS world 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: --- --- * **Messages**: Menu to configure all messages. --- * **Messages On**: Show all messages. --- * **Messages Off**: Disable all messages. --- * **Tracking**: Menu to configure missile tracking messages. --- * **To All**: Shows missile tracking messages to all players. --- * **To Target**: Shows missile tracking messages only to the player where the missile is targetted at. --- * **Tracking On**: Show missile tracking messages. --- * **Tracking Off**: Disable missile tracking messages. --- * **Frequency Increase**: Increases the missile tracking message frequency with one second. --- * **Frequency Decrease**: Decreases the missile tracking message frequency with one second. --- * **Alerts**: Menu to configure alert messages. --- * **To All**: Shows alert messages to all players. --- * **To Target**: Shows alert messages only to the player where the missile is (was) targetted at. --- * **Hits On**: Show missile hit alert messages. --- * **Hits Off**: Disable missile hit alert messages. --- * **Launches On**: Show missile launch messages. --- * **Launches Off**: Disable missile launch messages. --- * **Details**: Menu to configure message details. --- * **Range On**: Shows range information when a missile is fired to a target. --- * **Range Off**: Disable range information when a missile is fired to a target. --- * **Bearing On**: Shows bearing information when a missile is fired to a target. --- * **Bearing Off**: Disable bearing information when a missile is fired to a target. --- * **Distance**: Menu to configure the distance when a missile needs to be destroyed when near to a player, during tracking. This will improve/influence hit calculation accuracy, but has the risk of damaging the aircraft when the missile reaches the aircraft before the distance is measured. --- * **50 meter**: Destroys the missile when the distance to the aircraft is below or equal to 50 meter. --- * **100 meter**: Destroys the missile when the distance to the aircraft is below or equal to 100 meter. --- * **150 meter**: Destroys the missile when the distance to the aircraft is below or equal to 150 meter. --- * **200 meter**: Destroys the missile when the distance to the aircraft is below or equal to 200 meter. --- --- --- 1.1) MISSILETRAINER construction methods: --- ----------------------------------------- --- 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: --- ------------------------------------------- --- A MISSILETRAINER object will behave differently based on the usage of initialization methods: --- --- * @{#MISSILETRAINER.InitMessagesOnOff}: Sets by default the display of any message to be ON or OFF. --- * @{#MISSILETRAINER.InitTrackingToAll}: Sets by default the missile tracking report for all players or only for those missiles targetted to you. --- * @{#MISSILETRAINER.InitTrackingOnOff}: Sets by default the display of missile tracking report to be ON or OFF. --- * @{#MISSILETRAINER.InitTrackingFrequency}: Increases, decreases the missile tracking message display frequency with the provided time interval in seconds. --- * @{#MISSILETRAINER.InitAlertsToAll}: Sets by default the display of alerts to be shown to all players or only to you. --- * @{#MISSILETRAINER.InitAlertsHitsOnOff}: Sets by default the display of hit alerts ON or OFF. --- * @{#MISSILETRAINER.InitAlertsLaunchesOnOff}: Sets by default the display of launch alerts ON or OFF. --- * @{#MISSILETRAINER.InitRangeOnOff}: Sets by default the display of range information of missiles ON of OFF. --- * @{#MISSILETRAINER.InitBearingOnOff}: Sets by default the display of bearing information of missiles ON of OFF. --- * @{#MISSILETRAINER.InitMenusOnOff}: Allows to configure the options through the radio menu. --- --- === --- --- 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 Set#SET_CLIENT DBClients --- @extends Base#BASE -MISSILETRAINER = { - ClassName = "MISSILETRAINER", - TrackingMissiles = {}, -} - -function MISSILETRAINER._Alive( Client, self ) - - if self.Briefing then - Client:Message( self.Briefing, 15, "Trainer" ) - end - - if self.MenusOnOff == true then - Client:Message( "Use the 'Radio Menu' -> 'Other (F10)' -> 'Missile Trainer' menu options to change the Missile Trainer settings (for all players).", 15, "Trainer" ) - - Client.MainMenu = MENU_CLIENT:New( Client, "Missile Trainer", nil ) -- Menu#MENU_CLIENT - - Client.MenuMessages = MENU_CLIENT:New( Client, "Messages", Client.MainMenu ) - Client.MenuOn = MENU_CLIENT_COMMAND:New( Client, "Messages On", Client.MenuMessages, self._MenuMessages, { MenuSelf = self, MessagesOnOff = true } ) - Client.MenuOff = MENU_CLIENT_COMMAND:New( Client, "Messages Off", Client.MenuMessages, self._MenuMessages, { MenuSelf = self, MessagesOnOff = false } ) - - Client.MenuTracking = MENU_CLIENT:New( Client, "Tracking", Client.MainMenu ) - Client.MenuTrackingToAll = MENU_CLIENT_COMMAND:New( Client, "To All", Client.MenuTracking, self._MenuMessages, { MenuSelf = self, TrackingToAll = true } ) - Client.MenuTrackingToTarget = MENU_CLIENT_COMMAND:New( Client, "To Target", Client.MenuTracking, self._MenuMessages, { MenuSelf = self, TrackingToAll = false } ) - Client.MenuTrackOn = MENU_CLIENT_COMMAND:New( Client, "Tracking On", Client.MenuTracking, self._MenuMessages, { MenuSelf = self, TrackingOnOff = true } ) - Client.MenuTrackOff = MENU_CLIENT_COMMAND:New( Client, "Tracking Off", Client.MenuTracking, self._MenuMessages, { MenuSelf = self, TrackingOnOff = false } ) - Client.MenuTrackIncrease = MENU_CLIENT_COMMAND:New( Client, "Frequency Increase", Client.MenuTracking, self._MenuMessages, { MenuSelf = self, TrackingFrequency = -1 } ) - Client.MenuTrackDecrease = MENU_CLIENT_COMMAND:New( Client, "Frequency Decrease", Client.MenuTracking, self._MenuMessages, { MenuSelf = self, TrackingFrequency = 1 } ) - - Client.MenuAlerts = MENU_CLIENT:New( Client, "Alerts", Client.MainMenu ) - Client.MenuAlertsToAll = MENU_CLIENT_COMMAND:New( Client, "To All", Client.MenuAlerts, self._MenuMessages, { MenuSelf = self, AlertsToAll = true } ) - Client.MenuAlertsToTarget = MENU_CLIENT_COMMAND:New( Client, "To Target", Client.MenuAlerts, self._MenuMessages, { MenuSelf = self, AlertsToAll = false } ) - Client.MenuHitsOn = MENU_CLIENT_COMMAND:New( Client, "Hits On", Client.MenuAlerts, self._MenuMessages, { MenuSelf = self, AlertsHitsOnOff = true } ) - Client.MenuHitsOff = MENU_CLIENT_COMMAND:New( Client, "Hits Off", Client.MenuAlerts, self._MenuMessages, { MenuSelf = self, AlertsHitsOnOff = false } ) - Client.MenuLaunchesOn = MENU_CLIENT_COMMAND:New( Client, "Launches On", Client.MenuAlerts, self._MenuMessages, { MenuSelf = self, AlertsLaunchesOnOff = true } ) - Client.MenuLaunchesOff = MENU_CLIENT_COMMAND:New( Client, "Launches Off", Client.MenuAlerts, self._MenuMessages, { MenuSelf = self, AlertsLaunchesOnOff = false } ) - - Client.MenuDetails = MENU_CLIENT:New( Client, "Details", Client.MainMenu ) - Client.MenuDetailsDistanceOn = MENU_CLIENT_COMMAND:New( Client, "Range On", Client.MenuDetails, self._MenuMessages, { MenuSelf = self, DetailsRangeOnOff = true } ) - Client.MenuDetailsDistanceOff = MENU_CLIENT_COMMAND:New( Client, "Range Off", Client.MenuDetails, self._MenuMessages, { MenuSelf = self, DetailsRangeOnOff = false } ) - Client.MenuDetailsBearingOn = MENU_CLIENT_COMMAND:New( Client, "Bearing On", Client.MenuDetails, self._MenuMessages, { MenuSelf = self, DetailsBearingOnOff = true } ) - Client.MenuDetailsBearingOff = MENU_CLIENT_COMMAND:New( Client, "Bearing Off", Client.MenuDetails, self._MenuMessages, { MenuSelf = self, DetailsBearingOnOff = false } ) - - Client.MenuDistance = MENU_CLIENT:New( Client, "Set distance to plane", Client.MainMenu ) - Client.MenuDistance50 = MENU_CLIENT_COMMAND:New( Client, "50 meter", Client.MenuDistance, self._MenuMessages, { MenuSelf = self, Distance = 50 / 1000 } ) - Client.MenuDistance100 = MENU_CLIENT_COMMAND:New( Client, "100 meter", Client.MenuDistance, self._MenuMessages, { MenuSelf = self, Distance = 100 / 1000 } ) - Client.MenuDistance150 = MENU_CLIENT_COMMAND:New( Client, "150 meter", Client.MenuDistance, self._MenuMessages, { MenuSelf = self, Distance = 150 / 1000 } ) - Client.MenuDistance200 = MENU_CLIENT_COMMAND:New( Client, "200 meter", Client.MenuDistance, self._MenuMessages, { MenuSelf = self, Distance = 200 / 1000 } ) - else - if Client.MainMenu then - Client.MainMenu:Remove() - end - end - - local ClientID = Client:GetID() - self:T( ClientID ) - if not self.TrackingMissiles[ClientID] then - self.TrackingMissiles[ClientID] = {} - end - self.TrackingMissiles[ClientID].Client = Client - if not self.TrackingMissiles[ClientID].MissileData then - self.TrackingMissiles[ClientID].MissileData = {} - end -end - ---- Creates the main object which is handling missile tracking. --- When a missile is fired a SCHEDULER is set off that follows the missile. When near a certain a client player, the missile will be destroyed. --- @param #MISSILETRAINER self --- @param #number Distance The distance in meters when a tracked missile needs to be destroyed when close to a player. --- @param #string Briefing (Optional) Will show a text to the players when starting their mission. Can be used for briefing purposes. --- @return #MISSILETRAINER -function MISSILETRAINER:New( Distance, Briefing ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( Distance ) - - if Briefing then - self.Briefing = Briefing - end - - self.Schedulers = {} - self.SchedulerID = 0 - - self.MessageInterval = 2 - self.MessageLastTime = timer.getTime() - - self.Distance = Distance / 1000 - - _EVENTDISPATCHER:OnShot( self._EventShot, self ) - - self.DBClients = SET_CLIENT:New():FilterStart() - - --- for ClientID, Client in pairs( self.DBClients.Database ) do --- self:E( "ForEach:" .. Client.UnitName ) --- Client:Alive( self._Alive, self ) --- end --- - self.DBClients:ForEachClient( - function( Client ) - self:E( "ForEach:" .. Client.UnitName ) - Client:Alive( self._Alive, self ) - end - ) - - - --- self.DB:ForEachClient( --- --- @param Client#CLIENT Client --- function( Client ) --- --- ... actions ... --- --- end --- ) - - self.MessagesOnOff = true - - self.TrackingToAll = false - self.TrackingOnOff = true - self.TrackingFrequency = 3 - - self.AlertsToAll = true - self.AlertsHitsOnOff = true - self.AlertsLaunchesOnOff = true - - self.DetailsRangeOnOff = true - self.DetailsBearingOnOff = true - - self.MenusOnOff = true - - self.TrackingMissiles = {} - - self.TrackingScheduler = SCHEDULER:New( self, self._TrackMissiles, {}, 0.5, 0.05, 0 ) - - return self -end - --- Initialization methods. - - - ---- Sets by default the display of any message to be ON or OFF. --- @param #MISSILETRAINER self --- @param #boolean MessagesOnOff true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitMessagesOnOff( MessagesOnOff ) - self:F( MessagesOnOff ) - - self.MessagesOnOff = MessagesOnOff - if self.MessagesOnOff == true then - MESSAGE:New( "Messages ON", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Messages OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Sets by default the missile tracking report for all players or only for those missiles targetted to you. --- @param #MISSILETRAINER self --- @param #boolean TrackingToAll true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitTrackingToAll( TrackingToAll ) - self:F( TrackingToAll ) - - self.TrackingToAll = TrackingToAll - if self.TrackingToAll == true then - MESSAGE:New( "Missile tracking to all players ON", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Missile tracking to all players OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Sets by default the display of missile tracking report to be ON or OFF. --- @param #MISSILETRAINER self --- @param #boolean TrackingOnOff true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitTrackingOnOff( TrackingOnOff ) - self:F( TrackingOnOff ) - - self.TrackingOnOff = TrackingOnOff - if self.TrackingOnOff == true then - MESSAGE:New( "Missile tracking ON", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Missile tracking OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Increases, decreases the missile tracking message display frequency with the provided time interval in seconds. --- The default frequency is a 3 second interval, so the Tracking Frequency parameter specifies the increase or decrease from the default 3 seconds or the last frequency update. --- @param #MISSILETRAINER self --- @param #number TrackingFrequency Provide a negative or positive value in seconds to incraese or decrease the display frequency. --- @return #MISSILETRAINER self -function MISSILETRAINER:InitTrackingFrequency( TrackingFrequency ) - self:F( TrackingFrequency ) - - self.TrackingFrequency = self.TrackingFrequency + TrackingFrequency - if self.TrackingFrequency < 0.5 then - self.TrackingFrequency = 0.5 - end - if self.TrackingFrequency then - MESSAGE:New( "Missile tracking frequency is " .. self.TrackingFrequency .. " seconds.", 15, "Menu" ):ToAll() - end - - return self -end - ---- Sets by default the display of alerts to be shown to all players or only to you. --- @param #MISSILETRAINER self --- @param #boolean AlertsToAll true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitAlertsToAll( AlertsToAll ) - self:F( AlertsToAll ) - - self.AlertsToAll = AlertsToAll - if self.AlertsToAll == true then - MESSAGE:New( "Alerts to all players ON", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Alerts to all players OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Sets by default the display of hit alerts ON or OFF. --- @param #MISSILETRAINER self --- @param #boolean AlertsHitsOnOff true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitAlertsHitsOnOff( AlertsHitsOnOff ) - self:F( AlertsHitsOnOff ) - - self.AlertsHitsOnOff = AlertsHitsOnOff - if self.AlertsHitsOnOff == true then - MESSAGE:New( "Alerts Hits ON", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Alerts Hits OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Sets by default the display of launch alerts ON or OFF. --- @param #MISSILETRAINER self --- @param #boolean AlertsLaunchesOnOff true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitAlertsLaunchesOnOff( AlertsLaunchesOnOff ) - self:F( AlertsLaunchesOnOff ) - - self.AlertsLaunchesOnOff = AlertsLaunchesOnOff - if self.AlertsLaunchesOnOff == true then - MESSAGE:New( "Alerts Launches ON", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Alerts Launches OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Sets by default the display of range information of missiles ON of OFF. --- @param #MISSILETRAINER self --- @param #boolean DetailsRangeOnOff true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitRangeOnOff( DetailsRangeOnOff ) - self:F( DetailsRangeOnOff ) - - self.DetailsRangeOnOff = DetailsRangeOnOff - if self.DetailsRangeOnOff == true then - MESSAGE:New( "Range display ON", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Range display OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Sets by default the display of bearing information of missiles ON of OFF. --- @param #MISSILETRAINER self --- @param #boolean DetailsBearingOnOff true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitBearingOnOff( DetailsBearingOnOff ) - self:F( DetailsBearingOnOff ) - - self.DetailsBearingOnOff = DetailsBearingOnOff - if self.DetailsBearingOnOff == true then - MESSAGE:New( "Bearing display OFF", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Bearing display OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Enables / Disables the menus. --- @param #MISSILETRAINER self --- @param #boolean MenusOnOff true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitMenusOnOff( MenusOnOff ) - self:F( MenusOnOff ) - - self.MenusOnOff = MenusOnOff - if self.MenusOnOff == true then - MESSAGE:New( "Menus are ENABLED (only when a player rejoins a slot)", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Menus are DISABLED", 15, "Menu" ):ToAll() - end - - return self -end - - --- Menu functions - -function MISSILETRAINER._MenuMessages( MenuParameters ) - - local self = MenuParameters.MenuSelf - - if MenuParameters.MessagesOnOff ~= nil then - self:InitMessagesOnOff( MenuParameters.MessagesOnOff ) - end - - if MenuParameters.TrackingToAll ~= nil then - self:InitTrackingToAll( MenuParameters.TrackingToAll ) - end - - if MenuParameters.TrackingOnOff ~= nil then - self:InitTrackingOnOff( MenuParameters.TrackingOnOff ) - end - - if MenuParameters.TrackingFrequency ~= nil then - self:InitTrackingFrequency( MenuParameters.TrackingFrequency ) - end - - if MenuParameters.AlertsToAll ~= nil then - self:InitAlertsToAll( MenuParameters.AlertsToAll ) - end - - if MenuParameters.AlertsHitsOnOff ~= nil then - self:InitAlertsHitsOnOff( MenuParameters.AlertsHitsOnOff ) - end - - if MenuParameters.AlertsLaunchesOnOff ~= nil then - self:InitAlertsLaunchesOnOff( MenuParameters.AlertsLaunchesOnOff ) - end - - if MenuParameters.DetailsRangeOnOff ~= nil then - self:InitRangeOnOff( MenuParameters.DetailsRangeOnOff ) - end - - if MenuParameters.DetailsBearingOnOff ~= nil then - self:InitBearingOnOff( MenuParameters.DetailsBearingOnOff ) - end - - if MenuParameters.Distance ~= nil then - self.Distance = MenuParameters.Distance - MESSAGE:New( "Hit detection distance set to " .. self.Distance .. " meters", 15, "Menu" ):ToAll() - end - -end - ---- Detects if an SA site was shot with an anti radiation missile. In this case, take evasive actions based on the skill level set within the ME. --- @param #MISSILETRAINER self --- @param Event#EVENTDATA Event -function MISSILETRAINER:_EventShot( Event ) - self:F( { Event } ) - - local TrainerSourceDCSUnit = Event.IniDCSUnit - local TrainerSourceDCSUnitName = Event.IniDCSUnitName - local TrainerWeapon = Event.Weapon -- Identify the weapon fired - local TrainerWeaponName = Event.WeaponName -- return weapon type - - self:T( "Missile Launched = " .. TrainerWeaponName ) - - local TrainerTargetDCSUnit = TrainerWeapon:getTarget() -- Identify target - local TrainerTargetDCSUnitName = Unit.getName( TrainerTargetDCSUnit ) - local TrainerTargetSkill = _DATABASE.Templates.Units[TrainerTargetDCSUnitName].Template.skill - - self:T(TrainerTargetDCSUnitName ) - - local Client = self.DBClients:FindClient( TrainerTargetDCSUnitName ) - if Client then - - local TrainerSourceUnit = UNIT:Find( TrainerSourceDCSUnit ) - local TrainerTargetUnit = UNIT:Find( TrainerTargetDCSUnit ) - - if self.MessagesOnOff == true and self.AlertsLaunchesOnOff == true then - - local Message = MESSAGE:New( - string.format( "%s launched a %s", - TrainerSourceUnit:GetTypeName(), - TrainerWeaponName - ) .. self:_AddRange( Client, TrainerWeapon ) .. self:_AddBearing( Client, TrainerWeapon ), 5, "Launch Alert" ) - - if self.AlertsToAll then - Message:ToAll() - else - Message:ToClient( Client ) - end - end - - local ClientID = Client:GetID() - self:T( ClientID ) - local MissileData = {} - MissileData.TrainerSourceUnit = TrainerSourceUnit - MissileData.TrainerWeapon = TrainerWeapon - MissileData.TrainerTargetUnit = TrainerTargetUnit - MissileData.TrainerWeaponTypeName = TrainerWeapon:getTypeName() - MissileData.TrainerWeaponLaunched = true - table.insert( self.TrackingMissiles[ClientID].MissileData, MissileData ) - --self:T( self.TrackingMissiles ) - end -end - -function MISSILETRAINER:_AddRange( Client, TrainerWeapon ) - - local RangeText = "" - - if self.DetailsRangeOnOff then - - local PositionMissile = TrainerWeapon:getPoint() - local PositionTarget = Client:GetPointVec3() - - local Range = ( ( PositionMissile.x - PositionTarget.x )^2 + - ( PositionMissile.y - PositionTarget.y )^2 + - ( PositionMissile.z - PositionTarget.z )^2 - ) ^ 0.5 / 1000 - - RangeText = string.format( ", at %4.2fkm", Range ) - end - - return RangeText -end - -function MISSILETRAINER:_AddBearing( Client, TrainerWeapon ) - - local BearingText = "" - - if self.DetailsBearingOnOff then - - local PositionMissile = TrainerWeapon:getPoint() - local PositionTarget = Client:GetPointVec3() - - self:T2( { PositionTarget, PositionMissile }) - - local DirectionVector = { x = PositionMissile.x - PositionTarget.x, y = PositionMissile.y - PositionTarget.y, z = PositionMissile.z - PositionTarget.z } - local DirectionRadians = math.atan2( DirectionVector.z, DirectionVector.x ) - --DirectionRadians = DirectionRadians + routines.getNorthCorrection( PositionTarget ) - if DirectionRadians < 0 then - DirectionRadians = DirectionRadians + 2 * math.pi - end - local DirectionDegrees = DirectionRadians * 180 / math.pi - - BearingText = string.format( ", %d degrees", DirectionDegrees ) - end - - return BearingText -end - - -function MISSILETRAINER:_TrackMissiles() - self:F2() - - - local ShowMessages = false - if self.MessagesOnOff and self.MessageLastTime + self.TrackingFrequency <= timer.getTime() then - self.MessageLastTime = timer.getTime() - ShowMessages = true - end - - -- ALERTS PART - - -- Loop for all Player Clients to check the alerts and deletion of missiles. - for ClientDataID, ClientData in pairs( self.TrackingMissiles ) do - - local Client = ClientData.Client - self:T2( { Client:GetName() } ) - - for MissileDataID, MissileData in pairs( ClientData.MissileData ) do - self:T3( MissileDataID ) - - local TrainerSourceUnit = MissileData.TrainerSourceUnit - local TrainerWeapon = MissileData.TrainerWeapon - local TrainerTargetUnit = MissileData.TrainerTargetUnit - local TrainerWeaponTypeName = MissileData.TrainerWeaponTypeName - local TrainerWeaponLaunched = MissileData.TrainerWeaponLaunched - - if Client and Client:IsAlive() and TrainerSourceUnit and TrainerSourceUnit:IsAlive() and TrainerWeapon and TrainerWeapon:isExist() and TrainerTargetUnit and TrainerTargetUnit:IsAlive() then - local PositionMissile = TrainerWeapon:getPosition().p - local PositionTarget = Client:GetPointVec3() - - local Distance = ( ( PositionMissile.x - PositionTarget.x )^2 + - ( PositionMissile.y - PositionTarget.y )^2 + - ( PositionMissile.z - PositionTarget.z )^2 - ) ^ 0.5 / 1000 - - if Distance <= self.Distance then - -- Hit alert - TrainerWeapon:destroy() - if self.MessagesOnOff == true and self.AlertsHitsOnOff == true then - - self:T( "killed" ) - - local Message = MESSAGE:New( - string.format( "%s launched by %s killed %s", - TrainerWeapon:getTypeName(), - TrainerSourceUnit:GetTypeName(), - TrainerTargetUnit:GetPlayerName() - ), 15, "Hit Alert" ) - - if self.AlertsToAll == true then - Message:ToAll() - else - Message:ToClient( Client ) - end - - MissileData = nil - table.remove( ClientData.MissileData, MissileDataID ) - self:T(ClientData.MissileData) - end - end - else - if not ( TrainerWeapon and TrainerWeapon:isExist() ) then - if self.MessagesOnOff == true and self.AlertsLaunchesOnOff == true then - -- Weapon does not exist anymore. Delete from Table - local Message = MESSAGE:New( - string.format( "%s launched by %s self destructed!", - TrainerWeaponTypeName, - TrainerSourceUnit:GetTypeName() - ), 5, "Tracking" ) - - if self.AlertsToAll == true then - Message:ToAll() - else - Message:ToClient( Client ) - end - end - MissileData = nil - table.remove( ClientData.MissileData, MissileDataID ) - self:T( ClientData.MissileData ) - end - end - end - end - - if ShowMessages == true and self.MessagesOnOff == true and self.TrackingOnOff == true then -- Only do this when tracking information needs to be displayed. - - -- TRACKING PART - - -- For the current client, the missile range and bearing details are displayed To the Player Client. - -- For the other clients, the missile range and bearing details are displayed To the other Player Clients. - -- To achieve this, a cross loop is done for each Player Client <-> Other Player Client missile information. - - -- Main Player Client loop - for ClientDataID, ClientData in pairs( self.TrackingMissiles ) do - - local Client = ClientData.Client - self:T2( { Client:GetName() } ) - - - ClientData.MessageToClient = "" - ClientData.MessageToAll = "" - - -- Other Players Client loop - for TrackingDataID, TrackingData in pairs( self.TrackingMissiles ) do - - for MissileDataID, MissileData in pairs( TrackingData.MissileData ) do - self:T3( MissileDataID ) - - local TrainerSourceUnit = MissileData.TrainerSourceUnit - local TrainerWeapon = MissileData.TrainerWeapon - local TrainerTargetUnit = MissileData.TrainerTargetUnit - local TrainerWeaponTypeName = MissileData.TrainerWeaponTypeName - local TrainerWeaponLaunched = MissileData.TrainerWeaponLaunched - - if Client and Client:IsAlive() and TrainerSourceUnit and TrainerSourceUnit:IsAlive() and TrainerWeapon and TrainerWeapon:isExist() and TrainerTargetUnit and TrainerTargetUnit:IsAlive() then - - if ShowMessages == true then - local TrackingTo - TrackingTo = string.format( " -> %s", - TrainerWeaponTypeName - ) - - if ClientDataID == TrackingDataID then - if ClientData.MessageToClient == "" then - ClientData.MessageToClient = "Missiles to You:\n" - end - ClientData.MessageToClient = ClientData.MessageToClient .. TrackingTo .. self:_AddRange( ClientData.Client, TrainerWeapon ) .. self:_AddBearing( ClientData.Client, TrainerWeapon ) .. "\n" - else - if self.TrackingToAll == true then - if ClientData.MessageToAll == "" then - ClientData.MessageToAll = "Missiles to other Players:\n" - end - ClientData.MessageToAll = ClientData.MessageToAll .. TrackingTo .. self:_AddRange( ClientData.Client, TrainerWeapon ) .. self:_AddBearing( ClientData.Client, TrainerWeapon ) .. " ( " .. TrainerTargetUnit:GetPlayerName() .. " )\n" - end - end - end - end - end - end - - -- Once the Player Client and the Other Player Client tracking messages are prepared, show them. - if ClientData.MessageToClient ~= "" or ClientData.MessageToAll ~= "" then - local Message = MESSAGE:New( ClientData.MessageToClient .. ClientData.MessageToAll, 1, "Tracking" ):ToClient( Client ) - end - end - end - - return true -end ---- This module contains the PATROLZONE class. --- --- === --- --- 1) @{Patrol#PATROLZONE} class, extends @{Base#BASE} --- =================================================== --- The @{Patrol#PATROLZONE} class implements the core functions to patrol a @{Zone}. --- --- 1.1) PATROLZONE constructor: --- ---------------------------- --- @{PatrolZone#PATROLZONE.New}(): Creates a new PATROLZONE object. --- --- 1.2) Modify the PATROLZONE parameters: --- -------------------------------------- --- The following methods are available to modify the parameters of a PATROLZONE object: --- --- * @{PatrolZone#PATROLZONE.SetGroup}(): Set the AI Patrol Group. --- * @{PatrolZone#PATROLZONE.SetSpeed}(): Set the patrol speed of the AI, for the next patrol. --- * @{PatrolZone#PATROLZONE.SetAltitude}(): Set altitude of the AI, for the next patrol. --- --- 1.3) Manage the out of fuel in the PATROLZONE: --- ---------------------------------------------- --- When the PatrolGroup is out of fuel, it is required that a new PatrolGroup is started, before the old PatrolGroup 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 PatrolGroup will continue for a given time its patrol task in orbit, while a new PatrolGroup is targetted to the PATROLZONE. --- Once the time is finished, the old PatrolGroup will return to the base. --- Use the method @{PatrolZone#PATROLZONE.ManageFuel}() to have this proces in place. --- --- === --- --- @module PatrolZone --- @author FlightControl - - ---- PATROLZONE class --- @type PATROLZONE --- @field Group#GROUP PatrolGroup The @{Group} patrolling. --- @field Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. --- @field DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @field DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @field DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Group} in km/h. --- @field DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Group} in km/h. --- @extends Base#BASE -PATROLZONE = { - ClassName = "PATROLZONE", -} - ---- Creates a new PATROLZONE object, taking a @{Group} object as a parameter. The GROUP needs to be alive. --- @param #PATROLZONE self --- @param Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. --- @param DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @param DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @param DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Group} in km/h. --- @param DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Group} in km/h. --- @return #PATROLZONE self --- @usage --- -- Define a new PATROLZONE Object. This PatrolArea will patrol a group within PatrolZone between 3000 and 6000 meters, with a variying speed between 600 and 900 km/h. --- PatrolZone = ZONE:New( 'PatrolZone' ) --- PatrolGroup = GROUP:FindByName( "Patrol Group" ) --- PatrolArea = PATROLZONE:New( PatrolGroup, PatrolZone, 3000, 6000, 600, 900 ) -function PATROLZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed ) - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) - - self.PatrolZone = PatrolZone - self.PatrolFloorAltitude = PatrolFloorAltitude - self.PatrolCeilingAltitude = PatrolCeilingAltitude - self.PatrolMinSpeed = PatrolMinSpeed - self.PatrolMaxSpeed = PatrolMaxSpeed - - return self -end - ---- Set the @{Group} to act as the Patroller. --- @param #PATROLZONE self --- @param Group#GROUP PatrolGroup The @{Group} patrolling. --- @return #PATROLZONE self -function PATROLZONE:SetGroup( PatrolGroup ) - - self.PatrolGroup = PatrolGroup - self.PatrolGroupTemplateName = PatrolGroup:GetName() - self:NewPatrolRoute() - - if not self.PatrolOutOfFuelMonitor then - self.PatrolOutOfFuelMonitor = SCHEDULER:New( nil, _MonitorOutOfFuelScheduled, { self }, 1, 120, 0 ) - self.SpawnPatrolGroup = SPAWN:New( self.PatrolGroupTemplateName ) - end - - return self -end - ---- Sets (modifies) the minimum and maximum speed of the patrol. --- @param #PATROLZONE self --- @param DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Group} in km/h. --- @param DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Group} in km/h. --- @return #PATROLZONE self -function PATROLZONE:SetSpeed( PatrolMinSpeed, PatrolMaxSpeed ) - self:F2( { PatrolMinSpeed, PatrolMaxSpeed } ) - - self.PatrolMinSpeed = PatrolMinSpeed - self.PatrolMaxSpeed = PatrolMaxSpeed -end - ---- Sets the floor and ceiling altitude of the patrol. --- @param #PATROLZONE self --- @param DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @param DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @return #PATROLZONE self -function PATROLZONE:SetAltitude( PatrolFloorAltitude, PatrolCeilingAltitude ) - self:F2( { PatrolFloorAltitude, PatrolCeilingAltitude } ) - - self.PatrolFloorAltitude = PatrolFloorAltitude - self.PatrolCeilingAltitude = PatrolCeilingAltitude -end - - - ---- @param Group#GROUP PatrolGroup -function _NewPatrolRoute( PatrolGroup ) - - PatrolGroup:T( "NewPatrolRoute" ) - local PatrolZone = PatrolGroup:GetState( PatrolGroup, "PatrolZone" ) -- PatrolZone#PATROLZONE - PatrolZone:NewPatrolRoute() -end - ---- Defines a new patrol route using the @{PatrolZone} parameters and settings. --- @param #PATROLZONE self --- @return #PATROLZONE self -function PATROLZONE:NewPatrolRoute() - - self:F2() - - local PatrolRoute = {} - - if self.PatrolGroup:IsAlive() then - --- Determine if the PatrolGroup is within the PatrolZone. - -- If not, make a waypoint within the to that the PatrolGroup will fly at maximum speed to that point. - --- --- Calculate the current route point. --- local CurrentVec2 = self.PatrolGroup:GetPointVec2() --- local CurrentAltitude = self.PatrolGroup:GetUnit(1):GetAltitude() --- local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) --- local CurrentRoutePoint = CurrentPointVec3:RoutePointAir( --- POINT_VEC3.RoutePointAltType.BARO, --- POINT_VEC3.RoutePointType.TurningPoint, --- POINT_VEC3.RoutePointAction.TurningPoint, --- ToPatrolZoneSpeed, --- true --- ) --- --- PatrolRoute[#PatrolRoute+1] = CurrentRoutePoint - - self:T2( PatrolRoute ) - - if self.PatrolGroup:IsNotInZone( self.PatrolZone ) then - --- Find a random 2D point in PatrolZone. - local ToPatrolZoneVec2 = self.PatrolZone:GetRandomVec2() - self:T2( ToPatrolZoneVec2 ) - - --- Define Speed and Altitude. - local ToPatrolZoneAltitude = math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ) - local ToPatrolZoneSpeed = self.PatrolMaxSpeed - self:T2( ToPatrolZoneSpeed ) - - --- Obtain a 3D @{Point} from the 2D point + altitude. - local ToPatrolZonePointVec3 = POINT_VEC3:New( ToPatrolZoneVec2.x, ToPatrolZoneAltitude, ToPatrolZoneVec2.y ) - - --- Create a route point of type air. - local ToPatrolZoneRoutePoint = ToPatrolZonePointVec3:RoutePointAir( - POINT_VEC3.RoutePointAltType.BARO, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToPatrolZoneSpeed, - true - ) - - PatrolRoute[#PatrolRoute+1] = ToPatrolZoneRoutePoint - - end - - --- Define a random point in the @{Zone}. The AI will fly to that point within the zone. - - --- Find a random 2D point in PatrolZone. - local ToTargetVec2 = self.PatrolZone:GetRandomVec2() - self:T2( ToTargetVec2 ) - - --- Define Speed and Altitude. - local ToTargetAltitude = math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ) - local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) - self:T2( { self.PatrolMinSpeed, self.PatrolMaxSpeed, ToTargetSpeed } ) - - --- Obtain a 3D @{Point} from the 2D point + altitude. - local ToTargetPointVec3 = POINT_VEC3:New( ToTargetVec2.x, ToTargetAltitude, ToTargetVec2.y ) - - --- Create a route point of type air. - local ToTargetRoutePoint = ToTargetPointVec3:RoutePointAir( - POINT_VEC3.RoutePointAltType.BARO, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, - true - ) - - --ToTargetPointVec3:SmokeRed() - - PatrolRoute[#PatrolRoute+1] = ToTargetRoutePoint - - --- Now we're going to do something special, we're going to call a function from a waypoint action at the PatrolGroup... - self.PatrolGroup:WayPointInitialize( PatrolRoute ) - - --- Do a trick, link the NewPatrolRoute function of the PATROLGROUP object to the PatrolGroup in a temporary variable ... - self.PatrolGroup:SetState( self.PatrolGroup, "PatrolZone", self ) - self.PatrolGroup:WayPointFunction( #PatrolRoute, 1, "_NewPatrolRoute" ) - - --- NOW ROUTE THE GROUP! - self.PatrolGroup:WayPointExecute( 1, 2 ) - end - -end - ---- When the PatrolGroup is out of fuel, it is required that a new PatrolGroup is started, before the old PatrolGroup 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 PatrolGroup will continue for a given time its patrol task in orbit, while a new PatrolGroup is targetted to the PATROLZONE. --- Once the time is finished, the old PatrolGroup will return to the base. --- @param #PATROLZONE self --- @param #number PatrolFuelTresholdPercentage The treshold in percentage (between 0 and 1) when the PatrolGroup is considered to get out of fuel. --- @param #number PatrolOutOfFuelOrbitTime The amount of seconds the out of fuel PatrolGroup will orbit before returning to the base. --- @return #PATROLZONE self -function PATROLZONE:ManageFuel( PatrolFuelTresholdPercentage, PatrolOutOfFuelOrbitTime ) - - self.PatrolManageFuel = true - self.PatrolFuelTresholdPercentage = PatrolFuelTresholdPercentage - self.PatrolOutOfFuelOrbitTime = PatrolOutOfFuelOrbitTime - - if self.PatrolGroup then - self.PatrolOutOfFuelMonitor = SCHEDULER:New( self, self._MonitorOutOfFuelScheduled, {}, 1, 120, 0 ) - self.SpawnPatrolGroup = SPAWN:New( self.PatrolGroupTemplateName ) - end - return self -end - ---- @param #PATROLZONE self -function _MonitorOutOfFuelScheduled( self ) - self:F2( "_MonitorOutOfFuelScheduled" ) - - if self.PatrolGroup and self.PatrolGroup:IsAlive() then - - local Fuel = self.PatrolGroup:GetUnit(1):GetFuel() - if Fuel < self.PatrolFuelTresholdPercentage then - local OldPatrolGroup = self.PatrolGroup - local PatrolGroupTemplate = self.PatrolGroup:GetTemplate() - - local OrbitTask = OldPatrolGroup:TaskOrbitCircle( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ), self.PatrolMinSpeed ) - local TimedOrbitTask = OldPatrolGroup:TaskControlled( OrbitTask, OldPatrolGroup:TaskCondition(nil,nil,nil,nil,self.PatrolOutOfFuelOrbitTime,nil ) ) - OldPatrolGroup:SetTask( TimedOrbitTask, 10 ) - - local NewPatrolGroup = self.SpawnPatrolGroup:Spawn() - self.PatrolGroup = NewPatrolGroup - self:NewPatrolRoute() - end - else - self.PatrolOutOfFuelMonitor:Stop() - end -end--- This module contains the AIBALANCER class. --- --- === --- --- 1) @{AIBalancer#AIBALANCER} class, extends @{Base#BASE} --- ================================================ --- The @{AIBalancer#AIBALANCER} class controls the dynamic spawning of AI GROUPS depending on a SET_CLIENT. --- There will be as many AI GROUPS spawned as there at CLIENTS in SET_CLIENT not spawned. --- --- 1.1) AIBALANCER construction method: --- ------------------------------------ --- Create a new AIBALANCER object with the @{#AIBALANCER.New} method: --- --- * @{#AIBALANCER.New}: Creates a new AIBALANCER object. --- --- 1.2) AIBALANCER returns AI to Airbases: --- --------------------------------------- --- You can configure to have the AI to return to: --- --- * @{#AIBALANCER.ReturnToHomeAirbase}: Returns the AI to the home @{Airbase#AIRBASE}. --- * @{#AIBALANCER.ReturnToNearestAirbases}: Returns the AI to the nearest friendly @{Airbase#AIRBASE}. --- --- 1.3) AIBALANCER allows AI to patrol specific zones: --- --------------------------------------------------- --- Use @{AIBalancer#AIBALANCER.SetPatrolZone}() to specify a zone where the AI needs to patrol. --- --- --- === --- --- CREDITS --- ======= --- **Dutch_Baron (James)** Who you can search on the Eagle Dynamics Forums. --- Working together with James has resulted in the creation of the AIBALANCER class. --- James has shared his ideas on balancing AI with air units, and together we made a first design which you can use now :-) --- --- **SNAFU** --- Had a couple of mails with the guys to validate, if the same concept in the GCI/CAP script could be reworked within MOOSE. --- None of the script code has been used however within the new AIBALANCER moose class. --- --- @module AIBalancer --- @author FlightControl - ---- AIBALANCER class --- @type AIBALANCER --- @field Set#SET_CLIENT SetClient --- @field Spawn#SPAWN SpawnAI --- @field #boolean ToNearestAirbase --- @field Set#SET_AIRBASE ReturnAirbaseSet --- @field DCSTypes#Distance ReturnTresholdRange --- @field #boolean ToHomeAirbase --- @field PatrolZone#PATROLZONE PatrolZone --- @extends Base#BASE -AIBALANCER = { - ClassName = "AIBALANCER", - PatrolZones = {}, - AIGroups = {}, -} - ---- Creates a new AIBALANCER object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. --- @param #AIBALANCER self --- @param SetClient A SET_CLIENT object that will contain the CLIENT objects to be monitored if they are alive or not (joined by a player). --- @param SpawnAI A SPAWN object that will spawn the AI units required, balancing the SetClient. --- @return #AIBALANCER self -function AIBALANCER:New( SetClient, SpawnAI ) - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) - - self.SetClient = SetClient - if type( SpawnAI ) == "table" then - if SpawnAI.ClassName and SpawnAI.ClassName == "SPAWN" then - self.SpawnAI = { SpawnAI } - else - local SpawnObjects = true - for SpawnObjectID, SpawnObject in pairs( SpawnAI ) do - if SpawnObject.ClassName and SpawnObject.ClassName == "SPAWN" then - self:E( SpawnObject.ClassName ) - else - self:E( "other object" ) - SpawnObjects = false - end - end - if SpawnObjects == true then - self.SpawnAI = SpawnAI - else - error( "No SPAWN object given in parameter SpawnAI, either as a single object or as a table of objects!" ) - end - end - end - - self.ToNearestAirbase = false - self.ReturnHomeAirbase = false - - self.AIMonitorSchedule = SCHEDULER:New( self, self._ClientAliveMonitorScheduler, {}, 1, 10, 0 ) - - return self -end - ---- Returns the AI to the nearest friendly @{Airbase#AIRBASE}. --- @param #AIBALANCER self --- @param DCSTypes#Distance ReturnTresholdRange If there is an enemy @{Client#CLIENT} within the ReturnTresholdRange given in meters, the AI will not return to the nearest @{Airbase#AIRBASE}. --- @param Set#SET_AIRBASE ReturnAirbaseSet The SET of @{Set#SET_AIRBASE}s to evaluate where to return to. -function AIBALANCER:ReturnToNearestAirbases( ReturnTresholdRange, ReturnAirbaseSet ) - - self.ToNearestAirbase = true - self.ReturnTresholdRange = ReturnTresholdRange - self.ReturnAirbaseSet = ReturnAirbaseSet -end - ---- Returns the AI to the home @{Airbase#AIRBASE}. --- @param #AIBALANCER self --- @param DCSTypes#Distance ReturnTresholdRange If there is an enemy @{Client#CLIENT} within the ReturnTresholdRange given in meters, the AI will not return to the nearest @{Airbase#AIRBASE}. -function AIBALANCER:ReturnToHomeAirbase( ReturnTresholdRange ) - - self.ToHomeAirbase = true - self.ReturnTresholdRange = ReturnTresholdRange -end - ---- Let the AI patrol a @{Zone} with a given Speed range and Altitude range. --- @param #AIBALANCER self --- @param PatrolZone#PATROLZONE PatrolZone The @{PatrolZone} where the AI needs to patrol. --- @return PatrolZone#PATROLZONE self -function AIBALANCER:SetPatrolZone( PatrolZone ) - - self.PatrolZone = PatrolZone -end - ---- @param #AIBALANCER self -function AIBALANCER:_ClientAliveMonitorScheduler() - - self.SetClient:ForEachClient( - --- @param Client#CLIENT Client - function( Client ) - local ClientAIAliveState = Client:GetState( self, 'AIAlive' ) - self:T( ClientAIAliveState ) - if Client:IsAlive() then - if ClientAIAliveState == true then - Client:SetState( self, 'AIAlive', false ) - - local AIGroup = self.AIGroups[Client.UnitName] -- Group#GROUP - --- local PatrolZone = Client:GetState( self, "PatrolZone" ) --- if PatrolZone then --- PatrolZone = nil --- Client:ClearState( self, "PatrolZone" ) --- end - - if self.ToNearestAirbase == false and self.ToHomeAirbase == false then - AIGroup:Destroy() - else - -- We test if there is no other CLIENT within the self.ReturnTresholdRange of the first unit of the AI group. - -- If there is a CLIENT, the AI stays engaged and will not return. - -- If there is no CLIENT within the self.ReturnTresholdRange, then the unit will return to the Airbase return method selected. - - local PlayerInRange = { Value = false } - local RangeZone = ZONE_RADIUS:New( 'RangeZone', AIGroup:GetPointVec2(), self.ReturnTresholdRange ) - - self:E( RangeZone ) - - _DATABASE:ForEachPlayer( - --- @param Unit#UNIT RangeTestUnit - function( RangeTestUnit, RangeZone, AIGroup, PlayerInRange ) - self:E( { PlayerInRange, RangeTestUnit.UnitName, RangeZone.ZoneName } ) - if RangeTestUnit:IsInZone( RangeZone ) == true then - self:E( "in zone" ) - if RangeTestUnit:GetCoalition() ~= AIGroup:GetCoalition() then - self:E( "in range" ) - PlayerInRange.Value = true - end - end - end, - - --- @param Zone#ZONE_RADIUS RangeZone - -- @param Group#GROUP AIGroup - function( RangeZone, AIGroup, PlayerInRange ) - local AIGroupTemplate = AIGroup:GetTemplate() - if PlayerInRange.Value == false then - if self.ToHomeAirbase == true then - local WayPointCount = #AIGroupTemplate.route.points - local SwitchWayPointCommand = AIGroup:CommandSwitchWayPoint( 1, WayPointCount, 1 ) - AIGroup:SetCommand( SwitchWayPointCommand ) - AIGroup:MessageToRed( "Returning to home base ...", 30 ) - else - -- Okay, we need to send this Group back to the nearest base of the Coalition of the AI. - --TODO: i need to rework the POINT_VEC2 thing. - local PointVec2 = POINT_VEC2:New( AIGroup:GetPointVec2().x, AIGroup:GetPointVec2().y ) - local ClosestAirbase = self.ReturnAirbaseSet:FindNearestAirbaseFromPointVec2( PointVec2 ) - self:T( ClosestAirbase.AirbaseName ) - AIGroup:MessageToRed( "Returning to " .. ClosestAirbase:GetName().. " ...", 30 ) - local RTBRoute = AIGroup:RouteReturnToAirbase( ClosestAirbase ) - AIGroupTemplate.route = RTBRoute - AIGroup:Respawn( AIGroupTemplate ) - end - end - end - , RangeZone, AIGroup, PlayerInRange - ) - - end - end - else - if not ClientAIAliveState or ClientAIAliveState == false then - Client:SetState( self, 'AIAlive', true ) - - - -- OK, spawn a new group from the SpawnAI objects provided. - local SpawnAICount = #self.SpawnAI - local SpawnAIIndex = math.random( 1, SpawnAICount ) - local AIGroup = self.SpawnAI[SpawnAIIndex]:Spawn() - AIGroup:E( "spawning new AIGroup" ) - --TODO: need to rework UnitName thing ... - self.AIGroups[Client.UnitName] = AIGroup - - --- Now test if the AIGroup needs to patrol a zone, otherwise let it follow its route... - if self.PatrolZone then - self.PatrolZones[#self.PatrolZones+1] = PATROLZONE:New( - self.PatrolZone.PatrolZone, - self.PatrolZone.PatrolFloorAltitude, - self.PatrolZone.PatrolCeilingAltitude, - self.PatrolZone.PatrolMinSpeed, - self.PatrolZone.PatrolMaxSpeed - ) - - if self.PatrolZone.PatrolManageFuel == true then - self.PatrolZones[#self.PatrolZones]:ManageFuel( self.PatrolZone.PatrolFuelTresholdPercentage, self.PatrolZone.PatrolOutOfFuelOrbitTime ) - end - self.PatrolZones[#self.PatrolZones]:SetGroup( AIGroup ) - - --self.PatrolZones[#self.PatrolZones+1] = PatrolZone - - --Client:SetState( self, "PatrolZone", PatrolZone ) - end - end - end - end - ) - return true -end - - - ---- This module contains the AIRBASEPOLICE classes. --- --- === --- --- 1) @{AirbasePolice#AIRBASEPOLICE_BASE} class, extends @{Base#BASE} --- ================================================================== --- The @{AirbasePolice#AIRBASEPOLICE_BASE} class provides the main methods to monitor CLIENT behaviour at airbases. --- CLIENTS should not be allowed to: --- --- * Don't taxi faster than 40 km/h. --- * Don't take-off on taxiways. --- * Avoid to hit other planes on the airbase. --- * Obey ground control orders. --- --- 2) @{AirbasePolice#AIRBASEPOLICE_CAUCASUS} class, extends @{AirbasePolice#AIRBASEPOLICE_BASE} --- ============================================================================================= --- All the airbases on the caucasus map can be monitored using this class. --- If you want to monitor specific airbases, you need to use the @{#AIRBASEPOLICE_BASE.Monitor}() method, which takes a table or airbase names. --- The following names can be given: --- * AnapaVityazevo --- * Batumi --- * Beslan --- * Gelendzhik --- * Gudauta --- * Kobuleti --- * KrasnodarCenter --- * KrasnodarPashkovsky --- * Krymsk --- * Kutaisi --- * MaykopKhanskaya --- * MineralnyeVody --- * Mozdok --- * Nalchik --- * Novorossiysk --- * SenakiKolkhi --- * SochiAdler --- * Soganlug --- * SukhumiBabushara --- * TbilisiLochini --- * Vaziani --- --- @module AirbasePolice --- @author FlightControl - - ---- @type AIRBASEPOLICE_BASE --- @field Set#SET_CLIENT SetClient --- @extends Base#BASE - -AIRBASEPOLICE_BASE = { - ClassName = "AIRBASEPOLICE_BASE", - SetClient = nil, - Airbases = nil, - AirbaseNames = nil, -} - - ---- Creates a new AIRBASEPOLICE_BASE object. --- @param #AIRBASEPOLICE_BASE self --- @param SetClient A SET_CLIENT object that will contain the CLIENT objects to be monitored if they follow the rules of the airbase. --- @param Airbases A table of Airbase Names. --- @return #AIRBASEPOLICE_BASE self -function AIRBASEPOLICE_BASE:New( SetClient, Airbases ) - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) - self:E( { self.ClassName, SetClient, Airbases } ) - - self.SetClient = SetClient - self.Airbases = Airbases - - for AirbaseID, Airbase in pairs( self.Airbases ) do - Airbase.ZoneBoundary = ZONE_POLYGON_BASE:New( "Boundary", Airbase.PointsBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - for PointsRunwayID, PointsRunway in pairs( Airbase.PointsRunways ) do - Airbase.ZoneRunways[PointsRunwayID] = ZONE_POLYGON_BASE:New( "Runway " .. PointsRunwayID, PointsRunway ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - end - end - - -- -- Template - -- local TemplateBoundary = GROUP:FindByName( "Template Boundary" ) - -- self.Airbases.Template.ZoneBoundary = ZONE_POLYGON:New( "Template Boundary", TemplateBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local TemplateRunway1 = GROUP:FindByName( "Template Runway 1" ) - -- self.Airbases.Template.ZoneRunways[1] = ZONE_POLYGON:New( "Template Runway 1", TemplateRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - - self.SetClient:ForEachClient( - --- @param Client#CLIENT Client - function( Client ) - Client:SetState( self, "Speeding", false ) - Client:SetState( self, "Warnings", 0) - Client:SetState( self, "Taxi", false ) - end - ) - - self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, {}, 0, 2, 0.05 ) - - return self -end - ---- @type AIRBASEPOLICE_BASE.AirbaseNames --- @list <#string> - ---- Monitor a table of airbase names. --- @param #AIRBASEPOLICE_BASE self --- @param #AIRBASEPOLICE_BASE.AirbaseNames AirbaseNames A list of AirbaseNames to monitor. If this parameters is nil, then all airbases will be monitored. --- @return #AIRBASEPOLICE_BASE self -function AIRBASEPOLICE_BASE:Monitor( AirbaseNames ) - - if AirbaseNames then - if type( AirbaseNames ) == "table" then - self.AirbaseNames = AirbaseNames - else - self.AirbaseNames = { AirbaseNames } - end - end -end - ---- @param #AIRBASEPOLICE_BASE self -function AIRBASEPOLICE_BASE:_AirbaseMonitor() - - for AirbaseID, Airbase in pairs( self.Airbases ) do - - if not self.AirbaseNames or self.AirbaseNames[AirbaseID] then - - self:E( AirbaseID ) - - self.SetClient:ForEachClientInZone( Airbase.ZoneBoundary, - - --- @param Client#CLIENT Client - function( Client ) - - self:E( Client.UnitName ) - if Client:IsAlive() then - local NotInRunwayZone = true - for ZoneRunwayID, ZoneRunway in pairs( Airbase.ZoneRunways ) do - NotInRunwayZone = ( Client:IsNotInZone( ZoneRunway ) == true ) and NotInRunwayZone or false - end - - if NotInRunwayZone then - local Taxi = self:GetState( self, "Taxi" ) - self:E( Taxi ) - if Taxi == false then - Client:Message( "Welcome at " .. AirbaseID .. ". The maximum taxiing speed is " .. Airbase.MaximumSpeed " km/h.", 20, "ATC" ) - self:SetState( self, "Taxi", true ) - end - - local VelocityVec3 = Client:GetVelocity() - local Velocity = math.abs(VelocityVec3.x) + math.abs(VelocityVec3.y) + math.abs(VelocityVec3.z) - local IsAboveRunway = Client:IsAboveRunway() - local IsOnGround = Client:InAir() == false - self:T( IsAboveRunway, IsOnGround ) - - if IsAboveRunway and IsOnGround then - - if Velocity > Airbase.MaximumSpeed then - local IsSpeeding = Client:GetState( self, "Speeding" ) - - if IsSpeeding == true then - local SpeedingWarnings = Client:GetState( self, "Warnings" ) - self:T( SpeedingWarnings ) - - if SpeedingWarnings <= 5 then - Client:Message( "You are speeding on the taxiway! Slow down or you will be removed from this airbase! Your current velocity is " .. string.format( "%2.0f km/h", Velocity ), 5, "Warning " .. SpeedingWarnings .. " / 5" ) - Client:SetState( self, "Warnings", SpeedingWarnings + 1 ) - else - MESSAGE:New( "Player " .. Client:GetPlayerName() .. " has been removed from the airbase, due to a speeding violation ...", 10, "Airbase Police" ):ToAll() - Client:GetGroup():Destroy() - Client:SetState( self, "Speeding", false ) - Client:SetState( self, "Warnings", 0 ) - end - - else - Client:Message( "You are speeding on the taxiway! Slow down please ...! Your current velocity is " .. string.format( "%2.0f km/h", Velocity ), 5, "Attention! " ) - Client:SetState( self, "Speeding", true ) - Client:SetState( self, "Warnings", 1 ) - end - - else - Client:SetState( self, "Speeding", false ) - Client:SetState( self, "Warnings", 0 ) - end - end - - else - Client:SetState( self, "Speeding", false ) - Client:SetState( self, "Warnings", 0 ) - local Taxi = self:GetState( self, "Taxi" ) - if Taxi == true then - Client:Message( "You have progressed to the runway ... Await take-off clearance ...", 20, "ATC" ) - self:SetState( self, "Taxi", false ) - end - end - end - end - ) - end - end - - return true -end - - ---- @type AIRBASEPOLICE_CAUCASUS --- @field Set#SET_CLIENT SetClient --- @extends #AIRBASEPOLICE_BASE - -AIRBASEPOLICE_CAUCASUS = { - ClassName = "AIRBASEPOLICE_CAUCASUS", - Airbases = { - AnapaVityazevo = { - PointsBoundary = { - [1]={["y"]=242234.85714287,["x"]=-6616.5714285726,}, - [2]={["y"]=241060.57142858,["x"]=-5585.142857144,}, - [3]={["y"]=243806.2857143,["x"]=-3962.2857142868,}, - [4]={["y"]=245240.57142858,["x"]=-4816.5714285726,}, - [5]={["y"]=244783.42857144,["x"]=-5630.8571428583,}, - [6]={["y"]=243800.57142858,["x"]=-5065.142857144,}, - [7]={["y"]=242232.00000001,["x"]=-6622.2857142868,}, - }, - PointsRunways = { - [1] = { - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Batumi = { - PointsBoundary = { - [1]={["y"]=617567.14285714,["x"]=-355313.14285715,}, - [2]={["y"]=616181.42857142,["x"]=-354800.28571429,}, - [3]={["y"]=616007.14285714,["x"]=-355128.85714286,}, - [4]={["y"]=618230,["x"]=-356914.57142858,}, - [5]={["y"]=618727.14285714,["x"]=-356166,}, - [6]={["y"]=617572.85714285,["x"]=-355308.85714286,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=616442.28571429,["x"]=-355090.28571429,}, - [2]={["y"]=618450.57142857,["x"]=-356522,}, - [3]={["y"]=618407.71428571,["x"]=-356584.85714286,}, - [4]={["y"]=618361.99999999,["x"]=-356554.85714286,}, - [5]={["y"]=618324.85714285,["x"]=-356599.14285715,}, - [6]={["y"]=618250.57142856,["x"]=-356543.42857143,}, - [7]={["y"]=618257.7142857,["x"]=-356496.28571429,}, - [8]={["y"]=618237.7142857,["x"]=-356459.14285715,}, - [9]={["y"]=616555.71428571,["x"]=-355258.85714286,}, - [10]={["y"]=616486.28571428,["x"]=-355280.57142858,}, - [11]={["y"]=616410.57142856,["x"]=-355227.71428572,}, - [12]={["y"]=616441.99999999,["x"]=-355179.14285715,}, - [13]={["y"]=616401.99999999,["x"]=-355147.71428572,}, - [14]={["y"]=616441.42857142,["x"]=-355092.57142858,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Beslan = { - PointsBoundary = { - [1]={["y"]=842082.57142857,["x"]=-148445.14285715,}, - [2]={["y"]=845237.71428572,["x"]=-148639.71428572,}, - [3]={["y"]=845232,["x"]=-148765.42857143,}, - [4]={["y"]=844220.57142857,["x"]=-149168.28571429,}, - [5]={["y"]=843274.85714286,["x"]=-149125.42857143,}, - [6]={["y"]=842077.71428572,["x"]=-148554,}, - [7]={["y"]=842083.42857143,["x"]=-148445.42857143,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=842104.57142857,["x"]=-148460.57142857,}, - [2]={["y"]=845225.71428572,["x"]=-148656,}, - [3]={["y"]=845220.57142858,["x"]=-148750,}, - [4]={["y"]=842098.85714286,["x"]=-148556.28571429,}, - [5]={["y"]=842104,["x"]=-148460.28571429,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Gelendzhik = { - PointsBoundary = { - [1]={["y"]=297856.00000001,["x"]=-51151.428571429,}, - [2]={["y"]=299044.57142858,["x"]=-49720.000000001,}, - [3]={["y"]=298861.71428572,["x"]=-49580.000000001,}, - [4]={["y"]=298198.85714286,["x"]=-49842.857142858,}, - [5]={["y"]=297990.28571429,["x"]=-50151.428571429,}, - [6]={["y"]=297696.00000001,["x"]=-51054.285714286,}, - [7]={["y"]=297850.28571429,["x"]=-51160.000000001,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=297834.00000001,["x"]=-51107.428571429,}, - [2]={["y"]=297786.57142858,["x"]=-51068.857142858,}, - [3]={["y"]=298946.57142858,["x"]=-49686.000000001,}, - [4]={["y"]=298993.14285715,["x"]=-49725.714285715,}, - [5]={["y"]=297835.14285715,["x"]=-51107.714285715,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Gudauta = { - PointsBoundary = { - [1]={["y"]=517246.57142857,["x"]=-197850.28571429,}, - [2]={["y"]=516749.42857142,["x"]=-198070.28571429,}, - [3]={["y"]=515755.14285714,["x"]=-197598.85714286,}, - [4]={["y"]=515369.42857142,["x"]=-196538.85714286,}, - [5]={["y"]=515623.71428571,["x"]=-195618.85714286,}, - [6]={["y"]=515946.57142857,["x"]=-195510.28571429,}, - [7]={["y"]=517243.71428571,["x"]=-197858.85714286,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=517096.57142857,["x"]=-197804.57142857,}, - [2]={["y"]=515880.85714285,["x"]=-195590.28571429,}, - [3]={["y"]=515812.28571428,["x"]=-195628.85714286,}, - [4]={["y"]=517036.57142857,["x"]=-197834.57142857,}, - [5]={["y"]=517097.99999999,["x"]=-197807.42857143,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Kobuleti = { - PointsBoundary = { - [1]={["y"]=634427.71428571,["x"]=-318290.28571429,}, - [2]={["y"]=635033.42857143,["x"]=-317550.2857143,}, - [3]={["y"]=635864.85714286,["x"]=-317333.14285715,}, - [4]={["y"]=636967.71428571,["x"]=-317261.71428572,}, - [5]={["y"]=637144.85714286,["x"]=-317913.14285715,}, - [6]={["y"]=634630.57142857,["x"]=-318687.42857144,}, - [7]={["y"]=634424.85714286,["x"]=-318290.2857143,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=634509.71428571,["x"]=-318339.42857144,}, - [2]={["y"]=636767.42857143,["x"]=-317516.57142858,}, - [3]={["y"]=636790,["x"]=-317575.71428572,}, - [4]={["y"]=634531.42857143,["x"]=-318398.00000001,}, - [5]={["y"]=634510.28571429,["x"]=-318339.71428572,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - KrasnodarCenter = { - PointsBoundary = { - [1]={["y"]=366680.28571429,["x"]=11699.142857142,}, - [2]={["y"]=366654.28571429,["x"]=11225.142857142,}, - [3]={["y"]=367497.14285715,["x"]=11082.285714285,}, - [4]={["y"]=368025.71428572,["x"]=10396.57142857,}, - [5]={["y"]=369854.28571429,["x"]=11367.999999999,}, - [6]={["y"]=369840.00000001,["x"]=11910.857142856,}, - [7]={["y"]=366682.57142858,["x"]=11697.999999999,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=369205.42857144,["x"]=11789.142857142,}, - [2]={["y"]=369209.71428572,["x"]=11714.857142856,}, - [3]={["y"]=366699.71428572,["x"]=11581.714285713,}, - [4]={["y"]=366698.28571429,["x"]=11659.142857142,}, - [5]={["y"]=369208.85714286,["x"]=11788.57142857,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - KrasnodarPashkovsky = { - PointsBoundary = { - [1]={["y"]=386754,["x"]=6476.5714285703,}, - [2]={["y"]=389182.57142858,["x"]=8722.2857142846,}, - [3]={["y"]=388832.57142858,["x"]=9086.5714285703,}, - [4]={["y"]=386961.14285715,["x"]=7707.9999999989,}, - [5]={["y"]=385404,["x"]=9179.4285714274,}, - [6]={["y"]=383239.71428572,["x"]=7386.5714285703,}, - [7]={["y"]=383954,["x"]=6486.5714285703,}, - [8]={["y"]=385775.42857143,["x"]=8097.9999999989,}, - [9]={["y"]=386804,["x"]=7319.4285714274,}, - [10]={["y"]=386375.42857143,["x"]=6797.9999999989,}, - [11]={["y"]=386746.85714286,["x"]=6472.2857142846,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=385891.14285715,["x"]=8416.5714285703,}, - [2]={["y"]=385842.28571429,["x"]=8467.9999999989,}, - [3]={["y"]=384180.85714286,["x"]=6917.1428571417,}, - [4]={["y"]=384228.57142858,["x"]=6867.7142857132,}, - [5]={["y"]=385891.14285715,["x"]=8416.5714285703,}, - }, - [2] = { - [1]={["y"]=386714.85714286,["x"]=6674.857142856,}, - [2]={["y"]=386757.71428572,["x"]=6627.7142857132,}, - [3]={["y"]=389028.57142858,["x"]=8741.4285714275,}, - [4]={["y"]=388981.71428572,["x"]=8790.5714285703,}, - [5]={["y"]=386714.57142858,["x"]=6674.5714285703,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Krymsk = { - PointsBoundary = { - [1]={["y"]=293338.00000001,["x"]=-7575.4285714297,}, - [2]={["y"]=295199.42857144,["x"]=-5434.0000000011,}, - [3]={["y"]=295595.14285715,["x"]=-6239.7142857154,}, - [4]={["y"]=294152.2857143,["x"]=-8325.4285714297,}, - [5]={["y"]=293345.14285715,["x"]=-7596.8571428582,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=293522.00000001,["x"]=-7567.4285714297,}, - [2]={["y"]=293578.57142858,["x"]=-7616.0000000011,}, - [3]={["y"]=295246.00000001,["x"]=-5591.142857144,}, - [4]={["y"]=295187.71428573,["x"]=-5546.0000000011,}, - [5]={["y"]=293523.14285715,["x"]=-7568.2857142868,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Kutaisi = { - PointsBoundary = { - [1]={["y"]=682087.42857143,["x"]=-284512.85714286,}, - [2]={["y"]=685387.42857143,["x"]=-283662.85714286,}, - [3]={["y"]=685294.57142857,["x"]=-284977.14285715,}, - [4]={["y"]=682744.57142857,["x"]=-286505.71428572,}, - [5]={["y"]=682094.57142857,["x"]=-284527.14285715,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=682638,["x"]=-285202.28571429,}, - [2]={["y"]=685050.28571429,["x"]=-284507.42857144,}, - [3]={["y"]=685068.85714286,["x"]=-284578.85714286,}, - [4]={["y"]=682657.42857143,["x"]=-285264.28571429,}, - [5]={["y"]=682638.28571429,["x"]=-285202.85714286,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - MaykopKhanskaya = { - PointsBoundary = { - [1]={["y"]=456876.28571429,["x"]=-27665.42857143,}, - [2]={["y"]=457800,["x"]=-28392.857142858,}, - [3]={["y"]=459368.57142857,["x"]=-26378.571428573,}, - [4]={["y"]=459425.71428572,["x"]=-25242.857142858,}, - [5]={["y"]=458961.42857143,["x"]=-24964.285714287,}, - [6]={["y"]=456878.57142857,["x"]=-27667.714285715,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=457005.42857143,["x"]=-27668.000000001,}, - [2]={["y"]=459028.85714286,["x"]=-25168.857142858,}, - [3]={["y"]=459082.57142857,["x"]=-25216.857142858,}, - [4]={["y"]=457060,["x"]=-27714.285714287,}, - [5]={["y"]=457004.57142857,["x"]=-27669.714285715,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - MineralnyeVody = { - PointsBoundary = { - [1]={["y"]=703857.14285714,["x"]=-50226.000000002,}, - [2]={["y"]=707385.71428571,["x"]=-51911.714285716,}, - [3]={["y"]=707595.71428571,["x"]=-51434.857142859,}, - [4]={["y"]=707900,["x"]=-51568.857142859,}, - [5]={["y"]=707542.85714286,["x"]=-52326.000000002,}, - [6]={["y"]=706628.57142857,["x"]=-52568.857142859,}, - [7]={["y"]=705142.85714286,["x"]=-51790.285714288,}, - [8]={["y"]=703678.57142857,["x"]=-50611.714285716,}, - [9]={["y"]=703857.42857143,["x"]=-50226.857142859,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=703904,["x"]=-50352.571428573,}, - [2]={["y"]=707596.28571429,["x"]=-52094.571428573,}, - [3]={["y"]=707560.57142858,["x"]=-52161.714285716,}, - [4]={["y"]=703871.71428572,["x"]=-50420.571428573,}, - [5]={["y"]=703902,["x"]=-50352.000000002,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Mozdok = { - PointsBoundary = { - [1]={["y"]=832123.42857143,["x"]=-83608.571428573,}, - [2]={["y"]=835916.28571429,["x"]=-83144.285714288,}, - [3]={["y"]=835474.28571429,["x"]=-84170.571428573,}, - [4]={["y"]=832911.42857143,["x"]=-84470.571428573,}, - [5]={["y"]=832487.71428572,["x"]=-85565.714285716,}, - [6]={["y"]=831573.42857143,["x"]=-85351.42857143,}, - [7]={["y"]=832123.71428572,["x"]=-83610.285714288,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=832201.14285715,["x"]=-83699.428571431,}, - [2]={["y"]=832212.57142857,["x"]=-83780.571428574,}, - [3]={["y"]=835730.28571429,["x"]=-83335.714285717,}, - [4]={["y"]=835718.85714286,["x"]=-83246.571428574,}, - [5]={["y"]=832200.57142857,["x"]=-83700.000000002,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Nalchik = { - PointsBoundary = { - [1]={["y"]=759370,["x"]=-125502.85714286,}, - [2]={["y"]=761384.28571429,["x"]=-124177.14285714,}, - [3]={["y"]=761472.85714286,["x"]=-124325.71428572,}, - [4]={["y"]=761092.85714286,["x"]=-125048.57142857,}, - [5]={["y"]=760295.71428572,["x"]=-125685.71428572,}, - [6]={["y"]=759444.28571429,["x"]=-125734.28571429,}, - [7]={["y"]=759375.71428572,["x"]=-125511.42857143,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=759454.28571429,["x"]=-125551.42857143,}, - [2]={["y"]=759492.85714286,["x"]=-125610.85714286,}, - [3]={["y"]=761406.28571429,["x"]=-124304.28571429,}, - [4]={["y"]=761361.14285714,["x"]=-124239.71428572,}, - [5]={["y"]=759456,["x"]=-125552.57142857,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Novorossiysk = { - PointsBoundary = { - [1]={["y"]=278677.71428573,["x"]=-41656.571428572,}, - [2]={["y"]=278446.2857143,["x"]=-41453.714285715,}, - [3]={["y"]=278989.14285716,["x"]=-40188.000000001,}, - [4]={["y"]=279717.71428573,["x"]=-39968.000000001,}, - [5]={["y"]=280020.57142859,["x"]=-40208.000000001,}, - [6]={["y"]=278674.85714287,["x"]=-41660.857142858,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=278673.14285716,["x"]=-41615.142857144,}, - [2]={["y"]=278625.42857144,["x"]=-41570.571428572,}, - [3]={["y"]=279835.42857144,["x"]=-40226.000000001,}, - [4]={["y"]=279882.2857143,["x"]=-40270.000000001,}, - [5]={["y"]=278672.00000001,["x"]=-41614.857142858,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - SenakiKolkhi = { - PointsBoundary = { - [1]={["y"]=646036.57142857,["x"]=-281778.85714286,}, - [2]={["y"]=646045.14285714,["x"]=-281191.71428571,}, - [3]={["y"]=647032.28571429,["x"]=-280598.85714285,}, - [4]={["y"]=647669.42857143,["x"]=-281273.14285714,}, - [5]={["y"]=648323.71428571,["x"]=-281370.28571428,}, - [6]={["y"]=648520.85714286,["x"]=-281978.85714285,}, - [7]={["y"]=646039.42857143,["x"]=-281783.14285714,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=646060.85714285,["x"]=-281736,}, - [2]={["y"]=646056.57142857,["x"]=-281631.71428571,}, - [3]={["y"]=648442.28571428,["x"]=-281840.28571428,}, - [4]={["y"]=648432.28571428,["x"]=-281918.85714286,}, - [5]={["y"]=646063.71428571,["x"]=-281738.85714286,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - SochiAdler = { - PointsBoundary = { - [1]={["y"]=460642.28571428,["x"]=-164861.71428571,}, - [2]={["y"]=462820.85714285,["x"]=-163368.85714286,}, - [3]={["y"]=463649.42857142,["x"]=-163340.28571429,}, - [4]={["y"]=463835.14285714,["x"]=-164040.28571429,}, - [5]={["y"]=462535.14285714,["x"]=-165654.57142857,}, - [6]={["y"]=460678,["x"]=-165247.42857143,}, - [7]={["y"]=460635.14285714,["x"]=-164876,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=460831.42857143,["x"]=-165180,}, - [2]={["y"]=460878.57142857,["x"]=-165257.14285714,}, - [3]={["y"]=463663.71428571,["x"]=-163793.14285714,}, - [4]={["y"]=463612.28571428,["x"]=-163697.42857143,}, - [5]={["y"]=460831.42857143,["x"]=-165177.14285714,}, - }, - [2] = { - [1]={["y"]=460831.42857143,["x"]=-165180,}, - [2]={["y"]=460878.57142857,["x"]=-165257.14285714,}, - [3]={["y"]=463663.71428571,["x"]=-163793.14285714,}, - [4]={["y"]=463612.28571428,["x"]=-163697.42857143,}, - [5]={["y"]=460831.42857143,["x"]=-165177.14285714,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Soganlug = { - PointsBoundary = { - [1]={["y"]=894530.85714286,["x"]=-316928.28571428,}, - [2]={["y"]=896422.28571428,["x"]=-318622.57142857,}, - [3]={["y"]=896090.85714286,["x"]=-318934,}, - [4]={["y"]=894019.42857143,["x"]=-317119.71428571,}, - [5]={["y"]=894533.71428571,["x"]=-316925.42857143,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=894525.71428571,["x"]=-316964,}, - [2]={["y"]=896363.14285714,["x"]=-318634.28571428,}, - [3]={["y"]=896299.14285714,["x"]=-318702.85714286,}, - [4]={["y"]=894464,["x"]=-317031.71428571,}, - [5]={["y"]=894524.57142857,["x"]=-316963.71428571,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - SukhumiBabushara = { - PointsBoundary = { - [1]={["y"]=562541.14285714,["x"]=-219852.28571429,}, - [2]={["y"]=562691.14285714,["x"]=-219395.14285714,}, - [3]={["y"]=564326.85714286,["x"]=-219523.71428571,}, - [4]={["y"]=566262.57142857,["x"]=-221166.57142857,}, - [5]={["y"]=566069.71428571,["x"]=-221580.85714286,}, - [6]={["y"]=562534,["x"]=-219873.71428571,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=562684,["x"]=-219779.71428571,}, - [2]={["y"]=562717.71428571,["x"]=-219718,}, - [3]={["y"]=566046.85714286,["x"]=-221376.57142857,}, - [4]={["y"]=566012.28571428,["x"]=-221446.57142857,}, - [5]={["y"]=562684.57142857,["x"]=-219782.57142857,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - TbilisiLochini = { - PointsBoundary = { - [1]={["y"]=895172.85714286,["x"]=-314667.42857143,}, - [2]={["y"]=895337.42857143,["x"]=-314143.14285714,}, - [3]={["y"]=895990.28571429,["x"]=-314036,}, - [4]={["y"]=897730.28571429,["x"]=-315284.57142857,}, - [5]={["y"]=897901.71428571,["x"]=-316284.57142857,}, - [6]={["y"]=897684.57142857,["x"]=-316618.85714286,}, - [7]={["y"]=895173.14285714,["x"]=-314667.42857143,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=895261.14285715,["x"]=-314652.28571428,}, - [2]={["y"]=897654.57142857,["x"]=-316523.14285714,}, - [3]={["y"]=897711.71428571,["x"]=-316450.28571429,}, - [4]={["y"]=895327.42857143,["x"]=-314568.85714286,}, - [5]={["y"]=895261.71428572,["x"]=-314656,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Vaziani = { - PointsBoundary = { - [1]={["y"]=902122,["x"]=-318163.71428572,}, - [2]={["y"]=902678.57142857,["x"]=-317594,}, - [3]={["y"]=903275.71428571,["x"]=-317405.42857143,}, - [4]={["y"]=903418.57142857,["x"]=-317891.14285714,}, - [5]={["y"]=904292.85714286,["x"]=-318748.28571429,}, - [6]={["y"]=904542,["x"]=-319740.85714286,}, - [7]={["y"]=904042,["x"]=-320166.57142857,}, - [8]={["y"]=902121.42857143,["x"]=-318164.85714286,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=902239.14285714,["x"]=-318190.85714286,}, - [2]={["y"]=904014.28571428,["x"]=-319994.57142857,}, - [3]={["y"]=904064.85714285,["x"]=-319945.14285715,}, - [4]={["y"]=902294.57142857,["x"]=-318146,}, - [5]={["y"]=902247.71428571,["x"]=-318190.85714286,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - }, -} - ---- Creates a new AIRBASEPOLICE_CAUCASUS object. --- @param #AIRBASEPOLICE_CAUCASUS self --- @param SetClient A SET_CLIENT object that will contain the CLIENT objects to be monitored if they follow the rules of the airbase. --- @return #AIRBASEPOLICE_CAUCASUS self -function AIRBASEPOLICE_CAUCASUS:New( SetClient ) - - -- Inherits from BASE - local self = BASE:Inherit( self, AIRBASEPOLICE_BASE:New( SetClient, self.Airbases ) ) - - -- -- AnapaVityazevo - -- local AnapaVityazevoBoundary = GROUP:FindByName( "AnapaVityazevo Boundary" ) - -- self.Airbases.AnapaVityazevo.ZoneBoundary = ZONE_POLYGON:New( "AnapaVityazevo Boundary", AnapaVityazevoBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local AnapaVityazevoRunway1 = GROUP:FindByName( "AnapaVityazevo Runway 1" ) - -- self.Airbases.AnapaVityazevo.ZoneRunways[1] = ZONE_POLYGON:New( "AnapaVityazevo Runway 1", AnapaVityazevoRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- Batumi - -- local BatumiBoundary = GROUP:FindByName( "Batumi Boundary" ) - -- self.Airbases.Batumi.ZoneBoundary = ZONE_POLYGON:New( "Batumi Boundary", BatumiBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local BatumiRunway1 = GROUP:FindByName( "Batumi Runway 1" ) - -- self.Airbases.Batumi.ZoneRunways[1] = ZONE_POLYGON:New( "Batumi Runway 1", BatumiRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- Beslan - -- local BeslanBoundary = GROUP:FindByName( "Beslan Boundary" ) - -- self.Airbases.Beslan.ZoneBoundary = ZONE_POLYGON:New( "Beslan Boundary", BeslanBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local BeslanRunway1 = GROUP:FindByName( "Beslan Runway 1" ) - -- self.Airbases.Beslan.ZoneRunways[1] = ZONE_POLYGON:New( "Beslan Runway 1", BeslanRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- Gelendzhik - -- local GelendzhikBoundary = GROUP:FindByName( "Gelendzhik Boundary" ) - -- self.Airbases.Gelendzhik.ZoneBoundary = ZONE_POLYGON:New( "Gelendzhik Boundary", GelendzhikBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local GelendzhikRunway1 = GROUP:FindByName( "Gelendzhik Runway 1" ) - -- self.Airbases.Gelendzhik.ZoneRunways[1] = ZONE_POLYGON:New( "Gelendzhik Runway 1", GelendzhikRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- Gudauta - -- local GudautaBoundary = GROUP:FindByName( "Gudauta Boundary" ) - -- self.Airbases.Gudauta.ZoneBoundary = ZONE_POLYGON:New( "Gudauta Boundary", GudautaBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local GudautaRunway1 = GROUP:FindByName( "Gudauta Runway 1" ) - -- self.Airbases.Gudauta.ZoneRunways[1] = ZONE_POLYGON:New( "Gudauta Runway 1", GudautaRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- Kobuleti - -- local KobuletiBoundary = GROUP:FindByName( "Kobuleti Boundary" ) - -- self.Airbases.Kobuleti.ZoneBoundary = ZONE_POLYGON:New( "Kobuleti Boundary", KobuletiBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local KobuletiRunway1 = GROUP:FindByName( "Kobuleti Runway 1" ) - -- self.Airbases.Kobuleti.ZoneRunways[1] = ZONE_POLYGON:New( "Kobuleti Runway 1", KobuletiRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- KrasnodarCenter - -- local KrasnodarCenterBoundary = GROUP:FindByName( "KrasnodarCenter Boundary" ) - -- self.Airbases.KrasnodarCenter.ZoneBoundary = ZONE_POLYGON:New( "KrasnodarCenter Boundary", KrasnodarCenterBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local KrasnodarCenterRunway1 = GROUP:FindByName( "KrasnodarCenter Runway 1" ) - -- self.Airbases.KrasnodarCenter.ZoneRunways[1] = ZONE_POLYGON:New( "KrasnodarCenter Runway 1", KrasnodarCenterRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- KrasnodarPashkovsky - -- local KrasnodarPashkovskyBoundary = GROUP:FindByName( "KrasnodarPashkovsky Boundary" ) - -- self.Airbases.KrasnodarPashkovsky.ZoneBoundary = ZONE_POLYGON:New( "KrasnodarPashkovsky Boundary", KrasnodarPashkovskyBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local KrasnodarPashkovskyRunway1 = GROUP:FindByName( "KrasnodarPashkovsky Runway 1" ) - -- self.Airbases.KrasnodarPashkovsky.ZoneRunways[1] = ZONE_POLYGON:New( "KrasnodarPashkovsky Runway 1", KrasnodarPashkovskyRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- local KrasnodarPashkovskyRunway2 = GROUP:FindByName( "KrasnodarPashkovsky Runway 2" ) - -- self.Airbases.KrasnodarPashkovsky.ZoneRunways[2] = ZONE_POLYGON:New( "KrasnodarPashkovsky Runway 2", KrasnodarPashkovskyRunway2 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- Krymsk - -- local KrymskBoundary = GROUP:FindByName( "Krymsk Boundary" ) - -- self.Airbases.Krymsk.ZoneBoundary = ZONE_POLYGON:New( "Krymsk Boundary", KrymskBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local KrymskRunway1 = GROUP:FindByName( "Krymsk Runway 1" ) - -- self.Airbases.Krymsk.ZoneRunways[1] = ZONE_POLYGON:New( "Krymsk Runway 1", KrymskRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- Kutaisi - -- local KutaisiBoundary = GROUP:FindByName( "Kutaisi Boundary" ) - -- self.Airbases.Kutaisi.ZoneBoundary = ZONE_POLYGON:New( "Kutaisi Boundary", KutaisiBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local KutaisiRunway1 = GROUP:FindByName( "Kutaisi Runway 1" ) - -- self.Airbases.Kutaisi.ZoneRunways[1] = ZONE_POLYGON:New( "Kutaisi Runway 1", KutaisiRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- MaykopKhanskaya - -- local MaykopKhanskayaBoundary = GROUP:FindByName( "MaykopKhanskaya Boundary" ) - -- self.Airbases.MaykopKhanskaya.ZoneBoundary = ZONE_POLYGON:New( "MaykopKhanskaya Boundary", MaykopKhanskayaBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local MaykopKhanskayaRunway1 = GROUP:FindByName( "MaykopKhanskaya Runway 1" ) - -- self.Airbases.MaykopKhanskaya.ZoneRunways[1] = ZONE_POLYGON:New( "MaykopKhanskaya Runway 1", MaykopKhanskayaRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- MineralnyeVody - -- local MineralnyeVodyBoundary = GROUP:FindByName( "MineralnyeVody Boundary" ) - -- self.Airbases.MineralnyeVody.ZoneBoundary = ZONE_POLYGON:New( "MineralnyeVody Boundary", MineralnyeVodyBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local MineralnyeVodyRunway1 = GROUP:FindByName( "MineralnyeVody Runway 1" ) - -- self.Airbases.MineralnyeVody.ZoneRunways[1] = ZONE_POLYGON:New( "MineralnyeVody Runway 1", MineralnyeVodyRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- Mozdok - -- local MozdokBoundary = GROUP:FindByName( "Mozdok Boundary" ) - -- self.Airbases.Mozdok.ZoneBoundary = ZONE_POLYGON:New( "Mozdok Boundary", MozdokBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local MozdokRunway1 = GROUP:FindByName( "Mozdok Runway 1" ) - -- self.Airbases.Mozdok.ZoneRunways[1] = ZONE_POLYGON:New( "Mozdok Runway 1", MozdokRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- Nalchik - -- local NalchikBoundary = GROUP:FindByName( "Nalchik Boundary" ) - -- self.Airbases.Nalchik.ZoneBoundary = ZONE_POLYGON:New( "Nalchik Boundary", NalchikBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local NalchikRunway1 = GROUP:FindByName( "Nalchik Runway 1" ) - -- self.Airbases.Nalchik.ZoneRunways[1] = ZONE_POLYGON:New( "Nalchik Runway 1", NalchikRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- Novorossiysk - -- local NovorossiyskBoundary = GROUP:FindByName( "Novorossiysk Boundary" ) - -- self.Airbases.Novorossiysk.ZoneBoundary = ZONE_POLYGON:New( "Novorossiysk Boundary", NovorossiyskBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local NovorossiyskRunway1 = GROUP:FindByName( "Novorossiysk Runway 1" ) - -- self.Airbases.Novorossiysk.ZoneRunways[1] = ZONE_POLYGON:New( "Novorossiysk Runway 1", NovorossiyskRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- SenakiKolkhi - -- local SenakiKolkhiBoundary = GROUP:FindByName( "SenakiKolkhi Boundary" ) - -- self.Airbases.SenakiKolkhi.ZoneBoundary = ZONE_POLYGON:New( "SenakiKolkhi Boundary", SenakiKolkhiBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local SenakiKolkhiRunway1 = GROUP:FindByName( "SenakiKolkhi Runway 1" ) - -- self.Airbases.SenakiKolkhi.ZoneRunways[1] = ZONE_POLYGON:New( "SenakiKolkhi Runway 1", SenakiKolkhiRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- SochiAdler - -- local SochiAdlerBoundary = GROUP:FindByName( "SochiAdler Boundary" ) - -- self.Airbases.SochiAdler.ZoneBoundary = ZONE_POLYGON:New( "SochiAdler Boundary", SochiAdlerBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local SochiAdlerRunway1 = GROUP:FindByName( "SochiAdler Runway 1" ) - -- self.Airbases.SochiAdler.ZoneRunways[1] = ZONE_POLYGON:New( "SochiAdler Runway 1", SochiAdlerRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- local SochiAdlerRunway2 = GROUP:FindByName( "SochiAdler Runway 2" ) - -- self.Airbases.SochiAdler.ZoneRunways[2] = ZONE_POLYGON:New( "SochiAdler Runway 2", SochiAdlerRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- Soganlug - -- local SoganlugBoundary = GROUP:FindByName( "Soganlug Boundary" ) - -- self.Airbases.Soganlug.ZoneBoundary = ZONE_POLYGON:New( "Soganlug Boundary", SoganlugBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local SoganlugRunway1 = GROUP:FindByName( "Soganlug Runway 1" ) - -- self.Airbases.Soganlug.ZoneRunways[1] = ZONE_POLYGON:New( "Soganlug Runway 1", SoganlugRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- SukhumiBabushara - -- local SukhumiBabusharaBoundary = GROUP:FindByName( "SukhumiBabushara Boundary" ) - -- self.Airbases.SukhumiBabushara.ZoneBoundary = ZONE_POLYGON:New( "SukhumiBabushara Boundary", SukhumiBabusharaBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local SukhumiBabusharaRunway1 = GROUP:FindByName( "SukhumiBabushara Runway 1" ) - -- self.Airbases.SukhumiBabushara.ZoneRunways[1] = ZONE_POLYGON:New( "SukhumiBabushara Runway 1", SukhumiBabusharaRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- TbilisiLochini - -- local TbilisiLochiniBoundary = GROUP:FindByName( "TbilisiLochini Boundary" ) - -- self.Airbases.TbilisiLochini.ZoneBoundary = ZONE_POLYGON:New( "TbilisiLochini Boundary", TbilisiLochiniBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local TbilisiLochiniRunway1 = GROUP:FindByName( "TbilisiLochini Runway 1" ) - -- self.Airbases.TbilisiLochini.ZoneRunways[1] = ZONE_POLYGON:New( "TbilisiLochini Runway 1", TbilisiLochiniRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- Vaziani - -- local VazianiBoundary = GROUP:FindByName( "Vaziani Boundary" ) - -- self.Airbases.Vaziani.ZoneBoundary = ZONE_POLYGON:New( "Vaziani Boundary", VazianiBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local VazianiRunway1 = GROUP:FindByName( "Vaziani Runway 1" ) - -- self.Airbases.Vaziani.ZoneRunways[1] = ZONE_POLYGON:New( "Vaziani Runway 1", VazianiRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - - - -- -- Template - -- local TemplateBoundary = GROUP:FindByName( "Template Boundary" ) - -- self.Airbases.Template.ZoneBoundary = ZONE_POLYGON:New( "Template Boundary", TemplateBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local TemplateRunway1 = GROUP:FindByName( "Template Runway 1" ) - -- self.Airbases.Template.ZoneRunways[1] = ZONE_POLYGON:New( "Template Runway 1", TemplateRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - - return self - -end - ---- This module contains the DETECTION classes. --- --- === --- --- 1) @{Detection#DETECTION_BASE} class, extends @{Base#BASE} --- ========================================================== --- The @{Detection#DETECTION_BASE} class defines the core functions to administer detected objects. --- Detected objects are grouped in SETS of UNITS. --- --- 1.1) DETECTION constructor: --- ---------------------------- --- * @{Detection#DETECTION.New}(): Create a new DETECTION object. --- --- 1.2) DETECTION initialization: --- ------------------------------ --- By default, detection will return detected units with all the methods available. --- However, you can specify which units it found with specific detection methods. --- If you use one of the below functions, the detection will work with the detection method specified. --- You can specify to apply multiple detection methods. --- Use the following functions to report the units it detected using the methods Visual, Optical, Radar, IRST, RWR, DLINK: --- --- * @{Detection#DETECTION.InitDetectVisual}(): Detected using Visual. --- * @{Detection#DETECTION.InitDetectOptical}(): Detected using Optical. --- * @{Detection#DETECTION.InitDetectRadar}(): Detected using Radar. --- * @{Detection#DETECTION.InitDetectIRST}(): Detected using IRST. --- * @{Detection#DETECTION.InitDetectRWR}(): Detected using RWR. --- * @{Detection#DETECTION.InitDetectDLINK}(): Detected using DLINK. --- --- === --- --- @module Detection --- @author Mechanic : Concept & Testing --- @author FlightControl : Design & Programming - - - ---- DETECTION_BASE class --- @type DETECTION_BASE --- @field Group#GROUP FACGroup The GROUP in the Forward Air Controller role. --- @field DCSTypes#Distance DetectionRange The range till which targets are accepted to be detected. --- @field DCSTypes#Distance DetectionZoneRange The range till which targets are grouped upon the first detected target. --- @field #DETECTION_BASE.DetectedUnitSets DetectedUnitSets A list of @{Set#SET_UNIT}s containing the units in each set that were detected within a DetectedZoneRange. --- @field #DETECTION_BASE.DetectedUnitZones DetectedUnitZones A list of @{Zone#ZONE_UNIT}s containing the zones of the reference detected units. --- @extends Set#SET_BASE -DETECTION_BASE = { - ClassName = "DETECTION_BASE", - DetectedUnitSets = {}, - DetectedUnitZones = {}, - DetectedUnits = {}, - FACGroup = nil, - DetectionRange = nil, - DetectionZoneRange = nil, -} - ---- @type DETECTION_BASE.DetectedUnitSets --- @list - - ---- @type DETECTION_BASE.DetectedUnitZones --- @list - - ---- DETECTION constructor. --- @param #DETECTION_BASE self --- @return #DETECTION_BASE self -function DETECTION_BASE:New( FACGroup, DetectionRange, DetectionZoneRange ) - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) - - self.FACGroup = FACGroup - self.DetectionRange = DetectionRange - self.DetectionZoneRange = DetectionZoneRange - - self:InitDetectVisual( false ) - self:InitDetectOptical( false ) - self:InitDetectRadar( false ) - self:InitDetectRWR( false ) - self:InitDetectIRST( false ) - self:InitDetectDLINK( false ) - - self.DetectionScheduler = SCHEDULER:New(self, self._DetectionScheduler, { self, "Detection" }, 10, 30, 0.2 ) - - return self -end - ---- Detect Visual. --- @param #DETECTION_BASE self --- @param #boolean DetectVisual --- @return #DETECTION_BASE self -function DETECTION_BASE:InitDetectVisual( DetectVisual ) - - self.DetectVisual = DetectVisual -end - ---- Detect Optical. --- @param #DETECTION_BASE self --- @param #boolean DetectOptical --- @return #DETECTION_BASE self -function DETECTION_BASE:InitDetectOptical( DetectOptical ) - self:F2() - - self.DetectOptical = DetectOptical -end - ---- Detect Radar. --- @param #DETECTION_BASE self --- @param #boolean DetectRadar --- @return #DETECTION_BASE self -function DETECTION_BASE:InitDetectRadar( DetectRadar ) - self:F2() - - self.DetectRadar = DetectRadar -end - ---- Detect IRST. --- @param #DETECTION_BASE self --- @param #boolean DetectIRST --- @return #DETECTION_BASE self -function DETECTION_BASE:InitDetectIRST( DetectIRST ) - self:F2() - - self.DetectIRST = DetectIRST -end - ---- Detect RWR. --- @param #DETECTION_BASE self --- @param #boolean DetectRWR --- @return #DETECTION_BASE self -function DETECTION_BASE:InitDetectRWR( DetectRWR ) - self:F2() - - self.DetectRWR = DetectRWR -end - ---- Detect DLINK. --- @param #DETECTION_BASE self --- @param #boolean DetectDLINK --- @return #DETECTION_BASE self -function DETECTION_BASE:InitDetectDLINK( DetectDLINK ) - self:F2() - - self.DetectDLINK = DetectDLINK -end - ---- Gets the FAC group. --- @param #DETECTION_BASE self --- @return Group#GROUP self -function DETECTION_BASE:GetFACGroup() - self:F2() - - return self.FACGroup -end - ---- Get the detected @{Set#SET_UNIT}s. --- @param #DETECTION_BASE self --- @return #DETECTION_BASE.DetectedUnitSets DetectedUnitSets -function DETECTION_BASE:GetDetectionUnitSets() - - local DetectionUnitSets = self.DetectedUnitSets - return DetectionUnitSets -end - ---- Get the amount of SETs with detected units. --- @param #DETECTION_BASE self --- @return #number Count -function DETECTION_BASE:GetDetectionUnitSetCount() - - local DetectionUnitSetCount = #self.DetectedUnitSets - return DetectionUnitSetCount -end - ---- Get a SET of detected units using a given numeric index. --- @param #DETECTION_BASE self --- @param #number Index --- @return Set#SET_UNIT -function DETECTION_BASE:GetDetectionUnitSet( Index ) - - local DetectionUnitSet = self.DetectedUnitSets[Index] - if DetectionUnitSet then - return DetectionUnitSet - end - - return nil -end - ---- Form @{Set}s of detected @{Unit#UNIT}s in an array of @{Set#SET_UNIT}s. --- @param #DETECTION_BASE self -function DETECTION_BASE:_DetectionScheduler( SchedulerName ) - self:F2( { SchedulerName } ) - - self.DetectedUnitSets = {} - self.DetectedUnitZones = {} - - if self.FACGroup:IsAlive() then - local FACGroupName = self.FACGroup:GetName() - - local FACDetectedTargets = self.FACGroup:GetDetectedTargets( - self.DetectVisual, - self.DetectOptical, - self.DetectRadar, - self.DetectIRST, - self.DetectRWR, - self.DetectDLINK - ) - - for FACDetectedTargetID, FACDetectedTarget in pairs( FACDetectedTargets ) do - local FACObject = FACDetectedTarget.object - self:T2( FACObject ) - - if FACObject and FACObject:isExist() and FACObject.id_ < 50000000 then - - local FACDetectedUnit = UNIT:Find( FACObject ) - local FACDetectedUnitName = FACDetectedUnit:GetName() - - local FACDetectedUnitPositionVec3 = FACDetectedUnit:GetPointVec3() - local FACGroupPositionVec3 = self.FACGroup:GetPointVec3() - local Distance = ( ( FACDetectedUnitPositionVec3.x - FACGroupPositionVec3.x )^2 + - ( FACDetectedUnitPositionVec3.y - FACGroupPositionVec3.y )^2 + - ( FACDetectedUnitPositionVec3.z - FACGroupPositionVec3.z )^2 - ) ^ 0.5 / 1000 - - self:T( { FACGroupName, FACDetectedUnitName, Distance } ) - - if Distance <= self.DetectionRange then - - if not self.DetectedUnits[FACDetectedUnitName] then - self.DetectedUnits[FACDetectedUnitName] = {} - end - self.DetectedUnits[FACDetectedUnitName].DetectedUnit = UNIT:FindByName( FACDetectedUnitName ) - self.DetectedUnits[FACDetectedUnitName].Visible = FACDetectedTarget.visible - self.DetectedUnits[FACDetectedUnitName].Type = FACDetectedTarget.type - self.DetectedUnits[FACDetectedUnitName].Distance = FACDetectedTarget.distance - else - -- if beyond the DetectionRange then nullify... - if self.DetectedUnits[FACDetectedUnitName] then - self.DetectedUnits[FACDetectedUnitName] = nil - end - end - end - end - - -- okay, now we have a list of detected unit names ... - -- Sort the table based on distance ... - self:T( { "Sorting DetectedUnits table:", self.DetectedUnits } ) - table.sort( self.DetectedUnits, function( a, b ) return a.Distance < b.Distance end ) - self:T( { "Sorted Targets Table:", self.DetectedUnits } ) - - -- Now group the DetectedUnits table into SET_UNITs, evaluating the DetectionZoneRange. - - if self.DetectedUnits then - for DetectedUnitName, DetectedUnitData in pairs( self.DetectedUnits ) do - local DetectedUnit = DetectedUnitData.DetectedUnit -- Unit#UNIT - if DetectedUnit and DetectedUnit:IsAlive() then - self:T( DetectedUnit:GetName() ) - if #self.DetectedUnitSets == 0 then - self:T( { "Adding Unit Set #", 1 } ) - self.DetectedUnitZones[1] = ZONE_UNIT:New( DetectedUnitName, DetectedUnit, self.DetectionZoneRange ) - self.DetectedUnitSets[1] = SET_UNIT:New() - self.DetectedUnitSets[1]:AddUnit( DetectedUnit ) - else - local AddedToSet = false - for DetectedZoneIndex = 1, #self.DetectedUnitZones do - self:T( "Detected Unit Set #" .. DetectedZoneIndex ) - local DetectedUnitSet = self.DetectedUnitSets[DetectedZoneIndex] -- Set#SET_UNIT - DetectedUnitSet:Flush() - local DetectedZone = self.DetectedUnitZones[DetectedZoneIndex] -- Zone#ZONE_UNIT - if DetectedUnit:IsInZone( DetectedZone ) then - self:T( "Adding to Unit Set #" .. DetectedZoneIndex ) - DetectedUnitSet:AddUnit( DetectedUnit ) - AddedToSet = true - end - end - if AddedToSet == false then - local DetectedZoneIndex = #self.DetectedUnitZones + 1 - self:T( "Adding new zone #" .. DetectedZoneIndex ) - self.DetectedUnitZones[DetectedZoneIndex] = ZONE_UNIT:New( DetectedUnitName, DetectedUnit, self.DetectionZoneRange ) - self.DetectedUnitSets[DetectedZoneIndex] = SET_UNIT:New() - self.DetectedUnitSets[DetectedZoneIndex]:AddUnit( DetectedUnit ) - end - end - end - end - end - - -- Now all the tests should have been build, now make some smoke and flares... - - for DetectedZoneIndex = 1, #self.DetectedUnitZones do - local DetectedUnitSet = self.DetectedUnitSets[DetectedZoneIndex] -- Set#SET_UNIT - local DetectedZone = self.DetectedUnitZones[DetectedZoneIndex] -- Zone#ZONE_UNIT - self:T( "Detected Set #" .. DetectedZoneIndex ) - DetectedUnitSet:ForEachUnit( - --- @param Unit#UNIT DetectedUnit - function( DetectedUnit ) - self:T( DetectedUnit:GetName() ) - DetectedUnit:FlareRed() - end - ) - DetectedZone:FlareZone( POINT_VEC3.SmokeColor.White, 30, math.random( 0,90 ) ) - end - end -end--- This module contains the FAC classes. --- --- === --- --- 1) @{Fac#FAC_BASE} class, extends @{Base#BASE} --- ============================================== --- The @{Fac#FAC_BASE} class defines the core functions to report detected objects to: --- --- * CLIENTS --- * COALITIONS --- --- Detected objects are grouped in SETS of UNITS. --- --- 1.1) FAC constructor: --- ---------------------------- --- * @{Fac#FAC.New}(): Create a new FAC object. --- --- 1.2) FAC initialization: --- ------------------------------ --- --- === --- --- @module Fac --- @author Mechanic : Concept & Testing --- @author FlightControl : Design & Programming - - - ---- FAC_BASE class --- @type FAC_BASE --- @field Set#SET_CLIENT ClientSet The clients to which the FAC will report to. --- @field Detection#DETECTION_BASE Detection The DETECTION_BASE object that is used to report the detected objects. --- @extends Set#SET_BASE -FAC_BASE = { - ClassName = "FAC_BASE", - ClientSet = nil, - Detection = nil, -} - ---- FAC constructor. --- @param #FAC_BASE self --- @param Set#SET_CLIENT ClientSet --- @param Detection#DETECTION_BASE Detection --- @return #FAC_BASE self -function FAC_BASE:New( ClientSet, Detection ) - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) - - self.ClientSet = ClientSet - self.Detection = Detection - - self.FacScheduler = SCHEDULER:New(self, self._FacScheduler, { self, "Fac" }, 5, 15 ) - - return self -end - - ---- Report the detected @{Unit#UNIT}s detected within the @{DetectION#DETECTION_BASE} object to the @{Set#SET_CLIENT}s. --- @param #FAC_BASE self -function FAC_BASE:_FacScheduler( SchedulerName ) - self:F2( { SchedulerName } ) - - self.ClientSet:ForEachClient( - --- @param Client#CLIENT Client - function( Client ) - if Client:IsAlive() then - local DetectedUnitSets = self.Detection:GetDetectionUnitSets() - local DetectedMsg = { } - for DetectedUnitSetID, DetectedUnitSet in pairs( DetectedUnitSets ) do - local UnitSet = DetectedUnitSet -- Set#SET_UNIT - local MT = {} -- Message Text - local UnitTypes = {} - for DetectedUnitID, DetectedUnitData in pairs( UnitSet:GetSet() ) do - local DetectedUnit = DetectedUnitData -- Unit#UNIT - local UnitType = DetectedUnit:GetTypeName() - if not UnitTypes[UnitType] then - UnitTypes[UnitType] = 1 - else - UnitTypes[UnitType] = UnitTypes[UnitType] + 1 - end - end - for UnitTypeID, UnitType in pairs( UnitTypes ) do - MT[#MT+1] = UnitType .. " of " .. UnitTypeID - end - local MessageText = table.concat( MT, ", " ) - DetectedMsg[#DetectedMsg+1] = " - Group #" .. DetectedUnitSetID .. ": " .. MessageText - end - local FACGroup = self.Detection:GetFACGroup() - FACGroup:MessageToClient( "Reporting detected target groups:\n" .. table.concat( DetectedMsg, "\n" ), 12, Client ) - end - return true - end - ) - - return true -end -BASE:TraceOnOff( false ) +BASE:TraceOnOff( true ) env.info( '*** MOOSE INCLUDE END *** ' ) diff --git a/Moose Mission Setup/Moose.lua b/Moose Mission Setup/Moose.lua index 923532602..518c25c8a 100644 --- a/Moose Mission Setup/Moose.lua +++ b/Moose Mission Setup/Moose.lua @@ -1,23223 +1,45 @@ -env.info( '*** MOOSE STATIC INCLUDE START *** ' ) -env.info( 'Moose Generation Timestamp: 20160623_1441' ) +env.info( '*** MOOSE DYNAMIC INCLUDE START *** ' ) +env.info( 'Moose Generation Timestamp: 20160624_1231' ) + local base = _G Include = {} -Include.Files = {} + +Include.Path = function() + local str = debug.getinfo(2, "S").source + return str:match("(.*/)"):sub(1,-2):gsub("\\","/") +end + Include.File = function( IncludeFile ) -end - ---- Various routines --- @module routines --- @author Flightcontrol - -env.setErrorMessageBoxEnabled(false) - ---- Extract of MIST functions. --- @author Grimes - -routines = {} - - --- don't change these -routines.majorVersion = 3 -routines.minorVersion = 3 -routines.build = 22 - ------------------------------------------------------------------------------------------------------------------ - ----------------------------------------------------------------------------------------------- --- Utils- conversion, Lua utils, etc. -routines.utils = {} - ---from http://lua-users.org/wiki/CopyTable -routines.utils.deepCopy = function(object) - local lookup_table = {} - local function _copy(object) - if type(object) ~= "table" then - return object - elseif lookup_table[object] then - return lookup_table[object] - end - local new_table = {} - lookup_table[object] = new_table - for index, value in pairs(object) do - new_table[_copy(index)] = _copy(value) - end - return setmetatable(new_table, getmetatable(object)) - end - local objectreturn = _copy(object) - return objectreturn -end - - --- porting in Slmod's serialize_slmod2 -routines.utils.oneLineSerialize = function(tbl) -- serialization of a table all on a single line, no comments, made to replace old get_table_string function - - lookup_table = {} - - local function _Serialize( tbl ) - - if type(tbl) == 'table' then --function only works for tables! - - if lookup_table[tbl] then - return lookup_table[object] - end - - local tbl_str = {} - - lookup_table[tbl] = tbl_str - - tbl_str[#tbl_str + 1] = '{' - - for ind,val in pairs(tbl) do -- serialize its fields - local ind_str = {} - if type(ind) == "number" then - ind_str[#ind_str + 1] = '[' - ind_str[#ind_str + 1] = tostring(ind) - ind_str[#ind_str + 1] = ']=' - else --must be a string - ind_str[#ind_str + 1] = '[' - ind_str[#ind_str + 1] = routines.utils.basicSerialize(ind) - ind_str[#ind_str + 1] = ']=' - end - - local val_str = {} - if ((type(val) == 'number') or (type(val) == 'boolean')) then - val_str[#val_str + 1] = tostring(val) - val_str[#val_str + 1] = ',' - tbl_str[#tbl_str + 1] = table.concat(ind_str) - tbl_str[#tbl_str + 1] = table.concat(val_str) - elseif type(val) == 'string' then - val_str[#val_str + 1] = routines.utils.basicSerialize(val) - val_str[#val_str + 1] = ',' - tbl_str[#tbl_str + 1] = table.concat(ind_str) - tbl_str[#tbl_str + 1] = table.concat(val_str) - elseif type(val) == 'nil' then -- won't ever happen, right? - val_str[#val_str + 1] = 'nil,' - tbl_str[#tbl_str + 1] = table.concat(ind_str) - tbl_str[#tbl_str + 1] = table.concat(val_str) - elseif type(val) == 'table' then - if ind == "__index" then - -- tbl_str[#tbl_str + 1] = "__index" - -- tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it - else - - val_str[#val_str + 1] = _Serialize(val) - val_str[#val_str + 1] = ',' --I think this is right, I just added it - tbl_str[#tbl_str + 1] = table.concat(ind_str) - tbl_str[#tbl_str + 1] = table.concat(val_str) - end - elseif type(val) == 'function' then - -- tbl_str[#tbl_str + 1] = "function " .. tostring(ind) - -- tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it - else --- env.info('unable to serialize value type ' .. routines.utils.basicSerialize(type(val)) .. ' at index ' .. tostring(ind)) --- env.info( debug.traceback() ) - end - - end - tbl_str[#tbl_str + 1] = '}' - return table.concat(tbl_str) - else - return tostring(tbl) - end - end - - local objectreturn = _Serialize(tbl) - return objectreturn -end - ---porting in Slmod's "safestring" basic serialize -routines.utils.basicSerialize = function(s) - if s == nil then - return "\"\"" - else - if ((type(s) == 'number') or (type(s) == 'boolean') or (type(s) == 'function') or (type(s) == 'table') or (type(s) == 'userdata') ) then - return tostring(s) - elseif type(s) == 'string' then - s = string.format('%q', s) - return s - end - end -end - - -routines.utils.toDegree = function(angle) - return angle*180/math.pi -end - -routines.utils.toRadian = function(angle) - return angle*math.pi/180 -end - -routines.utils.metersToNM = function(meters) - return meters/1852 -end - -routines.utils.metersToFeet = function(meters) - return meters/0.3048 -end - -routines.utils.NMToMeters = function(NM) - return NM*1852 -end - -routines.utils.feetToMeters = function(feet) - return feet*0.3048 -end - -routines.utils.mpsToKnots = function(mps) - return mps*3600/1852 -end - -routines.utils.mpsToKmph = function(mps) - return mps*3.6 -end - -routines.utils.knotsToMps = function(knots) - return knots*1852/3600 -end - -routines.utils.kmphToMps = function(kmph) - return kmph/3.6 -end - -function routines.utils.makeVec2(Vec3) - if Vec3.z then - return {x = Vec3.x, y = Vec3.z} - else - return {x = Vec3.x, y = Vec3.y} -- it was actually already vec2. - end -end - -function routines.utils.makeVec3(Vec2, y) - if not Vec2.z then - if not y then - y = 0 - end - return {x = Vec2.x, y = y, z = Vec2.y} - else - return {x = Vec2.x, y = Vec2.y, z = Vec2.z} -- it was already Vec3, actually. - end -end - -function routines.utils.makeVec3GL(Vec2, offset) - local adj = offset or 0 - - if not Vec2.z then - return {x = Vec2.x, y = (land.getHeight(Vec2) + adj), z = Vec2.y} - else - return {x = Vec2.x, y = (land.getHeight({x = Vec2.x, y = Vec2.z}) + adj), z = Vec2.z} - end -end - -routines.utils.zoneToVec3 = function(zone) - local new = {} - if type(zone) == 'table' and zone.point then - new.x = zone.point.x - new.y = zone.point.y - new.z = zone.point.z - return new - elseif type(zone) == 'string' then - zone = trigger.misc.getZone(zone) - if zone then - new.x = zone.point.x - new.y = zone.point.y - new.z = zone.point.z - return new - end - end -end - --- gets heading-error corrected direction from point along vector vec. -function routines.utils.getDir(vec, point) - local dir = math.atan2(vec.z, vec.x) - dir = dir + routines.getNorthCorrection(point) - if dir < 0 then - dir = dir + 2*math.pi -- put dir in range of 0 to 2*pi - end - return dir -end - --- gets distance in meters between two points (2 dimensional) -function routines.utils.get2DDist(point1, point2) - point1 = routines.utils.makeVec3(point1) - point2 = routines.utils.makeVec3(point2) - return routines.vec.mag({x = point1.x - point2.x, y = 0, z = point1.z - point2.z}) -end - --- gets distance in meters between two points (3 dimensional) -function routines.utils.get3DDist(point1, point2) - return routines.vec.mag({x = point1.x - point2.x, y = point1.y - point2.y, z = point1.z - point2.z}) -end - - - --- From http://lua-users.org/wiki/SimpleRound --- use negative idp for rounding ahead of decimal place, positive for rounding after decimal place -routines.utils.round = function(num, idp) - local mult = 10^(idp or 0) - return math.floor(num * mult + 0.5) / mult -end - --- porting in Slmod's dostring -routines.utils.dostring = function(s) - local f, err = loadstring(s) - if f then - return true, f() - else - return false, err - end -end - - ---3D Vector manipulation -routines.vec = {} - -routines.vec.add = function(vec1, vec2) - return {x = vec1.x + vec2.x, y = vec1.y + vec2.y, z = vec1.z + vec2.z} -end - -routines.vec.sub = function(vec1, vec2) - return {x = vec1.x - vec2.x, y = vec1.y - vec2.y, z = vec1.z - vec2.z} -end - -routines.vec.scalarMult = function(vec, mult) - return {x = vec.x*mult, y = vec.y*mult, z = vec.z*mult} -end - -routines.vec.scalar_mult = routines.vec.scalarMult - -routines.vec.dp = function(vec1, vec2) - return vec1.x*vec2.x + vec1.y*vec2.y + vec1.z*vec2.z -end - -routines.vec.cp = function(vec1, vec2) - return { x = vec1.y*vec2.z - vec1.z*vec2.y, y = vec1.z*vec2.x - vec1.x*vec2.z, z = vec1.x*vec2.y - vec1.y*vec2.x} -end - -routines.vec.mag = function(vec) - return (vec.x^2 + vec.y^2 + vec.z^2)^0.5 -end - -routines.vec.getUnitVec = function(vec) - local mag = routines.vec.mag(vec) - return { x = vec.x/mag, y = vec.y/mag, z = vec.z/mag } -end - -routines.vec.rotateVec2 = function(vec2, theta) - return { x = vec2.x*math.cos(theta) - vec2.y*math.sin(theta), y = vec2.x*math.sin(theta) + vec2.y*math.cos(theta)} -end ---------------------------------------------------------------------------------------------------------------------------- - - - - --- acc- the accuracy of each easting/northing. 0, 1, 2, 3, 4, or 5. -routines.tostringMGRS = function(MGRS, acc) - if acc == 0 then - return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph - else - return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph .. ' ' .. string.format('%0' .. acc .. 'd', routines.utils.round(MGRS.Easting/(10^(5-acc)), 0)) - .. ' ' .. string.format('%0' .. acc .. 'd', routines.utils.round(MGRS.Northing/(10^(5-acc)), 0)) - end -end - ---[[acc: -in DM: decimal point of minutes. -In DMS: decimal point of seconds. -position after the decimal of the least significant digit: -So: -42.32 - acc of 2. -]] -routines.tostringLL = function(lat, lon, acc, DMS) - - local latHemi, lonHemi - if lat > 0 then - latHemi = 'N' - else - latHemi = 'S' - end - - if lon > 0 then - lonHemi = 'E' - else - lonHemi = 'W' - end - - lat = math.abs(lat) - lon = math.abs(lon) - - local latDeg = math.floor(lat) - local latMin = (lat - latDeg)*60 - - local lonDeg = math.floor(lon) - local lonMin = (lon - lonDeg)*60 - - if DMS then -- degrees, minutes, and seconds. - local oldLatMin = latMin - latMin = math.floor(latMin) - local latSec = routines.utils.round((oldLatMin - latMin)*60, acc) - - local oldLonMin = lonMin - lonMin = math.floor(lonMin) - local lonSec = routines.utils.round((oldLonMin - lonMin)*60, acc) - - if latSec == 60 then - latSec = 0 - latMin = latMin + 1 - end - - if lonSec == 60 then - lonSec = 0 - lonMin = lonMin + 1 - end - - local secFrmtStr -- create the formatting string for the seconds place - if acc <= 0 then -- no decimal place. - secFrmtStr = '%02d' - else - local width = 3 + acc -- 01.310 - that's a width of 6, for example. - secFrmtStr = '%0' .. width .. '.' .. acc .. 'f' - end - - return string.format('%02d', latDeg) .. ' ' .. string.format('%02d', latMin) .. '\' ' .. string.format(secFrmtStr, latSec) .. '"' .. latHemi .. ' ' - .. string.format('%02d', lonDeg) .. ' ' .. string.format('%02d', lonMin) .. '\' ' .. string.format(secFrmtStr, lonSec) .. '"' .. lonHemi - - else -- degrees, decimal minutes. - latMin = routines.utils.round(latMin, acc) - lonMin = routines.utils.round(lonMin, acc) - - if latMin == 60 then - latMin = 0 - latDeg = latDeg + 1 - end - - if lonMin == 60 then - lonMin = 0 - lonDeg = lonDeg + 1 - end - - local minFrmtStr -- create the formatting string for the minutes place - if acc <= 0 then -- no decimal place. - minFrmtStr = '%02d' - else - local width = 3 + acc -- 01.310 - that's a width of 6, for example. - minFrmtStr = '%0' .. width .. '.' .. acc .. 'f' - end - - return string.format('%02d', latDeg) .. ' ' .. string.format(minFrmtStr, latMin) .. '\'' .. latHemi .. ' ' - .. string.format('%02d', lonDeg) .. ' ' .. string.format(minFrmtStr, lonMin) .. '\'' .. lonHemi - - end -end - ---[[ required: az - radian - required: dist - meters - optional: alt - meters (set to false or nil if you don't want to use it). - optional: metric - set true to get dist and alt in km and m. - precision will always be nearest degree and NM or km.]] -routines.tostringBR = function(az, dist, alt, metric) - az = routines.utils.round(routines.utils.toDegree(az), 0) - - if metric then - dist = routines.utils.round(dist/1000, 2) - else - dist = routines.utils.round(routines.utils.metersToNM(dist), 2) - end - - local s = string.format('%03d', az) .. ' for ' .. dist - - if alt then - if metric then - s = s .. ' at ' .. routines.utils.round(alt, 0) - else - s = s .. ' at ' .. routines.utils.round(routines.utils.metersToFeet(alt), 0) - end - end - return s -end - -routines.getNorthCorrection = function(point) --gets the correction needed for true north - if not point.z then --Vec2; convert to Vec3 - point.z = point.y - point.y = 0 - end - local lat, lon = coord.LOtoLL(point) - local north_posit = coord.LLtoLO(lat + 1, lon) - return math.atan2(north_posit.z - point.z, north_posit.x - point.x) -end - - --- the main area -do - -- THE MAIN FUNCTION -- Accessed 100 times/sec. - routines.main = function() - timer.scheduleFunction(routines.main, {}, timer.getTime() + 2) --reschedule first in case of Lua error - ---------------------------------------------------------------------------------------------------------- - --area to add new stuff in - - routines.do_scheduled_functions() - end -- end of routines.main - - timer.scheduleFunction(routines.main, {}, timer.getTime() + 2) - -end - - -do - local idNum = 0 - - --Simplified event handler - routines.addEventHandler = function(f) --id is optional! - local handler = {} - idNum = idNum + 1 - handler.id = idNum - handler.f = f - handler.onEvent = function(self, event) - self.f(event) - end - world.addEventHandler(handler) - end - - routines.removeEventHandler = function(id) - for key, handler in pairs(world.eventHandlers) do - if handler.id and handler.id == id then - world.eventHandlers[key] = nil - return true - end - end - return false - end -end - --- need to return a Vec3 or Vec2? -function routines.getRandPointInCircle(point, radius, innerRadius) - local theta = 2*math.pi*math.random() - local rad = math.random() + math.random() - if rad > 1 then - rad = 2 - rad - end - - local radMult - if innerRadius and innerRadius <= radius then - radMult = (radius - innerRadius)*rad + innerRadius - else - radMult = radius*rad - end - - if not point.z then --might as well work with vec2/3 - point.z = point.y - end - - local rndCoord - if radius > 0 then - rndCoord = {x = math.cos(theta)*radMult + point.x, y = math.sin(theta)*radMult + point.z} - else - rndCoord = {x = point.x, y = point.z} - end - return rndCoord -end - -routines.goRoute = function(group, path) - local misTask = { - id = 'Mission', - params = { - route = { - points = routines.utils.deepCopy(path), - }, - }, - } - if type(group) == 'string' then - group = Group.getByName(group) - end - local groupCon = group:getController() - if groupCon then - groupCon:setTask(misTask) - return true - end - - Controller.setTask(groupCon, misTask) - return false -end - - --- Useful atomic functions from mist, ported. - -routines.ground = {} -routines.fixedWing = {} -routines.heli = {} - -routines.ground.buildWP = function(point, overRideForm, overRideSpeed) - - local wp = {} - wp.x = point.x - - if point.z then - wp.y = point.z - else - wp.y = point.y - end - local form, speed - - if point.speed and not overRideSpeed then - wp.speed = point.speed - elseif type(overRideSpeed) == 'number' then - wp.speed = overRideSpeed - else - wp.speed = routines.utils.kmphToMps(20) - end - - if point.form and not overRideForm then - form = point.form - else - form = overRideForm - end - - if not form then - wp.action = 'Cone' - else - form = string.lower(form) - if form == 'off_road' or form == 'off road' then - wp.action = 'Off Road' - elseif form == 'on_road' or form == 'on road' then - wp.action = 'On Road' - elseif form == 'rank' or form == 'line_abrest' or form == 'line abrest' or form == 'lineabrest'then - wp.action = 'Rank' - elseif form == 'cone' then - wp.action = 'Cone' - elseif form == 'diamond' then - wp.action = 'Diamond' - elseif form == 'vee' then - wp.action = 'Vee' - elseif form == 'echelon_left' or form == 'echelon left' or form == 'echelonl' then - wp.action = 'EchelonL' - elseif form == 'echelon_right' or form == 'echelon right' or form == 'echelonr' then - wp.action = 'EchelonR' - else - wp.action = 'Cone' -- if nothing matched - end - end - - wp.type = 'Turning Point' - - return wp - -end - -routines.fixedWing.buildWP = function(point, WPtype, speed, alt, altType) - - local wp = {} - wp.x = point.x - - if point.z then - wp.y = point.z - else - wp.y = point.y - end - - if alt and type(alt) == 'number' then - wp.alt = alt - else - wp.alt = 2000 - end - - if altType then - altType = string.lower(altType) - if altType == 'radio' or 'agl' then - wp.alt_type = 'RADIO' - elseif altType == 'baro' or 'asl' then - wp.alt_type = 'BARO' - end - else - wp.alt_type = 'RADIO' - end - - if point.speed then - speed = point.speed - end - - if point.type then - WPtype = point.type - end - - if not speed then - wp.speed = routines.utils.kmphToMps(500) - else - wp.speed = speed - end - - if not WPtype then - wp.action = 'Turning Point' - else - WPtype = string.lower(WPtype) - if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then - wp.action = 'Fly Over Point' - elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then - wp.action = 'Turning Point' - else - wp.action = 'Turning Point' - end - end - - wp.type = 'Turning Point' - return wp -end - -routines.heli.buildWP = function(point, WPtype, speed, alt, altType) - - local wp = {} - wp.x = point.x - - if point.z then - wp.y = point.z - else - wp.y = point.y - end - - if alt and type(alt) == 'number' then - wp.alt = alt - else - wp.alt = 500 - end - - if altType then - altType = string.lower(altType) - if altType == 'radio' or 'agl' then - wp.alt_type = 'RADIO' - elseif altType == 'baro' or 'asl' then - wp.alt_type = 'BARO' - end - else - wp.alt_type = 'RADIO' - end - - if point.speed then - speed = point.speed - end - - if point.type then - WPtype = point.type - end - - if not speed then - wp.speed = routines.utils.kmphToMps(200) - else - wp.speed = speed - end - - if not WPtype then - wp.action = 'Turning Point' - else - WPtype = string.lower(WPtype) - if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then - wp.action = 'Fly Over Point' - elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then - wp.action = 'Turning Point' - else - wp.action = 'Turning Point' - end - end - - wp.type = 'Turning Point' - return wp -end - -routines.groupToRandomPoint = function(vars) - local group = vars.group --Required - local point = vars.point --required - local radius = vars.radius or 0 - local innerRadius = vars.innerRadius - local form = vars.form or 'Cone' - local heading = vars.heading or math.random()*2*math.pi - local headingDegrees = vars.headingDegrees - local speed = vars.speed or routines.utils.kmphToMps(20) - - - local useRoads - if not vars.disableRoads then - useRoads = true - else - useRoads = false - end - - local path = {} - - if headingDegrees then - heading = headingDegrees*math.pi/180 - end - - if heading >= 2*math.pi then - heading = heading - 2*math.pi - end - - local rndCoord = routines.getRandPointInCircle(point, radius, innerRadius) - - local offset = {} - local posStart = routines.getLeadPos(group) - - offset.x = routines.utils.round(math.sin(heading - (math.pi/2)) * 50 + rndCoord.x, 3) - offset.z = routines.utils.round(math.cos(heading + (math.pi/2)) * 50 + rndCoord.y, 3) - path[#path + 1] = routines.ground.buildWP(posStart, form, speed) - - - if useRoads == true and ((point.x - posStart.x)^2 + (point.z - posStart.z)^2)^0.5 > radius * 1.3 then - path[#path + 1] = routines.ground.buildWP({['x'] = posStart.x + 11, ['z'] = posStart.z + 11}, 'off_road', speed) - path[#path + 1] = routines.ground.buildWP(posStart, 'on_road', speed) - path[#path + 1] = routines.ground.buildWP(offset, 'on_road', speed) - else - path[#path + 1] = routines.ground.buildWP({['x'] = posStart.x + 25, ['z'] = posStart.z + 25}, form, speed) - end - - path[#path + 1] = routines.ground.buildWP(offset, form, speed) - path[#path + 1] = routines.ground.buildWP(rndCoord, form, speed) - - routines.goRoute(group, path) - - return -end - -routines.groupRandomDistSelf = function(gpData, dist, form, heading, speed) - local pos = routines.getLeadPos(gpData) - local fakeZone = {} - fakeZone.radius = dist or math.random(300, 1000) - fakeZone.point = {x = pos.x, y, pos.y, z = pos.z} - routines.groupToRandomZone(gpData, fakeZone, form, heading, speed) - - return -end - -routines.groupToRandomZone = function(gpData, zone, form, heading, speed) - if type(gpData) == 'string' then - gpData = Group.getByName(gpData) - end - - if type(zone) == 'string' then - zone = trigger.misc.getZone(zone) - elseif type(zone) == 'table' and not zone.radius then - zone = trigger.misc.getZone(zone[math.random(1, #zone)]) - end - - if speed then - speed = routines.utils.kmphToMps(speed) - end - - local vars = {} - vars.group = gpData - vars.radius = zone.radius - vars.form = form - vars.headingDegrees = heading - vars.speed = speed - vars.point = routines.utils.zoneToVec3(zone) - - routines.groupToRandomPoint(vars) - - return -end - -routines.isTerrainValid = function(coord, terrainTypes) -- vec2/3 and enum or table of acceptable terrain types - if coord.z then - coord.y = coord.z - end - local typeConverted = {} - - if type(terrainTypes) == 'string' then -- if its a string it does this check - for constId, constData in pairs(land.SurfaceType) do - if string.lower(constId) == string.lower(terrainTypes) or string.lower(constData) == string.lower(terrainTypes) then - table.insert(typeConverted, constId) - end - end - elseif type(terrainTypes) == 'table' then -- if its a table it does this check - for typeId, typeData in pairs(terrainTypes) do - for constId, constData in pairs(land.SurfaceType) do - if string.lower(constId) == string.lower(typeData) or string.lower(constData) == string.lower(typeId) then - table.insert(typeConverted, constId) - end - end - end - end - for validIndex, validData in pairs(typeConverted) do - if land.getSurfaceType(coord) == land.SurfaceType[validData] then - return true - end - end - return false -end - -routines.groupToPoint = function(gpData, point, form, heading, speed, useRoads) - if type(point) == 'string' then - point = trigger.misc.getZone(point) - end - if speed then - speed = routines.utils.kmphToMps(speed) - end - - local vars = {} - vars.group = gpData - vars.form = form - vars.headingDegrees = heading - vars.speed = speed - vars.disableRoads = useRoads - vars.point = routines.utils.zoneToVec3(point) - routines.groupToRandomPoint(vars) - - return -end - - -routines.getLeadPos = function(group) - if type(group) == 'string' then -- group name - group = Group.getByName(group) - end - - local units = group:getUnits() - - local leader = units[1] - if not leader then -- SHOULD be good, but if there is a bug, this code future-proofs it then. - local lowestInd = math.huge - for ind, unit in pairs(units) do - if ind < lowestInd then - lowestInd = ind - leader = unit - end - end - end - if leader and Unit.isExist(leader) then -- maybe a little too paranoid now... - return leader:getPosition().p - end -end - ---[[ vars for routines.getMGRSString: -vars.units - table of unit names (NOT unitNameTable- maybe this should change). -vars.acc - integer between 0 and 5, inclusive -]] -routines.getMGRSString = function(vars) - local units = vars.units - local acc = vars.acc or 5 - local avgPos = routines.getAvgPos(units) - if avgPos then - return routines.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(avgPos)), acc) - end -end - ---[[ vars for routines.getLLString -vars.units - table of unit names (NOT unitNameTable- maybe this should change). -vars.acc - integer, number of numbers after decimal place -vars.DMS - if true, output in degrees, minutes, seconds. Otherwise, output in degrees, minutes. - - -]] -routines.getLLString = function(vars) - local units = vars.units - local acc = vars.acc or 3 - local DMS = vars.DMS - local avgPos = routines.getAvgPos(units) - if avgPos then - local lat, lon = coord.LOtoLL(avgPos) - return routines.tostringLL(lat, lon, acc, DMS) - end -end - ---[[ -vars.zone - table of a zone name. -vars.ref - vec3 ref point, maybe overload for vec2 as well? -vars.alt - boolean, if used, includes altitude in string -vars.metric - boolean, gives distance in km instead of NM. -]] -routines.getBRStringZone = function(vars) - local zone = trigger.misc.getZone( vars.zone ) - local ref = routines.utils.makeVec3(vars.ref, 0) -- turn it into Vec3 if it is not already. - local alt = vars.alt - local metric = vars.metric - if zone then - local vec = {x = zone.point.x - ref.x, y = zone.point.y - ref.y, z = zone.point.z - ref.z} - local dir = routines.utils.getDir(vec, ref) - local dist = routines.utils.get2DDist(zone.point, ref) - if alt then - alt = zone.y - end - return routines.tostringBR(dir, dist, alt, metric) - else - env.info( 'routines.getBRStringZone: error: zone is nil' ) - end -end - ---[[ -vars.units- table of unit names (NOT unitNameTable- maybe this should change). -vars.ref - vec3 ref point, maybe overload for vec2 as well? -vars.alt - boolean, if used, includes altitude in string -vars.metric - boolean, gives distance in km instead of NM. -]] -routines.getBRString = function(vars) - local units = vars.units - local ref = routines.utils.makeVec3(vars.ref, 0) -- turn it into Vec3 if it is not already. - local alt = vars.alt - local metric = vars.metric - local avgPos = routines.getAvgPos(units) - if avgPos then - local vec = {x = avgPos.x - ref.x, y = avgPos.y - ref.y, z = avgPos.z - ref.z} - local dir = routines.utils.getDir(vec, ref) - local dist = routines.utils.get2DDist(avgPos, ref) - if alt then - alt = avgPos.y - end - return routines.tostringBR(dir, dist, alt, metric) - end -end - - --- Returns the Vec3 coordinates of the average position of the concentration of units most in the heading direction. ---[[ vars for routines.getLeadingPos: -vars.units - table of unit names -vars.heading - direction -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees -]] -routines.getLeadingPos = function(vars) - local units = vars.units - local heading = vars.heading - local radius = vars.radius - if vars.headingDegrees then - heading = routines.utils.toRadian(vars.headingDegrees) - end - - local unitPosTbl = {} - for i = 1, #units do - local unit = Unit.getByName(units[i]) - if unit and unit:isExist() then - unitPosTbl[#unitPosTbl + 1] = unit:getPosition().p - end - end - if #unitPosTbl > 0 then -- one more more units found. - -- first, find the unit most in the heading direction - local maxPos = -math.huge - - local maxPosInd -- maxPos - the furthest in direction defined by heading; maxPosInd = - for i = 1, #unitPosTbl do - local rotatedVec2 = routines.vec.rotateVec2(routines.utils.makeVec2(unitPosTbl[i]), heading) - if (not maxPos) or maxPos < rotatedVec2.x then - maxPos = rotatedVec2.x - maxPosInd = i - end - end - - --now, get all the units around this unit... - local avgPos - if radius then - local maxUnitPos = unitPosTbl[maxPosInd] - local avgx, avgy, avgz, totNum = 0, 0, 0, 0 - for i = 1, #unitPosTbl do - if routines.utils.get2DDist(maxUnitPos, unitPosTbl[i]) <= radius then - avgx = avgx + unitPosTbl[i].x - avgy = avgy + unitPosTbl[i].y - avgz = avgz + unitPosTbl[i].z - totNum = totNum + 1 - end - end - avgPos = { x = avgx/totNum, y = avgy/totNum, z = avgz/totNum} - else - avgPos = unitPosTbl[maxPosInd] - end - - return avgPos - end -end - - ---[[ vars for routines.getLeadingMGRSString: -vars.units - table of unit names -vars.heading - direction -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees -vars.acc - number, 0 to 5. -]] -routines.getLeadingMGRSString = function(vars) - local pos = routines.getLeadingPos(vars) - if pos then - local acc = vars.acc or 5 - return routines.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(pos)), acc) - end -end - ---[[ vars for routines.getLeadingLLString: -vars.units - table of unit names -vars.heading - direction, number -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees -vars.acc - number of digits after decimal point (can be negative) -vars.DMS - boolean, true if you want DMS. -]] -routines.getLeadingLLString = function(vars) - local pos = routines.getLeadingPos(vars) - if pos then - local acc = vars.acc or 3 - local DMS = vars.DMS - local lat, lon = coord.LOtoLL(pos) - return routines.tostringLL(lat, lon, acc, DMS) - end -end - - - ---[[ vars for routines.getLeadingBRString: -vars.units - table of unit names -vars.heading - direction, number -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees -vars.metric - boolean, if true, use km instead of NM. -vars.alt - boolean, if true, include altitude. -vars.ref - vec3/vec2 reference point. -]] -routines.getLeadingBRString = function(vars) - local pos = routines.getLeadingPos(vars) - if pos then - local ref = vars.ref - local alt = vars.alt - local metric = vars.metric - - local vec = {x = pos.x - ref.x, y = pos.y - ref.y, z = pos.z - ref.z} - local dir = routines.utils.getDir(vec, ref) - local dist = routines.utils.get2DDist(pos, ref) - if alt then - alt = pos.y - end - return routines.tostringBR(dir, dist, alt, metric) - end -end - ---[[ vars for routines.message.add - vars.text = 'Hello World' - vars.displayTime = 20 - vars.msgFor = {coa = {'red'}, countries = {'Ukraine', 'Georgia'}, unitTypes = {'A-10C'}} - -]] - ---[[ vars for routines.msgMGRS -vars.units - table of unit names (NOT unitNameTable- maybe this should change). -vars.acc - integer between 0 and 5, inclusive -vars.text - text in the message -vars.displayTime - self explanatory -vars.msgFor - scope -]] -routines.msgMGRS = function(vars) - local units = vars.units - local acc = vars.acc - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getMGRSString{units = units, acc = acc} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } -end - ---[[ vars for routines.msgLL -vars.units - table of unit names (NOT unitNameTable- maybe this should change) (Yes). -vars.acc - integer, number of numbers after decimal place -vars.DMS - if true, output in degrees, minutes, seconds. Otherwise, output in degrees, minutes. -vars.text - text in the message -vars.displayTime - self explanatory -vars.msgFor - scope -]] -routines.msgLL = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local acc = vars.acc - local DMS = vars.DMS - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getLLString{units = units, acc = acc, DMS = DMS} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } - -end - - ---[[ -vars.units- table of unit names (NOT unitNameTable- maybe this should change). -vars.ref - vec3 ref point, maybe overload for vec2 as well? -vars.alt - boolean, if used, includes altitude in string -vars.metric - boolean, gives distance in km instead of NM. -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] -routines.msgBR = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local ref = vars.ref -- vec2/vec3 will be handled in routines.getBRString - local alt = vars.alt - local metric = vars.metric - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getBRString{units = units, ref = ref, alt = alt, metric = metric} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } - -end - - --------------------------------------------------------------------------------------------- --- basically, just sub-types of routines.msgBR... saves folks the work of getting the ref point. ---[[ -vars.units- table of unit names (NOT unitNameTable- maybe this should change). -vars.ref - string red, blue -vars.alt - boolean, if used, includes altitude in string -vars.metric - boolean, gives distance in km instead of NM. -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] -routines.msgBullseye = function(vars) - if string.lower(vars.ref) == 'red' then - vars.ref = routines.DBs.missionData.bullseye.red - routines.msgBR(vars) - elseif string.lower(vars.ref) == 'blue' then - vars.ref = routines.DBs.missionData.bullseye.blue - routines.msgBR(vars) - end -end - ---[[ -vars.units- table of unit names (NOT unitNameTable- maybe this should change). -vars.ref - unit name of reference point -vars.alt - boolean, if used, includes altitude in string -vars.metric - boolean, gives distance in km instead of NM. -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] - -routines.msgBRA = function(vars) - if Unit.getByName(vars.ref) then - vars.ref = Unit.getByName(vars.ref):getPosition().p - if not vars.alt then - vars.alt = true - end - routines.msgBR(vars) - end -end --------------------------------------------------------------------------------------------- - ---[[ vars for routines.msgLeadingMGRS: -vars.units - table of unit names -vars.heading - direction -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees (optional) -vars.acc - number, 0 to 5. -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] -routines.msgLeadingMGRS = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local heading = vars.heading - local radius = vars.radius - local headingDegrees = vars.headingDegrees - local acc = vars.acc - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getLeadingMGRSString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } - - -end ---[[ vars for routines.msgLeadingLL: -vars.units - table of unit names -vars.heading - direction, number -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees (optional) -vars.acc - number of digits after decimal point (can be negative) -vars.DMS - boolean, true if you want DMS. (optional) -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] -routines.msgLeadingLL = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local heading = vars.heading - local radius = vars.radius - local headingDegrees = vars.headingDegrees - local acc = vars.acc - local DMS = vars.DMS - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getLeadingLLString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc, DMS = DMS} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } - -end - ---[[ -vars.units - table of unit names -vars.heading - direction, number -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees (optional) -vars.metric - boolean, if true, use km instead of NM. (optional) -vars.alt - boolean, if true, include altitude. (optional) -vars.ref - vec3/vec2 reference point. -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] -routines.msgLeadingBR = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local heading = vars.heading - local radius = vars.radius - local headingDegrees = vars.headingDegrees - local metric = vars.metric - local alt = vars.alt - local ref = vars.ref -- vec2/vec3 will be handled in routines.getBRString - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getLeadingBRString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, metric = metric, alt = alt, ref = ref} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } -end - - -function spairs(t, order) - -- collect the keys - local keys = {} - for k in pairs(t) do keys[#keys+1] = k end - - -- if order function given, sort by it by passing the table and keys a, b, - -- otherwise just sort the keys - if order then - table.sort(keys, function(a,b) return order(t, a, b) end) - else - table.sort(keys) - end - - -- return the iterator function - local i = 0 - return function() - i = i + 1 - if keys[i] then - return keys[i], t[keys[i]] - end - end -end - - -function routines.IsPartOfGroupInZones( CargoGroup, LandingZones ) ---trace.f() - - local CurrentZoneID = nil - - if CargoGroup then - local CargoUnits = CargoGroup:getUnits() - for CargoUnitID, CargoUnit in pairs( CargoUnits ) do - if CargoUnit and CargoUnit:getLife() >= 1.0 then - CurrentZoneID = routines.IsUnitInZones( CargoUnit, LandingZones ) - if CurrentZoneID then - break - end - end - end - end - ---trace.r( "", "", { CurrentZoneID } ) - return CurrentZoneID -end - - - -function routines.IsUnitInZones( TransportUnit, LandingZones ) ---trace.f("", "routines.IsUnitInZones" ) - - local TransportZoneResult = nil - local TransportZonePos = nil - local TransportZone = nil - - -- fill-up some local variables to support further calculations to determine location of units within the zone. - if TransportUnit then - local TransportUnitPos = TransportUnit:getPosition().p - if type( LandingZones ) == "table" then - for LandingZoneID, LandingZoneName in pairs( LandingZones ) do - TransportZone = trigger.misc.getZone( LandingZoneName ) - if TransportZone then - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then - TransportZoneResult = LandingZoneID - break - end - end - end - else - TransportZone = trigger.misc.getZone( LandingZones ) - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then - TransportZoneResult = 1 - end - end - if TransportZoneResult then - --trace.i( "routines", "TransportZone:" .. TransportZoneResult ) - else - --trace.i( "routines", "TransportZone:nil logic" ) - end - return TransportZoneResult - else - --trace.i( "routines", "TransportZone:nil hard" ) - return nil - end -end - -function routines.IsUnitNearZonesRadius( TransportUnit, LandingZones, ZoneRadius ) ---trace.f("", "routines.IsUnitInZones" ) - - local TransportZoneResult = nil - local TransportZonePos = nil - local TransportZone = nil - - -- fill-up some local variables to support further calculations to determine location of units within the zone. - if TransportUnit then - local TransportUnitPos = TransportUnit:getPosition().p - if type( LandingZones ) == "table" then - for LandingZoneID, LandingZoneName in pairs( LandingZones ) do - TransportZone = trigger.misc.getZone( LandingZoneName ) - if TransportZone then - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= ZoneRadius ) then - TransportZoneResult = LandingZoneID - break - end - end - end - else - TransportZone = trigger.misc.getZone( LandingZones ) - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= ZoneRadius ) then - TransportZoneResult = 1 - end - end - if TransportZoneResult then - --trace.i( "routines", "TransportZone:" .. TransportZoneResult ) - else - --trace.i( "routines", "TransportZone:nil logic" ) - end - return TransportZoneResult - else - --trace.i( "routines", "TransportZone:nil hard" ) - return nil - end -end - - -function routines.IsStaticInZones( TransportStatic, LandingZones ) ---trace.f() - - local TransportZoneResult = nil - local TransportZonePos = nil - local TransportZone = nil - - -- fill-up some local variables to support further calculations to determine location of units within the zone. - local TransportStaticPos = TransportStatic:getPosition().p - if type( LandingZones ) == "table" then - for LandingZoneID, LandingZoneName in pairs( LandingZones ) do - TransportZone = trigger.misc.getZone( LandingZoneName ) - if TransportZone then - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportStaticPos.x - TransportZonePos.x)^2 + (TransportStaticPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then - TransportZoneResult = LandingZoneID - break - end - end - end - else - TransportZone = trigger.misc.getZone( LandingZones ) - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportStaticPos.x - TransportZonePos.x)^2 + (TransportStaticPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then - TransportZoneResult = 1 - end - end - ---trace.r( "", "", { TransportZoneResult } ) - return TransportZoneResult -end - - -function routines.IsUnitInRadius( CargoUnit, ReferencePosition, Radius ) ---trace.f() - - local Valid = true - - -- fill-up some local variables to support further calculations to determine location of units within the zone. - local CargoPos = CargoUnit:getPosition().p - local ReferenceP = ReferencePosition.p - - if (((CargoPos.x - ReferenceP.x)^2 + (CargoPos.z - ReferenceP.z)^2)^0.5 <= Radius) then - else - Valid = false - end - - return Valid -end - -function routines.IsPartOfGroupInRadius( CargoGroup, ReferencePosition, Radius ) ---trace.f() - - local Valid = true - - Valid = routines.ValidateGroup( CargoGroup, "CargoGroup", Valid ) - - -- fill-up some local variables to support further calculations to determine location of units within the zone - local CargoUnits = CargoGroup:getUnits() - for CargoUnitId, CargoUnit in pairs( CargoUnits ) do - local CargoUnitPos = CargoUnit:getPosition().p --- env.info( 'routines.IsPartOfGroupInRadius: CargoUnitPos.x = ' .. CargoUnitPos.x .. ' CargoUnitPos.z = ' .. CargoUnitPos.z ) - local ReferenceP = ReferencePosition.p --- env.info( 'routines.IsPartOfGroupInRadius: ReferenceGroupPos.x = ' .. ReferenceGroupPos.x .. ' ReferenceGroupPos.z = ' .. ReferenceGroupPos.z ) - - if ((( CargoUnitPos.x - ReferenceP.x)^2 + (CargoUnitPos.z - ReferenceP.z)^2)^0.5 <= Radius) then - else - Valid = false - break - end - end - - return Valid -end - - -function routines.ValidateString( Variable, VariableName, Valid ) ---trace.f() - - if type( Variable ) == "string" then - if Variable == "" then - error( "routines.ValidateString: error: " .. VariableName .. " must be filled out!" ) - Valid = false - end - else - error( "routines.ValidateString: error: " .. VariableName .. " is not a string." ) - Valid = false - end - ---trace.r( "", "", { Valid } ) - return Valid -end - -function routines.ValidateNumber( Variable, VariableName, Valid ) ---trace.f() - - if type( Variable ) == "number" then - else - error( "routines.ValidateNumber: error: " .. VariableName .. " is not a number." ) - Valid = false - end - ---trace.r( "", "", { Valid } ) - return Valid - -end - -function routines.ValidateGroup( Variable, VariableName, Valid ) ---trace.f() - - if Variable == nil then - error( "routines.ValidateGroup: error: " .. VariableName .. " is a nil value!" ) - Valid = false - end - ---trace.r( "", "", { Valid } ) - return Valid -end - -function routines.ValidateZone( LandingZones, VariableName, Valid ) ---trace.f() - - if LandingZones == nil then - error( "routines.ValidateGroup: error: " .. VariableName .. " is a nil value!" ) - Valid = false - end - - if type( LandingZones ) == "table" then - for LandingZoneID, LandingZoneName in pairs( LandingZones ) do - if trigger.misc.getZone( LandingZoneName ) == nil then - error( "routines.ValidateGroup: error: Zone " .. LandingZoneName .. " does not exist!" ) - Valid = false - break - end - end - else - if trigger.misc.getZone( LandingZones ) == nil then - error( "routines.ValidateGroup: error: Zone " .. LandingZones .. " does not exist!" ) - Valid = false - end - end - ---trace.r( "", "", { Valid } ) - return Valid -end - -function routines.ValidateEnumeration( Variable, VariableName, Enum, Valid ) ---trace.f() - - local ValidVariable = false - - for EnumId, EnumData in pairs( Enum ) do - if Variable == EnumData then - ValidVariable = true - break - end - end - - if ValidVariable then - else - error( 'TransportValidateEnum: " .. VariableName .. " is not a valid type.' .. Variable ) - Valid = false - end - ---trace.r( "", "", { Valid } ) - return Valid -end - -function routines.getGroupRoute(groupIdent, task) -- same as getGroupPoints but returns speed and formation type along with vec2 of point} - -- refactor to search by groupId and allow groupId and groupName as inputs - local gpId = groupIdent - if type(groupIdent) == 'string' and not tonumber(groupIdent) then - gpId = _DATABASE.Templates.Groups[groupIdent].groupId - end - - for coa_name, coa_data in pairs(env.mission.coalition) do - if (coa_name == 'red' or coa_name == 'blue') and type(coa_data) == 'table' then - if coa_data.country then --there is a country table - for cntry_id, cntry_data in pairs(coa_data.country) do - for obj_type_name, obj_type_data in pairs(cntry_data) do - if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" then -- only these types have points - if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! - for group_num, group_data in pairs(obj_type_data.group) do - if group_data and group_data.groupId == gpId then -- this is the group we are looking for - if group_data.route and group_data.route.points and #group_data.route.points > 0 then - local points = {} - - for point_num, point in pairs(group_data.route.points) do - local routeData = {} - if not point.point then - routeData.x = point.x - routeData.y = point.y - else - routeData.point = point.point --it's possible that the ME could move to the point = Vec2 notation. - end - routeData.form = point.action - routeData.speed = point.speed - routeData.alt = point.alt - routeData.alt_type = point.alt_type - routeData.airdromeId = point.airdromeId - routeData.helipadId = point.helipadId - routeData.type = point.type - routeData.action = point.action - if task then - routeData.task = point.task - end - points[point_num] = routeData - end - - return points - end - return - end --if group_data and group_data.name and group_data.name == 'groupname' - end --for group_num, group_data in pairs(obj_type_data.group) do - end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then - end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then - end --for obj_type_name, obj_type_data in pairs(cntry_data) do - end --for cntry_id, cntry_data in pairs(coa_data.country) do - end --if coa_data.country then --there is a country table - end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then - end --for coa_name, coa_data in pairs(mission.coalition) do -end - -routines.ground.patrolRoute = function(vars) - - - local tempRoute = {} - local useRoute = {} - local gpData = vars.gpData - if type(gpData) == 'string' then - gpData = Group.getByName(gpData) - end - - local useGroupRoute - if not vars.useGroupRoute then - useGroupRoute = vars.gpData - else - useGroupRoute = vars.useGroupRoute - end - local routeProvided = false - if not vars.route then - if useGroupRoute then - tempRoute = routines.getGroupRoute(useGroupRoute) - end - else - useRoute = vars.route - local posStart = routines.getLeadPos(gpData) - useRoute[1] = routines.ground.buildWP(posStart, useRoute[1].action, useRoute[1].speed) - routeProvided = true - end - - - local overRideSpeed = vars.speed or 'default' - local pType = vars.pType - local offRoadForm = vars.offRoadForm or 'default' - local onRoadForm = vars.onRoadForm or 'default' - - if routeProvided == false and #tempRoute > 0 then - local posStart = routines.getLeadPos(gpData) - - - useRoute[#useRoute + 1] = routines.ground.buildWP(posStart, offRoadForm, overRideSpeed) - for i = 1, #tempRoute do - local tempForm = tempRoute[i].action - local tempSpeed = tempRoute[i].speed - - if offRoadForm == 'default' then - tempForm = tempRoute[i].action - end - if onRoadForm == 'default' then - onRoadForm = 'On Road' - end - if (string.lower(tempRoute[i].action) == 'on road' or string.lower(tempRoute[i].action) == 'onroad' or string.lower(tempRoute[i].action) == 'on_road') then - tempForm = onRoadForm + if not Include.Files[ IncludeFile ] then + Include.Files[IncludeFile] = IncludeFile + env.info( "Include:" .. IncludeFile .. " from " .. Include.ProgramPath ) + local f = assert( base.loadfile( Include.ProgramPath .. IncludeFile .. ".lua" ) ) + if f == nil then + env.info( "Include:" .. IncludeFile .. " from " .. Include.MissionPath ) + local f = assert( base.loadfile( Include.MissionPath .. IncludeFile .. ".lua" ) ) + if f == nil then + error ("Could not load MOOSE file " .. IncludeFile .. ".lua" ) else - tempForm = offRoadForm - end - - if type(overRideSpeed) == 'number' then - tempSpeed = overRideSpeed - end - - - useRoute[#useRoute + 1] = routines.ground.buildWP(tempRoute[i], tempForm, tempSpeed) - end - - if pType and string.lower(pType) == 'doubleback' then - local curRoute = routines.utils.deepCopy(useRoute) - for i = #curRoute, 2, -1 do - useRoute[#useRoute + 1] = routines.ground.buildWP(curRoute[i], curRoute[i].action, curRoute[i].speed) - end - end - - useRoute[1].action = useRoute[#useRoute].action -- make it so the first WP matches the last WP - end - - local cTask3 = {} - local newPatrol = {} - newPatrol.route = useRoute - newPatrol.gpData = gpData:getName() - cTask3[#cTask3 + 1] = 'routines.ground.patrolRoute(' - cTask3[#cTask3 + 1] = routines.utils.oneLineSerialize(newPatrol) - cTask3[#cTask3 + 1] = ')' - cTask3 = table.concat(cTask3) - local tempTask = { - id = 'WrappedAction', - params = { - action = { - id = 'Script', - params = { - command = cTask3, - - }, - }, - }, - } - - - useRoute[#useRoute].task = tempTask - routines.goRoute(gpData, useRoute) - - return -end - -routines.ground.patrol = function(gpData, pType, form, speed) - local vars = {} - - if type(gpData) == 'table' and gpData:getName() then - gpData = gpData:getName() - end - - vars.useGroupRoute = gpData - vars.gpData = gpData - vars.pType = pType - vars.offRoadForm = form - vars.speed = speed - - routines.ground.patrolRoute(vars) - - return -end - -function routines.GetUnitHeight( CheckUnit ) ---trace.f( "routines" ) - - local UnitPoint = CheckUnit:getPoint() - local UnitPosition = { x = UnitPoint.x, y = UnitPoint.z } - local UnitHeight = UnitPoint.y - - local LandHeight = land.getHeight( UnitPosition ) - - --env.info(( 'CarrierHeight: LandHeight = ' .. LandHeight .. ' CarrierHeight = ' .. CarrierHeight )) - - --trace.f( "routines", "Unit Height = " .. UnitHeight - LandHeight ) - - return UnitHeight - LandHeight - -end - - - -Su34Status = { status = {} } -boardMsgRed = { statusMsg = "" } -boardMsgAll = { timeMsg = "" } -SpawnSettings = {} -Su34MenuPath = {} -Su34Menus = 0 - - -function Su34AttackCarlVinson(groupName) ---trace.menu("", "Su34AttackCarlVinson") - local groupSu34 = Group.getByName( groupName ) - local controllerSu34 = groupSu34.getController(groupSu34) - local groupCarlVinson = Group.getByName("US Carl Vinson #001") - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE ) - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) - if groupCarlVinson ~= nil then - controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupCarlVinson:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = true}}) - end - Su34Status.status[groupName] = 1 - MessageToRed( string.format('%s: ',groupName) .. 'Attacking carrier Carl Vinson. ', 10, 'RedStatus' .. groupName ) -end - -function Su34AttackWest(groupName) ---trace.f("","Su34AttackWest") - local groupSu34 = Group.getByName( groupName ) - local controllerSu34 = groupSu34.getController(groupSu34) - local groupShipWest1 = Group.getByName("US Ship West #001") - local groupShipWest2 = Group.getByName("US Ship West #002") - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE ) - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) - if groupShipWest1 ~= nil then - controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipWest1:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = true}}) - end - if groupShipWest2 ~= nil then - controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipWest2:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = true}}) - end - Su34Status.status[groupName] = 2 - MessageToRed( string.format('%s: ',groupName) .. 'Attacking invading ships in the west. ', 10, 'RedStatus' .. groupName ) -end - -function Su34AttackNorth(groupName) ---trace.menu("","Su34AttackNorth") - local groupSu34 = Group.getByName( groupName ) - local controllerSu34 = groupSu34.getController(groupSu34) - local groupShipNorth1 = Group.getByName("US Ship North #001") - local groupShipNorth2 = Group.getByName("US Ship North #002") - local groupShipNorth3 = Group.getByName("US Ship North #003") - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE ) - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) - if groupShipNorth1 ~= nil then - controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipNorth1:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = false}}) - end - if groupShipNorth2 ~= nil then - controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipNorth2:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = false}}) - end - if groupShipNorth3 ~= nil then - controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipNorth3:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = false}}) - end - Su34Status.status[groupName] = 3 - MessageToRed( string.format('%s: ',groupName) .. 'Attacking invading ships in the north. ', 10, 'RedStatus' .. groupName ) -end - -function Su34Orbit(groupName) ---trace.menu("","Su34Orbit") - local groupSu34 = Group.getByName( groupName ) - local controllerSu34 = groupSu34:getController() - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD ) - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) - controllerSu34:pushTask( {id = 'ControlledTask', params = { task = { id = 'Orbit', params = { pattern = AI.Task.OrbitPattern.RACE_TRACK } }, stopCondition = { duration = 600 } } } ) - Su34Status.status[groupName] = 4 - MessageToRed( string.format('%s: ',groupName) .. 'In orbit and awaiting further instructions. ', 10, 'RedStatus' .. groupName ) -end - -function Su34TakeOff(groupName) ---trace.menu("","Su34TakeOff") - local groupSu34 = Group.getByName( groupName ) - local controllerSu34 = groupSu34:getController() - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD ) - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.BYPASS_AND_ESCAPE ) - Su34Status.status[groupName] = 8 - MessageToRed( string.format('%s: ',groupName) .. 'Take-Off. ', 10, 'RedStatus' .. groupName ) -end - -function Su34Hold(groupName) ---trace.menu("","Su34Hold") - local groupSu34 = Group.getByName( groupName ) - local controllerSu34 = groupSu34:getController() - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD ) - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.BYPASS_AND_ESCAPE ) - Su34Status.status[groupName] = 5 - MessageToRed( string.format('%s: ',groupName) .. 'Holding Weapons. ', 10, 'RedStatus' .. groupName ) -end - -function Su34RTB(groupName) ---trace.menu("","Su34RTB") - Su34Status.status[groupName] = 6 - MessageToRed( string.format('%s: ',groupName) .. 'Return to Krasnodar. ', 10, 'RedStatus' .. groupName ) -end - -function Su34Destroyed(groupName) ---trace.menu("","Su34Destroyed") - Su34Status.status[groupName] = 7 - MessageToRed( string.format('%s: ',groupName) .. 'Destroyed. ', 30, 'RedStatus' .. groupName ) -end - -function GroupAlive( groupName ) ---trace.menu("","GroupAlive") - local groupTest = Group.getByName( groupName ) - - local groupExists = false - - if groupTest then - groupExists = groupTest:isExist() - end - - --trace.r( "", "", { groupExists } ) - return groupExists -end - -function Su34IsDead() ---trace.f() - -end - -function Su34OverviewStatus() ---trace.menu("","Su34OverviewStatus") - local msg = "" - local currentStatus = 0 - local Exists = false - - for groupName, currentStatus in pairs(Su34Status.status) do - - env.info(('Su34 Overview Status: GroupName = ' .. groupName )) - Alive = GroupAlive( groupName ) - - if Alive then - if currentStatus == 1 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Attacking carrier Carl Vinson. " - elseif currentStatus == 2 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Attacking supporting ships in the west. " - elseif currentStatus == 3 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Attacking invading ships in the north. " - elseif currentStatus == 4 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "In orbit and awaiting further instructions. " - elseif currentStatus == 5 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Holding Weapons. " - elseif currentStatus == 6 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Return to Krasnodar. " - elseif currentStatus == 7 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Destroyed. " - elseif currentStatus == 8 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Take-Off. " + env.info( "Include:" .. IncludeFile .. " loaded from " .. Include.MissionPath ) + return f() end else - if currentStatus == 7 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Destroyed. " - else - Su34Destroyed(groupName) - end - end - end - - boardMsgRed.statusMsg = msg -end - - -function UpdateBoardMsg() ---trace.f() - Su34OverviewStatus() - MessageToRed( boardMsgRed.statusMsg, 15, 'RedStatus' ) -end - -function MusicReset( flg ) ---trace.f() - trigger.action.setUserFlag(95,flg) -end - -function PlaneActivate(groupNameFormat, flg) ---trace.f() - local groupName = groupNameFormat .. string.format("#%03d", trigger.misc.getUserFlag(flg)) - --trigger.action.outText(groupName,10) - trigger.action.activateGroup(Group.getByName(groupName)) -end - -function Su34Menu(groupName) ---trace.f() - - --env.info(( 'Su34Menu(' .. groupName .. ')' )) - local groupSu34 = Group.getByName( groupName ) - - if Su34Status.status[groupName] == 1 or - Su34Status.status[groupName] == 2 or - Su34Status.status[groupName] == 3 or - Su34Status.status[groupName] == 4 or - Su34Status.status[groupName] == 5 then - if Su34MenuPath[groupName] == nil then - if planeMenuPath == nil then - planeMenuPath = missionCommands.addSubMenuForCoalition( - coalition.side.RED, - "SU-34 anti-ship flights", - nil - ) - end - Su34MenuPath[groupName] = missionCommands.addSubMenuForCoalition( - coalition.side.RED, - "Flight " .. groupName, - planeMenuPath - ) - - missionCommands.addCommandForCoalition( - coalition.side.RED, - "Attack carrier Carl Vinson", - Su34MenuPath[groupName], - Su34AttackCarlVinson, - groupName - ) - - missionCommands.addCommandForCoalition( - coalition.side.RED, - "Attack ships in the west", - Su34MenuPath[groupName], - Su34AttackWest, - groupName - ) - - missionCommands.addCommandForCoalition( - coalition.side.RED, - "Attack ships in the north", - Su34MenuPath[groupName], - Su34AttackNorth, - groupName - ) - - missionCommands.addCommandForCoalition( - coalition.side.RED, - "Hold position and await instructions", - Su34MenuPath[groupName], - Su34Orbit, - groupName - ) - - missionCommands.addCommandForCoalition( - coalition.side.RED, - "Report status", - Su34MenuPath[groupName], - Su34OverviewStatus - ) - end - else - if Su34MenuPath[groupName] then - missionCommands.removeItemForCoalition(coalition.side.RED, Su34MenuPath[groupName]) + env.info( "Include:" .. IncludeFile .. " loaded from " .. Include.ProgramPath ) + return f() end end end ---- Obsolete function, but kept to rework in framework. +Include.ProgramPath = "Scripts/Moose/" +Include.MissionPath = Include.Path() -function ChooseInfantry ( TeleportPrefixTable, TeleportMax ) ---trace.f("Spawn") - --env.info(( 'ChooseInfantry: ' )) +env.info( "Include.ProgramPath = " .. Include.ProgramPath) +env.info( "Include.MissionPath = " .. Include.MissionPath) - TeleportPrefixTableCount = #TeleportPrefixTable - TeleportPrefixTableIndex = math.random( 1, TeleportPrefixTableCount ) +Include.Files = {} - --env.info(( 'ChooseInfantry: TeleportPrefixTableIndex = ' .. TeleportPrefixTableIndex .. ' TeleportPrefixTableCount = ' .. TeleportPrefixTableCount .. ' TeleportMax = ' .. TeleportMax )) - - local TeleportFound = false - local TeleportLoop = true - local Index = TeleportPrefixTableIndex - local TeleportPrefix = '' - - while TeleportLoop do - TeleportPrefix = TeleportPrefixTable[Index] - if SpawnSettings[TeleportPrefix] then - if SpawnSettings[TeleportPrefix]['SpawnCount'] - 1 < TeleportMax then - SpawnSettings[TeleportPrefix]['SpawnCount'] = SpawnSettings[TeleportPrefix]['SpawnCount'] + 1 - TeleportFound = true - else - TeleportFound = false - end - else - SpawnSettings[TeleportPrefix] = {} - SpawnSettings[TeleportPrefix]['SpawnCount'] = 0 - TeleportFound = true - end - if TeleportFound then - TeleportLoop = false - else - if Index < TeleportPrefixTableCount then - Index = Index + 1 - else - TeleportLoop = false - end - end - --env.info(( 'ChooseInfantry: Loop 1 - TeleportPrefix = ' .. TeleportPrefix .. ' Index = ' .. Index )) - end - - if TeleportFound == false then - TeleportLoop = true - Index = 1 - while TeleportLoop do - TeleportPrefix = TeleportPrefixTable[Index] - if SpawnSettings[TeleportPrefix] then - if SpawnSettings[TeleportPrefix]['SpawnCount'] - 1 < TeleportMax then - SpawnSettings[TeleportPrefix]['SpawnCount'] = SpawnSettings[TeleportPrefix]['SpawnCount'] + 1 - TeleportFound = true - else - TeleportFound = false - end - else - SpawnSettings[TeleportPrefix] = {} - SpawnSettings[TeleportPrefix]['SpawnCount'] = 0 - TeleportFound = true - end - if TeleportFound then - TeleportLoop = false - else - if Index < TeleportPrefixTableIndex then - Index = Index + 1 - else - TeleportLoop = false - end - end - --env.info(( 'ChooseInfantry: Loop 2 - TeleportPrefix = ' .. TeleportPrefix .. ' Index = ' .. Index )) - end - end - - local TeleportGroupName = '' - if TeleportFound == true then - TeleportGroupName = TeleportPrefix .. string.format("#%03d", SpawnSettings[TeleportPrefix]['SpawnCount'] ) - else - TeleportGroupName = '' - end - - --env.info(('ChooseInfantry: TeleportGroupName = ' .. TeleportGroupName )) - --env.info(('ChooseInfantry: return')) - - return TeleportGroupName -end - -SpawnedInfantry = 0 - -function LandCarrier ( CarrierGroup, LandingZonePrefix ) ---trace.f() - --env.info(( 'LandCarrier: ' )) - --env.info(( 'LandCarrier: CarrierGroup = ' .. CarrierGroup:getName() )) - --env.info(( 'LandCarrier: LandingZone = ' .. LandingZonePrefix )) - - local controllerGroup = CarrierGroup:getController() - - local LandingZone = trigger.misc.getZone(LandingZonePrefix) - local LandingZonePos = {} - LandingZonePos.x = LandingZone.point.x + math.random(LandingZone.radius * -1, LandingZone.radius) - LandingZonePos.y = LandingZone.point.z + math.random(LandingZone.radius * -1, LandingZone.radius) - - controllerGroup:pushTask( { id = 'Land', params = { point = LandingZonePos, durationFlag = true, duration = 10 } } ) - - --env.info(( 'LandCarrier: end' )) -end - -EscortCount = 0 -function EscortCarrier ( CarrierGroup, EscortPrefix, EscortLastWayPoint, EscortEngagementDistanceMax, EscortTargetTypes ) ---trace.f() - --env.info(( 'EscortCarrier: ' )) - --env.info(( 'EscortCarrier: CarrierGroup = ' .. CarrierGroup:getName() )) - --env.info(( 'EscortCarrier: EscortPrefix = ' .. EscortPrefix )) - - local CarrierName = CarrierGroup:getName() - - local EscortMission = {} - local CarrierMission = {} - - local EscortMission = SpawnMissionGroup( EscortPrefix ) - local CarrierMission = SpawnMissionGroup( CarrierGroup:getName() ) - - if EscortMission ~= nil and CarrierMission ~= nil then - - EscortCount = EscortCount + 1 - EscortMissionName = string.format( EscortPrefix .. '#Escort %s', CarrierName ) - EscortMission.name = EscortMissionName - EscortMission.groupId = nil - EscortMission.lateActivation = false - EscortMission.taskSelected = false - - local EscortUnits = #EscortMission.units - for u = 1, EscortUnits do - EscortMission.units[u].name = string.format( EscortPrefix .. '#Escort %s %02d', CarrierName, u ) - EscortMission.units[u].unitId = nil - end - - - EscortMission.route.points[1].task = { id = "ComboTask", - params = - { - tasks = - { - [1] = - { - enabled = true, - auto = false, - id = "Escort", - number = 1, - params = - { - lastWptIndexFlagChangedManually = false, - groupId = CarrierGroup:getID(), - lastWptIndex = nil, - lastWptIndexFlag = false, - engagementDistMax = EscortEngagementDistanceMax, - targetTypes = EscortTargetTypes, - pos = - { - y = 20, - x = 20, - z = 0, - } -- end of ["pos"] - } -- end of ["params"] - } -- end of [1] - } -- end of ["tasks"] - } -- end of ["params"] - } -- end of ["task"] - - SpawnGroupAdd( EscortPrefix, EscortMission ) - - end -end - -function SendMessageToCarrier( CarrierGroup, CarrierMessage ) ---trace.f() - - if CarrierGroup ~= nil then - MessageToGroup( CarrierGroup, CarrierMessage, 30, 'Carrier/' .. CarrierGroup:getName() ) - end - -end - -function MessageToGroup( MsgGroup, MsgText, MsgTime, MsgName ) ---trace.f() - - if type(MsgGroup) == 'string' then - --env.info( 'MessageToGroup: Converted MsgGroup string "' .. MsgGroup .. '" into a Group structure.' ) - MsgGroup = Group.getByName( MsgGroup ) - end - - if MsgGroup ~= nil then - local MsgTable = {} - MsgTable.text = MsgText - MsgTable.displayTime = MsgTime - MsgTable.msgFor = { units = { MsgGroup:getUnits()[1]:getName() } } - MsgTable.name = MsgName - --routines.message.add( MsgTable ) - --env.info(('MessageToGroup: Message sent to ' .. MsgGroup:getUnits()[1]:getName() .. ' -> ' .. MsgText )) - end -end - -function MessageToUnit( UnitName, MsgText, MsgTime, MsgName ) ---trace.f() - - if UnitName ~= nil then - local MsgTable = {} - MsgTable.text = MsgText - MsgTable.displayTime = MsgTime - MsgTable.msgFor = { units = { UnitName } } - MsgTable.name = MsgName - --routines.message.add( MsgTable ) - end -end - -function MessageToAll( MsgText, MsgTime, MsgName ) ---trace.f() - - MESSAGE:New( MsgText, MsgTime, "Message" ):ToCoalition( coalition.side.RED ):ToCoalition( coalition.side.BLUE ) -end - -function MessageToRed( MsgText, MsgTime, MsgName ) ---trace.f() - - MESSAGE:New( MsgText, MsgTime, "To Red Coalition" ):ToCoalition( coalition.side.RED ) -end - -function MessageToBlue( MsgText, MsgTime, MsgName ) ---trace.f() - - MESSAGE:New( MsgText, MsgTime, "To Blue Coalition" ):ToCoalition( coalition.side.RED ) -end - -function getCarrierHeight( CarrierGroup ) ---trace.f() - - if CarrierGroup ~= nil then - if table.getn(CarrierGroup:getUnits()) == 1 then - local CarrierUnit = CarrierGroup:getUnits()[1] - local CurrentPoint = CarrierUnit:getPoint() - - local CurrentPosition = { x = CurrentPoint.x, y = CurrentPoint.z } - local CarrierHeight = CurrentPoint.y - - local LandHeight = land.getHeight( CurrentPosition ) - - --env.info(( 'CarrierHeight: LandHeight = ' .. LandHeight .. ' CarrierHeight = ' .. CarrierHeight )) - - return CarrierHeight - LandHeight - else - return 999999 - end - else - return 999999 - end - -end - -function GetUnitHeight( CheckUnit ) ---trace.f() - - local UnitPoint = CheckUnit:getPoint() - local UnitPosition = { x = CurrentPoint.x, y = CurrentPoint.z } - local UnitHeight = CurrentPoint.y - - local LandHeight = land.getHeight( CurrentPosition ) - - --env.info(( 'CarrierHeight: LandHeight = ' .. LandHeight .. ' CarrierHeight = ' .. CarrierHeight )) - - return UnitHeight - LandHeight - -end - - -_MusicTable = {} -_MusicTable.Files = {} -_MusicTable.Queue = {} -_MusicTable.FileCnt = 0 - - -function MusicRegister( SndRef, SndFile, SndTime ) ---trace.f() - - env.info(( 'MusicRegister: SndRef = ' .. SndRef )) - env.info(( 'MusicRegister: SndFile = ' .. SndFile )) - env.info(( 'MusicRegister: SndTime = ' .. SndTime )) - - - _MusicTable.FileCnt = _MusicTable.FileCnt + 1 - - _MusicTable.Files[_MusicTable.FileCnt] = {} - _MusicTable.Files[_MusicTable.FileCnt].Ref = SndRef - _MusicTable.Files[_MusicTable.FileCnt].File = SndFile - _MusicTable.Files[_MusicTable.FileCnt].Time = SndTime - - if not _MusicTable.Function then - _MusicTable.Function = routines.scheduleFunction( MusicScheduler, { }, timer.getTime() + 10, 10) - end - -end - -function MusicToPlayer( SndRef, PlayerName, SndContinue ) ---trace.f() - - --env.info(( 'MusicToPlayer: SndRef = ' .. SndRef )) - - local PlayerUnits = AlivePlayerUnits() - for PlayerUnitIdx, PlayerUnit in pairs(PlayerUnits) do - local PlayerUnitName = PlayerUnit:getPlayerName() - --env.info(( 'MusicToPlayer: PlayerUnitName = ' .. PlayerUnitName )) - if PlayerName == PlayerUnitName then - PlayerGroup = PlayerUnit:getGroup() - if PlayerGroup then - --env.info(( 'MusicToPlayer: PlayerGroup = ' .. PlayerGroup:getName() )) - MusicToGroup( SndRef, PlayerGroup, SndContinue ) - end - break - end - end - - --env.info(( 'MusicToPlayer: end' )) - -end - -function MusicToGroup( SndRef, SndGroup, SndContinue ) ---trace.f() - - --env.info(( 'MusicToGroup: SndRef = ' .. SndRef )) - - if SndGroup ~= nil then - if _MusicTable and _MusicTable.FileCnt > 0 then - if SndGroup:isExist() then - if MusicCanStart(SndGroup:getUnit(1):getPlayerName()) then - --env.info(( 'MusicToGroup: OK for Sound.' )) - local SndIdx = 0 - if SndRef == '' then - --env.info(( 'MusicToGroup: SndRef as empty. Queueing at random.' )) - SndIdx = math.random( 1, _MusicTable.FileCnt ) - else - for SndIdx = 1, _MusicTable.FileCnt do - if _MusicTable.Files[SndIdx].Ref == SndRef then - break - end - end - end - --env.info(( 'MusicToGroup: SndIdx = ' .. SndIdx )) - --env.info(( 'MusicToGroup: Queueing Music ' .. _MusicTable.Files[SndIdx].File .. ' for Group ' .. SndGroup:getID() )) - trigger.action.outSoundForGroup( SndGroup:getID(), _MusicTable.Files[SndIdx].File ) - MessageToGroup( SndGroup, 'Playing ' .. _MusicTable.Files[SndIdx].File, 15, 'Music-' .. SndGroup:getUnit(1):getPlayerName() ) - - local SndQueueRef = SndGroup:getUnit(1):getPlayerName() - if _MusicTable.Queue[SndQueueRef] == nil then - _MusicTable.Queue[SndQueueRef] = {} - end - _MusicTable.Queue[SndQueueRef].Start = timer.getTime() - _MusicTable.Queue[SndQueueRef].PlayerName = SndGroup:getUnit(1):getPlayerName() - _MusicTable.Queue[SndQueueRef].Group = SndGroup - _MusicTable.Queue[SndQueueRef].ID = SndGroup:getID() - _MusicTable.Queue[SndQueueRef].Ref = SndIdx - _MusicTable.Queue[SndQueueRef].Continue = SndContinue - _MusicTable.Queue[SndQueueRef].Type = Group - end - end - end - end -end - -function MusicCanStart(PlayerName) ---trace.f() - - --env.info(( 'MusicCanStart:' )) - - local MusicOut = false - - if _MusicTable['Queue'] ~= nil and _MusicTable.FileCnt > 0 then - --env.info(( 'MusicCanStart: PlayerName = ' .. PlayerName )) - local PlayerFound = false - local MusicStart = 0 - local MusicTime = 0 - for SndQueueIdx, SndQueue in pairs( _MusicTable.Queue ) do - if SndQueue.PlayerName == PlayerName then - PlayerFound = true - MusicStart = SndQueue.Start - MusicTime = _MusicTable.Files[SndQueue.Ref].Time - break - end - end - if PlayerFound then - --env.info(( 'MusicCanStart: MusicStart = ' .. MusicStart )) - --env.info(( 'MusicCanStart: MusicTime = ' .. MusicTime )) - --env.info(( 'MusicCanStart: timer.getTime() = ' .. timer.getTime() )) - - if MusicStart + MusicTime <= timer.getTime() then - MusicOut = true - end - else - MusicOut = true - end - end - - if MusicOut then - --env.info(( 'MusicCanStart: true' )) - else - --env.info(( 'MusicCanStart: false' )) - end - - return MusicOut -end - -function MusicScheduler() ---trace.scheduled("", "MusicScheduler") - - --env.info(( 'MusicScheduler:' )) - if _MusicTable['Queue'] ~= nil and _MusicTable.FileCnt > 0 then - --env.info(( 'MusicScheduler: Walking Sound Queue.')) - for SndQueueIdx, SndQueue in pairs( _MusicTable.Queue ) do - if SndQueue.Continue then - if MusicCanStart(SndQueue.PlayerName) then - --env.info(('MusicScheduler: MusicToGroup')) - MusicToPlayer( '', SndQueue.PlayerName, true ) - end - end - end - end - -end - - -env.info(( 'Init: Scripts Loaded v1.1' )) - ---- This module contains the BASE class. --- --- 1) @{#BASE} class --- ================= --- The @{#BASE} class is the super class for all the classes defined within MOOSE. --- --- It handles: --- --- * The construction and inheritance of child classes. --- * The tracing of objects during mission execution within the **DCS.log** file, under the **"Saved Games\DCS\Logs"** folder. --- --- Note: Normally you would not use the BASE class unless you are extending the MOOSE framework with new classes. --- --- 1.1) BASE constructor --- --------------------- --- Any class derived from BASE, must use the @{Base#BASE.New) constructor within the @{Base#BASE.Inherit) method. --- See an example at the @{Base#BASE.New} method how this is done. --- --- 1.2) BASE Trace functionality --- ----------------------------- --- The BASE class contains trace methods to trace progress within a mission execution of a certain object. --- Note that these trace methods are inherited by each MOOSE class interiting BASE. --- As such, each object created from derived class from BASE can use the tracing functions to trace its execution. --- --- 1.2.1) Tracing functions --- ------------------------ --- There are basically 3 types of tracing methods available within BASE: --- --- * @{#BASE.F}: Trace the beginning of a function and its given parameters. An F is indicated at column 44 in the DCS.log file. --- * @{#BASE.T}: 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}: Trace an exception within a function giving optional variables or parameters. An E is indicated at column 44 in the DCS.log file. An exception will always be traced. --- --- 1.2.2) Tracing levels --- --------------------- --- There are 3 tracing levels within MOOSE. --- These tracing levels were defined to avoid bulks of tracing to be generated by lots of objects. --- --- As such, the F and T methods have additional variants to trace level 2 and 3 respectively: --- --- * @{#BASE.F2}: Trace the beginning of a function and its given parameters with tracing level 2. --- * @{#BASE.F3}: Trace the beginning of a function and its given parameters with tracing level 3. --- * @{#BASE.T2}: Trace further logic within a function giving optional variables or parameters with tracing level 2. --- * @{#BASE.T3}: Trace further logic within a function giving optional variables or parameters with tracing level 3. --- --- 1.3) BASE Inheritance support --- =========================== --- The following methods are available to support inheritance: --- --- * @{#BASE.Inherit}: Inherits from a class. --- * @{#BASE.Inherited}: Returns the parent class from the class. --- --- Future --- ====== --- Further methods may be added to BASE whenever there is a need to make "overall" functions available within MOOSE. --- --- ==== --- --- @module Base --- @author FlightControl - - - -local _TraceOnOff = true -local _TraceLevel = 1 -local _TraceAll = false -local _TraceClass = {} -local _TraceClassMethod = {} - -local _ClassID = 0 - ---- The BASE Class --- @type BASE --- @field ClassName The name of the class. --- @field ClassID The ID number of the class. --- @field ClassNameAndID The name of the class concatenated with the ID number of the class. -BASE = { - ClassName = "BASE", - ClassID = 0, - Events = {}, - States = {} -} - ---- The Formation Class --- @type FORMATION --- @field Cone A cone formation. -FORMATION = { - Cone = "Cone" -} - - - ---- The base constructor. This is the top top class of all classed defined within the MOOSE. --- Any new class needs to be derived from this class for proper inheritance. --- @param #BASE self --- @return #BASE The new instance of the BASE class. --- @usage --- -- This declares the constructor of the class TASK, inheriting from BASE. --- --- TASK constructor --- -- @param #TASK self --- -- @param Parameter The parameter of the New constructor. --- -- @return #TASK self --- function TASK:New( Parameter ) --- --- local self = BASE:Inherit( self, BASE:New() ) --- --- self.Variable = Parameter --- --- return self --- end --- @todo need to investigate if the deepCopy is really needed... Don't think so. -function BASE:New() - local self = routines.utils.deepCopy( self ) -- Create a new self instance - local MetaTable = {} - setmetatable( self, MetaTable ) - self.__index = self - _ClassID = _ClassID + 1 - self.ClassID = _ClassID - self.ClassNameAndID = string.format( '%s#%09d', self.ClassName, self.ClassID ) - return self -end - ---- This is the worker method to inherit from a parent class. --- @param #BASE self --- @param Child is the Child class that inherits. --- @param #BASE Parent is the Parent class that the Child inherits from. --- @return #BASE Child -function BASE:Inherit( Child, Parent ) - local Child = routines.utils.deepCopy( Child ) - --local Parent = routines.utils.deepCopy( Parent ) - --local Parent = Parent - if Child ~= nil then - setmetatable( Child, Parent ) - Child.__index = Child - end - --Child.ClassName = Child.ClassName .. '.' .. Child.ClassID - self:T( 'Inherited from ' .. Parent.ClassName ) - return Child -end - ---- This is the worker method to retrieve the Parent class. --- @param #BASE self --- @param #BASE Child is the Child class from which the Parent class needs to be retrieved. --- @return #BASE -function BASE:Inherited( Child ) - local Parent = getmetatable( Child ) --- env.info('Inherited class of ' .. Child.ClassName .. ' is ' .. Parent.ClassName ) - return Parent -end - ---- Get the ClassName + ClassID of the class instance. --- The ClassName + ClassID is formatted as '%s#%09d'. --- @param #BASE self --- @return #string The ClassName + ClassID of the class instance. -function BASE:GetClassNameAndID() - return self.ClassNameAndID -end - ---- Get the ClassName of the class instance. --- @param #BASE self --- @return #string The ClassName of the class instance. -function BASE:GetClassName() - return self.ClassName -end - ---- Get the ClassID of the class instance. --- @param #BASE self --- @return #string The ClassID of the class instance. -function BASE:GetClassID() - return self.ClassID -end - ---- Set a new listener for the class. --- @param self --- @param DCSTypes#Event Event --- @param #function EventFunction --- @return #BASE -function BASE:AddEvent( Event, EventFunction ) - self:F( Event ) - - self.Events[#self.Events+1] = {} - self.Events[#self.Events].Event = Event - self.Events[#self.Events].EventFunction = EventFunction - self.Events[#self.Events].EventEnabled = false - - return self -end - ---- Returns the event dispatcher --- @param #BASE self --- @return Event#EVENT -function BASE:Event() - - return _EVENTDISPATCHER -end - - - - - ---- Enable the event listeners for the class. --- @param #BASE self --- @return #BASE -function BASE:EnableEvents() - self:F( #self.Events ) - - for EventID, Event in pairs( self.Events ) do - Event.Self = self - Event.EventEnabled = true - end - self.Events.Handler = world.addEventHandler( self ) - - return self -end - - ---- Disable the event listeners for the class. --- @param #BASE self --- @return #BASE -function BASE:DisableEvents() - self:F() - - world.removeEventHandler( self ) - for EventID, Event in pairs( self.Events ) do - Event.Self = nil - Event.EventEnabled = false - end - - return self -end - - -local BaseEventCodes = { - "S_EVENT_SHOT", - "S_EVENT_HIT", - "S_EVENT_TAKEOFF", - "S_EVENT_LAND", - "S_EVENT_CRASH", - "S_EVENT_EJECTION", - "S_EVENT_REFUELING", - "S_EVENT_DEAD", - "S_EVENT_PILOT_DEAD", - "S_EVENT_BASE_CAPTURED", - "S_EVENT_MISSION_START", - "S_EVENT_MISSION_END", - "S_EVENT_TOOK_CONTROL", - "S_EVENT_REFUELING_STOP", - "S_EVENT_BIRTH", - "S_EVENT_HUMAN_FAILURE", - "S_EVENT_ENGINE_STARTUP", - "S_EVENT_ENGINE_SHUTDOWN", - "S_EVENT_PLAYER_ENTER_UNIT", - "S_EVENT_PLAYER_LEAVE_UNIT", - "S_EVENT_PLAYER_COMMENT", - "S_EVENT_SHOOTING_START", - "S_EVENT_SHOOTING_END", - "S_EVENT_MAX", -} - ---onEvent( {[1]="S_EVENT_BIRTH",[2]={["subPlace"]=5,["time"]=0,["initiator"]={["id_"]=16884480,},["place"]={["id_"]=5000040,},["id"]=15,["IniUnitName"]="US F-15C@RAMP-Air Support Mountains#001-01",},} --- Event = { --- id = enum world.event, --- time = Time, --- initiator = Unit, --- target = Unit, --- place = Unit, --- subPlace = enum world.BirthPlace, --- weapon = Weapon --- } - ---- Creation of a Birth Event. --- @param #BASE self --- @param DCSTypes#Time EventTime The time stamp of the event. --- @param DCSObject#Object Initiator The initiating object of the event. --- @param #string IniUnitName The initiating unit name. --- @param place --- @param subplace -function BASE:CreateEventBirth( EventTime, Initiator, IniUnitName, place, subplace ) - self:F( { EventTime, Initiator, IniUnitName, place, subplace } ) - - local Event = { - id = world.event.S_EVENT_BIRTH, - time = EventTime, - initiator = Initiator, - IniUnitName = IniUnitName, - place = place, - subplace = subplace - } - - world.onEvent( Event ) -end - ---- Creation of a Crash Event. --- @param #BASE self --- @param DCSTypes#Time EventTime The time stamp of the event. --- @param DCSObject#Object Initiator The initiating object of the event. -function BASE:CreateEventCrash( EventTime, Initiator ) - self:F( { EventTime, Initiator } ) - - local Event = { - id = world.event.S_EVENT_CRASH, - time = EventTime, - initiator = Initiator, - } - - world.onEvent( Event ) -end - --- TODO: Complete DCSTypes#Event structure. ---- The main event handling function... This function captures all events generated for the class. --- @param #BASE self --- @param DCSTypes#Event event -function BASE:onEvent(event) - --self:F( { BaseEventCodes[event.id], event } ) - - if self then - for EventID, EventObject in pairs( self.Events ) do - if EventObject.EventEnabled then - --env.info( 'onEvent Table EventObject.Self = ' .. tostring(EventObject.Self) ) - --env.info( 'onEvent event.id = ' .. tostring(event.id) ) - --env.info( 'onEvent EventObject.Event = ' .. tostring(EventObject.Event) ) - if event.id == EventObject.Event then - if self == EventObject.Self then - if event.initiator and event.initiator:isExist() then - event.IniUnitName = event.initiator:getName() - end - if event.target and event.target:isExist() then - event.TgtUnitName = event.target:getName() - end - --self:T( { BaseEventCodes[event.id], event } ) - --EventObject.EventFunction( self, event ) - end - end - end - end - end -end - -function BASE:SetState( Object, StateName, State ) - - local ClassNameAndID = Object:GetClassNameAndID() - - if not self.States[ClassNameAndID] then - self.States[ClassNameAndID] = {} - end - self.States[ClassNameAndID][StateName] = State - self:F2( { ClassNameAndID, StateName, State } ) - - return self.States[ClassNameAndID][StateName] -end - -function BASE:GetState( Object, StateName ) - - local ClassNameAndID = Object:GetClassNameAndID() - - if self.States[ClassNameAndID] then - local State = self.States[ClassNameAndID][StateName] - self:F2( { ClassNameAndID, StateName, State } ) - return State - end - - return nil -end - -function BASE:ClearState( Object, StateName ) - - local ClassNameAndID = Object:GetClassNameAndID() - if self.States[ClassNameAndID] then - self.States[ClassNameAndID][StateName] = nil - end -end - --- Trace section - --- Log a trace (only shown when trace is on) --- TODO: Make trace function using variable parameters. - ---- Set trace on or off --- Note that when trace is off, no debug statement is performed, increasing performance! --- When Moose is loaded statically, (as one file), tracing is switched off by default. --- So tracing must be switched on manually in your mission if you are using Moose statically. --- When moose is loading dynamically (for moose class development), tracing is switched on by default. --- @param BASE self --- @param #boolean TraceOnOff Switch the tracing on or off. --- @usage --- -- Switch the tracing On --- BASE:TraceOn( true ) --- --- -- Switch the tracing Off --- BASE:TraceOn( false ) -function BASE:TraceOnOff( TraceOnOff ) - _TraceOnOff = TraceOnOff -end - ---- Set trace level --- @param #BASE self --- @param #number Level -function BASE:TraceLevel( Level ) - _TraceLevel = Level - self:E( "Tracing level " .. Level ) -end - ---- Trace all methods in MOOSE --- @param #BASE self --- @param #boolean TraceAll true = trace all methods in MOOSE. -function BASE:TraceAll( TraceAll ) - - _TraceAll = TraceAll - - if _TraceAll then - self:E( "Tracing all methods in MOOSE " ) - else - self:E( "Switched off tracing all methods in MOOSE" ) - end -end - ---- Set tracing for a class --- @param #BASE self --- @param #string Class -function BASE:TraceClass( Class ) - _TraceClass[Class] = true - _TraceClassMethod[Class] = {} - self:E( "Tracing class " .. Class ) -end - ---- Set tracing for a specific method of class --- @param #BASE self --- @param #string Class --- @param #string Method -function BASE:TraceClassMethod( Class, Method ) - if not _TraceClassMethod[Class] then - _TraceClassMethod[Class] = {} - _TraceClassMethod[Class].Method = {} - end - _TraceClassMethod[Class].Method[Method] = true - self:E( "Tracing method " .. Method .. " of class " .. Class ) -end - ---- Trace a function call. This function is private. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:_F( Arguments, DebugInfoCurrentParam, DebugInfoFromParam ) - - if ( _TraceAll == true ) or ( _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName] ) then - - local DebugInfoCurrent = DebugInfoCurrentParam and DebugInfoCurrentParam or debug.getinfo( 2, "nl" ) - local DebugInfoFrom = DebugInfoFromParam and DebugInfoFromParam or debug.getinfo( 3, "l" ) - - local Function = "function" - if DebugInfoCurrent.name then - Function = DebugInfoCurrent.name - end - - if _TraceAll == true or _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName].Method[Function] then - local LineCurrent = 0 - if DebugInfoCurrent.currentline then - LineCurrent = DebugInfoCurrent.currentline - end - local LineFrom = 0 - if DebugInfoFrom then - LineFrom = DebugInfoFrom.currentline - end - env.info( string.format( "%6d(%6d)/%1s:%20s%05d.%s(%s)" , LineCurrent, LineFrom, "F", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) - end - end -end - ---- Trace a function call. Must be at the beginning of the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:F( Arguments ) - - if _TraceOnOff then - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - if _TraceLevel >= 1 then - self:_F( Arguments, DebugInfoCurrent, DebugInfoFrom ) - end - end -end - - ---- Trace a function call level 2. Must be at the beginning of the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:F2( Arguments ) - - if _TraceOnOff then - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - if _TraceLevel >= 2 then - self:_F( Arguments, DebugInfoCurrent, DebugInfoFrom ) - end - end -end - ---- Trace a function call level 3. Must be at the beginning of the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:F3( Arguments ) - - if _TraceOnOff then - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - if _TraceLevel >= 3 then - self:_F( Arguments, DebugInfoCurrent, DebugInfoFrom ) - end - end -end - ---- Trace a function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:_T( Arguments, DebugInfoCurrentParam, DebugInfoFromParam ) - - if ( _TraceAll == true ) or ( _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName] ) then - - local DebugInfoCurrent = DebugInfoCurrentParam and DebugInfoCurrentParam or debug.getinfo( 2, "nl" ) - local DebugInfoFrom = DebugInfoFromParam and DebugInfoFromParam or debug.getinfo( 3, "l" ) - - local Function = "function" - if DebugInfoCurrent.name then - Function = DebugInfoCurrent.name - end - - if _TraceAll == true or _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName].Method[Function] then - local LineCurrent = 0 - if DebugInfoCurrent.currentline then - LineCurrent = DebugInfoCurrent.currentline - end - local LineFrom = 0 - if DebugInfoFrom then - LineFrom = DebugInfoFrom.currentline - end - env.info( string.format( "%6d(%6d)/%1s:%20s%05d.%s" , LineCurrent, LineFrom, "T", self.ClassName, self.ClassID, routines.utils.oneLineSerialize( Arguments ) ) ) - end - end -end - ---- Trace a function logic level 1. Can be anywhere within the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:T( Arguments ) - - if _TraceOnOff then - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - if _TraceLevel >= 1 then - self:_T( Arguments, DebugInfoCurrent, DebugInfoFrom ) - end - end -end - - ---- Trace a function logic level 2. Can be anywhere within the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:T2( Arguments ) - - if _TraceOnOff then - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - if _TraceLevel >= 2 then - self:_T( Arguments, DebugInfoCurrent, DebugInfoFrom ) - end - end -end - ---- Trace a function logic level 3. Can be anywhere within the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:T3( Arguments ) - - if _TraceOnOff then - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - if _TraceLevel >= 3 then - self:_T( Arguments, DebugInfoCurrent, DebugInfoFrom ) - end - end -end - ---- Log an exception which will be traced always. Can be anywhere within the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:E( Arguments ) - - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - local Function = "function" - if DebugInfoCurrent.name then - Function = DebugInfoCurrent.name - end - - local LineCurrent = DebugInfoCurrent.currentline - local LineFrom = -1 - if DebugInfoFrom then - LineFrom = DebugInfoFrom.currentline - end - - env.info( string.format( "%6d(%6d)/%1s:%20s%05d.%s(%s)" , LineCurrent, LineFrom, "E", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) -end - - - ---- This module contains the SCHEDULER class. --- --- 1) @{Scheduler#SCHEDULER} class, extends @{Base#BASE} --- ===================================================== --- The @{Scheduler#SCHEDULER} class models time events calling given event handling functions. --- --- 1.1) SCHEDULER constructor --- -------------------------- --- The SCHEDULER class is quite easy to use: --- --- * @{Scheduler#SCHEDULER.New}: Setup a new scheduler and start it with the specified parameters. --- --- 1.2) SCHEDULER timer stop and start --- ----------------------------------- --- The SCHEDULER can be stopped and restarted with the following methods: --- --- * @{Scheduler#SCHEDULER.Start}: (Re-)Start the scheduler. --- * @{Scheduler#SCHEDULER.Stop}: Stop the scheduler. --- --- @module Scheduler --- @author FlightControl - - ---- The SCHEDULER class --- @type SCHEDULER --- @field #number ScheduleID the ID of the scheduler. --- @extends Base#BASE -SCHEDULER = { - ClassName = "SCHEDULER", -} - ---- SCHEDULER constructor. --- @param #SCHEDULER self --- @param #table TimeEventObject Specified for which Moose object the timer is setup. If a value of nil is provided, a scheduler will be setup without an object reference. --- @param #function TimeEventFunction The event function to be called when a timer event occurs. The event function needs to accept the parameters specified in TimeEventFunctionArguments. --- @param #table TimeEventFunctionArguments Optional arguments that can be given as part of scheduler. The arguments need to be given as a table { param1, param 2, ... }. --- @param #number StartSeconds Specifies the amount of seconds that will be waited before the scheduling is started, and the event function is called. --- @param #number RepeatSecondsInterval Specifies the interval in seconds when the scheduler will call the event function. --- @param #number RandomizationFactor Specifies a randomization factor between 0 and 1 to randomize the RepeatSecondsInterval. --- @param #number StopSeconds Specifies the amount of seconds when the scheduler will be stopped. --- @return #SCHEDULER self -function SCHEDULER:New( TimeEventObject, TimeEventFunction, TimeEventFunctionArguments, StartSeconds, RepeatSecondsInterval, RandomizationFactor, StopSeconds ) - local self = BASE:Inherit( self, BASE:New() ) - self:F2( { TimeEventObject, TimeEventFunction, TimeEventFunctionArguments, StartSeconds, RepeatSecondsInterval, RandomizationFactor, StopSeconds } ) - - self.TimeEventObject = TimeEventObject - self.TimeEventFunction = TimeEventFunction - self.TimeEventFunctionArguments = TimeEventFunctionArguments - self.StartSeconds = StartSeconds - self.Repeat = false - - if RepeatSecondsInterval then - self.RepeatSecondsInterval = RepeatSecondsInterval - else - self.RepeatSecondsInterval = 0 - end - - if RandomizationFactor then - self.RandomizationFactor = RandomizationFactor - else - self.RandomizationFactor = 0 - end - - if StopSeconds then - self.StopSeconds = StopSeconds - end - - - self.StartTime = timer.getTime() - - self:Start() - - return self -end - ---- (Re-)Starts the scheduler. --- @param #SCHEDULER self --- @return #SCHEDULER self -function SCHEDULER:Start() - self:F2( self.TimeEventObject ) - - if self.RepeatSecondsInterval ~= 0 then - self.Repeat = true - end - self.ScheduleID = timer.scheduleFunction( self._Scheduler, self, timer.getTime() + self.StartSeconds + .01 ) - - return self -end - ---- Stops the scheduler. --- @param #SCHEDULER self --- @return #SCHEDULER self -function SCHEDULER:Stop() - self:F2( self.TimeEventObject ) - - self.Repeat = false - if self.ScheduleID then - timer.removeFunction( self.ScheduleID ) - end - self.ScheduleID = nil - - return self -end - --- Private Functions - ---- @param #SCHEDULER self -function SCHEDULER:_Scheduler() - self:F2( self.TimeEventFunctionArguments ) - - local ErrorHandler = function( errmsg ) - - env.info( "Error in SCHEDULER function:" .. errmsg ) - env.info( debug.traceback() ) - - return errmsg - end - - local Status, Result - if self.TimeEventObject then - Status, Result = xpcall( function() return self.TimeEventFunction( self.TimeEventObject, unpack( self.TimeEventFunctionArguments ) ) end, ErrorHandler ) - else - Status, Result = xpcall( function() return self.TimeEventFunction( unpack( self.TimeEventFunctionArguments ) ) end, ErrorHandler ) - end - - self:T( { self.TimeEventFunctionArguments, Status, Result, self.StartTime, self.RepeatSecondsInterval, self.RandomizationFactor, self.StopSeconds } ) - - if Status and ( ( Result == nil ) or ( Result and Result ~= false ) ) then - if self.Repeat and ( not self.StopSeconds or ( self.StopSeconds and timer.getTime() <= self.StartTime + self.StopSeconds ) ) then - local ScheduleTime = - timer.getTime() + - self.RepeatSecondsInterval + - math.random( - - ( self.RandomizationFactor * self.RepeatSecondsInterval / 2 ), - ( self.RandomizationFactor * self.RepeatSecondsInterval / 2 ) - ) + - 0.01 - self:T( { self.TimeEventFunctionArguments, "Repeat:", timer.getTime(), ScheduleTime } ) - return ScheduleTime -- returns the next time the function needs to be called. - else - timer.removeFunction( self.ScheduleID ) - self.ScheduleID = nil - end - else - timer.removeFunction( self.ScheduleID ) - self.ScheduleID = nil - end - - return nil -end - - - - - - - - - - - - - - - - ---- The EVENT class models an efficient event handling process between other classes and its units, weapons. --- @module Event --- @author FlightControl - ---- The EVENT structure --- @type EVENT --- @field #EVENT.Events Events -EVENT = { - ClassName = "EVENT", - ClassID = 0, -} - -local _EVENTCODES = { - "S_EVENT_SHOT", - "S_EVENT_HIT", - "S_EVENT_TAKEOFF", - "S_EVENT_LAND", - "S_EVENT_CRASH", - "S_EVENT_EJECTION", - "S_EVENT_REFUELING", - "S_EVENT_DEAD", - "S_EVENT_PILOT_DEAD", - "S_EVENT_BASE_CAPTURED", - "S_EVENT_MISSION_START", - "S_EVENT_MISSION_END", - "S_EVENT_TOOK_CONTROL", - "S_EVENT_REFUELING_STOP", - "S_EVENT_BIRTH", - "S_EVENT_HUMAN_FAILURE", - "S_EVENT_ENGINE_STARTUP", - "S_EVENT_ENGINE_SHUTDOWN", - "S_EVENT_PLAYER_ENTER_UNIT", - "S_EVENT_PLAYER_LEAVE_UNIT", - "S_EVENT_PLAYER_COMMENT", - "S_EVENT_SHOOTING_START", - "S_EVENT_SHOOTING_END", - "S_EVENT_MAX", -} - ---- The Event structure --- @type EVENTDATA --- @field id --- @field initiator --- @field target --- @field weapon --- @field IniDCSUnit --- @field IniDCSUnitName --- @field Unit#UNIT IniUnit --- @field #string IniUnitName --- @field IniDCSGroup --- @field IniDCSGroupName --- @field TgtDCSUnit --- @field TgtDCSUnitName --- @field Unit#UNIT TgtUnit --- @field #string TgtUnitName --- @field TgtDCSGroup --- @field TgtDCSGroupName --- @field Weapon --- @field WeaponName --- @field WeaponTgtDCSUnit - ---- The Events structure --- @type EVENT.Events --- @field #number IniUnit - -function EVENT:New() - local self = BASE:Inherit( self, BASE:New() ) - self:F2() - self.EventHandler = world.addEventHandler( self ) - return self -end - -function EVENT:EventText( EventID ) - - local EventText = _EVENTCODES[EventID] - - return EventText -end - - ---- Initializes the Events structure for the event --- @param #EVENT self --- @param DCSWorld#world.event EventID --- @param #string EventClass --- @return #EVENT.Events -function EVENT:Init( EventID, EventClass ) - self:F3( { _EVENTCODES[EventID], EventClass } ) - if not self.Events[EventID] then - self.Events[EventID] = {} - end - if not self.Events[EventID][EventClass] then - self.Events[EventID][EventClass] = {} - end - return self.Events[EventID][EventClass] -end - - ---- Create an OnDead event handler for a group --- @param #EVENT self --- @param #table EventTemplate --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param EventSelf The self instance of the class for which the event is. --- @param #function OnEventFunction --- @return #EVENT -function EVENT:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, OnEventFunction ) - self:F2( EventTemplate.name ) - - for EventUnitID, EventUnit in pairs( EventTemplate.units ) do - OnEventFunction( self, EventUnit.name, EventFunction, EventSelf ) - end - return self -end - ---- Set a new listener for an S_EVENT_X event independent from a unit or a weapon. --- @param #EVENT self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @param EventID --- @return #EVENT -function EVENT:OnEventGeneric( EventFunction, EventSelf, EventID ) - self:F2( { EventID } ) - - local Event = self:Init( EventID, EventSelf:GetClassNameAndID() ) - Event.EventFunction = EventFunction - Event.EventSelf = EventSelf - return self -end - - ---- Set a new listener for an S_EVENT_X event --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @param EventID --- @return #EVENT -function EVENT:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, EventID ) - self:F2( EventDCSUnitName ) - - local Event = self:Init( EventID, EventSelf:GetClassNameAndID() ) - if not Event.IniUnit then - Event.IniUnit = {} - end - Event.IniUnit[EventDCSUnitName] = {} - Event.IniUnit[EventDCSUnitName].EventFunction = EventFunction - Event.IniUnit[EventDCSUnitName].EventSelf = EventSelf - return self -end - - ---- Create an OnBirth event handler for a group --- @param #EVENT self --- @param Group#GROUP EventGroup --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnBirthForTemplate( EventTemplate, EventFunction, EventSelf ) - self:F2( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnBirthForUnit ) - - return self -end - ---- Set a new listener for an S_EVENT_BIRTH event, and registers the unit born. --- @param #EVENT self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf --- @return #EVENT -function EVENT:OnBirth( EventFunction, EventSelf ) - self:F2() - - self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_BIRTH ) - - return self -end - ---- Set a new listener for an S_EVENT_BIRTH event. --- @param #EVENT self --- @param #string EventDCSUnitName The id of the unit for the event to be handled. --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf --- @return #EVENT -function EVENT:OnBirthForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_BIRTH ) - - return self -end - ---- Create an OnCrash event handler for a group --- @param #EVENT self --- @param Group#GROUP EventGroup --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnCrashForTemplate( EventTemplate, EventFunction, EventSelf ) - self:F2( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnCrashForUnit ) - - return self -end - ---- Set a new listener for an S_EVENT_CRASH event. --- @param #EVENT self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf --- @return #EVENT -function EVENT:OnCrash( EventFunction, EventSelf ) - self:F2() - - self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_CRASH ) - - return self -end - ---- Set a new listener for an S_EVENT_CRASH event. --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnCrashForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_CRASH ) - - return self -end - ---- Create an OnDead event handler for a group --- @param #EVENT self --- @param Group#GROUP EventGroup --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnDeadForTemplate( EventTemplate, EventFunction, EventSelf ) - self:F2( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnDeadForUnit ) - - return self -end - ---- Set a new listener for an S_EVENT_DEAD event. --- @param #EVENT self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf --- @return #EVENT -function EVENT:OnDead( EventFunction, EventSelf ) - self:F2() - - self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_DEAD ) - - return self -end - - ---- Set a new listener for an S_EVENT_DEAD event. --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnDeadForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_DEAD ) - - return self -end - ---- Set a new listener for an S_EVENT_PILOT_DEAD event. --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnPilotDeadForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_PILOT_DEAD ) - - return self -end - ---- Create an OnDead event handler for a group --- @param #EVENT self --- @param #table EventTemplate --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnLandForTemplate( EventTemplate, EventFunction, EventSelf ) - self:F2( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnLandForUnit ) - - return self -end - ---- Set a new listener for an S_EVENT_LAND event. --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnLandForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_LAND ) - - return self -end - ---- Create an OnDead event handler for a group --- @param #EVENT self --- @param #table EventTemplate --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnTakeOffForTemplate( EventTemplate, EventFunction, EventSelf ) - self:F2( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnTakeOffForUnit ) - - return self -end - ---- Set a new listener for an S_EVENT_TAKEOFF event. --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnTakeOffForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_TAKEOFF ) - - return self -end - ---- Create an OnDead event handler for a group --- @param #EVENT self --- @param #table EventTemplate --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnEngineShutDownForTemplate( EventTemplate, EventFunction, EventSelf ) - self:F2( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnEngineShutDownForUnit ) - - return self -end - ---- Set a new listener for an S_EVENT_ENGINE_SHUTDOWN event. --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnEngineShutDownForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_ENGINE_SHUTDOWN ) - - return self -end - ---- Set a new listener for an S_EVENT_ENGINE_STARTUP event. --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnEngineStartUpForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_ENGINE_STARTUP ) - - return self -end - ---- Set a new listener for an S_EVENT_SHOT event. --- @param #EVENT self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnShot( EventFunction, EventSelf ) - self:F2() - - self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_SHOT ) - - return self -end - ---- Set a new listener for an S_EVENT_SHOT event for a unit. --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnShotForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_SHOT ) - - return self -end - ---- Set a new listener for an S_EVENT_HIT event. --- @param #EVENT self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnHit( EventFunction, EventSelf ) - self:F2() - - self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_HIT ) - - return self -end - ---- Set a new listener for an S_EVENT_HIT event. --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnHitForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_HIT ) - - return self -end - ---- Set a new listener for an S_EVENT_PLAYER_ENTER_UNIT event. --- @param #EVENT self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnPlayerEnterUnit( EventFunction, EventSelf ) - self:F2() - - self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_PLAYER_ENTER_UNIT ) - - return self -end - ---- Set a new listener for an S_EVENT_PLAYER_LEAVE_UNIT event. --- @param #EVENT self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnPlayerLeaveUnit( EventFunction, EventSelf ) - self:F2() - - self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_PLAYER_LEAVE_UNIT ) - - return self -end - - - -function EVENT:onEvent( Event ) - self:F2( { _EVENTCODES[Event.id], Event } ) - - if self and self.Events and self.Events[Event.id] then - if Event.initiator and Event.initiator:getCategory() == Object.Category.UNIT then - Event.IniDCSUnit = Event.initiator - Event.IniDCSGroup = Event.IniDCSUnit:getGroup() - Event.IniDCSUnitName = Event.IniDCSUnit:getName() - Event.IniUnitName = Event.IniDCSUnitName - Event.IniUnit = UNIT:FindByName( Event.IniDCSUnitName ) - Event.IniDCSGroupName = "" - if Event.IniDCSGroup and Event.IniDCSGroup:isExist() then - Event.IniDCSGroupName = Event.IniDCSGroup:getName() - end - end - if Event.target then - if Event.target and Event.target:getCategory() == Object.Category.UNIT then - Event.TgtDCSUnit = Event.target - Event.TgtDCSGroup = Event.TgtDCSUnit:getGroup() - Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() - Event.TgtUnitName = Event.TgtDCSUnitName - Event.TgtUnit = UNIT:FindByName( Event.TgtDCSUnitName ) - Event.TgtDCSGroupName = "" - if Event.TgtDCSGroup and Event.TgtDCSGroup:isExist() then - Event.TgtDCSGroupName = Event.TgtDCSGroup:getName() - end - end - end - if Event.weapon then - Event.Weapon = Event.weapon - Event.WeaponName = Event.Weapon:getTypeName() - --Event.WeaponTgtDCSUnit = Event.Weapon:getTarget() - end - self:E( { _EVENTCODES[Event.id], Event } ) - for ClassName, EventData in pairs( self.Events[Event.id] ) do - if Event.IniDCSUnitName and EventData.IniUnit and EventData.IniUnit[Event.IniDCSUnitName] then - self:E( { "Calling event function for class ", ClassName, " unit ", Event.IniDCSUnitName } ) - EventData.IniUnit[Event.IniDCSUnitName].EventFunction( EventData.IniUnit[Event.IniDCSUnitName].EventSelf, Event ) - else - if Event.IniDCSUnit and not EventData.IniUnit then - self:E( { "Calling event function for class ", ClassName } ) - EventData.EventFunction( EventData.EventSelf, Event ) - end - end - end - end -end - ---- Encapsulation of DCS World Menu system in a set of MENU classes. --- @module Menu - ---- The MENU class --- @type MENU --- @extends Base#BASE -MENU = { - ClassName = "MENU", - MenuPath = nil, - MenuText = "", - MenuParentPath = nil -} - ---- -function MENU:New( MenuText, MenuParentPath ) - - -- Arrange meta tables - local Child = BASE:Inherit( self, BASE:New() ) - - Child.MenuPath = nil - Child.MenuText = MenuText - Child.MenuParentPath = MenuParentPath - return Child -end - ---- The COMMANDMENU class --- @type COMMANDMENU --- @extends Menu#MENU -COMMANDMENU = { - ClassName = "COMMANDMENU", - CommandMenuFunction = nil, - CommandMenuArgument = nil -} - -function COMMANDMENU:New( MenuText, ParentMenu, CommandMenuFunction, CommandMenuArgument ) - - -- Arrange meta tables - - local MenuParentPath = nil - if ParentMenu ~= nil then - MenuParentPath = ParentMenu.MenuPath - end - - local Child = BASE:Inherit( self, MENU:New( MenuText, MenuParentPath ) ) - - Child.MenuPath = missionCommands.addCommand( MenuText, MenuParentPath, CommandMenuFunction, CommandMenuArgument ) - Child.CommandMenuFunction = CommandMenuFunction - Child.CommandMenuArgument = CommandMenuArgument - return Child -end - ---- The SUBMENU class --- @type SUBMENU --- @extends Menu#MENU -SUBMENU = { - ClassName = "SUBMENU" -} - -function SUBMENU:New( MenuText, ParentMenu ) - - -- Arrange meta tables - local MenuParentPath = nil - if ParentMenu ~= nil then - MenuParentPath = ParentMenu.MenuPath - end - - local Child = BASE:Inherit( self, MENU:New( MenuText, MenuParentPath ) ) - - Child.MenuPath = missionCommands.addSubMenu( MenuText, MenuParentPath ) - return Child -end - --- This local variable is used to cache the menus registered under clients. --- Menus don't dissapear when clients are destroyed and restarted. --- So every menu for a client created must be tracked so that program logic accidentally does not create --- the same menus twice during initialization logic. --- These menu classes are handling this logic with this variable. -local _MENUCLIENTS = {} - ---- The MENU_CLIENT class --- @type MENU_CLIENT --- @extends Menu#MENU -MENU_CLIENT = { - ClassName = "MENU_CLIENT" -} - ---- Creates a new menu item for a group --- @param self --- @param Client#CLIENT MenuClient The Client owning the menu. --- @param #string MenuText The text for the menu. --- @param #table ParentMenu The parent menu. --- @return #MENU_CLIENT self -function MENU_CLIENT:New( MenuClient, MenuText, ParentMenu ) - - -- Arrange meta tables - local MenuParentPath = {} - if ParentMenu ~= nil then - MenuParentPath = ParentMenu.MenuPath - end - - local self = BASE:Inherit( self, MENU:New( MenuText, MenuParentPath ) ) - self:F( { MenuClient, MenuText, ParentMenu } ) - - self.MenuClient = MenuClient - self.MenuClientGroupID = MenuClient:GetClientGroupID() - self.MenuParentPath = MenuParentPath - self.MenuText = MenuText - self.ParentMenu = ParentMenu - - self.Menus = {} - - if not _MENUCLIENTS[self.MenuClientGroupID] then - _MENUCLIENTS[self.MenuClientGroupID] = {} - end - - local MenuPath = _MENUCLIENTS[self.MenuClientGroupID] - - self:T( { MenuClient:GetClientGroupName(), MenuPath[table.concat(MenuParentPath)], MenuParentPath, MenuText } ) - - local MenuPathID = table.concat(MenuParentPath) .. "/" .. MenuText - if MenuPath[MenuPathID] then - missionCommands.removeItemForGroup( self.MenuClient:GetClientGroupID(), MenuPath[MenuPathID] ) - end - - self.MenuPath = missionCommands.addSubMenuForGroup( self.MenuClient:GetClientGroupID(), MenuText, MenuParentPath ) - MenuPath[MenuPathID] = self.MenuPath - - self:T( { MenuClient:GetClientGroupName(), self.MenuPath } ) - - if ParentMenu and ParentMenu.Menus then - ParentMenu.Menus[self.MenuPath] = self - end - return self -end - ---- Removes the sub menus recursively of this MENU_CLIENT. --- @param #MENU_CLIENT self --- @return #MENU_CLIENT self -function MENU_CLIENT:RemoveSubMenus() - self:F( self.MenuPath ) - - for MenuID, Menu in pairs( self.Menus ) do - Menu:Remove() - end - -end - ---- Removes the sub menus recursively of this MENU_CLIENT. --- @param #MENU_CLIENT self --- @return #MENU_CLIENT self -function MENU_CLIENT:Remove() - self:F( self.MenuPath ) - - self:RemoveSubMenus() - - if not _MENUCLIENTS[self.MenuClientGroupID] then - _MENUCLIENTS[self.MenuClientGroupID] = {} - end - - local MenuPath = _MENUCLIENTS[self.MenuClientGroupID] - - if MenuPath[table.concat(self.MenuParentPath) .. "/" .. self.MenuText] then - MenuPath[table.concat(self.MenuParentPath) .. "/" .. self.MenuText] = nil - end - - missionCommands.removeItemForGroup( self.MenuClient:GetClientGroupID(), self.MenuPath ) - self.ParentMenu.Menus[self.MenuPath] = nil - return nil -end - - ---- The MENU_CLIENT_COMMAND class --- @type MENU_CLIENT_COMMAND --- @extends Menu#MENU -MENU_CLIENT_COMMAND = { - ClassName = "MENU_CLIENT_COMMAND" -} - ---- Creates a new radio command item for a group --- @param self --- @param Client#CLIENT MenuClient The Client owning the menu. --- @param MenuText The text for the menu. --- @param ParentMenu The parent menu. --- @param CommandMenuFunction A function that is called when the menu key is pressed. --- @param CommandMenuArgument An argument for the function. --- @return Menu#MENU_CLIENT_COMMAND self -function MENU_CLIENT_COMMAND:New( MenuClient, MenuText, ParentMenu, CommandMenuFunction, CommandMenuArgument ) - - -- Arrange meta tables - - local MenuParentPath = {} - if ParentMenu ~= nil then - MenuParentPath = ParentMenu.MenuPath - end - - local self = BASE:Inherit( self, MENU:New( MenuText, MenuParentPath ) ) - - self.MenuClient = MenuClient - self.MenuClientGroupID = MenuClient:GetClientGroupID() - self.MenuParentPath = MenuParentPath - self.MenuText = MenuText - self.ParentMenu = ParentMenu - - if not _MENUCLIENTS[self.MenuClientGroupID] then - _MENUCLIENTS[self.MenuClientGroupID] = {} - end - - local MenuPath = _MENUCLIENTS[self.MenuClientGroupID] - - self:T( { MenuClient:GetClientGroupName(), MenuPath[table.concat(MenuParentPath)], MenuParentPath, MenuText, CommandMenuFunction, CommandMenuArgument } ) - - local MenuPathID = table.concat(MenuParentPath) .. "/" .. MenuText - if MenuPath[MenuPathID] then - missionCommands.removeItemForGroup( self.MenuClient:GetClientGroupID(), MenuPath[MenuPathID] ) - end - - self.MenuPath = missionCommands.addCommandForGroup( self.MenuClient:GetClientGroupID(), MenuText, MenuParentPath, CommandMenuFunction, CommandMenuArgument ) - MenuPath[MenuPathID] = self.MenuPath - - self.CommandMenuFunction = CommandMenuFunction - self.CommandMenuArgument = CommandMenuArgument - - ParentMenu.Menus[self.MenuPath] = self - - return self -end - -function MENU_CLIENT_COMMAND:Remove() - self:F( self.MenuPath ) - - if not _MENUCLIENTS[self.MenuClientGroupID] then - _MENUCLIENTS[self.MenuClientGroupID] = {} - end - - local MenuPath = _MENUCLIENTS[self.MenuClientGroupID] - - if MenuPath[table.concat(self.MenuParentPath) .. "/" .. self.MenuText] then - MenuPath[table.concat(self.MenuParentPath) .. "/" .. self.MenuText] = nil - end - - missionCommands.removeItemForGroup( self.MenuClient:GetClientGroupID(), self.MenuPath ) - self.ParentMenu.Menus[self.MenuPath] = nil - return nil -end - - ---- The MENU_COALITION class --- @type MENU_COALITION --- @extends Menu#MENU -MENU_COALITION = { - ClassName = "MENU_COALITION" -} - ---- Creates a new coalition menu item --- @param #MENU_COALITION self --- @param DCSCoalition#coalition.side MenuCoalition The coalition owning the menu. --- @param #string MenuText The text for the menu. --- @param #table ParentMenu The parent menu. --- @return #MENU_COALITION self -function MENU_COALITION:New( MenuCoalition, MenuText, ParentMenu ) - - -- Arrange meta tables - local MenuParentPath = {} - if ParentMenu ~= nil then - MenuParentPath = ParentMenu.MenuPath - end - - local self = BASE:Inherit( self, MENU:New( MenuText, MenuParentPath ) ) - self:F( { MenuCoalition, MenuText, ParentMenu } ) - - self.MenuCoalition = MenuCoalition - self.MenuParentPath = MenuParentPath - self.MenuText = MenuText - self.ParentMenu = ParentMenu - - self.Menus = {} - - self:T( { MenuParentPath, MenuText } ) - - self.MenuPath = missionCommands.addSubMenuForCoalition( self.MenuCoalition, MenuText, MenuParentPath ) - - self:T( { self.MenuPath } ) - - if ParentMenu and ParentMenu.Menus then - ParentMenu.Menus[self.MenuPath] = self - end - return self -end - ---- Removes the sub menus recursively of this MENU_COALITION. --- @param #MENU_COALITION self --- @return #MENU_COALITION self -function MENU_COALITION:RemoveSubMenus() - self:F( self.MenuPath ) - - for MenuID, Menu in pairs( self.Menus ) do - Menu:Remove() - end - -end - ---- Removes the sub menus recursively of this MENU_COALITION. --- @param #MENU_COALITION self --- @return #MENU_COALITION self -function MENU_COALITION:Remove() - self:F( self.MenuPath ) - - self:RemoveSubMenus() - missionCommands.removeItemForCoalition( self.MenuCoalition, self.MenuPath ) - self.ParentMenu.Menus[self.MenuPath] = nil - - return nil -end - - ---- The MENU_COALITION_COMMAND class --- @type MENU_COALITION_COMMAND --- @extends Menu#MENU -MENU_COALITION_COMMAND = { - ClassName = "MENU_COALITION_COMMAND" -} - ---- Creates a new radio command item for a group --- @param #MENU_COALITION_COMMAND self --- @param DCSCoalition#coalition.side MenuCoalition The coalition owning the menu. --- @param MenuText The text for the menu. --- @param ParentMenu The parent menu. --- @param CommandMenuFunction A function that is called when the menu key is pressed. --- @param CommandMenuArgument An argument for the function. --- @return #MENU_COALITION_COMMAND self -function MENU_COALITION_COMMAND:New( MenuCoalition, MenuText, ParentMenu, CommandMenuFunction, CommandMenuArgument ) - - -- Arrange meta tables - - local MenuParentPath = {} - if ParentMenu ~= nil then - MenuParentPath = ParentMenu.MenuPath - end - - local self = BASE:Inherit( self, MENU:New( MenuText, MenuParentPath ) ) - - self.MenuCoalition = MenuCoalition - self.MenuParentPath = MenuParentPath - self.MenuText = MenuText - self.ParentMenu = ParentMenu - - self:T( { MenuParentPath, MenuText, CommandMenuFunction, CommandMenuArgument } ) - - self.MenuPath = missionCommands.addCommandForCoalition( self.MenuCoalition, MenuText, MenuParentPath, CommandMenuFunction, CommandMenuArgument ) - - self.CommandMenuFunction = CommandMenuFunction - self.CommandMenuArgument = CommandMenuArgument - - ParentMenu.Menus[self.MenuPath] = self - - return self -end - ---- Removes a radio command item for a coalition --- @param #MENU_COALITION_COMMAND self --- @return #MENU_COALITION_COMMAND self -function MENU_COALITION_COMMAND:Remove() - self:F( self.MenuPath ) - - missionCommands.removeItemForCoalition( self.MenuCoalition, self.MenuPath ) - self.ParentMenu.Menus[self.MenuPath] = nil - return nil -end ---- This module contains the CONTROLLABLE class. --- --- 1) @{Controllable#CONTROLLABLE} class, extends @{Base#BASE} --- =========================================================== --- The @{Controllable#CONTROLLABLE} class is a wrapper class to handle the DCS Controllable objects: --- --- * Support all DCS Controllable APIs. --- * Enhance with Controllable specific APIs not in the DCS Controllable API set. --- * Handle local Controllable Controller. --- * Manage the "state" of the DCS Controllable. --- --- 1.1) CONTROLLABLE constructor --- ----------------------------- --- The CONTROLLABLE class provides the following functions to construct a CONTROLLABLE instance: --- --- * @{#CONTROLLABLE.New}(): Create a CONTROLLABLE instance. --- --- 1.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. --- 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. --- --- ### 1.2.1) Assigned task methods --- --- Assigned task methods make the controllable execute the task where the location of the (possible) targets of the task are known before being detected. --- This is different from the EnRoute tasks, where the targets of the task need to be detected before the task can be executed. --- --- Find below a list of the **assigned task** methods: --- --- * @{#CONTROLLABLE.TaskAttackControllable}: (AIR) Attack a Controllable. --- * @{#CONTROLLABLE.TaskAttackMapObject}: (AIR) Attacking the map object (building, structure, e.t.c). --- * @{#CONTROLLABLE.TaskAttackUnit}: (AIR) Attack the Unit. --- * @{#CONTROLLABLE.TaskBombing}: (AIR) Delivering weapon at the point on the ground. --- * @{#CONTROLLABLE.TaskBombingRunway}: (AIR) Delivering weapon on the runway. --- * @{#CONTROLLABLE.TaskEmbarking}: (AIR) Move the controllable to a Vec2 Point, wait for a defined duration and embark a controllable. --- * @{#CONTROLLABLE.TaskEmbarkToTransport}: (GROUND) Embark to a Transport landed at a location. --- * @{#CONTROLLABLE.TaskEscort}: (AIR) Escort another airborne controllable. --- * @{#CONTROLLABLE.TaskFAC_AttackControllable}: (AIR + GROUND) The task makes the controllable/unit a FAC and orders the FAC to control the target (enemy ground controllable) destruction. --- * @{#CONTROLLABLE.TaskFireAtPoint}: (GROUND) Fire at a VEC2 point until ammunition is finished. --- * @{#CONTROLLABLE.TaskFollow}: (AIR) Following another airborne controllable. --- * @{#CONTROLLABLE.TaskHold}: (GROUND) Hold ground controllable from moving. --- * @{#CONTROLLABLE.TaskHoldPosition}: (AIR) Hold position at the current position of the first unit of the controllable. --- * @{#CONTROLLABLE.TaskLand}: (AIR HELICOPTER) Landing at the ground. For helicopters only. --- * @{#CONTROLLABLE.TaskLandAtZone}: (AIR) Land the controllable at a @{Zone#ZONE_RADIUS). --- * @{#CONTROLLABLE.TaskOrbitCircle}: (AIR) Orbit at the current position of the first unit of the controllable at a specified alititude. --- * @{#CONTROLLABLE.TaskOrbitCircleAtVec2}: (AIR) Orbit at a specified position at a specified alititude during a specified duration with a specified speed. --- * @{#CONTROLLABLE.TaskRefueling}: (AIR) Refueling from the nearest tanker. No parameters. --- * @{#CONTROLLABLE.TaskRoute}: (AIR + GROUND) Return a Misson task to follow a given route defined by Points. --- * @{#CONTROLLABLE.TaskRouteToVec2}: (AIR + GROUND) Make the Controllable move to a given point. --- * @{#CONTROLLABLE.TaskRouteToVec3}: (AIR + GROUND) Make the Controllable move to a given point. --- * @{#CONTROLLABLE.TaskRouteToZone}: (AIR + GROUND) Route the controllable to a given zone. --- * @{#CONTROLLABLE.TaskReturnToBase}: (AIR) Route the controllable to an airbase. --- --- ### 1.2.2) EnRoute task methods --- --- EnRoute tasks require the targets of the task need to be detected by the controllable (using its sensors) before the task can be executed: --- --- * @{#CONTROLLABLE.EnRouteTaskAWACS}: (AIR) Aircraft will act as an AWACS for friendly units (will provide them with information about contacts). No parameters. --- * @{#CONTROLLABLE.EnRouteTaskEngageControllable}: (AIR) Engaging a controllable. The task does not assign the target controllable to the unit/controllable to attack now; it just allows the unit/controllable to engage the target controllable as well as other assigned targets. --- * @{#CONTROLLABLE.EnRouteTaskEngageTargets}: (AIR) Engaging targets of defined types. --- * @{#CONTROLLABLE.EnRouteTaskEWR}: (AIR) Attack the Unit. --- * @{#CONTROLLABLE.EnRouteTaskFAC}: (AIR + GROUND) The task makes the controllable/unit a FAC and lets the FAC to choose a targets (enemy ground controllable) around as well as other assigned targets. --- * @{#CONTROLLABLE.EnRouteTaskFAC_EngageControllable}: (AIR + GROUND) The task makes the controllable/unit a FAC and lets the FAC to choose the target (enemy ground controllable) as well as other assigned targets. --- * @{#CONTROLLABLE.EnRouteTaskTanker}: (AIR) Aircraft will act as a tanker for friendly units. No parameters. --- --- ### 1.2.3) Preparation task methods --- --- There are certain task methods that allow to tailor the task behaviour: --- --- * @{#CONTROLLABLE.TaskWrappedAction}: Return a WrappedAction Task taking a Command. --- * @{#CONTROLLABLE.TaskCombo}: Return a Combo Task taking an array of Tasks. --- * @{#CONTROLLABLE.TaskCondition}: Return a condition section for a controlled task. --- * @{#CONTROLLABLE.TaskControlled}: Return a Controlled Task taking a Task and a TaskCondition. --- --- ### 1.2.4) 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. --- --- 1.3) CONTROLLABLE 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. --- --- 1.4) CONTROLLABLE Option methods --- ------------------------- --- Controllable **Option methods** change the behaviour of the Controllable while being alive. --- --- ### 1.4.1) Rule of Engagement: --- --- * @{#CONTROLLABLE.OptionROEWeaponFree} --- * @{#CONTROLLABLE.OptionROEOpenFire} --- * @{#CONTROLLABLE.OptionROEReturnFire} --- * @{#CONTROLLABLE.OptionROEEvadeFire} --- --- To check whether an ROE option is valid for a specific controllable, use: --- --- * @{#CONTROLLABLE.OptionROEWeaponFreePossible} --- * @{#CONTROLLABLE.OptionROEOpenFirePossible} --- * @{#CONTROLLABLE.OptionROEReturnFirePossible} --- * @{#CONTROLLABLE.OptionROEEvadeFirePossible} --- --- ### 1.4.2) Rule on thread: --- --- * @{#CONTROLLABLE.OptionROTNoReaction} --- * @{#CONTROLLABLE.OptionROTPassiveDefense} --- * @{#CONTROLLABLE.OptionROTEvadeFire} --- * @{#CONTROLLABLE.OptionROTVertical} --- --- To test whether an ROT option is valid for a specific controllable, use: --- --- * @{#CONTROLLABLE.OptionROTNoReactionPossible} --- * @{#CONTROLLABLE.OptionROTPassiveDefensePossible} --- * @{#CONTROLLABLE.OptionROTEvadeFirePossible} --- * @{#CONTROLLABLE.OptionROTVerticalPossible} --- --- === --- --- @module Controllable --- @author FlightControl - ---- The CONTROLLABLE class --- @type CONTROLLABLE --- @extends Base#BASE --- @field DCSControllable#Controllable DCSControllable The DCS controllable class. --- @field #string ControllableName The name of the controllable. -CONTROLLABLE = { - ClassName = "CONTROLLABLE", - ControllableName = "", - ControllableID = 0, - Controller = nil, - DCSControllable = nil, - WayPointFunctions = {}, -} - ---- A DCSControllable --- @type DCSControllable --- @field id_ The ID of the controllable in DCS - ---- Create a new CONTROLLABLE from a DCSControllable --- @param #CONTROLLABLE self --- @param DCSControllable#Controllable ControllableName The DCS Controllable name --- @return #CONTROLLABLE self -function CONTROLLABLE:New( ControllableName ) - local self = BASE:Inherit( self, BASE:New() ) - self:F2( ControllableName ) - self.ControllableName = ControllableName - return self -end - --- DCS Controllable methods support. - ---- Get the controller for the CONTROLLABLE. --- @param #CONTROLLABLE self --- @return DCSController#Controller -function CONTROLLABLE:_GetController() - self:F2( { self.ControllableName } ) - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - local ControllableController = DCSControllable:getController() - self:T3( ControllableController ) - return ControllableController - end - - return nil -end - - - --- Tasks - ---- Popping current Task from the controllable. --- @param #CONTROLLABLE self --- @return Controllable#CONTROLLABLE self -function CONTROLLABLE:PopCurrentTask() - self:F2() - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - local Controller = self:_GetController() - Controller:popTask() - return self - end - - return nil -end - ---- Pushing Task on the queue from the controllable. --- @param #CONTROLLABLE self --- @return Controllable#CONTROLLABLE self -function CONTROLLABLE:PushTask( DCSTask, WaitTime ) - self:F2() - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - local Controller = self:_GetController() - - -- 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 ) - - if WaitTime then - --routines.scheduleFunction( Controller.pushTask, { Controller, DCSTask }, timer.getTime() + WaitTime ) - SCHEDULER:New( Controller, Controller.pushTask, { DCSTask }, WaitTime ) - else - Controller:pushTask( DCSTask ) - end - - return self - end - - return nil -end - ---- Clearing the Task Queue and Setting the Task on the queue from the controllable. --- @param #CONTROLLABLE self --- @return Controllable#CONTROLLABLE self -function CONTROLLABLE:SetTask( DCSTask, WaitTime ) - self:F2( { DCSTask } ) - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - - local Controller = self:_GetController() - - -- When a controllable SPAWNs, it takes about a second to get the controllable in the simulator. Setting tasks to unspawned controllables provides unexpected results. - -- Therefore we schedule the functions to set the mission and options for the Controllable. - -- Controller.setTask( Controller, DCSTask ) - - if not WaitTime then - WaitTime = 1 - end - --routines.scheduleFunction( Controller.setTask, { Controller, DCSTask }, timer.getTime() + WaitTime ) - SCHEDULER:New( Controller, Controller.setTask, { DCSTask }, WaitTime ) - - return self - end - - return nil -end - - ---- Return a condition section for a controlled task. --- @param #CONTROLLABLE self --- @param DCSTime#Time time --- @param #string userFlag --- @param #boolean userFlagValue --- @param #string condition --- @param DCSTime#Time duration --- @param #number lastWayPoint --- return DCSTask#Task -function CONTROLLABLE:TaskCondition( time, userFlag, userFlagValue, condition, duration, lastWayPoint ) - self:F2( { time, userFlag, userFlagValue, condition, duration, lastWayPoint } ) - - local DCSStopCondition = {} - DCSStopCondition.time = time - DCSStopCondition.userFlag = userFlag - DCSStopCondition.userFlagValue = userFlagValue - DCSStopCondition.condition = condition - DCSStopCondition.duration = duration - DCSStopCondition.lastWayPoint = lastWayPoint - - self:T3( { DCSStopCondition } ) - return DCSStopCondition -end - ---- Return a Controlled Task taking a Task and a TaskCondition. --- @param #CONTROLLABLE self --- @param DCSTask#Task DCSTask --- @param #DCSStopCondition DCSStopCondition --- @return DCSTask#Task -function CONTROLLABLE:TaskControlled( DCSTask, DCSStopCondition ) - self:F2( { DCSTask, DCSStopCondition } ) - - local DCSTaskControlled - - DCSTaskControlled = { - id = 'ControlledTask', - params = { - task = DCSTask, - stopCondition = DCSStopCondition - } - } - - self:T3( { DCSTaskControlled } ) - return DCSTaskControlled -end - ---- Return a Combo Task taking an array of Tasks. --- @param #CONTROLLABLE self --- @param DCSTask#TaskArray DCSTasks Array of @{DCSTask#Task} --- @return DCSTask#Task -function CONTROLLABLE:TaskCombo( DCSTasks ) - self:F2( { DCSTasks } ) - - local DCSTaskCombo - - DCSTaskCombo = { - id = 'ComboTask', - params = { - tasks = DCSTasks - } - } - - self:T3( { DCSTaskCombo } ) - return DCSTaskCombo -end - ---- Return a WrappedAction Task taking a Command. --- @param #CONTROLLABLE self --- @param DCSCommand#Command DCSCommand --- @return DCSTask#Task -function CONTROLLABLE:TaskWrappedAction( DCSCommand, Index ) - self:F2( { DCSCommand } ) - - local DCSTaskWrappedAction - - DCSTaskWrappedAction = { - id = "WrappedAction", - enabled = true, - number = Index, - auto = false, - params = { - action = DCSCommand, - }, - } - - self:T3( { DCSTaskWrappedAction } ) - return DCSTaskWrappedAction -end - ---- Executes a command action --- @param #CONTROLLABLE self --- @param DCSCommand#Command DCSCommand --- @return #CONTROLLABLE self -function CONTROLLABLE:SetCommand( DCSCommand ) - self:F2( DCSCommand ) - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - local Controller = self:_GetController() - Controller:setCommand( DCSCommand ) - return self - end - - return nil -end - ---- Perform a switch waypoint command --- @param #CONTROLLABLE self --- @param #number FromWayPoint --- @param #number ToWayPoint --- @return DCSTask#Task -function CONTROLLABLE:CommandSwitchWayPoint( FromWayPoint, ToWayPoint, Index ) - self:F2( { FromWayPoint, ToWayPoint, Index } ) - - local CommandSwitchWayPoint = { - id = 'SwitchWaypoint', - params = { - fromWaypointIndex = FromWayPoint, - goToWaypointIndex = ToWayPoint, - }, - } - - self:T3( { CommandSwitchWayPoint } ) - return CommandSwitchWayPoint -end - ---- Perform stop route command --- @param #CONTROLLABLE self --- @param #boolean StopRoute --- @return DCSTask#Task -function CONTROLLABLE:CommandStopRoute( StopRoute, Index ) - self:F2( { StopRoute, Index } ) - - local CommandStopRoute = { - id = 'StopRoute', - params = { - value = StopRoute, - }, - } - - self:T3( { CommandStopRoute } ) - return CommandStopRoute -end - - --- TASKS FOR AIR CONTROLLABLES - - ---- (AIR) Attack a Controllable. --- @param #CONTROLLABLE self --- @param Controllable#CONTROLLABLE AttackControllable 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 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 #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 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 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 #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 "AttackControllable" and "AttackUnit" tasks. If the flag is false then attackQty is a desired attack quantity for "Bombing" and "BombingRunway" tasks. --- @return DCSTask#Task The DCS task structure. -function CONTROLLABLE:TaskAttackControllable( AttackControllable, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit ) - self:F2( { self.ControllableName, AttackControllable, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit } ) - - -- AttackControllable = { - -- id = 'AttackControllable', - -- params = { - -- controllableId = Controllable.ID, - -- weaponType = number, - -- expend = enum AI.Task.WeaponExpend, - -- attackQty = number, - -- directionEnabled = boolean, - -- direction = Azimuth, - -- altitudeEnabled = boolean, - -- altitude = Distance, - -- attackQtyLimit = boolean, - -- } - -- } - - local DirectionEnabled = nil - if Direction then - DirectionEnabled = true - end - - local AltitudeEnabled = nil - if Altitude then - AltitudeEnabled = true - end - - local DCSTask - DCSTask = { id = 'AttackControllable', - params = { - controllableId = AttackControllable:GetID(), - weaponType = WeaponType, - expend = WeaponExpend, - attackQty = AttackQty, - directionEnabled = DirectionEnabled, - direction = Direction, - altitudeEnabled = AltitudeEnabled, - altitude = Altitude, - attackQtyLimit = AttackQtyLimit, - }, - }, - - self:T3( { DCSTask } ) - return DCSTask -end - - ---- (AIR) Attack the Unit. --- @param #CONTROLLABLE self --- @param Unit#UNIT AttackUnit The unit. --- @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 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 #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 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 #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 "AttackControllable" and "AttackUnit" tasks. If the flag is false then attackQty is a desired attack quantity for "Bombing" and "BombingRunway" tasks. --- @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 DCSTask#Task The DCS task structure. -function CONTROLLABLE:TaskAttackUnit( AttackUnit, WeaponType, WeaponExpend, AttackQty, Direction, AttackQtyLimit, ControllableAttack ) - self:F2( { self.ControllableName, AttackUnit, WeaponType, WeaponExpend, AttackQty, Direction, AttackQtyLimit, ControllableAttack } ) - - -- AttackUnit = { - -- id = 'AttackUnit', - -- params = { - -- unitId = Unit.ID, - -- weaponType = number, - -- expend = enum AI.Task.WeaponExpend - -- attackQty = number, - -- direction = Azimuth, - -- attackQtyLimit = boolean, - -- controllableAttack = boolean, - -- } - -- } - - local DCSTask - DCSTask = { id = 'AttackUnit', - params = { - unitId = AttackUnit:GetID(), - weaponType = WeaponType, - expend = WeaponExpend, - attackQty = AttackQty, - direction = Direction, - attackQtyLimit = AttackQtyLimit, - controllableAttack = ControllableAttack, - }, - }, - - self:T3( { DCSTask } ) - return DCSTask -end - - ---- (AIR) Delivering weapon at the point on the ground. --- @param #CONTROLLABLE self --- @param DCSTypes#Vec2 PointVec2 2D-coordinates of the point to deliver weapon at. --- @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 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 #number AttackQty (optional) Desired quantity of passes. The parameter is not the same in AttackControllable and AttackUnit tasks. --- @param 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 #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 DCSTask#Task The DCS task structure. -function CONTROLLABLE:TaskBombing( PointVec2, WeaponType, WeaponExpend, AttackQty, Direction, ControllableAttack ) - self:F2( { self.ControllableName, PointVec2, WeaponType, WeaponExpend, AttackQty, Direction, ControllableAttack } ) - --- Bombing = { --- id = 'Bombing', --- params = { --- point = Vec2, --- weaponType = number, --- expend = enum AI.Task.WeaponExpend, --- attackQty = number, --- direction = Azimuth, --- controllableAttack = boolean, --- } --- } - - local DCSTask - DCSTask = { id = 'Bombing', - params = { - point = PointVec2, - weaponType = WeaponType, - expend = WeaponExpend, - attackQty = AttackQty, - direction = Direction, - controllableAttack = ControllableAttack, - }, - }, - - self:T3( { DCSTask } ) - return DCSTask -end - ---- (AIR) Orbit at a specified position at a specified alititude during a specified duration with a specified speed. --- @param #CONTROLLABLE self --- @param 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. --- @return #CONTROLLABLE self -function CONTROLLABLE:TaskOrbitCircleAtVec2( Point, Altitude, Speed ) - self:F2( { self.ControllableName, Point, Altitude, Speed } ) - - -- pattern = enum AI.Task.OribtPattern, - -- point = Vec2, - -- point2 = Vec2, - -- speed = Distance, - -- altitude = Distance - - local LandHeight = land.getHeight( Point ) - - self:T3( { LandHeight } ) - - local DCSTask = { id = 'Orbit', - params = { pattern = AI.Task.OrbitPattern.CIRCLE, - point = Point, - speed = Speed, - altitude = Altitude + LandHeight - } - } - - - -- local AITask = { id = 'ControlledTask', - -- params = { task = { id = 'Orbit', - -- params = { pattern = AI.Task.OrbitPattern.CIRCLE, - -- point = Point, - -- speed = Speed, - -- altitude = Altitude + LandHeight - -- } - -- }, - -- stopCondition = { duration = Duration - -- } - -- } - -- } - -- ) - - return DCSTask -end - ---- (AIR) Orbit at 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. --- @return #CONTROLLABLE self -function CONTROLLABLE:TaskOrbitCircle( Altitude, Speed ) - self:F2( { self.ControllableName, Altitude, Speed } ) - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - local ControllablePoint = self:GetPointVec2() - return self:TaskOrbitCircleAtVec2( ControllablePoint, Altitude, Speed ) - end - - return nil -end - - - ---- (AIR) Hold position at the current position of the first unit of the controllable. --- @param #CONTROLLABLE self --- @param #number Duration The maximum duration in seconds to hold the position. --- @return #CONTROLLABLE self -function CONTROLLABLE:TaskHoldPosition() - self:F2( { self.ControllableName } ) - - return self:TaskOrbitCircle( 30, 10 ) -end - - - - ---- (AIR) Attacking the map object (building, structure, e.t.c). --- @param #CONTROLLABLE self --- @param DCSTypes#Vec2 PointVec2 2D-coordinates of the point the map object is closest to. The distance between the point and the map object must not be greater than 2000 meters. Object id is not used here because Mission Editor doesn't support map object identificators. --- @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 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 #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 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 #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 DCSTask#Task The DCS task structure. -function CONTROLLABLE:TaskAttackMapObject( PointVec2, WeaponType, WeaponExpend, AttackQty, Direction, ControllableAttack ) - self:F2( { self.ControllableName, PointVec2, WeaponType, WeaponExpend, AttackQty, Direction, ControllableAttack } ) - --- AttackMapObject = { --- id = 'AttackMapObject', --- params = { --- point = Vec2, --- weaponType = number, --- expend = enum AI.Task.WeaponExpend, --- attackQty = number, --- direction = Azimuth, --- controllableAttack = boolean, --- } --- } - - local DCSTask - DCSTask = { id = 'AttackMapObject', - params = { - point = PointVec2, - weaponType = WeaponType, - expend = WeaponExpend, - attackQty = AttackQty, - direction = Direction, - controllableAttack = ControllableAttack, - }, - }, - - self:T3( { DCSTask } ) - return DCSTask -end - - ---- (AIR) Delivering weapon on the runway. --- @param #CONTROLLABLE self --- @param 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 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 #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 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 #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 DCSTask#Task The DCS task structure. -function CONTROLLABLE:TaskBombingRunway( Airbase, WeaponType, WeaponExpend, AttackQty, Direction, ControllableAttack ) - self:F2( { self.ControllableName, Airbase, WeaponType, WeaponExpend, AttackQty, Direction, ControllableAttack } ) - --- BombingRunway = { --- id = 'BombingRunway', --- params = { --- runwayId = AirdromeId, --- weaponType = number, --- expend = enum AI.Task.WeaponExpend, --- attackQty = number, --- direction = Azimuth, --- controllableAttack = boolean, --- } --- } - - local DCSTask - DCSTask = { id = 'BombingRunway', - params = { - point = Airbase:GetID(), - weaponType = WeaponType, - expend = WeaponExpend, - attackQty = AttackQty, - direction = Direction, - controllableAttack = ControllableAttack, - }, - }, - - self:T3( { DCSTask } ) - return DCSTask -end - - ---- (AIR) Refueling from the nearest tanker. No parameters. --- @param #CONTROLLABLE self --- @return DCSTask#Task The DCS task structure. -function CONTROLLABLE:TaskRefueling() - self:F2( { self.ControllableName } ) - --- Refueling = { --- id = 'Refueling', --- params = {} --- } - - local DCSTask - DCSTask = { id = 'Refueling', - params = { - }, - }, - - self:T3( { DCSTask } ) - return DCSTask -end - - ---- (AIR HELICOPTER) Landing at the ground. For helicopters only. --- @param #CONTROLLABLE self --- @param DCSTypes#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 ) - self:F2( { self.ControllableName, Point, Duration } ) - --- Land = { --- id= 'Land', --- params = { --- point = Vec2, --- durationFlag = boolean, --- duration = Time --- } --- } - - local DCSTask - if Duration and Duration > 0 then - DCSTask = { id = 'Land', - params = { - point = Point, - durationFlag = true, - duration = Duration, - }, - } - else - DCSTask = { id = 'Land', - params = { - point = Point, - durationFlag = false, - }, - } - end - - self:T3( DCSTask ) - return DCSTask -end - ---- (AIR) Land the controllable at a @{Zone#ZONE_RADIUS). --- @param #CONTROLLABLE self --- @param Zone#ZONE Zone The zone where to land. --- @param #number Duration The duration in seconds to stay on the ground. --- @return #CONTROLLABLE self -function CONTROLLABLE:TaskLandAtZone( Zone, Duration, RandomPoint ) - self:F2( { self.ControllableName, Zone, Duration, RandomPoint } ) - - local Point - if RandomPoint then - Point = Zone:GetRandomVec2() - else - Point = Zone:GetPointVec2() - end - - local DCSTask = self:TaskLandAtVec2( Point, Duration ) - - self:T3( DCSTask ) - return DCSTask -end - - - ---- (AIR) Following another airborne controllable. --- The unit / controllable will follow lead unit of another controllable, wingmens of both controllables will continue following their leaders. --- If another controllable is on land the unit / controllable will orbit around. --- @param #CONTROLLABLE self --- @param Controllable#CONTROLLABLE FollowControllable The controllable to be followed. --- @param DCSTypes#Vec3 PointVec3 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 DCSTask#Task The DCS task structure. -function CONTROLLABLE:TaskFollow( FollowControllable, PointVec3, LastWaypointIndex ) - self:F2( { self.ControllableName, FollowControllable, PointVec3, LastWaypointIndex } ) - --- Follow = { --- id = 'Follow', --- params = { --- controllableId = Controllable.ID, --- pos = Vec3, --- lastWptIndexFlag = boolean, --- lastWptIndex = number --- } --- } - - local LastWaypointIndexFlag = nil - if LastWaypointIndex then - LastWaypointIndexFlag = true - end - - local DCSTask - DCSTask = { id = 'Follow', - params = { - controllableId = FollowControllable:GetID(), - pos = PointVec3, - lastWptIndexFlag = LastWaypointIndexFlag, - lastWptIndex = LastWaypointIndex, - }, - }, - - self:T3( { DCSTask } ) - return DCSTask -end - - ---- (AIR) Escort another airborne controllable. --- The unit / controllable will follow lead unit of another controllable, wingmens of both controllables will continue following their leaders. --- The unit / controllable will also protect that controllable from threats of specified types. --- @param #CONTROLLABLE self --- @param Controllable#CONTROLLABLE EscortControllable The controllable to be escorted. --- @param DCSTypes#Vec3 PointVec3 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 DCSTypes#AttributeNameArray TargetTypes Array of AttributeName that is contains threat categories allowed to engage. --- @return DCSTask#Task The DCS task structure. -function CONTROLLABLE:TaskEscort( FollowControllable, PointVec3, LastWaypointIndex, EngagementDistance, TargetTypes ) - self:F2( { self.ControllableName, FollowControllable, PointVec3, LastWaypointIndex, EngagementDistance, TargetTypes } ) - --- Escort = { --- id = 'Escort', --- params = { --- controllableId = Controllable.ID, --- pos = Vec3, --- lastWptIndexFlag = boolean, --- lastWptIndex = number, --- engagementDistMax = Distance, --- targetTypes = array of AttributeName, --- } --- } - - local LastWaypointIndexFlag = nil - if LastWaypointIndex then - LastWaypointIndexFlag = true - end - - local DCSTask - DCSTask = { id = 'Follow', - params = { - controllableId = FollowControllable:GetID(), - pos = PointVec3, - lastWptIndexFlag = LastWaypointIndexFlag, - lastWptIndex = LastWaypointIndex, - engagementDistMax = EngagementDistance, - targetTypes = TargetTypes, - }, - }, - - self:T3( { DCSTask } ) - return DCSTask -end - - --- GROUND TASKS - ---- (GROUND) Fire at a VEC2 point until ammunition is finished. --- @param #CONTROLLABLE self --- @param DCSTypes#Vec2 PointVec2 The point to fire at. --- @param DCSTypes#Distance Radius The radius of the zone to deploy the fire at. --- @return DCSTask#Task The DCS task structure. -function CONTROLLABLE:TaskFireAtPoint( PointVec2, Radius ) - self:F2( { self.ControllableName, PointVec2, Radius } ) - - -- FireAtPoint = { - -- id = 'FireAtPoint', - -- params = { - -- point = Vec2, - -- radius = Distance, - -- } - -- } - - local DCSTask - DCSTask = { id = 'FireAtPoint', - params = { - point = PointVec2, - radius = Radius, - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - ---- (GROUND) Hold ground controllable from moving. --- @param #CONTROLLABLE self --- @return DCSTask#Task The DCS task structure. -function CONTROLLABLE:TaskHold() - self:F2( { self.ControllableName } ) - --- Hold = { --- id = 'Hold', --- params = { --- } --- } - - local DCSTask - DCSTask = { id = 'Hold', - params = { - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - --- TASKS FOR AIRBORNE AND GROUND UNITS/CONTROLLABLES - ---- (AIR + GROUND) The task makes the controllable/unit a FAC and orders the FAC to control the target (enemy ground controllable) destruction. --- The killer is player-controlled allied CAS-aircraft that is in contact with the FAC. --- If the task is assigned to the controllable lead unit will be a FAC. --- @param #CONTROLLABLE self --- @param Controllable#CONTROLLABLE AttackControllable 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 DCSTypes#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 DCSTask#Task The DCS task structure. -function CONTROLLABLE:TaskFAC_AttackControllable( AttackControllable, WeaponType, Designation, Datalink ) - self:F2( { self.ControllableName, AttackControllable, WeaponType, Designation, Datalink } ) - --- FAC_AttackControllable = { --- id = 'FAC_AttackControllable', --- params = { --- controllableId = Controllable.ID, --- weaponType = number, --- designation = enum AI.Task.Designation, --- datalink = boolean --- } --- } - - local DCSTask - DCSTask = { id = 'FAC_AttackControllable', - params = { - controllableId = AttackControllable:GetID(), - weaponType = WeaponType, - designation = Designation, - datalink = Datalink, - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - --- EN-ROUTE TASKS FOR AIRBORNE CONTROLLABLES - ---- (AIR) Engaging targets of defined types. --- @param #CONTROLLABLE self --- @param 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 DCSTypes#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 DCSTask#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskEngageTargets( Distance, TargetTypes, Priority ) - self:F2( { self.ControllableName, Distance, TargetTypes, Priority } ) - --- EngageTargets ={ --- id = 'EngageTargets', --- params = { --- maxDist = Distance, --- targetTypes = array of AttributeName, --- priority = number --- } --- } - - local DCSTask - DCSTask = { id = 'EngageTargets', - params = { - maxDist = Distance, - targetTypes = TargetTypes, - priority = Priority - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - - ---- (AIR) Engaging a targets of defined types at circle-shaped zone. --- @param #CONTROLLABLE self --- @param DCSTypes#Vec2 PointVec2 2D-coordinates of the zone. --- @param DCSTypes#Distance Radius Radius of the zone. --- @param DCSTypes#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 DCSTask#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskEngageTargets( PointVec2, Radius, TargetTypes, Priority ) - self:F2( { self.ControllableName, PointVec2, Radius, TargetTypes, Priority } ) - --- EngageTargetsInZone = { --- id = 'EngageTargetsInZone', --- params = { --- point = Vec2, --- zoneRadius = Distance, --- targetTypes = array of AttributeName, --- priority = number --- } --- } - - local DCSTask - DCSTask = { id = 'EngageTargetsInZone', - params = { - point = PointVec2, - zoneRadius = Radius, - targetTypes = TargetTypes, - priority = Priority - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - ---- (AIR) Engaging a controllable. The task does not assign the target controllable to the unit/controllable to attack now; it just allows the unit/controllable to engage the target controllable as well as other assigned targets. --- @param #CONTROLLABLE self --- @param Controllable#CONTROLLABLE AttackControllable 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 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 #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 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 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 #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 "AttackControllable" and "AttackUnit" tasks. If the flag is false then attackQty is a desired attack quantity for "Bombing" and "BombingRunway" tasks. --- @return DCSTask#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskEngageControllable( AttackControllable, Priority, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit ) - self:F2( { self.ControllableName, AttackControllable, Priority, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit } ) - - -- EngageControllable = { - -- id = 'EngageControllable ', - -- params = { - -- controllableId = Controllable.ID, - -- weaponType = number, - -- expend = enum AI.Task.WeaponExpend, - -- attackQty = number, - -- directionEnabled = boolean, - -- direction = Azimuth, - -- altitudeEnabled = boolean, - -- altitude = Distance, - -- attackQtyLimit = boolean, - -- priority = number, - -- } - -- } - - local DirectionEnabled = nil - if Direction then - DirectionEnabled = true - end - - local AltitudeEnabled = nil - if Altitude then - AltitudeEnabled = true - end - - local DCSTask - DCSTask = { id = 'EngageControllable', - params = { - controllableId = AttackControllable:GetID(), - weaponType = WeaponType, - expend = WeaponExpend, - attackQty = AttackQty, - directionEnabled = DirectionEnabled, - direction = Direction, - altitudeEnabled = AltitudeEnabled, - altitude = Altitude, - attackQtyLimit = AttackQtyLimit, - priority = Priority, - }, - }, - - self:T3( { DCSTask } ) - return DCSTask -end - - ---- (AIR) Attack the Unit. --- @param #CONTROLLABLE self --- @param Unit#UNIT AttackUnit The UNIT. --- @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 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 #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 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 #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 "AttackControllable" and "AttackUnit" tasks. If the flag is false then attackQty is a desired attack quantity for "Bombing" and "BombingRunway" tasks. --- @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 DCSTask#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskEngageUnit( AttackUnit, Priority, WeaponType, WeaponExpend, AttackQty, Direction, AttackQtyLimit, ControllableAttack ) - self:F2( { self.ControllableName, AttackUnit, Priority, WeaponType, WeaponExpend, AttackQty, Direction, AttackQtyLimit, ControllableAttack } ) - - -- EngageUnit = { - -- id = 'EngageUnit', - -- params = { - -- unitId = Unit.ID, - -- weaponType = number, - -- expend = enum AI.Task.WeaponExpend - -- attackQty = number, - -- direction = Azimuth, - -- attackQtyLimit = boolean, - -- controllableAttack = boolean, - -- priority = number, - -- } - -- } - - local DCSTask - DCSTask = { id = 'EngageUnit', - params = { - unitId = AttackUnit:GetID(), - weaponType = WeaponType, - expend = WeaponExpend, - attackQty = AttackQty, - direction = Direction, - attackQtyLimit = AttackQtyLimit, - controllableAttack = ControllableAttack, - priority = Priority, - }, - }, - - self:T3( { DCSTask } ) - return DCSTask -end - - - ---- (AIR) Aircraft will act as an AWACS for friendly units (will provide them with information about contacts). No parameters. --- @param #CONTROLLABLE self --- @return DCSTask#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskAWACS( ) - self:F2( { self.ControllableName } ) - --- AWACS = { --- id = 'AWACS', --- params = { --- } --- } - - local DCSTask - DCSTask = { id = 'AWACS', - params = { - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - ---- (AIR) Aircraft will act as a tanker for friendly units. No parameters. --- @param #CONTROLLABLE self --- @return DCSTask#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskTanker( ) - self:F2( { self.ControllableName } ) - --- Tanker = { --- id = 'Tanker', --- params = { --- } --- } - - local DCSTask - DCSTask = { id = 'Tanker', - params = { - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - --- En-route tasks for ground units/controllables - ---- (GROUND) Ground unit (EW-radar) will act as an EWR for friendly units (will provide them with information about contacts). No parameters. --- @param #CONTROLLABLE self --- @return DCSTask#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskEWR( ) - self:F2( { self.ControllableName } ) - --- EWR = { --- id = 'EWR', --- params = { --- } --- } - - local DCSTask - DCSTask = { id = 'EWR', - params = { - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - --- En-route tasks for airborne and ground units/controllables - ---- (AIR + GROUND) The task makes the controllable/unit a FAC and lets the FAC to choose the target (enemy ground controllable) as well as other assigned targets. --- The killer is player-controlled allied CAS-aircraft that is in contact with the FAC. --- If the task is assigned to the controllable lead unit will be a FAC. --- @param #CONTROLLABLE self --- @param Controllable#CONTROLLABLE AttackControllable 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 DCSTypes#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 DCSTask#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskFAC_EngageControllable( AttackControllable, Priority, WeaponType, Designation, Datalink ) - self:F2( { self.ControllableName, AttackControllable, WeaponType, Priority, Designation, Datalink } ) - --- FAC_EngageControllable = { --- id = 'FAC_EngageControllable', --- params = { --- controllableId = Controllable.ID, --- weaponType = number, --- designation = enum AI.Task.Designation, --- datalink = boolean, --- priority = number, --- } --- } - - local DCSTask - DCSTask = { id = 'FAC_EngageControllable', - params = { - controllableId = AttackControllable:GetID(), - weaponType = WeaponType, - designation = Designation, - datalink = Datalink, - priority = Priority, - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - ---- (AIR + GROUND) The task makes the controllable/unit a FAC and lets the FAC to choose a targets (enemy ground controllable) around as well as other assigned targets. --- 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 DCSTypes#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 DCSTask#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskFAC( Radius, Priority ) - self:F2( { self.ControllableName, Radius, Priority } ) - --- FAC = { --- id = 'FAC', --- params = { --- radius = Distance, --- priority = number --- } --- } - - local DCSTask - DCSTask = { id = 'FAC', - params = { - radius = Radius, - priority = Priority - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - - - ---- (AIR) Move the controllable to a Vec2 Point, wait for a defined duration and embark a controllable. --- @param #CONTROLLABLE self --- @param DCSTypes#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 DCSTask#Task The DCS task structure -function CONTROLLABLE:TaskEmbarking( Point, Duration, EmbarkingControllable ) - self:F2( { self.ControllableName, Point, Duration, EmbarkingControllable.DCSControllable } ) - - local DCSTask - DCSTask = { id = 'Embarking', - params = { x = Point.x, - y = Point.y, - duration = Duration, - controllablesForEmbarking = { EmbarkingControllable.ControllableID }, - durationFlag = true, - distributionFlag = false, - distribution = {}, - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - ---- (GROUND) Embark to a Transport landed at a location. - ---- Move to a defined Vec2 Point, and embark to a controllable when arrived within a defined Radius. --- @param #CONTROLLABLE self --- @param DCSTypes#Vec2 Point The point where to wait. --- @param #number Radius The radius of the embarking zone around the Point. --- @return DCSTask#Task The DCS task structure. -function CONTROLLABLE:TaskEmbarkToTransport( Point, Radius ) - self:F2( { self.ControllableName, Point, Radius } ) - - local DCSTask --DCSTask#Task - DCSTask = { id = 'EmbarkToTransport', - params = { x = Point.x, - y = Point.y, - zoneRadius = Radius, - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - - ---- (AIR + GROUND) Return a mission task from a mission template. --- @param #CONTROLLABLE self --- @param #table TaskMission A table containing the mission task. --- @return DCSTask#Task -function CONTROLLABLE:TaskMission( TaskMission ) - self:F2( Points ) - - local DCSTask - DCSTask = { id = 'Mission', params = { TaskMission, }, } - - self:T3( { DCSTask } ) - return DCSTask -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 DCSTask#Task -function CONTROLLABLE:TaskRoute( Points ) - self:F2( Points ) - - local DCSTask - DCSTask = { id = 'Mission', params = { route = { points = Points, }, }, } - - self:T3( { DCSTask } ) - return DCSTask -end - ---- (AIR + GROUND) Make the Controllable move to fly to a given point. --- @param #CONTROLLABLE self --- @param DCSTypes#Vec3 Point The destination point in Vec3 format. --- @param #number Speed The speed to travel. --- @return #CONTROLLABLE self -function CONTROLLABLE:TaskRouteToVec2( Point, Speed ) - self:F2( { Point, Speed } ) - - local ControllablePoint = self:GetUnit( 1 ):GetPointVec2() - - 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 DCSTypes#Vec3 Point The destination point in Vec3 format. --- @param #number Speed The speed to travel. --- @return #CONTROLLABLE self -function CONTROLLABLE:TaskRouteToVec3( Point, Speed ) - self:F2( { Point, Speed } ) - - local ControllablePoint = self:GetUnit( 1 ):GetPointVec3() - - local PointFrom = {} - PointFrom.x = ControllablePoint.x - PointFrom.y = ControllablePoint.z - PointFrom.alt = ControllablePoint.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 GoPoints A table of Route Points. --- @return #CONTROLLABLE self -function CONTROLLABLE:Route( GoPoints ) - self:F2( GoPoints ) - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - local Points = routines.utils.deepCopy( GoPoints ) - local MissionTask = { id = 'Mission', params = { route = { points = Points, }, }, } - local Controller = self:_GetController() - --Controller.setTask( Controller, MissionTask ) - --routines.scheduleFunction( Controller.setTask, { Controller, MissionTask}, timer.getTime() + 1 ) - SCHEDULER:New( Controller, Controller.setTask, { MissionTask }, 1 ) - return self - end - - return nil -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 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:GetPointVec2() - - local PointFrom = {} - PointFrom.x = ControllablePoint.x - PointFrom.y = ControllablePoint.y - PointFrom.type = "Turning Point" - PointFrom.action = "Cone" - PointFrom.speed = 20 / 1.6 - - - local PointTo = {} - local ZonePoint - - if Randomize then - ZonePoint = Zone:GetRandomVec2() - else - ZonePoint = Zone:GetPointVec2() - 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 / 1.6 - end - - local Points = { PointFrom, PointTo } - - self:T3( Points ) - - self:Route( Points ) - - return self - end - - return nil -end - ---- (AIR) Return the Controllable to an @{Airbase#AIRBASE} --- A speed can be given in km/h. --- A given formation can be given. --- @param #CONTROLLABLE self --- @param Airbase#AIRBASE ReturnAirbase The @{Airbase#AIRBASE} to return to. --- @param #number Speed (optional) The speed. --- @return #string The route -function CONTROLLABLE:RouteReturnToAirbase( ReturnAirbase, Speed ) - self:F2( { ReturnAirbase, Speed } ) - --- Example --- [4] = --- { --- ["alt"] = 45, --- ["type"] = "Land", --- ["action"] = "Landing", --- ["alt_type"] = "BARO", --- ["formation_template"] = "", --- ["properties"] = --- { --- ["vnav"] = 1, --- ["scale"] = 0, --- ["angle"] = 0, --- ["vangle"] = 0, --- ["steer"] = 2, --- }, -- end of ["properties"] --- ["ETA"] = 527.81058817743, --- ["airdromeId"] = 12, --- ["y"] = 243127.2973737, --- ["x"] = -5406.2803440839, --- ["name"] = "DictKey_WptName_53", --- ["speed"] = 138.88888888889, --- ["ETA_locked"] = false, --- ["task"] = --- { --- ["id"] = "ComboTask", --- ["params"] = --- { --- ["tasks"] = --- { --- }, -- end of ["tasks"] --- }, -- end of ["params"] --- }, -- end of ["task"] --- ["speed_locked"] = true, --- }, -- end of [4] - - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - - local ControllablePoint = self:GetPointVec2() - local ControllableVelocity = self:GetMaxVelocity() - - local PointFrom = {} - PointFrom.x = ControllablePoint.x - PointFrom.y = ControllablePoint.y - PointFrom.type = "Turning Point" - PointFrom.action = "Turning Point" - PointFrom.speed = ControllableVelocity - - - local PointTo = {} - local AirbasePoint = ReturnAirbase:GetPointVec2() - - PointTo.x = AirbasePoint.x - PointTo.y = AirbasePoint.y - PointTo.type = "Land" - PointTo.action = "Landing" - PointTo.airdromeId = ReturnAirbase:GetID()-- Airdrome ID - self:T(PointTo.airdromeId) - --PointTo.alt = 0 - - local Points = { PointFrom, PointTo } - - self:T3( Points ) - - local Route = { points = Points, } - - return Route - end - - return nil -end - --- Commands - ---- Do Script command --- @param #CONTROLLABLE self --- @param #string DoScript --- @return #DCSCommand -function CONTROLLABLE:CommandDoScript( DoScript ) - - local DCSDoScript = { - id = "Script", - params = { - command = DoScript, - }, - } - - self:T3( DCSDoScript ) - return DCSDoScript -end - - ---- Return the mission template of the controllable. --- @param #CONTROLLABLE self --- @return #table The MissionTemplate --- TODO: Rework the method how to retrieve a template ... -function CONTROLLABLE:GetTaskMission() - self:F2( self.ControllableName ) - - return routines.utils.deepCopy( _DATABASE.Templates.Controllables[self.ControllableName].Template ) -end - ---- Return the mission route of the controllable. --- @param #CONTROLLABLE self --- @return #table The mission route defined by points. -function CONTROLLABLE:GetTaskRoute() - self:F2( self.ControllableName ) - - return routines.utils.deepCopy( _DATABASE.Templates.Controllables[self.ControllableName].Template.route.points ) -end - ---- Return the route of a controllable by using the @{Database#DATABASE} class. --- @param #CONTROLLABLE self --- @param #number Begin The route point from where the copy will start. The base route point is 0. --- @param #number End The route point where the copy will end. The End point is the last point - the End point. The last point has base 0. --- @param #boolean Randomize Randomization of the route, when true. --- @param #number Radius When randomization is on, the randomization is within the radius. -function CONTROLLABLE:CopyRoute( Begin, End, Randomize, Radius ) - self:F2( { Begin, End } ) - - local Points = {} - - -- Could be a Spawned Controllable - local ControllableName = string.match( self:GetName(), ".*#" ) - if ControllableName then - ControllableName = ControllableName:sub( 1, -2 ) - else - ControllableName = self:GetName() - end - - self:T3( { ControllableName } ) - - local Template = _DATABASE.Templates.Controllables[ControllableName].Template - - if Template then - if not Begin then - Begin = 0 - end - if not End then - End = 0 - end - - for TPointID = Begin + 1, #Template.route.points - End do - if Template.route.points[TPointID] then - Points[#Points+1] = routines.utils.deepCopy( Template.route.points[TPointID] ) - if Randomize then - if not Radius then - Radius = 500 - end - Points[#Points].x = Points[#Points].x + math.random( Radius * -1, Radius ) - Points[#Points].y = Points[#Points].y + math.random( Radius * -1, Radius ) - end - end - end - return Points - else - error( "Template not found for Controllable : " .. ControllableName ) - end - - return nil -end - - ---- Return the detected targets of the controllable. --- The optional parametes specify the detection methods that can be applied. --- If no detection method is given, the detection will use all the available methods by default. --- @param Controllable#CONTROLLABLE self --- @param #boolean DetectVisual (optional) --- @param #boolean DetectOptical (optional) --- @param #boolean DetectRadar (optional) --- @param #boolean DetectIRST (optional) --- @param #boolean DetectRWR (optional) --- @param #boolean DetectDLINK (optional) --- @return #table DetectedTargets -function CONTROLLABLE:GetDetectedTargets( DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK ) - self:F2( self.ControllableName ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local DetectionVisual = ( DetectVisual and DetectVisual == true ) and Controller.Detection.VISUAL or nil - local DetectionOptical = ( DetectOptical and DetectOptical == true ) and Controller.Detection.OPTICAL or nil - local DetectionRadar = ( DetectRadar and DetectRadar == true ) and Controller.Detection.RADAR or nil - local DetectionIRST = ( DetectIRST and DetectIRST == true ) and Controller.Detection.IRST or nil - local DetectionRWR = ( DetectRWR and DetectRWR == true ) and Controller.Detection.RWR or nil - local DetectionDLINK = ( DetectDLINK and DetectDLINK == true ) and Controller.Detection.DLINK or nil - - - return self:_GetController():getDetectedTargets( DetectionVisual, DetectionOptical, DetectionRadar, DetectionIRST, DetectionRWR, DetectionDLINK ) - end - - return nil -end - -function CONTROLLABLE:IsTargetDetected( DCSObject ) - self:F2( self.ControllableName ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - - local TargetIsDetected, TargetIsVisible, TargetLastTime, TargetKnowType, TargetKnowDistance, TargetLastPos, TargetLastVelocity - = self:_GetController().isTargetDetected( self:_GetController(), DCSObject, - Controller.Detection.VISUAL, - Controller.Detection.OPTIC, - Controller.Detection.RADAR, - Controller.Detection.IRST, - Controller.Detection.RWR, - Controller.Detection.DLINK - ) - return TargetIsDetected, TargetIsVisible, TargetLastTime, TargetKnowType, TargetKnowDistance, TargetLastPos, TargetLastVelocity - end - - return nil -end - --- Options - ---- Can the CONTROLLABLE hold their weapons? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROEHoldFirePossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() or self:IsGround() or self:IsShip() then - return true - end - - return false - end - - return nil -end - ---- Holding weapons. --- @param Controllable#CONTROLLABLE self --- @return Controllable#CONTROLLABLE self -function CONTROLLABLE:OptionROEHoldFire() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD ) - elseif self:IsGround() then - Controller:setOption( AI.Option.Ground.id.ROE, AI.Option.Ground.val.ROE.WEAPON_HOLD ) - elseif self:IsShip() then - Controller:setOption( AI.Option.Naval.id.ROE, AI.Option.Naval.val.ROE.WEAPON_HOLD ) - end - - return self - end - - return nil -end - ---- Can the CONTROLLABLE attack returning on enemy fire? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROEReturnFirePossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() or self:IsGround() or self:IsShip() then - return true - end - - return false - end - - return nil -end - ---- Return fire. --- @param #CONTROLLABLE self --- @return #CONTROLLABLE self -function CONTROLLABLE:OptionROEReturnFire() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.RETURN_FIRE ) - elseif self:IsGround() then - Controller:setOption( AI.Option.Ground.id.ROE, AI.Option.Ground.val.ROE.RETURN_FIRE ) - elseif self:IsShip() then - Controller:setOption( AI.Option.Naval.id.ROE, AI.Option.Naval.val.ROE.RETURN_FIRE ) - end - - return self - end - - return nil -end - ---- Can the CONTROLLABLE attack designated targets? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROEOpenFirePossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() or self:IsGround() or self:IsShip() then - return true - end - - return false - end - - return nil -end - ---- Openfire. --- @param #CONTROLLABLE self --- @return #CONTROLLABLE self -function CONTROLLABLE:OptionROEOpenFire() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE ) - elseif self:IsGround() then - Controller:setOption( AI.Option.Ground.id.ROE, AI.Option.Ground.val.ROE.OPEN_FIRE ) - elseif self:IsShip() then - Controller:setOption( AI.Option.Naval.id.ROE, AI.Option.Naval.val.ROE.OPEN_FIRE ) - end - - return self - end - - return nil -end - ---- Can the CONTROLLABLE attack targets of opportunity? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROEWeaponFreePossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() then - return true - end - - return false - end - - return nil -end - ---- Weapon free. --- @param #CONTROLLABLE self --- @return #CONTROLLABLE self -function CONTROLLABLE:OptionROEWeaponFree() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_FREE ) - end - - return self - end - - return nil -end - ---- Can the CONTROLLABLE ignore enemy fire? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROTNoReactionPossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() then - return true - end - - return false - end - - return nil -end - - ---- No evasion on enemy threats. --- @param #CONTROLLABLE self --- @return #CONTROLLABLE self -function CONTROLLABLE:OptionROTNoReaction() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.NO_REACTION ) - end - - return self - end - - return nil -end - ---- Can the CONTROLLABLE evade using passive defenses? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROTPassiveDefensePossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() then - return true - end - - return false - end - - return nil -end - ---- Evasion passive defense. --- @param #CONTROLLABLE self --- @return #CONTROLLABLE self -function CONTROLLABLE:OptionROTPassiveDefense() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.PASSIVE_DEFENCE ) - end - - return self - end - - return nil -end - ---- Can the CONTROLLABLE evade on enemy fire? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROTEvadeFirePossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() then - return true - end - - return false - end - - return nil -end - - ---- Evade on fire. --- @param #CONTROLLABLE self --- @return #CONTROLLABLE self -function CONTROLLABLE:OptionROTEvadeFire() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) - end - - return self - end - - return nil -end - ---- Can the CONTROLLABLE evade on fire using vertical manoeuvres? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROTVerticalPossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() then - return true - end - - return false - end - - return nil -end - - ---- Evade on fire using vertical manoeuvres. --- @param #CONTROLLABLE self --- @return #CONTROLLABLE self -function CONTROLLABLE:OptionROTVertical() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.BYPASS_AND_ESCAPE ) - end - - return self - end - - return nil -end - ---- 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 @{Controllable@CONTROLLABLE:WayPointExecute) to start the execution of the new mission plan. --- Note that when WayPointInitialize is called, the Mission of the controllable is RESTARTED! --- @param #CONTROLLABLE self --- @param #table WayPoints If WayPoints is given, then use the route. --- @return #CONTROLLABLE -function CONTROLLABLE:WayPointInitialize( WayPoints ) - - if WayPoints then - self.WayPoints = WayPoints - else - self.WayPoints = self:GetTaskRoute() - end - - return self -end - - ---- Registers a waypoint function that will be executed when the controllable moves over the WayPoint. --- @param #CONTROLLABLE self --- @param #number WayPoint The waypoint number. Note that the start waypoint on the route is WayPoint 1! --- @param #number WayPointIndex When defining multiple WayPoint functions for one WayPoint, use WayPointIndex to set the sequence of actions. --- @param #function WayPointFunction The waypoint function to be called when the controllable moves over the waypoint. The waypoint function takes variable parameters. --- @return #CONTROLLABLE -function CONTROLLABLE:WayPointFunction( WayPoint, WayPointIndex, WayPointFunction, ... ) - self:F2( { WayPoint, WayPointIndex, WayPointFunction } ) - - table.insert( self.WayPoints[WayPoint].task.params.tasks, WayPointIndex ) - self.WayPoints[WayPoint].task.params.tasks[WayPointIndex] = self:TaskFunction( WayPoint, WayPointIndex, WayPointFunction, arg ) - return self -end - - -function CONTROLLABLE:TaskFunction( WayPoint, WayPointIndex, FunctionString, FunctionArguments ) - self:F2( { WayPoint, WayPointIndex, FunctionString, FunctionArguments } ) - - local DCSTask - - local DCSScript = {} - DCSScript[#DCSScript+1] = "local MissionControllable = CONTROLLABLE:Find( ... ) " - - if FunctionArguments and #FunctionArguments > 0 then - DCSScript[#DCSScript+1] = FunctionString .. "( MissionControllable, " .. table.concat( FunctionArguments, "," ) .. ")" - else - DCSScript[#DCSScript+1] = FunctionString .. "( MissionControllable )" - end - - DCSTask = self:TaskWrappedAction( - self:CommandDoScript( - table.concat( DCSScript ) - ), WayPointIndex - ) - - self:T3( DCSTask ) - - return DCSTask - -end - ---- Executes the WayPoint plan. --- The function gets a WayPoint parameter, that you can use to restart the mission at a specific WayPoint. --- Note that when the WayPoint parameter is used, the new start mission waypoint of the controllable will be 1! --- @param #CONTROLLABLE self --- @param #number WayPoint The WayPoint from where to execute the mission. --- @param #number WaitTime The amount seconds to wait before initiating the mission. --- @return #CONTROLLABLE -function CONTROLLABLE:WayPointExecute( WayPoint, WaitTime ) - - if not WayPoint then - WayPoint = 1 - end - - -- When starting the mission from a certain point, the TaskPoints need to be deleted before the given WayPoint. - for TaskPointID = 1, WayPoint - 1 do - table.remove( self.WayPoints, 1 ) - end - - self:T3( self.WayPoints ) - - self:SetTask( self:TaskRoute( self.WayPoints ), WaitTime ) - - return self -end - - ---- This module contains the GROUP class. --- --- 1) @{Group#GROUP} class, extends @{Controllable#CONTROLLABLE} --- ============================================================= --- The @{Group#GROUP} class is a wrapper class to handle the DCS Group objects: --- --- * Support all DCS Group APIs. --- * Enhance with Group specific APIs not in the DCS Group API set. --- * Handle local Group Controller. --- * Manage the "state" of the DCS Group. --- --- **IMPORTANT: ONE SHOULD NEVER SANATIZE these GROUP OBJECT REFERENCES! (make the GROUP object references nil).** --- --- 1.1) GROUP reference methods --- ----------------------- --- 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. --- --- 1.2) GROUP task methods --- ----------------------- --- Several group 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 @{Group#GROUP.PushTask} or @{Group#SetTask} method to assign the task to the GROUP. --- Tasks are specific for the category of the GROUP, more specific, for AIR, GROUND or AIR and GROUND. --- Each task description where applicable indicates for which group category the task is valid. --- There are 2 main subdivisions of tasks: Assigned tasks and EnRoute tasks. --- --- ### 1.2.1) Assigned task methods --- --- Assigned task methods make the group execute the task where the location of the (possible) targets of the task are known before being detected. --- This is different from the EnRoute tasks, where the targets of the task need to be detected before the task can be executed. --- --- Find below a list of the **assigned task** methods: --- --- * @{#GROUP.TaskAttackGroup}: (AIR) Attack a Group. --- * @{#GROUP.TaskAttackMapObject}: (AIR) Attacking the map object (building, structure, e.t.c). --- * @{#GROUP.TaskAttackUnit}: (AIR) Attack the Unit. --- * @{#GROUP.TaskBombing}: (AIR) Delivering weapon at the point on the ground. --- * @{#GROUP.TaskBombingRunway}: (AIR) Delivering weapon on the runway. --- * @{#GROUP.TaskEmbarking}: (AIR) Move the group to a Vec2 Point, wait for a defined duration and embark a group. --- * @{#GROUP.TaskEmbarkToTransport}: (GROUND) Embark to a Transport landed at a location. --- * @{#GROUP.TaskEscort}: (AIR) Escort another airborne group. --- * @{#GROUP.TaskFAC_AttackGroup}: (AIR + GROUND) The task makes the group/unit a FAC and orders the FAC to control the target (enemy ground group) destruction. --- * @{#GROUP.TaskFireAtPoint}: (GROUND) Fire at a VEC2 point until ammunition is finished. --- * @{#GROUP.TaskFollow}: (AIR) Following another airborne group. --- * @{#GROUP.TaskHold}: (GROUND) Hold ground group from moving. --- * @{#GROUP.TaskHoldPosition}: (AIR) Hold position at the current position of the first unit of the group. --- * @{#GROUP.TaskLand}: (AIR HELICOPTER) Landing at the ground. For helicopters only. --- * @{#GROUP.TaskLandAtZone}: (AIR) Land the group at a @{Zone#ZONE_RADIUS). --- * @{#GROUP.TaskOrbitCircle}: (AIR) Orbit at the current position of the first unit of the group at a specified alititude. --- * @{#GROUP.TaskOrbitCircleAtVec2}: (AIR) Orbit at a specified position at a specified alititude during a specified duration with a specified speed. --- * @{#GROUP.TaskRefueling}: (AIR) Refueling from the nearest tanker. No parameters. --- * @{#GROUP.TaskRoute}: (AIR + GROUND) Return a Misson task to follow a given route defined by Points. --- * @{#GROUP.TaskRouteToVec2}: (AIR + GROUND) Make the Group move to a given point. --- * @{#GROUP.TaskRouteToVec3}: (AIR + GROUND) Make the Group move to a given point. --- * @{#GROUP.TaskRouteToZone}: (AIR + GROUND) Route the group to a given zone. --- * @{#GROUP.TaskReturnToBase}: (AIR) Route the group to an airbase. --- --- ### 1.2.2) EnRoute task methods --- --- EnRoute tasks require the targets of the task need to be detected by the group (using its sensors) before the task can be executed: --- --- * @{#GROUP.EnRouteTaskAWACS}: (AIR) Aircraft will act as an AWACS for friendly units (will provide them with information about contacts). No parameters. --- * @{#GROUP.EnRouteTaskEngageGroup}: (AIR) Engaging a group. The task does not assign the target group to the unit/group to attack now; it just allows the unit/group to engage the target group as well as other assigned targets. --- * @{#GROUP.EnRouteTaskEngageTargets}: (AIR) Engaging targets of defined types. --- * @{#GROUP.EnRouteTaskEWR}: (AIR) Attack the Unit. --- * @{#GROUP.EnRouteTaskFAC}: (AIR + GROUND) The task makes the group/unit a FAC and lets the FAC to choose a targets (enemy ground group) around as well as other assigned targets. --- * @{#GROUP.EnRouteTaskFAC_EngageGroup}: (AIR + GROUND) The task makes the group/unit a FAC and lets the FAC to choose the target (enemy ground group) as well as other assigned targets. --- * @{#GROUP.EnRouteTaskTanker}: (AIR) Aircraft will act as a tanker for friendly units. No parameters. --- --- ### 1.2.3) Preparation task methods --- --- There are certain task methods that allow to tailor the task behaviour: --- --- * @{#GROUP.TaskWrappedAction}: Return a WrappedAction Task taking a Command. --- * @{#GROUP.TaskCombo}: Return a Combo Task taking an array of Tasks. --- * @{#GROUP.TaskCondition}: Return a condition section for a controlled task. --- * @{#GROUP.TaskControlled}: Return a Controlled Task taking a Task and a TaskCondition. --- --- ### 1.2.4) 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: --- --- * @{#GROUP.TaskMission}: (AIR + GROUND) Return a mission task from a mission template. --- --- 1.3) GROUP Command methods --- -------------------------- --- Group **command methods** prepare the execution of commands using the @{#GROUP.SetCommand} method: --- --- * @{#GROUP.CommandDoScript}: Do Script command. --- * @{#GROUP.CommandSwitchWayPoint}: Perform a switch waypoint command. --- --- 1.4) GROUP Option methods --- ------------------------- --- Group **Option methods** change the behaviour of the Group while being alive. --- --- ### 1.4.1) Rule of Engagement: --- --- * @{#GROUP.OptionROEWeaponFree} --- * @{#GROUP.OptionROEOpenFire} --- * @{#GROUP.OptionROEReturnFire} --- * @{#GROUP.OptionROEEvadeFire} --- --- To check whether an ROE option is valid for a specific group, use: --- --- * @{#GROUP.OptionROEWeaponFreePossible} --- * @{#GROUP.OptionROEOpenFirePossible} --- * @{#GROUP.OptionROEReturnFirePossible} --- * @{#GROUP.OptionROEEvadeFirePossible} --- --- ### 1.4.2) Rule on thread: --- --- * @{#GROUP.OptionROTNoReaction} --- * @{#GROUP.OptionROTPassiveDefense} --- * @{#GROUP.OptionROTEvadeFire} --- * @{#GROUP.OptionROTVertical} --- --- To test whether an ROT option is valid for a specific group, use: --- --- * @{#GROUP.OptionROTNoReactionPossible} --- * @{#GROUP.OptionROTPassiveDefensePossible} --- * @{#GROUP.OptionROTEvadeFirePossible} --- * @{#GROUP.OptionROTVerticalPossible} --- --- 1.5) GROUP Zone validation methods --- ---------------------------------- --- The group can be validated whether it is completely, partly or not within a @{Zone}. --- Use the following Zone validation methods on the group: --- --- * @{#GROUP.IsCompletelyInZone}: Returns true if all units of the group are within a @{Zone}. --- * @{#GROUP.IsPartlyInZone}: Returns true if some units of the group are within a @{Zone}. --- * @{#GROUP.IsNotInZone}: Returns true if none of the group units of the group are within a @{Zone}. --- --- The zone can be of any @{Zone} class derived from @{Zone#ZONE_BASE}. So, these methods are polymorphic to the zones tested on. --- --- @module Group --- @author FlightControl - ---- The GROUP class --- @type GROUP --- @extends Controllable#CONTROLLABLE --- @field DCSGroup#Group DCSGroup The DCS group class. --- @field #string GroupName The name of the group. -GROUP = { - ClassName = "GROUP", - GroupName = "", - GroupID = 0, - Controller = nil, - DCSGroup = nil, - WayPointFunctions = {}, -} - ---- A DCSGroup --- @type DCSGroup --- @field id_ The ID of the group in DCS - ---- Create a new GROUP from a DCSGroup --- @param #GROUP self --- @param DCSGroup#Group GroupName The DCS Group name --- @return #GROUP self -function GROUP:Register( GroupName ) - local self = BASE:Inherit( self, CONTROLLABLE:New( GroupName ) ) - self:F2( GroupName ) - self.GroupName = GroupName - return self -end - --- Reference methods. - ---- Find the GROUP wrapper class instance using the DCS Group. --- @param #GROUP self --- @param DCSGroup#Group DCSGroup The DCS Group. --- @return #GROUP The GROUP. -function GROUP:Find( DCSGroup ) - - local GroupName = DCSGroup:getName() -- Group#GROUP - local GroupFound = _DATABASE:FindGroup( GroupName ) - GroupFound:E( { GroupName, GroupFound:GetClassNameAndID() } ) - return GroupFound -end - ---- Find the created GROUP using the DCS Group Name. --- @param #GROUP self --- @param #string GroupName The DCS Group Name. --- @return #GROUP The GROUP. -function GROUP:FindByName( GroupName ) - - local GroupFound = _DATABASE:FindGroup( GroupName ) - return GroupFound -end - --- DCS Group methods support. - ---- Returns the DCS Group. --- @param #GROUP self --- @return DCSGroup#Group The DCS Group. -function GROUP:GetDCSObject() - local DCSGroup = Group.getByName( self.GroupName ) - - if DCSGroup then - return DCSGroup - end - - return nil -end - - ---- Returns if the DCS Group is alive. --- When the group exists at run-time, this method will return true, otherwise false. --- @param #GROUP self --- @return #boolean true if the DCS Group is alive. -function GROUP:IsAlive() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupIsAlive = DCSGroup:isExist() - self:T3( GroupIsAlive ) - return GroupIsAlive - end - - 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. --- @param #GROUP self -function GROUP:Destroy() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - for Index, UnitData in pairs( DCSGroup:getUnits() ) do - self:CreateEventCrash( timer.getTime(), UnitData ) - end - DCSGroup:destroy() - DCSGroup = nil - end - - return nil -end - ---- Returns category of the DCS Group. --- @param #GROUP self --- @return DCSGroup#Group.Category The category ID -function GROUP:GetCategory() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - if DCSGroup then - local GroupCategory = DCSGroup:getCategory() - self:T3( GroupCategory ) - return GroupCategory - end - - return nil -end - ---- Returns the category name of the DCS Group. --- @param #GROUP self --- @return #string Category name = Helicopter, Airplane, Ground Unit, Ship -function GROUP:GetCategoryName() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - if DCSGroup then - local CategoryNames = { - [Group.Category.AIRPLANE] = "Airplane", - [Group.Category.HELICOPTER] = "Helicopter", - [Group.Category.GROUND] = "Ground Unit", - [Group.Category.SHIP] = "Ship", - } - local GroupCategory = DCSGroup:getCategory() - self:T3( GroupCategory ) - - return CategoryNames[GroupCategory] - end - - return nil -end - - ---- Returns the coalition of the DCS Group. --- @param #GROUP self --- @return DCSCoalitionObject#coalition.side The coalition side of the DCS Group. -function GROUP:GetCoalition() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - if DCSGroup then - local GroupCoalition = DCSGroup:getCoalition() - self:T3( GroupCoalition ) - return GroupCoalition - end - - return nil -end - ---- Returns the country of the DCS Group. --- @param #GROUP self --- @return DCScountry#country.id The country identifier. --- @return #nil The DCS Group is not existing or alive. -function GROUP:GetCountry() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - if DCSGroup then - local GroupCountry = DCSGroup:getUnit(1):getCountry() - self:T3( GroupCountry ) - return GroupCountry - end - - return nil -end - ---- Returns the name of the DCS Group. --- @param #GROUP self --- @return #string The DCS Group name. -function GROUP:GetName() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupName = DCSGroup:getName() - self:T3( GroupName ) - return GroupName - end - - return nil -end - ---- Returns the DCS Group identifier. --- @param #GROUP self --- @return #number The identifier of the DCS Group. -function GROUP:GetID() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupID = DCSGroup:getID() - self:T3( GroupID ) - return GroupID - 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 Unit#UNIT The UNIT wrapper class. -function GROUP:GetUnit( UnitNumber ) - self:F2( { self.GroupName, UnitNumber } ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local UnitFound = UNIT:Find( DCSGroup:getUnit( UnitNumber ) ) - self:T3( UnitFound.UnitName ) - self:T2( UnitFound ) - return UnitFound - end - - return nil -end - ---- Returns the DCS Unit with number UnitNumber. --- If the underlying DCS Unit does not exist, the method will return nil. . --- @param #GROUP self --- @param #number UnitNumber The number of the DCS Unit to be returned. --- @return DCSUnit#Unit The DCS Unit. -function GROUP:GetDCSUnit( UnitNumber ) - self:F2( { self.GroupName, UnitNumber } ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local DCSUnitFound = DCSGroup:getUnit( UnitNumber ) - self:T3( DCSUnitFound ) - return DCSUnitFound - end - - return nil -end - ---- Returns current size of the DCS Group. --- If some of the DCS Units of the DCS Group are destroyed the size of the DCS Group is changed. --- @param #GROUP self --- @return #number The DCS Group size. -function GROUP:GetSize() - self:F2( { self.GroupName } ) - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupSize = DCSGroup:getSize() - self:T3( GroupSize ) - return GroupSize - 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 } ) - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupInitialSize = DCSGroup:getInitialSize() - self:T3( GroupInitialSize ) - return GroupInitialSize - end - - return nil -end - ---- Returns the UNITs wrappers of the DCS Units of the DCS Group. --- @param #GROUP self --- @return #table The UNITs wrappers. -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 the DCS Units of the DCS Group. --- @param #GROUP self --- @return #table The DCS Units. -function GROUP:GetDCSUnits() - self:F2( { self.GroupName } ) - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local DCSUnits = DCSGroup:getUnits() - self:T3( DCSUnits ) - return DCSUnits - end - - return nil -end - - ---- Activates a GROUP. --- @param #GROUP self -function GROUP:Activate() - self:F2( { self.GroupName } ) - trigger.action.activateGroup( self:GetDCSObject() ) - return self:GetDCSObject() -end - - ---- Gets the type name of the group. --- @param #GROUP self --- @return #string The type name of the group. -function GROUP:GetTypeName() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupTypeName = DCSGroup:getUnit(1):getTypeName() - self:T3( GroupTypeName ) - return( GroupTypeName ) - end - - return nil -end - ---- Gets the CallSign of the first DCS Unit of the DCS Group. --- @param #GROUP self --- @return #string The CallSign of the first DCS Unit of the DCS Group. -function GROUP:GetCallsign() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupCallSign = DCSGroup:getUnit(1):getCallsign() - self:T3( GroupCallSign ) - return GroupCallSign - end - - return nil -end - ---- Returns the current point (Vec2 vector) of the first DCS Unit in the DCS Group. --- @return DCSTypes#Vec2 Current Vec2 point of the first DCS Unit of the DCS Group. -function GROUP:GetPointVec2() - self:F2( self.GroupName ) - - local GroupPointVec2 = self:GetUnit(1):GetPointVec2() - self:T3( GroupPointVec2 ) - return GroupPointVec2 -end - ---- Returns the current point (Vec3 vector) of the first DCS Unit in the DCS Group. --- @return DCSTypes#Vec3 Current Vec3 point of the first DCS Unit of the DCS Group. -function GROUP:GetPointVec3() - self:F2( self.GroupName ) - - local GroupPointVec3 = self:GetUnit(1):GetPointVec3() - self:T3( GroupPointVec3 ) - return GroupPointVec3 -end - - - --- Is Zone Functions - ---- Returns true if all units of the group are within a @{Zone}. --- @param #GROUP self --- @param Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the Group is completely within the @{Zone#ZONE_BASE} -function GROUP:IsCompletelyInZone( Zone ) - self:F2( { self.GroupName, Zone } ) - - for UnitID, UnitData in pairs( self:GetUnits() ) do - local Unit = UnitData -- Unit#UNIT - if Zone:IsPointVec3InZone( Unit:GetPointVec3() ) then - else - return false - end - end - - return true -end - ---- Returns true if some units of the group are within a @{Zone}. --- @param #GROUP self --- @param Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the Group is completely within the @{Zone#ZONE_BASE} -function GROUP:IsPartlyInZone( Zone ) - self:F2( { self.GroupName, Zone } ) - - for UnitID, UnitData in pairs( self:GetUnits() ) do - local Unit = UnitData -- Unit#UNIT - if Zone:IsPointVec3InZone( Unit:GetPointVec3() ) then - return true - end - end - - return false -end - ---- Returns true if none of the group units of the group are within a @{Zone}. --- @param #GROUP self --- @param Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the Group is completely within the @{Zone#ZONE_BASE} -function GROUP:IsNotInZone( Zone ) - self:F2( { self.GroupName, Zone } ) - - for UnitID, UnitData in pairs( self:GetUnits() ) do - local Unit = UnitData -- Unit#UNIT - if Zone:IsPointVec3InZone( Unit:GetPointVec3() ) then - return false - end - end - - return true -end - ---- Returns if the group is of an air category. --- If the group is a helicopter or a plane, then this method will return true, otherwise false. --- @param #GROUP self --- @return #boolean Air category evaluation result. -function GROUP:IsAir() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local IsAirResult = DCSGroup:getCategory() == Group.Category.AIRPLANE or DCSGroup:getCategory() == Group.Category.HELICOPTER - self:T3( IsAirResult ) - return IsAirResult - end - - return nil -end - ---- Returns if the DCS Group contains Helicopters. --- @param #GROUP self --- @return #boolean true if DCS Group contains Helicopters. -function GROUP:IsHelicopter() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupCategory = DCSGroup:getCategory() - self:T2( GroupCategory ) - return GroupCategory == Group.Category.HELICOPTER - end - - return nil -end - ---- Returns if the DCS Group contains AirPlanes. --- @param #GROUP self --- @return #boolean true if DCS Group contains AirPlanes. -function GROUP:IsAirPlane() - self:F2() - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupCategory = DCSGroup:getCategory() - self:T2( GroupCategory ) - return GroupCategory == Group.Category.AIRPLANE - end - - return nil -end - ---- Returns if the DCS Group contains Ground troops. --- @param #GROUP self --- @return #boolean true if DCS Group contains Ground troops. -function GROUP:IsGround() - self:F2() - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupCategory = DCSGroup:getCategory() - self:T2( GroupCategory ) - return GroupCategory == Group.Category.GROUND - end - - return nil -end - ---- Returns if the DCS Group contains Ships. --- @param #GROUP self --- @return #boolean true if DCS Group contains Ships. -function GROUP:IsShip() - self:F2() - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupCategory = DCSGroup:getCategory() - self:T2( GroupCategory ) - return GroupCategory == Group.Category.SHIP - end - - return nil -end - ---- Returns if all units of the group are on the ground or landed. --- If all units of this group are on the ground, this function will return true, otherwise false. --- @param #GROUP self --- @return #boolean All units on the ground result. -function GROUP:AllOnGround() - self:F2() - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local AllOnGroundResult = true - - for Index, UnitData in pairs( DCSGroup:getUnits() ) do - if UnitData:inAir() then - AllOnGroundResult = false - end - end - - self:T3( AllOnGroundResult ) - return AllOnGroundResult - end - - return nil -end - ---- Returns the current maximum velocity of the group. --- Each unit within the group gets evaluated, and the maximum velocity (= the unit which is going the fastest) is returned. --- @param #GROUP self --- @return #number Maximum velocity found. -function GROUP:GetMaxVelocity() - self:F2() - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local MaxVelocity = 0 - - for Index, UnitData in pairs( DCSGroup:getUnits() ) do - - local Velocity = UnitData:getVelocity() - local VelocityTotal = math.abs( Velocity.x ) + math.abs( Velocity.y ) + math.abs( Velocity.z ) - - if VelocityTotal < MaxVelocity then - MaxVelocity = VelocityTotal - end - end - - return MaxVelocity - end - - return nil -end - ---- Returns the current minimum height of the group. --- Each unit within the group gets evaluated, and the minimum height (= the unit which is the lowest elevated) is returned. --- @param #GROUP self --- @return #number Minimum height found. -function GROUP:GetMinHeight() - self:F2() - -end - ---- Returns the current maximum height of the group. --- Each unit within the group gets evaluated, and the maximum height (= the unit which is the highest elevated) is returned. --- @param #GROUP self --- @return #number Maximum height found. -function GROUP:GetMaxHeight() - self:F2() - -end - ---- @param Group#GROUP self -function GROUP:Respawn( Template ) - - local Vec3 = self:GetPointVec3() - --Template.x = Vec3.x - --Template.y = Vec3.z - Template.x = nil - Template.y = nil - - self:E( #Template.units ) - for UnitID, UnitData in pairs( self:GetUnits() ) do - local GroupUnit = UnitData -- Unit#UNIT - self:E( GroupUnit:GetName() ) - if GroupUnit:IsAlive() then - local GroupUnitVec3 = GroupUnit:GetPointVec3() - local GroupUnitHeading = GroupUnit:GetHeading() - Template.units[UnitID].alt = GroupUnitVec3.y - Template.units[UnitID].x = GroupUnitVec3.x - Template.units[UnitID].y = GroupUnitVec3.z - Template.units[UnitID].heading = GroupUnitHeading - self:E( { UnitID, Template.units[UnitID], Template.units[UnitID] } ) - end - end - - _DATABASE:Spawn( Template ) - -end - -function GROUP:GetTemplate() - - return _DATABASE.Templates.Groups[self:GetName()].Template - -end - ---- Return the mission template of the group. --- @param #GROUP self --- @return #table The MissionTemplate -function GROUP:GetTaskMission() - self:F2( self.GroupName ) - - return routines.utils.deepCopy( _DATABASE.Templates.Groups[self.GroupName].Template ) -end - ---- Return the mission route of the group. --- @param #GROUP self --- @return #table The mission route defined by points. -function GROUP:GetTaskRoute() - self:F2( self.GroupName ) - - return routines.utils.deepCopy( _DATABASE.Templates.Groups[self.GroupName].Template.route.points ) -end - ---- Return the route of a group by using the @{Database#DATABASE} class. --- @param #GROUP self --- @param #number Begin The route point from where the copy will start. The base route point is 0. --- @param #number End The route point where the copy will end. The End point is the last point - the End point. The last point has base 0. --- @param #boolean Randomize Randomization of the route, when true. --- @param #number Radius When randomization is on, the randomization is within the radius. -function GROUP:CopyRoute( Begin, End, Randomize, Radius ) - self:F2( { Begin, End } ) - - local Points = {} - - -- Could be a Spawned Group - local GroupName = string.match( self:GetName(), ".*#" ) - if GroupName then - GroupName = GroupName:sub( 1, -2 ) - else - GroupName = self:GetName() - end - - self:T3( { GroupName } ) - - local Template = _DATABASE.Templates.Groups[GroupName].Template - - if Template then - if not Begin then - Begin = 0 - end - if not End then - End = 0 - end - - for TPointID = Begin + 1, #Template.route.points - End do - if Template.route.points[TPointID] then - Points[#Points+1] = routines.utils.deepCopy( Template.route.points[TPointID] ) - if Randomize then - if not Radius then - Radius = 500 - end - Points[#Points].x = Points[#Points].x + math.random( Radius * -1, Radius ) - Points[#Points].y = Points[#Points].y + math.random( Radius * -1, Radius ) - end - end - end - return Points - else - error( "Template not found for Group : " .. GroupName ) - end - - return nil -end - - --- Message APIs - ---- Returns a message for a coalition or a client. --- @param #GROUP self --- @param #string Message The message text --- @param DCSTypes#Duration Duration The duration of the message. --- @return Message#MESSAGE -function GROUP:Message( Message, Duration ) - self:F2( { Message, Duration } ) - - local DCSGroup = self:GetDCSObject() - if DCSGroup then - return MESSAGE:New( Message, Duration, self:GetCallsign() .. " (" .. self:GetTypeName() .. ")" ) - end - - return nil -end - ---- Send a message to all coalitions. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #GROUP self --- @param #string Message The message text --- @param DCSTypes#Duration Duration The duration of the message. -function GROUP:MessageToAll( Message, Duration ) - self:F2( { Message, Duration } ) - - local DCSGroup = self:GetDCSObject() - if DCSGroup then - self:Message( Message, Duration ):ToAll() - end - - return nil -end - ---- Send a message to the red coalition. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #GROUP self --- @param #string Message The message text --- @param DCSTYpes#Duration Duration The duration of the message. -function GROUP:MessageToRed( Message, Duration ) - self:F2( { Message, Duration } ) - - local DCSGroup = self:GetDCSObject() - if DCSGroup then - self:Message( Message, Duration ):ToRed() - end - - return nil -end - ---- Send a message to the blue coalition. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #GROUP self --- @param #string Message The message text --- @param DCSTypes#Duration Duration The duration of the message. -function GROUP:MessageToBlue( Message, Duration ) - self:F2( { Message, Duration } ) - - local DCSGroup = self:GetDCSObject() - if DCSGroup then - self:Message( Message, Duration ):ToBlue() - end - - return nil -end - ---- Send a message to a client. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #GROUP self --- @param #string Message The message text --- @param DCSTypes#Duration Duration The duration of the message. --- @param Client#CLIENT Client The client object receiving the message. -function GROUP:MessageToClient( Message, Duration, Client ) - self:F2( { Message, Duration } ) - - local DCSGroup = self:GetDCSObject() - if DCSGroup then - self:Message( Message, Duration ):ToClient( Client ) - end - - return nil -end ---- This module contains the UNIT class. --- --- 1) @{Unit#UNIT} class, extends @{Controllable#CONTROLLABLE} --- =========================================================== --- The @{Unit#UNIT} class is a wrapper class to handle the DCS Unit objects: --- --- * Support all DCS Unit APIs. --- * Enhance with Unit specific APIs not in the DCS Unit API set. --- * Handle local Unit Controller. --- * Manage the "state" of the DCS Unit. --- --- --- 1.1) UNIT reference methods --- ---------------------- --- For each DCS Unit object alive within a running mission, a UNIT wrapper object (instance) will be created within the _@{DATABASE} object. --- This is done at the beginning of the mission (when the mission starts), and dynamically when new DCS Unit objects are spawned (using the @{SPAWN} class). --- --- The UNIT class **does not contain a :New()** method, rather it provides **:Find()** methods to retrieve the object reference --- using the DCS Unit or the DCS UnitName. --- --- Another thing to know is that UNIT objects do not "contain" the DCS Unit object. --- The UNIT methods will reference the DCS Unit object by name when it is needed during API execution. --- If the DCS Unit object does not exist or is nil, the UNIT methods will return nil and log an exception in the DCS.log file. --- --- The UNIT class provides the following functions to retrieve quickly the relevant UNIT instance: --- --- * @{#UNIT.Find}(): Find a UNIT instance from the _DATABASE object using a DCS Unit object. --- * @{#UNIT.FindByName}(): Find a UNIT instance from the _DATABASE object using a DCS Unit name. --- --- IMPORTANT: ONE SHOULD NEVER SANATIZE these UNIT OBJECT REFERENCES! (make the UNIT object references nil). --- --- 1.2) DCS UNIT APIs --- ------------------ --- The DCS Unit APIs are used extensively within MOOSE. The UNIT class has for each DCS Unit API a corresponding method. --- To be able to distinguish easily in your code the difference between a UNIT API call and a DCS Unit API call, --- the first letter of the method is also capitalized. So, by example, the DCS Unit method @{DCSUnit#Unit.getName}() --- is implemented in the UNIT class as @{#UNIT.GetName}(). --- --- 1.3) Smoke, Flare Units --- ----------------------- --- The UNIT class provides methods to smoke or flare units easily. --- The @{#UNIT.SmokeBlue}(), @{#UNIT.SmokeGreen}(),@{#UNIT.SmokeOrange}(), @{#UNIT.SmokeRed}(), @{#UNIT.SmokeRed}() methods --- will smoke the unit in the corresponding color. Note that smoking a unit is done at the current position of the DCS Unit. --- When the DCS Unit moves for whatever reason, the smoking will still continue! --- The @{#UNIT.FlareGreen}(), @{#UNIT.FlareRed}(), @{#UNIT.FlareWhite}(), @{#UNIT.FlareYellow}() --- methods will fire off a flare in the air with the corresponding color. Note that a flare is a one-off shot and its effect is of very short duration. --- --- 1.4) Location Position, Point --- ----------------------------- --- The UNIT class provides methods to obtain the current point or position of the DCS Unit. --- The @{#UNIT.GetPointVec2}(), @{#UNIT.GetPointVec3}() will obtain the current **location** of the DCS Unit in a Vec2 (2D) or a **point** in a Vec3 (3D) vector respectively. --- If you want to obtain the complete **3D position** including oriëntation and direction vectors, consult the @{#UNIT.GetPositionVec3}() method respectively. --- --- 1.5) Test if alive --- ------------------ --- The @{#UNIT.IsAlive}(), @{#UNIT.IsActive}() methods determines if the DCS Unit is alive, meaning, it is existing and active. --- --- 1.6) Test for proximity --- ----------------------- --- The UNIT class contains methods to test the location or proximity against zones or other objects. --- --- ### 1.6.1) Zones --- 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}. --- --- ### 1.6.2) Units --- Test if another DCS Unit is within a given radius of the current DCS Unit, use the @{#UNIT.OtherUnitInRadius}() method. --- --- @module Unit --- @author FlightControl - - - - - ---- The UNIT class --- @type UNIT --- @extends Controllable#CONTROLLABLE --- @field #UNIT.FlareColor FlareColor --- @field #UNIT.SmokeColor SmokeColor -UNIT = { - ClassName="UNIT", - CategoryName = { - [Unit.Category.AIRPLANE] = "Airplane", - [Unit.Category.HELICOPTER] = "Helicoper", - [Unit.Category.GROUND_UNIT] = "Ground Unit", - [Unit.Category.SHIP] = "Ship", - [Unit.Category.STRUCTURE] = "Structure", - }, - FlareColor = { - Green = trigger.flareColor.Green, - Red = trigger.flareColor.Red, - White = trigger.flareColor.White, - Yellow = trigger.flareColor.Yellow - }, - SmokeColor = { - Green = trigger.smokeColor.Green, - Red = trigger.smokeColor.Red, - White = trigger.smokeColor.White, - Orange = trigger.smokeColor.Orange, - Blue = trigger.smokeColor.Blue - }, - } - ---- FlareColor --- @type UNIT.FlareColor --- @field Green --- @field Red --- @field White --- @field Yellow - ---- SmokeColor --- @type UNIT.SmokeColor --- @field Green --- @field Red --- @field White --- @field Orange --- @field Blue - --- Registration. - ---- Create a new UNIT from DCSUnit. --- @param #UNIT self --- @param DCSUnit#Unit DCSUnit --- @param Database#DATABASE Database --- @return Unit#UNIT -function UNIT:Register( UnitName ) - - local self = BASE:Inherit( self, CONTROLLABLE:New() ) - self:F2( UnitName ) - self.UnitName = UnitName - return self -end - --- Reference methods. - ---- Finds a UNIT from the _DATABASE using a DCSUnit object. --- @param #UNIT self --- @param DCSUnit#Unit DCSUnit An existing DCS Unit object reference. --- @return Unit#UNIT self -function UNIT:Find( DCSUnit ) - - local UnitName = DCSUnit:getName() - local UnitFound = _DATABASE:FindUnit( UnitName ) - return UnitFound -end - ---- Find a UNIT in the _DATABASE using the name of an existing DCS Unit. --- @param #UNIT self --- @param #string UnitName The Unit Name. --- @return Unit#UNIT self -function UNIT:FindByName( UnitName ) - - local UnitFound = _DATABASE:FindUnit( UnitName ) - return UnitFound -end - - ---- @param #UNIT self --- @return DCSUnit#Unit -function UNIT:GetDCSObject() - - local DCSUnit = Unit.getByName( self.UnitName ) - - if DCSUnit then - return DCSUnit - end - - return nil -end - ---- Returns coalition of the Unit. --- @param Unit#UNIT self --- @return DCSCoalitionObject#coalition.side The side of the coalition. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetCoalition() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitCoalition = DCSUnit:getCoalition() - self:T3( UnitCoalition ) - return UnitCoalition - end - - return nil -end - ---- Returns country of the Unit. --- @param Unit#UNIT self --- @return DCScountry#country.id The country identifier. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetCountry() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitCountry = DCSUnit:getCountry() - self:T3( UnitCountry ) - return UnitCountry - end - - return nil -end - - ---- Returns DCS Unit object name. --- The function provides access to non-activated units too. --- @param Unit#UNIT self --- @return #string The name of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetName() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitName = self.UnitName - return UnitName - end - - return nil -end - - ---- Returns if the unit is alive. --- @param Unit#UNIT self --- @return #boolean true if Unit is alive. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:IsAlive() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitIsAlive = DCSUnit:isExist() - return UnitIsAlive - end - - return false -end - ---- Returns if the unit is activated. --- @param Unit#UNIT self --- @return #boolean true if Unit is activated. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:IsActive() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - - local UnitIsActive = DCSUnit:isActive() - return UnitIsActive - end - - return nil -end - ---- Returns if the unit is located above a runway. --- @param Unit#UNIT self --- @return #boolean true if Unit is above a runway. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:IsAboveRunway() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - - local PointVec2 = self:GetPointVec2() - local SurfaceType = land.getSurfaceType( PointVec2 ) - local IsAboveRunway = SurfaceType == land.SurfaceType.RUNWAY - - self:T2( IsAboveRunway ) - return IsAboveRunway - end - - return nil -end - - - ---- Returns name of the player that control the unit or nil if the unit is controlled by A.I. --- @param Unit#UNIT self --- @return #string Player Name --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetPlayerName() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - - local PlayerName = DCSUnit:getPlayerName() - if PlayerName == nil then - PlayerName = "" - end - return PlayerName - end - - return nil -end - ---- Returns the unit's unique identifier. --- @param Unit#UNIT self --- @return DCSUnit#Unit.ID Unit ID --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetID() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitID = DCSUnit:getID() - return UnitID - end - - return nil -end - ---- Returns the unit's number in the group. --- The number is the same number the unit has in ME. --- It may not be changed during the mission. --- If any unit in the group is destroyed, the numbers of another units will not be changed. --- @param Unit#UNIT self --- @return #number The Unit number. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetNumber() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitNumber = DCSUnit:getNumber() - return UnitNumber - end - - return nil -end - ---- Returns the unit's group if it exist and nil otherwise. --- @param Unit#UNIT self --- @return Group#GROUP The Group of the Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetGroup() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitGroup = GROUP:Find( DCSUnit:getGroup() ) - return UnitGroup - end - - return nil -end - - ---- Returns the unit's callsign - the localized string. --- @param Unit#UNIT self --- @return #string The Callsign of the Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetCallSign() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitCallSign = DCSUnit:getCallsign() - return UnitCallSign - end - - return nil -end - ---- Returns the unit's health. Dead units has health <= 1.0. --- @param Unit#UNIT self --- @return #number The Unit's health value. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetLife() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitLife = DCSUnit:getLife() - return UnitLife - end - - return nil -end - ---- Returns the Unit's initial health. --- @param Unit#UNIT self --- @return #number The Unit's initial health value. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetLife0() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitLife0 = DCSUnit:getLife0() - return UnitLife0 - end - - return nil -end - ---- Returns relative amount of fuel (from 0.0 to 1.0) the unit has in its internal tanks. If there are additional fuel tanks the value may be greater than 1.0. --- @param Unit#UNIT self --- @return #number The relative amount of fuel (from 0.0 to 1.0). --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetFuel() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitFuel = DCSUnit:getFuel() - return UnitFuel - end - - return nil -end - ---- Returns the Unit's ammunition. --- @param Unit#UNIT self --- @return DCSUnit#Unit.Ammo --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetAmmo() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitAmmo = DCSUnit:getAmmo() - return UnitAmmo - end - - return nil -end - ---- Returns the unit sensors. --- @param Unit#UNIT self --- @return DCSUnit#Unit.Sensors --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetSensors() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitSensors = DCSUnit:getSensors() - return UnitSensors - end - - return nil -end - --- Need to add here a function per sensortype --- unit:hasSensors(Unit.SensorType.RADAR, Unit.RadarType.AS) - ---- Returns two values: --- --- * First value indicates if at least one of the unit's radar(s) is on. --- * Second value is the object of the radar's interest. Not nil only if at least one radar of the unit is tracking a target. --- @param Unit#UNIT self --- @return #boolean Indicates if at least one of the unit's radar(s) is on. --- @return DCSObject#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 ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitRadarOn, UnitRadarObject = DCSUnit:getRadar() - return UnitRadarOn, UnitRadarObject - end - - return nil, nil -end - --- Need to add here functions to check if radar is on and which object etc. - ---- Returns unit descriptor. Descriptor type depends on unit category. --- @param Unit#UNIT self --- @return DCSUnit#Unit.Desc The Unit descriptor. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetDesc() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitDesc = DCSUnit:getDesc() - self:T2( UnitDesc ) - return UnitDesc - end - - self:E( "Unit " .. self.UnitName .. "not found!" ) - return nil -end - - ---- Returns the type name of the DCS Unit. --- @param Unit#UNIT self --- @return #string The type name of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetTypeName() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitTypeName = DCSUnit:getTypeName() - self:T3( UnitTypeName ) - return UnitTypeName - end - - return nil -end - - - ---- Returns the prefix name of the DCS Unit. A prefix name is a part of the name before a '#'-sign. --- DCS Units spawned with the @{SPAWN} class contain a '#'-sign to indicate the end of the (base) DCS Unit name. --- The spawn sequence number and unit number are contained within the name after the '#' sign. --- @param Unit#UNIT self --- @return #string The name of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetPrefix() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitPrefix = string.match( self.UnitName, ".*#" ):sub( 1, -2 ) - self:T3( UnitPrefix ) - return UnitPrefix - end - - return nil -end - - - ---- Returns the @{DCSTypes#Vec2} vector indicating the point in 2D of the DCS Unit within the mission. --- @param Unit#UNIT self --- @return DCSTypes#Vec2 The 2D point vector of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetPointVec2() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitPointVec3 = DCSUnit:getPosition().p - - local UnitPointVec2 = {} - UnitPointVec2.x = UnitPointVec3.x - UnitPointVec2.y = UnitPointVec3.z - - self:T2( UnitPointVec2 ) - return UnitPointVec2 - end - - return nil -end - - ---- Returns the @{DCSTypes#Vec3} vector indicating the point in 3D of the DCS Unit within the mission. --- @param Unit#UNIT self --- @return DCSTypes#Vec3 The 3D point vector of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetPointVec3() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitPointVec3 = DCSUnit:getPosition().p - self:T3( UnitPointVec3 ) - return UnitPointVec3 - end - - return nil -end - ---- Returns the @{DCSTypes#Position3} position vectors indicating the point and direction vectors in 3D of the DCS Unit within the mission. --- @param Unit#UNIT self --- @return DCSTypes#Position The 3D position vectors of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetPositionVec3() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitPosition = DCSUnit:getPosition() - self:T3( UnitPosition ) - return UnitPosition - end - - return nil -end - ---- Returns the DCS Unit velocity vector. --- @param Unit#UNIT self --- @return DCSTypes#Vec3 The velocity vector --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetVelocity() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitVelocityVec3 = DCSUnit:getVelocity() - self:T3( UnitVelocityVec3 ) - return UnitVelocityVec3 - end - - return nil -end - --- Is functions - ---- Returns true if the unit is within a @{Zone}. --- @param #UNIT self --- @param Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the unit is within the @{Zone#ZONE_BASE} -function UNIT:IsInZone( Zone ) - self:F2( { self.UnitName, Zone } ) - - if self:IsAlive() then - local IsInZone = Zone:IsPointVec3InZone( self:GetPointVec3() ) - - self:T( { IsInZone } ) - return IsInZone - else - return false - end -end - ---- Returns true if the unit is not within a @{Zone}. --- @param #UNIT self --- @param Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the unit is not within the @{Zone#ZONE_BASE} -function UNIT:IsNotInZone( Zone ) - self:F2( { self.UnitName, Zone } ) - - if self:IsAlive() then - local IsInZone = not Zone:IsPointVec3InZone( self:GetPointVec3() ) - - self:T( { IsInZone } ) - return IsInZone - else - return false - end -end - ---- Returns true if the DCS Unit is in the air. --- @param Unit#UNIT self --- @return #boolean true if in the air. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:InAir() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitInAir = DCSUnit:inAir() - self:T3( UnitInAir ) - return UnitInAir - end - - return nil -end - ---- Returns the altitude of the DCS Unit. --- @param Unit#UNIT self --- @return DCSTypes#Distance The altitude of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetAltitude() - self:F2() - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitPointVec3 = DCSUnit:getPoint() --DCSTypes#Vec3 - return UnitPointVec3.y - end - - return nil -end - ---- Returns true if there is an **other** DCS Unit within a radius of the current 2D point of the DCS Unit. --- @param Unit#UNIT self --- @param Unit#UNIT AwaitUnit The other UNIT wrapper object. --- @param Radius The radius in meters with the DCS Unit in the centre. --- @return true If the other DCS Unit is within the radius of the 2D point of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:OtherUnitInRadius( AwaitUnit, Radius ) - self:F2( { self.UnitName, AwaitUnit.UnitName, Radius } ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitPos = self:GetPointVec3() - local AwaitUnitPos = AwaitUnit:GetPointVec3() - - if (((UnitPos.x - AwaitUnitPos.x)^2 + (UnitPos.z - AwaitUnitPos.z)^2)^0.5 <= Radius) then - self:T3( "true" ) - return true - else - self:T3( "false" ) - return false - end - end - - return nil -end - ---- Returns the DCS Unit category name as defined within the DCS Unit Descriptor. --- @param Unit#UNIT self --- @return #string The DCS Unit Category Name -function UNIT:GetCategoryName() - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitCategoryName = self.CategoryName[ self:GetDesc().category ] - return UnitCategoryName - end - - return nil -end - ---- Returns the DCS Unit heading. --- @param Unit#UNIT self --- @return #number The DCS Unit heading -function UNIT:GetHeading() - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - - local UnitPosition = DCSUnit:getPosition() - if UnitPosition then - local UnitHeading = math.atan2( UnitPosition.x.z, UnitPosition.x.x ) - if UnitHeading < 0 then - UnitHeading = UnitHeading + 2 * math.pi - end - self:T2( UnitHeading ) - return UnitHeading - end - end - - return nil -end - - ---- Signal a flare at the position of the UNIT. --- @param #UNIT self -function UNIT:Flare( FlareColor ) - self:F2() - trigger.action.signalFlare( self:GetPointVec3(), FlareColor , 0 ) -end - ---- Signal a white flare at the position of the UNIT. --- @param #UNIT self -function UNIT:FlareWhite() - self:F2() - trigger.action.signalFlare( self:GetPointVec3(), trigger.flareColor.White , 0 ) -end - ---- Signal a yellow flare at the position of the UNIT. --- @param #UNIT self -function UNIT:FlareYellow() - self:F2() - trigger.action.signalFlare( self:GetPointVec3(), trigger.flareColor.Yellow , 0 ) -end - ---- Signal a green flare at the position of the UNIT. --- @param #UNIT self -function UNIT:FlareGreen() - self:F2() - trigger.action.signalFlare( self:GetPointVec3(), trigger.flareColor.Green , 0 ) -end - ---- Signal a red flare at the position of the UNIT. --- @param #UNIT self -function UNIT:FlareRed() - self:F2() - trigger.action.signalFlare( self:GetPointVec3(), trigger.flareColor.Red, 0 ) -end - ---- Smoke the UNIT. --- @param #UNIT self -function UNIT:Smoke( SmokeColor ) - self:F2() - trigger.action.smoke( self:GetPointVec3(), SmokeColor ) -end - ---- Smoke the UNIT Green. --- @param #UNIT self -function UNIT:SmokeGreen() - self:F2() - trigger.action.smoke( self:GetPointVec3(), trigger.smokeColor.Green ) -end - ---- Smoke the UNIT Red. --- @param #UNIT self -function UNIT:SmokeRed() - self:F2() - trigger.action.smoke( self:GetPointVec3(), trigger.smokeColor.Red ) -end - ---- Smoke the UNIT White. --- @param #UNIT self -function UNIT:SmokeWhite() - self:F2() - trigger.action.smoke( self:GetPointVec3(), trigger.smokeColor.White ) -end - ---- Smoke the UNIT Orange. --- @param #UNIT self -function UNIT:SmokeOrange() - self:F2() - trigger.action.smoke( self:GetPointVec3(), trigger.smokeColor.Orange ) -end - ---- Smoke the UNIT Blue. --- @param #UNIT self -function UNIT:SmokeBlue() - self:F2() - trigger.action.smoke( self:GetPointVec3(), trigger.smokeColor.Blue ) -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 UnitDescriptor = self.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 - ---- This module contains the ZONE classes, inherited from @{Zone#ZONE_BASE}. --- There are essentially two core functions that zones accomodate: --- --- * Test if an object is within the zone boundaries. --- * Provide the zone behaviour. Some zones are static, while others are moveable. --- --- The object classes are using the zone classes to test the zone boundaries, which can take various forms: --- --- * Test if completely within the zone. --- * Test if partly within the zone (for @{Group#GROUP} objects). --- * Test if not in the zone. --- * Distance to the nearest intersecting point of the zone. --- * Distance to the center of the zone. --- * ... --- --- Each of these ZONE classes have a zone name, and specific parameters defining the zone type: --- --- * @{Zone#ZONE_BASE}: The ZONE_BASE class defining the base for all other zone classes. --- * @{Zone#ZONE_RADIUS}: The ZONE_RADIUS class defined by a zone name, a location and a radius. --- * @{Zone#ZONE}: The ZONE class, defined by the zone name as defined within the Mission Editor. --- * @{Zone#ZONE_UNIT}: The ZONE_UNIT class defined by a zone around a @{Unit#UNIT} with a radius. --- * @{Zone#ZONE_POLYGON}: The ZONE_POLYGON class defined by a sequence of @{Group#GROUP} waypoints within the Mission Editor, forming a polygon. --- --- Each zone implements two polymorphic functions defined in @{Zone#ZONE_BASE}: --- --- * @{#ZONE_BASE.IsPointVec2InZone}: Returns if a location is within the zone. --- * @{#ZONE_BASE.IsPointVec3InZone}: Returns if a point is within the zone. --- --- === --- --- 1) @{Zone#ZONE_BASE} class, extends @{Base#BASE} --- ================================================ --- The ZONE_BASE class defining the base for all other zone classes. --- --- === --- --- 2) @{Zone#ZONE_RADIUS} class, extends @{Zone#ZONE_BASE} --- ======================================================= --- The ZONE_RADIUS class defined by a zone name, a location and a radius. --- --- === --- --- 3) @{Zone#ZONE} class, extends @{Zone#ZONE_RADIUS} --- ========================================== --- The ZONE class, defined by the zone name as defined within the Mission Editor. --- --- === --- --- 4) @{Zone#ZONE_UNIT} class, extends @{Zone#ZONE_RADIUS} --- ======================================================= --- The ZONE_UNIT class defined by a zone around a @{Unit#UNIT} with a radius. --- --- === --- --- 5) @{Zone#ZONE_POLYGON} class, extends @{Zone#ZONE_BASE} --- ======================================================== --- The ZONE_POLYGON class defined by a sequence of @{Group#GROUP} waypoints within the Mission Editor, forming a polygon. --- --- === --- --- @module Zone --- @author FlightControl - - - - - - - - - ---- The ZONE_BASE class --- @type ZONE_BASE --- @field #string ZoneName Name of the zone. --- @extends Base#BASE -ZONE_BASE = { - ClassName = "ZONE_BASE", - } - - ---- The ZONE_BASE.BoundingSquare --- @type ZONE_BASE.BoundingSquare --- @field DCSTypes#Distance x1 The lower x coordinate (left down) --- @field DCSTypes#Distance y1 The lower y coordinate (left down) --- @field DCSTypes#Distance x2 The higher x coordinate (right up) --- @field DCSTypes#Distance y2 The higher y coordinate (right up) - - ---- ZONE_BASE constructor --- @param #ZONE_BASE self --- @param #string ZoneName Name of the zone. --- @return #ZONE_BASE self -function ZONE_BASE:New( ZoneName ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( ZoneName ) - - self.ZoneName = ZoneName - - return self -end - ---- Returns if a location is within the zone. --- @param #ZONE_BASE self --- @param DCSTypes#Vec2 PointVec2 The location to test. --- @return #boolean true if the location is within the zone. -function ZONE_BASE:IsPointVec2InZone( PointVec2 ) - self:F2( PointVec2 ) - - return false -end - ---- Returns if a point is within the zone. --- @param #ZONE_BASE self --- @param DCSTypes#Vec3 PointVec3 The point to test. --- @return #boolean true if the point is within the zone. -function ZONE_BASE:IsPointVec3InZone( PointVec3 ) - self:F2( PointVec3 ) - - local InZone = self:IsPointVec2InZone( { x = PointVec3.x, y = PointVec3.z } ) - - return InZone -end - ---- Define a random @{DCSTypes#Vec2} within the zone. --- @param #ZONE_BASE self --- @return DCSTypes#Vec2 The Vec2 coordinates. -function ZONE_BASE:GetRandomVec2() - return { x = 0, y = 0 } -end - ---- Get the bounding square the zone. --- @param #ZONE_BASE self --- @return #ZONE_BASE.BoundingSquare The bounding square. -function ZONE_BASE:GetBoundingSquare() - return { x1 = 0, y1 = 0, x2 = 0, y2 = 0 } -end - - ---- Smokes the zone boundaries in a color. --- @param #ZONE_BASE self --- @param SmokeColor The smoke color. -function ZONE_BASE:SmokeZone( SmokeColor ) - self:F2( SmokeColor ) - -end - - ---- The ZONE_RADIUS class, defined by a zone name, a location and a radius. --- @type ZONE_RADIUS --- @field DCSTypes#Vec2 PointVec2 The current location of the zone. --- @field DCSTypes#Distance Radius The radius of the zone. --- @extends Zone#ZONE_BASE -ZONE_RADIUS = { - ClassName="ZONE_RADIUS", - } - ---- Constructor of ZONE_RADIUS, taking the zone name, the zone location and a radius. --- @param #ZONE_RADIUS self --- @param #string ZoneName Name of the zone. --- @param DCSTypes#Vec2 PointVec2 The location of the zone. --- @param DCSTypes#Distance Radius The radius of the zone. --- @return #ZONE_RADIUS self -function ZONE_RADIUS:New( ZoneName, PointVec2, Radius ) - local self = BASE:Inherit( self, ZONE_BASE:New( ZoneName ) ) - self:F( { ZoneName, PointVec2, Radius } ) - - self.Radius = Radius - self.PointVec2 = PointVec2 - - return self -end - ---- Smokes the zone boundaries in a color. --- @param #ZONE_RADIUS self --- @param #POINT_VEC3.SmokeColor SmokeColor The smoke color. --- @param #number Points (optional) The amount of points in the circle. --- @return #ZONE_RADIUS self -function ZONE_RADIUS:SmokeZone( SmokeColor, Points ) - self:F2( SmokeColor ) - - local Point = {} - local PointVec2 = self:GetPointVec2() - - Points = Points and Points or 360 - - local Angle - local RadialBase = math.pi*2 - - for Angle = 0, 360, 360 / Points do - local Radial = Angle * RadialBase / 360 - Point.x = PointVec2.x + math.cos( Radial ) * self:GetRadius() - Point.y = PointVec2.y + math.sin( Radial ) * self:GetRadius() - POINT_VEC2:New( Point.x, Point.y ):Smoke( SmokeColor ) - end - - return self -end - - ---- Flares the zone boundaries in a color. --- @param #ZONE_RADIUS self --- @param #POINT_VEC3.FlareColor FlareColor The flare color. --- @param #number Points (optional) The amount of points in the circle. --- @param DCSTypes#Azimuth Azimuth (optional) Azimuth The azimuth of the flare. --- @return #ZONE_RADIUS self -function ZONE_RADIUS:FlareZone( FlareColor, Points, Azimuth ) - self:F2( { FlareColor, Azimuth } ) - - local Point = {} - local PointVec2 = self:GetPointVec2() - - Points = Points and Points or 360 - - local Angle - local RadialBase = math.pi*2 - - for Angle = 0, 360, 360 / Points do - local Radial = Angle * RadialBase / 360 - Point.x = PointVec2.x + math.cos( Radial ) * self:GetRadius() - Point.y = PointVec2.y + math.sin( Radial ) * self:GetRadius() - POINT_VEC2:New( Point.x, Point.y ):Flare( FlareColor, Azimuth ) - end - - return self -end - ---- Returns the radius of the zone. --- @param #ZONE_RADIUS self --- @return DCSTypes#Distance The radius of the zone. -function ZONE_RADIUS:GetRadius() - self:F2( self.ZoneName ) - - self:T2( { self.Radius } ) - - return self.Radius -end - ---- Sets the radius of the zone. --- @param #ZONE_RADIUS self --- @param DCSTypes#Distance Radius The radius of the zone. --- @return DCSTypes#Distance The radius of the zone. -function ZONE_RADIUS:SetRadius( Radius ) - self:F2( self.ZoneName ) - - self.Radius = Radius - self:T2( { self.Radius } ) - - return self.Radius -end - ---- Returns the location of the zone. --- @param #ZONE_RADIUS self --- @return DCSTypes#Vec2 The location of the zone. -function ZONE_RADIUS:GetPointVec2() - self:F2( self.ZoneName ) - - self:T2( { self.PointVec2 } ) - - return self.PointVec2 -end - ---- Sets the location of the zone. --- @param #ZONE_RADIUS self --- @param DCSTypes#Vec2 PointVec2 The new location of the zone. --- @return DCSTypes#Vec2 The new location of the zone. -function ZONE_RADIUS:SetPointVec2( PointVec2 ) - self:F2( self.ZoneName ) - - self.PointVec2 = PointVec2 - - self:T2( { self.PointVec2 } ) - - return self.PointVec2 -end - ---- Returns the point of the zone. --- @param #ZONE_RADIUS self --- @param DCSTypes#Distance Height The height to add to the land height where the center of the zone is located. --- @return DCSTypes#Vec3 The point of the zone. -function ZONE_RADIUS:GetPointVec3( Height ) - self:F2( self.ZoneName ) - - local PointVec2 = self:GetPointVec2() - - local PointVec3 = { x = PointVec2.x, y = land.getHeight( self:GetPointVec2() ) + Height, z = PointVec2.y } - - self:T2( { PointVec3 } ) - - return PointVec3 -end - - ---- Returns if a location is within the zone. --- @param #ZONE_RADIUS self --- @param DCSTypes#Vec2 PointVec2 The location to test. --- @return #boolean true if the location is within the zone. -function ZONE_RADIUS:IsPointVec2InZone( PointVec2 ) - self:F2( PointVec2 ) - - local ZonePointVec2 = self:GetPointVec2() - - if (( PointVec2.x - ZonePointVec2.x )^2 + ( PointVec2.y - ZonePointVec2.y ) ^2 ) ^ 0.5 <= self:GetRadius() then - return true - end - - return false -end - ---- Returns if a point is within the zone. --- @param #ZONE_RADIUS self --- @param DCSTypes#Vec3 PointVec3 The point to test. --- @return #boolean true if the point is within the zone. -function ZONE_RADIUS:IsPointVec3InZone( PointVec3 ) - self:F2( PointVec3 ) - - local InZone = self:IsPointVec2InZone( { x = PointVec3.x, y = PointVec3.z } ) - - return InZone -end - ---- Returns a random location within the zone. --- @param #ZONE_RADIUS self --- @return DCSTypes#Vec2 The random location within the zone. -function ZONE_RADIUS:GetRandomVec2() - self:F( self.ZoneName ) - - local Point = {} - local PointVec2 = self:GetPointVec2() - - local angle = math.random() * math.pi*2; - Point.x = PointVec2.x + math.cos( angle ) * math.random() * self:GetRadius(); - Point.y = PointVec2.y + math.sin( angle ) * math.random() * self:GetRadius(); - - self:T( { Point } ) - - return Point -end - - - ---- The ZONE class, defined by the zone name as defined within the Mission Editor. The location and the radius are automatically collected from the mission settings. --- @type ZONE --- @extends Zone#ZONE_RADIUS -ZONE = { - ClassName="ZONE", - } - - ---- Constructor of ZONE, taking the zone name. --- @param #ZONE self --- @param #string ZoneName The name of the zone as defined within the mission editor. --- @return #ZONE -function ZONE:New( ZoneName ) - - local Zone = trigger.misc.getZone( ZoneName ) - - if not Zone then - error( "Zone " .. ZoneName .. " does not exist." ) - return nil - end - - local self = BASE:Inherit( self, ZONE_RADIUS:New( ZoneName, { x = Zone.point.x, y = Zone.point.z }, Zone.radius ) ) - self:F( ZoneName ) - - self.Zone = Zone - - return self -end - - ---- The ZONE_UNIT class defined by a zone around a @{Unit#UNIT} with a radius. --- @type ZONE_UNIT --- @field Unit#UNIT ZoneUNIT --- @extends Zone#ZONE_RADIUS -ZONE_UNIT = { - ClassName="ZONE_UNIT", - } - ---- Constructor to create a ZONE_UNIT instance, taking the zone name, a zone unit and a radius. --- @param #ZONE_UNIT self --- @param #string ZoneName Name of the zone. --- @param Unit#UNIT ZoneUNIT The unit as the center of the zone. --- @param DCSTypes#Distance Radius The radius of the zone. --- @return #ZONE_UNIT self -function ZONE_UNIT:New( ZoneName, ZoneUNIT, Radius ) - local self = BASE:Inherit( self, ZONE_RADIUS:New( ZoneName, ZoneUNIT:GetPointVec2(), Radius ) ) - self:F( { ZoneName, ZoneUNIT:GetPointVec2(), Radius } ) - - self.ZoneUNIT = ZoneUNIT - - return self -end - - ---- Returns the current location of the @{Unit#UNIT}. --- @param #ZONE_UNIT self --- @return DCSTypes#Vec2 The location of the zone based on the @{Unit#UNIT}location. -function ZONE_UNIT:GetPointVec2() - self:F( self.ZoneName ) - - local ZonePointVec2 = self.ZoneUNIT:GetPointVec2() - - self:T( { ZonePointVec2 } ) - - return ZonePointVec2 -end - --- Polygons - ---- The ZONE_POLYGON_BASE class defined by an array of @{DCSTypes#Vec2}, forming a polygon. --- @type ZONE_POLYGON_BASE --- @field #ZONE_POLYGON_BASE.ListVec2 Polygon The polygon defined by an array of @{DCSTypes#Vec2}. --- @extends Zone#ZONE_BASE -ZONE_POLYGON_BASE = { - ClassName="ZONE_POLYGON_BASE", - } - ---- A points array. --- @type ZONE_POLYGON_BASE.ListVec2 --- @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. --- @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.. --- @return #ZONE_POLYGON_BASE self -function ZONE_POLYGON_BASE:New( ZoneName, PointsArray ) - local self = BASE:Inherit( self, ZONE_BASE:New( ZoneName ) ) - self:F( { ZoneName, PointsArray } ) - - local i = 0 - - self.Polygon = {} - - for i = 1, #PointsArray do - self.Polygon[i] = {} - self.Polygon[i].x = PointsArray[i].x - self.Polygon[i].y = PointsArray[i].y - end - - return self -end - ---- Flush polygon coordinates as a table in DCS.log. --- @param #ZONE_POLYGON_BASE self --- @return #ZONE_POLYGON_BASE self -function ZONE_POLYGON_BASE:Flush() - self:F2() - - self:E( { Polygon = self.ZoneName, Coordinates = self.Polygon } ) - - return self -end - - ---- Smokes the zone boundaries in a color. --- @param #ZONE_POLYGON_BASE self --- @param #POINT_VEC3.SmokeColor SmokeColor The smoke color. --- @return #ZONE_POLYGON_BASE self -function ZONE_POLYGON_BASE:SmokeZone( SmokeColor ) - self:F2( SmokeColor ) - - local i - local j - local Segments = 10 - - i = 1 - j = #self.Polygon - - while i <= #self.Polygon do - self:T( { i, j, self.Polygon[i], self.Polygon[j] } ) - - local DeltaX = self.Polygon[j].x - self.Polygon[i].x - local DeltaY = self.Polygon[j].y - self.Polygon[i].y - - for Segment = 0, Segments do -- We divide each line in 5 segments and smoke a point on the line. - local PointX = self.Polygon[i].x + ( Segment * DeltaX / Segments ) - local PointY = self.Polygon[i].y + ( Segment * DeltaY / Segments ) - POINT_VEC2:New( PointX, PointY ):Smoke( SmokeColor ) - end - j = i - i = i + 1 - end - - return self -end - - - - ---- Returns if a location is within the zone. --- @param #ZONE_POLYGON_BASE self --- @param DCSTypes#Vec2 PointVec2 The location to test. --- @return #boolean true if the location is within the zone. -function ZONE_POLYGON_BASE:IsPointVec2InZone( PointVec2 ) - self:F2( PointVec2 ) - - local i - local j - local c = false - - i = 1 - j = #self.Polygon - - while i < #self.Polygon do - j = i - i = i + 1 - self:T( { i, j, self.Polygon[i], self.Polygon[j] } ) - if ( ( ( self.Polygon[i].y > PointVec2.y ) ~= ( self.Polygon[j].y > PointVec2.y ) ) and - ( PointVec2.x < ( self.Polygon[j].x - self.Polygon[i].x ) * ( PointVec2.y - self.Polygon[i].y ) / ( self.Polygon[j].y - self.Polygon[i].y ) + self.Polygon[i].x ) - ) then - c = not c - end - self:T2( { "c = ", c } ) - end - - self:T( { "c = ", c } ) - return c -end - ---- Define a random @{DCSTypes#Vec2} within the zone. --- @param #ZONE_POLYGON_BASE self --- @return DCSTypes#Vec2 The Vec2 coordinate. -function ZONE_POLYGON_BASE:GetRandomVec2() - self:F2() - - --- It is a bit tricky to find a random point within a polygon. Right now i am doing it the dirty and inefficient way... - local Vec2Found = false - local Vec2 - local BS = self:GetBoundingSquare() - - self:T2( BS ) - - while Vec2Found == false do - Vec2 = { x = math.random( BS.x1, BS.x2 ), y = math.random( BS.y1, BS.y2 ) } - self:T2( Vec2 ) - if self:IsPointVec2InZone( Vec2 ) then - Vec2Found = true - end - end - - self:T2( Vec2 ) - - return Vec2 -end - ---- Get the bounding square the zone. --- @param #ZONE_POLYGON_BASE self --- @return #ZONE_POLYGON_BASE.BoundingSquare The bounding square. -function ZONE_POLYGON_BASE:GetBoundingSquare() - - local x1 = self.Polygon[1].x - local y1 = self.Polygon[1].y - local x2 = self.Polygon[1].x - local y2 = self.Polygon[1].y - - for i = 2, #self.Polygon do - self:T2( { self.Polygon[i], x1, y1, x2, y2 } ) - x1 = ( x1 > self.Polygon[i].x ) and self.Polygon[i].x or x1 - x2 = ( x2 < self.Polygon[i].x ) and self.Polygon[i].x or x2 - y1 = ( y1 > self.Polygon[i].y ) and self.Polygon[i].y or y1 - y2 = ( y2 < self.Polygon[i].y ) and self.Polygon[i].y or y2 - - end - - return { x1 = x1, y1 = y1, x2 = x2, y2 = y2 } -end - - - - - ---- The ZONE_POLYGON class defined by a sequence of @{Group#GROUP} waypoints within the Mission Editor, forming a polygon. --- @type ZONE_POLYGON --- @extends Zone#ZONE_POLYGON_BASE -ZONE_POLYGON = { - ClassName="ZONE_POLYGON", - } - ---- 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. --- @param #ZONE_POLYGON self --- @param #string ZoneName Name of the zone. --- @param Group#GROUP ZoneGroup The GROUP waypoints as defined within the Mission Editor define the polygon shape. --- @return #ZONE_POLYGON self -function ZONE_POLYGON:New( ZoneName, ZoneGroup ) - - local GroupPoints = ZoneGroup:GetTaskRoute() - - local self = BASE:Inherit( self, ZONE_POLYGON_BASE:New( ZoneName, GroupPoints ) ) - self:F( { ZoneName, ZoneGroup, self.Polygon } ) - - return self -end - ---- This module contains the CLIENT class. --- --- 1) @{Client#CLIENT} class, extends @{Unit#UNIT} --- =============================================== --- 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#CLIENT} class is a wrapper class to handle the DCS Unit objects that have the skillset defined as __Client__ or __Player__: --- --- * Wraps the DCS Unit objects with skill level set to Player or Client. --- * Support all DCS Unit APIs. --- * Enhance with Unit specific APIs not in the DCS Group API set. --- * When player joins Unit, execute alive init logic. --- * Handles messages to players. --- * Manage the "state" of the DCS Unit. --- --- Clients are being used by the @{MISSION} class to follow players and register their successes. --- --- 1.1) CLIENT reference methods --- ----------------------------- --- For each DCS Unit having skill level Player or Client, a CLIENT wrapper object (instance) will be created within the _@{DATABASE} object. --- This is done at the beginning of the mission (when the mission starts). --- --- The CLIENT class does not contain a :New() method, rather it provides :Find() methods to retrieve the object reference --- using the DCS Unit or the DCS UnitName. --- --- Another thing to know is that CLIENT objects do not "contain" the DCS Unit object. --- The CLIENT methods will reference the DCS Unit object by name when it is needed during API execution. --- If the DCS Unit object does not exist or is nil, the CLIENT methods will return nil and log an exception in the DCS.log file. --- --- The CLIENT class provides the following functions to retrieve quickly the relevant CLIENT instance: --- --- * @{#CLIENT.Find}(): Find a CLIENT instance from the _DATABASE object using a DCS Unit object. --- * @{#CLIENT.FindByName}(): Find a CLIENT instance from the _DATABASE object using a DCS Unit name. --- --- IMPORTANT: ONE SHOULD NEVER SANATIZE these CLIENT OBJECT REFERENCES! (make the CLIENT object references nil). --- --- @module Client --- @author FlightControl - ---- The CLIENT class --- @type CLIENT --- @extends Unit#UNIT -CLIENT = { - ONBOARDSIDE = { - NONE = 0, - LEFT = 1, - RIGHT = 2, - BACK = 3, - FRONT = 4 - }, - ClassName = "CLIENT", - ClientName = nil, - ClientAlive = false, - ClientTransport = false, - ClientBriefingShown = false, - _Menus = {}, - _Tasks = {}, - Messages = { - } -} - - ---- Finds a CLIENT from the _DATABASE using the relevant DCS Unit. --- @param #CLIENT self --- @param #string ClientName Name of the DCS **Unit** as defined within the Mission Editor. --- @param #string ClientBriefing Text that describes the briefing of the mission when a Player logs into the Client. --- @return #CLIENT --- @usage --- -- Create new Clients. --- local Mission = MISSIONSCHEDULER.AddMission( 'Russia Transport Troops SA-6', 'Operational', 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', 'Russia' ) --- Mission:AddGoal( DeploySA6TroopsGoal ) --- --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 1' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 3' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 2' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 4' ):Transport() ) -function CLIENT:Find( DCSUnit ) - local ClientName = DCSUnit:getName() - local ClientFound = _DATABASE:FindClient( ClientName ) - - if ClientFound then - ClientFound:F( ClientName ) - return ClientFound - end - - error( "CLIENT not found for: " .. ClientName ) -end - - ---- Finds a CLIENT from the _DATABASE using the relevant Client Unit Name. --- As an optional parameter, a briefing text can be given also. --- @param #CLIENT self --- @param #string ClientName Name of the DCS **Unit** as defined within the Mission Editor. --- @param #string ClientBriefing Text that describes the briefing of the mission when a Player logs into the Client. --- @return #CLIENT --- @usage --- -- Create new Clients. --- local Mission = MISSIONSCHEDULER.AddMission( 'Russia Transport Troops SA-6', 'Operational', 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', 'Russia' ) --- Mission:AddGoal( DeploySA6TroopsGoal ) --- --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 1' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 3' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 2' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 4' ):Transport() ) -function CLIENT:FindByName( ClientName, ClientBriefing ) - local ClientFound = _DATABASE:FindClient( ClientName ) - - if ClientFound then - ClientFound:F( { ClientName, ClientBriefing } ) - ClientFound:AddBriefing( ClientBriefing ) - ClientFound.MessageSwitch = true - - return ClientFound - end - - error( "CLIENT not found for: " .. ClientName ) -end - -function CLIENT:Register( ClientName ) - local self = BASE:Inherit( self, UNIT:Register( ClientName ) ) - - self:F( ClientName ) - self.ClientName = ClientName - self.MessageSwitch = true - self.ClientAlive2 = false - - --self.AliveCheckScheduler = routines.scheduleFunction( self._AliveCheckScheduler, { self }, timer.getTime() + 1, 5 ) - self.AliveCheckScheduler = SCHEDULER:New( self, self._AliveCheckScheduler, { "Client Alive " .. ClientName }, 1, 5 ) - - self:E( self ) - return self -end - - ---- Transport defines that the Client is a Transport. Transports show cargo. --- @param #CLIENT self --- @return #CLIENT -function CLIENT:Transport() - self:F() - - self.ClientTransport = true - return self -end - ---- AddBriefing adds a briefing to a CLIENT when a player joins a mission. --- @param #CLIENT self --- @param #string ClientBriefing is the text defining the Mission briefing. --- @return #CLIENT self -function CLIENT:AddBriefing( ClientBriefing ) - self:F( ClientBriefing ) - self.ClientBriefing = ClientBriefing - self.ClientBriefingShown = false - - return self -end - ---- Show the briefing of a CLIENT. --- @param #CLIENT self --- @return #CLIENT self -function CLIENT:ShowBriefing() - self:F( { self.ClientName, self.ClientBriefingShown } ) - - if not self.ClientBriefingShown then - self.ClientBriefingShown = true - local Briefing = "" - if self.ClientBriefing then - Briefing = Briefing .. self.ClientBriefing - end - Briefing = Briefing .. " Press [LEFT ALT]+[B] to view the complete mission briefing." - self:Message( Briefing, 60, "Briefing" ) - end - - return self -end - ---- Show the mission briefing of a MISSION to the CLIENT. --- @param #CLIENT self --- @param #string MissionBriefing --- @return #CLIENT self -function CLIENT:ShowMissionBriefing( MissionBriefing ) - self:F( { self.ClientName } ) - - if MissionBriefing then - self:Message( MissionBriefing, 60, "Mission Briefing" ) - end - - return self -end - - - ---- Resets a CLIENT. --- @param #CLIENT self --- @param #string ClientName Name of the Group as defined within the Mission Editor. The Group must have a Unit with the type Client. -function CLIENT:Reset( ClientName ) - self:F() - self._Menus = {} -end - --- Is Functions - ---- Checks if the CLIENT is a multi-seated UNIT. --- @param #CLIENT self --- @return #boolean true if multi-seated. -function CLIENT:IsMultiSeated() - self:F( self.ClientName ) - - local ClientMultiSeatedTypes = { - ["Mi-8MT"] = "Mi-8MT", - ["UH-1H"] = "UH-1H", - ["P-51B"] = "P-51B" - } - - if self:IsAlive() then - local ClientTypeName = self:GetClientGroupUnit():GetTypeName() - if ClientMultiSeatedTypes[ClientTypeName] then - return true - end - end - - return false -end - ---- Checks for a client alive event and calls a function on a continuous basis. --- @param #CLIENT self --- @param #function CallBack Function. --- @return #CLIENT -function CLIENT:Alive( CallBackFunction, ... ) - self:F() - - self.ClientCallBack = CallBackFunction - self.ClientParameters = arg - - return self -end - ---- @param #CLIENT self -function CLIENT:_AliveCheckScheduler( SchedulerName ) - self:F( { SchedulerName, self.ClientName, self.ClientAlive2, self.ClientBriefingShown, self.ClientCallBack } ) - - if self:IsAlive() then - if self.ClientAlive2 == false then - self:ShowBriefing() - if self.ClientCallBack then - self:T("Calling Callback function") - self.ClientCallBack( self, unpack( self.ClientParameters ) ) - end - self.ClientAlive2 = true - end - else - if self.ClientAlive2 == true then - self.ClientAlive2 = false - end - end - - return true -end - ---- Return the DCSGroup of a Client. --- This function is modified to deal with a couple of bugs in DCS 1.5.3 --- @param #CLIENT self --- @return DCSGroup#Group -function CLIENT:GetDCSGroup() - self:F3() - --- local ClientData = Group.getByName( self.ClientName ) --- if ClientData and ClientData:isExist() then --- self:T( self.ClientName .. " : group found!" ) --- return ClientData --- else --- return nil --- end - - local ClientUnit = Unit.getByName( self.ClientName ) - - local CoalitionsData = { AlivePlayersRed = coalition.getPlayers( coalition.side.RED ), AlivePlayersBlue = coalition.getPlayers( coalition.side.BLUE ) } - for CoalitionId, CoalitionData in pairs( CoalitionsData ) do - self:T3( { "CoalitionData:", CoalitionData } ) - for UnitId, UnitData in pairs( CoalitionData ) do - self:T3( { "UnitData:", UnitData } ) - if UnitData and UnitData:isExist() then - - --self:E(self.ClientName) - if ClientUnit then - local ClientGroup = ClientUnit:getGroup() - if ClientGroup then - self:T3( "ClientGroup = " .. self.ClientName ) - if ClientGroup:isExist() and UnitData:getGroup():isExist() then - if ClientGroup:getID() == UnitData:getGroup():getID() then - self:T3( "Normal logic" ) - self:T3( self.ClientName .. " : group found!" ) - self.ClientGroupID = ClientGroup:getID() - self.ClientGroupName = ClientGroup:getName() - return ClientGroup - end - else - -- Now we need to resolve the bugs in DCS 1.5 ... - -- Consult the database for the units of the Client Group. (ClientGroup:getUnits() returns nil) - self:T3( "Bug 1.5 logic" ) - local ClientGroupTemplate = _DATABASE.Templates.Units[self.ClientName].GroupTemplate - self.ClientGroupID = ClientGroupTemplate.groupId - self.ClientGroupName = _DATABASE.Templates.Units[self.ClientName].GroupName - self:T3( self.ClientName .. " : group found in bug 1.5 resolvement logic!" ) - return ClientGroup - end - -- else - -- error( "Client " .. self.ClientName .. " not found!" ) - end - else - --self:E( { "Client not found!", self.ClientName } ) - end - end - end - end - - -- For non player clients - if ClientUnit then - local ClientGroup = ClientUnit:getGroup() - if ClientGroup then - self:T3( "ClientGroup = " .. self.ClientName ) - if ClientGroup:isExist() then - self:T3( "Normal logic" ) - self:T3( self.ClientName .. " : group found!" ) - return ClientGroup - end - end - end - - self.ClientGroupID = nil - self.ClientGroupUnit = nil - - return nil -end - - --- TODO: Check DCSTypes#Group.ID ---- Get the group ID of the client. --- @param #CLIENT self --- @return DCSTypes#Group.ID -function CLIENT:GetClientGroupID() - - local ClientGroup = self:GetDCSGroup() - - --self:E( self.ClientGroupID ) -- Determined in GetDCSGroup() - return self.ClientGroupID -end - - ---- Get the name of the group of the client. --- @param #CLIENT self --- @return #string -function CLIENT:GetClientGroupName() - - local ClientGroup = self:GetDCSGroup() - - self:T( self.ClientGroupName ) -- Determined in GetDCSGroup() - return self.ClientGroupName -end - ---- Returns the UNIT of the CLIENT. --- @param #CLIENT self --- @return Unit#UNIT -function CLIENT:GetClientGroupUnit() - self:F2() - - local ClientDCSUnit = Unit.getByName( self.ClientName ) - - self:T( self.ClientDCSUnit ) - if ClientDCSUnit and ClientDCSUnit:isExist() then - local ClientUnit = _DATABASE:FindUnit( self.ClientName ) - self:T2( ClientUnit ) - return ClientUnit - end -end - ---- Returns the DCSUnit of the CLIENT. --- @param #CLIENT self --- @return DCSTypes#Unit -function CLIENT:GetClientGroupDCSUnit() - self:F2() - - local ClientDCSUnit = Unit.getByName( self.ClientName ) - - if ClientDCSUnit and ClientDCSUnit:isExist() then - self:T2( ClientDCSUnit ) - return ClientDCSUnit - end -end - - ---- Evaluates if the CLIENT is a transport. --- @param #CLIENT self --- @return #boolean true is a transport. -function CLIENT:IsTransport() - self:F() - return self.ClientTransport -end - ---- Shows the @{Cargo#CARGO} contained within the CLIENT to the player as a message. --- The @{Cargo#CARGO} is shown using the @{Message#MESSAGE} distribution system. --- @param #CLIENT self -function CLIENT:ShowCargo() - self:F() - - local CargoMsg = "" - - for CargoName, Cargo in pairs( CARGOS ) do - if self == Cargo:IsLoadedInClient() then - CargoMsg = CargoMsg .. Cargo.CargoName .. " Type:" .. Cargo.CargoType .. " Weight: " .. Cargo.CargoWeight .. "\n" - end - end - - if CargoMsg == "" then - CargoMsg = "empty" - end - - self:Message( CargoMsg, 15, "Co-Pilot: Cargo Status", 30 ) - -end - --- 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. --- @param #CLIENT self --- @param #string Message is the text describing the message. --- @param #number MessageDuration is the duration in seconds that the Message should be displayed. --- @param #string MessageCategory is the category of the message (the title). --- @param #number MessageInterval is the interval in seconds between the display of the @{Message#MESSAGE} when the CLIENT is in the air. --- @param #string MessageID is the identifier of the message when displayed with intervals. -function CLIENT:Message( Message, MessageDuration, MessageCategory, MessageInterval, MessageID ) - self:F( { Message, MessageDuration, MessageCategory, MessageInterval } ) - - if not self.MenuMessages then - if self:GetClientGroupID() then - self.MenuMessages = MENU_CLIENT:New( self, 'Messages' ) - self.MenuRouteMessageOn = MENU_CLIENT_COMMAND:New( self, 'Messages On', self.MenuMessages, CLIENT.SwitchMessages, { self, true } ) - self.MenuRouteMessageOff = MENU_CLIENT_COMMAND:New( self,'Messages Off', self.MenuMessages, CLIENT.SwitchMessages, { self, false } ) - end - end - - if self.MessageSwitch == true then - if MessageCategory == nil then - MessageCategory = "Messages" - end - if MessageID ~= nil then - if self.Messages[MessageID] == nil then - self.Messages[MessageID] = {} - self.Messages[MessageID].MessageId = MessageID - self.Messages[MessageID].MessageTime = timer.getTime() - self.Messages[MessageID].MessageDuration = MessageDuration - if MessageInterval == nil then - self.Messages[MessageID].MessageInterval = 600 - else - self.Messages[MessageID].MessageInterval = MessageInterval - end - MESSAGE:New( Message, MessageDuration, MessageCategory ):ToClient( self ) - else - if self:GetClientGroupDCSUnit() and not self:GetClientGroupDCSUnit():inAir() then - if timer.getTime() - self.Messages[MessageID].MessageTime >= self.Messages[MessageID].MessageDuration + 10 then - MESSAGE:New( Message, MessageDuration , MessageCategory):ToClient( self ) - self.Messages[MessageID].MessageTime = timer.getTime() - end - else - if timer.getTime() - self.Messages[MessageID].MessageTime >= self.Messages[MessageID].MessageDuration + self.Messages[MessageID].MessageInterval then - MESSAGE:New( Message, MessageDuration, MessageCategory ):ToClient( self ) - self.Messages[MessageID].MessageTime = timer.getTime() - end - end - end - else - MESSAGE:New( Message, MessageDuration, MessageCategory ):ToClient( self ) - end - end -end ---- This module contains the STATIC class. --- --- 1) @{Static#STATIC} class, extends @{Unit#UNIT} --- =============================================== --- 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: --- --- * Wraps the DCS Static objects. --- * Support all DCS Static APIs. --- * Enhance with Static specific APIs not in the DCS API set. --- --- 1.1) STATIC reference methods --- ----------------------------- --- For each DCS Static will have a STATIC wrapper object (instance) within the _@{DATABASE} object. --- This is done at the beginning of the mission (when the mission starts). --- --- The STATIC class does not contain a :New() method, rather it provides :Find() methods to retrieve the object reference --- using the Static Name. --- --- Another thing to know is that STATIC objects do not "contain" the DCS Static object. --- The STATIc methods will reference the DCS Static object by name when it is needed during API execution. --- If the DCS Static object does not exist or is nil, the STATIC methods will return nil and log an exception in the DCS.log file. --- --- The STATIc class provides the following functions to retrieve quickly the relevant STATIC instance: --- --- * @{#STATIC.FindByName}(): Find a STATIC instance from the _DATABASE object using a DCS Static name. --- --- IMPORTANT: ONE SHOULD NEVER SANATIZE these STATIC OBJECT REFERENCES! (make the STATIC object references nil). --- --- @module Static --- @author FlightControl - - - - - - ---- The STATIC class --- @type STATIC --- @extends Unit#UNIT -STATIC = { - ClassName = "STATIC", -} - - ---- Finds a STATIC from the _DATABASE using the relevant Static Name. --- As an optional parameter, a briefing text can be given also. --- @param #STATIC self --- @param #string StaticName Name of the DCS **Static** as defined within the Mission Editor. --- @return #STATIC -function STATIC:FindByName( StaticName ) - local StaticFound = _DATABASE:FindStatic( StaticName ) - - if StaticFound then - StaticFound:F( { StaticName } ) - - return StaticFound - end - - error( "STATIC not found for: " .. StaticName ) -end - -function STATIC:Register( StaticName ) - local self = BASE:Inherit( self, UNIT:Register( StaticName ) ) - - self:F( StaticName ) - - return self -end - - -function STATIC:GetDCSUnit() - local DCSStatic = StaticObject.getByName( self.UnitName ) - - if DCSStatic then - return DCSStatic - end - - return nil -end ---- This module contains the AIRBASE classes. --- --- === --- --- 1) @{Airbase#AIRBASE} class, extends @{Base#BASE} --- ================================================= --- The @{AIRBASE} class is a 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. --- --- --- 1.1) AIRBASE reference methods --- ------------------------------ --- For each DCS Airbase object alive within a running mission, a AIRBASE wrapper object (instance) will be created within the _@{DATABASE} object. --- This is done at the beginning of the mission (when the mission starts). --- --- The AIRBASE class **does not contain a :New()** method, rather it provides **:Find()** methods to retrieve the object reference --- using the DCS Airbase or the DCS AirbaseName. --- --- Another thing to know is that AIRBASE objects do not "contain" the DCS Airbase object. --- The AIRBASE methods will reference the DCS Airbase object by name when it is needed during API execution. --- If the DCS Airbase object does not exist or is nil, the AIRBASE methods will return nil and log an exception in the DCS.log file. --- --- The AIRBASE class provides the following functions to retrieve quickly the relevant AIRBASE instance: --- --- * @{#AIRBASE.Find}(): Find a AIRBASE instance from the _DATABASE object using a DCS Airbase object. --- * @{#AIRBASE.FindByName}(): Find a AIRBASE instance from the _DATABASE object using a DCS Airbase name. --- --- IMPORTANT: ONE SHOULD NEVER SANATIZE these AIRBASE OBJECT REFERENCES! (make the AIRBASE object references nil). --- --- 1.2) DCS AIRBASE APIs --- --------------------- --- The DCS Airbase APIs are used extensively within MOOSE. The AIRBASE class has for each DCS Airbase API a corresponding method. --- To be able to distinguish easily in your code the difference between a AIRBASE API call and a DCS Airbase API call, --- the first letter of the method is also capitalized. So, by example, the DCS Airbase method @{DCSAirbase#Airbase.getName}() --- is implemented in the AIRBASE class as @{#AIRBASE.GetName}(). --- --- More functions will be added --- ---------------------------- --- During the MOOSE development, more functions will be added. --- --- @module Airbase --- @author FlightControl - - - - - ---- The AIRBASE class --- @type AIRBASE --- @extends Base#BASE -AIRBASE = { - ClassName="AIRBASE", - CategoryName = { - [Airbase.Category.AIRDROME] = "Airdrome", - [Airbase.Category.HELIPAD] = "Helipad", - [Airbase.Category.SHIP] = "Ship", - }, - } - --- Registration. - ---- Create a new AIRBASE from DCSAirbase. --- @param #AIRBASE self --- @param DCSAirbase#Airbase DCSAirbase --- @param Database#DATABASE Database --- @return Airbase#AIRBASE -function AIRBASE:Register( AirbaseName ) - - local self = BASE:Inherit( self, BASE:New() ) - self:F2( AirbaseName ) - self.AirbaseName = AirbaseName - return self -end - --- Reference methods. - ---- Finds a AIRBASE from the _DATABASE using a DCSAirbase object. --- @param #AIRBASE self --- @param DCSAirbase#Airbase DCSAirbase An existing DCS Airbase object reference. --- @return Airbase#AIRBASE self -function AIRBASE:Find( DCSAirbase ) - - local AirbaseName = DCSAirbase:getName() - local AirbaseFound = _DATABASE:FindAirbase( AirbaseName ) - return AirbaseFound -end - ---- Find a AIRBASE in the _DATABASE using the name of an existing DCS Airbase. --- @param #AIRBASE self --- @param #string AirbaseName The Airbase Name. --- @return Airbase#AIRBASE self -function AIRBASE:FindByName( AirbaseName ) - - local AirbaseFound = _DATABASE:FindAirbase( AirbaseName ) - return AirbaseFound -end - -function AIRBASE:GetDCSAirbase() - local DCSAirbase = Airbase.getByName( self.AirbaseName ) - - if DCSAirbase then - return DCSAirbase - end - - return nil -end - ---- Returns coalition of the Airbase. --- @param Airbase#AIRBASE self --- @return DCSCoalitionObject#coalition.side The side of the coalition. --- @return #nil The DCS Airbase is not existing or alive. -function AIRBASE:GetCoalition() - self:F2( self.AirbaseName ) - - local DCSAirbase = self:GetDCSAirbase() - - if DCSAirbase then - local AirbaseCoalition = DCSAirbase:getCoalition() - self:T3( AirbaseCoalition ) - return AirbaseCoalition - end - - return nil -end - ---- Returns country of the Airbase. --- @param Airbase#AIRBASE self --- @return DCScountry#country.id The country identifier. --- @return #nil The DCS Airbase is not existing or alive. -function AIRBASE:GetCountry() - self:F2( self.AirbaseName ) - - local DCSAirbase = self:GetDCSAirbase() - - if DCSAirbase then - local AirbaseCountry = DCSAirbase:getCountry() - self:T3( AirbaseCountry ) - return AirbaseCountry - end - - return nil -end - - ---- Returns DCS Airbase object name. --- The function provides access to non-activated units too. --- @param Airbase#AIRBASE self --- @return #string The name of the DCS Airbase. --- @return #nil The DCS Airbase is not existing or alive. -function AIRBASE:GetName() - self:F2( self.AirbaseName ) - - local DCSAirbase = self:GetDCSAirbase() - - if DCSAirbase then - local AirbaseName = self.AirbaseName - return AirbaseName - end - - return nil -end - - ---- Returns if the airbase is alive. --- @param Airbase#AIRBASE self --- @return #boolean true if Airbase is alive. --- @return #nil The DCS Airbase is not existing or alive. -function AIRBASE:IsAlive() - self:F2( self.AirbaseName ) - - local DCSAirbase = self:GetDCSAirbase() - - if DCSAirbase then - local AirbaseIsAlive = DCSAirbase:isExist() - return AirbaseIsAlive - end - - return false -end - ---- Returns the unit's unique identifier. --- @param Airbase#AIRBASE self --- @return DCSAirbase#Airbase.ID Airbase ID --- @return #nil The DCS Airbase is not existing or alive. -function AIRBASE:GetID() - self:F2( self.AirbaseName ) - - local DCSAirbase = self:GetDCSAirbase() - - if DCSAirbase then - local AirbaseID = DCSAirbase:getID() - return AirbaseID - end - - return nil -end - ---- Returns the Airbase's callsign - the localized string. --- @param Airbase#AIRBASE self --- @return #string The Callsign of the Airbase. --- @return #nil The DCS Airbase is not existing or alive. -function AIRBASE:GetCallSign() - self:F2( self.AirbaseName ) - - local DCSAirbase = self:GetDCSAirbase() - - if DCSAirbase then - local AirbaseCallSign = DCSAirbase:getCallsign() - return AirbaseCallSign - end - - return nil -end - - - ---- Returns unit descriptor. Descriptor type depends on unit category. --- @param Airbase#AIRBASE self --- @return DCSAirbase#Airbase.Desc The Airbase descriptor. --- @return #nil The DCS Airbase is not existing or alive. -function AIRBASE:GetDesc() - self:F2( self.AirbaseName ) - - local DCSAirbase = self:GetDCSAirbase() - - if DCSAirbase then - local AirbaseDesc = DCSAirbase:getDesc() - return AirbaseDesc - end - - return nil -end - - ---- Returns the type name of the DCS Airbase. --- @param Airbase#AIRBASE self --- @return #string The type name of the DCS Airbase. --- @return #nil The DCS Airbase is not existing or alive. -function AIRBASE:GetTypeName() - self:F2( self.AirbaseName ) - - local DCSAirbase = self:GetDCSAirbase() - - if DCSAirbase then - local AirbaseTypeName = DCSAirbase:getTypeName() - self:T3( AirbaseTypeName ) - return AirbaseTypeName - end - - return nil -end - - ---- Returns the @{DCSTypes#Vec2} vector indicating the point in 2D of the DCS Airbase within the mission. --- @param Airbase#AIRBASE self --- @return DCSTypes#Vec2 The 2D point vector of the DCS Airbase. --- @return #nil The DCS Airbase is not existing or alive. -function AIRBASE:GetPointVec2() - self:F2( self.AirbaseName ) - - local DCSAirbase = self:GetDCSAirbase() - - if DCSAirbase then - local AirbasePointVec3 = DCSAirbase:getPosition().p - - local AirbasePointVec2 = {} - AirbasePointVec2.x = AirbasePointVec3.x - AirbasePointVec2.y = AirbasePointVec3.z - - self:T3( AirbasePointVec2 ) - return AirbasePointVec2 - end - - return nil -end - ---- Returns the DCS Airbase category as defined within the DCS Airbase Descriptor. --- @param Airbase#AIRBASE self --- @return DCSAirbase#Airbase.Category The DCS Airbase Category -function AIRBASE:GetCategory() - local DCSAirbase = self:GetDCSAirbase() - - if DCSAirbase then - local AirbaseCategory = self:GetDesc().category - return AirbaseCategory - end - - return nil -end - - ---- Returns the DCS Airbase category name as defined within the DCS Airbase Descriptor. --- @param Airbase#AIRBASE self --- @return #string The DCS Airbase Category Name -function AIRBASE:GetCategoryName() - local DCSAirbase = self:GetDCSAirbase() - - if DCSAirbase then - local AirbaseCategoryName = self.CategoryName[ self:GetDesc().category ] - return AirbaseCategoryName - end - - return nil -end - - ---- This module contains the DATABASE class, managing the database of mission objects. --- --- ==== --- --- 1) @{Database#DATABASE} class, extends @{Base#BASE} --- =================================================== --- Mission designers can use the DATABASE class to refer to: --- --- * UNITS --- * GROUPS --- * CLIENTS --- * AIRPORTS --- * PLAYERSJOINED --- * PLAYERS --- --- On top, for internal MOOSE administration purposes, the DATBASE administers the Unit and Group TEMPLATES as defined within the Mission Editor. --- --- Moose will automatically create one instance of the DATABASE class into the **global** object _DATABASE. --- Moose refers to _DATABASE within the framework extensively, but you can also refer to the _DATABASE object within your missions if required. --- --- 1.1) DATABASE iterators --- ----------------------- --- You can iterate the database with the available iterator methods. --- The iterator methods will walk the DATABASE set, and call for each element within the set a function that you provide. --- The following iterator methods are currently available within the DATABASE: --- --- * @{#DATABASE.ForEachUnit}: Calls a function for each @{UNIT} it finds within the DATABASE. --- * @{#DATABASE.ForEachGroup}: Calls a function for each @{GROUP} it finds within the DATABASE. --- * @{#DATABASE.ForEachPlayer}: Calls a function for each alive player it finds within the DATABASE. --- * @{#DATABASE.ForEachPlayerJoined}: Calls a function for each joined player it finds within the DATABASE. --- * @{#DATABASE.ForEachClient}: Calls a function for each @{CLIENT} it finds within the DATABASE. --- * @{#DATABASE.ForEachClientAlive}: Calls a function for each alive @{CLIENT} it finds within the DATABASE. --- --- === --- --- @module Database --- @author FlightControl - ---- DATABASE class --- @type DATABASE --- @extends Base#BASE -DATABASE = { - ClassName = "DATABASE", - Templates = { - Units = {}, - Groups = {}, - ClientsByName = {}, - ClientsByID = {}, - }, - UNITS = {}, - STATICS = {}, - GROUPS = {}, - PLAYERS = {}, - PLAYERSJOINED = {}, - CLIENTS = {}, - AIRBASES = {}, - NavPoints = {}, -} - -local _DATABASECoalition = - { - [1] = "Red", - [2] = "Blue", - } - -local _DATABASECategory = - { - ["plane"] = Unit.Category.AIRPLANE, - ["helicopter"] = Unit.Category.HELICOPTER, - ["vehicle"] = Unit.Category.GROUND_UNIT, - ["ship"] = Unit.Category.SHIP, - ["static"] = Unit.Category.STRUCTURE, - } - - ---- Creates a new DATABASE object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. --- @param #DATABASE self --- @return #DATABASE --- @usage --- -- Define a new DATABASE Object. This DBObject will contain a reference to all Group and Unit Templates defined within the ME and the DCSRTE. --- DBObject = DATABASE:New() -function DATABASE:New() - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) - - _EVENTDISPATCHER:OnBirth( self._EventOnBirth, self ) - _EVENTDISPATCHER:OnDead( self._EventOnDeadOrCrash, self ) - _EVENTDISPATCHER:OnCrash( self._EventOnDeadOrCrash, self ) - - - -- Follow alive players and clients - _EVENTDISPATCHER:OnPlayerEnterUnit( self._EventOnPlayerEnterUnit, self ) - _EVENTDISPATCHER:OnPlayerLeaveUnit( self._EventOnPlayerLeaveUnit, self ) - - self:_RegisterTemplates() - self:_RegisterGroupsAndUnits() - self:_RegisterClients() - self:_RegisterStatics() - self:_RegisterPlayers() - self:_RegisterAirbases() - - return self -end - ---- Finds a Unit based on the Unit Name. --- @param #DATABASE self --- @param #string UnitName --- @return Unit#UNIT The found Unit. -function DATABASE:FindUnit( UnitName ) - - local UnitFound = self.UNITS[UnitName] - return UnitFound -end - - ---- Adds a Unit based on the Unit Name in the DATABASE. --- @param #DATABASE self -function DATABASE:AddUnit( DCSUnitName ) - - if not self.UNITS[DCSUnitName] then - self.UNITS[DCSUnitName] = UNIT:Register( DCSUnitName ) - end - - return self.UNITS[DCSUnitName] -end - - ---- Deletes a Unit from the DATABASE based on the Unit Name. --- @param #DATABASE self -function DATABASE:DeleteUnit( DCSUnitName ) - - --self.UNITS[DCSUnitName] = nil -end - ---- Adds a Static based on the Static Name in the DATABASE. --- @param #DATABASE self -function DATABASE:AddStatic( DCSStaticName ) - - if not self.STATICS[DCSStaticName] then - self.STATICS[DCSStaticName] = STATIC:Register( DCSStaticName ) - end -end - - ---- Deletes a Static from the DATABASE based on the Static Name. --- @param #DATABASE self -function DATABASE:DeleteStatic( DCSStaticName ) - - --self.STATICS[DCSStaticName] = nil -end - ---- Finds a STATIC based on the StaticName. --- @param #DATABASE self --- @param #string StaticName --- @return Static#STATIC The found STATIC. -function DATABASE:FindStatic( StaticName ) - - local StaticFound = self.STATICS[StaticName] - return StaticFound -end - ---- Adds a Airbase based on the Airbase Name in the DATABASE. --- @param #DATABASE self -function DATABASE:AddAirbase( DCSAirbaseName ) - - if not self.AIRBASES[DCSAirbaseName] then - self.AIRBASES[DCSAirbaseName] = AIRBASE:Register( DCSAirbaseName ) - end -end - - ---- Deletes a Airbase from the DATABASE based on the Airbase Name. --- @param #DATABASE self -function DATABASE:DeleteAirbase( DCSAirbaseName ) - - --self.AIRBASES[DCSAirbaseName] = nil -end - ---- Finds a AIRBASE based on the AirbaseName. --- @param #DATABASE self --- @param #string AirbaseName --- @return Airbase#AIRBASE The found AIRBASE. -function DATABASE:FindAirbase( AirbaseName ) - - local AirbaseFound = self.AIRBASES[AirbaseName] - return AirbaseFound -end - - ---- Finds a CLIENT based on the ClientName. --- @param #DATABASE self --- @param #string ClientName --- @return Client#CLIENT The found CLIENT. -function DATABASE:FindClient( ClientName ) - - local ClientFound = self.CLIENTS[ClientName] - return ClientFound -end - - ---- Adds a CLIENT based on the ClientName in the DATABASE. --- @param #DATABASE self -function DATABASE:AddClient( ClientName ) - - if not self.CLIENTS[ClientName] then - self.CLIENTS[ClientName] = CLIENT:Register( ClientName ) - end - - return self.CLIENTS[ClientName] -end - - ---- Finds a GROUP based on the GroupName. --- @param #DATABASE self --- @param #string GroupName --- @return Group#GROUP The found GROUP. -function DATABASE:FindGroup( GroupName ) - - local GroupFound = self.GROUPS[GroupName] - return GroupFound -end - - ---- Adds a GROUP based on the GroupName in the DATABASE. --- @param #DATABASE self -function DATABASE:AddGroup( GroupName ) - - if not self.GROUPS[GroupName] then - self.GROUPS[GroupName] = GROUP:Register( GroupName ) - end - - return self.GROUPS[GroupName] -end - ---- Adds a player based on the Player Name in the DATABASE. --- @param #DATABASE self -function DATABASE:AddPlayer( UnitName, PlayerName ) - - if PlayerName then - self:E( { "Add player for unit:", UnitName, PlayerName } ) - self.PLAYERS[PlayerName] = self:FindUnit( UnitName ) - self.PLAYERSJOINED[PlayerName] = PlayerName - end -end - ---- Deletes a player from the DATABASE based on the Player Name. --- @param #DATABASE self -function DATABASE:DeletePlayer( PlayerName ) - - if PlayerName then - self:E( { "Clean player:", PlayerName } ) - self.PLAYERS[PlayerName] = nil - end -end - - ---- Instantiate new Groups within the DCSRTE. --- This method expects EXACTLY the same structure as a structure within the ME, and needs 2 additional fields defined: --- SpawnCountryID, SpawnCategoryID --- This method is used by the SPAWN class. --- @param #DATABASE self --- @param #table SpawnTemplate --- @return #DATABASE self -function DATABASE:Spawn( SpawnTemplate ) - self:F2( SpawnTemplate.name ) - - self:T2( { SpawnTemplate.SpawnCountryID, SpawnTemplate.SpawnCategoryID } ) - - -- Copy the spawn variables of the template in temporary storage, nullify, and restore the spawn variables. - local SpawnCoalitionID = SpawnTemplate.SpawnCoalitionID - local SpawnCountryID = SpawnTemplate.SpawnCountryID - local SpawnCategoryID = SpawnTemplate.SpawnCategoryID - - -- Nullify - SpawnTemplate.SpawnCoalitionID = nil - SpawnTemplate.SpawnCountryID = nil - SpawnTemplate.SpawnCategoryID = nil - - self:_RegisterTemplate( SpawnTemplate, SpawnCoalitionID, SpawnCategoryID, SpawnCountryID ) - - self:T3( SpawnTemplate ) - coalition.addGroup( SpawnCountryID, SpawnCategoryID, SpawnTemplate ) - - -- Restore - SpawnTemplate.SpawnCoalitionID = SpawnCoalitionID - SpawnTemplate.SpawnCountryID = SpawnCountryID - SpawnTemplate.SpawnCategoryID = SpawnCategoryID - - local SpawnGroup = self:AddGroup( SpawnTemplate.name ) - return SpawnGroup -end - ---- Set a status to a Group within the Database, this to check crossing events for example. -function DATABASE:SetStatusGroup( GroupName, Status ) - self:F2( Status ) - - self.Templates.Groups[GroupName].Status = Status -end - ---- Get a status to a Group within the Database, this to check crossing events for example. -function DATABASE:GetStatusGroup( GroupName ) - self:F2( Status ) - - if self.Templates.Groups[GroupName] then - return self.Templates.Groups[GroupName].Status - else - return "" - end -end - ---- Private method that registers new Group Templates within the DATABASE Object. --- @param #DATABASE self --- @param #table GroupTemplate --- @return #DATABASE self -function DATABASE:_RegisterTemplate( GroupTemplate, CoalitionID, CategoryID, CountryID ) - - local GroupTemplateName = env.getValueDictByKey(GroupTemplate.name) - - local TraceTable = {} - - if not self.Templates.Groups[GroupTemplateName] then - self.Templates.Groups[GroupTemplateName] = {} - self.Templates.Groups[GroupTemplateName].Status = nil - end - - -- Delete the spans from the route, it is not needed and takes memory. - if GroupTemplate.route and GroupTemplate.route.spans then - GroupTemplate.route.spans = nil - end - - self.Templates.Groups[GroupTemplateName].GroupName = GroupTemplateName - self.Templates.Groups[GroupTemplateName].Template = GroupTemplate - self.Templates.Groups[GroupTemplateName].groupId = GroupTemplate.groupId - self.Templates.Groups[GroupTemplateName].UnitCount = #GroupTemplate.units - self.Templates.Groups[GroupTemplateName].Units = GroupTemplate.units - self.Templates.Groups[GroupTemplateName].CategoryID = CategoryID - self.Templates.Groups[GroupTemplateName].CoalitionID = CoalitionID - 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" - - for unit_num, UnitTemplate in pairs( GroupTemplate.units ) do - - local UnitTemplateName = env.getValueDictByKey(UnitTemplate.name) - self.Templates.Units[UnitTemplateName] = {} - self.Templates.Units[UnitTemplateName].UnitName = UnitTemplateName - self.Templates.Units[UnitTemplateName].Template = UnitTemplate - self.Templates.Units[UnitTemplateName].GroupName = GroupTemplateName - self.Templates.Units[UnitTemplateName].GroupTemplate = GroupTemplate - self.Templates.Units[UnitTemplateName].GroupId = GroupTemplate.groupId - self.Templates.Units[UnitTemplateName].CategoryID = CategoryID - self.Templates.Units[UnitTemplateName].CoalitionID = CoalitionID - self.Templates.Units[UnitTemplateName].CountryID = CountryID - - if UnitTemplate.skill and (UnitTemplate.skill == "Client" or UnitTemplate.skill == "Player") then - self.Templates.ClientsByName[UnitTemplateName] = UnitTemplate - self.Templates.ClientsByName[UnitTemplateName].CategoryID = CategoryID - self.Templates.ClientsByName[UnitTemplateName].CoalitionID = CoalitionID - self.Templates.ClientsByName[UnitTemplateName].CountryID = CountryID - self.Templates.ClientsByID[UnitTemplate.unitId] = UnitTemplate - end - - TraceTable[#TraceTable+1] = self.Templates.Units[UnitTemplateName].UnitName - end - - self:E( TraceTable ) -end - -function DATABASE:GetCoalitionFromClientTemplate( ClientName ) - return self.Templates.ClientsByName[ClientName].CoalitionID -end - -function DATABASE:GetCategoryFromClientTemplate( ClientName ) - return self.Templates.ClientsByName[ClientName].CategoryID -end - -function DATABASE:GetCountryFromClientTemplate( ClientName ) - return self.Templates.ClientsByName[ClientName].CountryID -end - ---- Airbase - -function DATABASE:GetCoalitionFromAirbase( AirbaseName ) - return self.AIRBASES[AirbaseName]:GetCoalition() -end - -function DATABASE:GetCategoryFromAirbase( AirbaseName ) - return self.AIRBASES[AirbaseName]:GetCategory() -end - - - ---- Private method that registers all alive players in the mission. --- @param #DATABASE self --- @return #DATABASE self -function DATABASE:_RegisterPlayers() - - local CoalitionsData = { AlivePlayersRed = coalition.getPlayers( coalition.side.RED ), AlivePlayersBlue = coalition.getPlayers( coalition.side.BLUE ) } - for CoalitionId, CoalitionData in pairs( CoalitionsData ) do - for UnitId, UnitData in pairs( CoalitionData ) do - self:T3( { "UnitData:", UnitData } ) - if UnitData and UnitData:isExist() then - local UnitName = UnitData:getName() - local PlayerName = UnitData:getPlayerName() - if not self.PLAYERS[PlayerName] then - self:E( { "Add player for unit:", UnitName, PlayerName } ) - self:AddPlayer( UnitName, PlayerName ) - end - end - end - end - - return self -end - - ---- Private method that registers all Groups and Units within in the mission. --- @param #DATABASE self --- @return #DATABASE self -function DATABASE:_RegisterGroupsAndUnits() - - local CoalitionsData = { GroupsRed = coalition.getGroups( coalition.side.RED ), GroupsBlue = coalition.getGroups( coalition.side.BLUE ) } - 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:AddGroup( DCSGroupName ) - - for DCSUnitId, DCSUnit in pairs( DCSGroup:getUnits() ) do - - local DCSUnitName = DCSUnit:getName() - self:E( { "Register Unit:", DCSUnitName } ) - self:AddUnit( DCSUnitName ) - end - else - self:E( { "Group does not exist: ", DCSGroup } ) - end - - end - end - - return self -end - ---- Private method that registers all Units of skill Client or Player within in the mission. --- @param #DATABASE self --- @return #DATABASE self -function DATABASE:_RegisterClients() - - for ClientName, ClientTemplate in pairs( self.Templates.ClientsByName ) do - self:E( { "Register Client:", ClientName } ) - self:AddClient( ClientName ) - end - - return self -end - ---- @param #DATABASE self -function DATABASE:_RegisterStatics() - - local CoalitionsData = { GroupsRed = coalition.getStaticObjects( coalition.side.RED ), GroupsBlue = coalition.getStaticObjects( coalition.side.BLUE ) } - 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:AddStatic( DCSStaticName ) - else - self:E( { "Static does not exist: ", DCSStatic } ) - end - end - end - - return self -end - ---- @param #DATABASE self -function DATABASE:_RegisterAirbases() - - local CoalitionsData = { AirbasesRed = coalition.getAirbases( coalition.side.RED ), AirbasesBlue = coalition.getAirbases( coalition.side.BLUE ), AirbasesNeutral = coalition.getAirbases( coalition.side.NEUTRAL ) } - for CoalitionId, CoalitionData in pairs( CoalitionsData ) do - for DCSAirbaseId, DCSAirbase in pairs( CoalitionData ) do - - local DCSAirbaseName = DCSAirbase:getName() - - self:E( { "Register Airbase:", DCSAirbaseName } ) - self:AddAirbase( DCSAirbaseName ) - end - end - - return self -end - - ---- Events - ---- Handles the OnBirth event for the alive units set. --- @param #DATABASE self --- @param Event#EVENTDATA Event -function DATABASE:_EventOnBirth( Event ) - self:F2( { Event } ) - - if Event.IniDCSUnit then - self:AddUnit( Event.IniDCSUnitName ) - self:AddGroup( Event.IniDCSGroupName ) - self:_EventOnPlayerEnterUnit( Event ) - end -end - - ---- Handles the OnDead or OnCrash event for alive units set. --- @param #DATABASE self --- @param Event#EVENTDATA Event -function DATABASE:_EventOnDeadOrCrash( Event ) - self:F2( { Event } ) - - if Event.IniDCSUnit then - if self.UNITS[Event.IniDCSUnitName] then - self:DeleteUnit( Event.IniDCSUnitName ) - -- add logic to correctly remove a group once all units are destroyed... - end - end -end - - ---- Handles the OnPlayerEnterUnit event to fill the active players table (with the unit filter applied). --- @param #DATABASE self --- @param Event#EVENTDATA Event -function DATABASE:_EventOnPlayerEnterUnit( Event ) - self:F2( { Event } ) - - if Event.IniUnit then - local PlayerName = Event.IniUnit:GetPlayerName() - if not self.PLAYERS[PlayerName] then - self:AddPlayer( Event.IniUnitName, PlayerName ) - end - end -end - - ---- Handles the OnPlayerLeaveUnit event to clean the active players table. --- @param #DATABASE self --- @param Event#EVENTDATA Event -function DATABASE:_EventOnPlayerLeaveUnit( Event ) - self:F2( { Event } ) - - if Event.IniUnit then - local PlayerName = Event.IniUnit:GetPlayerName() - if self.PLAYERS[PlayerName] then - self:DeletePlayer( PlayerName ) - end - end -end - ---- Iterators - ---- Iterate the DATABASE and call an iterator function for the given set, providing the Object for each element within the set and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called when there is an alive player in the database. --- @return #DATABASE self -function DATABASE:ForEach( IteratorFunction, FinalizeFunction, arg, Set ) - self:F2( arg ) - - local function CoRoutine() - local Count = 0 - for ObjectID, Object in pairs( Set ) do - self:T2( Object ) - IteratorFunction( Object, unpack( arg ) ) - Count = Count + 1 --- if Count % 100 == 0 then --- coroutine.yield( false ) --- end - end - return true - end - --- local co = coroutine.create( CoRoutine ) - local co = CoRoutine - - local function Schedule() - --- local status, res = coroutine.resume( co ) - local status, res = co() - self:T3( { status, res } ) - - if status == false then - error( res ) - end - if res == false then - return true -- resume next time the loop - end - if FinalizeFunction then - FinalizeFunction( unpack( arg ) ) - end - return false - end - - local Scheduler = SCHEDULER:New( self, Schedule, {}, 0.001, 0.001, 0 ) - - return self -end - - ---- Iterate the DATABASE and call an iterator function for each **alive** UNIT, providing the UNIT and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the database. The function needs to accept a UNIT parameter. --- @return #DATABASE self -function DATABASE:ForEachUnit( IteratorFunction, FinalizeFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, FinalizeFunction, arg, self.UNITS ) - - return self -end - ---- Iterate the DATABASE and call an iterator function for each **alive** GROUP, providing the GROUP and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the database. The function needs to accept a GROUP parameter. --- @return #DATABASE self -function DATABASE:ForEachGroup( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.GROUPS ) - - return self -end - - ---- Iterate the DATABASE and call an iterator function for each **ALIVE** player, providing the player name and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called when there is an player in the database. The function needs to accept the player name. --- @return #DATABASE self -function DATABASE:ForEachPlayer( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.PLAYERS ) - - return self -end - - ---- Iterate the DATABASE and call an iterator function for each player who has joined the mission, providing the Unit of the player and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called when there is was a player in the database. The function needs to accept a UNIT parameter. --- @return #DATABASE self -function DATABASE:ForEachPlayerJoined( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.PLAYERSJOINED ) - - return self -end - ---- Iterate the DATABASE and call an iterator function for each CLIENT, providing the CLIENT to the function and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called when there is an alive player in the database. The function needs to accept a CLIENT parameter. --- @return #DATABASE self -function DATABASE:ForEachClient( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.CLIENTS ) - - return self -end - - -function DATABASE:_RegisterTemplates() - self:F2() - - self.Navpoints = {} - self.UNITS = {} - --Build routines.db.units and self.Navpoints - for CoalitionName, coa_data in pairs(env.mission.coalition) do - - if (CoalitionName == 'red' or CoalitionName == 'blue') and type(coa_data) == 'table' then - --self.Units[coa_name] = {} - - ---------------------------------------------- - -- build nav points DB - self.Navpoints[CoalitionName] = {} - if coa_data.nav_points then --navpoints - for nav_ind, nav_data in pairs(coa_data.nav_points) do - - if type(nav_data) == 'table' then - self.Navpoints[CoalitionName][nav_ind] = routines.utils.deepCopy(nav_data) - - self.Navpoints[CoalitionName][nav_ind]['name'] = nav_data.callsignStr -- name is a little bit more self-explanatory. - self.Navpoints[CoalitionName][nav_ind]['point'] = {} -- point is used by SSE, support it. - self.Navpoints[CoalitionName][nav_ind]['point']['x'] = nav_data.x - self.Navpoints[CoalitionName][nav_ind]['point']['y'] = 0 - self.Navpoints[CoalitionName][nav_ind]['point']['z'] = nav_data.y - end - end - end - ------------------------------------------------- - if coa_data.country then --there is a country table - for cntry_id, cntry_data in pairs(coa_data.country) do - - local CountryName = string.upper(cntry_data.name) - --self.Units[coa_name][countryName] = {} - --self.Units[coa_name][countryName]["countryId"] = cntry_data.id - - if type(cntry_data) == 'table' then --just making sure - - for obj_type_name, obj_type_data in pairs(cntry_data) do - - if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then --should be an unncessary check - - local CategoryName = obj_type_name - - if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! - - --self.Units[coa_name][countryName][category] = {} - - for group_num, GroupTemplate in pairs(obj_type_data.group) do - - if GroupTemplate and GroupTemplate.units and type(GroupTemplate.units) == 'table' then --making sure again- this is a valid group - self:_RegisterTemplate( - GroupTemplate, - coalition.side[string.upper(CoalitionName)], - _DATABASECategory[string.lower(CategoryName)], - country.id[string.upper(CountryName)] - ) - end --if GroupTemplate and GroupTemplate.units then - end --for group_num, GroupTemplate in pairs(obj_type_data.group) do - end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then - end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then - end --for obj_type_name, obj_type_data in pairs(cntry_data) do - end --if type(cntry_data) == 'table' then - end --for cntry_id, cntry_data in pairs(coa_data.country) do - end --if coa_data.country then --there is a country table - end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then - end --for coa_name, coa_data in pairs(mission.coalition) do - - return self -end - - - - ---- This module contains the SET classes. --- --- === --- --- 1) @{Set#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). --- --- === --- --- 2) @{Set#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. --- --- 2.1) SET_GROUP construction method: --- ----------------------------------- --- Create a new SET_GROUP object with the @{#SET_GROUP.New} method: --- --- * @{#SET_GROUP.New}: Creates a new SET_GROUP object. --- --- 2.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. --- --- 2.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). --- --- 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}. --- --- 2.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. --- --- ==== --- --- 3) @{Set#SET_UNIT} class, extends @{Set#SET_BASE} --- =================================================== --- Mission designers can use the @{Set#SET_UNIT} class to build sets of units belonging to certain: --- --- * Coalitions --- * Categories --- * Countries --- * Unit types --- * Starting with certain prefix strings. --- --- 3.1) SET_UNIT construction method: --- ---------------------------------- --- 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: --- ----------------------------------------- --- UNITs can be added and removed using the @{Set#SET_UNIT.AddUnitsByName} and @{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: --- ------------------------------ --- You can set filter criteria to define the set of units within the SET_UNIT. --- Filter criteria are defined by: --- --- * @{#SET_UNIT.FilterCoalitions}: Builds the SET_UNIT with the units belonging to the coalition(s). --- * @{#SET_UNIT.FilterCategories}: Builds the SET_UNIT with the units belonging to the category(ies). --- * @{#SET_UNIT.FilterTypes}: Builds the SET_UNIT with the units belonging to the unit type(s). --- * @{#SET_UNIT.FilterCountries}: Builds the SET_UNIT with the units belonging to the country(ies). --- * @{#SET_UNIT.FilterPrefixes}: Builds the SET_UNIT with the units starting with the same prefix string(s). --- --- 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. --- --- 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}. --- --- 3.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. --- --- Planned iterators methods in development are (so these are not yet available): --- --- * @{#SET_UNIT.ForEachUnitInUnit}: Calls a function for each unit contained within the SET_UNIT. --- * @{#SET_UNIT.ForEachUnitCompletelyInZone}: Iterate and call an iterator function for each **alive** UNIT presence completely in a @{Zone}, providing the UNIT and optional parameters to the called function. --- * @{#SET_UNIT.ForEachUnitNotInZone}: Iterate and call an iterator function for each **alive** UNIT presence not in a @{Zone}, providing the UNIT and optional parameters to the called function. --- --- === --- --- 4) @{Set#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 construction method: --- ---------------------------------- --- 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. --- --- ==== --- --- 5) @{Set#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 construction --- ----------------------------- --- 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. --- --- ==== --- --- @module Set --- @author FlightControl - - ---- SET_BASE class --- @type SET_BASE --- @extends Base#BASE -SET_BASE = { - ClassName = "SET_BASE", - Set = {}, -} - ---- 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() ) - - self.Database = Database - - self.YieldInterval = 10 - self.TimeInterval = 0.001 - - return self -end - ---- Finds an @{Base#BASE} object based on the object Name. --- @param #SET_BASE self --- @param #string ObjectName --- @return 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 - ---- Adds a @{Base#BASE} object in the @{Set#SET_BASE}, using the Object Name as the index. --- @param #SET_BASE self --- @param #string ObjectName --- @param Base#BASE Object --- @return Base#BASE The added BASE Object. -function SET_BASE:Add( ObjectName, Object ) - - self.Set[ObjectName] = Object -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 ) - - self.Set[ObjectName] = nil -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 - - - ---- 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 - - _EVENTDISPATCHER:OnBirth( self._EventOnBirth, self ) - _EVENTDISPATCHER:OnDead( self._EventOnDeadOrCrash, self ) - _EVENTDISPATCHER:OnCrash( self._EventOnDeadOrCrash, self ) - - -- Follow alive players and clients --- _EVENTDISPATCHER:OnPlayerEnterUnit( self._EventOnPlayerEnterUnit, self ) --- _EVENTDISPATCHER:OnPlayerLeaveUnit( self._EventOnPlayerLeaveUnit, self ) - - - return self -end - ---- Iterate the SET_BASE while identifying the nearest object from a @{Point#POINT_VEC2}. --- @param #SET_BASE self --- @param Point#POINT_VEC2 PointVec2 A @{Point#POINT_VEC2} object from where to evaluate the closest object in the set. --- @return 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:GetPointVec2() ) - else - local Distance = PointVec2:DistanceFromVec2( ObjectData:GetPointVec2() ) - 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 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 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 Event#EVENTDATA Event -function SET_BASE:_EventOnDeadOrCrash( Event ) - self:F2( { Event } ) - - if Event.IniDCSUnit then - local ObjectName, Object = self:FindInDatabase( Event ) - if ObjectName and Object 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 Event#EVENTDATA Event ---function SET_BASE:_EventOnPlayerEnterUnit( Event ) --- self:F3( { Event } ) --- --- if Event.IniDCSUnit then --- if self:IsIncludeObject( Event.IniDCSUnit ) then --- if not self.PlayersAlive[Event.IniDCSUnitName] then --- self:E( { "Add player for unit:", Event.IniDCSUnitName, Event.IniDCSUnit:getPlayerName() } ) --- self.PlayersAlive[Event.IniDCSUnitName] = Event.IniDCSUnit:getPlayerName() --- self.ClientsAlive[Event.IniDCSUnitName] = _DATABASE.Clients[ Event.IniDCSUnitName ] --- end --- end --- end ---end --- ------ Handles the OnPlayerLeaveUnit event to clean the active players table. ----- @param #SET_BASE self ----- @param Event#EVENTDATA Event ---function SET_BASE:_EventOnPlayerLeaveUnit( Event ) --- self:F3( { Event } ) --- --- if Event.IniDCSUnit then --- if self:IsIncludeObject( Event.IniDCSUnit ) then --- if self.PlayersAlive[Event.IniDCSUnitName] then --- self:E( { "Cleaning player for unit:", Event.IniDCSUnitName, Event.IniDCSUnit:getPlayerName() } ) --- self.PlayersAlive[Event.IniDCSUnitName] = nil --- self.ClientsAlive[Event.IniDCSUnitName] = nil --- 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 ) - - local function CoRoutine() - local Count = 0 - for ObjectID, Object in pairs( Set ) do - self:T2( 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 - 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 - - local Scheduler = SCHEDULER:New( self, Schedule, {}, self.TimeInterval, self.TimeInterval, 0 ) - - 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 - ---- Flushes the current SET_BASE contents in the log ... (for debug reasons). --- @param #SET_BASE self --- @return #string A string with the names of the objects. -function SET_BASE:Flush() - self:F3() - - local ObjectNames = "" - for ObjectName, Object in pairs( self.Set ) do - ObjectNames = ObjectNames .. ObjectName .. ", " - end - self:T( { "Objects in Set:", ObjectNames } ) - - return ObjectNames -end - --- SET_GROUP - ---- SET_GROUP class --- @type SET_GROUP --- @extends Set#SET_BASE -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_UNIT, - 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 - ---- Add GROUP(s) to SET_GROUP. --- @param 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 Set#SET_GROUP self --- @param 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 ) - end - - return self -end - - - - ---- Finds a Group based on the Group Name. --- @param #SET_GROUP self --- @param #string GroupName --- @return Group#GROUP The found Group. -function SET_GROUP:FindGroup( GroupName ) - - local GroupFound = self.Set[GroupName] - return GroupFound -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 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 - - 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_GROUP self --- @param Event#EVENTDATA Event --- @return #string The name of the GROUP --- @return #table The GROUP -function SET_GROUP:AddInDatabase( Event ) - self:F3( { Event } ) - - if not self.Database[Event.IniDCSGroupName] then - self.Database[Event.IniDCSGroupName] = GROUP:Register( Event.IniDCSGroupName ) - self:T3( self.Database[Event.IniDCSGroupName] ) - 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 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 **alive** GROUP, 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.Set ) - - 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 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.Set, - --- @param Zone#ZONE_BASE ZoneObject - -- @param 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 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.Set, - --- @param Zone#ZONE_BASE ZoneObject - -- @param 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 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.Set, - --- @param Zone#ZONE_BASE ZoneObject - -- @param 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 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 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 - end - end - MooseGroupInclude = MooseGroupInclude and MooseGroupCoalition - 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 - end - end - MooseGroupInclude = MooseGroupInclude and MooseGroupCategory - 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 - 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, 1 ) then - MooseGroupPrefix = true - end - end - MooseGroupInclude = MooseGroupInclude and MooseGroupPrefix - end - - self:T2( MooseGroupInclude ) - return MooseGroupInclude -end - ---- SET_UNIT class --- @type SET_UNIT --- @extends Set#SET_BASE -SET_UNIT = { - ClassName = "SET_UNIT", - Units = {}, - Filter = { - Coalitions = nil, - Categories = nil, - Types = nil, - Countries = nil, - UnitPrefixes = nil, - }, - FilterMeta = { - Coalitions = { - red = coalition.side.RED, - blue = coalition.side.BLUE, - neutral = coalition.side.NEUTRAL, - }, - Categories = { - plane = Unit.Category.AIRPLANE, - helicopter = Unit.Category.HELICOPTER, - ground = Unit.Category.GROUND_UNIT, - ship = Unit.Category.SHIP, - structure = Unit.Category.STRUCTURE, - }, - }, -} - - ---- Creates a new SET_UNIT object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. --- @param #SET_UNIT self --- @return #SET_UNIT --- @usage --- -- Define a new SET_UNIT Object. This DBObject will contain a reference to all alive Units. --- DBObject = SET_UNIT:New() -function SET_UNIT:New() - - -- Inherits from BASE - local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.UNITS ) ) - - _EVENTDISPATCHER:OnBirth( self._EventOnBirth, self ) - _EVENTDISPATCHER:OnDead( self._EventOnDeadOrCrash, self ) - _EVENTDISPATCHER:OnCrash( self._EventOnDeadOrCrash, self ) - - return self -end - ---- Add UNIT(s) to SET_UNIT. --- @param #SET_UNIT self --- @param #string AddUnit A single UNIT. --- @return #SET_UNIT self -function SET_UNIT:AddUnit( AddUnit ) - self:F2( AddUnit:GetName() ) - - self:Add( AddUnit:GetName(), AddUnit ) - - return self -end - - ---- Add UNIT(s) to SET_UNIT. --- @param #SET_UNIT self --- @param #string AddUnitNames A single name or an array of UNIT names. --- @return #SET_UNIT self -function SET_UNIT:AddUnitsByName( AddUnitNames ) - - local AddUnitNamesArray = ( type( AddUnitNames ) == "table" ) and AddUnitNames or { AddUnitNames } - - self:T( AddUnitNamesArray ) - for AddUnitID, AddUnitName in pairs( AddUnitNamesArray ) do - self:Add( AddUnitName, UNIT:FindByName( AddUnitName ) ) - end - - return self -end - ---- Remove UNIT(s) from SET_UNIT. --- @param Set#SET_UNIT self --- @param Unit#UNIT RemoveUnitNames A single name or an array of UNIT names. --- @return self -function SET_UNIT:RemoveUnitsByName( RemoveUnitNames ) - - local RemoveUnitNamesArray = ( type( RemoveUnitNames ) == "table" ) and RemoveUnitNames or { RemoveUnitNames } - - for RemoveUnitID, RemoveUnitName in pairs( RemoveUnitNamesArray ) do - self:Remove( RemoveUnitName.UnitName ) - end - - return self -end - - ---- Finds a Unit based on the Unit Name. --- @param #SET_UNIT self --- @param #string UnitName --- @return Unit#UNIT The found Unit. -function SET_UNIT:FindUnit( UnitName ) - - local UnitFound = self.Set[UnitName] - return UnitFound -end - - - ---- Builds a set of units of coalitions. --- Possible current coalitions are red, blue and neutral. --- @param #SET_UNIT self --- @param #string Coalitions Can take the following values: "red", "blue", "neutral". --- @return #SET_UNIT self -function SET_UNIT:FilterCoalitions( Coalitions ) - if not self.Filter.Coalitions then - self.Filter.Coalitions = {} - end - if type( Coalitions ) ~= "table" then - Coalitions = { Coalitions } - end - for CoalitionID, Coalition in pairs( Coalitions ) do - self.Filter.Coalitions[Coalition] = Coalition - end - return self -end - - ---- Builds a set of units out of categories. --- Possible current categories are plane, helicopter, ground, ship. --- @param #SET_UNIT self --- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship". --- @return #SET_UNIT self -function SET_UNIT:FilterCategories( Categories ) - if not self.Filter.Categories then - self.Filter.Categories = {} - end - if type( Categories ) ~= "table" then - Categories = { Categories } - end - for CategoryID, Category in pairs( Categories ) do - self.Filter.Categories[Category] = Category - end - return self -end - - ---- Builds a set of units of defined unit types. --- Possible current types are those types known within DCS world. --- @param #SET_UNIT self --- @param #string Types Can take those type strings known within DCS world. --- @return #SET_UNIT self -function SET_UNIT:FilterTypes( Types ) - if not self.Filter.Types then - self.Filter.Types = {} - end - if type( Types ) ~= "table" then - Types = { Types } - end - for TypeID, Type in pairs( Types ) do - self.Filter.Types[Type] = Type - end - return self -end - - ---- Builds a set of units of defined countries. --- Possible current countries are those known within DCS world. --- @param #SET_UNIT self --- @param #string Countries Can take those country strings known within DCS world. --- @return #SET_UNIT self -function SET_UNIT:FilterCountries( Countries ) - if not self.Filter.Countries then - self.Filter.Countries = {} - end - if type( Countries ) ~= "table" then - Countries = { Countries } - end - for CountryID, Country in pairs( Countries ) do - self.Filter.Countries[Country] = Country - end - return self -end - - ---- Builds a set of units of defined unit prefixes. --- All the units starting with the given prefixes will be included within the set. --- @param #SET_UNIT self --- @param #string Prefixes The prefix of which the unit name starts with. --- @return #SET_UNIT self -function SET_UNIT:FilterPrefixes( Prefixes ) - if not self.Filter.UnitPrefixes then - self.Filter.UnitPrefixes = {} - end - if type( Prefixes ) ~= "table" then - Prefixes = { Prefixes } - end - for PrefixID, Prefix in pairs( Prefixes ) do - self.Filter.UnitPrefixes[Prefix] = Prefix - end - return self -end - - - - ---- Starts the filtering. --- @param #SET_UNIT self --- @return #SET_UNIT self -function SET_UNIT:FilterStart() - - if _DATABASE then - self:_FilterStart() - end - - return self -end - ---- Handles the Database to check on an event (birth) that the Object was added in the Database. --- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! --- @param #SET_UNIT self --- @param Event#EVENTDATA Event --- @return #string The name of the UNIT --- @return #table The UNIT -function SET_UNIT:AddInDatabase( Event ) - self:F3( { Event } ) - - if not self.Database[Event.IniDCSUnitName] then - self.Database[Event.IniDCSUnitName] = UNIT:Register( Event.IniDCSUnitName ) - self:T3( self.Database[Event.IniDCSUnitName] ) - end - - return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] -end - ---- Handles the Database to check on any event that Object exists in the Database. --- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! --- @param #SET_UNIT self --- @param Event#EVENTDATA Event --- @return #string The name of the UNIT --- @return #table The UNIT -function SET_UNIT:FindInDatabase( Event ) - self:F3( { Event } ) - - self:E( { Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] } ) - return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] -end - ---- Iterate the SET_UNIT and call an interator function for each **alive** UNIT, providing the UNIT and optional parameters. --- @param #SET_UNIT self --- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the SET_UNIT. The function needs to accept a UNIT parameter. --- @return #SET_UNIT self -function SET_UNIT:ForEachUnit( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set ) - - return self -end - ---- Iterate the SET_UNIT and call an iterator function for each **alive** UNIT presence completely in a @{Zone}, providing the UNIT and optional parameters to the called function. --- @param #SET_UNIT self --- @param Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the SET_UNIT. The function needs to accept a UNIT parameter. --- @return #SET_UNIT self -function SET_UNIT:ForEachUnitCompletelyInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set, - --- @param Zone#ZONE_BASE ZoneObject - -- @param Unit#UNIT UnitObject - function( ZoneObject, UnitObject ) - if UnitObject:IsCompletelyInZone( ZoneObject ) then - return true - else - return false - end - end, { ZoneObject } ) - - return self -end - ---- Iterate the SET_UNIT and call an iterator function for each **alive** UNIT presence not in a @{Zone}, providing the UNIT and optional parameters to the called function. --- @param #SET_UNIT self --- @param Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the SET_UNIT. The function needs to accept a UNIT parameter. --- @return #SET_UNIT self -function SET_UNIT:ForEachUnitNotInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set, - --- @param Zone#ZONE_BASE ZoneObject - -- @param Unit#UNIT UnitObject - function( ZoneObject, UnitObject ) - if UnitObject:IsNotInZone( ZoneObject ) then - return true - else - return false - end - end, { ZoneObject } ) - - return self -end - - - ------ Iterate the SET_UNIT and call an interator function for each **alive** player, providing the Unit of the player and optional parameters. ----- @param #SET_UNIT self ----- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_UNIT. The function needs to accept a UNIT parameter. ----- @return #SET_UNIT self ---function SET_UNIT:ForEachPlayer( IteratorFunction, ... ) --- self:F2( arg ) --- --- self:ForEach( IteratorFunction, arg, self.PlayersAlive ) --- --- return self ---end --- --- ------ Iterate the SET_UNIT and call an interator function for each client, providing the Client to the function and optional parameters. ----- @param #SET_UNIT self ----- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_UNIT. The function needs to accept a CLIENT parameter. ----- @return #SET_UNIT self ---function SET_UNIT:ForEachClient( IteratorFunction, ... ) --- self:F2( arg ) --- --- self:ForEach( IteratorFunction, arg, self.Clients ) --- --- return self ---end - - ---- --- @param #SET_UNIT self --- @param Unit#UNIT MUnit --- @return #SET_UNIT self -function SET_UNIT:IsIncludeObject( MUnit ) - self:F2( MUnit ) - local MUnitInclude = true - - if self.Filter.Coalitions then - local MUnitCoalition = false - for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do - self:T3( { "Coalition:", MUnit:GetCoalition(), self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) - if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == MUnit:GetCoalition() then - MUnitCoalition = true - end - end - MUnitInclude = MUnitInclude and MUnitCoalition - end - - if self.Filter.Categories then - local MUnitCategory = false - for CategoryID, CategoryName in pairs( self.Filter.Categories ) do - self:T3( { "Category:", MUnit:GetDesc().category, self.FilterMeta.Categories[CategoryName], CategoryName } ) - if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == MUnit:GetDesc().category then - MUnitCategory = true - end - end - MUnitInclude = MUnitInclude and MUnitCategory - end - - if self.Filter.Types then - local MUnitType = false - for TypeID, TypeName in pairs( self.Filter.Types ) do - self:T3( { "Type:", MUnit:GetTypeName(), TypeName } ) - if TypeName == MUnit:GetTypeName() then - MUnitType = true - end - end - MUnitInclude = MUnitInclude and MUnitType - end - - if self.Filter.Countries then - local MUnitCountry = false - for CountryID, CountryName in pairs( self.Filter.Countries ) do - self:T3( { "Country:", MUnit:GetCountry(), CountryName } ) - if country.id[CountryName] == MUnit:GetCountry() then - MUnitCountry = true - end - end - MUnitInclude = MUnitInclude and MUnitCountry - end - - if self.Filter.UnitPrefixes then - local MUnitPrefix = false - for UnitPrefixId, UnitPrefix in pairs( self.Filter.UnitPrefixes ) do - self:T3( { "Prefix:", string.find( MUnit:GetName(), UnitPrefix, 1 ), UnitPrefix } ) - if string.find( MUnit:GetName(), UnitPrefix, 1 ) then - MUnitPrefix = true - end - end - MUnitInclude = MUnitInclude and MUnitPrefix - end - - self:T2( MUnitInclude ) - return MUnitInclude -end - - ---- SET_CLIENT - ---- SET_CLIENT class --- @type SET_CLIENT --- @extends Set#SET_BASE -SET_CLIENT = { - ClassName = "SET_CLIENT", - Clients = {}, - Filter = { - Coalitions = nil, - Categories = nil, - Types = nil, - Countries = nil, - ClientPrefixes = nil, - }, - FilterMeta = { - Coalitions = { - red = coalition.side.RED, - blue = coalition.side.BLUE, - neutral = coalition.side.NEUTRAL, - }, - Categories = { - plane = Unit.Category.AIRPLANE, - helicopter = Unit.Category.HELICOPTER, - ground = Unit.Category.GROUND_UNIT, - ship = Unit.Category.SHIP, - structure = Unit.Category.STRUCTURE, - }, - }, -} - - ---- Creates a new SET_CLIENT object, building a set of clients belonging to a coalitions, categories, countries, types or with defined prefix names. --- @param #SET_CLIENT self --- @return #SET_CLIENT --- @usage --- -- Define a new SET_CLIENT Object. This DBObject will contain a reference to all Clients. --- DBObject = SET_CLIENT:New() -function SET_CLIENT:New() - -- Inherits from BASE - local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.CLIENTS ) ) - - return self -end - ---- Add CLIENT(s) to SET_CLIENT. --- @param Set#SET_CLIENT self --- @param #string AddClientNames A single name or an array of CLIENT names. --- @return self -function SET_CLIENT:AddClientsByName( AddClientNames ) - - local AddClientNamesArray = ( type( AddClientNames ) == "table" ) and AddClientNames or { AddClientNames } - - for AddClientID, AddClientName in pairs( AddClientNamesArray ) do - self:Add( AddClientName, CLIENT:FindByName( AddClientName ) ) - end - - return self -end - ---- Remove CLIENT(s) from SET_CLIENT. --- @param Set#SET_CLIENT self --- @param Client#CLIENT RemoveClientNames A single name or an array of CLIENT names. --- @return self -function SET_CLIENT:RemoveClientsByName( RemoveClientNames ) - - local RemoveClientNamesArray = ( type( RemoveClientNames ) == "table" ) and RemoveClientNames or { RemoveClientNames } - - for RemoveClientID, RemoveClientName in pairs( RemoveClientNamesArray ) do - self:Remove( RemoveClientName.ClientName ) - end - - return self -end - - ---- Finds a Client based on the Client Name. --- @param #SET_CLIENT self --- @param #string ClientName --- @return 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() - 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 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 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.Set ) - - 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 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.Set, - --- @param Zone#ZONE_BASE ZoneObject - -- @param 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 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.Set, - --- @param Zone#ZONE_BASE ZoneObject - -- @param 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 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 -end - ---- SET_AIRBASE - ---- SET_AIRBASE class --- @type SET_AIRBASE --- @extends Set#SET_BASE -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 AIRBASEs to SET_AIRBASE. --- @param 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 Set#SET_AIRBASE self --- @param 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 ) - end - - return self -end - - ---- Finds a Airbase based on the Airbase Name. --- @param #SET_AIRBASE self --- @param #string AirbaseName --- @return 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() - 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 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 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.Set ) - - return self -end - ---- Iterate the SET_AIRBASE while identifying the nearest @{Airbase#AIRBASE} from a @{Point#POINT_VEC2}. --- @param #SET_AIRBASE self --- @param Point#POINT_VEC2 PointVec2 A @{Point#POINT_VEC2} object from where to evaluate the closest @{Airbase#AIRBASE}. --- @return Airbase#AIRBASE The closest @{Airbase#AIRBASE}. -function SET_AIRBASE:FindNearestAirbaseFromPointVec2( PointVec2 ) - self:F2( PointVec2 ) - - local NearestAirbase = self:FindNearestObjectFromPointVec2( PointVec2 ) - return NearestAirbase -end - - - ---- --- @param #SET_AIRBASE self --- @param 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 ---- This module contains the POINT classes. --- --- 1) @{Point#POINT_VEC3} class, extends @{Base#BASE} --- =============================================== --- The @{Point#POINT_VEC3} class defines a 3D point in the simulator. --- --- 1.1) POINT_VEC3 constructor --- --------------------------- --- --- A new POINT instance can be created with: --- --- * @{#POINT_VEC3.New}(): a 3D point. --- --- 2) @{Point#POINT_VEC2} class, extends @{Point#POINT_VEC3} --- ========================================================= --- 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. --- --- 2.1) POINT_VEC2 constructor --- --------------------------- --- --- A new POINT instance can be created with: --- --- * @{#POINT_VEC2.New}(): a 2D point. --- --- @module Point --- @author FlightControl - ---- The POINT_VEC3 class --- @type POINT_VEC3 --- @extends Base#BASE --- @field #POINT_VEC3.SmokeColor SmokeColor --- @field #POINT_VEC3.FlareColor FlareColor --- @field #POINT_VEC3.RoutePointAltType RoutePointAltType --- @field #POINT_VEC3.RoutePointType RoutePointType --- @field #POINT_VEC3.RoutePointAction RoutePointAction -POINT_VEC3 = { - ClassName = "POINT_VEC3", - SmokeColor = { - Green = trigger.smokeColor.Green, - Red = trigger.smokeColor.Red, - White = trigger.smokeColor.White, - Orange = trigger.smokeColor.Orange, - Blue = trigger.smokeColor.Blue - }, - FlareColor = { - Green = trigger.flareColor.Green, - Red = trigger.flareColor.Red, - White = trigger.flareColor.White, - Yellow = trigger.flareColor.Yellow - }, - RoutePointAltType = { - BARO = "BARO", - }, - RoutePointType = { - TurningPoint = "Turning Point", - }, - RoutePointAction = { - TurningPoint = "Turning Point", - }, -} - - ---- SmokeColor --- @type POINT_VEC3.SmokeColor --- @field Green --- @field Red --- @field White --- @field Orange --- @field Blue - - - ---- FlareColor --- @type POINT_VEC3.FlareColor --- @field Green --- @field Red --- @field White --- @field Yellow - - - ---- RoutePoint AltTypes --- @type POINT_VEC3.RoutePointAltType --- @field BARO "BARO" - - - ---- RoutePoint Types --- @type POINT_VEC3.RoutePointType --- @field TurningPoint "Turning Point" - - - ---- RoutePoint Actions --- @type POINT_VEC3.RoutePointAction --- @field TurningPoint "Turning Point" - - - --- Constructor. - ---- Create a new POINT_VEC3 object. --- @param #POINT_VEC3 self --- @param DCSTypes#Distance x The x coordinate of the Vec3 point, pointing to the North. --- @param DCSTypes#Distance y The y coordinate of the Vec3 point, pointing Upwards. --- @param DCSTypes#Distance z The z coordinate of the Vec3 point, pointing to the Right. --- @return Point#POINT_VEC3 self -function POINT_VEC3:New( x, y, z ) - - local self = BASE:Inherit( self, BASE:New() ) - self.PointVec3 = { x = x, y = y, z = z } - self:F2( self.PointVec3 ) - return self -end - - ---- Build an air type route point. --- @param #POINT_VEC3 self --- @param #POINT_VEC3.RoutePointAltType AltType The altitude type. --- @param #POINT_VEC3.RoutePointType Type The route point type. --- @param #POINT_VEC3.RoutePointAction Action The route point action. --- @param DCSTypes#Speed Speed Airspeed in km/h. --- @param #boolean SpeedLocked true means the speed is locked. --- @return #table The route point. -function POINT_VEC3:RoutePointAir( AltType, Type, Action, Speed, SpeedLocked ) - self:F2( { AltType, Type, Action, Speed, SpeedLocked } ) - - local RoutePoint = {} - RoutePoint.x = self.PointVec3.x - RoutePoint.y = self.PointVec3.z - RoutePoint.alt = self.PointVec3.y - RoutePoint.alt_type = AltType - - RoutePoint.type = Type - RoutePoint.action = Action - - RoutePoint.speed = Speed / 3.6 - RoutePoint.speed_locked = true - --- ["task"] = --- { --- ["id"] = "ComboTask", --- ["params"] = --- { --- ["tasks"] = --- { --- }, -- end of ["tasks"] --- }, -- end of ["params"] --- }, -- end of ["task"] - - - RoutePoint.task = {} - RoutePoint.task.id = "ComboTask" - RoutePoint.task.params = {} - RoutePoint.task.params.tasks = {} - - - return RoutePoint -end - - ---- Smokes the point in a color. --- @param #POINT_VEC3 self --- @param Point#POINT_VEC3.SmokeColor SmokeColor -function POINT_VEC3:Smoke( SmokeColor ) - self:F2( { SmokeColor, self.PointVec3 } ) - trigger.action.smoke( self.PointVec3, SmokeColor ) -end - ---- Smoke the POINT_VEC3 Green. --- @param #POINT_VEC3 self -function POINT_VEC3:SmokeGreen() - self:F2() - self:Smoke( POINT_VEC3.SmokeColor.Green ) -end - ---- Smoke the POINT_VEC3 Red. --- @param #POINT_VEC3 self -function POINT_VEC3:SmokeRed() - self:F2() - self:Smoke( POINT_VEC3.SmokeColor.Red ) -end - ---- Smoke the POINT_VEC3 White. --- @param #POINT_VEC3 self -function POINT_VEC3:SmokeWhite() - self:F2() - self:Smoke( POINT_VEC3.SmokeColor.White ) -end - ---- Smoke the POINT_VEC3 Orange. --- @param #POINT_VEC3 self -function POINT_VEC3:SmokeOrange() - self:F2() - self:Smoke( POINT_VEC3.SmokeColor.Orange ) -end - ---- Smoke the POINT_VEC3 Blue. --- @param #POINT_VEC3 self -function POINT_VEC3:SmokeBlue() - self:F2() - self:Smoke( POINT_VEC3.SmokeColor.Blue ) -end - ---- Flares the point in a color. --- @param #POINT_VEC3 self --- @param Point#POINT_VEC3.FlareColor --- @param DCSTypes#Azimuth (optional) Azimuth The azimuth of the flare direction. The default azimuth is 0. -function POINT_VEC3:Flare( FlareColor, Azimuth ) - self:F2( { FlareColor, self.PointVec3 } ) - trigger.action.signalFlare( self.PointVec3, FlareColor, Azimuth and Azimuth or 0 ) -end - ---- Flare the POINT_VEC3 White. --- @param #POINT_VEC3 self --- @param DCSTypes#Azimuth (optional) Azimuth The azimuth of the flare direction. The default azimuth is 0. -function POINT_VEC3:FlareWhite( Azimuth ) - self:F2( Azimuth ) - self:Flare( POINT_VEC3.FlareColor.White, Azimuth ) -end - ---- Flare the POINT_VEC3 Yellow. --- @param #POINT_VEC3 self --- @param DCSTypes#Azimuth (optional) Azimuth The azimuth of the flare direction. The default azimuth is 0. -function POINT_VEC3:FlareYellow( Azimuth ) - self:F2( Azimuth ) - self:Flare( POINT_VEC3.FlareColor.Yellow, Azimuth ) -end - ---- Flare the POINT_VEC3 Green. --- @param #POINT_VEC3 self --- @param DCSTypes#Azimuth (optional) Azimuth The azimuth of the flare direction. The default azimuth is 0. -function POINT_VEC3:FlareGreen( Azimuth ) - self:F2( Azimuth ) - self:Flare( POINT_VEC3.FlareColor.Green, Azimuth ) -end - ---- Flare the POINT_VEC3 Red. --- @param #POINT_VEC3 self -function POINT_VEC3:FlareRed( Azimuth ) - self:F2( Azimuth ) - self:Flare( POINT_VEC3.FlareColor.Red, Azimuth ) -end - - ---- The POINT_VEC2 class --- @type POINT_VEC2 --- @field DCSTypes#Vec2 PointVec2 --- @extends Point#POINT_VEC3 -POINT_VEC2 = { - ClassName = "POINT_VEC2", - } - ---- Create a new POINT_VEC2 object. --- @param #POINT_VEC2 self --- @param DCSTypes#Distance x The x coordinate of the Vec3 point, pointing to the North. --- @param DCSTypes#Distance y The y coordinate of the Vec3 point, pointing to the Right. --- @param 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. --- @return Point#POINT_VEC2 -function POINT_VEC2:New( x, y, LandHeightAdd ) - - local LandHeight = land.getHeight( { ["x"] = x, ["y"] = y } ) - if LandHeightAdd then - LandHeight = LandHeight + LandHeightAdd - end - - local self = BASE:Inherit( self, POINT_VEC3:New( x, LandHeight, y ) ) - self:F2( { x, y, LandHeightAdd } ) - - self.PointVec2 = { x = x, y = y } - - return self -end - ---- Calculate the distance from a reference @{Point#POINT_VEC2}. --- @param #POINT_VEC2 self --- @param #POINT_VEC2 PointVec2Reference The reference @{Point#POINT_VEC2}. --- @return DCSTypes#Distance The distance from the reference @{Point#POINT_VEC2} in meters. -function POINT_VEC2:DistanceFromPointVec2( PointVec2Reference ) - self:F2( PointVec2Reference ) - - local Distance = ( ( PointVec2Reference.PointVec2.x - self.PointVec2.x ) ^ 2 + ( PointVec2Reference.PointVec2.y - self.PointVec2.y ) ^2 ) ^0.5 - - self:T2( Distance ) - return Distance -end - ---- Calculate the distance from a reference @{DCSTypes#Vec2}. --- @param #POINT_VEC2 self --- @param DCSTypes#Vec2 Vec2Reference The reference @{DCSTypes#Vec2}. --- @return DCSTypes#Distance The distance from the reference @{DCSTypes#Vec2} in meters. -function POINT_VEC2:DistanceFromVec2( Vec2Reference ) - self:F2( Vec2Reference ) - - local Distance = ( ( Vec2Reference.x - self.PointVec2.x ) ^ 2 + ( Vec2Reference.y - self.PointVec2.y ) ^2 ) ^0.5 - - self:T2( Distance ) - return Distance -end - - ---- The main include file for the MOOSE system. - -Include.File( "Routines" ) -Include.File( "Base" ) -Include.File( "Scheduler" ) -Include.File( "Event" ) -Include.File( "Menu" ) -Include.File( "Controllable" ) -Include.File( "Group" ) -Include.File( "Unit" ) -Include.File( "Zone" ) -Include.File( "Client" ) -Include.File( "Static" ) -Include.File( "Airbase" ) -Include.File( "Database" ) -Include.File( "Set" ) -Include.File( "Point" ) Include.File( "Moose" ) -Include.File( "Scoring" ) -Include.File( "Cargo" ) -Include.File( "Message" ) -Include.File( "Stage" ) -Include.File( "Task" ) -Include.File( "GoHomeTask" ) -Include.File( "DestroyBaseTask" ) -Include.File( "DestroyGroupsTask" ) -Include.File( "DestroyRadarsTask" ) -Include.File( "DestroyUnitTypesTask" ) -Include.File( "PickupTask" ) -Include.File( "DeployTask" ) -Include.File( "NoTask" ) -Include.File( "RouteTask" ) -Include.File( "Mission" ) -Include.File( "CleanUp" ) -Include.File( "Spawn" ) -Include.File( "Movement" ) -Include.File( "Sead" ) -Include.File( "Escort" ) -Include.File( "MissileTrainer" ) -Include.File( "PatrolZone" ) -Include.File( "AIBalancer" ) -Include.File( "AirbasePolice" ) -Include.File( "Detection" ) -Include.File( "FAC" ) --- The order of the declarations is important here. Don't touch it. - ---- Declare the event dispatcher based on the EVENT class -_EVENTDISPATCHER = EVENT:New() -- #EVENT - ---- Declare the main database object, which is used internally by the MOOSE classes. -_DATABASE = DATABASE:New() -- Database#DATABASE - ---- Scoring system for MOOSE. --- This scoring class calculates the hits and kills that players make within a simulation session. --- Scoring is calculated using a defined algorithm. --- With a small change in MissionScripting.lua, the scoring can also be logged in a CSV file, that can then be uploaded --- to a database or a BI tool to publish the scoring results to the player community. --- @module Scoring --- @author FlightControl - - ---- The Scoring class --- @type SCORING --- @field Players A collection of the current players that have joined the game. --- @extends Base#BASE -SCORING = { - ClassName = "SCORING", - ClassID = 0, - Players = {}, -} - -local _SCORINGCoalition = - { - [1] = "Red", - [2] = "Blue", - } - -local _SCORINGCategory = - { - [Unit.Category.AIRPLANE] = "Plane", - [Unit.Category.HELICOPTER] = "Helicopter", - [Unit.Category.GROUND_UNIT] = "Vehicle", - [Unit.Category.SHIP] = "Ship", - [Unit.Category.STRUCTURE] = "Structure", - } - ---- Creates a new SCORING object to administer the scoring achieved by players. --- @param #SCORING self --- @param #string GameName The name of the game. This name is also logged in the CSV score file. --- @return #SCORING self --- @usage --- -- Define a new scoring object for the mission Gori Valley. --- ScoringObject = SCORING:New( "Gori Valley" ) -function SCORING:New( GameName ) - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) - - if GameName then - self.GameName = GameName - else - error( "A game name must be given to register the scoring results" ) - end - - - _EVENTDISPATCHER:OnDead( self._EventOnDeadOrCrash, self ) - _EVENTDISPATCHER:OnCrash( self._EventOnDeadOrCrash, self ) - _EVENTDISPATCHER:OnHit( self._EventOnHit, self ) - - --self.SchedulerId = routines.scheduleFunction( SCORING._FollowPlayersScheduled, { self }, 0, 5 ) - self.SchedulerId = SCHEDULER:New( self, self._FollowPlayersScheduled, {}, 0, 5 ) - - self:ScoreMenu() - - return self - -end - ---- Creates a score radio menu. Can be accessed using Radio -> F10. --- @param #SCORING self --- @return #SCORING self -function SCORING:ScoreMenu() - self.Menu = SUBMENU:New( 'Scoring' ) - self.AllScoresMenu = COMMANDMENU:New( 'Score All Active Players', self.Menu, SCORING.ReportScoreAll, self ) - --- = COMMANDMENU:New('Your Current Score', ReportScore, SCORING.ReportScorePlayer, self ) - return self -end - ---- Follows new players entering Clients within the DCSRTE. --- TODO: Need to see if i can catch this also with an event. It will eliminate the schedule ... -function SCORING:_FollowPlayersScheduled() - self:F3( "_FollowPlayersScheduled" ) - - local ClientUnit = 0 - local CoalitionsData = { AlivePlayersRed = coalition.getPlayers(coalition.side.RED), AlivePlayersBlue = coalition.getPlayers(coalition.side.BLUE) } - local unitId - local unitData - local AlivePlayerUnits = {} - - for CoalitionId, CoalitionData in pairs( CoalitionsData ) do - self:T3( { "_FollowPlayersScheduled", CoalitionData } ) - for UnitId, UnitData in pairs( CoalitionData ) do - self:_AddPlayerFromUnit( UnitData ) - end - end - - return true -end - - ---- Track DEAD or CRASH events for the scoring. --- @param #SCORING self --- @param Event#EVENTDATA Event -function SCORING:_EventOnDeadOrCrash( Event ) - self:F( { Event } ) - - local TargetUnit = nil - local TargetGroup = nil - local TargetUnitName = "" - local TargetGroupName = "" - local TargetPlayerName = "" - local TargetCoalition = nil - local TargetCategory = nil - local TargetType = nil - local TargetUnitCoalition = nil - local TargetUnitCategory = nil - local TargetUnitType = nil - - if Event.IniDCSUnit then - - TargetUnit = Event.IniDCSUnit - TargetUnitName = Event.IniDCSUnitName - TargetGroup = Event.IniDCSGroup - TargetGroupName = Event.IniDCSGroupName - TargetPlayerName = TargetUnit:getPlayerName() - - TargetCoalition = TargetUnit:getCoalition() - --TargetCategory = TargetUnit:getCategory() - TargetCategory = TargetUnit:getDesc().category -- Workaround - TargetType = TargetUnit:getTypeName() - - TargetUnitCoalition = _SCORINGCoalition[TargetCoalition] - TargetUnitCategory = _SCORINGCategory[TargetCategory] - TargetUnitType = TargetType - - self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType } ) - end - - for PlayerName, PlayerData in pairs( self.Players ) do - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Something got killed" ) - - -- Some variables - local InitUnitName = PlayerData.UnitName - local InitUnitType = PlayerData.UnitType - local InitCoalition = PlayerData.UnitCoalition - local InitCategory = PlayerData.UnitCategory - local InitUnitCoalition = _SCORINGCoalition[InitCoalition] - local InitUnitCategory = _SCORINGCategory[InitCategory] - - self:T( { InitUnitName, InitUnitType, InitUnitCoalition, InitCoalition, InitUnitCategory, InitCategory } ) - - -- What is he hitting? - if TargetCategory then - if PlayerData and PlayerData.Hit and PlayerData.Hit[TargetCategory] and PlayerData.Hit[TargetCategory][TargetUnitName] then -- Was there a hit for this unit for this player before registered??? - if not PlayerData.Kill[TargetCategory] then - PlayerData.Kill[TargetCategory] = {} - end - if not PlayerData.Kill[TargetCategory][TargetType] then - PlayerData.Kill[TargetCategory][TargetType] = {} - PlayerData.Kill[TargetCategory][TargetType].Score = 0 - PlayerData.Kill[TargetCategory][TargetType].ScoreKill = 0 - PlayerData.Kill[TargetCategory][TargetType].Penalty = 0 - PlayerData.Kill[TargetCategory][TargetType].PenaltyKill = 0 - end - - if InitCoalition == TargetCoalition then - PlayerData.Penalty = PlayerData.Penalty + 25 - PlayerData.Kill[TargetCategory][TargetType].Penalty = PlayerData.Kill[TargetCategory][TargetType].Penalty + 25 - PlayerData.Kill[TargetCategory][TargetType].PenaltyKill = PlayerData.Kill[TargetCategory][TargetType].PenaltyKill + 1 - MESSAGE:New( "Player '" .. PlayerName .. "' killed a friendly " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. - PlayerData.Kill[TargetCategory][TargetType].PenaltyKill .. " times. Penalty: -" .. PlayerData.Kill[TargetCategory][TargetType].Penalty .. - ". Score Total:" .. PlayerData.Score - PlayerData.Penalty, - 5 ):ToAll() - self:ScoreCSV( PlayerName, "KILL_PENALTY", 1, -125, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - else - PlayerData.Score = PlayerData.Score + 10 - PlayerData.Kill[TargetCategory][TargetType].Score = PlayerData.Kill[TargetCategory][TargetType].Score + 10 - PlayerData.Kill[TargetCategory][TargetType].ScoreKill = PlayerData.Kill[TargetCategory][TargetType].ScoreKill + 1 - MESSAGE:New( "Player '" .. PlayerName .. "' killed an enemy " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. - PlayerData.Kill[TargetCategory][TargetType].ScoreKill .. " times. Score: " .. PlayerData.Kill[TargetCategory][TargetType].Score .. - ". Score Total:" .. PlayerData.Score - PlayerData.Penalty, - 5 ):ToAll() - self:ScoreCSV( PlayerName, "KILL_SCORE", 1, 10, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - end - end - end - end - end -end - - - ---- Add a new player entering a Unit. -function SCORING:_AddPlayerFromUnit( UnitData ) - self:F( UnitData ) - - if UnitData and UnitData:isExist() then - local UnitName = UnitData:getName() - local PlayerName = UnitData:getPlayerName() - local UnitDesc = UnitData:getDesc() - local UnitCategory = UnitDesc.category - local UnitCoalition = UnitData:getCoalition() - local UnitTypeName = UnitData:getTypeName() - - self:T( { PlayerName, UnitName, UnitCategory, UnitCoalition, UnitTypeName } ) - - if self.Players[PlayerName] == nil then -- I believe this is the place where a Player gets a life in a mission when he enters a unit ... - self.Players[PlayerName] = {} - self.Players[PlayerName].Hit = {} - self.Players[PlayerName].Kill = {} - self.Players[PlayerName].Mission = {} - - -- for CategoryID, CategoryName in pairs( SCORINGCategory ) do - -- self.Players[PlayerName].Hit[CategoryID] = {} - -- self.Players[PlayerName].Kill[CategoryID] = {} - -- end - self.Players[PlayerName].HitPlayers = {} - self.Players[PlayerName].HitUnits = {} - self.Players[PlayerName].Score = 0 - self.Players[PlayerName].Penalty = 0 - self.Players[PlayerName].PenaltyCoalition = 0 - self.Players[PlayerName].PenaltyWarning = 0 - end - - if not self.Players[PlayerName].UnitCoalition then - self.Players[PlayerName].UnitCoalition = UnitCoalition - else - if self.Players[PlayerName].UnitCoalition ~= UnitCoalition then - self.Players[PlayerName].Penalty = self.Players[PlayerName].Penalty + 50 - self.Players[PlayerName].PenaltyCoalition = self.Players[PlayerName].PenaltyCoalition + 1 - MESSAGE:New( "Player '" .. PlayerName .. "' changed coalition from " .. _SCORINGCoalition[self.Players[PlayerName].UnitCoalition] .. " to " .. _SCORINGCoalition[UnitCoalition] .. - "(changed " .. self.Players[PlayerName].PenaltyCoalition .. " times the coalition). 50 Penalty points added.", - 2 - ):ToAll() - self:ScoreCSV( PlayerName, "COALITION_PENALTY", 1, -50, self.Players[PlayerName].UnitName, _SCORINGCoalition[self.Players[PlayerName].UnitCoalition], _SCORINGCategory[self.Players[PlayerName].UnitCategory], self.Players[PlayerName].UnitType, - UnitName, _SCORINGCoalition[UnitCoalition], _SCORINGCategory[UnitCategory], UnitData:getTypeName() ) - end - end - self.Players[PlayerName].UnitName = UnitName - self.Players[PlayerName].UnitCoalition = UnitCoalition - self.Players[PlayerName].UnitCategory = UnitCategory - self.Players[PlayerName].UnitType = UnitTypeName - - if self.Players[PlayerName].Penalty > 100 then - if self.Players[PlayerName].PenaltyWarning < 1 then - MESSAGE:New( "Player '" .. PlayerName .. "': WARNING! If you continue to commit FRATRICIDE and have a PENALTY score higher than 150, you will be COURT MARTIALED and DISMISSED from this mission! \nYour total penalty is: " .. self.Players[PlayerName].Penalty, - 30 - ):ToAll() - self.Players[PlayerName].PenaltyWarning = self.Players[PlayerName].PenaltyWarning + 1 - end - end - - if self.Players[PlayerName].Penalty > 150 then - ClientGroup = GROUP:NewFromDCSUnit( UnitData ) - ClientGroup:Destroy() - MESSAGE:New( "Player '" .. PlayerName .. "' committed FRATRICIDE, he will be COURT MARTIALED and is DISMISSED from this mission!", - 10 - ):ToAll() - end - - end -end - - ---- Registers Scores the players completing a Mission Task. -function SCORING:_AddMissionTaskScore( PlayerUnit, MissionName, Score ) - self:F( { PlayerUnit, MissionName, Score } ) - - local PlayerName = PlayerUnit:getPlayerName() - - if not self.Players[PlayerName].Mission[MissionName] then - self.Players[PlayerName].Mission[MissionName] = {} - self.Players[PlayerName].Mission[MissionName].ScoreTask = 0 - self.Players[PlayerName].Mission[MissionName].ScoreMission = 0 - end - - self:T( PlayerName ) - self:T( self.Players[PlayerName].Mission[MissionName] ) - - self.Players[PlayerName].Score = self.Players[PlayerName].Score + Score - self.Players[PlayerName].Mission[MissionName].ScoreTask = self.Players[PlayerName].Mission[MissionName].ScoreTask + Score - - MESSAGE:New( "Player '" .. PlayerName .. "' has finished another Task in Mission '" .. MissionName .. "'. " .. - Score .. " Score points added.", - 20 ):ToAll() - - self:ScoreCSV( PlayerName, "TASK_" .. MissionName:gsub( ' ', '_' ), 1, Score, PlayerUnit:getName() ) -end - - ---- Registers Mission Scores for possible multiple players that contributed in the Mission. -function SCORING:_AddMissionScore( MissionName, Score ) - self:F( { MissionName, Score } ) - - for PlayerName, PlayerData in pairs( self.Players ) do - - if PlayerData.Mission[MissionName] then - PlayerData.Score = PlayerData.Score + Score - PlayerData.Mission[MissionName].ScoreMission = PlayerData.Mission[MissionName].ScoreMission + Score - MESSAGE:New( "Player '" .. PlayerName .. "' has finished Mission '" .. MissionName .. "'. " .. - Score .. " Score points added.", - 20 ):ToAll() - self:ScoreCSV( PlayerName, "MISSION_" .. MissionName:gsub( ' ', '_' ), 1, Score ) - end - end -end - ---- Handles the OnHit event for the scoring. --- @param #SCORING self --- @param Event#EVENTDATA Event -function SCORING:_EventOnHit( Event ) - self:F( { Event } ) - - local InitUnit = nil - local InitUnitName = "" - local InitGroup = nil - local InitGroupName = "" - local InitPlayerName = nil - - local InitCoalition = nil - local InitCategory = nil - local InitType = nil - local InitUnitCoalition = nil - local InitUnitCategory = nil - local InitUnitType = nil - - local TargetUnit = nil - local TargetUnitName = "" - local TargetGroup = nil - local TargetGroupName = "" - local TargetPlayerName = "" - - local TargetCoalition = nil - local TargetCategory = nil - local TargetType = nil - local TargetUnitCoalition = nil - local TargetUnitCategory = nil - local TargetUnitType = nil - - if Event.IniDCSUnit then - - InitUnit = Event.IniDCSUnit - InitUnitName = Event.IniDCSUnitName - InitGroup = Event.IniDCSGroup - InitGroupName = Event.IniDCSGroupName - InitPlayerName = InitUnit:getPlayerName() - - InitCoalition = InitUnit:getCoalition() - --TODO: Workaround Client DCS Bug - --InitCategory = InitUnit:getCategory() - InitCategory = InitUnit:getDesc().category - InitType = InitUnit:getTypeName() - - InitUnitCoalition = _SCORINGCoalition[InitCoalition] - InitUnitCategory = _SCORINGCategory[InitCategory] - InitUnitType = InitType - - self:T( { InitUnitName, InitGroupName, InitPlayerName, InitCoalition, InitCategory, InitType , InitUnitCoalition, InitUnitCategory, InitUnitType } ) - end - - - if Event.TgtDCSUnit then - - TargetUnit = Event.TgtDCSUnit - TargetUnitName = Event.TgtDCSUnitName - TargetGroup = Event.TgtDCSGroup - TargetGroupName = Event.TgtDCSGroupName - TargetPlayerName = TargetUnit:getPlayerName() - - TargetCoalition = TargetUnit:getCoalition() - --TODO: Workaround Client DCS Bug - --TargetCategory = TargetUnit:getCategory() - TargetCategory = TargetUnit:getDesc().category - TargetType = TargetUnit:getTypeName() - - TargetUnitCoalition = _SCORINGCoalition[TargetCoalition] - TargetUnitCategory = _SCORINGCategory[TargetCategory] - TargetUnitType = TargetType - - self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType, TargetUnitCoalition, TargetUnitCategory, TargetUnitType } ) - end - - if InitPlayerName ~= nil then -- It is a player that is hitting something - self:_AddPlayerFromUnit( InitUnit ) - if self.Players[InitPlayerName] then -- This should normally not happen, but i'll test it anyway. - if TargetPlayerName ~= nil then -- It is a player hitting another player ... - self:_AddPlayerFromUnit( TargetUnit ) - self.Players[InitPlayerName].HitPlayers = self.Players[InitPlayerName].HitPlayers + 1 - end - - self:T( "Hitting Something" ) - -- What is he hitting? - if TargetCategory then - if not self.Players[InitPlayerName].Hit[TargetCategory] then - self.Players[InitPlayerName].Hit[TargetCategory] = {} - end - if not self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName] then - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName] = {} - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score = 0 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty = 0 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit = 0 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit = 0 - end - local Score = 0 - if InitCoalition == TargetCoalition then - self.Players[InitPlayerName].Penalty = self.Players[InitPlayerName].Penalty + 10 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty + 10 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit + 1 - MESSAGE:New( "Player '" .. InitPlayerName .. "' hit a friendly " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit .. " times. Penalty: -" .. self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty .. - ". Score Total:" .. self.Players[InitPlayerName].Score - self.Players[InitPlayerName].Penalty, - 2 - ):ToAll() - self:ScoreCSV( InitPlayerName, "HIT_PENALTY", 1, -25, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - else - self.Players[InitPlayerName].Score = self.Players[InitPlayerName].Score + 10 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score + 1 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit + 1 - MESSAGE:New( "Player '" .. InitPlayerName .. "' hit a target " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit .. " times. Score: " .. self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score .. - ". Score Total:" .. self.Players[InitPlayerName].Score - self.Players[InitPlayerName].Penalty, - 2 - ):ToAll() - self:ScoreCSV( InitPlayerName, "HIT_SCORE", 1, 1, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - end - end - end - elseif InitPlayerName == nil then -- It is an AI hitting a player??? - - end -end - - -function SCORING:ReportScoreAll() - - env.info( "Hello World " ) - - local ScoreMessage = "" - local PlayerMessage = "" - - self:T( "Score Report" ) - - for PlayerName, PlayerData in pairs( self.Players ) do - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) - - -- Some variables - local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] - local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local PlayerScore = 0 - local PlayerPenalty = 0 - - ScoreMessage = ":\n" - - local ScoreMessageHits = "" - - for CategoryID, CategoryName in pairs( _SCORINGCategory ) do - self:T( CategoryName ) - if PlayerData.Hit[CategoryID] then - local Score = 0 - local ScoreHit = 0 - local Penalty = 0 - local PenaltyHit = 0 - self:T( "Hit scores exist for player " .. PlayerName ) - for UnitName, UnitData in pairs( PlayerData.Hit[CategoryID] ) do - Score = Score + UnitData.Score - ScoreHit = ScoreHit + UnitData.ScoreHit - Penalty = Penalty + UnitData.Penalty - PenaltyHit = UnitData.PenaltyHit - end - local ScoreMessageHit = string.format( "%s:%d ", CategoryName, Score - Penalty ) - self:T( ScoreMessageHit ) - ScoreMessageHits = ScoreMessageHits .. ScoreMessageHit - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageHits ~= "" then - ScoreMessage = ScoreMessage .. " Hits: " .. ScoreMessageHits .. "\n" - end - - local ScoreMessageKills = "" - for CategoryID, CategoryName in pairs( _SCORINGCategory ) do - self:T( "Kill scores exist for player " .. PlayerName ) - if PlayerData.Kill[CategoryID] then - local Score = 0 - local ScoreKill = 0 - local Penalty = 0 - local PenaltyKill = 0 - - for UnitName, UnitData in pairs( PlayerData.Kill[CategoryID] ) do - Score = Score + UnitData.Score - ScoreKill = ScoreKill + UnitData.ScoreKill - Penalty = Penalty + UnitData.Penalty - PenaltyKill = PenaltyKill + UnitData.PenaltyKill - end - - local ScoreMessageKill = string.format( " %s:%d ", CategoryName, Score - Penalty ) - self:T( ScoreMessageKill ) - ScoreMessageKills = ScoreMessageKills .. ScoreMessageKill - - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageKills = ScoreMessageKills .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageKills ~= "" then - ScoreMessage = ScoreMessage .. " Kills: " .. ScoreMessageKills .. "\n" - end - - local ScoreMessageCoalitionChangePenalties = "" - if PlayerData.PenaltyCoalition ~= 0 then - ScoreMessageCoalitionChangePenalties = ScoreMessageCoalitionChangePenalties .. string.format( " -%d (%d changed)", PlayerData.Penalty, PlayerData.PenaltyCoalition ) - PlayerPenalty = PlayerPenalty + PlayerData.Penalty - end - if ScoreMessageCoalitionChangePenalties ~= "" then - ScoreMessage = ScoreMessage .. " Coalition Penalties: " .. ScoreMessageCoalitionChangePenalties .. "\n" - end - - local ScoreMessageMission = "" - local ScoreMission = 0 - local ScoreTask = 0 - for MissionName, MissionData in pairs( PlayerData.Mission ) do - ScoreMission = ScoreMission + MissionData.ScoreMission - ScoreTask = ScoreTask + MissionData.ScoreTask - ScoreMessageMission = ScoreMessageMission .. "'" .. MissionName .. "'; " - end - PlayerScore = PlayerScore + ScoreMission + ScoreTask - - if ScoreMessageMission ~= "" then - ScoreMessage = ScoreMessage .. " Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ")\n" - end - - PlayerMessage = PlayerMessage .. string.format( "Player '%s' Score:%d (%d Score -%d Penalties)%s", PlayerName, PlayerScore - PlayerPenalty, PlayerScore, PlayerPenalty, ScoreMessage ) - end - end - MESSAGE:New( PlayerMessage, 30, "Player Scores" ):ToAll() -end - - -function SCORING:ReportScorePlayer() - - env.info( "Hello World " ) - - local ScoreMessage = "" - local PlayerMessage = "" - - self:T( "Score Report" ) - - for PlayerName, PlayerData in pairs( self.Players ) do - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) - - -- Some variables - local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] - local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local PlayerScore = 0 - local PlayerPenalty = 0 - - ScoreMessage = "" - - local ScoreMessageHits = "" - - for CategoryID, CategoryName in pairs( _SCORINGCategory ) do - self:T( CategoryName ) - if PlayerData.Hit[CategoryID] then - local Score = 0 - local ScoreHit = 0 - local Penalty = 0 - local PenaltyHit = 0 - self:T( "Hit scores exist for player " .. PlayerName ) - for UnitName, UnitData in pairs( PlayerData.Hit[CategoryID] ) do - Score = Score + UnitData.Score - ScoreHit = ScoreHit + UnitData.ScoreHit - Penalty = Penalty + UnitData.Penalty - PenaltyHit = UnitData.PenaltyHit - end - local ScoreMessageHit = string.format( "\n %s = %d score(%d;-%d) hits(#%d;#-%d)", CategoryName, Score - Penalty, Score, Penalty, ScoreHit, PenaltyHit ) - self:T( ScoreMessageHit ) - ScoreMessageHits = ScoreMessageHits .. ScoreMessageHit - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageHits ~= "" then - ScoreMessage = ScoreMessage .. "\n Hits: " .. ScoreMessageHits .. " " - end - - local ScoreMessageKills = "" - for CategoryID, CategoryName in pairs( _SCORINGCategory ) do - self:T( "Kill scores exist for player " .. PlayerName ) - if PlayerData.Kill[CategoryID] then - local Score = 0 - local ScoreKill = 0 - local Penalty = 0 - local PenaltyKill = 0 - - for UnitName, UnitData in pairs( PlayerData.Kill[CategoryID] ) do - Score = Score + UnitData.Score - ScoreKill = ScoreKill + UnitData.ScoreKill - Penalty = Penalty + UnitData.Penalty - PenaltyKill = PenaltyKill + UnitData.PenaltyKill - end - - local ScoreMessageKill = string.format( "\n %s = %d score(%d;-%d) hits(#%d;#-%d)", CategoryName, Score - Penalty, Score, Penalty, ScoreKill, PenaltyKill ) - self:T( ScoreMessageKill ) - ScoreMessageKills = ScoreMessageKills .. ScoreMessageKill - - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageKills = ScoreMessageKills .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageKills ~= "" then - ScoreMessage = ScoreMessage .. "\n Kills: " .. ScoreMessageKills .. " " - end - - local ScoreMessageCoalitionChangePenalties = "" - if PlayerData.PenaltyCoalition ~= 0 then - ScoreMessageCoalitionChangePenalties = ScoreMessageCoalitionChangePenalties .. string.format( " -%d (%d changed)", PlayerData.Penalty, PlayerData.PenaltyCoalition ) - PlayerPenalty = PlayerPenalty + PlayerData.Penalty - end - if ScoreMessageCoalitionChangePenalties ~= "" then - ScoreMessage = ScoreMessage .. "\n Coalition: " .. ScoreMessageCoalitionChangePenalties .. " " - end - - local ScoreMessageMission = "" - local ScoreMission = 0 - local ScoreTask = 0 - for MissionName, MissionData in pairs( PlayerData.Mission ) do - ScoreMission = ScoreMission + MissionData.ScoreMission - ScoreTask = ScoreTask + MissionData.ScoreTask - ScoreMessageMission = ScoreMessageMission .. "'" .. MissionName .. "'; " - end - PlayerScore = PlayerScore + ScoreMission + ScoreTask - - if ScoreMessageMission ~= "" then - ScoreMessage = ScoreMessage .. "\n Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ") " - end - - PlayerMessage = PlayerMessage .. string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties ):%s", PlayerName, PlayerScore - PlayerPenalty, PlayerScore, PlayerPenalty, ScoreMessage ) - end - end - MESSAGE:New( PlayerMessage, 30, "Player Scores" ):ToAll() - -end - - -function SCORING:SecondsToClock(sSeconds) - local nSeconds = sSeconds - if nSeconds == 0 then - --return nil; - return "00:00:00"; - else - nHours = string.format("%02.f", math.floor(nSeconds/3600)); - nMins = string.format("%02.f", math.floor(nSeconds/60 - (nHours*60))); - nSecs = string.format("%02.f", math.floor(nSeconds - nHours*3600 - nMins *60)); - return nHours..":"..nMins..":"..nSecs - end -end - ---- Opens a score CSV file to log the scores. --- @param #SCORING self --- @param #string ScoringCSV --- @return #SCORING self --- @usage --- -- Open a new CSV file to log the scores of the game Gori Valley. Let the name of the CSV file begin with "Player Scores". --- ScoringObject = SCORING:New( "Gori Valley" ) --- ScoringObject:OpenCSV( "Player Scores" ) -function SCORING:OpenCSV( ScoringCSV ) - self:F( ScoringCSV ) - - if lfs and io and os then - if ScoringCSV then - self.ScoringCSV = ScoringCSV - local fdir = lfs.writedir() .. [[Logs\]] .. self.ScoringCSV .. " " .. os.date( "%Y-%m-%d %H-%M-%S" ) .. ".csv" - - self.CSVFile, self.err = io.open( fdir, "w+" ) - if not self.CSVFile then - error( "Error: Cannot open CSV file in " .. lfs.writedir() ) - end - - self.CSVFile:write( '"GameName","RunTime","Time","PlayerName","ScoreType","PlayerUnitCoaltion","PlayerUnitCategory","PlayerUnitType","PlayerUnitName","TargetUnitCoalition","TargetUnitCategory","TargetUnitType","TargetUnitName","Times","Score"\n' ) - - self.RunTime = os.date("%y-%m-%d_%H-%M-%S") - else - error( "A string containing the CSV file name must be given." ) - end - else - self:E( "The MissionScripting.lua file has not been changed to allow lfs, io and os modules to be used..." ) - end - return self -end - - ---- Registers a score for a player. --- @param #SCORING self --- @param #string PlayerName The name of the player. --- @param #string ScoreType The type of the score. --- @param #string ScoreTimes The amount of scores achieved. --- @param #string ScoreAmount The score given. --- @param #string PlayerUnitName The unit name of the player. --- @param #string PlayerUnitCoalition The coalition of the player unit. --- @param #string PlayerUnitCategory The category of the player unit. --- @param #string PlayerUnitType The type of the player unit. --- @param #string TargetUnitName The name of the target unit. --- @param #string TargetUnitCoalition The coalition of the target unit. --- @param #string TargetUnitCategory The category of the target unit. --- @param #string TargetUnitType The type of the target unit. --- @return #SCORING self -function SCORING:ScoreCSV( PlayerName, ScoreType, ScoreTimes, ScoreAmount, PlayerUnitName, PlayerUnitCoalition, PlayerUnitCategory, PlayerUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - --write statistic information to file - local ScoreTime = self:SecondsToClock( timer.getTime() ) - PlayerName = PlayerName:gsub( '"', '_' ) - - if PlayerUnitName and PlayerUnitName ~= '' then - local PlayerUnit = Unit.getByName( PlayerUnitName ) - - if PlayerUnit then - if not PlayerUnitCategory then - --PlayerUnitCategory = SCORINGCategory[PlayerUnit:getCategory()] - PlayerUnitCategory = _SCORINGCategory[PlayerUnit:getDesc().category] - end - - if not PlayerUnitCoalition then - PlayerUnitCoalition = _SCORINGCoalition[PlayerUnit:getCoalition()] - end - - if not PlayerUnitType then - PlayerUnitType = PlayerUnit:getTypeName() - end - else - PlayerUnitName = '' - PlayerUnitCategory = '' - PlayerUnitCoalition = '' - PlayerUnitType = '' - end - else - PlayerUnitName = '' - PlayerUnitCategory = '' - PlayerUnitCoalition = '' - PlayerUnitType = '' - end - - if not TargetUnitCoalition then - TargetUnitCoalition = '' - end - - if not TargetUnitCategory then - TargetUnitCategory = '' - end - - if not TargetUnitType then - TargetUnitType = '' - end - - if not TargetUnitName then - TargetUnitName = '' - end - - if lfs and io and os then - self.CSVFile:write( - '"' .. self.GameName .. '"' .. ',' .. - '"' .. self.RunTime .. '"' .. ',' .. - '' .. ScoreTime .. '' .. ',' .. - '"' .. PlayerName .. '"' .. ',' .. - '"' .. ScoreType .. '"' .. ',' .. - '"' .. PlayerUnitCoalition .. '"' .. ',' .. - '"' .. PlayerUnitCategory .. '"' .. ',' .. - '"' .. PlayerUnitType .. '"' .. ',' .. - '"' .. PlayerUnitName .. '"' .. ',' .. - '"' .. TargetUnitCoalition .. '"' .. ',' .. - '"' .. TargetUnitCategory .. '"' .. ',' .. - '"' .. TargetUnitType .. '"' .. ',' .. - '"' .. TargetUnitName .. '"' .. ',' .. - '' .. ScoreTimes .. '' .. ',' .. - '' .. ScoreAmount - ) - - self.CSVFile:write( "\n" ) - end -end - - -function SCORING:CloseCSV() - if lfs and io and os then - self.CSVFile:close() - end -end - ---- CARGO Classes --- @module CARGO - - - - - - - ---- Clients are those Groups defined within the Mission Editor that have the skillset defined as "Client" or "Player". --- These clients are defined within the Mission Orchestration Framework (MOF) - -CARGOS = {} - - -CARGO_ZONE = { - ClassName="CARGO_ZONE", - CargoZoneName = '', - CargoHostUnitName = '', - SIGNAL = { - TYPE = { - SMOKE = { ID = 1, TEXT = "smoke" }, - FLARE = { ID = 2, TEXT = "flare" } - }, - COLOR = { - GREEN = { ID = 1, TRIGGERCOLOR = trigger.smokeColor.Green, TEXT = "A green" }, - RED = { ID = 2, TRIGGERCOLOR = trigger.smokeColor.Red, TEXT = "A red" }, - WHITE = { ID = 3, TRIGGERCOLOR = trigger.smokeColor.White, TEXT = "A white" }, - ORANGE = { ID = 4, TRIGGERCOLOR = trigger.smokeColor.Orange, TEXT = "An orange" }, - BLUE = { ID = 5, TRIGGERCOLOR = trigger.smokeColor.Blue, TEXT = "A blue" }, - YELLOW = { ID = 6, TRIGGERCOLOR = trigger.flareColor.Yellow, TEXT = "A yellow" } - } - } -} - ---- Creates a new zone where cargo can be collected or deployed. --- The zone functionality is useful to smoke or indicate routes for cargo pickups or deployments. --- Provide the zone name as declared in the mission file into the CargoZoneName in the :New method. --- An optional parameter is the CargoHostName, which is a Group declared with Late Activation switched on in the mission file. --- The CargoHostName is the "host" of the cargo zone: --- --- * It will smoke the zone position when a client is approaching the zone. --- * Depending on the cargo type, it will assist in the delivery of the cargo by driving to and from the client. --- --- @param #CARGO_ZONE self --- @param #string CargoZoneName The name of the zone as declared within the mission editor. --- @param #string CargoHostName The name of the Group "hosting" the zone. The Group MUST NOT be a static, and must be a "mobile" unit. -function CARGO_ZONE:New( CargoZoneName, CargoHostName ) local self = BASE:Inherit( self, ZONE:New( CargoZoneName ) ) - self:F( { CargoZoneName, CargoHostName } ) - - self.CargoZoneName = CargoZoneName - self.SignalHeight = 2 - --self.CargoZone = trigger.misc.getZone( CargoZoneName ) - - - if CargoHostName then - self.CargoHostName = CargoHostName - end - - self:T( self.CargoZoneName ) - - return self -end - -function CARGO_ZONE:Spawn() - self:F( self.CargoHostName ) - - if self.CargoHostName then -- Only spawn a host in the zone when there is one given as a parameter in the New function. - if self.CargoHostSpawn then - local CargoHostGroup = self.CargoHostSpawn:GetGroupFromIndex() - if CargoHostGroup and CargoHostGroup:IsAlive() then - else - self.CargoHostSpawn:ReSpawn( 1 ) - end - else - self:T( "Initialize CargoHostSpawn" ) - self.CargoHostSpawn = SPAWN:New( self.CargoHostName ):Limit( 1, 1 ) - self.CargoHostSpawn:ReSpawn( 1 ) - end - end - - return self -end - -function CARGO_ZONE:GetHostUnit() - self:F( self ) - - if self.CargoHostName then - - -- A Host has been given, signal the host - local CargoHostGroup = self.CargoHostSpawn:GetGroupFromIndex() - local CargoHostUnit - if CargoHostGroup and CargoHostGroup:IsAlive() then - CargoHostUnit = CargoHostGroup:GetUnit(1) - else - CargoHostUnit = StaticObject.getByName( self.CargoHostName ) - end - - return CargoHostUnit - end - - return nil -end - -function CARGO_ZONE:ReportCargosToClient( Client, CargoType ) - self:F() - - local SignalUnit = self:GetHostUnit() - - if SignalUnit then - - local SignalUnitTypeName = SignalUnit:getTypeName() - - local HostMessage = "" - - local IsCargo = false - for CargoID, Cargo in pairs( CARGOS ) do - if Cargo.CargoType == Task.CargoType then - if Cargo:IsStatusNone() then - HostMessage = HostMessage .. " - " .. Cargo.CargoName .. " - " .. Cargo.CargoType .. " (" .. Cargo.Weight .. "kg)" .. "\n" - IsCargo = true - end - end - end - - if not IsCargo then - HostMessage = "No Cargo Available." - end - - Client:Message( HostMessage, 20, SignalUnitTypeName .. ": Reporting Cargo", 10 ) - end -end - - -function CARGO_ZONE:Signal() - self:F() - - local Signalled = false - - if self.SignalType then - - if self.CargoHostName then - - -- A Host has been given, signal the host - - local SignalUnit = self:GetHostUnit() - - if SignalUnit then - - self:T( 'Signalling Unit' ) - local SignalVehiclePos = SignalUnit:GetPointVec3() - SignalVehiclePos.y = SignalVehiclePos.y + 2 - - if self.SignalType.ID == CARGO_ZONE.SIGNAL.TYPE.SMOKE.ID then - - trigger.action.smoke( SignalVehiclePos, self.SignalColor.TRIGGERCOLOR ) - Signalled = true - - elseif self.SignalType.ID == CARGO_ZONE.SIGNAL.TYPE.FLARE.ID then - - trigger.action.signalFlare( SignalVehiclePos, self.SignalColor.TRIGGERCOLOR , 0 ) - Signalled = false - - end - end - - else - - local ZonePointVec3 = self:GetPointVec3( self.SignalHeight ) -- Get the zone position + the landheight + 2 meters - - if self.SignalType.ID == CARGO_ZONE.SIGNAL.TYPE.SMOKE.ID then - - trigger.action.smoke( ZonePointVec3, self.SignalColor.TRIGGERCOLOR ) - Signalled = true - - elseif self.SignalType.ID == CARGO_ZONE.SIGNAL.TYPE.FLARE.ID then - trigger.action.signalFlare( ZonePointVec3, self.SignalColor.TRIGGERCOLOR, 0 ) - Signalled = false - - end - end - end - - return Signalled - -end - -function CARGO_ZONE:WhiteSmoke( SignalHeight ) - self:F() - - self.SignalType = CARGO_ZONE.SIGNAL.TYPE.SMOKE - self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.WHITE - - if SignalHeight then - self.SignalHeight = SignalHeight - end - - return self -end - -function CARGO_ZONE:BlueSmoke( SignalHeight ) - self:F() - - self.SignalType = CARGO_ZONE.SIGNAL.TYPE.SMOKE - self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.BLUE - - if SignalHeight then - self.SignalHeight = SignalHeight - end - - return self -end - -function CARGO_ZONE:RedSmoke( SignalHeight ) - self:F() - - self.SignalType = CARGO_ZONE.SIGNAL.TYPE.SMOKE - self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.RED - - if SignalHeight then - self.SignalHeight = SignalHeight - end - - return self -end - -function CARGO_ZONE:OrangeSmoke( SignalHeight ) - self:F() - - self.SignalType = CARGO_ZONE.SIGNAL.TYPE.SMOKE - self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.ORANGE - - if SignalHeight then - self.SignalHeight = SignalHeight - end - - return self -end - -function CARGO_ZONE:GreenSmoke( SignalHeight ) - self:F() - - self.SignalType = CARGO_ZONE.SIGNAL.TYPE.SMOKE - self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.GREEN - - if SignalHeight then - self.SignalHeight = SignalHeight - end - - return self -end - - -function CARGO_ZONE:WhiteFlare( SignalHeight ) - self:F() - - self.SignalType = CARGO_ZONE.SIGNAL.TYPE.FLARE - self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.WHITE - - if SignalHeight then - self.SignalHeight = SignalHeight - end - - return self -end - -function CARGO_ZONE:RedFlare( SignalHeight ) - self:F() - - self.SignalType = CARGO_ZONE.SIGNAL.TYPE.FLARE - self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.RED - - if SignalHeight then - self.SignalHeight = SignalHeight - end - - return self -end - -function CARGO_ZONE:GreenFlare( SignalHeight ) - self:F() - - self.SignalType = CARGO_ZONE.SIGNAL.TYPE.FLARE - self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.GREEN - - if SignalHeight then - self.SignalHeight = SignalHeight - end - - return self -end - -function CARGO_ZONE:YellowFlare( SignalHeight ) - self:F() - - self.SignalType = CARGO_ZONE.SIGNAL.TYPE.FLARE - self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.YELLOW - - if SignalHeight then - self.SignalHeight = SignalHeight - end - - return self -end - - -function CARGO_ZONE:GetCargoHostUnit() - self:F( self ) - - if self.CargoHostSpawn then - local CargoHostGroup = self.CargoHostSpawn:GetGroupFromIndex(1) - if CargoHostGroup and CargoHostGroup:IsAlive() then - local CargoHostUnit = CargoHostGroup:GetUnit(1) - if CargoHostUnit and CargoHostUnit:IsAlive() then - return CargoHostUnit - end - end - end - - return nil -end - -function CARGO_ZONE:GetCargoZoneName() - self:F() - - return self.CargoZoneName -end - -CARGO = { - ClassName = "CARGO", - STATUS = { - NONE = 0, - LOADED = 1, - UNLOADED = 2, - LOADING = 3 - }, - CargoClient = nil -} - ---- Add Cargo to the mission... Cargo functionality needs to be reworked a bit, so this is still under construction. I need to make a CARGO Class... -function CARGO:New( CargoType, CargoName, CargoWeight ) local self = BASE:Inherit( self, BASE:New() ) - self:F( { CargoType, CargoName, CargoWeight } ) - - - self.CargoType = CargoType - self.CargoName = CargoName - self.CargoWeight = CargoWeight - - self:StatusNone() - - return self -end - -function CARGO:Spawn( Client ) - self:F() - - return self - -end - -function CARGO:IsNear( Client, LandingZone ) - self:F() - - local Near = true - - return Near - -end - - -function CARGO:IsLoadingToClient() - self:F() - - if self:IsStatusLoading() then - return self.CargoClient - end - - return nil - -end - - -function CARGO:IsLoadedInClient() - self:F() - - if self:IsStatusLoaded() then - return self.CargoClient - end - - return nil - -end - - -function CARGO:UnLoad( Client, TargetZoneName ) - self:F() - - self:StatusUnLoaded() - - return self -end - -function CARGO:OnBoard( Client, LandingZone ) - self:F() - - local Valid = true - - self.CargoClient = Client - local ClientUnit = Client:GetClientGroupDCSUnit() - - return Valid -end - -function CARGO:OnBoarded( Client, LandingZone ) - self:F() - - local OnBoarded = true - - return OnBoarded -end - -function CARGO:Load( Client ) - self:F() - - self:StatusLoaded( Client ) - - return self -end - -function CARGO:IsLandingRequired() - self:F() - return true -end - -function CARGO:IsSlingLoad() - self:F() - return false -end - - -function CARGO:StatusNone() - self:F() - - self.CargoClient = nil - self.CargoStatus = CARGO.STATUS.NONE - - return self -end - -function CARGO:StatusLoading( Client ) - self:F() - - self.CargoClient = Client - self.CargoStatus = CARGO.STATUS.LOADING - self:T( "Cargo " .. self.CargoName .. " loading to Client: " .. self.CargoClient:GetClientGroupName() ) - - return self -end - -function CARGO:StatusLoaded( Client ) - self:F() - - self.CargoClient = Client - self.CargoStatus = CARGO.STATUS.LOADED - self:T( "Cargo " .. self.CargoName .. " loaded in Client: " .. self.CargoClient:GetClientGroupName() ) - - return self -end - -function CARGO:StatusUnLoaded() - self:F() - - self.CargoClient = nil - self.CargoStatus = CARGO.STATUS.UNLOADED - - return self -end - - -function CARGO:IsStatusNone() - self:F() - - return self.CargoStatus == CARGO.STATUS.NONE -end - -function CARGO:IsStatusLoading() - self:F() - - return self.CargoStatus == CARGO.STATUS.LOADING -end - -function CARGO:IsStatusLoaded() - self:F() - - return self.CargoStatus == CARGO.STATUS.LOADED -end - -function CARGO:IsStatusUnLoaded() - self:F() - - return self.CargoStatus == CARGO.STATUS.UNLOADED -end - - -CARGO_GROUP = { - ClassName = "CARGO_GROUP" -} - - -function CARGO_GROUP:New( CargoType, CargoName, CargoWeight, CargoGroupTemplate, CargoZone ) local self = BASE:Inherit( self, CARGO:New( CargoType, CargoName, CargoWeight ) ) - self:F( { CargoType, CargoName, CargoWeight, CargoGroupTemplate, CargoZone } ) - - self.CargoSpawn = SPAWN:NewWithAlias( CargoGroupTemplate, CargoName ) - self.CargoZone = CargoZone - - CARGOS[self.CargoName] = self - - return self - -end - -function CARGO_GROUP:Spawn( Client ) - self:F( { Client } ) - - local SpawnCargo = true - - if self:IsStatusNone() then - local CargoGroup = Group.getByName( self.CargoName ) - if CargoGroup and CargoGroup:isExist() then - SpawnCargo = false - end - - elseif self:IsStatusLoading() then - - local Client = self:IsLoadingToClient() - if Client and Client:GetDCSGroup() then - SpawnCargo = false - else - local CargoGroup = Group.getByName( self.CargoName ) - if CargoGroup and CargoGroup:isExist() then - SpawnCargo = false - end - end - - elseif self:IsStatusLoaded() then - - local ClientLoaded = self:IsLoadedInClient() - -- Now test if another Client is alive (not this one), and it has the CARGO, then this cargo does not need to be initialized and spawned. - if ClientLoaded and ClientLoaded ~= Client then - local ClientGroup = Client:GetDCSGroup() - if ClientLoaded:GetClientGroupDCSUnit() and ClientLoaded:GetClientGroupDCSUnit():isExist() then - SpawnCargo = false - else - self:StatusNone() - end - else - -- Same Client, but now in initialize, so set back the status to None. - self:StatusNone() - end - - elseif self:IsStatusUnLoaded() then - - SpawnCargo = false - - end - - if SpawnCargo then - if self.CargoZone:GetCargoHostUnit() then - --- ReSpawn the Cargo from the CargoHost - self.CargoGroupName = self.CargoSpawn:SpawnFromUnit( self.CargoZone:GetCargoHostUnit(), 60, 30, 1 ):GetName() - else - --- ReSpawn the Cargo in the CargoZone without a host ... - self:T( self.CargoZone ) - self.CargoGroupName = self.CargoSpawn:SpawnInZone( self.CargoZone, true, 1 ):GetName() - end - self:StatusNone() - end - - self:T( { self.CargoGroupName, CARGOS[self.CargoName].CargoGroupName } ) - - return self -end - -function CARGO_GROUP:IsNear( Client, LandingZone ) - self:F() - - local Near = false - - if self.CargoGroupName then - local CargoGroup = Group.getByName( self.CargoGroupName ) - if routines.IsPartOfGroupInRadius( CargoGroup, Client:GetPositionVec3(), 250 ) then - Near = true - end - end - - return Near - -end - - -function CARGO_GROUP:OnBoard( Client, LandingZone, OnBoardSide ) - self:F() - - local Valid = true - - local ClientUnit = Client:GetClientGroupDCSUnit() - - local CarrierPos = ClientUnit:getPoint() - local CarrierPosMove = ClientUnit:getPoint() - local CarrierPosOnBoard = ClientUnit:getPoint() - - local CargoGroup = Group.getByName( self.CargoGroupName ) - - local CargoUnit = CargoGroup:getUnit(1) - local CargoPos = CargoUnit:getPoint() - - self.CargoInAir = CargoUnit: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 - - local Points = {} - - self:T( 'CargoPos x = ' .. CargoPos.x .. " z = " .. CargoPos.z ) - self:T( 'CarrierPosMove x = ' .. CarrierPosMove.x .. " z = " .. CarrierPosMove.z ) - - Points[#Points+1] = routines.ground.buildWP( CargoPos, "Cone", 10 ) - - self:T( 'Points[1] x = ' .. Points[1].x .. " y = " .. Points[1].y ) - - if OnBoardSide == nil then - OnBoardSide = CLIENT.ONBOARDSIDE.NONE - end - - if OnBoardSide == CLIENT.ONBOARDSIDE.LEFT then - - self:T( "TransportCargoOnBoard: Onboarding LEFT" ) - CarrierPosMove.z = CarrierPosMove.z - 25 - CarrierPosOnBoard.z = CarrierPosOnBoard.z - 5 - Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) - - elseif OnBoardSide == CLIENT.ONBOARDSIDE.RIGHT then - - self:T( "TransportCargoOnBoard: Onboarding RIGHT" ) - CarrierPosMove.z = CarrierPosMove.z + 25 - CarrierPosOnBoard.z = CarrierPosOnBoard.z + 5 - Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) - - elseif OnBoardSide == CLIENT.ONBOARDSIDE.BACK then - - self:T( "TransportCargoOnBoard: Onboarding BACK" ) - CarrierPosMove.x = CarrierPosMove.x - 25 - CarrierPosOnBoard.x = CarrierPosOnBoard.x - 5 - Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) - - elseif OnBoardSide == CLIENT.ONBOARDSIDE.FRONT then - - self:T( "TransportCargoOnBoard: Onboarding FRONT" ) - CarrierPosMove.x = CarrierPosMove.x + 25 - CarrierPosOnBoard.x = CarrierPosOnBoard.x + 5 - Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) - - elseif OnBoardSide == CLIENT.ONBOARDSIDE.NONE then - - self:T( "TransportCargoOnBoard: Onboarding CENTRAL" ) - Points[#Points+1] = routines.ground.buildWP( CarrierPos, "Cone", 10 ) - - end - self:T( "TransportCargoOnBoard: Routing " .. self.CargoGroupName ) - - --routines.scheduleFunction( routines.goRoute, { self.CargoGroupName, Points}, timer.getTime() + 4 ) - SCHEDULER:New( self, routines.goRoute, { self.CargoGroupName, Points}, 4 ) - end - - self:StatusLoading( Client ) - - return Valid - -end - - -function CARGO_GROUP:OnBoarded( Client, LandingZone ) - self:F() - - local OnBoarded = false - - local CargoGroup = Group.getByName( self.CargoGroupName ) - - if not self.CargoInAir then - if routines.IsPartOfGroupInRadius( CargoGroup, Client:GetPositionVec3(), 25 ) then - CargoGroup:destroy() - self:StatusLoaded( Client ) - OnBoarded = true - end - else - CargoGroup:destroy() - self:StatusLoaded( Client ) - OnBoarded = true - end - - return OnBoarded -end - - -function CARGO_GROUP:UnLoad( Client, TargetZoneName ) - self:F() - - self:T( 'self.CargoName = ' .. self.CargoName ) - - local CargoGroup = self.CargoSpawn:SpawnFromUnit( Client:GetClientGroupUnit(), 60, 30 ) - - self.CargoGroupName = CargoGroup:GetName() - self:T( 'self.CargoGroupName = ' .. self.CargoGroupName ) - - CargoGroup:TaskRouteToZone( ZONE:New( TargetZoneName ), true ) - - self:StatusUnLoaded() - - return self -end - - -CARGO_PACKAGE = { - ClassName = "CARGO_PACKAGE" -} - - -function CARGO_PACKAGE:New( CargoType, CargoName, CargoWeight, CargoClient ) local self = BASE:Inherit( self, CARGO:New( CargoType, CargoName, CargoWeight ) ) - self:F( { CargoType, CargoName, CargoWeight, CargoClient } ) - - self.CargoClient = CargoClient - - CARGOS[self.CargoName] = self - - return self - -end - - -function CARGO_PACKAGE:Spawn( Client ) - self:F( { self, Client } ) - - -- this needs to be checked thoroughly - - local CargoClientGroup = self.CargoClient:GetDCSGroup() - if not CargoClientGroup then - if not self.CargoClientSpawn then - self.CargoClientSpawn = SPAWN:New( self.CargoClient:GetClientGroupName() ):Limit( 1, 1 ) - end - self.CargoClientSpawn:ReSpawn( 1 ) - end - - local SpawnCargo = true - - if self:IsStatusNone() then - - elseif self:IsStatusLoading() or self:IsStatusLoaded() then - - local CargoClientLoaded = self:IsLoadedInClient() - if CargoClientLoaded and CargoClientLoaded:GetDCSGroup() then - SpawnCargo = false - end - - elseif self:IsStatusUnLoaded() then - - SpawnCargo = false - - else - - end - - if SpawnCargo then - self:StatusLoaded( self.CargoClient ) - end - - return self -end - - -function CARGO_PACKAGE:IsNear( Client, LandingZone ) - self:F() - - local Near = false - - if self.CargoClient and self.CargoClient:GetDCSGroup() then - self:T( self.CargoClient.ClientName ) - self:T( 'Client Exists.' ) - - if routines.IsUnitInRadius( self.CargoClient:GetClientGroupDCSUnit(), Client:GetPositionVec3(), 150 ) then - Near = true - end - end - - return Near - -end - - -function CARGO_PACKAGE:OnBoard( Client, LandingZone, OnBoardSide ) - self:F() - - local Valid = true - - local ClientUnit = Client:GetClientGroupDCSUnit() - - local CarrierPos = ClientUnit:getPoint() - local CarrierPosMove = ClientUnit:getPoint() - local CarrierPosOnBoard = ClientUnit:getPoint() - local CarrierPosMoveAway = ClientUnit:getPoint() - - local CargoHostGroup = self.CargoClient:GetDCSGroup() - local CargoHostName = self.CargoClient:GetDCSGroup():getName() - - local CargoHostUnits = CargoHostGroup:getUnits() - local CargoPos = CargoHostUnits[1]:getPoint() - - local Points = {} - - self:T( 'CargoPos x = ' .. CargoPos.x .. " z = " .. CargoPos.z ) - self:T( 'CarrierPosMove x = ' .. CarrierPosMove.x .. " z = " .. CarrierPosMove.z ) - - Points[#Points+1] = routines.ground.buildWP( CargoPos, "Cone", 10 ) - - self:T( 'Points[1] x = ' .. Points[1].x .. " y = " .. Points[1].y ) - - if OnBoardSide == nil then - OnBoardSide = CLIENT.ONBOARDSIDE.NONE - end - - if OnBoardSide == CLIENT.ONBOARDSIDE.LEFT then - - self:T( "TransportCargoOnBoard: Onboarding LEFT" ) - CarrierPosMove.z = CarrierPosMove.z - 25 - CarrierPosOnBoard.z = CarrierPosOnBoard.z - 5 - CarrierPosMoveAway.z = CarrierPosMoveAway.z - 20 - Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosMoveAway, "Cone", 10 ) - - elseif OnBoardSide == CLIENT.ONBOARDSIDE.RIGHT then - - self:T( "TransportCargoOnBoard: Onboarding RIGHT" ) - CarrierPosMove.z = CarrierPosMove.z + 25 - CarrierPosOnBoard.z = CarrierPosOnBoard.z + 5 - CarrierPosMoveAway.z = CarrierPosMoveAway.z + 20 - Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosMoveAway, "Cone", 10 ) - - elseif OnBoardSide == CLIENT.ONBOARDSIDE.BACK then - - self:T( "TransportCargoOnBoard: Onboarding BACK" ) - CarrierPosMove.x = CarrierPosMove.x - 25 - CarrierPosOnBoard.x = CarrierPosOnBoard.x - 5 - CarrierPosMoveAway.x = CarrierPosMoveAway.x - 20 - Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosMoveAway, "Cone", 10 ) - - elseif OnBoardSide == CLIENT.ONBOARDSIDE.FRONT then - - self:T( "TransportCargoOnBoard: Onboarding FRONT" ) - CarrierPosMove.x = CarrierPosMove.x + 25 - CarrierPosOnBoard.x = CarrierPosOnBoard.x + 5 - CarrierPosMoveAway.x = CarrierPosMoveAway.x + 20 - Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosMoveAway, "Cone", 10 ) - - elseif OnBoardSide == CLIENT.ONBOARDSIDE.NONE then - - self:T( "TransportCargoOnBoard: Onboarding FRONT" ) - CarrierPosMove.x = CarrierPosMove.x + 25 - CarrierPosOnBoard.x = CarrierPosOnBoard.x + 5 - CarrierPosMoveAway.x = CarrierPosMoveAway.x + 20 - Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosMoveAway, "Cone", 10 ) - - end - self:T( "Routing " .. CargoHostName ) - - --routines.scheduleFunction( routines.goRoute, { CargoHostName, Points}, timer.getTime() + 4 ) - SCHEDULER:New( self, routines.goRoute, { CargoHostName, Points }, 4 ) - - return Valid - -end - - -function CARGO_PACKAGE:OnBoarded( Client, LandingZone ) - self:F() - - local OnBoarded = false - - if self.CargoClient and self.CargoClient:GetDCSGroup() then - if routines.IsUnitInRadius( self.CargoClient:GetClientGroupDCSUnit(), self.CargoClient:GetPositionVec3(), 10 ) then - - -- Switch Cargo from self.CargoClient to Client ... Each cargo can have only one client. So assigning the new client for the cargo is enough. - self:StatusLoaded( Client ) - - -- All done, onboarded the Cargo to the new Client. - OnBoarded = true - end - end - - return OnBoarded -end - - -function CARGO_PACKAGE:UnLoad( Client, TargetZoneName ) - self:F() - - self:T( 'self.CargoName = ' .. self.CargoName ) - --self:T( 'self.CargoHostName = ' .. self.CargoHostName ) - - --self.CargoSpawn:FromCarrier( Client:GetDCSGroup(), TargetZoneName, self.CargoHostName ) - self:StatusUnLoaded() - - return Cargo -end - - -CARGO_SLINGLOAD = { - ClassName = "CARGO_SLINGLOAD" -} - - -function CARGO_SLINGLOAD:New( CargoType, CargoName, CargoWeight, CargoZone, CargoHostName, CargoCountryID ) - local self = BASE:Inherit( self, CARGO:New( CargoType, CargoName, CargoWeight ) ) - self:F( { CargoType, CargoName, CargoWeight, CargoZone, CargoHostName, CargoCountryID } ) - - self.CargoHostName = CargoHostName - - -- Cargo will be initialized around the CargoZone position. - self.CargoZone = CargoZone - - self.CargoCount = 0 - self.CargoStaticName = string.format( "%s#%03d", self.CargoName, self.CargoCount ) - - -- The country ID needs to be correctly set. - self.CargoCountryID = CargoCountryID - - CARGOS[self.CargoName] = self - - return self - -end - - -function CARGO_SLINGLOAD:IsLandingRequired() - self:F() - return false -end - - -function CARGO_SLINGLOAD:IsSlingLoad() - self:F() - return true -end - - -function CARGO_SLINGLOAD:Spawn( Client ) - self:F( { self, Client } ) - - local Zone = trigger.misc.getZone( self.CargoZone ) - - local ZonePos = {} - ZonePos.x = Zone.point.x + math.random( Zone.radius / 2 * -1, Zone.radius / 2 ) - ZonePos.y = Zone.point.z + math.random( Zone.radius / 2 * -1, Zone.radius / 2 ) - - self:T( "Cargo Location = " .. ZonePos.x .. ", " .. ZonePos.y ) - - --[[ - -- This does not work in 1.5.2. - CargoStatic = StaticObject.getByName( self.CargoName ) - if CargoStatic then - CargoStatic:destroy() - end - --]] - - CargoStatic = StaticObject.getByName( self.CargoStaticName ) - - if CargoStatic and CargoStatic:isExist() then - CargoStatic:destroy() - end - - -- I need to make every time a new cargo due to bugs in 1.5.2. - - self.CargoCount = self.CargoCount + 1 - self.CargoStaticName = string.format( "%s#%03d", self.CargoName, self.CargoCount ) - - local CargoTemplate = { - ["category"] = "Cargo", - ["shape_name"] = "ab-212_cargo", - ["type"] = "Cargo1", - ["x"] = ZonePos.x, - ["y"] = ZonePos.y, - ["mass"] = self.CargoWeight, - ["name"] = self.CargoStaticName, - ["canCargo"] = true, - ["heading"] = 0, - } - - coalition.addStaticObject( self.CargoCountryID, CargoTemplate ) - --- end - - return self -end - - -function CARGO_SLINGLOAD:IsNear( Client, LandingZone ) - self:F() - - local Near = false - - return Near -end - - -function CARGO_SLINGLOAD:IsInLandingZone( Client, LandingZone ) - self:F() - - local Near = false - - local CargoStaticUnit = StaticObject.getByName( self.CargoName ) - if CargoStaticUnit then - if routines.IsStaticInZones( CargoStaticUnit, LandingZone ) then - Near = true - end - end - - return Near -end - - -function CARGO_SLINGLOAD:OnBoard( Client, LandingZone, OnBoardSide ) - self:F() - - local Valid = true - - - return Valid -end - - -function CARGO_SLINGLOAD:OnBoarded( Client, LandingZone ) - self:F() - - local OnBoarded = false - - local CargoStaticUnit = StaticObject.getByName( self.CargoName ) - if CargoStaticUnit then - if not routines.IsStaticInZones( CargoStaticUnit, LandingZone ) then - OnBoarded = true - end - end - - return OnBoarded -end - - -function CARGO_SLINGLOAD:UnLoad( Client, TargetZoneName ) - self:F() - - self:T( 'self.CargoName = ' .. self.CargoName ) - self:T( 'self.CargoGroupName = ' .. self.CargoGroupName ) - - self:StatusUnLoaded() - - return Cargo -end ---- This module contains the MESSAGE class. --- --- 1) @{Message#MESSAGE} class, extends @{Base#BASE} --- ================================================= --- Message System to display Messages to Clients, Coalitions or All. --- Messages are shown on the display panel for an amount of seconds, and will then disappear. --- Messages can contain a category which is indicating the category of the message. --- --- 1.1) MESSAGE construction methods --- --------------------------------- --- Messages are created with @{Message#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. --- --- 1.2) Send messages with MESSAGE To methods --- ------------------------------------------ --- Messages are sent to: --- --- * Clients with @{Message#MESSAGE.ToClient}. --- * Coalitions with @{Message#MESSAGE.ToCoalition}. --- * All Players with @{Message#MESSAGE.ToAll}. --- --- @module Message --- @author FlightControl - ---- The MESSAGE class --- @type MESSAGE --- @extends Base#BASE -MESSAGE = { - ClassName = "MESSAGE", - MessageCategory = 0, - MessageID = 0, -} - - ---- Creates a new MESSAGE object. Note that these MESSAGE objects are not yet displayed on the display panel. You must use the functions @{ToClient} or @{ToCoalition} or @{ToAll} to send these Messages to the respective recipients. --- @param self --- @param #string MessageText is the text of the Message. --- @param #number MessageDuration is a number in seconds of how long the MESSAGE should be shown on the display panel. --- @param #string MessageCategory (optional) is a string expressing the "category" of the Message. The category will be shown as the first text in the message followed by a ": ". --- @return #MESSAGE --- @usage --- -- Create a series of new Messages. --- -- MessageAll is meant to be sent to all players, for 25 seconds, and is classified as "Score". --- -- MessageRED is meant to be sent to the RED players only, for 10 seconds, and is classified as "End of Mission", with ID "Win". --- -- MessageClient1 is meant to be sent to a Client, for 25 seconds, and is classified as "Score", with ID "Score". --- -- MessageClient1 is meant to be sent to a Client, for 25 seconds, and is classified as "Score", with ID "Score". --- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", 25, "End of Mission" ) --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", 25, "Penalty" ) --- MessageClient1 = MESSAGE:New( "Congratulations, you've just hit a target", 25, "Score" ) --- MessageClient2 = MESSAGE:New( "Congratulations, you've just killed a target", 25, "Score") -function MESSAGE:New( MessageText, MessageDuration, MessageCategory ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( { MessageText, MessageDuration, MessageCategory } ) - - -- When no MessageCategory is given, we don't show it as a title... - if MessageCategory and MessageCategory ~= "" then - self.MessageCategory = MessageCategory .. ": " - else - self.MessageCategory = "" - end - - self.MessageDuration = MessageDuration - self.MessageTime = timer.getTime() - self.MessageText = MessageText - - self.MessageSent = false - self.MessageGroup = false - self.MessageCoalition = false - - 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 Client#CLIENT Client is the Group of the Client. --- @return #MESSAGE --- @usage --- -- Send the 2 messages created with the @{New} method to the Client Group. --- -- Note that the Message of MessageClient2 is overwriting the Message of MessageClient1. --- ClientGroup = Group.getByName( "ClientGroup" ) --- --- MessageClient1 = MESSAGE:New( "Congratulations, you've just hit a target", "Score", 25, "Score" ):ToClient( ClientGroup ) --- MessageClient2 = MESSAGE:New( "Congratulations, you've just killed a target", "Score", 25, "Score" ):ToClient( ClientGroup ) --- or --- MESSAGE:New( "Congratulations, you've just hit a target", "Score", 25, "Score" ):ToClient( ClientGroup ) --- MESSAGE:New( "Congratulations, you've just killed a target", "Score", 25, "Score" ):ToClient( ClientGroup ) --- or --- MessageClient1 = MESSAGE:New( "Congratulations, you've just hit a target", "Score", 25, "Score" ) --- MessageClient2 = MESSAGE:New( "Congratulations, you've just killed a target", "Score", 25, "Score" ) --- MessageClient1:ToClient( ClientGroup ) --- MessageClient2:ToClient( ClientGroup ) -function MESSAGE:ToClient( Client ) - self:F( Client ) - - if Client and Client:GetClientGroupID() 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 ) - end - - return self -end - ---- Sends a MESSAGE to the Blue coalition. --- @param #MESSAGE self --- @return #MESSAGE --- @usage --- -- Send a message created with the @{New} method to the BLUE coalition. --- MessageBLUE = MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToBlue() --- or --- MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToBlue() --- or --- MessageBLUE = MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ) --- MessageBLUE:ToBlue() -function MESSAGE:ToBlue() - self:F() - - self:ToCoalition( coalition.side.BLUE ) - - return self -end - ---- Sends a MESSAGE to the Red Coalition. --- @param #MESSAGE self --- @return #MESSAGE --- @usage --- -- Send a message created with the @{New} method to the RED coalition. --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToRed() --- or --- MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToRed() --- or --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ) --- MessageRED:ToRed() -function MESSAGE:ToRed( ) - self:F() - - self:ToCoalition( coalition.side.RED ) - - return self -end - ---- Sends a MESSAGE to a Coalition. --- @param #MESSAGE self --- @param CoalitionSide needs to be filled out by the defined structure of the standard scripting engine @{coalition.side}. --- @return #MESSAGE --- @usage --- -- Send a message created with the @{New} method to the RED coalition. --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToCoalition( coalition.side.RED ) --- or --- MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToCoalition( coalition.side.RED ) --- or --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ) --- MessageRED:ToCoalition( coalition.side.RED ) -function MESSAGE:ToCoalition( CoalitionSide ) - self:F( CoalitionSide ) - - if CoalitionSide then - self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration ) - trigger.action.outTextForCoalition( CoalitionSide, self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration ) - end - - return self -end - ---- Sends a MESSAGE to all players. --- @param #MESSAGE self --- @return #MESSAGE --- @usage --- -- Send a message created to all players. --- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ):ToAll() --- or --- MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ):ToAll() --- or --- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ) --- MessageAll:ToAll() -function MESSAGE:ToAll() - self:F() - - self:ToCoalition( coalition.side.RED ) - self:ToCoalition( coalition.side.BLUE ) - - return self -end - - - ------ The MESSAGEQUEUE class ----- @type MESSAGEQUEUE ---MESSAGEQUEUE = { --- ClientGroups = {}, --- CoalitionSides = {} ---} --- ---function MESSAGEQUEUE:New( RefreshInterval ) --- local self = BASE:Inherit( self, BASE:New() ) --- self:F( { RefreshInterval } ) --- --- self.RefreshInterval = RefreshInterval --- --- --self.DisplayFunction = routines.scheduleFunction( self._DisplayMessages, { self }, 0, RefreshInterval ) --- self.DisplayFunction = SCHEDULER:New( self, self._DisplayMessages, {}, 0, RefreshInterval ) --- --- return self ---end --- ------ This function is called automatically by the MESSAGEQUEUE scheduler. ---function MESSAGEQUEUE:_DisplayMessages() --- --- -- First we display all messages that a coalition needs to receive... Also those who are not in a client (CA module clients...). --- for CoalitionSideID, CoalitionSideData in pairs( self.CoalitionSides ) do --- for MessageID, MessageData in pairs( CoalitionSideData.Messages ) do --- if MessageData.MessageSent == false then --- --trigger.action.outTextForCoalition( CoalitionSideID, MessageData.MessageCategory .. '\n' .. MessageData.MessageText:gsub("\n$",""):gsub("\n$",""), MessageData.MessageDuration ) --- MessageData.MessageSent = true --- end --- local MessageTimeLeft = ( MessageData.MessageTime + MessageData.MessageDuration ) - timer.getTime() --- if MessageTimeLeft <= 0 then --- MessageData = nil --- end --- end --- end --- --- -- Then we send the messages for each individual client, but also to be included are those Coalition messages for the Clients who belong to a coalition. --- -- Because the Client messages will overwrite the Coalition messages (for that Client). --- for ClientGroupName, ClientGroupData in pairs( self.ClientGroups ) do --- for MessageID, MessageData in pairs( ClientGroupData.Messages ) do --- if MessageData.MessageGroup == false then --- trigger.action.outTextForGroup( Group.getByName(ClientGroupName):getID(), MessageData.MessageCategory .. '\n' .. MessageData.MessageText:gsub("\n$",""):gsub("\n$",""), MessageData.MessageDuration ) --- MessageData.MessageGroup = true --- end --- local MessageTimeLeft = ( MessageData.MessageTime + MessageData.MessageDuration ) - timer.getTime() --- if MessageTimeLeft <= 0 then --- MessageData = nil --- end --- end --- --- -- Now check if the Client also has messages that belong to the Coalition of the Client... --- for CoalitionSideID, CoalitionSideData in pairs( self.CoalitionSides ) do --- for MessageID, MessageData in pairs( CoalitionSideData.Messages ) do --- local CoalitionGroup = Group.getByName( ClientGroupName ) --- if CoalitionGroup and CoalitionGroup:getCoalition() == CoalitionSideID then --- if MessageData.MessageCoalition == false then --- trigger.action.outTextForGroup( Group.getByName(ClientGroupName):getID(), MessageData.MessageCategory .. '\n' .. MessageData.MessageText:gsub("\n$",""):gsub("\n$",""), MessageData.MessageDuration ) --- MessageData.MessageCoalition = true --- end --- end --- local MessageTimeLeft = ( MessageData.MessageTime + MessageData.MessageDuration ) - timer.getTime() --- if MessageTimeLeft <= 0 then --- MessageData = nil --- end --- end --- end --- end --- --- return true ---end --- ------ The _MessageQueue object is created when the MESSAGE class module is loaded. -----_MessageQueue = MESSAGEQUEUE:New( 0.5 ) --- ---- Stages within a @{TASK} within a @{MISSION}. All of the STAGE functionality is considered internally administered and not to be used by any Mission designer. --- @module STAGE --- @author Flightcontrol - - - - - - - ---- The STAGE class --- @type -STAGE = { - ClassName = "STAGE", - MSG = { ID = "None", TIME = 10 }, - FREQUENCY = { NONE = 0, ONCE = 1, REPEAT = -1 }, - - Name = "NoStage", - StageType = '', - WaitTime = 1, - Frequency = 1, - MessageCount = 0, - MessageInterval = 15, - MessageShown = {}, - MessageShow = false, - MessageFlash = false -} - - -function STAGE:New() - local self = BASE:Inherit( self, BASE:New() ) - self:F() - return self -end - -function STAGE:Execute( Mission, Client, Task ) - - local Valid = true - - return Valid -end - -function STAGE:Executing( Mission, Client, Task ) - -end - -function STAGE:Validate( Mission, Client, Task ) - local Valid = true - - return Valid -end - - -STAGEBRIEF = { - ClassName = "BRIEF", - MSG = { ID = "Brief", TIME = 1 }, - Name = "Brief", - StageBriefingTime = 0, - StageBriefingDuration = 1 -} - -function STAGEBRIEF:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - return self -end - ---- Execute --- @param #STAGEBRIEF self --- @param Mission#MISSION Mission --- @param Client#CLIENT Client --- @param Task#TASK Task --- @return #boolean -function STAGEBRIEF:Execute( Mission, Client, Task ) - local Valid = BASE:Inherited(self):Execute( Mission, Client, Task ) - self:F() - Client:ShowMissionBriefing( Mission.MissionBriefing ) - self.StageBriefingTime = timer.getTime() - return Valid -end - -function STAGEBRIEF:Validate( Mission, Client, Task ) - local Valid = STAGE:Validate( Mission, Client, Task ) - self:T() - - if timer.getTime() - self.StageBriefingTime <= self.StageBriefingDuration then - return 0 - else - self.StageBriefingTime = timer.getTime() - return 1 - end - -end - - -STAGESTART = { - ClassName = "START", - MSG = { ID = "Start", TIME = 1 }, - Name = "Start", - StageStartTime = 0, - StageStartDuration = 1 -} - -function STAGESTART:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - return self -end - -function STAGESTART:Execute( Mission, Client, Task ) - self:F() - local Valid = BASE:Inherited(self):Execute( Mission, Client, Task ) - if Task.TaskBriefing then - Client:Message( Task.TaskBriefing, 30, "Command" ) - else - Client:Message( 'Task ' .. Task.TaskNumber .. '.', 30, "Command" ) - end - self.StageStartTime = timer.getTime() - return Valid -end - -function STAGESTART:Validate( Mission, Client, Task ) - self:F() - local Valid = STAGE:Validate( Mission, Client, Task ) - - if timer.getTime() - self.StageStartTime <= self.StageStartDuration then - return 0 - else - self.StageStartTime = timer.getTime() - return 1 - end - - return 1 - -end - -STAGE_CARGO_LOAD = { - ClassName = "STAGE_CARGO_LOAD" -} - -function STAGE_CARGO_LOAD:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - return self -end - -function STAGE_CARGO_LOAD:Execute( Mission, Client, Task ) - self:F() - local Valid = BASE:Inherited(self):Execute( Mission, Client, Task ) - - for LoadCargoID, LoadCargo in pairs( Task.Cargos.LoadCargos ) do - LoadCargo:Load( Client ) - end - - if Mission.MissionReportFlash and Client:IsTransport() then - Client:ShowCargo() - end - - return Valid -end - -function STAGE_CARGO_LOAD:Validate( Mission, Client, Task ) - self:F() - local Valid = STAGE:Validate( Mission, Client, Task ) - - return 1 -end - - -STAGE_CARGO_INIT = { - ClassName = "STAGE_CARGO_INIT" -} - -function STAGE_CARGO_INIT:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - return self -end - -function STAGE_CARGO_INIT:Execute( Mission, Client, Task ) - self:F() - local Valid = BASE:Inherited(self):Execute( Mission, Client, Task ) - - for InitLandingZoneID, InitLandingZone in pairs( Task.LandingZones.LandingZones ) do - self:T( InitLandingZone ) - InitLandingZone:Spawn() - end - - - self:T( Task.Cargos.InitCargos ) - for InitCargoID, InitCargoData in pairs( Task.Cargos.InitCargos ) do - self:T( { InitCargoData } ) - InitCargoData:Spawn( Client ) - end - - return Valid -end - - -function STAGE_CARGO_INIT:Validate( Mission, Client, Task ) - self:F() - local Valid = STAGE:Validate( Mission, Client, Task ) - - return 1 -end - - - -STAGEROUTE = { - ClassName = "STAGEROUTE", - MSG = { ID = "Route", TIME = 5 }, - Frequency = STAGE.FREQUENCY.REPEAT, - Name = "Route" -} - -function STAGEROUTE:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - self.MessageSwitch = true - return self -end - - ---- Execute the routing. --- @param #STAGEROUTE self --- @param Mission#MISSION Mission --- @param Client#CLIENT Client --- @param Task#TASK Task -function STAGEROUTE:Execute( Mission, Client, Task ) - self:F() - local Valid = BASE:Inherited(self):Execute( Mission, Client, Task ) - - local RouteMessage = "Fly to: " - self:T( Task.LandingZones ) - for LandingZoneID, LandingZoneName in pairs( Task.LandingZones.LandingZoneNames ) do - RouteMessage = RouteMessage .. "\n " .. LandingZoneName .. ' at ' .. routines.getBRStringZone( { zone = LandingZoneName, ref = Client:GetClientGroupDCSUnit():getPoint(), true, true } ) .. ' km.' - end - - if Client:IsMultiSeated() then - Client:Message( RouteMessage, self.MSG.TIME, "Co-Pilot", 20, "Route" ) - else - Client:Message( RouteMessage, self.MSG.TIME, "Command", 20, "Route" ) - end - - - if Mission.MissionReportFlash and Client:IsTransport() then - Client:ShowCargo() - end - - return Valid -end - -function STAGEROUTE:Validate( Mission, Client, Task ) - self:F() - local Valid = STAGE:Validate( Mission, Client, Task ) - - -- check if the Client is in the landing zone - self:T( Task.LandingZones.LandingZoneNames ) - Task.CurrentLandingZoneName = routines.IsUnitNearZonesRadius( Client:GetClientGroupDCSUnit(), Task.LandingZones.LandingZoneNames, 500 ) - - if Task.CurrentLandingZoneName then - - Task.CurrentLandingZone = Task.LandingZones.LandingZones[Task.CurrentLandingZoneName].CargoZone - Task.CurrentCargoZone = Task.LandingZones.LandingZones[Task.CurrentLandingZoneName] - - if Task.CurrentCargoZone then - if not Task.Signalled then - Task.Signalled = Task.CurrentCargoZone:Signal() - end - end - - self:T( 1 ) - return 1 - end - - self:T( 0 ) - return 0 -end - - - -STAGELANDING = { - ClassName = "STAGELANDING", - MSG = { ID = "Landing", TIME = 10 }, - Name = "Landing", - Signalled = false -} - -function STAGELANDING:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - return self -end - ---- Execute the landing coordination. --- @param #STAGELANDING self --- @param Mission#MISSION Mission --- @param Client#CLIENT Client --- @param Task#TASK Task -function STAGELANDING:Execute( Mission, Client, Task ) - self:F() - - if Client:IsMultiSeated() then - Client:Message( "We have arrived at the landing zone.", self.MSG.TIME, "Co-Pilot" ) - else - Client:Message( "You have arrived at the landing zone.", self.MSG.TIME, "Command" ) - end - - Task.HostUnit = Task.CurrentCargoZone:GetHostUnit() - - self:T( { Task.HostUnit } ) - - if Task.HostUnit then - - Task.HostUnitName = Task.HostUnit:GetPrefix() - Task.HostUnitTypeName = Task.HostUnit:GetTypeName() - - local HostMessage = "" - Task.CargoNames = "" - - local IsFirst = true - - for CargoID, Cargo in pairs( CARGOS ) do - if Cargo.CargoType == Task.CargoType then - - if Cargo:IsLandingRequired() then - self:T( "Task for cargo " .. Cargo.CargoType .. " requires landing.") - Task.IsLandingRequired = true - end - - if Cargo:IsSlingLoad() then - self:T( "Task for cargo " .. Cargo.CargoType .. " is a slingload.") - Task.IsSlingLoad = true - end - - if IsFirst then - IsFirst = false - Task.CargoNames = Task.CargoNames .. Cargo.CargoName .. "( " .. Cargo.CargoWeight .. " )" - else - Task.CargoNames = Task.CargoNames .. "; " .. Cargo.CargoName .. "( " .. Cargo.CargoWeight .. " )" - end - end - end - - if Task.IsLandingRequired then - HostMessage = "Land the helicopter to " .. Task.TEXT[1] .. " " .. Task.CargoNames .. "." - else - HostMessage = "Use the Radio menu and F6 to find the cargo, then fly or land near the cargo and " .. Task.TEXT[1] .. " " .. Task.CargoNames .. "." - end - - local Host = "Command" - if Task.HostUnitName then - Host = Task.HostUnitName .. " (" .. Task.HostUnitTypeName .. ")" - else - if Client:IsMultiSeated() then - Host = "Co-Pilot" - end - end - - Client:Message( HostMessage, self.MSG.TIME, Host ) - - end -end - -function STAGELANDING:Validate( Mission, Client, Task ) - self:F() - - Task.CurrentLandingZoneName = routines.IsUnitNearZonesRadius( Client:GetClientGroupDCSUnit(), Task.LandingZones.LandingZoneNames, 500 ) - if Task.CurrentLandingZoneName then - - -- Client is in de landing zone. - self:T( Task.CurrentLandingZoneName ) - - Task.CurrentLandingZone = Task.LandingZones.LandingZones[Task.CurrentLandingZoneName].CargoZone - Task.CurrentCargoZone = Task.LandingZones.LandingZones[Task.CurrentLandingZoneName] - - if Task.CurrentCargoZone then - if not Task.Signalled then - Task.Signalled = Task.CurrentCargoZone:Signal() - end - end - else - if Task.CurrentLandingZone then - Task.CurrentLandingZone = nil - end - if Task.CurrentCargoZone then - Task.CurrentCargoZone = nil - end - Task.Signalled = false - Task:RemoveCargoMenus( Client ) - self:T( -1 ) - return -1 - end - - - local DCSUnitVelocityVec3 = Client:GetClientGroupDCSUnit():getVelocity() - local DCSUnitVelocity = ( DCSUnitVelocityVec3.x ^2 + DCSUnitVelocityVec3.y ^2 + DCSUnitVelocityVec3.z ^2 ) ^ 0.5 - - local DCSUnitPointVec3 = Client:GetClientGroupDCSUnit():getPoint() - local LandHeight = land.getHeight( { x = DCSUnitPointVec3.x, y = DCSUnitPointVec3.z } ) - local DCSUnitHeight = DCSUnitPointVec3.y - LandHeight - - self:T( { Task.IsLandingRequired, Client:GetClientGroupDCSUnit():inAir() } ) - if Task.IsLandingRequired and not Client:GetClientGroupDCSUnit():inAir() then - self:T( 1 ) - Task.IsInAirTestRequired = true - return 1 - end - - self:T( { DCSUnitVelocity, DCSUnitHeight, LandHeight, Task.CurrentCargoZone.SignalHeight } ) - if Task.IsLandingRequired and DCSUnitVelocity <= 0.05 and DCSUnitHeight <= Task.CurrentCargoZone.SignalHeight then - self:T( 1 ) - Task.IsInAirTestRequired = false - return 1 - end - - self:T( 0 ) - return 0 -end - -STAGELANDED = { - ClassName = "STAGELANDED", - MSG = { ID = "Land", TIME = 10 }, - Name = "Landed", - MenusAdded = false -} - -function STAGELANDED:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - return self -end - -function STAGELANDED:Execute( Mission, Client, Task ) - self:F() - - if Task.IsLandingRequired then - - local Host = "Command" - if Task.HostUnitName then - Host = Task.HostUnitName .. " (" .. Task.HostUnitTypeName .. ")" - else - if Client:IsMultiSeated() then - Host = "Co-Pilot" - end - end - - Client:Message( 'You have landed within the landing zone. Use the radio menu (F10) to ' .. Task.TEXT[1] .. ' the ' .. Task.CargoType .. '.', - self.MSG.TIME, Host ) - - if not self.MenusAdded then - Task.Cargo = nil - Task:RemoveCargoMenus( Client ) - Task:AddCargoMenus( Client, CARGOS, 250 ) - end - end -end - - - -function STAGELANDED:Validate( Mission, Client, Task ) - self:F() - - if not routines.IsUnitNearZonesRadius( Client:GetClientGroupDCSUnit(), Task.CurrentLandingZoneName, 500 ) then - self:T( "Client is not anymore in the landing zone, go back to stage Route, and remove cargo menus." ) - Task.Signalled = false - Task:RemoveCargoMenus( Client ) - self:T( -2 ) - return -2 - end - - local DCSUnitVelocityVec3 = Client:GetClientGroupDCSUnit():getVelocity() - local DCSUnitVelocity = ( DCSUnitVelocityVec3.x ^2 + DCSUnitVelocityVec3.y ^2 + DCSUnitVelocityVec3.z ^2 ) ^ 0.5 - - local DCSUnitPointVec3 = Client:GetClientGroupDCSUnit():getPoint() - local LandHeight = land.getHeight( { x = DCSUnitPointVec3.x, y = DCSUnitPointVec3.z } ) - local DCSUnitHeight = DCSUnitPointVec3.y - LandHeight - - self:T( { Task.IsLandingRequired, Client:GetClientGroupDCSUnit():inAir() } ) - if Task.IsLandingRequired and Task.IsInAirTestRequired == true and Client:GetClientGroupDCSUnit():inAir() then - self:T( "Client went back in the air. Go back to stage Landing." ) - self:T( -1 ) - return -1 - end - - self:T( { DCSUnitVelocity, DCSUnitHeight, LandHeight, Task.CurrentCargoZone.SignalHeight } ) - if Task.IsLandingRequired and Task.IsInAirTestRequired == false and DCSUnitVelocity >= 2 and DCSUnitHeight >= Task.CurrentCargoZone.SignalHeight then - self:T( "It seems the Client went back in the air and over the boundary limits. Go back to stage Landing." ) - self:T( -1 ) - return -1 - end - - -- Wait until cargo is selected from the menu. - if Task.IsLandingRequired then - if not Task.Cargo then - self:T( 0 ) - return 0 - end - end - - self:T( 1 ) - return 1 -end - -STAGEUNLOAD = { - ClassName = "STAGEUNLOAD", - MSG = { ID = "Unload", TIME = 10 }, - Name = "Unload" -} - -function STAGEUNLOAD:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - return self -end - ---- Coordinate UnLoading --- @param #STAGEUNLOAD self --- @param Mission#MISSION Mission --- @param Client#CLIENT Client --- @param Task#TASK Task -function STAGEUNLOAD:Execute( Mission, Client, Task ) - self:F() - - if Client:IsMultiSeated() then - Client:Message( 'The ' .. Task.CargoType .. ' are being ' .. Task.TEXT[2] .. ' within the landing zone. Wait until the helicopter is ' .. Task.TEXT[3] .. '.', - "Co-Pilot" ) - else - Client:Message( 'You are unloading the ' .. Task.CargoType .. ' ' .. Task.TEXT[2] .. ' within the landing zone. Wait until the helicopter is ' .. Task.TEXT[3] .. '.', - "Command" ) - end - Task:RemoveCargoMenus( Client ) -end - -function STAGEUNLOAD:Executing( Mission, Client, Task ) - self:F() - env.info( 'STAGEUNLOAD:Executing() Task.Cargo.CargoName = ' .. Task.Cargo.CargoName ) - - local TargetZoneName - - if Task.TargetZoneName then - TargetZoneName = Task.TargetZoneName - else - TargetZoneName = Task.CurrentLandingZoneName - end - - if Task.Cargo:UnLoad( Client, TargetZoneName ) then - Task.ExecuteStage = _TransportExecuteStage.SUCCESS - if Mission.MissionReportFlash then - Client:ShowCargo() - end - end -end - ---- Validate UnLoading --- @param #STAGEUNLOAD self --- @param Mission#MISSION Mission --- @param Client#CLIENT Client --- @param Task#TASK Task -function STAGEUNLOAD:Validate( Mission, Client, Task ) - self:F() - env.info( 'STAGEUNLOAD:Validate()' ) - - if routines.IsUnitNearZonesRadius( Client:GetClientGroupDCSUnit(), Task.CurrentLandingZoneName, 500 ) then - else - Task.ExecuteStage = _TransportExecuteStage.FAILED - Task:RemoveCargoMenus( Client ) - if Client:IsMultiSeated() then - Client:Message( 'The ' .. Task.CargoType .. " haven't been successfully " .. Task.TEXT[3] .. ' within the landing zone. Task and mission has failed.', - _TransportStageMsgTime.DONE, "Co-Pilot" ) - else - Client:Message( 'The ' .. Task.CargoType .. " haven't been successfully " .. Task.TEXT[3] .. ' within the landing zone. Task and mission has failed.', - _TransportStageMsgTime.DONE, "Command" ) - end - return 1 - end - - if not Client:GetClientGroupDCSUnit():inAir() then - else - Task.ExecuteStage = _TransportExecuteStage.FAILED - Task:RemoveCargoMenus( Client ) - if Client:IsMultiSeated() then - Client:Message( 'The ' .. Task.CargoType .. " haven't been successfully " .. Task.TEXT[3] .. ' within the landing zone. Task and mission has failed.', - _TransportStageMsgTime.DONE, "Co-Pilot" ) - else - Client:Message( 'The ' .. Task.CargoType .. " haven't been successfully " .. Task.TEXT[3] .. ' within the landing zone. Task and mission has failed.', - _TransportStageMsgTime.DONE, "Command" ) - end - return 1 - end - - if Task.ExecuteStage == _TransportExecuteStage.SUCCESS then - if Client:IsMultiSeated() then - Client:Message( 'The ' .. Task.CargoType .. ' have been sucessfully ' .. Task.TEXT[3] .. ' within the landing zone.', _TransportStageMsgTime.DONE, "Co-Pilot" ) - else - Client:Message( 'The ' .. Task.CargoType .. ' have been sucessfully ' .. Task.TEXT[3] .. ' within the landing zone.', _TransportStageMsgTime.DONE, "Command" ) - end - Task:RemoveCargoMenus( Client ) - Task.MissionTask:AddGoalCompletion( Task.MissionTask.GoalVerb, Task.CargoName, 1 ) -- We set the cargo as one more goal completed in the mission. - return 1 - end - - return 1 -end - -STAGELOAD = { - ClassName = "STAGELOAD", - MSG = { ID = "Load", TIME = 10 }, - Name = "Load" -} - -function STAGELOAD:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - return self -end - -function STAGELOAD:Execute( Mission, Client, Task ) - self:F() - - if not Task.IsSlingLoad then - - local Host = "Command" - if Task.HostUnitName then - Host = Task.HostUnitName .. " (" .. Task.HostUnitTypeName .. ")" - else - if Client:IsMultiSeated() then - Host = "Co-Pilot" - end - end - - Client:Message( 'The ' .. Task.CargoType .. ' are being ' .. Task.TEXT[2] .. ' within the landing zone. Wait until the helicopter is ' .. Task.TEXT[3] .. '.', - _TransportStageMsgTime.EXECUTING, Host ) - - -- Route the cargo to the Carrier - - Task.Cargo:OnBoard( Client, Task.CurrentCargoZone, Task.OnBoardSide ) - Task.ExecuteStage = _TransportExecuteStage.EXECUTING - else - Task.ExecuteStage = _TransportExecuteStage.EXECUTING - end -end - -function STAGELOAD:Executing( Mission, Client, Task ) - self:F() - - -- If the Cargo is ready to be loaded, load it into the Client. - - local Host = "Command" - if Task.HostUnitName then - Host = Task.HostUnitName .. " (" .. Task.HostUnitTypeName .. ")" - else - if Client:IsMultiSeated() then - Host = "Co-Pilot" - end - end - - if not Task.IsSlingLoad then - self:T( Task.Cargo.CargoName) - - if Task.Cargo:OnBoarded( Client, Task.CurrentCargoZone ) then - - -- Load the Cargo onto the Client - Task.Cargo:Load( Client ) - - -- Message to the pilot that cargo has been loaded. - Client:Message( "The cargo " .. Task.Cargo.CargoName .. " has been loaded in our helicopter.", - 20, Host ) - Task.ExecuteStage = _TransportExecuteStage.SUCCESS - - Client:ShowCargo() - end - else - Client:Message( "Hook the " .. Task.CargoNames .. " onto the helicopter " .. Task.TEXT[3] .. " within the landing zone.", - _TransportStageMsgTime.EXECUTING, Host ) - for CargoID, Cargo in pairs( CARGOS ) do - self:T( "Cargo.CargoName = " .. Cargo.CargoName ) - - if Cargo:IsSlingLoad() then - local CargoStatic = StaticObject.getByName( Cargo.CargoStaticName ) - if CargoStatic then - self:T( "Cargo is found in the DCS simulator.") - local CargoStaticPosition = CargoStatic:getPosition().p - self:T( "Cargo Position x = " .. CargoStaticPosition.x .. ", y = " .. CargoStaticPosition.y .. ", z = " .. CargoStaticPosition.z ) - local CargoStaticHeight = routines.GetUnitHeight( CargoStatic ) - if CargoStaticHeight > 5 then - self:T( "Cargo is airborne.") - Cargo:StatusLoaded() - Task.Cargo = Cargo - Client:Message( 'The Cargo has been successfully hooked onto the helicopter and is now being sling loaded. Fly outside the landing zone.', - self.MSG.TIME, Host ) - Task.ExecuteStage = _TransportExecuteStage.SUCCESS - break - end - else - self:T( "Cargo not found in the DCS simulator." ) - end - end - end - end - -end - -function STAGELOAD:Validate( Mission, Client, Task ) - self:F() - - self:T( "Task.CurrentLandingZoneName = " .. Task.CurrentLandingZoneName ) - - local Host = "Command" - if Task.HostUnitName then - Host = Task.HostUnitName .. " (" .. Task.HostUnitTypeName .. ")" - else - if Client:IsMultiSeated() then - Host = "Co-Pilot" - end - end - - if not Task.IsSlingLoad then - if not routines.IsUnitNearZonesRadius( Client:GetClientGroupDCSUnit(), Task.CurrentLandingZoneName, 500 ) then - Task:RemoveCargoMenus( Client ) - Task.ExecuteStage = _TransportExecuteStage.FAILED - Task.CargoName = nil - Client:Message( "The " .. Task.CargoType .. " loading has been aborted. You flew outside the pick-up zone while loading. ", - self.MSG.TIME, Host ) - self:T( -1 ) - return -1 - end - - local DCSUnitVelocityVec3 = Client:GetClientGroupDCSUnit():getVelocity() - local DCSUnitVelocity = ( DCSUnitVelocityVec3.x ^2 + DCSUnitVelocityVec3.y ^2 + DCSUnitVelocityVec3.z ^2 ) ^ 0.5 - - local DCSUnitPointVec3 = Client:GetClientGroupDCSUnit():getPoint() - local LandHeight = land.getHeight( { x = DCSUnitPointVec3.x, y = DCSUnitPointVec3.z } ) - local DCSUnitHeight = DCSUnitPointVec3.y - LandHeight - - self:T( { Task.IsLandingRequired, Client:GetClientGroupDCSUnit():inAir() } ) - if Task.IsLandingRequired and Task.IsInAirTestRequired == true and Client:GetClientGroupDCSUnit():inAir() then - Task:RemoveCargoMenus( Client ) - Task.ExecuteStage = _TransportExecuteStage.FAILED - Task.CargoName = nil - Client:Message( "The " .. Task.CargoType .. " loading has been aborted. Re-start the " .. Task.TEXT[3] .. " process. Don't fly outside the pick-up zone.", - self.MSG.TIME, Host ) - self:T( -1 ) - return -1 - end - - self:T( { DCSUnitVelocity, DCSUnitHeight, LandHeight, Task.CurrentCargoZone.SignalHeight } ) - if Task.IsLandingRequired and Task.IsInAirTestRequired == false and DCSUnitVelocity >= 2 and DCSUnitHeight >= Task.CurrentCargoZone.SignalHeight then - Task:RemoveCargoMenus( Client ) - Task.ExecuteStage = _TransportExecuteStage.FAILED - Task.CargoName = nil - Client:Message( "The " .. Task.CargoType .. " loading has been aborted. Re-start the " .. Task.TEXT[3] .. " process. Don't fly outside the pick-up zone.", - self.MSG.TIME, Host ) - self:T( -1 ) - return -1 - end - - if Task.ExecuteStage == _TransportExecuteStage.SUCCESS then - Task:RemoveCargoMenus( Client ) - Client:Message( "Good Job. The " .. Task.CargoType .. " has been sucessfully " .. Task.TEXT[3] .. " within the landing zone.", - self.MSG.TIME, Host ) - Task.MissionTask:AddGoalCompletion( Task.MissionTask.GoalVerb, Task.CargoName, 1 ) - self:T( 1 ) - return 1 - end - - else - if Task.ExecuteStage == _TransportExecuteStage.SUCCESS then - CargoStatic = StaticObject.getByName( Task.Cargo.CargoStaticName ) - if CargoStatic and not routines.IsStaticInZones( CargoStatic, Task.CurrentLandingZoneName ) then - Client:Message( "Good Job. The " .. Task.CargoType .. " has been sucessfully " .. Task.TEXT[3] .. " and flown outside of the landing zone.", - self.MSG.TIME, Host ) - Task.MissionTask:AddGoalCompletion( Task.MissionTask.GoalVerb, Task.Cargo.CargoName, 1 ) - self:T( 1 ) - return 1 - end - end - - end - - - self:T( 0 ) - return 0 -end - - -STAGEDONE = { - ClassName = "STAGEDONE", - MSG = { ID = "Done", TIME = 10 }, - Name = "Done" -} - -function STAGEDONE:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'AI' - return self -end - -function STAGEDONE:Execute( Mission, Client, Task ) - self:F() - -end - -function STAGEDONE:Validate( Mission, Client, Task ) - self:F() - - Task:Done() - - return 0 -end - -STAGEARRIVE = { - ClassName = "STAGEARRIVE", - MSG = { ID = "Arrive", TIME = 10 }, - Name = "Arrive" -} - -function STAGEARRIVE:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - return self -end - - ---- Execute Arrival --- @param #STAGEARRIVE self --- @param Mission#MISSION Mission --- @param Client#CLIENT Client --- @param Task#TASK Task -function STAGEARRIVE:Execute( Mission, Client, Task ) - self:F() - - if Client:IsMultiSeated() then - Client:Message( 'We have arrived at ' .. Task.CurrentLandingZoneName .. ".", self.MSG.TIME, "Co-Pilot" ) - else - Client:Message( 'We have arrived at ' .. Task.CurrentLandingZoneName .. ".", self.MSG.TIME, "Command" ) - end - -end - -function STAGEARRIVE:Validate( Mission, Client, Task ) - self:F() - - Task.CurrentLandingZoneID = routines.IsUnitInZones( Client:GetClientGroupDCSUnit(), Task.LandingZones ) - if ( Task.CurrentLandingZoneID ) then - else - return -1 - end - - return 1 -end - -STAGEGROUPSDESTROYED = { - ClassName = "STAGEGROUPSDESTROYED", - DestroyGroupSize = -1, - Frequency = STAGE.FREQUENCY.REPEAT, - MSG = { ID = "DestroyGroup", TIME = 10 }, - Name = "GroupsDestroyed" -} - -function STAGEGROUPSDESTROYED:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'AI' - return self -end - ---function STAGEGROUPSDESTROYED:Execute( Mission, Client, Task ) --- --- Client:Message( 'Task: Still ' .. DestroyGroupSize .. " of " .. Task.DestroyGroupCount .. " " .. Task.DestroyGroupType .. " to be destroyed!", self.MSG.TIME, Mission.Name .. "/Stage" ) --- ---end - -function STAGEGROUPSDESTROYED:Validate( Mission, Client, Task ) - self:F() - - if Task.MissionTask:IsGoalReached() then - return 1 - else - return 0 - end -end - -function STAGEGROUPSDESTROYED:Execute( Mission, Client, Task ) - self:F() - self:T( { Task.ClassName, Task.Destroyed } ) - --env.info( 'Event Table Task = ' .. tostring(Task) ) - -end - - - - - - - - - - - - - ---[[ - _TransportStage: Defines the different stages of which of transport missions can be in. This table is internal and is used to control the sequence of messages, actions and flow. - - - _TransportStage.START - - _TransportStage.ROUTE - - _TransportStage.LAND - - _TransportStage.EXECUTE - - _TransportStage.DONE - - _TransportStage.REMOVE ---]] -_TransportStage = { - HOLD = "HOLD", - START = "START", - ROUTE = "ROUTE", - LANDING = "LANDING", - LANDED = "LANDED", - EXECUTING = "EXECUTING", - LOAD = "LOAD", - UNLOAD = "UNLOAD", - DONE = "DONE", - NEXT = "NEXT" -} - -_TransportStageMsgTime = { - HOLD = 10, - START = 60, - ROUTE = 5, - LANDING = 10, - LANDED = 30, - EXECUTING = 30, - LOAD = 30, - UNLOAD = 30, - DONE = 30, - NEXT = 0 -} - -_TransportStageTime = { - HOLD = 10, - START = 5, - ROUTE = 5, - LANDING = 1, - LANDED = 1, - EXECUTING = 5, - LOAD = 5, - UNLOAD = 5, - DONE = 1, - NEXT = 0 -} - -_TransportStageAction = { - REPEAT = -1, - NONE = 0, - ONCE = 1 -} ---- The TASK Classes define major end-to-end activities within a MISSION. The TASK Class is the Master Class to orchestrate these activities. From this class, many concrete TASK classes are inherited. --- @module TASK - - - - - - - ---- The TASK class --- @type TASK --- @extends Base#BASE -TASK = { - - -- Defines the different signal types with a Task. - SIGNAL = { - COLOR = { - RED = { ID = 1, COLOR = trigger.smokeColor.Red, TEXT = "A red" }, - GREEN = { ID = 2, COLOR = trigger.smokeColor.Green, TEXT = "A green" }, - BLUE = { ID = 3, COLOR = trigger.smokeColor.Blue, TEXT = "A blue" }, - WHITE = { ID = 4, COLOR = trigger.smokeColor.White, TEXT = "A white" }, - ORANGE = { ID = 5, COLOR = trigger.smokeColor.Orange, TEXT = "An orange" } - }, - TYPE = { - SMOKE = { ID = 1, TEXT = "smoke" }, - FLARE = { ID = 2, TEXT = "flare" } - } - }, - ClassName = "TASK", - Mission = {}, -- Owning mission of the Task - Name = '', - Stages = {}, - Stage = {}, - Cargos = { - InitCargos = {}, - LoadCargos = {} - }, - LandingZones = { - LandingZoneNames = {}, - LandingZones = {} - }, - ActiveStage = 0, - TaskDone = false, - TaskFailed = false, - GoalTasks = {} -} - ---- Instantiates a new TASK Base. Should never be used. Interface Class. --- @return TASK -function TASK:New() - local self = BASE:Inherit( self, BASE:New() ) - self:F() - - -- assign Task default values during construction - self.TaskBriefing = "Task: No Task." - self.Time = timer.getTime() - self.ExecuteStage = _TransportExecuteStage.NONE - - return self -end - -function TASK:SetStage( StageSequenceIncrement ) - self:F( { StageSequenceIncrement } ) - - local Valid = false - if StageSequenceIncrement ~= 0 then - self.ActiveStage = self.ActiveStage + StageSequenceIncrement - if 1 <= self.ActiveStage and self.ActiveStage <= #self.Stages then - self.Stage = self.Stages[self.ActiveStage] - self:T( { self.Stage.Name } ) - self.Frequency = self.Stage.Frequency - Valid = true - else - Valid = false - env.info( "TASK:SetStage() self.ActiveStage is smaller or larger than self.Stages array. self.ActiveStage = " .. self.ActiveStage ) - end - end - self.Time = timer.getTime() - return Valid -end - -function TASK:Init() - self:F() - self.ActiveStage = 0 - self:SetStage(1) - self.TaskDone = false - self.TaskFailed = false -end - - ---- Get progress of a TASK. --- @return string GoalsText -function TASK:GetGoalProgress() - self:F2() - - local GoalsText = "" - for GoalVerb, GoalVerbData in pairs( self.GoalTasks ) do - local Goals = self:GetGoalCompletion( GoalVerb ) - if Goals and Goals ~= "" then - Goals = '(' .. Goals .. ')' - else - Goals = '( - )' - end - GoalsText = GoalsText .. GoalVerb .. ': ' .. self:GetGoalCount(GoalVerb) .. ' goals ' .. Goals .. ' of ' .. self:GetGoalTotal(GoalVerb) .. ' goals completed (' .. self:GetGoalPercentage(GoalVerb) .. '%); ' - end - - if GoalsText == "" then - GoalsText = "( - )" - end - - return GoalsText -end - ---- Show progress of a TASK. --- @param MISSION Mission Group structure describing the Mission. --- @param CLIENT Client Group structure describing the Client. -function TASK:ShowGoalProgress( Mission, Client ) - self:F2() - - local GoalsText = "" - for GoalVerb, GoalVerbData in pairs( self.GoalTasks ) do - if Mission:IsCompleted() then - else - local Goals = self:GetGoalCompletion( GoalVerb ) - if Goals and Goals ~= "" then - else - Goals = "-" - end - GoalsText = GoalsText .. self:GetGoalProgress() - end - end - - if Mission.MissionReportFlash or Mission.MissionReportShow then - Client:Message( GoalsText, 10, "Mission Command: Task Status", 30, "Task status" ) - end -end - ---- Sets a TASK to status Done. -function TASK:Done() - self:F2() - self.TaskDone = true -end - ---- Returns if a TASK is done. --- @return bool -function TASK:IsDone() - self:F2( self.TaskDone ) - return self.TaskDone -end - ---- Sets a TASK to status failed. -function TASK:Failed() - self:F() - self.TaskFailed = true -end - ---- Returns if a TASk has failed. --- @return bool -function TASK:IsFailed() - self:F2( self.TaskFailed ) - return self.TaskFailed -end - -function TASK:Reset( Mission, Client ) - self:F2() - self.ExecuteStage = _TransportExecuteStage.NONE -end - ---- Returns the Goals of a TASK --- @return @table Goals -function TASK:GetGoals() - return self.GoalTasks -end - ---- Returns if a TASK has Goal(s). --- @param #TASK self --- @param #string GoalVerb is the name of the Goal of the TASK. --- @return bool -function TASK:Goal( GoalVerb ) - self:F2( { GoalVerb } ) - if not GoalVerb then - GoalVerb = self.GoalVerb - end - self:T2( {self.GoalTasks[GoalVerb] } ) - if self.GoalTasks[GoalVerb] and self.GoalTasks[GoalVerb].GoalTotal > 0 then - return true - else - return false - end -end - ---- Sets the total Goals to be achieved of the Goal Name --- @param number GoalTotal is the number of times the GoalVerb needs to be achieved. --- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. -function TASK:SetGoalTotal( GoalTotal, GoalVerb ) - self:F2( { GoalTotal, GoalVerb } ) - - if not GoalVerb then - GoalVerb = self.GoalVerb - end - self.GoalTasks[GoalVerb] = {} - self.GoalTasks[GoalVerb].Goals = {} - self.GoalTasks[GoalVerb].GoalTotal = GoalTotal - self.GoalTasks[GoalVerb].GoalCount = 0 - return self -end - ---- Gets the total of Goals to be achieved within the TASK of the GoalVerb. --- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. -function TASK:GetGoalTotal( GoalVerb ) - self:F2( { GoalVerb } ) - if not GoalVerb then - GoalVerb = self.GoalVerb - end - if self:Goal( GoalVerb ) then - return self.GoalTasks[GoalVerb].GoalTotal - else - return 0 - end -end - ---- Sets the total of Goals currently achieved within the TASK of the GoalVerb. --- @param number GoalCount is the total number of Goals achieved within the TASK. --- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. --- @return TASK -function TASK:SetGoalCount( GoalCount, GoalVerb ) - self:F2() - if not GoalVerb then - GoalVerb = self.GoalVerb - end - if self:Goal( GoalVerb) then - self.GoalTasks[GoalVerb].GoalCount = GoalCount - end - return self -end - ---- Increments the total of Goals currently achieved within the TASK of the GoalVerb, with the given GoalCountIncrease. --- @param number GoalCountIncrease is the number of new Goals achieved within the TASK. --- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. --- @return TASK -function TASK:IncreaseGoalCount( GoalCountIncrease, GoalVerb ) - self:F2( { GoalCountIncrease, GoalVerb } ) - if not GoalVerb then - GoalVerb = self.GoalVerb - end - if self:Goal( GoalVerb) then - self.GoalTasks[GoalVerb].GoalCount = self.GoalTasks[GoalVerb].GoalCount + GoalCountIncrease - end - return self -end - ---- Gets the total of Goals currently achieved within the TASK of the GoalVerb. --- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. --- @return TASK -function TASK:GetGoalCount( GoalVerb ) - self:F2() - if not GoalVerb then - GoalVerb = self.GoalVerb - end - if self:Goal( GoalVerb ) then - return self.GoalTasks[GoalVerb].GoalCount - else - return 0 - end -end - ---- Gets the percentage of Goals currently achieved within the TASK of the GoalVerb. --- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. --- @return TASK -function TASK:GetGoalPercentage( GoalVerb ) - self:F2() - if not GoalVerb then - GoalVerb = self.GoalVerb - end - if self:Goal( GoalVerb ) then - return math.floor( self:GetGoalCount( GoalVerb ) / self:GetGoalTotal( GoalVerb ) * 100 + .5 ) - else - return 100 - end -end - ---- Returns if all the Goals of the TASK were achieved. --- @return bool -function TASK:IsGoalReached() - self:F2() - - local GoalReached = true - - for GoalVerb, Goals in pairs( self.GoalTasks ) do - self:T2( { "GoalVerb", GoalVerb } ) - if self:Goal( GoalVerb ) then - local GoalToDo = self:GetGoalTotal( GoalVerb ) - self:GetGoalCount( GoalVerb ) - self:T2( "GoalToDo = " .. GoalToDo ) - if GoalToDo <= 0 then - else - GoalReached = false - break - end - else - break - end - end - - self:T( { GoalReached, self.GoalTasks } ) - return GoalReached -end - ---- Adds an Additional Goal for the TASK to be achieved. --- @param string GoalVerb is the name of the Goal of the TASK. --- @param string GoalTask is a text describing the Goal of the TASK to be achieved. --- @param number GoalIncrease is a number by which the Goal achievement is increasing. -function TASK:AddGoalCompletion( GoalVerb, GoalTask, GoalIncrease ) - self:F2( { GoalVerb, GoalTask, GoalIncrease } ) - - if self:Goal( GoalVerb ) then - self.GoalTasks[GoalVerb].Goals[#self.GoalTasks[GoalVerb].Goals+1] = GoalTask - self.GoalTasks[GoalVerb].GoalCount = self.GoalTasks[GoalVerb].GoalCount + GoalIncrease - end - return self -end - ---- Returns if the additional Goal for the TASK was completed. --- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. --- @return string Goals -function TASK:GetGoalCompletion( GoalVerb ) - self:F2( { GoalVerb } ) - - if self:Goal( GoalVerb ) then - local Goals = "" - for GoalID, GoalName in pairs( self.GoalTasks[GoalVerb].Goals ) do Goals = Goals .. GoalName .. " + " end - return Goals:gsub(" + $", ""), self.GoalTasks[GoalVerb].GoalCount - end -end - -function TASK.MenuAction( Parameter ) - Parameter.ReferenceTask.ExecuteStage = _TransportExecuteStage.EXECUTING - Parameter.ReferenceTask.Cargo = Parameter.CargoTask -end - -function TASK:StageExecute() - self:F() - - local Execute = false - - if self.Frequency == STAGE.FREQUENCY.REPEAT then - Execute = true - elseif self.Frequency == STAGE.FREQUENCY.NONE then - Execute = false - elseif self.Frequency >= 0 then - Execute = true - self.Frequency = self.Frequency - 1 - end - - return Execute - -end - ---- Work function to set signal events within a TASK. -function TASK:AddSignal( SignalUnitNames, SignalType, SignalColor, SignalHeight ) - self:F() - - local Valid = true - - if Valid then - if type( SignalUnitNames ) == "table" then - self.LandingZoneSignalUnitNames = SignalUnitNames - else - self.LandingZoneSignalUnitNames = { SignalUnitNames } - end - self.LandingZoneSignalType = SignalType - self.LandingZoneSignalColor = SignalColor - self.Signalled = false - if SignalHeight ~= nil then - self.LandingZoneSignalHeight = SignalHeight - else - self.LandingZoneSignalHeight = 0 - end - - if self.TaskBriefing then - self.TaskBriefing = self.TaskBriefing .. " " .. SignalColor.TEXT .. " " .. SignalType.TEXT .. " will be fired when entering the landing zone." - end - end - - return Valid -end - ---- When the CLIENT is approaching the landing zone, a RED SMOKE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddSmokeRed( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.SMOKE, TASK.SIGNAL.COLOR.RED, SignalHeight ) -end - ---- When the CLIENT is approaching the landing zone, a GREEN SMOKE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddSmokeGreen( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.SMOKE, TASK.SIGNAL.COLOR.GREEN, SignalHeight ) -end - ---- When the CLIENT is approaching the landing zone, a BLUE SMOKE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddSmokeBlue( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.SMOKE, TASK.SIGNAL.COLOR.BLUE, SignalHeight ) -end - ---- When the CLIENT is approaching the landing zone, a WHITE SMOKE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddSmokeWhite( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.SMOKE, TASK.SIGNAL.COLOR.WHITE, SignalHeight ) -end - ---- When the CLIENT is approaching the landing zone, an ORANGE SMOKE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddSmokeOrange( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.SMOKE, TASK.SIGNAL.COLOR.ORANGE, SignalHeight ) -end - ---- When the CLIENT is approaching the landing zone, a RED FLARE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddFlareRed( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.FLARE, TASK.SIGNAL.COLOR.RED, SignalHeight ) -end - ---- When the CLIENT is approaching the landing zone, a GREEN FLARE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddFlareGreen( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.FLARE, TASK.SIGNAL.COLOR.GREEN, SignalHeight ) -end - ---- When the CLIENT is approaching the landing zone, a BLUE FLARE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddFlareBlue( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.FLARE, TASK.SIGNAL.COLOR.BLUE, SignalHeight ) -end - ---- When the CLIENT is approaching the landing zone, a WHITE FLARE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddFlareWhite( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.FLARE, TASK.SIGNAL.COLOR.WHITE, SignalHeight ) -end - ---- When the CLIENT is approaching the landing zone, an ORANGE FLARE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddFlareOrange( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.FLARE, TASK.SIGNAL.COLOR.ORANGE, SignalHeight ) -end ---- A GOHOMETASK orchestrates the travel back to the home base, which is a specific zone defined within the ME. --- @module GOHOMETASK - ---- The GOHOMETASK class --- @type -GOHOMETASK = { - ClassName = "GOHOMETASK", -} - ---- Creates a new GOHOMETASK. --- @param table{string,...}|string LandingZones Table of Landing Zone names where Home(s) are located. --- @return GOHOMETASK -function GOHOMETASK:New( LandingZones ) - local self = BASE:Inherit( self, TASK:New() ) - self:F( { LandingZones } ) - local Valid = true - - Valid = routines.ValidateZone( LandingZones, "LandingZones", Valid ) - - if Valid then - self.Name = 'Fly Home' - self.TaskBriefing = "Task: Fly back to your home base. Your co-pilot will provide you with the directions (required flight angle in degrees) and the distance (in km) to your home base." - if type( LandingZones ) == "table" then - self.LandingZones = LandingZones - else - self.LandingZones = { LandingZones } - end - self.Stages = { STAGEBRIEF:New(), STAGESTART:New(), STAGEROUTE:New(), STAGEARRIVE:New(), STAGEDONE:New() } - self.SetStage( self, 1 ) - end - - return self -end ---- A DESTROYBASETASK will monitor the destruction of Groups and Units. This is a BASE class, other classes are derived from this class. --- @module DESTROYBASETASK --- @see DESTROYGROUPSTASK --- @see DESTROYUNITTYPESTASK --- @see DESTROY_RADARS_TASK - - - ---- The DESTROYBASETASK class --- @type DESTROYBASETASK -DESTROYBASETASK = { - ClassName = "DESTROYBASETASK", - Destroyed = 0, - GoalVerb = "Destroy", - DestroyPercentage = 100, -} - ---- Creates a new DESTROYBASETASK. --- @param #DESTROYBASETASK self --- @param #string DestroyGroupType Text describing the group to be destroyed. f.e. "Radar Installations", "Ships", "Vehicles", "Command Centers". --- @param #string DestroyUnitType Text describing the unit types to be destroyed. f.e. "SA-6", "Row Boats", "Tanks", "Tents". --- @param #list<#string> DestroyGroupPrefixes Table of Prefixes of the Groups to be destroyed before task is completed. --- @param #number DestroyPercentage defines the %-tage that needs to be destroyed to achieve mission success. eg. If in the Group there are 10 units, then a value of 75 would require 8 units to be destroyed from the Group to complete the @{TASK}. --- @return DESTROYBASETASK -function DESTROYBASETASK:New( DestroyGroupType, DestroyUnitType, DestroyGroupPrefixes, DestroyPercentage ) - local self = BASE:Inherit( self, TASK:New() ) - self:F() - - self.Name = 'Destroy' - self.Destroyed = 0 - self.DestroyGroupPrefixes = DestroyGroupPrefixes - self.DestroyGroupType = DestroyGroupType - self.DestroyUnitType = DestroyUnitType - if DestroyPercentage then - self.DestroyPercentage = DestroyPercentage - end - self.TaskBriefing = "Task: Destroy " .. DestroyGroupType .. "." - self.Stages = { STAGEBRIEF:New(), STAGESTART:New(), STAGEGROUPSDESTROYED:New(), STAGEDONE:New() } - self.SetStage( self, 1 ) - - return self -end - ---- Handle the S_EVENT_DEAD events to validate the destruction of units for the task monitoring. --- @param #DESTROYBASETASK self --- @param Event#EVENTDATA Event structure of MOOSE. -function DESTROYBASETASK:EventDead( Event ) - self:F( { Event } ) - - if Event.IniDCSUnit then - local DestroyUnit = Event.IniDCSUnit - local DestroyUnitName = Event.IniDCSUnitName - local DestroyGroup = Event.IniDCSGroup - local DestroyGroupName = Event.IniDCSGroupName - - --TODO: I need to fix here if 2 groups in the mission have a similar name with GroupPrefix equal, then i should differentiate for which group the goal was reached! - --I may need to test if for the goalverb that group goal was reached or something. Need to think about it a bit more ... - local UnitsDestroyed = 0 - for DestroyGroupPrefixID, DestroyGroupPrefix in pairs( self.DestroyGroupPrefixes ) do - self:T( DestroyGroupPrefix ) - if string.find( DestroyGroupName, DestroyGroupPrefix, 1, true ) then - self:T( BASE:Inherited(self).ClassName ) - UnitsDestroyed = self:ReportGoalProgress( DestroyGroup, DestroyUnit ) - self:T( UnitsDestroyed ) - end - end - - self:T( { UnitsDestroyed } ) - self:IncreaseGoalCount( UnitsDestroyed, self.GoalVerb ) - end - -end - ---- Validate task completeness of DESTROYBASETASK. --- @param DestroyGroup Group structure describing the group to be evaluated. --- @param DestroyUnit Unit structure describing the Unit to be evaluated. -function DESTROYBASETASK:ReportGoalProgress( DestroyGroup, DestroyUnit ) - self:F() - - return 0 -end ---- DESTROYGROUPSTASK --- @module DESTROYGROUPSTASK - - - ---- The DESTROYGROUPSTASK class --- @type -DESTROYGROUPSTASK = { - ClassName = "DESTROYGROUPSTASK", - GoalVerb = "Destroy Groups", -} - ---- Creates a new DESTROYGROUPSTASK. --- @param #DESTROYGROUPSTASK self --- @param #string DestroyGroupType String describing the group to be destroyed. --- @param #string DestroyUnitType String describing the unit to be destroyed. --- @param #list<#string> DestroyGroupNames Table of string containing the name of the groups to be destroyed before task is completed. --- @param #number DestroyPercentage defines the %-tage that needs to be destroyed to achieve mission success. eg. If in the Group there are 10 units, then a value of 75 would require 8 units to be destroyed from the Group to complete the @{TASK}. ----@return DESTROYGROUPSTASK -function DESTROYGROUPSTASK:New( DestroyGroupType, DestroyUnitType, DestroyGroupNames, DestroyPercentage ) - local self = BASE:Inherit( self, DESTROYBASETASK:New( DestroyGroupType, DestroyUnitType, DestroyGroupNames, DestroyPercentage ) ) - self:F() - - self.Name = 'Destroy Groups' - self.GoalVerb = "Destroy " .. DestroyGroupType - - _EVENTDISPATCHER:OnDead( self.EventDead , self ) - _EVENTDISPATCHER:OnCrash( self.EventDead , self ) - - return self -end - ---- Report Goal Progress. --- @param #DESTROYGROUPSTASK self --- @param DCSGroup#Group DestroyGroup Group structure describing the group to be evaluated. --- @param DCSUnit#Unit DestroyUnit Unit structure describing the Unit to be evaluated. --- @return #number The DestroyCount reflecting the amount of units destroyed within the group. -function DESTROYGROUPSTASK:ReportGoalProgress( DestroyGroup, DestroyUnit ) - self:F( { DestroyGroup, DestroyUnit, self.DestroyPercentage } ) - - local DestroyGroupSize = DestroyGroup:getSize() - 1 -- When a DEAD event occurs, the getSize is still one larger than the destroyed unit. - local DestroyGroupInitialSize = DestroyGroup:getInitialSize() - self:T( { DestroyGroupSize, DestroyGroupInitialSize - ( DestroyGroupInitialSize * self.DestroyPercentage / 100 ) } ) - - local DestroyCount = 0 - if DestroyGroup then - if DestroyGroupSize <= DestroyGroupInitialSize - ( DestroyGroupInitialSize * self.DestroyPercentage / 100 ) then - DestroyCount = 1 - end - else - DestroyCount = 1 - end - - self:T( DestroyCount ) - - return DestroyCount -end ---- Task class to destroy radar installations. --- @module DESTROYRADARSTASK - - - ---- The DESTROYRADARS class --- @type -DESTROYRADARSTASK = { - ClassName = "DESTROYRADARSTASK", - GoalVerb = "Destroy Radars" -} - ---- Creates a new DESTROYRADARSTASK. --- @param table{string,...} DestroyGroupNames Table of string containing the group names of which the radars are be destroyed. --- @return DESTROYRADARSTASK -function DESTROYRADARSTASK:New( DestroyGroupNames ) - local self = BASE:Inherit( self, DESTROYGROUPSTASK:New( 'radar installations', 'radars', DestroyGroupNames ) ) - self:F() - - self.Name = 'Destroy Radars' - - _EVENTDISPATCHER:OnDead( self.EventDead , self ) - - return self -end - ---- Report Goal Progress. --- @param Group DestroyGroup Group structure describing the group to be evaluated. --- @param Unit DestroyUnit Unit structure describing the Unit to be evaluated. -function DESTROYRADARSTASK:ReportGoalProgress( DestroyGroup, DestroyUnit ) - self:F( { DestroyGroup, DestroyUnit } ) - - local DestroyCount = 0 - if DestroyUnit and DestroyUnit:hasSensors( Unit.SensorType.RADAR, Unit.RadarType.AS ) then - if DestroyUnit and DestroyUnit:getLife() <= 1.0 then - self:T( 'Destroyed a radar' ) - DestroyCount = 1 - end - end - return DestroyCount -end ---- Set TASK to destroy certain unit types. --- @module DESTROYUNITTYPESTASK - - - ---- The DESTROYUNITTYPESTASK class --- @type -DESTROYUNITTYPESTASK = { - ClassName = "DESTROYUNITTYPESTASK", - GoalVerb = "Destroy", -} - ---- Creates a new DESTROYUNITTYPESTASK. --- @param string DestroyGroupType String describing the group to be destroyed. f.e. "Radar Installations", "Fleet", "Batallion", "Command Centers". --- @param string DestroyUnitType String describing the unit to be destroyed. f.e. "radars", "ships", "tanks", "centers". --- @param table{string,...} DestroyGroupNames Table of string containing the group names of which the radars are be destroyed. --- @param string DestroyUnitTypes Table of string containing the type names of the units to achieve mission success. --- @return DESTROYUNITTYPESTASK -function DESTROYUNITTYPESTASK:New( DestroyGroupType, DestroyUnitType, DestroyGroupNames, DestroyUnitTypes ) - local self = BASE:Inherit( self, DESTROYBASETASK:New( DestroyGroupType, DestroyUnitType, DestroyGroupNames ) ) - self:F( { DestroyGroupType, DestroyUnitType, DestroyGroupNames, DestroyUnitTypes } ) - - if type(DestroyUnitTypes) == 'table' then - self.DestroyUnitTypes = DestroyUnitTypes - else - self.DestroyUnitTypes = { DestroyUnitTypes } - end - - self.Name = 'Destroy Unit Types' - self.GoalVerb = "Destroy " .. DestroyGroupType - - _EVENTDISPATCHER:OnDead( self.EventDead , self ) - - return self -end - ---- Report Goal Progress. --- @param Group DestroyGroup Group structure describing the group to be evaluated. --- @param Unit DestroyUnit Unit structure describing the Unit to be evaluated. -function DESTROYUNITTYPESTASK:ReportGoalProgress( DestroyGroup, DestroyUnit ) - self:F( { DestroyGroup, DestroyUnit } ) - - local DestroyCount = 0 - for UnitTypeID, UnitType in pairs( self.DestroyUnitTypes ) do - if DestroyUnit and DestroyUnit:getTypeName() == UnitType then - if DestroyUnit and DestroyUnit:getLife() <= 1.0 then - DestroyCount = DestroyCount + 1 - end - end - end - return DestroyCount -end ---- A PICKUPTASK orchestrates the loading of CARGO at a specific landing zone. --- @module PICKUPTASK --- @parent TASK - ---- The PICKUPTASK class --- @type -PICKUPTASK = { - ClassName = "PICKUPTASK", - TEXT = { "Pick-Up", "picked-up", "loaded" }, - GoalVerb = "Pick-Up" -} - ---- Creates a new PICKUPTASK. --- @param table{string,...}|string LandingZones Table of Zone names where Cargo is to be loaded. --- @param CARGO_TYPE CargoType Type of the Cargo. The type must be of the following Enumeration:.. --- @param number OnBoardSide Reflects from which side the cargo Group will be on-boarded on the Carrier. -function PICKUPTASK:New( CargoType, OnBoardSide ) - local self = BASE:Inherit( self, TASK:New() ) - self:F() - - -- self holds the inherited instance of the PICKUPTASK Class to the BASE class. - - local Valid = true - - if Valid then - self.Name = 'Pickup Cargo' - self.TaskBriefing = "Task: Fly to the indicated landing zones and pickup " .. CargoType .. ". Your co-pilot will provide you with the directions (required flight angle in degrees) and the distance (in km) to the pickup zone." - self.CargoType = CargoType - self.GoalVerb = CargoType .. " " .. self.GoalVerb - self.OnBoardSide = OnBoardSide - self.IsLandingRequired = true -- required to decide whether the client needs to land or not - self.IsSlingLoad = false -- Indicates whether the cargo is a sling load cargo - self.Stages = { STAGE_CARGO_INIT:New(), STAGE_CARGO_LOAD:New(), STAGEBRIEF:New(), STAGESTART:New(), STAGEROUTE:New(), STAGELANDING:New(), STAGELANDED:New(), STAGELOAD:New(), STAGEDONE:New() } - self.SetStage( self, 1 ) - end - - return self -end - -function PICKUPTASK:FromZone( LandingZone ) - self:F() - - self.LandingZones.LandingZoneNames[LandingZone.CargoZoneName] = LandingZone.CargoZoneName - self.LandingZones.LandingZones[LandingZone.CargoZoneName] = LandingZone - - return self -end - -function PICKUPTASK:InitCargo( InitCargos ) - self:F( { InitCargos } ) - - if type( InitCargos ) == "table" then - self.Cargos.InitCargos = InitCargos - else - self.Cargos.InitCargos = { InitCargos } - end - - return self -end - -function PICKUPTASK:LoadCargo( LoadCargos ) - self:F( { LoadCargos } ) - - if type( LoadCargos ) == "table" then - self.Cargos.LoadCargos = LoadCargos - else - self.Cargos.LoadCargos = { LoadCargos } - end - - return self -end - -function PICKUPTASK:AddCargoMenus( Client, Cargos, TransportRadius ) - self:F() - - for CargoID, Cargo in pairs( Cargos ) do - - self:T( { Cargo.ClassName, Cargo.CargoName, Cargo.CargoType, Cargo:IsStatusNone(), Cargo:IsStatusLoaded(), Cargo:IsStatusLoading(), Cargo:IsStatusUnLoaded() } ) - - -- If the Cargo has no status, allow the menu option. - if Cargo:IsStatusNone() or ( Cargo:IsStatusLoading() and Client == Cargo:IsLoadingToClient() ) then - - local MenuAdd = false - if Cargo:IsNear( Client, self.CurrentCargoZone ) then - MenuAdd = true - end - - if MenuAdd then - if Client._Menus[Cargo.CargoType] == nil then - Client._Menus[Cargo.CargoType] = {} - end - - if not Client._Menus[Cargo.CargoType].PickupMenu then - Client._Menus[Cargo.CargoType].PickupMenu = missionCommands.addSubMenuForGroup( - Client:GetClientGroupID(), - self.TEXT[1] .. " " .. Cargo.CargoType, - nil - ) - self:T( 'Added PickupMenu: ' .. self.TEXT[1] .. " " .. Cargo.CargoType ) - end - - if Client._Menus[Cargo.CargoType].PickupSubMenus == nil then - Client._Menus[Cargo.CargoType].PickupSubMenus = {} - end - - Client._Menus[Cargo.CargoType].PickupSubMenus[ #Client._Menus[Cargo.CargoType].PickupSubMenus + 1 ] = missionCommands.addCommandForGroup( - Client:GetClientGroupID(), - Cargo.CargoName .. " ( " .. Cargo.CargoWeight .. "kg )", - Client._Menus[Cargo.CargoType].PickupMenu, - self.MenuAction, - { ReferenceTask = self, CargoTask = Cargo } - ) - self:T( 'Added PickupSubMenu' .. Cargo.CargoType .. ":" .. Cargo.CargoName .. " ( " .. Cargo.CargoWeight .. "kg )" ) - end - end - end - -end - -function PICKUPTASK:RemoveCargoMenus( Client ) - self:F() - - for MenuID, MenuData in pairs( Client._Menus ) do - for SubMenuID, SubMenuData in pairs( MenuData.PickupSubMenus ) do - missionCommands.removeItemForGroup( Client:GetClientGroupID(), SubMenuData ) - self:T( "Removed PickupSubMenu " ) - SubMenuData = nil - end - if MenuData.PickupMenu then - missionCommands.removeItemForGroup( Client:GetClientGroupID(), MenuData.PickupMenu ) - self:T( "Removed PickupMenu " ) - MenuData.PickupMenu = nil - end - end - - for CargoID, Cargo in pairs( CARGOS ) do - self:T( { Cargo.ClassName, Cargo.CargoName, Cargo.CargoType, Cargo:IsStatusNone(), Cargo:IsStatusLoaded(), Cargo:IsStatusLoading(), Cargo:IsStatusUnLoaded() } ) - if Cargo:IsStatusLoading() and Client == Cargo:IsLoadingToClient() then - Cargo:StatusNone() - end - end - -end - - - -function PICKUPTASK:HasFailed( ClientDead ) - self:F() - - local TaskHasFailed = self.TaskFailed - return TaskHasFailed -end - ---- A DEPLOYTASK orchestrates the deployment of CARGO within a specific landing zone. --- @module DEPLOYTASK - - - ---- A DeployTask --- @type DEPLOYTASK -DEPLOYTASK = { - ClassName = "DEPLOYTASK", - TEXT = { "Deploy", "deployed", "unloaded" }, - GoalVerb = "Deployment" -} - - ---- Creates a new DEPLOYTASK object, which models the sequence of STAGEs to unload a cargo. --- @function [parent=#DEPLOYTASK] New --- @param #string CargoType Type of the Cargo. --- @return #DEPLOYTASK The created DeployTask -function DEPLOYTASK:New( CargoType ) - local self = BASE:Inherit( self, TASK:New() ) - self:F() - - local Valid = true - - if Valid then - self.Name = 'Deploy Cargo' - self.TaskBriefing = "Fly to one of the indicated landing zones and deploy " .. CargoType .. ". Your co-pilot will provide you with the directions (required flight angle in degrees) and the distance (in km) to the deployment zone." - self.CargoType = CargoType - self.GoalVerb = CargoType .. " " .. self.GoalVerb - self.Stages = { STAGE_CARGO_INIT:New(), STAGE_CARGO_LOAD:New(), STAGEBRIEF:New(), STAGESTART:New(), STAGEROUTE:New(), STAGELANDING:New(), STAGELANDED:New(), STAGEUNLOAD:New(), STAGEDONE:New() } - self.SetStage( self, 1 ) - end - - return self -end - -function DEPLOYTASK:ToZone( LandingZone ) - self:F() - - self.LandingZones.LandingZoneNames[LandingZone.CargoZoneName] = LandingZone.CargoZoneName - self.LandingZones.LandingZones[LandingZone.CargoZoneName] = LandingZone - - return self -end - - -function DEPLOYTASK:InitCargo( InitCargos ) - self:F( { InitCargos } ) - - if type( InitCargos ) == "table" then - self.Cargos.InitCargos = InitCargos - else - self.Cargos.InitCargos = { InitCargos } - end - - return self -end - - -function DEPLOYTASK:LoadCargo( LoadCargos ) - self:F( { LoadCargos } ) - - if type( LoadCargos ) == "table" then - self.Cargos.LoadCargos = LoadCargos - else - self.Cargos.LoadCargos = { LoadCargos } - end - - return self -end - - ---- When the cargo is unloaded, it will move to the target zone name. --- @param string TargetZoneName Name of the Zone to where the Cargo should move after unloading. -function DEPLOYTASK:SetCargoTargetZoneName( TargetZoneName ) - self:F() - - local Valid = true - - Valid = routines.ValidateString( TargetZoneName, "TargetZoneName", Valid ) - - if Valid then - self.TargetZoneName = TargetZoneName - end - - return Valid - -end - -function DEPLOYTASK:AddCargoMenus( Client, Cargos, TransportRadius ) - self:F() - - local ClientGroupID = Client:GetClientGroupID() - - self:T( ClientGroupID ) - - for CargoID, Cargo in pairs( Cargos ) do - - self:T( { Cargo.ClassName, Cargo.CargoName, Cargo.CargoType, Cargo.CargoWeight } ) - - if Cargo:IsStatusLoaded() and Client == Cargo:IsLoadedInClient() then - - if Client._Menus[Cargo.CargoType] == nil then - Client._Menus[Cargo.CargoType] = {} - end - - if not Client._Menus[Cargo.CargoType].DeployMenu then - Client._Menus[Cargo.CargoType].DeployMenu = missionCommands.addSubMenuForGroup( - ClientGroupID, - self.TEXT[1] .. " " .. Cargo.CargoType, - nil - ) - self:T( 'Added DeployMenu ' .. self.TEXT[1] ) - end - - if Client._Menus[Cargo.CargoType].DeploySubMenus == nil then - Client._Menus[Cargo.CargoType].DeploySubMenus = {} - end - - if Client._Menus[Cargo.CargoType].DeployMenu == nil then - self:T( 'deploymenu is nil' ) - end - - Client._Menus[Cargo.CargoType].DeploySubMenus[ #Client._Menus[Cargo.CargoType].DeploySubMenus + 1 ] = missionCommands.addCommandForGroup( - ClientGroupID, - Cargo.CargoName .. " ( " .. Cargo.CargoWeight .. "kg )", - Client._Menus[Cargo.CargoType].DeployMenu, - self.MenuAction, - { ReferenceTask = self, CargoTask = Cargo } - ) - self:T( 'Added DeploySubMenu ' .. Cargo.CargoType .. ":" .. Cargo.CargoName .. " ( " .. Cargo.CargoWeight .. "kg )" ) - end - end - -end - -function DEPLOYTASK:RemoveCargoMenus( Client ) - self:F() - - local ClientGroupID = Client:GetClientGroupID() - self:T( ClientGroupID ) - - for MenuID, MenuData in pairs( Client._Menus ) do - if MenuData.DeploySubMenus ~= nil then - for SubMenuID, SubMenuData in pairs( MenuData.DeploySubMenus ) do - missionCommands.removeItemForGroup( ClientGroupID, SubMenuData ) - self:T( "Removed DeploySubMenu " ) - SubMenuData = nil - end - end - if MenuData.DeployMenu then - missionCommands.removeItemForGroup( ClientGroupID, MenuData.DeployMenu ) - self:T( "Removed DeployMenu " ) - MenuData.DeployMenu = nil - end - end - -end ---- A NOTASK is a dummy activity... But it will show a Mission Briefing... --- @module NOTASK - ---- The NOTASK class --- @type -NOTASK = { - ClassName = "NOTASK", -} - ---- Creates a new NOTASK. -function NOTASK:New() - local self = BASE:Inherit( self, TASK:New() ) - self:F() - - local Valid = true - - if Valid then - self.Name = 'Nothing' - self.TaskBriefing = "Task: Execute your mission." - self.Stages = { STAGEBRIEF:New(), STAGESTART:New(), STAGEDONE:New() } - self.SetStage( self, 1 ) - end - - return self -end ---- A ROUTETASK orchestrates the travel to a specific zone defined within the ME. --- @module ROUTETASK - ---- The ROUTETASK class --- @type -ROUTETASK = { - ClassName = "ROUTETASK", - GoalVerb = "Route", -} - ---- Creates a new ROUTETASK. --- @param table{sring,...}|string LandingZones Table of Zone Names where the target is located. --- @param string TaskBriefing (optional) Defines a text describing the briefing of the task. --- @return ROUTETASK -function ROUTETASK:New( LandingZones, TaskBriefing ) - local self = BASE:Inherit( self, TASK:New() ) - self:F( { LandingZones, TaskBriefing } ) - - local Valid = true - - Valid = routines.ValidateZone( LandingZones, "LandingZones", Valid ) - - if Valid then - self.Name = 'Route To Zone' - if TaskBriefing then - self.TaskBriefing = TaskBriefing .. " Your co-pilot will provide you with the directions (required flight angle in degrees) and the distance (in km) to the target objective." - else - self.TaskBriefing = "Task: Fly to specified zone(s). Your co-pilot will provide you with the directions (required flight angle in degrees) and the distance (in km) to the target objective." - end - if type( LandingZones ) == "table" then - self.LandingZones = LandingZones - else - self.LandingZones = { LandingZones } - end - self.Stages = { STAGEBRIEF:New(), STAGESTART:New(), STAGEROUTE:New(), STAGEARRIVE:New(), STAGEDONE:New() } - self.SetStage( self, 1 ) - end - - return self -end - ---- A MISSION is the main owner of a Mission orchestration within MOOSE . The Mission framework orchestrates @{CLIENT}s, @{TASK}s, @{STAGE}s etc. --- A @{CLIENT} needs to be registered within the @{MISSION} through the function @{AddClient}. A @{TASK} needs to be registered within the @{MISSION} through the function @{AddTask}. --- @module Mission - ---- The MISSION class --- @type MISSION --- @extends Base#BASE --- @field #MISSION.Clients _Clients --- @field #string MissionBriefing -MISSION = { - ClassName = "MISSION", - Name = "", - MissionStatus = "PENDING", - _Clients = {}, - _Tasks = {}, - _ActiveTasks = {}, - GoalFunction = nil, - MissionReportTrigger = 0, - MissionProgressTrigger = 0, - MissionReportShow = false, - MissionReportFlash = false, - MissionTimeInterval = 0, - MissionCoalition = "", - SUCCESS = 1, - FAILED = 2, - REPEAT = 3, - _GoalTasks = {} -} - ---- @type MISSION.Clients --- @list - -function MISSION:Meta() - - local self = BASE:Inherit( self, BASE:New() ) - self:F() - - return self -end - ---- This is the main MISSION declaration method. Each Mission is like the master or a Mission orchestration between, Clients, Tasks, Stages etc. --- @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 string 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"... --- @return MISSION --- @usage --- -- Declare a few missions. --- local Mission = MISSIONSCHEDULER.AddMission( 'Russia Transport Troops SA-6', 'Operational', 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', 'Russia' ) --- local Mission = MISSIONSCHEDULER.AddMission( 'Patriots', 'Primary', 'Our intelligence reports that 3 Patriot SAM defense batteries are located near Ruisi, Kvarhiti and Gori.', 'Russia' ) --- local Mission = MISSIONSCHEDULER.AddMission( 'Package Delivery', 'Operational', 'In order to be in full control of the situation, we need you to deliver a very important package at a secret location. Fly undetected through the NATO defenses and deliver the secret package. The secret agent is located at waypoint 4.', 'Russia' ) --- local Mission = MISSIONSCHEDULER.AddMission( 'Rescue General', 'Tactical', 'Our intelligence has received a remote signal behind Gori. We believe it is a very important Russian General that was captured by Georgia. Go out there and rescue him! Ensure you stay out of the battle zone, keep south. Waypoint 4 is the location of our Russian General.', 'Russia' ) --- local Mission = MISSIONSCHEDULER.AddMission( 'NATO Transport Troops', 'Operational', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.', 'NATO' ) --- local Mission = MISSIONSCHEDULER.AddMission( 'SA-6 SAMs', 'Primary', 'Our intelligence reports that 3 SA-6 SAM defense batteries are located near Didmukha, Khetagurov and Berula. Eliminate the Russian SAMs.', 'NATO' ) --- local Mission = MISSIONSCHEDULER.AddMission( 'NATO Sling Load', 'Operational', 'Fly to the cargo pickup zone at Dzegvi or Kaspi, and sling the cargo to Soganlug airbase.', 'NATO' ) --- local Mission = MISSIONSCHEDULER.AddMission( 'Rescue secret agent', 'Tactical', 'In order to be in full control of the situation, we need you to rescue a secret agent from the woods behind enemy lines. Avoid the Russian defenses and rescue the agent. Keep south until Khasuri, and keep your eyes open for any SAM presence. The agent is located at waypoint 4 on your kneeboard.', 'NATO' ) -function MISSION:New( MissionName, MissionPriority, MissionBriefing, MissionCoalition ) - - self = MISSION:Meta() - self:T({ MissionName, MissionPriority, MissionBriefing, MissionCoalition }) - - local Valid = true - - Valid = routines.ValidateString( MissionName, "MissionName", Valid ) - Valid = routines.ValidateString( MissionPriority, "MissionPriority", Valid ) - Valid = routines.ValidateString( MissionBriefing, "MissionBriefing", Valid ) - Valid = routines.ValidateString( MissionCoalition, "MissionCoalition", Valid ) - - if Valid then - self.Name = MissionName - self.MissionPriority = MissionPriority - self.MissionBriefing = MissionBriefing - self.MissionCoalition = MissionCoalition - end - - return self -end - ---- Returns if a Mission has completed. --- @return bool -function MISSION:IsCompleted() - self:F() - return self.MissionStatus == "ACCOMPLISHED" -end - ---- Set a Mission to completed. -function MISSION:Completed() - self:F() - self.MissionStatus = "ACCOMPLISHED" - self:StatusToClients() -end - ---- Returns if a Mission is ongoing. --- treturn bool -function MISSION:IsOngoing() - self:F() - return self.MissionStatus == "ONGOING" -end - ---- Set a Mission to ongoing. -function MISSION:Ongoing() - self:F() - self.MissionStatus = "ONGOING" - --self:StatusToClients() -end - ---- Returns if a Mission is pending. --- treturn bool -function MISSION:IsPending() - self:F() - return self.MissionStatus == "PENDING" -end - ---- Set a Mission to pending. -function MISSION:Pending() - self:F() - self.MissionStatus = "PENDING" - self:StatusToClients() -end - ---- Returns if a Mission has failed. --- treturn bool -function MISSION:IsFailed() - self:F() - return self.MissionStatus == "FAILED" -end - ---- Set a Mission to failed. -function MISSION:Failed() - self:F() - self.MissionStatus = "FAILED" - self:StatusToClients() -end - ---- Send the status of the MISSION to all Clients. -function MISSION:StatusToClients() - self:F() - if self.MissionReportFlash then - for ClientID, Client in pairs( self._Clients ) do - Client:Message( self.MissionCoalition .. ' "' .. self.Name .. '": ' .. self.MissionStatus .. '! ( ' .. self.MissionPriority .. ' mission ) ', 10, "Mission Command: Mission Status") - end - end -end - ---- Handles the reporting. After certain time intervals, a MISSION report MESSAGE will be shown to All Players. -function MISSION:ReportTrigger() - self:F() - - if self.MissionReportShow == true then - self.MissionReportShow = false - return true - else - if self.MissionReportFlash == true then - if timer.getTime() >= self.MissionReportTrigger then - self.MissionReportTrigger = timer.getTime() + self.MissionTimeInterval - return true - else - return false - end - else - return false - end - end -end - ---- Report the status of all MISSIONs to all active Clients. -function MISSION:ReportToAll() - self:F() - - local AlivePlayers = '' - for ClientID, Client in pairs( self._Clients ) do - if Client:GetDCSGroup() then - if Client:GetClientGroupDCSUnit() then - if Client:GetClientGroupDCSUnit():getLife() > 0.0 then - if AlivePlayers == '' then - AlivePlayers = ' Players: ' .. Client:GetClientGroupDCSUnit():getPlayerName() - else - AlivePlayers = AlivePlayers .. ' / ' .. Client:GetClientGroupDCSUnit():getPlayerName() - end - end - end - end - end - local Tasks = self:GetTasks() - local TaskText = "" - for TaskID, TaskData in pairs( Tasks ) do - TaskText = TaskText .. " - Task " .. TaskID .. ": " .. TaskData.Name .. ": " .. TaskData:GetGoalProgress() .. "\n" - end - MESSAGE:New( self.MissionCoalition .. ' "' .. self.Name .. '": ' .. self.MissionStatus .. ' ( ' .. self.MissionPriority .. ' mission )' .. AlivePlayers .. "\n" .. TaskText:gsub("\n$",""), 10, "Mission Command: Mission Report" ):ToAll() -end - - ---- Add a goal function to a MISSION. Goal functions are called when a @{TASK} within a mission has been completed. --- @param function GoalFunction is the function defined by the mission designer to evaluate whether a certain goal has been reached after a @{TASK} finishes within the @{MISSION}. A GoalFunction must accept 2 parameters: Mission, Client, which contains the current MISSION object and the current CLIENT object respectively. --- @usage --- PatriotActivation = { --- { "US SAM Patriot Zerti", false }, --- { "US SAM Patriot Zegduleti", false }, --- { "US SAM Patriot Gvleti", false } --- } --- --- function DeployPatriotTroopsGoal( Mission, Client ) --- --- --- -- Check if the cargo is all deployed for mission success. --- for CargoID, CargoData in pairs( Mission._Cargos ) do --- if Group.getByName( CargoData.CargoGroupName ) then --- CargoGroup = Group.getByName( CargoData.CargoGroupName ) --- if CargoGroup then --- -- Check if the cargo is ready to activate --- CurrentLandingZoneID = routines.IsUnitInZones( CargoGroup:getUnits()[1], Mission:GetTask( 2 ).LandingZones ) -- The second task is the Deploytask to measure mission success upon --- if CurrentLandingZoneID then --- if PatriotActivation[CurrentLandingZoneID][2] == false then --- -- Now check if this is a new Mission Task to be completed... --- trigger.action.setGroupAIOn( Group.getByName( PatriotActivation[CurrentLandingZoneID][1] ) ) --- PatriotActivation[CurrentLandingZoneID][2] = true --- MessageToBlue( "Mission Command: Message to all airborne units! The " .. PatriotActivation[CurrentLandingZoneID][1] .. " is armed. Our air defenses are now stronger.", 60, "BLUE/PatriotDefense" ) --- MessageToRed( "Mission Command: Our satellite systems are detecting additional NATO air defenses. To all airborne units: Take care!!!", 60, "RED/PatriotDefense" ) --- Mission:GetTask( 2 ):AddGoalCompletion( "Patriots activated", PatriotActivation[CurrentLandingZoneID][1], 1 ) -- Register Patriot activation as part of mission goal. --- end --- end --- end --- end --- end --- end --- --- local Mission = MISSIONSCHEDULER.AddMission( 'NATO Transport Troops', 'Operational', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.', 'NATO' ) --- Mission:AddGoalFunction( DeployPatriotTroopsGoal ) -function MISSION:AddGoalFunction( GoalFunction ) - self:F() - self.GoalFunction = GoalFunction -end - ---- Register a new @{CLIENT} to participate within the mission. --- @param CLIENT Client is the @{CLIENT} object. The object must have been instantiated with @{CLIENT:New}. --- @return CLIENT --- @usage --- Add a number of Client objects to the Mission. --- Mission:AddClient( CLIENT:FindByName( 'US UH-1H*HOT-Deploy Troops 1', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'US UH-1H*RAMP-Deploy Troops 3', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'US UH-1H*HOT-Deploy Troops 2', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'US UH-1H*RAMP-Deploy Troops 4', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.' ):Transport() ) -function MISSION:AddClient( Client ) - self:F( { Client } ) - - local Valid = true - - if Valid then - self._Clients[Client.ClientName] = Client - end - - return Client -end - ---- Find a @{CLIENT} object within the @{MISSION} by its ClientName. --- @param CLIENT ClientName is a string defining the Client Group as defined within the ME. --- @return CLIENT --- @usage --- -- Seach for Client "Bomber" within the Mission. --- local BomberClient = Mission:FindClient( "Bomber" ) -function MISSION:FindClient( ClientName ) - self:F( { self._Clients[ClientName] } ) - return self._Clients[ClientName] -end - - ---- Register a @{TASK} to be completed within the @{MISSION}. Note that there can be multiple @{TASK}s registered to be completed. Each TASK can be set a certain Goal. The MISSION will not be completed until all Goals are reached. --- @param TASK Task is the @{TASK} object. The object must have been instantiated with @{TASK:New} or any of its inherited @{TASK}s. --- @param number TaskNumber is the sequence number of the TASK within the MISSION. This number does have to be chronological. --- @return TASK --- @usage --- -- Define a few tasks for the Mission. --- PickupZones = { "NATO Gold Pickup Zone", "NATO Titan Pickup Zone" } --- PickupSignalUnits = { "NATO Gold Coordination Center", "NATO Titan Coordination Center" } --- --- -- Assign the Pickup Task --- local PickupTask = PICKUPTASK:New( PickupZones, CARGO_TYPE.ENGINEERS, CLIENT.ONBOARDSIDE.LEFT ) --- PickupTask:AddSmokeBlue( PickupSignalUnits ) --- PickupTask:SetGoalTotal( 3 ) --- Mission:AddTask( PickupTask, 1 ) --- --- -- Assign the Deploy Task --- local PatriotActivationZones = { "US Patriot Battery 1 Activation", "US Patriot Battery 2 Activation", "US Patriot Battery 3 Activation" } --- local PatriotActivationZonesSmokeUnits = { "US SAM Patriot - Battery 1 Control", "US SAM Patriot - Battery 2 Control", "US SAM Patriot - Battery 3 Control" } --- local DeployTask = DEPLOYTASK:New( PatriotActivationZones, CARGO_TYPE.ENGINEERS ) --- --DeployTask:SetCargoTargetZoneName( 'US Troops Attack ' .. math.random(2) ) --- DeployTask:AddSmokeBlue( PatriotActivationZonesSmokeUnits ) --- DeployTask:SetGoalTotal( 3 ) --- DeployTask:SetGoalTotal( 3, "Patriots activated" ) --- Mission:AddTask( DeployTask, 2 ) - -function MISSION:AddTask( Task, TaskNumber ) - self:F() - - self._Tasks[TaskNumber] = Task - self._Tasks[TaskNumber]:EnableEvents() - self._Tasks[TaskNumber].ID = TaskNumber - - return Task - end - ---- Get the TASK idenified by the TaskNumber from the Mission. This function is useful in GoalFunctions. --- @param number TaskNumber is the number of the @{TASK} within the @{MISSION}. --- @return TASK --- @usage --- -- Get Task 2 from the Mission. --- Task2 = Mission:GetTask( 2 ) - -function MISSION:GetTask( TaskNumber ) - self:F() - - local Valid = true - - local Task = nil - - if type(TaskNumber) ~= "number" then - Valid = false - end - - if Valid then - Task = self._Tasks[TaskNumber] - end - - return Task -end - ---- Get all the TASKs from the Mission. This function is useful in GoalFunctions. --- @return {TASK,...} Structure of TASKS with the @{TASK} number as the key. --- @usage --- -- Get Tasks from the Mission. --- Tasks = Mission:GetTasks() --- env.info( "Task 2 Completion = " .. Tasks[2]:GetGoalPercentage() .. "%" ) -function MISSION:GetTasks() - self:F() - - return self._Tasks -end - - ---[[ - _TransportExecuteStage: Defines the different stages of Transport unload/load execution. This table is internal and is used to control the validity of Transport load/unload timing. - - - _TransportExecuteStage.EXECUTING - - _TransportExecuteStage.SUCCESS - - _TransportExecuteStage.FAILED - ---]] -_TransportExecuteStage = { - NONE = 0, - EXECUTING = 1, - SUCCESS = 2, - FAILED = 3 -} - - ---- The MISSIONSCHEDULER is an OBJECT and is the main scheduler of ALL active MISSIONs registered within this scheduler. It's workings are considered internal and is automatically created when the Mission.lua file is included. --- @type MISSIONSCHEDULER --- @field #MISSIONSCHEDULER.MISSIONS Missions -MISSIONSCHEDULER = { - Missions = {}, - MissionCount = 0, - TimeIntervalCount = 0, - TimeIntervalShow = 150, - TimeSeconds = 14400, - TimeShow = 5 -} - ---- @type MISSIONSCHEDULER.MISSIONS --- @list <#MISSION> Mission - ---- This is the main MISSIONSCHEDULER Scheduler function. It is considered internal and is automatically created when the Mission.lua file is included. -function MISSIONSCHEDULER.Scheduler() - - - -- loop through the missions in the TransportTasks - for MissionName, MissionData in pairs( MISSIONSCHEDULER.Missions ) do - - local Mission = MissionData -- #MISSION - - if not Mission:IsCompleted() then - - -- This flag will monitor if for this mission, there are clients alive. If this flag is still false at the end of the loop, the mission status will be set to Pending (if not Failed or Completed). - local ClientsAlive = false - - for ClientID, ClientData in pairs( Mission._Clients ) do - - local Client = ClientData -- Client#CLIENT - - if Client:IsAlive() then - - -- There is at least one Client that is alive... So the Mission status is set to Ongoing. - ClientsAlive = true - - -- If this Client was not registered as Alive before: - -- 1. We register the Client as Alive. - -- 2. We initialize the Client Tasks and make a link to the original Mission Task. - -- 3. We initialize the Cargos. - -- 4. We flag the Mission as Ongoing. - if not Client.ClientAlive then - Client.ClientAlive = true - Client.ClientBriefingShown = false - for TaskNumber, Task in pairs( Mission._Tasks ) do - -- Note that this a deepCopy. Each client must have their own Tasks with own Stages!!! - Client._Tasks[TaskNumber] = routines.utils.deepCopy( Mission._Tasks[TaskNumber] ) - -- Each MissionTask must point to the original Mission. - Client._Tasks[TaskNumber].MissionTask = Mission._Tasks[TaskNumber] - Client._Tasks[TaskNumber].Cargos = Mission._Tasks[TaskNumber].Cargos - Client._Tasks[TaskNumber].LandingZones = Mission._Tasks[TaskNumber].LandingZones - end - - Mission:Ongoing() - end - - - -- For each Client, check for each Task the state and evolve the mission. - -- This flag will indicate if the Task of the Client is Complete. - local TaskComplete = false - - for TaskNumber, Task in pairs( Client._Tasks ) do - - if not Task.Stage then - Task:SetStage( 1 ) - end - - - local TransportTime = timer.getTime() - - if not Task:IsDone() then - - if Task:Goal() then - Task:ShowGoalProgress( Mission, Client ) - end - - --env.info( 'Scheduler: Mission = ' .. Mission.Name .. ' / Client = ' .. Client.ClientName .. ' / Task = ' .. Task.Name .. ' / Stage = ' .. Task.ActiveStage .. ' - ' .. Task.Stage.Name .. ' - ' .. Task.Stage.StageType ) - - -- Action - if Task:StageExecute() then - Task.Stage:Execute( Mission, Client, Task ) - end - - -- Wait until execution is finished - if Task.ExecuteStage == _TransportExecuteStage.EXECUTING then - Task.Stage:Executing( Mission, Client, Task ) - end - - -- Validate completion or reverse to earlier stage - if Task.Time + Task.Stage.WaitTime <= TransportTime then - Task:SetStage( Task.Stage:Validate( Mission, Client, Task ) ) - end - - if Task:IsDone() then - --env.info( 'Scheduler: Mission '.. Mission.Name .. ' Task ' .. Task.Name .. ' Stage ' .. Task.Stage.Name .. ' done. TaskComplete = ' .. string.format ( "%s", TaskComplete and "true" or "false" ) ) - TaskComplete = true -- when a task is not yet completed, a mission cannot be completed - - else - -- break only if this task is not yet done, so that future task are not yet activated. - TaskComplete = false -- when a task is not yet completed, a mission cannot be completed - --env.info( 'Scheduler: Mission "'.. Mission.Name .. '" Task "' .. Task.Name .. '" Stage "' .. Task.Stage.Name .. '" break. TaskComplete = ' .. string.format ( "%s", TaskComplete and "true" or "false" ) ) - break - end - - if TaskComplete then - - if Mission.GoalFunction ~= nil then - Mission.GoalFunction( Mission, Client ) - end - if MISSIONSCHEDULER.Scoring then - MISSIONSCHEDULER.Scoring:_AddMissionTaskScore( Client:GetClientGroupDCSUnit(), Mission.Name, 25 ) - end - --- if not Mission:IsCompleted() then --- end - end - end - end - - local MissionComplete = true - for TaskNumber, Task in pairs( Mission._Tasks ) do - if Task:Goal() then --- Task:ShowGoalProgress( Mission, Client ) - if Task:IsGoalReached() then - else - MissionComplete = false - end - else - MissionComplete = false -- If there is no goal, the mission should never be ended. The goal status will be set somewhere else. - end - end - - if MissionComplete then - Mission:Completed() - if MISSIONSCHEDULER.Scoring then - MISSIONSCHEDULER.Scoring:_AddMissionScore( Mission.Name, 100 ) - end - else - if TaskComplete then - -- Reset for new tasking of active client - Client.ClientAlive = false -- Reset the client tasks. - end - end - - - else - if Client.ClientAlive then - env.info( 'Scheduler: Client "' .. Client.ClientName .. '" is inactive.' ) - Client.ClientAlive = false - - -- This is tricky. If we sanitize Client._Tasks before sanitizing Client._Tasks[TaskNumber].MissionTask, then the original MissionTask will be sanitized, and will be lost within the garbage collector. - -- So first sanitize Client._Tasks[TaskNumber].MissionTask, after that, sanitize only the whole _Tasks structure... - --Client._Tasks[TaskNumber].MissionTask = nil - --Client._Tasks = nil - end - end - end - - -- If all Clients of this Mission are not activated, then the Mission status needs to be put back into Pending status. - -- But only if the Mission was Ongoing. In case the Mission is Completed or Failed, the Mission status may not be changed. In these cases, this will be the last run of this Mission in the Scheduler. - if ClientsAlive == false then - if Mission:IsOngoing() then - -- Mission status back to pending... - Mission:Pending() - end - end - end - - Mission:StatusToClients() - - if Mission:ReportTrigger() then - Mission:ReportToAll() - end - end - - return true -end - ---- Start the MISSIONSCHEDULER. -function MISSIONSCHEDULER.Start() - if MISSIONSCHEDULER ~= nil then - --MISSIONSCHEDULER.SchedulerId = routines.scheduleFunction( MISSIONSCHEDULER.Scheduler, { }, 0, 2 ) - MISSIONSCHEDULER.SchedulerId = SCHEDULER:New( nil, MISSIONSCHEDULER.Scheduler, { }, 0, 2 ) - end -end - ---- Stop the MISSIONSCHEDULER. -function MISSIONSCHEDULER.Stop() - if MISSIONSCHEDULER.SchedulerId then - routines.removeFunction(MISSIONSCHEDULER.SchedulerId) - MISSIONSCHEDULER.SchedulerId = nil - end -end - ---- This is the main MISSION declaration method. Each Mission is like the master or a Mission orchestration between, Clients, Tasks, Stages etc. --- @param Mission is the MISSION object instantiated by @{MISSION:New}. --- @return MISSION --- @usage --- -- Declare a mission. --- Mission = MISSION:New( 'Russia Transport Troops SA-6', --- 'Operational', --- 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', --- 'Russia' ) --- MISSIONSCHEDULER:AddMission( Mission ) -function MISSIONSCHEDULER.AddMission( Mission ) - MISSIONSCHEDULER.Missions[Mission.Name] = Mission - MISSIONSCHEDULER.MissionCount = MISSIONSCHEDULER.MissionCount + 1 - -- Add an overall AI Client for the AI tasks... This AI Client will facilitate the Events in the background for each Task. - --MissionAdd:AddClient( CLIENT:Register( 'AI' ) ) - - return Mission -end - ---- Remove a MISSION from the MISSIONSCHEDULER. --- @param MissionName is the name of the MISSION given at declaration using @{AddMission}. --- @usage --- -- Declare a mission. --- Mission = MISSION:New( 'Russia Transport Troops SA-6', --- 'Operational', --- 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', --- 'Russia' ) --- MISSIONSCHEDULER:AddMission( Mission ) --- --- -- Now remove the Mission. --- MISSIONSCHEDULER:RemoveMission( 'Russia Transport Troops SA-6' ) -function MISSIONSCHEDULER.RemoveMission( MissionName ) - MISSIONSCHEDULER.Missions[MissionName] = nil - MISSIONSCHEDULER.MissionCount = MISSIONSCHEDULER.MissionCount - 1 -end - ---- Find a MISSION within the MISSIONSCHEDULER. --- @param MissionName is the name of the MISSION given at declaration using @{AddMission}. --- @return MISSION --- @usage --- -- Declare a mission. --- Mission = MISSION:New( 'Russia Transport Troops SA-6', --- 'Operational', --- 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', --- 'Russia' ) --- MISSIONSCHEDULER:AddMission( Mission ) --- --- -- Now find the Mission. --- MissionFind = MISSIONSCHEDULER:FindMission( 'Russia Transport Troops SA-6' ) -function MISSIONSCHEDULER.FindMission( MissionName ) - return MISSIONSCHEDULER.Missions[MissionName] -end - --- Internal function used by the MISSIONSCHEDULER menu. -function MISSIONSCHEDULER.ReportMissionsShow( ) - for MissionName, Mission in pairs( MISSIONSCHEDULER.Missions ) do - Mission.MissionReportShow = true - Mission.MissionReportFlash = false - end -end - --- Internal function used by the MISSIONSCHEDULER menu. -function MISSIONSCHEDULER.ReportMissionsFlash( TimeInterval ) - local Count = 0 - for MissionName, Mission in pairs( MISSIONSCHEDULER.Missions ) do - Mission.MissionReportShow = false - Mission.MissionReportFlash = true - Mission.MissionReportTrigger = timer.getTime() + Count * TimeInterval - Mission.MissionTimeInterval = MISSIONSCHEDULER.MissionCount * TimeInterval - env.info( "TimeInterval = " .. Mission.MissionTimeInterval ) - Count = Count + 1 - end -end - --- Internal function used by the MISSIONSCHEDULER menu. -function MISSIONSCHEDULER.ReportMissionsHide( Prm ) - for MissionName, Mission in pairs( MISSIONSCHEDULER.Missions ) do - Mission.MissionReportShow = false - Mission.MissionReportFlash = false - end -end - ---- Enables a MENU option in the communications menu under F10 to control the status of the active missions. --- This function should be called only once when starting the MISSIONSCHEDULER. -function MISSIONSCHEDULER.ReportMenu() - local ReportMenu = SUBMENU:New( 'Status' ) - local ReportMenuShow = COMMANDMENU:New( 'Show Report Missions', ReportMenu, MISSIONSCHEDULER.ReportMissionsShow, 0 ) - local ReportMenuFlash = COMMANDMENU:New('Flash Report Missions', ReportMenu, MISSIONSCHEDULER.ReportMissionsFlash, 120 ) - local ReportMenuHide = COMMANDMENU:New( 'Hide Report Missions', ReportMenu, MISSIONSCHEDULER.ReportMissionsHide, 0 ) -end - ---- Show the remaining mission time. -function MISSIONSCHEDULER:TimeShow() - self.TimeIntervalCount = self.TimeIntervalCount + 1 - if self.TimeIntervalCount >= self.TimeTriggerShow then - local TimeMsg = string.format("%00d", ( self.TimeSeconds / 60 ) - ( timer.getTime() / 60 )) .. ' minutes left until mission reload.' - MESSAGE:New( TimeMsg, self.TimeShow, "Mission time" ):ToAll() - self.TimeIntervalCount = 0 - end -end - -function MISSIONSCHEDULER:Time( TimeSeconds, TimeIntervalShow, TimeShow ) - - self.TimeIntervalCount = 0 - self.TimeSeconds = TimeSeconds - self.TimeIntervalShow = TimeIntervalShow - self.TimeShow = TimeShow -end - ---- Adds a mission scoring to the game. -function MISSIONSCHEDULER:Scoring( Scoring ) - - self.Scoring = Scoring -end - ---- The CLEANUP class keeps an area clean of crashing or colliding airplanes. It also prevents airplanes from firing within this area. --- @module CleanUp --- @author Flightcontrol - - - - - - - ---- The CLEANUP class. --- @type CLEANUP --- @extends Base#BASE -CLEANUP = { - ClassName = "CLEANUP", - ZoneNames = {}, - TimeInterval = 300, - CleanUpList = {}, -} - ---- Creates the main object which is handling the cleaning of the debris within the given Zone Names. --- @param #CLEANUP self --- @param #table ZoneNames Is a table of zone names where the debris should be cleaned. Also a single string can be passed with one zone name. --- @param #number TimeInterval The interval in seconds when the clean activity takes place. The default is 300 seconds, thus every 5 minutes. --- @return #CLEANUP --- @usage --- -- Clean these Zones. --- CleanUpAirports = CLEANUP:New( { 'CLEAN Tbilisi', 'CLEAN Kutaisi' }, 150 ) --- or --- CleanUpTbilisi = CLEANUP:New( 'CLEAN Tbilisi', 150 ) --- CleanUpKutaisi = CLEANUP:New( 'CLEAN Kutaisi', 600 ) -function CLEANUP:New( ZoneNames, TimeInterval ) local self = BASE:Inherit( self, BASE:New() ) - self:F( { ZoneNames, TimeInterval } ) - - if type( ZoneNames ) == 'table' then - self.ZoneNames = ZoneNames - else - self.ZoneNames = { ZoneNames } - end - if TimeInterval then - self.TimeInterval = TimeInterval - end - - _EVENTDISPATCHER:OnBirth( self._OnEventBirth, self ) - - --self.CleanUpScheduler = routines.scheduleFunction( self._CleanUpScheduler, { self }, timer.getTime() + 1, TimeInterval ) - self.CleanUpScheduler = SCHEDULER:New( self, self._CleanUpScheduler, {}, 1, TimeInterval ) - - return self -end - - ---- Destroys a group from the simulator, but checks first if it is still existing! --- @param #CLEANUP self --- @param DCSGroup#Group GroupObject The object to be destroyed. --- @param #string CleanUpGroupName The groupname... -function CLEANUP:_DestroyGroup( GroupObject, CleanUpGroupName ) - self:F( { GroupObject, CleanUpGroupName } ) - - if GroupObject then -- and GroupObject:isExist() then - trigger.action.deactivateGroup(GroupObject) - self:T( { "GroupObject Destroyed", GroupObject } ) - end -end - ---- Destroys a @{DCSUnit#Unit} from the simulator, but checks first if it is still existing! --- @param #CLEANUP self --- @param DCSUnit#Unit CleanUpUnit The object to be destroyed. --- @param #string CleanUpUnitName The Unit name ... -function CLEANUP:_DestroyUnit( CleanUpUnit, CleanUpUnitName ) - self:F( { CleanUpUnit, CleanUpUnitName } ) - - if CleanUpUnit then - local CleanUpGroup = Unit.getGroup(CleanUpUnit) - -- TODO Client bug in 1.5.3 - if CleanUpGroup and CleanUpGroup:isExist() then - local CleanUpGroupUnits = CleanUpGroup:getUnits() - if #CleanUpGroupUnits == 1 then - local CleanUpGroupName = CleanUpGroup:getName() - --self:CreateEventCrash( timer.getTime(), CleanUpUnit ) - CleanUpGroup:destroy() - self:T( { "Destroyed Group:", CleanUpGroupName } ) - else - CleanUpUnit:destroy() - self:T( { "Destroyed Unit:", CleanUpUnitName } ) - end - self.CleanUpList[CleanUpUnitName] = nil -- Cleaning from the list - CleanUpUnit = nil - end - end -end - --- TODO check DCSTypes#Weapon ---- Destroys a missile from the simulator, but checks first if it is still existing! --- @param #CLEANUP self --- @param DCSTypes#Weapon MissileObject -function CLEANUP:_DestroyMissile( MissileObject ) - self:F( { MissileObject } ) - - if MissileObject and MissileObject:isExist() then - MissileObject:destroy() - self:T( "MissileObject Destroyed") - end -end - -function CLEANUP:_OnEventBirth( Event ) - self:F( { Event } ) - - self.CleanUpList[Event.IniDCSUnitName] = {} - self.CleanUpList[Event.IniDCSUnitName].CleanUpUnit = Event.IniDCSUnit - self.CleanUpList[Event.IniDCSUnitName].CleanUpGroup = Event.IniDCSGroup - self.CleanUpList[Event.IniDCSUnitName].CleanUpGroupName = Event.IniDCSGroupName - self.CleanUpList[Event.IniDCSUnitName].CleanUpUnitName = Event.IniDCSUnitName - - _EVENTDISPATCHER:OnEngineShutDownForUnit( Event.IniDCSUnitName, self._EventAddForCleanUp, self ) - _EVENTDISPATCHER:OnEngineStartUpForUnit( Event.IniDCSUnitName, self._EventAddForCleanUp, self ) - _EVENTDISPATCHER:OnHitForUnit( Event.IniDCSUnitName, self._EventAddForCleanUp, self ) - _EVENTDISPATCHER:OnPilotDeadForUnit( Event.IniDCSUnitName, self._EventCrash, self ) - _EVENTDISPATCHER:OnDeadForUnit( Event.IniDCSUnitName, self._EventCrash, self ) - _EVENTDISPATCHER:OnCrashForUnit( Event.IniDCSUnitName, self._EventCrash, self ) - _EVENTDISPATCHER:OnShotForUnit( Event.IniDCSUnitName, self._EventShot, self ) - - --self:AddEvent( world.event.S_EVENT_ENGINE_SHUTDOWN, self._EventAddForCleanUp ) - --self:AddEvent( world.event.S_EVENT_ENGINE_STARTUP, self._EventAddForCleanUp ) --- self:AddEvent( world.event.S_EVENT_HIT, self._EventAddForCleanUp ) -- , self._EventHitCleanUp ) --- self:AddEvent( world.event.S_EVENT_CRASH, self._EventCrash ) -- , self._EventHitCleanUp ) --- --self:AddEvent( world.event.S_EVENT_DEAD, self._EventCrash ) --- self:AddEvent( world.event.S_EVENT_SHOT, self._EventShot ) --- --- self:EnableEvents() - - -end - ---- Detects if a crash event occurs. --- Crashed units go into a CleanUpList for removal. --- @param #CLEANUP self --- @param DCSTypes#Event event -function CLEANUP:_EventCrash( Event ) - self:F( { Event } ) - - --TODO: This stuff is not working due to a DCS bug. Burning units cannot be destroyed. - -- self:T("before getGroup") - -- local _grp = Unit.getGroup(event.initiator)-- Identify the group that fired - -- self:T("after getGroup") - -- _grp:destroy() - -- self:T("after deactivateGroup") - -- event.initiator:destroy() - - self.CleanUpList[Event.IniDCSUnitName] = {} - self.CleanUpList[Event.IniDCSUnitName].CleanUpUnit = Event.IniDCSUnit - self.CleanUpList[Event.IniDCSUnitName].CleanUpGroup = Event.IniDCSGroup - self.CleanUpList[Event.IniDCSUnitName].CleanUpGroupName = Event.IniDCSGroupName - self.CleanUpList[Event.IniDCSUnitName].CleanUpUnitName = Event.IniDCSUnitName - -end - ---- Detects if a unit shoots a missile. --- If this occurs within one of the zones, then the weapon used must be destroyed. --- @param #CLEANUP self --- @param DCSTypes#Event event -function CLEANUP:_EventShot( Event ) - self:F( { Event } ) - - -- Test if the missile was fired within one of the CLEANUP.ZoneNames. - local CurrentLandingZoneID = 0 - CurrentLandingZoneID = routines.IsUnitInZones( Event.IniDCSUnit, self.ZoneNames ) - if ( CurrentLandingZoneID ) then - -- Okay, the missile was fired within the CLEANUP.ZoneNames, destroy the fired weapon. - --_SEADmissile:destroy() - --routines.scheduleFunction( CLEANUP._DestroyMissile, { self, Event.Weapon }, timer.getTime() + 0.1) - SCHEDULER:New( self, CLEANUP._DestroyMissile, { Event.Weapon }, 0.1 ) - end -end - - ---- Detects if the Unit has an S_EVENT_HIT within the given ZoneNames. If this is the case, destroy the unit. --- @param #CLEANUP self --- @param DCSTypes#Event event -function CLEANUP:_EventHitCleanUp( Event ) - self:F( { Event } ) - - if Event.IniDCSUnit then - if routines.IsUnitInZones( Event.IniDCSUnit, self.ZoneNames ) ~= nil then - self:T( { "Life: ", Event.IniDCSUnitName, ' = ', Event.IniDCSUnit:getLife(), "/", Event.IniDCSUnit:getLife0() } ) - if Event.IniDCSUnit:getLife() < Event.IniDCSUnit:getLife0() then - self:T( "CleanUp: Destroy: " .. Event.IniDCSUnitName ) - --routines.scheduleFunction( CLEANUP._DestroyUnit, { self, Event.IniDCSUnit }, timer.getTime() + 0.1) - SCHEDULER:New( self, CLEANUP._DestroyUnit, { Event.IniDCSUnit }, 0.1 ) - end - end - end - - if Event.TgtDCSUnit then - if routines.IsUnitInZones( Event.TgtDCSUnit, self.ZoneNames ) ~= nil then - self:T( { "Life: ", Event.TgtDCSUnitName, ' = ', Event.TgtDCSUnit:getLife(), "/", Event.TgtDCSUnit:getLife0() } ) - if Event.TgtDCSUnit:getLife() < Event.TgtDCSUnit:getLife0() then - self:T( "CleanUp: Destroy: " .. Event.TgtDCSUnitName ) - --routines.scheduleFunction( CLEANUP._DestroyUnit, { self, Event.TgtDCSUnit }, timer.getTime() + 0.1 ) - SCHEDULER:New( self, CLEANUP._DestroyUnit, { Event.TgtDCSUnit }, 0.1 ) - end - end - end -end - ---- Add the @{DCSUnit#Unit} to the CleanUpList for CleanUp. -function CLEANUP:_AddForCleanUp( CleanUpUnit, CleanUpUnitName ) - self:F( { CleanUpUnit, CleanUpUnitName } ) - - self.CleanUpList[CleanUpUnitName] = {} - self.CleanUpList[CleanUpUnitName].CleanUpUnit = CleanUpUnit - self.CleanUpList[CleanUpUnitName].CleanUpUnitName = CleanUpUnitName - self.CleanUpList[CleanUpUnitName].CleanUpGroup = Unit.getGroup(CleanUpUnit) - self.CleanUpList[CleanUpUnitName].CleanUpGroupName = Unit.getGroup(CleanUpUnit):getName() - self.CleanUpList[CleanUpUnitName].CleanUpTime = timer.getTime() - self.CleanUpList[CleanUpUnitName].CleanUpMoved = false - - self:T( { "CleanUp: Add to CleanUpList: ", Unit.getGroup(CleanUpUnit):getName(), CleanUpUnitName } ) - -end - ---- Detects if the Unit has an S_EVENT_ENGINE_SHUTDOWN or an S_EVENT_HIT within the given ZoneNames. If this is the case, add the Group to the CLEANUP List. --- @param #CLEANUP self --- @param DCSTypes#Event event -function CLEANUP:_EventAddForCleanUp( Event ) - - if Event.IniDCSUnit then - if self.CleanUpList[Event.IniDCSUnitName] == nil then - if routines.IsUnitInZones( Event.IniDCSUnit, self.ZoneNames ) ~= nil then - self:_AddForCleanUp( Event.IniDCSUnit, Event.IniDCSUnitName ) - end - end - end - - if Event.TgtDCSUnit then - if self.CleanUpList[Event.TgtDCSUnitName] == nil then - if routines.IsUnitInZones( Event.TgtDCSUnit, self.ZoneNames ) ~= nil then - self:_AddForCleanUp( Event.TgtDCSUnit, Event.TgtDCSUnitName ) - end - end - end - -end - -local CleanUpSurfaceTypeText = { - "LAND", - "SHALLOW_WATER", - "WATER", - "ROAD", - "RUNWAY" - } - ---- At the defined time interval, CleanUp the Groups within the CleanUpList. --- @param #CLEANUP self -function CLEANUP:_CleanUpScheduler() - self:F( { "CleanUp Scheduler" } ) - - local CleanUpCount = 0 - for CleanUpUnitName, UnitData in pairs( self.CleanUpList ) do - CleanUpCount = CleanUpCount + 1 - - self:T( { CleanUpUnitName, UnitData } ) - local CleanUpUnit = Unit.getByName(UnitData.CleanUpUnitName) - local CleanUpGroupName = UnitData.CleanUpGroupName - local CleanUpUnitName = UnitData.CleanUpUnitName - if CleanUpUnit then - self:T( { "CleanUp Scheduler", "Checking:", CleanUpUnitName } ) - if _DATABASE:GetStatusGroup( CleanUpGroupName ) ~= "ReSpawn" then - local CleanUpUnitVec3 = CleanUpUnit:getPoint() - --self:T( CleanUpUnitVec3 ) - local CleanUpUnitVec2 = {} - CleanUpUnitVec2.x = CleanUpUnitVec3.x - CleanUpUnitVec2.y = CleanUpUnitVec3.z - --self:T( CleanUpUnitVec2 ) - local CleanUpSurfaceType = land.getSurfaceType(CleanUpUnitVec2) - --self:T( CleanUpSurfaceType ) - - if CleanUpUnit and CleanUpUnit:getLife() <= CleanUpUnit:getLife0() * 0.95 then - if CleanUpSurfaceType == land.SurfaceType.RUNWAY then - if CleanUpUnit:inAir() then - local CleanUpLandHeight = land.getHeight(CleanUpUnitVec2) - local CleanUpUnitHeight = CleanUpUnitVec3.y - CleanUpLandHeight - self:T( { "CleanUp Scheduler", "Height = " .. CleanUpUnitHeight } ) - if CleanUpUnitHeight < 30 then - self:T( { "CleanUp Scheduler", "Destroy " .. CleanUpUnitName .. " because below safe height and damaged." } ) - self:_DestroyUnit(CleanUpUnit, CleanUpUnitName) - end - else - self:T( { "CleanUp Scheduler", "Destroy " .. CleanUpUnitName .. " because on runway and damaged." } ) - self:_DestroyUnit(CleanUpUnit, CleanUpUnitName) - end - end - end - -- Clean Units which are waiting for a very long time in the CleanUpZone. - if CleanUpUnit then - local CleanUpUnitVelocity = CleanUpUnit:getVelocity() - local CleanUpUnitVelocityTotal = math.abs(CleanUpUnitVelocity.x) + math.abs(CleanUpUnitVelocity.y) + math.abs(CleanUpUnitVelocity.z) - if CleanUpUnitVelocityTotal < 1 then - if UnitData.CleanUpMoved then - if UnitData.CleanUpTime + 180 <= timer.getTime() then - self:T( { "CleanUp Scheduler", "Destroy due to not moving anymore " .. CleanUpUnitName } ) - self:_DestroyUnit(CleanUpUnit, CleanUpUnitName) - end - end - else - UnitData.CleanUpTime = timer.getTime() - UnitData.CleanUpMoved = true - end - end - - else - -- Do nothing ... - self.CleanUpList[CleanUpUnitName] = nil -- Not anymore in the DCSRTE - end - else - self:T( "CleanUp: Group " .. CleanUpUnitName .. " cannot be found in DCS RTE, removing ..." ) - self.CleanUpList[CleanUpUnitName] = nil -- Not anymore in the DCSRTE - end - end - self:T(CleanUpCount) - - return true -end - ---- This module contains the SPAWN class. --- --- 1) @{Spawn#SPAWN} class, extends @{Base#BASE} --- ============================================= --- The @{#SPAWN} class allows to spawn dynamically new groups, based on pre-defined initialization settings, modifying the behaviour when groups are spawned. --- For each group to be spawned, within the mission editor, a group has to be created with the "late activation flag" set. We call this group the *"Spawn Template"* of the SPAWN object. --- A reference to this Spawn Template needs to be provided when constructing the SPAWN object, by indicating the name of the group within the mission editor in the constructor methods. --- --- Within the SPAWN object, there is an internal index that keeps track of which group from the internal group list was spawned. --- When new groups get spawned by using the SPAWN functions (see below), it will be validated whether the Limits (@{#SPAWN.Limit}) of the SPAWN object are not reached. --- When all is valid, a new group will be created by the spawning methods, and the internal index will be increased with 1. --- --- Regarding the name of new spawned groups, a _SpawnPrefix_ will be assigned for each new group created. --- If you want to have the Spawn Template name to be used as the _SpawnPrefix_ name, use the @{#SPAWN.New} constructor. --- However, when the @{#SPAWN.NewWithAlias} constructor was used, the Alias name will define the _SpawnPrefix_ name. --- Groups will follow the following naming structure when spawned at run-time: --- --- 1. Spawned groups will have the name _SpawnPrefix_#ggg, where ggg is a counter from 0 to 999. --- 2. Spawned units will have the name _SpawnPrefix_#ggg-uu, where uu is a counter from 0 to 99 for each new spawned unit belonging to the group. --- --- Some additional notes that need to be remembered: --- --- * Templates are actually groups defined within the mission editor, with the flag "Late Activation" set. As such, these groups are never used within the mission, but are used by the @{#SPAWN} module. --- * It is important to defined BEFORE you spawn new groups, a proper initialization of the SPAWN instance is done with the options you want to use. --- * When designing a mission, NEVER name groups using a "#" within the name of the group Spawn Template(s), or the SPAWN module logic won't work anymore. --- --- 1.1) SPAWN construction methods --- ------------------------------- --- Create a new SPAWN object with the @{#SPAWN.New} or the @{#SPAWN.NewWithAlias} methods: --- --- * @{#SPAWN.New}: Creates a new SPAWN object taking the name of the group that functions as the Template. --- --- 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 functions will modify this list of groups so that when a group gets spawned, ALL information is already prepared when spawning. This is done for performance reasons. --- So in principle, the group list will contain all parameters and configurations after initialization, and when groups get actually spawned, this spawning can be done quickly and efficient. --- --- 1.2) SPAWN initialization methods --- --------------------------------- --- A spawn object will behave differently based on the usage of initialization methods: --- --- * @{#SPAWN.Limit}: Limits the amount of groups that can be alive at the same time and that can be dynamically spawned. --- * @{#SPAWN.RandomizeRoute}: Randomize the routes of spawned groups. --- * @{#SPAWN.RandomizeTemplate}: Randomize the group templates so that when a new group is spawned, a random group template is selected from one of the templates defined. --- * @{#SPAWN.Uncontrolled}: Spawn plane groups uncontrolled. --- * @{#SPAWN.Array}: Make groups visible before they are actually activated, and order these groups like a batallion in an array. --- * @{#SPAWN.InitRepeat}: Re-spawn groups when they land at the home base. Similar functions are @{#SPAWN.InitRepeatOnLanding} and @{#SPAWN.InitRepeatOnEngineShutDown}. --- --- 1.3) SPAWN spawning methods --- --------------------------- --- Groups can be spawned at different times and methods: --- --- * @{#SPAWN.Spawn}: Spawn one new group based on the last spawned index. --- * @{#SPAWN.ReSpawn}: Re-spawn a group based on a given index. --- * @{#SPAWN.SpawnScheduled}: Spawn groups at scheduled but randomized intervals. You can use @{#SPAWN.SpawnScheduleStart} and @{#SPAWN.SpawnScheduleStop} to start and stop the schedule respectively. --- * @{#SPAWN.SpawnFromUnit}: Spawn a new group taking the position of a @{UNIT}. --- * @{#SPAWN.SpawnInZone}: Spawn a new group in a @{ZONE}. --- --- Note that @{#SPAWN.Spawn} and @{#SPAWN.ReSpawn} return a @{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. --- --- 1.4) SPAWN object cleaning --- -------------------------- --- Sometimes, it will occur during a mission run-time, that ground or especially air objects get damaged, and will while being damged stop their activities, while remaining alive. --- In such cases, the SPAWN object will just sit there and wait until that group gets destroyed, but most of the time it won't, --- and it may occur that no new groups are or can be spawned as limits are reached. --- To prevent this, a @{#SPAWN.CleanUp} initialization method has been defined that will silently monitor the status of each spawned group. --- Once a group has a velocity = 0, and has been waiting for a defined interval, that group will be cleaned or removed from run-time. --- There is a catch however :-) If a damaged group has returned to an airbase within the coalition, that group will not be considered as "lost"... --- In such a case, when the inactive group is cleaned, a new group will Re-spawned automatically. --- This models AI that has succesfully returned to their airbase, to restart their combat activities. --- Check the @{#SPAWN.CleanUp} for further info. --- --- --- @module Spawn --- @author FlightControl - ---- SPAWN Class --- @type SPAWN --- @extends Base#BASE --- @field ClassName --- @field #string SpawnTemplatePrefix --- @field #string SpawnAliasPrefix -SPAWN = { - ClassName = "SPAWN", - SpawnTemplatePrefix = nil, - SpawnAliasPrefix = nil, -} - - - ---- Creates the main object to spawn a GROUP defined in the DCS ME. --- @param #SPAWN self --- @param #string SpawnTemplatePrefix is the name of the Group in the ME that defines the Template. Each new group will have the name starting with SpawnTemplatePrefix. --- @return #SPAWN --- @usage --- -- NATO helicopters engaging in the battle field. --- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ) --- @usage local Plane = SPAWN:New( "Plane" ) -- Creates a new local variable that can initiate new planes with the name "Plane#ddd" using the template "Plane" as defined within the ME. -function SPAWN:New( SpawnTemplatePrefix ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( { SpawnTemplatePrefix } ) - - local TemplateGroup = Group.getByName( SpawnTemplatePrefix ) - if TemplateGroup then - self.SpawnTemplatePrefix = SpawnTemplatePrefix - self.SpawnIndex = 0 - self.SpawnCount = 0 -- The internal counter of the amount of spawning the has happened since SpawnStart. - self.AliveUnits = 0 -- Contains the counter how many units are currently alive - self.SpawnIsScheduled = false -- Reflects if the spawning for this SpawnTemplatePrefix is going to be scheduled or not. - self.SpawnTemplate = self._GetTemplate( self, SpawnTemplatePrefix ) -- Contains the template structure for a Group Spawn from the Mission Editor. Note that this group must have lateActivation always on!!! - self.Repeat = false -- Don't repeat the group from Take-Off till Landing and back Take-Off by ReSpawning. - self.UnControlled = false -- When working in UnControlled mode, all planes are Spawned in UnControlled mode before the scheduler starts. - self.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.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. - else - error( "SPAWN:New: There is no group declared in the mission editor with SpawnTemplatePrefix = '" .. SpawnTemplatePrefix .. "'" ) - end - - return self -end - ---- Creates a new SPAWN instance to create new groups based on the defined template and using a new alias for each new group. --- @param #SPAWN self --- @param #string SpawnTemplatePrefix is the name of the Group in the ME that defines the Template. --- @param #string SpawnAliasPrefix is the name that will be given to the Group at runtime. --- @return #SPAWN --- @usage --- -- NATO helicopters engaging in the battle field. --- Spawn_BE_KA50 = SPAWN:NewWithAlias( 'BE KA-50@RAMP-Ground Defense', 'Helicopter Attacking a City' ) --- @usage local PlaneWithAlias = SPAWN:NewWithAlias( "Plane", "Bomber" ) -- Creates a new local variable that can instantiate new planes with the name "Bomber#ddd" using the template "Plane" as defined within the ME. -function SPAWN:NewWithAlias( SpawnTemplatePrefix, SpawnAliasPrefix ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( { SpawnTemplatePrefix, SpawnAliasPrefix } ) - - local TemplateGroup = Group.getByName( SpawnTemplatePrefix ) - if TemplateGroup then - self.SpawnTemplatePrefix = SpawnTemplatePrefix - self.SpawnAliasPrefix = SpawnAliasPrefix - self.SpawnIndex = 0 - self.SpawnCount = 0 -- The internal counter of the amount of spawning the has happened since SpawnStart. - self.AliveUnits = 0 -- Contains the counter how many units are currently alive - self.SpawnIsScheduled = false -- Reflects if the spawning for this SpawnTemplatePrefix is going to be scheduled or not. - self.SpawnTemplate = self._GetTemplate( self, SpawnTemplatePrefix ) -- Contains the template structure for a Group Spawn from the Mission Editor. Note that this group must have lateActivation always on!!! - self.Repeat = false -- Don't repeat the group from Take-Off till Landing and back Take-Off by ReSpawning. - self.UnControlled = false -- When working in UnControlled mode, all planes are Spawned in UnControlled mode before the scheduler starts. - self.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.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. - else - error( "SPAWN:New: There is no group declared in the mission editor with SpawnTemplatePrefix = '" .. SpawnTemplatePrefix .. "'" ) - end - - 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 function should be used... --- When a @{#SPAWN.New} is executed and the limit of the amount of units alive is reached, then no new spawn will happen of the group, until some of these units of the spawn object will be destroyed. --- @param #SPAWN self --- @param #number SpawnMaxUnitsAlive The maximum amount of units that can be alive at runtime. --- @param #number SpawnMaxGroups The maximum amount of groups that can be spawned. When the limit is reached, then no more actual spawns will happen of the group. --- This parameter is useful to define a maximum amount of airplanes, ground troops, helicopters, ships etc within a supply area. --- This parameter accepts the value 0, which defines that there are no maximum group limits, but there are limits on the maximum of units that can be alive at the same time. --- @return #SPAWN self --- @usage --- -- NATO helicopters engaging in the battle field. --- -- This helicopter group consists of one Unit. So, this group will SPAWN maximum 2 groups simultaneously within the DCSRTE. --- -- There will be maximum 24 groups spawned during the whole mission lifetime. --- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):Limit( 2, 24 ) -function SPAWN:Limit( SpawnMaxUnitsAlive, SpawnMaxGroups ) - self:F( { self.SpawnTemplatePrefix, SpawnMaxUnitsAlive, SpawnMaxGroups } ) - - self.SpawnMaxUnitsAlive = SpawnMaxUnitsAlive -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time. - self.SpawnMaxGroups = SpawnMaxGroups -- The maximum amount of groups that can be spawned. - - for SpawnGroupID = 1, self.SpawnMaxGroups do - self:_InitializeSpawnGroups( SpawnGroupID ) - end - - return self -end - - ---- Randomizes the defined route of the SpawnTemplatePrefix group in the ME. This is very useful to define extra variation of the behaviour of groups. --- @param #SPAWN self --- @param #number SpawnStartPoint is the waypoint where the randomization begins. --- Note that the StartPoint = 0 equaling the point where the group is spawned. --- @param #number SpawnEndPoint is the waypoint where the randomization ends counting backwards. --- This parameter is useful to avoid randomization to end at a waypoint earlier than the last waypoint on the route. --- @param #number SpawnRadius is the radius in meters in which the randomization of the new waypoints, with the original waypoint of the original template located in the middle ... --- @return #SPAWN --- @usage --- -- NATO helicopters engaging in the battle field. --- -- The KA-50 has waypoints Start point ( =0 or SP ), 1, 2, 3, 4, End point (= 5 or DP). --- -- Waypoints 2 and 3 will only be randomized. The others will remain on their original position with each new spawn of the helicopter. --- -- The randomization of waypoint 2 and 3 will take place within a radius of 2000 meters. --- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):RandomizeRoute( 2, 2, 2000 ) -function SPAWN:RandomizeRoute( SpawnStartPoint, SpawnEndPoint, SpawnRadius ) - self:F( { self.SpawnTemplatePrefix, SpawnStartPoint, SpawnEndPoint, SpawnRadius } ) - - self.SpawnRandomizeRoute = true - self.SpawnRandomizeRouteStartPoint = SpawnStartPoint - self.SpawnRandomizeRouteEndPoint = SpawnEndPoint - self.SpawnRandomizeRouteRadius = SpawnRadius - - for GroupID = 1, self.SpawnMaxGroups do - self:_RandomizeRoute( GroupID ) - end - - return self -end - - ---- This function is rather complicated to understand. But I'll try to explain. --- This function becomes useful when you need to spawn groups with random templates of groups defined within the mission editor, --- but they will all follow the same Template route and have the same prefix name. --- In other words, this method randomizes between a defined set of groups the template to be used for each new spawn of a group. --- @param #SPAWN self --- @param #string SpawnTemplatePrefixTable A table with the names of the groups defined within the mission editor, from which one will be choosen when a new group will be spawned. --- @return #SPAWN --- @usage --- -- NATO Tank Platoons invading Gori. --- -- Choose between 13 different 'US Tank Platoon' configurations for each new SPAWN the Group to be spawned for the --- -- 'US Tank Platoon Left', 'US Tank Platoon Middle' and 'US Tank Platoon Right' SpawnTemplatePrefixes. --- -- Each new SPAWN will randomize the route, with a defined time interval of 200 seconds with 40% time variation (randomization) and --- -- with a limit set of maximum 12 Units alive simulteneously and 150 Groups to be spawned during the whole mission. --- Spawn_US_Platoon = { 'US Tank Platoon 1', 'US Tank Platoon 2', 'US Tank Platoon 3', 'US Tank Platoon 4', 'US Tank Platoon 5', --- 'US Tank Platoon 6', 'US Tank Platoon 7', 'US Tank Platoon 8', 'US Tank Platoon 9', 'US Tank Platoon 10', --- 'US Tank Platoon 11', 'US Tank Platoon 12', 'US Tank Platoon 13' } --- Spawn_US_Platoon_Left = SPAWN:New( 'US Tank Platoon Left' ):Limit( 12, 150 ):Schedule( 200, 0.4 ):RandomizeTemplate( Spawn_US_Platoon ):RandomizeRoute( 3, 3, 2000 ) --- Spawn_US_Platoon_Middle = SPAWN:New( 'US Tank Platoon Middle' ):Limit( 12, 150 ):Schedule( 200, 0.4 ):RandomizeTemplate( Spawn_US_Platoon ):RandomizeRoute( 3, 3, 2000 ) --- Spawn_US_Platoon_Right = SPAWN:New( 'US Tank Platoon Right' ):Limit( 12, 150 ):Schedule( 200, 0.4 ):RandomizeTemplate( Spawn_US_Platoon ):RandomizeRoute( 3, 3, 2000 ) -function SPAWN:RandomizeTemplate( SpawnTemplatePrefixTable ) - self:F( { self.SpawnTemplatePrefix, SpawnTemplatePrefixTable } ) - - self.SpawnTemplatePrefixTable = SpawnTemplatePrefixTable - self.SpawnRandomizeTemplate = true - - for SpawnGroupID = 1, self.SpawnMaxGroups do - self:_RandomizeTemplate( SpawnGroupID ) - end - - return self -end - - - - - ---- For planes and helicopters, when these groups go home and land on their home airbases and farps, they normally would taxi to the parking spot, shut-down their engines and wait forever until the Group is removed by the runtime environment. --- This function is used to re-spawn automatically (so no extra call is needed anymore) the same group after it has landed. --- This will enable a spawned group to be re-spawned after it lands, until it is destroyed... --- Note: When the group is respawned, it will re-spawn from the original airbase where it took off. --- So ensure that the routes for groups that respawn, always return to the original airbase, or players may get confused ... --- @param #SPAWN self --- @return #SPAWN self --- @usage --- -- RU Su-34 - AI Ship Attack --- -- Re-SPAWN the Group(s) after each landing and Engine Shut-Down automatically. --- SpawnRU_SU34 = SPAWN:New( 'TF1 RU Su-34 Krymsk@AI - Attack Ships' ):Schedule( 2, 3, 1800, 0.4 ):SpawnUncontrolled():RandomizeRoute( 1, 1, 3000 ):RepeatOnEngineShutDown() -function SPAWN:InitRepeat() - self:F( { self.SpawnTemplatePrefix, self.SpawnIndex } ) - - self.Repeat = true - self.RepeatOnEngineShutDown = false - self.RepeatOnLanding = true - - return self -end - ---- Respawn group after landing. --- @param #SPAWN self --- @return #SPAWN self -function SPAWN:InitRepeatOnLanding() - self:F( { self.SpawnTemplatePrefix } ) - - self:InitRepeat() - self.RepeatOnEngineShutDown = false - self.RepeatOnLanding = true - - return self -end - - ---- Respawn after landing when its engines have shut down. --- @param #SPAWN self --- @return #SPAWN self -function SPAWN:InitRepeatOnEngineShutDown() - self:F( { self.SpawnTemplatePrefix } ) - - self:InitRepeat() - self.RepeatOnEngineShutDown = true - self.RepeatOnLanding = false - - return self -end - - ---- CleanUp groups when they are still alive, but inactive. --- When groups are still alive and have become inactive due to damage and are unable to contribute anything, then this group will be removed at defined intervals in seconds. --- @param #SPAWN self --- @param #string SpawnCleanUpInterval The interval to check for inactive groups within seconds. --- @return #SPAWN self --- @usage Spawn_Helicopter:CleanUp( 20 ) -- CleanUp the spawning of the helicopters every 20 seconds when they become inactive. -function SPAWN:CleanUp( SpawnCleanUpInterval ) - self:F( { self.SpawnTemplatePrefix, SpawnCleanUpInterval } ) - - self.SpawnCleanUpInterval = SpawnCleanUpInterval - self.SpawnCleanUpTimeStamps = {} - --self.CleanUpFunction = routines.scheduleFunction( self._SpawnCleanUpScheduler, { self }, timer.getTime() + 1, SpawnCleanUpInterval ) - self.CleanUpScheduler = SCHEDULER:New( self, self._SpawnCleanUpScheduler, {}, 1, SpawnCleanUpInterval, 0.2 ) - return self -end - - - ---- Makes the groups visible before start (like a batallion). --- The method will take the position of the group as the first position in the array. --- @param #SPAWN self --- @param #number SpawnAngle The angle in degrees how the groups and each unit of the group will be positioned. --- @param #number SpawnWidth The amount of Groups that will be positioned on the X axis. --- @param #number SpawnDeltaX The space between each Group on the X-axis. --- @param #number SpawnDeltaY The space between each Group on the Y-axis. --- @return #SPAWN self --- @usage --- -- Define an array of Groups. --- Spawn_BE_Ground = SPAWN:New( 'BE Ground' ):Limit( 2, 24 ):Visible( 90, "Diamond", 10, 100, 50 ) -function SPAWN:Array( SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY ) - self:F( { self.SpawnTemplatePrefix, SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY } ) - - self.SpawnVisible = true -- When the first Spawn executes, all the Groups need to be made visible before start. - - local SpawnX = 0 - local SpawnY = 0 - local SpawnXIndex = 0 - local SpawnYIndex = 0 - - for SpawnGroupID = 1, self.SpawnMaxGroups do - self:T( { SpawnX, SpawnY, SpawnXIndex, SpawnYIndex } ) - - self.SpawnGroups[SpawnGroupID].Visible = true - self.SpawnGroups[SpawnGroupID].Spawned = false - - SpawnXIndex = SpawnXIndex + 1 - if SpawnWidth and SpawnWidth ~= 0 then - if SpawnXIndex >= SpawnWidth then - SpawnXIndex = 0 - SpawnYIndex = SpawnYIndex + 1 - end - end - - local SpawnRootX = self.SpawnGroups[SpawnGroupID].SpawnTemplate.x - local SpawnRootY = self.SpawnGroups[SpawnGroupID].SpawnTemplate.y - - self:_TranslateRotate( SpawnGroupID, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle ) - - self.SpawnGroups[SpawnGroupID].SpawnTemplate.lateActivation = true - self.SpawnGroups[SpawnGroupID].SpawnTemplate.visible = true - - self.SpawnGroups[SpawnGroupID].Visible = true - - _EVENTDISPATCHER:OnBirthForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnBirth, self ) - _EVENTDISPATCHER:OnCrashForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnDeadOrCrash, self ) - _EVENTDISPATCHER:OnDeadForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnDeadOrCrash, self ) - - if self.Repeat then - _EVENTDISPATCHER:OnTakeOffForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnTakeOff, self ) - _EVENTDISPATCHER:OnLandForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnLand, self ) - end - if self.RepeatOnEngineShutDown then - _EVENTDISPATCHER:OnEngineShutDownForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnEngineShutDown, self ) - end - - self.SpawnGroups[SpawnGroupID].Group = _DATABASE:Spawn( self.SpawnGroups[SpawnGroupID].SpawnTemplate ) - - SpawnX = SpawnXIndex * SpawnDeltaX - SpawnY = SpawnYIndex * SpawnDeltaY - end - - return self -end - - - ---- Will spawn a group based on the internal index. --- Note: Uses @{DATABASE} module defined in MOOSE. --- @param #SPAWN self --- @return Group#GROUP The group that was spawned. You can use this group for further actions. -function SPAWN:Spawn() - self:F( { self.SpawnTemplatePrefix, self.SpawnIndex } ) - - return self:SpawnWithIndex( self.SpawnIndex + 1 ) -end - ---- Will re-spawn a group based on a given index. --- Note: Uses @{DATABASE} module defined in MOOSE. --- @param #SPAWN self --- @param #string SpawnIndex The index of the group to be spawned. --- @return Group#GROUP The group that was spawned. You can use this group for further actions. -function SPAWN:ReSpawn( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex } ) - - if not SpawnIndex then - SpawnIndex = 1 - end - --- TODO: This logic makes DCS crash and i don't know why (yet). - local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) - if SpawnGroup then - local SpawnDCSGroup = SpawnGroup:GetDCSGroup() - if SpawnDCSGroup then - SpawnGroup:Destroy() - end - end - - return self:SpawnWithIndex( SpawnIndex ) -end - ---- Will spawn a group with a specified index number. --- Uses @{DATABASE} global object defined in MOOSE. --- @param #SPAWN self --- @return Group#GROUP The group that was spawned. You can use this group for further actions. -function SPAWN:SpawnWithIndex( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnMaxGroups } ) - - if self:_GetSpawnIndex( SpawnIndex ) then - - if self.SpawnGroups[self.SpawnIndex].Visible then - self.SpawnGroups[self.SpawnIndex].Group:Activate() - else - self:T( self.SpawnGroups[self.SpawnIndex].SpawnTemplate ) - _EVENTDISPATCHER:OnBirthForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnBirth, self ) - _EVENTDISPATCHER:OnCrashForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnDeadOrCrash, self ) - _EVENTDISPATCHER:OnDeadForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnDeadOrCrash, self ) - - if self.Repeat then - _EVENTDISPATCHER:OnTakeOffForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnTakeOff, self ) - _EVENTDISPATCHER:OnLandForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnLand, self ) - end - if self.RepeatOnEngineShutDown then - _EVENTDISPATCHER:OnEngineShutDownForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnEngineShutDown, self ) - end - - self:T( self.SpawnGroups[self.SpawnIndex].SpawnTemplate ) - - self.SpawnGroups[self.SpawnIndex].Group = _DATABASE:Spawn( self.SpawnGroups[self.SpawnIndex].SpawnTemplate ) - - -- If there is a SpawnFunction hook defined, call it. - if self.SpawnFunctionHook then - self.SpawnFunctionHook( self.SpawnGroups[self.SpawnIndex].Group, unpack( self.SpawnFunctionArguments ) ) - end - -- TODO: Need to fix this by putting an "R" in the name of the group when the group repeats. - --if self.Repeat then - -- _DATABASE:SetStatusGroup( SpawnTemplate.name, "ReSpawn" ) - --end - end - - self.SpawnGroups[self.SpawnIndex].Spawned = true - return self.SpawnGroups[self.SpawnIndex].Group - else - --self:E( { self.SpawnTemplatePrefix, "No more Groups to Spawn:", SpawnIndex, self.SpawnMaxGroups } ) - end - - return nil -end - ---- Spawns new groups at varying time intervals. --- This is useful if you want to have continuity within your missions of certain (AI) groups to be present (alive) within your missions. --- @param #SPAWN self --- @param #number SpawnTime The time interval defined in seconds between each new spawn of new groups. --- @param #number SpawnTimeVariation The variation to be applied on the defined time interval between each new spawn. --- The variation is a number between 0 and 1, representing the %-tage of variation to be applied on the time interval. --- @return #SPAWN self --- @usage --- -- NATO helicopters engaging in the battle field. --- -- The time interval is set to SPAWN new helicopters between each 600 seconds, with a time variation of 50%. --- -- The time variation in this case will be between 450 seconds and 750 seconds. --- -- This is calculated as follows: --- -- Low limit: 600 * ( 1 - 0.5 / 2 ) = 450 --- -- High limit: 600 * ( 1 + 0.5 / 2 ) = 750 --- -- Between these two values, a random amount of seconds will be choosen for each new spawn of the helicopters. --- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):Schedule( 600, 0.5 ) -function SPAWN:SpawnScheduled( SpawnTime, SpawnTimeVariation ) - self:F( { SpawnTime, SpawnTimeVariation } ) - - if SpawnTime ~= nil and SpawnTimeVariation ~= nil then - self.SpawnScheduler = SCHEDULER:New( self, self._Scheduler, {}, 1, SpawnTime, SpawnTimeVariation ) - end - - return self -end - ---- Will re-start the spawning scheduler. --- Note: This function is only required to be called when the schedule was stopped. -function SPAWN:SpawnScheduleStart() - self:F( { self.SpawnTemplatePrefix } ) - - self.SpawnScheduler:Start() -end - ---- Will stop the scheduled spawning scheduler. -function SPAWN:SpawnScheduleStop() - self:F( { self.SpawnTemplatePrefix } ) - - self.SpawnScheduler:Stop() -end - - ---- Allows to place a CallFunction hook when a new group spawns. --- The provided function 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. --- @param #SPAWN self --- @param #function SpawnFunctionHook 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 -function SPAWN:SpawnFunction( SpawnFunctionHook, ... ) - self:F( SpawnFunction ) - - self.SpawnFunctionHook = SpawnFunctionHook - self.SpawnFunctionArguments = {} - if arg then - self.SpawnFunctionArguments = arg - end - - return self -end - - - - ---- Will spawn a group from a hosting unit. This function is mostly advisable to be used if you want to simulate spawning from air units, like helicopters, which are dropping infantry into a defined Landing Zone. --- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. --- You can use the returned group to further define the route to be followed. --- @param #SPAWN self --- @param Unit#UNIT HostUnit The air or ground unit dropping or unloading the group. --- @param #number OuterRadius The outer radius in meters where the new group will be spawned. --- @param #number InnerRadius The inner radius in meters where the new group will NOT be spawned. --- @param #number SpawnIndex (Optional) The index which group to spawn within the given zone. --- @return Group#GROUP that was spawned. --- @return #nil Nothing was spawned. -function SPAWN:SpawnFromUnit( HostUnit, OuterRadius, InnerRadius, SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, HostUnit, OuterRadius, InnerRadius, SpawnIndex } ) - - if HostUnit and HostUnit:IsAlive() then -- and HostUnit:getUnit(1):inAir() == false then - - if SpawnIndex then - else - SpawnIndex = self.SpawnIndex + 1 - end - - if self:_GetSpawnIndex( SpawnIndex ) then - - local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate - - if SpawnTemplate then - - local UnitPoint = HostUnit:GetPointVec2() - - self:T( { "Current point of ", self.SpawnTemplatePrefix, UnitPoint } ) - - --for PointID, Point in pairs( SpawnTemplate.route.points ) do - --Point.x = UnitPoint.x - --Point.y = UnitPoint.y - --Point.alt = nil - --Point.alt_type = nil - --end - - SpawnTemplate.route.points[1].x = UnitPoint.x - SpawnTemplate.route.points[1].y = UnitPoint.y - - if not InnerRadius then - InnerRadius = 10 - end - - if not OuterRadius then - OuterRadius = 50 - end - - -- Apply SpawnFormation - for UnitID = 1, #SpawnTemplate.units do - if InnerRadius == 0 then - SpawnTemplate.units[UnitID].x = UnitPoint.x - SpawnTemplate.units[UnitID].y = UnitPoint.y - else - local CirclePos = routines.getRandPointInCircle( UnitPoint, OuterRadius, InnerRadius ) - SpawnTemplate.units[UnitID].x = CirclePos.x - SpawnTemplate.units[UnitID].y = CirclePos.y - end - self:T( 'SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) - end - - local SpawnPos = routines.getRandPointInCircle( UnitPoint, OuterRadius, InnerRadius ) - local Point = {} - Point.type = "Turning Point" - Point.x = SpawnPos.x - Point.y = SpawnPos.y - Point.action = "Cone" - Point.speed = 5 - - table.insert( SpawnTemplate.route.points, 2, Point ) - - return self:SpawnWithIndex( self.SpawnIndex ) - end - end - end - - return nil -end - ---- Will spawn a Group within a given @{Zone#ZONE}. --- Once the group is spawned within the zone, it will continue on its route. --- The first waypoint (where the group is spawned) is replaced with the zone coordinates. --- @param #SPAWN self --- @param Zone#ZONE Zone The zone where the group is to be spawned. --- @param #number ZoneRandomize (Optional) Set to true if you want to randomize the starting point in the zone. --- @param #number SpawnIndex (Optional) The index which group to spawn within the given zone. --- @return Group#GROUP that was spawned. --- @return #nil when nothing was spawned. -function SPAWN:SpawnInZone( Zone, ZoneRandomize, SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, Zone, ZoneRandomize, SpawnIndex } ) - - if Zone then - - if SpawnIndex then - else - SpawnIndex = self.SpawnIndex + 1 - end - - if self:_GetSpawnIndex( SpawnIndex ) then - - local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate - - if SpawnTemplate then - - local ZonePoint - - if ZoneRandomize == true then - ZonePoint = Zone:GetRandomVec2() - else - ZonePoint = Zone:GetPointVec2() - end - - SpawnTemplate.route.points[1].x = ZonePoint.x - SpawnTemplate.route.points[1].y = ZonePoint.y - - -- Apply SpawnFormation - for UnitID = 1, #SpawnTemplate.units do - local ZonePointUnit = Zone:GetRandomVec2() - SpawnTemplate.units[UnitID].x = ZonePointUnit.x - SpawnTemplate.units[UnitID].y = ZonePointUnit.y - self:T( 'SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) - end - - return self:SpawnWithIndex( self.SpawnIndex ) - end - end - end - - return nil -end - - - - ---- Will spawn a plane group in uncontrolled mode... --- This will be similar to the uncontrolled flag setting in the ME. --- @return #SPAWN self -function SPAWN:UnControlled() - self:F( { self.SpawnTemplatePrefix } ) - - self.SpawnUnControlled = true - - for SpawnGroupID = 1, self.SpawnMaxGroups do - self.SpawnGroups[SpawnGroupID].UnControlled = true - end - - return self -end - - - ---- Will return the SpawnGroupName either with with a specific count number or without any count. --- @param #SPAWN self --- @param #number SpawnIndex Is the number of the Group that is to be spawned. --- @return #string SpawnGroupName -function SPAWN:SpawnGroupName( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex } ) - - local SpawnPrefix = self.SpawnTemplatePrefix - if self.SpawnAliasPrefix then - SpawnPrefix = self.SpawnAliasPrefix - end - - if SpawnIndex then - local SpawnName = string.format( '%s#%03d', SpawnPrefix, SpawnIndex ) - self:T( SpawnName ) - return SpawnName - else - self:T( SpawnPrefix ) - return SpawnPrefix - end - -end - ---- Find the first alive group. --- @param #SPAWN self --- @param #number SpawnCursor A number holding the index from where to find the first group from. --- @return Group#GROUP, #number The group found, the new index where the group was found. --- @return #nil, #nil When no group is found, #nil is returned. -function SPAWN:GetFirstAliveGroup( SpawnCursor ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnCursor } ) - - for SpawnIndex = 1, self.SpawnCount do - local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) - if SpawnGroup and SpawnGroup:IsAlive() then - SpawnCursor = SpawnIndex - return SpawnGroup, SpawnCursor - end - end - - return nil, nil -end - - ---- Find the next alive group. --- @param #SPAWN self --- @param #number SpawnCursor A number holding the last found previous index. --- @return Group#GROUP, #number The group found, the new index where the group was found. --- @return #nil, #nil When no group is found, #nil is returned. -function SPAWN:GetNextAliveGroup( SpawnCursor ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnCursor } ) - - SpawnCursor = SpawnCursor + 1 - for SpawnIndex = SpawnCursor, self.SpawnCount do - local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) - if SpawnGroup and SpawnGroup:IsAlive() then - SpawnCursor = SpawnIndex - return SpawnGroup, SpawnCursor - end - end - - return nil, nil -end - ---- Find the last alive group during runtime. -function SPAWN:GetLastAliveGroup() - self:F( { self.SpawnTemplatePrefixself.SpawnAliasPrefix } ) - - self.SpawnIndex = self:_GetLastIndex() - for SpawnIndex = self.SpawnIndex, 1, -1 do - local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) - if SpawnGroup and SpawnGroup:IsAlive() then - self.SpawnIndex = SpawnIndex - return SpawnGroup - end - end - - self.SpawnIndex = nil - return nil -end - - - ---- Get the group from an index. --- Returns the group from the SpawnGroups list. --- If no index is given, it will return the first group in the list. --- @param #SPAWN self --- @param #number SpawnIndex The index of the group to return. --- @return Group#GROUP self -function SPAWN:GetGroupFromIndex( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnIndex } ) - - if not SpawnIndex then - SpawnIndex = 1 - end - - if self.SpawnGroups and self.SpawnGroups[SpawnIndex] then - local SpawnGroup = self.SpawnGroups[SpawnIndex].Group - return SpawnGroup - else - return nil - end -end - ---- Get the group index from a DCSUnit. --- The method will search for a #-mark, and will return the index behind the #-mark of the DCSUnit. --- It will return nil of no prefix was found. --- @param #SPAWN self --- @param DCSUnit The DCS unit to be searched. --- @return #string The prefix --- @return #nil Nothing found -function SPAWN:_GetGroupIndexFromDCSUnit( DCSUnit ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, DCSUnit } ) - - if DCSUnit and DCSUnit:getName() then - local IndexString = string.match( DCSUnit:getName(), "#.*-" ):sub( 2, -2 ) - self:T( IndexString ) - - if IndexString then - local Index = tonumber( IndexString ) - self:T( { "Index:", IndexString, Index } ) - return Index - end - end - - return nil -end - ---- Return the prefix of a DCSUnit. --- 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 DCSUnit The DCS unit to be searched. --- @return #string The prefix --- @return #nil Nothing found -function SPAWN:_GetPrefixFromDCSUnit( DCSUnit ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, DCSUnit } ) - - if DCSUnit and DCSUnit:getName() then - local SpawnPrefix = string.match( DCSUnit:getName(), ".*#" ) - if SpawnPrefix then - SpawnPrefix = SpawnPrefix:sub( 1, -2 ) - end - self:T( SpawnPrefix ) - return SpawnPrefix - end - - return nil -end - ---- Return the group within the SpawnGroups collection with input a DCSUnit. -function SPAWN:_GetGroupFromDCSUnit( DCSUnit ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, DCSUnit } ) - - if DCSUnit then - local SpawnPrefix = self:_GetPrefixFromDCSUnit( DCSUnit ) - - if self.SpawnTemplatePrefix == SpawnPrefix or ( self.SpawnAliasPrefix and self.SpawnAliasPrefix == SpawnPrefix ) then - local SpawnGroupIndex = self:_GetGroupIndexFromDCSUnit( DCSUnit ) - local SpawnGroup = self.SpawnGroups[SpawnGroupIndex].Group - self:T( SpawnGroup ) - return SpawnGroup - end - end - - return nil -end - - ---- Get the index from a given group. --- The function will search the name of the group for a #, and will return the number behind the #-mark. -function SPAWN:GetSpawnIndexFromGroup( SpawnGroup ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnGroup } ) - - local IndexString = string.match( SpawnGroup:GetName(), "#.*$" ):sub( 2 ) - local Index = tonumber( IndexString ) - - self:T( IndexString, Index ) - return Index - -end - ---- Return the last maximum index that can be used. -function SPAWN:_GetLastIndex() - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) - - return self.SpawnMaxGroups -end - ---- Initalize the SpawnGroups collection. -function SPAWN:_InitializeSpawnGroups( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnIndex } ) - - if not self.SpawnGroups[SpawnIndex] then - self.SpawnGroups[SpawnIndex] = {} - self.SpawnGroups[SpawnIndex].Visible = false - self.SpawnGroups[SpawnIndex].Spawned = false - self.SpawnGroups[SpawnIndex].UnControlled = false - self.SpawnGroups[SpawnIndex].SpawnTime = 0 - - self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix = self.SpawnTemplatePrefix - self.SpawnGroups[SpawnIndex].SpawnTemplate = self:_Prepare( self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix, SpawnIndex ) - end - - self:_RandomizeTemplate( SpawnIndex ) - self:_RandomizeRoute( SpawnIndex ) - --self:_TranslateRotate( SpawnIndex ) - - return self.SpawnGroups[SpawnIndex] -end - - - ---- Gets the CategoryID of the Group with the given SpawnPrefix -function SPAWN:_GetGroupCategoryID( SpawnPrefix ) - local TemplateGroup = Group.getByName( SpawnPrefix ) - - if TemplateGroup then - return TemplateGroup:getCategory() - else - return nil - end -end - ---- Gets the CoalitionID of the Group with the given SpawnPrefix -function SPAWN:_GetGroupCoalitionID( SpawnPrefix ) - local TemplateGroup = Group.getByName( SpawnPrefix ) - - if TemplateGroup then - return TemplateGroup:getCoalition() - else - return nil - end -end - ---- Gets the CountryID of the Group with the given SpawnPrefix -function SPAWN:_GetGroupCountryID( SpawnPrefix ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnPrefix } ) - - local TemplateGroup = Group.getByName( SpawnPrefix ) - - if TemplateGroup then - local TemplateUnits = TemplateGroup:getUnits() - return TemplateUnits[1]:getCountry() - else - return nil - end -end - ---- Gets the Group Template from the ME environment definition. --- This method used the @{DATABASE} object, which contains ALL initial and new spawned object in MOOSE. --- @param #SPAWN self --- @param #string SpawnTemplatePrefix --- @return @SPAWN self -function SPAWN:_GetTemplate( SpawnTemplatePrefix ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnTemplatePrefix } ) - - local SpawnTemplate = nil - - SpawnTemplate = routines.utils.deepCopy( _DATABASE.Templates.Groups[SpawnTemplatePrefix].Template ) - - if SpawnTemplate == nil then - error( 'No Template returned for SpawnTemplatePrefix = ' .. SpawnTemplatePrefix ) - end - - SpawnTemplate.SpawnCoalitionID = self:_GetGroupCoalitionID( SpawnTemplatePrefix ) - SpawnTemplate.SpawnCategoryID = self:_GetGroupCategoryID( SpawnTemplatePrefix ) - SpawnTemplate.SpawnCountryID = self:_GetGroupCountryID( SpawnTemplatePrefix ) - - self:T( { SpawnTemplate } ) - return SpawnTemplate -end - ---- Prepares the new Group Template. --- @param #SPAWN self --- @param #string SpawnTemplatePrefix --- @param #number SpawnIndex --- @return #SPAWN self -function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) - - local SpawnTemplate = self:_GetTemplate( SpawnTemplatePrefix ) - SpawnTemplate.name = self:SpawnGroupName( SpawnIndex ) - - SpawnTemplate.groupId = nil - --SpawnTemplate.lateActivation = false - SpawnTemplate.lateActivation = false -- TODO BUGFIX - - if SpawnTemplate.SpawnCategoryID == Group.Category.GROUND then - self:T( "For ground units, visible needs to be false..." ) - SpawnTemplate.visible = false -- TODO BUGFIX - end - - if SpawnTemplate.SpawnCategoryID == Group.Category.HELICOPTER or SpawnTemplate.SpawnCategoryID == Group.Category.AIRPLANE then - SpawnTemplate.uncontrolled = false - end - - for UnitID = 1, #SpawnTemplate.units do - SpawnTemplate.units[UnitID].name = string.format( SpawnTemplate.name .. '-%02d', UnitID ) - SpawnTemplate.units[UnitID].unitId = nil - SpawnTemplate.units[UnitID].x = SpawnTemplate.route.points[1].x - SpawnTemplate.units[UnitID].y = SpawnTemplate.route.points[1].y - end - - self:T( { "Template:", SpawnTemplate } ) - return SpawnTemplate - -end - ---- Private method randomizing the routes. --- @param #SPAWN self --- @param #number SpawnIndex The index of the group to be spawned. --- @return #SPAWN -function SPAWN:_RandomizeRoute( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeRoute, self.SpawnRandomizeRouteStartPoint, self.SpawnRandomizeRouteEndPoint, self.SpawnRandomizeRouteRadius } ) - - if self.SpawnRandomizeRoute then - local SpawnTemplate = self.SpawnGroups[SpawnIndex].SpawnTemplate - local RouteCount = #SpawnTemplate.route.points - - for t = self.SpawnRandomizeRouteStartPoint + 1, ( RouteCount - self.SpawnRandomizeRouteEndPoint ) do - SpawnTemplate.route.points[t].x = SpawnTemplate.route.points[t].x + math.random( self.SpawnRandomizeRouteRadius * -1, self.SpawnRandomizeRouteRadius ) - SpawnTemplate.route.points[t].y = SpawnTemplate.route.points[t].y + math.random( self.SpawnRandomizeRouteRadius * -1, self.SpawnRandomizeRouteRadius ) - -- TODO: manage altitude for airborne units ... - SpawnTemplate.route.points[t].alt = nil - --SpawnGroup.route.points[t].alt_type = nil - self:T( 'SpawnTemplate.route.points[' .. t .. '].x = ' .. SpawnTemplate.route.points[t].x .. ', SpawnTemplate.route.points[' .. t .. '].y = ' .. SpawnTemplate.route.points[t].y ) - end - end - - return self -end - ---- Private method that randomizes the template of the group. --- @param #SPAWN self --- @param #number SpawnIndex --- @return #SPAWN self -function SPAWN:_RandomizeTemplate( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeTemplate } ) - - if self.SpawnRandomizeTemplate then - self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix = self.SpawnTemplatePrefixTable[ math.random( 1, #self.SpawnTemplatePrefixTable ) ] - self.SpawnGroups[SpawnIndex].SpawnTemplate = self:_Prepare( self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix, SpawnIndex ) - self.SpawnGroups[SpawnIndex].SpawnTemplate.route = routines.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 - for UnitID = 1, #self.SpawnGroups[SpawnIndex].SpawnTemplate.units do - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].heading = self.SpawnTemplate.units[1].heading - end - end - - self:_RandomizeRoute( SpawnIndex ) - - return self -end - -function SPAWN:_TranslateRotate( SpawnIndex, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle } ) - - -- Translate - local TranslatedX = SpawnX - local TranslatedY = SpawnY - - -- Rotate - -- From Wikipedia: https://en.wikipedia.org/wiki/Rotation_matrix#Common_rotations - -- x' = x \cos \theta - y \sin \theta\ - -- y' = x \sin \theta + y \cos \theta\ - local RotatedX = - TranslatedX * math.cos( math.rad( SpawnAngle ) ) - + TranslatedY * math.sin( math.rad( SpawnAngle ) ) - local RotatedY = TranslatedX * math.sin( math.rad( SpawnAngle ) ) - + TranslatedY * math.cos( math.rad( SpawnAngle ) ) - - -- Assign - self.SpawnGroups[SpawnIndex].SpawnTemplate.x = SpawnRootX - RotatedX - self.SpawnGroups[SpawnIndex].SpawnTemplate.y = SpawnRootY + RotatedY - - - local SpawnUnitCount = table.getn( self.SpawnGroups[SpawnIndex].SpawnTemplate.units ) - for u = 1, SpawnUnitCount do - - -- Translate - local TranslatedX = SpawnX - local TranslatedY = SpawnY - 10 * ( u - 1 ) - - -- Rotate - local RotatedX = - TranslatedX * math.cos( math.rad( SpawnAngle ) ) - + TranslatedY * math.sin( math.rad( SpawnAngle ) ) - local RotatedY = TranslatedX * math.sin( math.rad( SpawnAngle ) ) - + TranslatedY * math.cos( math.rad( SpawnAngle ) ) - - -- Assign - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].x = SpawnRootX - RotatedX - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].y = SpawnRootY + RotatedY - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].heading = self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].heading + math.rad( SpawnAngle ) - end - - return self -end - ---- Get the next index of the groups to be spawned. This function is complicated, as it is used at several spaces. -function SPAWN:_GetSpawnIndex( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnMaxGroups, self.SpawnMaxUnitsAlive, self.AliveUnits, #self.SpawnTemplate.units } ) - - - if ( self.SpawnMaxGroups == 0 ) or ( SpawnIndex <= self.SpawnMaxGroups ) then - if ( self.SpawnMaxUnitsAlive == 0 ) or ( self.AliveUnits < self.SpawnMaxUnitsAlive * #self.SpawnTemplate.units ) or self.UnControlled then - if SpawnIndex and SpawnIndex >= self.SpawnCount + 1 then - self.SpawnCount = self.SpawnCount + 1 - SpawnIndex = self.SpawnCount - end - self.SpawnIndex = SpawnIndex - if not self.SpawnGroups[self.SpawnIndex] then - self:_InitializeSpawnGroups( self.SpawnIndex ) - end - else - return nil - end - else - return nil - end - - return self.SpawnIndex -end - - --- TODO Need to delete this... _DATABASE does this now ... -function SPAWN:_OnBirth( event ) - - if timer.getTime0() < timer.getAbsTime() then -- dont need to add units spawned in at the start of the mission if mist is loaded in init line - if event.initiator and event.initiator:getName() then - local EventPrefix = self:_GetPrefixFromDCSUnit( event.initiator ) - if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then - self:T( { "Birth event: " .. event.initiator:getName(), event } ) - --MessageToAll( "Mission command: unit " .. SpawnTemplatePrefix .. " spawned." , 5, EventPrefix .. '/Event') - self.AliveUnits = self.AliveUnits + 1 - self:T( "Alive Units: " .. self.AliveUnits ) - end - end - end - -end - ---- Obscolete --- @todo Need to delete this... _DATABASE does this now ... -function SPAWN:_OnDeadOrCrash( event ) - self:F( self.SpawnTemplatePrefix, event ) - - if event.initiator and event.initiator:getName() then - local EventPrefix = self:_GetPrefixFromDCSUnit( event.initiator ) - if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then - self:T( { "Dead event: " .. event.initiator:getName(), event } ) --- local DestroyedUnit = Unit.getByName( EventPrefix ) --- if DestroyedUnit and DestroyedUnit.getLife() <= 1.0 then - --MessageToAll( "Mission command: unit " .. SpawnTemplatePrefix .. " crashed." , 5, EventPrefix .. '/Event') - self.AliveUnits = self.AliveUnits - 1 - self:T( "Alive Units: " .. self.AliveUnits ) --- end - end - end -end - ---- Will detect AIR Units taking off... When the event takes place, the spawned Group is registered as airborne... --- This is needed to ensure that Re-SPAWNing only is done for landed AIR Groups. --- @todo Need to test for AIR Groups only... -function SPAWN:_OnTakeOff( event ) - self:F( self.SpawnTemplatePrefix, event ) - - if event.initiator and event.initiator:getName() then - local SpawnGroup = self:_GetGroupFromDCSUnit( event.initiator ) - if SpawnGroup then - self:T( { "TakeOff event: " .. event.initiator:getName(), event } ) - self:T( "self.Landed = false" ) - self.Landed = false - end - end -end - ---- Will detect AIR Units landing... When the event takes place, the spawned Group is registered as landed. --- This is needed to ensure that Re-SPAWNing is only done for landed AIR Groups. --- @todo Need to test for AIR Groups only... -function SPAWN:_OnLand( event ) - self:F( self.SpawnTemplatePrefix, event ) - - local SpawnUnit = event.initiator - if SpawnUnit and SpawnUnit:isExist() and Object.getCategory(SpawnUnit) == Object.Category.UNIT then - local SpawnGroup = self:_GetGroupFromDCSUnit( SpawnUnit ) - if SpawnGroup then - self:T( { "Landed event:" .. SpawnUnit:getName(), event } ) - self.Landed = true - self:T( "self.Landed = true" ) - if self.Landed and self.RepeatOnLanding then - local SpawnGroupIndex = self:GetSpawnIndexFromGroup( SpawnGroup ) - self:T( { "Landed:", "ReSpawn:", SpawnGroup:GetName(), SpawnGroupIndex } ) - self:ReSpawn( SpawnGroupIndex ) - end - end - end -end - ---- Will detect AIR Units shutting down their engines ... --- When the event takes place, and the method @{RepeatOnEngineShutDown} was called, the spawned Group will Re-SPAWN. --- But only when the Unit was registered to have landed. --- @param #SPAWN self --- @see _OnTakeOff --- @see _OnLand --- @todo Need to test for AIR Groups only... -function SPAWN:_OnEngineShutDown( event ) - self:F( self.SpawnTemplatePrefix, event ) - - local SpawnUnit = event.initiator - if SpawnUnit and SpawnUnit:isExist() and Object.getCategory(SpawnUnit) == Object.Category.UNIT then - local SpawnGroup = self:_GetGroupFromDCSUnit( SpawnUnit ) - if SpawnGroup then - self:T( { "EngineShutDown event: " .. SpawnUnit:getName(), event } ) - if self.Landed and self.RepeatOnEngineShutDown then - local SpawnGroupIndex = self:GetSpawnIndexFromGroup( SpawnGroup ) - self:T( { "EngineShutDown: ", "ReSpawn:", SpawnGroup:GetName(), SpawnGroupIndex } ) - self:ReSpawn( SpawnGroupIndex ) - end - end - end -end - ---- This function is called automatically by the Spawning scheduler. --- It is the internal worker method SPAWNing new Groups on the defined time intervals. -function SPAWN:_Scheduler() - self:F( { "_Scheduler", self.SpawnTemplatePrefix, self.SpawnAliasPrefix, self.SpawnIndex, self.SpawnMaxGroups, self.SpawnMaxUnitsAlive } ) - - -- Validate if there are still groups left in the batch... - self:Spawn() - - return true -end - -function SPAWN:_SpawnCleanUpScheduler() - self:F( { "CleanUp Scheduler:", self.SpawnTemplatePrefix } ) - - local SpawnCursor - local SpawnGroup, SpawnCursor = self:GetFirstAliveGroup( SpawnCursor ) - - self:T( { "CleanUp Scheduler:", SpawnGroup } ) - - while SpawnGroup do - - if SpawnGroup:AllOnGround() and SpawnGroup:GetMaxVelocity() < 1 then - if not self.SpawnCleanUpTimeStamps[SpawnGroup:GetName()] then - self.SpawnCleanUpTimeStamps[SpawnGroup:GetName()] = timer.getTime() - else - if self.SpawnCleanUpTimeStamps[SpawnGroup:GetName()] + self.SpawnCleanUpInterval < timer.getTime() then - self:T( { "CleanUp Scheduler:", "Cleaning:", SpawnGroup } ) - SpawnGroup:Destroy() - end - end - else - self.SpawnCleanUpTimeStamps[SpawnGroup:GetName()] = nil - end - - SpawnGroup, SpawnCursor = self:GetNextAliveGroup( SpawnCursor ) - - self:T( { "CleanUp Scheduler:", SpawnGroup } ) - - end - - return true -- Repeat - -end ---- Limit the simultaneous movement of Groups within a running Mission. --- This module is defined to improve the performance in missions, and to bring additional realism for GROUND vehicles. --- Performance: If in a DCSRTE there are a lot of moving GROUND units, then in a multi player mission, this WILL create lag if --- the main DCS execution core of your CPU is fully utilized. So, this class will limit the amount of simultaneous moving GROUND units --- on defined intervals (currently every minute). --- @module MOVEMENT - ---- the MOVEMENT class --- @type -MOVEMENT = { - ClassName = "MOVEMENT", -} - ---- Creates the main object which is handling the GROUND forces movement. --- @param table{string,...}|string MovePrefixes is a table of the Prefixes (names) of the GROUND Groups that need to be controlled by the MOVEMENT Object. --- @param number MoveMaximum is a number that defines the maximum amount of GROUND Units to be moving during one minute. --- @return MOVEMENT --- @usage --- -- Limit the amount of simultaneous moving units on the ground to prevent lag. --- Movement_US_Platoons = MOVEMENT:New( { 'US Tank Platoon Left', 'US Tank Platoon Middle', 'US Tank Platoon Right', 'US CH-47D Troops' }, 15 ) - -function MOVEMENT:New( MovePrefixes, MoveMaximum ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( { MovePrefixes, MoveMaximum } ) - - if type( MovePrefixes ) == 'table' then - self.MovePrefixes = MovePrefixes - else - self.MovePrefixes = { MovePrefixes } - end - self.MoveCount = 0 -- The internal counter of the amount of Moveing the has happened since MoveStart. - self.MoveMaximum = MoveMaximum -- Contains the Maximum amount of units that are allowed to move... - self.AliveUnits = 0 -- Contains the counter how many units are currently alive - self.MoveUnits = {} -- Reflects if the Moving for this MovePrefixes is going to be scheduled or not. - - _EVENTDISPATCHER:OnBirth( self.OnBirth, self ) - --- self:AddEvent( world.event.S_EVENT_BIRTH, self.OnBirth ) --- --- self:EnableEvents() - - self:ScheduleStart() - - return self -end - ---- Call this function to start the MOVEMENT scheduling. -function MOVEMENT:ScheduleStart() - self:F() - --self.MoveFunction = routines.scheduleFunction( self._Scheduler, { self }, timer.getTime() + 1, 120 ) - self.MoveFunction = SCHEDULER:New( self, self._Scheduler, {}, 1, 120 ) -end - ---- Call this function to stop the MOVEMENT scheduling. --- @todo need to implement it ... Forgot. -function MOVEMENT:ScheduleStop() - self:F() - -end - ---- Captures the birth events when new Units were spawned. --- @todo This method should become obsolete. The new @{DATABASE} class will handle the collection administration. -function MOVEMENT:OnBirth( Event ) - self:F( { Event } ) - - if timer.getTime0() < timer.getAbsTime() then -- dont need to add units spawned in at the start of the mission if mist is loaded in init line - if Event.IniDCSUnit then - self:T( "Birth object : " .. Event.IniDCSUnitName ) - if Event.IniDCSGroup and Event.IniDCSGroup:isExist() then - for MovePrefixID, MovePrefix in pairs( self.MovePrefixes ) do - if string.find( Event.IniDCSUnitName, MovePrefix, 1, true ) then - self.AliveUnits = self.AliveUnits + 1 - self.MoveUnits[Event.IniDCSUnitName] = Event.IniDCSGroupName - self:T( self.AliveUnits ) - end - end - end - end - _EVENTDISPATCHER:OnCrashForUnit( Event.IniDCSUnitName, self.OnDeadOrCrash, self ) - _EVENTDISPATCHER:OnDeadForUnit( Event.IniDCSUnitName, self.OnDeadOrCrash, self ) - end - -end - ---- Captures the Dead or Crash events when Units crash or are destroyed. --- @todo This method should become obsolete. The new @{DATABASE} class will handle the collection administration. -function MOVEMENT:OnDeadOrCrash( Event ) - self:F( { Event } ) - - if Event.IniDCSUnit then - self:T( "Dead object : " .. Event.IniDCSUnitName ) - for MovePrefixID, MovePrefix in pairs( self.MovePrefixes ) do - if string.find( Event.IniDCSUnitName, MovePrefix, 1, true ) then - self.AliveUnits = self.AliveUnits - 1 - self.MoveUnits[Event.IniDCSUnitName] = nil - self:T( self.AliveUnits ) - end - end - end -end - ---- This function is called automatically by the MOVEMENT scheduler. A new function is scheduled when MoveScheduled is true. -function MOVEMENT:_Scheduler() - self:F( { self.MovePrefixes, self.MoveMaximum, self.AliveUnits, self.MovementGroups } ) - - if self.AliveUnits > 0 then - local MoveProbability = ( self.MoveMaximum * 100 ) / self.AliveUnits - self:T( 'Move Probability = ' .. MoveProbability ) - - for MovementUnitName, MovementGroupName in pairs( self.MoveUnits ) do - local MovementGroup = Group.getByName( MovementGroupName ) - if MovementGroup and MovementGroup:isExist() then - local MoveOrStop = math.random( 1, 100 ) - self:T( 'MoveOrStop = ' .. MoveOrStop ) - if MoveOrStop <= MoveProbability then - self:T( 'Group continues moving = ' .. MovementGroupName ) - trigger.action.groupContinueMoving( MovementGroup ) - else - self:T( 'Group stops moving = ' .. MovementGroupName ) - trigger.action.groupStopMoving( MovementGroup ) - end - else - self.MoveUnits[MovementUnitName] = nil - end - end - end - return true -end ---- Provides defensive behaviour to a set of SAM sites within a running Mission. --- @module Sead --- @author to be searched on the forum --- @author (co) Flightcontrol (Modified and enriched with functionality) - ---- The SEAD class --- @type SEAD --- @extends Base#BASE -SEAD = { - ClassName = "SEAD", - TargetSkill = { - Average = { Evade = 50, DelayOff = { 10, 25 }, DelayOn = { 10, 30 } } , - Good = { Evade = 30, DelayOff = { 8, 20 }, DelayOn = { 20, 40 } } , - High = { Evade = 15, DelayOff = { 5, 17 }, DelayOn = { 30, 50 } } , - Excellent = { Evade = 10, DelayOff = { 3, 10 }, DelayOn = { 30, 60 } } - }, - SEADGroupPrefixes = {} -} - ---- Creates the main object which is handling defensive actions for SA sites or moving SA vehicles. --- When an anti radiation missile is fired (KH-58, KH-31P, KH-31A, KH-25MPU, HARM missiles), the SA will shut down their radars and will take evasive actions... --- Chances are big that the missile will miss. --- @param table{string,...}|string SEADGroupPrefixes which is a table of Prefixes of the SA Groups in the DCSRTE on which evasive actions need to be taken. --- @return SEAD --- @usage --- -- CCCP SEAD Defenses --- -- Defends the Russian SA installations from SEAD attacks. --- SEAD_RU_SAM_Defenses = SEAD:New( { 'RU SA-6 Kub', 'RU SA-6 Defenses', 'RU MI-26 Troops', 'RU Attack Gori' } ) -function SEAD:New( SEADGroupPrefixes ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( SEADGroupPrefixes ) - if type( SEADGroupPrefixes ) == 'table' then - for SEADGroupPrefixID, SEADGroupPrefix in pairs( SEADGroupPrefixes ) do - self.SEADGroupPrefixes[SEADGroupPrefix] = SEADGroupPrefix - end - else - self.SEADGroupNames[SEADGroupPrefixes] = SEADGroupPrefixes - end - _EVENTDISPATCHER:OnShot( self.EventShot, self ) - - return self -end - ---- Detects if an SA site was shot with an anti radiation missile. In this case, take evasive actions based on the skill level set within the ME. --- @see SEAD -function SEAD:EventShot( Event ) - self:F( { Event } ) - - local SEADUnit = Event.IniDCSUnit - local SEADUnitName = Event.IniDCSUnitName - local SEADWeapon = Event.Weapon -- Identify the weapon fired - local SEADWeaponName = Event.WeaponName -- return weapon type - --trigger.action.outText( string.format("Alerte, depart missile " ..string.format(SEADWeaponName)), 20) --debug message - -- Start of the 2nd loop - self:T( "Missile Launched = " .. SEADWeaponName ) - if SEADWeaponName == "KH-58" or SEADWeaponName == "KH-25MPU" or SEADWeaponName == "AGM-88" or SEADWeaponName == "KH-31A" or SEADWeaponName == "KH-31P" then -- Check if the missile is a SEAD - local _evade = math.random (1,100) -- random number for chance of evading action - local _targetMim = Event.Weapon:getTarget() -- Identify target - local _targetMimname = Unit.getName(_targetMim) - local _targetMimgroup = Unit.getGroup(Weapon.getTarget(SEADWeapon)) - local _targetMimgroupName = _targetMimgroup:getName() - local _targetMimcont= _targetMimgroup:getController() - local _targetskill = _DATABASE.Templates.Units[_targetMimname].Template.skill - self:T( self.SEADGroupPrefixes ) - self:T( _targetMimgroupName ) - local SEADGroupFound = false - for SEADGroupPrefixID, SEADGroupPrefix in pairs( self.SEADGroupPrefixes ) do - if string.find( _targetMimgroupName, SEADGroupPrefix, 1, true ) then - SEADGroupFound = true - self:T( 'Group Found' ) - break - end - end - if SEADGroupFound == true then - if _targetskill == "Random" then -- when skill is random, choose a skill - local Skills = { "Average", "Good", "High", "Excellent" } - _targetskill = Skills[ math.random(1,4) ] - end - self:T( _targetskill ) -- debug message for skill check - if self.TargetSkill[_targetskill] then - if (_evade > self.TargetSkill[_targetskill].Evade) then - self:T( string.format("Evading, target skill " ..string.format(_targetskill)) ) --debug message - local _targetMim = Weapon.getTarget(SEADWeapon) - local _targetMimname = Unit.getName(_targetMim) - local _targetMimgroup = Unit.getGroup(Weapon.getTarget(SEADWeapon)) - local _targetMimcont= _targetMimgroup:getController() - routines.groupRandomDistSelf(_targetMimgroup,300,'Diamond',250,20) -- move randomly - local SuppressedGroups1 = {} -- unit suppressed radar off for a random time - local function SuppressionEnd1(id) - id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN) - SuppressedGroups1[id.groupName] = nil - end - local id = { - groupName = _targetMimgroup, - ctrl = _targetMimcont - } - local delay1 = math.random(self.TargetSkill[_targetskill].DelayOff[1], self.TargetSkill[_targetskill].DelayOff[2]) - if SuppressedGroups1[id.groupName] == nil then - SuppressedGroups1[id.groupName] = { - SuppressionEndTime1 = timer.getTime() + delay1, - SuppressionEndN1 = SuppressionEndCounter1 --Store instance of SuppressionEnd() scheduled function - } - Controller.setOption(_targetMimcont, AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN) - timer.scheduleFunction(SuppressionEnd1, id, SuppressedGroups1[id.groupName].SuppressionEndTime1) --Schedule the SuppressionEnd() function - --trigger.action.outText( string.format("Radar Off " ..string.format(delay1)), 20) - end - - local SuppressedGroups = {} - local function SuppressionEnd(id) - id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.RED) - SuppressedGroups[id.groupName] = nil - end - local id = { - groupName = _targetMimgroup, - ctrl = _targetMimcont - } - local delay = math.random(self.TargetSkill[_targetskill].DelayOn[1], self.TargetSkill[_targetskill].DelayOn[2]) - if SuppressedGroups[id.groupName] == nil then - SuppressedGroups[id.groupName] = { - SuppressionEndTime = timer.getTime() + delay, - SuppressionEndN = SuppressionEndCounter --Store instance of SuppressionEnd() scheduled function - } - timer.scheduleFunction(SuppressionEnd, id, SuppressedGroups[id.groupName].SuppressionEndTime) --Schedule the SuppressionEnd() function - --trigger.action.outText( string.format("Radar On " ..string.format(delay)), 20) - end - end - end - end - end -end ---- Taking the lead of AI escorting your flight. --- --- @{#ESCORT} class --- ================ --- The @{#ESCORT} class allows you to interact with escorting AI on your flight and take the lead. --- Each escorting group can be commanded with a whole set of radio commands (radio menu in your flight, and then F10). --- --- The radio commands will vary according the category of the group. The richest set of commands are with Helicopters and AirPlanes. --- Ships and Ground troops will have a more limited set, but they can provide support through the bombing of targets designated by the other escorts. --- --- RADIO MENUs that can be created: --- ================================ --- Find a summary below of the current available commands: --- --- Navigation ...: --- --------------- --- Escort group navigation functions: --- --- * **"Join-Up and Follow at x meters":** The escort group fill follow you at about x meters, and they will follow you. --- * **"Flare":** Provides menu commands to let the escort group shoot a flare in the air in a color. --- * **"Smoke":** Provides menu commands to let the escort group smoke the air in a color. Note that smoking is only available for ground and naval troops. --- --- Hold position ...: --- ------------------ --- Escort group navigation functions: --- --- * **"At current location":** Stops the escort group and they will hover 30 meters above the ground at the position they stopped. --- * **"At client location":** Stops the escort group and they will hover 30 meters above the ground at the position they stopped. --- --- Report targets ...: --- ------------------- --- Report targets will make the escort group to report any target that it identifies within a 8km range. Any detected target can be attacked using the 4. Attack nearby targets function. (see below). --- --- * **"Report now":** Will report the current detected targets. --- * **"Report targets on":** Will make the escort group to report detected targets and will fill the "Attack nearby targets" menu list. --- * **"Report targets off":** Will stop detecting targets. --- --- Scan targets ...: --- ----------------- --- Menu items to pop-up the escort group for target scanning. After scanning, the escort group will resume with the mission or defined task. --- --- * **"Scan targets 30 seconds":** Scan 30 seconds for targets. --- * **"Scan targets 60 seconds":** Scan 60 seconds for targets. --- --- Attack targets ...: --- ------------------- --- This menu item will list all detected targets within a 15km range. Depending on the level of detection (known/unknown) and visuality, the targets type will also be listed. --- --- Request assistance from ...: --- ---------------------------- --- This menu item will list all detected targets within a 15km range, as with the menu item **Attack Targets**. --- This menu item allows to request attack support from other escorts supporting the current client group. --- eg. the function allows a player to request support from the Ship escort to attack a target identified by the Plane escort with its Tomahawk missiles. --- eg. the function allows a player to request support from other Planes escorting to bomb the unit with illumination missiles or bombs, so that the main plane escort can attack the area. --- --- ROE ...: --- -------- --- Sets the Rules of Engagement (ROE) of the escort group when in flight. --- --- * **"Hold Fire":** The escort group will hold fire. --- * **"Return Fire":** The escort group will return fire. --- * **"Open Fire":** The escort group will open fire on designated targets. --- * **"Weapon Free":** The escort group will engage with any target. --- --- Evasion ...: --- ------------ --- Will define the evasion techniques that the escort group will perform during flight or combat. --- --- * **"Fight until death":** The escort group will have no reaction to threats. --- * **"Use flares, chaff and jammers":** The escort group will use passive defense using flares and jammers. No evasive manoeuvres are executed. --- * **"Evade enemy fire":** The rescort group will evade enemy fire before firing. --- * **"Go below radar and evade fire":** The escort group will perform evasive vertical manoeuvres. --- --- Resume Mission ...: --- ------------------- --- Escort groups can have their own mission. This menu item will allow the escort group to resume their Mission from a given waypoint. --- Note that this is really fantastic, as you now have the dynamic of taking control of the escort groups, and allowing them to resume their path or mission. --- --- 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 initialization methods. --- ============================== --- The following menus are created within the RADIO MENU 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: --- --- -- First find the GROUP object and the CLIENT object. --- local EscortClient = CLIENT:FindByName( "Unit Name" ) -- The Unit Name is the name of the unit flagged with the skill Client in the mission editor. --- local EscortGroup = GROUP:FindByName( "Group Name" ) -- The Group Name is the name of the group that will escort the Escort Client. --- --- -- Now use these 2 objects to construct the new EscortPlanes object. --- EscortPlanes = ESCORT:New( EscortClient, EscortGroup, "Desert", "Welcome to the mission. You are escorted by a plane with code name 'Desert', which can be instructed through the F10 radio menu." ) --- --- --- --- @module Escort --- @author FlightControl - ---- ESCORT class --- @type ESCORT --- @extends Base#BASE --- @field Client#CLIENT EscortClient --- @field Group#GROUP EscortGroup --- @field #string EscortName --- @field #ESCORT.MODE EscortMode The mode the escort is in. --- @field Scheduler#SCHEDULER FollowScheduler The instance of the SCHEDULER class. --- @field #number FollowDistance The current follow distance. --- @field #boolean ReportTargets If true, nearby targets are reported. --- @Field DCSTypes#AI.Option.Air.val.ROE OptionROE Which ROE is set to the EscortGroup. --- @field DCSTypes#AI.Option.Air.val.REACTION_ON_THREAT OptionReactionOnThreat Which REACTION_ON_THREAT is set to the EscortGroup. --- @field Menu#MENU_CLIENT EscortMenuResumeMission -ESCORT = { - ClassName = "ESCORT", - EscortName = nil, -- The Escort Name - EscortClient = nil, - EscortGroup = nil, - EscortMode = 1, - MODE = { - FOLLOW = 1, - MISSION = 2, - }, - Targets = {}, -- The identified targets - FollowScheduler = nil, - ReportTargets = true, - OptionROE = AI.Option.Air.val.ROE.OPEN_FIRE, - OptionReactionOnThreat = AI.Option.Air.val.REACTION_ON_THREAT.ALLOW_ABORT_MISSION, - SmokeDirectionVector = false, - TaskPoints = {} -} - ---- ESCORT.Mode class --- @type ESCORT.MODE --- @field #number FOLLOW --- @field #number MISSION - ---- MENUPARAM type --- @type MENUPARAM --- @field #ESCORT ParamSelf --- @field #Distance ParamDistance --- @field #function ParamFunction --- @field #string ParamMessage - ---- ESCORT class constructor for an AI group --- @param #ESCORT self --- @param Client#CLIENT EscortClient The client escorted by the EscortGroup. --- @param Group#GROUP EscortGroup The group AI escorting the EscortClient. --- @param #string EscortName Name of the escort. --- @return #ESCORT self --- @usage --- -- Declare a new EscortPlanes object as follows: --- --- -- First find the GROUP object and the CLIENT object. --- local EscortClient = CLIENT:FindByName( "Unit Name" ) -- The Unit Name is the name of the unit flagged with the skill Client in the mission editor. --- local EscortGroup = GROUP:FindByName( "Group Name" ) -- The Group Name is the name of the group that will escort the Escort Client. --- --- -- Now use these 2 objects to construct the new EscortPlanes object. --- EscortPlanes = ESCORT:New( EscortClient, EscortGroup, "Desert", "Welcome to the mission. You are escorted by a plane with code name 'Desert', which can be instructed through the F10 radio menu." ) -function ESCORT:New( EscortClient, EscortGroup, EscortName, EscortBriefing ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( { EscortClient, EscortGroup, EscortName } ) - - self.EscortClient = EscortClient -- Client#CLIENT - self.EscortGroup = EscortGroup -- Group#GROUP - self.EscortName = EscortName - self.EscortBriefing = EscortBriefing - - -- Set EscortGroup known at EscortClient. - if not self.EscortClient._EscortGroups then - self.EscortClient._EscortGroups = {} - end - - if not self.EscortClient._EscortGroups[EscortGroup:GetName()] then - self.EscortClient._EscortGroups[EscortGroup:GetName()] = {} - self.EscortClient._EscortGroups[EscortGroup:GetName()].EscortGroup = self.EscortGroup - self.EscortClient._EscortGroups[EscortGroup:GetName()].EscortName = self.EscortName - self.EscortClient._EscortGroups[EscortGroup:GetName()].Targets = {} - end - - self.EscortMenu = MENU_CLIENT:New( self.EscortClient, self.EscortName ) - - self.EscortGroup:WayPointInitialize(1) - - self.EscortGroup:OptionROTVertical() - self.EscortGroup:OptionROEOpenFire() - - EscortGroup:MessageToClient( EscortGroup:GetCategoryName() .. " '" .. EscortName .. "' (" .. EscortGroup:GetCallsign() .. ") reporting! " .. - "We're escorting your flight. " .. - "Use the Radio Menu and F10 and use the options under + " .. EscortName .. "\n", - 60, EscortClient - ) - - self.FollowDistance = 100 - self.CT1 = 0 - self.GT1 = 0 - self.FollowScheduler = SCHEDULER:New( self, self._FollowScheduler, {}, 1, .5, .01 ) - self.EscortMode = ESCORT.MODE.MISSION - self.FollowScheduler:Stop() - - 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 #ESCORT self --- @param #boolean SmokeDirection If true, then the direction vector will be smoked. -function ESCORT:TestSmokeDirectionVector( SmokeDirection ) - self.SmokeDirectionVector = ( SmokeDirection == true ) and true or false -end - - ---- Defines the default menus --- @param #ESCORT self --- @return #ESCORT -function ESCORT:Menus() - self:F() - - self:MenuFollowAt( 100 ) - self:MenuFollowAt( 200 ) - self:MenuFollowAt( 300 ) - self:MenuFollowAt( 400 ) - - self:MenuScanForTargets( 100, 60 ) - - self:MenuHoldAtEscortPosition( 30 ) - self:MenuHoldAtLeaderPosition( 30 ) - - self:MenuFlare() - self:MenuSmoke() - - self:MenuReportTargets( 60 ) - self:MenuAssistedAttack() - self:MenuROE() - self:MenuEvasion() - self:MenuResumeMission() - - - return self -end - - - ---- Defines a menu slot to let the escort Join and Follow you at a certain distance. --- This menu will appear under **Navigation**. --- @param #ESCORT self --- @param DCSTypes#Distance Distance The distance in meters that the escort needs to follow the client. --- @return #ESCORT -function ESCORT:MenuFollowAt( Distance ) - self:F(Distance) - - if self.EscortGroup:IsAir() then - if not self.EscortMenuReportNavigation then - self.EscortMenuReportNavigation = MENU_CLIENT:New( self.EscortClient, "Navigation", self.EscortMenu ) - end - - if not self.EscortMenuJoinUpAndFollow then - self.EscortMenuJoinUpAndFollow = {} - end - - self.EscortMenuJoinUpAndFollow[#self.EscortMenuJoinUpAndFollow+1] = MENU_CLIENT_COMMAND:New( self.EscortClient, "Join-Up and Follow at " .. Distance, self.EscortMenuReportNavigation, ESCORT._JoinUpAndFollow, { ParamSelf = self, ParamDistance = Distance } ) - - self.EscortMode = ESCORT.MODE.FOLLOW - end - - return self -end - ---- Defines a menu slot to let the escort hold at their current position and stay low with a specified height during a specified time in seconds. --- This menu will appear under **Hold position**. --- @param #ESCORT self --- @param 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 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 #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed. --- @return #ESCORT --- TODO: Implement Seconds parameter. Challenge is to first develop the "continue from last activity" function. -function ESCORT:MenuHoldAtEscortPosition( Height, Seconds, MenuTextFormat ) - self:F( { Height, Seconds, MenuTextFormat } ) - - if self.EscortGroup:IsAir() then - - if not self.EscortMenuHold then - self.EscortMenuHold = MENU_CLIENT:New( self.EscortClient, "Hold position", self.EscortMenu ) - end - - if not Height then - Height = 30 - end - - if not Seconds then - Seconds = 0 - end - - local MenuText = "" - if not MenuTextFormat then - if Seconds == 0 then - MenuText = string.format( "Hold at %d meter", Height ) - else - MenuText = string.format( "Hold at %d meter for %d seconds", Height, Seconds ) - end - else - if Seconds == 0 then - MenuText = string.format( MenuTextFormat, Height ) - else - MenuText = string.format( MenuTextFormat, Height, Seconds ) - end - end - - if not self.EscortMenuHoldPosition then - self.EscortMenuHoldPosition = {} - end - - self.EscortMenuHoldPosition[#self.EscortMenuHoldPosition+1] = MENU_CLIENT_COMMAND - :New( - self.EscortClient, - MenuText, - self.EscortMenuHold, - ESCORT._HoldPosition, - { ParamSelf = self, - ParamOrbitGroup = self.EscortGroup, - ParamHeight = Height, - ParamSeconds = Seconds - } - ) - end - - return self -end - - ---- Defines a menu slot to let the escort hold at the client position and stay low with a specified height during a specified time in seconds. --- This menu will appear under **Navigation**. --- @param #ESCORT self --- @param 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 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 #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain one or two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed. --- @return #ESCORT --- TODO: Implement Seconds parameter. Challenge is to first develop the "continue from last activity" function. -function ESCORT:MenuHoldAtLeaderPosition( Height, Seconds, MenuTextFormat ) - self:F( { Height, Seconds, MenuTextFormat } ) - - if self.EscortGroup:IsAir() then - - if not self.EscortMenuHold then - self.EscortMenuHold = MENU_CLIENT:New( self.EscortClient, "Hold position", self.EscortMenu ) - end - - if not Height then - Height = 30 - end - - if not Seconds then - Seconds = 0 - end - - local MenuText = "" - if not MenuTextFormat then - if Seconds == 0 then - MenuText = string.format( "Rejoin and hold at %d meter", Height ) - else - MenuText = string.format( "Rejoin and hold at %d meter for %d seconds", Height, Seconds ) - end - else - if Seconds == 0 then - MenuText = string.format( MenuTextFormat, Height ) - else - MenuText = string.format( MenuTextFormat, Height, Seconds ) - end - end - - if not self.EscortMenuHoldAtLeaderPosition then - self.EscortMenuHoldAtLeaderPosition = {} - end - - self.EscortMenuHoldAtLeaderPosition[#self.EscortMenuHoldAtLeaderPosition+1] = MENU_CLIENT_COMMAND - :New( - self.EscortClient, - MenuText, - self.EscortMenuHold, - ESCORT._HoldPosition, - { ParamSelf = self, - ParamOrbitGroup = self.EscortClient, - ParamHeight = Height, - ParamSeconds = Seconds - } - ) - end - - return self -end - ---- Defines a menu slot to let the escort scan for targets at a certain height for a certain time in seconds. --- This menu will appear under **Scan targets**. --- @param #ESCORT self --- @param 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 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 #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain one or two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed. --- @return #ESCORT -function ESCORT:MenuScanForTargets( Height, Seconds, MenuTextFormat ) - self:F( { Height, Seconds, MenuTextFormat } ) - - if self.EscortGroup:IsAir() then - if not self.EscortMenuScan then - self.EscortMenuScan = MENU_CLIENT:New( self.EscortClient, "Scan for targets", self.EscortMenu ) - end - - if not Height then - Height = 100 - end - - if not Seconds then - Seconds = 30 - end - - local MenuText = "" - if not MenuTextFormat then - if Seconds == 0 then - MenuText = string.format( "At %d meter", Height ) - else - MenuText = string.format( "At %d meter for %d seconds", Height, Seconds ) - end - else - if Seconds == 0 then - MenuText = string.format( MenuTextFormat, Height ) - else - MenuText = string.format( MenuTextFormat, Height, Seconds ) - end - end - - if not self.EscortMenuScanForTargets then - self.EscortMenuScanForTargets = {} - end - - self.EscortMenuScanForTargets[#self.EscortMenuScanForTargets+1] = MENU_CLIENT_COMMAND - :New( - self.EscortClient, - MenuText, - self.EscortMenuScan, - ESCORT._ScanTargets, - { ParamSelf = self, - ParamScanDuration = 30 - } - ) - end - - return self -end - - - ---- Defines a menu slot to let the escort disperse a flare in a certain color. --- This menu will appear under **Navigation**. --- The flare will be fired from the first unit in the group. --- @param #ESCORT self --- @param #string MenuTextFormat Optional parameter that shows the menu option text. If no text is given, the default text will be displayed. --- @return #ESCORT -function ESCORT:MenuFlare( MenuTextFormat ) - self:F() - - if not self.EscortMenuReportNavigation then - self.EscortMenuReportNavigation = MENU_CLIENT:New( self.EscortClient, "Navigation", self.EscortMenu ) - end - - local MenuText = "" - if not MenuTextFormat then - MenuText = "Flare" - else - MenuText = MenuTextFormat - end - - if not self.EscortMenuFlare then - self.EscortMenuFlare = MENU_CLIENT:New( self.EscortClient, MenuText, self.EscortMenuReportNavigation, ESCORT._Flare, { ParamSelf = self } ) - self.EscortMenuFlareGreen = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release green flare", self.EscortMenuFlare, ESCORT._Flare, { ParamSelf = self, ParamColor = UNIT.FlareColor.Green, ParamMessage = "Released a green flare!" } ) - self.EscortMenuFlareRed = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release red flare", self.EscortMenuFlare, ESCORT._Flare, { ParamSelf = self, ParamColor = UNIT.FlareColor.Red, ParamMessage = "Released a red flare!" } ) - self.EscortMenuFlareWhite = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release white flare", self.EscortMenuFlare, ESCORT._Flare, { ParamSelf = self, ParamColor = UNIT.FlareColor.White, ParamMessage = "Released a white flare!" } ) - self.EscortMenuFlareYellow = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release yellow flare", self.EscortMenuFlare, ESCORT._Flare, { ParamSelf = self, ParamColor = UNIT.FlareColor.Yellow, ParamMessage = "Released a yellow flare!" } ) - end - - return self -end - ---- Defines a menu slot to let the escort disperse a smoke in a certain color. --- This menu will appear under **Navigation**. --- Note that smoke menu options will only be displayed for ships and ground units. Not for air units. --- The smoke will be fired from the first unit in the group. --- @param #ESCORT self --- @param #string MenuTextFormat Optional parameter that shows the menu option text. If no text is given, the default text will be displayed. --- @return #ESCORT -function ESCORT:MenuSmoke( MenuTextFormat ) - self:F() - - if not self.EscortGroup:IsAir() then - if not self.EscortMenuReportNavigation then - self.EscortMenuReportNavigation = MENU_CLIENT:New( self.EscortClient, "Navigation", self.EscortMenu ) - end - - local MenuText = "" - if not MenuTextFormat then - MenuText = "Smoke" - else - MenuText = MenuTextFormat - end - - if not self.EscortMenuSmoke then - self.EscortMenuSmoke = MENU_CLIENT:New( self.EscortClient, "Smoke", self.EscortMenuReportNavigation, ESCORT._Smoke, { ParamSelf = self } ) - self.EscortMenuSmokeGreen = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release green smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.Green, ParamMessage = "Releasing green smoke!" } ) - self.EscortMenuSmokeRed = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release red smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.Red, ParamMessage = "Releasing red smoke!" } ) - self.EscortMenuSmokeWhite = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release white smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.White, ParamMessage = "Releasing white smoke!" } ) - self.EscortMenuSmokeOrange = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release orange smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.Orange, ParamMessage = "Releasing orange smoke!" } ) - self.EscortMenuSmokeBlue = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release blue smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.Blue, ParamMessage = "Releasing blue smoke!" } ) - end - end - - return self -end - ---- Defines a menu slot to let the escort report their current detected targets with a specified time interval in seconds. --- This menu will appear under **Report targets**. --- Note that if a report targets menu is not specified, no targets will be detected by the escort, and the attack and assisted attack menus will not be displayed. --- @param #ESCORT self --- @param 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. --- @return #ESCORT -function ESCORT:MenuReportTargets( Seconds ) - self:F( { Seconds } ) - - if not self.EscortMenuReportNearbyTargets then - self.EscortMenuReportNearbyTargets = MENU_CLIENT:New( self.EscortClient, "Report targets", self.EscortMenu ) - end - - if not Seconds then - Seconds = 30 - end - - -- Report Targets - self.EscortMenuReportNearbyTargetsNow = MENU_CLIENT_COMMAND:New( self.EscortClient, "Report targets now!", self.EscortMenuReportNearbyTargets, ESCORT._ReportNearbyTargetsNow, { ParamSelf = self } ) - self.EscortMenuReportNearbyTargetsOn = MENU_CLIENT_COMMAND:New( self.EscortClient, "Report targets on", self.EscortMenuReportNearbyTargets, ESCORT._SwitchReportNearbyTargets, { ParamSelf = self, ParamReportTargets = true } ) - self.EscortMenuReportNearbyTargetsOff = MENU_CLIENT_COMMAND:New( self.EscortClient, "Report targets off", self.EscortMenuReportNearbyTargets, ESCORT._SwitchReportNearbyTargets, { ParamSelf = self, ParamReportTargets = false, } ) - - -- Attack Targets - self.EscortMenuAttackNearbyTargets = MENU_CLIENT:New( self.EscortClient, "Attack targets", self.EscortMenu ) - - - --self.ReportTargetsScheduler = routines.scheduleFunction( self._ReportTargetsScheduler, { self }, timer.getTime() + 1, Seconds ) - self.ReportTargetsScheduler = SCHEDULER:New( self, self._ReportTargetsScheduler, {}, 1, Seconds ) - - return self -end - ---- Defines a menu slot to let the escort attack its detected targets using assisted attack from another escort joined also with the client. --- This menu will appear under **Request assistance from**. --- Note that this method needs to be preceded with the method MenuReportTargets. --- @param #ESCORT self --- @return #ESCORT -function ESCORT:MenuAssistedAttack() - self:F() - - -- Request assistance from other escorts. - -- This is very useful to let f.e. an escorting ship attack a target detected by an escorting plane... - self.EscortMenuTargetAssistance = MENU_CLIENT:New( self.EscortClient, "Request assistance from", self.EscortMenu ) - - return self -end - ---- Defines a menu to let the escort set its rules of engagement. --- All rules of engagement will appear under the menu **ROE**. --- @param #ESCORT self --- @return #ESCORT -function ESCORT:MenuROE( MenuTextFormat ) - self:F( MenuTextFormat ) - - if not self.EscortMenuROE then - -- Rules of Engagement - self.EscortMenuROE = MENU_CLIENT:New( self.EscortClient, "ROE", self.EscortMenu ) - if self.EscortGroup:OptionROEHoldFirePossible() then - self.EscortMenuROEHoldFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Hold Fire", self.EscortMenuROE, ESCORT._ROE, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROEHoldFire(), ParamMessage = "Holding weapons!" } ) - end - if self.EscortGroup:OptionROEReturnFirePossible() then - self.EscortMenuROEReturnFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Return Fire", self.EscortMenuROE, ESCORT._ROE, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROEReturnFire(), ParamMessage = "Returning fire!" } ) - end - if self.EscortGroup:OptionROEOpenFirePossible() then - self.EscortMenuROEOpenFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Open Fire", self.EscortMenuROE, ESCORT._ROE, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROEOpenFire(), ParamMessage = "Opening fire on designated targets!!" } ) - end - if self.EscortGroup:OptionROEWeaponFreePossible() then - self.EscortMenuROEWeaponFree = MENU_CLIENT_COMMAND:New( self.EscortClient, "Weapon Free", self.EscortMenuROE, ESCORT._ROE, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROEWeaponFree(), ParamMessage = "Opening fire on targets of opportunity!" } ) - end - end - - return self -end - - ---- Defines a menu to let the escort set its evasion when under threat. --- All rules of engagement will appear under the menu **Evasion**. --- @param #ESCORT self --- @return #ESCORT -function ESCORT:MenuEvasion( MenuTextFormat ) - self:F( MenuTextFormat ) - - if self.EscortGroup:IsAir() then - if not self.EscortMenuEvasion then - -- Reaction to Threats - self.EscortMenuEvasion = MENU_CLIENT:New( self.EscortClient, "Evasion", self.EscortMenu ) - if self.EscortGroup:OptionROTNoReactionPossible() then - self.EscortMenuEvasionNoReaction = MENU_CLIENT_COMMAND:New( self.EscortClient, "Fight until death", self.EscortMenuEvasion, ESCORT._ROT, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROTNoReaction(), ParamMessage = "Fighting until death!" } ) - end - if self.EscortGroup:OptionROTPassiveDefensePossible() then - self.EscortMenuEvasionPassiveDefense = MENU_CLIENT_COMMAND:New( self.EscortClient, "Use flares, chaff and jammers", self.EscortMenuEvasion, ESCORT._ROT, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROTPassiveDefense(), ParamMessage = "Defending using jammers, chaff and flares!" } ) - end - if self.EscortGroup:OptionROTEvadeFirePossible() then - self.EscortMenuEvasionEvadeFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Evade enemy fire", self.EscortMenuEvasion, ESCORT._ROT, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROTEvadeFire(), ParamMessage = "Evading on enemy fire!" } ) - end - if self.EscortGroup:OptionROTVerticalPossible() then - self.EscortMenuOptionEvasionVertical = MENU_CLIENT_COMMAND:New( self.EscortClient, "Go below radar and evade fire", self.EscortMenuEvasion, ESCORT._ROT, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROTVertical(), ParamMessage = "Evading on enemy fire with vertical manoeuvres!" } ) - end - end - end - - return self -end - ---- Defines a menu to let the escort resume its mission from a waypoint on its route. --- All rules of engagement will appear under the menu **Resume mission from**. --- @param #ESCORT self --- @return #ESCORT -function ESCORT:MenuResumeMission() - self:F() - - if not self.EscortMenuResumeMission then - -- Mission Resume Menu Root - self.EscortMenuResumeMission = MENU_CLIENT:New( self.EscortClient, "Resume mission from", self.EscortMenu ) - end - - return self -end - - ---- @param #MENUPARAM MenuParam -function ESCORT._HoldPosition( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - local OrbitGroup = MenuParam.ParamOrbitGroup -- Group#GROUP - local OrbitUnit = OrbitGroup:GetUnit(1) -- Unit#UNIT - local OrbitHeight = MenuParam.ParamHeight - local OrbitSeconds = MenuParam.ParamSeconds -- Not implemented yet - - self.FollowScheduler:Stop() - - local PointFrom = {} - local GroupPoint = EscortGroup:GetUnit(1):GetPointVec3() - PointFrom = {} - PointFrom.x = GroupPoint.x - PointFrom.y = GroupPoint.z - PointFrom.speed = 250 - PointFrom.type = AI.Task.WaypointType.TURNING_POINT - PointFrom.alt = GroupPoint.y - PointFrom.alt_type = AI.Task.AltitudeType.BARO - - local OrbitPoint = OrbitUnit:GetPointVec2() - local PointTo = {} - PointTo.x = OrbitPoint.x - PointTo.y = OrbitPoint.y - PointTo.speed = 250 - PointTo.type = AI.Task.WaypointType.TURNING_POINT - PointTo.alt = OrbitHeight - PointTo.alt_type = AI.Task.AltitudeType.BARO - PointTo.task = EscortGroup:TaskOrbitCircleAtVec2( OrbitPoint, OrbitHeight, 0 ) - - local Points = { PointFrom, PointTo } - - EscortGroup:OptionROEHoldFire() - EscortGroup:OptionROTPassiveDefense() - - EscortGroup:SetTask( EscortGroup:TaskRoute( Points ) ) - EscortGroup:MessageToClient( "Orbiting at location.", 10, EscortClient ) - -end - ---- @param #MENUPARAM MenuParam -function ESCORT._JoinUpAndFollow( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - self.Distance = MenuParam.ParamDistance - - self:JoinUpAndFollow( EscortGroup, EscortClient, self.Distance ) -end - ---- JoinsUp and Follows a CLIENT. --- @param Escort#ESCORT self --- @param Group#GROUP EscortGroup --- @param Client#CLIENT EscortClient --- @param DCSTypes#Distance Distance -function ESCORT:JoinUpAndFollow( EscortGroup, EscortClient, Distance ) - self:F( { EscortGroup, EscortClient, Distance } ) - - self.FollowScheduler:Stop() - - EscortGroup:OptionROEHoldFire() - EscortGroup:OptionROTPassiveDefense() - - self.EscortMode = ESCORT.MODE.FOLLOW - - self.CT1 = 0 - self.GT1 = 0 - self.FollowScheduler:Start() - - EscortGroup:MessageToClient( "Rejoining and Following at " .. Distance .. "!", 30, EscortClient ) -end - ---- @param #MENUPARAM MenuParam -function ESCORT._Flare( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - local Color = MenuParam.ParamColor - local Message = MenuParam.ParamMessage - - EscortGroup:GetUnit(1):Flare( Color ) - EscortGroup:MessageToClient( Message, 10, EscortClient ) -end - ---- @param #MENUPARAM MenuParam -function ESCORT._Smoke( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - local Color = MenuParam.ParamColor - local Message = MenuParam.ParamMessage - - EscortGroup:GetUnit(1):Smoke( Color ) - EscortGroup:MessageToClient( Message, 10, EscortClient ) -end - - ---- @param #MENUPARAM MenuParam -function ESCORT._ReportNearbyTargetsNow( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - self:_ReportTargetsScheduler() - -end - -function ESCORT._SwitchReportNearbyTargets( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - self.ReportTargets = MenuParam.ParamReportTargets - - if self.ReportTargets then - if not self.ReportTargetsScheduler then - --self.ReportTargetsScheduler = routines.scheduleFunction( self._ReportTargetsScheduler, { self }, timer.getTime() + 1, 30 ) - self.ReportTargetsScheduler = SCHEDULER:New( self, self._ReportTargetsScheduler, {}, 1, 30 ) - end - else - routines.removeFunction( self.ReportTargetsScheduler ) - self.ReportTargetsScheduler = nil - end -end - ---- @param #MENUPARAM MenuParam -function ESCORT._ScanTargets( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - local ScanDuration = MenuParam.ParamScanDuration - - self.FollowScheduler:Stop() - - if EscortGroup:IsHelicopter() then - SCHEDULER:New( EscortGroup, EscortGroup.PushTask, - { EscortGroup:TaskControlled( - EscortGroup:TaskOrbitCircle( 200, 20 ), - EscortGroup:TaskCondition( nil, nil, nil, nil, ScanDuration, nil ) - ) - }, - 1 - ) - elseif EscortGroup:IsAirPlane() then - SCHEDULER:New( EscortGroup, EscortGroup.PushTask, - { EscortGroup:TaskControlled( - EscortGroup:TaskOrbitCircle( 1000, 500 ), - EscortGroup:TaskCondition( nil, nil, nil, nil, ScanDuration, nil ) - ) - }, - 1 - ) - end - - EscortGroup:MessageToClient( "Scanning targets for " .. ScanDuration .. " seconds.", ScanDuration, EscortClient ) - - if self.EscortMode == ESCORT.MODE.FOLLOW then - self.FollowScheduler:Start() - end - -end - ---- @param Group#GROUP EscortGroup -function _Resume( EscortGroup ) - env.info( '_Resume' ) - - local Escort = EscortGroup:GetState( EscortGroup, "Escort" ) - env.info( "EscortMode = " .. Escort.EscortMode ) - if Escort.EscortMode == ESCORT.MODE.FOLLOW then - Escort:JoinUpAndFollow( EscortGroup, Escort.EscortClient, Escort.Distance ) - end - -end - ---- @param #MENUPARAM MenuParam -function ESCORT._AttackTarget( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - - local EscortClient = self.EscortClient - local AttackUnit = MenuParam.ParamUnit -- Unit#UNIT - - self.FollowScheduler:Stop() - - self:T( AttackUnit ) - - if EscortGroup:IsAir() then - EscortGroup:OptionROEOpenFire() - EscortGroup:OptionROTPassiveDefense() - EscortGroup:SetState( EscortGroup, "Escort", self ) --- routines.scheduleFunction( --- EscortGroup.PushTask, --- { EscortGroup, --- EscortGroup:TaskCombo( --- { EscortGroup:TaskAttackUnit( AttackUnit ), --- EscortGroup:TaskFunction( 1, 2, "_Resume", {"''"} ) --- } --- ) --- }, timer.getTime() + 10 --- ) - SCHEDULER:New( EscortGroup, - EscortGroup.PushTask, - { EscortGroup:TaskCombo( - { EscortGroup:TaskAttackUnit( AttackUnit ), - EscortGroup:TaskFunction( 1, 2, "_Resume", { "''" } ) - } - ) - }, 10 - ) - else --- routines.scheduleFunction( --- EscortGroup.PushTask, --- { EscortGroup, --- EscortGroup:TaskCombo( --- { EscortGroup:TaskFireAtPoint( AttackUnit:GetPointVec2(), 50 ) --- } --- ) --- }, timer.getTime() + 10 --- ) - SCHEDULER:New( EscortGroup, - EscortGroup.PushTask, - { EscortGroup:TaskCombo( - { EscortGroup:TaskFireAtPoint( AttackUnit:GetPointVec2(), 50 ) - } - ) - }, 10 - ) - end - - EscortGroup:MessageToClient( "Engaging Designated Unit!", 10, EscortClient ) - -end - ---- @param #MENUPARAM MenuParam -function ESCORT._AssistTarget( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - local EscortGroupAttack = MenuParam.ParamEscortGroup - local AttackUnit = MenuParam.ParamUnit -- Unit#UNIT - - self.FollowScheduler:Stop() - - self:T( AttackUnit ) - - if EscortGroupAttack:IsAir() then - EscortGroupAttack:OptionROEOpenFire() - EscortGroupAttack:OptionROTVertical() --- routines.scheduleFunction( --- EscortGroupAttack.PushTask, --- { EscortGroupAttack, --- EscortGroupAttack:TaskCombo( --- { EscortGroupAttack:TaskAttackUnit( AttackUnit ), --- EscortGroupAttack:TaskOrbitCircle( 500, 350 ) --- } --- ) --- }, timer.getTime() + 10 --- ) - SCHDULER:New( EscortGroupAttack, - EscortGroupAttack.PushTask, - { EscortGroupAttack:TaskCombo( - { EscortGroupAttack:TaskAttackUnit( AttackUnit ), - EscortGroupAttack:TaskOrbitCircle( 500, 350 ) - } - ) - }, 10 - ) - else --- routines.scheduleFunction( --- EscortGroupAttack.PushTask, --- { EscortGroupAttack, --- EscortGroupAttack:TaskCombo( --- { EscortGroupAttack:TaskFireAtPoint( AttackUnit:GetPointVec2(), 50 ) --- } --- ) --- }, timer.getTime() + 10 --- ) - SCHEDULER:New( EscortGroupAttack, - EscortGroupAttack.PushTask, - { EscortGroupAttack:TaskCombo( - { EscortGroupAttack:TaskFireAtPoint( AttackUnit:GetPointVec2(), 50 ) - } - ) - }, 10 - ) - end - EscortGroupAttack:MessageToClient( "Assisting with the destroying the enemy unit!", 10, EscortClient ) - -end - ---- @param #MENUPARAM MenuParam -function ESCORT._ROE( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - local EscortROEFunction = MenuParam.ParamFunction - local EscortROEMessage = MenuParam.ParamMessage - - pcall( function() EscortROEFunction() end ) - EscortGroup:MessageToClient( EscortROEMessage, 10, EscortClient ) -end - ---- @param #MENUPARAM MenuParam -function ESCORT._ROT( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - local EscortROTFunction = MenuParam.ParamFunction - local EscortROTMessage = MenuParam.ParamMessage - - pcall( function() EscortROTFunction() end ) - EscortGroup:MessageToClient( EscortROTMessage, 10, EscortClient ) -end - ---- @param #MENUPARAM MenuParam -function ESCORT._ResumeMission( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - local WayPoint = MenuParam.ParamWayPoint - - self.FollowScheduler:Stop() - - local WayPoints = EscortGroup:GetTaskRoute() - self:T( WayPoint, WayPoints ) - - for WayPointIgnore = 1, WayPoint do - table.remove( WayPoints, 1 ) - end - - --routines.scheduleFunction( EscortGroup.SetTask, {EscortGroup, EscortGroup:TaskRoute( WayPoints ) }, timer.getTime() + 1 ) - SCHEDULER:New( EscortGroup, EscortGroup.SetTask, { EscortGroup:TaskRoute( WayPoints ) }, 1 ) - - EscortGroup:MessageToClient( "Resuming mission from waypoint " .. WayPoint .. ".", 10, EscortClient ) -end - ---- Registers the waypoints --- @param #ESCORT self --- @return #table -function ESCORT:RegisterRoute() - self:F() - - local EscortGroup = self.EscortGroup -- Group#GROUP - - local TaskPoints = EscortGroup:GetTaskRoute() - - self:T( TaskPoints ) - - return TaskPoints -end - ---- @param Escort#ESCORT self -function ESCORT:_FollowScheduler() - self:F( { self.FollowDistance } ) - - self:T( {self.EscortClient.UnitName, self.EscortGroup.GroupName } ) - if self.EscortGroup:IsAlive() and self.EscortClient:IsAlive() then - - local ClientUnit = self.EscortClient:GetClientGroupUnit() - local GroupUnit = self.EscortGroup:GetUnit( 1 ) - local FollowDistance = self.FollowDistance - - self:T( {ClientUnit.UnitName, GroupUnit.UnitName } ) - - if self.CT1 == 0 and self.GT1 == 0 then - self.CV1 = ClientUnit:GetPointVec3() - self:T( { "self.CV1", self.CV1 } ) - self.CT1 = timer.getTime() - self.GV1 = GroupUnit:GetPointVec3() - self.GT1 = timer.getTime() - else - local CT1 = self.CT1 - local CT2 = timer.getTime() - local CV1 = self.CV1 - local CV2 = ClientUnit:GetPointVec3() - self.CT1 = CT2 - self.CV1 = CV2 - - local CD = ( ( CV2.x - CV1.x )^2 + ( CV2.y - CV1.y )^2 + ( CV2.z - CV1.z )^2 ) ^ 0.5 - local CT = CT2 - CT1 - - local CS = ( 3600 / CT ) * ( CD / 1000 ) - - self:T2( { "Client:", CS, CD, CT, CV2, CV1, CT2, CT1 } ) - - local GT1 = self.GT1 - local GT2 = timer.getTime() - local GV1 = self.GV1 - local GV2 = GroupUnit:GetPointVec3() - self.GT1 = GT2 - self.GV1 = GV2 - - local GD = ( ( GV2.x - GV1.x )^2 + ( GV2.y - GV1.y )^2 + ( GV2.z - GV1.z )^2 ) ^ 0.5 - local GT = GT2 - GT1 - - local GS = ( 3600 / GT ) * ( GD / 1000 ) - - self:T2( { "Group:", GS, GD, GT, GV2, GV1, GT2, GT1 } ) - - -- Calculate the group direction vector - local GV = { x = GV2.x - CV2.x, y = GV2.y - CV2.y, z = GV2.z - CV2.z } - - -- Calculate GH2, GH2 with the same height as CV2. - local GH2 = { x = GV2.x, y = CV2.y, z = GV2.z } - - -- Calculate the angle of GV to the orthonormal plane - local alpha = math.atan2( GV.z, GV.x ) - - -- Now we calculate the intersecting vector between the circle around CV2 with radius FollowDistance and GH2. - -- From the GeoGebra model: CVI = (x(CV2) + FollowDistance cos(alpha), y(GH2) + FollowDistance sin(alpha), z(CV2)) - local CVI = { x = CV2.x + FollowDistance * math.cos(alpha), - y = GH2.y, - z = CV2.z + FollowDistance * math.sin(alpha), - } - - -- Calculate the direction vector DV of the escort group. We use CVI as the base and CV2 as the direction. - local DV = { x = CV2.x - CVI.x, y = CV2.y - CVI.y, z = CV2.z - CVI.z } - - -- We now calculate the unary direction vector DVu, so that we can multiply DVu with the speed, which is expressed in meters / s. - -- We need to calculate this vector to predict the point the escort group needs to fly to according its speed. - -- The distance of the destination point should be far enough not to have the aircraft starting to swipe left to right... - local DVu = { x = DV.x / FollowDistance, y = DV.y / FollowDistance, z = DV.z / FollowDistance } - - -- Now we can calculate the group destination vector GDV. - local GDV = { x = DVu.x * CS * 8 + CVI.x, y = CVI.y, z = DVu.z * CS * 8 + CVI.z } - - if self.SmokeDirectionVector == true then - trigger.action.smoke( GDV, trigger.smokeColor.Red ) - end - - self:T2( { "CV2:", CV2 } ) - self:T2( { "CVI:", CVI } ) - self:T2( { "GDV:", GDV } ) - - -- Measure distance between client and group - local CatchUpDistance = ( ( GDV.x - GV2.x )^2 + ( GDV.y - GV2.y )^2 + ( GDV.z - GV2.z )^2 ) ^ 0.5 - - -- The calculation of the Speed would simulate that the group would take 30 seconds to overcome - -- the requested Distance). - local Time = 10 - local CatchUpSpeed = ( CatchUpDistance - ( CS * 8.4 ) ) / Time - - local Speed = CS + CatchUpSpeed - if Speed < 0 then - Speed = 0 - end - - self:T( { "Client Speed, Escort Speed, Speed, FollowDistance, Time:", CS, GS, Speed, FollowDistance, Time } ) - - -- Now route the escort to the desired point with the desired speed. - self.EscortGroup:TaskRouteToVec3( GDV, Speed / 3.6 ) -- DCS models speed in Mps (Miles per second) - end - - return true - end - - return false -end - - ---- Report Targets Scheduler. --- @param #ESCORT self -function ESCORT:_ReportTargetsScheduler() - self:F( self.EscortGroup:GetName() ) - - if self.EscortGroup:IsAlive() and self.EscortClient:IsAlive() then - local EscortGroupName = self.EscortGroup:GetName() - local EscortTargets = self.EscortGroup:GetDetectedTargets() - - local ClientEscortTargets = self.EscortClient._EscortGroups[EscortGroupName].Targets - - local EscortTargetMessages = "" - for EscortTargetID, EscortTarget in pairs( EscortTargets ) do - local EscortObject = EscortTarget.object - self:T( EscortObject ) - if EscortObject and EscortObject:isExist() and EscortObject.id_ < 50000000 then - - local EscortTargetUnit = UNIT:Find( EscortObject ) - local EscortTargetUnitName = EscortTargetUnit:GetName() - - - - -- local EscortTargetIsDetected, - -- EscortTargetIsVisible, - -- EscortTargetLastTime, - -- EscortTargetKnowType, - -- EscortTargetKnowDistance, - -- EscortTargetLastPos, - -- EscortTargetLastVelocity - -- = self.EscortGroup:IsTargetDetected( EscortObject ) - -- - -- self:T( { EscortTargetIsDetected, - -- EscortTargetIsVisible, - -- EscortTargetLastTime, - -- EscortTargetKnowType, - -- EscortTargetKnowDistance, - -- EscortTargetLastPos, - -- EscortTargetLastVelocity } ) - - - local EscortTargetUnitPositionVec3 = EscortTargetUnit:GetPointVec3() - local EscortPositionVec3 = self.EscortGroup:GetPointVec3() - local Distance = ( ( EscortTargetUnitPositionVec3.x - EscortPositionVec3.x )^2 + - ( EscortTargetUnitPositionVec3.y - EscortPositionVec3.y )^2 + - ( EscortTargetUnitPositionVec3.z - EscortPositionVec3.z )^2 - ) ^ 0.5 / 1000 - - self:T( { self.EscortGroup:GetName(), EscortTargetUnit:GetName(), Distance, EscortTarget } ) - - if Distance <= 15 then - - if not ClientEscortTargets[EscortTargetUnitName] then - ClientEscortTargets[EscortTargetUnitName] = {} - end - ClientEscortTargets[EscortTargetUnitName].AttackUnit = EscortTargetUnit - ClientEscortTargets[EscortTargetUnitName].visible = EscortTarget.visible - ClientEscortTargets[EscortTargetUnitName].type = EscortTarget.type - ClientEscortTargets[EscortTargetUnitName].distance = EscortTarget.distance - else - if ClientEscortTargets[EscortTargetUnitName] then - ClientEscortTargets[EscortTargetUnitName] = nil - end - end - end - end - - self:T( { "Sorting Targets Table:", ClientEscortTargets } ) - table.sort( ClientEscortTargets, function( a, b ) return a.Distance < b.Distance end ) - self:T( { "Sorted Targets Table:", ClientEscortTargets } ) - - -- Remove the sub menus of the Attack menu of the Escort for the EscortGroup. - self.EscortMenuAttackNearbyTargets:RemoveSubMenus() - - if self.EscortMenuTargetAssistance then - self.EscortMenuTargetAssistance:RemoveSubMenus() - end - - --for MenuIndex = 1, #self.EscortMenuAttackTargets do - -- self:T( { "Remove Menu:", self.EscortMenuAttackTargets[MenuIndex] } ) - -- self.EscortMenuAttackTargets[MenuIndex] = self.EscortMenuAttackTargets[MenuIndex]:Remove() - --end - - - if ClientEscortTargets then - for ClientEscortTargetUnitName, ClientEscortTargetData in pairs( ClientEscortTargets ) do - - for ClientEscortGroupName, EscortGroupData in pairs( self.EscortClient._EscortGroups ) do - - if ClientEscortTargetData and ClientEscortTargetData.AttackUnit:IsAlive() then - - local EscortTargetMessage = "" - local EscortTargetCategoryName = ClientEscortTargetData.AttackUnit:GetCategoryName() - local EscortTargetCategoryType = ClientEscortTargetData.AttackUnit:GetTypeName() - if ClientEscortTargetData.type then - EscortTargetMessage = EscortTargetMessage .. EscortTargetCategoryName .. " (" .. EscortTargetCategoryType .. ") at " - else - EscortTargetMessage = EscortTargetMessage .. "Unknown target at " - end - - local EscortTargetUnitPositionVec3 = ClientEscortTargetData.AttackUnit:GetPointVec3() - local EscortPositionVec3 = self.EscortGroup:GetPointVec3() - local Distance = ( ( EscortTargetUnitPositionVec3.x - EscortPositionVec3.x )^2 + - ( EscortTargetUnitPositionVec3.y - EscortPositionVec3.y )^2 + - ( EscortTargetUnitPositionVec3.z - EscortPositionVec3.z )^2 - ) ^ 0.5 / 1000 - - self:T( { self.EscortGroup:GetName(), ClientEscortTargetData.AttackUnit:GetName(), Distance, ClientEscortTargetData.AttackUnit } ) - if ClientEscortTargetData.visible == false then - EscortTargetMessage = EscortTargetMessage .. string.format( "%.2f", Distance ) .. " estimated km" - else - EscortTargetMessage = EscortTargetMessage .. string.format( "%.2f", Distance ) .. " km" - end - - if ClientEscortTargetData.visible then - EscortTargetMessage = EscortTargetMessage .. ", visual" - end - - if ClientEscortGroupName == EscortGroupName then - - MENU_CLIENT_COMMAND:New( self.EscortClient, - EscortTargetMessage, - self.EscortMenuAttackNearbyTargets, - ESCORT._AttackTarget, - { ParamSelf = self, - ParamUnit = ClientEscortTargetData.AttackUnit - } - ) - EscortTargetMessages = EscortTargetMessages .. "\n - " .. EscortTargetMessage - else - if self.EscortMenuTargetAssistance then - local MenuTargetAssistance = MENU_CLIENT:New( self.EscortClient, EscortGroupData.EscortName, self.EscortMenuTargetAssistance ) - MENU_CLIENT_COMMAND:New( self.EscortClient, - EscortTargetMessage, - MenuTargetAssistance, - ESCORT._AssistTarget, - { ParamSelf = self, - ParamEscortGroup = EscortGroupData.EscortGroup, - ParamUnit = ClientEscortTargetData.AttackUnit - } - ) - end - end - else - ClientEscortTargetData = nil - end - end - end - - if EscortTargetMessages ~= "" and self.ReportTargets == true then - self.EscortGroup:MessageToClient( "Detected targets within 15 km range:" .. EscortTargetMessages:gsub("\n$",""), 20, self.EscortClient ) - else - self.EscortGroup:MessageToClient( "No targets detected!", 20, self.EscortClient ) - end - end - - if self.EscortMenuResumeMission then - self.EscortMenuResumeMission:RemoveSubMenus() - - -- if self.EscortMenuResumeWayPoints then - -- for MenuIndex = 1, #self.EscortMenuResumeWayPoints do - -- self:T( { "Remove Menu:", self.EscortMenuResumeWayPoints[MenuIndex] } ) - -- self.EscortMenuResumeWayPoints[MenuIndex] = self.EscortMenuResumeWayPoints[MenuIndex]:Remove() - -- end - -- end - - local TaskPoints = self:RegisterRoute() - for WayPointID, WayPoint in pairs( TaskPoints ) do - local EscortPositionVec3 = self.EscortGroup:GetPointVec3() - local Distance = ( ( WayPoint.x - EscortPositionVec3.x )^2 + - ( WayPoint.y - EscortPositionVec3.z )^2 - ) ^ 0.5 / 1000 - MENU_CLIENT_COMMAND:New( self.EscortClient, "Waypoint " .. WayPointID .. " at " .. string.format( "%.2f", Distance ).. "km", self.EscortMenuResumeMission, ESCORT._ResumeMission, { ParamSelf = self, ParamWayPoint = WayPointID } ) - end - end - - return true - end - - return false -end ---- This module contains the MISSILETRAINER class. --- --- === --- --- 1) @{MissileTrainer#MISSILETRAINER} class, extends @{Base#BASE} --- =============================================================== --- The @{#MISSILETRAINER} class uses the DCS world 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: --- --- * **Messages**: Menu to configure all messages. --- * **Messages On**: Show all messages. --- * **Messages Off**: Disable all messages. --- * **Tracking**: Menu to configure missile tracking messages. --- * **To All**: Shows missile tracking messages to all players. --- * **To Target**: Shows missile tracking messages only to the player where the missile is targetted at. --- * **Tracking On**: Show missile tracking messages. --- * **Tracking Off**: Disable missile tracking messages. --- * **Frequency Increase**: Increases the missile tracking message frequency with one second. --- * **Frequency Decrease**: Decreases the missile tracking message frequency with one second. --- * **Alerts**: Menu to configure alert messages. --- * **To All**: Shows alert messages to all players. --- * **To Target**: Shows alert messages only to the player where the missile is (was) targetted at. --- * **Hits On**: Show missile hit alert messages. --- * **Hits Off**: Disable missile hit alert messages. --- * **Launches On**: Show missile launch messages. --- * **Launches Off**: Disable missile launch messages. --- * **Details**: Menu to configure message details. --- * **Range On**: Shows range information when a missile is fired to a target. --- * **Range Off**: Disable range information when a missile is fired to a target. --- * **Bearing On**: Shows bearing information when a missile is fired to a target. --- * **Bearing Off**: Disable bearing information when a missile is fired to a target. --- * **Distance**: Menu to configure the distance when a missile needs to be destroyed when near to a player, during tracking. This will improve/influence hit calculation accuracy, but has the risk of damaging the aircraft when the missile reaches the aircraft before the distance is measured. --- * **50 meter**: Destroys the missile when the distance to the aircraft is below or equal to 50 meter. --- * **100 meter**: Destroys the missile when the distance to the aircraft is below or equal to 100 meter. --- * **150 meter**: Destroys the missile when the distance to the aircraft is below or equal to 150 meter. --- * **200 meter**: Destroys the missile when the distance to the aircraft is below or equal to 200 meter. --- --- --- 1.1) MISSILETRAINER construction methods: --- ----------------------------------------- --- 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: --- ------------------------------------------- --- A MISSILETRAINER object will behave differently based on the usage of initialization methods: --- --- * @{#MISSILETRAINER.InitMessagesOnOff}: Sets by default the display of any message to be ON or OFF. --- * @{#MISSILETRAINER.InitTrackingToAll}: Sets by default the missile tracking report for all players or only for those missiles targetted to you. --- * @{#MISSILETRAINER.InitTrackingOnOff}: Sets by default the display of missile tracking report to be ON or OFF. --- * @{#MISSILETRAINER.InitTrackingFrequency}: Increases, decreases the missile tracking message display frequency with the provided time interval in seconds. --- * @{#MISSILETRAINER.InitAlertsToAll}: Sets by default the display of alerts to be shown to all players or only to you. --- * @{#MISSILETRAINER.InitAlertsHitsOnOff}: Sets by default the display of hit alerts ON or OFF. --- * @{#MISSILETRAINER.InitAlertsLaunchesOnOff}: Sets by default the display of launch alerts ON or OFF. --- * @{#MISSILETRAINER.InitRangeOnOff}: Sets by default the display of range information of missiles ON of OFF. --- * @{#MISSILETRAINER.InitBearingOnOff}: Sets by default the display of bearing information of missiles ON of OFF. --- * @{#MISSILETRAINER.InitMenusOnOff}: Allows to configure the options through the radio menu. --- --- === --- --- 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 Set#SET_CLIENT DBClients --- @extends Base#BASE -MISSILETRAINER = { - ClassName = "MISSILETRAINER", - TrackingMissiles = {}, -} - -function MISSILETRAINER._Alive( Client, self ) - - if self.Briefing then - Client:Message( self.Briefing, 15, "Trainer" ) - end - - if self.MenusOnOff == true then - Client:Message( "Use the 'Radio Menu' -> 'Other (F10)' -> 'Missile Trainer' menu options to change the Missile Trainer settings (for all players).", 15, "Trainer" ) - - Client.MainMenu = MENU_CLIENT:New( Client, "Missile Trainer", nil ) -- Menu#MENU_CLIENT - - Client.MenuMessages = MENU_CLIENT:New( Client, "Messages", Client.MainMenu ) - Client.MenuOn = MENU_CLIENT_COMMAND:New( Client, "Messages On", Client.MenuMessages, self._MenuMessages, { MenuSelf = self, MessagesOnOff = true } ) - Client.MenuOff = MENU_CLIENT_COMMAND:New( Client, "Messages Off", Client.MenuMessages, self._MenuMessages, { MenuSelf = self, MessagesOnOff = false } ) - - Client.MenuTracking = MENU_CLIENT:New( Client, "Tracking", Client.MainMenu ) - Client.MenuTrackingToAll = MENU_CLIENT_COMMAND:New( Client, "To All", Client.MenuTracking, self._MenuMessages, { MenuSelf = self, TrackingToAll = true } ) - Client.MenuTrackingToTarget = MENU_CLIENT_COMMAND:New( Client, "To Target", Client.MenuTracking, self._MenuMessages, { MenuSelf = self, TrackingToAll = false } ) - Client.MenuTrackOn = MENU_CLIENT_COMMAND:New( Client, "Tracking On", Client.MenuTracking, self._MenuMessages, { MenuSelf = self, TrackingOnOff = true } ) - Client.MenuTrackOff = MENU_CLIENT_COMMAND:New( Client, "Tracking Off", Client.MenuTracking, self._MenuMessages, { MenuSelf = self, TrackingOnOff = false } ) - Client.MenuTrackIncrease = MENU_CLIENT_COMMAND:New( Client, "Frequency Increase", Client.MenuTracking, self._MenuMessages, { MenuSelf = self, TrackingFrequency = -1 } ) - Client.MenuTrackDecrease = MENU_CLIENT_COMMAND:New( Client, "Frequency Decrease", Client.MenuTracking, self._MenuMessages, { MenuSelf = self, TrackingFrequency = 1 } ) - - Client.MenuAlerts = MENU_CLIENT:New( Client, "Alerts", Client.MainMenu ) - Client.MenuAlertsToAll = MENU_CLIENT_COMMAND:New( Client, "To All", Client.MenuAlerts, self._MenuMessages, { MenuSelf = self, AlertsToAll = true } ) - Client.MenuAlertsToTarget = MENU_CLIENT_COMMAND:New( Client, "To Target", Client.MenuAlerts, self._MenuMessages, { MenuSelf = self, AlertsToAll = false } ) - Client.MenuHitsOn = MENU_CLIENT_COMMAND:New( Client, "Hits On", Client.MenuAlerts, self._MenuMessages, { MenuSelf = self, AlertsHitsOnOff = true } ) - Client.MenuHitsOff = MENU_CLIENT_COMMAND:New( Client, "Hits Off", Client.MenuAlerts, self._MenuMessages, { MenuSelf = self, AlertsHitsOnOff = false } ) - Client.MenuLaunchesOn = MENU_CLIENT_COMMAND:New( Client, "Launches On", Client.MenuAlerts, self._MenuMessages, { MenuSelf = self, AlertsLaunchesOnOff = true } ) - Client.MenuLaunchesOff = MENU_CLIENT_COMMAND:New( Client, "Launches Off", Client.MenuAlerts, self._MenuMessages, { MenuSelf = self, AlertsLaunchesOnOff = false } ) - - Client.MenuDetails = MENU_CLIENT:New( Client, "Details", Client.MainMenu ) - Client.MenuDetailsDistanceOn = MENU_CLIENT_COMMAND:New( Client, "Range On", Client.MenuDetails, self._MenuMessages, { MenuSelf = self, DetailsRangeOnOff = true } ) - Client.MenuDetailsDistanceOff = MENU_CLIENT_COMMAND:New( Client, "Range Off", Client.MenuDetails, self._MenuMessages, { MenuSelf = self, DetailsRangeOnOff = false } ) - Client.MenuDetailsBearingOn = MENU_CLIENT_COMMAND:New( Client, "Bearing On", Client.MenuDetails, self._MenuMessages, { MenuSelf = self, DetailsBearingOnOff = true } ) - Client.MenuDetailsBearingOff = MENU_CLIENT_COMMAND:New( Client, "Bearing Off", Client.MenuDetails, self._MenuMessages, { MenuSelf = self, DetailsBearingOnOff = false } ) - - Client.MenuDistance = MENU_CLIENT:New( Client, "Set distance to plane", Client.MainMenu ) - Client.MenuDistance50 = MENU_CLIENT_COMMAND:New( Client, "50 meter", Client.MenuDistance, self._MenuMessages, { MenuSelf = self, Distance = 50 / 1000 } ) - Client.MenuDistance100 = MENU_CLIENT_COMMAND:New( Client, "100 meter", Client.MenuDistance, self._MenuMessages, { MenuSelf = self, Distance = 100 / 1000 } ) - Client.MenuDistance150 = MENU_CLIENT_COMMAND:New( Client, "150 meter", Client.MenuDistance, self._MenuMessages, { MenuSelf = self, Distance = 150 / 1000 } ) - Client.MenuDistance200 = MENU_CLIENT_COMMAND:New( Client, "200 meter", Client.MenuDistance, self._MenuMessages, { MenuSelf = self, Distance = 200 / 1000 } ) - else - if Client.MainMenu then - Client.MainMenu:Remove() - end - end - - local ClientID = Client:GetID() - self:T( ClientID ) - if not self.TrackingMissiles[ClientID] then - self.TrackingMissiles[ClientID] = {} - end - self.TrackingMissiles[ClientID].Client = Client - if not self.TrackingMissiles[ClientID].MissileData then - self.TrackingMissiles[ClientID].MissileData = {} - end -end - ---- Creates the main object which is handling missile tracking. --- When a missile is fired a SCHEDULER is set off that follows the missile. When near a certain a client player, the missile will be destroyed. --- @param #MISSILETRAINER self --- @param #number Distance The distance in meters when a tracked missile needs to be destroyed when close to a player. --- @param #string Briefing (Optional) Will show a text to the players when starting their mission. Can be used for briefing purposes. --- @return #MISSILETRAINER -function MISSILETRAINER:New( Distance, Briefing ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( Distance ) - - if Briefing then - self.Briefing = Briefing - end - - self.Schedulers = {} - self.SchedulerID = 0 - - self.MessageInterval = 2 - self.MessageLastTime = timer.getTime() - - self.Distance = Distance / 1000 - - _EVENTDISPATCHER:OnShot( self._EventShot, self ) - - self.DBClients = SET_CLIENT:New():FilterStart() - - --- for ClientID, Client in pairs( self.DBClients.Database ) do --- self:E( "ForEach:" .. Client.UnitName ) --- Client:Alive( self._Alive, self ) --- end --- - self.DBClients:ForEachClient( - function( Client ) - self:E( "ForEach:" .. Client.UnitName ) - Client:Alive( self._Alive, self ) - end - ) - - - --- self.DB:ForEachClient( --- --- @param Client#CLIENT Client --- function( Client ) --- --- ... actions ... --- --- end --- ) - - self.MessagesOnOff = true - - self.TrackingToAll = false - self.TrackingOnOff = true - self.TrackingFrequency = 3 - - self.AlertsToAll = true - self.AlertsHitsOnOff = true - self.AlertsLaunchesOnOff = true - - self.DetailsRangeOnOff = true - self.DetailsBearingOnOff = true - - self.MenusOnOff = true - - self.TrackingMissiles = {} - - self.TrackingScheduler = SCHEDULER:New( self, self._TrackMissiles, {}, 0.5, 0.05, 0 ) - - return self -end - --- Initialization methods. - - - ---- Sets by default the display of any message to be ON or OFF. --- @param #MISSILETRAINER self --- @param #boolean MessagesOnOff true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitMessagesOnOff( MessagesOnOff ) - self:F( MessagesOnOff ) - - self.MessagesOnOff = MessagesOnOff - if self.MessagesOnOff == true then - MESSAGE:New( "Messages ON", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Messages OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Sets by default the missile tracking report for all players or only for those missiles targetted to you. --- @param #MISSILETRAINER self --- @param #boolean TrackingToAll true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitTrackingToAll( TrackingToAll ) - self:F( TrackingToAll ) - - self.TrackingToAll = TrackingToAll - if self.TrackingToAll == true then - MESSAGE:New( "Missile tracking to all players ON", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Missile tracking to all players OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Sets by default the display of missile tracking report to be ON or OFF. --- @param #MISSILETRAINER self --- @param #boolean TrackingOnOff true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitTrackingOnOff( TrackingOnOff ) - self:F( TrackingOnOff ) - - self.TrackingOnOff = TrackingOnOff - if self.TrackingOnOff == true then - MESSAGE:New( "Missile tracking ON", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Missile tracking OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Increases, decreases the missile tracking message display frequency with the provided time interval in seconds. --- The default frequency is a 3 second interval, so the Tracking Frequency parameter specifies the increase or decrease from the default 3 seconds or the last frequency update. --- @param #MISSILETRAINER self --- @param #number TrackingFrequency Provide a negative or positive value in seconds to incraese or decrease the display frequency. --- @return #MISSILETRAINER self -function MISSILETRAINER:InitTrackingFrequency( TrackingFrequency ) - self:F( TrackingFrequency ) - - self.TrackingFrequency = self.TrackingFrequency + TrackingFrequency - if self.TrackingFrequency < 0.5 then - self.TrackingFrequency = 0.5 - end - if self.TrackingFrequency then - MESSAGE:New( "Missile tracking frequency is " .. self.TrackingFrequency .. " seconds.", 15, "Menu" ):ToAll() - end - - return self -end - ---- Sets by default the display of alerts to be shown to all players or only to you. --- @param #MISSILETRAINER self --- @param #boolean AlertsToAll true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitAlertsToAll( AlertsToAll ) - self:F( AlertsToAll ) - - self.AlertsToAll = AlertsToAll - if self.AlertsToAll == true then - MESSAGE:New( "Alerts to all players ON", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Alerts to all players OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Sets by default the display of hit alerts ON or OFF. --- @param #MISSILETRAINER self --- @param #boolean AlertsHitsOnOff true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitAlertsHitsOnOff( AlertsHitsOnOff ) - self:F( AlertsHitsOnOff ) - - self.AlertsHitsOnOff = AlertsHitsOnOff - if self.AlertsHitsOnOff == true then - MESSAGE:New( "Alerts Hits ON", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Alerts Hits OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Sets by default the display of launch alerts ON or OFF. --- @param #MISSILETRAINER self --- @param #boolean AlertsLaunchesOnOff true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitAlertsLaunchesOnOff( AlertsLaunchesOnOff ) - self:F( AlertsLaunchesOnOff ) - - self.AlertsLaunchesOnOff = AlertsLaunchesOnOff - if self.AlertsLaunchesOnOff == true then - MESSAGE:New( "Alerts Launches ON", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Alerts Launches OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Sets by default the display of range information of missiles ON of OFF. --- @param #MISSILETRAINER self --- @param #boolean DetailsRangeOnOff true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitRangeOnOff( DetailsRangeOnOff ) - self:F( DetailsRangeOnOff ) - - self.DetailsRangeOnOff = DetailsRangeOnOff - if self.DetailsRangeOnOff == true then - MESSAGE:New( "Range display ON", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Range display OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Sets by default the display of bearing information of missiles ON of OFF. --- @param #MISSILETRAINER self --- @param #boolean DetailsBearingOnOff true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitBearingOnOff( DetailsBearingOnOff ) - self:F( DetailsBearingOnOff ) - - self.DetailsBearingOnOff = DetailsBearingOnOff - if self.DetailsBearingOnOff == true then - MESSAGE:New( "Bearing display OFF", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Bearing display OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Enables / Disables the menus. --- @param #MISSILETRAINER self --- @param #boolean MenusOnOff true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitMenusOnOff( MenusOnOff ) - self:F( MenusOnOff ) - - self.MenusOnOff = MenusOnOff - if self.MenusOnOff == true then - MESSAGE:New( "Menus are ENABLED (only when a player rejoins a slot)", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Menus are DISABLED", 15, "Menu" ):ToAll() - end - - return self -end - - --- Menu functions - -function MISSILETRAINER._MenuMessages( MenuParameters ) - - local self = MenuParameters.MenuSelf - - if MenuParameters.MessagesOnOff ~= nil then - self:InitMessagesOnOff( MenuParameters.MessagesOnOff ) - end - - if MenuParameters.TrackingToAll ~= nil then - self:InitTrackingToAll( MenuParameters.TrackingToAll ) - end - - if MenuParameters.TrackingOnOff ~= nil then - self:InitTrackingOnOff( MenuParameters.TrackingOnOff ) - end - - if MenuParameters.TrackingFrequency ~= nil then - self:InitTrackingFrequency( MenuParameters.TrackingFrequency ) - end - - if MenuParameters.AlertsToAll ~= nil then - self:InitAlertsToAll( MenuParameters.AlertsToAll ) - end - - if MenuParameters.AlertsHitsOnOff ~= nil then - self:InitAlertsHitsOnOff( MenuParameters.AlertsHitsOnOff ) - end - - if MenuParameters.AlertsLaunchesOnOff ~= nil then - self:InitAlertsLaunchesOnOff( MenuParameters.AlertsLaunchesOnOff ) - end - - if MenuParameters.DetailsRangeOnOff ~= nil then - self:InitRangeOnOff( MenuParameters.DetailsRangeOnOff ) - end - - if MenuParameters.DetailsBearingOnOff ~= nil then - self:InitBearingOnOff( MenuParameters.DetailsBearingOnOff ) - end - - if MenuParameters.Distance ~= nil then - self.Distance = MenuParameters.Distance - MESSAGE:New( "Hit detection distance set to " .. self.Distance .. " meters", 15, "Menu" ):ToAll() - end - -end - ---- Detects if an SA site was shot with an anti radiation missile. In this case, take evasive actions based on the skill level set within the ME. --- @param #MISSILETRAINER self --- @param Event#EVENTDATA Event -function MISSILETRAINER:_EventShot( Event ) - self:F( { Event } ) - - local TrainerSourceDCSUnit = Event.IniDCSUnit - local TrainerSourceDCSUnitName = Event.IniDCSUnitName - local TrainerWeapon = Event.Weapon -- Identify the weapon fired - local TrainerWeaponName = Event.WeaponName -- return weapon type - - self:T( "Missile Launched = " .. TrainerWeaponName ) - - local TrainerTargetDCSUnit = TrainerWeapon:getTarget() -- Identify target - local TrainerTargetDCSUnitName = Unit.getName( TrainerTargetDCSUnit ) - local TrainerTargetSkill = _DATABASE.Templates.Units[TrainerTargetDCSUnitName].Template.skill - - self:T(TrainerTargetDCSUnitName ) - - local Client = self.DBClients:FindClient( TrainerTargetDCSUnitName ) - if Client then - - local TrainerSourceUnit = UNIT:Find( TrainerSourceDCSUnit ) - local TrainerTargetUnit = UNIT:Find( TrainerTargetDCSUnit ) - - if self.MessagesOnOff == true and self.AlertsLaunchesOnOff == true then - - local Message = MESSAGE:New( - string.format( "%s launched a %s", - TrainerSourceUnit:GetTypeName(), - TrainerWeaponName - ) .. self:_AddRange( Client, TrainerWeapon ) .. self:_AddBearing( Client, TrainerWeapon ), 5, "Launch Alert" ) - - if self.AlertsToAll then - Message:ToAll() - else - Message:ToClient( Client ) - end - end - - local ClientID = Client:GetID() - self:T( ClientID ) - local MissileData = {} - MissileData.TrainerSourceUnit = TrainerSourceUnit - MissileData.TrainerWeapon = TrainerWeapon - MissileData.TrainerTargetUnit = TrainerTargetUnit - MissileData.TrainerWeaponTypeName = TrainerWeapon:getTypeName() - MissileData.TrainerWeaponLaunched = true - table.insert( self.TrackingMissiles[ClientID].MissileData, MissileData ) - --self:T( self.TrackingMissiles ) - end -end - -function MISSILETRAINER:_AddRange( Client, TrainerWeapon ) - - local RangeText = "" - - if self.DetailsRangeOnOff then - - local PositionMissile = TrainerWeapon:getPoint() - local PositionTarget = Client:GetPointVec3() - - local Range = ( ( PositionMissile.x - PositionTarget.x )^2 + - ( PositionMissile.y - PositionTarget.y )^2 + - ( PositionMissile.z - PositionTarget.z )^2 - ) ^ 0.5 / 1000 - - RangeText = string.format( ", at %4.2fkm", Range ) - end - - return RangeText -end - -function MISSILETRAINER:_AddBearing( Client, TrainerWeapon ) - - local BearingText = "" - - if self.DetailsBearingOnOff then - - local PositionMissile = TrainerWeapon:getPoint() - local PositionTarget = Client:GetPointVec3() - - self:T2( { PositionTarget, PositionMissile }) - - local DirectionVector = { x = PositionMissile.x - PositionTarget.x, y = PositionMissile.y - PositionTarget.y, z = PositionMissile.z - PositionTarget.z } - local DirectionRadians = math.atan2( DirectionVector.z, DirectionVector.x ) - --DirectionRadians = DirectionRadians + routines.getNorthCorrection( PositionTarget ) - if DirectionRadians < 0 then - DirectionRadians = DirectionRadians + 2 * math.pi - end - local DirectionDegrees = DirectionRadians * 180 / math.pi - - BearingText = string.format( ", %d degrees", DirectionDegrees ) - end - - return BearingText -end - - -function MISSILETRAINER:_TrackMissiles() - self:F2() - - - local ShowMessages = false - if self.MessagesOnOff and self.MessageLastTime + self.TrackingFrequency <= timer.getTime() then - self.MessageLastTime = timer.getTime() - ShowMessages = true - end - - -- ALERTS PART - - -- Loop for all Player Clients to check the alerts and deletion of missiles. - for ClientDataID, ClientData in pairs( self.TrackingMissiles ) do - - local Client = ClientData.Client - self:T2( { Client:GetName() } ) - - for MissileDataID, MissileData in pairs( ClientData.MissileData ) do - self:T3( MissileDataID ) - - local TrainerSourceUnit = MissileData.TrainerSourceUnit - local TrainerWeapon = MissileData.TrainerWeapon - local TrainerTargetUnit = MissileData.TrainerTargetUnit - local TrainerWeaponTypeName = MissileData.TrainerWeaponTypeName - local TrainerWeaponLaunched = MissileData.TrainerWeaponLaunched - - if Client and Client:IsAlive() and TrainerSourceUnit and TrainerSourceUnit:IsAlive() and TrainerWeapon and TrainerWeapon:isExist() and TrainerTargetUnit and TrainerTargetUnit:IsAlive() then - local PositionMissile = TrainerWeapon:getPosition().p - local PositionTarget = Client:GetPointVec3() - - local Distance = ( ( PositionMissile.x - PositionTarget.x )^2 + - ( PositionMissile.y - PositionTarget.y )^2 + - ( PositionMissile.z - PositionTarget.z )^2 - ) ^ 0.5 / 1000 - - if Distance <= self.Distance then - -- Hit alert - TrainerWeapon:destroy() - if self.MessagesOnOff == true and self.AlertsHitsOnOff == true then - - self:T( "killed" ) - - local Message = MESSAGE:New( - string.format( "%s launched by %s killed %s", - TrainerWeapon:getTypeName(), - TrainerSourceUnit:GetTypeName(), - TrainerTargetUnit:GetPlayerName() - ), 15, "Hit Alert" ) - - if self.AlertsToAll == true then - Message:ToAll() - else - Message:ToClient( Client ) - end - - MissileData = nil - table.remove( ClientData.MissileData, MissileDataID ) - self:T(ClientData.MissileData) - end - end - else - if not ( TrainerWeapon and TrainerWeapon:isExist() ) then - if self.MessagesOnOff == true and self.AlertsLaunchesOnOff == true then - -- Weapon does not exist anymore. Delete from Table - local Message = MESSAGE:New( - string.format( "%s launched by %s self destructed!", - TrainerWeaponTypeName, - TrainerSourceUnit:GetTypeName() - ), 5, "Tracking" ) - - if self.AlertsToAll == true then - Message:ToAll() - else - Message:ToClient( Client ) - end - end - MissileData = nil - table.remove( ClientData.MissileData, MissileDataID ) - self:T( ClientData.MissileData ) - end - end - end - end - - if ShowMessages == true and self.MessagesOnOff == true and self.TrackingOnOff == true then -- Only do this when tracking information needs to be displayed. - - -- TRACKING PART - - -- For the current client, the missile range and bearing details are displayed To the Player Client. - -- For the other clients, the missile range and bearing details are displayed To the other Player Clients. - -- To achieve this, a cross loop is done for each Player Client <-> Other Player Client missile information. - - -- Main Player Client loop - for ClientDataID, ClientData in pairs( self.TrackingMissiles ) do - - local Client = ClientData.Client - self:T2( { Client:GetName() } ) - - - ClientData.MessageToClient = "" - ClientData.MessageToAll = "" - - -- Other Players Client loop - for TrackingDataID, TrackingData in pairs( self.TrackingMissiles ) do - - for MissileDataID, MissileData in pairs( TrackingData.MissileData ) do - self:T3( MissileDataID ) - - local TrainerSourceUnit = MissileData.TrainerSourceUnit - local TrainerWeapon = MissileData.TrainerWeapon - local TrainerTargetUnit = MissileData.TrainerTargetUnit - local TrainerWeaponTypeName = MissileData.TrainerWeaponTypeName - local TrainerWeaponLaunched = MissileData.TrainerWeaponLaunched - - if Client and Client:IsAlive() and TrainerSourceUnit and TrainerSourceUnit:IsAlive() and TrainerWeapon and TrainerWeapon:isExist() and TrainerTargetUnit and TrainerTargetUnit:IsAlive() then - - if ShowMessages == true then - local TrackingTo - TrackingTo = string.format( " -> %s", - TrainerWeaponTypeName - ) - - if ClientDataID == TrackingDataID then - if ClientData.MessageToClient == "" then - ClientData.MessageToClient = "Missiles to You:\n" - end - ClientData.MessageToClient = ClientData.MessageToClient .. TrackingTo .. self:_AddRange( ClientData.Client, TrainerWeapon ) .. self:_AddBearing( ClientData.Client, TrainerWeapon ) .. "\n" - else - if self.TrackingToAll == true then - if ClientData.MessageToAll == "" then - ClientData.MessageToAll = "Missiles to other Players:\n" - end - ClientData.MessageToAll = ClientData.MessageToAll .. TrackingTo .. self:_AddRange( ClientData.Client, TrainerWeapon ) .. self:_AddBearing( ClientData.Client, TrainerWeapon ) .. " ( " .. TrainerTargetUnit:GetPlayerName() .. " )\n" - end - end - end - end - end - end - - -- Once the Player Client and the Other Player Client tracking messages are prepared, show them. - if ClientData.MessageToClient ~= "" or ClientData.MessageToAll ~= "" then - local Message = MESSAGE:New( ClientData.MessageToClient .. ClientData.MessageToAll, 1, "Tracking" ):ToClient( Client ) - end - end - end - - return true -end ---- This module contains the PATROLZONE class. --- --- === --- --- 1) @{Patrol#PATROLZONE} class, extends @{Base#BASE} --- =================================================== --- The @{Patrol#PATROLZONE} class implements the core functions to patrol a @{Zone}. --- --- 1.1) PATROLZONE constructor: --- ---------------------------- --- @{PatrolZone#PATROLZONE.New}(): Creates a new PATROLZONE object. --- --- 1.2) Modify the PATROLZONE parameters: --- -------------------------------------- --- The following methods are available to modify the parameters of a PATROLZONE object: --- --- * @{PatrolZone#PATROLZONE.SetGroup}(): Set the AI Patrol Group. --- * @{PatrolZone#PATROLZONE.SetSpeed}(): Set the patrol speed of the AI, for the next patrol. --- * @{PatrolZone#PATROLZONE.SetAltitude}(): Set altitude of the AI, for the next patrol. --- --- 1.3) Manage the out of fuel in the PATROLZONE: --- ---------------------------------------------- --- When the PatrolGroup is out of fuel, it is required that a new PatrolGroup is started, before the old PatrolGroup 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 PatrolGroup will continue for a given time its patrol task in orbit, while a new PatrolGroup is targetted to the PATROLZONE. --- Once the time is finished, the old PatrolGroup will return to the base. --- Use the method @{PatrolZone#PATROLZONE.ManageFuel}() to have this proces in place. --- --- === --- --- @module PatrolZone --- @author FlightControl - - ---- PATROLZONE class --- @type PATROLZONE --- @field Group#GROUP PatrolGroup The @{Group} patrolling. --- @field Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. --- @field DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @field DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @field DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Group} in km/h. --- @field DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Group} in km/h. --- @extends Base#BASE -PATROLZONE = { - ClassName = "PATROLZONE", -} - ---- Creates a new PATROLZONE object, taking a @{Group} object as a parameter. The GROUP needs to be alive. --- @param #PATROLZONE self --- @param Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. --- @param DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @param DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @param DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Group} in km/h. --- @param DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Group} in km/h. --- @return #PATROLZONE self --- @usage --- -- Define a new PATROLZONE Object. This PatrolArea will patrol a group within PatrolZone between 3000 and 6000 meters, with a variying speed between 600 and 900 km/h. --- PatrolZone = ZONE:New( 'PatrolZone' ) --- PatrolGroup = GROUP:FindByName( "Patrol Group" ) --- PatrolArea = PATROLZONE:New( PatrolGroup, PatrolZone, 3000, 6000, 600, 900 ) -function PATROLZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed ) - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) - - self.PatrolZone = PatrolZone - self.PatrolFloorAltitude = PatrolFloorAltitude - self.PatrolCeilingAltitude = PatrolCeilingAltitude - self.PatrolMinSpeed = PatrolMinSpeed - self.PatrolMaxSpeed = PatrolMaxSpeed - - return self -end - ---- Set the @{Group} to act as the Patroller. --- @param #PATROLZONE self --- @param Group#GROUP PatrolGroup The @{Group} patrolling. --- @return #PATROLZONE self -function PATROLZONE:SetGroup( PatrolGroup ) - - self.PatrolGroup = PatrolGroup - self.PatrolGroupTemplateName = PatrolGroup:GetName() - self:NewPatrolRoute() - - if not self.PatrolOutOfFuelMonitor then - self.PatrolOutOfFuelMonitor = SCHEDULER:New( nil, _MonitorOutOfFuelScheduled, { self }, 1, 120, 0 ) - self.SpawnPatrolGroup = SPAWN:New( self.PatrolGroupTemplateName ) - end - - return self -end - ---- Sets (modifies) the minimum and maximum speed of the patrol. --- @param #PATROLZONE self --- @param DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Group} in km/h. --- @param DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Group} in km/h. --- @return #PATROLZONE self -function PATROLZONE:SetSpeed( PatrolMinSpeed, PatrolMaxSpeed ) - self:F2( { PatrolMinSpeed, PatrolMaxSpeed } ) - - self.PatrolMinSpeed = PatrolMinSpeed - self.PatrolMaxSpeed = PatrolMaxSpeed -end - ---- Sets the floor and ceiling altitude of the patrol. --- @param #PATROLZONE self --- @param DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @param DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @return #PATROLZONE self -function PATROLZONE:SetAltitude( PatrolFloorAltitude, PatrolCeilingAltitude ) - self:F2( { PatrolFloorAltitude, PatrolCeilingAltitude } ) - - self.PatrolFloorAltitude = PatrolFloorAltitude - self.PatrolCeilingAltitude = PatrolCeilingAltitude -end - - - ---- @param Group#GROUP PatrolGroup -function _NewPatrolRoute( PatrolGroup ) - - PatrolGroup:T( "NewPatrolRoute" ) - local PatrolZone = PatrolGroup:GetState( PatrolGroup, "PatrolZone" ) -- PatrolZone#PATROLZONE - PatrolZone:NewPatrolRoute() -end - ---- Defines a new patrol route using the @{PatrolZone} parameters and settings. --- @param #PATROLZONE self --- @return #PATROLZONE self -function PATROLZONE:NewPatrolRoute() - - self:F2() - - local PatrolRoute = {} - - if self.PatrolGroup:IsAlive() then - --- Determine if the PatrolGroup is within the PatrolZone. - -- If not, make a waypoint within the to that the PatrolGroup will fly at maximum speed to that point. - --- --- Calculate the current route point. --- local CurrentVec2 = self.PatrolGroup:GetPointVec2() --- local CurrentAltitude = self.PatrolGroup:GetUnit(1):GetAltitude() --- local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) --- local CurrentRoutePoint = CurrentPointVec3:RoutePointAir( --- POINT_VEC3.RoutePointAltType.BARO, --- POINT_VEC3.RoutePointType.TurningPoint, --- POINT_VEC3.RoutePointAction.TurningPoint, --- ToPatrolZoneSpeed, --- true --- ) --- --- PatrolRoute[#PatrolRoute+1] = CurrentRoutePoint - - self:T2( PatrolRoute ) - - if self.PatrolGroup:IsNotInZone( self.PatrolZone ) then - --- Find a random 2D point in PatrolZone. - local ToPatrolZoneVec2 = self.PatrolZone:GetRandomVec2() - self:T2( ToPatrolZoneVec2 ) - - --- Define Speed and Altitude. - local ToPatrolZoneAltitude = math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ) - local ToPatrolZoneSpeed = self.PatrolMaxSpeed - self:T2( ToPatrolZoneSpeed ) - - --- Obtain a 3D @{Point} from the 2D point + altitude. - local ToPatrolZonePointVec3 = POINT_VEC3:New( ToPatrolZoneVec2.x, ToPatrolZoneAltitude, ToPatrolZoneVec2.y ) - - --- Create a route point of type air. - local ToPatrolZoneRoutePoint = ToPatrolZonePointVec3:RoutePointAir( - POINT_VEC3.RoutePointAltType.BARO, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToPatrolZoneSpeed, - true - ) - - PatrolRoute[#PatrolRoute+1] = ToPatrolZoneRoutePoint - - end - - --- Define a random point in the @{Zone}. The AI will fly to that point within the zone. - - --- Find a random 2D point in PatrolZone. - local ToTargetVec2 = self.PatrolZone:GetRandomVec2() - self:T2( ToTargetVec2 ) - - --- Define Speed and Altitude. - local ToTargetAltitude = math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ) - local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) - self:T2( { self.PatrolMinSpeed, self.PatrolMaxSpeed, ToTargetSpeed } ) - - --- Obtain a 3D @{Point} from the 2D point + altitude. - local ToTargetPointVec3 = POINT_VEC3:New( ToTargetVec2.x, ToTargetAltitude, ToTargetVec2.y ) - - --- Create a route point of type air. - local ToTargetRoutePoint = ToTargetPointVec3:RoutePointAir( - POINT_VEC3.RoutePointAltType.BARO, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, - true - ) - - --ToTargetPointVec3:SmokeRed() - - PatrolRoute[#PatrolRoute+1] = ToTargetRoutePoint - - --- Now we're going to do something special, we're going to call a function from a waypoint action at the PatrolGroup... - self.PatrolGroup:WayPointInitialize( PatrolRoute ) - - --- Do a trick, link the NewPatrolRoute function of the PATROLGROUP object to the PatrolGroup in a temporary variable ... - self.PatrolGroup:SetState( self.PatrolGroup, "PatrolZone", self ) - self.PatrolGroup:WayPointFunction( #PatrolRoute, 1, "_NewPatrolRoute" ) - - --- NOW ROUTE THE GROUP! - self.PatrolGroup:WayPointExecute( 1, 2 ) - end - -end - ---- When the PatrolGroup is out of fuel, it is required that a new PatrolGroup is started, before the old PatrolGroup 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 PatrolGroup will continue for a given time its patrol task in orbit, while a new PatrolGroup is targetted to the PATROLZONE. --- Once the time is finished, the old PatrolGroup will return to the base. --- @param #PATROLZONE self --- @param #number PatrolFuelTresholdPercentage The treshold in percentage (between 0 and 1) when the PatrolGroup is considered to get out of fuel. --- @param #number PatrolOutOfFuelOrbitTime The amount of seconds the out of fuel PatrolGroup will orbit before returning to the base. --- @return #PATROLZONE self -function PATROLZONE:ManageFuel( PatrolFuelTresholdPercentage, PatrolOutOfFuelOrbitTime ) - - self.PatrolManageFuel = true - self.PatrolFuelTresholdPercentage = PatrolFuelTresholdPercentage - self.PatrolOutOfFuelOrbitTime = PatrolOutOfFuelOrbitTime - - if self.PatrolGroup then - self.PatrolOutOfFuelMonitor = SCHEDULER:New( self, self._MonitorOutOfFuelScheduled, {}, 1, 120, 0 ) - self.SpawnPatrolGroup = SPAWN:New( self.PatrolGroupTemplateName ) - end - return self -end - ---- @param #PATROLZONE self -function _MonitorOutOfFuelScheduled( self ) - self:F2( "_MonitorOutOfFuelScheduled" ) - - if self.PatrolGroup and self.PatrolGroup:IsAlive() then - - local Fuel = self.PatrolGroup:GetUnit(1):GetFuel() - if Fuel < self.PatrolFuelTresholdPercentage then - local OldPatrolGroup = self.PatrolGroup - local PatrolGroupTemplate = self.PatrolGroup:GetTemplate() - - local OrbitTask = OldPatrolGroup:TaskOrbitCircle( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ), self.PatrolMinSpeed ) - local TimedOrbitTask = OldPatrolGroup:TaskControlled( OrbitTask, OldPatrolGroup:TaskCondition(nil,nil,nil,nil,self.PatrolOutOfFuelOrbitTime,nil ) ) - OldPatrolGroup:SetTask( TimedOrbitTask, 10 ) - - local NewPatrolGroup = self.SpawnPatrolGroup:Spawn() - self.PatrolGroup = NewPatrolGroup - self:NewPatrolRoute() - end - else - self.PatrolOutOfFuelMonitor:Stop() - end -end--- This module contains the AIBALANCER class. --- --- === --- --- 1) @{AIBalancer#AIBALANCER} class, extends @{Base#BASE} --- ================================================ --- The @{AIBalancer#AIBALANCER} class controls the dynamic spawning of AI GROUPS depending on a SET_CLIENT. --- There will be as many AI GROUPS spawned as there at CLIENTS in SET_CLIENT not spawned. --- --- 1.1) AIBALANCER construction method: --- ------------------------------------ --- Create a new AIBALANCER object with the @{#AIBALANCER.New} method: --- --- * @{#AIBALANCER.New}: Creates a new AIBALANCER object. --- --- 1.2) AIBALANCER returns AI to Airbases: --- --------------------------------------- --- You can configure to have the AI to return to: --- --- * @{#AIBALANCER.ReturnToHomeAirbase}: Returns the AI to the home @{Airbase#AIRBASE}. --- * @{#AIBALANCER.ReturnToNearestAirbases}: Returns the AI to the nearest friendly @{Airbase#AIRBASE}. --- --- 1.3) AIBALANCER allows AI to patrol specific zones: --- --------------------------------------------------- --- Use @{AIBalancer#AIBALANCER.SetPatrolZone}() to specify a zone where the AI needs to patrol. --- --- --- === --- --- CREDITS --- ======= --- **Dutch_Baron (James)** Who you can search on the Eagle Dynamics Forums. --- Working together with James has resulted in the creation of the AIBALANCER class. --- James has shared his ideas on balancing AI with air units, and together we made a first design which you can use now :-) --- --- **SNAFU** --- Had a couple of mails with the guys to validate, if the same concept in the GCI/CAP script could be reworked within MOOSE. --- None of the script code has been used however within the new AIBALANCER moose class. --- --- @module AIBalancer --- @author FlightControl - ---- AIBALANCER class --- @type AIBALANCER --- @field Set#SET_CLIENT SetClient --- @field Spawn#SPAWN SpawnAI --- @field #boolean ToNearestAirbase --- @field Set#SET_AIRBASE ReturnAirbaseSet --- @field DCSTypes#Distance ReturnTresholdRange --- @field #boolean ToHomeAirbase --- @field PatrolZone#PATROLZONE PatrolZone --- @extends Base#BASE -AIBALANCER = { - ClassName = "AIBALANCER", - PatrolZones = {}, - AIGroups = {}, -} - ---- Creates a new AIBALANCER object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. --- @param #AIBALANCER self --- @param SetClient A SET_CLIENT object that will contain the CLIENT objects to be monitored if they are alive or not (joined by a player). --- @param SpawnAI A SPAWN object that will spawn the AI units required, balancing the SetClient. --- @return #AIBALANCER self -function AIBALANCER:New( SetClient, SpawnAI ) - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) - - self.SetClient = SetClient - if type( SpawnAI ) == "table" then - if SpawnAI.ClassName and SpawnAI.ClassName == "SPAWN" then - self.SpawnAI = { SpawnAI } - else - local SpawnObjects = true - for SpawnObjectID, SpawnObject in pairs( SpawnAI ) do - if SpawnObject.ClassName and SpawnObject.ClassName == "SPAWN" then - self:E( SpawnObject.ClassName ) - else - self:E( "other object" ) - SpawnObjects = false - end - end - if SpawnObjects == true then - self.SpawnAI = SpawnAI - else - error( "No SPAWN object given in parameter SpawnAI, either as a single object or as a table of objects!" ) - end - end - end - - self.ToNearestAirbase = false - self.ReturnHomeAirbase = false - - self.AIMonitorSchedule = SCHEDULER:New( self, self._ClientAliveMonitorScheduler, {}, 1, 10, 0 ) - - return self -end - ---- Returns the AI to the nearest friendly @{Airbase#AIRBASE}. --- @param #AIBALANCER self --- @param DCSTypes#Distance ReturnTresholdRange If there is an enemy @{Client#CLIENT} within the ReturnTresholdRange given in meters, the AI will not return to the nearest @{Airbase#AIRBASE}. --- @param Set#SET_AIRBASE ReturnAirbaseSet The SET of @{Set#SET_AIRBASE}s to evaluate where to return to. -function AIBALANCER:ReturnToNearestAirbases( ReturnTresholdRange, ReturnAirbaseSet ) - - self.ToNearestAirbase = true - self.ReturnTresholdRange = ReturnTresholdRange - self.ReturnAirbaseSet = ReturnAirbaseSet -end - ---- Returns the AI to the home @{Airbase#AIRBASE}. --- @param #AIBALANCER self --- @param DCSTypes#Distance ReturnTresholdRange If there is an enemy @{Client#CLIENT} within the ReturnTresholdRange given in meters, the AI will not return to the nearest @{Airbase#AIRBASE}. -function AIBALANCER:ReturnToHomeAirbase( ReturnTresholdRange ) - - self.ToHomeAirbase = true - self.ReturnTresholdRange = ReturnTresholdRange -end - ---- Let the AI patrol a @{Zone} with a given Speed range and Altitude range. --- @param #AIBALANCER self --- @param PatrolZone#PATROLZONE PatrolZone The @{PatrolZone} where the AI needs to patrol. --- @return PatrolZone#PATROLZONE self -function AIBALANCER:SetPatrolZone( PatrolZone ) - - self.PatrolZone = PatrolZone -end - ---- @param #AIBALANCER self -function AIBALANCER:_ClientAliveMonitorScheduler() - - self.SetClient:ForEachClient( - --- @param Client#CLIENT Client - function( Client ) - local ClientAIAliveState = Client:GetState( self, 'AIAlive' ) - self:T( ClientAIAliveState ) - if Client:IsAlive() then - if ClientAIAliveState == true then - Client:SetState( self, 'AIAlive', false ) - - local AIGroup = self.AIGroups[Client.UnitName] -- Group#GROUP - --- local PatrolZone = Client:GetState( self, "PatrolZone" ) --- if PatrolZone then --- PatrolZone = nil --- Client:ClearState( self, "PatrolZone" ) --- end - - if self.ToNearestAirbase == false and self.ToHomeAirbase == false then - AIGroup:Destroy() - else - -- We test if there is no other CLIENT within the self.ReturnTresholdRange of the first unit of the AI group. - -- If there is a CLIENT, the AI stays engaged and will not return. - -- If there is no CLIENT within the self.ReturnTresholdRange, then the unit will return to the Airbase return method selected. - - local PlayerInRange = { Value = false } - local RangeZone = ZONE_RADIUS:New( 'RangeZone', AIGroup:GetPointVec2(), self.ReturnTresholdRange ) - - self:E( RangeZone ) - - _DATABASE:ForEachPlayer( - --- @param Unit#UNIT RangeTestUnit - function( RangeTestUnit, RangeZone, AIGroup, PlayerInRange ) - self:E( { PlayerInRange, RangeTestUnit.UnitName, RangeZone.ZoneName } ) - if RangeTestUnit:IsInZone( RangeZone ) == true then - self:E( "in zone" ) - if RangeTestUnit:GetCoalition() ~= AIGroup:GetCoalition() then - self:E( "in range" ) - PlayerInRange.Value = true - end - end - end, - - --- @param Zone#ZONE_RADIUS RangeZone - -- @param Group#GROUP AIGroup - function( RangeZone, AIGroup, PlayerInRange ) - local AIGroupTemplate = AIGroup:GetTemplate() - if PlayerInRange.Value == false then - if self.ToHomeAirbase == true then - local WayPointCount = #AIGroupTemplate.route.points - local SwitchWayPointCommand = AIGroup:CommandSwitchWayPoint( 1, WayPointCount, 1 ) - AIGroup:SetCommand( SwitchWayPointCommand ) - AIGroup:MessageToRed( "Returning to home base ...", 30 ) - else - -- Okay, we need to send this Group back to the nearest base of the Coalition of the AI. - --TODO: i need to rework the POINT_VEC2 thing. - local PointVec2 = POINT_VEC2:New( AIGroup:GetPointVec2().x, AIGroup:GetPointVec2().y ) - local ClosestAirbase = self.ReturnAirbaseSet:FindNearestAirbaseFromPointVec2( PointVec2 ) - self:T( ClosestAirbase.AirbaseName ) - AIGroup:MessageToRed( "Returning to " .. ClosestAirbase:GetName().. " ...", 30 ) - local RTBRoute = AIGroup:RouteReturnToAirbase( ClosestAirbase ) - AIGroupTemplate.route = RTBRoute - AIGroup:Respawn( AIGroupTemplate ) - end - end - end - , RangeZone, AIGroup, PlayerInRange - ) - - end - end - else - if not ClientAIAliveState or ClientAIAliveState == false then - Client:SetState( self, 'AIAlive', true ) - - - -- OK, spawn a new group from the SpawnAI objects provided. - local SpawnAICount = #self.SpawnAI - local SpawnAIIndex = math.random( 1, SpawnAICount ) - local AIGroup = self.SpawnAI[SpawnAIIndex]:Spawn() - AIGroup:E( "spawning new AIGroup" ) - --TODO: need to rework UnitName thing ... - self.AIGroups[Client.UnitName] = AIGroup - - --- Now test if the AIGroup needs to patrol a zone, otherwise let it follow its route... - if self.PatrolZone then - self.PatrolZones[#self.PatrolZones+1] = PATROLZONE:New( - self.PatrolZone.PatrolZone, - self.PatrolZone.PatrolFloorAltitude, - self.PatrolZone.PatrolCeilingAltitude, - self.PatrolZone.PatrolMinSpeed, - self.PatrolZone.PatrolMaxSpeed - ) - - if self.PatrolZone.PatrolManageFuel == true then - self.PatrolZones[#self.PatrolZones]:ManageFuel( self.PatrolZone.PatrolFuelTresholdPercentage, self.PatrolZone.PatrolOutOfFuelOrbitTime ) - end - self.PatrolZones[#self.PatrolZones]:SetGroup( AIGroup ) - - --self.PatrolZones[#self.PatrolZones+1] = PatrolZone - - --Client:SetState( self, "PatrolZone", PatrolZone ) - end - end - end - end - ) - return true -end - - - ---- This module contains the AIRBASEPOLICE classes. --- --- === --- --- 1) @{AirbasePolice#AIRBASEPOLICE_BASE} class, extends @{Base#BASE} --- ================================================================== --- The @{AirbasePolice#AIRBASEPOLICE_BASE} class provides the main methods to monitor CLIENT behaviour at airbases. --- CLIENTS should not be allowed to: --- --- * Don't taxi faster than 40 km/h. --- * Don't take-off on taxiways. --- * Avoid to hit other planes on the airbase. --- * Obey ground control orders. --- --- 2) @{AirbasePolice#AIRBASEPOLICE_CAUCASUS} class, extends @{AirbasePolice#AIRBASEPOLICE_BASE} --- ============================================================================================= --- All the airbases on the caucasus map can be monitored using this class. --- If you want to monitor specific airbases, you need to use the @{#AIRBASEPOLICE_BASE.Monitor}() method, which takes a table or airbase names. --- The following names can be given: --- * AnapaVityazevo --- * Batumi --- * Beslan --- * Gelendzhik --- * Gudauta --- * Kobuleti --- * KrasnodarCenter --- * KrasnodarPashkovsky --- * Krymsk --- * Kutaisi --- * MaykopKhanskaya --- * MineralnyeVody --- * Mozdok --- * Nalchik --- * Novorossiysk --- * SenakiKolkhi --- * SochiAdler --- * Soganlug --- * SukhumiBabushara --- * TbilisiLochini --- * Vaziani --- --- @module AirbasePolice --- @author FlightControl - - ---- @type AIRBASEPOLICE_BASE --- @field Set#SET_CLIENT SetClient --- @extends Base#BASE - -AIRBASEPOLICE_BASE = { - ClassName = "AIRBASEPOLICE_BASE", - SetClient = nil, - Airbases = nil, - AirbaseNames = nil, -} - - ---- Creates a new AIRBASEPOLICE_BASE object. --- @param #AIRBASEPOLICE_BASE self --- @param SetClient A SET_CLIENT object that will contain the CLIENT objects to be monitored if they follow the rules of the airbase. --- @param Airbases A table of Airbase Names. --- @return #AIRBASEPOLICE_BASE self -function AIRBASEPOLICE_BASE:New( SetClient, Airbases ) - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) - self:E( { self.ClassName, SetClient, Airbases } ) - - self.SetClient = SetClient - self.Airbases = Airbases - - for AirbaseID, Airbase in pairs( self.Airbases ) do - Airbase.ZoneBoundary = ZONE_POLYGON_BASE:New( "Boundary", Airbase.PointsBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - for PointsRunwayID, PointsRunway in pairs( Airbase.PointsRunways ) do - Airbase.ZoneRunways[PointsRunwayID] = ZONE_POLYGON_BASE:New( "Runway " .. PointsRunwayID, PointsRunway ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - end - end - - -- -- Template - -- local TemplateBoundary = GROUP:FindByName( "Template Boundary" ) - -- self.Airbases.Template.ZoneBoundary = ZONE_POLYGON:New( "Template Boundary", TemplateBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local TemplateRunway1 = GROUP:FindByName( "Template Runway 1" ) - -- self.Airbases.Template.ZoneRunways[1] = ZONE_POLYGON:New( "Template Runway 1", TemplateRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - - self.SetClient:ForEachClient( - --- @param Client#CLIENT Client - function( Client ) - Client:SetState( self, "Speeding", false ) - Client:SetState( self, "Warnings", 0) - Client:SetState( self, "Taxi", false ) - end - ) - - self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, {}, 0, 2, 0.05 ) - - return self -end - ---- @type AIRBASEPOLICE_BASE.AirbaseNames --- @list <#string> - ---- Monitor a table of airbase names. --- @param #AIRBASEPOLICE_BASE self --- @param #AIRBASEPOLICE_BASE.AirbaseNames AirbaseNames A list of AirbaseNames to monitor. If this parameters is nil, then all airbases will be monitored. --- @return #AIRBASEPOLICE_BASE self -function AIRBASEPOLICE_BASE:Monitor( AirbaseNames ) - - if AirbaseNames then - if type( AirbaseNames ) == "table" then - self.AirbaseNames = AirbaseNames - else - self.AirbaseNames = { AirbaseNames } - end - end -end - ---- @param #AIRBASEPOLICE_BASE self -function AIRBASEPOLICE_BASE:_AirbaseMonitor() - - for AirbaseID, Airbase in pairs( self.Airbases ) do - - if not self.AirbaseNames or self.AirbaseNames[AirbaseID] then - - self:E( AirbaseID ) - - self.SetClient:ForEachClientInZone( Airbase.ZoneBoundary, - - --- @param Client#CLIENT Client - function( Client ) - - self:E( Client.UnitName ) - if Client:IsAlive() then - local NotInRunwayZone = true - for ZoneRunwayID, ZoneRunway in pairs( Airbase.ZoneRunways ) do - NotInRunwayZone = ( Client:IsNotInZone( ZoneRunway ) == true ) and NotInRunwayZone or false - end - - if NotInRunwayZone then - local Taxi = self:GetState( self, "Taxi" ) - self:E( Taxi ) - if Taxi == false then - Client:Message( "Welcome at " .. AirbaseID .. ". The maximum taxiing speed is " .. Airbase.MaximumSpeed " km/h.", 20, "ATC" ) - self:SetState( self, "Taxi", true ) - end - - local VelocityVec3 = Client:GetVelocity() - local Velocity = math.abs(VelocityVec3.x) + math.abs(VelocityVec3.y) + math.abs(VelocityVec3.z) - local IsAboveRunway = Client:IsAboveRunway() - local IsOnGround = Client:InAir() == false - self:T( IsAboveRunway, IsOnGround ) - - if IsAboveRunway and IsOnGround then - - if Velocity > Airbase.MaximumSpeed then - local IsSpeeding = Client:GetState( self, "Speeding" ) - - if IsSpeeding == true then - local SpeedingWarnings = Client:GetState( self, "Warnings" ) - self:T( SpeedingWarnings ) - - if SpeedingWarnings <= 5 then - Client:Message( "You are speeding on the taxiway! Slow down or you will be removed from this airbase! Your current velocity is " .. string.format( "%2.0f km/h", Velocity ), 5, "Warning " .. SpeedingWarnings .. " / 5" ) - Client:SetState( self, "Warnings", SpeedingWarnings + 1 ) - else - MESSAGE:New( "Player " .. Client:GetPlayerName() .. " has been removed from the airbase, due to a speeding violation ...", 10, "Airbase Police" ):ToAll() - Client:GetGroup():Destroy() - Client:SetState( self, "Speeding", false ) - Client:SetState( self, "Warnings", 0 ) - end - - else - Client:Message( "You are speeding on the taxiway! Slow down please ...! Your current velocity is " .. string.format( "%2.0f km/h", Velocity ), 5, "Attention! " ) - Client:SetState( self, "Speeding", true ) - Client:SetState( self, "Warnings", 1 ) - end - - else - Client:SetState( self, "Speeding", false ) - Client:SetState( self, "Warnings", 0 ) - end - end - - else - Client:SetState( self, "Speeding", false ) - Client:SetState( self, "Warnings", 0 ) - local Taxi = self:GetState( self, "Taxi" ) - if Taxi == true then - Client:Message( "You have progressed to the runway ... Await take-off clearance ...", 20, "ATC" ) - self:SetState( self, "Taxi", false ) - end - end - end - end - ) - end - end - - return true -end - - ---- @type AIRBASEPOLICE_CAUCASUS --- @field Set#SET_CLIENT SetClient --- @extends #AIRBASEPOLICE_BASE - -AIRBASEPOLICE_CAUCASUS = { - ClassName = "AIRBASEPOLICE_CAUCASUS", - Airbases = { - AnapaVityazevo = { - PointsBoundary = { - [1]={["y"]=242234.85714287,["x"]=-6616.5714285726,}, - [2]={["y"]=241060.57142858,["x"]=-5585.142857144,}, - [3]={["y"]=243806.2857143,["x"]=-3962.2857142868,}, - [4]={["y"]=245240.57142858,["x"]=-4816.5714285726,}, - [5]={["y"]=244783.42857144,["x"]=-5630.8571428583,}, - [6]={["y"]=243800.57142858,["x"]=-5065.142857144,}, - [7]={["y"]=242232.00000001,["x"]=-6622.2857142868,}, - }, - PointsRunways = { - [1] = { - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Batumi = { - PointsBoundary = { - [1]={["y"]=617567.14285714,["x"]=-355313.14285715,}, - [2]={["y"]=616181.42857142,["x"]=-354800.28571429,}, - [3]={["y"]=616007.14285714,["x"]=-355128.85714286,}, - [4]={["y"]=618230,["x"]=-356914.57142858,}, - [5]={["y"]=618727.14285714,["x"]=-356166,}, - [6]={["y"]=617572.85714285,["x"]=-355308.85714286,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=616442.28571429,["x"]=-355090.28571429,}, - [2]={["y"]=618450.57142857,["x"]=-356522,}, - [3]={["y"]=618407.71428571,["x"]=-356584.85714286,}, - [4]={["y"]=618361.99999999,["x"]=-356554.85714286,}, - [5]={["y"]=618324.85714285,["x"]=-356599.14285715,}, - [6]={["y"]=618250.57142856,["x"]=-356543.42857143,}, - [7]={["y"]=618257.7142857,["x"]=-356496.28571429,}, - [8]={["y"]=618237.7142857,["x"]=-356459.14285715,}, - [9]={["y"]=616555.71428571,["x"]=-355258.85714286,}, - [10]={["y"]=616486.28571428,["x"]=-355280.57142858,}, - [11]={["y"]=616410.57142856,["x"]=-355227.71428572,}, - [12]={["y"]=616441.99999999,["x"]=-355179.14285715,}, - [13]={["y"]=616401.99999999,["x"]=-355147.71428572,}, - [14]={["y"]=616441.42857142,["x"]=-355092.57142858,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Beslan = { - PointsBoundary = { - [1]={["y"]=842082.57142857,["x"]=-148445.14285715,}, - [2]={["y"]=845237.71428572,["x"]=-148639.71428572,}, - [3]={["y"]=845232,["x"]=-148765.42857143,}, - [4]={["y"]=844220.57142857,["x"]=-149168.28571429,}, - [5]={["y"]=843274.85714286,["x"]=-149125.42857143,}, - [6]={["y"]=842077.71428572,["x"]=-148554,}, - [7]={["y"]=842083.42857143,["x"]=-148445.42857143,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=842104.57142857,["x"]=-148460.57142857,}, - [2]={["y"]=845225.71428572,["x"]=-148656,}, - [3]={["y"]=845220.57142858,["x"]=-148750,}, - [4]={["y"]=842098.85714286,["x"]=-148556.28571429,}, - [5]={["y"]=842104,["x"]=-148460.28571429,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Gelendzhik = { - PointsBoundary = { - [1]={["y"]=297856.00000001,["x"]=-51151.428571429,}, - [2]={["y"]=299044.57142858,["x"]=-49720.000000001,}, - [3]={["y"]=298861.71428572,["x"]=-49580.000000001,}, - [4]={["y"]=298198.85714286,["x"]=-49842.857142858,}, - [5]={["y"]=297990.28571429,["x"]=-50151.428571429,}, - [6]={["y"]=297696.00000001,["x"]=-51054.285714286,}, - [7]={["y"]=297850.28571429,["x"]=-51160.000000001,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=297834.00000001,["x"]=-51107.428571429,}, - [2]={["y"]=297786.57142858,["x"]=-51068.857142858,}, - [3]={["y"]=298946.57142858,["x"]=-49686.000000001,}, - [4]={["y"]=298993.14285715,["x"]=-49725.714285715,}, - [5]={["y"]=297835.14285715,["x"]=-51107.714285715,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Gudauta = { - PointsBoundary = { - [1]={["y"]=517246.57142857,["x"]=-197850.28571429,}, - [2]={["y"]=516749.42857142,["x"]=-198070.28571429,}, - [3]={["y"]=515755.14285714,["x"]=-197598.85714286,}, - [4]={["y"]=515369.42857142,["x"]=-196538.85714286,}, - [5]={["y"]=515623.71428571,["x"]=-195618.85714286,}, - [6]={["y"]=515946.57142857,["x"]=-195510.28571429,}, - [7]={["y"]=517243.71428571,["x"]=-197858.85714286,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=517096.57142857,["x"]=-197804.57142857,}, - [2]={["y"]=515880.85714285,["x"]=-195590.28571429,}, - [3]={["y"]=515812.28571428,["x"]=-195628.85714286,}, - [4]={["y"]=517036.57142857,["x"]=-197834.57142857,}, - [5]={["y"]=517097.99999999,["x"]=-197807.42857143,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Kobuleti = { - PointsBoundary = { - [1]={["y"]=634427.71428571,["x"]=-318290.28571429,}, - [2]={["y"]=635033.42857143,["x"]=-317550.2857143,}, - [3]={["y"]=635864.85714286,["x"]=-317333.14285715,}, - [4]={["y"]=636967.71428571,["x"]=-317261.71428572,}, - [5]={["y"]=637144.85714286,["x"]=-317913.14285715,}, - [6]={["y"]=634630.57142857,["x"]=-318687.42857144,}, - [7]={["y"]=634424.85714286,["x"]=-318290.2857143,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=634509.71428571,["x"]=-318339.42857144,}, - [2]={["y"]=636767.42857143,["x"]=-317516.57142858,}, - [3]={["y"]=636790,["x"]=-317575.71428572,}, - [4]={["y"]=634531.42857143,["x"]=-318398.00000001,}, - [5]={["y"]=634510.28571429,["x"]=-318339.71428572,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - KrasnodarCenter = { - PointsBoundary = { - [1]={["y"]=366680.28571429,["x"]=11699.142857142,}, - [2]={["y"]=366654.28571429,["x"]=11225.142857142,}, - [3]={["y"]=367497.14285715,["x"]=11082.285714285,}, - [4]={["y"]=368025.71428572,["x"]=10396.57142857,}, - [5]={["y"]=369854.28571429,["x"]=11367.999999999,}, - [6]={["y"]=369840.00000001,["x"]=11910.857142856,}, - [7]={["y"]=366682.57142858,["x"]=11697.999999999,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=369205.42857144,["x"]=11789.142857142,}, - [2]={["y"]=369209.71428572,["x"]=11714.857142856,}, - [3]={["y"]=366699.71428572,["x"]=11581.714285713,}, - [4]={["y"]=366698.28571429,["x"]=11659.142857142,}, - [5]={["y"]=369208.85714286,["x"]=11788.57142857,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - KrasnodarPashkovsky = { - PointsBoundary = { - [1]={["y"]=386754,["x"]=6476.5714285703,}, - [2]={["y"]=389182.57142858,["x"]=8722.2857142846,}, - [3]={["y"]=388832.57142858,["x"]=9086.5714285703,}, - [4]={["y"]=386961.14285715,["x"]=7707.9999999989,}, - [5]={["y"]=385404,["x"]=9179.4285714274,}, - [6]={["y"]=383239.71428572,["x"]=7386.5714285703,}, - [7]={["y"]=383954,["x"]=6486.5714285703,}, - [8]={["y"]=385775.42857143,["x"]=8097.9999999989,}, - [9]={["y"]=386804,["x"]=7319.4285714274,}, - [10]={["y"]=386375.42857143,["x"]=6797.9999999989,}, - [11]={["y"]=386746.85714286,["x"]=6472.2857142846,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=385891.14285715,["x"]=8416.5714285703,}, - [2]={["y"]=385842.28571429,["x"]=8467.9999999989,}, - [3]={["y"]=384180.85714286,["x"]=6917.1428571417,}, - [4]={["y"]=384228.57142858,["x"]=6867.7142857132,}, - [5]={["y"]=385891.14285715,["x"]=8416.5714285703,}, - }, - [2] = { - [1]={["y"]=386714.85714286,["x"]=6674.857142856,}, - [2]={["y"]=386757.71428572,["x"]=6627.7142857132,}, - [3]={["y"]=389028.57142858,["x"]=8741.4285714275,}, - [4]={["y"]=388981.71428572,["x"]=8790.5714285703,}, - [5]={["y"]=386714.57142858,["x"]=6674.5714285703,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Krymsk = { - PointsBoundary = { - [1]={["y"]=293338.00000001,["x"]=-7575.4285714297,}, - [2]={["y"]=295199.42857144,["x"]=-5434.0000000011,}, - [3]={["y"]=295595.14285715,["x"]=-6239.7142857154,}, - [4]={["y"]=294152.2857143,["x"]=-8325.4285714297,}, - [5]={["y"]=293345.14285715,["x"]=-7596.8571428582,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=293522.00000001,["x"]=-7567.4285714297,}, - [2]={["y"]=293578.57142858,["x"]=-7616.0000000011,}, - [3]={["y"]=295246.00000001,["x"]=-5591.142857144,}, - [4]={["y"]=295187.71428573,["x"]=-5546.0000000011,}, - [5]={["y"]=293523.14285715,["x"]=-7568.2857142868,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Kutaisi = { - PointsBoundary = { - [1]={["y"]=682087.42857143,["x"]=-284512.85714286,}, - [2]={["y"]=685387.42857143,["x"]=-283662.85714286,}, - [3]={["y"]=685294.57142857,["x"]=-284977.14285715,}, - [4]={["y"]=682744.57142857,["x"]=-286505.71428572,}, - [5]={["y"]=682094.57142857,["x"]=-284527.14285715,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=682638,["x"]=-285202.28571429,}, - [2]={["y"]=685050.28571429,["x"]=-284507.42857144,}, - [3]={["y"]=685068.85714286,["x"]=-284578.85714286,}, - [4]={["y"]=682657.42857143,["x"]=-285264.28571429,}, - [5]={["y"]=682638.28571429,["x"]=-285202.85714286,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - MaykopKhanskaya = { - PointsBoundary = { - [1]={["y"]=456876.28571429,["x"]=-27665.42857143,}, - [2]={["y"]=457800,["x"]=-28392.857142858,}, - [3]={["y"]=459368.57142857,["x"]=-26378.571428573,}, - [4]={["y"]=459425.71428572,["x"]=-25242.857142858,}, - [5]={["y"]=458961.42857143,["x"]=-24964.285714287,}, - [6]={["y"]=456878.57142857,["x"]=-27667.714285715,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=457005.42857143,["x"]=-27668.000000001,}, - [2]={["y"]=459028.85714286,["x"]=-25168.857142858,}, - [3]={["y"]=459082.57142857,["x"]=-25216.857142858,}, - [4]={["y"]=457060,["x"]=-27714.285714287,}, - [5]={["y"]=457004.57142857,["x"]=-27669.714285715,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - MineralnyeVody = { - PointsBoundary = { - [1]={["y"]=703857.14285714,["x"]=-50226.000000002,}, - [2]={["y"]=707385.71428571,["x"]=-51911.714285716,}, - [3]={["y"]=707595.71428571,["x"]=-51434.857142859,}, - [4]={["y"]=707900,["x"]=-51568.857142859,}, - [5]={["y"]=707542.85714286,["x"]=-52326.000000002,}, - [6]={["y"]=706628.57142857,["x"]=-52568.857142859,}, - [7]={["y"]=705142.85714286,["x"]=-51790.285714288,}, - [8]={["y"]=703678.57142857,["x"]=-50611.714285716,}, - [9]={["y"]=703857.42857143,["x"]=-50226.857142859,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=703904,["x"]=-50352.571428573,}, - [2]={["y"]=707596.28571429,["x"]=-52094.571428573,}, - [3]={["y"]=707560.57142858,["x"]=-52161.714285716,}, - [4]={["y"]=703871.71428572,["x"]=-50420.571428573,}, - [5]={["y"]=703902,["x"]=-50352.000000002,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Mozdok = { - PointsBoundary = { - [1]={["y"]=832123.42857143,["x"]=-83608.571428573,}, - [2]={["y"]=835916.28571429,["x"]=-83144.285714288,}, - [3]={["y"]=835474.28571429,["x"]=-84170.571428573,}, - [4]={["y"]=832911.42857143,["x"]=-84470.571428573,}, - [5]={["y"]=832487.71428572,["x"]=-85565.714285716,}, - [6]={["y"]=831573.42857143,["x"]=-85351.42857143,}, - [7]={["y"]=832123.71428572,["x"]=-83610.285714288,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=832201.14285715,["x"]=-83699.428571431,}, - [2]={["y"]=832212.57142857,["x"]=-83780.571428574,}, - [3]={["y"]=835730.28571429,["x"]=-83335.714285717,}, - [4]={["y"]=835718.85714286,["x"]=-83246.571428574,}, - [5]={["y"]=832200.57142857,["x"]=-83700.000000002,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Nalchik = { - PointsBoundary = { - [1]={["y"]=759370,["x"]=-125502.85714286,}, - [2]={["y"]=761384.28571429,["x"]=-124177.14285714,}, - [3]={["y"]=761472.85714286,["x"]=-124325.71428572,}, - [4]={["y"]=761092.85714286,["x"]=-125048.57142857,}, - [5]={["y"]=760295.71428572,["x"]=-125685.71428572,}, - [6]={["y"]=759444.28571429,["x"]=-125734.28571429,}, - [7]={["y"]=759375.71428572,["x"]=-125511.42857143,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=759454.28571429,["x"]=-125551.42857143,}, - [2]={["y"]=759492.85714286,["x"]=-125610.85714286,}, - [3]={["y"]=761406.28571429,["x"]=-124304.28571429,}, - [4]={["y"]=761361.14285714,["x"]=-124239.71428572,}, - [5]={["y"]=759456,["x"]=-125552.57142857,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Novorossiysk = { - PointsBoundary = { - [1]={["y"]=278677.71428573,["x"]=-41656.571428572,}, - [2]={["y"]=278446.2857143,["x"]=-41453.714285715,}, - [3]={["y"]=278989.14285716,["x"]=-40188.000000001,}, - [4]={["y"]=279717.71428573,["x"]=-39968.000000001,}, - [5]={["y"]=280020.57142859,["x"]=-40208.000000001,}, - [6]={["y"]=278674.85714287,["x"]=-41660.857142858,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=278673.14285716,["x"]=-41615.142857144,}, - [2]={["y"]=278625.42857144,["x"]=-41570.571428572,}, - [3]={["y"]=279835.42857144,["x"]=-40226.000000001,}, - [4]={["y"]=279882.2857143,["x"]=-40270.000000001,}, - [5]={["y"]=278672.00000001,["x"]=-41614.857142858,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - SenakiKolkhi = { - PointsBoundary = { - [1]={["y"]=646036.57142857,["x"]=-281778.85714286,}, - [2]={["y"]=646045.14285714,["x"]=-281191.71428571,}, - [3]={["y"]=647032.28571429,["x"]=-280598.85714285,}, - [4]={["y"]=647669.42857143,["x"]=-281273.14285714,}, - [5]={["y"]=648323.71428571,["x"]=-281370.28571428,}, - [6]={["y"]=648520.85714286,["x"]=-281978.85714285,}, - [7]={["y"]=646039.42857143,["x"]=-281783.14285714,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=646060.85714285,["x"]=-281736,}, - [2]={["y"]=646056.57142857,["x"]=-281631.71428571,}, - [3]={["y"]=648442.28571428,["x"]=-281840.28571428,}, - [4]={["y"]=648432.28571428,["x"]=-281918.85714286,}, - [5]={["y"]=646063.71428571,["x"]=-281738.85714286,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - SochiAdler = { - PointsBoundary = { - [1]={["y"]=460642.28571428,["x"]=-164861.71428571,}, - [2]={["y"]=462820.85714285,["x"]=-163368.85714286,}, - [3]={["y"]=463649.42857142,["x"]=-163340.28571429,}, - [4]={["y"]=463835.14285714,["x"]=-164040.28571429,}, - [5]={["y"]=462535.14285714,["x"]=-165654.57142857,}, - [6]={["y"]=460678,["x"]=-165247.42857143,}, - [7]={["y"]=460635.14285714,["x"]=-164876,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=460831.42857143,["x"]=-165180,}, - [2]={["y"]=460878.57142857,["x"]=-165257.14285714,}, - [3]={["y"]=463663.71428571,["x"]=-163793.14285714,}, - [4]={["y"]=463612.28571428,["x"]=-163697.42857143,}, - [5]={["y"]=460831.42857143,["x"]=-165177.14285714,}, - }, - [2] = { - [1]={["y"]=460831.42857143,["x"]=-165180,}, - [2]={["y"]=460878.57142857,["x"]=-165257.14285714,}, - [3]={["y"]=463663.71428571,["x"]=-163793.14285714,}, - [4]={["y"]=463612.28571428,["x"]=-163697.42857143,}, - [5]={["y"]=460831.42857143,["x"]=-165177.14285714,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Soganlug = { - PointsBoundary = { - [1]={["y"]=894530.85714286,["x"]=-316928.28571428,}, - [2]={["y"]=896422.28571428,["x"]=-318622.57142857,}, - [3]={["y"]=896090.85714286,["x"]=-318934,}, - [4]={["y"]=894019.42857143,["x"]=-317119.71428571,}, - [5]={["y"]=894533.71428571,["x"]=-316925.42857143,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=894525.71428571,["x"]=-316964,}, - [2]={["y"]=896363.14285714,["x"]=-318634.28571428,}, - [3]={["y"]=896299.14285714,["x"]=-318702.85714286,}, - [4]={["y"]=894464,["x"]=-317031.71428571,}, - [5]={["y"]=894524.57142857,["x"]=-316963.71428571,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - SukhumiBabushara = { - PointsBoundary = { - [1]={["y"]=562541.14285714,["x"]=-219852.28571429,}, - [2]={["y"]=562691.14285714,["x"]=-219395.14285714,}, - [3]={["y"]=564326.85714286,["x"]=-219523.71428571,}, - [4]={["y"]=566262.57142857,["x"]=-221166.57142857,}, - [5]={["y"]=566069.71428571,["x"]=-221580.85714286,}, - [6]={["y"]=562534,["x"]=-219873.71428571,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=562684,["x"]=-219779.71428571,}, - [2]={["y"]=562717.71428571,["x"]=-219718,}, - [3]={["y"]=566046.85714286,["x"]=-221376.57142857,}, - [4]={["y"]=566012.28571428,["x"]=-221446.57142857,}, - [5]={["y"]=562684.57142857,["x"]=-219782.57142857,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - TbilisiLochini = { - PointsBoundary = { - [1]={["y"]=895172.85714286,["x"]=-314667.42857143,}, - [2]={["y"]=895337.42857143,["x"]=-314143.14285714,}, - [3]={["y"]=895990.28571429,["x"]=-314036,}, - [4]={["y"]=897730.28571429,["x"]=-315284.57142857,}, - [5]={["y"]=897901.71428571,["x"]=-316284.57142857,}, - [6]={["y"]=897684.57142857,["x"]=-316618.85714286,}, - [7]={["y"]=895173.14285714,["x"]=-314667.42857143,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=895261.14285715,["x"]=-314652.28571428,}, - [2]={["y"]=897654.57142857,["x"]=-316523.14285714,}, - [3]={["y"]=897711.71428571,["x"]=-316450.28571429,}, - [4]={["y"]=895327.42857143,["x"]=-314568.85714286,}, - [5]={["y"]=895261.71428572,["x"]=-314656,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Vaziani = { - PointsBoundary = { - [1]={["y"]=902122,["x"]=-318163.71428572,}, - [2]={["y"]=902678.57142857,["x"]=-317594,}, - [3]={["y"]=903275.71428571,["x"]=-317405.42857143,}, - [4]={["y"]=903418.57142857,["x"]=-317891.14285714,}, - [5]={["y"]=904292.85714286,["x"]=-318748.28571429,}, - [6]={["y"]=904542,["x"]=-319740.85714286,}, - [7]={["y"]=904042,["x"]=-320166.57142857,}, - [8]={["y"]=902121.42857143,["x"]=-318164.85714286,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=902239.14285714,["x"]=-318190.85714286,}, - [2]={["y"]=904014.28571428,["x"]=-319994.57142857,}, - [3]={["y"]=904064.85714285,["x"]=-319945.14285715,}, - [4]={["y"]=902294.57142857,["x"]=-318146,}, - [5]={["y"]=902247.71428571,["x"]=-318190.85714286,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - }, -} - ---- Creates a new AIRBASEPOLICE_CAUCASUS object. --- @param #AIRBASEPOLICE_CAUCASUS self --- @param SetClient A SET_CLIENT object that will contain the CLIENT objects to be monitored if they follow the rules of the airbase. --- @return #AIRBASEPOLICE_CAUCASUS self -function AIRBASEPOLICE_CAUCASUS:New( SetClient ) - - -- Inherits from BASE - local self = BASE:Inherit( self, AIRBASEPOLICE_BASE:New( SetClient, self.Airbases ) ) - - -- -- AnapaVityazevo - -- local AnapaVityazevoBoundary = GROUP:FindByName( "AnapaVityazevo Boundary" ) - -- self.Airbases.AnapaVityazevo.ZoneBoundary = ZONE_POLYGON:New( "AnapaVityazevo Boundary", AnapaVityazevoBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local AnapaVityazevoRunway1 = GROUP:FindByName( "AnapaVityazevo Runway 1" ) - -- self.Airbases.AnapaVityazevo.ZoneRunways[1] = ZONE_POLYGON:New( "AnapaVityazevo Runway 1", AnapaVityazevoRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- Batumi - -- local BatumiBoundary = GROUP:FindByName( "Batumi Boundary" ) - -- self.Airbases.Batumi.ZoneBoundary = ZONE_POLYGON:New( "Batumi Boundary", BatumiBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local BatumiRunway1 = GROUP:FindByName( "Batumi Runway 1" ) - -- self.Airbases.Batumi.ZoneRunways[1] = ZONE_POLYGON:New( "Batumi Runway 1", BatumiRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- Beslan - -- local BeslanBoundary = GROUP:FindByName( "Beslan Boundary" ) - -- self.Airbases.Beslan.ZoneBoundary = ZONE_POLYGON:New( "Beslan Boundary", BeslanBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local BeslanRunway1 = GROUP:FindByName( "Beslan Runway 1" ) - -- self.Airbases.Beslan.ZoneRunways[1] = ZONE_POLYGON:New( "Beslan Runway 1", BeslanRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- Gelendzhik - -- local GelendzhikBoundary = GROUP:FindByName( "Gelendzhik Boundary" ) - -- self.Airbases.Gelendzhik.ZoneBoundary = ZONE_POLYGON:New( "Gelendzhik Boundary", GelendzhikBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local GelendzhikRunway1 = GROUP:FindByName( "Gelendzhik Runway 1" ) - -- self.Airbases.Gelendzhik.ZoneRunways[1] = ZONE_POLYGON:New( "Gelendzhik Runway 1", GelendzhikRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- Gudauta - -- local GudautaBoundary = GROUP:FindByName( "Gudauta Boundary" ) - -- self.Airbases.Gudauta.ZoneBoundary = ZONE_POLYGON:New( "Gudauta Boundary", GudautaBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local GudautaRunway1 = GROUP:FindByName( "Gudauta Runway 1" ) - -- self.Airbases.Gudauta.ZoneRunways[1] = ZONE_POLYGON:New( "Gudauta Runway 1", GudautaRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- Kobuleti - -- local KobuletiBoundary = GROUP:FindByName( "Kobuleti Boundary" ) - -- self.Airbases.Kobuleti.ZoneBoundary = ZONE_POLYGON:New( "Kobuleti Boundary", KobuletiBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local KobuletiRunway1 = GROUP:FindByName( "Kobuleti Runway 1" ) - -- self.Airbases.Kobuleti.ZoneRunways[1] = ZONE_POLYGON:New( "Kobuleti Runway 1", KobuletiRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- KrasnodarCenter - -- local KrasnodarCenterBoundary = GROUP:FindByName( "KrasnodarCenter Boundary" ) - -- self.Airbases.KrasnodarCenter.ZoneBoundary = ZONE_POLYGON:New( "KrasnodarCenter Boundary", KrasnodarCenterBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local KrasnodarCenterRunway1 = GROUP:FindByName( "KrasnodarCenter Runway 1" ) - -- self.Airbases.KrasnodarCenter.ZoneRunways[1] = ZONE_POLYGON:New( "KrasnodarCenter Runway 1", KrasnodarCenterRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- KrasnodarPashkovsky - -- local KrasnodarPashkovskyBoundary = GROUP:FindByName( "KrasnodarPashkovsky Boundary" ) - -- self.Airbases.KrasnodarPashkovsky.ZoneBoundary = ZONE_POLYGON:New( "KrasnodarPashkovsky Boundary", KrasnodarPashkovskyBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local KrasnodarPashkovskyRunway1 = GROUP:FindByName( "KrasnodarPashkovsky Runway 1" ) - -- self.Airbases.KrasnodarPashkovsky.ZoneRunways[1] = ZONE_POLYGON:New( "KrasnodarPashkovsky Runway 1", KrasnodarPashkovskyRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- local KrasnodarPashkovskyRunway2 = GROUP:FindByName( "KrasnodarPashkovsky Runway 2" ) - -- self.Airbases.KrasnodarPashkovsky.ZoneRunways[2] = ZONE_POLYGON:New( "KrasnodarPashkovsky Runway 2", KrasnodarPashkovskyRunway2 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- Krymsk - -- local KrymskBoundary = GROUP:FindByName( "Krymsk Boundary" ) - -- self.Airbases.Krymsk.ZoneBoundary = ZONE_POLYGON:New( "Krymsk Boundary", KrymskBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local KrymskRunway1 = GROUP:FindByName( "Krymsk Runway 1" ) - -- self.Airbases.Krymsk.ZoneRunways[1] = ZONE_POLYGON:New( "Krymsk Runway 1", KrymskRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- Kutaisi - -- local KutaisiBoundary = GROUP:FindByName( "Kutaisi Boundary" ) - -- self.Airbases.Kutaisi.ZoneBoundary = ZONE_POLYGON:New( "Kutaisi Boundary", KutaisiBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local KutaisiRunway1 = GROUP:FindByName( "Kutaisi Runway 1" ) - -- self.Airbases.Kutaisi.ZoneRunways[1] = ZONE_POLYGON:New( "Kutaisi Runway 1", KutaisiRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- MaykopKhanskaya - -- local MaykopKhanskayaBoundary = GROUP:FindByName( "MaykopKhanskaya Boundary" ) - -- self.Airbases.MaykopKhanskaya.ZoneBoundary = ZONE_POLYGON:New( "MaykopKhanskaya Boundary", MaykopKhanskayaBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local MaykopKhanskayaRunway1 = GROUP:FindByName( "MaykopKhanskaya Runway 1" ) - -- self.Airbases.MaykopKhanskaya.ZoneRunways[1] = ZONE_POLYGON:New( "MaykopKhanskaya Runway 1", MaykopKhanskayaRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- MineralnyeVody - -- local MineralnyeVodyBoundary = GROUP:FindByName( "MineralnyeVody Boundary" ) - -- self.Airbases.MineralnyeVody.ZoneBoundary = ZONE_POLYGON:New( "MineralnyeVody Boundary", MineralnyeVodyBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local MineralnyeVodyRunway1 = GROUP:FindByName( "MineralnyeVody Runway 1" ) - -- self.Airbases.MineralnyeVody.ZoneRunways[1] = ZONE_POLYGON:New( "MineralnyeVody Runway 1", MineralnyeVodyRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- Mozdok - -- local MozdokBoundary = GROUP:FindByName( "Mozdok Boundary" ) - -- self.Airbases.Mozdok.ZoneBoundary = ZONE_POLYGON:New( "Mozdok Boundary", MozdokBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local MozdokRunway1 = GROUP:FindByName( "Mozdok Runway 1" ) - -- self.Airbases.Mozdok.ZoneRunways[1] = ZONE_POLYGON:New( "Mozdok Runway 1", MozdokRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- Nalchik - -- local NalchikBoundary = GROUP:FindByName( "Nalchik Boundary" ) - -- self.Airbases.Nalchik.ZoneBoundary = ZONE_POLYGON:New( "Nalchik Boundary", NalchikBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local NalchikRunway1 = GROUP:FindByName( "Nalchik Runway 1" ) - -- self.Airbases.Nalchik.ZoneRunways[1] = ZONE_POLYGON:New( "Nalchik Runway 1", NalchikRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- Novorossiysk - -- local NovorossiyskBoundary = GROUP:FindByName( "Novorossiysk Boundary" ) - -- self.Airbases.Novorossiysk.ZoneBoundary = ZONE_POLYGON:New( "Novorossiysk Boundary", NovorossiyskBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local NovorossiyskRunway1 = GROUP:FindByName( "Novorossiysk Runway 1" ) - -- self.Airbases.Novorossiysk.ZoneRunways[1] = ZONE_POLYGON:New( "Novorossiysk Runway 1", NovorossiyskRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- SenakiKolkhi - -- local SenakiKolkhiBoundary = GROUP:FindByName( "SenakiKolkhi Boundary" ) - -- self.Airbases.SenakiKolkhi.ZoneBoundary = ZONE_POLYGON:New( "SenakiKolkhi Boundary", SenakiKolkhiBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local SenakiKolkhiRunway1 = GROUP:FindByName( "SenakiKolkhi Runway 1" ) - -- self.Airbases.SenakiKolkhi.ZoneRunways[1] = ZONE_POLYGON:New( "SenakiKolkhi Runway 1", SenakiKolkhiRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- SochiAdler - -- local SochiAdlerBoundary = GROUP:FindByName( "SochiAdler Boundary" ) - -- self.Airbases.SochiAdler.ZoneBoundary = ZONE_POLYGON:New( "SochiAdler Boundary", SochiAdlerBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local SochiAdlerRunway1 = GROUP:FindByName( "SochiAdler Runway 1" ) - -- self.Airbases.SochiAdler.ZoneRunways[1] = ZONE_POLYGON:New( "SochiAdler Runway 1", SochiAdlerRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- local SochiAdlerRunway2 = GROUP:FindByName( "SochiAdler Runway 2" ) - -- self.Airbases.SochiAdler.ZoneRunways[2] = ZONE_POLYGON:New( "SochiAdler Runway 2", SochiAdlerRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- Soganlug - -- local SoganlugBoundary = GROUP:FindByName( "Soganlug Boundary" ) - -- self.Airbases.Soganlug.ZoneBoundary = ZONE_POLYGON:New( "Soganlug Boundary", SoganlugBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local SoganlugRunway1 = GROUP:FindByName( "Soganlug Runway 1" ) - -- self.Airbases.Soganlug.ZoneRunways[1] = ZONE_POLYGON:New( "Soganlug Runway 1", SoganlugRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- SukhumiBabushara - -- local SukhumiBabusharaBoundary = GROUP:FindByName( "SukhumiBabushara Boundary" ) - -- self.Airbases.SukhumiBabushara.ZoneBoundary = ZONE_POLYGON:New( "SukhumiBabushara Boundary", SukhumiBabusharaBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local SukhumiBabusharaRunway1 = GROUP:FindByName( "SukhumiBabushara Runway 1" ) - -- self.Airbases.SukhumiBabushara.ZoneRunways[1] = ZONE_POLYGON:New( "SukhumiBabushara Runway 1", SukhumiBabusharaRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- TbilisiLochini - -- local TbilisiLochiniBoundary = GROUP:FindByName( "TbilisiLochini Boundary" ) - -- self.Airbases.TbilisiLochini.ZoneBoundary = ZONE_POLYGON:New( "TbilisiLochini Boundary", TbilisiLochiniBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local TbilisiLochiniRunway1 = GROUP:FindByName( "TbilisiLochini Runway 1" ) - -- self.Airbases.TbilisiLochini.ZoneRunways[1] = ZONE_POLYGON:New( "TbilisiLochini Runway 1", TbilisiLochiniRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - -- -- Vaziani - -- local VazianiBoundary = GROUP:FindByName( "Vaziani Boundary" ) - -- self.Airbases.Vaziani.ZoneBoundary = ZONE_POLYGON:New( "Vaziani Boundary", VazianiBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local VazianiRunway1 = GROUP:FindByName( "Vaziani Runway 1" ) - -- self.Airbases.Vaziani.ZoneRunways[1] = ZONE_POLYGON:New( "Vaziani Runway 1", VazianiRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - -- - -- - -- - - - -- -- Template - -- local TemplateBoundary = GROUP:FindByName( "Template Boundary" ) - -- self.Airbases.Template.ZoneBoundary = ZONE_POLYGON:New( "Template Boundary", TemplateBoundary ):SmokeZone(POINT_VEC3.SmokeColor.White):Flush() - -- - -- local TemplateRunway1 = GROUP:FindByName( "Template Runway 1" ) - -- self.Airbases.Template.ZoneRunways[1] = ZONE_POLYGON:New( "Template Runway 1", TemplateRunway1 ):SmokeZone(POINT_VEC3.SmokeColor.Red):Flush() - - return self - -end - ---- This module contains the DETECTION classes. --- --- === --- --- 1) @{Detection#DETECTION_BASE} class, extends @{Base#BASE} --- ========================================================== --- The @{Detection#DETECTION_BASE} class defines the core functions to administer detected objects. --- Detected objects are grouped in SETS of UNITS. --- --- 1.1) DETECTION constructor: --- ---------------------------- --- * @{Detection#DETECTION.New}(): Create a new DETECTION object. --- --- 1.2) DETECTION initialization: --- ------------------------------ --- By default, detection will return detected units with all the methods available. --- However, you can specify which units it found with specific detection methods. --- If you use one of the below functions, the detection will work with the detection method specified. --- You can specify to apply multiple detection methods. --- Use the following functions to report the units it detected using the methods Visual, Optical, Radar, IRST, RWR, DLINK: --- --- * @{Detection#DETECTION.InitDetectVisual}(): Detected using Visual. --- * @{Detection#DETECTION.InitDetectOptical}(): Detected using Optical. --- * @{Detection#DETECTION.InitDetectRadar}(): Detected using Radar. --- * @{Detection#DETECTION.InitDetectIRST}(): Detected using IRST. --- * @{Detection#DETECTION.InitDetectRWR}(): Detected using RWR. --- * @{Detection#DETECTION.InitDetectDLINK}(): Detected using DLINK. --- --- === --- --- @module Detection --- @author Mechanic : Concept & Testing --- @author FlightControl : Design & Programming - - - ---- DETECTION_BASE class --- @type DETECTION_BASE --- @field Group#GROUP FACGroup The GROUP in the Forward Air Controller role. --- @field DCSTypes#Distance DetectionRange The range till which targets are accepted to be detected. --- @field DCSTypes#Distance DetectionZoneRange The range till which targets are grouped upon the first detected target. --- @field #DETECTION_BASE.DetectedUnitSets DetectedUnitSets A list of @{Set#SET_UNIT}s containing the units in each set that were detected within a DetectedZoneRange. --- @field #DETECTION_BASE.DetectedUnitZones DetectedUnitZones A list of @{Zone#ZONE_UNIT}s containing the zones of the reference detected units. --- @extends Set#SET_BASE -DETECTION_BASE = { - ClassName = "DETECTION_BASE", - DetectedUnitSets = {}, - DetectedUnitZones = {}, - DetectedUnits = {}, - FACGroup = nil, - DetectionRange = nil, - DetectionZoneRange = nil, -} - ---- @type DETECTION_BASE.DetectedUnitSets --- @list - - ---- @type DETECTION_BASE.DetectedUnitZones --- @list - - ---- DETECTION constructor. --- @param #DETECTION_BASE self --- @return #DETECTION_BASE self -function DETECTION_BASE:New( FACGroup, DetectionRange, DetectionZoneRange ) - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) - - self.FACGroup = FACGroup - self.DetectionRange = DetectionRange - self.DetectionZoneRange = DetectionZoneRange - - self:InitDetectVisual( false ) - self:InitDetectOptical( false ) - self:InitDetectRadar( false ) - self:InitDetectRWR( false ) - self:InitDetectIRST( false ) - self:InitDetectDLINK( false ) - - self.DetectionScheduler = SCHEDULER:New(self, self._DetectionScheduler, { self, "Detection" }, 10, 30, 0.2 ) - - return self -end - ---- Detect Visual. --- @param #DETECTION_BASE self --- @param #boolean DetectVisual --- @return #DETECTION_BASE self -function DETECTION_BASE:InitDetectVisual( DetectVisual ) - - self.DetectVisual = DetectVisual -end - ---- Detect Optical. --- @param #DETECTION_BASE self --- @param #boolean DetectOptical --- @return #DETECTION_BASE self -function DETECTION_BASE:InitDetectOptical( DetectOptical ) - self:F2() - - self.DetectOptical = DetectOptical -end - ---- Detect Radar. --- @param #DETECTION_BASE self --- @param #boolean DetectRadar --- @return #DETECTION_BASE self -function DETECTION_BASE:InitDetectRadar( DetectRadar ) - self:F2() - - self.DetectRadar = DetectRadar -end - ---- Detect IRST. --- @param #DETECTION_BASE self --- @param #boolean DetectIRST --- @return #DETECTION_BASE self -function DETECTION_BASE:InitDetectIRST( DetectIRST ) - self:F2() - - self.DetectIRST = DetectIRST -end - ---- Detect RWR. --- @param #DETECTION_BASE self --- @param #boolean DetectRWR --- @return #DETECTION_BASE self -function DETECTION_BASE:InitDetectRWR( DetectRWR ) - self:F2() - - self.DetectRWR = DetectRWR -end - ---- Detect DLINK. --- @param #DETECTION_BASE self --- @param #boolean DetectDLINK --- @return #DETECTION_BASE self -function DETECTION_BASE:InitDetectDLINK( DetectDLINK ) - self:F2() - - self.DetectDLINK = DetectDLINK -end - ---- Gets the FAC group. --- @param #DETECTION_BASE self --- @return Group#GROUP self -function DETECTION_BASE:GetFACGroup() - self:F2() - - return self.FACGroup -end - ---- Get the detected @{Set#SET_UNIT}s. --- @param #DETECTION_BASE self --- @return #DETECTION_BASE.DetectedUnitSets DetectedUnitSets -function DETECTION_BASE:GetDetectionUnitSets() - - local DetectionUnitSets = self.DetectedUnitSets - return DetectionUnitSets -end - ---- Get the amount of SETs with detected units. --- @param #DETECTION_BASE self --- @return #number Count -function DETECTION_BASE:GetDetectionUnitSetCount() - - local DetectionUnitSetCount = #self.DetectedUnitSets - return DetectionUnitSetCount -end - ---- Get a SET of detected units using a given numeric index. --- @param #DETECTION_BASE self --- @param #number Index --- @return Set#SET_UNIT -function DETECTION_BASE:GetDetectionUnitSet( Index ) - - local DetectionUnitSet = self.DetectedUnitSets[Index] - if DetectionUnitSet then - return DetectionUnitSet - end - - return nil -end - ---- Form @{Set}s of detected @{Unit#UNIT}s in an array of @{Set#SET_UNIT}s. --- @param #DETECTION_BASE self -function DETECTION_BASE:_DetectionScheduler( SchedulerName ) - self:F2( { SchedulerName } ) - - self.DetectedUnitSets = {} - self.DetectedUnitZones = {} - - if self.FACGroup:IsAlive() then - local FACGroupName = self.FACGroup:GetName() - - local FACDetectedTargets = self.FACGroup:GetDetectedTargets( - self.DetectVisual, - self.DetectOptical, - self.DetectRadar, - self.DetectIRST, - self.DetectRWR, - self.DetectDLINK - ) - - for FACDetectedTargetID, FACDetectedTarget in pairs( FACDetectedTargets ) do - local FACObject = FACDetectedTarget.object - self:T2( FACObject ) - - if FACObject and FACObject:isExist() and FACObject.id_ < 50000000 then - - local FACDetectedUnit = UNIT:Find( FACObject ) - local FACDetectedUnitName = FACDetectedUnit:GetName() - - local FACDetectedUnitPositionVec3 = FACDetectedUnit:GetPointVec3() - local FACGroupPositionVec3 = self.FACGroup:GetPointVec3() - local Distance = ( ( FACDetectedUnitPositionVec3.x - FACGroupPositionVec3.x )^2 + - ( FACDetectedUnitPositionVec3.y - FACGroupPositionVec3.y )^2 + - ( FACDetectedUnitPositionVec3.z - FACGroupPositionVec3.z )^2 - ) ^ 0.5 / 1000 - - self:T( { FACGroupName, FACDetectedUnitName, Distance } ) - - if Distance <= self.DetectionRange then - - if not self.DetectedUnits[FACDetectedUnitName] then - self.DetectedUnits[FACDetectedUnitName] = {} - end - self.DetectedUnits[FACDetectedUnitName].DetectedUnit = UNIT:FindByName( FACDetectedUnitName ) - self.DetectedUnits[FACDetectedUnitName].Visible = FACDetectedTarget.visible - self.DetectedUnits[FACDetectedUnitName].Type = FACDetectedTarget.type - self.DetectedUnits[FACDetectedUnitName].Distance = FACDetectedTarget.distance - else - -- if beyond the DetectionRange then nullify... - if self.DetectedUnits[FACDetectedUnitName] then - self.DetectedUnits[FACDetectedUnitName] = nil - end - end - end - end - - -- okay, now we have a list of detected unit names ... - -- Sort the table based on distance ... - self:T( { "Sorting DetectedUnits table:", self.DetectedUnits } ) - table.sort( self.DetectedUnits, function( a, b ) return a.Distance < b.Distance end ) - self:T( { "Sorted Targets Table:", self.DetectedUnits } ) - - -- Now group the DetectedUnits table into SET_UNITs, evaluating the DetectionZoneRange. - - if self.DetectedUnits then - for DetectedUnitName, DetectedUnitData in pairs( self.DetectedUnits ) do - local DetectedUnit = DetectedUnitData.DetectedUnit -- Unit#UNIT - if DetectedUnit and DetectedUnit:IsAlive() then - self:T( DetectedUnit:GetName() ) - if #self.DetectedUnitSets == 0 then - self:T( { "Adding Unit Set #", 1 } ) - self.DetectedUnitZones[1] = ZONE_UNIT:New( DetectedUnitName, DetectedUnit, self.DetectionZoneRange ) - self.DetectedUnitSets[1] = SET_UNIT:New() - self.DetectedUnitSets[1]:AddUnit( DetectedUnit ) - else - local AddedToSet = false - for DetectedZoneIndex = 1, #self.DetectedUnitZones do - self:T( "Detected Unit Set #" .. DetectedZoneIndex ) - local DetectedUnitSet = self.DetectedUnitSets[DetectedZoneIndex] -- Set#SET_UNIT - DetectedUnitSet:Flush() - local DetectedZone = self.DetectedUnitZones[DetectedZoneIndex] -- Zone#ZONE_UNIT - if DetectedUnit:IsInZone( DetectedZone ) then - self:T( "Adding to Unit Set #" .. DetectedZoneIndex ) - DetectedUnitSet:AddUnit( DetectedUnit ) - AddedToSet = true - end - end - if AddedToSet == false then - local DetectedZoneIndex = #self.DetectedUnitZones + 1 - self:T( "Adding new zone #" .. DetectedZoneIndex ) - self.DetectedUnitZones[DetectedZoneIndex] = ZONE_UNIT:New( DetectedUnitName, DetectedUnit, self.DetectionZoneRange ) - self.DetectedUnitSets[DetectedZoneIndex] = SET_UNIT:New() - self.DetectedUnitSets[DetectedZoneIndex]:AddUnit( DetectedUnit ) - end - end - end - end - end - - -- Now all the tests should have been build, now make some smoke and flares... - - for DetectedZoneIndex = 1, #self.DetectedUnitZones do - local DetectedUnitSet = self.DetectedUnitSets[DetectedZoneIndex] -- Set#SET_UNIT - local DetectedZone = self.DetectedUnitZones[DetectedZoneIndex] -- Zone#ZONE_UNIT - self:T( "Detected Set #" .. DetectedZoneIndex ) - DetectedUnitSet:ForEachUnit( - --- @param Unit#UNIT DetectedUnit - function( DetectedUnit ) - self:T( DetectedUnit:GetName() ) - DetectedUnit:FlareRed() - end - ) - DetectedZone:FlareZone( POINT_VEC3.SmokeColor.White, 30, math.random( 0,90 ) ) - end - end -end--- This module contains the FAC classes. --- --- === --- --- 1) @{Fac#FAC_BASE} class, extends @{Base#BASE} --- ============================================== --- The @{Fac#FAC_BASE} class defines the core functions to report detected objects to: --- --- * CLIENTS --- * COALITIONS --- --- Detected objects are grouped in SETS of UNITS. --- --- 1.1) FAC constructor: --- ---------------------------- --- * @{Fac#FAC.New}(): Create a new FAC object. --- --- 1.2) FAC initialization: --- ------------------------------ --- --- === --- --- @module Fac --- @author Mechanic : Concept & Testing --- @author FlightControl : Design & Programming - - - ---- FAC_BASE class --- @type FAC_BASE --- @field Set#SET_CLIENT ClientSet The clients to which the FAC will report to. --- @field Detection#DETECTION_BASE Detection The DETECTION_BASE object that is used to report the detected objects. --- @extends Set#SET_BASE -FAC_BASE = { - ClassName = "FAC_BASE", - ClientSet = nil, - Detection = nil, -} - ---- FAC constructor. --- @param #FAC_BASE self --- @param Set#SET_CLIENT ClientSet --- @param Detection#DETECTION_BASE Detection --- @return #FAC_BASE self -function FAC_BASE:New( ClientSet, Detection ) - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) - - self.ClientSet = ClientSet - self.Detection = Detection - - self.FacScheduler = SCHEDULER:New(self, self._FacScheduler, { self, "Fac" }, 5, 15 ) - - return self -end - - ---- Report the detected @{Unit#UNIT}s detected within the @{DetectION#DETECTION_BASE} object to the @{Set#SET_CLIENT}s. --- @param #FAC_BASE self -function FAC_BASE:_FacScheduler( SchedulerName ) - self:F2( { SchedulerName } ) - - self.ClientSet:ForEachClient( - --- @param Client#CLIENT Client - function( Client ) - if Client:IsAlive() then - local DetectedUnitSets = self.Detection:GetDetectionUnitSets() - local DetectedMsg = { } - for DetectedUnitSetID, DetectedUnitSet in pairs( DetectedUnitSets ) do - local UnitSet = DetectedUnitSet -- Set#SET_UNIT - local MT = {} -- Message Text - local UnitTypes = {} - for DetectedUnitID, DetectedUnitData in pairs( UnitSet:GetSet() ) do - local DetectedUnit = DetectedUnitData -- Unit#UNIT - local UnitType = DetectedUnit:GetTypeName() - if not UnitTypes[UnitType] then - UnitTypes[UnitType] = 1 - else - UnitTypes[UnitType] = UnitTypes[UnitType] + 1 - end - end - for UnitTypeID, UnitType in pairs( UnitTypes ) do - MT[#MT+1] = UnitType .. " of " .. UnitTypeID - end - local MessageText = table.concat( MT, ", " ) - DetectedMsg[#DetectedMsg+1] = " - Group #" .. DetectedUnitSetID .. ": " .. MessageText - end - local FACGroup = self.Detection:GetFACGroup() - FACGroup:MessageToClient( "Reporting detected target groups:\n" .. table.concat( DetectedMsg, "\n" ), 12, Client ) - end - return true - end - ) - - return true -end -BASE:TraceOnOff( false ) +BASE:TraceOnOff( true ) env.info( '*** MOOSE INCLUDE END *** ' ) diff --git a/Moose Test Missions/Moose_Test_FAC/Moose_Test_FAC.lua b/Moose Test Missions/Moose_Test_FAC/Moose_Test_FAC.lua index 1fb3a1d3a..8e51ca4db 100644 --- a/Moose Test Missions/Moose_Test_FAC/Moose_Test_FAC.lua +++ b/Moose Test Missions/Moose_Test_FAC/Moose_Test_FAC.lua @@ -2,7 +2,7 @@ local FACGroup = GROUP:FindByName( "FAC Group" ) -local FACDetection = DETECTION_BASE:New( FACGroup, 1000, 250 ) +local FACDetection = DETECTION_UNITGROUPS:New( FACGroup, 1000, 250 ) local FACClientSet = SET_CLIENT:New():FilterCoalitions( "blue" ):FilterStart() -local FACReporting = FAC_BASE:New( FACClientSet, FACDetection ) \ No newline at end of file +local FACReporting = FAC_REPORTING:New( FACClientSet, FACDetection ) \ No newline at end of file diff --git a/Moose Test Missions/Moose_Test_FAC/Moose_Test_FAC.miz b/Moose Test Missions/Moose_Test_FAC/Moose_Test_FAC.miz index 1b4b3cc567c73eac2337e9b62d002c69de6f4f01..89b5a40b91a3b07d01997d0cd6188798998e5f7f 100644 GIT binary patch delta 30666 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 z zf#I`L8)M?Nt^VF8pB=MZ^cGAIg4bv=kPpNWrRO%u07M=6Z8ENvDB8Gtcd8!r>af`N z2)0O6gU0CZrgrRR+;*@QH=!X2bQFIfPGbSWkFfZNzS-SXX9+dtiAs_Q+zEXR&1u%k zD!}YD^{s~t6{>)sq_hpoj!EtSq@xf|v>2ifQhMqVKv1dCB4R*3B&;$zf>NOV0$@zG z^r6;^6R^fjrffC^c|}zY?Ze)kI%NG0A@<<-i}L zK_rJP{(gymNy+wwtwhRv%4R=x)r^ZAsj94p=Tr4$1E{VeHJKol#xDhN7+%zkY zUAeS$ogBB?^#}e|UxQRj84X(f#ec%zY!L{sv@NAJbA`OH_Ux!S8}#OSO49`v(uXbd zSO;kl&NN7aFih5s;=;=tA~22vSx7d{>4672xSDkOCRXmAY!G~R`N?HPrUSIvhV1AF_XjsRq@V*r$mCa+6DJ{w zJjW37tV0`h5Yvmu z+!YF48wRE+>fJz;>fOL0US7U^~dQL_6UPc#&+x6E6xQX9= zoK17Uh$(hnCDpuyh2j1q2^=M>h^P-QNk-AUxB?nY7uIIy=Zww}vohBHwU}3$xpe$2 zuUl{@^hI7wClOwEC>cRWB^yKJmzUyHknYJ0``ACE^aQ!9S};4{(x7*o{5FY z+xt%XrbPV)#))n;niO!@QkYE`w;MQ{Ls`cGRO(3O25mjO#GdVnCSOXHF?bDH_3GLq z3c`R9wtDmgE(;j}_pY8T@YI;qIEI3w=;}vvwmQ>bCgvN}5vY}km{sUHs2k=}U~;O- zl1}0!DC&Qp{kn~@{^nWCFr!cX$f{|&y$t0Mn%99E*Wfb!EhqzO)h{VM2huoFYhgvk zb`fZevhq)u2w-L7g&^aO6hH zx&urTNXxc3z)`J-Co3w^x`ADQC|j>KnrR-NeD;5YiC`NSy`+nP>784bUeK!C)0qq5 z$>HgzpAJusf{>!q*-Hbz2ZONQ(N$fz|*}OHmNya7igfl-Uky z2rXlC9`UvTpN*yHhVX8KHUq3(nhPCdvtd@)C)5PE&OUT<)Q0cxzO%BN?lKxlKC+r^ zORC7DRRgKo@G&f`;S<8UQ>P<&%C~sMa_u2SGE^B`@$b_)FR{ zOFz1prs5N&PJ&MU6S$^+RvmJK5H1<^Zy?2*Jsfa>r9KW^?*{VNpZm9&$Nr24KevBo z+A8qPZ~FhnH~fs+-}r`Kk*m{_eMWlqlYK|B;##N2%O34Z?F~KKXJG9f?sNAx{&1gr zy|IV;jAiq~ea>%qRL_}RAJsDv@A9mk+g<*wp3Ag1e^&3i`r=l+2lkxY_`sgC zB@(y1gXf&yAo$>%Gh4p6v_(5W1Whq>~nB z5RBBgSVKeOW);n~Xx7mHrWSvRsurKgE^{B9O=keDEuN@4gClrsxe_hQDpMr*?D+qP zKurNlk}Pf1^Imat$cNeMKQYS#io(vG5KS*r)4|CSU%11(Pz zG#GAO6*O?SrVAQ)&B~x@5QT9vz)|*EOLBUYc9X>hv{Pf`w-JxiQPm0;}W;|(bfBwnoLust-&!0?h zY~!bXEaZVI(I#Yxmu}F6MCPb0c6C%-f#E{y$~211tC&H(qM+e;IhJNt7P^waoT@3U zP~0t=unYWy069R$zkQH@8==<)eV+6oZGxAIC`EAmchOT*CX&lupW~_K@GUH?j z&!H`lV{pVLxe7*K+(bvhsDUIHGp37bZy2pcl5io>3U2>V+q7Y9D(eITInX=w(2I}h z)Y1oDrsLXrb=n!`!-J0}3n{ngRM3B+Mf*rC!cJE)LPf1;Z5xn(5T)2QkrACletPY7 zjtyzlwOi7GV{xRU29pPMM@$k&bY? zDWMIRm&~7|>4XV?=AgrwXg8FVQJyPZ1%sHXTArpUy2TVlWh*@gb&|%%l}KsN;I@kh zi*qdDw+BnM-RFlklG{WcadGjY8%G#{9%T~uP}>C~Fqxkt7wCx{4H*&_<*k&Bv$ClR zAC`xgqvK|cVr3kK;I>Z~eboOv31ct_wkMqOBl@6+B~fdCAPYkx_wezRivRd^<@-@~ zqf^41Jw0|5I6dArm1uoWkDn)vjw9(w_@s$*47y|-ao)K`WRY+p)5r6{p-m;IUm6A~ zvbqt;mvi?mV3l>dlyf}P`SkH~)np6`5z*{2q)3NuhJb5CUr0sSPSzd#MgkihyT42P zBC5NQj}?o5xwLgo+1)6b*mgXSZ;k+qooF*W+;b!>4<8W~IDw#OoP z)9t}Ls9U8KKaj~(ItGJXQOqI{QO#B~sGsPVpe$+~b+c(3<+A*(kOX67OiyW7nokFs zor?+wZyqPe<*w;)t=5e%q6C;cj*hALJR=@)85=Wym}FNhVsUt@#v_bfWz5dejdonq z{Y@lRkE%GcdNkbVkA}v#sm6bD<0Px1XOdS@-7bm(W({fUj*Qs1rQQ^$&*o~K{Zt*X znypClm95=B*cI0}#1s||r;%N#m=@^OPKr!ousq)(MR0zkS6{w9t|tMv*gYA1^U^Q7 zY@*J8;DC-1v68q3)HGV(*{OG%rv-3|W z_xs6ltf-$>?AeQd`bfO>g2o=|*$1b1~cxLK7qi>j!Gb^?Sy8Wp7y&b?GO znJ7=bpfN8_&O5mEnafZbOYSJ^IX3m)Y%woz%fGlzEGnLszWVCp+2Y6X?A2Edn|N_@ zFql$+X@P1V)ob>O{C!dW?BAt}w>{r!3w6nS2O-mL2MUmTl!kZp158YnR= zuK$#lcuZ@w*8fTBzEf;Wk0RJIh|N0wZ9JXvRlS4k-IdAKjBAu0zmI1>hwEJx%b|;@ z3DkVNcLg6s?d}z{rv+llr$Q>$I#sxy@T!1A zIak85!o>@-Z3iOCP3AK#5KazCbm1z!QcPBy9-~Mx5Nc&*Uv9-Itq!zaaTdbPMA5s21-%gX&PA6}fCx%R&mPo`J16lh-72gi?i z^w@tIDwx;XoP>i)%7XD67v@7Yor*OuA=$Oq$5RV>8imVf2cp@e1y|2yg6h_Gn47s#ihiiOR7dE+|ZgMXBR3_FxEYPK?cq=FeQQt zgb5oMKOB>Zi5v9t*Wv#joqSE3?fCTk;n4*pIyrmy%lU`Xa{0vOR5{@=+oo)Y*PE$+ z$HjOObkm|Q%lqZHYU?a*@_O6<*bPOL3l-;n_Fx3-+CTdFD%D@Mf(SFrONCh4Z4+VA)-26rSBz@Rf=J<%GiTi*PKHH&RDt` zxX)y;5avfZxYivUc>1{P!08BUMzdV${tH4+#R1BsN%R3tN{vI0(oD{?OWXJd%oj~- zR`1cIZPtTWnP^^xcn(JsodOFE%p%Bx;^U51UI_l?7Wcx(LD49G3Dds`R&8O93(iqu z*G9xtunc{8BwigIC0-yWC>tj(KKY7sNJ`x(A$nT2C2}e-!57c1QmJi>I(3PL5mlR8 z-HRWNp@mP~M!IR+Hp*$M{8lusUkh_23lg7cq*aAJJjJCi1(HOQg@fc8z#Mivt$XZ!DME z9t?u<)BMr80Go41awg!7H1^<(rUv&EZLmc+f_i<`-SN_}k_w<2kysT3y4=!nslZ^A z3=R0&8lljCUlYWC%Mvpk+;v);7xueI5h!6$mw)8ao?=; zj^+Gj{QKOvmY*IE^|yX2BrU8P&7;WaZ!+n_M$t;dwOAKK1(CETH@X@XH}!8EpK1a!Avzs3AFelYs=n65aFr&siH^t0l2-?qfA3%!sz zdHhwO;pf$@6Z(5I0Rgf``ur|DShp_NLD0VJT&()(j?j4p^^+j+B2SmrZC4}2=!G3c z`(QQa7rrDlQxDfndUM-U`ACOCp3=sDMk%~I%-zo$7FBnGWPa5Fbpv8joYi62@=mbW z3=uvjXfK7}r@f5ccooOZfY1>Hs0&_F9_d zqvCx6X@iLbo7)@SU%}Sl4av4Rx`CK8IFo6sV_UjsEWGT1J!$B^8a$#@6Z+kMz`{j# znbcO1R5J>~ze&wHanKM4PD0EJnsO1Fc`h_`ka9&R!m$$^`zwUg4MwY!?gptYs{7;A zEUG)fYLJTEV5FsDXDE$QQO@vy3#*@}CN9-}TrT;D^h@#{yC*fY(C@ z^0r^r4@eUY`a(-nV*_|Ky5!)0c6w|fK?a>plIs;lFzF;-TLp-B-+*t00E*D9X|u{L z-bbW(>fE64>+dLP%-Yq}UlEC+>BKfFr9ycF@; zHaknaHn&R-y$C%JJFM1Nmn+d673hTT0}U4>0Y2Nf(*l&7+cj>Bqayq(G8Q+vJ9gPx zsU~AkAZgB1ClX+1Bc(k!-fYAZOq#U7W-GsIzZ4cfX*FghO>h#u)o5&t(%#& z-g9U)Z_L<@HN6}0wKp16$s0ytes@5FgZ46X1To&+wXNOZGxB(*!c%VY)BrHa=NF}s zG|z0GJ9lr`^U#Kd`uBuQzgC~Jtdgd;!W4?@-{L#*$SHyes__4rpKO#XvG=Lf}VS&nDf{U4lsb#Zp|`PNz549FnxE0d`m zzvhI+)lO+{waxc`d?diH=(yJFIiI&|dUh7S_SYwp)k9z4n^#wQd+QQ5tI4LHVyhze zs!~RINnS5;8~vT);o!m7zP77f|Ndo4pW+5sOGc;jsX5^xQ$h_OYKTRPv>?B!6h&=Q zFSamB>-|H2^r6zY&9#$Au2aVoNoK*h zc@~;P;EEscmvmDqS@h)d@nA8(Vs4$86&#lm7j0QCICu7Fb4mk4n#tr~R|Ksg+k>bcMbmb8*!10x>&AFjL%e1_1AN+! z?m;{E1QN7=yJA1fCgSC~VE3esp!1xmlTVqzsWp!h;&6&cBYr^`xCY@q#&d+|aO#0K@j*1ihmGN*s&9L&5E>;;OqcB=YVmP%`p6Un;flpvLpmp#R55X>?n{keJFf-M zm(ja88?CoK5wAmw zQaZ3>kp2zY6Au}DVT8}CZ3}fgnuu*PBpG@$zWUkV>vgf%=%1W1S{4!C1GW5R$|U_d z-y98aKiu^cW56=t5ZQ+ z)HD!(<(%B>DD(%>1VhZmtlx7}b=`n{svPbEBDbNeV1<6nLgXrhoJQp@YAO9tU# z(+^5VNDA9iisNqKlJaq{s1N28mqgM>o3OZxn+RmY^FG+9azEmz6lDcJkQU^yR4CbR zk{;t9fA#svnS?jca0jlzkVAwFULU>w`WyIvV{p*^k+-L~6fyYft0HDz)yQ8pp($l| zPb4x{kw6Tg=_7*WLgK;SkXV;az`m#aJ4Q%*1s$}=-!I|S1~ zoq&%2G>D?v-5{EsD@J@hea7%1$6FhJ7=xl3O@35|T;YijPV{YFKjB<0#*Ze!hk16> zwKtURQ@@ysaF^Ta8N?A-^84&!q=101Xc;$dmlAvz;cE|&%l@B|wct!z&^oRo< zx1M{hzOGTHf6z-q)X_oo+rTXF@rPPlU-?T+1>@t^%uZuOpdM)tnFWn8w;;lQOL}fN zoll1HmvGS&!2Bh*gX#iOoAnErF`G++3{4B{Lx0SOiHhQeU#q%th%fs&x&=7c*3n=4?KJu)wMk4;H@ib{XbfzP z@Nu|duDIgSbTZVxCHD+qM;pffFosCznz4uIP0A#|`cDtmNuf(K7(_zHK9_su*Xd-B z0eC1(C+Yo;Cee=PA%9v`%K@dKrfCr$mj!@eB`H4VBZegi4e>KvQ4AD+t|br(0Wmgy z6jPG_aRUuRny%7Nsp&e6cG^&DBbQo!s-7%Xvx#Ip=1e}EcDovvxBBQ66Ny+Oh>{-; z6Zg`iZ(nF)n?_{OgD$Anw-ZvXofEY&i2avRK;9p`esG|(w%h2H%HFTN=;PdtD{c7y z`Y~H$BppZI2ZirLz(fgutdrb?B@F?^PbZ<$$m%63lluZ&f`C3APv?j77FwmwZzM7& zI-4|J%`pMe?W4z>!{&xH7mCY3CI!;`;Dc5HfJqaM5xF(AYl^ zo58_CRLG_;6qpp44S6pO^e$~@#xch5j!ImMED-H~$kZWaoO;}U2_5=s3OHSs{v8QK$5$#fx~7(efOl2&YfzH`kIe8zl^7|K{fqlHFLcb!_zs{ z%lEb0;b2Ce9tx?{L^D4YMCW-y-(RM)LOeQtBz@**^PLFr7`)``Wp#*Z0iV~GaT~8@ zOU6y7VlgBHh2gL1C*9E6wX=|lNfHL%113S}1sn0=)q>c6qn-qXUwAA^>nWr|AsPdd zfZOI;AsVl0L74np+}8PHT-KRtM590DJy|b56Lg-+2dY*C2PWx@dO!seeDlD>m`7W~ zBFSa9QQ=eT*<~5>{xSJ+Cev$?c0kRss!b^qtsDI~wak8MTR$}}@}nSCO;s!zLUO*t zbrT*9_RkA{+Q~oCx*s%&qc?qVBsVvaJVJe6k(8PEDUAs8kbZQk3VkIYXMZgz!c|eS z7!ET;!AI7W*lY|tFl398V}!nK(bRJ}8s(5xWuLWeY;2NE-mhc)zkH<$IK zou-J;oC<(zHjd9wSg8?R(Oc&6SAeDyw|f^!D3U{ej6CKTz3Bvu?&yKE+^-H~1EcS| zgWNd=bJeRtE0o6*6DLN61^s=+VmHMbBkR*@URj6`iW(`-VY{-ZIP#xls6u-7s<0`P zI7SVxK94*z^EK&BOZi4$mSaiXj~ZrChd*T9q_#%E!oKD4jP;_tDp`U|$9anx35gmM zMrs0o*~u-ici>7^$M2&M3JZM&BcJ=$a+h|+rXyC95A0Pq4tO}Rk+~f99WmNy?O<*{ zZ!nmB?K>@6M`Kx$IoN8E9E>4kjrC9jPS#K|ugVu&2xl3{Ei9)^aCarl5wXq_a^)oC z3&U88G8}wpv%2Z1+MNf`4V%FvB4lZ5@~2LJ4M6m&b{(tAZVwd;UxZb398PX&YieFR zItqai-FME3m!m10VI~C$hQ$s0TMFy%S3-UR!J{b)fV*))IiPyCoG~eKS0J<9;k>RPg4CvxCUOQZL3!x;UhA!<&~s4lUJgBf zhZ@KqQ9nP&om~4W10>U?0c86?_ZJ+Ca7l;T>6{se20l$uD)%bA=3vG0q>`*nS8kwz z$FEO2d0;^V^L@A*J!6^=No?$ViiGI$`AqM@V!7kFktEyE#1i>s-|~$NzJuS9jnU1> z03wl%p?TMKW@6}bhufR}M#o!fbm%01HnBM(lLJtb7Khel7#y}4?2R=1Kp}Z6HaNH` z>jC z4D@aqABRl=! zm&JtjQJPt>J5d;RXP)CuJt@Xae)c5DbrV`mDYe?MbTx^^`O&2(%)uB1tb zW{S78>}?hbsH9=2Rk^02LCM>hWE`UAS(=9?kGLn|fyX8$q5#M-615?Jm|76(ZYTzn+!?-*r`b=Bjg*E(tP`7rGUMohNa@RmWTa^f;r};@PRWh+c`SCJ9x)e zAx+5cUFneBlt>_x)*{D$Cgh*fqi450bV^+W(f2TBpzyVy)CnCAu#!q1sx!1av`naZ zs88v6@5FkZX{|1Te*YYmB%Zg0*HP88b3$Cgq_T&GioI#uSKTwfQu>~)%T)L*THZzy zjSp4z6C-bVx2p3QOg2#Z41^4=&$I-ao9&sY_w`j7%&@(52Gh5HgVNyUgoSS8hPMN5 z$USv?9=v9h?335D-}=#O;ik`CyME8<>@~=>FoT}HM$oK3ezlD~c{8{oJbFJcPaH_p z>F@Ds`GkOfG@CwtLZ916pI^-ID#9MWdLC*Iwl7?T7TGgf9yw^SZNQJ``1Qqo24Ef) z?%B${AyqLh^2CmRemu1U$0v?=+zjNZiUo zIzCq`L*;T9H9}BH2PF^JWL1qH7iQmChj+}zPot@CPmVv-pTgnxvnll;Op3CQt+_zT z1eY-ZnK~`OxZYv*{8Z}J~ak*o_C2qXIg&!7jHJ^-9i~opG-S>cnX-u{JBkpd~HPLRmMDri8 z41Qy~8pxi1tEUfjZk?t=i>hMW2`a&3rh37P=kWqCatMd1h$c*%Bx#hZ>!!ibYI|5v zS*fwki59T`ql*i8ch9nZz|LUF?|3gO$^M%b>cZ$@RSVTr&ap(SqfK{i!rKZ0qol|Aj~i0 z^tqN|s{t+JxrC@GxT^M`t)ryir&INpQ~Tjs55wskG#?FNzot{d|G74{netm&MOUi; zHVxi?SGKZM_S!u1Oa(uSNh~(%*-LO{hoOo@7@t^)dUJ4;0_zXVVyzUFxu6Zc zvNO1vEh-O1*n^|yvmBk84|3Mh9_aLqmPI%?boYU|A8jozhhi51)8h}7_Z6YzX|)`G zlVPCM+CLrTnk7M9I)ve_r`Po%1*k6!BAfu*w1^SI1mCrVyfV5d=+CO6YpI4on9Rn` zd|Qxt!T|POgt(n{?!*&Qci{6g=mq1{PN2N3TdkQY*X=uOfJ9})^T~Xb`X%WO=d3(D z+%zB0;bG6bm4}C2GFKiRcFbLQc-Sd_d!;mwQ&H*y$~s>n5sDq5Up~sFY?zoRpf=*r zoK>?P{oeh=5BwE>;r|E69v4t;-;6^!rgL604(^zhW$3NWS#N|jQkvO4r?{QYt={O5 z2)<1#cD&TEll(ySV-?U@q6{PMK^X4+f8pgfP>q?gAUEp3al=?9! zb4t$-VsR z{=hg0;RL@%Bkw2wB8zzss;SZJ#K@L*x`G5fxb%mv2Lk!E9-Z)il3srM1$Dnt)C7*D zJi6yziS8-qQdg_qrRqLmTMoQS9W}s*QjAx=D0Y@aR4Oi*`lKzWdFqY$MX?;GMnxGf z+k}3`cU?o<;77USN?m8UCi7yoEXT9B)q|6-F3yfVHw~XA<_Sb320j(Y#73sg?TEaI zDaG3Z42C%SapO;aPCh@q|K-DrlQY-+ammhxaZ@JGgJXFZ@7i*N{WIgr)!Sz_i~C7#3OS?q29jwbY`}>BRV#Yxd5^d%`UDj;!-11P z2S*R?^@3(YT_L%QN=;Y^>znJ%G)_jY?nP)B{w1wbpu3|t;Dh8SOHduPX~Ltvnf7So zlOLri4nVDcFadr>e4sTgte}me#v}K)Ov2N@q5r;8FLYSeB8J*7V8gxc(DAA1Gl*Id ze4=S-UB3f=OZ7e=22|DJ$Y=j=_O70b?KxeCSMo&=lxEn@kd5m=1JI zd~HpgIO9uzz?9$4zDV~-Cn={p+j)U7Kz!QOYNcI&>GblfPEwnYeXs_ARE>oTURCr9 zuP1GS{drYQf1P293vLW)#XsnA5a7DKLmn>4b9V%om4dGB}ipUT5#J z(%Zd@OBTOE+5DuX^Di#Hzq8ef44_on*}wwdh%=;}4J;{V^M&9sbT&I*CSR+6!_|-z zu9i@jTn(Sct3_SqXsyGRnIvNt9rrZQ#nA|V30cvXZBFNZoSI`uj&v*jm@j6N&R(XH z;bca9{go@_s}jZa_9ZtOU(d3|4N!2(uly|yI76Bz?q4V>f43g3*5r#bLvL-5s0BFeaLJ+NVo2PCaVt;!eRifkjK7l zmf04ZFB*l6O;B&1@VM*6OBiN}?CPJ8$m)=f*s!B?yD(R}YzP8zFzpOUET4uOr zH#8?d??>#T<26)K&gd?%{&v}aY?ZybxNL4vp_@*J-SPH_MZ<(hK_3_yLf68mLV~2Z z^55J{$6V_tmhL-c_fE+l*1GSI+9A0u{!g9pbZws=xbKPGdm`QZf8Pzc8&X5uwm!&) z&FqIufNiB2`lD?{nVW)A_U|z1F2mHY)AWGcZY>3-TqWeTo5QRNb9)DWJS7Wto9ika zaJS33N;lkX=Bv^jXrIjIRVfLb-!%cc7P7q8iBAAB0jWk#p+R4E*RuT}y(y<=B1ssg zuQ>Sj0Vc5maS$Hfr%|&oRux+CEig4{uxTs_oM!Y0T{f_qn!RyfYWZTg=XT)U-cBp; zzawK!twu}UJ5$8W9HxnX%1DiY3Dhat54;1XFvrD#PejkB(;k7+(jH-@gbraTQGW=} zxy3TjD#sf_A+@H%9|%wA2I0wa+6hon+6ROq<^n?C zmP9$BS&x-6nogd935E%z#qEL$ugffrvvajc))}vB7qEn8afoz(?~(fm(K;-kV(@h#U!T-0>nS&QXVv}-tob%%L3v49DvUUUpTcNqp* zD4`|BLNmuhER-k_77{?HaDWvAT+m05vO+Mp(Enlt2G=V$0E2n+;xCvpEBt~PGomk; zZRy|(F8`L?=+3Ht1V|2$asn@yZ>hKo=|(}=g%uq{U0A|;gD$uP6LWDV?`drd6yf?I zPDC2C)jNU^5w;wH1t0@{OHXoL)`G;g(5r{cw>RYRyngk=a>SHbGn(ij=x6Zi&x z&$dF;49X0GW|nBSUaS}JxAoGoubIMA=Vi$#n!lg#&D?&r??N6T1Bu_bX19lN(pHl zky32b{((}gRZg5#hD&vblESWI#HYY=>tm!?$^Icy-NK}53-mJ1eT>vHF;d)+D-1qi zaq-v_rA5rRp7<#C-~8|>aBqv6L2wkttEmh8|d%RI~x*wsz%Twv5KaLrypQS}|< z{cX#C!dC7hJ`7E3n2kaMY>~4y&}fpDA6v!T*AZI9<*09?!mXGy|2ELh^92Qnu>o#d zP!;Rf9WBneI)_worlld{Eaa9W#&6ca?r10#Hh}q8J6;@IE_6eZ?`Ra&DtaQ#e(t;A zjd#Jj7Ot_5zIQ7lHQ1ziT`z0SvyOYwS(TfAg5US;CCPrm#Gt3#=^%|xp0chOja%;{ zSrHm}UUFCBGg{ycPxxoYH%b_l5kP#9hFAK6@nCawY-qPWydt4@%;Ncx6T3 z4=1DMh1|4dnd({9N*ioKvTq_Jqg9bF+J%I=n#N>Fy<<{Xz)ggsaJegqL>s+i@IwI zFi~~FV{j0&f=q<7ZBnPT4 zqH@9Nrpc^)pw&@g2v!Fz#Hu4_5~>akAk~o;0@XpIc?}-CnGK4~2u=XsP+QA?4Nw3_ z?+^tTp1rQ8=~acdm1-of)d!p+nn4RGPYB8Jz=alnaG1j2g_Ku#WCSq8e8VMW2Qh$K zH){F^GDvnml)KcO`#HKeJ?vU$n_aP6f4tc-;W+&I0w9c>x zvFWu0K4kjUeegrBYpgN+K`e5e0T7waaUTS+iXe#OI1>+qP~uubA;h>#1Ve~sS>X_w z4%Q(g!rv`}Q-n;nKP*DjytcrIj6I=NL*0i*EE66fO}R=Gx(bZr&_NA<&Dk$ZVx{=; z3|rs17=v46-(Il>D>bkKfr4WBASl4)G-CjV;?xdd0>XbSfdX2RaZyygC;X}FFr5j1 zS_yWv=m=F79eaS4C13Zz0m-&JZdt_KcI5Kyak@JNVfMCL^(h0$3_c&qtOS6WZyS<% zoqdw#>Gz%S)^*0a7E-W(-u`$CgA1sUa(&!ynu}}5AV%EpGcV=>|3cfX<|W92?7X|6&8Zrz|bfowrD z2c@kB%DiqcbD(ykdlfO$p|hbg;iubB(oT=~yoDNdf@*FIL%FDbb?CT$0#`H0FM0iQ z;yiG)@1s$G^lBl}zJo$$;Gc%l*FAy04vJa@>UzDnYhUA(UxH&Pw=^=~l{TfXQ=05K zK8a(Rm^%g_?xkVeaUi#rmIdVwz}(Tip}kO_Wo*UySzk!&X(P^>V;`J6q-+$!hOZDE zj^P4jg`}ht*>{=bh2f!Wz0*F0G@h>zG3Y+_PBq;>hgLWm#9&B-cm_6uyf7ISV4TXhc8+zTEFAZNGYav(Q*V&5$ zq!yO7C?q(545&HKbjuHHI*x-5%XW0Swwlq?%en2!4AK%ov1r z#Ja;cv@Pdh*1^*kv~HxT7xs1qqr}^muEN*t6<#R~?0VWaLV6s%-r^zP@|`3|j|14@ zDt?XelW3LlgYR<6T7%!lIl56R}iGXFdFRZ{1&?5}Xz?JUV z-4X&oJ2C*#4sG%v?dD~-PE5P0?Dc}$QD~|Y-ox7032Qfgb$z(@Rxis3wwp<;xR}mS zYdHIIu67S-?+<9t2mf`Me8;S{cKly9D(p@p08MMI5F%zIMloU_$BlZA0$c|Q@Rr&5 z_8|d(jb_J^52%yPu9T-4c)bzAj>sAbi;X_S&zFifpVdlNJ4GCzp&Bc^!$fM54Yar+q;=n6MiU< zw~C#ZWV0vsK! zOM7}kUyPh<#lDy_bHOi$9W9y-dolcX4)+4R)4&(QvwGx9AQ%-7y*Egqy4GncM86n+ ziPr~zF$Lz~U%Gjy$>=!fU4p&@DkoiY)!>(g*d_cj1i(NO_iPkW!oyL}ywlP5?Xuxy zKDn5c_Si_*2{I9ov(fsrNdEWHW;j>)_>D3S3v*OTEMM|VOkU@bZM)k)W=T_~%hF&{ zInMidw>V=aJMFBdCl@oGwYh4KNO1^%;kwFYSj44gZRvlU3vzel98Q~NN&?0bmN7n+ znXw!%Y82`N6kNPZb#T-l(TUfa!R7-;Z&6d2w>Bdav7m9oHtE&IIjy}Fw)R%CwME3M za{BTWZx7EGv}ECl;$Zqh=KJMj(=H<<FY_&lK}lv+=?(<7bNbfOd3Csol;@A!I>HdsaVRW~LpT?79j1 z_6Bzpl>xGXGSDD^E_}TxOP!;COkcjF*KbwxjD$2)wu5pKXCi`O`SI2Cyo@sm$*}D$ z#-}e6d_uE~U(PNPY)k=K1eQzJ>ILz%){2>0YXv5+RzH=zn;z#=8WaxP26}(FbFjyj zhvUR5|JsttBa2_p*wI zcx8=bJq(PJ#rvQ@-HK^kX#sMui9maj(oS#hPR(oWPkFoiiT857%Ssy=AM2ZIsDBjX zB($gV1>J9Pwd6~Ye3>hM`TMg!#*0bSBJ?j2`d6+{qwWE}RQ;W&=1i%%TLOwgkq9+; zIi!MV_M8P|0w4-oDkHl=B(ET9^lcGKe?;^){rAS*eraaf%s5_^J=f_%Z%10NJ4$Qh zyU5kAar0NKFrJ-H&uP^lWsTZpXhzNL>6R5HgnJxc?k~o|W|alK=DKkEV`# zAolv?gI9jRzs~2ga(t@1-_4I-mlIj}6(^LcDz{G-40+eQ@+=gXJ=0`K!S~eiAZWhD^`T<$Y}{ z!wd~3!=O|@yM0rC+i|x+SDS4c7x)Oe@18G)$0vDiq?d`e$jw*^#X7Cf2M$}% z8$lX2+}`Z>)H7c*W>qTE=N)=Q*hWATNDDwOovV2*koS5MVUl^^Rs;{W#zAvojgX@s zO&$AcM_GI6M)`6ylUZzE-6V4_?IdT8elol7t)S%Yr=eucQjwM$y%nUq{nVq3P!7wS zl@AVB8$~C77dwB^P8VTZ26mrnvoJ|IP14_Xo+^voVlrKn+V`19{wzOazVxDf}(DWs#&)LG!C|06k$!+}>#fK4V^y+Q5qm{L5$D21ywU!AS z2c5lcxSV#C{=YuEv3RbHC4{5?{>75Xu6_*h7wtw+Ns|wSVU3Q6{$^v7TpmB+3f6*j^SPmxT1S8Am63E7?H@T5Gwh^3pe z{+Y(yg!Hc$^NX_>fx+dmbiQgWmag|8L@RVYKA#@PC<+gXinD4x2yTHuHI^ zsplL45UmaYuIKTFt~gRYUeN@BH!`DtjX(c}f+SMZ-LfLnU6P-)v4|0?7N&#oWV~o& zvY%Q_^6gKLUtgT1rJT*?mnrJgIVfR2Nmla7w5n(vidu;eQ@%fo8slvI$7$RfelJf_ zu3C;K(Qa3fr<^C1o0Ufk8YALec7MzrFx72#9)CHW@wl_NqJ!+uDFm^MH~6`Kt`}p( zuXY|va*ea1`E9wdbKXfw%b&$qG00Q;_vDP~I6me1^n#|qyYtg`c%pl8OzY0`ksT0s zIh(HiG(d;nFlXcQW6I?RI!o-0U1L4hNSw!q`#VPuzj<^}Y%ARyVv=Bu)d`BR+t{XhaMenDkwL<_A9=#t`pUoSB>J4nJBno336pD5jqH*Z^#Rm6-DW{~HRUnM)? zPnEJ5$l9@;FI1uiZOrIp9j|E#!b#(E^TGa#D)zdxpN|+DM4P;FN#n|#HbR4`BYQ%% z=CpDBaf|Vc@R}s*L^J(JKUwApH3`Lc$wn)cV*1kI@nU+$Yq1fnkA|#&Cn_OPHI=My zX7iUXNwjG9@bCwD_YI{A%q9y6X1kzHKb|ObL+>epu@?07Pu<0aroBhlM5P+E=Mk=%{InWs5ZR1YxeN6orik|kDq?+F7x+|QFeGgcc#5c z`wAk2RX1bBS7Qo2`fbjCt*_q+K|p^5BWFsC-`S(IjbTj@F!hK0Z1v)dHTp-VP|r<1 z2jET_ESl)qM{;U-!13xPWlK_cUt5zRvVcf-Ma4fYE7JD$6I@`7epgmlzrjbkz@PZK ztYrA1q&_}_^zak2!gfee(cp=HUsf5-cW6=l*cCn2QaaFxLr+eBrl-@26!#pLLbawe z1Wstgv7X^i`i-PyK={{WfA#@41QD(zgNr6lE9sNykj%whQgqQtska!Y%g(yt$5u#d zT+eP#bg(GCCTj5mN8nw0BNDzbt7Yjbf5Ty?*AEsF9vkXT9Lrtm<6>d(ZFm$Q!;}*X zUK6%!9;V{f;c~Hmm@oM4D|S!|oLp4&IZE>}Aohtx`4S${7|J%h+d}Mr%~#-qz0+Nq zZR+t^dT$BUYQIJ_nti2dAi%kR2Ia?Fc$J-`)YY-h?0i2 z6%R_D4@w??%HnzxH&-bC9ojRc5F_)Zlzb%EkrEZHDnNRA^G;1)@qksohpD8}`MzuA ze2a^>Lk^DYG;MEp?*WBM>TYdSmOMaAWfF7DQ&-AhhFFnM-+M4B9-A0sdUeq|!0rNB z-{+{_h#3=ZI`G55Hxg_cu}{hJYmKJ_NJn@a&s~Xs#yBmqsu6eK-^a)M#D+2mFX`JH zdU<(>Pfh zZds9krrwKZlr`;$Ux^U9Z{%eXxyYb#j{)p^L|Z4NIILbL*`&yB1xU?Ic2c|%%@?QE z{+T4#wu={xX>Ul?TanWWLv)%2;^Q1%5juehu7fZKHsVNLBL|#daho)HzXVue7?)4d z0wxIb86h!Z6zKE7BiXle5t_=+0}5x+Pgw+i=5up^U^BF^@0Qi^U~_bA=>@N(=Zn{4 zx%Wv$z$Jq0oEX$7^pbGl_N-C!pPRX%aj;T*=GO^Xeb0j0RyRbG`kpduT6*YP6Dd$w z6QG58#{J$p=wXz1W$AAWwg^O43c`=2Uw~bw_VqT*tN8`3<4@-c-t@SnAG}6CJ1b9r z`HEFKAAW~F^;uT*>zr@8`D1)JYDU&>9P+I-uXsxFlCsmDjr&42Ur%8kx~gk2~nnIra5@v{5)_>-tco5d4awLtqJ7nYH$mC>_VIV$ zJ>P%ybmzOHpJ@5`5e2$@wDrj__HqDHK&`*ZdH>)I7N2?fLXZeC&G{W_f_==uoOav_ zlGqP>k0BzVV;@*isXcn1khl;rUPEsEfojaZ6yM0}bXN!-df5VZh z+|urLI!RsI6Kwfle+M5E7&{x9*v}lx&QWA!RJr{Q+_-9kZz@uWb15)1Za8GC+)6QtJO+ctt9Q9 zkyPzckR+UG>W{6w+`}6_+a2dhe>GXj2=uvJrzU{XbOxxi%(p4iYTE{ay;x=4Dl4sj8@xtuwY-StZHB1DnNK>pmCKFi9nkP&LW#;d>ys z<;auyywn@!r^@GgFe~dW{glm>K zKr~5lkSboO68j%Jsa?S6^tSa5Lp<8Nnpk8Wysu89Yf)`PGlNI9K@3POK92b#EBu< zo)5B?e6SHuvEn3xw7}Bbe`t}J5l0#gW$TFd@(I{=9>oduM>WGKLS!6hbB@t!E1n3u zfed6?g^egR>ZH&Bi2~`-z$R>>lpy_xioV17poWi;fRAy0bNr z2rMq?uH&Gai7AuGIc(v$XUV4Gb#8NdSh7nFjM+)YKNFj47i15r35LS4v6Y$eZjjt-zf-$;6A$qk;gD)3;~ zHD2^gaepv}cyhGUKkDuF9@CBE9BVg7RCiDz8W#}ur!S7*gswrf^hvMX!>^C}a0BI0 zzqhl$_n4?a0L)jtf8%5LQimb(QqPOtNPZYo0K*hlI?0uiQeJMw*$qp(-`kkm^<$(w z-(o9BvDdb?H`Ip@Y<=Ls1HCDTv3XCid2!hR`+d&J!^PTCLFHKY##*8j0YhCy7w|Qf znJt``GaK17(BgZLCKAC}OE$TvOF7KTghOqNfYlNiz8Ymye_p=nNh=1mhn8^)20@5i zJCUe}uBO0NlJaZ2?{lDKjo~iQ1l7A2JAcllihC}_R_^$ixxB`cg~bARJdH)^;k0pK z0xE@RNbBpxo+-DEk>0E*ZiavR_i-5+wfZoO_s4aa_36FJaq}tqB$51E3lk~!$}TX))MNgh|IAhY(*k0 zhu>N(w>$6IpPywWnmDKFZxdKuw~U@^FtcL_QEynE3mOXP6L@Sbf%V7lEfEB@K=6~GncPXt-ki87 ziPx*Opf4&)_9;Xd3v8*`v3<7I_s^%1k*fe16S)dtQIV?v7Z% zMC(Ekt&49QOLx(orEYEis%`mEJmNra5KlQke^Ey#CZ!HnMPlJ|2r|@k{2n2ji62)N zSgXS(nY+k_UPTJhPK+GA(g{TTGEUm{zBEDiX~R(0mC7t^8-xT zAv2dhZ-{2RAj0vQT-7DHsYS0Y%bi0Gxm_JO*(O~fP33M~F;(Ie09Q;_F)M^c56n1k ze?v}-hKo9^Mr@7-z_4%Lf)JAV#vz*2?QjU7D`XNtqql*j81ZqSbz6OO>MCm&j#ph- zF~Yq!o^F0wD#9*G@fY8Gt}ho_U-bI5aM$}8?TSGfksV1HsTiOYch9Jj;|T$Gt7dLR zmV;=m$ZkxD_IFAu%kfa2ig#K8Mf_Rqm1Fba9NYyqX$40?N@eDz%2dCE-Nd5|m15mpK= zy|wHv&p&bolNjbARR0oI7dVSK+fiX2hFtP6qfJVB`|?LV=N%~(3{eT0mU}X-e|3lg zaI|dehjB}C9Zf`wSSpQc_{{K6p`ct#6>RKI;IO3=s4Gj0@G8a40xJGo5mh+TMHY3& zyplJ6GmNt*8L{c`D7R1`INikF3n{`fu;nR@3rwn61ksaSRvT6XB0nt*PP33iPWqRL zqLREARFDHI^lkecQbwl2H*2M5e`tWFApD2r*25tTrJ{-EruTZqYRA)b4tWd6?FepC zn%2_{3U)V@FWK^Tf$IkzMbeIdWp2!-GLql00PP%8oIL7x;0h-9WQ^-Na*O>7ZHV1% zEg+j!HO)&9m4^p!MOtLMr2McQqhnNT$Zp4swXlFiY=CX&kje@S9g)(|2LZ zu-rwB8RCjHvuQRpXVnyTmrg#gJo8K}oMT#?(+^mS4Z9MtMMQyl8YfwYwrRtE>byp$ z)%6`Loy9e_ShSWoWac+;f1o;xwz=@FDJczzE3}zXNJO0IfIwMMux_pd*^0S0V+i#} z*yt+&+t$~Mwqx;3qN$lB_y%+G+r{K`LG#MRmr61Y1o3^8*rJcLgi<|&G0`_47YnKv zl2!^OWpSP&ESbWW;aLkxq!Um(#}{FJyPaO#fe*{Kl|PM*0DquKe~`Y>d04i(9;bx@ zwR^ag*{$VS6?5Yqo$zL>8eKTv(bb$YrLrYnvq%pRqrY+jy>bHGcPG#*4^Y&)=#>X( zi6zx-dVqcyBlJU5(}zQD1{h#If}y2Dk2AF2gs(U8bKU|=PJqZai=k`QWd1trl`rX5 z)?W+zl71M^_a-H=e-rIj&ZKyw`O2B}4?B}?-56rIU9&p#t>G)L)U8^@#l2ELjAQ&# z(T4GrTPjA)?%FMdjb0IK>W+%mg;(CF|CcxFhw-iYqmtWlNAWn>#{cnfXS<}p?ei~j zO3iXh9cOUjd|392UFc-Lv`h8f5%~)eEij7eBTiBYt&NQ> z`nN^tGK`MDfcsZUXVTJoxbY}G10lo5MS3PnSw87hp5^TV6Zf8ZMD)Aq4aMm)OYCm5PWCyG?9LaFPYMd zU?_FYY54q@e|kg+$TfO844F~aR)#q-Uox{E5KH#6Ddbr1oe7;2xHe%-Z}8NWd6AdL z>IhrNI)*U1J;m)70xWVePo2Bzs!NQ{O1_JxpudVzx&>wnmU~y7k+*FC+GF6^FrPp+ z6qCM~T6J^W(*;aBSq zk3kRwn4XruiA#@Z0TFyxw98-F1bN7dv;Zl!i6^i>FV=v9b4W;#ts2WiYkTyCTTp}c&@ zCW5Xa8RTR5Y4=n2O}rFZ-ifO<4#w>P**=i)y+BGd=?0B)ffTc`o^8|Xi};8Wwtv^? zQv4if3Hbm|Pr#-rv%n144~YdXXAzSM_PvVde{E(&MS+}G9EF^Nr8SL_ruc)An4d5X zwLT#YF5%Sh6w%n?R?ks}k#`uSuZ!W9^TOLYOeojz5nDe&msL>TyhYKbLA1Su+#bht z)JJnNme6C-r4xD^dP>OV+j1hkfc3YdSbGNud}7QiGW9 zUN-ZFo)+KGBbRvBWJLy1lJo)Vx8HUy^A;er*MgjUM((9{je;(%R-W%fvfC&Y6bjJ= z0S5frrtY-{Kh}LGar>`<3lDy@rVXL0e*}-IGDPflvgh)L$nsGIy+u1)#-58Bqy%Sx zuQxO5iDWs_m27X-CEwvsvh3+VkcYV(gv$??E>dO*ODFfWurD`;lB@P^W&|B%+@aB+ zMkGwZw=-<~h=q!0Caqc;O_E+<$Xm+%2hw%++g<$2#gWPBT%5NPaTO^9!NKfMe>tX2 zP3D)wi83XN$MGm-6C9-+P0B$AoAL&7h{R!~X;vS`X;vS`X;vS`iLFmfcgh&uQ8+R$ z;T*82!9GC|m`Nw%^dC$CN}3yWn)bd^#6&dLFSNUJ^!XuN95g7)tmLU0FN`<`!Q>xC z(Jv(rCTJ~yazeM_l*y1@gddtue<*7o>)tS*7JT^(-Na0p`yCBLnV{z@3c-6NE!B2Hri~KRiO%2ZZ|h z(ca#HUBprE^UwE=G#t$6&(C32NhR(UqhgYNK7kdqlmXA!V?xCZy!UMI5<~DP8=B-M z*{~w^49@-x_?7my&*Iy~Ar_}rlRehkgS3HS?4qOhjIBq570RSVG% zVu->!*cP8Bqidn}-QNEG;WMH5VUQv*qZsRV zUQSV6>HYE(Tt<#Y;N8rI*zJbd)u_11hE=y?G;NQ-s?)XR=rTFGe;(l?hANDx`FNP) zM48_B=c{T}L09?dFJL2t{v~!}F5>B=_$?ozP2i$g(fv za&Xwkjx*qu{b?6#Pwq@kZCw^8(DW^Kfh5LgF7>A^2fMP}8hagHc$1`{vwya8ytmaG zL;uaE99G>;0+bT+4Lp$RZG+{=V^R#B)`vIiTLT^E=g^bB$lw8mDGmaVzP@gw8}B&bWBQtqNq`h~s-lh$#8hb%v}>AMns`2N$|lCCaLRwc zI;6ApRhK@bt6HnP_*6!u6^h#yekuM;VJb z*-TVPYPD}5R&k3M!k)wU^GB9gWb_2uR|G{uJl%PAfSttOgj@UhC3dLIuI3_=S95Q@ zfntG>^*qs0FIFgNQ*VdB|8Pi4Fb8)%Xvc%qrkj-1H4zwf4VECwYk)(6Z(3bYEkG44 zLcocQe*sLebT*-ERv|CeG1*?;nH5)LsGqWGvG`P5ITa!Z1d!N+nN7*ZVTcMiX?g9^ zPWW$SWZ4-k&CCtV@?TjzvR|hBl2*V*8N-_je}FlX2#4RX>y*!o5vGs}=T528)DI_OvLi z?uG?F#kUu}$xh#EE4(^JO4}E1;u_AQ7{wK4ifhDeqmfefE2QX9NXe0aAY3PSC)sBfdt0)R`kcxuA}vLrJsOZD!pl&Tha8LF(tlf1t{y zgbK*#h0(yB}=972uWZrqz!T)*(H75t@ z-M2eAckiO*$tiy4)hBoUE!TdA#Y2m{y87cd(6Ih!@$RDaq2J$m-S4K~liuHX-Tw)_ z_tl>FTkgF2yjOI~^$)tMXwlZ#e`h6r$Yg0J)MjTHcM})g$myiY5(Hh8qb$_S-^|9WZKH$CZjV^xdAMQNfqy1z0 z{Q96ucvR&>kOFSt;tr(V5NDT|egsaoC3l8OmauVT?%br>6;s=89B0{3e}`&MXe%GX z1(FlIIH4@~iC*}(oZ{BiHE7HP1J6rXrtoqi=@AK}a5;b-w1FA;0EO1YZtSQ~R4D&6 zwocen6th8tX+*BJBNIVgZObSu(~~C1Rwg&8kTYXJgf2qJm+YDtj<@Ko617W`sCJ&E zm%#-U^rM4pFcH7${(!b8e?f(Vo>N2+N1POd>6pMJdCS@_21vbf*V-kBA=9o%jUe6J z>1|9Z=s=#wO*ZU}n=q3NZ-bYw$4FaRfy1R__srtV))_=)wj#&Yt;6wW#b7dwY4aq} zt&S(Na&bRUM!nvU%Re*{f1(|hJTj>A%(!|J&f=cSn!|n;Re_2Ve~o2UK?>Bl!9vaE z2{ni4h=4{nO0QIvILdy#&L`BKQi{+(-orGG(Qyha1hCl^6I4Uf3i)SP&bVS_FuI;e zr97miIF`kmG^^KZDaJ6vKO*>1!=it&_e5Q9oDPbJX?t0`nELa z_UgX(xM9^P2Y)XbTokgR&i_CDV>tT#*z-#qieie{+G-Z~0+L^emLy$equM zV05Cc9=8G^)Ha4?-WJ-=W?ZQ{;L_i|Fb0u?tV(6|E1z3=xreuTnd?#EPBuZzZR!kC z=FWG8VQu*s!JSKM9{H2_6XUF3%8lTU|6W8)4@geU1?AzyS+8!jf2$rZ-Ib6|^fe@HNQYuLQmn&qhdFPU13as8m-t{Pj?^3$86A!0RWPrk z^D3HZh*UU04Z?n<359dWw-f%-1HMSmtgvbItVjOrBU3F(WKL~QoS+a)#-F;P_Vg|X7{Ls)r#^UwaW+)^$&e@ zeqTl}e*t+AT*4vah64#N5)<2e1f(o$7=#3fZOY2DS3Vs~riW()(i5D2d1{~4DKLn<$VbUA15&5I zbg?8*E1{;2b(lbWreioMu3WG(8+1}R5bV^8dTc_TLyd>&$#p&& zK6~1(IP1b15Be;{Pcku^Pel}nRV-FXn!6j#o#fmIV-&M6XXDtT9yHg4NarQ5hByydy#9k&X`m~gn zo7xBGpsc2=T=*sCF-2nmF~o}_e~KG@@-e)xssNl_T}*ZEEbP+he_z&rl&BmfQy;~`%MDi-*J-s$UPwSLBfxUe`@+iHkf3; zCA0x+2fQq?7Zj4;%>5u1h0P@lGURx-Le^lgIfaNd$S6RW&M!tc(of7fxV^>aKo>LV zplU}J=R0($G8K>`Q1`ra>?XT)gj&3tP7&@&YVGb^c1LaQURE)g8Aw%gNyhKn=#3_4 zzr;yxxCe$ZS;3|NQlMWVe`jZJfGD^lo!k7>wv9rFRX*ZYjal%9Re?J$*FcGlcv^ERB)sv%yvrDHL$fE{>QGTM_3dsgul z+{Pp2n~R2p>$#0Xg4ipauy(-VoOZw;F4*LjlnP2AmbaIxy#i0~f35g=fwji#P}j#x zHD8T?7Z{r9+l}_@Rvo^vPZw|+t?Mg)Zk#)jJz7niiR+0VSUpWJtYj_(9_v}y$KbkP zmLR4ovNC$Qnl?ddw+7xEaU=s;(pISJ z%H6z)-!$CK1;bAMe_wXT21EiN%# zRqI*%7M@E5o9|XsMB?@-;&&;$gcIDLSZv=xEk#$m^RTRUU^R<+ft<0tw^sL6h$-D7 zYeZN&wB+m$hAt+10|1xMAi7q&fXE~GUv>X#a$L|&uIKsze4el zf+E9*frRZHj4LA7K|yv0Ac?4)?&U%iHWQa7th+Uu?`tS!`UiG4pt=r{^b0{Wa|D_2 z>)Rl>1EQ=)+Z!xhuSeHQKW3YOP@H{+<9#^0gC|~sI}HL7+G`Mq$PEEGD`MS5D`2~L zcRs$xja9oRf2UnyxK(=qQ8(6-K5!yM=Q$zyG{{&H%yNTRI{Q?N1T+eRc$D8t<=Zov zInR^QZ$8|3Z`jiMCX%UcXW(hp6 z91lCUm7%W6DBMv_+D0f05DlC}nJL>dR_+S3NG64u(Th1>1?o zes^uD@-z`|dug^`SQ*2W>=+uGV{t|H#zryAmCeaUn?&DQ?HCck=V*L)y}}TD0;z1* zuf-G;70^qD>L%ImQ=nV|#AkytdlMHaF9~bA=^w&U z#U#f!CFZhFI{d3>X8OJb52je}q6po|4tC=;&90uL^lU1mhQFPEK_mJl9wl4BE{*w(xNS~F>c#X?*Ie@9!8 z!?{WFj@GV_tn`upm#JWg8itWHjNpO7>ZfIAAOcW@V!czRKsjx2jUjf)lMzmnzyk}j z*pa?-u3zumjDqtuoz5w`GwZGl$(SDq$5dG`1%({Nj6B*2vnomMJyr=|8~hJ+*@`B* zNMl7=S43{TaX~7lJ9Ren_VP6Of3CQu7mBpviPnL@tL6j?LZ8x5oWd=TOj)HhJ7SH9 z;v_4tGJF^df1c)7q(SC0OrbZQG^IYsUO$XA#oufrbarZ?j;}9IvWd=ne9f=@@w;6r zFCS}ThLperLS!qHX7=e4j)aef$;m7enTA=Gwr=b9#X8J_0?U5CngFD>e}1yWsMpPU zh?~r=Ff}N-#qvoxCBb0L#CSqZf!CB~P>&lZx&>Dx)gIoqH2k{iF_i`)!x{J?QBbUk zOj$Lbm<;T#ICN$MG8)8V#y1K|)ysg3ZUbdd zr6F6FLmqD`4BYDT3fqHJf2f^$B(l|IYXiNq^(-GdOmK(T3|2N_fMzw7GCAQ+OEp?_ zoBM+f7;p8VC^5@h3Es9&myEn9#&7V}1GfMOlrvx81PTHd$;DH?0c8SvT}S9RTeKS5 zEm|5`7mv0JElP%#WsqED(95PbAeYkt7&9DI--Zl)PL!-)1_ff`eZyBOTNbaE`lDvlRb`|AlYU1g>lRJZ zjCSjWv0JxTanAM7{iO}ddTE@Ge}?m_apAz^rGD?_MTVP7Z%@hmB`D|;MUhWO8Ek!V z6r6(&J-|u(3*R-Wf4__CjM5GoKcLf+BRoM|q;DR`5g?IQFft*8V!35uo=9xqoc$10qNAL(<%g9&uYSk7|4?ebK+>&?F3>I$1VlKcy zF~Pr@6uW{-#}~HJI^K3m|6?ucsZIke3xT?0tU$V3boOn#!8`Db5_*q4hmWlMWIK%M z9BA6Stbbka^<43r4G@f&k<{A%V7f|>?168LCfydoSYvFRHWOF?QPaDWr zmNK+S0X(75^!=aX;a~zer<44OrtR7I9AKe0*!6mu-tHiC?>pMu7QaVbunDjCN60mP zV(Q`Pf3|{C>mt&X?Hq|lvXg1vd;g}eq59p^X{_OIpKI6_HZ6a%ccjg&)UeNGhW}s! zOcO|#ax*}I@LC}7XrV3Fa7~(QcnfK^lTCpF<+r25UfF9qyE%FQ?=ifW^&!i)B+Ire zx$WG_!LfHxkF-pO^vr_LuCof>3l#2fNGb>je|jDZQ_c#iCW(Y51quDQdmAR^%5CiD zUmSd4UCGL&td)%!);Hb6l5v@}-DE@7E+sqc%Ndw{ZO*x5WCW<5uj6z)ON&SS={%kN zI+*bAE_+T6HRPnc3h`Q(D2pZ45_{D;25<9*V4tri&4VDQu;8%})%a0YG531L{TROE zf7$JZd#zNYvCz|@Xfx3UxJx#n;5U&9XO)IJ<#drpdp;Gqoag5tKk@U5l(8yBnSq!k z@fok8+ZME-?GNa_^*6ZQx+H>GUQPpDr&_Fek~&3wt(|;S-|v zgPyTO;*2e>+kk=Q?w1Ui!HMp2dc@cY%q^@lTGZnl&0D; z4uW;F^DrG_;kMhFUf3fxu zvPT+9VbLPHu3xj!CG>V^g+_px&&v=K!^!1{9b$y%NT=!O8J0ZwylD#~uN!=wGjp~Frt^S0zdKXmb zFu9q*Bj)?ce1z$LjQdCzFhOA0e_x!+tP4tJE7BU>^+y=RrF8dpGhy=H^m5XW$Q!LE zJabYwdUIJJS0NR9lPO_;+$5xJS#CPOK-KYJTUUk|<-W2`hy(v~Jcx809|DIHg|_XV zx5}4|7&FG1f^&R{Bwn?j_64=pRQ;&i`1~@xOnuv7&%BtoKw1^Vf*klTe_@y|w7tN_ zWjA@1=S&|M%uAzT)HczEvv=~zwVm*3|Ky~9baeNX zb@(ai-GkA2Hq8q7)?N>%xiMM0EVMb|^vY~o;jk3nqQj)wCcNBYz<_rs2E6uvJib`X zH|2%7pk&@3D&?5!QckOjf9ao|e27KBBn~qU*;m79|5K=lhJ@Et=!0V%-oppqP|-3T zwmLd|K;^n{e)riy?-YLTyr}Eo|JWXbr8(dh>8Fm~yuD3HUkx%MU4+FA&frOEIvrk_ z6hp}63tl_X+AY5Ls-1B{Hlw^5c2kXgkDdMj4ACQ#;XU!DQDy>Lf5Tj?6FLv@TRHeW z9bV4UwxO4<_2i`Z1px+M3N;AZYr)5p_ysM*>Js3=LgbW+zhVlar-}9bJZJ3o$^Z7IiOZw%WpPuFU1nhEyL)3tN9qM(5 z53a-aRde=bo@XkCJ~XLFa&&H5tQ zvbxZ>u(^|5%jAT_oiKR-k0p6vGfb6@>34#EnF%-KVc1NsnQgx$#VACBw)1n)gWyzN z2+6CwjTPxpqrf?&7b~RFC)*)l;y)^pXH0 zjPL-F2&*J&$3e16Shu<^7hy%iMHcdg3+2N(!;8T1k^Qp*;F{5O5-VPg1nCA|9x8Kw zi;Gv4sf4W{uFh%nyn(ZWhYsXkQIxw^)wOp9Kmd~R7$V{02GIcx6rWQ~%S-IsSL9LK zqJ@1Jf9joYn19_(9lsi8PnfO=w)EgOc6k24H93s9330-3XjXYpX_h~jT;T(K)e^S$ z_2+InGS1__a$?EDmiV@3}R}~R zFktEgS(w!IozJpKPLDNFeW%j06Icx-?EpN8{NfP1aUawiWSB;RbOW-O9cc!y$g;vb zi=QwB5kZwK(~RDsI4^dWvl#%b((+!_ODuXvB3Q;*Y>kW921snCI*MZe0C}aHw%8)uC*b*<^I7#ASCvc0vWupG&hN9iOR<`-~3jm7{iIp{w4 z`V?YgF$R(>ouZtCP~)-i8~H*T{-M5wKG3F@q~Lz=8a6e~7z3u7E~^h?bc#PC`c+d< z*Q5Z}TopZ08AMX3J&~83SCwX>vr)Fv)%-$?L|d@%q!U$V zJI+Q%0$6J5|1yP7F!;hyel}O3Z?%+Cr>!e4uTBLt3aLJu@IoX z$u#~MS&F-Lz05FvK2ZNP6GmiB7vsIA@Q|Fn4KpQ}2Krd?G%efm_Sle9JzVte+kNH>od{T+aJ8R+d*Z0+f%&hq@K!4o^1DwZ4BV#|A{G~SlEe& zFN7;=vNTjK#0}P2QwD(f1-g1qk1ut1B__VB5J$*#Sz`hC;2v&9^$lxoTIaQix7@6Y zVhc#1#rJtWO9O}T00f5kY%u(VJP9K&^92a|FP-6F^VW=Mf2BxZqq*~yGm6BX-G{Or;9^0h%qm*!l zLD&ZH)M}EQe`oVzv(v)Pa0hV24X-=0i3DN<5a|#WZHfblc#tqmYSj@tn20QCt8(ixcANtYZG+Dw?0jF%n;^?{B!c;aIKQO7b+|Ve}Uq0^|xX=Bythz*Yjbw`5H?&5ySujMvC5n>c5oo(h5)>dwBn`FzIC8qy_UjO02 zK3A({>gK!tWdwMO_V?(6|13>m6}%h{q1yNoU;Va%!b-(NAD1P?>YOK&92txU8SLeZ zh})`4I2jrmlTQnJeUp~V0$cN zbQg1A`e8np0Mv^~N}gD}G6@#=GM$}uD>fKi+>z48ImKw=wk=0NZc*vz@h7OQh73O) zf9dVLq4#7VX@SP)k)#(GIDCSqlbtxhOYWLVob?`W zo$Aw3Jx?^=EbI8``n7^N=9{I1(G`2kf6=~vy7aHg&_SdVHYhG4?FPFwDByi=~AU2aDYT@zbqFX(tY z;J9f=%1Ue8lH>%ftxK6S#tRRlFTs9M71=pPXUb<{S?wYASzc&YaCR!+DRNG`lU#(` zEbIO*W99))ey6#nyG1S(EOn(zfBmtr)E_F^5px|W2Sl)emfNScU6!Eu!)xxT)T=Vk z!I1`nI?HEBs2x*?@^lz;31ZH9!X{>vIVYlLJPCJ7{-C`&7nOBhmU+G3f8WGhG~4k~6VYw2 zzw3q3tSXJ1)1Q*k=V~2g-V#wD>f;IebT+(|k7FcKOCy^jk}#?G#3Iz2M-=4Z!-#PSgb zh}GBV)1%(;asSKkf7D2RaB_O&l_EYpT)>|o0ee$Zq%X`G`7LyDT%2q>G^jtl+<2=LM?id4}r| z&e&SC<1Gek)kM!~b~*kbH~{zoOopWPd9!7UIclwat=secXc<9lPz7~^Ze&dvgS8og!&Ie~wLT~NOX+Eq505V!c#z!x!N zODnO9P)G_Y6Dc6N4&QK&%uqk&vz@^|DS7h+4xJ$ZO<+Xo=VwV)X?i^!~ zGfUt(Y7BFVfAfHX5Nw2H{j3R+p1-7$sWiWr6s+Gk!$^rO?7^=97RsGXQYvkXuje^b z!rN<1>{wwh!v8TTe)}y9S=CVJO`w}G3M&qVosGvSN*I0|Qvmtk+2ZgKX{@HAvBSYw zj#G{kmvEfNIboU=Wr!JbA#qu7C|Vs7h8inaIFL4ce_n>0bnR48Pssmy zIEy?QhO_X6rk5l31F>qTcSYJd`1I&YihuJxc*m&Zl3>GnA00g;@rM7h+$8j^_yJUm z5XVINp%cXoUCVJe$B#`-vXRH!4PBl;qz0QowZ!15d;`?7U;AKhF%AATNF2hJ29xk# zBW;5hf3senU)**2``WJC)mPn4n)1@Smv`NMNfIK=U(yx%M&#+s>A-Fn_Xdc+T8Fob z_KP0YuL{!m!8psOp!{MJjC;f>24_2#5igVsio0M!9swi?z(h(X$)mK;aq!jo;OBgX z8pF;!N109h_-i)L>`+u%+f6D^fYCV~)&K)ae{mWzeLe$0T!8psv!f!lfWn<5zmbEw zL#)sR6sA`wD25=MHTWSTNk&$gam#B5q2hAB%#)5*0!?uue?pu!3eNrhph%wvZ2))~ zGjU|K=}$~Z()po6a6CF{z(O4=o%BCE>^)={aLjP@@aPync#n_qm|5fD$u`+0>|AjU ze>c1j@<~2Rj`6JiRP$a>%|O)wO^Bd;czkehC~#g{2KV#VBFqXMy$d zLRjxlE>j!S8GM&mef3fQ)PnTNLP%dd%H|HJ-!MV2Iy&wherQ2^bs@AP;E$hdaB$*+ zNeGbOF^Z?(d`2Ltg_BQ@KCv2jQEDS#f9ci-4|~T}xtFBecrut_IV^^MQ3XeLMrvwd zDk}^+wig<~9{o0jW!x;N`do-;NCHA_ZUJtBYnWp|UK`f!PeBU-yU4*De_3Bm z)8B|`iBA&+HcOY25v0OfUlXq`Q4~~dJQ$`deiA$S6V(>wjy}znK&je2frLg2ZV92C zL4gv1rVr)m0Jhx$ZAFNB9^jM*5Kk%EGep29L_5PNikNQB=B{MJS z`3NL^sQsdPJ5UKo+DpWOPs={9kn>_mLfAP~je<|G0 zM$wKUw1`5j9omXlWRFgkKEGI&tcoO)?u(_$=^&ig;7u8{fvGyoDO`a# zgMa;vs93K348ZL+?nb!~f7zwOGfg2)ry%T`I@^|iJiD9nJ4pl?g!d+GcOAs(+@ftt z>DT~2Q9Bi6V5a=)r}=y^30IfrCT*|UXX$L1PKln}y3cmrQq`~oR^Q5jtM<7?BhvDN zr$}w*$uC;yhG|evlFcD>sb)Zv6i1ns8~{#-IKx?XX5Pl@f({fuf2h_6PFwXbxL{JH zcSP7Gc<;1C-3kZD>uWd`(nK$ZLPD#n@k$eBF%1Yew02t7O3DaPR8W${ymP2Hofn#Nabw6u70P6felXDy zlD-({wa{tcn0TmAf0|s|-2PSBj{YI)~3Kdc4^!hzIiR2=YmAJ4q z4_A6DCMgK$gPQqBGv8huaf1m7Xs`Rf1UF6CX*`Z4*v!hLS{ScFUqzOBt7lM2+1oVG_@{Sg{Z-tDJ-OxqERWH z!9D3fU4=44`vT8VAG#ElkHbVJMqoTKxjUKnmpnvAz764`WriopDpH_Yf$KUzZ12j z`r*w8v-?dc&Job9iJK)?B*L!Qid0VGMR~mR|C&iTR=dmTJRi&-C17ik&)WV`qf_w( z|18nTe}4|nN_K&tlUG&8BLU1~+@kx-$;8>&%1r;3&WwWDY=iGuK=Bokf6^v}UcwM^xfCyVRnL{NkA-Otrt&5-X%end1_xU~cDJsQP9Tw^rw2yAAd85m)98rk5*=5-=B?}0Kyvlt%IWP~KS4icyMU*id* z=;{PCWjDXcQVc)j>91$b;~A@P=b3YLqWH2P`#-O3)uG#8x`$}F1|;?yk7}*GDjcIo zG=Hr9r-#S2C?1je`W=IO1dGb(VOjYyg(KlzvavhXf74^<_<%Tt_nf*7hN7&utEAh? zAmUqF$u+H?x969a_Y(F9&}&c0vIbIe%2j4e&)9#%c2l%fL#X~+P)}L(ZDfRj%-Y5` z_oN|RfT;lJLLuN?;3Kp@Wy41ot+OlCA%F7=UbU;FCxPwwN#V&0QpX% z#;7IAU>?^iHz0jcE(94s=s)aQrCdBEuEvnYF!-L`ms`{Bo2MYE+%GRl$aZ zrm-XmN*pu}1>4-P%!!K@{JltGv4~^i;RFp;6PhPColIa8n8a#h>(T{T6hEen^?xaY ztgY{mHfSOv)S>jBm8k?x#UB&N`b2_e^mj-jIE|YYAZYymm^L;*8~8BdE{Ovta?{i? zRrH+WF@0=+KIo~-T@naQ<)$fw6jOX@^O!nTr4BIpoZXIMit}{4is@s@Ser6#NioHl zx+BH(F_ElIB)6lO;zZrDV)~di)_0m zRq;(Ash)=dIOdkTpi3snZgs6^(+_5;^R;-(S0pHhYaNkGI`d$S$mFhUVYq=&LD{ zCO*523nHxVA_iBV_GhchmAM58ize4Y)^(YA$`20sX~?!EFY^2wOBy1)$}eX^h2Rhz zzlZq_g(YDz0P^kEY?NYAF@HK80k3LzI&Hk3V>vM)jsPy5(Dm^sMNbeoh0~F)h0pGX zscJ*+FXtT(1G=A(i!T|J*sT|J(x9xf z0CFAsIHQR{QCP5-B^Eyj#CS}}q3+Cbl7fKPmEPEOuHp!!Dmt83q-6&Cy$WW1ao9ga zF&xhQ@wm6&J3jf5GhJUS(r8fV`AA7ZToP8wIDYBUk@12fh8E&x&M7M9h~)fdH!4-D zSh~-^+|2lkFwfiCX@9_8>L<*tx?oGxH~~4(f=CI28T-kYOa{d1XrXcPD7&B^U#75L zO+Z$OXGHaY)PtgxEIl{^?-)3;zHFs?AS8 z=De+uc*hx-&t@PPf4A2nlj0~-9xm`P&QYr(YqXfH(bKF21%C)&WO@8~b_bGJDWV5o zUSs&pre`KdVHvU|QRz>l&t+3b^&^AMSqfxE16pVsr2#n%p?<#ps``zzE-kv%8&uM7 zoS9r;=VfOp^}Rol3a{N|S!lGnHcO5y&>pFFtCZ3WPFgoGu#NmLhF5oOJ>{&Zx`Azz z?r^;%`nW-UD1SLrMH@&|lccB&g_N{Xc(wG0!YBz0CLo0TIud#g1~>6oCw^2L9&2_sLp;m7=N|7x~s8rnr$4&DM^2tY45PE zU7$@~YR@YiEQjpkmE~k9Z<9);Fr|e{20)kTCVtG$&gs$q(VMDI@)M^Im`#xogzmn? zt6Wiu^Qyf9W3;CXwwAQ-VCB~0SOvmyFwxR8(;HRs(T2{Wh!A+^_LG*r9ATD>P-kpS z{ggqp4}b3gu1YlL&{05TB?I=QZJJYu*QIe#k?gOY|TAUnQ8dt^Yd%db{o9_4)h z*_vBg4Ee<>SU2KGIT`z8w-KutyYq~+~#8I zgnu%c?Jt__4FsUKJ*dKKs_gSg=~k)ga~bj)JjkZ-(|ZW;(_`hovGTv8m4Bbegl*lt zl{Ww|k!xuGo7jTHG4KwbLt6@A)^caz@pDYWh6?0qo0Z1?$!ERO2Ok|ALsVnd0SQ{E z6RIu`MJH0{!E8`Gid3pmqFLnfV;LiKmw%NfMw@$@{&rQwwHF+%g+7gcIpTk%rAM~6 zhP7GFGhBYr1lq|2&O{BQ!8xYNmO=Q65AvlSNsLFJ4-EO4?cCF_*5QtisyOuL%V!DD6}JciGf=^&T|OF;dS5+T(~vTIc@DHT_} z=&ZD8MKv!6RQ-L~3VI96!zq@9LVxmfl}D2g?qzMSh%_HR5lBv+EZRPbOD(@u6<|9s z>DyhM48Q#5ytOv4(#qW6O1s*+xy}-(cTF4LN|B(yf&CQC@eM9I%ZNoP4+nMj_nVIf z#Ys9Xay)*(goMB7ITRt^q6av_p6l=00t1?y!cs?fbGuEb4r* zRog1km^5kzrdY7E_17k!&VNO8x!IfAR_ZcKE^F)WDpkegWF;WgGGeTEA+$nMJ(Hp8>2&`50ThNl;_qmG;VymwXpwoNw-Q)yE46Llk)JV17p~ z8%#D|G&Eb-u{;F_CMq`&44F595L6BPmEZY%UgFv=rx5hiXZ;7Ce1Gv-(2mf= zpm+S?ks8b`qe0IsQ*;IaS`ruT&|SMhDHc)|I4`OqaJKB0@R2$egdB|b{lx{Ya2N11 z9qnAw2X=-754pQ5sS#BqxUu^E)I$41-|moocx2{0lo7OB;8FjPV*N<$X8MblI8IH} zsagNdFN-5Tye+OiE{@mb{sSY-o?BVC6C!ouC2UF&{7$d@#%FcNtH8 zNLaI^G1j~Jbmt6frz7}j5SwM`%#s8$io9$|kGgi_ynd`~JlT=kTCV&kpNxo_CFmCX zV(UONOoEvN{K9mxPfHBgxD}>vIl1L7HArSKX4LI%lN-03G=E5BKuctk>hK#|)Ljrr zUX|S7-gjYOiRp}yc!1ze3T}IXXPGO!Sm^|J%qde=5|Tln@+DyVh33O!?v)xclhS_w>sCJX(IMKfz)F9DZz&Zhk?!=ANO`Ll2pAj#0$wy4@prMML{7f7tK zds0y}?0D%45@w2*=qGZG?kE5B`zaY-Ke4iB=+!n@UDot2CC_jNtdR{7hgr;gPJXNf_IuFJi26IkM!gg5&I2Shof zNFvqLhoqzg(~;u+j4!69im^Pw|4~p{8VrS_n3_lBI>I3e&m7=j5pE3qHGjhROfVerRu{lg>UT?c?i} zM{Mh3wOXxKtJP{{FD@mlfk)9q;oNONi5D#@SG@~6z6T^#>w=s zIL^|!4lHvLN4*uP;QJ3LI9!XoH*^HF6wyl6Lx1?md*Y;b{@9_+@j_DJzfBeR0RnB_ zwr@k#ZfGxIWd<|bx9L{e6Jw0Egq2AOH>Z{tnE9fZlX}^;Pe(` z4gyK4?odNP%LB3OSK>+6_Bt4$9;>+82fW&~V0sQwTMnM3i|u{6^;ib=d1w`jtwFpq z4}Z2Qj~h_)*Z82?@6MpM9MhV>Vw!gYs=uY%ag_Vv%Je$@ZD@v>*r4)|hIJG`|1@6c zMeD?8Y=6dustSU)0#!?s0I)7))3F)InHmx|zH1d?+YR@}s=}`CU17w7l zZbh1xN?M%Rl2;dqp9ALz?De>Nh{tOC9k72lDjB%l7}7}mx2PET_};oeHHciqMYv8t zon1{c>}4vil~LPl2@aEdr-29ev*(9>+f)GF;$yg)17`I1F|+cqih;Vpftp?}8)&%FoQz+Vk)mLo2yYYlt8 zKbQg|gWn}}W#p1wnUBkc#qZad`kv*Lfa zOV+Q}Yoq6Xeq*A$O%>g|Q9sW{jp{jPAI&`N9Fc5eUUBUyZEPWGKT*S6Yr7b$IGEYG zJzHmMvzRx+XvDaSAq)t!&VOj3s6~umJ&#E32E%q%_V?S4O;8<%>h4%zW`Kc`z$$WB zC@-;)IAPj3zb&?#t!iBU?pC#cCGKxWYlX3z(PCD#yBN`!&>=S}dCZrzp*h?8CK11- z>1=yp9+%|zFvPj$DxcG8FIsGCvr}Mhds`X6Z1+M#U104B_<)BjYJaNpt)X!m+|^_! z7{}ltM$L$OSO>Km%ygd9*6|5)ANv!GmeVP|ueiuR=8IJVjQo7L5S0=2xlbmQ_w0@A zvy;R(=`cX`&BBzpUlyb{*>5+2%}E{eMZ|6{m5pJ-ypDwNg8EtDI-?sQrJg`4?^+iY^j z+r}I3SiSMwO;uiM9Kdi_cNX0uyk8#;hes#J@?hi?uVxjWW*iJx7jl*C_=|-$ z2XWeAcVH6`*0JevVd z&(kt{fTEO>Va82H=7BAma92w6-hS~4@p>T`$V~p>4{T*#6?eQv(Pq)+OVa3v1;W-6 za#}wm6Fg*#Gfw)N3RZXNA(nc z@jI08>lt+)yQ-a6wZb3w#2Z5l@M3<=Ne_4e41eQp1Et#T>uiyt2HNIa4bvK90yYT+ z4w$O)pnqcEQ3fn*MF58CDn^Mpgv)Nia-_)QA2M4LClyG6Mk#WUEk0%V(k%Dd>H+XU z!i;4?s1FG+V%PAQ+e$~KZcvW*S}jicxtIp10y_a>bqqx&^&y5iJD5+>RW>gh5Mlnz z3n2N5w?~{OM~I@0b#82-jzv={%f(3mGEL`n(72>x(DQ7L>6h0Kq~T^o7O40uI)=Hgrox~3({TWMrd9ta@nMqUEvbY zM1RlA6iXrD%4|ltla(;Vv_|2?z<$ogJ5oBzd&=B@7`!nupz1}6EaY8gH;all4inh- zr5KW^^q1j%ts=qYJI8~w6T0KOzyAh5L8Q$g9{t<~j-Ahe@k)#yn|S)azx*sqjG#f^ z;lBf#=ovIT&OS+R?Hpj4BM9vYm?7LjJbyTUb$FV*}4gS0~WllTKg6$JF(q#@(NSaBo6-ElI zt9R(on-mzYSd<9IPFm^*sKPm5HUYJkB+HzZo|1XunX%Of$q|U3S&3o67xugT zi|i8U;AvgR;+Aaeh9PZWE<+OND1YNL4Svtu{WUiIDP7$ZIVAu5{?t5$P4^Pwh-DxI zEC(a!0f;D5TvNcy%M4HlR16yX7V_A9N#(O4h6Ql79@#)QC z$**ugW--6Y(*&Rz*u+X0HE3qem|y49ugu}BgbFx(xEQXmB#$!l=p?g`et&$(vKv!E z?lmA-QBf9Q8A+@PJHtAH_a`=TK_E%i3Xx}g1(}6tKCfn8HoMA!W9KYi2-^#|KnM&} z!V?k2WYi%Vbr1+jCTfd;mOy*!cOd4C3q0`Sn`B%kIpD^z885xVy7J|j!l+sKh2GN#rFp$-dr z@sH%m!9TLAk2$XTzfa4XyibUi2vO@GfMIc!&L+z%92|U>;hy6{^WT>?Dg-`{G_?~o z^%~K2@l#QZDjg1H8rbG1WD+xjk3~Lmhsu|r3>WhmWMY0_O>dCHi+_9qe2{`*U}Dnb zXDf8J%1T^OXQ(bg-ikQv1^@yJvi#j3cwy~kP#>VDbd%TPpm5Vedl*;q@7J~OeqBZF zhl-;*WSPC%c1q8`{(0|aZkr#(w;3DVO*|Aj`pa=N*#e9r->IeC37mGlQ%kuMxQcKD z&rec-mU8PHK3%F+S%0+-ODRpMmvk(kwG=HG8VTx2dW_Z_bA!@Lle-H3bs9!qwE!-P zViK7dN9Deh@49<@zvaX;=#WKpu)lxu`pv7OAxO54#Z1FCDN2jN#VACevH3{5VY6I; zv&9|0?DjTO3S1=TZ0>m_aMv- zlxa_xH&A+O!k|5HGhq~wpj~XWHDPRP_u&_uS!24o(2^|n21U)mfQ>~}a5Wf4?F8n3!lIjXdiLD#MECjD8vRq~?L&K{`YE%}uHhMAew|XQTu`H`k z2PeQPU6yph5q}rJNn#$Fj=GGA2hPI|-=>TV7iE8Cx|$!X6$DjmJw5ZV4*^DXkuuh4 zW49Vw2Hf#IdLmDBpn5K z0DiwA(^_;4Ju7={1Y={UmUY%%s-bKurdO7EmQXD{N`G6!B|#HVmIyIVFv?L*5srSd zSFf(!(TY3GW%dG)KV^uK69IqM&hjd`qr zsG?j(Jvz|(+zY`L>j0JWhnVriPa)0jRe!m>=tAm`I{i+kw`t|zncFeFU75Z9 zZ4C_1kn*EB)vR9>x-x))&&^}&4h%Uc6ZXhOj=uHgrM?)1_JeE|fAK-v^A2#?=BJs~ zovZ191dx?*eH!^Fl@*UVrg+GcKztuWF^3lN63!3f$Mi5J9*P{BEWMY(3;2f_dn# z+&Lej9Yso+KLc&@PT#H{xBcdFSLk@qs_Zu({eo1E{?a!YP3t#mYq6abN!zVc&h(@% zw}_r|*BLy(nvcD^MYMj&Ra&$fMMIRh#WF_drEa)XQ*C^|vQqtM7y!}F$G7n%1VmZyw6c~iGg;VE3JAVs( zj3TcT;ZosDco(bp*LP3pG7U4U0ujO3?XBry(z# z4mVq;Z11~%)o=R6e$w~tjz0dEPUB>pd1zIK=r*l{u``OWvir-#Y&yPAoL^oyi7s2P zcvBb2ML|4~U*WKS75g%lZJub}q1 zHp;cp(qY1Z*O6%~W(O-+E7fvP+X2K!PMtWdpOm0TM|L5o8)mXqqR zrBG7`t?`L_Er6`}=zk~b!C?;!)MKdzDZpYF@feE3GwHs~B6-)vTrO-ueRl0A#>CDV z%v^{N3F=OG9P`)Qk7lI{J|!cFY7~?f?6fUqM@3c=xSR*EDi z$Inj&rw7BMgTv(2;fpi7Xeo3Q-@|F~A>(~%*!i%=kZrtY#R9EK&%$+(cUfA4HbxD- z=asP5`*72x!+#3qFv&L0c}WBCTC&rR7J0h4$TPQyx!s5kkxe)xw9`Fu(A(!V7aDFZ z^|V!~XHv??kjcpG62x%}(QoD))3bx55aZE$Sg8>eX0TX*9>V7LX$rojb5KQ!Y4@q9 zNenLJxXrZn>xcZeu|Pk%vu|Qsh0`}rY3T}=L@mwbRDWbPgbw4oRqwPMYA^V!xxnt> z3~xIge&mz-=7I@X7B#_Dgt!Ug=s{ovQQ@7BvROXP)#R^G)H(6BcNb@@hX;5S2izb{ z?jI|(TvT)|aTdJzqTXtmz>X3BKMhc)`jq+9>_siAu{&B?K#^}3*w8zx<7;wWL@|Ze z!ypKUkFOsx!Q znB8#~Zb+FoK4hz8%@CJsMz{<|7-kP;XS0tz~=vgUk{PxN?xGFVoti z%pXT}WNoV9{eE|h>@gA+TzfWn)Fq^?$iti4R~eQ;)|P9axo^N&Ca9nhm-3 zGyG#Rf}x{F_oiX!*dYa$cHAh!Z@Ek-dqQgFBeEErnEK!eIJbfR7wiwl_mUqqV4ocR z@o@kA3|zj}ma>;J*Op;7DJ+5{`zx?^ydW*aSnL>XSljEM##DIF#ZSuDo1jlQp}GtD z%zq30sye=!P-(X}Gbhh~cesCMDXI4?OVsX{6BccQSFd=d$}vSkrCMuY;Srjh%7t z@Jg3|BiL@bSF8OV%0N~^)80<}a-%2>g?+1#k#INUm1-uSw0W1arZP&r`0B(biN$Uv zQJ;GMY&S%}7OPxPjG;flF@*yS@DuV&d=H7FC*^#HY5xTd8Km(fy^;`^phyD#0Dl9z z;oTwavz34+ne~oJ(JdsQw?+hxz6sCgm~34@y~-*uMA5!WO6v%w{(Tu;BDEf~rfWN7 zBX$E?Rfl?sz9b@^V&tmgs07s4xI1RUX?y6EX^j1FTdJ&TG8EozcB)YK_Bvwh{0NIY z4oF*KZ&*OGv|P}APp-x`nq0`8B!6b8rv^$tC?TY?Uf79+?^A{y4{erWAcIJS>1rKP z-Ic+R!XPd(pk}tKb)V1;J}K&&^Q2Cqg`ZO4oS(SI7w)w`bWT4Ux;Ac_596we|{TxdXt9mp{OP6ZlOD+6CeerzlYS~KRXaTuffqx=*<}s;d zQB_KLuM}fX-%-u>%d_TjtXX%|J57_Pr))H!WOF_-VoOn=i|-NQ=R*;UPg zOj7bwJxR^aqyaAB3q-dV;%N)fcMk^9*UROoIh4+Y9yytmtN_A1{>x7;#5!v@K59T= zs%AGr0i_lEt}POfnRzcxY-3ym-lqA2&oy+H3G6R^og?G?bJ65Z`g^s#jT^^JJpuV} zkG|NujB;WlAxGvJ+<($9Gd@6_1I#&Zxa(9;XrBDk9X_NErT%))pHAA5*!*<*B{(&9 zt6@gC#E#C&ot87^VQjh(#ONF#tkQg-@YNj}a-wS%MR zko4?$B-ruF>szhtFD<^1B&t@~LXYP9>N|}`eD#w%h0ovgs(*lT&joDKU>%rER^qxC zG>4tIIUZ){to|xcgpR`LJV{oOL%` zPw9JO3MU+22Y)!KCt|BzHx<^};dlM2-&`F@px0(`I~^&$eF(J=#rCf1hl*9KamuIo z7H)%(u)yl`I7%c(pk2TC_PfrK+C=hD_wSQ$$Mfwtp1Hlo%}*Xbu4%d+aeteofJ*h#3`v4qH?!^r9Di5L zxR>H`)VxvF(PCiwI5@#8n<%XN8pWAW-n3nhi*oT2TBF}~*pSG5OURLSQ@`qa2iGP# zha>+LS?U)&T{iLot-ilXWX4?)C6=S5<@S)!}qB?@UZciMu9ZJMIZtLCKe zJ~;*}bA&2PnRx|tqs>NBHlOxmNKMmK<&kv_A%BWd3?32sWk!HYtd^@RBr`4=Dd)>s zKn=?`)$~gAdaRlu>la;of$4^?&;mpcA9bdmMqW@BvE?-GLi43koS|AFxcM&y6g~fD zxuU2*ysOFKi=gg3Ywwg98t2Q=fn;BMc0twn+#<4e_<9-LU47Ji{Bh(3u_lI3xb(M(; z)*gZmgrH*lY9gQ6_*P7iWM;K|9B_ zcW`7GQ(Rgxg_Xp_`fg~EPkjD(nNRYC1n^_`FuOX@x`B5GW+UJDofvA}q69cZZYzk# zH9nkl1Y=u0hL%H^PT6Po5TFGlyC%WyCpxmhBW>?Y&i zH4FU3=lhxQoz9FgazZ#S_y@7cUI-b|+lJa+KSvNye1@nLZ#PjK_BKc2s6-7j^QpgZ zpp@gohimmx-a|;UXn^aQ-Wva+ZGU`=WTO&|YQ?(}Ez|tEgkgR4yQ2cN-t`>aoEO(C zZ@BM*N=$%_bgB`veV{Jx%ul?&MgY0l8wIitjgWU;eSJ_J!{@=x&C#Yth+e60-!)IX z%tv@RVm|$DW(SUY%jpr>&GA)*5?8^)<8#B$o3T<#hfh<8-oSCiOQHjXh<^v;&6CHj z^Kls2knNKG+Xb#ay4t%hBvKNamGCJBIx9nf6rXSV9kd+}K-Y=BrHQ!FKYxz>wM!+N zSRR^KVM}EQn)(`Eymu;52A7jD#rO+avu|36P%s}vo8@r}+kHqG_iL9xfXV%7ylLd$ zXpb@Dek~&e>aoXzL`e?GIDbv)0>hb(D2!DbBM@%;mw4BwGpl7ossz;M-EUp_u znR$i|{RsvT^a8U9I(=;wan|T()S?&aY^RxUk3EsGX&}0;TX#Qj|9|Eb{HCG6o0|=Y zw&y~R)#j-hH0#0AB9x_UNE`gpIfF0dfUB^_4jVYpo^*AM{la}TJPeYyC%H9~S_XUh z!@=?S8-AsXqP+t$WXew=ElbI+eL07ji*SlLoqyyb`~j;7Jm^C~=a2AGP+4F!crq_& z9_!`f92<9UFHj5q8-Hs1*xdOIr=<{&9a?xPCVwM`XCng~^3>qg66&)Q zlI!m`X<5B`1W*;Uk0{^b^5zZ=X)ZtTehg1NTAh_T)my}X{s%x9*o|KyphsrYNrpwD+wn;tTL8(xkt=G~*L z_lJJ~qv*lW@Xg>1g!3tQBR|jQi))Fbdom--E2Z%w$$veTnD2=lIlT3B=)R)Pz&!5PTzD6ae{R zxnSq)(w?yA7@0zE*JR)3C2H$mwUU1lu=oM}HT3s{1+fOqu*cFPM$ZqtAc86P?jbS@v&Qw*cc@jtSoN&ZeAV&)ie}y5Dsya ziB*hIbt*$c`rKRGs zeN=XX4U$K2V=H@rEu%1CZ;n*+!f?q>(-ii+xSaM{!KQ+p2F7q)K3L$Zs`}2Ypr;x*&^fPd}Xi9@%cJn_a7zqgf6#RUX3qPy6DD&rbC++XJGBReys-X?FyLDIqvc-yWnSM7qiV5oAQ6JfwJj z!|UbZ04^xPB{o_@qA)~PqdYJ%Kw;asDxYiGAWy3En=jME%Y5-=0Kds67|}N!wFghq zegoJ$L0k`#3VmjnU|HINu2@?8jUeq(2n44BT?{BO86_D(8ghg0IqcK>;mh;0gMX8^ z$IU}!dRxOh8=RhJ6Njrm8wN4Xm)M{*bR_eIx*ndv2j47+5RvG4st&4h1%W)N9^ZnI4RKR)c=;A)=JUu)z96?e(<`Y8R zCG*X&93v6<4U#W;&>ps=ibJf1?)JQjHEdiM?N_|-^?;A}(F2TWoQK4RXn&P07LMu+ zJg6}&czTN2SlwXQ8e2Nv`JYe==HqA#JK*#67nrFihFVNix+(%_Sf_vKBs%@`3<$d> z4XM>HQNM`TD;0d|6U}OVej^yJzNwiSts}MR##>blAG&z*Azk&?b}-(vzKjuNb9o-g zgdx8l!UAGp>ip%rX4_)bP=9UHBgVzP?LBXu;-<#O$rH9A#O;X9Ed;OuQeZv`X2U|v zF4rj?UpZ<(cxa-WK@&y&%IS`WM^u_*Ux5ErA80cR@^Qd#!ak694<7YzMEHgUe0B}q z>gqRWk`M73Zr=pC`eIw(bbhAQg94AcJ?)~Z>U<84K`@eofwh&%}hyKWoair2j=OlAv;c^}d znO5CtGKLe!Z=puij2@<#-&@H@?K~s&d3LEI3v}{JXdSf)Nonm=jCR`^v*#@iEtZW_^)4A^H!&b z>-;3So2E%P4Sdf9?=@^!=$NWNM{PzPjcP9TbveW0$&afUFp>s1^?~Y7br$tb-J4q@ zZ1uEPi;4W#M}G~aN`3kA>W7Pn*7EbBtp=5Xb+O^NT00vZ6wnmuc}>_)H=lSC?Dq$Q zvjI9IVR#$%x8?;S>>Ni{Kh1_W>8DvoF$uf%hwSp1zsl1a&KRjtkCkl)f_lAXpifC- z;rv=o@-5`mKqcSCi>@L?|FIser;1iw5CF&t7%-53oPX4A4NBPg5RexY&>wx*fuOF^ zYnN*3{7*o${ngk0!{E^l*d91(35zAI8(_4X&jz^{*&rYi+I3!L=%wn=h!f)EiH<&3 zhz#7yJPKQ4I4x$0faU#6*xs!lt|%<F zbtF#bXgtgpO+Gz4RQ*^R46Q?~h&BrZ=S*-+5f$hPG=>ZkEb;IJ8YV2+E zk6p&d0Oi3q=>!%Fy0Kki<7Tu17fXKqfC8`a3z*<1mX>s<5AUKpvbzfnhR(O)%WQ!X zvg~8O;dE85U0h@k5j=-CE%~kigG8CiZGZAnm`0E00_-O$CHj%acuh9S$%ifF8faeN zTJmm#K5x^Rz;EaIXNoG_L5wM0JNB2X2@Hv<{G>y}Va~GY4c6}b#1vy4LF>-Ss1JY< z=}DMNlqn3!ukDBCngi30?7I@k9TbENX&1l;%CBtTYtHw>6EMEb#IanhZ9o!7^M7%f z?=4ji+tv_==}ceB0Bo{v`MjJjHZAb!Q$&atOW*5U>v8%dFFE|bjJ-txDWMF9=j~Dg zDX=Vk;>aGfG~F_@Do?L(@`VHu(3A|Rl6sd?J zyW}Oe0sNtixPsP6ha(b2*uIn(*5#tejl5KJa* zYTgqUz5(vczkOc;PwCmeOoP*C2L$oN$dUJ|Si*R5w0w!|?=v?e!Mx3f-TZu%-#QiQ zV?O0oBY+W`T>j*qzAQfBm7+dPk)Q%H>|m0orB@T#irVK>Un)SYa>rR}HGd$+t+Gvo zi;oKwjZ~b!0>!Z?d}z=>Mo;LTQpmbTxLWV(co}@aVkkqSEhsesY7M6cS65ekds_^? z^1HT|xa`P_aCBq)SHJpIV%vDCZQ$u@yCxnjm(t2|+1!ZFC#BO2&Ai!_-mK&fI@~%e zrWu-La}Gd)6Dli7dXPwDu7AOGJi{w%*~lZ@PbwDU^&$nLf=qtX>i-Rr!DDlDA z2C&@cC3y)LfH+!SUia-iG+m)={MKa|ZG_yXv6x02@RnF!{uAkpeSIjO;*~a@QB@C~ zAHFy_J;cez^z|0s6jlt3pi*0KeWArM(JoP3Q@m^KIfnr#-I)vsD}P=r(?%Hz1=54H zf5%{pKV{}&WS}A!={(Tqj-zXmFCw}ni05Li_=;{sFVKR2*z6&DiHSZY*s*p5$Gluk z=eq``D>?Sd`hQf0?<26l_GCSEC6r5uakK1^=6#m+A?(^V%SfYm#jv%C{WO+h!X;s>A zjiU^nyAyypT6sDjHS9CBcA3GkD#EQG}C?$N44RIU@Y+%z7#_ zOIzM{Zdq6Z#z3dkWWu_?^JF4mB3lU}7YAf4#tp6S8WWjZs<`c1jgDZYQ&_tKretF9 z>sIn!Sc$U@mw#!?qFPM%Kgy|L2 zNH>_z(-qWwoqnjQe9X)IVj?epAu=m(x!2IgJRgcV$bUC)t>#dqDe%?;D%hYmDg1}6 zMX)#mZi8SZAmotV*&}9LhE=JYD>6I_&s>8dL%a)$7VsPdZ&Oi)*v5dL%&dRnE#e(7 z;om;Fb!`I4Gntz*wcE~0N1GMSRwY$uBh3L4Mw`|$8fY1CozN1vS=!<~O^oX^ z3r}gE)_;MeeB|ENj0j@L(9bcsaPU?P#uax<|{iie2oQ) z8*=0+u_P5NZKPm90oRT4CZixqlT}k+Uch`LuORMRc&Ty%BZPjZe&wL{_wk$)cNJfiBz^5e3gnUi=Nt@{2fBna#dw^_KLU${yoRGGvWWH^j!bm}GO;9%gk##*a7-X? z>>Q=&Ng8{JQyU{M=~K~F)FmfT!YeQaqHwY~sIeGrb)^ioE}lzXU?<8UzoZy+K5@}p z1*%!(!n7f-lb%zghL!?n@`%WX(0_-%_Y$EN#%yY)MJx<3uTI9!4Pqh#Zd6#Cvg{IL4=P7XD4vTKglnP8`@T8v#S)XaTh8%aV41C*!$;) z?|(ml=tg_QP!eSL`Jws!;L+EQe|tK3{pQg(yk;H1T0JW>jHjpwShuY=sDJqTKsuDx z;oF+N?St!lOy@aXi_~qGy$nYxCGRD85m$OK&yXm$fVMW_gp$peJSI+_GwS{iFrP=t7gM+ z?${GMvUys$=++^Xp^wHn7Cgj2a*z@&tVRkQ*>23TKcC}i-G~w8u|lS zh#H%D0q_`Ic_SKUFAb}@I*X7ZfRyB%K#2{CmD6F>TOSPuME;}!Dao*9H@PDDaHDiN z&*}U{_YVYxo7I*yx?VP)7jsxhtCb2a)Vog&Qf+>P0I-i9dw(_R6?j<;J#AunnB0Nf z>RM<+n7K#6!tNhHQ7CE3clLLlkx2Jl76F>@ys$IG6cxnC@$|n{vx_yN>TKA8e<;G% ztVN-X+Vf(ZwEqTuuR6!tr1>*87vkcn0ci3VSC%G7gh~4w8~l=y5v1C@a%U> zoe`OBliJ9DKYy>@s6B{rSl{7@FQrntZTwd#nf`W9lfG9-zgI}VS4h8CNdLv&u{JkS z!#$zt@&!S(as4mR3RGe}(kFr;yGKeC&Dx90eSNeBZ?(z&d|sF3*L4fG@Y?vH|{- z|9qYk$aXQSQGJtz#8cSAZy9@yN9hN2?d3Ke^2_Vau`bG?JC5Fly~uLnMU>WWC9py_J?A+MIuhy_*st>2L5X6L#6QY5|`#gX&xo(by>S_aJ#CNzRm@X zI!FOfWCOUjQ76e5KWRpcMDAEQab4mblM-OA;)asUih_TinQD4-gBls&9z_Ar$5qP$ z1y;p0$9p_>ao6KvCQE~z#RWqin!UT^6{OX;R2MR}ph@j0q=M6;u+;q=8HE(uiig9u zd=>M3IpVbslDlO8GxxfKQIerO5O$)#!xNaUa#{^%G8g*nM*Bx%$x>1{uUbIDi*oO- zS=~hr4vT+_oL9kJZ#{NsU+I|#3HIL)j)eRald6jF(Vvj!& zYcYCV`c@`&GsTCbp{Rqq-El@{5ApQ&Us5Tpmfn9|@Qll^>cv(C=cgO|%`X8$?HIZR zfl;Lm{~)TU&`_N>`41rRWkoiC25yRJD~<=F!EzYlML3_pp>sN^{gjOdRfoML`>`wr zJ~S-O+3_#auJdd>EOPQ)QBODhT#nTBvwdL4*`SzKGOV%vH8O-4{fRdie}u{bq_0Sy zRpNj6_>T*iqipCPbm4uFGdY{+Ca$7B7!2{L?3&lG&ikMD75JAmxCf*VzCv3EE5c_=Nnkm`qr|Hpu+4+5tu4zJcfPsb@g0_V zGfSw8a7PO&H^fHbuhGQe>?QZOjxfW`&c{$*Jh#_yrBt))In5XyZ3Mp;HL)n~x&r{@ zdE;7Y&uYwqH12vD_Of%5j-DacgOG%YC`bbMPem{k^TZr(AY+baBm;kjDJw>?Y@dH; zIl#$9j*^Xno)?Mz!u6*n=C*61mO{wlOT6Mj6hw=j@#@LyH+rgx2$$LY7`h~rp;ovd z_H$nR9gbKUpgnF-CGN>x#Rj_VO@7MhBr3>OxhgE4(bIbfiL)Mxj_h78vrg$oWRLUA z`8OYOGF^hgX#C{O2UUCZJG?5(Dj$CZuMMPvo(h-PJx;UH6TBmul;sFFy&p5OY@J_hgL=_cq3QUz?yFlba4pp!d)}thC$xVB5r)s_ zQekY-&9@(zM`}Z{C%9Zra0rIdX)yL9FnVSmCR& z!q?^{p%1IS9)s~ltnkfP;ajo7?XBf-*pATwCMR0@3S>WwlK3$)v!^=5`CX5_(1p!| zxFgq8J>)lxeEO=ZSmyf3^z5rLO^C#0aRO>>J z@m*y(H7)`m(Ana(c3S~>O`Djj#<#LXpp6(yXj!6bQREW2tpl*0<&ic{|Ebz#NpH!h z>Ja}UXAdeCpL7Owk&VHY$y(0{?*@iwHX}p%1O&9WliXx{nONOW_8EV8OSU;Ee-f}c zy)divQ3sF=9|f>s+jWAe?BfD?%PzKL0P$lnE=Sk$b~I2gN!?*LdiK-zz4T45!XQJ| zu7O3+hP1_eATPfAs-_X8GOXd>4Nkp@4zh+$uwg$FCfhcvMs)Wj6U#SLGbAD$s_327qx$yiql3I>ilxKG0Gxpfmh9qxg63#k7CSm3=Xr1n>1K&8FVX8t)$IEmh)K z+9xIF=A;cl#^qX7wBs&Uf>wi6XU-klJ8GV$Z;Z~8=VyNwT03pNcW5U4>wGrnA#|dY zUZWIPPW5aw_623M#%{Es*Fg^t@K$TyLcN0(05Bu zxHEDD<8V>yvc@akB%&uh-T(1N<5Rb`njZH@;7@ zwT|4O)M#9R@C`?J3Bu*!t^JPm*Xxerb^+2a2E9Z2KmzVHCNv+GvXAjkp9rBLauzX*>Ykp=w0ou}inoZw7%A%U=X z`f~)x*UJK#ArQ6NoPrU%!VkygB(T{e4zh{UXA*LU3jRIfCTTD}YVl1x%V`O)LC zj)?e-$vHHld5bCiI9R4?1J_cN7qx-mc&UG!TQe`;M{k%E`r#O%E$$QRKCLdfQ&@5C zk(<|;F)jGu+W>ePa1SV1NPH`dL?zoEN zU6dv=q3_Efh25%*Z;1C7-A%l%^|cD$pw`!2OwOCQv7=5HkD3(;DeRgwA5}R<5|@8L zON3H4ipJ5yhbIS}Y!yTlj`n0u-LRE@d}lw3lb)<$vu}oO3Xh6FozCWs7PFa8OooVW z_^66I;q`WC`a*Ig_js8&W%o&J-)5kQBFVW=J%R+c+v%S^VfIZ_&%ty#ZCGcH)|bGs)@38Xuz6<+=iFE#zTK9`WSPXbq+{n2J{T<$*BAZtQRxWo8^;!>T9SX zCkXuyZCX4G)J7c#qbx4mW-1yTh=7Cu)v5?uKJ-YK2`H_v(2U#CCZVvctkj18Eo6Kj zb!gEpUc^CwLYG|>64=l1;gFat8HZsx?AA0W5C9tYbA;FgL|Abad!B*bR+YT^VgaUq&BYRU_@%rD}*> zI$o@n-l<>exm`ECn|9h2vxtIv2NktzuYs1Tdg_vz>Ya7ft|>wmH)sfhC$d<6AXo|%>!T`>Mop74RG5*mbn z_Qsb}bZe+g-_Pi|Rx1*pS<@H@d)i*mIpStInehF!SwCJktx@oQ@|_6y&3TpD4q|JV zzpOd4(@3%C8xc6Gh9+3-TL5+x^^vFxXV9O$3{t!iccrtw8J1*)2lIb-&h^jiQ$ckf zqeD5Vb7zm$iU!JB`|B8>Cn;}u7Oy7hswD6tqwt1b39#oS+(nBrsl!sdj7$uB_5 zM`xMqiRIqDNULCGI1D>+6m>^LP_T_@dR@JH#TA&q`Om{%+j=4B(4{2bYGVXeb?%mK zUVW|Zfr$Ht{!WeeB$OZw&!@16In*nA;8#0!HTvt*@+bBt-Z6iM@PE;O_l((u8^zbm z%LZ;0%`W!pxB3WNB&C_t>Ls@o;wFvWBlN|)Zt_#f&vYvdf3KHnGM>P(9S#W#ANSnD z0~~cdf`KJ8qVT%O4{6@hMVCa@pMN}AQ1`Y@NUmQc8#YiX(nP+h)}%tX>y>l!nz_S( zA}80lF-Btbs+WI4CEYnjn7~hw--M0fonkST3om48pttVT0*OzVlM>#n55kjU7$%+% z;fs%qG96Kx?g*%0*g;tNL1gd?@hoqaL(`a6L>ZRv=CdaW1?|hW%k>NhIb_N#q?15xQD*l-37E-8FAJfB!`A+F9r&Gw+hm;8)b}MAXM*mh#$`Xh zj<+mAD(!|4wRVgS;<;l?LE~3xHopXnLGlaMLf<({B0*sx#msY=&L{F&9wuwuG?wRqY3(_4iy4oRRScM1RtxF2 z4DmJ7bC@&3d<3+|U+jKEZ+|6-2oh9DRw9LmCB1)A(}fY$lOF<7V}ABYU`zZRJoMIg z?Cme=qTmllBNseC94!U@vxle9#o`C}Y>ezZv{8%tdfVV8`P7%5`moT-+h3TA;>O&# z_*CJ7ddFv+kyvZBY{8m0rB_dA;!Z>25-nN9zPlvYdUG!>>ZYV*$&Z~w3dkX;t3j3Z zKVN_5^#9&=zfP3DYu!qyf!ExnO9{NtC9^ToN}S%<6}%S z(X7Gdq;QmThVjkzcTJ4DHVa0f4f#6I6NA24*LdG{!M9JpMaG~ob!~?*y>AeIVlhlq zgeklfODM@ds1<-YXpG|I_MA@=+R%j$6i;t#>KkU9zantH;f{j&=-zM+U#YP17vz6_ zF+LyWpO=;E9u1NW-_-D`7;%-JLT(qZrr#Y|7^U{U>?pONj53^N6&o8da2sjL5Yu#n zvDq+bIv-`M08e=rgPU~$H#7Z(9O{Vbr!h$ko*Ix2u3wmktdvm(&F^3qcwczrsQ`{ObP!ut)Xsu z?SY^Ad@>*d0ryw;2A!0c+#D#~$qrr*i~eLz1x%##Nx4AvB@h}x*3o`X=nQ{Dw<3*G znIEu06Z_AAU@S|_PxK4D*P0b;Du^% zM5YsgSSH1_od%ol@@-$BN+_6J`ZH?N+{#s_mB{Ta<9fKzx~o?BvxT;TGO5jGf0nY= zmvK~dqqGNU=|^Wz(;#F4ZbFhf!U}}ee)gS51@u-gUvy>k+S7^jhr>ul|0fiiS zyy3uj&Y>dZX1U}_AUNZ^N;mZ}4bwotHP8-EyUo>Kk+sBrrHjvuQp5%VmtV4SDKgSl=a2Z4 zTEvu1+$cZ50g$m&oS&2)otZt#dx;B5ZMGua)U#aLYZe;W=~wm>7llq+qWxEYHVnk7 zz$(JK4oLZKX|Hg+F)rTg5@*8;xOkNR{PdU@L+LVKpo0`fIIe$~-~Mq7`&s_;pUSTR zPslGk(S!``*ZHrqw#V4R%$Lp*wNNv&g|qx==1QfGT2?TvQCAS1DLHs@cTDYmY)YF= z$<=0a`!Ac!20Gm)G?>FUQD`3H8oJEvhk20HDf6CZ9aGz?S;rtX+F-9fWZ&`nJM<}Z*lkG3OxXV`I7(Ey)p6(7j90;dX-fjiPm~BUcioJOuwhPY@ zYCBPdF<|w%?NTagYIRbmb`9k=!4&=zEUnf@y3)z31axd%^!RHr7zU2Tz4YvGS;8ukzyFxPF2Pr{=b^b^kqFPWrW5?!cy(&gEFJ;h&=s>RfX^6eD=$rEr){En++p~U&!%$WWc4}9b?Ye&pEV17!G~M)-&$Nm!54F4@6hmeU z^-t9V_TIz=}Loh32;}oRY*1GcGx-Hr||yf z`MhqP2(I%M2>;0JhWj4YXzqD2ks;?piH6T}>ZPZeRuM~pe`@P^K8%wzjA9X5+C9`U zt|xz`x|$>3LWJWfvYzG%p*|pV)9mL}A`g>AmC{Da)`j)$Ysk*^^(JG9M*14ZhU)6r z!qU8prG5!;wfu#~pd95r)4grf^sbxpg1GD2Ak2OfskFHn1_X-%(`7SnATbjHn+Ns* z=w&&c9lkH(Slw&X1H;|p+LZ)_)lh7f%-VnMG@F;OoyYBeM7>SW==Gi3j_3h5q(%t> z0*7`lw%P%`5~?8L(kFV-6rI(fwhPf zD={b|B`}=~^9ODQ1Aaw9b1u4=CVj&F9NT!YY&eLGJf}IARQG$IergC%Mz1LK2`PVo z;3gg8-7L)Yf|%u_^eb`A<{}dsIV9HdkH_uHWv<($>&N<-e2QFFb+Eo zQ-pW~q*(74CvOQ`g`<+&yT($!m*vxDXz%P=qiRtX z&1dCBrjokdm9XPN4`PK#`$bM9LEB3@Ko595lLj^~B&zDa#&oG?Y_Jvc-lGpcIQ+`U z@n8Y#%dRD5DPDhk_P(rib!}V!z<=9UB@*D60w0T44&jofskLXt!?ngyu2p~C)e!&a z;M2qXho7Gw9Gh~ybnCstj8M}NWc9B!0DI6eQOImB^UXUE&?%I2x4ZO8F$U?(gG~*ONhd?wT3vP$ZX9_l;SS| z$aM6NU+P*>{n9$U_Vy2HcXfY6bA_P~I5CU!y0+KRoU-$Cwz}^&TdUdywuGB^>v`FQ zLa=F{1rX1*GoI`4jjI-_cc2X?ljymUYC#wh+77Q^iigh(Q-4N<=f96qD&7#|fmAT_xz9yeVOHgl>O>s$X$xFUoX5 z?fA1!b^C7bigexy{>O3tUO3t8{@oSqtQ**w>CgA`p#ja4qMS=jCd*IS%d&7)-?k;y z$c{#^yNOPzzDTZxFG`jWVhin$l+5uPZ+Vf5F}FU*p6`~$U%k|Iv+J3ARe8M&> z^raN{i1%mcIfe94jKiwib*(nx z)5TuhE~b3ZFxQ~Ha(;_tg~8FQ8fc*W*jEjZ@4#bP*dKUnX}&C; z6cCiF$ac)tWO7sOy!>)LN`I2lv>ac)jK5ySrbyq%Ac4lk&(DADm)FEQ&g1NT8ySbV)cU z>6DpaAVL({8})xLr5e>nQ6Y*sy$VMQ`DmHV#fq!ib9c%D&xcOZOu7%((7aVdP3A2m zpxHLZQk>!+O<#Lztgd_ES_XVq#=U>mU6py6Qp&(f>qvGdnSR&K%d3NIW$V>J+Wl^+ z!))04r52)gRNcY4sn1SAYen-JT^LyHyaddObEHLLvdVv^dY)FC!?VKtmD!Lp*i~`= z4Z?bLN9hIeiyMU~3D}w5exN^7sVYYS!Bw}n`h73f6^DxHqhZk-$=kcp+Um?NAmfO- zV4FCbw_97i*ghNvhF^VW3c)9M>96k*Bqf^vqJ?cAUV9VNh_=z{3O=mqJSAS{@dNw4 zEDi|}LA-y(QK!>0XKRmdIPokyu6}f zgBe)TG+c#}?V*S1W=9kC8uGHGZuR=+Z7te6tDw{kt~LNps`DFW1HJ*!cqbSpWunnL ztelZ(+%Q@AN0J^G0+1UN0Kp95n8!|(j9-gg>$L;%t>id4++tPfLg&+cWD&Onj8$ z*MQg#O&LRt3=50b>%CyzKsL|BID=t})XP+`VY_S!tldC_ffVouxy}NNzVZ-*Hkv`e z_ZNTl_O^#-)p?1Ff-Odqa!}sWYSDxtvS$2XtL?j$yTVpy=zHPkzmA<9ZbnM3z|5PQ z07mFwQ#0iQ8~p?S=Q!AS=xN*H!f%Kp(Gr32X>kF4;}*W1au~RKTuGE9>mFCq=4va6 z2okQQF!a?RpP;V|+=4PlOe4*C%P(*dNg{t-o+$j7R~(%>p<>wxKGdMKmmu-;F^9Mp z8-1F3DI#b`;YFK|s_EE<%013EX*ojw9BaI&a06m<|1cXt6GBwd8iyia0&`8ZwHiT+ zt$9tw^B1mn)k6Z`kxr{!+eeq89pDP9(d9TI1;#kmEAVxx6MkBj;f5SM`5w59S!91% zPP2jSpDw2%iSEE=fnCMyHs{gtoeszIgI_Je=w%BE`?*hFB(*yNO%t1=?i=d4>I=%jwg2(F>= z6l1sa@Q?%d2a8=oaLcmgUf-Kec;!maeZrXFs9ccLLn0jJUQbqeZggAFGP{89E(ZKD zKW%|v%)dh-_*9e4tgC1U7kGzMcTZ=X`(5przj(oihCj}mkxeHh#__|SGjo47lh;tu zTrr1uy!Ue_|5T2joC8VIJvIQuRun~V0aF_S2wgtwcd5I9c`FrqenNb#IfH^ zT>+SEVPYL&dkqnzW$_R)c1eG9iaDkJeys>Mb-^Hp{$ST+!VlNycaFeVzU{7Wj-v_Gbh3!l3JARv zwmFpSX>T_>bkJWoGgOK)Ae<%(A)Z`WPonn!GQ<^WhL%-7KjUJ~oEv{!ioq-NN(<$#S@;a6!?x)2yKSAe=G?Q$UW4x~G^2eNB*OV6`AteYk#!@1&YmE_lx{;jd zMWu4IG{yDJ7@%=riF{11#G`YRu62$?(#hx&2)&Kbs#|dQWKf$xj8)g9&{%iyA9HxO zGZcr}&lTbhhV~&0VPk(kK0Q#e2SvjY$AaU69t0FSj}yajL!>}~dg3e}3w8+`>b^XL z_d)0597QSy^(;smV?{D6M%bsxf6mXR6E@I{(F=`JLGFM4$k4uqn-_dXbN>J|h0=mP zA7+&eCpwhr&{R*}#KZqbtuA_%v92`OV zT-k)NcPV4sxH>o;!U~Zu_)HgAX&=AaaE|NpZ9RGRnOgz3Pc*BPQv8^TpI;$eu3++q z;tb6L!}&hXl?`1bnQ!iYt@sHzY+1Lz@t@5R)aI=tr;~QZRpkz#{?tRq^{P+h+9k6& zpT7$&bDAfyE>y_=UhUWp;3V6 z(f8{Xv?7@e3epabPZmWH)GOYk?pz5BRegRADuk@95(C{Go|j<7;y5yU(Lp& z6LXr4o)}xld|D(-U-*|r?uKmmMrrI!=gpl$ssxIPV z;PFG^&WXNCC20((Q*PeF5eST8B6$z7i~#lhYvOje>N$n`B(4`92}1{$4dTiT|1rE* zZP#n14=1BleSFNVYyetWZbrA({J%(StCq~May8Pe@(Kcx1$vd?iiISKC~B$#9WMnytFm>V>w1`FXfH9E$O~J zj0bu1*Kk=fhwcpqWYF*7p=UTq`3!!-5FfvHczl2QQQ|?d#(9NsFhc$$AXU#*k%RP@ z?WsK}Pb3ZB%Y;~AOHYUj1<>9aS;%3ZvJN$H0L_o&{d0YGg}Cyq7fEGGMIcK8EtH;F zY(LoI6&>`mY_e2>)QLjnO*?_q+%w8+?P|!HS}~z~j30ovWI35nOX#CP$_8dPlc_x4 zJkx)h;EL>8bZX}1@KZikuN_6sdxBj-DDL3^c>TP@FgRxwU6!UOc@UEKGFufPRtiXI zbR#)s1K9Zr*zPm@85^Xo6k=dsa@(@ozp?KDVVHtjNHaQ=T8?RaqD8UQDQKoqv710O z^k-127C7yd-V>?>!|Z4J%SnM2?o9mTNN#^Yilx?N7fs+G#V z=>8_wCmh{ED&Pl#p)`+r2FMvQJWR^}Vq%=G^I?r5oEAcGZ{Gbsmx&ut0G8|C0V*Th zE(D+v?NHdv7Y0NRzGH?8oSc-;*?%XIpo8uz7~0mME<=Rbc@^G$72VGoTD*ejdVhb3 z^+TqQ#cSpM%)TViq_@m@pW))S6;5$(!#w@Lt-EeLa)6{iCZ%;0e^AaC1 z-5`JgwcasXjY|yfvOLSqz;G}@7vWyev3U`^!qI*6)=hs*KIsM~CGDG+_WE#)T94Tc9K@3UXydTb`lPr0l>n)yv)Zo4IYPY*tbylxZ#tIu~frthy*E` zcS8UN-rl*ac|{_WDbgyGC!xTIT|@A@P|kc#HphHa0pnxr#0>Kej7d)eo+N)=>}2oA zRK+BpZ^~4=*a--DF36^^Z;cyaWW`PbYh-%u)$5&?ee=N(O&8XBWO_&CptvaPZC^eb z7YKq7sl`@?Y|~en2pAmfz2C6FaUuT%12RydGVf(o{v2+X&pv}6W0pBA2XiqLpJroN zZzoT1N9BrZd+0uYnHvKIxD|hbZc2$!6WI5`6bcv5^kEwc9=v95rFc}AzOrR*;qL^2 zeOQ(QfYbo?N)yjBhy)q(X9kT8G+R-Xne_`Ur`0~Yo8ngi?L=kt6x1ZMML zHSu}{B|;3=>kJD(-4Y#S=5O_L$nc*@ zL7Brz3*sXOTjDPY^f~B#^3mpNKy&!tE8Cyj|L@WNAG~(-`J=>qw0C?Y?W#T*#i2XW zyvXCi@wUK@vT>!+OVPaow8G3Cf_P%=U!iTD?JH*m+K~8+Bz+MC`q9nE8`7QW>Dznd zBm7xL!ke$X@02Toa>Relo7g)RU>cnto}ZA2*je}Y_dn-F`##NRUdb2wJz!oqp2EPh z5#j_ksR`&d8TCmK&bWCtnPlfrm?Bj2`1sLdkYA4luGyvH+R}Rb2*7Q=Za$fwZ6x|r zs@t-o!_5b;vjJ6#_NXWGVL2`~STPZCq#cAbzmowq0&Pt#ron&rsM(Zt6RoOxwHEox zbsX-X)H}c6`!|ge+@2amx<%PdPC>~gs&EuCLJA6bpNm`nPOeHz)|3=+{Oy|m+6{5< z=MynbW+Ra)oEH>e&<@ltKvHu!vUgOl$0X5(ZqBkwcf*P{mLVJwku?~8Gw)rM%P!{g zT0OEZXEfqIYR7-hM!|t7sKa|<^x)*x{W8yP041quf1+c_0Dc!2d<^-Cidnp}u^r#u*YRvN`VEAqg${UInm@-dco9{IOF|F^x`2=I>3@|*xM5c>Qp?*ytmwq3&-98Lv zke^L2%{94~bI<0)HCjCY***k0;kZYvAK6|$&88z#Uv__~!{&JVw3q`bKxNv5)?q)3 zeUz=J?fc$`N1Jc9w&o3O?$^)P*^oz@QVv3o?e*?vnNedsdp*ukPw-C-rnjAJZxIm< zm&olNN~Ckz?vRSu6ehwh69?vmG_a%My1^OfFvi+hukW)P6~-XoMW(z(5O*{E+K)9M z)!k{s-Uok-L8=%i5&P|6n7++Yb$p98M2$Gwq<$hejx?;Sz8VcD$%hG_DjG;KrMB)l zI4wpED0u*fXx?w>#@e#>aOPokGQGJO z1C8?mA5pgHBnfT!byrn?=5rT5gyL{x798DJxe(P5Dxl%d5DIR)R}5rOYsDZvpNxk) zprmoK`rh7Q%704PUonL$d!NFw@bSs%-syiq5_|_ABew#?>pVF4tnMHf);Gm~N!T>? z$V$@z6a%DzEt<2{B(@5U#D3NJwcl<%$OAmqZ#V3K3)Y$#2q1n?V!GCS@LgYTzZTL$ zV;XlSAJRwb>NMk}C5|w>(dNM?E$u$TMt;#g`$}wMfLL(g&*{J_3XWeDI#8RtFra_* zbh{>h()E=p=lS!Nn0w|#B1Gs7&{!M^a9y%!rqT$h`$!!N9H%fKOp_el)%;^x@W4(H zdCik)HTqy3&6O}DV7f?*rz|jz58iJT7pqE(F5+TYd9jMP7D+VB?mkJtJ5pfz4HYKNt0LLXFP!q z>}PyIR}O4sVN1)e&mElXKRiCYb#4)M8-GaxtDrXf+f@jgJdJRMvvW4-XxE5GIdTy| zyD(dd1YJhyAUk*4(InZ#OY)#yu0%385|qPSZf?*nRZ5-8^!PX298FE(bzOfy?i-m8 zwzt^NS;p6Z!gl8$v+)d0=z!Be=zDG-l=(qK2?E{PR1-lC`IMXB)8V_ zy;|3>u%pDBP03(BBU;_?Z5L^U(gIs@Q6ZKv$(JS6gL!|^d-h2P>p@AFlN0pZmi#^^4J5kmN zSxIs+XMo`KH4qnI025hSii4%ee$0v?+3JOcJ2fVs!PVU*TOrdl<&HP~znGn92$qdT z+DKNf1Jc0#_=_*De_R%$%}0E9{6UG#js99c10&&tt?#_}BF%E{0=|F1skxxa$Y|Ig zJ1eK!tY$fQ_3775-M&8<0+Ub|a$m{(E91u%Hsml_J^~e-f}u}yfdJ(80xSpk#|#}| z%CXnc$#wZ;wT`f13_7O2LByRwnzX91hG)Srx(T-p>T@ zO==E<-J5C?CGe)Y3P*qJPux`rmIS=YE+(4b&rK@;4N)r7CK|)yufoD+ z#U|+>VxXPW@%nrxMSdmqJ!rwtx$^-x z6**&Xyr6hmi6DM8mD{ONo{i6>GxZco4X9!{OPGGzWCk7*2H1Z=>hoK2#@rP}O?;{} zgdv}R=NVKN>NMc$Z7GQqYPlXA-a4&UNj2V%%f*EvgY$2D1nPN1;f)8V+<_VRaR+Uy zll+=Q#Z7s$iQ@|7VmYoYN&pZ(UTaNYFSXMm=Ce$4S zu8=ARt$o09o4$W(nS)-Hw!(NlgrQ|241&W`)`6B#<~)oOni2(QZc)1mm_%32_G`Lq z-p_Ax4*9_A-CnPi7c%8g+43INs3&M;ObeBus<4p)DJ_U1=va4J zUT0U?Pfu!>jXR>oq+n%Lm2{|1S|!3nYos7B98SUO*1akwHx;UjPLCz+2HX_zEmNd1YU{f zas7`sWvD|py}m%E8{9>0`JOjy{qQDV*tUc^)DjoIs`63EX0NF~80YgK=0q`?Mcu!Z zDCieewd!VA!21o$X{NPp0ZaO7R3K;R+z0*317LsqmQfCjcy9`vlZCVM0ezpslE6;E z1`{8f9-v0Z-Lc1FjiO>yuY~`%2 zSZFR7RGMo?3DCRCeEK6+fqM3pa6@NghqQc@U(d{i%N66uBsS_wVtx2CVB>-Puw?a! zuMU3*LvF2o%$+A00w|^AhOJ|9&yKXAixcHyW%CdAjrW=2?}#XT>ggYgJ8{*!;*zvN4LgM?_;bq$5MaP}_14ouCb;DJDg zGn3Q)2YQh!BF$(C$5-Ps(l)#=BVnEq{lg1HrrMg8%YfJ`kZhO*(s5-iZ^$723XWA- ztoas2HEdmy1As&!nj9nKIai0oA&Pa?jh`U|Yh7FX&f@Bo8pK@*ZM0aqNYFCFp$dQP z$GwR6nZLM3XNo?4MAr1hk3Ktu5U1-*G!dve4uQoK0L3{m3U=56MvNt2Zr+U*y3GZY z$WWkQ(Ug>34mVm!H$-q$(PZH}ch(ACl$G655yPtNa=C@RH19O*&t0~$OwY?;6~V;O z7xp`bQJiSB_UAm0zOXC77=E%`E`EPnrd8tGtX7fhTKTL1eKG6WitRyrBj|O*%_Zy3 z1?a_>Q1rausKf}yvC?)+l-AL>0_7$qprJZ$qBK{}p*nAu#B1|PtxJ!1&8U59M61Ky z32!*&q8U#Y5CKsv8+w8{@=?;+=T2#nol1hSb;Z{UJ#LzAt+Eb|UC=NqLrQ<1SenZ2 zX3fMcEYvOZ6mlHDb$Qa-2)!NhFSQX)Oh1Jg!@fsDqiiy*jk<1bJH}#=ez6{`YinkW z{|KX#4~jDsc~RPgj2(Va-FamdP{jsZYElv<16rA9IjQtj9s7|rI)Al<@Ch0>#CqS{ zvv7BDmsl?S^eBQ8f9{!qv1)&M^-Yp_VaN!NCo5r`z)zl7rR~I?+Lp#sZ*!p-jp1Y} zpi6Edai4susHjese2mh?7M}-&kfgR z_%p1z!HRkQiN`n>%5iO6cDf+)fH4=Nz<8yi3yu!%TaeFzizH&uVRKfXO?CC4_$pP5kk2$`g2u=Ro#-PIMOcV~7 z)%G|^p4d38t_gokcpxbI1O}{nQG*BI5RD=SaBF}+;ZI+afZv|DAjSznw@sb|nc3I7ydFqXB#(4XF8FM3?fhCtrWQL7;%~A3nOT@WF^e zOc zLc2h-H1JgQwwc$-|NXlE{kngmUni;|m42Nb^7DtXF+^EVI$l21Y}UV}waWL*q~g#Q zb`J%Nw6AWok|&AIZ&1b6DO$lb=lYA5`;1^1{lN6{Nr~4Soe7$JUe`+g<5gbaW2b(l zPgk`&&o+<<`Sxd{S)5QA~u0%{)ax&Yh=F}q9QU@$&)Isrhv zQJ#}wlUbd#U}6XbU*}75b@WlQ{%#YjxNj$4*^`C-U@hEzJTJrJvcARW0C%{2H|P2P|@C*6%9d|4jdjp z@fI`bNE`^Xwx~AoZ!sBmOI&u7UNtd-znk(xz(C+aR$HzbZ=k$&9j89nSrvYCg0cVl z`-9r=fq%X(`5gm?De#7G!T&qEMhDtmX}CV`P~6yt_U}ptyYSJ;=A=R#&rwt$&|_=H zuNHsvRTd9A6`!81!anO=&Y__Z@A}IA_DKAG^D{gCAjW44dXbbH(>46O1VLEBUmC5T zF$xl>@wC9r#93|ej}`eSewgC<%0j%hG|`nfs?;d?%#%~QwLKb!M*9S~7hCc4>y7CU zH!P5X8gg%tB3Jmwa>nZ@8CAK^uIY2}7&d>0EVg`-)6AXFmk#^(Iij#iNYf(9Z|*@( zUvdUT%l6iky|Je3O)k^Y;A{ljLl)T5}tVpXVr;0?O%0->*#L74XDcp7)V9 ztrZ?1)KG#}cfaUfy#H1AMx?BH;`>6P`{|Q%9tnKKfM8a=et@qzD`uE5R=2wR7$bk4 zDVPK|pRsy|vv!SJe&v;BDHqBM(X^mc1$I1Otcc_t2l#4H~_NOD}O+aTe zsJL9(N_(AeUtC}Q>hAY`?;n43^!b11`wtHqxe$+ZX9q&YGi&2`CKmZ|jUF)QxQk}yT+$?5~?Zy&XSUz>kqQ`uyLgx0v> z(ehNIxKck|?IQQpBZEItO239$!Wp-ke1m#nHQvh;HsTpq&%@_4ICwx_Fav6@%(y81 z+nAOm`5Y$?$zCs$o)I(TiS>cI)r(`|{x&x*-HF4y z7w*kyf4`g^9xT_7oA_w%HSPbswEw-d|EiZ3I8DK!;NR&k#SMwj7&RPPo_wNnT+=w) z@|SjZHguTYcs4wy{q1;2`~Q1HErP6*txcpAGRX__tEm4IE&gjgA8b3~`QT?uPe;cG zYaT54kv5t`8~J}J-Fy|f>gi<9Qn!SYTctQ7!G;l;2*xOioL^btBp+fwIGTAp)d=Dw zM`nxm@%D`oM~c*G4Wei^hzO|us(1hqxVRE;SK)32Vh7byou%b994%q8EB0zz(c(l- zNZTFG{Rw^Sv>%;>W`Z8fiehTIhAccPCPiMsM4{T}#K?av-arC{8Grc*=Qt(|e|UH@ zBw+e7BoZ^@T|)sdA!1~5myQ>D#;ou1BdB6F?lPzj?eJ}N8Av4T&0R)1w8L#(hO2P% zV&g9Vg0*jZl!?T(qs(-qj`BAJhQDmu@NPV{Sc?+(=s>iq0XK@W0`;260kK6ma4c~v@mdBY$TsO{4M*27(QQFE)`&?3O z>GUo2nMur=`>bS8r(65{A#59OF59P2TY7wJvW+BW>ttKW&|wxWyhXoAGY@Mhay$`g zEWQO?0@$PPc!=UB3=aLM@j3eZ9uOlNS7>yUiVS`g2yrQt_S2>IQxot@)+hKi$OMIr zFd~0!$Ma8;+^CzKp`?vOj;V}O2Esc?VJJ5{aR-OXze^+is_);+=WCO)vqD){EXvl+ z6&E z4f|0}JNDujw9wf_45V58#?rKO=;QhMO*?;ie*QP~lGD8CRMh<7&9ce>8_hMYx_+(+ zAynki>)zg#C+)%~cI+MAcuiytOK%o=GibMV(h`!8c!9XF@$1ZMWE*zbnj~2N2R49{ z`^NJa=2JJ5p}`;{gqT!iJ*i=urHD-4B9cacEaj_$X{)TGD(;90jl z6qGglD~wFN0TQ4v$vc;Y6_W?<=zf2bgF<9AC!fHGFHAo1MG0emIstvRZudnibY2f? zHu@}rRjn8P(rl+ho11$Hed-Uw6(N@4?BUnRs~39SvM7OJoEyIr%ecxWOyin=5!;l^ zka0o^u#UEkyYmgQoOI(FRM?1TQ17qen2;Fqi^&jf#UD9%p*s`#DFk>4A)SAXt16ut zsqZfQ1*}|=nZ!(Wj!j8fneo(O`9iPT9!E!8B^i=2EwY;k`6d<%(?X=&*GaKj7T1QB zYvV%>+pNM_E9bmb!5JwlDUBfLaNuFVo9tcN96EVe3Te?f)J)*2V&`>h_|`|((1tsw zPHs4-oSueriitu;(xpphkez=+NgVxxpAI?TmqR*o)vMfDN3DJFsXsffG| zUUxBh9>v^;&Dc*5tbiHn%6Y1{iG{7d=oE zVygYTPf3vnK{bC-DIYDxWDg&erLF)}K&!tjqjO&v_Y30UYwfZC7tTg{@0jm>JctPPv2c2`Wy`NFO23B-X?q zU&Ir@T0IT~58LJMHDK9Z44|cd0C(hqD>Z;6DO({oL}@ZV2HD^^arw?&heAy<8}t)@ zBnZf*_XbUh5SxA({tDj|>+n~KlD;lmTZT0WWk?Ja}y$2HXZllgeQ7!`#2!~I7`fk;_@ZmEE{4BG1t zYD_;Vg-P-|v4~L#i?nR%mo1$#k3IT%1Li;_VvQ@TPGKTn90~xUCO=q-uzxh3olS~; zW{%Uf%xyd~JHFD;4E8!)*@~gp4BE2Ggdj<-VTcX#8Dj_fL6;t(XQOOj#JQlh|jgT6Se`%KGf*asd55l`i1 zr9=0HEvn}(FiO{HNXW*Ymi|920b?+rop>NvYDf1;Rlp@ov?SY|ZfU9cHcc1|gy*w) zQtD=VWglm#$_f4ECotsv$9O{jMlf@4FmEZA`Wc$73`?<0VZ-!)S)nX1LpWPN7@OP* z?l`xUphxWo(3{)jJ_c_6C%KOu-!k_%;eRZE<(V^HcN+o(`f9PYYwhtK&sgvLh>5U$ zuUPk`!>u}a;M{oUGLvS zHNpkjXHQ}o*1g<+8@BJ3Ci*rYvHl^;C&grZ1{Yuo@2tfWguo}$Uxr^Ssta5%WFlX` z1xpG0j^LyiIhUx-xYJ*A8y)Gb~6roAzmc_g8rp?w_O8{x=9lx88OfXkJUS zI~Nqoiq6G>#PdEaESn`^jlCls&S(HNML9q6H;_g8oO@5td1IMbJO&9 zN?JF6CB9Ypx&Q14P&Y1g6bv?HVZ)&@V+#Y;Om7{HiVC&{mi!2|1ivkA$H0MteManB z9Tq5)DHIYRC6P*LCkR(iXVN#qkN&`ERlfmS6V4G(zM<@v-()K`~Y^kY-T3>fUUMy(N*2hk>%6qVanf70Yz1GR_sCY82?Aw{e3RmJ&u{3aDD2U*s&ep?HQ|Ci} z5a_5Ck&#Ja>IkY6%z3w4AW)Pqts)B&i{1dl?VBBdhPSdX)9_zE*Yo*`??g--; zp8izUDY42kAwj=6OecP6oDKx$D2pC{$d}+k+(%zQiNl#lq^U=i=}EO-rWkae%%PyB zbbN>h)GqfY-mHQ)A=86g)Lj1uY*_%Wu4Y4X1Rk zBkaJuV){tX>vVo6(xpPMM^a$tOfLfr8GvU{H$###q$k4$@edM>dYe3;GPbp{_Az47 z5h<#d?R66?XfKpYL=zA#L-fE7(E}YK`*mr*F1@c$@oRjDw9`xpLWV>1z#F0mGDHvB zhKRTTBLoOCfX<+5h7@H;4*rIJKh;O*HINc{&|VW=2G7RVxlU2S5K}GOVL{H!(tcXn zPs_|ag-^m47UAoL<{x|l6v7|6)U5K$)zck$Mb$k@2wF)I62hjh*Yp^&p-l=?o@2M> zC4B-le^E~HUg(6AOZctkldFpya9_eb!C?+MeGi9z_!Kz}-0<1^5FfjLE6kbOCMTJy zkj2v{@-qKvG5@8o&-S0u=8wrKn<;jOc{0mK<3&|7GN|^E5ksfZk0q1ogQ9#;oX&D{ zS&oXyT|jk+()DW}aDrP{M|Dma|zkY##fUi z7U~?M8x9#}LOi>`{4B+*JEIiO zdLWYb6;pDL^Mchbiq3(ZPr$_IJV`2wghd*&RZ->TDve%NjafaIFlh7OFY!u4h(m@r zY=Qtf00PMgjN|7;UM*%Sc%r4{aZ|}TeN|I|ImDrUc_rh2E3O2I%xi3^)M+4T;jc^Y z>r?((cP0EeAtHi|m1%KIx0ZgPnNW={MS$|TN|7y{=RXyi$&h9v>|o5`ai>C?p;|M} zhy-}0SA&|!q4P#>?nHS(G$A7b=DY^qSmuUrngRU#I-#ojMS=i8U-gixTw0j-f_G`8 z?v5u_zAQL@elEkyri1*(e+9D|X=TS4U9YuZ<~no^_-3xF9p|O!Hp)uQdX4q9epdac z&TMq2=A=Ytm;}nw2K;?v__o2@lz2UcT?10Mtn@#>s!ad$FzRc@1mPw}fGF6AdF-IAzO51b{ZM1g8_2U;d6ni#$IVd>`M6wVIp% zhi?nR$2PTnrb^NTw1&Z~3$~7;*nAwJY_#Bi@V&vc1d^K=wkcktVm57vmT^SO&WN5y z5wUOTpbc8+f)gCrbWb<|S*kbO>=AeKijzjL?j|Qx1N)FX_g1bVKZ>!1d02m^ z{N>@^dC$$Q!xIwEJ1*%S%}Z&QJ{8+JcVmVDWq@muPdZcMY)%Sx{S>n| zrc2sRtlHYG8dRDm7gma1xOKo1Uo4k@RyT`M^kj(OI+*PV16;*NxS}EcZfahmK*$(K zyYzSTaO>*%29IXcd?_wvKdzM|;@TF)FI50dw8L?P;iWrnc|*aoqUVQykj8na7bmwa zzaqh*3mxf+X zyxTNCtGiENiQ9)s{8BIL^#+(aM5W)0U{=m`qVAzQ)2ELaz0hlKCfImDuBK+yHERK* zi*4P;ZEi?I$h9fm`b}7WIM>mP$~uw`OK1;Su!s&2cc^|Z9ab0A+MW)ee)0w~3@W!x zhGFc=T4^0m?c`@&{wgkB>HY=#;X*_QuivS(OmxWW&iDq_2Hpel?kgxK*;mZrdy;F3 z(BN2RYiCy$zs9pB$GKjpaY^&@kSCqy8`E%}|Jv%cvl36=4Fm{(;M4CM3-|fq@$unj z_fH<}A3nVQ2m)pD%DzZOf84uw5C5j0cL(N!->5^A;$#LGirI^P^=q!58F;7znyvu; z!@Me9{BZyH{x##f*+23iJRpuYjp2XPF^ZM=rJPMh=908BJyf=Y>2+yeXv-%BZIPDv zlEKnka>puuU7lWlP30F;aw)PwI2T~Nm*rx%ic8CoDr}QT;Ad3j-X(sP`eK>%@qK>% zm!j!@F2^G?$}hou!0&zoJRXO0Q!UVBa{taH4d-gZIG?})+uhZ0L5R-D3_J;f_Iqb? z>H_tVpMS%*Ug>Fvt6BN;cto%8f@3c^KZ#J#H#|Euc1iJn&dXo&*{G6$J&MDvhC;{} zgw^t#c9VrClN_kOD5?ywpG_9nGFr+U%0`g}>FR1_YXPiOa|q5XwSUALj}9sa8~!xL z6j}lA8?m??pefCxzBrp}&+kuh=iR)HiUfb+-NkZxK7Im`YCHuk20N$tu?;Hu89G!h zD!boKnF$1cGynNDQZZXldMxgfy!0~*ruLX7$0+lqhoY2AZh>h>vij!EBl?aC?v8ow zeRJoCql*E2^`ta^M={*nZJeh%O=9AOOxrSjV?MV~4Q-y3__iB&D8#}}YYL)r%F(&Xa25tnRL7NdO8E`VT62O^*_zy6DfW1r%&w0qXmNVBG>6l( z89qpkNcd;g>dmOkxl|@C3w$VSC|pw;2ZgqAP;8|QY+xVNLz66HS)!~E?8X8@wYy72 z|ChXf8my6xPf+)UazQ51Z!)Y`6N==zpRs`m+EwGxS}|z2u`<^@Ao~*^M1i+Ps@CQs zvOG#@68IO<5+`V$(BX*zW z71yk={@oPF;`VrIWM;G=I%WP3$$MtEZhVP<4pQ+t^kIUkfaKQ=>c)E!3#hov%;{oo zF6@<)*|_=%Tp0H70H=4cA#^4{8=>fWP(b>5S??x#^&Ip*8X9j9-rC{&AT$EpV_&^V zql#=^`VEdN`?wQWaL|1D1R(^kKjgbRx6~XX4%@q}?(N3=w|86p+pG6ams`W#L9jf3 zE2re7k)-j1($=ARE%hIQ{s`YS1kynGf2p@Y5xg409xk#2C}h?e=Sh7A&M=aIla3Y`P!X1LsE5Wn zFM;LEl-yK5VYNx|b1{)i2)wb``+`(|NRO6}jQp|$gyqa%EmapKofrcPt77O3`Cc&O zW2iocrQOr~27_+#@5g!1gq{|(2pWyg&x;xKXtJcsb0bqS4;Ezh%JXK5xRtmd96}}W z!H=e389*$Kaa;6s3EV!<;kKL19#q4tRXJC&<9YND=g!f`9}m|zB#!7w2|iVS*ihWd zn-y0Q$mF*=__??GKYPwD|3uq!$iQR7?a6?wC9N%llr3O=%=J^U)z6VToc*6jB zFut@`WnpH*4{}SXg+O+gX{6eDLXM)$?&^ZVFUr_(rHu%;4c=+0NUogKDtQ374R%J8 zMW21su1%4nnAk$-KdQ)ai{}Y{EuyEFF45Kjym6~^a3O8()2l^+Cn%c{_|!4pDy46_ z*P%LBb`{w!KDvK!cyi1Iy5$HC!0~+XQ*QbP`EKyoa8CaiVE1KW+%Ei%am$=rRst#6}qS} z5A%vlkG6p36t}JHL*BOfiImGchAzJO&f5^LFq@NSQsnSrWQNt}bdRu+9yWDPN?5-w zfC*Q$pAUBA9PIEZsF4?xyC2vMXn*I3i}D31*O~cvf;V|>K6|~2cGA!|*QGEsznBZo z$HioXQu*TX{S$j$U=BWiWQ%Pp%U1Pjqx$*hd{h_^jyvlgVS_dzujhmGG6WD+Yr9ub zBsr8^d#|6=(b6R;=br#9t zYcqa)u$YR|_XUk?47$6QJWnq`_r$k8t748J)s^Yj%V(?*9_Ok6;oOt!T{AwWq(;W>>-UFSVJL>^*E?zGqJx&cu@ z-t9{zk)gLtr4CJhl*~l2cJ`XLL|>~|6JV-ryBpeDCa{Hp;-V*!8E%|!iC9H^&%CzV zOzlCj5w!=!tx>z%pf<=GU$4IjBu#rW=*<)c;Yin!4Bo0N7;GD=;zsl}OF1sO619Xq zb=U$~fT`W^AyBGejkb1>#cN-FUYN5BqRYaHK zVEEvvY_;H9W30uOGy|IB#+5!;Lwnot6yK9JA#AO;c9Iw&$1jLGM3T-1?G#|NsTi-< zkhi;&M&Is#&ejRs-ASV`k%PP2XW?!d5BCyzxVL>C?gc#5Q0?(~Af`?PbG=MsdNt@$ zcTc=LK8Kifbd{eNe)TeyD-eA3pM0(F6n!h8&c*gtNZegok=6=6A)d%bXfX||J1fx;ZW9m!fn_R8Mfq%8~& zo)@q6uq?~6EZd!;2u}nwXU~SRS(PiVc^i`V9zg!Zew18q2$7=s67Z{?i6WjF$R0@K z=5#E~!FXlQp3$9Zl=NM!{}a$&vI< zS`^rSxh;`za^zG+YIHhplFV?v%<|OEW^nnUB!1$Ow+@#ziO?N0?7k>D$~qWD6})@N_% zP-Z@&$k=F`)|4{5Tp_f$UTZ7QmU}xiwiYRKRPzOcL zEpT!Fe!A_2NSqF%RVmcNTbIv&7ta<2>kDd=Q7Uc}J(0DsC^IQEisr*I$!&hFuja4_ z-c7-a^`r8SJI2ccQk6Y{I+xx)acQl ztNg>zhc`7U9%6ogchGT?K^7#4)p!I{A#6Tg9aS|83qM9Wzswqco56X1dLLPrO?8=# zHM!(>Kc8E8TSCO0%hd<}5<_aqEH0Ir&EZt{PG)XG_1DhVk^y_$$$7g%y9eLCguA7P zxqYuchS!l&TY33XxuKt5bRYoSP6GhJefK+MB;RS*)9U*^mUbTeEk#tTfxkjc%s`cD zmEg}!mw!dC641V@KnKZx{VWFeO()+~UFT0*#8|by_G|swrI{>SH(q$TtKRkv4VP0zRWi5Jvv(Gz&?>7v~NEf$saA zmnt332;f~j_1%fO{$Z}OD=q0fq2OvPA_YH9H=^gKsl$wwSQ^tb95$e7>IG@;*3Rv6 zv;kdN_t#^qqfh>SO^uYD#h)KTGYM}!`&wO6OOFtyhWcbXkoB7E^L(3Ntl`3ORqA+< zO09Y{4GH69)Ih2Ay%+XeIG>0y*F@W6aR2hfYt>?`FTB;3dg!#UtVjnf3Y!lDxbA}j~$A$+0=t+v}NQ&@pa1c`s_ zTokvX`>D1Ah)z-c07dj8>I~-obk5Stt0ktGnWT*TyAa_YwlzCOi5;2wfV6-(zfNVA zELL+)E2+X{e0x<-J!R*9cI4!hltSR;jN4a#w~GxqdQD>;?hUANfx2xUxlW|7 zdZI$}S(qWgvReIltpe=J_l)e)j!%bGSqMywr}JA^_bIwl z)_@9sYmLvqiAL2R5d&*Rfc^Rttk5H2V-_YqTnBYmrL!O}4Ek}ie#Q^Z=^ovHTPyp^U}-R6Lj2HIUoV;c-{f?u>w|4sAuHFQsI{4+?>d480wi%P9>x3t-FP>9rn-_3~kqc^C5R|}cGeDZjI zlwPdU)Q1Hy^rmo5MDP#-(PjT=du<-g3F$$s==cYopp>B(9(-=vru{m0AAeZm&Ro;T zZkQy~23$0D&2&!ro58dPm2JN5tLEwHSl4A#KRk}tkwFUU*=h8};gd5gDM~-hva4-A zA>F4|4DsUjkd*GU=DkR4<2+ra<=usU4&sx9beuOODP27u=bJv|(KzMg3ziQ3QCS-* zmWQ5X^+{Y6RG2OP>F{Nfu`BQ$|3Y%mauG7E$`wSlcc#F_GW$WrBdc6?guS&Is^XQ^ zdu|Gi&o8U@-B>N1V`SC?f$l5xTE_lT4JDLyiTet=mZ-PX|Ni}sQ!iC%wL4dT1it$m z97Ta=B=&d8_I1qmbI$yaPy}BucEfhA;Ei$nh|_26a#!@li7r{-?YsgHC!qKrvpb-R z?++rU$97q%*RR)MlNf#yA+>b(iQ&S#c;9ci??AYZRnr7RFlCT-5o^UxGm`a4sA&>d z6a|*?^>2jLFX?4ruecKvr=D|vlJ)4?9C-~*H?f*pd|Kp(Z8v&7N*XPXeYbY7i)7>W z9xpZ7IrjK1n3-p3Zn#X51HgB4nXS`K^a%YB$^oV)m+x~hSuakQccp5_6pxPGN0Vn{ zjI8^S{t@HC`Pp!Q+;0QZ%4c;Ah_orO9TJ*V*x`z)Epl+XFa=|=*qdgzG&YJ4G z2|RN8m~(y1@n?BJ99A_)pB#Vwp*Z&Z<%1#v9&Sj-8-|Mm*3QXllSyV+=IkN9X+KKV z7QyH&Loj!N&{34=bsM6S---}na@Qk54=N?_APsDN8S%wspjjS1}mT_ve4Ri+{g1MsuZq9 z_Jr%mV{48eb1f7S3pbYq!@u_u%?|51##OPRt6SAE-8)&^-C})r2Wz}LT<6{KTJK-6 z-m@DMo;$d<_ERJWsr+5Oc;aOd+ydLg!WGfJ+&iWWxp&8Zu(U(Ng-s0F4-aG$ z+f?Y2nF+tit{;BCDy)@qJ$Axd@v+mf1n=FY6xcmBEbHJ7xP>)tS@XlKjk*d!-F|A; zQwGlMrp%rixWgKCSp|vEm%qKcqLxYQs9*L*SSy0c?WQ_h`@nGcnc(tlZQ>;2$BD(o zQlD3(4vLb$b_M5uhVdTs*c?zp;7jn%niv6kmN;7nJMs@p`2+X~__Q?l#D+uBvOw05V9dKGr zz|vb1j?L|K1~N(j$+zHlzod4_t2%;GUew9f^>3<}3d|UPVz!twPu;ZI<;QNMLUFlO z!HYcilFd2uOX|vz~5e^GrO*gGfOcoTl`aun`3O zFz64DBdQ&lZ=Gg%POo5{Y2)Eu&G7>=O1R_g1CuMz!=h3{8i&ixVskxDw`7u{pb`3_ z4+qeP$to#-ZnTFjQUrc>otpFLZny5zm;9GpbY^fF`bLh?BDbl<(PD!|xRZyegLK;& zDWMBRw|If6v|kjMg65YOXe0;KnRM_2 zkPB!tVtJkv!3exA;XKXFGIW`or+zCh&vOX=CQow+3+hP@`D8!GVVeBl!zU+Se&Gm# zj6RuvkD^PKxUjHcO`{Rvk2uL#oFX(^dwJWkK;wk0(qGf_Yf>n|3mK2N9LXQBMJP~z zu*|YGml+yhR?}PzgTRx;Fqo;R=L71Mn5 z2=9wbwKkrRCrFMtadQZoYvuuQUN4Y1AEFTD0kzotlEVBofZb>_)sFEGBxs(D&C}t3 zH>m2=B8C|kAT#*CG|58#ZiF1tI_7mLf1Pd&bPV>pG(b!S`TIANuzz5CdIt<7Eqs^7 zz(j0}vBgMininI(^xxR<+?FINjY)ug10wcoZd}r_xU3%SZN5@*yJ$}PdpM@|x`lLm zT+|V}{E+Xe5m-@|3WRE*z(~0lN(4}URF?v&$AaN{PC@-qac5AOYfH%RipvIDg^jK2 zettw?@cPw?motu*IwyXe@nF~T)!;KGI8@x)vPr^ls4!IF2{`%YgIw8cV?MdwoIFFf z?=yaARMPc=we&5LxbqoDKNg?2)%n^<7#6>ij=wa4ieA4<*GUSY)2rl{l`9W_xOMjH z&kg!&Sp-iON0Gj6pk-FBixgifm+1hlnzrIH%QRN2fAkC#P+~)+)xgk6+G+HAO?BSQ zyL&5A>Tp``p}8}Cd`?De>tx)voBvHd)UJBLlv_K@mt{5C1+-9^zixsnRLQ7Q?aX0` z=||W4`s}}5WRmDykOTXeqRj1o+U&y}+9d*UbRYl+^f;k+&5s1+gPJj9B(W>XDTS=M(X6QE7jOP6pNU>yf-De9 zht%vt%OZ@9q+A@E890tE@i;WN?8(g)X1oeq5!^~P{!rAj(Ys1EcA73o*bB;I$O}&UNUdHQ|Hw7{h$J5vRsZA| z9tHX8@zKCI1vZ;-WTs3ycD~<4e*8uo43X$vMf0J2JzT6?&VA>?OvkFVG+A$*_Ya?( z#P!up-80a27#OCY0nDL)837egXPGFTnG&7Pw23MOEEO)Y2g!F2tRHXo;LQ*%HC8q| zdB?^fCOb6FKF>DCD`d8%mNhc?Sd($F7$ul|HHS4+{}HKgIVC*oVla3BIlv^irY6EAejmocrC=BdqnFj2ckNYf_wS8CW8r`o zgWPGiH7{-hs|*X*5u@UYJ$7r{2#-4&5|<^%gm7W4`PjjD>}oo8HylpqNwFA=vZ!=3 zPcO3<=@SSmG&U?{>6JUwHo{=`%-S?I z%TV5h+GKi3vO>%dEz8!)`o_fdO$tYt`BPq0$|vv2k{=(ck--Psw}b3XrF5kqJ&Rte zLr=brpmk?|kE64X9@A#Qt2M+PQ)r`w*!xyo&<+ItpAYe$&WC6a+~fBV2q(s|&kE6e zkwQ*3WWZfew2dWZawFtZi?_?nW*Mfx{o+?RlRmbXr)z3D2*FmtPLL;w$De?3@!UM| zlU!!kW(MMG{Z&qp5|im2)?AXAv5O+MeS(9MP134=lwurikKQH`q~(XSsz^ZHs?UPM z&xpY5PZjZ_eYa7p$G9ptQ|@;gCA1AE+8EY?Fxy1F6aYR5*Qe>gFJBy3O@el|1BS@! z<55Ow<@yl=^iBgcV~RrW1ilLy4`sKyr&|Kt@@I)uxR^*m(fqz!V&Wl|n$Jgz_4zVw z-NnLxBKEocDi{IdWnRNrksOAqB5jBRHRp7@0PZ{5|5lD=tP!<^wG!sj)KF}dn1ylH zjr^i!A5Hq$I|v_|0_iTj_gg~575!Veyq4x$nQQIL!_!T6)pD-$s!Z%YJbwT1)5Fh? z9zSW2*bSAFC3+$22LgBZ$=&QF)liJiNr|X`A@iG6a=AELhoas54+HZI&mJ`(Fb09%oDio7S4n^o+!15N|i=8rm z)J-$-yc9arD6nUXi|ci2<*TNIUEIIwCOt9L&a#idb1oQyCQ>Pn1>3s@@S+-Ed1AD= zQ-}NWG=b@2!|e^?Ur{|+fG$8Smp1`gk~!`OH^G#BmMyv9{NC6(hQ)Riv=aK4d2r}q zF1#!ZDSx=2eG?^yxTGl}hn%9Q^5M0Ai8*@m_|fs%Y4JfU!5eQpy55{W|MC4~4Wp&? zF*q=J`;9kXNs~o4g?{89(DUbfz3}7Y0_@x)vIsdI@xH#yi*-=8E}kyDNO36BNHI7; zuW&V#UoT;O!g0y-b;|27zr7^h1wsBiK_4IOEx7whjyydzf&{VvZU|z;D7wafi)Lfv z-Uv6C-hr>O;jO+oUjCdfE>^`e=iJ!q95e_*AP?W7EuBJ2r=K5w@Z}qCm|q_yW*rIh z%Q7`rUEy(cu?iR0H`poo%G(0pl#6+bGlzBn81(wphDGq<(ec|yhhIeb`Fa7RfOLsz zf*`72K$z*AgR-BUoSYV`xX)344@hDHHcwGN0GVPFP)3k1@pI7yM5}CPF0(983qkAj zbJ5Un`Z;^Ef>b4+%O^W=85I42*meBL$JeEfnd=`IrS9G-7rE$2bZEN-#IuPiNlYav6yB-Bx!jlJ%cb*TSl-Z~ ztR+3tNSEH=TOM@53qSCG(Y6_7eFRpFekv}=OJOsO(vt{SEEWqZNW+`}ftH0V4dshj zjS7aC8N0`9S?Py&g)pryO?S712XkQ9+}$GQHnASp=!jB-oz|rW-U!%+XFjMlYBxjj z&79)Pb3;u9O81sq5arSc`D`A{m!I-#zBEhjh~U;pb=@NVQr{|n>p^=zAg`UIi5l>L z4p);t9@r3(ja2Te44x16HBVnx;Oh{v);?a-^*UQ)o>u0L9SVLfRuZpeJaLpRuvK(8 z1hb@;DMMvNDQKCQi+QZSdeGpQu3@`8eW<7LX%-L*FEs?o;c>A-g( zwi+W=(WL^R$i1sLXrN?fA@|=?EN^e?^{=<5oqnb2x9?PctD`=Z!`|i7_q-oQqM+a+ zU%n*?6jao>3+p`G=&YR2+F%9CE|4F2*coz`&n*ErQm;Smqr&qIeXuHXj=@6hIz5C% z^JCZ;q+i9IgBD#!w~Jp?iRsH_6uz6m4v!5pX8YN>71osST=aTT?R7yS_v9%wZFP-@ zFSnO#Xau2ui)R?wX^6X+W?RRX(U)&Qhd80*;wL}j(AhpQ19G-A&t0UF-l=oO!-914 z#0iVYLQqiK-S3h8nJzwbzcbOJCD^3KWSy&D)P-7sS%v%>V-K`}lJEKkAQt|^N9xjC z_W$$^p3w#^?836{E4~}3P<*lJVUw5{cnav4g_gvBlIY}SN0D|Wl!8{wFfb9_h_Lp;FXrz6wDmW(B~esM?L(N&I0Dpa!}UxolkyiPTuIva9UqIdZSzSs}`=$*lRdFN7O1C zv&)HQN7=QBBt0nc-!Yk>9g{hjS0OqDJA3|I&|O}l&6d^|C2lp$YjatE^>#@07}IIW z=v#iObqKcCjA6QFbnwJZ65@JYu>HuSjCQIz5GBaz;Q~mLjemZD z9OKZJo5(LVagVdy_IUiOVRMy&>l}Rm!BZh5O#8`+BkdCG(Ep81zm2w-3k6-&a&#R& z9r(3K&-7>S^C>Os2ZQPtx3EcPLZVzNLvT7d4p29gs2u zjjmYq+@J`<$v$6glsX|KAjK{&5EX5v}T$G}JY+TvG zv7<(|4M&;IUoWW^qD|~`iDY?pn z+O1E1uv3N=vFa3JT|1moTr#nL(XP7lgIwyu>g^YujEj1tq>xfjjn8XK%SSV;A=2y( zmC{~Q+kYJ_{auqgXade(oP2tGRNU#8)+7+p?EFQxT%4y5Yve>ONDKiM3TF)F&oPAt=Ub>v*<9&=yqv%GM)W9K zy}pSyaQq*p+8p)-aPxi@4dp&Ax895Nt?b#eC|emAIMkF&ox`8A#oT!DiwzlLOd0@% zEK;VJQ-L^nn%Dun$eqE;?9e#Oqc>3jOxnr9M|Bm{*P8IOtW|4qM zCc&H;rq$v`f_3Y zPIFKk`Tb$Kgbw}f`Qn9Dc0Gq&tV9Yv$(~;0yNg!II>}erJXs%qr7Kiz{o#uwfBqu- zIe&3uf4sTOUs%6^$|7HEe}9(TyvVLTfnk!vLNk$ayw^Ngu5Qw=viXe^XTQy}7u%no z!9ly9%a^H=%VAVv}O94*p~@4^NV3#Fz$>)~%4!FueyoEGTn0yP{ro+%C0&bnfHibvo zGFx}Ph6y@tKm1?`MuL`x#6KZ=a=n6i9U9^MmX-)z>$9||+@|9DcP{cDDw;@jO!TIT zr6w(fppmqHQzbu<(wTob%0fk~a$L%?^8_kHgNe*NKT| z<^a*Ht$YSfx1cUBvC3Ae(XmjsF6G^IwLf zn%-js%%kKkx-RC__}!)UQd?cto+jN|C6$uqw%WIt zhyhT4;?ufW*;*HVwda@Qnc^*MpL-B9!;KAD$e8(Zr%S_~_;eHg^jm-&jHIX8&xJk;qfT)v2z#>nkOF9b z0*HIKEu+_3b7fQ3r5oF*fS6VW^=MDFHjYsM~>gV8l7U4V4nSg zSL>LNA^R}&(Da-)LKiu^Ij=^5biKWQ`1NP*Up5HpvG9JU`^?ybgIKfyU+osb8A#n; zvy1Y7 z!^!0$+2~Gn`fvk*OzgZ@Bcr&xNJ->uY}BscB4kqW#v&#GHz(9g9~!U;y3jm@rT!iM z{rTOG*ZuuD@z}7Z8#`>Er{;DF+%ddZL$O?Rwx+4MgHG8m$->&OJIAznJX-`GZ|9Vu z@S1pK4Ig1`H+?!(M;!DH;O<+04W6YZ67r*Ou<^iE51fq#28>EOU{W4MZWP!45=9U| zd-wP64sH&9c(*_7_XoqybTsJ=`_oDM-NCO=`qp^d8+S|@eCdzlm$3!ix1f97ahEGj z8G1CDjyhO`ABHaQzycmjyWJK&L;k1H(?~w&PoJr5YB-MJ$4!Qw7NSEwtC_U9E?5v)f0jD1I?B{IRSFjqi%r@-d^)#ZTAb~ zbv`(bHTa5M^F@%-PU{|eH||YFX-3w*cTBB&F)M|*4s1F;i$VEf>uLE{eBs2;DAAw6i_`IY&uj;^>)f&-0RGWzijwX z7=Gly_qP>CHvDYnQAg=2Q3`4H*oGf&?F<_fu?j9cv|mYWI2#9lr9O3a(hEKcb3e1d zK};i$^hf=XQZ!MTuA?1D>xXs9EeSFMo!9wV|pn}B&#-s7m(8?ot83f{M%XWij{P&>`D33N=Ck?Il2(9oghcPLZw4l<>F|4G_Cv-}TnxhvW|wxP!}pVYdep*Y>1F)A__+Lp}6 zR=e+Zj3P}~mU;Rayrf~h8wT(bn2=elR*S(7DrtNBdN$PX;Ee!QPn)H;1%4)eyrpSk zIrMZqa@#L|Q;gGof&W{EFXf5uF?PWd>5ELo?y`FM$F4w+C&QU?FJMe{C-qC7QExOE z$?DXiJ(|@oc>sDa_R$`X1|jH?4L$A;w!J3Mu=oi~zO)i&_mD6T`rdfW=IxQGG_>mLQIIurU5{2du24BVZHaSjgK0Whr> zS-tMSnpr}F7DoKk$2BmtwaOk}S%8OKQ5pt+p7mr(^0~?)YzcN)!nP>i?T`h2``wX@ zO(r>+`ukUUSimhEXK*O+HCOsr8a&hBI0J93$iO>r1$^pJ&QS&PBbRxZ(yg{nw@UK2 zj#@bnwa>_%jwd7T(D860ir*as2kdm#(+UW&cNucSad5y+r_+G~o^_{T%0mkn=Gk6< zd9+U^UD5pMEI49eymp675GGgFXNX4y2P*JtS!X6VP~3V4D$ER)Y%5@<0s~(s5|m=u zim#_zIJ3nA-tU&lUbuz5t_?KM6k&=CQ|!6=11kp}9im8obE!4f_kW0Og;NjCN-8{6498{4*RV`AI3ZQHhO+s>DJ@9)ze zgQuUFQ(fJ4>aFV2UGFQLf1&1r&U8@){RvQ#BK+s{rS#Y-4-|PQ`P|?!nx*rQO@qde zH-7{CE2~BRTP2}vjOw!O0^lypaq(A6uw;$zN1SYeN)gg4@+mw5w}lF+P(dM_qv`kJ z(bdXuU(4c$UUIcnO6fA%Yb)|n8(Tad178sp=$ne9}A|9wDOso4pfd1-G4UGCWY2qm(=eeV)?%M!> zkX8`?HT!5R9?4Z)Te`{5fnZwz(Y0$>7iHwP-8#IU{%w=R$PD3i2iQ+}o0ofJltYGL zEtr3EuW0us+W}D0Ob}evIl3^x=H$S7!gUS&kmO#CE1y8iyvt&4WYCMpIlPB}#|;Gh zY<4`020XI+wvRvTNJvwTEb#sdl|S5{2P^_khFIS7ML`f(@I*=1-lKv5jpM~Dax<1Z zb6R;sYbij8lWa5EHzIy{ZZjn#?1#Hmz02F54HT+ zL75YapT%1SYZfpxE6qjBppUi&du2@e5t^#7QH|x_2gatN!GAip&7!bOl&rekI{oA@ zcRLSvn_%e#*%Wm}a_x&p_s8dWk80~>0daw(#wD4+a}(rxbAM_}Hy3$S9rSbmF>YPt zzS4T}WQeBxlg%gaSA~=$7=AU7^d}mZE-X{4I~G=l;N?ULo;y51%1zl@ht1zx2tkyQ zNfQ!+hj@Lx1`=>r4bSdVnax#`onwf%%4IU71Xq;vz;*fR68LvqP*-+p-6~OZ=)&mm zJyIXYi}FpLi|XP&n(OBHBg3jAX9^)Oa7pc8#7zh#2l8Nn^18=Bkn9~OP%57MIc({) zWyxZ`xP@H6>Wq9*hw5J-Usd%q(EWXO|7fri5^x4{BJ2ZoMUj6Aj_vkr>@M88CINV# zP*d3RG9-_1f8_5=q{ck%I}4)aAU^!cGDA+|b&nRcbhZPBg_SGA;dYau zoR8Ld(94s?hdTF*dIivVfA_4p)GI|I{zb|_al`mXn z@Dd?8Bw(XPZhzYh0hIezx3B$gxo08Eano!}(eY38L!_%t%W|uwBG!Jxrp92wwAC@} z)0{{J;!xzLcI2mh;@_N-&tB$!*#FX>JvUC@hzjPA(eB)_jhGO~2b=Iyg9*=0x18st zz7~wl-u1dR5Hz8IHjz|Ov)c}xL-NVvy_jul%$wEGIp@6RQp*Y9oV!6a8ZRqmh(SLc z^RDZQs3EVyg*-#3r9pJNCPf#5U5hFRoVJ)#n{k4K&hGlI!K)HdjfK_V?8MnPD{1-o zJh-DS8Mx2s0`fXGu>gPZJcjr7D0o1D!^ZpJ2#J1k?-Vy(;n_U4u!Es0qg|#yywyqcDsqhJRE9X)?feKEeg{2Mt zJ(`Bmm?y$#aptug>IicZ)xO$x*+jJ4A2E4A3B){50K83Na-un2T4@DaPM znpGVW!h#LHi$T`315MTZee~dgJODZQK>4%%%6Y_VFir>f6J;f;f zR~*lsy;df8uy3{U`tyxW+J6&G-mT{@EZz29qqGh?`pQ{x`1H1J6|@98cIc{&iqKdj zQfBj29sm=cEqVI-O+wt8eX_!6M%CRFkHgB&fEQVv)XSFBPz)l+C?{AH7cg7=sZQBB z>oqB0*4}EPWf3LkUIUE=*!>QonrVNYAyV5~(=-%yN(KZ~`fclu$ynf*vQWrd;F~5M z`IAn?d847#CLZ6VnmaQ@_1kY+76p?h8S?b5daKJbh;$Qh<-bz|V z=<~z}huPjhxSWTlR=Pf$aAp-akL|H|lieKPgc%)DobMLn;)uZ7p!FExVX|OiMy@Ic zxUNE%yqcXLzXygJ{ZdtZ<7`Q@{;P9TbvkXtvLPZD&WCIp!?DRW4t#| zy5TpVdH`V18))h2qKO61!JUtXdN+?i*NxY(+R03WyGXj@O}*<~-M2&oU0Je1@lXil z{(gL(SwfaWn#qNSq}R=X_0*2lXMhnSp4+sl^0!w>CF5zoZ{OU#OzHIEE%1P{a zv@oCdb-A7pmchYu+Ys3rymX|-j^G9EuG+$iP{Wa11}LBAoB^&rcwsJnZ}>uGnhvFz zHVW@R^OoclEB-Z1F=hI(#eex-BVJ=_FzKrDMo5UlTvPP35~mMAEh#P#qv0#2C%o$` z^yQK8Zm=iVnt&nutHYWQSv!hg|CcrtK{mw}ZT(l%4?&Lr*@rfWXsXe>PYlAo+&C!uy891XGOUqZ&+s$ap>Y9~C&!{u5)FKL}C7iX76A*a3&nXp=F9l49hf?TY7PiUVO^JWPxvLnRlg{0h6_X zq@au~?zetbf7RKFiP%eB;5R{EU4Ghl?4?2C_H~}JrpTJE`2>MLaxs&Jz+nEiVk|7I z-SK={a4H!bC3=FL`EavC8~=Q&?86eu>?ATdpD7~{=fFFgUg*># zFYoqb-ZUC+?$6rEn+5W`^dnzytAmEs(|h=z($`c6My=zxV6u(ko95|gf0iW=ovh(t zp&86~_V`Ava6XTaHhKqb3N7c0_X!u{{lr4^Sg-6I>jo-*vc17|YAZ1#JXs*XfulP| zn`h@u+IZ&2HRwR|v>5)+++YK~8QF^$h0_%`ue@2xO|i;MTRRF(>kChMm;ixoElR`y z{);q$4c-Arjvk=^*941NwBv0;DFarhX3$VLYu^a?OA1}3d$!BU^{A3$f%~QAt$;`l z-V)$;e|on63bCROqGzBSi8cqxEDrt7?ruMB9t5VSAI=fMK*y^rbLv_fyFboyG1X{K z@JJp%6|(ZQz>yq+ZCu$uOhO_Hobcvwitc=~z&!&+hX>w&KbV*NgP}sh0069CsJ0pcC(7a*V*ddFf- zYSE5(y__7G0yoNYZAH>U9jy`#R7W&%BlKh!IuhZz`BiBYhEph^S)b;>Mqfx}wDm_n z;WV3pFFO-K`0E-S&-@X_sy?j*zO!94G&IiwZxS7EM~0oFjx(PpSklU~UxLdaz)_$! z=n@$37Ia_wDR*we9b$IgyDOJF^T^e9cnT^uZDI?9GyHu(pv5i7oT1OdZxl(gtmHR@ z{CmR!MMK`*1G}4h7|S{K&4T7ZI7Drrt)so=3ak}H#!4WBSJ}awOM^=0*(WROy9r2v z)|2`7u)LJnI%#QP=3xIGFGZaP{v>G-1oc#&vtar7EztTf8-LsW75=@U)u`;BWVnKZ zvA=IP;28qv$FbT*8GP%^E#k|m7d`0NN2;Xp$^{ka3%BZf;#Vpy%0s)0+#WpG13kX< zRpx^A)7*`xs7b4wGuvbc5ud~ml|SJov18RGQ$rDvGM(JGVf-4-WpVWt5XGYW^kz-( z1&)44s?eJ42qpI8ud)HIWK>k0@dF4;D)%ID07HH!jopOz<%Gl$?MAf5VN}?`4?-0n zizVoFq+nMHn)C{lEG?e$9R25wBFjj3uClh~R(Ftfm)QJ)v9h*@+azcWVPmED?*zF2xdlv5WwN45VFqrE?UNMR$fl-@pyukT7Hs*w; zNp3YN;8-FGBwPtaK<^&vv-W0viKds0C-{5loOp5 z`*$6U-x4lJR?{}2z@>K^)z>n*vA=AwN5ATwGkvlJt!O|F#T$JyVL*x18P(3PsY&%v z#p>B&^~T7J{(-7~@xn8P(cb-y{@Jlj!&30?&m7&VJV+~8>RS!I&a$2yG=P+5SQHFo zw)4=eNM?A;P(RruDDlN%FdC=4)^9iL?@MDbrYZAJewLX;FW_2akh6mshA2#c4OvYW zJaz0y)_JqDq9%8If?ARQIEayBd9Y|oU5$kty~%FP-sh9xlE)O;S}Ag!`6KB&ITrKhrHI^&yKX-LC~m{IZ?{uK36SBgcL1fsHPF|Q`@Hx zA5*_sqgVgq<2QNJkGe*ybcB(os=H|3>;1(`AuPJ&wQ7nCEjQI)AeoBlAs93zY$`xu zo=syx)zykIUxN~(tSY%pP&)fuJ90}G0Dh z!+oK%xFXEX%M)zH_Ch~WZ3J)EvyIn5*0v?g_{2ja^*eJ?hj?)mh^Tg$sY=y54me3}wsu>MI{GRc#%buKmY z5bfSe>FBbJ*U*{>p8rE>9a-=={odDB2h6T{04nQZqpwC!nzn%UOOn2~X&6m`G>qxM zr%c2#v&_Frufw;-Wxm$Uz9mVh9pKH}hkBwq$K`C0pS+j4SVP$^u<5%eSuqjRYdX%* zp=IMv!{qrI8#L$4!QeYV=JTO)mKf@}{lhbAH8>^uYm$O~Q7@KiR5LBpmbbT|N6y^d z3Y0MT9XPKKB}@az5}lkk@Il3G>&Qm`ttsOF2e*37H*+f=AVfs2Ds^FYIdO>c>A+3R z{qCvOeSX-;S`?SHuvWU1o5esBoOw?>fFTwp@Qdkv zaPTa?$Vj1mp_i|vg>*;3D$=h(TAV;SefBuPU3y&0ZiKE z`{BwSS34hhDQnN9Sn|@D;i7Ppiw3EiKl`FwI{fx5(1qwsrFKM3U`6NT6uHT;*qSfYc1 zn`7{wrx*;$6V$sE8zK1LS=sQFpH`}(G0*nQWAIAZuPISaO>C+r;@TAhV?Y8xLEv0N zYAC?(p1JslB^Yrxf_q;?cW2h}Nr0i=sgOt}F59sWeuIId$c|-`5`~G(uIpv$pCF06 zvvR!iE9F_N^syC^w*$MZs{pxj5!_!AcW{QcT;=%B%xPSPCdjPq&#wNuKf}opvKXw9 z>~rVrQYsiGF^RcGG5aa@C8&I}1wg55CS5mRlXJ+FtFhbJbK5|klWgq3>wvHp@!K!C zX0Wr7cUx%^(OhG_BOyz|0WognoY(c1YtZ0r8gQt;ImD9I|s&}f_Z&5mjR z_^yRv^qq%JXRG8`8bk@{lY#BhP!@==J<am zKEu$J6d8^0@~`EO!zMPcECH%z?cSzc&Dxw3f8saR52LF#xFwBIMuBd4wc2)yRCwFy zN?51NCsv8ncfG{41Y?_Zt2IWs`jXUoj}d8re*b0_FBoQw)uZ!X)+K0-bYiw*_)*LZ zZ2GoBD1>q0V6zc9*L@^TpRWq%D-8_H8W_;E?~Ek21RYHn2!H=!rl%ARCxmJ-Aw+Pv4L*yy zXO0d_c*qBomH;%_NK|r=O)jVNH>8dhuI9kb3`wnCN??W}6oE9zS>9u*R!|WM{*Nu-Bk4;wWb%<1PGf<6F9jnD^OB{AfgcBgH z+ztd0ECDc`5vAdsgSqeAV&PF}+j_iYeFgvMPf$EU#6x!c{1c5%l7Hs<mKLEe5=*yq)(t?WX{}d7a#H)sb^E`+@gkI9-gJKxo#dXq=EE3;p#C?pM}8 zC`y948-l#)F3KoEc7#UkORRO-2@s54KF;G{87XsezhF2 z9eJYPRB9;rLz$`^mX1=yk%9ckZOy|ftap9grx^=pq&&5>@C>g#?-P&4G_%)8fUPVquREc&L@34w6bT=P$;ti#)XEP@PF*%ob`CpS z0gjjotZtaYf6vOq3r5EtKr_#2FwpBXg>(KjX!h9p{3GI`0UWkrK#oV{A?{V-90KZ` zla&kA=m}6PV>%xoD&1g^Ave1F9>gwogw>~nE#XUGTrHL#lJZT!xjbpKku895x#PM3 zI-TJ*+o@`__df7Guz{?h&i7g`z4uJ{+0s;d9`u%S3tHGx1`x6#4PDA$(Fp>|>mkwv(%6CiGMee%VmxdoY zP2N9wXHj+hvtP%u_Rma{wU`JVe$z|t>IK~(`6~W(d-d%<(dkIPJIPZhY8>6J!|9?Hw z>0t$5{(^uE#l0Jo1LJA%L@QG(R{>Dsg_aqyB(x-LOC2>|AJGsbKE%8Xe4|pW-AR>+ zqSs@H1h>Bbc5Q1`s20oOzT9uuzTTa7wYQ|3Hg`YodHl3}zeu}cyV&k3fLDjtL(hO6 zNwe!A`JqnDwX~Kdm(^A0u!6THcjvbORqi$v=Q^*)!^@X5pz+g#^_T7GSJxNf+7I>b z(Y zEzNb2()(8d^AEJEtk4C6GLNjXIu^EkS>H3bVrws_>n=qYBzwdj79Gx9){aPJiY~H5 z?=_{=!Y*!cVRu}5l z7UnJ(0`1ag`T+*HVMbh28=SOF=dRYC&P^KS>k^;97C$CW>lodCs|HfdEr7xzf>|{; ztYJ$EdR*eq)}w%3v$jo+v1{tltNAIO_YlR~q-RRP0KE}=YZB3ruE6wa?yZe)*7xqH zNg(te-YmLe%+sqG{L7AqyVJ%tEz6FF*bg6CmZh)*H|&kO0Z*0jBpTiQNmVNiH{v(e z<`p;K@qCs&+E~F^$Lig~US3_RV+W_$u-{wNzn6@grHx}5;M6(B;us1DdRau(+Tq#* z_HOQ0ig(>-jcrGgnW!8?56)&XPN(^m^iW$v6exLG)_q!wHUy?mxit~}Vjk_b>)s~9 zQqxq8k9gGASe?-oeh1^n{?OS-OWQgXUehITYOX%#@x#HTkUVr2V3n@2HCeGU;&9Wf zyUEeuAn>>5wWnk0?BMKO_ru|E>!aNP0Y&OIM|v`gN7@Lm@FVkewr-HrRT>^$+1TuE zNDrl>^k|)SBQ9(y;jTHk4*w*xB-Bl>gKF_>Y6EHt;;Y?8JDF8GvteQ^uH>%I{_X_W za-ZX{ayJ;s?purDsywM(`9-eF{*i5+PI9&r+&IJupBv?T-dDR#!xc3J4 zrM=_ou0_gD%|sKf)}$lU*rb2fA93LfQvWR8nm0LLfKC?|XY!*^*I8QSb`oyJYmX27 z^ndm5_G>4wZ;#djl)E2SI_Q7_0`BHGSlB~~CjIT?u^W`G;DHXm7pg8tUuqmK+8(&x zC^xy?dER^4Qjc6Z6s}0=2HpH`SVKupt43xdda|c2HH_=NR~*+`qdpa`o~e<3)}sbO zE`*N;RJVov8TwMNcx?;yWcX@ug8^Ek)MOo1-H~jg@T^NU>`Za~OP&K(g)J;Tc=6nP za2h+-ejhpz>oQ-e@UZhhyeZmx4cz!k8#b{!q}Q_CiF}$mf^D_2VDz+v#^c)%ImCxD zVK=iu1IC2}=+N=Ffri;2mVC7P@JQP`7-XN0M}@18F5&sKVS&iX^s>3xZ@DYFpct^K zjrtv5?&`tQ?lijB^F;{68ils(2>V&CQ5h6(!s=vvf1h_3??I6}0}xul|2Nq#NbHZEu!@Dab zPx-o-gA4|WQe0QyPfbbjcQgO4(-7^n@u^;T;03XlWsGd>a|15$Gu5V~%ehxO4nyI) zfGaFNAYd+zY+iw1{-3B*$9_IT%aKBtP1p2utYpNJgk{Kk{BcTNXDNTOM#A zA2-c^6AQz?6H647TQZ_8Ffvb^lA~i5J0?>}Y^b}`pvqNL>rV4PdBSGYRp7=LFCmG5 zKWj;zA=D}<{_qEQ(m217&=>9O6)wx}Sb;UF**O z<9>fhvU6g;nh=8cisY8`jTCiDWE0ynt%TOsfj*1MN+mh-F*oIUk>sp>rj>36wC!QH zAK%ik3N`cnugBSuU5$fq=MLS-4E)L3Toun#o6DhvmmMMS+^YJ43;whU<%#9)XnXoU#@!8fAyXbBK!E|8EbJnZWn7mMZt+Q1hik3 zkXjfYoVPlgHODh74G{iuYd5PTw>E9x!uUadM&ut5oOy$Y81NC(+1QfGV>C~(!@0L% zvyQ7TBOx*XNyx)Datt?O92%TNTIWR% zL<##zf${fe#nAGWmwJkXgd>Os-(+f*9ZSDG2*OMn)D9xfz-#TDUAtVo4B&gmZGW-k zg?Q^Z=pfHc9BHs&fk)DS4UGrcAGk$Lo-(7yO?Df=Jg5`Om|uDA?E5lcWT*0H-+RcQ zU!KkYZ@dnD%@}x7__I@AvR|>MY?E4&C7=ouVG5#|n zvNyftl}vcg|2$=*#-Ae-DH{1Y=YW?WCYHsR_iNJ_Uy@c=s+D=H+kYW2J_`9VdEA4r zIqZ7@AmK#uDXguW84kLsy{2@R>Kt>@!-)0)ed;wq0*=;c7U#&42y0N2BDk=+7Mc2H zIXG|HQ{%=<4N;RR-)XwSn&9mN91rgPrS7j_AFKk_Ch?{906`I19I~m7j4ju3U&me% z9}bV_6~5=bXWju|8A;xBTZnhH)YMHu314&e<_u7*woQ-J{?m}Ki5f}nJmBloaZO%8 zyN(OrgXaUyYANC?_nV5P{ax#u?-IAhBa*4(z;)G)B-i(X{kyaZ?1?8F&HlDnQ03Ao z?~RQ#RibBSgqTeh2|@Gdg?=NrRhXO9R7rVrmfSxomx@lm<8ROf<2t!5{krQaD5$TW zdG>vo(jpFbb}{>^l7DHLiAszuSlBkDU6@3e!F*on~E|YoP z?LRea=Jzu|_HtU60;>Yan@_cHIypyik#W^e9=%$tkpBK@tJ$zQt+>o^cRqA^x^$9n zU0Ar=i!$j%l|j2{Oh3A4Ww;y-cYmc|{R5L|De^ep^oIgO0Z@#D5hCcd+Qx1O4C?FD zMeZdIu^`@RFHIb|?Csh8)OFu=iMqTPIn=#T5WFKN4LGg)G1wi~q~e~QUlJ?hETGvu zv7Vd>Qk*NLCB?{JOP}o>Q9XT##ERxme&8?dnsh&tAN9wxK3DmHJGB%WV42hk>nuWX zzbv6?Y7WpPR>f0?ju=v+qd~U@3bb0hTzQ};Nv6Vkz=Ri!gEgm!PeNvf^)5q#ZSLd> z)uZ#I6ij&S>e{-WRqE(I98DV+Gn8v_GJpgZg{8=o0FthX#s!>e?fIa|P;9lJb-HLMTXTidO&BW7&WnP5JMRTO=J6OO!$*m!}+ zGc0$|#tn9HZ=8`&gVz-X0)E-x;=wcXP&(cHnTQ#A1a#EToGUopsxOnCt&-f1Jw<=p z2x*@=MYqt6xHaCaoUqdW`L9C_(-6q+_Hjn;e`XLV+o5!OjVH-mb;CeUlOR^EdQ2J= ziS|iYtx#-wOR1SOvZwHVmr|=20T;5<7PWW~Rk5E~+)I7jRzu7ootp2{_^h%T zAtZ~OJC(EY69aZA*$S?eA$&EE^z(j&(fCyWQoS{f6O*AXBz6(*!ZpWA~zE^4VdS6nV+k=PC&Vq;aDqK`;>5wC%W z&ET(0Q&mQb>go?v$(nYqj2jl;4yDfUI{`GSnq{5p#T^%Q_cj-*cCB4$&dfLMsY|m@ zEJ<#5J+wBLf3F^zmGP1*g2^1h9kc9gzqZ)V7D#RbekV|@_ z;O~}i?v${o?j|QY5^fZU--~6h(;H^_3fQ4y?9s@-MY+EjO8Tr(32M*-m_JD)>1{k`p;To{b3p`KgHP+CvRCYd zmSB@1<3Z7G`?9i>mELA`BVt_cVibU`LuS_}xXgwJ{H=X~0jOB9FuhPJ1F%BTtG(}f z*^zMDC7x6XpjnIPE9ST2hXX5Sa%rw_^O!z;lqgttC}WmoW67RkWGa_JvIK zN@MeY0R;u^PG9~GfnDJoJe13lbB`#Z2J?P976ieu+*;VYheyKWbmys!x_eTfQbjlfr_zX=VzH6wEe--&`{$x6+Of$0TF7YyGNBB)tbOfOf5givsv3$b z=Cl^HTPiYDpE*v4ycDYhe7B4ijOfd#6t@Cf?t3JR&_W}2Q_d=t{&Tc=}hlg5V4?=GsF zg(Wz&&kdHYO8hb3E$+vyzH#5J5~l6f+&0hRv}O?$5>_;9Zeh$DFN=3gAOgTKAprTF zV>|q^-x9q9sB*=zR==s(g@Eq(T2Pnn74oeqQ_9BpV6+3u>RWdR>+g?^u!#&C@b}pg zta(_aI*BC)fg>6o3kk(*{X-Wgzn$l?h|}$0wBz^3{zzX=IYr=WLbouP@9QdPYNxZ-5cNIKr@{SMtcN52AP!6Fx zIba;#wHXVL4x@@dR3HmZEjUNCJsNPwJ5mLg-6rb*!XZ@Q)MLn7DiZD~Bcn}Bn<4x6 zs>O0W{4owzEGVvu07G`1qwWigA#c&Dx6~LJ?(~d$d}I|J&k$J$yT-SKE>S24-X!?r z|8yj~Z|x@dWF5eQMwG7PdQ_dl{|gh)JYLo$ee%s=bMbki8CI#6)T_;NW-sOnT^? zJw@=LgG3JFqR4N|8Jw0iF(LaO?eaqQ229X+6EOv!p}!H+JnAl#NEaI%7u%zCGxS`J zt0&zRUQw8UgkoukE4sKXXB2Q{I$N+$s!l3Hyq#yEvV_!+Xihauud%wg|D6Vv|4hT# z_cZ8c{BIgKeg!#Fkmc$_L973hmq^gX75QW`l?Zg~6%TZb{8YnS+z}`iR4I}nvBO+s zXL|Xgi`%gfzmxtg+Xmqn#DBDxoh0lS2$Kp}*-0l_b3vf~Xx+i-A>!!f9)mslulnhI z#gzE&aBcmZo+CE@Q$PJ%{lNdIZ}UI(chW+h5J!{)dDDL%IMfdD-zc!U+hIbpUpi|9H?Wuu#BB1iI1sDhY7X{VLTw zWyjAUNI}x6FA`)+rhQI*e)n=L9_a8d4Um>dm?0p*kPmJy66jD%LxrCMf1-djORqL@ zOPH}}%c8|hI5L+{k2woVi#f~wYHlbJgqVXyn8ANG#*lAsa=T23YtHiS^ykk0$8L@v zd-i=|s4u%OFi^lWkB~8Wj;hx0XnM8^YaH}3!zRlR+CEjVl(mQW;w_P0$>qzxL8`1@t70>EKu2@|)>G@%gz#Q_*6^ZIIB%^3CB-`IFjp`ua^Hck> zOJa_bhjUuULD^SB+5dn)_?bFes$9Sb_P;HlG7^u1e#VRm@s3cdyUe+%^gk3gjV z2;^rAm|RXEOc#h_@b_c(2j|#rt3`^8ZXqzCFo3oSJ)dQ-zdwVr;^l%mFG;tNi~;Tj zU13z7?E9N52o9P@;#7_B+-jB^&c3EW5u}>6tpx8vd8-)(1?JLDO{<@F;$}-L6?p@= zJNv;|*o}7vY>up_@@eMuV2K5(+kKmbu zclCEu@q~B#Nk_wd z$TQuF+&K((OBgj7g^qU{ZCLQ`r*RDT5Ij^=WE9TE! zDp~cE*oDPPAT~C*Fkv7}8O^6}$s=#yk1WjVQo1zR72u6~^%GW225cxe%AX}mN}BtO zRp%+-?xsj37_b|bAShDGbuw+L@jHV!w?wn#-$sw-b(17O90?DmV!OMn%o#T%*M)qP|5@AaX+t_io9`SKp8!HdL^VOU=zI zOvrfKYUj5Kc;vLk3Z7bQ$iAvjmHY*Rs-Dd1hlZ;8zUJ?k8WVs6WC7V{?wlIAhL5o8|I118$xHt!GC?hIz@iW!EOI_s~A z`E7d7cn$&1s@mA}QHujmaxh`FxKTe1Iai#I207PjLf%;CD7AN(s9U;vC4m_a0d-l0 zzITjK@N@$$QD4`_Nixs%@9TTBie`Ye=1*=5e5cTV%}#&-PL8}1tcb+i(!!IedMuaJ z{$fTk1+0?%QIW=5GPw+v%~S=v{n*jHZ&xtzscp{6oLXMdXNYs> zTbWa?1cl3yMa+UGJCO`fm$-S%Z1GcgZU_;>;ytu+zI-N$hMZ$H@hb;Zffpp%Qsf68 ziV{G41&~V&-||lb^tPm&yEjb<*E14tLs6u918CGMOsJ`Yhj^QQYKGF0elg+=(XRy5 z;$a2+R-%O<1$@X&@&#y?2hbH!uk}|YmwDl$1NZNP(6b9t;-)>uDE_D5T6IOSZd@yB z0+$3yYI(zd3l_TznRyF#y<)ibOO2ecjMPmVKY*}v??`aCB&FC&aw~M|p2J-Ga-OKJ zMGE-K^TIun`=UE}zM(Bse=@@+aVI^`-%dPjc<(P!@4!HjrjJxVxH+3&T2yFYj~2tu*q>;EZ(|c^#g#h@Q~|8q==u93#7&b~_KA~aXO0;Q0ZlGH zPo=)j95U9nL7uCBV_D8wU|9+-qy{Rx^&^FHKY+#g>MX%JW0sXw`idssE5;DJ z0h_IGrV%xc#7zCfu_FO0@VIfD)oVC;+6Ywa?XcN}bt%_+^s z3tE27(`haLKs9B$2gJj*&uH}7C%XKIDFtp`hjX*Hl}W~E^qa?bY#+O6dgi{lRf5+- zB|K?~Bkg^pAOG7rt-x6g`_G@D?Ej5Rf={ZNB92gP8x4S*K~}fRH#q-+ zCa97M!aVkYpxd3E(p+}E{~=TAobSAEId^Iv6I0)D{c2%JS*j^oJ;o+s`u`#ogroXU zs^t!2Hqs)>(0!i&gVXqA>3N}l)$(&TSc}QJ21P@u9lL(xAG^f_K9Xpv1!=x?H&3qx z+Vd|1@sEdP1Q_cBRn}ve>ZdSdSA8$$O}q2*#9i9aJ!ArD`4C61zv~V`sx=@-r2gu( zNZN$_-Ip}5MckK^Egnewf;7Es?h-H9R`4dDU+ADFDIm+!k1&diAVe}L=)_RB?oFE@ z9OEo@;3MH0%+6!o{;1eJnqaQ&5N91f1}`oIOeoV9gudJ9303NX_?VpKHhw%s?>Au+ zqNyus7FMVD~wr-om?&UsR*|JVw%1**M4!kv& zxVX009=U6K$=*Qbrftu?0kCI17Ylc_*8uzmp8L5v0fN7{7y_Bn658ymjyfx?S3?yoFlmpde+qDR-3JtBlrB}L7q%`H; zH~yh*_zQFHZ$IuS6#)0P-Jj+RfW=3eiU3E| ztMDzhOfCX^y*`}~hzPM*##$#&2ZDj`1k|oi$7O^I|JyOI-}MA}|9OCps-?qjEjcY* zq)$s-1j}L$kadh7b|@ttBe(C%rijvrAN`NFx#cb7B=GvAwR1?UJOM0Zk*iw*wHSNQ zScDE_^%4qWf2CR+5K05J{y)DK_Ys!t8O-B^mwfPyid;M?4$Sc>5{(}PZ#U2!v2$$v zKK4Q_mLa>JA!kf5hZ$`N5T5WH$YV?m+?uMV`89<|!q-0{OE4-(T{h zmu16jwq@}y8fT}~Ei2|`)kALx6Nh$~g(*r{V=EXXN*MDe$*!eJ7^8tF@JtUD*T?XR zH`HfRMH{mmodutS4niTfTnX`)BT9JDa+mqcC7i<~2}3j5l>Y(ke5QF@%0q%#ywXZ0 z_}}Z3x@7pf&qc_Pz7*sL@fBR5=?F;Hl4gi@q8GOJOJWz_5M0XdxLykgo)0QC0cb8c zRN&$u-4V`1H;W090Dn=`HUx4q$(ThDUB#<`r!9J)6YyC1P)exf?Magb<~XZS1Gv}p z=CMdZ9~I=PN##9F?BH4y@CE;eKeAy0#&EceWQZADOs?kw#uY2qYna6JVtDN89%SDc z*72}v&!}NRS)w5x``<>8f1lJI0G3pUG#&*<>!MCc^RImSgt8{|5we34r%xAp=~^2~qT?vV|?s#M~) z`Cd;#lasEIXZg+NBP2oxwJ?m1Bx?OjJTUn7yn+9T+`m)PZ_mpbd5Ah(pEQHhDvCJ) z?7^A&ik+FF`o#22>1)ytmUiE>$&ZX*?Q zA$o;5Ia0#-pgBQH^PTnPP6SGh*-YjTVov%n6Zf&Jq9;bl1lm&|95GSC95F@7gNTxA z7N`%}e`ltH-~9;xZ;G6mhwcRbAOFupzQ-=I$1WK7?e|A_+4&O%Ov?xvGxI2-<2SY0 zv%ya}ccjK(k5N}T?=nF%(uu0K7T!mz{+ATr3v;ZlpPz$s@G6IW`##n+ZU{x5Ti2=M zXJmw9Zz+~9w%=M8#kmU!q5^Ne#;rIvf>^zbyM{1vTg~vgN?5&AK>9HShCYKpy0iju zicut^8`cq|KgY3z65J53y)T^^p0LjH*2ke%$GFrgBp)!XdHw^6Aq@!yf02@+y^_;x?CZubou=Yvm!nK8bSg ze^S6t-ZQX5z4I~wkSSV1fFQoEJUs$*4#>w0M+IDQhS~G%>SzuEDV^}E=`5B8GWTS?_P+m1sTdEP601l{5$sb=P398l~TGdhOx0F)JOe@D<45w$v_eA#BzA9Vz|@J?+pqtl-j8IZW03AA4N&ug z3h#!4AITb^V_vWiGbL|&%-!)ozS-#=lk`v}2b(JB=gfmo3_=}AyxD25S+}PL5T>2@ zCfu7)wFxb{0h3SU-+1`ic97+(fqjEF{Y*T=Ka#7pD*hE23DXk_4@EET|4?xmAvpYY z9Dd0jFeVZ#oU6S|zW2&=OsJZS-2I{jBpTsj>tPlPprVI=Dn=mb>_j(!|M42%fxAo? zBwfp|a<V|F;5)Ibl=@{A^s#TYmpJ5Z`O5@_o()fr!@vn3S-y44IbDR9{a0njD6BA;Q% zYJOV2_5ZHc+JT<`1JZaK5)Ppcpe~gq9M7MV6>?@NL(#*v%R|w}K+?}7Te#E*to+~U z3jNx430mGInXav(o~7X_f`Aoh-2a0b=l-JO z8>*|HOQ{2U>^y2S2MCxt!3)yE=V>xYo&qnFyS@LTg)61b@|5${wW#DhU$HuyXLR$! ze>RY`;Qy6enUJ1REmz4huI&A&WF!@M%x`M?RFL|C4H$jBx$Ai`-`UTd#Vm#=R7lqh zWhyPPhU82SXT^3_2TASmUPSdNwCBXbS~WhRJv#E^307`t#u%FNG|pm6@)O?!N^mh% z+B=`POr>eago|Zwtqkzo@%|zxI|Jswd<-q8zGQS@GeRRM`B?{kNW}~IceAV*#dD)fe6Jn`;s7pbo8sRZ^y0d?``dxZS4keY9 z-g3&5x%NS;#Psh;Fhu2?^*Ct;@W2-W-0L?8D|fjdKW*&R`9@+4ue3{=CIp|NbJylu zglyo)SG5+#1_~EF@R8H`SG%7_0B&82IaQ9TpWOcFrM0uU_2p({7TC5ct;Bkpd50>=>Y9y&GIQQm9XU0Rf+*c>yr~-%-8$Z7VTvtrY`24uP zp@)GDL?r#HhYPN@DNGaP7f0$xO(fEHV#It9Mt^*0=1G0{^H{KeFL5!m{HfMv^<=9I07MTw`tP3S1_V(1s0MRXpv5luTXfJJM94~xm4o|GcRi%g1 zCZI6bmi4jX?zpY;ak?oQ_(ln=TyMn_(_)k9slWbLsB-hUo}Ew74VlP_8i+iJwX&$A z=hW$c*E^AqK9To0{^QTm=Ry&t*iP6sRGfS;d}zDQu*CY>(Tmf?${h63z(v(y7>QV~ zi%Ji%?SEvk*uVWE0YG4?9E%XNUQq*yaF><2%=NUN?MjkYGKSZ5|mMl2ypvX#e zcKp>hsdQaCS8X`U{K|xM67S}TWXb;&vMzOqKE*M&+~USpeZWeob4DPKgA2%n239)G zaD^vrPIoQcZm92{PDC5xL^Z6s2b$|nL zz#lmp75cq&a+ZOKqMQ6LOU~LU&jC)XDUS%wG=Pm44^FX;mJ5+_RPv?E{$J)iyc(tV z5)+4j*aF|_G7L!xG33+N-Y&<*rxl^H{8_Sqct?I2HzFY3{imh$#og4>V>g!88hXc= zmh>!GcF`J#&Hc!23>}rBymPLB%Z6FHCVEqu{-1?RO{Q7>aKaXD0`b~X` zv|?_Dmjd&7eN{!mM5plkDm@>D4BF)v$=F4F{HM^!=}$y+5%vfmk-3=-BK)RGb+sSA zHrk5)E_xv04*h9a#D@Y8%{3J)kjt9`B|{`__mDO1&nlLro#Od?V$uJ|(2-i$l5@W* z=XsfIz8L_(r;PUz$>RO1V|+1pU?gObqxf?G#XvfpMEK?cU$TuG7o7;B#|IZq*pmON zsw$FvJ6TpHO!2i$oOdN`%>0Ib}gli}3rT*v4Vxa^;h9edd|dnV$FS zGZBC&f2Zhin)A{%6R$+8>tF6Mw%CKxVQkOLl6GSFWuK}3Hgq5zKdx6nQqZqrg!9`h z_r?lB+9&8sbY_wL$h}2%!=kIKf4+!A4bcTq14Sq2gcO-MUY~h&WA+Rk0hm{Ubw6qT z+UyG%_G4iLyY%QwZ-uv}zw_-ZRiSkCZ%7 zZKR)}tYdCt=_Dm5k1oJrrxvjh==_emlX)r|CyFjIr+b#z5;JieC_O^#$Mqq$XI|A2 zYO2u&^iHXa-F^ArvYz4btTY!cYjXg|))v#xD(ohX+`d zA816bW_N=g!)a#Vrt?$GcIj(kJsU}Nc;Yu4_=29o(pKquTmAT|%O)*9Rc(I`Kclo_ z|5$Pu|3Wc^b)N(l`GFZ1JkRkba*_GVRJiK_W#NyX7k!kf{B+Y@0YmQ7!cwvcn6zFR zr(z6T#r&ng<4_Hr5Z0O?JdYSVI$SbK_GF(~5g*&U|8Yf3I6f1u8nF8F*iz zG)V&|x21epopq(4FV9J7U?KqxllA^^B2` z=jlNkcSVT>eyG}fJ|K^}8%N|XCB?JP^jfq}INbh=|dpGZ_I_3Wq9;n)2seE{v@sIXcM5B-;Le%#(oX7HrW1B4rXdU}^ zD=8pE-8}zOL>ElVvV`nuEui_#x+U@gTIBZbH}3~&4?vINMEid%p7)r!<$iRNG=B3! zvC!@`+}J}S^>Ci>m6Esf!Qe%AmRnW~+{n!7;`Cl#Qtf)>)}wh(a>KsAr0gtf3dAxy zW1@m^R@!-L+~!^hSeA)D&`8m&8?+x7M;R z%{e;o80vTpy!fe%3Jj4UAzu|ox>qXtdfAtxe=ec)&F<~%KsoC^7m4coC{!G1NfI7k zc&f*K5uv945PoU--0g66sWDi$GlJFYzK*d&hk>iLKtQ~W5>&_h&0Kw3gLbWio<7&_ z*E?(FU;C8^Ip=S0)e#@l!Bz(mvDKY%k0i%n^iEeNOiahKp_OF6uQMO-P)-9nnF%s; z(tpd>@Rtcx(|2n!gKiXkFd_*&vNNK!K`quTY_XaB6^uodnyUh6?g6fI?Q0XVb$lea zm`z+FYGyy{oOuDB_H7J-Xk10;Zyk@u%TLj4fXKp%#Mvo{ZYOe-rJ}S zPg{Dc;wJcVEs*8op6*n##S@tVPcr1vKsq3lgU8!eCXM<4A@l(dLZkSl)!f$QE7F(3 zXOopQ#`%*bv{orBG>|h*$}>LyoxzUd_CTa^S>m>+KJ)m-5ZcaZfMlhTV-n_%A~;M( zmZB){WOh_oRA&oD_;TF`LIb68yaYJU)XhFLf~%GB*gu-GWV@GH)q0^VotQnkQxf~m zGuf1UrIM2${;hg&An>{-!kB8*jjrn}&Ak$Ga{ijXYsw4u&whb5wZ{km)hxmrpF#j= z6+EfKGQy-<*ItN7$LAk4QApEW!iN=4-D(>OZ%oX1`(%V0S4;{4L@T$954P< z$5c^~fXdW+DwBIPm6`Jd7aZ7x8(M!s z(O$8&mNw^U!->@qt>KGCNTjqk!n6eta3fV4N;6H`2X-HoHh2A37xQ6A;Ee=({^Hte zSCjA5Kdmkzlq^K@sOUh&s^L9{&Yk(cy!7k5yE)xDr86Do-YCo`r;+uC@pGBynb*U5 z&-AQdKE))9Y$Pz)%D)QuJe>`KS=@iC&#N3^P3N>~gWr5R3{SiQUgUJI~yj67ofm$^iRVn?ZnFPT+5C%5{1oH~!p>`(M)Gd(BIWea65Dk|{ZlM4}t>Tjosq!+o}`r;}UYJ@bF zly&$`AV;Pzi!N>TPAeEv zI!JbwV;(G%`2tERSO3`C?OP@JmXo@Bw;L9G?rx_`M?tzKkzM45k^s~z=*H(OS9c9h z<&?K!8C_#zMYaN)a#iA}DBfkVX}^>EbKr2%Cv(0<>0!LqIPZ_~!r6slO1d5?sp@oa zjM7w*9oJ2@K)-_haTwh=zM(*fyO2fqwBE5l>J@KJ^Ju-Wz8iNQ%|GitA1-}d=yrWU zLXeo@#`M+Ih{*9oR57s7eCc@(#Z=zKu5jieyh9@Y?=WOqe|1xMur}|5Al>a8mqscgIZ>l^dIH5FncB{EIP9MLxTz=lbyFo$_z71t z!k^n!@ z5c~wKpc>I#IqjUMCF$l&K5eFLm}hpNu$xS_3E+KsS{tTW$QkBbCJI)6UG`E!?F}2Z zw_<+GC+&Z7zlx9_*i9% z=joCPrim2`WD<@YYl3Ap3$C;Zr%HQ^MsZ%$KtJMpl;k!i7fLJU>k1_E0AP>4I;B|OC(PaZjQ?-N8jzTqO0zk3WsLh{wr z&A$FZ{fO~u2)v=S7~+D>0`1SlW6^juGSry%q9mQ_zsdeRMv;B^i~;xc?^p-H^$?u$ZA(FLD{7%t9tmi3p0pK{{6 zW4pfx(2i$*uqAvpu5w#pHDkK0pz4mwb#dr1BK=@L9Q`W3j8$|tHh(2@&MV5VdHi>y zK>U%@nfC-jr+sq(JZwN11%AdLo*fj*lZ$`y@)D`_b3_AMFXf(FNY4khmD zL7x&IT5v(egMxKH&u~8Q_-1`Xb)U<%Tw)nIA^1Q**H%2MiZoee6eg}g&5IDfc2$s- zQq#YLB%~T4z+)EuI*>bvsD!@PSODp>dfDWO%=}{6ex<5mfq=ke;mmu&CZkz$XH(f& z*dxH#p!x(eBr+0GbB)DqF_fe^eLV|VJF`kKe&Tc%WB%n>*#PHzzxS(k?mW)2eM5q| z?l7*7yLw8;)*mRaqY?5dwt^mB`?cBgGF82Vh`^TE54)FPlDX#yO?j%=F%U@=}m?Lh+~7%YxFgyr7Z5&ne95_Svr_t zC1|SVz4@Nx!Y|KB@R5d{O;IMU*%+YH36cC&%`k#;^13H`|O~ zghaER#MIX%La_ol+HRN$4IZ+a94BOIR+^SwCD-R9%ySOO_vBOicj~U}S`P7t@aYs5 z4Wi6*Un?%ii(n^?vtlmpsHwHhiJM6Sgf;vntUzQaU-Hd*t;;NKP!cMuUxmXqo7nXZ z2PzgHEz`6U4(Ry(F~fWZ^^$BxGmg%$Q!~X?-pbw&IsFi<_wM&nbP-_;JJILmxms8v zpDci8GsR?-i&@bo@Zstv5lL4?T!IM?q59_7&5z6K-vs%(FDjU$e-XDY1if{NP!iMYd`OJI~Gki|<&uqab zMOOBVyttlhk7cv;?}RNbAFF=<-p}3{8cZ7Y6KjaCGA@#Cbn-Y&0DuWi=3#shnpaCQ zBjBkx@iBL-B>5j5p3-q!Y-%ruw+s|me@Bm<7g=2e*}JuP9u{`A4mTIz{n@?SPSfbu zUB`Pc#l%*q_xbGg9PCWc#jblHcrLo7=)lj60AUY_JiUYuUrC`k?TjvPuitI=@v=(I zr^zCf&*f2JfD_8<{a+`Pdtz-Phj;yG!g>BNz>e+}4Db=Wt|7Hc!n(u0(Tk&x!F~~y zEO;HcIUK4?g%Xl4UHgFt#DL@U&kZnC81Q_pS;}m1RP?dZqOOE+dFG8tS!*2H-b=e9 z;Lztom@IiKbXt14wny+lxBcDZ!!sY_!~tgKy?#6UrqYcXw-{}e+~_8`aTTe!d8u@M zRsMaKAK_CFNaD^>bXqZ2GcMtoS-F^;X2X}&{(jBZnF{QD!zRx9sEqr~1wIAG*ng}pSkrqQ z1Y@a*>=+aUAap){4f1gj><3?{F&?r2e~Fd{<_k~QY`7I*Wun)iC+2!}2OmO<0uvOY zl|o)s^j^74dZ}~`yS?Iz4lJVzTY*a=e?1hUB3#SWK{MsVtWuKC`?Km9XmkRt1qrXCiDOL-P73&~oJ0K=Xy&MZGw1(n$n&$@A- zD(6-u4nm^V)-vD`0WIfqgIpF>p;SogNfvd-ONbVAVvd!Bf9rwk3U5l{i1$;M!`(kj z^=(KX)+D=*uimo6hP}QH9WGG+=t%`L+wo*&o=u8bHIC^drL@|DDTuOGZOW8HLDX0= z(J1$8lq73oIFeUcK_Gtx5J(irss3C3=2(M3uaK0VwWiD~))f6ZX_z7CKl#^cHzzHl zzWC#%PB4Rd^SMAGAYkCXiwscTw4vbKgzQ!QZ@Pg&e{cMqObi6oiAqI}j50|o6f$Cm z|Nn})z{CvtpNaoFu^R+B@$b?9OU%naW{R7e3}vX6Px=4SrWIL1$NsBVO%O9Ez&AA1 zHz@EwQ&V0b#kv1O#ov0kl9@7vL{Qjj_<*k^4ymtU1_j}70Nq0=+chUy1R9wsCu)@` ZZ|lTa68f1b>vf{wbrui^@d_Xl^eXmv2E)~Cbl(6Zr*Qw_x?C*^*ZOQ?ym0M zyLNR|(I)(3K6t$qR$Jc^m?v5q<^>o^+K>bodYZ%q7*d;w5ZEmeJlAPi|05^}h(9Dq z(giU-phQ*9VV@aA06OA}`K|=1@JLJzIe}X-A5%Hd+6cMIPAy|7P%1)iU7o4xj zld+#y5^?Jug-McU`e{@=!9e6%7jD$t)O+%Z>!ySp%Gouf;gokcr2*Q~zm4x1U(y!} zfFiE*Coa$zzQ*O6+r#;5q>m8l(Yz*>qGT6=g5r#^PJmjhjIozBmW0744X+_g$5I}d ziicK-k%ZTU6t6o+&f(-(WC4yn{wq?JN>4GVJ%Sr7_b?CEU-FSmWkU5O|IK;5=lsi* z>l`UPSrkkHUFj&|yu74>YM@cXgvjj#K>MuQz`ItWR!>0RQH~?>k*=cKwC-`1;GcIt z*~C*&jkEo1D&;$ykTIZX?x6j+<$h8CJ~L|#@tLm!1!&4=E0{e&#P^FkomuAMC-itU z!`$v#4bv5@-Mn1c_7sTXOdPQt*072cnOtCiSZP-Ns##cfK4VxwR{RAHfdK-700IN@ zeHuXaMNby0lI*eA0o_n1!M0Ti_>wI2>!8ZmfwU|vs2LdKycIeRyL4J)&!RH~oJ`Wy zm>qn4X)ZI-*st9alg6$7cmB7rpFDT10DmFD-OmTVr-#GEO1xR3MkX@ZxUV%vZbAQp z>UNqtZ|PD6*qa=tN+qdxLXn8(933Bqi34+%4=|m;GDxy+BE&lq>Kt(C-*}o zLKdCweAxfJ-aAPqUv7m739Xs!6rHZwPMpAf{fTq;FTnn*Cl@p7&L~r$I<<**vGgdH zI{9@-sdefYa9BC9G99XCQDv*{N6*-XJR9W@o%VZ(JuF`i6;uUci2-ki)NYXK?;r-} zIZ$g6XzK08gM-ZDLzv4smEGHRcUSO<#8s>A9%R@px>>8vIG&QfM1dbBgC>(%X6#4C;sW9}^D zgcN5}g*9-)YgirWzsp${|8oBwH7#I8F8pKcoLA>vQUF4Qi^?rsYA5HXcw%0lH@04Q z&G*=x16X;~rUKhE>tx|*H3jpF_vP)jS`g(0CGk*(J8H(E^g#>6v7$r`A*`JoWhmsP z(@>bAY{)^5<-pa`b<@zkEWH^Lly_`7#iuGD)CyB2w7E*C+U9iPs5CC6t7>Yf-s-;x38C%9S)n8M(URr4r zN+l2Vn%RI53zabR$NnT57M6A$uM9CMwr%dEOj1jV4_DJ>_c)pgn)4tDp(i!oc4DY@ zrcw_KYPQ=w^Fz(fXuN+~9y$rQdFstMJCoWpeY89YymaLEdwshqe;og7ePgxtI({5? z04VtAH+%Y_(}PABbsakkz1V+M<}q5Q#lGTZW(WQy(Uq4kI_?{F@tBHAXiL7xN<-n3 zDo`@@HRD|`bBW3gzqfU{$=)rNXNku4$opfQ2O*>a+zPn|N2@|7O&Te`lzJNPWzq;c zC{UKHKBHbivXcB-{2mV<@M>3tua1&)3UE@jr#;dx`LoT?P!I)6-!9sj3TKsvo<1Q; z9nKs?zFwb^yQuH<5sM1vjZrsu{WMR+Ke}h0g9C})6!vKp2%4cP<>^2(!6hRiaZNVN z>AEObO+Z{qsse|KSBxwp3ZI~>3>{gt$Ld4|XZ4;5Wohjga8=|iTpzNjkDdqGzdn;vrh$hFpEzt)9VNFGd(_%4JSybMWdQkFeHBX z6VcPc6g8_+DDE&NjTCb0rRXEt=}lt4T~e<9LLlWV@f5_d!s#d*AQ7s}nd3TT`u`SZ<#8rE_TYb|{>qPrA zT%D$w<1M7nu1^r(?TG^!6k5v%mgaLcMdEkt(wEE+1%olAaCZ0^8dzFq9p`a4jfx!{ z#TyR!NR!B&@tlJOB-4~`ts@BWkJ6et;@7{EVHhe%7QxO(K?cf^6f+eD;EzF&6g5(?_7VCp!>@kjlRF57Zh2JTWqOVRk*!f1b3whzW z6il=!mDg#cS_3mt0ReNX%BBiv7gph=Tgxc)Ih+EBjF>!9)+8=%4g=ADAw3%*&Sw)Y za|!RAN1}^dQli!6&dwrI_$UVE}FC%w; zbXE7=6}%L~qauvDinWK%V}d$>zxMXzAdo#bId!`N-SrH=_fk6@_oLlPw}{y;BC1uX zGb(1LpMYOyA=rM#re-$}s)2>sF5WfStH#o?zc*H41-#)B@rH(=6jCMjXDo7BO}0PW zlN3;$HjoybES!DDLaW^9@^4DyOGKvTL?)Qq8*c1=?KUr*nK2*gkaL$CQI;SC}=esE~Kri zfDr+1+wa6V|1tb1nKHPq;u(|(>nfx6#Y3Fr$@+=hz=l17v7nJ}!~ei$Dn$x^Piz*7 z?vQeR(WAZ2`o-3r_gR!k_vL%<%tp<&@Aaqo#Vc$?IpW_uv3%jJ0UuW|HGzAse-DA# zaVB8J7`XN9;6JjI#|g+W_$d2){>&H3&_V_ze2G+FN0*%!Y=A1WSUEJHFBAmjjT*l6Z|cj)ViV#eo-+9J<^TM zNJ{)#t?(#x`Qm<*k(yEYq)Y@%Uo-Q3Sj~*Tm43IsisT7zwt6#Tc;g@zi5LF~DHu%u z{o?Rz;lNw;nCykj=pmT>I)6B{1Okvij$xDmEKHi6R#Bx$#-Amg9j#2!qZS&owj$dS zR$+dri3(iv{Azjc2OK3)({DImsIvMJvwcQGBcqZT!iu(BvH+=H1GsuE#Q^qU3IyY> zq)3vyS%FB7H@h#&>?w?!smWYQ!&QGBw!>!NMtr5 z>K5$37N!|b0m{q+@5WMi4S>bt6Ez+liSFeC5sczKNascV+IW zjn0}4@M$_dvu?WW1;Tt+pi4`u{`h^J8P}3Za@CNgQMz5X^O+X+@6@uWqU+hlCR)y&tg^o=!S@{LY|Gtxf?*%s z&br5GG#X${pfPFN8el|VL>p-z8er6*#c7n9U{Zj4uf8`0q3w`h(Gc6m1K`cf;jKaJ ztdLw`hA~rpi1-g;;IuE>-Z7Fz3(a0iCeg!!-JLzZ_lYh)GMCPoGjG$PYCf)+UQ#R= zc4fMhF(vWfCJ#AW_X%68pS$8}Jy)LQvpP|zyl#41+F*P11oYB#5Ij6iLE9)V18-cKcn|{$y3h)^>UaCH$ zTdqr2>vW|@u3Oi;BH;bta|Th9;(k^0!14Il!b1&u7E{S0P|p*)#IjdT1QKT%qtC=9 zfA#buTA!rWn1?0JEsxONhV0Oc74t={8V(@9{5QOU#>8stVQ}B_u#+4sWYN(Eu1@Bh zz*kAWN?YjZ`id&Y@gK^zd+ybHoZwg;VKnRR)F7TG!L#H67T557v@aZ(u(1G#CDDqxAH?pne%JKk?Jj8KWP#ncQ2Ht#N2I)UD5xCOy zF+ zF4Y~4Uj8y*E%CmV)y04(=wPjh{ar5Ua3V$5v8dG9x9Y`IArh!WNHERNNoW9Z-rK1l zg&yj)3ZT`AD=Wm|2SvMG;yTBS=NvXYup#T&r@s>o4{!Dl@n*NNzWhm-XA zVtVy~ccGQJ^6F($a{qW*egu4nzfpLMjvlTAnzvk8KG26uYESDn?Ig)vqT?5(o z))6;Q@ywsI>8~(MRnV{UWe;dycH`y%!^aCt7F1m+EWRmdT%ia>HuHEqd154ksDHZ~ zu7>vl6CAL7@k?&L!51uBUY86kuafP1P513BUfN&1L8Nb~`?IoLY?rikZc=F<1;?f# zO>U-m%eO<{Q1LjxKhDBOFp4DhRq&)Fpe}lF0>~`_PEX3Bsu9qM5CiHYz-o|`xp?L& zN`Ce3q46N}T`hAH2nximz;Sa4t#57**L~gz3^955?O|vJPAh#`Eyt?g*R4CeJkpkAmHm z14e@&tE*@dvYGAgwg)Q5rw$Nh(ASrn>H}-4P}us`3R~)h!u$J@{`@8ti!1QXA61+Z z^=Yx}3%PnFdI@#fnf=rHm%u352qU z!veyPB3?EM)AllkiDDP+$etJnKy~5_(_eriRL6Gu5Hd4M^^I^L(Vvz9@MB3Cx8P7N z#J;YGUoU`U zGx-Vsasp<0qQjE>Jr|+Yw6-1159-+hHd>V04mH{Ipkr3qSEfo2jfwI~J24wUA$P1H zHc<-HC-}&gPzu6eT0}*0C~Ci6K*b|+P_0utEbKy<3jx0_P_0AQ2D`#&;&3>!lHh`t zn2B_GzMm=4cVTPM04}D&r3sh<7p1RTaA(H$E13Zdj-N92St2W8=rdh5T(v7j4L4Zj z^I2DRHIgM$CILTFm95i7CDOikVe)$y-W9D0b_7t3tm#7LB+I`?e}!2ZV@%*@-hu&> zb}Ipxc8C5bHuOdcg>Gh$j-c;G=wIG<_NeD!x@SF8{adFj#svN+`Ycdif(70ORJAH4 zKs7-VPEEoEi@Y7W2$U%P*WCbvlM znoD4%)-}b?N>XLSFxB7_Jw893ST#1h25Sxg!9mdbd=q$sbxHSZZ2i0E2TS1+N=d5( zSnv=I`Hn(zW}&o!+Pf!nLgOTOl!PrszhMwbi#MAQJ&xd`eVv&7<450S2UgglPXWm;}fw>e*WLZFM#_=>scVIbw9yjohIP9|^ zL6T6l$UweB4+N}A5yX5V-HaK?4S?q&X*4$C;EpaC{3qI(10@1%FjQDbl$Ir_IwHQd`+wL53@kS&ifC9) zB&)Kc-0)#yt4+~r=1=vJEL@r_-|QS_e&1Vy;GM8xGzDmK3{{#z^TO62>-;5mM0YKL zrREA7QpfAJ&iaGU?cH%Cdtw#<<&FCdR!Gen2|=msj=X|{WtTaE+WmyIinX6k!SvlUmrM=l! zh*I<3B$U!HW)5mu#C*5@jaINCiJSeyQw?-X9F7Paj-hf=iD_PpXq*=3|HGIbi}ej} z{GnTrk}>+daIIs6HZ=ExN)9_haEYO({^tsfhqOs;&;G>`f8jwYuV_Uo7Ba z*4r_<;OZ*(inSzQ<({47TEAaw^H~Dn1WvYFHxA0>`X_RJQ z(}0u0j}~Q|i<&QXC?Aha?@(5(cyR{yc81TXKxT6;xf^#LD#`>K3ApZa8E zgc%|!$5uZ9%{gf)>HMSs`5+Z5jB~is3e!kRy8t^qC=rYVfg@cYQ8ckaCNr3d2oES|;$80|0F}#bz zDI0YGHg+QVX_G@%U~v9_|9-pk+5g=6#NwRA_pE0F1cQ@i4HG8;!?=rNb75;ol-#8B zj+c+LJX)@Hi~5_bxM{|054e4*a~6(dE;xc>)5cAwQ;n#3fx!xFn)5xPFn%F#am4X) z!jR{1G(j%_Gm<{55MS{wGY8EHEoC_27~|V7%{QDNXxIx@!))lx*C{g`sRA7tMEH&Q z(Qo=P!Dv!+nsf$$?jmA0uXd`wb6_+F8*|#xS$@s+cI}EYbUog(X-qP%Rk$(m{eBc@ z;JbthlPRr8rJV!OIoG8C>XRzzFEL_bwY<|My_5x4Ej~@yO}gF4H+Pzb9s$5>NM~Wsw(6BbwFA?kU+Tu- z+ml^3RDBQO9V^b;@SMZPeBqnBb%+zAI(kFdHljH=0VgAPZqo{RNJGybU&HDrVdIv| z3I`A0n&jUm)}DdAu*Mu#VB$Z8_sj$Z(S(9OZ!FL@GL-|GV`YgYNAthgYY%6N(DsJ+ zHZU}u4;v7|OR{ubB=W`5oip4DgzuTE3D~8%>K}bhm3VU36#gpqmVZ##`baVhv~JxP zS`n$xlI`6zb*=%Sr%d7!2pcyQ8vi?qJj&{6k|PUsSba49H-EC(R90z#qa>J{CkPwWd+^W)dABbNd>oqImJl|62a2=cVliqjOpWq zz_(TXYxTbg98c1D{-P@XZk>0%&jPW0&$kqk79XcioDzpCRKIf6%gi!{s9Vr55l@8> z`t%k$d143tygCtV??Nh>XCo74N<#Esas`}a3Q`4A*f9a`Tv#iTrb39^MHZ?-!TVj- zk~wn+;dmT?JkgD{($Fsu9J^=iFk_<9p0+82z&QT`PlT#bg(n;+FjP;zFfA|St6Z^H76f&aUaC>%_~J_nT_=Pp4Z!1> zGeQ+e&vHADxy>kZIPFsPd_{1zWKj}Dhh342o4fPp>*eY2t4}lkd}#m+!p6<6^=Yt`r}XIAX)V5hb?+!-~>FTd{IFhtTQl)2p4TOg9fA9%q0! z3c86yp2krJ5%KD%x%&~xs8Ce}IU2#Jlg3|nw7QU>J=MdgCbNokl@ZLP_5jm@*S9Wj zur_R0@%5ZamE-7JF@=z_p-mSY{(u)!Rg;q0PV1Uv8R=P4$ZMVur@_O%gWZ*~;R6E} zcpNU>IUaHqRBBNzkCii!tz{>3Hsj^mv{zb3^zBALw;N6x9JVP0c08RtteEcrBpipW zy4|Q}q~=Z&joE}dIsS5Oi0VSu_6-5Bz-Bufup(fL{@99uT=(&*i@3WEn+g= zm~lE_!nRisai$P#6@UK7Vm)j`z9bqt zQjFVWohlH@kvxJ~mYmo9Jq*UB^JDR&%If490k5D}2F+-dkHocC^U^Rbh9rkX$%BUi z{$dw|4R;v*Fi02%7UQxB>LC>XR48` zR3rM-Nlf&&%wSeh11>R5JEEq~F;ttWQf?k_MD5Q&%Dc~6k=;X7_L zJtEZmsC{Hi8)nFV+>)ZmkVD7T36Ivv%~q-yyaLH+Fn@!5#?r);&yB-#_xr~}~u8p-R){*D;M%Xk-PZR zs*hy(0!X_>)F@FXBP4MSkS z#lvfKJci4bSEo7Ql(GAEJu8=JumKB20%nqvM;W_nMwWR>2W<2WkZkTH`r=F;w_K2G z`piWP5E1t;+QoBC*sa4#Jq=%ikOKp=AzD{q461TWdajz1D;v$TowX!k+q;1aD}ark zCqMyY`Gnk3wd6iP`Lwn?1g=?!T)@jAzgVh>HV0akx;{)=n*NZy!x~n#eLUG|-Q23GG1iZo!w?Yu&2m=sX2Spur#K4oIwTA%JJptZ;o+Gg7h0{GzeEHY zMoRV8Wr$Voc3YV0)lZ!R%W%r|_7>B%bOQ)${v{ZxiN+1hdT|`%KHwC?SuH`y=%=-{ z#Sol;@ra0EL=p$}Dz?I6Xz`MEr#K~wYoz2{VN8h9zk#WRs%!*2aLAWA6d!9YSh`jcRX=WK<9AUvhPQP6CJI*GMVq zO>cU`J;5$8TCmeyP>lE8^?h%D<}JIV_h zJ0p``q7U$*fS?{8>-}za#p?rj@*QME&Roh~C|K>SVeIfXakJCBj_Ri>8Y=)E0`SnF zM01vrY-v*8-?S%i$JBf{CIoqynK3L<8X_5*T0G&#`SosvnagKeQpm#W1u1+(LSAl` zo&{RNjEQG}<4RN`;(UvZ$my4xpfmTZ`^*u=yImAJ9 zo}4}*ZMFG_TZ(W=HZqrtgBx z>L7Cybx=89#pi#ipbvmkISGAvLybR1auglmA5`3K$aDco9L4K>`h6A158j4b=%oFcARi{Cw<}7zo9)P@6Ln)f3)0EE{LA(^T~F`w%m*Y z6V$4;gsFjo-uzL;IE!xZtd35;gH#$E?JEg+0*1cz z)ePdFu_H9H{`3JTY7BroBS+s@wqHk(4T*UwuvpEbk;jFBVjpuxYcpqesY5#B%jyut zAnGYs0|#o>d|cgxP0TF#RmoWDkvDApwJ3Ljx*%m)QD+8%!k;sz9mdIf4|m98*q-?z z)^r>=dl<$4C0+?qn{8~XfsC$Z#as^i7u}K30;6@H-}3;7@Dtq6l2OB^PA1Yj&k#Qv zvQm=B2bQJt$L%O)T9I`dwrr)0HhB96N5FMQvZA|ZVMj2y=*fltobcS%=l3Ckhyl}kCtzL*oRQ8RopkqaxX6N@wW`u3K92P+iH_tb@=pnXOml|E`!r})g z$dLb_JuCqDakqQ@uv-(-n1KJ3r8~~e$Ix9{sRz_HJ0&ao4!C2YEpa)t-uxbim<0Np z==AZcT8V7mo13UKl$Sf#CYef~J09QZlmlk)o@U)HV7%HRqZK`bg}_%`xh_g=tbpvL z?CMOM9;p~h(hgX}L&dPRXXYwoT1xBW499k|X`M1BT_d2h+5!;^2o@N(m4KfAK_)6=P+*6j~3AWS$BVsz3r3a4_tew8(B z9Z3Wf`ZIEsT`k)h+E!a?*3ljOkeF(gQx&A5E!W3JJa;2>JPt8%hVO35&zJeLUPY4l zJ`Iix6>qgqqzYmHMT)m_mbu}2B^zOCN)h8T9~^MzwqCE#CCEvFsC$_ga^|{TFVD3h zb`wf_NlsqQ>F|V>x^}H9m>oi%aB)!*%wz;eK-?9cF27rt7!vaVfmKtU-qAqcd|s1# zc6@NTl85Z6k>Kz}ZJ8tD1(%&=o8jsc%Ly^Rel+5BS`Z+x#Bl1`!}KkSv)3%kTDx^k zLvx8!vMQHwc)I&x9whuScQWE8F4EFr^+IILjOpFv@smX?#m6L8Q*{aO zAqxO>6k7i&u5LN-T7_TAPc1{M}1R5C<7Ct&c)WWY9xN#l)#k z&Z>HcYJckoKgf?4{2qUL!wE6Sig;~C@8#AJ!5SeLV%*^EAi zW*aQ}p@@2GoP=)}(;gL+5P=ScN;RcJ&VT{+K}eJ9PmR-=j-VU{%~eXXoz4i%P$E4~ z0_6H=R2FO}!bW-5CsUM`Ja6jzyzm!nvva;-AnUaeFy8TnrrFDdFL*?@cLjivEJ%@a z>G2ik((K~G$3DD;4efNtK)ZT11B;DJG#~V9$d3-66yGm7)z_8j0}5`~BGBohC3=C( zSRaw@N%o7-Z#e2o;7xBS%tlflT>VjTG!5enz0vXFO7Zx`g2|Y0S z=cXanqc%4Lo0&@|Bl*F&F#?eB?VSe_1KkzoRUDlXty^Q2=}v*}*3!k|I`g`bMA{No znGDff$=>DMa%cNF3ZYOtDLw0a7{SF{xh``#@W=v;z-^VRn&{)eh~<>~h>zp|#Tb7~q! zJDXoqXizrXvp<-qHF60Tgf6v|XOl*3eE=j;VUY}q1&rz4nW7Pg4XhS_fzO%%C(0G9 z8WLwnp_bTKy75(G;gVz|m@Tw;lY2ZYu|>9~Hs>nqQ7B|<@N_EOur}ol8T}FI1rnnM zS)o>0Ak9?)iY~b-0e}M19nZ8WEMDbs5XHJ+3mA`P3zPK+H0S>3a=Sr=CD`Fc0k$tF zuhkQj>kJ~OyO>R{&UXnR}sS}#i=>;>hovsE(yAGbww7sBA=&v!=QrLZCJ=5OQy|6YpEHX zge{wZm71C(l0OFj#;SN&;&JPW2bmYafwElb4TKaGOhy zKfzdWf}t}Xcu&{I(>wOsuy{F@m*W9yX;35i3O^A&uDxaVGzz$Uez;T{HapHjQyQsy z7zHw!QzC{(f!nDZrY(%Y(8UY#>i3^ZXV&CUjmJb>

(r?MhalwKK^ywPiR>d=rJr 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 delta 173265 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

i2E?dQBgdzYhY1K$I9$MO@YD2~Bjf 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%0DetUdVPjf$}U> z8Pb1z@cN6+#vFCbH~%SXUT4pG$)mmusBjuRnr;v655#uzd?tYW*&x@y%g z7s)A)884V>6Lviw@vPp`s5KMU*d%rOQ6JO_{oSu_5p3MkWSO!u`-<#*=V%uAY6 zH87V9&&X^Xhc8!0ZXeX8708J_4s$7-jw%^v^-Kq;s{&PZXss787n_06oZ8#Q(oq#` z%F#^~5^WQV*}#W)NQHL4$j(MH5$=nH_}Ms!Mbp!{n#@Oat7_g%h}n(Q%x8`-BT(gC z=BhoJZQju=umy0h3`d*S>OoN71bZ|Rdp|=+AQr=xmB~)Y98u>fN5>XT^7XVr2n=~ zN{`H_!-Gz7?&fYu8E48br1+E4(9-A8OuFw0TGmvi%2RWQDrm2U=w+ayP!xL5(=MUh z-jp;LIRV}ZfT&9S)%ai$lv>;@NMIzjR9GJLE8^QLEAG5>&M?!}Wh0*+ovs=tqmEuQ*&2dej3uHsP%qAL|1t zzVc3b8pvI$c+FJs@00s5TH z*?+DIdG$4m9emjualI){S&P~F*#8VaIWKMGzH!Rs z2$e*{M5ztvyP|$zN9Bp>f?3L=@hy_N$5Xw!^a~PXhkIw7fmOwBxuV^6U|nsKG$*_$ zcSiTg#)@8AGZ9p16$Mb#<;QU=p(TxHd8Utw`K3~29V1#(__te;_)!8dJ5aX%55 zbi7ei{`V#A?O7)>Y5BF;Gu}0(Rvj-#dj+13#r!&1Z4HVv<2)PK(A8F;gU;qyzI9{UH))Zkkp zs{j-gCR$V3>k*#)Hd|Ymt{<(4k?$%JX@!A84w=pSGA@tB{EW;BdceE2|EU)CTyTTEInfnt!YFk3f5EdBbk`Nk*(AcXWY`&?aOe%k$twZlt`$)uJ?7e{oS zm7X8GQo?>$*E<4cLT+`Tv_lNc6eFD@Yrv`9GD$jlS z$M;D41oyLl>tq}N+|sXY6*#QI;2GI><0gM{%n~Zhv41Wu+t;1zUDcgRXZ{^g@l-B8pn=!lT#)xaUqb!r%6`JOB9l-J8#RVm1KE6wBixjxanVmY1`H0(of^K`%K6vN#VS=3WvdTLu9DQEb|S zBjZAWDw0KEP7XKeuKPsOyXed>AYhmQ5upfEb$+kz#)8?%9lLKrjJy@dZ^SO51X1?v zQ@z`dMI9#`(kHF+D8$~uCw)4p!mOD5)G{>{=*wR(Cn8;rVp~V2zkn&kO|C}9{7Z#o zXM(bhJZSB|lcO@H2j1bFhd(wmNXh({K zw_iWA2~W6`Y8@-Ke(tGB0)h4Xye%4PZq}p!y>kUmm)MX3ax_%rhEC0#6oHePbL$^- zE2%y2a-+eL)@v?DwcJRUp9-#rJW6kjJ-U+DaodT%ieW+ga1nX}CZjMMP_uzgNXNm^ zV__Y%`?y2`4Q2uHIG8{#z19^H#=ayK3266D=q_$C8nm1BXT=`AuT>b?;GM0v_C|}z zxfjE70sAa6%-pgZxnDrZix52y_mSb|3o&~}Sy4riJg!~KUjoD<6V;9@)Sljyp2>d( zsDtJO+cv34vh%o)j)89?tjA8#>CYNyghfG^nL~m`=%j3SNt;G_v6riK?GMmick@YO zA}{rx^i%uvfBaOBC3s^MjemR!&DP_b19o_8fe;u~$?#~Jsu;rpYP!(|+Z61N0u7ZuyR5>)-Zy&LS&rYbTh z^J%VZM`%H4gXH|0rCWW`}b&77Uxczy*GkEcKOS1+yHXOCBV)rHJdjD8+|} z6Y~(W4z$)V^x3>|II88s^L{7kua6S%#vNAY*-bFbcL$@XlE)%g?c(v9Y_eg9ZjgYMtm@K`HT+JO{xY;ZBMBC5;jQSy5pnt6} zeOg1Gz~*p1;A@Bb4(swTe)o(XZr22>^|ciTy1qXX#~T?*#|R)Ghf6-^-JYjDGbJ$F z{|viFY#$R|TRZ642=)xg*JiwleQ58DfS7gTs&rJOy&II%%j2cbW=;pyhjWWg0qf44 zRQ@JGGDPf^+qgU;G5pf9Y#n)Nkz+-S#+r%|L3;rwG7FB($-rGATq_qLxGhHJX#AN9 zw)-ioZB(HmRHy7@y`y$-xRAr<^6X``)l_h+n*zz+Mroyay0$>Q4G2uvLZWt7xJ(kx zYcO@ru2FUn-)#3R*I}_PL73bof{g2B@Ro#G9f0sHiR$&`aW_hS*re^rIR9cdIusYh zp1}mNw6$wV4!R3$jmSLe*Y=AL&ot2Z4}5J&E^=8iZDqdMCfwcia(fB{^L>4#&SzuR zuRS?HPiv2CaHAsMq%-fdS|7t1s7@sF=JtwwQ=4E{1o-cFJEU7`{hqFFv!^#lDyJyL z+spOCs>Bg5Qf3VqT61m{S$^hvh-5tN0tswX@zWlQL#ornnQ>B@X)pnB5z zi;`*P!vt;WHolD=srS(2UB*=h`vO2-hN!`!2a!cfnWdmmu{p}8mznPb03bAQdvijs zslk3y&e2<4CTE>z<6EW11%Z{Qk~)&{F`S@h4@LA%2MxieKGk5bpL3IvRiNP)n&;?H zME2Z)L|%e&iTmS*2j=(qfzT*EID_HcZBqTw$_F+lLEXYK)|AfPg1(oNsLBeip@Z4M zaD<%j@YPuS-L>YlI6=XgQYYz>jpeMc`?Oc_@5cCfT+2aAscW`gIShigjci-K_ZuCl z3qRVMi(?LhjW-7aE9!j`l=DBMv4UOGy%h9IHpON_C%)%HWB zG7t=C-W+H4RpV&;8Rc`v*P8DsIPl2AVVdk7o7_nUt(u3ATVD#VkfkZ|sn-hqv(S-; zF-xjDM;)+LCG+pYC9>;-AS;YFgkWBw6Iez2A0IEh1qkQoQ7uYUD@}wHgtm$b8mJp` z*v6T#4E0;`O*rpG0W;n&%>~bQ*1H+?w*LYaEaD4N^?~yd%_tx-D=FZ zC6n@3b%l}psWW$^k@(e@S_7bQk2}vy1i^X&Aaa~!Gp`>JmaXRJp?zP4*ACl;nT3H7 zCr2>eW{ssmrgYanM^>pPcf>x3aY7SoD6cvomYKDrtFXC#7jz(6Z{e6hL6WGDj^g4 z966@9vBLC6k;{j^DH?bi+ynqYavx5S~`~2?vPn%9D_$@G1uAy0!PTPeiVp26;7Oxy##c0ek(XspNDk z1gd>yJBj1W9En_A^jDlMi^A&6c^)UGe3LF8L+lbyv1NfI;eC73=5(r(5x{UZ-z$B5 z-^xbYluU{%6sEJ~-D@I{!Ga$0Vzn7d<#Jc0!XBn;U&t0Ft!FcR->&`yoc#nmW%2rQ zn61UF%p9Ov-kk^c_ClBogO&lO2wm2}O^{`%L0s^}q-l7-b}BpW__569fT}~uGeeet z)tV>oTdc4H?UfgFz90HAK6NMM*Cw}h5CvFp@fcC^{F}Z$bK`L1b(^hg{tG^^{iwj( z?EoO@^WDSj;Rh>^!|)v{FbyBD8&XNGH0-TppwK za~YWaR^+-n8NUeo8l9KQVA$}HJEH0D0;bi5IzGG?kM@*j%re1cV<2l*-wS&&8www z*T?7s)Qez$j@myyDHt0-02qOpS3+UyP?R^bgSXtOzK9<^1XZnkI^yB+FyicxapHB2 z=1i}^!@c51Rm;_ilX`hPiuC<>9>BCsLeVvw^2j}is*|?OaR@sjeAC8GTs^+KS+Dtv z+PzyZ+|FgV-Qaa+mm(-Ppg}|LEakvO|csA1!O1b9*8-<+V?TmC2i z#uU*5o+6c&Qk4wK3^l9%ycDbV$I+PfU;BWPUDs-sDu>d(ftKyAc~&RhwuQrR7B)o}*BaRP7WTvD@`2P8YnKH-~^?0u2iICUH$3SPpRcs)0T)FH-SB_``0 zNq3Hp#o+radLQO4kvf>;LvyXRU>9D7=;N&g@S1mW)cPo>&vla?_a}r4dBXEwUyCnr zGb_^c_SkjU9>+COEf5NM(`bqJd~2*0-qv(Kxk~_}=<`YBBEFj5?k|2Sou<*1rm~nq zj}zxflX1izA;GLchkK+k55k0DPr7GZvROzyw<{B-H-Y#tQb}VT-^|NJud(7~g#R@A z2`Rp^wt`xY;o}XhSqp$=(qvmd!c1I_k5s|*qn*!f3w#CfHmNoX;3F-2XEUaHClYmU$Mw1rt3IPf68!jy|T zVc3O}x6wH`rMDrcb&~>a9F+-8Ta|Qm!&bS~og+mJvL4+b;~;vd5jYf zl_F6os_*ao{y%^1adyu8yg#qk^GQ7hT{}#5cq@Uw>UrWi{>0=v?yK}pT3WK>8hpKa zDv}d5`q+7Ir0Sa;W9rSB)>err$Se|KN&2BIY~&IHG|o3Gk3jS0H3N69>;QtgdjE!J zmD?^I3;hsiW=zD$pO01~zdNht3gl!H!ji^H|2;F6vJ00kD=b2sZb^U~2ezMRtT6|q z-#u!nA+omXugDbwrY^l)cSj3R+)uOgpBYyIBm~dbG*W;0SducQvQI*nSSff9RhKsR z`Xp(Tydv$qwOf15{k%vf>X-q{Ezr$u`c3MtrN@MS!PbWTrVQ?A)n>g z%N=cei8sf*sbzixp!}d;V$7$npxVQb8d*!0TW4_?>@$$mGvyfNgF^531-xlP|3set zGZ8mwIvhN@r7V#bDN=x&Mn2@6IuKeaT}?Et(U^Y7axMXKV7_kXL1_MsnrqoDYS3V8 z?bpqX%6#b6Q&V$WYArpj<=Sq>Mq^y!WKq-Vz%O<40!kbekj#=!OtqC)0;F5+ILzO`iU~(dYzoV4&YhVOlVc6j^)sywGK&mpQoCMZ27|&M5GrIC zkMdZ?&1WxafWzay1(Y@6U7=rpn{ONl9thXEhEHu~{mP6S|8q8agj|gd}77DgrJeGbGbA#7Js>vR?4#L+S|>!2=KmX z?exS0x@q5MYX1~zerK-vFEoNIxB@pzwNhm@KBV7%9Ae+h(L2%&&gRaJ+K-qu>&fWk zNJKllN9HhMZyLvNxThylEM~OCT}{5I-{=$LaJJRb!NWwd4(`$!`|PPN%}FLGL-E&b zPs!=J7rCInz|D|z< z{1E4Oax8pY|DEJe@%7}4-VMmKQq-pkQD*#LIrJx?XTGc2x7p=3-R~Eyot_9BVj%i( zS*5Gqu7T;X^7f#dU7@R}sJk_%23CIJo4s5i-GlFT79CAoOW6g#I9Dp`KE(2ljm-Hsc4zy~uJ zszwD~>sAz@&1eLhx%bto;m?oUH13LKk>Bq9{45 z<*wKT=QL$nIhmYzB|j#VnxTTJ|Hvix6=vBJo%L5qU|y<89ZZJY@~!ALK^(W>z+rGO zS4@D7s~A%~JO&Ig$z*62@RpK-n{Wz)%}Cs|8J245eg`5H!`t+ebQG)k?K}U+vlq+| z@2_+mjpwkRJQeK&1&}9j4{4ZNQ}N_~Y!akDuWlPGRSVI?X9pU@j&+=yt}3WK6zc!* zZ>$Pfqx-%?xc@F#eq8SgI+vsP3w0xCsZw=hL?c(G9V#yuaDulv*IO^}ji^1*DO&5v z%{yw&BLFVw0f@2a$4Zl_cw0Qt9D5Au7wDRsqWJ3;q~ZD)Ha*(N>{jM^Q_6Ml-;Pd2 zeiHbb06x5FeO_|XWBkUMS6-s3=UaIlI3Q`x(J`O)?~JM}8?{LC{1RsTdu_P?@$mIl zp>5qzKRAabyQ=&r&iUMTmfA6itE-BTFThI0>X7HEGc_@_U;L@m$jYOm{N?!@eEl#J z*Ns_Rn#OzZ6#xx-7qQBT9k8l@E6@#sQ`eP#=iD!HS{+TG@|VAQs^maV3zNwfrZ&Xc z|HoFTeLdvV5}n6F7b0QT>Em5DmtY^NNfBtO5(iQj%B(GeNwv2&BArS;JL;l7-LtiO z)4Ze^aH;_4D?%?Yu1~sO#${N&GSd_;t(VwwkYx>22qlsyFODft8m&$L63wi8t4PUE zLPb8#aYnOh|CaqVSa~BTI2A*Usj$_GeD$!y*B7cT>~-1kh0YfIO?Jx%C_VFCd>?nt zme)#6EU`|VnusBekP%jXrPfv)aV{UW> zKJg^UhT?X}uNkjB*u25OT4T5F!@5YK#w?`9`I2n0g}{RCwFtJ(RJ6Sk!2GVXZ(4`2 zF*}cHe75*{QzOG9>A{k_>JYKl*=YTihN(9>5zDU7P5-BBV_qL@TOUJ@Fs3{9skn>< z<0pwNf1hgljsb9I&9o>9zg9cej#L!4$~a6f@btO~1hzJ0=q4&@o=Xl#Im&SD&bBPo z@iCi?F^nJ4f$~@Og$2{ooDrPAiGmjg1UJ7q{`@R=A$Hz3%`u}}j?2VwSsw%~iQV>_U##nSWYW(Ggb-!&%t+N&}u6MJ|~6=sF2 zcK}Rda3ls#+MAgNKtsNTp=!Kl#s0^+*gml+3PlEYI#4ir?eLuu=!VRRJlijF23_F{ zO@3u~T{lA_{r57VqVmPRz>)?77ik>pcVM?mBT?qsvj?A+Pto`+mMr(s&p6<>1LgChj(%8MSQF&>e=>jsv8K& zk>Gl3WpAcgb<65+LR!dXaVwKJ-JiVhxOC8l*Dk7L$&r}Fu?i1OBo;6`Z%Kxz9B*Mm z8E*|ewFIh*8X?^2bfLQmpp|@!f~-ih^~-hMnwMD@hr z-7jKW5w=4`{J{PSc*m0-!`5D8>0zLS$H(K&NWtxuq62YZK>{w>WcQp{!uxBK_B>X$ zm2XD|7YZBy7_Z;9g?hBau^!X?y%N-FO8R(S`!QhDK_tM-A|;Q(Y{FJ7MJlKf&wfeT z!^Cv3Rc{^q#@2}+mBa^cP+AF3O|BoXt?UdEAhRoylg>yB0Bl_e-#D0>r~g~S_`z=U zIDW$gX$YqkVMpkUywnkCGYPsq4zQO?R=Vd=co8G&V3h0aB**D)fAf}*HQ&|rXYbsi zRY?Huc>e0>x!*C2ionJ3=+yIS2X@5iDWFc^8yUbjqpk8y|aDfkHPzg)y|jyiisiy?eDRT-*HX3Twh{ z{_KIq_6cUDH|DP78?DI3d$&_#z~@!TW}c9(7Y!buKQ6e++`{Y)fo@-7&Y$UDEpqF+y|@RWL(9}fJP z%@0Yv|8w>1pnG~}b3N?~??ARM5Jqdqi_gjNy|obau&*7xX|?H|)cqS+_F^ z*1hw-8lg>(-gEFlK-t16cpxlL$1KUUqn^vM1-7WS!%BiLmLvA|hO4^mlSB`am$R%T z2lE8ami9*EeA=o__Hxi(2dQ}!-~Vxwq_7H;d*e( zZg_p~OW32ksX1F&o$v`PVrdAajOf2d%%-k8Is^r&DmeMQf=sDHekImw?qb~HzdP9M2C;Wgz5vbDe5?K?Dofv)JHV)snfw1(*z!wjtavB{} zayZ-ghE5q>(?YfPRhT?ko~x>bW_l{qM{>eg)K!=U?d&HY-U_69Wa&WLU*Dk1r=%8> z^quO#E{%pF^&I2!c;B2RNkwcyURDPwmPCR-LN|AhRBU3D0q7kkv&k{5t=t`n?ug+! z_1gFTpIN!?7~u!x+&uWNfai1djlHIVd7T;&0QtXAhe??bHhLO!6oYm~t zI>2GFA*$^H7;%D0Fh_SvzoYy5WtB{o;{LBPk^vg4&R?Wr_N6Ud|Fo7|wZGPG%voB% z1V(V+#bj-`eLT1!0W8LWao?PX{otk zZbhV9R^E4MC%~N55 z#jo5_5-Un_YTWrOJePqMk;_9haDELRs%>>n?q0~(b1wkzik8m0S}0OYeTx>G^6U0+ zUFcbgHtelvy@ID3gcNf8{_q+9{G^*tI^IdiJ$k%+fY>^^SZk@}s-i-ikBHc-b-nUK z_T}xs!QC}6=!zVA%^31YLhy(4ck?8TcFU$)#ym+C7dK=`Qz)f zkgTlOPK`tvutun_;tHx&ILNAI&8Il5^=<)$mxx@E6`Xn(Bw<)p_Vd8-?(2Fp|CArdZ^O#THt$b#E5Mq78ihe_4vP?=(&B>}cEXH%bB1 z_}Ki(d4syX^Yn;Zl8l>WcjL)LcEJ$~qOupgkpi{@ug#hUo~DP1thz<)1L)MDV3os@ zZ}0iaSI4&OIm6q*pMpw+r2l7h)KD{{%isr>rFT!(HG6j)(x+SN*9S8OH>#J;SH zdpFM+0eX1{%7>VwlL7rNcDv%_2%Mi|ohaibFN;&;;4~Gf$)7iT+mR$n&`ry*iseMaor3CC#fb3GN;!O8Uyt%3s!`mo{KQ{DD&F%-$8`43Y&?3acw7n0jCM-@ zJXpk;1F!g1E1X>N=)|}H75GQtS{u|BFS`n(NVBunngj|TGY8e>|MFUZ3KNlSapF`8 zKA>ors(|N2p>l6@f3|uXA?SH6#=-BBIN97#Hqk$$Z&871v>EGo0WQkxz%?&3Y?+E zH0V!mkYY_R@>G+;#2ocvDyN;>Pji~6BZM2NAbz^-N5*-!zFMn@sbsb$*O8yI>x^E- zG7Kg;udpRv#*LV#oA0%F5~AS&pf_(a8iOB`J%W`+)Ds+gmfzf&yPS2x$s&d zr~zF4j}`;D?P4s1xtm)}xcpeYC;h>(zqwo1uUA2Gqu5^zHuUnNpCv@r@hH9;OsEsEu zJ|mWs3yb(9nB(~L^GwErS5nvxcciB3Vf>=`#Ad}p$4P{S`N^!FQ+s!Zbw4P6pUh!$ z%w@WYSoLoJ22wiY-#l&}12cVqQu^lDW;Nyo@r6%UJtC*O4e}J^b~DC)W;}fRyT;0U zVE7~)Cz5WKzBgrd>g-m|aAR$z8{4gIFNFO;Jb$QajedhOKf3TrNpeFTp>zEe%FPa~ zIdiV%Nj+O&aOaI$GxwAC*xSW7(MtALcUI1V&YZHF^&21P5$J0iGoq9eJjS(aFx|NU z%}9Ak&qkrBM|x@9f0R}`wrsi2a9S|SqC%B%dpdWnafE}9K84pQxh&cUr-I7S5`0?g zxYB=ZZWSA4g&pzfVW$f^4JA{z#W_%V6%&I&gd@v&-cdZDS&)jD9Pk%)SvEtjjz5#C zVYNZX6``PP(Kxh>$gPO9;B1ggA<|Pe#INJ!iT=lDb@g;Vg{%G|?Mk-H4=%qe#e_qk znl#0+%FQqMJD_*M?tjp34L@3F^FX1;21_d*aO{NM%gxA*$Vc6smYh~|@pcn++T#g> zi09D#S<89D!f5f)fsIv-t%~G+Oje#KjTB7E?Zh%N$I=5?Ipkse$%DP=RO->Y*`DFD-w+xn_S}UUK=8jmu|fV4BJ$^L&(k4-^2cThE-KX4Wd|WH z+6$;rT6%Kpl+5pXtN#lu9Hro!=1!ICH>rNcu%xZLL@o;zCAuTD0^exli6`|*t8cuh z5A(zbQ=lepX+@?JST*}^PZl~wb1=+jZsW7i--HNuq7SmnRRC|h7|qy+8laWVs96)v zR))D}^P(|n`6_hF5^a4D@$zUd#5mvU*Yreba4i`MHT~ljxE2*aosJ^WgQBf=2b))Day0>XobsfF( z^>Mr((PLyTs(uFWx@BkIJT-w+oNB*=v^Y#i1qC>+v2Y;Dfk-z*KvlToz+r3*%$Q$2 z?XR9$`Hk<&&M=RdVmd`J_8Gv6ie!JIVZm*thqXpbS2td|5t*yFI0h7upsv*1SYWu` zZd86F1H;_y+1Q-Lodq}qs?@~rs-tw02~>fM*Y9`yx_fNK#^MC*8JNCc!l40~BIMqG zJSx63qlD>}qbysZ}v^1jREo;WdM}>^cp~vNi8NZ%rUX}93$>gYhpCXQA zf0aUAyg5z7Z+FU|V_`$(xvL;_i}6GLe}T^O z5nO#B#Cr6nS!*~p)7a&ncM^y-fsIXCCqX6r^c7pRl*0ted5X|ZgcfWwk4p86w7Ugu zZWR62v$b06KA~V}R4iQNUBI;4!jwyN%Sk;m6oJU_&`KZc9fm-r$Lv$_;|{q|2DjYK%DXK{fr z7ml{hs{~rEcJU*Hyj)f9eb&NPJO+y|UJbP16kZnmL_^cbsE&WJ-ELifFQ*e0b!7=F9Vq#)(2U#0zwB40>Xd$a&3?71O2-%c z8ISn~LkcAoFM2mp|6_9tdiUtaHkGK67$Fnuh$8N&Ex4Jil?m+mUpg#V>~Mgr$y_zs zDsxQVF8g?adq4fXHhe;L5Jg%ueW5RMy4CPMwnMg68zD9KQEcoeJ9deXrNGKJuL$;7 z>o)pM=?~-Eqz@@B0l_V7Tl{c847NqC973u(ULd zu2?&*X9i67@!oje5STTtwaj&O0P{`k+%NxwK)aRh%(7Ji=GIsV9bVuKiD8l-{|#&; zG^)&tqSEih9X=P)kMcYr{YJW-L-XLnDl)JTXXVRNlwwzNWgIZlJiNAbBHm(3F;z@N zcED+l2D*ew6*;B#Pf~pmQI5&afq5ZpJ8qyj6V2_&RVf1w*Z0|P!9W8154N7x8AW@Y zl3^4MyLj}ZLpe~eS{6&~+V_@ixv8NtrNh=J#og(uTD*i(Qhl|rul#47XyEQtsW`cq z0*0#6oQu24!kjwl*>_=-ET^K>K_7F+n}53)_f+H61$@)As$YhkldC~FA!hz#Q~b@1 zvH-m(YTNE_XKFwdN98SVQn6G{9DdmIB(t(L`*CW0GobaCeSq|6bbIx83yX<)x?3;_z?Sz)t(mB2J296 z60?=KN`KmRMqKdGR{&*qE62_Rg;t8h|7^cLC8>T|aW9{dzBhKsM4VC+aggDJ)#?4$ zA57fL(0TWo8d9#Bq;*@wN!N${^j!oQ$t*x#fw=}y1J)Wglv z=>`ep9839;LjNIRy`10=?wlOUOfh3#I7EpLk#KMf_em|eWbGp%ZV0mZ4k(d>GPMN{ zF}@GA^n>aD`kldgwC72FX*}Nw>kS3*#GY{$(k|OD~slG96nENWFYwwdw2fc zeHWLTf34IHJ~^K8uZPrBT`PD_x+#XqP>{-VN%#`{o60(4_w;!CZE7Qe3zh&zW* zJGK?>h*BDddImn&#F2ftfO4LFJCjcin)OAJ?Y2{cKXNO^rM@4UUtY7GoqMm?S>_rd zu&%Z)e!kOOEp5TRIgSk~z9nh(uJjBi4>sVNLli6Y4HfkewzhXvb1QpWys}^qh_IG# zXAJm53;pcZR?B6PmYmedi2R6!9=QNHhB8I%Sym4ltKhR3*oZH{NKT!1gHamB{Ogv} z{9uJwGL=gi_z2uxH@Zy@Fqi9&2ZPqW*l6)Bm09;%-y_b__GTJX>L?_sCL5TSumANC z7x0d_RV-nE_476D$+zspw1TXSn!<%Lr#r$9J1Lx>n z<n+@Dy;{S!wJEl^&wnlinkXdPaUq^k?S0 zQ2D31>-zc_be-GRw4^1=!ioBQud(438mrR{vUz-I=H;=YUKA`5HTSouNApX6Mhttu z^rk87=h4>VojYqS{>+$XeZZml1M9lN_piJuxpdbN+N0J)!eiLXjx_TD31R3JQFCye z%0p6Tnt#Ll{54C>*AwWziqgZbe65pq6OUTBZ0=YIIE}qK3AaA?PC>2FV|p!ylnnY5}vSM-xCLt&2*ZMtbjls%r0WRRrOzO@C%M&5AGS zV)x#dY5sl0w;3j1of}1Dsn#HIj})lS<@k3gw!|EO59JorJ@5OLc+L1X0TiZTdmaJc z*gkiwnuYqhE$nP6*$>ZKrdD3TB`!eCIZ~XDLF#e2uIGp(?LoSM?_D>QI>a929-Lbp z9L##wAhW*q5rMK_`u<7T=+ZV01meBdJs=4wR^fWMZr&XN9P6=cigewDE<}naTJ+EM z>k*w8J%Vq~AIq#`HiM)RiAwldjVC4zPD~MG$XcrKGL8{4=q?Sj?zNX6FX@WpoK-#a z70N0FIJ0VP_PaBep%N`%p?3mr8C9b9vB_Sc>#)T{^G(SqAZfr)NAgDHpjRfo)Z=IW zmX7yR)5_(ySK&@|?{isSqyWd1YVmmZgk;9OmH4z(to{`2|9*T;{x6Ij+oEjS> zX2~%*e3O3i#yV~YM+HYn#l%!>&zkyIBs;%0_JCCmnRjDe0V%KOr84n*HwS&O$Pm-% zjoO7&>JDcrr`T<4&BfRGry2e4PJ+nkHvx-4{ZApv9z>7?y<45;+*Q_1aap)LrvEm- z!2@4ZWP*fV+nUbPhiP6<_WZOX?*!Z{M=?+H8mT8ZM;m8Xzgi|IE4jZS1!2gYz0qTj z=TNJ>Pu)@jX}f?`jaYsDIK#JdD>dSE$2mdXcn4bXzJ1D4x>)AanG^Cb30P3XtkotT z;6~njiH{X-mc=KmMGHXS)ST+<=*~dNTh?D>1!`Y9M_zeANbxuAgUL&M!g{|>(CA3_ zwSce&CoRA|fc z>!ZNxzbjBLOr}u!T;Y^hEz50odLp!9tZ8}%pC6JpWKe-hAyn2Hc>Hj~yt%WE10q4K zdgGY`)B=i_1&NwvZ9VfabUWJ;>Y*EslM*?~G_euf#3V^|-qvzzg!@3Wk)q|W$zI~L z{kZKDmFw;ZjU!Z#)_H#%kzI)^@WXga55N(E6Sp9#oD|+3{Q3DQ)i?li0LI3I6OkJ= z=7iRhJ2P3fbG*+^ii%(|f-G zj~a@Y;?(sY37z68^t5ldyc8v3?4`~8T;)FvtNFY;JUE%KMlXAbqN zzPj0w4qdxy{^dBQ54~6f`gf2v9?e&p?zSoI<^Wjdr+a^kx12|Q%MYu}06;3+C-Vb9 z;@CTa2Q!yDqUUl6&Kj6+hDK9(lLdPnsb&+al;;^rgn~;~x_^-DS-CeDuMxg(ae z&>zv&^E}nVA#Lix&bWvphP>>>;Wa@JXeY3thbCQ1eIZb1Za(MA@9~{9pQdIZ-2@T- zQ6iRE_R~H9A9CXCnUiRR#c@oJW>W{Ur0}K-koMLhYuIoI2y}v#Zk|U!+^7#A<|pSa z{7U;XcH2Ojq3!f*V;Tv9B=rp&ft=V5;+}2lNMUC?I#Ev>L0gZb zgw`H~X_x^b;xfsR0nl&88xx;<=6un>A^=wLpuDVgd9*6>#ey#(q&x9U;t;W>6P4WP z=YPr1@WWr(?u&O@*pnAY#Xf33ocJTET7E3Gcl^ zz*g0!lYwVKgS5BRKcAgi=M$TKn`2P#GZPzmw{`SZU*G#Vf1$eW9!PL2D@rjNCvnr- z%mZam(Rnwh4Sea(b2e3wA~dTF77U{fA?VX8pRlm+?@)YLmUxh~eQ&)fk1>V33-v zwGKYEGG3?7(*7vYzVJIcGh*}E{QbGP2a^HsZl0@o4J%VJw+O6_>k(SiWj}*3`|v92 z4%W#xb+e-J?wvPsvx>9Y>Sg+wdMRoPPK+iY=9Ik*4xp%T#(Dn7)^2UH7{MNhwBKA& zc6jc>zKZTGsXC3$6@W`{i|ymv9GC%T*>*iV&^GVpH2FHig~DDW=2^o=J4ADYmF(xc z`b=gYo{ys3d$iEY*+P5vh($imUjS(69KY{e;>4t^)SvU1Jl6EYr~94YP5-yBtLrA} z(kOaGzvCM!H&Sq)|CXe|QeWO3(J2k(HwJxS+NLsp0d-8A?lLUgSTd+O>O>FqaJ*XN z8T*Vp5VDvIF7SrRX#A7BnX7tsvr_a;ckOodrGJ;~9nxurH8|d(5Z*M$*r)Q&s@8Q- z6WEa$-T&Vu!Sx=&v?W|@f|>X?a-QSAOTsOgEhy#F7X{Ok;o?&WDD}MtO8Q#6hby;hNC#8YNO@gc;~v;2`R6?$1^8xsdhFfDgN8{pbC4QE986>VDP~OsN%`E33E5lLa^O z8x8|IkH;myv^;F7xQ?cnRDgDnMk0!fHfPnX>>Pl7D$(mY;PCPRQNLA(h?fD?B}`7z ztcf_Pp4{-T7#Zl4eN{LX{cqmY#Yc!J&itqBklUId$)@lFl^QKTDR)Mk0>h+rvtt4! zHP&Mk596NG2Aq}YSw977s};|U7k#HJkeWL^0`r_dd=WcN1?9J%~vZm>X~dl z!}^NErC3YcYkiGzCqqeR=XLed)zLUmh+NrvqQ=r-&GV}!M zvo$0NNEeYSMxD+ovf1}e^rI$DhAcT%b$@k$jz(_d$nc2CwTnZ!> zYqeB#S#*~pRW~KGtlH1fyF%hrOb-IhVr|xswRVWF{c8I^V;hO8KI5<+&nr=a*ThnUr2V8hY_StbBU(;$!x8^C=GKZm}f_7VZ z3}6LH``hXoBGB=ICvhExHk34I&o)~^8G~|tzGU0)gz#NIJf>X9z{*nUD`VhJ{c_ar zjL4gxej=EDOf;CJ8hoGFj ztr|~`#pC-`D-GoBnM=Ipl^g*rI~|Sl>VeYM!-mS0Kw#zN9+@-6D%((b^0d6xSVS1v zirikZv?ePOl)oVO&Af%sNfub$s#&b#qmQF<-oOBwk^b3Joq_1 zZ2oMU(&tlq2d`@utyT{?qkqKpw9@O};MHZ1Mg`gulkgq4)|e3#5GQ#Zhrm^IMyV@n zQV2sE8CN2+HjwH+U?8KfJSnj?Dw>Tz$QB7E52nD`>uaow22K8CYHXOXilNYkJooy{ zsAX?RI<>Q!6;f(_x()(ltK1d@pX4T*!?m)kO%4K;wmT;OeIc-l)T=rRN`5BDv()j5 zRS0)Q-^c%ByPW8D_-xr^yrv<8^vICipP~YiViox3 zb6Q1mCRz-FcDF#1=;k_J>#scaVgHm%8kd6^i@b9il|A2Qd?)g!LLE<&pzc#wgWgNv z&o$ap#<>=H;A6UH*nxPU5ASWv*xaCTlPu-Y-${kcPGrIobM>O(;Z4Vq(_qCm*A6sL zj-Nd(BD@FL*ciY0scjmXj&HK~m$ zAJ*?`Ugta77*oaUfN6(&7da{Vm5-7hhF)|dNN^qA0I)7T+v@3dsA zRo~swhRQ2$-X3u1hW_p1U{W}L+~oB48@Z$5Zii`AEPeEVaVEjZ@c5lDxsAPP{>g&` zC5SD+a#^xWk249;WHvKnD*kAd7%s6FCN3u9AZ|5_O6mD4X&2?>=`g+Iq@YxiXqe*H zJCV0!2h7CS7obAYrr-@(?$;r`nrec2tfLVQTDQ%^Ka7UPQEYkUIx2~$G*XOG;=Onr zG1%E;4|($r_zB&-NNd!gHl}ybqAR8=z*GoyxmY~g&on19`&VGtV1ubJ7#50&sac|@ z(@C;GgnMMbL1oL1Vqzx{f_kCKUP}VJqZwM0@yD_>0dLcnvZ@vH0B$I}V1%Yu-_#|_ zQ(|H~;!Gj`%F%ua5zU`|M~D>l_+Bhk2S3VCq}d`Pnl6c3nl%be34{YU=}1BH zmDSnzw%gPpsdbw+Q}x+Rmnqhiu!GLsr#WZZ4gL2rOnvfKQV^@v1shVYD(y4ho?3)} zuFSIDL+dpr5+|tq7jdUIX@HTcPwJE|Z)e)Xw0%Yk5U5fD2^9D^^HC@;Q>vehU*QH) z`p47C0wqUkEZ>{R=4|4(r$sg+`Dc6>GhHB{Hgj8w@U{qJ2VDB2QTz) zTG*Gg^IC=c`Fp*uTk6(!Q+js42YiC@ytGH@_LthTw`_lSSp9LUv-nWZ{vTTex2BuV z#H^Pww&mA{phBqkIqMJmQ4S?>!xyYo)uPv`3A#5Tv0v%$L$C6)d_6;o<3pLPVH;~$ z_75G02OrNzYj?-_kLp(=Zy&r2?j*XLKe5o7RelsPFnYiIFHu7zx(X!2cu}`uP5n~- zy1FT@^6gJWm!19<9eOThwJ#4!)6V)^`@4Nm@AjFxTG^Vcr5oIx-)qk8hMfPb*=F$9 zzQIuRgG0zh%2mXs#XCaR%~~_!$Uav)hmEjZvEhM{;EeC9ezN=$a02Dk1kx|1mBzdFU>Ul2On6Do>Lp zJ$O9lSdrrwcWNV;Y>pS|Oxh9mHtC`jE15lTzq^Qm!s%G&WITe|n#$f5Ybf;@Q(~FV z_l(fQkxkrt9j;7oIXTMjhSq>?K>p0$+j-3ubeOINy3r@KN`U`gOfJFp8{{ubH_{pQ;4QdX8C<|FD?FWS5RD!mcvY2DKifc9K6TSx>*@1nAl@oR|yDWBKZam`&27y ze+PJw*f13R&JUJvPxijz#-P${qS-;jAAlJV9)=6pI=ilKR6eXDtQdLpiDfZtRYmU^@1s2*}N0|d;f`Lua92(=u-&rmqe_5z?} zQ)+=$$p*XVmt@@9v`PPqJD{_Q-|Syt-#bK~SPp|8JDAQIH&8fPkYA2Ge)0L`aPW|` za!_|dQqRtgnlB^d*zhSTWgSf$uD~CfGvB+W$Gga?jDIvmo@U%c3-$B)9%v_y&^3W1{@^+$Hs*poXekaAA~NI z@nM7N(sr~<)}{Y`FLIu_-JuWdjExVQxW3UAV z(=+RS^G>ID2?UPZDhw_f@(EKG(_x(XJ!RFzrz0pKT9}p3p#R|>E2m_Uux}Z;UAseU zz8*#c*sI!7B&Z;jHe^?DNQP#*nWwbnu8wo~F4J`K1Sok~y>wu4wBjH@O0c;qTU6sw zS$_fj@fevQ|6um(tmDhef0NaGe7%ALe%~)Y>iw;)M^G$tY*2ByxCF(y^bLJ|k=6h? z;`RXO$c%exfVJ{;pGd0Mg6s0skPqZjrhUy{6M5C7K|%J7X}3o*c1j_Mfv208#_VnVTDq(DGa{TRPFHIEdY>Sc;Lc z2k{hH5`Z0iv)^llEi>5Nt;K9*v7O4y&wbZ_uv?{Co*C)D#Qgw2V~EbP+al&bfk{$Ejco}tG)TnSQ29Ng-TMlhEisA7`a5g4-owzolb@5`G>bbpw^#}aB=3h= zPQ8cDL*!qeN8Un}Er+;-Y&BaL;ZEg$315*an>=!~AfpDEUE7>fg**x;RCrn0I=XgXvpv@F5%8o ziA_)%f>4I2KE%x27QW%1Z|9*ajLQ) zW(A4CS6KF?4;(XrLX`>FRHwqL5a+85h=Mmd|JG%O)4a{(V6J%~*h78R`pQ-4P^SdI z4P!Qc-Kug>97aEZEN12)uZHLaDc&D0MC$ai)cgm@EnD?!k%JC7^(iokz;FPK{uW-G zz;P}sD*9J#*{x0MEHHGo@S0|Zy|-FNx;bD=zvW#HPrW0Uib->BBFDC}KXu7`%x?nI zbw~|RP(N{_z3|&IKD@W-nwe}R+6?NIT-v>mJ>peMQ{$Y~4C$!vTISrEQlf?NUFb9X z+&c)b!8oMw2dc62bQ?2cY?1aGm3(n0C*#b;Uzn*wHfsF+mctm;$O4JtH&Y!R-dVq!9jT@ou~iv4(qcj$!3ETCa-$J2814^V_acv7dU@f$!?MpBIvw%~ z8~c`d2QJ020y??$?7eC6wOQznWZI#CYjyzqlZ3!(#qYbW*{te&g}OCs$F_Otk|~Zv z$%(ct`__I4BoyUbKvK+)#Oc<^Gb?KK4=wL`Gqk^Odp}lCl1n|+M9de{4iz`PD9O=6j}#9wVcrNRIzOA` zEKIM@Ols@8=xsf?{#HO0lMHvP&lb1c6zsXsx|9Xr3cL`iVY3PEIdXi>6|4I71EVak zIn|TU@B&BdXP9RomIle?SR=v^D&UvS*6=Z5kNPmBYdq>QJLTo>Ghyespq*R)u{~8S z0mSU-KsIlGG}R9e;{R=LV+MclKufg@T;IE~wNT=qu^~BLy&kt&BkFqQn*)9X7j-=Y z45F5AIP*TM$h-)?VS>V>u4M526!OQ=HEgb>Okf3Yd!=Nd1WTI$551?~&vERjTiB@7 z=l}tUer$JHyV6&mBgP_vYi`zTe?VX$antg5upn4L$%shnW{G`5u=J zZ+~n}&)1r>IrfvJP;Zf8ICLBUtp48La^so)wc(FHkZt;F z?Q`tAcBXS4q^&X9u92nM|7j-C3b5FdRT+g*$%M5dFoOc%}G8Zbv|+s%jnh z8eC|~L(AcGm{>~~SO)iJQE_M4{bo{CObt|7cjhq^purqYhTbl#+GNmjUI4_vh(gNl zemE}u-u!^h{4im_RpUQ4A(M)Zymq??p(JVs+GQack}G&Dx0qS(70Elss-C~Tbo{>xBdf0X00lHjSX2p#i64@I1e`EtQst*D~L+taB50Ni+9~g98`?&cT>uy=u>1 zYAXEC3=yaxK)&6Qto{uHgp4I=S=tQk_*E1dTE0~q8y`LLoKe%hb%9A5uN-SN@{vQg z1W({+1Y*X#LwJnnaF zd6RFkKcWPMJUW=N_l*V!JtO8E1(M$zTcZk`$MuD>IMn-X)SdXaG&oW-tu@uY3VmT` z?9WmrIA5YEmsYJv2kc~&DioIYif}7o+&OCy+tmY-ACs4?!UeI9H2E;HaF%QBG2L8g z<8gDD%9&5`2npb3CKf4>(fCd(QVDSX2b%2ngnOXGkZHY?Rtg46oNZ2i=lt##q=Wp> zr~cL8m?mo`LG_}ZeHGK_>iP*5O1Ui*W3$C1?M|Kal=Xqo%OV>i#7~@xJkWg7TPsst z$SvT+{c;Lj-S|M96;Q%nj7DN0b#kg!@1vx?8SwDJ&Q4&J76jd$W1sv76gpkaxu2x8 z1Z`xBkl>JChE;o7oEZCq}*rZLZ2(}dt8;Lb*2y_|}MaAWR2$wKbN zJ!(y^c`C0Ncn0!=NXuzG3uKsMJ=s`6gmi=UxLo@76|w6;Mt9^abn7T&bW+m~)HDOZ> zwm77_Z%kQM_P4`<>@&7B-ef$uNpp-|&_hBew*G1-4qYmEpKi6pJK*rQarK*uzlPo% z&pE`PkG$wuFYl)UQR-UTu4JiiD5k52VSf2#IEfB>^*=VGF=CgIbVsISMMFPL&?J|~ zg05mLc3p{kzv#z3Y-fW8J`u6Ze2$YMH74G5P=)5$WjhM=Y^v2xn71R$ zNOXElgdgsX-CD%E7aS}W#}BmN^oTNSiyxOxY3oU_a zXCxSX%3ROHRrd;Mm`7BPQ=Rrtlss3H&wv6FH1v*Xfxm8ca)N>583_3q8)Ze3@v66E zi?gAdPetOf2X1j7*02z`y^+<<`ccSXOZcdg7DW6fIpqL_eZDE3FsQTM<4~SkDkXdU zK_VJV{=?k+nZBxy(b{dj!zjD5)l4NY!7fV|^g!lUBz?Zo3uSk$>cKPMANKEukS(iC-n zuQ*qVgtFlmA!gYjsYym&a1c31EVk;UcMn_%7~uV}_wwn~q5lUBuxS9Zj&lCEYJE|8 zl7Ze)i~!KEiy@+^f_q@Z_v1TvYFj=0SH1-6Zk)hm_H4blN1_{rJG)_xu|vBk-2hEc zJPQ2J`D55!1#Q_m?nE)mwu&FDWW+mrS8?yOhut^4XpIkZ#M^%hRR0c|17iRLTRV&0fl0@2!~*QUY5~ZBuI-i*e$83uSj$72T4@i?FWN zg^3IWMmMTYe^brp!87IUSM$xtteF%+ZQpUrRcg^C2vl)v=ajN>(2MX1A z<~r-wt-eD3e{3qDoodd?7Jc0B$>n50*>#z zW1|b!SOCDy2ANugJJvn_rEmaw#^XTAn9Ms#%tU`v$mO;8U<7fkuE63Uxk)%WV$Qty zqW=KUH^IRnh?tojKVr0oAUJ7mD@F8+re#Eoee^KmU(DfNyZqX2%CNZXg^uW^OU1VC z1&B3aT`t`6yXp})US!EV=A~j9<~hxxt5KqLOf|#%`>cS=uPDsjXw*jK{2LcDA+An& znH$=cmqKInZW_C;%DQhUPU8ciL2K^k-EM79ZZ#67KH>aoy}U$5PJg2~{EMONT*k3L$22O0i6z(57Om z@*eJznzNYRdI1Wg3NZ}Hlp{gV+ed&P%#N~%Oq+$P1`}#RvexA;f`X|pDyGNA>{R0b z62pmMV?~qV6$zojVh&dYpPM)>hkITSSvCO&j&djZOhYBNZ}Eu!(2%Bq8rI}mLEx`b z;h*qZbq%M#Zj0_iOiJ|$GMKX0ntUmcxK04|M_wfBCg6TsX_9@BTzHo&`XRD`zCDQ? zZx^&0eIb2`(za32-W+4X@(~fb7k3c$CS%TaE2V%&k*+f!1;)6(k6gHfZpBRA-@a#2 z_##*gwYIr@P7{64(q(mGV#bc~)LW6#`V^YcRyu*0XWF$N0z5w&JEuOv>MRd?|9fkX z*-x>$argXO82m#IGQm9fhrQpS=tc45x=-AHic!ZgZl;NCL12N~f6OQKWO{wIc^)LlX)X zlQLE(09!ZX3x3Sxh@!WSpjtzsYx^4K2ok`5YCiZMy~YZk0EtCUCRlWY7E!dk-cr1L zXoNbI#=23Risd4^*PIR5oi2l#hSoUogFygh+Kq%vCGhW&uoxvBL$S~FvSI~3_r8GP zEHv>(eXT$=bRno$ z^#j#I%`jrTf-za@m0JAC@M^8d?QQmZL@>?O@r9l|008AUOyQ8MBDqLfHt<>h(3nEZ zNEi1dsOg?1jz4I<@o(!e>%o|0N5jz*iGT?wjhf^| zm^|(RlDSfkFVX(Xx7^W=%egB4`$bFM%1oKw$kNUNu=S|iflOb-UBMI+3iBZ)`}G>q z42}KSzci0+=1T$J;Cwt2kwHH*cO&=$x?uyp%S|;lj8H$qHpAN7z31c*;nJV|o)g?Ctw? z`UDSUhYd(IAHpXMKsSNqZ{sLybnm1sOzQU63+K$O5V**W*m z`QH{OWrmaHBeJRyd^ zM{Rj`eWNgUZiKtZXXBr<#TFmvEFezJ$!`0*&|tZxPk8?ms{G$|qv<)DMt6nD5V3v- zs$rG{Uoid2<7Sq3Pcv-|+LJ)XQpyeoW9Gn&ZYQz9vLc5EMSRGn{LoJAGM!t&d}tzI zdk_2e0fHCYes8B(VU6PykfbraJH5?}|1NPqxrQbNI*Hm$yG?t^6?wT=E|%KYoYFDU znCSg_1P{)LHC)90!oVTM8ff(&#Ugxo<5RL~kFfjf`WZ$y9^=G+Cq=BN8jzcUan64S zHo?qOpg-D%UoRqn`oWOK$&NFINfjAEwSvDNV4hTKUIyJukk7tF+T3$Y`j^Va}P$QN;h2J2O59=*!%mx2xi7H+GYxvTRVjz^quRs$PmzG%TN{{|8GI zdCMB4d>t^8 zr;$y~MjfX(Mq2O&Nyi{9w8iyRKtRy+5KZbJ5I~`^|KP$Q^BW9KI=An(3vmB%CFi^1>dl zMuN3P^0NOxdsf`H@}{5xhei&F(Ys`PvH6!J_VIWKzT2lNC#!MNc%y1mOTAvJ)Y*R5 zT7$3Pdt@RKb(PxO{_BD8tCL>uApKrLLm$v%MW+&Y^k=z$KJ~aYo!FrTH5 z3&w~Jv_v4yZQ2ULvDl{?mrzD_ofEhZ&|8gy%n8yYM4Wd;Z=j>kgaTOb#Pw+;n=16h zw`XJ6`JaM4sT&*C_d+_g@5G}Ig*G)8Tb0!SjV0U`~!I1!?A8 z@>vNER*K)1rCV7s$izx`f~YG5XtiJ)qHg8W5#<38ElNJx3)}LzBealIaaxl~YqlM_ zo+jMn)unarP>3>S+~s6ib3v4iVvNYL(V=yRu)*8;QU>&=2pw*Yr-7g};nw?8LrXlu z;Q@iNcUi`ry0;Ggg@DoS=pP|>tR_Z%4nCZlg20_l?kGGs5P9Xwn$*&lhiUEhIIU+A z!*iFPR=O{t7-g|B8{!`|N;(EqE0W7EA14{t;G>STqw6?CKyO+636clbOVuX%RPHeY z0^Q0Fke*}sv6lRu#){xA)8=~t8As$eFi3W-a%`wjs2QP#+e`_U_%M;B%KgkbJ$0)l zfv)jFB`4zM$h}S6%k@fE%U&RPBsAJ%1Lj!YT)MVUVe(?f8k=eEhXASH-B@@2YHshs z8)l{i@Fb|Kv{%G;UFMSx6|E`6$tmGz_4~zsW)gfaswbf7qPd^K9-C9`xzexH(z~_x z{95DlmTvPJM?+P$(aM?ehud5jc~;j;|L;#ro{m;Az6Zt__1x$?6|Z+x`uXz$knsR* z(~5i;>XhOLgS=1xiMr&vG77B`COq=5CWVObm622ZQF$Sj@b z_$C+ZgJYq32>vs{I`~!#2m%k;+7o&=k`=H5z67|tk*VTaj9s{DoxaJiLg_4TKj)Z+ zjGJX(JDV)u%j(L)RJRl<)QW>&bX70&T&t16jx*=cqk2P4iwpV-g*prAU{`}uvjqUI zNoaIrM?mXI0J$`r!h@n_xDRTs_bPmI0jLksspZdk60=G1LfoT|YXz|D zd<9M(X=~tz6z3-5%%SZ^5?=*5H&?e~gxruON=vM*tnhMP2fn!xju~uxX7p4Cj_gR^ z+K_e5#@RE^Xn=o48G4iurcG#(BoVOq7^diGQaAB(>sBJo$#+)zNbmzWBRJ;|0;RBj z#I-Nf{Q8qH&Hbugz{NCDMQv_%sY{GhcpQMF5zvOx$69LbBBuaK`okah?0oAQa|M8j zgARbpu^L*o=pQWpL6Q;2e3d@DK?Cx(vlN|~yKBR)3sz~6 z?5eJ<=;{MqXQ7mEko1$T3{zNPEeF`@+a=2^ElImki)NPuG#u562qiw)QohzA-$4m9 zrumjQT`UHrx9tvu`s5)L6}LxOdN(qq^u?Cthraow zRp$i~K7`FB=7KCK688vnz4yh-O#2qwY|XXc1LmFTAaA!>yS>|@-HSOo}*KZh9S6zAA^oRND>EIxn;(M*WsKEDiTM1l3=5s>>eQ?7Mzj zU@q3{27fWFYoMz2rG!KImS)|$6e4EP_l@D24+mv<2_senx{vU7YnfnQHlHJ*T zH@9CBShlRJi#4=A|NUok>OVt^CfIWWLQ3G#WAmUzBmVTlox2YvJ`%F?f?W%478yI8 zBT%&H>aD0h%B<$&Vc1Y5v+6p#U&dNC*VWw`n_HL>h z#_Pu>c=nu8F`>7X7#(PNjGP&Q?F)YRi3t959zafCpoaIavo?=)Ryv3ATvj6EYv#)pVv<}Vsk&g<9rgNW9tI;inzImm z0+L4DknERs-uL2ek_F<|C$QsH_6`#Lz;WEYlvuKh4<2(1P5tR&5F{#p75Wa!}ZI@L|6pEtxqO6IM8Us~c z=L409>bXV7|8Z?{cosR$T*gZ^x+@X^g(U7JQb18BFFjyzU4`c0EvWpTLd+=U@ij$pTU@?-3YXtu2t@}!KJ2g1XaN6D}xRwg=X!g4}bu8 zpJCnVO4-=Lxz<}eVmh>UjczMn(t7J}I6Fs3N>H&m$J{AK(9|i%pHDp4vXgKh!-hIf z{d9ln{ZF#Y^?g+6t$_>+sqQWHWL@Fm3PdN6e0E}mK1vekF80aDYPF#IMozJWbkaX6 z^CPV(rufqJ?b6Kb>pVQXKF3Jf>suZi0TlDU7L~W^n5E2+(Jp!02~v0uKr0#?^K$UB zsc&^+Wx4@SkqEjmJu0@50Rrn$d$&X-K z$3i7=45+kOPxhVKKE9K0!W2c01b0#{9R zPz3}{5GcbQTML%xWlfdeNoN98zi_8eVi;-8W;PGc9KRcRq78;yZ~0*M6^4-M;0?y2 zsg*!MUTUn7$>KET^DTa73qvLUfzuE-$@WLu=7~gT539o&j%^80x3xOc`d9P@Bmem5<>8UEB>s5ZML z;Y0TKr@A!9G^#c2Cj-Sq(-jK?_UwrDE23#C$VXjHQ%>_VvU@=3B^65PgXS0yws`(l zy@Z#tzUzMjs{PuR4NqO*3Svn;M6?%XU-_jX;Z3ZUaO1>>gymZuS?vT*!0j+wnQjX) zdKGG|bNmp4)|A}bY6=-h{SFZbZ>JIRCS50P22+-Ofa?4e3uE>=rwxi__ZZ*k1Ydhd zt#`u(9Yr=nGi{^ap{bTn20?F_tmkb1f%3~U&aO8vV*$xX#PkIA zTGN>pdUk9)yM)kYU^h+0PWcoF#Awgo`gpG6JZTfWMV}vFYZCMso>{C<-)0jEnJ zWLn6R3^FqtZ_OG`zFn_&h`NxLm<^mRw|6G427E&gppYm3<<3m zTk#ES)sGKQEN`JT-U!Uw-sX-2LBJ>aZN!1^llC{zYvaYA+-?<0t=T0z3%WIQn#J)D z|6phR=gxhT3SL&?5iLAKA1O;nJpjY(lAk-b#WvRua}Bo}ON4tih1_vu06hE7$!c!X zcRX|!dRo(EOkhn3+DrD$ulVxn-=LSL@ZP?q@m?Hg8GX1}MrEHrGjFNi!Ukd%#p=vf z(Av*kocx$z&JRbWBv7$V*J*2tBSr8Fauc+ab+AM%8Z2XSNTpE;N0K+knq#G-ASItQ z--XC3{fhoT-USH*{>+GItemDS@XZWFqcTG2imtQj0r_%7;l|k<_gQGRj1{ZT`wdp> zU(j}yU+V?XK}wG7{lOY@F7LxZRwHqd#Kl4bn2y%qk#o`Ajosq2kF=8{ z?azRx^OAj1mgx)uluA4=wdP=>Hf31WCcs#8U2Xtd4tSG9c4s!G`W(|pqs9?%MYwtA z{tw%aXT43HJ*-a)so0jR?n|_g?}>xL#sZJd#V|d@OP)D2SITaI##s0idcQrpPe8gn zoj&2%{@M87B*6cGfh|SJyid=mlP1@vP6rQ~TlE3eZVCE(AH54xU6c2Z0-=?7NIwNE zR|NuM@lWs0dPSEs))Yz*Zt-YqrEcz?_d@tSI|LGsABbDe zVp##$>WkJD2a(A3PA#1$ydO`4os00L`Gm3}&Wcp?&$7o}m%BJdSr zy1!rV?xpVP4}=giin06EJ=Go808O71>E6Y5lKb`xCh5%!zi9!;bfrB+ma;r<4hR)m#!Q-#Poh+_w=S~qkkP2aOB+c~u$9FEgzhO)MCGDaFqgHFN z7DFRZB*Zdhf7-i)?Oj43&?akzo*xK1i4ppQG(|9RyyNYhrtk6~heBUe+Pa#GzfXgl zjlt6(pm_*Q!v8$Vx{_$^r!CQ_nsoXCH=dk+@BZ|t$HB8k4t)G+R7IPI3FgMQ-gln9 zks>^TJLvKAo1(3n0N(+W7WMfe@j=>v53def62GwmU1+RM;p|M*b0KiNlos? z7am%^d#cI)cX1ApwuWY_c{0&_aO|UTAZ@x-R3z0qNx2a&P(X+<0|>nhLFPPa_ZItR zdQD>_-VQvCQ~nRSn61lIOfz|@6g=zGx^El_hZ~ftRV3N#!!iP0J1%*C@#sKr+9$sJ zAKT5v^S@2NMX+ko(oCFz)_*}fx$AA#-IIcmEK+>gjR&(SM#MC3)aetGn+p2*Mo`S%h_)>s|6I zL!ISO^ z{(Ow8W))OWayP$yaW_i-RuT6{_oF|%d!&JRKD07I@yiV|Bk1gB-2AVc*UX60V+&Z2 zRr9=jM@!2!>tz5y_%Cx+O<+cpV=J{8?&FHcJeTUh&Yh)vYhq;`<~BL@C;77uE6f(2 zUeX!2E&eQ23B(bKO5qL5N?p)w{?KO~OeGy+cOplhF0I0$1&;?RTfpDl&tyY=&My@O zC%@2wuXxwCKHX87bs|l5Td+D1o_LUq!DDUyv4qNuu?jz}XaqQwdAWfnJ0m*yVOV=? z;SJHw9M6yZ2KDN`w5@G4nT&Ne$TWmHUQ$DG3^UymNa?kCR#(@RQ@Jx+M23bC%e(*P zlDFR-sYZW@=S{o7Kqc|sjm!grKLs2YTL<6a8vo4;Hd_5v*&;6um9WSna<1nNx&Ddp zQ2|Ao8Eq=$JWe_%=4x^G$x3N&c;;32`;jG5Z3;A_!s>xi064(AzUQ&xvvgMNZRSB- z1)2=cf|PY2SVaeDRZmx9tFCm;7QfqGa z7O*KKS}4~A1Gy|nnW=)))wIhY2^|}iDX%z^HsIhGQ_E5U=q#OOx_=SM=adEr=6Te_ zTBGluEb-EvO#SYl39LO)Cvf! zud?Q=Bqn0GyHb=v%6l>rN6#u@QESI1h+^L~%ppdfBy2zII#M*I@J$(nOqQt#E-!4^ z^{!N}&^|5IY$r(@C%cT`vp3&o`j1?`4u-t_$XXUf`I>65W?I&u2Mce5EA;OZ4&Pnv zyVHBie^_z*e{8A}cqew*LfSC##w{@|tiwwAGu>7$bVl)_1^ODkbQHcuzj;W;#^Cza zEr1b-L%?krtxLCG-30Q7Cg6zc1>9b0KC4;OhUe7_`rpryxir*jXiC;9ifaOESDNbg zH)*-{?$r?-)D$|t>s_JlY1q{!nH7!Y3cCGEB*YkU0T|#A`|=<0&5m~-ZqmZtWNSK- zN~c*RMgbTp#O@Deo#7t9^FSg^x| zz5)vih33?=kD6Imz1F^?n!sSiT@(|J_rPHLJ3in#L=( zPFo{&__GT!vhA$V+qA!A@4L1v*3@g@PZF2`De6kghRm{M+=Wn)9h^9Q!?UA~q|yUW!*R`O}! zied7ygdRf8&H1aR)6j-ye?g0qP)WiOWh1wJnPmsKG6%|< zyqpbI;;kvzK3QF+i}BH-AkZ?6nH+efL zFZT({7*q_)p5d+G(XD{HOn=hqXpE6R?Pls5mtoiXT7YYOCP6eLHdQrivIzrk zkJ!i-^2?QPub4dH0r`Mg;in|uXU`mSwg*f)RL11f?$7uoosT<-@=%4t<(DNJA2nSf zNVgEOceI6Ovz#vZo$8R=9u@W&H?bY>DsJGEb4pJ`b>p1ycH-UueKo-PCZIAu+$uPb ztsejBtxDzJKCw0Dm^z>m*79NQkaACaTk2*y$_Pi5EsmSJz;i}pE5&xV{F(Bp%nXx` zjN3H*txfvhQAB-$hVe%H1wo&j?M^=R>)g%yTWXvYv>ZRO&R;f=F9m`(Y_bFQ%V-KW z{vI)_xFhxX86jrawhK*8OhB%0&@xBwi|z*kH}5F^^EsY>X&xkfPX_VBG>>H2bj+s# zz5%$;WT?S+p74sHJj`$_QYxX87nfsdGJt&8^)sv$_=syR9wxK} zR&{$L+@kh0KUCNg13WI#T8;~qX-zFoIvQvau{y%r{W4x0j{E_Lcza}bBr*pED+K4; zIcT%S`_brF^v`Iz8cid;nrv~o5G{IqKSg%}C6{IcdUkW!$4?kFfc5yDzzLSl6BBA8 zs?jv@L6^8a7~i&FR;{|qf*%Z>al+GMX|OEc5KJ|>0SHNhcN{R4FIt|4e=3(gOx;hB zoR!iZxH4(1I{>yei?Jx3Ta8|DQU7;n$UJc|=8kSj>i;2dA;w?ejM3btszrX#SykP^ z@gpwim^CbYxQzONSp|oo)73($CX$-|o-eMejF3`nzEuqCXtBlWXZ9q&FfjXpW%xHf z%jy|02t{UwFmj&^vjf|P(VloP{!(elHR2ZBSQCS_h0aWTD^@N2JPAfNpVCmjh4iUS zKRVMIJ_-hLr(_fIEB(W{+j}Tn8#y-RHmV9+2?9{**0<{Hb>P1q1zjW z;TiA0VH?8CKqD>g6oXE~E9=!@`=IpDSz{&B^EUt?G(w&0Y57~168vHO4dq5f6M;+& z7yF;_1 z0zCwyh9|6=oA#xx=kv>bS7k?=pU;;Xv-S8}HWBfWE|GiiC(YVJlm}zoj@`WVjxk2yoFf+S_f@_h`Sl4TCpYI3RWktW%I7{hrL|pOP z7(A^ynm1(LT6zBWgqy2&z1non?3T~+%5(xy`X@i#&)w2B+;i({LoBDP0og*E1ut_fbK}GQwkenWq*%V-S2XBVmYbEQzIfwFO+w`fc(4%|FEorH zzD3x7m__-LpiJ~c{fbj2O0&WH8X12dnM#uVI;7)a}7f zryZn%2AaYPg^SCrD>5ysf2Ml~4O~NWney#UWiaqh?~@;yy`VuI07OnNLB6n*m5LV7e1Ec~~NA;XSvOb&Frx%4A-wjdvTvmo71n~?;fA~_( z8f0%OSM4e+Dd1B_&irjWzQaSE!wcn}X`-Q>6I&cJI(duI>d+-|Kk5*v|+rD{d zH>)M)XFhOGvHXYK;c4)R84lwn~Pu|7$L^(@kxW0^e1k?qy!l@y;4>9Kel+HP#Tx1N%lI!!8a=63=;v~ zoxYL778yr%8p?Y3tks(5+@l3phl6bri(l$iTjJfmTA_6oL@g@%_w-;A0szDZ>`o2w z%p!m*Ml;61j38m+Nf5hGv-P^{R`xMFEAS_ZJ?XdxR`N+g1?UOKI@4On546X!e<^>tb95Z3`!?|*-tWVU%%x-6ppr#m(-y#J*-LZKVSxeMvD8bP_*A%e}~Mt z%_u>)VUGXOJUEOOx0smCO0QQ3*_@xttrU*aqLvktU+jFwj7^p*{wXc)D9fBXvsv-g z6@LFn*_$IZIUQEHmUoc@|EXd39Z!0u;aOFu-9tM9&h|j;l=;~p)61r(e`qryiiQOH z`mt(oqaX`i+o42a?I+(QGuz1BbAe4k!n=ameb91-vwL;o$bh^AQooI^PxWHM`yyjn zzkIJuYQqQW$n=7o_+o~4XNu_6Pl6=+zyOd8bYgdw)=K0=6V2|Yg#5G}XS6#S6=dYy zakg_jmb(YEfPz>#e$3QMKTLJvkh-G=a7Qv59}1I~y{5$L1u=d)Kz>LQX%S|>CS#|t zzNm_;uYP}hKu1TYS%X8d&K~CA+&FXK}M8+LhhQVQF`z4W|{$gX}-JAJ&v4 zb&3AHxiw?^Ut^9-odh>!eO&XGPnNV<>uo8Hkunp&3}AzXgO~s%!Wy@ZX8hC-5;y!< z&^+~1m3LqD0s+8JA*3W9%BUqZ@2gJD(V244zt;kIt1bMn6jyzsnIgmC=Zt+Dq4Hh| z2Y4Eg9HB-dB_xe9mH_B9fG+(P-KZ_X7JC!Nk_@p(Ar(ikIa4JBm&^7lt^EGV)%vp? zAf>E_2QzAh?kE$BPuDa)3Dzh2^eb(S5AOpx)0M;e%ij~Q}pFqbO9O$FrzZcRq=MU)oR0r17ob1Vb*72|4}E^ zT4n1!m)Qg7w)ReKb4v9a!c5iWBz{xPBl};gBOm=LfDx)OkaSA!|hb|r@#)qw!-8;e#eXqCloXFm?O5KC7;NJAbR2_Q3*yU!25V*zD z<@KU1i}ETm9zA7@zM-q?9PD%ZQJW-0>2sgcP;Bf0&v&WN3+LhTS?0Ka75iaZgRZ7Y z%G;82&mNLXpfaCsOyIm?C)4in$DBHcv8U^+j@U0&KtU2Z^?-7jfsRLs7_d1K@i4Y& zKA!5-T)aKV8>*gLenDxK=^i+~;>jXPI-ilsofQ2R^T~I55OznedNgP>7Nj^3e^tQJ zoUXS1Y7HY?SbuOq>eZAO7hx=;)TuGjzIDJD0o+JJ$(sm2%i@UF!+-u>u2}u2->)y; zGwQ0UO7l}c2{Q-XmCAgAKQu8&t_&(G>HT(THt>Kg+OvBHa1RqZ5#7~xs?5Ax&2t7z zd)1?qrI;~=)2DsCZ0@BY^ASjxxgiridnHb~RmwtF2@qOI(Ht?Mqk~x%X2ZZ)iyQu#7z*1$mHwn|;ZOBBXQG=M4W<7YKNqe74HlL7{)7DGaJU=k7+~Mglw%>sQc@4Iit|yd0-623AdliAhBGIJuu|_1>~_4jZZ66my`ReG zOH`@Z8+c`-w$mo*=aTl|sr#u@_BRJDUQl*hRh#RCH}`P;m;BOq+Si+hA6#yJON%v4 zLLT``nT3f#|QC{)+4 z8fv++E^}CA4m4mShAS=!Lu*ly9FI2I3&63XvVAmgd=a*iq*ZMG^n(1ive#q!IG%o? z6~u4_?V0rT5NWdzfYuvV9H__hN`tvjMnmDr=wTw7V`h1rl8DP)X+SoyLAmwC%Zh&n&XV+ z6<@5gCA&J?PSt6-_BPA3H9cgDZ z?w8(ftQcmdgnaaxA5`~~8FwD8l8(FUGJF{@FYuPbHMi5r=YMSJZeyF9PQ?}G2JLz+ zPN-igJp$dL3jy)zN<{R2;o02RMGsj|Zm2x2WlB5_VIkWt$p%q$|ASWA>zgs$qF7kG zZUuYj{SCQ*eh+_Gt-k0AzUjTRgw5$PNtJr37Fkyl@C@qcQulZ31a=W$&-r4gu;Whw zQ7`wisBdFS>EHb30rJRyl5&c!7DG*hew=AEGvZL*gp?e7+F44XpI7;C52o#h(^d^qAt2=y?>AjkhlZ(q`18Hneyiv zqgR6T;27UxHIcK*Ql8mEbJ0srM4d`rzp1}XxHa^Sdw3?z>*_&KZP1ckO%ZVMVY+%O zcE^t;`g+3mFoa?@VWY#a?TiJ8WO&$U|GbbbW~2VCs(2;L^kNz}!`cmI`TkB!3x71E z-FbDTa_kxh0Bv*jQEYyLvbW>(j0iBLpMi2pOX&o##u8sPDzffszEQr>=sps2CTeR2 zr_#EB-iMO&!)MeozC76k`ytIRW`nrJ@8<#lLdhmwCyFoT;}%lk$P!3CjD)!mX?DXL zt)85IF%O*KlY=ni4h?$ru2^)*eWqO9PxgBOc;t~m$WUT8n+b>s-JK2{HohHTs~!ZM ze)5S6A-!sYSv;u+-VZfDK&aEt3N zU!ihi>{;mNF{nhQbBoZAxMr=H`wHwT zMaJ5iABm|vU52H*f0=|h&KD-B@nK&RSq$+@$x1AjyOKrjVKhH1$~emT5l;C72P?<} zX}5@Rd3CmvJ?o=QQw=WLL?X~8j$!UR4u2ybzRs97dB9I1f#F7bg<(NA2aE}7f2c(| ziW}hGLQbfP4}8Tw$su_qGQ_Vjys-_rnnX%_wF z8R4rka%ETXXm7JTc4^2|C9@9AQ2fhOSsh`50wU{WX|aT{fvBY&eaek>Y6j?Sn_q!tzF2xOY3D?=Y*?o;f(mLl z!R*qj!>y&ZzXM9P#Vvz`-U(K<$7+IsiT74j-Fymc$z;>V^-1-}sjW;U!eOL2V8y_D zY&ES3XeXO0=xKOwN6pzTt6)&}WgkwQ771Pv^!Qx9JG1htIQu8y;eZ4{{RbB18qDP< zi9UOT{SvVxw9gpq@t$jslU z@lpc>^ri?Geuf}UM!Lh>9y3xTzs3wlHpVIOvirP+>2fEnsH`}tPYk7RE8P^%tG?^d zrS}JW;v#MFT(U3F6Au1jeZSt;7EXTtdw{iDpI^{PDu$0_VKW~sj9NMW7syYE(pXGC zF=N=Y`3vl8krAG*TElN5dyzBsEa2MfN{63P!GAwc03ns??2pk++(YK&iNtX&ezb@T z9IsE?V<0amiC?qX&QezuWt~*`Xw2QRD;U^}#!ggFjyzcQh31Bw0M->;J`H(DjzCM=9?f1a%=fcmDa2)+r&U`-uRP6<%g{!x z6iLY<>!VC)2*s?t+}gVrEiWndnT$@*%PK<^0+)D%eu{`MST#j~Kp-WvKT7?|m#({H z-<05Qj1R(jg!9+>odJN9c!JIExQR}2ZUF!w@hLp^wBkr~oWxZgVfzO60to8@eQi5UL(fv?`yvm*m^H^)Or8<&r=}Jl2$U zL9{(Bt15BsF=P7bS*!atej_EH2e=cW&IP*FIKR;zXz8Cc$ztD97x1tC>c!dLO}e(> zS?v5EXLajJcBQVYPty1mGnTwcuEm@*b(qE5nyjV|njY?i)9M&F?&(xvPcV8VKgiB& zU^LAW1oGpuOwe90X^*=tp9b%8<|p5y+XKD9@*vaEE{#80)#Ux|^kVMjj}IsG(dWXk zNtGkY{p56?4PZ^;t2@1UihWD*fB@(g%z`$fEoyIT0d_ZqC&k@Lni8Q8Yvx~S+(+lTigS>Cke#Z9 z>EbQ^pyoXu;*9!A(N77BK~u3)Z5DBKC&7~VvC`tE;z=c7FTMM><_Mxb(qM~>E6@Hi=JEpn-fi&BbD^E{*#eM za8e4x_9VQ}uZ;_A7ccl&fFQnJd36lguiz>#>8HPbsz{*Y83DARs`c)D{3%mEGv=p4 zbc%pxh1apvg^?nSb#=wDauGL374a?mmL(a3^+<4p=1N_^dxq+idfZO?c=*9@B-$*$ zN%;OI%Z2mt>>brU;7Dl5(LEp9wjcLs_!&U1{L|Fo!D+-CLVOr}PMr?eSq4jBe4Sd( zc0@*>szXvqSnzN8IOF1Wp%CY!F4wX~&{q6TR-1p;tzwhgM zy`E2qPMPXHw^TrUh`5Id1SsX_KkL}-Rw?g1{!xC=*4k#aTp3o$1GlbcWQ&*cAKV3u z+m#>-Qflo_&kX$73GFHZmlV60?nNb3Vx%rBpv$b-At2bm98&?d8`742qa{0+Otpif%mGTE1Ky06WX z28{!fAZQmpdS8C4mux0x$jWOWEVA}d;EZLq=Jvp=-^3xcM*7J^kJE&`7Q_o7n*Yt> z2lyLmsU^6m(6g)bj||0cZeq3mww!XF{VNvmS_j%g!DgQ7`+UB4b?&EErT8)L+=pSj zz)r76;2{tG$bVb$vG-3fK?=*Cb^k@6b*FD2OJYl`AIGhJjDQB&aJkrUCLf59_yckM zskUqk&q_lV(^ro$nWc)YEG`idUD^@e)Q;i+%xc%APLy59)G?Al#M`FV1g>5%42VKW zPa)jZeWr6Se7mSR`$=E)+I;2AqaIbi=GpOTay`L#U^<;AqIt&V`zt?im&y5#jqP8k zz78GS`x@&_9UY7N{JGa3|6$s>;(CX--Fr6^hEV#`K|ST{zq>Veh#n9l?&O2k+y4zFlicL0c8`9kso5s!@&whBm z5FHG6;2vNNH3@Kn&#p@a-q#MZo7Jf{{{2d-<7d$ODe9lvYG821XBK4|W278x}95>f7Hgbi^U|#pN9vSh7mF? zjat^OS(bevRzx4}%|=Ymya*1u!{Wn;8mCmMdJ9fkn8dRyr}krfyl1r8n}GO&akHBR z+1B&Orgt*pwUk%EP%vv(u)XP^arp^?PV7SoghmYBS6A`f)Vm;iS}9_e?r3YdN`H8H zV|UFAqE|+~SI|j$YB9#m%s_u3YWHR|LB0JVre+Oz2g7x)&?zJNebM}|{E6qS>FhrG z5;#*zuFq5B0Mq~}Lgg6yq{v{=*XU`~NKCn9!89Q7(iQ$`Th0^o=D8W|zqH-PB?k~s zy)Lb#l{wo-cY)z2_~Mk>PwlP_wq_Q9VI_ZX;|hxniNw@+_WP7nKB74@Hd)=NEZKJo znxhm8?#VcgBH;oBG16? zh1yJ8+D|Y;y9Z2*9B88Y+Rv!+ zJ6mE_W-asas5UT0c3tVW9fw+=0T#4HhZSA8B)t8j*5{{+FpqRe!fiC}37eR8a7*D4 zVH($VX)NX1`+khx2dUrz15&|0mOOe_@(UIVo&Q_Xd{s$wYF2R6Eh^` zMd#(e!+Ep}cXg}T^{lX4(@%`ex{6p-CBVAPft^^n?Cep+th0+nIHhZh-<0vNtsi^o ze!A%iMm40inb!I1A?)n;25XY+JsN)s?h%m=` zeYt5TFcRCWz)eo%DQrvaSKYU#YRshH2ompAm67yU?YxxjpMfo9v@UsvFo-`ipZZe$ zDb@F?jFpSGxjxh*!5Gm5V_aOsoY7f{VhSc!IP&JiIo-d1uKlvl*wI0M$mYmYi&Gps z34B;}0lb=+vt6%OF5da`Xriqwr|SV+^3Mjgs4*rH43KBE8K9kp%ra%#5tl4&HLkzC zhQ^Y;WNdeNiLW1G&43Y1ELA9&y8yDM*Z83vQ?v0iB|;+$F_{!$R;KY^)=z$uqv_b2mAcLFk9L)d zy{%r1U6ca7igFC?Tg~F}Kq6lMb0U`<+sq-2K&FD|693=RnBnsy#K?5{L- znHoj)6y?|(L^Ci11^OpS^l=Z4ewbAlG+>hXOR?*teLoI|8OecI8DqzArWw}~GbTJ- z_bkq+c#9}-c*{dM4<~29p>fW3M|#m1;|jR_Z`9tLlQY&2{&$>E0Fa)aZsMxxFwGg-N zk+KzZUEsV+2#pJ2NgO~)Lvf{=UR5p93cMpY!od`g|Hp^R17%AxCHE3;M;d$E%E#D$ zibvaJ-qLCQcBQN+rCq7|UHm^3?(|lM7BVqiJw}t}V7)dUE!1yWA_NwYc zUJbx8M8>n&dZp(Y%It#qvIDV$cE!!P%8mapiC}~>aftE3V))S>yH9EJly1%rVYVv6 z2c}tXXcA7^j{UtGIx+>^nE3%SV4V&>r&DvGH1=V3He)Q${=Pk|v(71;h(<>EGxV36 zPygfRC1ta-Z4^kU2SQsBvHkv-jGY@LQME0K%Y)Xwt7|!vHTPc6Vn4I2vZXmY3f-5$ z0jXI(n0A*ynF*mAWv;Ot{p$=srI++^+sw5<1!)P>Vo=Dq^u`PFQgnKG5F|*IB&4F* znpyNJpyG<(2SBe$WEl^NFU^le**gE5XMUgpdUQgOEcd<>61mfrG%CX7{X53)S#WSi z!lJiike-g8{;Z>-&^kOLTk^!-GHpiv_d|sDn0lS8PHxJ^jU@@jv2l{}DtWLdge&BB zh7*IZigl3>Abh5UW=g+@=h$aaMO+Hj(})fhXD8^SYGuJqn@?Gi#Pk&bO~xu6}a!*vt_ zKU@C%V*kqJ@+ur;TcUgvJmgHWzyYk zl0A_!HYF?Zj%P@T{|urJQthJtjfo$iB8iUWR~@L?SUfD@}%|21O^W#bG1 z@*`o&J;OWo!7gT_CXuA8vIGENRy3_x3|)}5{*k{T$8bM{dY0=WsTyd2&K<{OOa}J> zB@{p>l)diKHjF@s-Tf;wzh7Q~@i9==1!Su1FLA}Q2fW+C#O#<^#m?4?orZ5^Km8hf zKVUkG`&f&2HYB|fTpy__qpy_S3VS$ywT5M}cvQ&r))V9G^LR&RW`vhQ<^3_Wqx_c} z*8=$d$MmgxE@=1i{k`$imUVAFV{=^i-MSuP>ixrz$!X8yik?l|JwDos5L_B8Uy!<( z&u~@-eR|??X{fvE^Q!fqx4}Z(JZF<5H;mpQ_1}Bns~P_mLwiwI__*aLM!%kIasUIX zIiZ&QcOhUby{7&@e5Rv`AcWv#S-SEQ9Uu<1Qa0xvXfu2;Th)HWZ{z;RLv znp93gN>?=v5)|#uUVY;v28BFq4zBFJW6^P2?h&%Y@q4sKzwz*GabQ3mF}2`yquRmg zmf6njXa3=wL>~D3+}qVldaAlI^3z16*&0~0SHPXNhns5+uZ|H@8b4oY#MsnmP z3gkGWu_@CSKawbZ97))#A!6{2?2VE)C6#7ZYi_-%FGunGWb!7pB4#O%tYBlQP?e`cbS&(dT7h^gxUoBA8#W)iyn6dAf{yBv@aCpJ5& z4MerSBzdVS|7|Q+{v{W`0bGg?jilt;Yq7aw{(TJPr%0;KWbJ1&r0YXDjpC0g zQF=dJ3mY7K`=l$JvxXYr9uN7SS=}u_$s`NrRuJ4yqdevW)(Ik)7Of-Qj5AyAx6}e$ z@5s2ot!g7HO_Y2u$F8TU?c)|D_dRbOJsLgs&3F98sR;&>4*jeI_*j@-jFCM8ZWa&t z?vc#Qk9`aM0x$HjdsFIL8xqq&5R1b@d$V?t%p!WGX(;Zkt(8F>HY)|^)%Hz4O59Ak zCUWBGiiOwJ?37_L*>Q&U(g2M4a#wb+Nj@ZELo$Xdn4rz<;d#$aL!yH5o^U-{s6p4OIxkqBY# zJ=f_QXq%c8zBYO&F)5vuJW&RLV^1hQiLyIPS=AB8v3UiOvS#+YDaH_`;Y`?Fjo&Xd z=MgY=Q=J&KO%G;opYxbeTTNBsB*S$AIDNALK05o>(Ndm{dj9A~#c0dHxn8I2Xtl3Z zuh8D>#p{r?y9H9II@5<-h$9Ju{HM1EI4?awn@7<5$4xbxhhDEStZ<#Qzs*&1&Y2Hp z%t_{;-|V&~L0(4Le&g&~7@nrSBd*Vy7DPYuKMYk|%BYvj!4#fu=w$3Tq|%Me=FFUWvGb-(#I3Q21NB(A3<4G0B;R|M;}uDPZRnJ^=6+k92* zR4L0@?gt>f+`~c_#{Gn4=@Z5 zfxYVp6G>IByUG zZM{+6y1-1#8isMwO}(8Btw-9;;8oP%DP#?H$|+7UfXv_^l!F1)DKDg5QGg%hIR9re z%rn<_t42P$8HjN3GJ+mBXXrvNfL$oeSh?c?3yjO$Y_91eth{3cKX_TfXk0WufF}3E za5WJiGai}m7A4;sA`VFP&}qx_PrCABYfwgf_R8YJgZdhxvjoyix{}S@ zQeq`M+i$tfL{kJszx1INBNuXek2(K~1snV_;t>*jyX{-uDQY_lC zd@u{j|bNWLINI$t*gf^fR~mGO6#Hp3#TTjBEg1Ks?U0RSCKSK;KHXuIO3mDPCp&~4ciK*~ zym|kLc6FmNbk566*)W#WPhQkzjr9x0^lAT@z0JwW4GQ)IuFNXhzIsSljAAooD}$M+ zZYpX88p=4w|8?{Ol5>dZ=GGMg*B@i$v$-l~aS|0fK^IdKq{kWK#N$iBli#+E{nuLF zwB2MeT&6n=jw4sQc<1*ngMosByyXX5)QxD1&ud^+i~@i6N8Fgm@Oh;7KxAfeRV&(i z3i)=jHwo$P)#bv7kn1h*zHRD@4`nwEL-#CN*@(rX(Ts7PVvo12_cvn=v9}Q2h1!j_ z_7A2~a44cZ%GV;olw0aN(#*1bwTuur;jly?={Pmt%}k;kcqaz~l-DHmMa6c@GbWP5 zkq9KhoNg{rsj>vXsNe^>^iSc9HXkOW749_(h!&3c_JH$DB3*8??{*4FF{J?fAW)Zn z)9hP#d2mje#mvPXa@jGoBHHmL5QGTqYWB9(R`Mn`j)qBvyTP8QwR)ENwzJ{0xm+N% zx0bHM`gfP2Nsx}NsoGn=)V%CxP;%mLU0sr^N}owudMJ0~u;pcyxDVhC|9x7U>B`Vu z>|>y;gzPb!qy519vERijDY+UuhIpUH#Tf^d+wWcl4$9&$fYI0^o}7XfO2`w4Y4aUW z8oEMHn!Z~JP5E{^f7mcnEiwdSj~`2s()j-cEPXSTqzPOdw@|0*pZ9(>;HKRb&nYS0u8Oc8WxAGRs(w1Jg|GDV0xJZH8e8hBYqa{r7{Z(Z(*m-hm8NKyZch z?C+`H|1ANQOCj{gGNb-(cW-8SA8 z1*b~;`Znzej}bKL4f2FizpeYC_uKEBOiSb?5s_BbH^69y1*3;{9;VCwMsKAJ@-UIV z;ICh9%N&Avjh2IIe~ zSfE@6p8b#Hy$sc%tf#R3zspXkx*!%Efz=9F+H#GQX`{9XkPw5{`2AJrN^R~&I zA9DXZNgaYa>ua9iT>|C>22}du;<$!xVZrzhF(4RZmvV~G7||I*I392j zKIa9|iJa&m$XG71jBN}=Z6c%YRf20DOFCS8lHcYtJECJIUsL zmqA};F6PBFj5r)Re7Wzyz$)sw{|dV9cItPy*WA7hhJ4By!^#gYZ@zfEvwkYU0`<4U z!r+@C#?-~<-qNXo{g1PvXa!L3#Su4SNN663Q4|qsN-NG@HWIEmyk%?KvN{JrFycvG zZlJou03^UqKqW%Ci-A#oNz1bHPz^@KcGN6$`zf8EUHRp8>1duQE+fDbAXq8xxBndXV z5877no$)YqvL>5VBKMMk`_Z9gz3c#hU%A*tURV$$&rvO&jRc8tbJ8@9kCamITDN^L z?bDI^{ptf51%Ycc9xbV;zXE3edF&)sjJ`-;#xCB?>PkTg4>h|)NmS^8G5Oj7kBxqu??3VL zY$L|!KuS3Gx$Vt51m)YOzi(Ah-j`lObpZ&j>cI5vH$=+vFuOr|s%f zJ(<6RGOpE7J0m5YYf0iM5V@6nIZBtIG&-T#KBO&%WGvGKu360Q|1Oxg0WQEf>6Eic zX@kuY)5q@w|C`W7lU-&p+J0b3q>HJlHPrVb;@|T@Q(i{v1Udc0?%kw2R&`_y*@Z3h zf5C>`dcKO0jOnt|u$jx4R824{1n?BaE&#i@E|4*19;6W?qYpRBZULM@ zci1`6e`(UEBaI7ey|1!OIw_T6OL~38A%$lhrBV_X*aZts9LQ-sq~1%wj6CY0(1Yy3 zMhRs8Q@8m0Z3)IA0B=h1s^vdsMm47?W#jBydPf=%h@``|n=M;MZYlr|g`}YJfp2b@ zA@K^q`;j$Ev+8WZ=CJem{@*9g$!Gn&X>Z+jJr9Y{UH&EUCc|LFbG&*GHOqKf2m~0? zLrz@y$oZWuD>M#MuA6t!7Qm2>m#&VWV`G^|`+N^FaIuMH2EFzbl+;+6A zys~a-iZYF&i21KtK9-noXpnSb;A}T4Jf5A{r@URAG6n)Ayxh6vminK53DY9};JL%^ zKN)<)T`qA!2CQ2Nv0~uT9Z;7Hff?Jzo9^tmmDB)n|G9XEvTYv&P$ccl$Z9YO2sjfn zy6P;B9CM!Og@5iN$W}e{#IY{!T*w=7BHr$O^+!~zuU2LuL^x>Gl2VqPD+Zc|sy){3N z%cvT?MKsPb{1U*fn{>$@0H&X*B+-I;cQ0n_n6{mGB0cF3 z22cVAWJ)v2yF>WZ$0TwskNHlzeEX`*+WOEW`) zF|$_q*A*L?m$&j(-HvCPu6h94lL-h6Kt#dY+3nO&xV5J55JipD&bB?`*UHKoL7TbE zI*+zE8w-F&+j*YypK#fQ002;ZehGE17NFk8GUOx|vKaLC9_3hamX~Y@6ZJ@>Dk7G% z#%v_!Q({!>*~yN#4PYQM#LA`AziOL7o~cSsCR1i8@k55wCC<~+BakUu!-ci6!0zrC zMI_qWjJ$Xgop7(Rx~RkUPAlze+yw=;LqE4#dy?Wp>h~+eEs*knNIz`|{GEg|44V4;Hr+jejjKfKXjw5*|MC|Lq0wjYt4OQR(Vc6MU)Gf zlSaUYM*1H5YszM(yzsj`;_*CtBG8{YLY%Vh$58Y-F zM!QmYwGRlCCkZy-2{W37EX!VDS#=jx?wU~2w#{9r_g+pBGZ@+<(YAcBBV2Y^55Hkj zG&aY29M9F7!>E*s(`Ne`G%h%Aog;B@m89JtYJ7Lp5Fo^C5y1d z2-WOja_yyY+x+V&Xm@{3Uc>-iD534X3(aS&;_t)2QNlJu*w~d4o(YCRez4o^e$ihu zdYpoy4qDQ9?gIFciG5IAwMo7H)?Op|uOdQ!752n}pmH@XQ<9=jKOyE=E*Q$vsnkEd zQOh-&0}ogDBLZu^P)BhKpr-W(ga8<7RLfaBXj&P|s>6&Yo@Pr$PIv2O$gSV>P1Y1s zNwt_@ngspB3KvFa z05FShDGi9?&C2jvkrP4tH~oyao(~^z4m4=XjJ_J)1w>!8Xe}<)huek zUi(MW^l#2>!gQ@P{l7KI!(QM5%vQ&11zy**jHtTEo+kX_Xn>C4X^drV*H^NQC?u&apniL zKRu#>uj&6!J1x@Eni*;gpC04Yv{Cx2lkgB32x;JZL?u@_e_@cWh5yGC zp|!oWTNCr#fNlnM3ErC+_i3*_djwWsYf<`9SIAfnq=Vs~I2vLxHG{8B(rFw{T4VbD zcR0TRmN;~l3S{2bP+SWpA|C=kGl<%Kj2H)-xwWdvAHN3hFAgQBK?eoi>1*YDmlbSO zW1cKwSNMBAZsh;~b1Q=X;<Wk+<5-G%x zzFMC2vRTZ|OMh~^I=1B;(L*P?3s=cs^($XdbIzOvmpeP9e5(R$HY(M=;$KMbikMe8bdDq6Epsq-K+OCyhc-PSJT)lcloTO2 z_jOmMz>kEl)&cTOf8EGWKL7z|!BhD%);qK+Xfxx@bG!EOy0TIY|H5Du0>3 z0{}!vR;w%HdvC&|0o#mf-L3+oV6pjL|F61g`4dx{;=P~S)m`u2YAT>@8Btasd>!wt z=vddfagOhyw8Uqrq{?y$xSTM55@R7=ZtPf}BIq~%Omwm-$d0wb4C4A%{dOw!0X&7@ zjz-R|E0(d@W4&J8yPav|?e1 ztdYA{CQLn4v6mg(=W_o)hkKFRyjry=h~7WZ@yQ-}wl;RIa#NqO112&8n_;)|u{EEu zu-;muB!%b8wRs?9Orv#ahd5Ve7RUoLgz2H(qLgn10SJcTOw$NjEVMGVhA`!RH-o8< z12(CvyLD%gN_T57=XznmXIe+LXW803*DncBT&cmLcmO)5LBvz(!8%5{^RVKTPs?Qm zwk>RJ%VnNDWRszl|C9DhBI2ep0|hJ8C#ik{2|1GT`691aGVsW9`&S(+OhFTOvq&+` zfep{u3N-C*v$`&jmskOyRq1LAa_0YD+inj94+?abQjrfOT|VnwWAi&fM!oD!{Ywe$|#YoF5khPLK@ zDFZXim+QBnH}5~VfU!7@6Lo3#*kCi+qz*5FTULs*_BRjhtV+1hPGw*<*ow!MufXZ- zFwF1x_vf?diw19i%x*F#{;N2Xw1icVUq-{XnE?i!;Pt2o7AIT@%FQp^wvr}A_e?u= z!Zf%qxcS}xVMD2}$Wt9KDXMh($V4PYne5wn6S=Gevq4jZ2eTg2&l}@uZH<3){94c* z0*f^R$a#8!>fRVtCiddUI5|J~?$>^sz0#IF?sI-$!0T-XQ!Qpw#`@1eyxGX))pY-C zaus=mg%Qir8$6@`n^9cL?-wUYcS+#~Vx)i6+WRBul58U-8-dG8oAsUw>ovGv%!&|* zQC_>>E6M>QY79e0+%b0PpoI8fy<=}=$W$r1w4e3#)oK#d?_`g*b0v=6`e&yU!KpF@ z_mZKHxO6X+z?eh&f@5zQbx>a+$jZiB@~1t6l12FoSZ3#@kw;nunu~UY8f9s(jST@C zBX_3*3PN1CkbV?YdAxv#26$nNT}yR&?`WWAh@%B8OhO~O7|tw@g=GD*f;s@`M?J1^ zX6%1V((MvU<@_s{ovusmG=S+F;SrgY?b4?7%lD96s80Em+f>)NHY;jKbk@@Ltb^*v zKzR0+6uu}W0=MfayEMnku2}`QDw=}-6x=xCE{UsCEEfc|5;XRb%^#`wp48HUE}Jc> z<)vDcl`9wZ@}72|JJ_x-Xyp|ok3pmhigG9~z`pLmFmDkp1U}@1dPhB)t4JujVsqDR zSGU=ZJG~<9`{$edXfNoCPHUXq+DH?Im!wrs66&+tcR}HrZiMmv;H*wAdZF%3duWVj z?|NgC_45VW*oz>Iq^pBXeuBrIV3{KvWu^Dpni>NNV-o)KWiv2D~c+nq8HArY^Bu z;WN3U@nZKr6mVi(oWNkcNUE^oqEo`&F93XuiXZrM7GwU1B`|!_JnCg)NiwZe00})hI7p_jOmQRq0h#NI8E}*+Qmxif6i4-Ox zpA*Kk`TfFImEaNl;ck#VAcERyX)B|Y-hi?pAg4&wwY_17g95yuOqPv9LJt?oBac90!H;0W4u=GXy)YyY@0`cAt1LO zYzNct1!(-FlRN}mIP2^8{7^MbnYudl=dKEGSj_2`XkbenF5YymJO zrEaW!TgElkRIOzx;l7IHE%N6XU9yltzg0F&s;CU#M9V+ocR&xcg#gKvZ}tHJUI%r3 zLSm}f?9s`VUh{O1N(K{A3t_~&De2$7R7zf9Beoz#{c*9E1p?k^LNSasB#lySd08#~ zHt5sv>hw;nY3q5q%u8#2RuR=No)yXcX1^d$x_ESuq^``(f0Yg`rY$<8*WdEeJOSHN z5Pea+(iZE^Kol}{?(RBVL_jbgzzVu8OybST_Hl#R)jVMM<@e6IPv5E14Q>`I@Ek}NkQ=@)pyfTVvt0tTJkbL+4;C9X^(j)#jOeD({d>=WKyORP?rQ!Tt3tyD ziA{GNzxusR@m-KDNrZ3EGn3T08_G(ag9%^=vrBV%peu)TNE0|)t41xqiQoFUnPNy$ zEr=%@b_$L&YK`aRD{JI;IX3CH)xO6%$6EU`HA*PiQY+xG_RWOU?a^*2Zoo;(v#h~p z3DvBhe}8e&vzmUfxO~#A&g?hV=9zyf)>;f!FwvKx6hr&AwNgqI?g)|Y8`2iUQ zi1|zv9F(HZEdArx&HmyYD$Y;#ve}}C0+mD{*|fcE%0|&<&bPiyOZ&<-oBM}H`oIoJh~ zT3IwPpD%U814`n}a04l-?1_)OqX*Y`)q@Cm-1b~bz!DgPxAizwkAo*{~r_l*r9n*DASY2 z#m(ts)*@#@hAyfMbM)co#pEzf{z=vAV*8(-Rt7auhl5P`VqZp12L*ldxq)@ny`J)V z!1~d;Pb`nM-}n0t%h!C?Zb05&av083CCu$t%Z7W&YppovaLVMZS4wS^ozVp57kWPi zBi%xaZMZszO4bH|*@0f)f5&bPj`vG9pu3jcXKLPfK~AN`oG>vux9^ZSDyW8wFf!Hh2-V#sVPOg6Q4IWDs+>hap5#^LAyVjbDT@nQYh zf}A-<5lfXCi_8BR=v@?$6|4IZ^z*2|0T<`BP2bg@w*m-ej~T60w*ne;i4Rb(;dWO3 z=9+=$@mZ*w)TlnPFk_5+nzx@nEJ{kL{OYG^47#*@X2+BrkTfyBW!J!KV@jS?%2B)+9qcS{eYu8yIdlC_0as`DO9ZiqzuJg&i=8zCAa>=YT| z^K}Jff+BL^B9U!dA*y!ghPZi#>C-s>nv$?{Tn;w-C;oct?@AuRE=gQ3suq@%_Wj$q zktte<+2#Gk&aF4G5Wy7^j=%4s&|+JcOPo>&^*8H8@0=&so4L+H7oV5=K3{LoQNN@( z$j+hbf8fJJ%f7?flR_1^x-((aMUKz6sO}A9Xp(^iA}YY@96Hk`zAiP1QcuwmHBvCn zH9OEnh!{(cn<<`-r9_Dui+>bw@NH~c#}fy;CH=|a2j4l*6Zd8qG71(M*nYnA1_|2h zp~H|uRr7puB>}X+P8W633M0i~4>aFmlxva#Qi9HBYLvPB=jgpN@W5FWUtr~0=LAwo$^ag1nL$@ee$)J*UfRcW)BbK)csfR7AyvXV-UA7qb1$2 z_nY;q3&2nD-T?f40hgeOY*QBi=>3?Cl|Xq`bMp9`8JR)FMUe?spq`S0h+^{jA>F0s zBA8QeT>s_aaV)cPGM?vNO(q@mTPzAx@O|3q*bj6giU5<`vAg=&Sh9)#K{EJ@x@9QJ=q9AXCI1k@)UvmS0YJS_e<6RB^uI$6@6aRv##bb95g zly6~KY{U8nFei_nRHB6(30Ic1?QM#x`pucpWY^=LM^RBOhJip4J1+{KM3q@GMs&RoKmLep>Xi7<|g!6g7LvI8OHWsk`Jlpn^y={tMC}l6W+%s-|_XcplmS-LzsjF_+q@gJm zo*J2r3AJ*Q6u<`wUgq-9NmTbMrL@>5Lfd(3{pGj!j;6c0 z7sr;o;L;HlqZY3IrIBQO?Tw=9FD3`oai(skX+jYfxSrx&c=_o5j;+?q^V_3gku~WQ zYeE9TSl)a`vA+I2BzS?H#j8wue6?cH8BzEUc!xOL*{AN9vv9s7im#^_7ckRQ?QyGP z#Z2dQzA#08F-Kh}vEA=FvEBbSy6{|Rq$(gtb&HC}O3^&0 z=_FIan)6EL1v4rb-3Eps?K=GD@anNFOyA2z08+wCKqLA76aqKfV^Tl-IcRo^NO$Qq#{^|Q}RC4(0tk{mUPxD*7e$@8DL+#Owz5d5UFtJfk~iHM=pa+ z>E}ZFGGk(MwiV|j3d48@?t38tvPR21n~~UDGDigM2B%bw7SIq2Un?A zH!y_YJ2zduj8iE%*9pCVCYJ(dHz_1K7HT&I#4Y z=X`}dQ?W$SrA0LZpZg4Sm{V02@oyXK6=L3F1|qzin^vrZiAnZ6Ft$<+e0^vq%s#hN z8hXQb3&aa5NToDAU8^bxDrGUrihCo9Z}K~z*XjD$t#x~#jktYeA6gJDvuv1o^w8`o z%x$CM?L=h`m$L+S&elHRLpI$bFy{r+CiwOkkx}tjjxn7V;bQP_H|iE5;1iPHqu+W? z!E499o7{Fo`a=|9NI#9CT@|n7nk37u-&>_JjqP~P=Vv1YDk63rSE@oVJUfkcWY=Hz zgLkiXZxlk@7$w_(Njbg?1>yFB->a1=1ox{5d=Sk3=hxlz+hWCt){*vRzQo~vTZL9^ zNWkOpCD2=9YBkQ5k>-A*l+kqU@@8&;iG9qny3jG(Pz1|mXwLA%qq8l%EncZTjJNQT ziiGxYj9sz#E4PVV-X9m>A=>v+a<5(G3(G3ZyqaJNd!a6K#&^dsfh~+ZzUq)lDOt;! z4KVKp2p3h`ndQ$3Ap*}6v(nlCa&S9OXlZO$GPBfh;TAn+U_r|DgRy60^b-Vomk z0^Dnsowkcv@DE-LiEb8U7-!2@^7%4rT`Ag%yE7(g^qS;F`;+hoiOmwuZ|P&|yqze$ zvNV^h%(uAU@q&4V8nEnB5F5v^rM>+Wds+q1xEK|g7@DaWWB=;)SxQ0*}>i|Qz48Uc*@qbL4PotZ-$3Q~T(3O}a8RP78kt10smdLCZ$<|Fz&39yPfv*I< z!*l7x&YUVB}XY#F!i92k!Ug9PsMWtSE{2~(8zB@#Kq1T0+!F?TQ#{s84H zf1y8yCd^p*F;mcXb#wA>mV=O5*;=ne>PIKV-2wP=%{$@{c707nz#%{7o1ND+PZ83k zRmJxzXiztL$;TJTdJ;w1AI@3XpV){Fk`wtJ?GHOwND0`P%JtG7i9kIGoE~yTd~GHC zkLm5*pGChodTRbm?(n_r#C#vDySLxl?{te(DDeA^Grz^k?7qW)lB~y+W&8R(Db8P< z(|8r_A9 z3grIt^z*!H|FgOxvDgbTSp~4tI{!5;>|X5G%zO4S^GWQHzdCXf8&K3d>N6!fom^V` zPq87{A6)NpE0H;ou#X8nFt&zj(oCP~}#a4@wUgqgYHi_5Zs5 z(r$6$@B)HIQnAA|kvMvqe4#4*{a^KWlbx4W62KpQt_@lPxVbY`p?y-0IpA^Q%3@iY zr{_i3eO5466h6@09hWi>W>?#iZ*!?mA+T1Dr-pqK<=CP^E1NE+CzFC_sL#|{U0^F| zQBl5^1&^W*4kN~r=#RzM!6}Ck5o5&B1RYg>OC%y6d;13qzNPfq!RsQ>W8H>5Ws*A^)Q>Zjpwat2)8#I6yM#pwd^dy zCgjAdoV%Sfo4>t$*<1?ouTM@~f7J;-l5exYn->*zch=~hK_WQqR{tRGngb%X4=77F z#k!gVv;(yujQLfOSOY-Jc*^Ru-7Z2SerHB^#CgWZ$++m{>)vcH zOhDVQpS~5f4I>uDR>BY$O?4|izLS+`!!oEnsD$?RnW?*h&?&>L^-{Id?%#|?$hRQ% z^rg7@_GHo$zzF!>q0Q(Y^9yd}LpmmAf7JGdG0aG?F2gP7U*MCNkYQpN>EGEb2RO%3 z;yFCNZ0r36+g-{Zl@RyQzIKs#9x{d+lc~0}d^y8fVT?NQza>tH*9?YO?qy@WI(=Qd zBz%z{iy7w!q#EkbG9YPZxWXq{1g@VD;4-@hnl5wet4I7uLh37=Ig8FdGjL z(2yLrY-Ae#(%7;lu~$R^?5(m_9~xxhBitH9$e;}S!X_+`&Tg#ZR$sg8s zTY*$^N0NOID^Z4O5y|hT=@)|@L!|b9?6;Xkz5u0Vf_OKlH_MgSfU>_ha^zZfLs| zac&5Fi`}ire$>g4;>s!o&N<^)Z028Bx7w5Zg@Vc>FJi_Ry~_@-Z1&1tGmg+H!;LXC zx0vlctIZpYG!91OHhQfhc6VyTOE;|^5SDqr&PVOmO^I$Z@&`AA!-_8#Vz2{l5Tu;2 zI3q>;w1o|j9f@`Em)=~ja9o!fP>+!`n^>n_$g;QvzMX~{uKggOqV>f9(opIME;!wp zuJ{GvP#W7~=~M$kk&E;n7~7_CIws>jRi?tUkMO}42&$w4=_VoSJ(?EcqR!nAW z9o~EV^bma_G0a?nqDcYt5=&JtJ{|6K!|eo{)Y%6|Ow`zvnC%NR)!hhTk=^0DAIUikyNw2Nz!|#L~RWw{^Iqmwsj;88j zAIPfc%et!8MLKzR1Q_=u##wuRG-=3|U>MfEZ?JDLOFJ;R!A3 zJJ@NWer#){xjBS2LwD8(^I5C?=KmtgUoLuH53U}dwE1$04b&O3y@S=wN^wfj*+cW_ z<;Bcv3t^zTdPIrGe`A_~a$5&|)gzXq632G|Xk8|3;+yA}oBk%QRx-EY+M&H+laT?& z!3W)}#=XcL?iapzsW6eKotO>=r`8$$sLE(qe0uU4#9#jkQtx~3-zA1S zc;u<;oBEQ$mSqdsGztNDY-;9{sN%yFh>dLY+w*ibkS5xe6?#$*JPgv3-20q9I2|oc z33GkZVdo@!7<~b(iTnP@qbh#85YJi2lly0Ec%1K`;anjGL z4YlBjz{tL=U%E8y(MEm%g)_<(~dKd*J0nZ%=uPPS|}7 z(XQZV*OxI)GB!>Z7r+&J)wj6&I3hUURYl{B8mh$pYGo_9&2GI*4UOw&;PZ**L*iFT2g=i0zJnSZuDpb{&oMR;UcQ^+2A0yL1bd{bn@r39+zKFYArDLf^psG-_f(= zbbe<&L|*u-1PQ@qxr~^Do|!uTW!0NpT-_LB*7hl`XGmUEvRO!rkh5B%2V+cQgJnfu zhH;64Mn4cjIHpulnMP0+F_>UToH9081F9(eVS*ONPoOZTZvy&txUBkqC(8ZeHv0(Q zMtCuu!+9=D*UV-Z%x;e*J4zX0zUJhms@SfSW}5|YCf=p%PI>oNXjf7KI7X~S`xlSt zp*5wul9^w^G49P>OF{sJ&o$cG-U?*!`CQQj-cz&N?adk3qj+N1Q+<(6)m< z{JlLpnVc4@Y7S%w_!n!V;Rb_yRu<{SlE?_FAfOCf{WH$JiN7Z3k`dKRp3qw}#~X{X z>>qr_9hm54(i`(m;hU@C9UPe#1y3Zr!7YrcJP`ZMeGASK5dZF7hZ}scO`y}FgoIK3 zP-cnO^f^_{56V{NPQ$R^-ol!*F~YMc#JBHFC9(kDWh($d*`{OI(v(XXC43NY{{7&E z!IFw@U4a=$a45N?kCqC@e+D}&;z5;eQBUOTJLac^o21l0y~RhwT02M1^QNSD^qF#t zhB`^MvD&eVz0E8U=ui1D*%b?$ec}~$b*J%%+rbZB`2Tm}w`*N;LGka{4W3SgD$zS3 zKby@6q(6OrBaaR{uK#o2lWj12GX3|>kMVxt2%u5~kFun2vku=6BzosQ{&&wPlm3t; za6a6sfMJVASs1c``_%dryV}+H7oc0_MTF+ew;$gcmwYaJXomOip)B!VTod3?IaS4z zoA?yQ3|Gx=loOc&-@ev`wobo_uzY{IDDRHkS1wRiwa5U0C!#FeiSNoqWS^3#R&G}9f z;+qe)^OtM%U4<|azk0gNT8$do@9eJc;KK3FMjIR(a;?C$U*44!4QQ2KQllHhT!3HL z#&+y-JjXlb$fmDHL@5ys`5TT+@w_B=CTlKAVE4eu%{ILiZOGOMveE7?6uSJn>zP(w z(t{fYYY+udO&qvvhWm?bCfQB4gB6==JT-C>V)!c%@8aOt9MFLDZS+Ub`pceb300d2 zkvP=X*Rli$FpKl#j3TGYM`6jzK|I&LfqPA?Yacg4GV4v>&^Ol46~iyr{2g=R>?Pkh zOJOp*8Bd_J`82{WlB}ts$S?unaqRu-{@E~YCCTpc*}#Zc9Z@c80(eTH^ObOw?0n%g zK*jb?ofz=`=YDO4KT17Ao4QdY zN}SNY!3sQ!+r1*%0)-M=6$!T5WL3Cs^C*kv0PL9C{HVItmvvy5?~0ufhJ1AXa~99n zc=a_&34WzBCd=GJRPu%#RQZj==TOb3$e?*3_&E5G=xE8KG02uZcLIVTxy7%ZoY2qkoj7ma2JZjpA6+#BwS07!*HeSnvh{~ztfE*y2`XoU}cEP+@2dytj1y*WH7m z*A1Ovz2DFP=iEFpra4l^HZx`KJ0eFR1D~as3ogh{Ayw z_CEKwM3J7_)O}JBlUfc#SZ{-e0{+y6CPWws$$xZ~`ZIQ%`9ut*J9}jHikDS14vu8X z+Eo>=^>3KO`^}tw&c)d%%6W?=+^|;&n&R|yZ<{SzW135ZUek<(nl<27Ozb@ym6|vL z$R*M5f=6wiWQHw*7*Q~Lt_TfbLHghfJX$1&B}dIJD5}1o#JkOF%YrA}!GCm0COl3`ybJtvNOZVOBS0uw)!MQ-wm#;U1pHlM=NHD}lS|>m2Me2HyyWJ@4e^SY9_-o$2<6_# z4c$5MCo`u_o>yAepVxe#3!{P@g(Xww)2RIZ>-Vk$+t~b?e?=>|X=ok6&JUU2<|$ub zqOSTyvX@b?u5s`2{5r0bMAaZpRPJt;#T-Zbg}ETDK|E`lF#|SSd9Th&EqL=brOgP? zotp0ww@YnYUdk^!H8-L3ZWFW_=_@n3H&DoE;W4EFT(NSUz{n{NDCAVM(8u)a z2xZw->Pc3^;Df{Ntt-z(%qr0$4YQq~r4yZ{6oN{enN@&dr(&nO!aCFXH7PMHeN1-H zx`oE8%7jo5K~=+437)M}w3lv9)g1E+b^W{+w$ysPE^~1O3crZ z<3+=T`3K>Je(A8zlWkBjUejsVU~Z;bh3=!A5I77WVjpryr*4X^6w`XsN4`p3QD=9;<{%{JK+Q#;hRLTf+>t+PQCZ8O^Pr3%6gr=Sf^ zGJ4WTMHr`6WlDPcoL>!z8+&?lt;(I|6cc@JQxkn&Yvzw$p9tgbWneEIZr0PkvOi|# z>3@c+zC@KR>0UA8_aRaDlL6kTrimOAmUhct!?8XAFV(QLF<<<|z-HE)<@f?Qj-F?y zBG}O-hA))TdlkCQgiARodQ6^HR*uKSL1AjXF{#f-X4lAnuBdCyAys3g-i<;OdFze3 zEUPQKmTnj#)fQbkSQU4y^UXS`@qX5E7(V!pL^8m^E?lCZs6d}+17au^xedU3Dc(o* z!8S_MUYFQ$Qs@!&a^g2OwX>ki`y)liwI@+VZAIt>*xqjMw-2`0SbT)iMY^X`2$0Z@ z{B{`e%TTu-q#ocR5%nu5!>tDKlG6H72bN2WFUVXAh^YD^gb6w z_h+=%`qoRD4oaD}jt<)N-}D$zRW@;hqMWVY<}~z)ykDPJOTNC1+b2N+GvC5-i>)kP z{fWAC-lmYWW7>VYf_%s|kVv}yUF$c@wo?tWNOD?*<`?Z1v7oWO723n4#6W-O{Mt%| z{X68|_Ss|#H&Ezx?b=FdCh*nGZn>vFT4O7U$5CJU1Upganxtmh8xuW;rQ4in{GgID z{Xs`9Y1Kb1;cmHxx6&?8S?$GiV9H)xhlW7o)#>DJHc<_&*;&?vv;Ueu&+#jO@(1k* z`u?)zaXSD4c9GNxrhB$Oo!5$Pm4mq6`oSs=Jv8EJg0LsBT4RWNbbNjbo1P)R2hh(@ z)@EYmxy7l@cuq_)1BNx3bdrrk-ObC4hyZS1V1SoO>^YQ2Q;yxUrI?pii0ZCIEc)fJ_qy}8(7USk<0*9=LD zEtYzBHdN}h6<{=HmuhxaseSq}z~9D0*Yt6C#S6BaKZV|MY3b*7(wGLy)lneFf7_<4 zFMf=kfy%YI7KoZtX~~A=Iv`%usDDP-2_sB={_ssv01%v*-Ly) z^NA#-v|aKZ9x)3Qv9~pd{cYgJf8#G{4}(H?>KzGnjw*hyTGc*GFH|nv(2}5xTs^1Tzwm&XM`~06tthxXUV>rW|DyJoVuZ)y`d*o39`s!9IYI?OjR<~5f&wW#~wBG!z z`xej1jmzAfPn+Uck9{*(vZ?TM^3vFNdCa)QNKe%QV(iZ~d41L2@>N`?iSsbg!>UI* z#mkKu`(yXBp?Od+i?c-F>jmg8hj}V)l*Wv|CpS&k20}&O>KIqSyB>L_CZ?>y zsSalM7Zh;&JNqjcVYiVRdFZO?nDi ztBCm}m1)+%$addcFP8Wwe}mG9u7fw2O~}cSbZeo!`F*{wQ>(o=H;}a`H%?g~ zT}9b*|B}1hU?aaPw@61Cn=RzlDWF-QM(zS%@#G(=zp|WvG@SepwhAklsjHb%x#rDZ z{RAIsBMN~+hK%T?(_jI>?VO5>c6TYghN{W8GI(aV0;LSBPyX&guJyjk@@|bk@3lDJ zW2?12;```A4W%)P0r~Sj%T~_*H3vj^(L@iHeSBI+0JIHVY&)I4LUy_(H`-UgUDbFI zV>A;8*tyGRqB$+7-f{?~zmSVS)50G14Ti>=wv!SwO&IV}e#Z`lOk!gIy2Lj@$6}*$ zuOWI$+d@%WCpE{%&~y2`^f`2EU}>st!V9Hp?Zlv$ejinFPHXPp*}Q(q7~ko+@0q^QPFViW38${TY~y^LM4m1u^B*f3Rek<5&8W()1zs|_J&26TyBqg>4VRb z?kQ20NU$PY&T`}&`X7*XoFoJVB6qNry&RICF)t8wQH6PzJhEG{LiwPDS0@sxwu&nI za?_g)?vQB4FOFhY4M;GkxKwfAW$K5;EPdy}E9ZgE%UoM_o-uSGUe3JQkHZjlnXY%* z-5}DXyIETK8uMW(P&CcR*qTtM3=qALHY{dJ6gd>@JP9BdrQUI2IwRF&6%JT&9a#~_ zYmb_ixcvJ!D{E%V7hd(cVGK-XgJL1%t&{hNUzbDEtPPc`}084FmzAd(EzRXTI5a6(**v)mSzTEx zy6T5ZqNR!JS`+B3S!sp)OoD-5T*>4@5dVTm(>{( zL^e;PlgX3-7b4XMo_wVpNLw|YYwP-gTI>%~qHytAItDx>noKlA9r*KmeLX3?iIdjA z&QdOlutCfQ5^Yx*7~g9BfFS51s<^C54j<#qVaoh2`I(;oUW-l4?46>3F_CGU;U6<; zghQ~WGnvR2g{tJisI+dZ`EM0D^-U0&&`-NRCT}2_EH!p2{cd+fYkZINRb_Gsv_knP zjOo`a8p-Y`ND<(mZw@stL$a$>D{?-dI5SCgUoAiatkBAe9^I^N$Qy%7jCFdCpL%{? zgFCU{L0!hx`p3Cfi|oS^GQUmaHk_kqF2&2u%hEL>xMzx~KxwxWqQ0ZPGE>GLuSmOf2M5VkZ6HDufA8@y}W<8|t&JKa_O;h;sQWYyG4J zD2L5#e#>;A|L)C?L}8-8U#btcc_HF_-LJU_1cpGz0y%#&iwV7Yi8mx6V<;U$cSewk zD%F=pinFLrIE>BncxNJJs~}-gR>5DzdbQkfx(%(>{I1}y18|-sFzCq>U%(e{Zm^>g z)ch~CsnK`eVxLUr+}14J@5|e8neTxI%4Bh()u^iulZ3Fy95BFOSug z8n|xAXaPP%T2pXM+GY1sc6#j_EPHKbz~vy_Oe0v`h608JFNE~ic+47xEo9?ar_tVg|QtsVyv9@3h`M7b=Tr|OVbTi5oLTtA{CjcP zweL`<7KZYUpY5qHO8hioYj+_ z3+7ynrZp4ADyQ-xH(E%+qt8eLSb?olmP*1m zV_8Y8Eu6{&;)L6za8mZZd1mM!xBx zN=~_#pb-eWrKA1&M*45sTIq$M&yStoD!4)O5;L1j)Tcz@*Blo7`?!%}tk$(5JX=op z_bJG1LKy|~JQXwa<$_@iI9_^Fc2MTMf!b20sNW>Vz#NrF@mBvpeZj|Tb%Bs1Cl^^} zzp2P3xOfaHDDAt{PYpZrQ7a1Kd$N4DgwbE}dhNhwFlpQL{?*3hPK*^5O4SnosQQc7 zG5$=bLhWp3ORRNe>Kkm0#gsso03mWd$CUB@f;OJbtB3K8DKS>vKfZ1bgGcoLz`! zt(2v2BHJcVe}PQXF4I75vg2G-gQ4jj{^JIl!{8LJu?NInxfXOVYQ5RCS!e<=m@TKr z+VJ=fo#8}b2WK+y#@Oa~PRCKOjKbKb_nat1JkJx^QL~%nxFB(OrlHzs&Tu1Qc#a(h z!7V1RoVf8WXZAsYB)9z+H~y%Xsqu$NUdLpFIJI$_WHJ=0c2z1t2`j4=;~v!@bM2bZ z3U8x8)pjV>-%#<=XBFhzOeFGB8dm7Ov-1U2K3L>{zGI5)N2&VAj8cmdc7Y7VEbjX!d&Q2b+x_S(UN4Nl0?sjzr zpe6$dpw7D?pK;8<=rPX^wduh@4D z|64(o4g&wtb1(y~aTNX*`03d8vwHCM_O7tDE0rC1lKFiFjOzn@ z?$1tDksoC1rqt}xTxVbL04~58RfOsLA101BeXl?RQhD>Ymc-3c9E~9JmlCtW+a|}3 zJ1_EOj$KVxY&V*IHf>VZ? ztq5e{%E?JM@@Gqyam2>;DZs9lpbbuUY zHF6rl4o(=(h(L#($S1Ezu5_pyx)k>Z!e@D^IFv^7I#@(rL&u>EB(e1s602luVr$u*I$AS3hLS<(A~fOA8T|J?&w6DNH_XYZ_(iC{5HG}A?Y7>4nm}ac<%Z8GUptBPIV5FjG0$$Aj z<(MG`mg^bKG|QJ}J0tbw4EsZu^#^@2H!}&}ITI02to>u(fcHU-BqY-54E2jub8TmY zVPS+>PblK*Jy@*`9 zusyud)B#l3Q{gW26zZNwy$zuoitB5{;eg|+|lVh#W${(gxYT;eV zQXw}B_l~PPr-oK28vda)-Q?A8yoXIhJU-?Uihh%{)%%meiK$JvZ*B5Bj$3>0?${@< z)=Xm|1(tFtpjap^9!2wO=hCc65Xmce9)9m}Yj8fJ6O9jaVUq#HKH@XPsft$0umT#M zU3X;Iz}J-f4=Wg)ewRpzjQ6OI$g!LL3Qkir*nC=2cvz|i2d^v*lYgwR$>aC(xZ`$Y zLj}u(7~{Qcx6;doV!<{{^UC2}?sbksbN>DMzwo}ij%iTYig$}>I^Hp;>EIr_xlkh^ z@7AbcM6IMsg%Bnr{?@bD(pm-Ze5bIgY2->qcP+qp#`KZeoAGFzKVwJ-?~ohMMpyV4Z3 z?rb@~J>J|U8lbIPG+*|#TGHEpBkdJCQsHthRkO;Pa4H2;Fa(2F-)qBPUiN8nXia3~ z5R5`s&u{XOoTgv9IC#G+`ujK`D5+_ecEEIZvlbI$)!dvloT~y3*ilF&AQR|}zcPSH zG);gQ^dsvN_LCui*sU%)zHG*%s|PwH+j@+Ot+-Q0P}ctN?mV}N^Hc&kpDSDzpzu5W zdSbsb=UkazF~8FYKr$}ItBNXb!IuW0f_iLYE3=Q!$Zhla=%$^>NdNrbiDbEBQYtVl zt#vockMj-bg=|_zI2-W!L6#NrL&1vNUmFc8X2GWeFTR35zX^fEDZoGKO6mbf#I;A^BLAJZFgl$T z+bVGt{{q z3N8`%MTX6kl{;U!U;`2dwySsu({g`KjZDm`%YF*j9=U7BWk}$S!wLdC^}Fm%mm8xo z#RS3ST-!(3;>RHh8twhaQp1g1Z z>q2|@hKR%C67_z&c$eU&1ITU=J*S@Q-d#2}HT-WOpe_jj0CPy6PS;Y{XfXZz<*~Qw8*!!nO`Q3F z>}Lset%Ts~q7s-iqbtj{sui0)sr~)-f}Q_fCaUn=Qc2TPeQrb}lkOSnIaF?zz?F!K z0MR(}^i&Ne{pVjL>nt%v7^DMX0AJ#VgCSP9A;q+Z9W?KLJ5HM??5c9V@{Emitn58U zu35=XJtz0Z=@l~V^LEE29Nob(u|^#8@H&4t+#7lM#}xT0<#wmI&ha;9xCQc z=VCX_&o2JAdzksZgZ#-p$HGv1r#S;6NGua);3G$=tU9JpQaG#sVH zk^2=5$)U)s2dA(eA&Eg2a8Y|lOkv=}L8v<8EXK_1I-Kq{nP`V`QY zY~0p<-&w;R_Oe_BE7)n>obJj?msmZsDLsRVf_?=h(I4*0-?mH|?hsjeMn(}Q(PDOj zvy224Etx@VC1w>wsgX@13rOGAxKiYI8kGK=!d)JUw$)9J5z`TsFqedANKh#eA;9)5 zqc2PZ;lC60Jnh9`Qesk z6L-h`65XuhdpIfV9N7EV;zmom#&Z-ckSNDaP$}#9)x8O_6PWg=9@JnUehGDkLGv;0 z89D`}B}B>wMst;uFKmfA|4LV^8nR z*fI=L2kwhD9GgwP^=##dT?A#g2S41G9$kjad8P~WyHxd)0>Wexl|9J;CL>*(u#VYFLTMZ;9n z(5`U!<|gG{1E~yMZrhS{aB}TRUXx4SFu)UYMLpO1m9Ush;Uj~~v0E=C@`1tso%nF8 zysO{o=vtNhh1RQHJzmwPb-1*zY6urT%^k`h#?8JaFTPI=wTN`%Q;&HH>=iPdIwk(k ze~0A%tdrjP&;3Avk5+$ADF4Y#&b8ST`tB2|l==5wXieamu=uoPeE#K7V{H3K5cHgm z_lWAhH&a;!Lat3@&PX)Zl^q&~-}~oV*(vsn!?e*6e?H;{#Ik zG@4PqJL*Y86PW2glIrLs+&^X#=;|(h!LN0EKP{v|lNtKc*HGCNRuS~pl zWPe4FN?r*^W-xt*4JbUO`@vK)M(LXlCKxAfIrmt+I~wH{8}*h8yCv`v1=XSOCJtIB zT^#{{nylSJRgR3TF*JU+r?_&MO29`ljTM$~Nz=X=EE3e0*Vx`OfU$83*$eZzaxQhk z;~HuJAG{@A8rtveCha1n{?&)84w%MG0R*LPRfsH*f(9PE71mrXx=xDKa|l}okP`!vrV5Rz-a0eF4@VGxFhe zCo5sGPuT5aNZtXdxoRyG?<0n^qk67q!^`9Zo$mzXvysJ?=!t0c34{1L*EKpMVPRX?g}ga)O|&t zcU&Hsf16OCRl-*X00>lyXV08iA2Hg)u}-q%l|zzk6xb-7AMFo9h8wg6UUptZQa;%# zatDbW49;*oRX$s7Y2AJ=nKR$)Wo2DVo zvCb#I43-~yFRg3z*(_y-u@=OST2<~S2l=s|A$m`$1-^Gu!_ z2x2+FX~~V*0yE~Ugaqcl6Krc+f3u!Vm}cv;%Vl&R6RDQ0XeVWvOI2oVLZ35DS%|v));`<2y^Sd_%iv-?keL!INSW9wsj&2B zF-c1BqlP;Bw{=dZ=Za>VnH@#9MuCozJmCM4Gt4i~xaiiY;{x|~y?rt8NE&gyPe6~Sac?<;b7_EUQy)dDw# zXBS3iPBkGXIv?cKWhorVi;kU?>@f}wVm^3YJn>#XT~52aU5=JTPhPI|9PyA|FN z8M}M4_zM8JyUy-G;UPpjspXdYeHJaVD7gVHcms4zkB224edL_V`FZ33UQi1 zRd-YIJ`Fb8=5rI`SL66%r;(Rz*?e)fv2B}?b*CuR`RRc zkZc;P3AVWJK4`SPgjOpkHa>&XjBZOv<<2!@xKZqXJ_e+9)vT)U1&257`bbDz4aw;& zfZ$i?F}98|74IF-6=5@N!Kqp{c;aAM<;0TyZ#3hrex3uvhVk3Dly9xQU`}vPBwg6p zl94_e4IigqdNboT4&9BH{N}xRG?d{J&4^RND^xSj>WPWB=L4>-@7*TZcH*^W$rROv zSirBq#Y@3dGb{8@m3m*nc=<0Q<;Ch z^nujiB_e1->shdnlrB@Ec5G72;c0XtL5ek7%(`Ic+nCzn)e=uwSfR(+360F0yWP~} zRjTr|qzlYn_SasH@boy3zEr{<6fb}X32#m8x;BO&x=4$Q6E$BFlJj;Uz&T9*8V%*? zw&P-CVbdHhf^T5@2m~zFfMo*y{!03k^}KjSRg^`V_WFe2?m3swEO-4Rrkp;FyAIoSDf6VnjQh2%pQl9G5(XfkC3?slR<a&h;e1MYcn!mG8VFI^cF8_a}zONT!vD`tIRt6(R_S+kA0Gd6lJhY@h+>mqol-n zH#y};-^D-HRo{@sK;9DUpdL)#cZxOXHwra|BasDlli4PH{m*xjNJjd3NviYi6rsER z++Y7_igGx~_uB`JZY~(R%*PA^ zsJ0vKngx z3F>*iEvL(;s%c?(O}Vk>6Igj1LPuo%8!NLU1S4*vd0oR^~CU zlMJqc`UtL`WM761ZFv6s19yg(!RSiWmB+dXvh6|F#u$(y_2r1D5!tqQv<}8F(@>Y^ zhmkB`IlJCe_gd?2WZ=LH-rshZY}Sm;CtR zw!QJVV?VK;r`z^X?5ozs^UWTo78Y3n_Nd_sE1KVw40!2IOG4c=M^Eshfp7#fbY~)^ zd*MJNLg`gyo5v(uMre_OeJZ*BKJy^cYdF@i=4gYc@ud34eT$UY+tt-hlATY&3V>1= zS%kw+YU9~1PIRsvM_qhRi+{LD91QGaty!RA z5zTc>I-$@VBQatgSUD-2_b$0R*U+`m%mShRGV6ng2xTK$jpG{Qsm2xMH;%P}a&icGaS;sS3LK!gFC`{Owq%>j%{@r8;!2{HXu=e*H^Wo*xo-c|QRoFeGZ%F`2U(udwYX``|k^I+4k^w&>i9@yu+XNujvwut{X)YJ6q+5{u0F^T->scu0@z* zt?>4GW#I%2+wju-tCU7@Xa!Giw5qeyTVZIIEl&St61h1XbC2%0X&s~(@{cIuAaV7l zP-2o(|MoaTYa#w`R|QmdwENX8-^~Y1$MxWC zm5Cdh8H6V?FCQ)`q^EOJSBjOR6*y6HGSlW8ko^>tPG@TyGyWV|==C$WZ?jq+8zhh1 z`iFTAfsGG<)tj|EKG(^OR_@DC2^8<`h?Jsa{PLS~Xv(?G!?C693x+yD zeMLsP$rM^|9NfWs1)R1-YpY=eI%C>VxIj)+2251gkE4?lLx9K}U`yH`!~&TRg=CdG zB^8oQ{AgD7$DZtR0zmN=m#gt#tepbGRl$9ky$t6 zHz0*r@n(iBnapx(Ye>DjO^NA{f||-C{%OSNSr zaXN|YrS(H)B$dNS%*Y&VaUcRIg}sU4^z`2Pm^x!c zCcXAAWg5+BbOr)dKM&w_{6)QVTTl^g*5+sqMqd1MNo3uQ%tCoH$}kX&Jz!4zmrh23 zfvfxDKJP>ym*7(Kuiw{jZb`*nw=6n8$dJW?{hSfN_N?K9-#$+Ew%E{Jor}4~wFgAn z_53!=xpc79bo#jj73IWfbG-XwS2<8DD=_aQ)a>q1>EYf7kX+(T^5(@8{$a zJ^P@}QtOeIpD7Oh(3$?w;%;@8rciC9k9<>$V!+wL>93UtoY92~1=9DtL5(o~{D-A`#k0#@YghV=rb-gMoouVPTAiU%c}n4Tb)lCMI80 zJsp~CQj(!PUjo!pDr16{Aghe)#m0!69?nSg;wR&fp$P`aWSSM0GdBviU2S~%A**)G zI9JG6Fjz*k_SL4dk-jom%DBSnjqFBccD6ht!@UV*GFP(yO8`!!)U`3y1BGIY-$C}* zrRg~Yn=z?riS|yELHT2F&>?+tZ~pqg_vhc{QxOhLnKTF0NPK?FxhCFE7Z|VKTK8SvMWghvu7jMw_DcFh66aY z*ivolY^XUe=`OG$U^XS4yhmj0*;v~(*iwod2YT-jFLwL8T+mGl6QuXA^i{}=Ap5_8 z6Qdwku2^ecEX+s9_L3iNV#SkGr-+&;X9w@gOAQ6W3=stAtYem+jS)#&vx@av7 zxh_M<05#7(HK8K+s@s1ja^gm5EW{57?0vt@ixxdti4UTrFI9|7TtG%KXsjL|kQ!-( z>8k0U+_AC3*CO6wjJmc*Za2!NrmF^!tp>}=9-i40Ylb3ap{@QyV~i&Q(r&SxJFWKyoTKB2N+$wFPXSc z2^vows~~=?uo4A>AuPhiU8T974s&^`rUr(+q8+#Uu!)S_5eiC$^Cf))zB?NQ-$NPE zGvM)cT$m(M%o&6#?8)|tL}TTjnc>9Q%(jLUHH9Xf|4z6P+;)yw-+DMq?HZG14Y+*O zbJ4`DkFXas+IFA)U*UALSM3r*Uv#6awpKBzi9J8CeT4WFA!}W=tL>v%7)efJ!JiqI z3i6c_w&Ak0ShcM^>q(DCHzb2D<=@8jUBOn>{7b_RK>(jhiVVs$E~cIkLbDt##E;f0 zZ2M96ThQn)Jr;K~vV6;oen=)Lw)%yY+x{BF^VGS9gCSw*w{nVt)=L{qr`c<?a)K4f_qK3o6fkd)AlWiS=QZRAAo+-!6 z9Y>Olhp)-oS^!wq#w(=(9iEVa+JQ%KMMB@U<_r{=AQ)>brMAY(dB>En;XbhU8aj1o zU}1%Ii+bs<|DCwnFmE`2^%G$W+o&sDQp;Ei`h&ArXJuuI8Kr-EzP=d^HA)PPf7PJf zJ>Tn=_xq>cGL$xdY8Gy<&SN4yRRUs~f1T?&{XyC*;(M0E zm8v!qOcyf*?%!Lqx8irUII?GHHyLkj((-SFu9IpTeX+Xp$s7OMSzC$;q;T{TvA zcUP!nf%`CA@%PK&oRIH#q9zHUhi#Zmo)tt=v|Uq>mm5Y|Usv&*^x-SBk{1r)ZEa%`!Q$H&^#DZOf{2g9!*;YF zV1O;59I$7W`RW{tfI_ z_vb(TfNIChDTbU;i}BS1eK?APJ=p&AAg@Htq%lRo6u4osLhl`Q;Hm)&2`z77lvUCn zg>VZTPd2De1e++3i=3ZL1@U|ZD*9$d{T(m8H_rwpq!2Vm+Yu$p#vr0ah|&nIVb+*H zImCo3yu7*Z4d2Z!@pC_w@#oY&CM{A1v1+}sMV$#gl_^Mw#QiCheqyN~F?mW|a2`R6 zXRepFSS$ty5<(W7=06(T0PJGLFh4Y|gz+Ka(pxLkkCVAXltX1a+S`Xa<4ND6h>;j) z)+isHamI>!i151eE{BGA>f;^`tBmXi-Ym(JM1q6JPD8Q8zse_Fv$V^>k$VPZjSi~IcZYHsQ;XmEQ#-i1xINWqI`o(_~$iRfO(%w*Bo?d zi8>%G{G^Xneh`qA;UBv8^xd-FB1>L6#pC%p~9DwGKcndFFY!yo}jZr;sk_57H zpr_QGNWM2SV-dy*)y$m3Fs>ZV&0ZUO#PYOd^~*{0zEQX;|I29&0XQfn63eLSREfmkUIuVW-zX?~rzE6Sl{9!+ zP5YF*rRvqj>Y$$4fx#ADm!?xXzGcp(Yjmc{U<9rNx2X*fF9Mi@7L2OT19&r%)10Wd>TQpD zcGjM9dw9U;Hdp)n!jz)htQKh@;V-oR--T2&j^P_ulS)Z8Em%K}MntCXyZGbYE+<@EMR zs?IGfr(&ITMTlm}_#*9m8;F4lpzM_XTNbhI$$ zHI~4xho#p0A1yBDnL~8+!ptA}rrt@T7h%L0Y{kvIp}l#oW72-=S1i4Ctv*fjM4!RO z4p8spbszejgsw9Nt{j>bHGVfWUy-r&jC1B-bXCkVrX4|z695qcEJ^msXw%nHT1}(G zJp(z=heR!V>AJ2uk6Lr#$$mKqM3S;bdtI-1p?pcg(yP^YS?LEk-k^`XJr*P1Xe@|OO)$5Hr#e5v@Dca0XB!K@o21L7A`GkDel)| z#&*z}Q0gURL^6Y!$>oNRFh@C9)GKnV!kF?gqV^#o492a#yo2sH0|9^pU`iyC7+ufn ziUKV`oI;}L0o`gK;x(!T?M>jw2kC3ys(AZr2^nMVW!lmg6*<$Z?&^tH=`u%eS~9#k zH0vhSCU1k`z@uh*b?jU^fSmb3Kb0}gu5lBIbfxC#HNss~OOVdVopWU`V=Zd%eAc;FE^Mn5rvVmY42$yQ!Dev?Y8ybEW)LWHXHJ zpIe%vB@+P^8T+yQMW^MxR&MP) zVJ_V_`o|VHE=%O%?-oyt-`i*3);kwxFz!`WZcwj@sWCxG8FmX+e1^CUUOBP|n5>Rz zJ{SLPL{8N?u^UA;5YoG!35yqiG0Lxx@~D<ob}*6S%zyyvDM|Ph|^XUnDwf2>eEX$}N(na@cAYh;j)+2;d>YK2SDPa3(#p^^)+_~Lg0QBV3YsYV^ ziU*tW%oRg?%eg;UnaKM5=KsvEb1$e(G5WpJ@0SKC;6_uV{jm_tgoPnr3gwJe#ll;N ziYm7H0}_0QHeNH6*)0F;Rw@ST;U7=MFDoh>_*zezI+D7`%0Gay=b(Z%f!`j?n*ZRD z(?T$}VzR?klaBZ%ye-P?yer6_w5c|ODAwTI7bzbpY9RoSeNKBrr59_ zkXt>~4I=v+6!h*L6cL)clGW6@Stw zH=U}a@j!~N6&;%8^=_`qo8K#^#->Mm@_8|M;s;dquK<@{_P(iizS>iQ7Wws@8YAYQ z1WgdYHJApV)NpV z=j4n^{N`l&!t_sqT%d$04mX&i?=Jv5>$%1(|j_R zH?+qIJGVES;1T2zWI1llhuVNw{2V^{>@?K-j%gfv{UfS-@vKWR<$h$vL|7`z2=*ii zJNf26^gw0BrAawMA2edj6bkY$mQT((hbQ86{qX0Vy{YiZE<;U?`iBXIi+aV}e||VE zFiiXMK?)Ts)e^zeT+4I159M~1ncJgIhn&nEuGg}HlCPG43pTa&rv-lv7-r0~t<1!rZY3Y8|F z2y4M&O}AiSZGdi^lZC-BbtfzK=D0B^FrOH7fRjM26B&e?Pe93mPehIl~}M^Unnn0ZAo(-4v$70;w5<&4?g?;Dtt zQTX^?m^XOV;@uUcssNjSM^+9g^qg$flYHm{^KQ%$n~5G@wchB1i)ALFgltO|Nr71; zD_8;?t))BMVv7gssV$cShu?zyIU1sHxmzdju=`xz*Vi8IQt0C2ME6ErJ1Rj&e3yalySt2qSY1sQQYyVQYdw zrU;N0z-BFsl2&%U049h=?4k=|xSyGwm92`vXjJ;K3wq0y23N4W%1v@qXAMJ@~9 z+p)0IwWG2qCH8ZmrI6Nqrp<;hg9;xo9G&n9RLG#b8nRSrK0)Bp!)^DqrZ@#>knPs! z<0()ML%V6HjZa2`YwQZ@TpBeQK`uxt?hc0DB-y3}-60A`LScFbzE!M!zrX;hG&6oz zl!L-}5XRE@Sjz$_P3`5%*}}BGIM(s!`AZNX3Wqhs?vgg#M^B`3nJyU43!6JDRpcWH zOV&_&+U(DFI=h7thT#=HZ1AnP?Ra7UMAwX+6@)2mi(pt_ zUJTc56($C0whqrm;7s&k5td+hZ@2RSHlw#K$>&XpgJxP zUI(Hm@}q2@&F#^!qE!hVsyttBkH1}2R~=7hS7mj0PZa;=ELP17j|jtBgX$mvd4810 zgqx#M?o`mtS3p%+rn6Zxk!;Uvi`ZWME8gtZbjf#89jPwK_;18nRt&RdIOUBBG9Ou_1wxsh1B^N&pVxnK2$1FTPs^Eg z2jo~3$Odx>=mODd-Q{`p?}FO+D(kf3{J%2bLSORCyKV2^`4VT3v*av;gGay?zN|&T1qV060iUZoknnP9!KBMHcuDmZ|=fcdZ zeu@nf#lsTxBev5MUX8PQZ0G!cLcrh7x?eU^1tTuG;wvwFNUQ8+KRX$XgESRC~grmNoK+ASx_%>Q$!Wz^ovQ@_!}J zyz}~~X!`YSmXGH3&LW+AH%H=?hBfrE zLsvH`b>)hu<4IkKe?7RX8LQUztWPCyu3Xjam(v@C*g}N)4^9Wx4^Miy8U)~v$PQWF zLa`)w)<_M>!&q@AjhaNz#^9?ZkH(Zbmp(p|d0_vL?0~6FaMG&iK2B)RoYMYu8_h8h zKX{77|MMT)eA&!NEb0XOU0}m8t81Z4D~&lx*g1E zWb-{M>f1OB8M6saW(4W!#zB>^%;lo(eU5I5qs%=eh?0kT1Z`s5YSwDt^1FUdWD5&# z@!@Uz3)M$foNfBPZWOr4y&`Rhp_l-u>6vW3zYvz(AuFs!2 zH$r_NWEp(XqE5<0@u_3;$Kc#sV2{0GOaj34N#gbxbe-+YDGP|z(v*X*`1XaRwhY3S znvf}$W4oBV*yL$=d^+>)(=&dnCx`a++FV;#iyf3Lkke0ynf{^0K~T@4eCZS9cO5I4G}F3oSWroKe8j!N2CIHzPC2P@BHG_4%+8<-FyMw(X=`9 zkIe_&w5)%hllVjN_<8(Psi%l2SvDjx*4T;K5vmz#bwqqmQE4IaihZRgw(?V;1i4s9^~y#yrpZ_2PLePca^YiI>?;L|N}IcL z=|48Xl;Dre0XzN-S-B7QEar0zblk7yTb-TMG_wMLGs~s4C~aRRioV@>J)8QGM=r03 zf4u!>HMGWh#f$aG6}KzwwvRZwx(XB`1RIdeM|s;$yG;R~m%{Mlo-RWhdoyDP&A#$2 zd<6f}nL__9r@h*c+iiXB9aHXi?=T#e-;AqXKESp8_1d_SRuNE}al}P?UMVoBkJNKm zSfjV}|F%1hi$jk(;f}SWe)uYQhkOQH@z|sJbbD?KPZZ*_PZ?o?|BAOT8f&x{Jpt21 zl^lJ<>#-D5Z=P`lW+gJXhi*UKhXq4O0ey*oG>gwjo|FTi(sgw5vOT-Kf&_@Zf^$=> zS{H1-#Zwi0A|u(aO{J7+>;H{!COh=pR2MP2t|;nW)`pZg3#)$#u|y3nn+al`1c+$< z8Lwz8Su*pFxlNg`cy@ZK_;Yb_$@9E|A8%jJ-72AZf&Xr63FJ3%tO}enUEP5@#VR=| z`Le&Vt`n$N&VM}F<~#rXi@chJu&3OphmdG}P25yc`4x`=%{qpw04lRb>6yZCuW&s_ z8U!{Vo0MF>kz+T8Rr$EDH*W*`%-i#1y=cvk;=7>K ze{5Db#(GV@522KPb+72Zv#je4746Kyhs|x86#M#|BO)ifuiN$;JrQfq&q#Cv@kMjp z_i-AB>hW4sWdz&&8YYe_CP?^pU;rtaBSu*3il;zHK^LCHZ3wKa4Q|YSFn9(q-AObyU_MH=v9rb3+ zoN)>S#VAn^(A0KQ-lE@3%WoS%04p{3B-~8xrLrg`mo5fN zhI?i7UOj8=SkwB%fEv7a^!*n0<1;pSp1F!&EQ-43g{6vM4=uyNaRIifWX(OYN0?r} z<@}knY@phZZCKocn~55Vx9J6)UVAD|l;m$u4FcE$lN8W%Gu9IUR)wCR6CQ^5q}!Ew`7c~KT{~c!*b_&oqKlBw5|uC|^AP)&Lsk(GsaaMVi0-9aQD|a1 zRL1grZ!dFo0i`+AsT0i#K`OhPOKS@@Qq+ZQE<~cW@8+0UTRK9R5R=V&pG~2w8EYCK zPvW$I*xhR^iWErmB?;1lAGyPvh%oyKJCDE+ur$xxh)?70BoMI6>#68uzPF;_X>z7; z2BLnkeIVjE=O$8?eYIl4wa}4Jrxp;pzIlBd`E=qVhepPKwnq;ef+$S0j~rf{q}!O7 zulPUlZJ_z;<4)TYzRskss{<)6pp!r{PGdXA^2$;TkNUL2irp%GfP0T58|O;Tvn!-YI@2{-j^T#|xZ6;_;hq2XB<~eXVtCiK{H9ykS07(^0u` zqr}J*JV((;kHmO{6{=knU_hR|KQ-1-L1J0uPDm23Stg|qS*`e^7QUY&$$ORCU@QaW z1_YBWAeIJidMfW-$oqO8BjfmWpMp7??4wqnW__0%oHQ0>TY{_V1RCphH;=-EetHa;bz0dRYkF76;T)o}BkkCw zv1-oy$HqCjxEO~}{*19BR_mAy{JwOnNknPM%uL)Q90OvG$a98`~jD>^r z3CHkDeZ+Q;gz7MPKi(NA3G|_R3a0&|F9u&nF4q_!HG z9q<7$fT3#SE(8Je#CReP6ESv3CQec}ncDOqps3QSx-eHbQnJUCYDNgdA3@+or;Q6~e>G;sk13#ALmGV|~Nj$lh z^T(LX>L>O-pAFysC%Vzq zv3l~(V6&rZDRU2*rBf8ep)Q>Z2*H(x`CDLW?wfxWY*Vjr8}B#S^(SYN!p!PZUYo8# zpXwd@34WW_ZR@~6J!>$~9t zrA3bFk4)saEH1B99`i++D{xi0qF5BG$bd@#g`FU1@mSfjkCJsNqfwOo;a+epiEup| z-`d+@x39+1E(M{p5wBLupT?(|>VivjPWirB73j|u%-@Ts)a%V8Ysa5cHSL`*QOS;Y zs{zg?Fn)bJTUb(LW_bKY$a=#rnIEXg?Yoz1k8ob;gG@h#xlT6d1xV}2+=FK)s=8f0 zd%=%SPZVD=q;`>_1zc4Q=rR8&K98n3^i^u;_=^7i01WP{v@4*Xa~&9}GnIg;a28or zbyfM2R-tK|A|e(9r4tICI6=R$;jth^L`bDat!mo?$25hb=#n87^#mE8qS@n7ZQbn4 zp6AhE=D22V6-V4c+_%$PhGUm=2~T#V;R@R;gLt1m7p)6;G^RO!>h4qWQoj-zl6d?Q zlxxh&Mf{%Ry>uyb&?n|lXS{6;@y*-ypCX_;7FT41^!yvoc>kj?m@0I)rQjpY zUbU~82G|Q^-OzVUi*9*PZTELr_)Vd)%B~@ zuP>zr?bM7pYV&zHR}dG=`ma2^1Y5i-A3 ze5xpoTuDY?2zMnEICflWmu`Ni-nK;-YB@KoJ{)|`#>4i6|S3R&DpF#IPg_!l8qk+ z^x9{;(sBhj&RYwVb9v)UFxT?qCSZ%nc5w4{%%!QDFCO*}ZJB@TQKvuqwBBnwcqP|H z?#-w0yOJOb|G~XES?RmM_J75A4i^7nFSlvWq*y-K@jXB_b=bJK*;pAR>k^V2e(4xB zmes_979RZ7oEZ!X?|_UVEIjoDTPi=7HdIBg?c=b0NvJz;Ai}xDuUI3?uMD?!#G(-T6dzn0cWP+0MNq#ccg;L;G#tcm+}+Kc%6bU0l18 z8hfTG_p1A!$LF(d3X7;0)#kMApPSea#_xLde$|WclS?f=N>abOx+-hs9T_02`t5v^ z6uirPy;?o4eNI4o!k|A>WX8N6m8C90K%>FNPQjyl*=>AVoK(-O?Nk%&S(?(MRYl*! zRz`>j|K8ZO-)|=X1&ZDqn=#{lmjgC{o8Z3Q&r$jb&#buZeBZ`{)sxU0DBp{6LfNJU z$Mxj9eegd*zWet`rknE8XD~6J7@M;xbBiS3=JMT*5YT_j_YkK zU0}Xp)v`8fn$k7c-y3-42&@yl0t-pRu%Hr&7lo%c38Azl^i<`^u3!M!q5~80!ja(o zrd3t*x&6nhL0mwQgROmxNrCf3z``ao&P=5z=Np1=Hm$#+KW?=~j|~nm)wi zV;oh0n9%26ul2I{Qm|0M*ekV*im2hMj;3UKfy1QurtW~c+TDZ#Yp`UP2;xZASy^+{ z^j;FgmH*ozPQ4eMzb5>o2pGOq>EyW26WD>6&J@~TU9fC`Pa^74%nCFX817qLD80n= zZioF7_j|Tw04V)c)jG{$B>KcxlJ(w-3ClSV_r*PQQY4a*gCws-+;)p8pza*L@*-#MfG@aKuOjey!OL z1`)_!l)t~M|DI}rK};*6HIbJtcA%}R+8R{L;(E>~dS6c`r#yLR`9oF_f@`f;j553p9vP@Hr8h?RC zxv{^iwwXufk)jR3!~R8*1jIReoU@yO zqol7nYV}^fNyY%u@Ynvml%P@f8vE>*%OYzRmzWz179~(4&!58@sRqucp`I+;99#Q8 zwphBufqXrdGrJW27r7Wj`CXwFq{6R|*3z&TU4wuXOtS7-9FsMmLb0D#1TNK*bM0e4 zrk#31ez970N38}0kh+>seu4LqBIoAc%o2I&#|SvuV#qn*=Jj~NiL}6_}Kk1<}SKc z2${qE1ean()-Gih;7e?q?GLgUDJnYD#BW{L5z~uxnK}?q#iG!v=x7hJQPZRDaVttl zgfoogPMa=RAUQ>|qf>})>g@irSrY9FDBNH%`FCvNrvmF1bxD>QcOQu)q&S3T`5cPk zOFNtnaO9ZoX_y&sA+L~PBdrzwA)`LCn-k)U(M7&`S|GLsfvKtmg-29OQ~ijgi>4J| zNN!{j{bq0*r_Tx{vD;uK~MMlBWGAot)?v_s-K^#*nOo!#7@?Y!ns^f7~xz&Ly>vnal!e{%U z7swzfzE@euSHjr${9)y_9P@E??{S3d>{HD*H;(?_H!Jm>2`Ji=4MZfSScs&ew{>Sx zfaSd%--@XWUp9t`YCkN3?A(C1zvbtt`y~T`X~PEt)U$5 ztKbWL32yMec^@1}dr4M=onw@eyxR4q9Y+^qzb4Ye z99{2_?s8!?m$S(e4W8Ar?XEr1^-9fAZp%o~+Z&==Vf@ z=%&AlzFDs`Xi=Kz4=WM($agW=6O{QNU}Tsh+O-zIkofER{HC!}6kt_^0;zFFFPn_SH!JBkq) z0;Djz%4p)VMHARJ7&C?&jxR zQL@zo&VW`_O%?e(kKy{YF^s^n(xHCZixv#`^}RSw?>VMJuC%qfhzAM=Xp%IP32 z=hQae7aX4pn?Iw_>oD^t_BXEaRB9)gyeQr-?mU?6l+nP|XS!wU-*&15FbeNj@(3-- z`y4_k4~#QABqX*DIi$_GpV&aW& zm=hmaTFLAwYT^{ZlObPUI5%4=(YjECE)y_(TI*sJF7m;MRNVvC&U(EOhQF*882!30 zSMW_oYnx$PJ3aev$;-|XXszr&wx{KDw|!nXy$ao(6VTvPg20wBNB`Io*2`WUm(M@b zdP9G(>_upS1l2q681eSn7r*oeu0}J%#R3onqtpx7A!*cK-1lj(oB@al#GmmEQT&Fo zJ@(%H;h!V%!H1k!6kE^}~w=p2MW_FH&2#ai+#Uv?}E{|Y^I{18U9l$ z>P30Zn)UhRlj9f85qWOO`X*n*mVZr4d2=?>-OvJe;D-I9a{t*mTjmzW-O*Sg*U8me zav9RKYTGZDQ?@?-d3WL0SmA}HkYUsgvS=d6Dg!ELCJRdEbH@$kG9FdA8beg#+YPt& z>|@3`MN>$%!*4omEYX)jltf2pG-(qLAjX++g< z{O|de-Q}giI1(Fyuz5hA3ofDg_r*ZeK~~YVMKW^hB6r!k2MM;VL$+e(h5g(Ti^hL) z0_oNDF0~Sft#ok-ceC@EFFP%p9oYj=WV&L&BANHJp?aljxW&1*fBV_ZvaMyM>QR3; z>IrB^9gUyRJTVMhl2|iF3;lE{*yRz66OcJFW}^}*3PI{4-B-(UFdv0EYSk*GpK9!l zIE>3M*67>{Md1g?BF5=a|3wPLCI;Q{c3t*ARi_t0<=M$=x+F8O)9p@jSPjX8^Y|5GVk#`99B(l%{cGGehMRy;`L)F?`nsBO11MT=?>4i*HI z9?^W+!HVZsKD1Z+F1=-SCz~FF(n5`X5ja!4lRL@T;Xdj(FiP%POWPQluBvy2u;npd z+L|j$i1E(XlTS!a-UkNB0sN!KB%I89ilt281tFDx-g*wJt9;RSbh$3>jMmJuwPXi7 zbtNz7*wK^sIFsbgx_eeoT9Tid{~Y$48p1?}K0J!6D0dh_|9QvcmJ`#XMOAFkVhVlF z_w}UIDTu)kgkK|1BZLZ}Clz)hD$M)3P)pZV_PKS>TxTE+nDE3k-pJ zQjmzo0U4w6b+csnJkD||D9sc+mSb_t}%a| zZDl!ZyD-jOY4B&kG9pVAOaL$H4g{O+t8(<13`DAf4l4p~K70XZWq|v(8!ibtl5MJ# zh@I<|tAKrpv6N79Xvl_gpv3w&(0PhB9FzKswV7n1b)KvH9{EBMD);b~t;-uwG`YCq zPAuoftmLns9N6-*$`GH@Kh%^QSSvAPCEL&N)4S`1KrTAx<_N~HbgIyr+x&5Xfg6UW z)mV+~Cs!n4r_#r6bVL9XELynbtkOOlH!9XRk$V(zy{cJk+oCYZNW<}ferJ$PJGIC3 zd1FJ)_}PrP*1$ppqGD{mD5&@P6x)k67ZTJGdH!YK1*&GRB=16;58g^X-^!#@+12vI zg`iA&Z>LpIpQtJG1~;cy*A)8-3jrXX>3~@=0K6T2rG@Kl9V7Jf}-m zS`1Md7cpts$~wLlaKTI!jx^Yq)J*E8HYxGGqIZ`zoSjVRBf{Twt6{ApFJ5bsrE+QV zhl}R;yv!Six9G`&E{wce16Ab-T~6Kr0JDNvm@Kp8!gT_h>{N#W=7me#4cKHe?AJle zn3&h345Le7x=3!pMk#AuAtj@Po$%k%p4}ylMm)_G7VEK89Wzx5=n4}|-a^HEO^Vd&q(QOrmgB=s$3o1*2gO>E}P7gM||Xn#e` z-~P@ZRFfU1E;A018*;w1zH!NpwL^!y0FLCWG*WNKggD0DB;IDuSD#yaasnF_;Jf2nnQ&Z`lKOA%?u4YNtW~%F2VxMNat5*I{B{A9*1f9#+>Nzj@k?GpSRN9cbZ!(wf$(l zAjRI+2~0zSw9{#@!Xm5UNWca9xSeW00T6?gWR7_cNYrv7yi}SQLasLS#BflWw|;Z! z&>XGqkirJT*Z&pIz2jm+A$4J88cARvc=fPU{Multnrx#Q5h#F=8Ha|`paQwFs#18}6|TLDxJ@-%e{@rWcjK)RK! z5B4zvAAyr$vX=k#q?ePP7bd|Y?;_AS-fpM#6wo9^5gX0>6)TXFyJ_CmDh?@0l=`4e zi-LHbuX0X}Nx0U0z^3;LA2XZ44N`x+@4_<9>W%%Q?7PXw4l9Y9#roCv^IVyggc3wRR)gHmU;(*a*NU@Y=* z$QX0!(C@=UkJ;WXZ>gdg*BFXI6~bQk1S6{{5zALTm176yt%^mnVrz0@am1&C+7pdj z`Jzj&SEMv59*~;)3c$eR=mu2Mru`%wcrD}ATwa>pHt8B(H9ekN)@0ou_Aa2S*|IhM zb{0@iU`KC9$oNxpX;4CD<=(jnfRFPC}NwSod~oNmG&&O*%go9;hz(kZkhQ zpkA$ya((d|3mVK%E-e{1`)Pca!*xKzZuM^qF8AK5=sH)OWB)rLA(i^k<<@YHG)0yk zSnc?=X1N~4od?)*H1T3<`8w|Jm`sz%DtT!oLF_JYJ1sI2fw@hoB zJnPuOsJ;EtiUR3@q}N@kTDpxUjd8;Jx8i1t%jb)OrJYWSX2UaWlJn>i!=}gY5R$`a zN|;?IHpr7nvj*s!n1~5hV87c4h!Tnv_SN#p5%II0qPJj{KeKV$jP55yZznT@X&e!| z=>7@FnWU$1{0&zkFY}edM1+5CS=>@IeH`pJR+Z$mm9gn8M^Wx`H-k*$0^9rTy3NEJ z5&@t)=9F%wYZEV7fj7i_vR&q^t_R7vBV$1B09koYJF|Tgq>z+J0`POD!mF;L`sO)i zs}ftl23V`m&ldZRFcZo5oX4YMxsGQPHkO^t59mcrJbMBZyE-vjW}x#K#o#UY&m5_? z#7Sp@R^T4xH!df5^hQ$FecPm$q6fGO4~V|PnSz=F9$ElBp$v%W7*Kkf5`)|rHN_J$ zT$6gU6I3`>xNACka->vj@ux#b_%P)aZDY3j7GhG&@}gpRD%UIHO2=@nerlr4wTk)nIdh-Wi}`MP zt0No#*uv~}zoRSudw?wVPcMm`Xy5WN%#Y!0cV^*9jy++qUahR*x*2>`{rK?S-0xn= zZjTQCJ}dYfd&oe1$1bzxc7!l_Xci z{TIw>z2rUpfQdQ{01SK-S>pI$8W(Dd?b`ZJ{mh-McR}w2G#Tflsap?*n_GT;C=Yu+ z(Cc6}_|&96*6WwYtu3=4tNgmIC$67gNg=?ELC84A0&ai6PW5%(_ zyNX=}3E2rsr0t%pXd|zdg_4$6(psRDv{RWYd)J4DFI*2z^LE0OQVMxzn?y57w%Jp6 z63Mc`D`_vDYneIufVeCMDfvtcRiq;YCP#_QbkDoz>%w8=DGQB#R!m)C6i{Q)8;Eq9 zgaU%IuI0C`3bm7M@w8A%I0|E`4F(8MpwahnrC*-##+pmMvv(R@Y6ZJI{D^OktIk*T zc78h#lM^k9q^*Cn<+-T8Dw?@}RyXBw6H2c~eQv$7p%pI4|IG4vZNw_lX=7A-Q;-l_ zMQ^OB6@1u$Ot$IkhGd5xYegyt8!Z4und?;(YwSx38@=Fi;5W7|Sp0r^vVNrZXHFte zAkDC_G>WbltiS>PjUhIf;>7MxC^y|gvO>U8YzBh z8ks-(Wy{J$$%~Nwj^%}!@|Ne~@4%ou_!)IQ4X;vl()#rz z0}C!W3qZ~bwVFH)z=xK!Wx<{~Zp$a;OO` z|N4}eIW8{1ZHsVKVplLx_&L^~s%V`5VzicyJ`lX$?Ui0NT&sfZd*21Jw_1v2 zirwrD`~;F#{gbWi_D~E7^GGNcUgqvnCvROIi!OBzr1n__DwuC?((kphEJ za-kE%8%=|GWjot3#W7tVR}zM$gfH*3l*|t4n~df%_Da?h48zV>?Z|_mIYi)5CBpSU zv~Dan=z!+dk-t#*XlUB|@r;oRsEd$nfu&&h_E63yHS{!0PH?sv zr?7**-J8*9GB>2ww5B8@=O)OZ%F|PE~TxoHGAY3HKu84GtMQTISx4O(R6@z71 zoC77HcwVsZEr1`t#|mg7rws*}l}!rbR_&~OS^ z$6F&|tt*jQI^+!1Xv_@qk+|lq{~)gb`%V`AKH%d6>*Hy%Qg8bRO{T}1Ur0S;Ttb#$&>$`Myl`Ix=YN2em6CP2y074KU=o+N0j>#2 zjN~kko);>PVj+mJDuq6<2+6o%v^z( z`Wc@DFe&@w_Vy_DhP0_6G*(<}Lb-3w?k2nY2IkMNs9wX)RlwkATTAHZQG6A`WZn|7 zn&#SYcA|b*PbQlx*AYM2cym!aqsI+w_k`irB-0j}{)aAd!6nc8aX@c4E9W|-SG5sq zxj4PrHV$!}Dmjk`00dFw3hwFj$YmE4*IkLdV-No;T)w*iUc9FsD>1pOu_}DWEQzG> zQ&%3E$LO*rb%#|&;lmI(kBts|fa$1eN-_)Nlea?6>-hba05dZ!w+=NAx4X<1_yJW2;NRV*7Hf#^Y7LcGrCv!ekDR6#b2sz`(h( z^qYRorV32$Uq?Kh=nt(XWWz!qg=~1}%8GHdhH$Mc=kM2OHC5zdvm1WkMbo1O0hZP{ zlo{_kZ7S=j&PT|gD~9}!qI3Ue@^Sw-QcWS7^Vx>Xxh?0Am>COgHs@2Am_yE|=;*LH zAE!_`A2x)XNpc*cgjJ3?_om39sHo3(-+$o#WxF5u^|-Fr>-l8eMB(7|cSd-n28s!$=Qh_Fc$km4XVOt!6QLDyDRR;d&j%}O zf(IsYqRc8CBB$RXf@DQCIi^lIF zOgH{cFB@0{a?FF(%Et8)S-Z0gU1{bEDl+77{{R>G)O>-yJD=NJif@jgv-n32e5okU z@|x@CKAs0B3wFadwN4aM$73(E`)w-ac17H-(}tnpdCj-*c}7I#8~z$yq2 z*~)yS8>G^di^p46q6C7F2;((timq*n&|k)3ItmWPNPMKtVQ0_|fNwmj{tE2fcXUp&mDxh)wvT<`UH!)+iWoI zWMfC!qG~eW=0L)m?@OfyxUNU`na)!6ipxwZ-R#^0>52o7FYl8ACZ74e{vVsR>$FLq z;ru_x?f-=SV(-BgmS@cDDJR1RKillMXSSIOgUf;U0n=>>Q&VmeuF{>!&q+~7|6|M0 zzgt@Io@b~u1Pk1HK{Ukut}KeayrT&ehVG`nSg*5_G7m}Uzs>WW`zbzy`D35uw=Tys zBu{}L2K21Vqk-l+8^z>(fBSAqxzOgcQYq^@c^6o_9_jpEL6zt4^KJp~LQ5uEXSm>5 zJ&4AKW;HdqBK6Fbcq<}V81OUv4%IY$;udJ+jYN+amXe<91H{e7K7ck%9R#0>7>q{fq+GZ$iqV1f#ApK z^`H@=cIb}KdwS+0`L_n^*5j^TI_?#~to7$f+ofeME-4d=J<7&)Bdb_Xx?VeQj&spV z?uoD@UU81#KIskz3(!+&@XRBoY>c@+nd_w}Fg%J_p|$v?liirai0Fu$`QV@(Van@> zv~WzarJ8r3Km#pW`d3z0%F@n1)8w>}d#L(1&oTwt_y7?$}C-l`UlA5FD82|TSx@Z5!0d7vB*2ox} zC)y0}-G)G*gH2m?wsC#KYrzNk$38s^CWn$8>-`&P7Rp}e)7^`{E3XAz_SS;h6G0L` zxnl9?+$ZliRFx}`rq-$@<-NaCJ-Y3BXp(Irq!zO$L{Aa6LJ)>5VVTJC^^@LRG+^lhw<2j0W_9}Uzg|&24Qm^&AZ?To2Q$%Crqp30?rrT zgO*ClFWB;H2_W*$_}!RK@c)TkyuTCU?&+kZ;t4RuJz`SrNCp!BFZ*MPB{D#l@p4;|%c9mp2IzDa2Z$ za4rtE?l!$W(A`z5qa{ug@Z

S0M49bgnPDV5SMa#KCPVpZ?lgj{BEn$PE(>GNhe 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

}$oJ`c|v$Y~1-jOf3P4 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

Qm5>2Un4AJ{leY;^(n)4laf45>0nzL3?l z5WGT#GO=$&4h z#_NqekslGK9n0UTTa{$;YNLo&;XwR#(5E;@t8LM3wSff7pnJ48Uo76XAyMp9(q=o& z^%8ImxLKOA&(x3a57BMUwETH3{#|=jC8qyy{J8-uTk;-QNvh`+6p%mT+|KJA12}Li z!^o{r?zecdf@Q**DNwS7-Sce_-}%DdGD@@K@HK`ezccjXjQKJ=r>ZVgT!cYIX;YC& zdK3Ifv(N-m>3$W!jRM=XY*uDl`(qziorr>sWy_wb&=MtlO)-i*m{RV!Ti;l`rp=54!YmV1ENtUCKQZ|y1#zT#CgYrpB*aUMYBTRCRmVTa z?i|D^NK-4k$B3&JXu#BqrDU>Vs{XeJH=Uei0yiKX1(Q(?26u7KR0Q>!qJ(G^On8x- zLC`=-7kGdKcWGBA*JDN0_Qa5>zmh6mhPkfQ3X~NXC%Y7r)7Av4HB$f!tKUUko(+`- zUN5;xZ``0UQR{0JFMG9Hn32D?O;D9{uDj9PtVNROs+QM!m4?e- zu)CAXf6TqYo(+L~<#mbKaVMe|m>#XdK5ST|zUz8Dz6U0dER7#4(E+%fFu9twwjBJN z8608P0K2AK15RQvop!EXm_oUzpPHh>e{?g5h59fFLqh9?k0VHj(u@u05sX~cY}hw0UVskjq&)e2X7 zJ@DX13Ci-}I9s!A$W_#$v<&HYY4Vk_c=}}k;Ay*?QKJUS6AgAfaPkMnseembjL!T_ z?%QRU`TNbrdDVD|)=9Yj+bz)TZkA4`Jfl|#jnA_3iJ6!w6t<%K*G5?%4RORX3KBXF z9*<}xa5sg;hW|Bm3(lHd;YtOP0lX)ZxfQ;(olUns6)$xaTi0xfSfvOfNy`<7nGqL& z-~*>+lF+h>&Wbf#fge!pj=XSDy^{Q}BjU1~=)KG{CWC$KK3r?2s#)=TR>ur98$ zAcc1ZTlv{@I-To9FlZ1BfxwdTzy*v`>)1<^M^`U4<@K!ZIR9QDXV%Q3^~KU(z&Ryl z{2)qA;vEMJe_ng57poQdIOLsWFe&NwAZG86<>a{} z^FNtq6Q2Lk{Ttq_I zePgBt<=lw{pE`naOGiJr<@!oMAHlBQ;gz1wu}YXNc_j9&MCqrGnxXWJoHHTtpS$7> zi<+wflX?kt0HTiUQiYYXIz=j`+gr^h7UeuCw@)4V%k8;T_@lD7i5b*oQ-1TJKE|Hy zm7LK*=hcC@A`$N&(!X(&n(Ig^k8R}wRSJe0IGWm+djF$y zcw!dg2$asAby>s*Nj`&~8|$_D2$At^ey9J@#aR4lR}}iigkr4bU3+_J4Pzm85Yb6! zdO^$vJ^AwN!CvA$%QT}Q!fI70uQ;GQp!EKF$)~RcJrAW52esT&q^@YP>PSL;5_6t> zQJ@FlBIY5MqIX}9r8i6Cd<$t|av9d7?c&N8o;pA(RI|aVQTsaM-u|RCQhGQMT`Eau z@Ls7zW)X@`Pf?h6;*|vOCA+=Awq!tqCsPe*gcne1_WfH=(u50%xstu^D!6OSuA785 z^4_eeB+ys_7H9S0NfZ-fY5^qPOdD(P#qp55HRjk1(=P`o#FBj&`a$_J%$z&N`s7Uz z6C(j#JE|V8RPo;CZmAVlxyZH(S0bVHc@ZLKy-*>VK|=QxoP}cSH(-`lW_%P*BZx3l zhy@RDABA-svF^A!MmsRbd`UrtU*SUHN3rG^LPBb!H@6QcpP*xEH@~zuzA!MWv?zg1 zRM=OBE_)qvQrZpuGnqfqi21n3-z=I(=_j;hm{((EauNQDwaB$Q-Pr?X_F-l+%QB=w^_y? zvC9~Mj)bt?hy4Oj4xlrAPr@SnGXZlyzMbKtxC;^<(JhkQ;sMyE3&f>wlCWiQjqkX6 z7z+Rr+%!MHc!W!SO6|1^;%^ci{(e%7$Korz0IYDHE)(vDZ8~rLR$~ivG_S;yJu=e8 z=qKAnWU2asKH}}z(3bZCRg-zmNNfRFM>?jsEbKkR8q%meoW`r%vQAm(LhlVmfqWAb z6>|AP=*1r+R-J7}6G6JKTW0@!DgB zjWIovP^nkiVZEdDgY>?j`s>A`%MxW7I9ll}Q_^SIU6ILcQTD-2-f;Ee2 zW!`sR3s+ftjWqv9(U}J_{rG=eMTla{ooI895h3KL8D?^9HgXr{CTEU9xsOpfbLTETkt7uP{`UKC|L%{y_j*5H&)4I@yhS#%@DQ!^q|WUvOsIl=|23Io z?KZD%aS`rVpg2e(1G;)(1G%wjQ)t_20=u?kg8z)i;T)3bNqUZuR;YB9&(Lv5k}RvB zsjM|+TlqhIzTKNWf>F6groZGpFPLwz@Wx>8jy2IOgp)lKe!E8|3^DNH($G{eIdob^ zy#(;+7x3-7C7AN`fRc@(u=Yt>7L?X8pN6#F-8J(BpJYBR+z*oYiq`>{~Ft}>B;4T+VTqI7{v za*vYM+UBi!7m!~@mmR>=(DMEG9$%48f8B7fi>D?1vnUt`mZLpdX?5$j1C&|7u{@3D zg9u5Wjw};RU<~`ZM)nd=_9VHj$&I>wMfzIlzomP3FMKH2zG>;VH)(-vhG_aQbE>$ax#!ht+nRk$r4iqd*7~;O4mj3uH%%0#V5(%%Gg5=czzo28t^^H5PK z@W)FU9S_w&AysKsWUm8c{;m&yD{D?pr2O8Ejr0IS!mcv;jjv0`H6%6uE%LMZ2-U988%4PJmTV(#Zde^ijop{^JwkV6A^Aa(f8|GhDdDOMHKiHx< zRDJ1HowpD_XV|X2Pf83LYrE$%nA4nBPmPiQ9-YYxdpbXFnpGZJ5*d`k*&0gE-(Vh1 zf4s`x=C=BlEMI8QDN(=VJGekgDe;k_5DO@K)psXV zo~&WN3`xV!(tGl5Qj!Pb&&;~+#STPp#lPg>I-?WV)Cmq7{G1g-en;qDo;ouCdm{56 zHxDgpiA8_ipAN<69(+SEt2(}~y0-N^)HM`TXX#JH$lAb4LKIG9I0zXtSMW6@qxPvu zxmgHhf6zMGJL;9jeBAz7BN|78dG4qJB>%LxQqfLM8}fot!Onv}Lg||w6w!`XQ8W5bU!z@ipcIcxO>WAo*67u8#zOTwTh{ED> zYCC&4m56%#pAx=gJ%>la)nbhe0V)mF%6Nk2P0N^Q_duos$6MQ5NU-P9;*An*a@gC= zO7o8%uUvOn3VD2hs{{4Ui{?OubU)<<9XlnL3d-F5u$5#wRLvM`d)ri}d;1d_wyN)w z@~1p);#>NmGYN-i>v+?b?2%MjfLjxB~%3n^FOyM}hb7Lw6yK~Vzq z<(g|+as~X(4u2r#;VWe|2Dr{H{$S@Ao9O#mhmTT|FGH~iMeZ@Yk2gum34ycLVe35L z+Ue~yla$YjBmEjo>DNjKC7IHDRr%}S)5Om|sVjopA|K#UIr%f9tAburSGb)S4gsgEJ*nH>90%>`eW0}it?caRk~3!yFd z)AdAstR~UbnGxX+Zqe!Z6uRU(#xFPMYamwj1;MU%0}g++n8Ablyr_AZ%R9!DLNJ+N zj7XtSaQPf`YT5|cpis&?E7>w3ccE3(wtjD=D%vTe{kbo`PHqtsEYTRZta1$gqJMMvZdwVEB)nky zIMH7d_D}v9Irz}0r$qemy$2&|zBa?SpXL+)B)^?pO@0%%bcO_VH858#2bKjJ!fx@XRRW~e#!sHEIQq}`L@}BAwWwUAv6f2uW&X?gbE6i=@9R@%gM06g zGnUJo@qdhsfSCo>BVE$cHF^{4ms({eg2R_Hj;)bAsj||7I}7a(uEAAZk1!4l@_ufz zJHhv_m6UXilVi*-D%>G*v?rP^zN4p97CZPvZON_!s3_7JQs>b`Pv=C1j-^5gjTaRJ z2k5StvK4Y}G_4QcG3`y;k+!y1k{M|5Vs;g5vA5maE6p#I?-rIt&=qO{!-+63>d~ON zp_7wLTDN(|OL$vKNjh@Nyz!<^sU{j&F?jN%kIl@|;Ah~B$ymw&W*H|1;)tm{Oi=MT ze?or$;rwO%95u4k$ea$6UzAGjYOsFA&zO!kj}5gGdnBflpwquP`H-f6O;RmyCB1X0 z`}E@QmA`dIz?}>;E%4({agtFsXH5nRE*#!y&I#Nb8+r1cy=NG+=fE2-kM>R4l0ggA zki9LLB1C6(FFVSxyB12)-knemP*y^hB$NN(r}lbZI&&=_F3zvf4^28X{tXE zUjh0eBDIu|d!OD?Q`%{n<bYrKWt3Jotws8mPu?m?;IsIhG7o zl>=2_{ksJXw4|CHk7kKQ{R>OIl-*`6n zcE*sn639opiM=@X%c|QuG^{B4c?=LB?E9yUQjhyYcXyP$isQv%AT%!%hJ;p%RST?2 z5l5ad%29B!fwK^leTxpdt~nDL7GA#eGli*4-$& zaFhE-q50^{vy3Nxz>4jkW}aQ8br;wjNWNyCl?<`DOKX*h{9X(d1mLy2O7pPU`-1qW z6N6kVZ;Lk_lnxK)hHWMmFrY+-;q`@Y#_AH8y-=T!T>>JbF`r`SrF_ZM!-Gkie`|C< zPhskWVL;X{v}uA-6h&b5-pR1UVLe4zttCaK7*PS(3Q6DcA;73$(dXKyLesXc!J)KwbOPlO? zY5m2`o6`UKdG!1D@{;L(Mv6pvvJ>6Jx>cEa&GidyAQvZH(91`3av}V}_E~4LSG7Fu@?NWC zLI7=l{22V;lMVoRd$`72Fb)%NB(aJ-&; z++bx{sq?3Z;kpD2K2t-{z-ua4pKmBe|21$Se!h+V5{4pWCO9VD2Tg*;{I<*47c)+i z>cP50@03){eqZf~yd_e2!>+kZXt&Uu_?rXqb<*%TVZQfo_i6j#wEYOQDDR4Nrq7h3 zimf;NAVHe^mt?cOexO2#TPtD z`Mr6L3G}`c@a@#K2Kio{z_zzPHB#UFs-J##sS13nbiZ6zIQ=-4W>#3uUF#mXcSA89 zfuNM%N{9g)^UhjQo_ZPB=*ncM22))NvtBSOSX|!q{i$2nUZc|qw->eG5Tp($1vPj} z&x#+IeSY_af}c!DiJiGzNT&n|yK?Eon3@=>cL}`kRrlh0iH4}VN;7QA6o1KAblv>s z#L2El61A+9)hK$i>V%FH*V!VwF5$cR!$&_tEEM)tH(WAQiA*k#!|;vk{qp<#wp_AlJlh{ z;ZU>jwc2I?&&isG`UIIJ21TU)a{G|~{hnvz6*WhQqC3!hmKV;P70xn}y5}D8*zrw` z_4J_3Vw}&&dLg1%NPWJ|RrcR42iCXxXVc1uY)s|PrF6`4J?Tz^f>a#XxJ_}*qLEi5 zZjdAwYJ@gw00RLZ5t|6@IL`FgLoUb}-9-S{?+4%NB z>~saK#dy`wwZ%{P^!5&{`>fP13xPV6A^L6;cEsi_87vUeHxxp)@S$3{3^+onZ|!eS z?iPuL`;-)?Bw0_Z4E>M6=RnL*HZW1#1OPDTs(GwT=#HVIFjoZ(q2DA|0sxrw24uWq zb1}A;#z5Ko;TzVYy1`u9TI5YN#47s!rjXltx|3c@l~JLPcPg&y4?qTT)#UuD5k z|Hq(>x?O8bb)>r-@_xS-!Kz+X_*P7x>0zQpnq!U}G_)r8d`i^kLcrmNbIX4?Z+A8`jFS1bZlW#Dn^i1(H zk(~%I)ZgPrkqR8BKo7@At=k!x^&Rz+RFv}!zX^3DewnL8fnz}+bKZDQ{<4@$r_BT4_T(Pr zY(}8c!6zY)a3z7fL9SQ_!0`oJ!E(y^p8WQ0*9U4ZXKe3GDP5&b!BHV_8*I(p@`CE- zUVK_khWQeBk4$q>T)WdV3`~uq?`9xwRr5o#_o=dy1+97RtP5q@)}b63?+jvYyf_{Y zm6tXfhD{3Mtm6(oXiJ_?zb*RvWQG)Hcla98<*Yp=8fN^*cp$WD_8HAJR-hx8v1es0 z^Yk}PfR42)n8|59F^``bDPy|pIGfU^Cks~k4t6a6m~G6evU=TK3^ZLS_p=(CuvBH* zvD7?Fd>EzU062$O*EVKJKJil768PYO&%R6aA80I>(0JG?J5T-3UbVxGaaEkSS#D3# zHX8fs!9*>>dlURWnWeZtsrN3?N#T$@sTA{QRBR!C_LvKK4*(9m84;OKoD_`uKE#7mkO?L2u3gETl~kQA`#P&^gYqk?OU+W zbiD{c2GL}o@0v_QmSi}5Ie&GRaqHvyGp7Rx+ZbokYdYfl-L4J~$wp^2P+9N4cd#j^ zZ6^(m=KuiEF7V1(y}agc0z2UfdYh6+(J4r1jr#RtlT46Z@K7HLHa(xxy+>&3k+`1z zvh%PxsrPz`w&dg^6lGSY#C(Zvpsr zLgYE3B`XT{i=o|(hC}3I$-7_x5q>YGSojf-ZT%G~amV7psA9vrN+ZHF@I!ik7V^%X z*@z{kxF4DNw&3dukj|3ml52~%?;T(im7gw2E&FtJ$OIqi92l1g>soz!MN-t-yw|MV zpGUI@0HPb-0V7zGs@^x_8}M30YIzRb1f=CXg{PK7f-9D#?ZrPe5Z!FmD2FbWIf?&& zWNRfgu<8L5q;6Z6j$spk0DxnO&Lkx^;iaRz&RCG6YNFEo1(}8sn}71;5%$4UJ*_!R z`+Y}Q>7L}qoNJ=!DJeQUR({yai6^wjGPB0YI3O*ptw?j zUYFljx4Ha4qk0(1oFC}EWK4}Jz5ZY1e{O$}vLT9G7s(n_TY#2Wko19_9e?vCG$E!W zPIb@43u>!!GjI|Om7zOLDcQ>D!dzB0jrGeJ=qDw^u`#*Ifix}tGMYZ!uyOl9djKLQ#r09e$ zHt{yU$|t&jiKMw7)#E?u&?HZ(i-!6?B?4~bKH~dZpE4z@syNioQ776lPyMu4Pko?z zSrmU;Oo8bKuu#k#=A;3KBW$Z;L}-*456hl^-BQR~u>7kS*@XnOxyIvUW5v+)&jGgu zi{GH*W(<|xucK$;{IIeXp2A>-r-5Tjy-Voovj=naKoy83 zK_Qi1kLLF^WOA1r(WcS+ZoUy~3i$sqJTGzApnQ%v?=cljNh{C9ddC09_|_#QcLTV+ zRcK+35P~uQ@mt7zZlf=9zqthxBJYi!%)u#H9e$=E)r{wfQSN?2A{n_mBu^rF8Pf2 zY=X(&0}n)GiK~;8CJ&#s7zLhF$#gw=D5jof9e9}+@br#l(7)K->kbr2k%3Ym&4VC( z`XT3e`X%uZNwH%B_O6#Gj{j<(uDVM?N5|uuN9d$%Bt>E2nl#-v&NNLY7}|Z;y}JTe zL+HtRDg}oi`ulWk{|4LK2XUjU+dDArjr5&>h)QY2q^zx7sLC!xlNtDEC?wbw?lqK$ zG5XtiuYa>RkfDRF|EB#3sd%(haaROS@FhAgjTxUGbe|@`#0m{nd?%e3YajK43373y z#eDA#q>XBDOc}`JS#6Ka-+CH_^j)6k4b##pxLRd{8J}A8R^b_7%`8%3)%mLh1IrPgH4&3kYc5fnYS;l^EIpPP|OBj zaz$INNcT0Nnw2={ck+4&I^JJw1%|$95hf6{0W2o4J4S^osVvPK&j$Mebkm#n7(oDE zv@BifdTB<~RS)(21s0_O?NeYGv8#`E8rh;3H|igqx>?DJW@n@jAI4+F(7Iq*X0u9f zme#Og-%sN^3>31f-CD()|2Q5A&s7A-#kXY~KZ3hU!S7M(+8go0W6OInkf}r$ zZL9UwVsN$);iwkVWbw#$B9M)YE7xK*^Xz`095G_eD&-D*xH+`B6C3wc-|4%CsD*Ro zFVg{CF?JdMzj z%r8@u-1jWq`FAuQ!zqOKj@iHcCl{_(u$d%;HbIZBih?~F3{_G5Y@pbbBF*Keo|b#) z(C+K?jFYRC(amw{j#BIa92d_jwS&Ejfckxe=6Dhc^K2I#v>GjOwafol?>oh_aYP0dKo7wy}OmD>}12*yMgoR{@q`Ok+yG6{a#%t;^n9yiRi#6 zPb#A};9G{tYuiQgzj?3l7&(=e%Uw0KA1k;h3f}ro`i?&wFM0pa#!D4H6#J!zjv%Ne zv@?uwc6URi#wyh;WOx=LLD#o2Ix?k52{A6u8`E(W$-V&wISDNsek{2_QILPIAEa!! z+sZ|r%yO4 zC}}=cZ&!dfX6w_on8@1lxEoPGZ2d)C$`R(@&Bge=qHB>F~(DDr$*{B@9wZ zyVf;-e3x^p1m#dacx9}C)IMyf@&dzOkr!u}xN-Nb4c z9=ma7=H>ZZU%jcL?T|=ljHK6xf{{hHN>^+YpjZoQ`xHP(zpRE5d-D^Y_qk#qf-@Z| zd)*r#8372}Y0ZfKvaHC%jG{JI@;W*(KkxI-YiJ{VZ65>x+=lR7 z^R}yHnvpPk8G^72@n~jugrkJzF^Vtn%vW##tf2)uR1s?58(D=Z&V2g?{%lqQuA0(! zy4{Ru<~nu(skepyECqI?-gzYi{&t4xES0;+#qV`s-E3Z{Js10H8GXS+H;|KzQ zK43k0=m|1Ql=aiVALJyaUTded(p1jPhZ3@|4>tBW?g(R)w_5md_bG0#>OBICUN37( zzy^q`0sveNv?bF~jtC^2QUZP>c|6&UoHa{XFe(vZQS?kicrh{oxq(U6Q?2%;dOp5l zQn-|z${Ep%0LkO=MglP&1=7nMGxl&7ixR8UP}m`#5^pQ!=HU@8Ha!4?SVj8&H2Zma zAX}BLdWZ|&nYUK_qh%2UC-fA@wq$Rq9A(4`U=!(0ytn7hCK&LfM5kA#1FO*yQ;UGt zNL2Z1&d*rx!0oFG7*?-%?mx0mE2jp9YwUUomjw`L1(|u8Lp9 zB#gA{kSe6x{fi9vg2Lk(V%yy`Xcay7(CwSHlH={P4!Yr0v(Y5_gKgnv5x35JI9wa5 zzj{hZ#rC_iWH5JzJ$-^*{>#Pgkggj$d4B?yX_=mKeXUescK+GR27afKH;$;XPt|Ad zL7=VeWbBpBPvV05Lx-m$+8H`~Gk4~1x3=BXp>0;%-d>rQW5EZxVvnv-XroKs5jw3` z41Qjk{JN-$z5+xenbn8v$sqJ(pNu*dssmfFz;8b>3V%wnM8cJJo1;z{yjU3z=USaF)>T~fq4D$Xa%`;qaJ9eO-6gNXlqn`h75Nj z{mk{+oEt`VW`AZdNx%`IR76?tN$N=^oyx{ZKW4Mw%}9xFO1K~MaIS7_wVdnUt=2+ARr}|* zGqN}2cKQ6=R|z=tS0JcvuI$9Wx2|S2yj69IzgRIa%I%J1ZQUzD&qeN=1E~(V1q}uww)r3UQ$IR2O@`)Mj5N1#O?YM9*J{ zLWI8B9}lo$UvRn6OdU#%!DdQ(?|DmZ>6c?FW<+^kBBC-a!Yt)eqX9%mshl8}{_Oku zV#&|mUcKRf4(;(3$$@wD3hmgmUJYQxV*UFjyST7$B6x>vBzf zj`p2^*^6mp7}Aj}I2Y(z)}G}d{`Nha^09 zM0)=z*0pMbw-fxyZq*q_KNxhiFN&X!l;Scw82UsZk@x9jp+@Xdq59thI9SxG>&a%R zOb6oU6MU17UUIJ~U!^Vc`zUKL%%hb#rz-RH`=_G%N^6)uD>X|L;D~^U--8Ks1;)CD z{{^7af2pO)I|b3ETjM))C7Ph$bI(7yg@kn)eOr=V>H8oAwaj~aoW-skcIv=Q2wg+h zK(`DJ)T4AvLh1g4o4rJqg!tslbk{2Swo!ha{10hfpYFnS(d(B0nz>e1^5N zkJJ(&RrDn(tSDgB;=9O$NAAQ|@!M7N`+ zDkD{aCoZ%N=2dQzK$Y)>4ORBEO?uI1_%$K z_~P6V-eBKg{MXHr@y6Wy-^BPbEq*39V{$oxoCEN8o*FYk_hOd%e2;%)D;EP(7Emc| zD>Y%3xsD5nr}#^6vsK%u*x>j-aYyk;R+;)lnRy0g*{WQA^}Y1{sXwox$lox2!noH^ zR-lBC8@{6YoBPI#bh+lHtqE-}h^_rjdwWeh;zWw1d*!5HxTduyNg#@viuMd5r;|KL+AaLP63;ZbKyN_okX z8fJV{11+8{F2Xt!`8g?VBy^ z1pT3Yh0G8aL*B+bf2u6Ym4qhOQ>s~@lYLst{pfCt@Z-r0v#C5sxNZ4j<*10!E&0V; z&+co3Hld0?Gv=id?t_4Q$rpvxDtcWDq8!n}ZRyXx$rQkEA=V z_!axP*{Mf=YYNJljF*OMa{;+nbKjtzQe!Y<*f$f zgdk({JAZi39S-z7gyPFYa-ItU-+uY~$fcZo!fU%T{gzpT;I-_BsA6nP43T0^|6|O= zHq6SBN_3_DG8<0^o^q9Y%iuG0hyY(&4*Ai@++81I=AgVf6mZJ?GW~?nE9d&+wf1=B zzcsvpU@QQgS#b^-r#s^(>HxgpIj;6%A@t7|iXI!NMGuRlLo3I4Mc|f@!~R(lNBb+B z`%TRcDINR6-!|-6rj!#5yfh3NZdmiH$p4URv*{~v>khQe>5OW1c(N5Z%y*l1KzKD$d(tN53Tk_KtHG>g9a3M&^d`eC&1qyI4^ zDsL3>p{I65cG+SJesPD7F#yRO~>_=2X;SyOl%MWh;C7yPSEj!qUv9uDi_-P zNc^S=xaTi@xVOFOV!I}WG6Cagu>7F_{Zt`@(ph}WD3%xOVAc>&Cz2eW5L4*?i4Iea zvk^0p5^j{of9)`)%lz(&b#?n)Ny!YGC+|)%JIMl{GE4pm&G6wT-gADynilppa>p_& zB|n8#tzIy#A6`y&czOpsNq4lyoV=_xOjGzBRiwp@F&-+HdobMXT}+~<6410z{6H_w zwCI z=dJ4&cKKjy2U&wO{f-t-md012OMTZPuGuTuboMs?H`JpuWwZFaQY9}Iq8_Kta@J)z z?z_c=??Z#PFlFOgxJyaQK;X?UvH{Qp*9#s2H%yL6thi$+vTIjcU(f)>@QWj^$-_J% z8VJJY8!FR2<+ZR5D}#xv7FAXzd(7{^OZ&3frmlA00|~KNorenn_l5>wvNr1BP?+qB zV3Z>mPAG?B>3#M%Nje*#9<*69{EVu{fYiyf*r~$iSh33SG3&eifRqO$6ci#Q6@(OJ z6e})BT{HZRar#)m7=?~2pDQnG^02${QehcReeQmtk6m8~jCcpc8uw-Dl@RYFpHC~d z_=FV?H2{HUN@gE#aH{F0({1l=J2H<=Kz#pWKvj>=KZ7M5(<7bOJ+h;V4c{n`C|KH) z@pHge?mGae3+Zq`?YURHS6DwfQ=G}KU`0f&ik<$ar}J7Ya>0uw>1{Bk=nF5J(^P@%1^ptT}gP>!wgodM~;X8?tF2NR@6! zD99Vu&Yj)Hd&#DZspdY#n#ttdfI?z5@JG}I+zr9ItINDV3TA;!@!y%ROT<6RO{U9n zMf8{Nbt;u9I~YravRCew^~*MP25r;&zIHXZEL!WT@wf2yS+=~A16(6k&ji|A{097EKf(G3 zlRE#9c(Y^P?~DGrOp}wVXG*#2!-#j6U+4CZs-6x8q8cqu9hZBNqH;VLq}T;hfepu> z*>(0mSqlCC$DoAV_uKsDI=`sFM+DAP>@wCV$g*3GVF|UU=UXIuTMlyRmpsTtP zWb_-p|1R|f5*mGcAz3R1>16&3oZB*o+m&8R%;{00tp)gBR#|d{xS? z1}?0oCeRn%(RNgarlJ2HtkLF+ry~`ny4D+$Z2mk?jh;kZOx@(C zgZglfufWvU)XxPxIt$bgr9v);<7tky7w*c?B^ zB**Vs^&iNu`;vzFm>nX=-#f3)FT_p*eiO{_sG~f@;5|`4h z2O`=1s9KMRgr2mz8;V|

V*Uc^ltWDWEvo-EFHB%v`OAGOh6c<1%Ii(-|9SbQ-VZ z_!A`w)A$d5v{=oQl@L`y=zHx1ORpbl(8v6H_2{zpB$ojB^8*$FQqlH|G!MdJXfovH z)rVJas>LUQjA!B6q{=DnNu8s$doNybssCE*oGQDrf&%dYhf>;N8eJ=BWG$c4YY(Zv zc-snX#|o_v*b;y59hM_zK0^6M8I21{>D59zZi&1FSZFVsy%_lPw^@trmAy|eQEjm7 z&Hl|ob604$<=Vl_*z3)42GJ$}@4+-gMoO3IeBrK`JDa1!QWxlKY8v~BV-)k0D`caA z8jKBz$@deGc_E-&qrA$hj2;S;oLaY+SuUN?e-BDaB^koJqz-k2Prj)O8nPwoVn{1~Yx^(ZKT7&*F(iqCP)O zmqvMy0W3tHgF!>1t&RIB_{@~`bN|))jbH>^-BGI-rcfKRCMjw_A4_-`;PrcAg)LHs zbg}BK^u`&(C8}LT=;{@?z8_`GQv1fi9p^H6B4(skAsM>L^Pd^zvz$KfQ!wux$-Nm( z%aJb^8ySnGIO%{ZOFJH3!9*6nsgX+PMp4taYDY4a+_=#uATA~y#jU_9*LyY{ZJoZX za%T_GisdQGs8YR5^j5ICL{3I2zj_tf|CeAwQtkgkseBe978Mz0>7Z|Q7S z_NiIW5qdcb&=zX$+QZ}8MHtd>p)GYAK#8?w9HB-o--vTNAFyP4?< z|981RxHTR*p4#vz8)xXK9lNHrY?)6olp%McQk#aBomNxh6Ebbq%QK_hUxC*P##H^m zaBGF1&)r=E0+?d3tS#1FMyit*kOp}LsyA<`!9(gaveu#=?f&AymO+r5Ku{pgq@r>p zBhAs{F@0*9y}vi`(@<23mpTscy=~?+Y?RNoYt}!=RAu?mSNh*N4kR!`VA@| zySn(jj}y)rui(4A?AW9V$!f!(e{$aG4WdZeg)++A*}0dkCTZCErs|t3*5-mgXL`Y> z-#n=O$+nh8XyS&rNfb%`qx?_Q&6&#^u59*oPjhqP6`=hk@4qTQw|zclR?0=xXuhn< z;{(WDQv$BOe-DOdWIw$37~ZCLaBN2EdO7x(3KA9^Uh zz&%jVocP-VJaPJN}yH*0(UTs1ijf2OKiT6mnLtW24)OjWand&6l2bM>)f zwt)gj80YxCuDkwQ=*L6jKGy$Ka#L+S=*Zdw1-iR7%Y8W9vx2XQuXKKwEiC7&Uz}^s zT1r&k5}q%+`a(?7{z6G`{o9PcP26WZ9mboQY1?tt2K}6;sRmH7lD4>aq$fw@)6b2r zJa*S|M8w^PsyIniX-RsTA_9qXY_n3gzLu*7n2wBI`f(AgK3)B5MMkknA08f4`26WD z)**d!<14g?npL-lp`c8kbXW-gC z5_1;31~)>anFnSjk8)Bg#5eGb#{*gLD~NehorW=fDw6bCdUq8d zpt-`A%4XGl`px#hXX$ZN!_DvhbWKNjbH33n5f{^;I5r~ub6OZpK4LRiVuu}H%IlyNT%gJ>$IuojKn+$xH-lPsd{XIh;K*qOHRspqS`~e6AVo zBVZN*KA4j+t$hQp562rU=xm5zjwxjnc(bgR0*0w;{8J1om~pi3&kv8E(+dRk#H>Ox z_tavle-0TOK=$9`!#%H$W()r4v$UT>9<5ih=|_0e%&|(IpZg*t zTiRidehO5NS8}I03Xy#Se+5%=0&Vm;QwQMudRUds=F^!wZ`INJV+4*SVs!wx{4V^vY)2VVW_#I4~D1(PNm@ei5KPbhAZqe z#O8qF8U%LCE67s3h$8iBH;3Gp*i^@2)D+Jno0BX&VuL{Pac! zBce>eNqXfkC(UUOyf$tkv;yFpY?T7ZI|C@0saAK@`IgAQkVGI9qIK1yVxl``Jl&W+ zkEJipr(^r%J+!U@rRjV#VR@fwAxoYKbn#tgXyP-nv%TmC;D^JZdZ4%OIx_Ro?2_6u zr>S-M>Y9FeiOjXN@8h@;F*E%5@Kn6eq!g`sE7cca-H!z{PJDs|-slq_&{^d-?T8mP zS!n5KOKI`y+O^h%gnPT%vg)x(rmCP&Gz2L#-Pbs0IVy4WXm=wDO$-P?-*Kz{W`WDJ z@{(1!7{5yC^-RFcWo79%|MJigDZLCwUot@zFEf0wF|Fw8&U-QNBhPoEu126qjk!h62v+R3~0?&?$X8X|a*NV^a z);@ApaeB|hzO0@@D$FFB44Qv|_Euh5+4v}UX@jN0qs=?=9>Z`nAt2=+A&W5u2iV*n zNOv^b)1@B}iTu({D6vN`Z9~Y>D=#9XBzG{AlNJZq7_e{^;%w@Uqegct5G6Neq}(*R zsyhL$0s0*LHhcQ(mTC~(m+Pkwy=E1AiZ{5Gs(M0svDCuZr!7cRpG=372X*8s10ZCXq?F^fd9ehEjUNBei7t}7)Q=w2*4+O4AN11OG^4IM$=o%)(&#F&QCa!GU zy1D8z<}1^Lp@rD%5--ui);FQrrhryU{Un%>2oFupi9+5TkCIXa-^W-cjSpxR;!hoI z6c4(Ji1EQ=aBs75@niW>a~j0)yP&Sf82BWZys`XpQ~*zQ8`k_?(b>vb6xm7$pMUGI zeTkRf$p%wbr#W6kZ~REU?T7Ve1ui}LWq$KP+`sdYcOLQcD!)L16G=<@JTU2ex#p;3 z{R@cJ=agK*#+xPg;W|5?muRND;$f?}LW3WtDrhrmH^4~r)8r|KTuKZ~Lth&@{N}=) znAcm{me05WJ|HLCFLCuRIxoEpj1)z0AAB|D6YL)V0uy3X$J4JpMgsvLI>MH-WrnFe z0!S6<0MiyKDQ}>=SBswtTt2upW}fIqU8h$0KK?c43;VcQwQGFqQV^kXZQ4@We&%)0 zzy*sDk?;Td4P(Hwm3qU)b*GgqiVhqW;t35=3Y{?2{)iOY$ z#g~|@=s>D<(Gh!H{@^#~-r&NREM?qkC4W)DuOs%v)uFBL3xE{}Oof*YX1=98lCp4w zEqDR+Yen~nC`syK-a_QvT5HXfmarJXIoESY)_9}zT}WsBQ4NXg{XYf{sMW`!c(puz zqjlVmT;iQeq9i%=^KUVOsm&jK3~A44>hsd(_98R^xt0eTl(wte*8pt>Lk4+l!EwQB+-Y?U2&=T zP=asC76Au?ui0^H-*c!qI$)sg@9#n6QESBpW|nSo>*1)DvinhM75VpFiSz#$KC{rNV;?5k$aZ^SbSylP zKDvuHm}k z+5Fox@y8;G|dF8)mZ|NlplLt=8yq0N-jh)|AYhRqz>7|pRT$DD>x zNRD%6X3HVvY=+8l4jtq)M#-rhbDDQ4LgkS3`R)7r7p~jwx?ZpA`FuR?_e;hOwM(Z5{vvAy3A z#e`}QATE_>S}Lz^CXnEZNKT)KjpeP#BX@NVx@L8KIR}~qPhmZaYES)FSa`2gj9>~ zS4RPtClG~E@!akB?0f#9&0?G4+pw^I={X%L6$m|8_+(4aSiOk?P?A-Oxk@th5D{~= zzuyiZ5nN0*tj3Ek?IhSKSmh~FmGPr^PvQRgmj!o*2OQ!V$zc9=Of0pasu z=+h3GwMXPSIa86mC_kE3yC0_z=TY}mbWC=%c5zC!PiGaaONB_5`E8#e6@H|jRS zGDS|3a-)Q<6g}?^#qyS`3;&Z2hHy7ltY1Kx>?22E&#Hm-+=dE+tb#j7S2?A8*ST%@ z%&p!M4rP$j8r_FDB%XWVL)yDXQ-OmH0@n%U(=WzLX4x=8iHVToHzatgnD0W{!*5*c zcPhUBZGEq2`9*HitlN6hp=JKj{M~uJp-}}p2>qUklyF9dy89W77LzI3Sqn8Xr3zU4 zN!yJ%N;>D<|)~EAyvc zRMb7Fu6D>_%!PX-$EJuK??C!0`;ao0shLfj;>1OZ8vQ_0YrO0_6|iQcV{35708T(7 zG<-PARGMBuNf|eO3fejJKjT_5Rg@h|2~hcfFS@66g+V9!)%J&Wr_w#f_0mMp_wKr_ z5T0o~y8U?d2a9s)$RE2semB9+JMheXx!I3tO+G?O7yrHwL}Cu~zy8{o9UAnY6(RmSovd>;js~_R>;X~EY`5 z^-A#Cr|Z8Kl&w=<)4B+6X(u<>7u>PQ=gfGETA++B(>8~D)#lEB0S4j$UzN>iK}O|d z9ksvxgAPUiMfyOHZ7X1>*X-8qY2TZ90*NBl=&FKGDU;K!jsp1wr&J2v?XImdwF@;#M5JAAB&t@4l*Agb1%|;~({Ip|=ctEC)o<*oR z%JR*g^#%K9430W5@y;!wm@TWGhSSqHi_ysMLenZoVjhFk#SG$F(yZSZP}5Z^AV8e>8Aa@~B4+#pv`e zi-QlWEWg!%Kra38Z*^;VlW^EcGsFj;Ea+zgnym3|*MoN8IsElO*VX|@C1T8?Qz#)- zX~6XJa@E!(CukYC^iun{ADRijD3XpIwirkNt^&W-&~ItOYV*{(KjRD6sVX zd`n8?_LG=JZInxy(nWxGPQKXt>0iFgbUDMFu2>o;kJAb4KW6+;Z`eM<)4QXt3wxo} zouocOc$rH-1z&CI|0k@_8*(M98sz*|?kBs&hgJ{BELK!&&Cm4yI>^iBC#PA^16l#M zgHMzS8fuqxhTUb58v)zX_|4w*yN~70dNay{EqX=NoghI_-MwOQR-@5u%r$cIfMgYJ zQyao?127^qG$nLQ_dbobXlVWP-A7Ao7li$fV__ya*e(X{`6iuR{J&cN73>siq6k^z z(TilMkX*v$1wvWCam{~DFZj*CB8q7d=rqm@OMf$)FB3sM&wzfQ_bIN5C=wyowE7zT zWr5wf>j<*H9viU8{BrgUv{FGPzle|#d8wX*bu0+nOW`%Gc8bw~v?u5ko~;&f+maJ0 z-^~5^Fj&V&qo`3-^C1x4L<&2e8IN4JwMR(33!jOKCKLCz1f@AANS>4;vO(hw*h&t$ zWnY9A5MyJ{&peq`CpFmC+S(W#+y(=8VtGpfxrA^Psoi9Wcp9LQl9Q)txVXxSN`=u<{)0m zMfisLh66^POGI)*Zl!X8sQ|?5S1r4L8?}Cc^MosR$2CU`YNXIvm}q^4wfH0tG;rT; zE;~7nWl;HEla=P}ROcOf+8LDf;d(_kXkzpjKi%b%2zS6;-r{#^&v>*u=B9+AOPnHk z8Nn1MoWXw!m%p9#ZUiqBK5c*_PY)0As9yH0LrkQ+{*XXour zQ`3D1><&{U(g0VmKjWk`F>`dtwxv2BDc8DGQ-I!7-QLhM&pW_HY$Z%=!#TFE=6*1IVjFi<=Z-3C~ zBQdbg53cAI-5m|$Q}c4W6OdIVmXUB&KcZsW3a6A3Dv#U-@OLpzL^{Cwr#%6uMLQftP_s9`;pQlF_boI=bnuiM?f+)A~ z=Rw87>z8Dsfr7RJ4R(lzjThW?_ZN3O12|JfriMuWOZ?j9AoNCm@mopC`4xqxJ0wEX zncD4V)_8{(Yr0hzQ!~R%7zqC2%aYnOzmq;3h}QO;s5LNDFJ}~UmM)<@R*^a{*i5m` zn8`EDkSlefXU%5qU|#5Sn0c5E8}>6DnA_gXfnFcl!x4R9VUK{`3UWG^Ud2zuTHi71 z$%JE08n%kxX>^;n$?-8;-{L3)$qVQ3VQ{%QH$FL+OT;IyjAT&0H%89&Aba*SKFCXk zc6)p>V|^@#y7(EWseGynyGJ&%Yhzx)hA)Oh(+LCB7Zu7#J1YR^rx$uIlS8+&!hZ9Q zS1%acMtL0-H@m<>0~6nB{3Q49q8gtwZBYVVr(X=CC@JBEO~RLe>Kk|HUtByvzhhyq1qGkB^TKl!#ccnenL!of=XK|8 zxJ>2w?AX~2P0nmsLZh-qy6faeIo{($QaA;o6WEpjjc0*s+Ew9#qQh>pNVzddtIJbw z$@8fh_s)P#*@B2F;ZH9vze#opdHMF0|C3K)PutV_>>anKM%KELeWiwY8-L8%EzH}; zJv|5kZPf+AB+}m$R)J3DHPh(#fMbB>f-w9W_$(DSD|6G5ELcqvW*x2p)oEX89C+;;|5hdFin$ zAANs>q<9hiK*|08M(qT5AqjcsziKc?Z9j498jLDZ(IvP%NIU8$#eo`uvC*hM<(mV7 ztl&N~<@~jVo~Z%0IWqI3d=Pdz9qzsYpbNipVg-neN$)Q82qcyuHD(-7ca=jwbCHQA zGQaV|gPcK5%!d+7jxGwN(L?{!&YC<%FSNwHzZp!DvQyrO3 zGX;t4Nq4>IvCDXVxR4bX>)%b@X#Stqe$t#?z9#(V@AI7jmYARw`ut{gsCA zr%V=-qJ1%#z~#M%s@;0c6Hj^_h4QhcQj%yV0^0@EFyonbWC12fruo%}zH$U}h3eTI z_&eGNYKpA64dGbTKl7DNFH0!p%t+H#+w9N7z?Fred-%x)wcngVDcGqCD)l0KLbICn zk)h`=rCoTma}b45cp%&X0PyyuZnDZ956B6eP@9#|t3nXl+YOZ-L8hV_|NNf$x|svg znP6+}eGL#YkTeUgpW zAX0I@W2GPXB$S1#KAc1|k({UUD6!0*(J4{FFvr&Dpzr8YL@&#{WpuP;M&C4% z=NqI4^J9@fc^!MzD<^z!t_ki@rMGi;bNDs$4V?kOl_G>kO#3K1Qq#tTyQQYA5JpKU zu96VV4z`a~t!XYADyf(%N3{xG`BWX_FDYSlqk;s>h%bP8^tvt0g}_PWuy{TEAP}xLx<%#uYs_(DNENqMC3>QK@4}Ab z1(s?)iYO8UHQhcS=Lfu*Yw7YVTLyvlV@Y$@w{+@1G*2;UdJ-6qi7m}Lx3k3Wg`a=c z?^JOwcb?4oc7cIR?X?|sxQv^>n#;?uBNGZR-Mk_|rlt$ArG1Ef-a?cg+h9Yi@IVoe z35fhe(1zt{@KCou3_Z%WLax1XZ4AiZ&22()TP20}(e&;VwB;YAgY{EqCZt=L;qU#P zS`+A6Ih<7X?(uTaa)yr7`M+lNKl8EB&%h88%u@*B)6EUa=nX@7GiMDLnz`nP!G4F= zS_UghLoMRNnrxulAot=37%1nXP+ib|lUoa5aZU5SvdSOEm52n5!+W{uWUNl&mQuP1 zZ?>D{RO5%(-m_v#x&Pzn@NW|c$+dxFx8-Ulcb*2yoN;=P3t>kZ%$j@>5K5OT4Gg1)XiWBX~Ba%pm$)lv!_eRdJB%00#rN zQ0nS7i`AgSWXwzE-)oQlJ2}&~W?ek#R-cL%uOmdvffi2CVrRQL1FY*mo_-pOl3mcT ziV~Wa15B01u0mgJa`)2FYjRuCG#+aFG`H##PW5X-{L`{NaLF|&i0$ZdHZfjm+NQ`u z9Nf7x5n6dDQnmpMYB=oT$F+OH*XH!g$E)Z==zQ3oB%$h2?oXTpabo^P3Thm?QA1wJ z0}5CFP$OT9Mem(#R4d?txeE$qs6U*CfYB!@Wa4H(sxP6Rh~k~vLrRqR&cycCm4}*Lh(msNxze@J0Wr(%>j>bW}zH;=ALib zYha#;SJGqVvP#@46q``}ZAQ*L_CnUI)XF>|YF{KfR;N_=)+15-!!TRz00bKllxx@; z<`60myiMxqM@LAoDn@3-2jeH{b-X)uQ9jPs`J&6UCNZe}%O`oTKIB+!uVJ@bhzRM` zsC_bBX+%elIk7@LPMkkeCS{@jEZUSznt1G8zj#ZiYvO{rn>6)qg4Ur-j1p3D*?tys z{Ipl-d1N8WuJzFiIL!0N*I^JLgxW7{o(kEECFQsEt?p`F1)-}O;!a44kkVnK?@b?t ze2XbuppD^D$8oc(IV1#bVbPu}4Ff!4M%o;;$e7~QVPit1a(I;7P+>p80Ba*raE95<@fpqXgqt+a4e;oFs% zevpi+qW+oxNnX$Upw0Ydb~Z~jg47@1ZKT7E9Y$LFm#IA=2|+dE{M%XCzW z!MNFloR*|7rE_IcUL{-MK^N9W^ME2l;k#_M3VNdxT5yg8SrY1<1nVdpeE_#j%eZDT zVCF_{v(m(NMO+sY8a)ct$~_41t)}O`8%I>LQEAFv3d{BW)!(uVTE+UiuAKPtgI6V- z!6pNv>AO|UssX2c=I?~5t35wHrMBX#1Cdfs{x1i7BIWbVj&tOd@@uhPBRWq)<3`|k z!toO^=!<%Y?&M;RMq_=kM8hzbB69-$i4}FJE`8Kb|C|d#Lln_71dooca#C39EZIbg zb@(DF={^&-`~8y3{fqeh+T7gjQj;-Fa@+XX|G(u&`c<-)+q=%L=M$Ow{~ngjKDhRKvvUv< zr9jN2P-pQ}zKt6(j8p{Fbe^eO*zn?LY(f@xi@l00(($a3^)=YUU&ccudK8oB3G#0|JeyBRRMbDkG-mv{xp9X%+y-= zo!q1A?s7p>9ZgBeZ&YdWJ7-&d->W&|yP-P4zS-v*w%01)v9WbkTd8c+T;O7G>L=`9 z9WjcZy-&pZrdK&vUKMy;biYgOYa{+#&Q?z4T*(ZG(; z@?MReU36Hpwx49!1XZ*tDwwN}uSEa1^_A)^TM33>sX`johK)dg693i0|32Fmj9qVp z%_me`P<@zsWANG$MliVqs=WMl6`5dhlk5~$SutOpw?C*C5oveKn6)$mAH=ja>;@c} zz0t~Lpqp6f<6L4;wy;(Qb94uJH%UvaX%F*G4fv-$?!A~HCPIkLe*(PvZ-asvULCf$ z!($09oUChv7S1a54uPRgtfS0B2 zZ|vS&N^{^YnN;VUd|Qy2q63Hdu(*5uQ}2q0!} z*S1AeaVxy*%hv6PC{z# zfAY*5bJy<#1#WoHb6&KPdN-7bds={hv0>^Okfv6wF94L z&WH9*P1RxaN76$3E>Nrpj<&p%rdAhx7k7@9__1fyd$h&x1BgKftkRfHx}cIn*2`Y{pUCtJdXc}zhYIVj-P!=@ zQABsAI+xlw&yz6RlxsTR*w3|(UsVLBYl@I#CKn?ERpn6w=t<-O6|_(^!bK8zs50LE z6b}ZjxPEczv;Rm}jO7uLGc*%%3ZG8n&#JwtG@z)ij3O^_lgickb~dIyn+Fm+zbHT0 ze<%9**&Bf}zj5Opk4dMyNT z;~(Y_XyhCGBi(y5-x&@@W6OMK5K~8ih=)5*Ne4X!#8RNZhA>?-PsOhcdy9-Zoqch0 zd)&|M*41v_5~x_NMxXsm@g(FH7|~<56ImdnGW^`}J4c%$)p(^!@+e~(3{)a{?qEI( zYKj|IeBIcd4(+-VuLW2^uQwR9Fnu{gz~u?<+Wy$RFI6>)0&QzM zZ|Fv$s$hGF_(A)Fn-w**&yKte=n=KHri3e+!h}kTWWRu~pyS@a|8Zd1>?y++|D;QF zzbG$hMU}`mYV`i7q{f1+re)y8Q&P4o>vc&Ohj``^WnAtK0T#ZSCxk*}yCm{3v;CuB zS{u@!q7_r*JqgH`(QRGV-HCOo?_lout=B=&MYAr#fhF15R7rgT`@?8z&(7V_~ zx@@_~bAFExNznpPZN6RlFRwj$;`6dJU^hSr&}Rd(WCLi@N+QHQ#-|OQMM5jnMy0EV zus2cJ`e{wdO1bM#1f9JEvl2zIL90cP_HCOdubl_jT0PjSef<5oY-f)eESjiOPUVs$ zzTcg?-S$RLtD)S&uns78LM_!_)81s&CW=Sg2@jB|nz!!*0l-9cY{--;{LMlo+Kjkw zKNGqs=bXw7+!vF7WYXr8+(h{k0Par<&KoON0lNg;>^IjAliY1UkA6gDo`-6^x9_N( zt=#7Zosp1ZCk{GEgSEIzKr!3jHy)9UJ!O|E4=6K;V)3!8`RjcvjkRnHNZO&y^*ltB z@`Z5o%o{3}Mta`vt4`+WTy`c)=^ZuJ<^~CG0DL-m5=>0S^-a2mNZ5^?iN|ME32G~# zMV4X?8Dl!v#~YS&F!|stq>sR`prkw~D!sbR<1i8EAm6liGB#?xYV@2}G~xAxEqfW} zaJNNHueBIQ2-G=TbQSTs=J4s4Vz?q?=0($ZZ}#d*w<#WGG3mj(wzjDkl}Nn*?W;0w z63@6$U!|!-Rx`)X2WXn!U~tG+6`D^^$=9r*%^;AWz$V0{-@+rGe2?6)7)CtU+Q;sJUFNa)Y+cwzPWLT zBvstpsV@|&M*VR6k)B!5aTx$N!!6hB7(R)g5!;ie01KS0 zTP6GSTW?UkIe{WaY}y{q*@AHjq?VLyN&Mo?2?*xB5#eC@Y4U~Jhd<$#4SAt{N_T(i zpujdVQvicd(b1&OSEO5qC?@Z@qyYXJO*B=h9Kbt};f>Btn(`J$d>QZ^Lfz?O}c&dNI> zMgB?WOa{@1pgT>VT;hO!IjVAG%wE56-#VAif<%X=`x`aBOS4={0a8)w&-F%(?nk=XGeYVloJ@*B>BGCbylJpC$$k(*CN} zBr}-s1d|YFP`i{0l8x5%V!WqoKjM3o2Zx!nds z;C?btBup>D@9(6RXKfo%Ad9d|dQembD5~ly>e0RDsWma`sQg#6?ypGOTFM18^*?Q6 z&L#MW(f_6+5AJW=ojhxLKZCs4AqG(28SzAPIq;`pG@gITEoadE4roQ?*f54;5MSi| zO?535BL(zq-H6ikomqhRsbK0i2ZmK=j869H{@$(0qQ%LE1G*ktL1%Mg8d6xCa9j^d zfhJ>wN8c1*&<>QtYl2p#$HlTE&DM%e%Br8@Z@=Sccgc}<|FF&nzooj#aUu5|87Z4A zFZ1tF+f3MthLZ90ZBH9mH@v}L{?)6&F9Y5?xGt%Kgzn>OQ_lZ~D`)9WpV`_O*c z!`9wdHL$+K@qJu9jenG~i_&CNI+^P^#$AfOJd5hQC>sQtkH$ICB*ok^4z)gr#EaKX zNOB-7hX?#2N?p+M~vAsnV62py=h=DikiroQX3M@(SjT^jNRWQF{P^thxzR8)seV z|1&(}OgVKD(IkJRt?X07f77ls-j~gHv)g4&_dn`Gva{-I5+Z*coB!i5cl2Krp&i;{ zuqtJiuZ@~j<#A6vi6m!ca-gxhu(!DxP19*)EHorPiLztA@~W~axh9f!;Qum-MHk8R zR`|DZuH6s0tqA7Ijq^{-xiRoJNx@ia>azo_*x!~TDI>i=DbxC@Af0mQpLE!~*E~P$ zn&b7&T?MrvK8q~Nh6HV1Sd4;7%a&q?eDt{WptaO9uIDgp{b0J7&eC;?x+iT8Q89V4 z?_Sp*|0W!2S4m0)7#W3QqF!;W5ZlVvYF{(r@_4D7;0TPn>|1UEDLU0r2ex^;kN5HK zQ1<-G{x7%rw&@^d9(OO~p!~qzI7>HZ8xZhQKQh{A@PCkchE6&G>6c-IJSt4N_Zf14XU6s-UYI4ov`QAu+Z5ezRju z@hiNil9IxgAY7_I_YtJK3Dsz#r?Rr+W81$->Cx{y-M!)VYUpC%y@+l=+A?iyfUdw-K*=ZwqnVb^EY- ztG!UJe58xw7y;XNDY=^wQ(id(uXPyAW0Sby2(;_GZ>A6N*NLd)N>f^O+KtvEB7rDo z6R7%hjTD(Q%Xt06X~4}jv-n)*vM?bZx3@uk*}{92NV-lK&i7Q!i)l+XNXY#eGbNM> z8Kr#p`3%P@NPYbZqGDWHbsStWnGG|d9SR;Tjm(JcWKKR^#eer%-uMC^7QiOL@~#s8M$WKG90`Im8JR!kX6 zAm+*k%7avLQwW2I^{Uky^zrW>xApOHViE@wRtQ zCLYj?`aC;X)1DLaMn=EuY|HZhigS~yYHEr~uV@8wQZJt^lZaV>F~HrwDUy81<#U*xN8|3LZNGvFqlvoddvyzf+Fri~?^ zaGKD)y6M17UDy5qpsuGNayt)G?lqepEeBRD%ITGcWSs1)t3)We2Niu(%}5sMgH?h{ zWpy4G;-3d-@m3c9II?hb>?B>r`Q0d-2N40MCn#i{`e#o=sY+jiF8|f0NL`%Q{!NW! zRu^rQWu8H(k&qf0|473^T0fS+kQezGH|Rc$;&BP#=nNx+LK0m zXP!G1eHma>{m*$jv$rC7F!QPauj#%**YXfj?9Ja%0T_mYs~Jg*JA*LE@;(#6k(q&t z{4MC6)ybNL+~$+e$}ZJ^P}DZm(U)-Z>kg8f9MF*VYB_O=zY$y0Z|NgajM`7a4Y!_i zM+n@!{T|49Ffr;o8k(nFia1)7wnaZ6WWij?-Z+U8h)oi*qSmu7gC{ofP*<%(c2tJf0JT75K5 zD!--b?sRm9-;a^TguMBVp|5gpPS@(}IVfe87r?oau+FKeoD z|APEQjJ$^0Hsj%4gc5L0@$OWGS06J_wz$6J!eoB2CdCqsqkH1UUI*=j0fdCaaQqEh za=WZTcv5orIJD?Nqmy|?S%v>z1K4aug`DH3x#00lU3Y0~wep_7BY2qAw>F3`rGeD1 z{chBm?=1QA{2{8`>SbCTv7bEVh|dNut31ud9{QOV4RHa4tvbIRW)wV)$*S#0K&@zQ z)NkK;29jy1PqNdLpL2?T>qL(f{xyx&(izN5{$1D?Mp-X2zU5l+blAPu$@Xs9tn%sm zFF(}o$LD!qz4D~nT{}}l{@$N-%;6_-G7wy*2Yx{gypn17Z0yDqCZ>&Xdge^1Mpsu+ zQ_5%qrygE12R+?Dx_=$8o)hvxyIpAdaEsCgGcf3*tB;V2V&iqWLJYYJ-tv8b$lor@>32SnrUsS-& zO|8Is+#(QO0zqg93F(Aa8zzmK2lC?}%K!=Wh&H#RK-?sBn?9d(?={3CKL~y+xlaT- zwS(z&aUlx4Rjh1&Uu=W%oAXc`Ka1BS{`$>_Vcj0tY7w#S1@ru3>PX>p`px>Dzbb26 zC+6|<vz)z;NgML0%mBO?KJ{5Pc=7HrVJ-GD|+<5=`b4Hr4$a|(rW}#n;gaG|9 zomk&IVxf{ecov)0*U+Y<32~c2uOeguCm(mkm4#_J*K)&iUQ;W4rngV2L^<| zNhymY?R9F258|S^WTjc*mCrD*c@n2^V&P9D6ZKdpAXO!X^5^!MQR5s*gGfl`{H)px zo1wd-sPXez!ylvKqpyL7W>c2!3BM!%=KYUD^YC)(Xqp?_QuZ@C;EzxRIsM*~_^2ec z)_2#07l`?zx#<**emC;h$=#ZSy!^Lj81x?o@{@R^@3{WMbh`ULbUy@CVf|Y)ydwZ0_8j zHs`7JNd;u7wM4!n|DrC)tfXqv#WKaU+`%OWI&;cM_D!zJx|5}g|9yKPS*X%DJ1Q}2 z#pJkxXu!zw*j`-u=r9|b(Ms(8bl-;;gTanO_#AyZx78x6G-t~%H#{*Pl1uFbi2GJ;J{x{K z>ogww_qvwC?}QHA6f%56{N(*Y5DM;5y6JS9G`hT^kxJO6CBUmE#gkD7Ud+H)_0^-RY1JB z;R++y=rrW5kA0CuO}WcJSC=6aWW%9^o9D&*yWJf})cugkk?S2z@x+)Z;ria=KCt3= z$Mg%1c>W)k~V6f79gYJGrL892Yd7*fj+80%s=rdsJ`sA|?c{B^$())S7?1r^4n?Ke=sBK~L zb`pWAyU8JJrZ#s>dT}S5PlRM|*K78yme=b3#OqN@&-2f7z1Hi0+ZFuc=*x(zRL-q| zsABKfwkV%Lo4CAA&kS8FeAw5Iq~yS-yy)?}WGo@`H5=?$xXHOX)q^f*3V#0VL{chy z)Ar?pOx2qzQ*)P7pQ1)0V=Wb!$kUEcZG!w1@3ux;(V9^&l{Y&VV!yeq7IgU@on5qC zcsgg2Zc4Qz6a<~ER}`+@B9h7%e0V(fQsPLkKmz=}7C6H4giE_GF> z+vM@MRk{bLT*-`WC4Cn@hHNZ1VSbCc&rfw)KH}bJGn3q@xzxeWiI)1m|3R$Um$Od^ zVL_I9tdZpJ6^er`&qChLgjY!h(s4k#of2!H$iPIkg){K2tf`%pm&<4(Ky<3dl6JBv zDK;K-ezZwlbsVEXYR#B^^9FvyI-=&lek*TrG^BDi&u=KE6I zj%^2U*%q~=2`&?0VLE^9x4ZcN$H7Hl3%prn1(+5IEx>&^1rcF)6EltAGW(ru7cso+ zm-AGSDsC!Y)G+&v#P#Jwwl}t>shR9i&RtupN_4nOj6V-SYmjN|jDr^8*j3G|{+5lo zTi?4pRR$~fXXIb|RfoQqJVKhkacSL+ddDu4{S?K=j&!vvT!6yJ@bO{xq>E(n$z{rt zrhhVlVF98%z}Ope0E{{G!`x3(o4wi<1a`RwmC^@|^)0J4Qzn~Q`=Tibq4cH_&qSE= zBCJ^4MGD#HqGG3k-sHhd;6aPLOr{gfWs%96f8Xt`t}GtIPsKj1a=t7Tl%@-FSp+rU z*%)E?ojW)W&Z@xxjBf>-bEcGMGdqLo98yYtPT~oQDQ5)iWUkA;B#u) zY48*jHmWnW)sC)tAP@~$XIm}-yBEfy=-@&Cvc}pT)J`u@1PE%$xk=w=**AprL9x1^ zPd`Jm44uyp51-jCRmx@9cT_Q*per9Q2hN|>g_?FiGWcZ-Qg6fbd__I-dT|m)mbn zw9Cp=Vtm3f-@)oV8|md0TnhT0P) zHr2vndnfF7>#9xpl+WbDZ2zD-GRAy_;bXaJH=+h-V7<6JO$OH|qAzH9Sq0hkTkJvZ zSgRQ(wuusJZ_d84_w&PyG%FkHIy3gTVdaJh?MX|m>?pRj0gqeBk%HJOn(YHFDf5Z{ zaDZY^@7x#6U_fHz4$?P0j65=;?x zm|jzc!91TdQMTZHw*0(lYb9qfK6)lM>eO*Wd2RouHqdd8sR;bh_V!VL1+LVd=KQd) z(y4iWqt<#pL%1`yD`UCgD)=^th@>3Qt_}?v68TD6Lz(?f(ACO3wj;J1Agfwq>|{eT zAlpjA`ZQy3gp zA+|bb%7LLd0cm zIBV^AKq~l3+*Y7{#f7o5hv#5vv9#?e&yYY9sDy~(=H5h*z~<6P^n1`&g`fNLE#Yd< z(kFgVelbeBn*^Y?{j{u+g27#4l4fcE%)y5ar6w&9=tn(VqO z<5Z%DVYyC~G&b%(G^o5}`RtaS`0tC>5f@Y$b8IE{^KNh;uRLAjW_N^`on=P}GsI0f zXy1!LphySoUy6O%tI9CitbQh>?C?T0tCAd?AOei`xVZa&nWGk=E);%4{DYW4@%QR? z0>oXXmZ<1d_)mwxF1wp|I<%!jl2X8^-4F73wf;GS1k$-5B0COYuMu+Akh0j~Br-*T z>Pf51X-REuyvHYAa@gJa-}XQNqhD#rs8mI3&R0qAoKwYug?AFx>x>pfKA7LSJ>jhk zp~8-hZZ&_Q(9tQnvFgNprZ9i$%%i^OZj3N9o0bN&0R4HLLTe4=0IGGRl*J63!)p^L zcVX9rj(wX|ea}H&`0IeGOq~-Rl~@9| zp!!|*n0^>m8}lM9{A(R{j>@Sr(sB_MPqz8&!wc7#2;r zB2ARJadMj{P<#SnWLBNOdGhf0%M+`mU$+p)(B&9;4YrERG$b zlmg=FUIa$T;BR^x8GXj-ISeoYul^yQBl}wFO@|TLtRL>_73g9WRHbEDd>6Lsbf*Ju z7ByYJxls*yvvt1Ak2abgdqsOFXlVD+Wel9{+JK zd2#>Ce)2L!aJ|tpA&?Z7pkU^1_bWy#s%iPql#2Ps+F%{@vpcJby0vECdL^xKBWAlF ze_2?~MasW_1^lN&VuhXz9I$@jcQq>fqepW{bo*SHsol76Fa(>MhgsdYRo_LB1BOjf zw01NEgI;@R6VEs&W%yP`vn{*BCAuH9TJV?1wSCnt0*)GsB)({Mr{cSPf~jJ1b(*u{ z3!ca`I1s>}UB2Q&7)xutK=CZf1UA7xwNvpElTiC-cUy8=e(2iFyNG@Ua0scFtl8H9 zJ@bB@x)mvxk)hK8Lcyy%7Cu6L8L!9~ecO5$*jRY-LcwEWR~@zr;%iNLsaPBNRaV;B zkkg&!W=!iB1HDIbuYZC18a6|}JD-@k64=u-)%(zK_1Y{2!B&@6xyIJVRUAK=cBz{% z&Hzt{s(stB`>nKu2g!Rsnryx_xTN*dKK@U|_SD)u{LE9Rlil|JI39~98qA|6E$w&c zKNKW$9GTbmd7ZO~DkX`)NxW2^&w5!~{@%kD0>eDV@9Do6*McvNz4p*ecWMnCe!O&Z zQy@HfI1Sne9p*-@fJziFpP#aM(xPyjd`enz{1+8HHekxeslz39zyk#MdZeQ9#n1~# zpfsf&2wzYj)?{Sa4{Y*uXC@(wa-<)5qhIQXRxM(2NJy0vG~Y`P=tC>2lM0>8LBZga zic<$v)`rHslVV-9^9A8mkg^7RI+K|luK-L3hJ};-T`>h-p^RBi4M8<=d((7&{6*azbJaEU(nZcZ5&U26DU0+ZMJBM|OhayE zC{qKI`$6W^7Jnult#lND(VkZafcTV=HNYvPnj_Q`WzF3lP;?N!QN{3#HAy#z%^rqN zl#VCnalKJkFE?KK50#Wuz~la_#(*y1A=v?f$QasB6&9ldfmc-}Efu$?u8dr2`YoBC zUC!E_bPnv7;r^_`C*os`g|dU+1@Cw2qKA^Nt^kC~eKZKQ{n2?ApkWK-HpkR0wAYZ-8e7WGQwo9twRt+(^URgtp zE{OJ@J`F_|vGFx2PXd*LW6SPM^0E$xxxaoj&P^=hKyBGIRbnTVEv)CT3vWR zYvPHu1)c3(Jgce2vlWRjP+g;Gy*<5ibBG~wCjTHW=aoGfGy)LFB zUrQ^5Vt+Qv>RLor=ykET%X$9Q4dlc==eNsL{O;twdPNJ5)~JU)C4mzp^H2TJ)cy26 zi;+CB8b7g0vZZ={+@Huf-;1*CfHR&oKiIeKU@kWfDSYkPD?s_j0FP_*wPtl*!;=qU z1ynM%E;C-<7UJCLPOfEpB=#B(=JeYzhi(n=rkgTXzScUva^yTyF|kY~ z|B4qT*A6BN(7C-8t6MUoIM3ilP?gls9k6YqrxZH+y(xUH_K?t4z7k1+Pr@#Xd%N=H zsVb<;gpc<(UHReM^CX?r4iI>|%hZk04Lx&jtwouSc7AMr2=0YY7f~GMsT>xkGOe|4AFUQ5oW?H- zPgbbx^lQu~LAD^@p@cF zat=Qi%u_ABxLuh9eGJpdU~;btP^ouj{ebLg$#dV$t;>7lS&_{0l7O4DorOpqMcR4w zA~cO$IeQB4uXz+c{b9Spl}FK7Z<~3osAC5ENjELL{o;xA&aogQ{mPf#E>({F z?>;`F!IR=hNGZ*ovrtD^E<~%HX?J8C^wB@LwPmHD`i@bwvIVb(hR)kX}U{RapCE z%yY`R2>>t-@7AyTaLF0r9wo>P^P1sj{nNSsl8@CY+_rk}<>Ov#xr#3O>=ofW5nvFD zayX@WIe_!&8uX-y+`qc01-A6Ow+myk7k$6(wzOne?piuK&n4Yv{qc+!@S58Cr;FI z?>%!h9EcMasb#4+05$i{GI641uF93WvRqlFsXZ_69{|7c<$TV$@9XND6ItL5cCy%| zr~UO7Y85Qbi+l7yN1t_TJ)0E6U;AmW&v{*;B>d0 z%VK$=fm!HR!9CbcQM>JzYx88auojd0jyWmwp_3FGYX+Gl>{j%_;MVlU z0=VNhIF9-LR(*cTkX3wDmQZQ|1-#KO3_Wdq5yrnO%^yj?(Y2+7{@ps+#_S$1!Qecw zz`a6YtJ)EkZfyI#TtoPO=js9;>0jIk)(wMNh8QaQWI{ZD@PF^{T>T9{iuMDHyl|9Cdx;w0%w?EXai55M!{-gLa z)fXqr%bkdG9`!ScCCco90PmEl;#cKX)ZI;9<8-TI0ETAW_`%|25a3ZV^`18dU0`sQ z8k}##AW7J_iW;cHiY22AE~_hNk#D#JmhZ68QURya3bM3|3{U35TvD1dDqHUUTjMI9 znJNg6PXg=ainc2I6)aWRJZwt_9CF*XwbBBUu*jwfdq`LOv_x(FgA=+h3gAhS`kH^V z%@NGY_1H{}){F5Tu>XGp%tHWdA5~xl({8y74z4&NgL&r$BC(8kVL@TbrqPdDsE$G9 z7eX>^ZGrC@sYD9w8LQXTu~B)(Lfsl`LKoTT?hD8Z23vh~qhr#m1=)^d2!ekrWIRw| zSqJ*W+N!wJgkc1UAYFBlW?c;V!}K~&OyX&OfhxIf-Dtr)+TC!)6IYO4-n4Y>S)<7} z19aOz*Kf79Jf4j$2BtrC=JJvG(OJcgk6R~bU3p2moC5Mq9(=5}z=FP@%T_!4XzB}~ zeL?UVe#H6Ys+0U|4&`H5uV7WcY@P5>ahrX=WKq+72+BbZrCar49AgkLj+ZF>VS{Y? zHe+7fHzd3ZrM=GS>7oR3$OgL02B!g`Vte8d=Cww?ylP)CHT;cZD1m)Lph}yaw(!2f{NjGg8OC z9n+kliCc)@XRPd<3U}8pFxj9i+r|n&RQot>#~xKO0YR2V{c{i>EAd=hQsH& z*wsY^3U|fXR`$`s6+3qvxsYl6=OA~e%%M+BW`Gt-#i$jg+C z+LI!Av|FJR_qr|G0U>X6E%w8wE_8eXik5Qe_GOz-YuGb+gf{LjCeNowCwKNJ^W^*v zpO$m&Dx_^Cq#p(~Z?p(tpY|&ds{@D2lr)^lo7;XRQ+}j_XFeY#3!@)Aoaa_}sxBY- z3Mvybc?Jl2(!Om%iA>Uu3H<5{)9^hVIx+rsu*aL2Vzrxqt$n}hjCqD0X)<;A7w_St z>ACRy`4%DG@WqHWHq8q}-q{unzfv2)9Im&y4FQB%7!jMR#!I3uC560`>aH0GJ@0E` zMBAH=f8^cIDQ)yrV3Yy%pnJw@8T&m4%|yESa+OwaL%dHxa- zmP~)5pDa6vv;?kKMso|}*=ys^tbcY~Q;9t}ovv0i~_0f-@AKBe!h zGQJBt>(%c2SZveroUd3_w;RD3p`1IEv}&c|i-wv(D>Vhx+qW;ibbv&kP8Wqt+#7p% ziSj_oh+GxLrDL#NlMsJvg+s`MnmDALtUt4pI;h#l8x}Y!d&vdJ3H&S z5M5ZYYPZ20fGtW;@|c30L*>6LPStRb?Wkk;qOKv~_&A|&XudK!n(QT{#c!xGS`QDk zi$NARzLmENV2;C?FtHSb*3d)cx5g?;a!m|o%%Ieo`)GF{*8$y~!xhdP*pf-b$*!#Y z*)Qjsf9`e?(~O$d4DMS3$cSwiiL=Br5X^Dh`&MZ_t|72Mz0Z4eNm+E(NMfe4$e=aVas~F^Iqu?S+5$sH zvUzOk#<6du!6*YjeZky1z98EDWm)dJI2rigIgCV9)_dWd=|Y_zeL0E~`NR5?hk=N^ z+~p^)B9L*ogv%NM&_d_GlOZY7!Fu*tWr7-fPi(GE-qHDCV2mplH80?K0^i{4DY=1K z3AvI6#Qiz;Q!D4G=eWy>d%!#OMh%_shfSm6Zg(`^=U?(zB^2k~c9o~r1%F&G$AoXvhgdiCdRe$| zpCkXe=9 zM>%}3q$1!k=iy>}~TNdjNDdwcF} z%p-;eSC5;sjeD$DY=Mqak9bC5v2;39o~7FjMlK8mz%@Gl;}@LsF@2BHkVoC0CLED}z9)e16o;TL1uU3;A{twQ|=(wF`_cWVcm z1_@ntATNdlFl8|aNYcr(bEWc3Pm-Y~2Gkn;xm*3_IJP!Y=-{3w?h(T|Z7DrBY$)Du;yJH4T-U9Q#bIVppH6Ts4|VX^Y$hAnE2_{Am+K}$BS3w| zpC)A%#9+&aGWxVaLc32#;>C6Qc*75(W+;KebAl(v=IPk-Otp4zNk&ZYQ0m@+h3A?y z;szUm$+LIIp~QMv`x;&wXoo#O?>O-&Ut-i9*~ImVTSRy?Y${|pZfklttX)+v*Jn2p z7ro{Zq30`!LV;l?&D9I-LM^U9nk@8Gd6W?P)$nYnuvD1t-a5>=*yAw-I-Jpe{qHpn zn4cEPzM}VWq8ttu{E3TGn~}*-SVKJAb_`-l`>~y!`hmVAWOdE&8I&)_EtI++uNQyN9IM6nMNIZ;?ww>PIH$Jfm5$o+8ADfM7A4A^Lq@ zsQde`On~blAc%zojQk-tD#Nr*xlKosaGq}->dAJ8u8w$Kl*nmVe*Z~WEQsqsd+pdq zHP$ebh+4%C-_wVS+t9ciwR!qfWt68SX)9$P0zm?U6;T(F_jRfI`F>A1J7Dx0&{6;% zfG58wF%i|Xna+s6^IO?>EWNO&w!mQ}`}O@21K=N{elx#|>tdwK-k%3Rht_*9%l|Mn zfyR(w?=p4FB5*d*0bSK_DEQP@Uhh!Ueu;F$R`|e%lE)O{rg30XHfrHqWYc`LQqiX%BJUnE zq1p^?Gd%jYXw3s&&o6?3--$`B3IAZD469<@KgayW!ST|@%%+5Ky2@}9L=^dCDKK4Q z$9LX8Skz`H_^VZzS;(l|b>4fq%#Q$KM-kkYDq8B^Kabts%b{gNaFOd4CrxLUbDROJ zU;hF5(svqa7cL`B@(n@>#iUq6OguB!&h@3rUZH-2rXd}fu8os;-5T@c7`?`k(4KUD zg5`F%D}qA*sYZEBe?`l{=aVc0^|v!9YO{s46=^gB*j$?lcK4#!0y;eklhGK!?4QO} zHYqPaDf8RExSA$pTk7|wDRWNSLEEG1zjNK%0*C@1;3vD0>glJAB1h-Feh^YfPPSGe zPaXnth>5uTHG%S+<7jSa$8Ga-+GQrI*L-#D2jqVaL;|w59}jSEjy^5sHqW#>X8!fc z)pY()Ci$)uc70Q&EFeJv950&hjGNja+18W$pQou@MAvUwpu5L~h?njf3TZs- zrlm0wZJ~hRRF$g(G3tLBxz4zO?rHD8<#*+x^Y@VT+h)^=WiK;(CnBxr@{uMXgcV4@ zVhS^J^T+d{cj~=AXweDsAznOU_?t)RWb#Ja?8KTXTi1W*Qe& zKL@L}v%b4cqX^Ix&u#%ulI|IW!IrexF- zlc6-__#(7|5ix6$ISh7}WYMrLJniu6o5NcTVn5SmU9h-^MS85xf9KBMq|HCBnb;0q zwiF7whemt~*f|d%6Ym!!dWbys>|n9aIgUe{D1KmH)!$gr+#{W6G3c(s)b}gG#h5Vf zlERM30T)^_#aUkAur?X`a|+J|)JFyjK_^KgAg}6VrJac)izgwFhw2!K8{Ixed>E4= z#-V=OE(WxnvVtXW%It3G3aWzGQC(bUA8L+AhVlPxBDYCm8R@&{stRz(191>bbZkmX zii;G*A&@00sbePOS^bz4FdTVV3;>d$S=W74YhsBinE~?CR>=PO zPdjjQ3QT%QO0lDtg?AYXl4$blTh-H9h(>@2XYFgop1A3}F*IL#er2(zT<3JDzBS}6 zbXzADcD2sS zx8Z@v@ItPlkn}am825EthO7W=&)#cqI{T0-)BsNhk{-^$1A49inXZ@`Y}Ayl?f}G@ z@x=$0bNX#-+yV&5KH9Zo{X(0?YL5?nZ5uXBm7qM{-$8!rtmi?4ng^9`ZT zAPhMN>|cJ6T^dq?t5iB{O!98amVXr+j<|-dcrpCyZWuSNnu{985VCdBGWAcWW(_X^ z;-H&SE3bAk#-u^K|9D?iJP+CcA9=fb28>~Xy%rU(TyDTPd4l_OL8tpNYqA=qWXPK=WE&;-ZgsrmDq# zZ=dG~0oZf?|M*S!Jg2|>wwK~hTdgwILOl}b&Lnd@=e_Xvz8P;ablzD6&O2i&OhqFn z>3(K@s@X=>P7f;Tl91fX)@S)smg^4@3kbr`nb9~-!gG{!v(hO#gjEGM$z-71eX`Xw z2fAI72HN^oTgcm*TjHJh9E4|6JVMj$w%1l7mj%G$oHZkMaX8&VLu@%>QeIlByujQ5 zl#}*i+yr#zrKhe)IyT9w-yTj@FfZ$p19(x@KE)}|v^4vIR*_9+Uec_EhTL=FT1oGf zs_v!G(b?xE@sfHvt0YAe-fDX*W$XpzU^6Z^I|_W=TUn{9E6vZS`KK;Xr?V=TE@Lv0 z^CpqB%fI9$b$(gdseck0fI2w`(dpFxv2UyPZTjgb^x9L}A z;}blT<_N#nr_t9RZF-X}-u;E+Sc8I6CQt5{(PtjU>od|fl{z3I%K94iHbqha*Yz1j zE+Ub6Vh6G0a&@f;ZT{rRmCxfAGM%o7-E+HOiE^XeGKb$hk;nXL9zB-tx({t~Xj!f& z7qi5<%D2i@15M#fTcP688o=rkPJy!A9IC#Jn?Wz5@M>pX{36}nXxb)sHkR2i$Lv(n zx9}=t8L{JU^qDVpvCN2N+^v7#SbI$8>rP;)@NIB@uf$hl(%P-+aA?beh7v$WY&5Z) z(een_e!wrpxjvt7KWT2URT1$vW}p)kLWcb?*ttNFTBR_uHo!po8{wZqH=m-BXSx9W!Z#db!Gqor5 zBr)Dl{dbNB5KtB@l!WzD`xGtsi5HAaJ|=$<8CZ|+V(S5+u=-|bXTzlLU=ScE`ZJnU zscz_djT^@_rSD3=b!{y%gS_A-xbrMPVNfdqB9)ZLsr%DUCokiwZcdjwR)b#7w3GhI zF&&z-$7esEGk6KFAA2=w{;xjaIVSudbi=`P<0cEvo;@l8YS!J-h$X_K6BWo-885u+ z_COG6z4ZJzd82$^+aKcWgQAwN`X^MAdqP71r!$cr9u00h`8!BmE|vI~rxF5a>qWSD z&&Ls?!w+1%jT1e*f1X5h`G7{%=xt0xTo3mRkym<6IihH8gHhdod5*9gLdyJh; z*U0tqd<6g6v6(#TP~pVP%$F_I32$V`DJ)jiVQ zVpqWUkf~|4871=Gh4kF87-`*6b(f=whDnoswivvj?(Tt4+0Tc(^(Zr>rD0LdP;r&R z!o{`BhyE|ak-tW3j7Dw;h9Q$}`z5YOI5bZq;17+&EpE>7*)-$$2~Zg}X}7DLOeGWV z8-?sj8m0a_SHFduYI!e9eeeGg)4ldPF}=96tPMdhd1q$H#WySSEuOTXy-_g`}kvd{*I7o2V7cp+k`Illm6BK zynttQjJN`Vvjt<1PeV!SiRy9xL%~oW$1WjUM!awM=zV(MY7N8ubh`+dP z>1quP>kJz?-c@keOdHZ-RE1n%3;OJ*CDl>h{U%5`c=K>(G_P>VidA4Vi0wAs=cq^i z>W8}7$0mpNb6R z6F*peIuQljm~|RM<}}Yt=hmRFyQw^@{O?@FEB`C+o!=~legE`;es;E-&KxkG3w>r# zM_34uU*{Dth2`>!6#En^sBFcRp7*7FbpGI|_W8CgD+v_}`Y^t4o4qDC+n(BG)@k>v zM_?`+lP&c4L^&#F6ecHKn<;Q1y*Nk4V9` z!Z{h`I!|UK=b+mcg?|{J^I!k0a>Yo5x;~WE0f^P!^UXM$<@&)=?)=ub09|T0^4uhe zyF~5OTB7A*D^(>~e_a0~Ph6i$*b#}c)~A}JK0iz)y~U*eN(P3Qcf^omAE6$ksg^6V z^0NKm2&bv!@N1>O`A7?_=~vwyUOwYAZ+ze@MeC?(f8FozxZ0a91bAV3UbGCud6lrBXAZtHuJBurH9Df3VAKN~TQzsZsoh z3YpeFob>eI7y-A=z)l&@llVdt4}3=!@Pu&p9^yMjsNfO75S2TEKWwHP$Kn+E?SeFZPCyTrgfzve$3fsN8r_-ZLOe2?| zXVUE5fnqy?x|5om(qsKRo$k7i-%E(?G(eJNC`W!8gAIdlkn}{M!KroGzpF#isA+5( zN-s6G-A60WqZ+<@+7hdup9~0u7+81Dt<neBm$bH~iFB}%V@@Y~&?mPcAkpZLU0&h!?BciNDZWrp27Tg#0pj9Ym z-0S!nBInPS6|3}LTw(SQSdX#6ycd#oR=i!heHpme1l~+=1(MJW)%^Y_T5h1k zml!YFj6)!d+eer#%c}*i2srbVE*DCF?9i5x*{C5gw};5~)w${jQ`Amp(gP;-cjbPp z0eEOM%x-S$I3Y@;vUaSp`?}!I122sK=p0{do9!dl1&=B51cw0v=Y;4ZXX?5v?APYh zO1(pxXUB$h?=YhefIe7IGyPCa({5Ux&FG`V~#-kv4mUK;A z_1KXl5B2Q-1(S&~))a(dUUyFRm1p58GNsbtOb3GB@@KgHW)jyy)4T$!=BSZ@~Y{W z@^{U8xcK_i`X!lU`lKDGagOd4j`_n1P;F2B!H#F7xXyXD+Ey2W2SqJF$U-sMy_1^c zZf{i-f@>&RcofsJi~9=Oz8svMufI1@w@u|@6pU$eP<(FQ;@d8@ z@VrTfdH4_a;Sa2wPS(E>A3w=SQ(AYFX-0>?DB>Bru_m%n_k6xBqfOR2(>Ain{Q#FX zr0#C?S7K6OD@FPx%2`@iYJkJY+Ru3tRFLA)ehG;s>N$k}Jo~l5M1wJi2Tq{OTca!f z$+zxVIBUlY_?N4_q}z$4$X!W+=9iam1n619R?^UJccY@1P&iNNJ!k0ZDCNcr9~KIp zYpsC){pTa^lmSDUx9M%+&6OofzFAqDp6gGKEq_0qG@4bq*vQLcYF(J!?5yNJ$ zf@{P2IdZ6xA7EKKPEIqCHgwmu_kZUeMA&F7F83L#RW3R-&)>MzBxDe_=lnO}hkNPL z6{#5uV?A{`Olt;o9~5SYk8YV`U>9q@ppWzm4s>`}vdv>K2&nVedQ{)DaY{GqfLE z(<>SLNm zUPIL-G|OL~&IU&__8MiPFqhxbEPAT18@x=DNAu!5ySreJ!!ODEr>`pEaay{&mh zNMlJHOkosT#MWL?*d@KCVVpjBe@oC=R${jFP&R?3yC1;I6y@cVMj(~EX*74<*;tuG zzu+%wN|8}m@e%i0sSq-6rwYLWL*D7oy$aHqySimYXIyy9hxp@6qkGg^nw%MUO%(WpOMqbTn3ZjW z=l67s`ewl^BdogeXy;Pk+*8Kx5IJe8z6dI>AGlc`^C7sT<5eJZOy7QNRY|&;SI;5w zyN;w0K$)w^-`dl?KFj zwwb8_L`#Tl6j&$-ZF1nJW$X$74}Di9f8q-treh2WcN|{fsu%CCwF|HOcWz2$4bc?K zb3e88a37-n2KfM=;BcZ@&Y4&5eI^kW-DdIYEauFDZgM(K%qsl5+!VlVQ^ehTO!>1@ z1h1-I{s-RP8HHfngjNsGtDDN0TAlgSud5#i z?emEmzV9RPqpP+7eZT6~;=hbj=55p6YNQ%n$B?D^uuGTu#ofSib;C~K4k53`X=gF? z4WU?F`*qjPM#(`^6H0~}4(WV-Gj|mBIKWx4V|V5#NcIzu?mK|Z-L3t!_GW&PcfKniCtLj7h+iJ1a?`>)k3vj@HI2pE{e9-P ze>ub?uRrASTklQ5QQA7+KfQekY5f67tBCDL&5C>`O3z#KVn%Zik9u@%% zU0A)8HbV5zQ;l#5mUSSwV>*Z>wYn;w5+*zmqo3bRPajp%$z2bUkaVeJ!4R(yDChUh zvsM!>$KeOS+|WuWro{5c%|LadVv7RoI?O^FPzV2gAHbwvT=wsd#mzydGkL+4Bn(A{ z&BPzqv2cEKhHumjv?pf}k+`dW$(tvbgMR*4AZx9H#d9*%A(zd3N8m?(A{<&5CQVA8 zrR0a+f|4*W=dIYxi%5HS=rd!|`kNMJiZZ9z=w+Ut;lJMFpZ!p1Im`bNm>h5oDa!M$ zCO}z~^C~1Mogiwnww*oD&CCr_8@*HZ_#33F^2e44iZcDo*wT(CdHs_hWnH zzVLovAJogmdt)Y_Ei?u(hSN-hLfk^ck$5Yavu_^p2-7S6c{0U?N_*FVK$3p6eS%DY zscPB@iB4!nd?d6X-ca`stLC)X4*?%`6jc_a zEn%3RRgrScrpSReI-Ig^NCMj%9s9hkm3$30MAVRau3WfA%xIzmLv@-K>LJKaD3pPp zq-!5gnM0qHz^Ce{4Fs{ZcsTgMO>YhvtDag`P2uJY&tqi%CX5cL6(nv7Id80|!$X@HbhfN|-vL39KfMrS+93@@(0AW4e>RJzdp5S^ zc;9HxrW3~P*RmP_g7bfvk_k3g2RQD zMN^uJYwPvZni29?tWlK6fCVZHNRak^{1=}>|ARsPN-?4wCOF&>)O~j|SKm4nyeURE z@8$_inA)`wGkYetq~6E1-q4OxC=(tKSDMp~shF1ggmy$2c+f1*>hU_Fcvpv{^b!-q z>2U105qF7HC3LFVRz7(8-9)5|^P2N`69532ZB3W;9t*I`cn+GPVq;%%hW1tweB=X= zS~-KV`Fc$OG}rF@*=A2^DzS1#CO!<_X2Xl@vl9}#v*mdLUe)NI+Y-r7^zi)ov4EcJ$J(M!C}@XL;-@=+v5e;b zYaC#koz4@@3*V&WU}~s3{LS3G@wZIdbuw8gVW$-okNhXJee3o2wKYwc764O(l9-cR zldYJm`X@b9Bkd3R_MH!Sw~&@0t-V5e}Gm&&7{?z zcc zI@JlEq&Y!$v*;SeHdTnt{k5Fhjygw)wdjS{vRv+Di1XH4S|;&g(EwPonr87)s3RxJ zty`nGeWsOLB}=kcOaH)Twi<*m9~F04&)790K0;h*5_q$yXJ3eL z=&=un&QCH^@$ZbSRyW8_G`FWm;P^LEB|y7u^Fm@Br@u%_psLpIETb5LAYjRdVsC%j zy&);B51-NH4s|5L6fKo_hZtM0#rRmx*o+g-1AgkQ8z`ng>_fLm)`9LJdldktYpyQa zGT-Rv%K_XQeBr_4mUUTRfwQtNu^FDEqoeX9hZ;_Bf?_(M?JP(W9Jg3Ct8leSbgfuH z+ZJb!XA{)<0>R|5p5a_Iu^T8 zlqwSa=lIX!e9(E6)hgN6^8?^*=G`EEt*obL;JhyOjeUODDO$Sy!MpSAloHCgb$Y~t z2=8SHB$ViL*OR4J=XzmzXp4VLB*cVZb}!0Wm}2dH--9X2^%mEWXmh{yc)(4}cd#gP zk)a0PJE+M`knR-JS$1zmLT;*K$lySKV{h`bUDP|NJsG%nI#WS$hb>*?y-pV^k~t6N zti0doT$ud)d8g`Sn`owcC_8Zab5wDm5uYnx!zp80!`BV6yZy4p&s*u3sN(o1GbyUY zD~S(u-^`RgllCoj51YG*v&6B}#%GYLb6Q%%^y(ojWMhX?VBdo=CQ?*9%@l@!PLlivszjGqGt8he>+S31* zDE9WXp7Vv9Fo;PGDg1?5arbqIBmWVducq^ih>W5tbN7!s-l^3<1PqRkV=VM#VP9wF zXd$r(AW+5%<^{vM_sa=w@V zpaj8LoI)q0)z>jfBB8rgIZH%rs_ckz_h2cvV+KrJ#R8#IEj*(bgZTqa=uIHSLw(&%m6C&{6t6mDC!O2O=L7q4Hc zZmT99|J>NP&_Pvylg|tjFaZoL<14fbqknX`$E)WmaYfeu;=dPicbnvqCip{uj(>(xO*0<7anTQ`v&waJ!&4UYfX#r@>2S+< z&&1OrEd0x*sl6F}AHWNdx=sazI7_O5(cg{QFPw^B@*G(`A;=#<9oZ>#MjoGh)+?{= z!Eq`V)fkm#L{^uV>=@X1IYj(X*VSR71Xk9UDXw&!`0mW+@nd5JczMW%G>G|JVxn+T zq_WfbtJ?Kr$wna2@LofdXX6kb7$ZxA%Z^vl< z6YA*O7scJ|sIKU6;TeS5+awpbd3&POUo;roHNwXIOG3g_;i~b@9Q3oG%0Gv{pQ{2( zwj{b^vp@eMyf}Jx#X!}RH{Wjqn7~%apR8}3lJ_sy8UW0ei8tJyzQGHH=**nX&}*RQ zYoZ74YZ-CooLYZq8&h5nnV|{A`R;UJ69VfwWpVeLv0H^ko0UCR#?>&_7Z)Am>?61y zQB%9orabi-QqTEMnp}#j?`kt*>4{tKcGti$WhTa9LbCHrfWc)o;hAG!FD{o(dsP49 zE!@URf%rD{o-y;89PCMkE1_AXDBEI4m`XDd;ra*$0LJK+6$E_MTN9OY;@zetXQizF z@3-XPc~nJB;=E~W&^eKI{JW3&Cuw(@S1a}CzVJ1MAOo2J`bUgRrE7LJrj;|z^(p|t zo6Eg?_5l+i!$fz-&J6&*qu`r|koKDEhGrD<;9@1yTrW>%yE4c>TG1RsY z5}89KVN;8%1*nEQjINsAC+c+W7Xw&oILRtm3=;#~(*!Q#UQX{X$bvu+x2Xe8Ypaol zqS7Vh+@uXLW?*uo&k@`7tvRf}qpB*}`j(mh%>$>ue8cw3n;ceA{b8jk`Y31Fz7x(9QqTrOEO2-o|LMoh$dn)O-(Tg{8Ik zSywQ}s080ao4XGk-_?#;W>RW7TYymds>*hSw7ahSVCWUhC55CxpF7xS)7C{rCBfAX z(xBtpq_%B6SxAAkzNOVu_`|cy*Qa%aU-vj~nJ*C&Z;j_*Z{|H!xBde%dA%B{gq@h| z+h%3DE=7=$BCqr^303FE|8@+eUS`N~*A^=2D!&m44!DrU4KR_9!bhf)9SAB8W4UUB z1~4d^sKuD{bW(ivxCY}6Z=f#Y48^Tv+jYtrk6V2V{RBJmQ4oox++**Ic!ScFeEdt4 zTx-#_l`7dO6Ptkxct&c%R>C%7hn*uFFKr{uVtkt23j(J3ROWdE4}nslQ-Yk*ufIG% z#k6`@*9Cb~kDjaZx1V-}gLYw@@X@+5M{84eVd1dNvKGFb z%yTj}7l0;mMjIneR3uXQai=-$tI`p3G*l+N-zKZmAr|q#$TX~mwv5oKAS=&=4FAMa zlUmv6-Zkx@v^P0~kMhJncCrnNn32a5Q!ST+@iT9W+rDdk@D_!^VZ~`X(|)fd$tuy- zJ5%7)b>7%c-<8O`7E@jLhtGp4^Yw@ad9**%l9~F<#RS2{$d9`X>h)Xlzn%xgF#v;;6mpOn&V7OMb<8o|fQRh?j=fC*_WH z)4Ukn5#vMX0Mi8HLfOMr&sDC~N6%gnyZq|-21Dz%+*euBnDC6b&o_t^a_-+E+00-V zN9)b<525lIWO6$cxWr!QLFr$q+s%49NVnT^mwKZLR>4ASh4mKIb~_w;lTQ0la+Y*a zxph-|w>j6>P7J5E!wYdoJ346o{-4}HB+5Qbrr*SR+-9>Nb1Fn9Nak=BRwSQQfOfMS z-E)UAej#zBk~^I2LmO7&-4~jI-TCe{ORUG;nON+16eAj?R1(mxM;i^b2_~vG^V9L_ zbABSD%HrY63(ed&GSW5WDEy7fY3pro%Ybw+Ij0}5&Q}jmO-7Wfno@0ie&CR0K-X$c z2T7`VtImyU7LmmOU*d)pj8?04Ghrv#bAD}LwoQl=7D7Hg-^$>NH`yNBCR;WuJkI{Z zgKQPxwL=zP^yrzI)^$Jz^f~~#bU@-ByYxR-h5GV2HdB)<=hR+CQ_-Z8x;t^l7R;N%@W8t-f}0J+Y7 z9?{3u@EtD2%P4-+ib&nz;|Et1YPLv)-pfz>O^s?T=>0I6zi2-o&ufPytXp-zRci-C z5Q1`kQz3-OyaWak`W^CGz@cgbL#Gm<4;+`{WpRMu%DE5hdC{2A9sZQEs8DpiHblU3 z)d^}XvW~oV^V_j+EW>_bIRh;{;f-@(@z)f)bwr0Dv$W2$-e!OM7LH(0n6*)ATZ!+> zK5w3Mjbm%7Yo!hr1G{U_9l<2c90M(gW_^kHx7DUMq-{6V1 zQq!+yQqSCubCi=OzJF@c7VyVEJ)oKA$WM1@VuO@_-|YI`PT_Z^R-`|hF zS(Ni9CUG#MFgG>$C$Hz<%_1L;YWArGp$4jzsc=zZvd$`b>Ky1M!_tTZ-P8CNRqyt4 zt@`#!Qm?ok-GXs`d-w7!;FM6BUIyG%gqfuyi-n#`oFP{o zZeQ1%2Qn|}APrXQcms)Vos(%!tAtttRd>qHb)iAeq%5WhJDe1{!?VX%M4M=JY)eVG z!5hrO{&O}$iQDf6S7-o7ng@)$5i?>Uu3hOV3kWv2>g+K1$w#C%rJy394KDLdg5?jl zy?;{j^_@(mZK`NX%j>IG;i$Aj8{D)0mdEXzBD%v?sZk~@^BKpCN%&As+>wte{JyN% zHx#+hQo>IYRM>@$P;wwT^Kpi;d2h|FKPvSR;^Z1>G#}B|hL+Q{6c@Td@6`TD7?Zg* zz-MskysB#Bc1{{|Wz?8GP3)hb?@4yKEv%Ey#v{F3efQtFpWE@hU;jHdeA+dqH!LG? zKFnBGnewN%BtX9LNXSun)-A7RBo|AK02-0IKOQ)>eI&h3^I(fC%h#2tFsWAEvJqvD z_TcR=R^Nis9Il2+zVsY3$5>`mK^MX0ZZ{t{q&2l;=?iswTN0exExv6W)jTnd@*sV?!Dq5_ zbPcgc8#>JSB3kD;n@}DXx=ovj4KYO}E`jZ|=NfQ#a-<_ow=?&rjb@tSVKu4%B5* z+}&@!A2IGxY1tyxwHl>0eqVgB(lN;FmhQi#o~r5>Ba5 z$r6wMh@WUqw9fRX=j^TtKK`(&VgDyat2`vhCt#Zgn}pOe+L1^}cD{Hk3~D3wJtUJx zBuJ!<@Vy3^`M)}*7+_M>aB#8pXJ7Qg9FXi)2mrk){cDHgJ6}4%0zH43XWt%taw_Lm za&?U(6;8Zq2EU_rJ20S-a&zT%YD zJ)x9{n~se7d{nJvyarMLdn7sgYc_0@utb!KKZcjsKI4f7_H^6vwFP%35Vnz?ecq%Pg10O@8E#NvQ~l zN9>hQ44R8i%TmR5k0dXw%iB*i%TZcJ9X!8^rd6jAu}uVz z-Lj&jKZd_m`g{8@>blx%U&I>({Hy-}&8NV4&(~cqO3h27T-)<|Ng2jjaCJN*?bFq! zzg+O2r92P8*8M#tzvgYses$tsQh2wC{{V{ne=6-PGmHroy&!TC%l`nH`WRJ3#ZLv8 zw{MCG1e+X_x-zA9;3tC9c#~xvT6{ERA+?Jhm@z^k<@tuLIW-&gj6}0{FAs*NNP;qv ziu+O%gQvKLqOVU!sHcTR$_uAjwHD1AALS!?EpCL8aanZtEq;CDf7#cA_>DGxllV{i zdrz|PpRGJYqii~l;>Y^GsHpX+Hj1XYdTa5I9Qtp?JO;OyT{2&5AE)?djPyP$M=BGq zx;;cuFIO0+b$sJ(J!SbL9}{9+zTHDu#I$f*ThuZT6p;^AYORK{ETmE47e6f*#jl8e zD1jxb+vcJ%VGKnP9b8m$_^D@^QNYBcL70sa*)M2DISfIVN<34;-BXi3k{8@Ap%O2a zmR^JVhJ++2LOFa@aLWu%HWG5jsSCqS8FJM`6!jqp)MB|<8AtV+M1?hTDOSsja8A+5 zy*#`;R4&#Gge3t&MenG2%qMV&ly!SoISEkTV2wa0G|2p38m7f`9k*Hr6G+BfSH)7& z$T8%EGa}%e!%cSpgrYGaR6^tM(#sENIF6~sIP+6gX3(HagvAI)KZL16FbZSMn&(%Z ze2*Cr%c?wORkwi$y#D|W1L3IJx)X>cm(~yP_tcjIEJv{_#mX+$d~vxhuGfe938Vl{%?J-K=SgdWqfQ5(y$@;;i>V+5vF_B3w*qZmHLDIgraA{f_dHH=+y;KjJOCfy@0^mvO%(a1)m?b6)WX6cj;`vN+uRqvf+FTp9{L$~EXW1C%>;%FYsK@O4Yd3rHXuJm9z>}Mm zbCQf|GK9DdKePmVf-VS@NVwFr z`-2^!N;*Y2Q)s`mJGBiHM=wu*vZrqnPq-a!gf|&4|i7!lt(F7 z#@Us2opy-SVNz0EI(xp#nu(>yuwl!247GJsqP9zNy`mA*7|K2>waX$IgVp;N^HnHpXPIzbks6LQfsRr<)e2dE zjcn4)wjp#FOO+LBQK_ofLLv2^VNztM+R0wg#Z#46yS|Brg(x8?mhE*)t`$(sN@|0W zk2jW@E_%p9f@wI8oYV5u{%iLF?Fur~-|*FBth!F%{ly&|OFC;!S+!uxaDUcAfhae?7d-l_Ied6rSZgiFFARoW&gyNWU-Fcl#V4CBMIoqp z_rcPvQo3$U{bx7ttXfpl-eckdB19x9oOO9>qFcKYW#qxcoDm{H_VY%!r}q9%|`D^fR7LdlD@xr-BwjCDmSqHS{r#?M#As#@&)?@{M)ahHMxn%w@bCG^g?j zXLZIKfk`BbiG9^OxG8bwSd-I9rp0%k7Og{5EoQWe>jd_9ks%e!S2mqhR@-rMY}nj3 zf?Cs$2>7d>`>su-*0UjSL_~EBX3g?mIJSnJi)h=U|w{GUc?0dvoy)x+{ zE@{dmsZzez=xVm{CZEz{;!A&~`iT8Y{{Z1XEq0ihan}XDo&yp7D*gMVmSt1$KDTvE z4p#7nBriy0MWk!dwfm1UtnDf}7V{S;m&2J-iO9=*5J*Id0J9O!Ls~ZCFX&gemhwBj z3&ccuW#Ow+@8Cz zxRDcL%PvJoi3QGL5%E;zxG`L;dR_;NH)C!rTew6L%uA@MwB$!)fbE0AA?pQ3N`ghx zL@o6zCWCVU*}l&4ExxqvI}*X=j9WLlZtz?oigR+03dNH(HP0STec1h`!`AKGQInZ zc?uC4tZ~FSk!brlsr}$U@s?y-t3F!N1I8;9trK$C0}<3e<4N=houO(z2t3)-ZAsDw zUJIpk`A3Z_E=!R>7?&{?R5Q1roz>1Bt76z5P`? zhQDEuRCNgtzLYa6>QIGa%iZFoio*bf!V~8EspQ?Fo&}LbCI0}GOek%*s0uLTFuTM_5 zQ(0+KT$#41i^`(jPnb5N^owi@*N$6TUown|F6GVgWM1#fU(L8DfoZXzf<*|C)7)|^oMMj5jN%^69MHR9aLha8Z7>iO_d=67n>m=6_)U8JT{Z& zsS%QRWgjZ4G~7Q|7E4ZJAFfk*72zQeMErajpg#Ze5nfcmq`yFPDqHDBrhD~^cp z73JxtIx>$H$YK{Hg)To04-6Z41c(G;BlxJ#z&$h*1%VWuG4n7_yEcz?i0M zA@0dUMv~oOFJxQFbj$^;1#C-Z5h(Uzr*QX(QR*-?vSln;P)MLscIkyd5Q|xpJv}S5mu0#I*ka zH47e+A99WLxQlWg5zpnSj_ggTHQ3C!0f|Yznpowj(eE>^o7Ky!yPCG9#I0`C39YQR-XxnP5+RVsmXxMTs9P^-rHAb$yOQ2ELlVnK^H(iQc!Q}a?Kg0o zd6x+;B1JA0!0Nn7c-eG1oHpr`%1wF!62?-jYaj8F+kEgJQ+| zO^iLQ@d60<4=rOj@Wab)rq!rKd43v{Ty-&Wz>Bs4QRtQ4;lK_LX5~b<}wLh{+WY{oH7X#2_rhl(QWCLsYNbb?pF1iAO?}6Dl~h z!jT~Rs->Bi5f0w2?iH3FB)4$dBdf(u6|%WP&s2swMz6KB4`AEE=H7fY49u;dSRz5* zBugZYnrXX`2ShTGd{pJW18~GcAAXuJsLBGFbn>XtjoVHDVsWu44WW@)HNuJjil(6T#YP05F3WjiHl5&5WH z%b^X(a^cISRZBT=AueFzr@pW39FNl?y8o))Da!!({}8e zgjxi1FD*;sh?QjXta9ViubjSdNpo<%TAF)wI?*<)8SUB0?74wCx~rl$8s^2PEaD=C z>>WN48YS6uGWTRxxVI5*UM#t4(b?MFq+Y=gpDVrq=m`I68hvKIXZVbKR7FE4X^p7!drn}n1X~lISdgNPgP@!ew8mU%I z40F$e-FSUj66Mq7uZm~C40`_naS=F&t3QNGB&{ZWWKgO|A|=Y_FAA7n;o2CP;9)ID zNLo~NR5sja&|`{3Q%dI=(j{^IK|6#zc~w31@yA#c@GQ`@@Tq<6Xbu8ghnTZYs%zVh zAV%ZOTeeO(;PB!_Q{})!H1Km**sIq!2IPu8HJh*IJ608RCbzyPo)+6KZx0&IBOIhw zQuv;j&*kN(mO_{%Y`y(lwD#p=JYbYu5mY6riCwo8!6R;x4Q#sTXDS%ItDF7REs}6d zsVH0s@hYXqA}}0DBdBV(dhG$krjSy~K3?j^c7RHRA@+HD>rXYYxjx}4$fDTGIvw-U z6q^pb+PL^?S(ZI(>*7X3VB9?z;49%6pfed(;^_@2U zCZHlG1!h#%Dllx|LPBXIb1I&9!1w^SVme#z@pRU#s>(gUxdQI?l|!vI#$|^Y6>bEC zavyJ&uxxSKZoCxkr4mFO%lB3hk0z_Zk9;G%@sV?W8tA6`bv0JnV&Y7R2^3e4?yiJo zW+lE%Y(ch4LON5HyD26nIk%zD;zGvlS(1{k<*w>!ZqqxqS22i0Ni!n%YU!?}M(>g; zPF~@Qa^Y^uL|rn|rnIz?p4>)lw21E9?jIFHEz3+}SQ~cW^nq#Rj|!LgA99mej;P0k zbd^KHHig4&HE{TgktnG3do@=(ODgCK8Mda~q**CTB=YqYaDWO;`=&Sgv$ZeO=#kcWaNmNlZOEZmov zY(!<%(p9R(`8nQq5*F#SPs>v?8qz$%vv#aHI4Y*}P zc|HvNG+2{nTP1J!WMdpR&8RaCtA=p-JF#c8(ALh{hs9Qmvv) zTUIF(WMts*UR8{`WlV|SMY|T%oN$L%GHs+bXW+(1wM|@^^SH(_CB- zI`E=LfVSBWi<{p|E0Mb;I=nv?D}qMF&aF|G;iVJkYu?(ORtL1i++^fbd1K#Ijy~f* z-0${JZ|*ExPwj-M6hW6Y5{lZvqPs!r`ybi^H%>Gp;EAsdKh0d7S4~H(qSULdP;_*C zWp0kQnL=@uJr@hct#JWF7X=DGioXE*tLRR%!kMe|OPwF}Df>^;{{UC?ekDz5brWiZ z?b*jjt$uFli&*<>?gWIR!%agj-3t_AQdIm^FsnO2QI2c8s%5g@AQVPF;X^v(U>7Wg zUHvsZs!Ywt>E+MNRit9;sQp3_)7_;w{8X)q*oke!khq1Xig(g%gKwzl+;w#K*1K0g zxZ;ADY2(xEtm7%j27(fCE*w*uv{u`wTq08xLC-%FdT5^FbR1?w%~JZYAdtj)a@9s( z29c;DEs&`Ui{_&Xr~pywtg_2PLO2h)v&7I1LNLyh@&jos#+0JF0Q|8jRZIZu$;ZE@ zmjUh>B10-x#X!RC89PWVk-Pr@={jKl0IwsRnB#(eic9B)s_E%lr;5w?m1$3MsT*;V z=8d-4pVIlYSM81y+Lvb-1+9zVmzMQ)r&4*?9=EY!mu;LGXu)KP} zjRMR7OM4P7Vj_%2ysxSJH|am7yg%`e(kpH{`~$^2bH_UGEAbXRn`=8F{V14j0(Y2? z(<@=EgLsV-&tU$I1XYR(A9RbuUjfs7S@gS9TX<^!0GIto-Oqyfvo0~!wa$lP`wjgi z7*mY3v|mfTmwZr#ToT!JaAEBg)GI3C>i(ztSEy_g!}RxiPKq8A*Yx!A{8Kdl04emv zn|Ey4w$0_n`S5KK=Y(>ZM~O$LKMi~pNmi)Ri;AB5;|fiu_f*a}n;$rSmJO=D*K#(P12|8$npGQr<388pWzt%PsO&27`VJK zdlI>JJf*xMhuvS+u^V(hn%<^OsS`U5C3zB%1NWQFhgsMv_h|BW}CdldV`|a5SBxMfb8E2}TraF}{ zt;x5C-fqN%bDzzorXw{cJXGAnZyO;w5zocE_S0NrLeZ|;mzlT{)I$5nCFEJ; zY1`g9*yh-jW0^#<9_q@bx*G6kF@11MdOF-3RD4-Vr?*iqr$e)NPCc|&VTa&stJehL zBW;g2*`{79&SMjJ_&(M85p2fP_J6W?<&N0eINOJO4mR6ulh70hkt&Z&y|wu#_Ko0P zA>#d28l5s<6ZEg6d}E^MJU*>HqH&zAdqy3$n>~x)E*WEp0=%59mYFYwJXQHmS@hqH zcy$)5UW+P0_q{KRcyC!#3RP8@)iP8b3*W<*yiRmIU}R8=3YVIvFG&x$?;J-`=3ZJh z!h*mi+C>DHDqh~<<*mYs4*`x|7}8xy$^aKU*$;+Pr*4H}IXGL?DSz`$H5Mz4gUFPn zEk6|*dJ#N~LZXTxN;oV+f`qw#YCmuf)qusn!$j_4cLO3#Dl7hVZHx>d=E+2I@1=a0 zOdyP;zTO(DVXZ|U!ffpjWwMvfGD;OJ@#3S$W(rrRJLe?mQE@@S5fZ7hO zLKWrZDld~`B-_w!;*v!LbMtr6Gl=nkNRZ1hFZWTM`6?q3@h763&X zB_tm1T8+@#OXNa1^CfE$b+o8}C69j^i?Zn@uK?syl$VbRh5L*^Whj+ey_Uf}w+L4; zSoeR0HD;CmP#;fTa;L1x6Ugb!r@oVJ#>4i4h15q;l$0gZI(@Zr%OEJodAq75c7Osp zvy}>lf{>S2buV}XI!aOF-&eJL#&f7WJioHDRx1%pOtPztT%oc=SpLt7vd4BJ9y*b9 z*3g;iMq5NWOHa*HEZJBsa4s8?>h^wW6;%sHGOuk!NQjvfE}u8UL^k8CTQxH8nHZ=z zG4XhYts5oG0VazyL4I0G8u5LsNA=LEULDW zzo$sIWIBkthe=iL=Mf|O2pq(%V>p%0sd7~Gd-UVDPG#;nokmGB{Vh{$PKq;%P` zH?X_wG7xZ{&R%X2uR5mIw4JGw9X<_w!3q%RO`tdL0%QDImy&7nj{wev=v6+MBG2MJ4U#t!}V$zA#Cd z338{rV@k^U!5+~LoROl;qs!u`gl|Fq;YvhAUK+RU)IgJZd+9DYY8McI^6*&rt3_3A zKvL5$H6_}k$5BY343eXrWPaha?>%Ko&!iF%l8{78KW%Ev%(jl;#=}J%)fm(>v@L_Q zBh){AIF_D35)l&NRNR!kq7otbs#TYfS!5kN6np%`S1oP_xNex2FLgY<2^RD0sh@xY zc)Vt$URp}b+DUQ1&JdJDd_2A?dc=_{xWUC7d8fBzT-wL(td&+yl#B2UjwR^#0O=G{ z%3U?3mP=I2E1eyy;c&`4+3EfoiOG05n?h8Ol$~A+`)R8Bk+ygWB_3hZMr|4s?h-(W zd;b9GLujGE=vk(hIQ(7JOF1?o#zqqtIQvJYpDT%0#B?DPl$B-1Ra;qYBE(zCEO_R; z)t2PcF~?CS@5_N!vGP!hB8z05^h{#J{lXqui2OxFGQR@*2uHg`ODkibhsjWpNp)1j zKbH1gG zX{<{eyVga!B~lV!HCi?;W2gPW+E|M%i8$X9Ej}vQcXCT(8bF_Ccv94z@uZXTXWUSF z*VV*iIS#6|$U91oTNdsyPHN~zUsD!F%R;l(NASmezV=pS|(klQ`$%^wAiGFn6($#;j8XaS+WVjj;95AdAuV;DL=4;Tp{|d zVUUbyUPAIlecmdZq{q91Nt?+KfxQwD&&*Uj;>!Us zj)kUfn;}e#n~21!B3fGVLu5$X=&K?EPh_f{nT2U2ZRFLga`RQwoL5p>30rfkhs{G` zx=RxFFfp=BY(Ue5Akp3!GSZ0Y11})hRLVq`gw1}sWZ-AR|BBESB#-%3~X<@RF z5az3^F8I=ARW3LyU5^CG6l*0nr^!m9)HE}*T_ZmZPHEe9pyt38eu3C)H zriqIk4i|1h*vy9_=B{1RX%lt1y<=)5=I}`fd%1dQCGjP)%e2Tz!}@Jz`oW01&ml73p-AA>d=g|Zoc4O40nlIRPO<`L=U zsj~12F(C?!gmp?(v1wwD&yUkW;EvU7q=04^OS+5OtYiIotMm_`p}!GS=lX^(^t)U0 zpPTsb@17#lnaN^ZRr`-mwN(5Ut83IbTZV{DNgGlbWLl$Mg6?fkHm4;qB5+WgY>3E7 zd^M|XIm)ENl8}g}id5GSop=OXXheyTBLv_@Ak#JwS$#m!^YPZbDeo=%?!$Ay-W$u4|9 z@}rGKyNEjM7i=yp-bXFoT#!auy`RfdjmFz}JtJd#TpMu)?;aEtb0R5msH}^QGHpew zWprcLHg9fh@dkxT;9LbABs|smf7GAZ-$&wEbsZMR{Jz#dMDS0otMpQ|ddq2G4)j!o&4 zlrrU^IY{k{w3iyZFRexYb}D`#cM zs(Sze)LgmzwGXDIoD4}JjCyKaao};u5o+q?tlv_^Bi))(m2YScl9aNS#Z$RJc!28O zXW^|xAp#z%mP}W;V6K6Xui>e0xP62}AEX8R-aI6se98#9(ge^);?rN!{{Y#K{{YiT{Kw+|0NP9c0Q%B|PvleEKSC}oakKrZ zS=rlMWp8tJ=;YaX*gyMMZ}y8qoqwhJh?UD7%6Q*JR%(AxQpS6-KI;7?cTZUTAF!*B z)(Xpi@zw|r5a<5@>0!s^ufaZ)PdmmIoO6SBe<}O7RQPnx{{Z~%K8GV25f8VI%U?Fi z$nBZXyOsyf{{X$#Gr5WyQ4%Qk)k3fzEB=`PbiRXFtD^Ft{{X}m1NqCp@YmY^0B89x z)f9isct21|$AR@x#>xKx@E?nJ<-EE)v-e6rxNG{ZF;n@q=O!XyyN8?%+g?1nVnbJr z%w3uo>9C@C;txk6`HT6e+NEW4yV%{3+O48^TiN^$>g>;@n{p!fswLNvV^Ui~(J;Qj z`);~z8?)@U8+BR~IWuBM{4%NYl-~wz4V*Wyp4P5~mn{1~#oifZA+s`CQCV>#$)C)3 z`zQ1@`a)wvzAcMO28tsrO6VC}zUfM{^Ou-X)|Nb-b{F&u?a9aYFE@oU+GqWv^?;fe zn@vkrV%0HYv9bLh_{XD=;@zZK+dcmPEt@iRBjH^`Q7+trE=K+b^by&cHr@Ss+70W_ z%XU_{d{gsLx_K#agIBZPpl@b7BejohLwixR7jHgUh_i740nVl}EwO{&Ha5$&jg8oQ z&ds)0wauZi!LbXi<@LC?ZZ{6;j6EU~5c12YsWLM)!s`ZJ&-QbRY_8Waw3hbo;;O`r zx3LTEQXz>Hd{yT7uY}R@UWNKsP>KOalU)mbBl_?gsll7L&V-H|>z8X9BT*$XC0fBgOQIcd;;eYhI z#~HiS^*`Is3C8wfuw$&aC`7@5N8*ihR{qTXq@vpuTl~jWAFaFwqV9VVt~FvsrOx5% zuM)4(A5>CoXQw?He^U4x@mWI)%U4q^>T2mCuM?>7PZsFf)#$SS0F?CT_5BTXy(+6e z<{VWwk`w9u)xy;3Qf*b2`Ht0PmC!QN$h=0g$1Pw#aZU6cMc!Q0vAAM^<|~>KeYHKH z2$0H6mrqq%>&Q>sB0&*ZXO~v46;q1oBK0HSD5BDL_f-DHe&NU5Tvw`v?bM8*d2U+Z zE0_4v3uPe@is>E;8heyj$PyPbAucJ?L}fY`#({AWkf7Bv;mJQ>M^AsWYL%5r`wP{! zUZ>m35vW;MZ3*X*84R->IZ|o#CHMby#k&_)<w;h1YNo6mVp{Ldg;36U&MJ4#DBxeifMFsNCQHsBPO-}$&v-KjXhKUPX!%5e*g%j81jIfkeiM(<7q@YZXtuvnrwMylJj z0dN(KHcWIUxG0QCIjVB`737X&C6u^SGj1-|9kD_QPmK|ia!U^_T+v#%l~vF&7bxoH ztKE&s5eXModD6#qpm&m`^}NbOo^s{xqc+OM)SlcKH$Xz-h^}81N^aIoWttSs#Dqn% zi)$WkUYhAy-C|mHoc<|r;I?GRM1P99l5E12R$UH95Nw=tcP^f)>_*x~(~;VXi(64U zi-!<=Mb-0HT2F1%<*OL3hn8)ZXv1r8w)QsSaUDM4Uln^4vBuDIK2!QmGZ5Pxdi+*c zk&b*@ZNBQ-dq}moDBfdNmkpzjUOD)`tyGgnA@?ohdUJWmo#xGx!XPE6Mb24& zQLcAu@Ka)UTRtvCH-a*bs^@smLRnW6uff^wBFj+a{dj98i?yWYt=%TLMlqCYSjQV2 z&5+R*Eh*j0Q|{2OX!4Ojyfjkou_7QL{_nhM-*;$4jhuAIv#6L{Z6lE58AQ9fYVuWE z2n|w~hs9Ac$^wv%t|+v-sq5T5WMbkdqt#HYWwa?*9z&d^tKp=sxoAMy$JQX$lDn={ zYoLX}mmY65CTxiWNOX*UHAVL5JY-44W&BlYqh_#(hb zs>!tHeJSS7QWfz^wZ&gaG>6Mmq}e=M(sQ>mRa6|u{{V{4razRbs70JfrI)NRN|6B&O+Q!$GZqzOocGaB2sL5R~aB=&deq!H*X zL#kXU_covqjJ_z+W~idd0s5H3O(oTDA)`R)sI=rg^=mAm(japAr7Wtk`4aLwghQ5< zT{>e`IQ~&YK+Q41JQ)BwW;?nY5EJ3XEkQtm(5h zT8+68Xb~hV$vEocDy(qULN(AsmU8mxq49P7LJ_82 ziF2hkshYOFyUQM`+qsl+z@Bd?`kb|AFBM+wh4&!1WMZO%r6JQt+`YLFI?nL3F0U6& zR5MD6amc9ksWeJLTqTgpsC&Aig_uOxV9uE{_54|Gbiq} z6<%aqZ`LMiL-?wx@;O$=629zQZ;0iW0`k{NWs!{Ii4%;srM@aEISgy58)*&p6Ksg; zY;s6Ol=|bYcjK=GeF-vdp8+TB=mXB_*A>K87X9Gf%$0AI!Sqs(RuqS8QpB`FkQDe%>1sW2UYMHOt=oxs1042-HMObZJMGC~s-Vq4Ez7a2dHPjk6Sed(_`jR4wmU>5(YF{X8 zlF-YkVY{}-h0a*;l)0(m(WtHJTNk6MS;xdRH{GF@S0)#bn~^0Sc^fGxTuUhs<=?8c zyF!`cB(1$4!&I)UqGcs*=Lk?pl~;zZvXtX1uW^WYXKNyWhASfPQL07Ddynin5l12|RZ`2{B7u#;5sD_ChN0?iz;W_I z_;D!kMW}dX%EqZCb>qyi*+_KOU%An^=J708M|$?|OWhjPGRnsME{7?!8%=p`Buv|g z$A+ruLSmA>xg0LgZKg0nL@o%g!>D}K-_yG?=%h_5aBv$@w%Fj2Zf~I>>C;}KGeX$L zs;{D)Mb~K?Qq0Tsq*VCo9#pQc<~c9A!`b$swU(nNF(Gr>LPNrq;IhwgvxoNKi3^C{ zA!-C5ISDdZEh)$HiKEgEr)GOpz;R~*x9&Gee0^u|)W4UG;leiudxhOv-Z#ErkwD`T zY^7X}J8W*O(sWo{xH#L(CFTDBRdBW{+Lsv{m|J4wf<(N0RmWMDS3=Qu9S$3AC@9dB zW6P$wUQ1kfH*Ci-P1xpMUNsRJa80pOLf+g}P=aakRehkzn#IOj7KSBO*rMJ}cWj$L z_oP}}<*ecDu$+Bhc##hCFZ?eZV6;@FmDRks}hxg~5`FIYvx z!%8&Q6DgJ)2xYs1Cx);06y?ZKA`+J<@{v^T7LW`Y5|?`El&R&m(pBVV8yM+tA?c*6 zVXU8>-=?2(J9)CA(IS{&FDt@aqno;ZYW)-FKmPz4d;XKp`jwh~ng0Mk{*&{Y9&kzH zB$0~vhuL4YbnCTX{6nl|$mSkwMa7X9yhwYizxF(*?jUie3nuqws(cH=RkFvdlI1st zcrrxgEaE*S;-_OP!3&I#iW6i+10)}8s+Ft_l(2UD$DUo_rgeN7yyVA2g4>7;qLL!@ zV;xnIy)DO6fZLOe#E7E2bqs0D7}T3Khhu@aCdbu`L{cOrA*p7?N<>_amcr<_fo~E$ zT%+B?`uxTHWRJgmtpXM86F<%hb6|#_gUX>j||D4^}XZPFdK1L zNR*o%I%$)2#>VCcwT+ww=LnOyhCD{I1XZ3+w++V`c$LP;WIQ8Jw}U>`(A}}d$(>FV z#Cb|pYQ?TTr$>bCGB*`M%0tp4DA4MzR5m)v=-V6G3$`WeE+Q)4LOjV)h1is+*=l;m z#`cOg41qUssJLn_726OuL|*=v%;%+v>W`s)4RiQTRcs)ibVQ^ zi<|DR!9Jz>53cY=^XYG-e#78@R`h-g zge}~(kpBR-j9LRLY7BBEihf$JE7nJpgiVfeB2lc#ydE9;O0S0w`U>`x@i;!a1!;+V zcMT1DA5T+R`h%s)GTfv~G*f2$s_p*(1(U?TK`zm|ee`kK-J5w2EIVwst&w$aSs(OQ z-+yX-K0YPW)P>g!f2EJlet@l=8q4~Rx69egsb802=zfTzj`JZRT8lPUk?{kIV;N_x zsNCAHrI-Q{5>ZKez8Y&6YnPOfl!r+Qr%(H;D&^`m;0+|X5bEx%QF15}lwzg%YV%cF zNq=x7A(!E-_VWYFaD+&8Diu#^ha*ZlOFFCMq4yL>xxQ*eWr&3`;rVGQ&ebiG3LDt*qFYk7yVZ zT8ox_S=7@nQ~0HuuA#e6mUYcKU55&n~I+BPG%4!|+bqrYVM zZZEX^Lgp!E>Rn+tE6&+kn@1y?@qsLK#7MPQ=$}Jj0%P}98zo9(qu8MvQhB`^_ zU6THb{h}8cXKA)eW$gr`yn7P9yTq`&;{*xi}7Lc7|G zES`x*;>TuM4N{Pott0qj;ij}%`2~7v$Exh#(O0va9^TxAvs;G71n|TrUeZtw0zVBu zCd^iLI_y8QmT=pDN7=oge$8d5n`9JE%2Z7pNQ&xNb7%It&FNabVSLnvOuX4Pd3$(` zFD^^-SJcwCz+7`ToC|id{{WUGU(rz%o78?{mhJX;4{T)dIP$L+$;TrfE}C&ive^3*hlZZgJDXBQTp2`0I(mnimMX@q`)vi0b1@PT z<>91imNyqN*xWd_^U)Q_d!^G$xk#DGgKfbgT|B;OoNgI3Hi5XI9VMA`(rCqWE+j4V zNn%_&i^ER<&cvJHh%b2e4w|fFF5HM*Wx^66K}6s>eYJ&J6zXW}k#xF>h}TBMHsU3l zZYoQdM7nFvui!ot)lsvMl^+}NE}gTdRkHqKX73&zS2=$ze0Pa|W}i>>i+XE> z{*&msFRwgWj+JqECOh;OP(g)ME*0?|Z}wHv{pt^%Xcjhi8g8Q zjeHMZ^zV&$ZAOaJTW{&e`mT?~d_S*S(yI(Ro5@5~Q`r(4@i1lN^&m>(E0P{G*=##) z2#`zGOXlyXW>xnSx-=q@Bar}-rB2z#O2u&{165qeW#X&4`-zwhR7GM%-^0yU89uSW z07OY5MIZB8tXbPazT=4tGR&7zWvZdm71TR`kf&Z*RHiQy7{`uuLv6H@z>-Ty^#1C2W4NJ+Jz{1_<)rCt zV}J)eQX-armWwMj@(*C;)2EM$rSVp@4mli$RFrDfRkD!{O&1)j0TC=^pXQ?sR`N>l zEjmO;m%~vAvf6+1{{V_HgvY5yg~uzV;gPED_$u-0D$!D==*5F27V$ZdhfQYC-2VXS z43tt4h?ZY9MhvP`4%>~BYEm4EwpDH=btSP0Ok35T3Rt%QG_?@MmKe3Yz%0<5_Is-F zWYSb7R9Ois>*e7oaH+w(%Sy1bQ7naZWmPEH<%wCS!xGdaoM_jPd%=0R2@Go`Yvp31 zOpHOY9V9{}@lj1OW`sI&SzBadUv9`sMfS$Ju&AqiNb^;+gPX;!tyyfz%Yf?Y)LvAs zm8DZF%CVX?IjN3E3AC8Q(KhTzQf1=RT|1$d1@1J)oGsn4JiBd*;^Jse68lJgYV{Rr zGcDBTX{Q>VVYS=6MZ6ZZYAJIidL1<`=seD+s9Ji-$KGu=A8O%sY0^@!RXS>mrYD}= zbw2rHD-_%N05K7BB2vmES@&03^c6Y8=BwgVWo%8e8(>a7^_<*!tDdJ$*xppqg>~Bp zs%&o8I6I*d4AipK_IRtt>S*Jnp7J{SHm9m=p3yUOAUnYnj+nix?XMoUO;T9vr&U>R zk?9*ViCVZJ>l=#sx~q<*8jGwc)#lT0ap+q+acc8oDB^G|h={9=(zZ=y4>pVG40d=- zcW7JB#ip`aRXgQ${Kq9)l;O2wLwMXeW8$s+tlH46w97U8!8YI)kwGePMy5TD{UrIc z{{S$F;UviyzM?6%JG+MtpCXtMM0={DjYVrhH~NBISD6EN$PytqUg{Q&NL%9H(!}}H z{{S+FCdhQT`Kp_z>M_0z{VYeHM)t}97l3&k64RclY0>pe*TKJ}UVl$M@BBt9hE7T< znR4@cY5pImdYU7DO9$e5c+)lhp~+Ca%csRy$4y%+$~NOztw-|_`bD{Cn}f^ZuE_AO z5Ou$l+rLX3?bh_Mr<%XiCAwrqNKHTXtiQT^NjvE_-_lfd-3x!-uk{fX)x2_T;P=X< zx(=&}q}hKdr{cPKT=R8+E&yiu0%NsK4PJS`i2RUrLwZkk&q_ecDS zy5s4u80_*`%lrGn+y41LEBi?+WZ@{6ho*@7XU8YHiw=IF@O1wGy@$8Iv!tJz(%^mo(}tuaK3e^vMd)WU0OdR?9=V|dQ_w}gdw*O>nRPI%=W zdR8_5`tX@+MP()Qt5~E=d4F|d{mJ7QfB3t9NaMwPH!FF|{X-q~qa`P<6HkVp_ZN+M zxPBdnJ}2SW-_2j@BmI4ai0>S}0FQ}k*XgeucC^rAAMc+F#%3z0ZQ__~p0IA3F)pDl z6?FcX@#fR;>?iTR2}QitSRAn{QQ+g+Qnz2EKBUI~0Ec4__LIQl{`o*}7_cFUJUMqt zxI1!<5H7&1Br0+JFR>5v+1G$~uMUcdj(d*1hY&$-Tdp8L6<`(A<|$f3uPk14x`vX%o|&FaWb;FHmP z+P&?2LP2XtDV1AT_|Q&+N^Q}J&g4}4?Lx|6<&?!^&8F(ut78g!30t?kh|;feAFm4F z2gpf}AJeQlm9)xNN8RewX8agr%@Ug1NSUpz=0K)AL%tkFqzR6qAf}J1Lhk+}*v8i& z`EQj~r$0OZ!CwkZd&^8b)!U49mmibY18M4LdvZ?!9_@(!esHzgm!PI8=eS*Tn|G^j z$`#Ugv~I1xBY#ijAHh?bKA0NOM}ulS8AZNE2VmP$jsE_RzycxQme3WCqb8Q*%c@>Y z6>G1NFR7l5iIr==ST0Z7W$vL5-4CI(TP;{fknWL(s=x4p>5ByTQaYJYAAVM8!SgjHs_pYI!vK@ z%t0q&GB8@Bq1(E$oUf95FS8)?=Y}GIPshM_E=wEI@;F-C-G!ST8BG|WF<0-xeOb=I zm#X7rc*$g5xw3)w&UsyyeD^;^F8yu*qs_c1`~hxLGU2rDa5qsek;(l76x4ciV`hS~kZ{g`O$N=(4Brbi4$>}XT|vteJzX|zx4Gor1K zqD&`WFRNOZvvelHD-UX}Vp99mp1XlZfB}52ZJ!HZ4@(}23^;DQKq1JDR$rO+`H1hX z2sA`gkMy5MOxLbC;3l~E6TeFYGEX4Dhh7xd_BQ=EQmwt*MH0D`=PjyPkYHpK*u^PJ zV3E|Ou$}UvwL2%??IbilNZ1(MC81Ej=Aq*YHkT5yU$E+uc)+5Z z=d)7koax0`e;paNqKRSqN1#LYB*cV7U_R2XI^kN?@DlvlYoCeNws~{GL!HlIRHk-1 zO>53pN{BOU(eep?MGS<#N;HZ}kbNZDHqaqs(AV&xNfz}SlL{ z)+w~%$dMd0m~DL+6M_rQlfV^M+d|Q9B1z(k+WDIY zKT8k3izGw`318Sd+rlpuQvr%4kw36mbsg^55&{X*l#q%Cs{aTAifgyE`@!ydv|WP4 zf(Ihoyn3N3D+)V+uWDvISO0zB>$!3qTqt8K<}ftP-$Vqi;dStT9h^4wxp^+Ud1G?a zQx9QoCZJiCtFCs5FL8{nYA;;y%i_bP=_mG9#Rz%3x4jo$0d05-g;JlpvN9ZtkdFQ;fk z&}0v_YysMOjlaciKw^HS)+6FIPTv{g8wGam-wIcTs5u26)V206jeFfm6nQsE|0B?w-u*lzNO1Mtk|$wH zW3^lXD^dp6#N_W408*&oT z#f@|E5dJ%wXKzln3Z>V*XXJQ8ljSN7+XJwvcqXaY4ZCp8>1RGcmz`Tx#n9CZ2W%HF=KMw8_h5|H`}g}-;(i3bJBWe`O@S})gkJVkUcw?=M$eEuk`u9~3 zU0gg>O*e!%Q0T>=oG7hWzeaDFzYh?o@vZ(9>vN?jS@rxjhluBx^(ZpYpu+MiYRi^( zQMQI8C46c%^uqU1F6>e{it*tU@Gf&2tHSujmJ&Q}RYkkufRtwyGKo=9CrI9!g%Vl>J#{v0fgS)L?SH4qyuSN&ZSjnbC0ohVM$ z(ivo^P=zl2Y2Ew&;;;cbC-7KSuCRjWY4|}e>LRJWyUbxgj^s-G+S7eS<(Te!`G2as zw5ypPOv*5R@t0coh)V_OFuld)ul8L_Gm=<*w`3Hx!G|k@zeN`KILOkPHSOt>Eoo+E zd+1UfY{#G_GE;j%(MD776CaqU3;*yLI&8SD9Q*BD$>W$Z(V<|tM20r&$guVn&>V33 zGj)35ZOr=8FVp(1G$i?FF?l>h8nuCVS@t0)+bg^w375kWaVn`*?{G(JY`~eSReTrm zO3%d?6NWjX_w2iYOZfsxSPIjIT`pn z?AB}AvsFIbShR26M60C}=VH2fv<*6PsKj5;)ltK9UaUj4r}9^!s_+A)SAkZme;J#w z<`!)G6}5MRen=49xcUWhkGOhPQD^hsNHZ4TR7{{&0NS9qO+MKzWsP{Ja~7_Tp9#-> zVW@e^rC!5u{GcEaZ|pK2mqu&HFNP2MCdG$LxF4Lv%>*W2sd?)~dCS-0R8G@aA{xfb zWy2HxhI?AsOV{?QTf_$=?QZ2mFM_;2c!=_jOy2rW8f?vdex}jLFLcV>KF0cIXBPGI z?}MIi3#2a9*{$jPr6YztDQrg#e(&1DK+U^(!Sn%D$sg5 zi(i$z#}I5*pD5Bk&_@oOh^?i6IDTK`t9*6kgP4L5E2e9OL&0}XBS%oa*hd2S#I7j_ zsQfM8K}Jb?k2U=6tJgR-1uTlY4^uOv7E$QO!jisBTyvvD&WDd!ILGW+gCpg98-9mZkDneuEMXdef&M2Po#(aVdBXESDFXkNO_+I z+E`>_E^Rdnu2{F);;N@m6QvT{^Ma`D_GjCENWmg=dF`JE3r%|polCC%f$sOs9?7n) zRF*3hM+XUV|9Dli14=ZcpCdJ;ov39pje|N1Zm+!|9LxJ)?MTY)MHa1zcZJqb1l7m zQ{APT?N-h9oPksYXjWAY`D6%E$2B;IDuvpFjubkYg3ombV;9BQsQ`phiT+40@%UtB ztWgd;JD#2rud|x=!m%ZZWSnr^OqH$Zj7yRfun-yfo|We}7}Dx4Ab#LFoMiEg{`fy3 z_T(`)zv`p$yrlljZo-`TdGR$(5XW!cz{%RReb7A6eNt2ALe50b7)0;KAy*r%MwBlw{iVH` zVbW|il{g1SFhf%MZb5C_a+Cv=s^P~C>a8#S2{tgW9wXo37j$a3lAxx>)>gch%CDc# z{GI6zS2?IW@S=dmtjyq%oJjhXMt?Oe|3>0Y^p_P`$Gx(T4%Z%sU=*6q~NU{&M-c>E@ zq&08d3Y}aDQca-occkvcgt&#r!>eoi99}w#?mqbn<~B!j)p2>9X;XRaFM0YG;}RFc zz&WiVN2LZnhIEG5?}eqwx1m>1=)`!%gxytpUv+!Vt7e_JCmGm}4SE~VU7v|hB*ss& z{t<-oefW0x+f3%C>6|knTARde6Zm{$v)uGgW${AYmO=bkK_6S??-#bU854f0Bpyp_ zY!jPkor1RPqC!s$#i?PO!MKw!O}1VXxs@bn<+*LiJF45H(PsJC_k52=TYGJ|ur*NGusb9S}VEMnf|jL8U~g3{3~N&*2wo(!)!&jwZBLH+;q^Vx zGFDl*(YNwu)@IL`Wi^!WhKviV#Y-9&i-BFWSp2ju!Vat%yBJ_v~U)X$^2qQ zhE>4}@A3xm3kCkQZOLpdv&qiv?;sH}>wHPgO(yZ$v~v9J0j=nbbZlF}Ve!Q1JMy2_ z)C*}3d{7~UgoL?*jyflOAE%+}TfeT>;e*?OGTr`e*Z&A}!x z2T|bVax1l?3`^V4Qr`e+mSiq<<;8}Qa!F67jsZoCW zxYVIVlP_jz(%5$NXC^GspW+)Qb7Ch`{%uQ_=5=1@Z)8&YxP2S{DrJN3Nc=IOVYZF- z6S|>+!jyEtf_JjoqL?JE;P8}#RWk#4`m+2S~B`_-0rhrYYz{Uh)qb{{kR-U6tU zl}4)PBYs73RT)RXg+VOE#EyG$-{q)&UvL79y?l^WU1iAc5`e@k0hjP@BhJXstdDN?Oya$Ycf_SHD`l)}Ov(2o9d4-jJZzzw;h4nd;hkHl&ZC3c zDW1xcoZ~eB$ITRcTPjDlS;0`2^%PdWr-Y3oJ5;o-*+6n^Fj^o&Eh71B+G%BhRBYwxuftoL z@|J1%F@S$g{F>ywFFdSeJn?lOqen{H63B2xIsnU;v|y+gCsoyBvZpd`+G5q8{qZ44 zxx{n)Lo&+ct;4jJMt#^T*~A`URS2V>zGQ#sKI?uN+EZ3g>jefbi-h#TZdEIGV>boV zsJID<+}XHAW1Vi7#`b5v-md(zcAhz9Np9}_GE5kqo;i%Zd+@b)Jo=W)GH!(KROv(>v*>oFIU#*b)AQ&5DpfGxN90yHa};gERUSs8tXg)cOAj~Y4Dw_z)-q?H$% zx4?L+o%FlY_L7s>tkxdDdwCUg-7`l^qNx=$)8z5GYe+$00r5IV`(`*mt;IRf9l*C&jKH2tU{ z?GsN2$vZbx{DI%4mgn(iLwEd0-QC708pjn9DIsEFD(JvUtI)dT7F*Lk2Km?YDx6!G z`(<*u!}g;74TVbT%fj~Y!aIo)+rRMbxAOJWf%ZU(XYCy)JG+)!)Td(YuaLU_JEN#a z=4(`VLr~!|JR+j@L9oUln;dS{37j>5O#W_Rj735x&WP8-%i$b*N-1@Z25}YBTFx5MB3V@RK}L|wom;qOQ_ z7rB*+3H;e>At74;CG#Q89?zESeL zGl8jP?|p0Kr<_mQ-&GHI)e0et)Ggg-KPfW{G%AL~YE_9RI7GAx|KphTO5x z)QPZJ;<3O&>vB6)_h&6|j7k$dv$pr=X<3Q4e}#m;CE4z3s4TITZnDYd z)$5#0ntjH28j!b?!3P$@;P(}yP2WJfBL}T5P7Xq3(&1VLU|XP*P(!6xjsJf$sZ(t- z*R$@}m5T!7TIjP@ab3a+5RR&iX~b-?Is!9PSh69q+%Qt!`tz^K)y7k+EHk`!&lJSm zQ^Yx;e(}lDFS4DneJXA#$8X)GWkHf!Us^f1!`ViG%Eoh7$w{Te!vbb0IsU=|ZQn}O zRX`@|aqp}YoB2`IX(fpoX`su<+pt^kJfzxf#2l!E?>3ER)%$>ctEn#2wp7N4xQb4| zb?&VyX(b|*ayZc)fn5x)IAPA87!6tF97i@}WCY2n6Z>|rp3dC`q?q(U8k0*bnu_s# z*Upm^&^~{M#m;+YrKweHrcgw5kLFOq;9SRp46^K){rS4rEP5#=)fK@|2RtGjzNee- z{iTsY_YhTY(lyX7{2*HbPECn@4LL}C9~#UB&c?~j$~ME=-c6w(Pp|-8NW=G8*Hmoz ze?L{#DUXGsP>59x2^@V`qf7o84>fbRA!}MZVmP{t*){0VzBBD+w9wxDMLRisDG);N zW+XOKgj~Fzn)N(*qhJ>@6XA-Zn+L+arFd*?6fh;JK*f*72LqH|Wt0F@w2r{e5=300E#6QYLc!}lQDFLFi|0yCi3fw5+04;yh&YPAD7CzCp7 z@0T64d>Vys3lP2L7%!0brwG%l`e`aE1%_k8XUO*ex|VuX$6K#e?fTGe@oSat2(0@G zbE7)Dsm__#a#_>Ht%}p>k7({X&DgVKzZZw}!Y)?d_hb{-qC^H-aDH_&;K z(I>e-sAw*H@_qs_XJ4_Fcvp6aGIvan#c`i))HL*}H^=gU5^ai(P7PrcMKX^IQ8xJ~ zF*sVrvBtvbh2RcF2gr981 z4+}F~I<;@&aQIOA>4-7*BZ3)(LV@vFgPy5InQ_;((H`|->uK-zJh}jj+`)#MrIv&TUTY}JEm1hbp~}Q4 zd|RLu%Vhk1yU$v-Dyw~5=r5p|^$-u)gv08;xyX`w%hSIJ-yzfuUg1n-UASXqsZGC9 z@Vjel#K+KvEx6b=I=vyeA8tOViMD$}!vj6K!oC>)S2NF|NDf-u`tYOQMafASaSH5~L|K_VRVO+oqT4bM`KE_=ej& zXpZ&=oqpe(ecDkS>B#6fwNM)<)+_#6hKU~!6 zQ;ME}f?GWrxuqpHd$+13&L2RvRQFC^44aNCdx0$BD+LFE{N*i0cS-bR+Xdn9ZCO^k z**?MS$ILDZbwVV`cP6!et`Or?ubUS0O^)z0xP4sle*=674}||c4N^+H#;k@)?kS7h zD65zbv9$d#S*s)>i8mRPA{HMI8iKrZC_adanPZV-3R)w4j!W;@;M!Xa{ab2aXECO^ zhQ=&1#<+yGPk#*MoP@6Nm1r`Szftia?vZe4>8PbK9dz{mJ{YiCpWFtyg&;E?go>Vx{sM2}K``5*}Nb?tN*OxA9q6N{i=7bqdRt zZ+YQbDHw63W;ST>7Skh<8TaY#t22@2Y3g7Su)EiK4Cu;T6;r<1YSSEr z!;+)@efcnh>|Ne4yN92X;V;}qzI3y29GbZb&4>v!d$OcG+hugFpxcBs;d%g8{6T4+ zQ|{}jV-fQ+ET3I{Y+X(gYJ2}2v5x34Yq>+oTBAFRKx>i~MVrYX=2PNeh-pH(w?0Kp z8>zx{zlkT}nEIqI2;AEnol>`6ze&$)VN@>+;1EVp*-nPd&ruR!J?F9SsE?+W=^bd& zVjASCY7O@B?FQ0Z!tCmVuFc}z+4Ecblgj#OIEsBHL||n>0@iUl*H~f?6{pxX&86wx zqgMqFM}owAJiB$)|R#8|eU{4RN${)h_ zqYBsAhA@KAkGE=@)`n-g63Y*$SyPumDRGVnaM#Hz6HB?6XV~;UF`Lr}X0UV3U ziJprB&bqWy%n2~=GEo8uh7a!rTl`73E)G?2Z}>-03bHtLF|I>pPfe~=Z(?2UyNslH zI2%9x5tmicH<@Ht{!^JKGZUdvR2B9J&qp5{L%Z}D)Hz`0=7=*>C{&$AmY%2uG)~x~ zNL-s;mCgVom1kl9NqJF5Ogl%#x>pPr1?ZE`-WGP5b-E3MqKhpUWCkR5FPbzj9M76* zk~7|{HJsZ?q|I2!y82wpt$m!T5YS3&wMBh2Lj;*9FZCE#&y2DD+yx!-JN2FqH$39( zR`Vj}POGYUjXAdx>l$-bvuYc)+$^9wyK6JiA$aWY4SH8` z(Q6K>Z&?DVy&cD+B#bgEv8{0w-iMXKF3arK_JciJ_u@dco$R+sGfzyy1A#|JQ?|f! zsv5W2U-2~O4A}x(5V~0x%QbcP+y@Q`o1M;$*7UDl`?{pl$4f??cICWT6}|&xy|Clk zNOQg6OzWvXIBOtRL>Z2#8;%nVDms_Lx>d)zUrVcql!jcAFDQCYIft08Um);CcW*_Y zz=qz?DxNfT5Wb&|lQwOAcooY__d;6|D5&JPVoD=3oAF*KU$e#cp6!SdaKlX#yUEV< zm5rJw;>#tb*=UdfbYjNUZzZz5jfvzkq$9O>J^5f**35nl{A+<4*b16SpmJrKSkm;| zN$^)13ei?`c_Q1xRvN!4+LO)5?eIet=rL3}1WsyOofDo+aKTzVrX81K9WjjXa4!hs{)` z_pjgf`2WtSTZk0L5Srw~gYXSwg!Q3Y+@GIJ99I&VQWbRm*Z^#KVzK1~ z1!vTUZoDJwa8Fat%Q84u<-ohJs55by359sZXQgN>T{>z^fcoP&lw?$wOcGj1Uya(Y zYx3#}6#6i6KMX81vriPb`P59Mp6KC4`(PSR*#&XZ#^s3_tg`vZWqN5I8iZ&A`K?@& zaVQ`rwv3)`7n|gol5ZT#WE^Sncp)M`=CM!@U!JdGvhA>Rp>Z!n1IyXK?%R?*&}QtI zO4fg5r`!G5r|<)#L5#A~gkg(fBROJ1scba0>&Hywr7i{$?o zxESD*MEm98pX=dlZRM@}p8dg}l(QLEKZNwaLzFdCH22qu)Y7%V`9ldc4IB1ru>LUh z6#$;)RR3sjJ~1_uG9f3Vxqwe(o}s!VYN7v2FXz00HlOTF%yI^*3`SI-C)*Uy`EUrj zh-LFlWYe4Ekk%*`GgUjwgw4bawetHG1&8M30ELvVTT)WLEx(GjRK_;39NTE#?{8l) z%#iMK;JAz5UX^JBq7dAmPPghb1(BoMR88Dh(RnsIV{Krv{R#8c7 z0ah+YZ<<2FyA48H+Dt}@Akncy2$>1ki%(n&zN;I|QS8NEtM&zed|MXWE!+JkYf;l)n4nMxSd-RmJJ0X2qe5$?~DylWl&h zvHaGw`r&?hmf2L%*-TC=h1JY~Z>N(l$Xt0u#~J-9UT@ZyPD?W(Ob{ZdkSeQc1AF@) zIcY&j2m;z(63y@Kr!p!sQ`a&rvQcgl_Y#@FHu1dusZGdnrlzYjX z%|=V*MzUJqVn|Z!9V4*KneK|m94&jmHGvx6O>;SmrH-FER=a!G++VYrbrf_^vtQ|* ztNTJtO?4JvX=^RitY{O>L-?$K^XRgrwz{pLR|PbiNcBEdTi2K=?f^)=SxiX&R8#%= z(N9fRXj5#LBe@V@g>CBLR*4I4=>CBrY?T_iZ)SXwehhWJE6(U|eICs%M%0&$3f0MM zco8Jh!2NP-1j*+`pBCl?cp+>q1KJ-*WH^Yw#oT8zqHbXWMn^c{IW?D#6~pyp4IgzV z)7G=sx3*)o_uUx?*DCEyTWZ!yJmN5w-io-0Y*>fCzN57I!~U@}lMdM?3NOdC5#}(5 zDM5_mOt3Yd+j*KT-0wdvya|w4jPShYLhfp)QzU1zZO42OO3OL^kNEn@Yv)*^2;b;J zUrbRtJ0rAT*rtJKUD|^e*%WFE(}Xl|y4)Mo94QW4j?~M+dZf0)H%v0#S;w=}Hg{|2 zaisIaf=5>dCGMXXQrhezNt+zLD+C1X10?QiwahZagS21HhLg_HK7Q;snNqGPFBDZ> z!~F4^r!0XGj#A@~r*39lSIYX=^RPYDk-&e2qJ#wEqS zphu?Tx8V@53a*=7!>i`7@w|ks#+^ek|A5-^aPFidff zedLWZ=D)LNdTGB6Q%cARb>1DFx)Aeu#y5SMZ4`yO(gPC3m4Y#HCOJ07uScwAv^Iat zKUlx_$K`+u@G4(u1I=sPN(?i0IjnNdj!f8~0yv_(ne26pO5zTUwV3Cpo)5ZZ+eUsU zxB1M4HvpN?=fC=Rbff8{F7#v}y&&Dt(@8ox+N{nrmw0Y^9A?2fcjC=l0Vl&j;|}W) z1)4)8jjB3YzMi@Hr)1R4I;urS8+H^%GqU-;%pG$Aj`Jps!s{MK$UBfLNqEXtk zM8&Q)xb5^mrP(;;k*TAx^CV**8mz0JQZ61Uniw>joQi-q+AWOxaW70%sMhuX+bt;SE4sw1{|^hz*i zr9WK02Ccps-CwDrN^>8S6B2`J4lt#bsR>*)9MUQjVObO}AGWyg%%p_h)`8@{nSy#3 zs)L&sZn`wZPQk7~uKA6G6xA-hq;;scDUlidV~=jzuB`gGMx$tP=nL_z`3##H4;>cy zvwRk}$vRVJz)v`#LB!(`sxofY*WKdyEIPKoJ0#nfH7rM@4+ycBsdNGGWL3+qw?eXA zEvmR9ybBt;cNf2PGYylg1HK9->%7E-$C>4_)Zw&TaV|zn=Dcl7)ATn>5+f>5imLCp zz2Nlp#juI)rc`!8OEFv7xkqc`{*v9q{gLmpqLiXFs8wnqH8g2!zq|#HuQkE`FBK*& z_-wGSAJ((<%qbBek&@l2T4qu)DyrqquwJX5B@roMit~`lOu`<~i8Dx2!}EBm^qVGs zgH$Nw)h;Ws%~w?Og*~LoLCPVq6>f)PZ>A2*g_9W#Fja-L{#JxN+vng%^c%E;?lqcx zm%&*N>nSl0vMY(C95ztd)(Soldg*yt!e%H)$PQ%TJyhXTK3kP~^V)Ad!Vltqi5^a9 zu49K~P&WiAeYP->Z4Sc>z5&iUo}Yq8g5=b4-+pXmt3P#5U%08$DlRa#C#$Bmk$AxC z-d9|_Ot-HT^`_zjFL)rRJ5~fJ!gY~;Id!c{SydJ%P?6SWk^2svcJjy$_V7<2nB(Ze z{*1C}kI)#_2?SccR6QD>FZ!@@#SkyVJcNn<-G6XO0A_0eGF&!dM5;8OP~y776tWsI ze~$#-D%)wLPu;N?{3CF+eMeSN37v_#K-7+!{Qm=}9 z@`PI_EvCZEnt%zGoCL@dz*sV#KjpXF-G`F^A1?pSaDb)lv*Xa(*Az@lRqjh{Byo9U zCT)a+Hora%PIXX$qyoeY@rY5w!#^$&$6QGphhRIS^}^St5sfA8X5m79g9ZF0@9cgi zkNr&3TvG8Ca>2X5#5WW{vP$@_!pb6D0Y-PMA_mRY9i}9b2rRIw!Il)TL7gt9Rc$&z zGN_io&=hy>CecD`H1$eGC6%3 z(l}b@RD3~v?JiX6`bj@ac|?+#bWi~qX;32<7+W6_q5oW1=d+qPjENmE^Q1E+WDUSs zx_s8vqlt{h9Boav^C0$Swh4eqI0(VFNKE;cv4O51K_6D+^|Q#d>P;1Y1a0ZrvVFCJ zU>=T-X_X|U{@dHe8iBb)shV@`8JY^dgnA_hp^(~XfxSlw^-dl6;#l(kDyn|x4zxrR zu^yDIaV@{S0?;_#?XT2$jb8fTO*t$Wj%04{yId_UO;z{bqtiq+^V~Tai9-P^^F2h- z)0%J6*fps49O!B|*dvu9Oh=f+!P-56Zda|<3S|Y|JXdJsg<4Gg|5e< zM!yPvoBO!=TwwFni2Bq>Z^#=2yBNQ045_*)@CKq)SI*w1(ZgMt$==g<%(~y2SF`9 zk+={C@=2>D6uulYP1Nac<)zQ4<9_M+6Bi`%HoFDSdivzVScB2|sB!hZMc_SZL6?VYWp}Gb znVO&m956|!N7040`dtOkqUbjeXi?TO>A<-k_Om&3Uhp9z^OGHYP%z_xG(Nhf4{khsT&}f?xfmvhxDKKmHjh*4DIv&{rUUGFV#9IMQle;Zj z%%jhFbDazKYKpa>`e{ZT_=px-7!;*QNR5|H$!UFV&94M}HT;i&0_0eAU~}!(;GfR? z6qfkI5X2F{1kQ6`B%o8Wk~;iLoeK4JA~YE(CEgsXzdSh^W95oz){wZbQ$w8~P@6E7 zQ87;CJ6hR*R_!Pl#gza>p1Sp(`eJZNu2$Spgh7Jz_INOchjLxN}m)8K-J{kCWI8 zIJ0%+(8h>PpbkC2h676I2KZ#ax0PKXGREuBkI|1m@v__0+Kn?jxT;%)Fp{bkqs?uy zMbPxyeG)-I7Ur_@Al+~8VZ>&grqoZFkK^*uO&y2|Xuo^sx?Fin_042f{&%Y~?t@9- zN^z3GNs=)EoESKwrmHLiG(l_bcRn#_u(odGxIY4w_MF@t`7#D8(OR(qDy{G&T$Y$L z*F8nyd$>vmIwC0uhjb5sC^1ln7U|NrWa79=3ZCAWzx`8m4{lAAc_ZS}v&0z1rRigW zuj+wzS`nCdE$!XNTH;}uXbn=R6jAJ})wwiGq075ozmMYQ^_~!|fEreaqP9>e zg!%DqpF~|bW<`{uzlX^RmjL)|SL%^ym-vSbuEVB`v#0jyLPI91_Agz;va7gR5}%MF z;xlDllD~}|ei;*y?V>JlGHzH!LDCNhQ|CJ8i#(W)eFfhWF*-?%T1dN8jY!i&rL9hZ zIuavbTo1zZm(?pG#uYw*^cfun9ju!^O84j02%rs$p z+7h*I{O`@)&>ha9)4P{AvYtKBn;h!YpM0>HKox`O7olz zW>uFFt#uZwa$B*j@qGjqG)UEHU`qZiH&yP&Whn}4cCrO;Q2oega4efG>x#pvJ5`(R zqytU@6q%>kwx}+hblm)nu4rnGmMCd_dYc+;wLYv_=)hHcunQSCUWru#|UmiT&N!lYc~ zl%lF|l>Eb3qNWfTndx#oNL^kxIhdO%reSPT@cBS8#Z_#NP$@6b7AuuO*IH`Vkgg`7 z9~eB*hbbL4Q}1 +v6=vMg3?=^H{xiCUjVyl;-n%E=gKrxJB)jnRycaFh*8B#;gJ zt`>2HM^}sUP4CT=yTO}1(u4t)WCD`{_e~SFm3W23RKVcV=Jq%*N3R|KiXa7>a|i+{9fme378@(o+a3bLrb_Q zt}tvD$SXSRedDxKSf!GTC|Y^uoB6KRs!P2i`mLGh1i(?Wdty*8oXj;vR%i8Gx6)mp z(G>W))0|BKMy9RK>)(V)tx#S%N+PGG1~PhXoq6aFq+|?Dev_2k?^c4abvqUdKkt}} z&Gy}33Y(i+RN~~N?=sks0R zsLWp;%|BC@zY$+fQEn^y6MiY$)A^4eEH@!V!)-IxWU4PAWk`3(&cy&P7~b6?jTT+- z1h4`@nTR28V0C=w%Nl+L0`I`#!apf$b(9khZTY>b_{aB|Siv&%ozh`nO}cs?uw=V~ zUvCQ3QHU94)_3;!GI-mj=W=!Hyx2rHQ@EC&dG!&6y%64Y{X;FS`!@gSl zqXB_GMGk%dz(xlLY}(21{N&aSAM%uHoLG zgi0^!9qA$B1dmy#HqK_ej)y-RN}#O_3P_^A;W|kPjpgk2yml0|9EM~lSMO7PH)b>V zF|?%ObQ#RZ><#H79H=svnX?};$YDzCv$Ggi`oWWykuC;sOtmAdhn#J2A?O@<#x@1w zA2E2_{RvenKZ*1LXN7c&KKMsK+@Q!t%uzdeStj$`eZ~jCM*q+d>e+Jk^2oTg$TV(r zadla|=xE{?YnBK6s=WVWKlJFSO=}|0*l(c?F8%eP!{Y-wJXZ;558zp|}{DaOe zR&QUi2hh_q=zK|jmqlm^v%B-~f zi0+p0?Xp=Yy~5h?5crNYP8=vmxvg#Ny4tqQh_#nAFX|KjTHmKPCy7ljb}3dGAHLWGu8P+;#DTBLQ!c(N4iW&Y$*uJV^u9CcRHWE!4mszqzSP8glWgvc)R(8h4i*3%|SccQ%IgJ!3A)M5yF~tv7@n zuxaaF&$}>|(^+-z-iRF8uF}C)5MTE6o!0}H8m5K;LJ=A%8YNYYQ#Jlu(6vTHt9@aV z*-}{6)Kjzr7O=rB<~GSb1j$xNj7K{z0`(GYiyq%Xs=YH!NI+kbRDD>T!)p1->KUS4 z-ROyBv!a5^4@e}2tBpaOPf&V3lY3mib;DO~A<>TC6U|Eg5exvRFG>a!DC@%3K4gqu zy{i4omiG0oI-g1K(a<}oxOd(~ZGH!GR(B?eP9iV%xMhvS^3FEBj z3jY&m;K0aK$7P8m#3zUDDb_KQ*ttcsq%!AqyHP`6X^ZRjzOeyyz5+bOxh4aHFsPU- zx!;cGbE-|}f=j17u~U9#gX7rZzP)__SLEK<^OS7=H+Z{B%E!9b4kCXdAzP z+JQc|RoMb^SniI<*@ro90Le7RWnWzZzv)^7Bj#hz-z%sgWZ7u`cpO*KOEy2>pLufZfMW>mQeGCDPn>~t|=GbZATQ$+E#4Bw>npX@#+ zw^R+QkRd97WT7=RdLvfWSJadsytX@>ORz|lsVX*_l;&>qWqJRhS;gc0M9HF-QDAj} zEKcFM#EbK2b@HS`f6OxjMrWZZ!Fs9efO%hsO(uLt3A8~OV+=rw8D>yp*STM$6IwUx zv4WsTj%DI^68FIi=NHij<58vCywS;`K@HaE)Xkwij1OcT-;m6d^xA_{?c`(<1X(W8 z>VdvU2f5${`Z#Lz65YoHF&!@~nOVA>)q9m%UW(r7LaMj(J&3vS$kTWZ&vEO$MEBZ< zS1H-Qnh$#A_`tiJ9m-d6CZUEST8;rF3gnVf{D})&fF$&fWrStY>yCcUYxcpH$M%!X z+0IgLfopO#>MAkW4JFFmk6jJLR7@HsyTp}T-nQ!}Y*D3^OS>0|EM+w4(LFiXc2-^a zi((YI-`mOj=ueY>Lt2)8!eN5uBf`?(6VLo5g%*FDl2QvGjgxb~-`@VYYn` zko^s!W8f^*E5do9m$k9(VZGn-e*yaENLp<$!=AMFfxFu`-)71Dh%)3~ZH7?Y@CUxF z%Jz&rczD3>lAqZtiOw}`uUlQ9Tg*;D)lfO(%Z%>ye2> zco)Jn#hVN=BmEi5=ZHhT_SPmi0P^0f-mR?>$dF2c;jQ`ma1)*HyX9Q47;tQ zdFQib?b0s@h>c}>1nZl^c+1?FNnLwn(D@R?Hkht;oXU5QPLtl>OI9Yw5W>ncchESrmKG>?!=eV0Cc#i znsA7ZElV${t@h#=-U#})i{T-bnxS9hal8`c8GN$&sL92Y1ecALktH#XdT3v{Ss=YQ zSx;B&=_-$QgyjPj!8l&CmRZaH0D48HOYJ3U;9U~6Zzy+^zs8d;-Ay64Xfp)enP9zh4I5JtY*Jdh zJ|Eje@5aSlRc$1%X4tiGIc5c@qtoq59&k~@K6F3h%y|8;CP9ARSm(el6Ka+FX>k4b#6PrJ}tO9kg5h7 z+bMM0pHicrG=GYlN}ywMH#SQ6Ta!(mu2Jz-GhCctKln!bi=K}hx`=9W$T5Jw;TxvQ zQ!9~CG9FqETOW8b{?1<&8q9_;3G9v3M7JyN(@t5VSqxxb@QxuJ>07VANYaT6V2#a< zxGgR+N?cTio@Ov!GT-g?$QK`*jcoR9Y7Yvk1v!>FyZXPH*n=5$Q$gS7h zF&_GPNU$P%X54IucW3a3(UOITzfi=rTAEbmyaoas9mwfzNP2Y9E~+?zeyi22V@u6Z zSZ46IB_SF-)hihou)kT9dAe$4{sj;*VB$q09<4O9$Sv?0U?tqFukl8sH8jP)C=YDw zP6-h z;vP^fKgE?Pb}A6Jhv9gGmY_EcqB-6|Bb;j1qk4(c;J;(Cn6u8QId35D5gOOcQgM>Q zn-hpDd&03X_UVm0)_EfdK1=#ueJ?P+!@F?9K99JTA{I9|jtBb35Wa7iklr9^kuH*o zNTcFPwQTv47rPSn&F!WV_8!&6z0jyAqP9)-QHw(k8)>ZG-ZnaINJxaWL~B`v%H~Y_ zv@VX=HE=$UaNsLzz=XSt{Z!#aFS|l(ZKP+YkwgdfMx~=}FkCZbc_&SxxB9VI>gJCq z)4^ks8SY@?*LHV6LiM;9k!C_eonXm|maTJ!+D7c$6CoJs?-C&@cv!rwc_!Ui8+TAkSHn#6( zIUvWnjMa4pRz35xe$8(laj_eBGa*HcYc{EKPcLciyBpa}^0U13QkR#T_SU{)!7WLk z?y%m?FB!9oc50I5{{T%4>P|9P{{Vn*TNxFsY38_#6eVY~yh+H$-z@rRd8#~Qw$EpH zfd2sbK4tLAoVuuDpk8)qi9E>(T61`6e8dc7&bsWM7;!h>tNc-yO)oiP+CMG5rBD8r z;}-5k##bXOhFvsy*oGdZaqPZrGcyf(H*7Pb7n6O=$-0nf)Z;#@vTc|WzIct#% zotEMXDE8+p`eRtnP;@U&ypfk@BrYi2Ij`Z3Pn_O`d043*{{T-dNOD__;nnxi=QMkV zt-FYYd6umCvWU^=GifCGOAJuB*<3cl;D59Z zru7WDerk_564HvW&TQ5m*pUs;6oiT;Pd`xdy(LIjXK>XaA-o=8m0z8<#7z~*(Y4sT zG@L>7!BmK;NU7$|y`O^dUh8q-tWR&UI7pW7d>p^?G}3osJgRvWvCWIY+_#^nHU^xfDSWge z>)I-)s=sp$ot41tML+vNoXEUG8r~hG=f!3V9|C^+m@iZ=Va`VI6GZtj`(+iG{1;E zspX*Mb=kP$4LAP)5m<=0Dl!)bN{*^Oh|EH-p();WZdV(V+RQp2IsX9nK*)#7M&>Fp z_XGO#vNtah-rs2$c#D^I;FM}{^$`{$wB3+l_Yr$&EeFiw!j$aU1TvKFdqV6*lqS<@ zEewQm^)6>h%u{3NR=4d#u@ZZBTUBm_5s;Bk(6Th2Sv6ekGqFoH_l{#$g_!0fPYBD) zQ-cm!Oh-F<>@6HN+}aypK`lP8Y2aqlw?j7=`bu^ahBBn~HEqk3h2bs~@mz8iP*M-2 zS78n|4l}e3#N$f^Em@>@zLVXKh={k1TakbG&RQ__DreIYrT5aCv9^6%*u}o}IfRIH zQe;q9zqHH0`e1fDZZSM!+n1-m@YLbEK--q8VBdW=J0YZ0yj$py#h7VMoE4a>nk?M2N-3ncrrF>w#lRoHtjlW$@`4wp$s zEa=0~tnHzc_vsYu!xC(pvAAr>IYvX7P|6W49GvCX=@QuN7i=2z3tMqp%Qr&f#6%i| zzs{vG9oiPc#=lBl&)DHNY!(}BBDYWyC%K9|3&k4NS80R2>7NJowe*pDv@Nu}$F7_L zR~%Cj66H@7N-dOxOjP9C?K^#N43+W%84|YGmXfO7EqOGBY)v~&x^W0eo5b>ocWR}K z6%XqMrXRM#Lnj~0PLY;$@HC0Yu>Raxjv&i7*pn=Zi74+J5F-3ljxueMs@hLF+N?_7 z7TN)|vm+}TVVMy*BDi@=n2W+aHMdBNaaPr@C+&r<%ZWH`i#$h+e6`b>nKFKoMA=sa zy-PU{!&jctae!RevJ(2T9Qbnd(#{IT;Fit^w=Lg)h-lAtqQXdA*|%HLf+Ln)IZ@^h z9iX|hqTj9-N6az)8c@o@h&*q!OG}JnX&<@~%mBA-(sx{JRz8O`yC-pVa1FL2#IlWG z^|4!&h_co?ANXa)Z4s8+K}hQJSDe+4T_`Rq1$&GQ;*_n+n@HC>)M#Sor&1EZ&$zpj zmP12UvA2=5h0H$SN+KuC_^CBwe5Umwa|v*Zcp{{Q%N~)c^Sul5T6K|}!^lMjTKmX` zRhu|PB`XVVGbTys3UwDXV$~W8nmcqXTJKByK>WqwtyigKTufi2$kDMOD7ZzZxTXA6 zx8m}|&3a5(jT>1w+(tq}%0pcX)GV=!QL>hs80sl5e}TQ*L9r(u_w0C;pfBgatrsd?&0;ql1ZWSeY;0q&5ODAne&x}4rw zdfwd0u@@SO$EU?z3d_Ns8*(h$*uByvz`3hz)p5HLbDgkum9qVbY-`(TwilqwNZ_?< z$rh*HT_T88d1Tfu5xG2$-?dCbZ6SSkcpI}Bw{9h*N8;+GGGi_9Z`$p}ovUcsJU!ypw?{c$RZlSOBNVX)+;h^T(ho58RnKt}eAn8h zDQAdcCd)4p0&$Ov@m6NnSX)7b*}bcm*patubPgyNhnYOz*NUxlIp=<-(Vd#?nK;Ne zZS#t09Wkp`ut#pEy!Lmn?hv#)l?=K_b6n}OsW`N?&uiF?gS>hUIcj6dgC1|)L^|Ra z9>KA_oZkqK_H$GL%0x9;+-jCRLuC6oaj_QLwJArKhnAYPhlS4X2iTXHx80Sx=Dgl2 zd8-GMXtS~x0?dlvhj%aa(@!-j6^Tb*%pRjcERQZTj5J=v_+>u9jsDA2RYT_8l z3!577xIfwXi^EUNUr=SR<+1k#tI=155m8CQActh!G2T6IFPE09HOj?dw^DN0vgl_} z>5nvMXI0S8%R$Fs+|E0($oLUTmTQrwGC1sQ#`AV9=}xLV)eIn+y@z?i-IFO2<(D>e z^Cra%b7TJiQqa@d=O zh)G5$G&O4r75u^-k-YlF9W;5f`9mB@KjAl<(n2UQ>G06iM~aNj$+++lZqRh;kkw|* z?F)U$M`PX{MG+b^khk22{tI(cBXAnIhL&ylLm3>lCC0@_oEZ@QYK5zhC0R(^**!>3 z3S>j7To7uKu0qBMo7nbTYFqB_7YZV)Hv_Vm`+c&wTYDdT=?J-U({r^gIu2af`D!w0 z&^8)-J8OX7V9h;ibS5AolJik^UI335!|*mocvsA>ocXHrV6vRJ-eGqa?g-rqHQyqR zL?m3PQRiaTE6Uq=G#Lv{?ET9A?~vTLNAj*_9LBp;3>i7>(&U61o`mL~B`Hrco`B4>xNf zapKKpm#k2+B&3_ni;*;97brl<# zwHtkk<9l#Ij&Bu6$4yMKvkw+sMtb;!w|mYD$5+0q7%tnjE4hdh-CZ@E;7vEwUXE))d*w`}kBYP1oT2M3bQpQ1>4o7j) zL5P&H)Am&3a`M}u3)jP|b~*TenxFF-v-}mD#R&>K&I(>Cc-cv2W-Ar(5crb;3n<~!CzUt_+p$Wd}BhbmjO2R`NKluUE}5Q)Heo@ zt2~HqMY8pJb!m@=n6kx$-Naj!v1VQ(qFIH;p$*8Oi8$dIFDyr!!%@4DiMygE{aJ|l zN|0f);#-2Z6^ntxffTuaZBp_}Er1+QTzJ{FaLnbO<*KuKl$x+jU1UTeN<-fvAMmNh zNXEmKBt;fvCX42#y~D||#sd9lHu-NHaw?<;`Od1 zC^FunDRS!OO3m_>DJOQNyIe#_w_~O}Mm0*?cNV#B+D^_O;EZ>J&C64@N!;d*z4K<` zHbt#<>gBB2sz)U*JeC`}l5RzFk!MOV$QibpmM=y`43U(x>C;+1Ou7)OmZqC;Hr}TT zJyh}9Uf*#x3PZkj#Cd>~dAduwv0s7S4u;fOWdxjhUCXAlDmzUZcbRTGw#d4)^?7P> z$B>M2V}+B0rBx8QzAAZo z7o?B+t-@^wd7NX+H1?B^!4l!_HZgFCYDc)Qd4}G$NKLpS9VIUfRAevaGak2ahgPny zaaFUEcr)*XrwejP8F3vn>2AvAZ}u}>ZUyMXgQRqqh-*b<3+OlT95(5dzr)P4CQFzz z_M+E5O4=KvDd@LfNY2XaY{L5xxZEQ2yfIptOpP*!$uNjNB;of0oh3t&NsBy#8TAR5Y$4k zSt%I(B)ctSNWWe%VuhBH8d$Wj^oS+@0Pvjbb~0}kX#Nr%H05e?L7j2*uIy}y(znH# zfbf%KDl)9gnXyk0`cigNZ)?796l~3TL`&OIFg=EIKT6Kdd0V(`54t{DMKZA~-%4)D zUAI@B#F;XdsFa9k#>2KW$P4LB*xv9(ym_!r;q!T^!eltBir8j8liirRG4$q^JSDsl zr6!)<(7*g8J1!#Ai!~_bTDfW^z5zbvcfOLHp1uD7-1yTcmP;9GE~<%EQuE1*w;xIE ziGVyffDo*?OZcf?+QC@gLdA#DLt@_g+u+JM%2hK_S*}_mBU)6Lev*3vEXBM^+~dYV z=k+Rus9PtY4n6dm?B5gF`|K9u8V3C$%)nl4!O&6R`G$$9xW-c*Of8%GeRg6yJBD3i z7;J7`vuN&yA|fXdoLAzl`7)g~HB0o0b~??T`ptzWW28uiGOgniH)|ZmIr>Vl7qFX~ zU7N#E+)Z>_wp?2R@t2f*K3dTh>|ru|H}sP1uLJr@_Kj!S4`~}ie&x-8p6sp20N*n) zG9x}{$aIe_Y>|%!SmM_`3uf`FdKR+Lg`I z+yJ>pOsZLznIpAV%pzh_UZg7ZQje&wj_^<4<@e&^;9dju&h@j(6%=f z+AIx&9irMtF%osbRz=3pH%T5;Q5{;mRdJUR4=lVLEw0%)aNLCs; zpXQ|Baout}r))6>Y~DFEA|fR%*+qV86+v=dPaEx9Yo4>iUMqH)w*!(3#mp)8B}OZd zPigZW)?qJW*rSg(YW3lt4fDi9Ds1Cb%YZvS>|Y$jHi#|*4;KTKvKHl3)3(71$J*aV z+%F5+@wLP8eVJEY2GJaFVx#5KI;)wol zsI<^8P?d7OP^!m7(A{Gb68#|?VSd?vX}3b^aJVgJ_^N!t(H$DM1SRzB+6$({U%C(_ z=H2k);;h@LUSQdJHeIe}UVS$7leth>*R# z&z4R}N6a;I)YJ4c{5?#J{6p!1w3qDx71P3E;y7EkGVU%>sA|;tgJh!FT1hN^mKfEv zT)4h75zp%$_EmZ6+5&XeEVuN$x^Gvd2`ZP0scWg5bj-TJUGJurAR?sQ){iTkwX9ZH z*CHZ?M<`++OssH>eHR$j)u^>4OLZQy5z{tS#HV@m)Y@hBBaIp-bVy*8m!S z4WRmbW3OTRvc-T6dYdj^5mc{JO-X*5r>W$dOZr7IhVBl{ym|%wS!T~Vzgs9q#^G;nBQ{{W@C6S}%5E%un1Hu;+uxqZXt zr8Sq@SCZ1&nAe}B>u#HDnH{h+YgaSHn9=5|38>UkgP*1I9|$NP7z4IHA~f8u2S^%B|*IXA=vfvBXH*5 zURyTSksa}MQ}e7>;p!`6CT0Hs=^DhF(i>HDxpNS_TB|R_ujU){bvDc?U60abwH9t~ znZ~XfaQGBlDH!;vPf)y+oh?on&AR;}*zq`f?V`I8mU0pzm-VW#ZmC(UZTJd`y`pFI zm~AbqevUYU?g-2K&Z>WmRb55+3XP-{FF#3!+c@|3d+U-U9VA5<`DssCOw~L#qcyQ8 zUVf4-w7#}4NWQ#vWstT{WkU6I@$MzOI-<8}lK1Hb+hQ3vad(qsNvY{6X{@8GT#cK< z@>`kDeLOb8;D~OXdI!b9ZQ}iuKUltio*3MgTm2w=SkP|SxVr%s@*P~o=An9(#HQ$( zn4@p{Lpwtkjkd)7!dt>F6r-$MOJ5DhrFHcE?JDTKa+wD6RGuvAldD(c&AJAq!jx`4 zo!zAqa%5ctU;hBeBH>f`mGvBSEh=T-{WZH(IH_VqI zUA86q)Bu~ycv3AzFAWLQHb&os$!!HUzL*`UILXTvipNO~o;9rXJFIiJLyo2u=h9=e zq9bw4YDzi8h`D|mZCH=Ox2c%-(rdMATZF#1+RKKXiAE$*FO;ilb++0}t9007Fj%j7An(oX5iM>E1Jk_O_lMl3!fB02)k$BnqRy2kvIShtcy}uPCd$oeC681nqx{2ND!3eM`;XlFAEb9?tm79Kj%_0=n`ngI z@iH8%92nkbB3s|3hh>bmU;V1ilFOQ_2FMrg#Fqa6NzTjfBmV&B6}U^3MZu|Mv7Xi} zr*7Sw+%Al5k{+TG8f>_hOdYLk9l;cFred8W;M8|8eTumEZ2KFA;I}u4i(6=HwC{&a z81B3=q-e`?#xV}nwkqh3?J;|Kx=N%w&ziGrvKy+(=t+a^_Q>2h2JtpzL#icQolRU8 z$4^R%^$jQX<7HfMcQ})3Bhn>aGt^|@_Y#`rX)6}nY}{g3&5{X7wGTC0Jw{Tzt2MD4 zwvz*S$d(}4Byu5?qgmwM;rUHHq*s{c4n?NvksjfX@Y9!UZzRHQu@CEOLn)|duAZxz=0m3trn|#8NDSoEpf~ZmP8x80v_?zThpmi6Pc;2k~NND zyUpLF0DCnM)x*>)3^-Eto+-1$;uF_EG3FUZja*j9$*Jxzb_X87OK7%(1;nvt?ZVdj zY_j8$SQ-g`)~n59KBk5hBFA-y;~Nva+>SU3vdS(*V_U1XSCg6OVs!XUC9tNkdlP7e zUM^9t&o|+Ybxd?G+-+B7HmsAyv<$W&*(-!ak`h*lEXH^*2f$yXhqpuR<7eLN=d(OE zEZN@!mNyO-w_Xy8myka-b|bP4s?(yZka4|<;#*m=yBleSZC)S+e9*YE7$zXB>oE;-~V}a7t}=6s0zE!#{V4adDJ7TNO)V5)FM4GuluR(k!2)zA5SfZArNlyqfY4w4Ld52C?#*{<=M#@GR`mpVmgLHOBKj6f|cLX z`(y3Jch^lS31=w}Dpr=iaRzQ7`bM@dd}VKN){^>}Kz>?&QsO+Z0DnmK#EWvgyhe2D z84i{3(SrJlM`!ZUX-R^r>u%jG)`v#bum_BhruXSU*_*~moJ!520^=>< zsEK)Me}+6+P1nCk=EAdLC5PLQzpte7y8qMc`OLTxd z&IxL$$#NbJE6>uMhl`uG#2m_8#i**Uh}9XmQnP=hTMu|lZZ~anZp2)r<|$B>KPjB& z=}Zy0-ZvL?IY-A!Or-z!GBH}e>X4J>A{V5pt>YIE#vBs_0Xix`t-c+q3%1j(n z^q68#T|(DsB;b)Ddhhe8mD{LgCT+**F4`ts2OGzl9El_AJSELod25x&n{anMoBopQ zdkt}JF{|aeQu?_$%jO!_6c%mC?0X&hLYl{kr!Kck;TJwz{@wWy8@lwlh@0Q}W8@3pI!+Wv4 z6)`O3rk`$tzo`>Eo4}Z&9H{8*mZh*6R$O#cVvR*MC4QSnfh`6-D3zuZ(WxS4Q%w1Iq z*z#v=m}jqqM<Zadf zip6v;32!DvEaS{2P&+Fv;6K&^S&IerTu1uw){-V{wE@fK$+!t9kd|aJ9aMXEhQP-2 zcqrKF8D&+C*p+6;H!d<0i0fW%s*KfI#-3hE19wgj)$vJrimG=mgJi`*8{rX)Zx{an ziym4{+o)3Q1JWBUT+-q1A*{@|39F(3BqunmW2>gB>%pbl%PYN$Hsfn=Zkee`CH8n} z=>$i>4r2tsqhv5h)Gb>SmyaSTXWmR=b|x@uQ;riVWF(+9BIHwBxH+_qFrTbPgQW|e$BHFHgQ#9nJ; zc72%kr-XK$gWGL3$>Yr5VfSx17WYlJq1Y5lCF&VVS1y&TTNQ0fR%zt!hVkdz-M5+& zb9uZ~nzrHNeGRtpA#X25Ehyjk{{W_%ZW`iVXBHx91WR9w%R_elU@ze~g~#g_i-)ak zP@-JL%saUMd@N7Ic^$i+2qRg(&yR zpe$}%;O{x8S^FhU8CVMgf$1VI4=9f{CTw+(Bu$&rp3yG|&;x#<;TFrMsAE?d01j;H z-`(Z^09t@6+-&RITNd+|0Us>@Vq=&ajJLiP)K{j0WpByL-wL;-#^PN)HMC_K!4tuU zeBv_VTuUTA1X8GNMVHB5;I}%n64T9Bn|@Nebtzpq6Pu728dww%I%nYHmF??}fPBT960+ zRnu0LL|d3eT5Q%g@ebTJFA2pR!|%3;EuLhc<8KKBOXjX-q|R);O0I^6I6F7mMg~|; zDS04xQ+D%iWJ12e60S$ZTLY>$cAph}FD|=g`Yv52AmClzU^gx<=M=YWlp64hrPJ|L zGvK&0PJKDC#k?)!#3Wn>P7D`P8dQ&)&nE8t#ANNx8ZZ)LL zpt<@%vEjNmix9cUMLAsm08I^364R%s-qKe807z~;^D%o6xCM{=?D$LBS!b68%@#$~ zLUZZ)iX)5v0NWQZE^SGnd$wBX@Ml7Yde);nVkO?+ieA;QEHaQrxW5LN` zzfFvL55mU5%ZsH(MAMr|YADxaZqUJ%nQlVQ(;ICzn;5m(ybkc1dimsOl6zDnQJ2MA zCNOf&>`$coKidZOZwm`>x;G5V*IYPKBOx6`wW2W(!PZ*s)2}#N=MzYzN?g^d{Fu)L zwCxJs)s?t%_c0B7&kSp8<+zJ6&22W=?N-p@_-+$tbavJkFAc^aaM=#?ayo+5u`W_{ zxsRl3Jj<@8mffl7TP=y&;r{^I3MFvwNb;~H(jHwkqGV6F>9GB-b^T5}sDt7jpDk&W z7;a->ovUrZ5s@l%h}388I!zlKN#~hYt{x*oC0NHyeU#yhnsLy}+Dom=WG#+mEQm|t zu6Ej9);LY1#jKfR*AT=32!}se+#}(rnX=hNs(j_!cW;|Fu;lY@HYWEK5pz#hx~kXY ztL@PF?-tvu?$EB+7R9ax-q=ehy}(0PhvBMZ!+}G3$zKxfW3rurwRD@j3;US$6bDbc zs3%7F-?~PWd&j`TEu+PQPtB~V9lYTy*zgxO7=VSoa{5VEG4a*?NbMD3&f0few}Va z5YLp6OHjwdSiLnm5^~i~J^E7?-+dW&p_^->cM`zyA>QD)<*Sos;lB=iqGQVvuRd$k zrq*++ge^uq?+N;0V})|vyKX2$iAF;zmg;vNiu4(4Cq&oMw;IGRKesq)0zEEpMd_vZ zmX<59UUzfokO{@_Neb+#Z^Abn#g%+-(*nL_{whXj#fN$sJ8jB;9&? zYTc#GI~>FWNPQtdh)E(tpUpz`l@zJz)^(LEH;i8vuSz0c6uGNq>T@uTnP82)THsbJ zyi!V!#Nge>L?Xhj(hH1Q>v^i-(Mad3x>SBGyu4(|`g(a>AJ$h$Q5%Ei)mKlcZY0mj zR$v^&93deagUVWpl|H7XMZFcB3dM>!yO6wj2=i|{>!UWx@^i{l%FtK3zSX-&izVYB zsr*h$VDgn(ub`t*yqlg3qWR4da=4}}=j!r!E)<9d#m+RIrEHF>ROqj?!)G3HD%oAGMa(kk z{{U5G<}&hi`Ab!A3L5PrLg2~i=i;mLYDoO5IOsEY`($F_I4cesiX%%Zk?tePrtPAH z<}gj?7Z6QLPOhrT9k@4YE0v)}EL(g#qPi0qX(I0NQJZ2fC^s>p7rk4WdW3X{(@L$5 zqpL}B)3jp8nxjrkS6`&Bl_x(mbVB8SH84BXQD1D<2I) z<12iPTPR>&$27^ZQ9^ISLOuC3Wn6nmUdG%K0TPa$QQ@cWt@t_T8EXm5cw?wbDP}{> zsF7KMgpNBUWoD#f@AOh)x2{I*7k5 z3hV7Q$2HW6>|mp*KG?_ksBq>i_#nH)$->ejB`$7Q)hxHE&ruhcDY=SVJb94v5c3X^_tv=OsmtcGX)05=#J7I4 zcfn<-e~PuHhE=MpEzGNoOM7wZOH_3YcOtYcnDbv3YoKl77XvNZ+Pn38>q;oTW#rDY zx0h%&u$)QZZ%f5AA=O>>+hfYY+?_?%*cIcA-C0`tG6QJY2$Y1h>Mw|@Q9efXbJh#C zdr^2LKS+sp$bFSwRkp}OvYNO*XcyM5>L+K_tHsqz1@$yq-KNd=Y1o6-;Mb%Y>eUke z0If|mT!!zo*THu3+_aU{l_G;L@ve(3kr`rpd3`vU0vo3BYt8WrtVP27HRk>$VXS%) z(o|&#ex46*<#FlP})ISAWjsF0rW%G{i#_7;%Kl2Ms5n7!4g&iGl%1i$M z;rWPL5287X8>!wSB1o@I9%8q+mg$-qQVTY}q)Qk=CzkT(A?1+~R#915yN#ctWLx4( zv>&7!6UA^dw|f+HURM!FM@>`JRSJD8H?-uQ`fxntw|KMJmxPHcIjyv%pvu-e-p+oP z7=|TkYX7Z`InX)hID_lnBsX1*5O$#U1}V|d@GEK%tQ_`fw&b?b=#0K0Qr zUYOdaL{Jnc%WLwUp(uG=6tlUczTc^6M&5!>82piW02ydW+ z!%0_4J+wGpn>nJ-(i-uRlQ^TpF&S#oRkA%reMQ+&#)(-MB zFJO;~y($jV!NFZBQEN%&4ejH1&JEu1OxNo*aj!vFY34Rlscd%XyKs=X$XaE?jZvMO zD-c>c@Juxw`^|7vbR{)GjC~X1hIPR;u2r62T^=A z$ga+IMI2P{-KhFxY#r0imJ5dBIDroVe|NcP$L8WA@g++l!(E8GaCxTQeIT!~pn~O$I!o`RRhuh8gErI9fBG|Pv=~cehq8e4qDt9!kC#yRs~N1(!E{l5PdnS} zCu=((#08zA!ftFt_1oNPCf)cCyZNhRt#-G#)_OeY=LUWs^s&aU+h0TU#|k}87IGe* zYUse^glN#{a9^h8A!*gV-YyaH(XhFR1%ic?>4ulGw5TlTHDLeA`OiBN|k8FKh}s~_)tRM|Wg zShDYF8+{>f^?{5_OiM8@_f&s-ujU;1JvH`@TYa{UNy5e4)y6$p)?eRU#=qS3WHQ@L zsx2;J3SQCGl{xWyuzz(^QO9kzk%5aifsty6lZB;gCGkz&!k-AHsuAs$)j1L?q9P*G zCnNIK-`;Q&ofT`)*lp=>fM5fs1&v&=Kk? zuQ_-kD6nJngxjL!#}6dt)f%PhHiXe-A-e6~Y%T=9`(VqyGIBiKRQ%&VCZFYdo^x zyFuvqPio(-oNSr6F%ji&c`aTq_SLHH+(TvPcl#^a6U?{+=@UfZsH;a`NHgs{0|ncg zdx4AH{a#wj28O(vt1dQ^p~Ci|w#ggbOh3CK?;kB)?zS^!+-zFoUXU{MgyYjy3caGu zi{6mjWZ8F)s;}4^9B!bM@U}4+~gqwdVB^58~G&1GEUA%zh_jw5tC-_kq<6u>>7~eD{CyFRMNQ$Dx#CrM_ zUc`x9BN;5bG_u4S9JC{Q6mjhuV4TDGYTF!cYY(>~0lL&q5hpf!N0z49nKQOp0#`&r zL{3B$xx7V797umMIUCZO5h8KIUom*8_h?G6E!{|Ct4~jghG5+UzL42AqTX3YG4@th zSYvs{R`4e8r4lkS8m8UJD=iwxoV+5>FKh%~;Y1 zh4%+H9vMWsdTANj`oRb5XT2fD$g@J=L+mdtQ){WBO5zKcgDsIOgsI1*xoTQjS5ZQG zo=Nna`d4hAm!tEL!6dXSM6* z%H_g8M{Gn}33P%G^4F@pI-=T7CVHl3I=`cvov(c_yGXciZaYfG-oPwgZTxHh0JI1j zbr+NXWFjO{`9`=ppZhx4>FL|fVvp$_`axT5_S;$Zcd|HveyQ25<94FUg{{KUPLhu_ zc?b5^Jv21yCjCWLjSrVx+DpfacJ<@guP*V-<>s$Z63cUrU6}im^g#AvS65p{+x9(f z1~!cTs=!IkF}OIB#-0)oT|W}no7O(#ucpd1Jb(0>cCgOXeIXvT-UF+JTa~zzn&5(7 z$$rxLin8eupCbC2xM+O$hvN@C_(h;2S0Fy5e-HTAYhwo{TdscSb`P+>{W;h@mF)C| zpga#`Y#X|5k;eUj;|8Z z%N-zP%NvGti56eOO^EZ(%!8ZcUTh#1RtA zNPmaoqW=IPj$9j_3O+8sq*vBJ-xwpYy zhH>Tc)Mxoh#z~vOM1*&ON$seXT(k^ivd$9A&F=-2%kb2zvlJNM?aS);3my8iTvyG7`ve(T`pVk8yC}SHQ-PNfHGpZ{ZYWXm+lf-*Ov~yo5y+7@*TcWJBTg!9L>Q<#T&JF#Y+QRxnc2;{ydFbJ^`!Q||V+@H!Z|!+P;8Vmr zrA1`?H&Jg!GXDS?o^WpVL)xv?oz=F+9TA_;k^#@$KtOyy==##-PeQjd-Q~Uj{Td8K@K5u0?tk6X+n=)2z2wX>qlZf)imY>Ca@<~$nGSK5oi%x~nXMXf zx)g{UorPbM@7sp`Dhh~-ptOLDj)9}2b^&SucM^<1mJr*WDT6XjVCha5Fev=ygCjQeHLy5Ltr8h!Xz z@V!bxYwgBT>#dDCEQA9u0s$0 z`cXL-+Tf$~BI#Lv8@)iwwt}i;nQKY@?PO=Rk84)4B|3i`@Wv9YuBNhHepwtd5*xfd z+Ujy}1c1}u2-?D?F6X9*_7H3j!l?N(k+BP6O_G$zI!U#dN-``t+d8n44;VjL|ej%b{NcC``~Uv|gG*+{Vws`iy<40wlqmjc1k z^`3SKh0?H_KUap+i$!fAuGo@BCt4%_$f*DgXY826kp`C1Rb+;PfH>yqe->tJ^WNBP z61qvoJ{FZ*=#MJu-@_?|U#VZnS*NW^V%ccIy~CT#$fc@Gx;5}8OlI+@%7JLr@2d2D zzqx0=`&W<}H(-KZH0c&?o984)+kBBBP+Uw5Rm+eY{UXR@o_TsG57*%cP?ZqL=GqfD z$wbuz$#S^+FlWqMhE7;EZU|C4aQv&9=S^q;t=w2zRSZJ|;K>i%cYaNyTRmJ83!(zS zKLSS@hd;FJ{k(R8=B#RCpJW^hcmAzXl4DTdW-Xa&cm#cM8p*Ns*d0tQ`w0vA8dDRR zunH2W&d44pI&&zqNSSMdahF%uhyH@Ln@=b<)!;^J5}wc>^9EUKONt~HmW|DdmNx`= zBBY2zZwVUW3_ClPr$R>V9wE(OhTcH-+^^L4@=)qFh`)2algo@z?kk7O>RU5aynjG% zx}Q#ujeHhkJcw?H^GlAYU0DC>c~^C3fGn`4WX zuWpFZJ*=Xtn|oVRW;|Ldx;a};xMsc5 zYv&7A|G6v^9T{-lMz@zOCn5UA)U1|DuZQai!qA&J12Q-H%|LuXR?yl?mQHWIAP)|2 zZK^T#tSoS^Zc5tBSUHwb<{h5&58V}|^V!b~9MEzNV~5Q+eAZ1HKQDCsFdQ6{EN+v=$&18NQnE1! z3dQ>}VU;-J8=d}?>rPjoE4Izp2j~s(MstEY%e*Cv--}aLWE48Bl>il%=)^z;%2%BfbXl00d1u+RsS6coktTyr-{ZGnD(*qt4dI@)r z95V&m^TUXOGHOHVe*auQNjBat4yQbO9Xd}(+oh_|Ea@0glw^CqB2I1aEc4hTxT{o+ zE%3$;A`Cp1d!b${hoCr%-$A^kBBX=bPjV#B; zDK{+qn7w=e0`XNyUus{YKl;<%Gy#jn%eH`-=x%3(DBa$_R+mywtC+ul88ZDa!BQA92!RP2xN4)H`m-~UYDR*QT|YNj8%p` zuu=KMS^p*OXU$9?rKHKpZxTcMSB5VNi^G7p3w504$}fd)V=c=KY!NWcSV3=rlcHdB z{chs8v!r)NvO#WF`I3k`m@UGkJ69~H*2d)-8fiEGT`P@u3kmSmAXD57hNYfac>Mn1 zJ<@}ua{&MiFI{<2L9E*ys2Xn#HvHf!2OS{u%?v&pM@K)*-X+7c!JVwRvzZarHzrNF zIu-A>s=fO{q?MmzuVN%uQ_`;t5q!gY5Iln9iA(wfe7Wi4@JK^!m-4(AaD*WJisNE3 zH7u($(hPa=XDmu)GEN{)z~ON+!7#h*@v)G#UH08H4{e9hm#8XN=}H>4Fl>+zagBRI zQ!Usm-Oa4>Bee#8_M&zIa%P=~6{HHxet6ChW&j7B%9W*&6aYz&^-&8CT?PkLg(^38 zYNfW%Q7D~C+x89j)(Cb0Z=P#WtJ(?5#)-yuhoZFq( zY$c}9!1Yd_#`)^>10+lT>1LZuHwVF^3w=d?!x5-Te?DX0*=Bt!sioa*hgshKn3}3X z{$Wp-Hx>bhr$HMW%Ce8!wA-BLO^rWQlpjbPn49aBZBp0sm1LrZ_ZDb8TNNkT-mP{ENxFAmelOjZ0ISk^Irw?VrbBP~= z3~{v-h$?-v(gKA7+Etw_?6UcqOptu$E(KB*fB ze{+w;fR-el{UhsApAFLD@-|?%i2iq_F0DmcJ7F1wjVr&q^7Gu`JsrY8xp=-=oZ>+HlM>ogleRpG8Xkp&%RG!O)+^e=y664M z!XQE;Xx4JAIDxGPQabIv0!gW?Iyj>_R#qeGya6gOk56!+|9v{cm=(nr433Us{c5uI7_tYxw4X<{ZSfCmP`yQz0YCB0sx2!230za4*-RNP|vOlnI6XV z(<#9eU+7@DqsoB+#`S$(UTsf(1K?4D^t&4nJ^jKom6^TohLwhP_y6$U_D~&by=NDp z{Bj2Z^QbMstpC(c&5GWXaA&|q1rhfQb$q7g=<-xlX>w?NAT;DO!* z@+kD}pk4d%>?=J<4|G!=B^J7{b5D1%zH_)_0~EXJnwzH#O7ODta#&7;#LxmYL=;nb zQa+cRqU5@XU`f5QH|TwFTZKD-TlBNMe@wCovM=Cf>y$@6^r8m?6NQzhC}wlm?6E6C>Mg`x zS!$}P^T140?z4FhY(M$AwKW28%!O^h=w)ts!?87NFljOYx>(>^Iji~Bl zJb_xqSXCrSy`dUsegAtmY9T}Pfr!2WUC_>;vN&_8Z{rI$e!EU-x7Yr8Z={i9-O9US2uIep;- zgR9@W%lagkv_-j8W@S%3L=#Juatg9g;Ts$>iy^&&!?E8Oc24GW6WP}1nuipv zz@aUE(-&#aZVxJYQ%miJv}&ND@_XIw$NRAvq>G&fG8!L1@D^H7Y(w}u`?l0h%Mk6q zmT+aA%E(29VKRe64NJ0V?fTCs^5DtTO!1Y_?N*QJoBg+YIDYea$EoKz^wYVw+^8#Q zYuwnqm`|ue`~3V~EFGR3n!U?oF;?Lw_KOT$A3e(5G|4&&dGHBZnuPuw(Ic9%i2?3=TV&XfKjrjSb1s!JdJxbdDE-N$Z!gM z`iDu`$XVI!TpQ7Rb6S+C*;;AVPA z{{NR%rdQacO7v4?r&Q4ex_(P^Lg=st2)J+F$7%pnS;+WW>4fGO!BGF(V;}8J?#O2A z`nzU8UUInwQ0JE4(U&^rLmIKl$LX5Hjce8Y&i^Y>f=@~T%~qYy*hv85srKk9!izxX z3dB8@GdSU2Pr#FlPmFCYKQeikAsnKrwd~2cC9VsCldyEj#Xqye!)a_}^Zn}1B~nF~ z_kj*wP+pr?%aEZeR4`j*RbMExu@?QpxHVDI z)ZPACa*%NK_%E$RuMJ=J(%)+PT5k;+9qjt+bBKA2$}yc;9B2unvSf0y_dQGnV}4GY zX=!P}|ERGCeEaX{pCzfhsvz9r7qebU$2J^J5zYN87#$pDI<3nG3nCYG5-x%~r#^kR ziLwK0`3QV}>y)te;6$+7OY_6~4{n2K*(R+YGggB8@4y$BCBd(gFFLUx{Vp+-)_*A$#wK1b<(zZ}m?+T~UG>Iz-bP@wwwUy`*g(Pl z?{uGc%wj_zBaS(GjEEh}in?LPneH||YM?J};5}peK;rb~#W@vO$Cm9w)5HYnhevB$ zsRl#I16&_dCl{cVh17}Qe?FtyN|wPA^-@-;%P@6Td@Ig7XeRi7H^zN=FJ0~jJ+CjA z#`=#xzB9x@$#Ijhmd##sm{DPj?sHwMyyEAThe#UH5*ehz8~U%C#Vt(UKf6`J>z58R zJ+fPx`XXkB^Dv<^)KHF$nUr~tc_4%Nub6<*IB$97rmVU$9Q&M**}PyrxwKY{ z5k@$M==|6A1pxnr{@doID;Mr&u@>haXYaItBGyLstv>vw`Pr1HOnEX7_mU(UkkZv) zmG_ahoN8=bVwY8-RqE|32~3IgF=NhvyT+i`|5R#OZVd&L;4d;_T53JK4-}>(;~2h_ z{M+}$y^1!Sm9L$tXC!!84~p0Wge2NnoHjTaL>kt+u43cE1!~1<5MeK!>l>N^2%FZ& z5+|t%8YjbOOWov_#X58G2v3Nc~k|q4KDyrFk=6R4K-AZvoU!wRLU1_(ZI^CbpaK#4vAq zcX_H&G%fk&A5fJ(+qhhm8JP3G1ARq|jmGLCYbZU2%lCfUvn~wae>V;hKGRz^)c3=0 zDJqr4e{DH7WQ}$+pezmOaf&`z759vL`-|r46RU&Rv)n@ZZEIEfJ+3;j_D%W+T zc>#U^ccnM60)&5%xXy+i;}TL%b_BgL6tZG%c5-pgk zReRhg{9|^>YAIs2+}rt-foz}n{$LG)ahjlMiI%q$*G=XqcwmI}yW<*xUr&A}xcryI z%Ckrh?87hvPJ;+DPkUgG_5oF3X#w?t%w_pZeT|Fl$Mq=pCR)!5xG^4{t zI3+#mQuC@4Iv8p$_P3}#gN|P#9$@Px5Z{`?n+E`c1c%b`Rg4dU$0TzH@aai0j17*vg#OoJJ62dEBye z*7Of3VIQ{iW!{b`JTosT-7OcLUSKWLaBzalY%Cp+kKUYMKg@%x_3nl^_uU#4t zaS>9Qi(~o0s;bTs0ga!st;@)9xs_6w!^9-)&ePKcX-`%x&xYzvtD<~vRsut(t`A83 z@GyRLTvY>7l6gJ`hBn8AVP0InBrr8x7q)cfO8r$;gpdVSbGBy2b6yI!$BYKKg22S6lWKSB&1)2!E#t+dYJ{e$W5;oNv4&nH{c?~9m{0mLoLS1DEec|9xtO*&4z zHJ>r+)h3a@>F?*afil9dk4r1K{DHi-Vu(SUAKJomKO~>lXx8#5?ETU&yK61NgXs>D z7JaaUF@2KJ1KXEm42gTLKXYu0AQ1qF^%R9--=h%`TOnESa~zkLIh*pFp3b9q(aI*TO@?S zO&do*iKWl2?K|{oS;U9Eax`?vK{N z>wi-ytujCR-;H9W7rL8fSvpbb`Z#gVfsEkJ5L{;A&r6tlGxR$wv4hWB-EQ*;FtfG> zx$n3X`l9&O^l;u;@Es`Y>5}`9(IJ@Owkiaod3H6*`0lQI!{j6`NCznU?cuD%mL;F| zp!;$14#0bH&>^!Ps+O>@4iB_5f*p)&^p$};+5Gt3BtQGEi;HrK8dXm%vRX9X9(qxn zcC4wBE4PZ0u%BLBGT)ODqih%y?PMRiL5ZKU3U|XWv=!iD-;{&uQ|H$L-t9iBL_WES zmmHctPawGnSAPe|kjdtBm@`B9fU(Sq5cEpf0{D~CZ$862dOcFau;tRc*gk8`YCJT% z0o0HYwoB^c{{VX2$eiIp4Coo5ceG=ID?>YycehLsahAFqP@*Y~r?=~>2(f)?w!nfG z6fi^H{%l<$B!YjSs_Rz|`hDU1995#@c;pOC>e2((lli&a2*zR6p(vg7rhlxPfzFkc zKa5M7)HI1KGSTCaLz%y;Ow0I&yM+`W?t1u%ca~rAe^ufG+%{P(kF&E?n_iKc4Gj^wjYjsdVkMe?`w*Xz62CdA#lUCNtHZfK zt2X~P7^yUHnK{gasX7o!{}6Nt-=8tUuxg)VppI@51_t<_B(#LvgxlmeuuE+`hjs=1 z)ql65*#DyAz)VY0Kij8W5SGVf81^wXZ&8-2Kwpz!JdsjO zANA?k(>?;f=94EelE#LuydhTpA{iA}RRU{an4HF6OlHV zwl7d+voVJ{F@GZ2-~e7O(nK7GfiklQY*QswH4oo(VS$_ROJzEE^r@Nl%gQg`$LIUw zen?C3ft`45#IQNtm1tu}@GAo{+i-fRP~Zf9dS83a#t_3ec;vKZx#G*5(7q`-o$>h8 z&9E|KT*N#O07I&_Z*A0G$u;cGB}mAM_%d)ugKjU;Pe|m+SHqHSXuHQIH;KarMt9^mzMLEI z>j7113-VR&&as*L4DX*w3!JYu{L(K&5Qd{3h|5JqFi{)Hg_WvmA!>in6Zg`K#K&|) z$gBQyA!4%#b}I*tNKuCfDdNQVMteeDOKI!w#w{D}RS|`Bc95#II>2=#otTJDS(hc7 z_eC0K-!M9CM2zH^-AD?UML@DTy+eymGmQ|VFxlI7#lA2qP^0q@o+#^F{ zzY&keYxik=7I7RpW@4803>qBqePq=o(qvqO`Gp9!3W5V3t8HA%Isi=$r+SJnR(0H$QOEYX zsk{lnAKXmbD$`AeqF=x~IOq%os&x!9H_BtevN}ku#dbubve$(0sQlM`P?hsM8M)0+ z$5+c?7zxDn2dM^E?cmJgN2_p=`d_ECTGN!yI2{xT%xK{gwq&Z1?zcYIuJhscdB>9l z1C7F!jP`fcEWL0Nl?d_Kb!;Mb^D*ef9z0pnZ+b=dv}!Z4kZ<0O^XT8#WxKDe6t{z0 z7GUh>FN`>ngoBixlAbq@)szqsd3N*bqtbMobhN;51N|1@=ie{s9V-nH*YX$&i`2)e z8nUMQLm8257oo3ctrB++K!@}`$~xEkSv8CK>a@+bAM>;Ca4A=zOt%N2r&_D-9#2() zGJD=g;Ruk(YE0JF$jaNxEm%|6s;l^8`!I;B?tyEiuhFSntP|;VdPiE69LB!*lb)^n z*wF3Po&1jQr{W^~PGkQOuG86YMac>ZczzVt+?-topPY<6r57F>XlV_e;eTQb@ap8yp1ytrh$Yr^!Tp@tD--Tq5&xsYIKXuFOz{(ui}+x|8!{AXVqbe_CzXw~gBQ&!NMcB>%htuse zE@nYK?RRcGae0&yP^%!u6Xgro;Tl!4v>cDiPkXd@E#2@$=Z53=+<4mm1kTOIgAz6@ zhdMV-^W{hLK}lb)w!Hz|oMxLD!Hxf7%V`YKEb>^k&vTJeU$hlw^JzY%;*;prO0ws9 z%OoEq{CHYD(-YjmlktWA$t-0_5hk^}gw3uImpagOFR`xrNrpoAHx-a6STl|17ZdEr zn{KNnGzwi1=t|}l@mySrq0=Iffi8kiuhAy%^ZkeF?OoxYwj-@+QG+Q75ioFay+SS3mlnZsn}Dpg-F!Z1(nAg$EBLc z-t+3@d>CAyK-e#m7L;^&Q(|^okj7`C(ge|r89Xa}4bM_-)pQ|lclbP z-MDn!yyx)2J#KTACae{WJT`tLEKPy^>G@<@QK?B!7t?y%)Pwr=P*jwicH<6LDGrI% z>49Yyh~PppKO@ka)j1vuX+bFc`(RVg0))e3GQ%~0qJs^nH8_8q8M(kyT&7~}_ru=H zbZ_Z}bby#^gD(9gRe@DW-aubYd*wt32M9EfTq4g)977a2`38P&Nx5{+5^v}DTQ48B z3U7VPT-RsGjk{A`b1<{g9_+$1+PLq3 zsXr5^Y(OvQg|_O{8uQ0rTUT@OG6cQjloU*Cqmi=vLgHS4lu%&2*jKcF-D%=;lued) zRC97&Z~1-qXDu%Sr%-Um?Da>V@{uI-$O*O7K!XPmSIbwr-$t0q(+EQY)3Wa&qd40& zBI!mg4z_E>EhI4%VX35XmPl~%mDszOozRtk;Ux=^QqapV5}&rgYxwNbCAX^kT!MLC zyZF)-bAE6f`=I>0l}qN=4N*(!*wb6S!B34swmzH&>8xnA2w7w$9=vWG>5=4B9kW9Gqw~-RQtOeInV8U2VfMU)>6l+xsQ_t;(Sen zs_h?%EGH48>FMZ1E{6t;pRkHTN<3slTKUdOe94mPclWNDDLrsWs!xtJ!z7uS%mdo( zRrVlB5+ipMN>p@w#ox5o`sRi&@l6~k;a5ok^H6FxCkGFYWd*eqc(Ug2_{yRy23*>y z${zb!)RI^Uzw-lF0MF_cEiR*r2Ff8KF6w+amKEPtSwv3OJ+IssomWVEGKg#hOF;ya zDJ6aWccTjDM1)e5HHI2|CmnF!BgJA1m-MQgWB((BuaGvVMmR$trFAhX5+{jX{*t+D zufM-3BG)xumJPv@0xFmMkEOkC+j?kEqK+`&X2Z;W2lF?&7~#q=KP8uehFCGtj7_@T z9lD)5HSfq6AK#`c*#oMJN}@XcK(TLdOs4i(pPAWBE7U2yUzfnQQ&Y)Lm+o#e#rEMF zS8wFiaf_>cFf5akE~)oo{XeXRE-ZKQX7|Fgg}ZKsZ_lc*GpF7SXNU+odPxYY-*5-F z^~phO?#^zw^_57d@KV{olz{vaC|KezmNs%D(BpZyx9c0>H0O_n)c%_PEC)+L84UzA zap-v@^tk$Qz@4S}%LHqBK^kCc_4^g8*w^KrNwl8_aswS0i~#3G29KSuqg0@UX^4Q2 zA8kXt?|*J?(xA-#QRf~ufPXVrSuWy6ZP=nfgejp`A}?H3*|e5?)i+uTcKg*dI~p@o z&B~S;UMxwbjRy3cRfn^AF(obB?)xb&&9=EUP!Sg)DtHu$Cfwp_={)L`-)bPqE?#ca>s<9sbu@WavRL~% zG(PkZ(Xo%X?h*$xWw8zJYGHG#_FxgDONpa%R4AS~Hg(%*-l;YY`elMH;L~tfVE?oQ zc55znYD^^F{>SlkpHriauk%K%q1+;^ER&#)u3KvN$FrY?PAB=+oO9MD#`YwMPC|j=+hql;&=%@r;I13`l!zCak{nN zCkb}AP7vSTx+`5h=l+w0&dl;@I-aIPBNiBFa-<*cj`-)OaHS->Io#UxzZ>blr-$6w zLXM7zRz?@c|B;9ip#^-Fe=K7`zPN~>5EI;vBE?m1ZHVhpIJ3;eBOH6)8j-ks>&uMD zGEKNDa_cCm^pE8Fc_;Uq8W*M9-nReU7&;67>V5f}SU$alGbpZ`zU3Cc>2&c0$LuKZ zvdaA@aAe*iN70ahW#i8=jmUvm^k}mtc>HRb*;koDc@QMH(C$Bw5^Z|SCct=(A zMrC!UgVMn8_d;-SWoFi5K|_6aDckp%M43p0oq6p|5jc9n8l3w1TX|+j5ri`yEmUw0 z;7d3fsyncm{!#yK@=+B_9#p3-k&&L=hw$}QVbFvuklhkJm)1VoL7v*MmV`qu{*vMpa0x4#6#UsyO zI^kTk6;9=bugPHS;K0b5SsU08@R@$1{Tb#yk2`u@Qwh3S3gT#I z%h{hi5{;7Uf8c+4IE%?=$<TBuedPvG_aq-D7*;PJh9{PH%oE=ZG{IDL~Cl+pIS z&jeTa(~n)p%!=pt#~=Rgv0<_aK5>56Kazn%&G5y2mG310^%~^OK^~>KV!Wws>78g7 z>UPN|o0e_R6|Fq_j7#=B#l4V*)mSg2zlkDf@Wee`!tIr(?N@q_VINRDQ%;jMI%6Vs z?_x1_61u+%nA-HzGW}qG;k@-Fw{O@F zDUhlAz7Ev}IvL&#(OpK}Mv4qazk1UsX7*-fKGH-@O@y#fdzZUJ&Nwc(%d(|1KAT_9 zNJnHbQzTZ9C%82{{mOoOgl|VIpiL?&b#izddVsG{@_!KjDX)Q&fc@d;hNaqwLq^)c)~*5 zz2*fN<7h!__FS3d0~-$8n4=lsI(z_%UjYy5j7%&J#^!G+Xig|>V^?jeICTbWE`O4_jO$$%2;z>Z<8p0xQHFuTP! zKrBS2qy=eGEFx;;wo>?=jeqX}6;*nw3TK~THW@9Q?c}ji`<3ChCU|ynv?=o(h#GTI z(zx=rk!^nPuH3!T*gYjJtZfKQmOm=%>)L{(KHdF)7v9!{><~a;w3EU!Ejq)bA#y26 zGLD+s`6eA#ay&-x-B8i`j!RTM>N2!9@i_xP?5O&^*9boEr5ljYvPuUS;_*-$%`mrb z%Mq?-pBT9)I?w)qHKf>Kl120BVI?hb(3t?UX4|gipTjqy6(ykfVtdm!AsmlfCnVkY z)T{;KLmG-W&+oA5vX}Mh&<35QI9ip+#lOT`KVHT!JbHACNK=zBaPvFov;vjGKd71} zUb|@iDJjK^y@N0{P@{S;mznXPX@GcG(_+TIemgvrbu`za`2;S{^ereNY3e zT*`PK>&JJt(T2188P0=J1xVg&`EJ=Zy2?D%2GlQ3l|b2x?_CQ!4`-d6G-M{wz*ZQ) zi6p3wT1!@ikFUVexwH9@Sa}kTGGVQp*Xr z$E%(}hU|*07|=9t&bY5)DflPWYMEf3V{X0b8jsD^Z9hxO**!Ji&$(O04}4?E<6aWow|VJou~PA^+@w5+$CHMMN)tR) z3H`Hn|B?BeoNnuS8zVpoGuY#=&HZOCYR==qCtlOilKP#NpHvo)rD@~nT#|Ddzb?Bs zn+EaV8esY0!RYrL0;l(N=N!2Njjiu4(9(#Jr`k-WNVIOYW_5J}mc0Q7t3U7MT?2k& z9z~4cP%SINhj~p*xM(kyeq-HulW8!=45ga-1ap-3a2{RArUj#SGINGAA>=;N))VP~ zdp0$$wf*VMso%WVu8&`t05-y@wpz1D)h-U5DZX2Q(O024VQ&vORK-PwG|uoO#DK(* zjly?Cp95fn94yCxl~WU+`)MadXRXrom>@;+h0;_QDW4WiGqX(h?`w2cx(|n;b{>=P zwg)Lu@veD{q~92SN8g~tToqPZh%_=}R^cifD7y@9zLOmUQ}3}34tj7vo@$>}T>9!a zE^$DE>W-hO%8N)-FR>-?=KfT>gR2?O1?p6UL0FG}nC?wCah(ZIo4m`1gu;J{91jv# zI$LQ4GItAw3>_y2T};+@8+tk(8wr0H)k%${$^yJ!G~>~D7emZF z%U(7!7J7kSNrf|InOW2`=5b?m;6x)Hy5T6-4t%-7q>a3h#ZOR3#Y0BuZ_eC)C*`T#$RY9@c0=?gULTK$HkCifxVT4;@y`h*G zFig*Bd4_-G?1fiRmCGS@!_#Ex*a|drNKifm4D~sc+Uyb34k1f?8EaLIAe()_4nlZ| zWt2`;9k5?Yu$qZ@^GRGIgSA!XoT`p+TJu!O;qUaY4vg%Jl&VonSgM!(@_e}3Ajo;& zai73+KyZ}bVDUlf#P%8IKDUnd1e%Vzm=im6Yrjd-ZIoA(g7vMBd<|fUG~})IDpfyp zf2&NgT1Q84sP*xD@4+$)+4^a{;=I_y0fq)cLC96AyKsIj&5&+X18CU}@BI`ptlTyI zRnJd#)0Af4rU%wDEa>_*p{)fZ74N_gjb4Of1ic}238mIlmls)z4BMw)OR)=J_xFy{ ztQk*Qxm=J5scMk!m@u=EmOg1}WkKf0XE^S7AIjHY=-Rs4|ABCJA_S+k~ ztaLPe7?L!!EdRYoljnK+Q%vY_xes5vf zMptDcWDr`!eqLI90_MmOL6A~?C9b)DF^`nNjD8yR!7lD{eOP^n7_Jnv;$1+mfVZoC zb;w;FcE>ReST&SV;*q|54-hmkJ7bA(k-PFJL?%DI0$Fafzm8pRhdDX1UCT$}`ZPWKai$wmxv1Q&L#3rJK3?;%Q+HG<+03HJ5xXFX%Enbnsb1w+QCp5Gg7|h15`n#Bc_E;RK1hhw zQ_M;laH$OK(Y(S^J`t7%XbBq3=qzw$LBDTH5p}2TGMm-%%U=*j6#DRb0kxmk zxKWIN2&4U3qg}eHn6@RV_w}4rM6MNhyrXK-vTLZBu}&?InYsBWz*%rm1+T?oQkF+l z8F`1wmF_zDHpAwbi0KtYZv0w6Z-Acdr*G{WkMFJWkX@rIyIT02f2`MGqUAkA*2F7) z4H)6h@fMdmhB4dp3^nRtb~z|3qw?izRMRYE2?MgLw`Y#q9eA>)3B?=OCw2^T^GD+W z1a-M6hbjcvQ03~L7@f8r5`c8c#F1Z9m52Q8%47NpCvYr}?Ag3Ql4W}QGSktr>{v&N ze_;^f4?3sD!L~tOKap_CsBM ziIW-;qE6Ws-~ZQCpo4j?|FFZ5;3JPH~7)&)Jdswlf98xbvA5SPVc7Os-pL4(mCqOZa&9zYeUaD1Xi| z1sf{HKU7GRO%3Y6=!}#${8e)!)$|-quWV*vFs<@m)Hl~3b^+Cl?-mVe(*}HO!U7Ae zpnAXUc72jXRD&xHwyRBF2r;gvk=c`8;OoBBLMr0nwhc=^6qFM$*uc9hi8FY{i4IVo zy@wDZWu;TRr$SZb=b^^yKXc26d%Lx^wDrQQ%qt%v>dH@MyCBm6y%{wD2Fz^8P-Y@c zZm$NPJZd22@zLI<$|;CC9~x_>^yB_ zSz#cSZHSuNfgy_chF)Z+%H;VU`tdNsvn7&Bv&RxPZgud2)~>;7l5ixvO!VEie{s+hWkx1w>a!R~VS#3IZj1G)`0CO7m^ z#U`r`mVfE~rDkPpwtae_396J#tv$ro*aab{XFF^Ow zG6)m+P}p1xXFan9RGzW)J9o0G{f*h)S9Jh$0=H~b-k0(%f!PO=9mOQf#2kN>*}S;R zHOUhNVYiWVVPFV)79v6lcrMnhVKW_qjN@D-fRP>tN;Ge zh3Xy@;|wlmFp0^oE4d<)Yjb2G+wGBMM&yfRUEkj^MK$j8gR>PIl?Q8Do<;At&QgiO z@-<~kPSww>WK%R70k!RPf~trQ32^jdN7D{gZHEyJ+keUL)i{fX|6%*Q-s`s77w#=g zg<5rJJfzT#kLE_pO4)FP$M}{U8OLySzI|7?y816fi^IisZs| zwE&fvhwwVr?WOVAqx%NGK7n?AdoAWQEn)@ze-v~`C+Zl=Y>2UvEMLq-CjKx9nEQb7 z*Nnrl?T-bKlfg$^giamZRS(~k1XVT4O5qUamVNKJ0Mmv#mk`lqs$M{ZLd~c}5XMYJ zXseLwx{m+w=l9?izH3SX782CI*7Jp9O-jg0wej)a^G&&0J?L6-&tZNw_EfP>F?N-% zaV#i27qu;CFZvvvJdhu5_EC9mkKnCDIW$B2(5_(Ye>eK&L7wrXn3wU)w)3Ru&YgZW zE4zUP_+g}O@t?Uh%OuvowcXPf2exTxyhAU|r1N^V{D1;=N+6hE^u0QHgK6x=6>v)KS; z_D_GgZo#Tme+8}7mi-E*l9|t7Jqd9cs19B6v8W9*TIKT7_b{0d-$|AX*wPPH7$*X0 zm=+8qCXRg91&TL0Q4JYZ)$SNq=;!L6)IUj7M0@BLxJEJ}Cq5nYx3t`RG=R*ciy%2Q z$$xGkSYroZtSMx_te^uH-IsSMiwo28-y3s5O_#m@ zyD^ZfbOPRDJVL4}RGNc)DQ&ookNO3GRdz&p^M$ zR?es{u3Fb%BbZ)+smb28<|2D631REafxlM&qC(emtPVmT0$=3Xt338&Dj6z4Th1<2 zfMo>^5`$hcDdpIx{H?#@zUcV(Wh-XqA47n!cT>=MGR?KtJ2Q>+p?=9zc0IX#NnQJE zvHL$<&izP=L#h9&**O{{X$6veEKhS9&cxO7^1QI4Y6YqLbkk~9|9tJ8(?wN-%o|rv zRQ`8kO=+Wi_cwFd)0N~m@ zA@9?2=1h&VD;sCo#}!GuW92V_9?3~5_h-;<6veSvwGO@+o^@4|Kf927Xk*ZziaWR? zD3{Gw%-kqt=r>w56nOF+jlg8+Twp!!1;&vNbjh@CGDN&bKES=u`=;c|4p$Yvvs~V= zZ0xW!m$UDgM8A!SAZ(uTIrQU)}?G()v)^9Z^ zMO{{-S8}?M;OwpNTyQrai6)|ipqUb_3dN%btjW^4#4i0J2=h-0SkcR6P8o!FfBo;**uv+91O*x}IenCn`wS{em*4dr*rwzxp39V;&-5QZ+*h9a zGgr#+ta>c&WMUqSgWF|hmFNWAPFHOvPgh6$(qceb&lIFC`X19Rcu}E=i?Z0Y2y&_Yh)LAths6^%yddkmb9$|!TxpRQLcu~V zY~4QZgN_!p_RGemjFm#Hg-_`JZa}*=VJSUvz-Q6G}_R`?+YRyCRVyy_jsRBc$N z#Ff<#@$0(BxCOl&X~yMWNtO_qqKiY-rv|Y300GBb+u@GVYEKH|gOsGii0e18m08zt z=5$T^rilq%BIL!4dshRvtn?-<0!**Fay(*$1lBvL!>474bujtbx-w~ZnngK!E%!jEgVV=~82kM3z%EhS=DQj1T z?B*1+#=Er635=v0qj|pIBn;EQR# zD?^6p|J||r_jL5!-6un$Y>lNFg}e4&I!Rj9+dC=~INzI)^OyHuIzq0U;LeNvnT}0= z{);!9AzsvLmS@<-%oq8CW}2Kr%fBYvui&wwQUv$=D4~P44{Lv*Zh7sdk`TC-M+g7A zgTIcmK)dfXdzFn;E^fxO{220!^6CJs!`2zU|AC)NEtRg!>b)f@N2ro~I|jU)TK1Gu zLhIxIxIQ$$Sgh&b`miaFkjMw8I@-P38i>K7_S zJI(Ola`@q!I=&}Osoim@I64RRea4xK}W{Lw{`^ZcL)gB$nC+h|;JXxPn_^=QB-1T7{2ii)4 zYL|=mPnkujHUG8yO@YRWN8UToDyKXG%zA^*pLEpKP5d5Ma?st*g>O|=tG!WV0sQA} z5_+p>%@w@%iQhet{B<{$%JNo13LbUgUwP&=+-Mnu9{x91PQXXH!K&_&v0>Zmy3($H zSUulm@?uD?(>m24t{&)n1>-kLN-~CrQfX7pzdHY%wFXKNJ|$RD3nM;Vg-T?olNn*U z5WjW8OR24;y#km~XkEr102BASy~bJLK?|zZ1;+*0`ykDh($t^`d#nt~hgwLVrBPp+#e_bwA$~P24L}VRveoCq8~k(yV8ILwX&NT zt+vTAgNmEAd=~#Q&lO90g-X~2Lm9;4U}UFP@JVD&stQ)g#-hK_sQI$90-_>akoWbS_)h>|J!`oB9k#%cUHJEd0amOM+3d|t)mB7aS_XywHRu49% zOfA*TZiK2?m@_V!EgwO_aIf?mwL~ zi1j;$(TsjI#iT6duyb*lN?xwX7!Qwh`P;9ts{OqrfU*C&}ciT zr4P$H$bI~8?#fYgo};u!ww}>}q=qIuy7Uhr=H;LFz1>*;R!;rog13;ThF8IV` zu`UUzPt=)yZ)?!=hb;!G9n_o zi4Wgeq0TiOE-9NZRd<-{73CB<+h7i`8&0gy9)dPl!gy$ahcdP7=EqaJIO(5D_2V@S zd|7PJ_1e_XTqBoi09_fj&$(=bX;&4J!UheGdT|3Oa_3e|*wI&1n;PzLVqG{kOp!#_@1uf$Mt#=tzYUsIlQyk)8p_-gjS_56J=&)*O7`0)`Ip5q78xH zaM0dnM*M&m=%tJyd$WRbH|;Tg5b-JpD%a=w$#!}6kbofJU75gXni#&Nwzu$>w}A-}s9&KZ)~2+MKnK6XmYL^rQ_|xem944(dUpIDPcCTya;6ASiTA z@_|7G{h4f(R#y9UE8(k5s-7OnxyRv?;$jVDoDuC9=Ga~4wUe;`t^8wUrz}6| z)B_I=>HqU@k4CVVt<_CGLM*&51B-Ih3(;|zbISCNlU=AhTof~UcJxR(N)syDnJP5o z@xMFDaxry|Hb`f(H%i1`1srY?hLaE3%vBnLXXVh)n9vsrJLngKXu z{P$MH0@vLuJ8l!L^n?CWd5%aWBhvFq^8wsp#BP2!D~Xb^jv!!o4Tp| zn7i8d{X&lHgrv3))-66&_^-X7a!29;2L_SZc|!iJyFy4QuwJy%7`x);PKgsP$! z7&M({KeX;0^(yLJyYC$qK5raB>D1k5#G~$w`D@k$7r4Yj7tOyTT5h+tPd-S z5|@Y!v&qk*dc_4;#`3$8wKY^R(@_llG@b{Ik#<8>ToS15uM(4hV>ZoO)whf}d?JET zI!yKtT@59?*nwsBM|`C##DJ0Q=x?;2T;fYkBij0%`02qvF=T`O`eO#hsg=jI?v00i z`rgW@()LeEF&`bAUMqQpsabOuZ)}!&52*K~ZI>S}5IzzjN~IVwhjpXPcXo|fWy3V6 zETMTuQ+W_uUh8gUQ73)zP43GH+-O#SI@wH1t4f^LZv5#K{Dj+~sXrx4)|eLnW@lE* zsGI;<>sli$1qeaI`Ymed{a8nfHj`Nf$%~@GEqdkyhp=<{ZWU@ zJ5T?RyE_Sk_5xJM1o|v3E}oRq*55`r;2XJ(kPt(D73pAa@}AYC41=#!`kGS94@;c9v+GE$|pP72h{R%&p&&9mlCM2zQw&D`+S0nK;*`8x$y=kOk{EEBU|) z1v90m5-oAs`-_XaYaaa|BW0;L;y|2@NL+Je^`~^%|JT#ok7SS5|1MLz31#5Zv3OkL zV8=+6>C;;8kzgyQXL-nq1v1APsa0&GMI6fP(=QXLeM*9C()q%X;RZ1uN&k1JN(i7l z0Sq^$?~`QZ6P-Aigylo4=Ku3#D|U#otY~VOap+bVsBON-NxggWekLV}$?Vqh=V8@< z)8sWhj@(&U*vb%nC%TZhPUs8Ei|j?5aNVe= zI<^=ttj}!=E)}4RPw9Q;hr`FCbSp&S2mDo8XH3Fe<_RsczE2sfTKOv8^#9`pKt$7B ztgEgyg(OKkoO}(8x{fogL#oi!mM#y9t)7bUOD{Tx!$jY*@51|w&=itniTx3(RCSyw z@1Fyg3d%Ot;;5r6MLC4;GRrj4jv&7Cy5)(oKLM|x+-l~v03A=KY4|~0BJ5BnSYFxZ zi?v|zLT_ky)<-QIZDZ@~OMH6(`GA-U#}f)64O} zYZVftig<{`K zs_#v#3aqax-DLQZT}83ak2ha{1*qKUIF%YJn)o!@mDOAKDI%IRhwuF7wZu%*b=%?8%cfEq zQbav%{K z9!%&dLi9140`S?mbId`K%q$w{6JkhS(o+Y5}J{!JcdGYfiZ`!Aab=@T=__ry+3+GFEHF07VfCjB6(uI7N$;`r3q zGs_zdTBLd7)}@w}CrWnHCXrWXQMStmwjPU%{FlH{Alxuy;~-p3F>>K zR!-MNiE`lYO?h0>%00s-3v!SAwU`&QQ*+OXCK3N;5j`iXEJh6s(}#_EDLrE^B5y=1 z8+I^hul01bF|XhLd!%%^h`2wrtJ~@DAd^jXFSHf*b$3ihe|u-wrdb<$E1vnu z#@W4gf!LpWM?fwPv~OpRz5W36E|7gK;b~r5oEW1Q z!R2IC3Zvn-;ViFl&BesG9uume_3B**-|bt%3`08JIw;kSh`t!iXZtz6xVp(4CnfNP zw}^ju;-KP8f6it@EQ7wxc*Q9^f#FT;p(?Zo;>rLsR=nIhXJ4Dn)S1!x1rt}>nZxW+ zkot9gx0G&&{h;$UQJN+0JH<>hzP-PFZZ^lt(cnNgWIh{ci27yR+Iwtn$Rn~SJTJgx zIPOIb^{VZ2KH<u4N zsT}{a@4Jpe=@&<-gO2R8DmL2b4}00#M-KS*xVucfJ#&JZ;_t?PA!~Riok#qq&oE0n zXlG^J^Gqcpw7ETdjztx)SGz`dB@GXkHLl@sG^eG^{jaMIU-k)kL{WD0xmqvhK8coT zsk=hwK4nf%{=1XSXE&ABF!Cw{@d*JYwz4tz9sV%`61DXzqcxPvRmmC{_<2n=w~WZ_ z^8R=xjUB%(c>ZgD+Ug2x?Q2|DX--rI7#YlZ8KW9qhkSy|;BI@6mIe`}lH@tQd1tv0 z#O)$!yl|ZyDvgS)kC@Q$lQ^Sxk?a=I1-6$O%(cTxpdGu$x@F$Or)1 zuVrRD622NYJFKK78JSlV zJTTxf(W3CN7ZI$#Ry?=4y#8frQVSeeO$|*mn5ad0W<;q6m5|;pxw{(9`P@E5{5%&f zO0_xkFeY%C&uC=S^~|2%$gkv3zk)MSW*QEg$g$|5HA|a*O-X3w6pqCbw!;XhNyO7> z8Om4-O}38OAL#i;fYPQeg?(RX{)R;S-uCskSIC=;AOGkU@;`b`Gir@~^02=}4|WZO z$H%AbtbHifG`i69*xfD`@|0mXkMG3>D-u^en6I1=`l<7b>rC3iLJzB%?;` z-nB_*vGrNsVn6*wL^r~3EL3EILRY%!UlQ1v*)dM@LKh_vN_IpqC!az6>z;c8B=R5vcic1 z!?t6Dd@6S{=-3<0PnStLsi=82b}Kx&28P`|CMQ1a%NjAk?nS$VpV7I9PNL+LeE79> zF}&W^Dw&Civ`J`9M@{G}DWA<^h#Ti@Uxp36U5$4`QbZXHk9}Vu;Oe#r)&C&l?2b`N z&PjA9F-(YhbtrtC`WCK>^&VKTDRGPQH{CFPnsUMC+{>y<->*K$+d{m`#pr?#RIC=f z1RDDrEk{o8E&+3{lF<(3pM*xH#Er0)Dvxxp+|^QMG)Yl~7zBZHeBim&MGi-3*{Ncs z($MVJO{F)Y)Vc!RX$`_}=jIMCsFjq(|Jsico@O)%e?HhBR8mpxDuYaxw`N_5An}P< zHe5^|wWL=e%ook?U=Dsqx+%PM zLBC}59mg&;SE$*!1ZkGD@C&N=D=GUz)VQ~1hNNSq4uc=>A9UFkh^Bq5Y9{oS^W4(X ziG$b#6sSGbRb-#25lS#Nj*m^G<$6J!uw2!d*Ie-)khTqc4QSS;oi~92KtoX1+EX}6 zlNYh6YkZOv^0*Q(CgEkt&Ac_&Tb0i)c^-Peyt-yv^U!Ax@Dd5;8Q?W&WkN0_oP^mM zt88>RQos6E&QG;d8R_II!w#sQ!R9aTHx&O>A6!dd+*R8-w0PtTh_myxI(FX2k~jO; zTwuzD3QNKp^t&f@XZ4Rq@Rfk$?CM4vN+en1iyHQn-3p!B^^TJYA8H&Mrpa3=f}y@-;0*I4-&?bZnG<7F27b$Zjnd zDR=+=0dZyo1&o~j=}`}Iz>XafVb7it9&!fDo2LfuUl)1j?#-@nx&g_)tQ)CAoG<>X zE%sDT6va7xLMYoY<^ZfU7cJkKzgb~W^MkCNpQo@e$_JHBWsd-g*4N`}iS9iT!~E=2 zy^+|I%nuTn_bQHU7TvrprU;2&lk$WP{KV+|DDo3eP16$P?g+h zOH{C+hbbp4-p9XTGBO8~w{Uo70=1?TL(fe^fv3Hzm_WsH;q^d&M+bOBwBnP$ zmVN9Dsya&tC#yTY()OH^B4GzVtcyE#oWOUW{I!dZO)A}6=#i|VY2p0#Rri`P8vdeO z8?N|EN{7TXk|*>|IE^1b$mUeR5H_iq?US3hFsR-$nCEy0Xh#WgEtKFbENJ<90B(Kn zZZ&~cg-9E<1W5?Lsy86oEDOJp^WNGOuVYI`E~;`_X4*1`1rdqQNbHy!qczh1-EpPq zD&lSZ`v|N>JZFbbHAGZ6GI62)ylREv)uNac3K?)l@>RENOB%2Lcjw{aI8&c+Ia71T z(Qms*%v-Us`8Otly{+=Kv2t(SUp9UE-yNOvTjAv!!_KA7=7>%{mmeF*g{Abs)5~uq z1jMyQK=_A1o3MsPN5qx1>_+8DmfS!}hqt6TkI{6+1;gZ0+;$-4DB79;Wpa)-J`4!> z@$RY^Jhd|Qy#KeLU?9;`8qc9ktop*2)J*4`f#dZ32m3}1HWb7_2*CFU+MUkL9s7J!{@O6S`DB{q^mjeg>;@5b*FHbDEUcT(qeMJN&}TTjjdcro)kcu8v6<8>O9k0K!_DL>P*%gW< zJ{kO%Y#fFY;|9@@uTNq}YN0i}Zw_ORbOI%mm#?GU^lF;_rMUHf`v;me_886Vq1x!v zvnr~)d8|KYZuUD>Eovcw=yWeN7SLvN%#xsBHeQCh8;q^A>6UUV-U|JVOf@NX!1)Ao ztiC#rfqEJ0@JX04?BK&$4qR9tx+ zKb6t`VIFINmZ93MNNDXA$6p)Gb0x8?YZV0O)W8c-@}1t2M>NGlc)}kjGqe1y?79>| zi*_bPG+eGx2q>*kR+Mbd6*@% z!@+@_;I!`gX?-u@&_3Gav*KN0`mJH}hH1MNyKpAD%Q2WNdetehLHoaLJCD4?MVVEI z=hn%@)!Cr#hK}Ryk2asE zMP*-vh+ru8KefB~CZQ73$VhpmACVUnnB@WQGa@HQ)*Gi037Mw08a=DChW9^M9^}4? z{C3~E(clPqj#G6dC(ARkWm6!&`Gj8t_21-{ibx%>ww>Uv&q4i=){E?Q=2rM555s7+|WyI-%z_OP8TkRzY&PtWh*-20i5lSAkJ8ZIQ z1H_a6jH_4oG{Onm!mZbZQe8Pzc!yjVW8Et!o|}F3ILt&+NUsgbZ4mM2q6S_aWA0#( zX%7phD6534nIz+MCqP&u4sSKh(upFpV;2VY`05TfXku){=ZsaQx7Rl6`vcM~T6j2} zz+R`Plb`fBF`{`aXoL03^(mcH^zv(R#yZAc@!H>aQVj%*e<*suB$Udkk>G5paoXYt zx|UdqgF2m9i{dnNUUY3~wpFo!+01KjplwCEvAOPJ@osN)W);dby8u@NCQ73A=?{&M zqT`bXUv)R$;7`-V0C6>an2(qNP)q`;^;cr~HT#7}%%kzT0smBhr6UOT!1W^Q2l?9@ zRYrlMuft8yreqkqLuf@x6QAcxBLrri3$Q9+&}5>qC_G@7fbCa7avyL}-_!#SHTa5k z_l=NBKxfXOsP2@4KoWH#W_Gf3a3O)3FVuz|nURhf94rO_qSTnDDvhtDu!d@$P=_Kp6>E+ zW24I7V5_sW5?tGp&3TXd?L?6Y|M4mQo#<48Bmzh8)diGtXgvL@E;=!ilDV+aq=-8y3B4MaM(C);tnJhs?hoib|#Md4G1FgNYw(4J>oil)Sd0IJ(@$(T;5W|YSYvOQrSL}!LNe~B140VWEjmkdlGSXOb3ZQ;Fp(Jpr)gzRt^1)4#LGxFeDvjRKbO_$F>$3@$Peyxi_}tu`GcWuDMecUA|t1* z!k^jf3=l3SESCPAdyH=}BR96?cBqHTakCr}t`q1SJ{uziyNb9EodJo427mb?Jsj&( zA9@FyLNit>7H9_tS0qU4+vJ%K+wGmkD|8daM6JISN+R}4{Y8K4kN0?q z@3MU}5HK~rcNRL+lo=u1-fYDn_*YA*6!G6&7M*MO-4klA9ks})WB2pJU*%t%-htMQ zEB}4s+W3p+F5}}hr)kt354XH0Ta&22cHap1RVFMLtjzl&R)_ifut8Dqy|c`2F7|&k z#g0jG<8$m|=c?8_!=+75Z?Mk>`9R+Qe!pXeC5no%D@MvXrRQBz@b01cJG|86^2}HJ zSPtuNyr*6B2Ju*DQ5=e^p-4Tp7(_Ol5#wmRRX~LyJ>qwXgqXKxYN;2n(1RR(q!J`) zfZUt~bsjHIGrWNz$u9JLW|_rDx&0w^;xu9(TOT$l_Xyzxyw*z26T3jc!bgupkNZ$X zEWmeT$BebF7s|NKQDqI*2BLoc_gM_UZL`)7D1i$&Ze4e4o96n2+ZYZA#N(so*Lo&e2@Z)sg!fvKpt+Y{$c za~Ft#L8Ax#Vfct2L#no>B4$D>+kp@>{O#unY4Mk-gY#ThIwyBIO>txPV$HG1irD{r zw0iejeCrz<%`n^(Cx*C-1@jGCc|w=ZBQ;Og%DQ?xj5+srWmRP^4uYxTq@A2w0lE~r zeQx>ziDP+Iq(ezGVSF^JgT zNQgv(3S=ys*{fOp_7BPLE%UQvs#tF)Gw4I9i#&nLFXWnB75Du$al zT*-dI?HE3?`1>GI{Mc^YVI~uE_3js;uBEPH|E(<7LP-ESv;>^+o8O`G(ZXGn$|E*Y z=E5E~E(`Zsa>q=r3ky0ea}*^1#jJsIzm4M)O{FY4t~HL#P)K&|C(AO`>rQEvwWWzc zNMVS3?{lg)8;JZ^xd0|u*o>Nl+77siS2OYdKC|ZijOG7{>GDv~ zg;G-ac%D{==5VVaSgUSmR=v0tE+L8v(Hb75AISsmH5FiZ-}{sNELa9ZfaRA#sLGrB zbFm%?0o8Fd{K|1J@yd3vJ4EgVC$c-)Pe%atoH1grk41vlwetrK_kQW+T%6z?$uPF% zf`V66V;=PC{SUo(R^k)+pX}fb$+7Pbmn;8HQ@&AYbboT8Usq?VZxx#>rAtS1yv0?B zTJeuAH>0iVLKsrDHr}zHQJHt{+z7`-jt-3t7&vGTnH73E5K+Az9T5n-*C|0fpfq)7 z+L#t_K|449w_%gy(vQV#_GlsRy6ym!QT~=JV zhTIKJ@p_q_PF?{3w~g+m{wQ@z>4*U6W`Jht2%z*jzeZ@$pE)3OAG%8UWYOWUW8M@- zf0Vef89u5v6naeAEwL_Un!Dqn_gd&Asu&2=?4(;n2P37xXwls&pGXU>tq7f?_k}dq zoufe;C$0ujlA~zVYtL)iOYlB1^EGEvq39Ze_!K|TW;UB1#vv5j8c-7$6R~q1bt9AY zd&jFMbZv*pdg`2F_19gyEf!alXi^|Qxz)JtFTuwULo)5G7LA!q&9ZZfQIOU=U|+;o zUlf7462IDUlC2S5Eh--Z(xrq+TAwIU~i9|p27tDTax z!^%S75mQ4C+uGY!l!9gF>WOPfgKZO%fe+ISPHNPsM2-)D^^g-tnHaE8yj*l98#U(? zf^%jYe^vU69EuiXy%0%h<){h#{@5;b`qQCPUnSih$m4V8!j6^r`+K^l(de(uhjPdy z4&lbXiaQD6#9WhRZDY`M)HaGB8V#*$0Vp{(EBCFuxsf<$zc0|l!FwSt=uYZ!d6^?T zvj?iQFa|rj`%QSS>^ekcy`N{_Y%S2;Je?i`YZxnT+K&(TYP91n{fFR=!XQK{kMMt$ ziIGa)jZ@kXNZJH_&&Qe0lgx^OhK5<{=FaN9n4F&{>RjFI4oZg^i#p9 z#Q#VLv#+)B4HZ>;6F`OTpVJzjTeSn$)l(tvyzY>v#Rb`q1ijm5+cNBT7iYVgT-}yi z7mJK(XvW((=-dLHM^DVV2C(N%TKKXaJpHe?`sVT7&Xcg0E3=!!Q7kT0&x18*nW!nn zqk8mzgn@s}cbau0hCRZz8-;Cdb4~ttrdBrvN8ht@TjkFjxP0T?dl+KeRPE~}`?L0okYDv5^qNn3Qi;Vm zFl4%9ThIB5lL)?FWue5R6^*RkCyf@S@)_IyO>#H>xPls`e^#bb3a)w#S~DNHz-HS0 zGQ>|pY8zs$4Xm58?ySYW+CE6W&r5ycyPM%r`6g|{n@rv1)Ui-%hh~%cTa8-etZGfQ zuWID-Uxbic_Q2eyEf-;sj!<`=#IW(`(RQv5)bf^#fN|m|^cVc`Kgoppy%j9(G8*$J*0J2(rH1RsEBy5MdS>N}b1?5&v)VKM;!Lbwl5kR<8IDQ= z>?QSns>MuPFvx#CP`_}r_m#0B6b=1sTwLiOeD;Bt6l3#^EZjd zG^v*@f&|JdW!s0iqHc#O7J8Apn-}U+IiCP#?Nw#p8*HoH=={pke%b9h8k;&)irj`# z53b@@WQ;>`nWd!K;enCuMmK{J_3~vo68hJqb;FHy1Q>a=Lh1wHeQ=j_!6RAxR7}SF zmODfiejedZ=kPC{Eu{q`MV@)TK2scr^{A3sv~qUfe$6lQM}3L7g%!9 z`m~3o%cl^9h?y_m@bBdaZIw<>yYhyg?f4A#?bGJP*n9WxhqH7YCvY!(j&7~jv>&p~ zmSbEr@XiuKA>zM2X5;}rXQo~T5dPI7 z+6L5M-Alq54duhMNAb=2MZ<*B8B#H4$*zgL2Vp|34O^RiNGhoaQuBd3=Ow!lD-pOn!*z>0FdrF3T~anl{Jln@)T=1(gGgUFOv_lD?=kMc5}+F1t0)EOa3V zOLubZ4w;ly$>Bmpt{Rib8CF*>A~@Ua!awCzncV|O)TzS?=%B77D>nPB8LGZmkA8Bq zJaQ60qFC<>BJ@f|u3IjtoK9Cv;H~w#D9l z_uDy-B@|QI9d+l7%g!}2EUi{B1f_rYgND}}P2JlsXc};LLRAf$NUd5qWGa3ksILg) z(Je2o`FG+(M=FfhGfgUeWzrfp`piK*vzm-lrlzbi?PKMPM!Kb_emuXHCtA|Y?3U*Cr$24fk;JVUL+Y=Jh+hPHdMUWZT)iBRBek_p z^P2$s)qon5kb7}x`{L$Hb*hA?JY4<93e@A=?B4+9l_TyAyhV*#&SBm{^R!zbW9ud(IMC2go*th{^761 zjcs*^Yq|Bt@F1JvUPz3Gjj@8hj$On056cU5h?8;m=L9vygBT$wgYFpqR1o%LAeZ5u z+N|o2Q?!BILZMVa&X&j{f~D)E0GAn^x>{%EL}50#<{-`V<(bFNnf4|!cgf@F#H%`yqpGw)A4i*+ zT*=`4%z>*s7<{P8xS3MtMWvu9TrN`fgeq$GLG8Fie|fd;ME^vWGik38L@97V z5N>XxV?^@Umi4h1Utzk{aMy1`AKd|WDXSCsgT-;#ncl^9YsK-+z?JutI}Pfz12>>F zrtCb<{+^9{(e7?jY3kdq@(cdGNiwPhFupvfZk?Kx-8~yS@#BA^z;YOoN&Y1|2`y2I zjPMS!+!bIx^l0+{74>hWNhU`OIGIVx`qIo5*SsBE`D3*i4acNV z@@vZs4_653d9vaRQdDVT%kXwc(XuOQn29K$K)pLpksS%0bz9F!O8hxuLi$xn^H_nO znl`XA+EmHT>7L~oute=;&(GT= zUZvw3-Q7rUJQA+m^qTIeh^}!8ZF1Cp zy*p&V6d0o4Px|X0-80}LGpjqoudm+U;~DVswY@XzJ_TDOe;Ngd4DG0dLmNl9`lGz> zRwg?BL8su8^x90|l_K+y)-G5C6&3|HNHOHjDAv|7R(QnWhf}e#4-Bo(NFF$~f4kU= z^Ju{k_|iS!|6}gRhV834gka{WnSA~Wx5FD@&*+b9r&9o=1kBpD>rhhxSAJPtABU5b z2VS$qnA9*`)hXRB^(9^rW75+gZQtx0G24~S7$GL7egfGX#0HUo2R_y~S~ybQwW5KU z!|<=fcR|H1Z}&X%TRC5ZMd@28Nv|W%Gg%Tz0r5vq!r#eu!XA|xc_rm=b2&Rs1=`=G z2J#*VtKU=es}Gy;3`c&0rHz2ZnV#|Syd?sS)0Z}5Y?>b<7J5P#6&M@e=C!H; zE+FNSFF*c~fvO|&2i17(WqnjEmNYroHL%ZBj3Pj9wLzLf5swsc#)6ihzELcaV7Z(Q zqK0kY1x9R;!B#;G>C4i==J~4?H1@z9F*^f;2yvl4%F`#|Rgxm!g@*h|X53`6bjkG> zm-J*QO;ou%*B#3etw7l{=`z5;{!u2w!)_ZfpomlLpL}EPveJ=1Vo=RckFR)CfiO=} zL0Bg4M85#-LZTU=ZuhYFP5#}Z&B{W}wm!)eolHGRIs)2QQsULG6|SMyMQF3yxUx>< z)BS&olFrWUd|Vk#@O&~a*}3=;_shRdC!#S&XHkAjxObY{7OI~1eog16?YEy|KBawb zOMl7}>(^RnX0$@lZd#nOI-qkMu(o-m;#|F`raTEAHodMn(Icy{6N0WjP~&=|gdQ>; z)Ii^9i2_{(4d>FQXvx9+>d)qQB<=*rN3FYHZiY8CY|$|I`#+W3q*#8q4sv~0HyhW(ruUW1C|)V_g2+>pl$_|2EWCw zi?M$!cg!pdv^vCYmd*{$V-y4O6)nqnE%bKIi8w7sA55Ti+@`sU0J+l>ua7Te^&E3# zDhk}UNYe~nyL;;P(sq?ByzfQIbnxhJ3|0v;TKGSv=HaB{=Zor5&DdDu#po_aay$7o z!$GAMD!J*~nSzeqQJKC<)Bfu_|h8aM@Fn@(TATQ+vIpRTlinHgmM$7+q z$IyPU&SvLM(I3|*c_c1>(BPD8Dwh;VIYnY-uQaW80Ui4)k=l-0Si-Gy9rKFK1wndw zTl6#$xiVq$ddf}Osv~3DhrRy*j{PgzlNm?S4CpjDIh{_O?8(@w7#%^>Z-Ca|k9w~v zdWPaWA3yPFXLfr!IZ;0GP88$_C&7J9>CG_#n?Ur>>|`w6r4WA&>204c+mv`W7_w=L ziB0YlI@d=GDPWo8bQ362r48~2EmU?5vSt6U zV?NRFbyH~LyT|VDSsI`BD$VkRd*`*=zij!CUee#R>gzbVN!)j@sYmG|eF^^DOk~0J z)4#k<^-lFKTWJclyJ6SuBTHH*HFR{xr_(*MqNTqD8z9VmJQTGKGFDx69A$3VfUETg2 z<1S<4Obc#!f~XuZU|$~pD8eu0Gg<&`u&>}BjBioeDj%0!ShC`A59NCAed(>dJ-k8mf+s~W{igkZdFsC7WqUUS5^Hg=K!3?x(=N_waA&b8b(cH*p7FBCR6oGwE6I1R>TvWM2t_e z?coPmU~!q5VoI+FXXAyr`fyfw6MS)}WYw?Fcv5iNMnj`ON0i-&s&BIj8F?`ICu72+ ze@{lR?c8wD`9If4s=j2nUc&5$%i+-O(3QF}h)0j`2@5r~ z3zS%&(Q-PkpOdFHG}1hV+4jraewfPc%yO3FvmVzR2%UHloA(`cFu4mr20!DThy9q2 zEMquLs82>V*l|A7-h&_(tz!53d;Hd-sDTflDn54R)p-kmTYZ;u-_vDC)_A*mi@@}T5bL+IUE7TQneMXrkH}bC9#~DCO zmy|r6b4xd5<{je|O9U`{&b5Im73H%Wd9fsOq0=TZaR{x6*0m3S zOpoisN{t~XwS$k6E|}TqCJ~LQ_3Oj1AkM3aXgQ2oJ^A;!Xz4AM_J?)YW-o#sx7*t{ z+!MUzH+#qM*oi%k0)}zXzG}H_%uE)ASLoEj9A=6klu*~^;a9Q~eiilpszk%NwFYD> zFM7ZYZ%-mH)EhGPE?N*EK})Oadm%dvl%m?VT?@=yXc_2#4XCZ@+fx5$fWM^IA|HL< zoP{=%pr61I~X*4o# zcRfvliL~86grfV^1%-}?Krep3w?^^CpNE(FzhyMp|41%(8;7N9`xE20r5}$h$S#e8 zHtBuktjD2*+LsP&B9Y>Grgzl3ii$?2gYxRi^yEbYcyxF-j;EWiR=wqynP{9_gBz~k z?D)xv9EzjuiJA>8TdRY|!1W7eGXyW%QF16VSA;utkQa85ZJxb67x}+CRDX}o;}B;u zlIJ!(FQ*Puu$XVmHWXL~b**(d-=*X?@Zo|A3Baxoqz38`0B4DnT5aNFN@eneFUAFR z*$#iDahQKc*g_@zW7eJ7m5So1G0WTfL`+dq93>!4xYka4TY-gV zCF8!dI;f0}Q?rSqr|kzaJ4_fBO$72p4iV|Y+XAH8!&*L2%p^94B;4Ght1+_wFp~D( zz|Mn^-e1crwb~z|_E)EwNMyH24K4Wp^A0U8EpAZ^h%GCd8Jr_C-^es%cUgRZ?~rK0 zbMgp;2l>R}%2`Latv9APk!Of5-*NEI27`#mEJ$tdiacO$^?^)>0>vjLT2!+hs}^e} z4Zft-2(Rv54-E^NC%1p+?%OE}eZon!|))mYr^t~Mc{3Iy^& zCbGDNLK2?W?mml8Zc5JrboZdTWInYqJwKm1d%cgUQnbgXgIXgX`i_uugeuF6G_AZ`f_(eAkluxYjp zYpgGv1&ck%w|iMGKhIK+?PGN?c1$Qi<=z-gUWlVo-`6}ipU}Au6g6y&drZvg2MnLI znqOs{XZq zETt9-`;l3OdDlJi$+Yl9v_=TDN8&tQNBlg04D(s&vZ4{H|Lscdwmw*-^3VG7T&p3& zy|XksvD@U7MAsH=;k59wF+%+hV8*dO%ef98rq1%DE!`8XZP&Mbi}It zqSg9!+=CS4w;aD7q1*rC!CyrJ{G#MLUMs8gC`~DckQlH<7 z_3q?)bU5l|cScr*=2}a0!=qe^Ed2Rph&ZCKh9Twqjh6lt8>>xS12y5z1LZ`}M;m)+ zzClrm*FR1$s_?ex*O(sih06|Wupw#T-K5ipY$ zU+oNa1V#sAX$*FtVT#VeX`dMe-7Zbx0z>^>uBsF_c6W)Tn_c$K`2^#@Zq$T`{^YuE zyTJ5cY_}eaaX{9h%c?q~OfaTowlh3b&fc49XORMY2ASBtE=Be}$C`{*QDC_{Jc39a zY3?EaWJUakNzEwrv7gvhByN9-ot?9cNnO37*T}vJ8=`VcCbwwTO7iAK1ihwM z?A3s#An69LK!5Ug`9xwyUIsAtRsD?XeUZiy)FGTsgd)I3WqwUH~6-ylD4_bMs>na-kS_p{83qkTEUbF{Jkk6OBE<4AnAL-pAUxR=P!SwO`mAY#sQIC$n-Fsy}+xTre0@Bk$(| z1Sx5bNQ&-c&wvPMqJOr=L{|EL!3BROzMF>lq<{E6o)C;%Xg5Gr@m~9s;OAccp%36P!h%&W>QD3 z)d;z@5A#tW-K3ry^n}L><7+Nl^ur*zfr$}kr~Oq+!IMR_JZBoj2!+oCa*cFCPoY?O)i+}B_W+EV&n^zulc2dwkin4ddSvRFi zHnbOQ@X=+-8=WK7r#j`UTP_bqk4)6tQtM)*N+Tr8q<@KjuA1g!(nVJ_?QSD(Hto2g zG3g#Lt|v>im)WUmBrADg%VCNj(<19T@|Ma zc|pt4Q_it^`L~;R8*<|`E?AdMdC7||ihJ)*b)|~0DUle9nnZbJD%mmhF?@K)pAWn% zQ6~lBNRNh9F1v}&#DA(apR+wN55OK#MT2RIsdVJNSBC4Fv z<~}bx5#L_hyg(}`aZ-(1BQB)oxmtKClti?Yi1fy^z1LF(Rm$d$&$%3Ff)PUXgVmUq zmawfD)03~JW{S%Pq}bwwEyd4g0{Fh#^Kn*Np1rDNW4+j2u(!9t{{XZ&D7<7P!{+|H zRps?mTP>%lN2yq*u|9?J?E??sUYsNBP?lE>@EhTL9i=u0U?zR7-C zOFOI&wQmea)-+6#Juf!yQPcjKsMnP_*;$(9)^Xw|zPL<4da9ABqHYpbV)W0$ISJck^-7I76Po*i5@c)hjDS{ozK z=rM{bxZ-jALAz{GT#B5u5Q%b)aXmjU^}1@RpQy^Z#RQy#cH~1W<;zVd?YY%gsadIl zE`%4Dxj0*&M?O`NYFgDO*=}=I>Q^jG@VZNJUER46ch>l4%N$88nyC%NQWpnL4kOd2RcdEBF4iF@E_lR?R}Naye6ttII2(5FD7ZwSrk++YRAKXZvC{7? zOzvWr%z_fo;cm)Wmap1Py<`J#lc0Cz}8MH?KI2UP$! zhwPvMAsD1+0EHEMhbjWs7MUQ4ek!Hp0n$Z7Er4|-R9wg5sRBk_CS2Y!P*m!nv7HJ> zWNX2{hP=)^%t$~U4sUtapH?P|eUD;g>j5hM$mrXO~OtxqKhrrUI-9ZA57GXDSy z*8)pNLzGt55Df z5d5bzeN*9iF5^D)wXKsYhf>|cAzb2Hz8|u>>G&^I3#r3b>OT*fnlXQ4`&8Mo#;v7v zp_781vArTPu70<|I*%o`g%7AYd91Mqxv{oeJ8)a#!~l?osDCwlL-bXb+I{stuAN$I zGF@Y7wtR$JXAoy7`E_}GwLgdIJyd>mIN-J$QM2SI=chCysd>3dU&T!*bqnnj^$N|Q zWxQJfdpEs0+=RGBL-SC*J#4#4nv~^qVqwRyl=_>%(J2WK^Hup(m3vH@wK!{JPR`QV zxB^YywGri$gQ%-7Dx&R`D=Ki;$eZ=`nhx>Hpg|$hQW~|S-il){P5Xvjud$BpyP#h5 z5%X59LZ(`o%PTdBtoDl3ToSqvj|G&us_MfwgntjrSdwDerp2AxcL)G{2&E+IthaI%Fw{3StGA?aHs<*+JbyGNHh19KyZH4QDBDx+?i3xWNbyH z61dGJ)LGY7IULU)CdLP~8^MCPrw3rL)@s8+8$JQG3K({u=3| znQJF9QpEAkrKRWV3=YE(-N@N^kW1UBd28r=DV5sCk>c_H0A*vqt}Y}|)FM3Exqa32 zSsxcucLbKBzLxG%Eafht^3@Ucg2lU=v6F&1OED z(^<9f?YssiA9kac)v=!vpV=ZW1rWD&%F{cC|Hiu?EmwJ$O6I z#|+P?oN{Yg(3V!(W7bi|o~9uYWa(pBjzdcokf*3^mO-Q+~sW?mY+u4moJ(d=&0 zVpw&~^_!wm2FYbE1E#p^CP!yYqh1XCq{l8XOTl@<%!GP+s^}&PKCY&0us+M&7Wqh= zh{rB&S}SD6G*>1%dpuBuujnv z4x)&wQ0SrKyc7w@gnSeL1YV|d{?n{1X{<*W02G@&;znzgtLaROZZSo`BH zQm5r&N1JvKUI`IJwba@~yfpNfA)j$Kr#2*l<7LN;NVxn}Y^vOmo;!~7vYyP-vtQ{zd z&1qxLdwjuJd$Sy2vbr%3M*i=ZhC`;hIy$ZAERE^o!ygga>|Psq;mYK|c;yrjzlu9U+<=(+NtSL@G5nw-y;^^u-sLb5z-+j z4>Y{BE#uVXY;2!RvbFLy@SeusZVo)~E?i<>8se+tO4qUJQSb##Wb5!+Pj;hU}i-}@DD-m)f#AV_d^(dnr zFIib(dyHGl!p$5})I6kJHR@4MNIq|8{2@yzQE8qQm)!vk_Jm;MFI=zCkx@Q&v zmXjgWI=l5%?URk+qf_WU&hVe@3`lhRJl_>lEy+jRr?Tv}2fS=san7&BLU#i#nDjlL zZNVUB)xT5&sp)B#g*;WTY&xqKqTEQcQ2zj#(v-AJ*|?QH}J+dR{^->bHK ziNebA<|1ov#(rS?ItH~3|ZV;v!dLLgm=Z^ zk!b0yBExCX+U*^`VqK5fJ+~O6*H<`jb&Lkey>!^)c&;GT#w1Mi+?dI>wHBbZw@GU2A*su{O(rop&dGj{dmDrOAg}hb zW$_E_Lc?YoM#l3JxZfN2%etKD6d`CP-l=gna}h5HQ*F&)h``0|sBr$!wu;~A-?KZLH!+)VzuJrIHpC9!3uw54 zkVJ^xEg~i6Dj9GopDhl5wvCtBy}rb-AEB#U=9v_T zxwOiT#ket{**NS^XkDXUcHM+B&A!a+m9Odzg=cw_Hmx)szU?K{MWqP3WkSXF12xWC z-)uI2w2s@cucY=cJMD?=#|d$a7U{v^dw3%yAs(oi=`S)+(QSn%%$xrJMV`y=@yn;a zlO3xw={rlc1HZ8wi7)EgMdT5eQ3hPRqZsp2!$0=}WO_en-Hi5s+O{==Z3fz5_ZN3N zIQOJj#_)gygrrkK(rKhi<)N0ut61`^9@h=}V0|M4Z7t2y*v`(J7A)i=3|kN*nHELS zq<&CO6-w7D-0~TrEb=dB>0F_tG=IYQ4&tOleU-vpxDqDhg7e;~u013&Qu%9#sCzAr+A(&y+kTbq(XC1CknEPm8{2C)v75(> zsGlKep&yZ{e91>btko_ZORtw>;{tij;y6XC_^a4gR#VNKi@P^w-YJROVXU^MZ#*rJ zib_HvRg=H)%SHYsr|(bE&j)8>HV4xjY~5!F!R?rB!6=A}ERO|Wio6$|={=-h%6?Mz z-P&d`xBF?d7{%+1iQQsPIz`H4gk=;Tmc0&?nf6X{=iukX%8CTzoEyb9l3^5Adp5PfInc zPUoDxYF9Xd?u$cZ%G{;#S8Af-d3CDZrJx&p2ICaGII>ND5qa`=$YbInD(L)8G^e4RvuomaFMS(vt(Dl*+bm184Vf*_qPc~$iEnO+ zOQr%j34Yq={7XNy=t@hDr`^Axe{HTTwwoT=NG45Ume;oAUNRUIzY13`Ss2*#{5{%f zv@!Zm`alWmEH_{!YKt?yXBoiuSZwkXxOyAYb<{2=&Os>%h=p^fMAf>LSnO5Fj*9(HlhU@` z?8>UY`oEP=7_;pE02+5_zgM*AK zudlGPP{OVKTP{m;m>B-jDRhl?B6Rs2P56qZaoA2Zw74&8R@+HqhP`iRe(@{TZOl-a zG1NZ@mZ?{d;|tTew8Lb52Z1M5N2>cV^S|lQ>|YtWvDtl~!tn9R-umPQBN7(g0U(m~ zY7(!Tr#55iI+XZ7F*d8ReZJW^J&$aTCt%B>DmNEaCJh|5a45vI`Kz^Rsp2&jIby>} zn}ZjHQ_NoawtIrBV!=6wxXUHGEXYK@W%#JK$kKN#6ng;yX9wC#scQ76%$(nI2n1`5 zqb9NI<&73QKFs#g`(*aB3fk*Cquyb6joc3GeUh%Kh|fZ8rh4~fyH#ViTPEht?GS~_joPif^{ay-; zB}G)tY-LID)cjZaRCZUiy@__+v^|#XWr*8eVK>Cqk;peZ&5%o`8CFzwt&N@9bv(_{ z0wE!lZM8{##*Px<2c#vP;mdqMnGqO^iPb4;tZP%wn;ji3W=fhd z-?H&IJHjsRBdbk(^u{fT^-9}sk!)UYh+82+B#Ml<@mj|fRyq}~rK@>ckhvw7id?l) z+3ARtSvAPM%a;x}x^m(`ggo6eDpOq`)68sjE?e9QI8n_tL~3zgQ#|tE(y?*5D|ZF< z2$YDwbrXv4XpN3g=Ee#l+r~unlzND`OT((Mt*V*h)dLCmb0Uhxlte^581NH$LK@Xl%GvGG(&R zUb9b4chITrJkFsRTGDy$J#MdIFEa3msE$J)w!JD*X1pGEQrN2`pFeE|9yWNr-HUGO zZNot*^H-@1(x-&`xFf)ChYoO@WQ@~d^(k-6LViDSh%LPl}x^-Q%b9h2A_7zUmk)^~;Vj?W-<& zqaj{iVosW*`7(XF6}C?ty1;QQ^G+5^q2V5(s(n_#i@MTYyxrn18_n{OW<@IJ$)l5| zDEX_Q0O8_9K*%Xe<*90iN_5M{B(|-$ZrE=Xb1g;ja@JKPosOC`mYqAuZ#*suK(vpB zvq;C2qrGno4Xec13`;#+#j3626EuC=npkN)boj_eRCNCUT{X>98cKIjcC6NhH8E>a zZq`mW-Z}V3_E$Gf`Bc!`uBGKu2LZA4TUEra1c!N$TZoRTI;y!hmZkV#O@jHgJ;ZnK zZ=8^pfh|$$sHV87yZ-=*a0_qjJ#gN_r0E!hxDR<3I5)+FGthTx|S$tfnNk$Y+5B{N-?1`;T4%15U?MN&<+=qA*h z$cecK$uyVxs;6%Pgl-KjpK|G(Ne3+{;;w$7SIfE8L25;7LD<>i7c90UP03O#68Fly z*VjYX-AbR_yT*3sX3!+Z8*S+}G63b(re76Tm$XaNQ(X)A zX3{*Qf?R!{H8-LWTRmXbXB#3|qre5jS5FGg7ZxeXHn3P{8X^%U%abG#t6tqmd2aYD4m?6aJHbLuB@vI! zLL(~~JMFxZH?&%JZQKk~sHt?dkPrx|C~I2wyTvb0Kmuwwm2D$pkJ=Ghr8&x{V;GiJycKZ_ zdp60DxL*jDhfQxrH#XB6^3RS;aM;s@=|jr7ipYq0tBb4J%zGXdyKmfan3cq?B;B}< zWz0QQ!#JL|OzhgilrF?d@I-PU0nQ`hsz%$Xr9{S|PWXwoR$_@VE~@CKxwJUCj8K8H zjkkVjPo$e$;-tJi$Wy|5Ml8>pCZlati zis4D^;bxs&zAE%$7Zb&nyJu9SS`(244amQk4KAQ7)PRzDCSRd)JRO z01`JCp{|u00Gid(W#M_32uyr6E~_i4vO6V3lsz-EJ*2V399$;x=sIJkN=M?am+@Ye zl03vd_rW^KHPxv*YIJyp4(6MI6frJhuP%*yX(y>#hDAcdToO@14qU6XI)(K-+TA{S zp`El0Tw%89bjlGM4sQr!86=`Z?&YrDo}#|iJdUP|bjqx4+1j@7p*Atzwkk6BM@?ns zsx^+^!{oYqgf^%p5hXn$ks;+O=!%!r;c7TFjVw22aSP110~B6SE^5Cy1IuO9@3tqh zE%S|BUJ(?u$9L7{t}5L}q1#8M_sQuj_G-<>ALy)Gf}v)Er-gAvUs+?PsKsg84>sE^ zm%7<|#j&`vA#HfJQp|oD^*TzvXY(E#=RINYETJ;UO>=9zqWTNJw$p=?3 z)p;r=>en3@jIn6xmoaDI5v##jZeQfmRt7D;N;m=8LdA!z@ zo;EtA*UQ>4i3sWulK6&()DcxzQfAAQu_f2}swcYU(CxtD;RwxDW!y5U#tLcOxU*{E zWg&iEQQ=rTZq)1URhVJ%Cxn|=hL9aTYN{({7gGNK!Lf|-X^|4hd8^QdJUWZ&Lg2VZ z31m^!Ri;>+_}`O$*AIHfX5P4RB2I|@8CMg|b(RlHrbb$AGGMqQP7yX$*1JwOhGsQz zdvRve-0vmeCEcs+u6nG}bad&Cri<4OGRidZ0K{|BHJWbV-PNH&EJTJZi56;FbpHUI zS3I1*xoY1M1Z5j7fn^=w}%6SGFDFnz+Y4QRS%579rEr*0XbPJF+FzIZKqQRa~0#X3VO& z63RMsl1omXeRMKYWinku-U$kihnQuiHA}ZbZn%s@C6tJ>?=3u|LYPjx5}u+$mZk8~ z7=%tbNa*pUwBcM^Hw9n)l_Uv+g>YbzB&73wMCIJMWOVh=pz zeQ`{`ioRQ^q|&W(>Q&WoIX$9rxf7Bl$g+%hSCsi>sn)W&T=Ku9GuFTTE5;ge?bPk{ zOX3>n=<@T2R(9S`i5R$Xo565i9&S?h*W6ETr^(+6jPSwYTR=AWmAj5Oxv$G!KT}6M zwx2`LS8pw5O&cw!!{sN5HE}_P@+7^ zU7N8u@4G|UyCc{)uCW8f4%IzzEM#1wJwJw)N~b%v~C`8H}%TAd`@l(qB zncD_C?VZB?ATzfpwmckgdc33Js#e@g?NhF`A-5k#XCM^d*pGB;Mc^`nM&9YZZRSJW z5-Op8F)(6^P6;cS$A+3~l&Z(&C6sZzaENz;Q4!Vm(tM;$s$6F3C+hAGD2$tt4zG%g z@db}Uz~2*bbWlWP0`k!fmnAZ>AznyHN0K7ChEy4}EQZa~l8}tzTB2NQMamSceGDGa zcERf#du@=xaX`1kak3^ExDl74vLoxRK0)zmm0IHhw4Aof<5oB>3LATd;YRg^xRw#H zcxedE=oExEzZ=FfBIy9|)-t&;vf6Z3J)hv=Z*Pcglfi$9vwix)^6BEqqPY>$N&f)e znjS$dJ>^YTE?Bs1RPx+Qv;yrzwJVLbMJxTD*tSJ5I}#k5zs7C~7R4T+9Yyeo6=>|- zM$#E~Z9MY}`bF(lE!vgl$M#=nxJn`<9mGY_H6C}tIIl8& z8`@UNU}b%=-$CBbUds!6E8VKPd+SEUv)-tzdY&)hN^PR&A&%BaqRHBG_?1Cx?AKyKcohH|)mSVK!Fy!xh?cR-Ad6O{+1|=_kfobp5m| zD=3@nc$d&F*R{-Zv_RPXo7*egA7^mq2NB0D+tMzighj1f#&Hq&MQU@lpvzLN-6 zT~Qpq8CQDFIzi*rT8~pWIP6j9udWHgB@q(Imdb{G);ciTa?Z!>o;|l4O^0l5&ROrT zF-yR=wPd_zT7F)y#Zd2pYYNAIIP&-M#rhy%~NZcgGv*y%86q`xV; zs6PnT1t`v;-#@~qW8qwK^6M1D9lp3ECmt!eD)&}ZDXbn#?qZAC32GttjJ_&|oVY47 z#F3(H?q>P5a@J1FTB@I+pQ7Jj{{Z37quwF5%v%A!wAk@AZZeYY?ZiNTIhWlj# zJNk6RbUtGGer@Hh-+d;ryUQD)e%lTvaI1HlEd&<8EgGkmb0n^=fg2l~{ciR=diuuQ znMGr%6Pxo_ru%hMfih#S`ZwS=yII?Y%Ixj9s=I%WS}-LZOx+sg>L2)bI%vYnT2I>j zxW=vaH|XoOc-5$dyMBjrZp(t?MWl%Y$KmPeuL=F9uNzeSj>WKh!MgN|;@0$)!Qj6% ztKX)r<*D;MMQmBD9v@?DR)vS0Ma^5WjN+WtHEFjYO13L=tk_m@&d%>H-MF{6xWcxV z3p_IrWxWoR2`dpE=`u|;PM!7lEeR}}|@{cts z>S)uWy|FV+gQ%@rYMq`F^l$BZ6ufPmH?y1k)N*W`w}hqBs<2NL>1wV>>c;Qj{Z%TP z)b!1R`U3Xlj=UFGgLe2^QWP&pA|f4L0j%e)%dt+Hej83Z*!3%4(SeI%F3S|#CdIAtMrBnpHQf z{pQG&(?~x~aSq4!6Spi62Xkw1a=C4mNj(+G5QdS|i{dEiu2*)Yx)|z8O6_+x@Smcm zW_EZf;JAJmR@RRePFmZu5g_Hvld7VfPO37q_ZL+7J+U2Qa!sNc}*Wo+K`q`x;HImK-R1ee-HRL|9OnRn@Z5lw7+ z=<9)PyM*@EV^7~_ZXhI9U;+Sx7>JK5Mu%$Em%NH`MvH2NOW&fF$KPQ#xJhE9jM_(X zK87M;ki{ZHPAc`DUOs79bhA@Ar3WW(^d;FF8{ox<93;6yQ!9p9Bq?~4!&syB*?)>` z$Z+qT=5?rP0OQ7?+R zQz=~zCtcQ)I>F%TG_VN~yd<1-q!fyL`sF9v!m%fcBNx{jJu{_J@VsVeejHuD@@GxXz*{ z1agw~6m!#8N2sS+mEfw>nKDBlaNJ)Bbe=0W8j*64lw)4HH8xuw4Qe$Jvy<)*(QDe3 zuGU!X57B>XoCC4NaPzss_k+v|F?pdc7f{y^QB9GvgW2h7EowMP@VT3U#IF7x_cf!S4+$4F+2Xr z_L#8?8;y>*6)`N|v24HQr#KIkhw}}1o-NSjO{yOEg!PQ9sV7yk`1AW8k6U9_ylZ!d+BP0#+~D+(@J^a{N@K zS{GAh#f`f+39&~Uc0fgtkdG~MOk}o$WNcRE{u^+2%$sgJEw6CP!(7{Fwx*4}blPfI z+KXiqaJ2nohdCu)YUE+^YFU$;uoEEz)Pz-Qvy&-|v9YmcE(#8#_C!nF{{W3{uFARu z!sV&B$of-gO^lW~>MmY1vSrk7iz`AG@RBBSO+vEHRARE)7{8h2*uAw@<8_1^gB2+A zR^2-+F~M1`zjMxR5MG-k;w*%dn12m==-9Q-I)99<5_m6a7Gs6&8_kzom&CG{O?q_Z z>U{NRSZhJX++qc4>B*lM#N~E|!N%Su;haY?)6-oERb5Vclvx|J z#BrZlycCHqn=0EYm@;FjX3p{Jag?&~%imd4T+W&;gxtSxeQqrg2@;=tWnAw`vON^6 zudD%ecrFxWsJO;lB~4QuOurDg;VK6CvGtNn;v*_;7aTn_Q0?9)9_AS@SAQBzjfj@2 z<#VFhJSTkcyyJu@g{HjDvbHN@+2}fEc3P5j*H}y2L5N%7Ht-PYBK*X)=2L$mS?9R+ zo`;^x6T!EMTHs@G`nL^mmfFlkw#UCzc=7nFk|MjbyZW8&UW#l_X@!k9Z@e3OVF6Sh zS>{g_cG2zJa8;{zv5kDeN+eqU0G7H@u{mo~R@MpyZrSI&85s_tA+4d9p(d(-UnBD&%QA1+-DcpC7gn0#WyOxv zWtPfH?qcz{akn}Pm&~8VT^KehaLr{}jRZEh*RC?lHR||lrwmt9fhuQFq`W&oC8@HC zA#|3Us*OJpw?;4F_~VY;A#hSoC95jM;+eH7wItKG$B3gA)7{NjVNIJuT-%o?XB-mK zzAYu3W6MhnSWxukaxYr#1Bxg^FaH1+)LvS@FL4c9HL+#9Vl2d&k@>t{YBx5bj8$wq zc);~GzZ9z0HrUQpKpbIr#Mu|ZQsY;XVr1RgiLIdZp0Vx%=43uyUfR+)B3hd_aU8{LQ>8}s<(=5mCiS)sKhq=KuB4u0NlYT0(F&${C}MZ4ZewD6CH zlE&)|*80E$E!fDrs*1C zywx*_YpV|!kf5(GglL}8u8pB~&f?_0Vpr-}3)OHE3AAEJ0ah_6{s%BjiOJ%B}ldvYS6TB2OpSBkawT2E#A z41K0nF@o=$kv2xj=^^fwdL2D=jWRrLucOMRX`_Jby}?=fBP^1DiXgciLMd9+>Ni~0 zk4(N>LD1m6o7@wd_bq$NFEwrWm2Mc~sncS|ce6VJ#k#a5XC#mJgZ}_ts>{u1V47+>Sv!k72HlYoa@;dg%)GV8i&9r{PS|OQ$M(V5 zXzfM{cUE@dM$qT`N2a&w-KbQwvF3GjRhIC6bG>lmg{jZ_C4Edmz||YjCoZHSL&Hv5 zyj0a}I*)x|%Ed7ewrs82R3k2G>Z407Wb-kdHNdO({(FT zr(8+@09goS)$J(_N0fEvw&OX8Y7uXlBC?E@sc7xFl+;oitWb&M7$OL8)KROYPTZKa zUDkoi5^b{6sC9jk(^uu?seW?D0Jn<>wlNU%YN_&fYGaz>KkJ7ZFKHoUk}foPSuq}a z1X!brNIT4sMg)YbQ>xjCtSHvvYZ=nmhH_k<|y-5&?_>oN61!IYM2&TWSL#( z4I`^})KxI;BWg70?wV)U=NS(X@YZhottV60B1iu*{vSX~XuwoU;laplUgeOE(NWu;S6%Wb^y?$8>@ha%G| z@)1#Tj?M4RX9_b8KLRrX%tHWNmNz`VikJt3LHjAOp+~g!gG03_{ylc}&K6;`y zSA`RAZyQ$Q@oN77Dr~r!Cut|82|1L14MbC8Xs)>n(uhY)kc*llMg_HN@JcrXQW7N7 z)lkd0xUHJnOE&K|*r5}A2d1%obUUd^t}-jHTqV?=Vy90{Aj-P5>*!{^lVnBj=9dw! z)ztDTt(el;JOr_;@ZafWGLc5G>T7vRXyoeYx@M}&e$%1brUwth*|ThkM4ZjPEqVPn znJsxeN~=XEtU<~$*tqmXyvjPfwdi|KJyB+%s6r!?ZI+OaFxNlscP=T36~POR7HyPB zbn4+$y{0v>%R;-(N$uYA!k=MQ>dOjpJtmg2k~4hZx4Vo_S2yufwp#m4w@2{HR`-pT zku~PXNK4DpS(Itv_d@f+4VM!wA|vdoRoJ{v)htk7@b`fpl%t^m)na_-QsmE(!!!sys@(ncAn&$8Dssbl}KY85+kwSW;a7 z0I%Vx%H`QvZUw425J4_|aqg`ZFR7EYjaK4A9O7N|wsz3xbk9;&#kU42!Xq6#)aBQ< z2xasL9B)vIdCQ3Y&$60MT#>dIVknO7L~@R%{>s!-j5dK1Hbv^t)ec;=H{8luLy(DT zaz)%yDkDxvXlCHk**q+ElW8n4R@oyfn}G=Vd6)9nO)c$#&D7NL>FRw!g5nK{12G^L zL_kD*Bc%qukupklJQl_3WKi3y?@GX5WwMYD0vP6nSX9cYv>hm{y20ev*974@t~_~{ z9V65BSCw9msp@p~l~-QlpV|kU7Q~BR=C3NMo%CGsKc$8w-KS28wp3bV_{Y+abv!&qG1K!VRre5|C&Rlk7`su6M-o{Qa1=)5Tq}X8WXHQlsKScL zUe_AZ4ZKRJ)H2~%wGK~4pIO^->w8Gxwxgv`OO{%4tS6<&zs4>iduj#3As16V?M0@D z=+E>Bnl__j^|CG}1MXc(nPeOsh)hSMd{w$*3}Woede_m% zU~e{q5V*#aU=0Yc%fvlp&GBifdBu!ZCeZp*V0JFs4h{TZt@bgi*STTUvydxP1qWoPn5?f~KA|M68%Sg+HekG+a zJz_~ b0m7Yj7<%AAuO+LMjrlHHLUb!idd@l_{wy`eL>VZ0)2NSqJ^%}9v240);L z0^ROB!tu+EuE&9vTXoyrtdQ>rbYa*FB^8SJt_$%>jMyRdNA|xSwy8hPsDD>O1q#|7;Vk1J&xViR$^hNB) zWcD`!yxXmwW!Fy_zI8DBi{ka8WClXm=jX~rMojIgD!CEtp2zIZ(i>~BuGBXB4Q~3u zi=x9ee$ME9P_oHud(1{u-LcsMS;dVG-aR902WI|`doCqz#yxN@<3n!W)L~Z9BON@V zMF5R8Y-xwMt=qMY=V_Z`vwugum&5|v-sArOw04kL1 z=N7F71VmE#2g)E+s?VVfH`Mmcn%nJ<+Xmja**&VW#2dAkTsda&p6G^I$cM@$S5_Nn zxjaL#J6o@%2XFZ2(i>>3+H+?1e>^W`?C%?xKH&2d2y%#useDG2oc7{1e2=0qTnA?G zV%}jmEw!Q$jJ+P*K(guO=A_7|8GM?I??`u!dbF2MyY8)i@zU1s{I?8 z`a@v2E)Qn*B;x-7!}|}6T@|)5id%%Ia1EHjq>#h}$Vm9cyzj+bwBXiVa6Z$Oe z$7lViV_l+l&9OMH&ponsrk3_N)*9`x`eDc>GZB!q2xTGaA*WNQT8n5pDAHwXo=5b@ z+wGw3Lv0`0=J5NG!)|Uj7H@9LblXko2D9;V7tLKgA~rWN>T0WKcw^SDeQRU(7Mkr> zUsICI-qH|dxWz}mq^l>Kb%NGCB$C~i9(!`w$vh*bvTvC-_hSd@zhe70WAtnKI^nG1 z1G+7n+0w-ECnXu&8(Y#Jh>XAo#atC1I+r`rFjMmDYo5Kl#~!}FwK2GQ;GB9xl8KQ? ze~P^ddQ;Hzs$|Sdj5f=bY(zDu2Z|%B-S*c?zS~L6$lhV@zoR!{T*tnY*aj_dDh*-I z9nHBpz)Zj;AH!S~-))aZjWhK(wXK22J3#s?;~k&iJTCEsspBQF26g5Z&BfbJ9mAl5z(uz*Li_6u zbT^0EOcq0K*#aDtyi{&L^D|=BJj|Q@;7ZruO4x{ zJjucmG2s!dCKc3`Yac&-C@??QgmcOvZ!r-rq^^oT2PJLIq4QPcsN38tB5{ubA+C)5 z412XBdAB1moIEk+8h+czj%#c&hVUCVu}nt~6}CbK+>WTClw@9BHH_lyJF@Es%U?>Y zOJn;~`e^ne*o~gfo%5D><<2j)9*#l{g@}N}9WJqwN;QL}HETh1^2;VYKW#pN{{Tn+ z(b+cMz%AJ~IPWdXc0#{(MJ}4P3Ynvp?zL9ZrVhgXj-8F{musziMC{KVyS2pd=Me45 z=O)%&LL=rKHP@YPB*zLW+8AF?&!e5j(K63zTPKFj>^)u}%Q6`!G^1TzKUes6yqq0p zNBEu(lXh0>{{Urnvfk?GO|rDO75cd8uU4&UeN7#Qmq{yh6_y%=`vgU#)=_9cW2AYAtJ15|RCVq=N94zX7|b!JN2(%ya^ z@v3f3Q@cXzf=#?6;baFMTFQ4Vpx<)TS)LB_19_LQ&~dWW9EgWXX|D>jGSt-io|&n= z6|tz_u53<*2(WSlX>DY23SRE(nu?$qr+XlzFN@ zG(hlH)=QS8>*o48&Q7?bKySis)Yf0kuh`C)) zDRiEBi8mRJdaJi99Cou@D_e)H;ks}{yTrO_-B>Qz+6`aVmkry2E1a@O{6$mT#>p72 zq)ubRO`|R$!8i}IkHty*bS2j=3fGv0#mOkP;F2$wA}XAIN`D6!J(PO|86IOY>^gS(a6BdE0M)8emEjYX@$ z;#Z{9JIunrMboIY3f9biXU&!p)3 zwxG5{xY^ukoDRjuGfhNF=e0}9mF2%rn_f>=msr@cPSYm^ynE90wZ(Il}M0tyvxtnU*80ki}O;p9lw{A%VyuG~It8SfJB08B?;G*ER<9dk{ zqo2iH5tK2^(M|a%JTbW4xu`@`qr9$|i>=_{>}?ol#gWtF9U~rEYRw~cr;XgLZacRK zSa5S9D2%z1w4KUwB;eW>SqQvRGtxSR!Ok_4cIb9e$yAx)GHpjs9#S85O6_4)-fCgK ztdNQChfn3MTCLo6bo75wjg4FlzAW&vTy&_{cTJN~Tb@T)tN4b6VLv7Dek5bwj1C4G(CP|v| z{8h+^-iM)5vdV@y#_h!!82mM_a~!cMjb<#_knEr3^@lBUdSlT*-CqVBW^PAZ0Tc@U0P5brna+Yy&u> zgi<$`mzGs(xoZYLX9#f}>48L-N^^N?lNm9pxV3^H%VOFPMS1iU_!b zE)H@*{wmTth;>#c+V^)h?a9=UXdbD5y1f3me~8De;khBo+&DYqaluj);qcd+&j-Bi zj`Kae**NBv`praM>fO6w=e^qSRm7YZ97ar(l=-yfT?rVe&sJ7yECTbMj@a)UMazt- zBxb{|?bEMufqi-DmnCr}6cN+n&&0LqQ=@{|@M~7{n>iSzxl)qR?i)?0-`rPjL?lEb z$Ae{BOc+zztP_OmZR>G!A?f}aBNISEzUfhHg%52=iY_}IW$q}ARhtn@ z?zqG*42Lwyco5TAGTZ?X7dPUkrz2&_fC29q=}*m6Lufe^hnuRFkXF!3Pn+AzOqrd- z0Fr9%)8U|{VaraRzB_WP_Xv*WpL?Ys^e+ml52Nva~ zMI|Z(t&3QbawI&et1PM~|L97n9eBMI6f$zxkN#? z3%LB%@%|;@Y>k?5eud#)HBYQnr&GA}J57u9hQwYJRPStjrCw52Wo=JWsHC!|LxAk7 zW4g6)Qc7AxbdhQH*1m3f!Q@m{RByf^T?W{3sjdQJ@ z%VODKHn$A=GC+idd5T3z;hf2un=$z!0^2!SUN(fjUh%J}op&MRqf2bCTM_L z6_#Y3H7%A!ENvaJ9p@7Jd^9%$+XtlV?c+bG*=$_Z-KM@@s-)A@G@pIp4Nf{!LF?wz zOKqHdQm$mA)5+-7tgDPc3)qyLE#W3!68F}rM#nStIJT@;{b=789C33(@pBbz<+AW{ zRq9o>W(Dj(L^p!rWns`hBKx)UvjNme7znC!CagLmDHe%e2xu zhUTLV&eJ?DAu{G=-dd<`m(VAx&r{I0Ulg~-Fpw?bDMe<t97m_f(BWrH`ZN9-V47 zCc3x+XL9DfJ}=6&Qj4|8>L+nV;{ML*pFwfKB92_u=um2k*OSev>nmw6owY6Th=~rY zi%g}Jdx+=GbqSLbGipN}T4j{EV_Gd$#>XR3X5^igH;!(}wJCClkNH$Kyq5~RDQ9$6 z$sd8mrS(7so8H}`%a@I?`NjIIVlo6GUozr6V!3>Y`)_auE7K<)U zN|=A5+sI^V(mTlr*F9BfZBFiv172=}Nj+UQRbgWx5l3L|Ycz z(nZ2v--@H&QnM=R6PFDOWg;BAqZ$VouPj#*-f9e`s;!p9E9ck{Y8fQ7k5^4L(2O%- z+i}5jN#!pWRZ}g=j_aK!!P?wn+d*ezYAB}j!ke-ZjdJxav}v@-tpskPs95(OZS3A4 z-XChsi!JVjp+pfbjWzk5WM`?F`hQ0rS5Jl~Yk=cUmZu&Tfg+lVm45D;#gUC={{V<- ze!3fnqTr%tT*OrvG_I;@(%D$l!qt}?LKsAN6iZEOxfN`3yJx4Vrr3~(i+LVp)zeL~ zWxSc>$|bmXx@1aOm;DBaTv&$79S3dBp%EXOmbHcTV=g%*7r4f18lw{F8n)y@8XfZTI?;Y zyKe|1j}Q@iM@?zrXwB6f9ZRb8?6SwnAn|C6+CX`uoNMg2W2y2E>y{@SwOFAJJnFJR z6pVa4ywuS%X%h!p+ zA#=N=hN>wpCN(QEwuB{oLfE*3r}>t&^6q4{nHJ*}#EK?FQ7p9ks_?~cCNonl5FO!f zPVJ>nztTl@bksqr<6b}CYh-#vknx;nH$n4H7wOHk@2ZP(2*Ef#~V%Ye4 ziF~3urCMaLbF&{_c2lspUd*=E2ZoHsz;bVPI-NM`CGd@Box=rF)NL?3F@txGPfXrt zOb5&6tA%VyX54Ky=cD9BB}5$~)mmkbBx2mjGgk!R?&3l{GOaSpQgO((-Q#S?xk_3~ zR+6jBB@rWdgU&dG9#YOiI;baTcK6wqrzDBSGN5w>t9(O#;y6vJC!4$GqwX3(X`80W zbdTnsSwSmzL`0z+^Bjx+02(rEY#!oax)#gpI0d&+CF^j4hr~YS0LR-JtUqH<*f& z@;q)x-R2jLZFAdccnvpWfVTCutHL9#>3?W+iaik!68q|TapcE^Y>#LgQMbLSZ4YbM zvp0`gSX~9a&f$EmU0xK1(Wv!DJ?&i1>p-!&w|#GUhwOP-Sl&8#j?q2;01seqGUQZ| zPIuQs9$B1*UJ$8?CnfDWuFMf_Biv1_qvzXiXg zCeC7*AJG(cUjyGm47bCr-es{Vx*|d=n2#{aQBsM-YaY|)Zprq`=?RZC&(Zg@oO1E2 zY(R;ptXXVBK^C$z<`IuJsxw(znc&dMde71baNnYjU=}@vY(6aMV|!C@w_8DpGUn`w zkO&YAc|eOu`9!=`OWmA~W7OdL8TxQ`t=SIC+3a2qgyA=sRf8%P_O}+~1Tr!rDFkFv zE^4S{A(EqTeS!2_?K`xN+qN{jK-+DavBE5_G+}MFL`QHV%u71D$8vg)B&(O!{i;Q7=ZQ0(!hS6v@mJe2*1rZeZtBGk;s@vq@t=2Xxf$MH3S1^A`-tAevF zCO)sDMvj*9$Id@N-_e`&iu!-q;=5t7duf8~_QYYg$7ySMv;=O2)yTw5SY!r7LMtLJ zQRT0h@&5p>dOxPwdTbf$GWM^x`HQ9D^!k?b9kD*E*x&s!y_eu_S(ey+B8wWjeUb~N z+1a;!99}M_;`kPxnQ50bd>sD(Y5p-;v-~=${{S)SPfg+Uw1#FrIQ=T0K!2eR&~F|- z%X=y9Yiu^BZLsSxa~vy%ZN|#p;K*ZdkZKqYHlpy?+5H*g-aX^=U#RMpR+nny`c6)= zk555X5p_O7>>sn7Q?y>yupO7R!%5J&#jX7yV-OZa(IX`x@}+(EolR8YicObk`Z@H{ zXC1lhFBP-dd=lpP?=cH@{T9@9n@CdQ9Z{4&2-lrfcOBU6(D=_{et~_xZPn8^7{lS`OzuC)h$azai z5q?_F&Z#DzT{J#}?B}wc%eEQd!}breye|qOAOb8t%tX(pQJjn7@m6gYYmE9}e^_C- z9krdUyLuT-GLaTz%^ITgf-8%xX~GF(00`!ow%_qefqlVSHzGxF^QM8ovhgoX8{`{< z%s!c&li_``eI4=5gTrlD3-aAuVRsjgZo#LTIc7#&x@V`Vn(Y%~!Q?-q=diqMV0%3* za~AD_;|jz!s_<6h1F`f|xGQ!vG9+aq)vHMK#9nw-)&o>@=fHT_8##Rn=H~lzOdG0>WZuCQ@R_5#6(Ay zn>Dq>rQBoP5j-z9AA0`)EjW^0R!e3qeNH+#v&|xg%)-TxITDJ2~HmtncoJ7HTN}-m_CCiiM z&A<9I_7mCP(ko~h;=IgxJ&OYE^ROMH zxLaH|w9FQ2L#E>BTO#)g5k_3qx0KkN^Id&d>Me1NpS)-P0I@cL*u;PR7Vr=FNU6;l zkf61xzqky03&gLmH(M8*FnCMro6fWQ8$)e8XKcFz*nP5O?=h{HWHAJq6~isSg=MI; z(HSdf4!P*ZJ+i+?oKnYOajk{C+G|^jGPp&;WF&?q5-7*YL(S%^qf(+&%*MA)uTIc- z#n#4cMh|-Jt;QE~5OGsWaze|zNM&8h%IlJtwLDn- zy8R^IMSjs7?0hz}7Q${Wwu7_$Y3w@|xE5r$SUt%Tws%>NFo~&;mR?%H+XlGfT@TN} zZ_(}s1E~ukDAtpzm9=VUU2(FAkhU>SU0q%(%;;R0>-HY#T*LP!yo8YwuN$vSsvl9| z4!%u|`&iF*+k#r9&0GwprmMR~G#e(!qT`)84N9W=gS&Z{maM=;VqDr&Nf#|<-(u;b zaV7R*f`lR(cxmeUs&^=!FOh!E;qG8mmvE1olcM?#le6uUTF07CNK?O0DE|Pev6gRX z*+m(0mo+fy6J}nJx;Sd)_=ct19Bg9Co|4BwgQKk6BZNZ;=NQRztZO!FV@hjgIy+mz zxNRa()0ZuCrqulw)c$)u9t2NGgu~K#%X5N!__g}1pbC~MYm*%d@ zH5(ldGpXsYe$`chP2sydFnUOC@MLe)`?ZH_pl89MR09 zD)sWqgUH2~Ng-{wgax~H#Ir3!-lJ13xYHkkRz@Qn^v6!Bdjlq5icE}LaV0LUnwFdD zEU{|?t-SZbFdjb=P}{X0=~7V=am5zWC8k zovep_7Fu+7oR%@Q7I=e(+l-2}(mpRWZBM3T;L5jCPOgq(Tv^)O7V^}{GvZRNyR>q0 z)LLz-5tbW#&ZHdCIO*e6nvj(0Hn1~x@3>!7S%~vey1gKh#fRM9FU+5>bpqM7q4yo8#Ja(OQUTkvQh`p^<4_--fwy+IHn-X!R)K zLN`rm9JOWe)|9qtb5vz@#LFhgOOfcF$SU{$tNy z4eOn+h;8idJlT>49%Mu1?xpy?pVfyR8`fo_k=eOPV@fI| zOdP&9#7{(A9y}u{X{-*0TWOIu>qrQjg%m>OmZ<*#rjpXghie4hG(#coi0UHzwC0GG zqi{$#!8RhvI#E(p<>jGhw=fOVFJqH4jS#kkHzK^sBt2A0d36`{s;y~U!JhRlNBZDg&1LVG z#al+zv4gf+K_KG7ScS7<AJTi8=-N_fc2b5 zm)J!A=qat4@)UJV2G9z)p?j17y^=hUL>%yB>!9wyE$%NPv4tu@}oTFAfvZCFHkd(Q)s1qX2f>zM9 z@lQ1de_=$2F^Xjzzik&ALqI|#Jt&FQ@6LkaA_f-oFIeEx(g=-o2w8AMM2q63l$DGK zTOr^>m&;2j4Z({kyojEvnVd>Q%aM8;qKJ)=apEy9nt806vNRGmZlqqVviYiJaneeq zO`Ngl@-45xQ5@g3MHs(ii|30B=w7vUJlCz!R_uX*?QbdCtJ+F>s@r; z^&EB4rLu#fwZZqAw_1?2kBYR*am3V>qf+gyuH>bpWvAgPxPL-CrJW5c7R3j0q(1&y z>Pg7Y239ItS^6F@a&W~Exrn)zw?%1IIFZ-P)cl^fLXwdTv*w9cxV#*7Q+4}{EOPi| zxM$nMy_$SA=yde`Wbyn)vcq78M-^J?JdscXremsrBxH!QAKO7|A@Y#wpc_N1*}LH^ zJpi7HOT(YdUN=;q#L)WR4C)p-dr5e&gh_W}$;+soXa`5tCD z{54TZ<{ydq1aE{}lY*XKH;SrhnXOfBNS?(B$2V<>B79u6YfWKpDz&`b5U%6IC8Ybf zR*S)lRAg-e{bwImPb`L~%JmnSX6^wmvMnk+JYCgTT!yK-`3W&-j;9AlRUSc8)#cB$ zjIl=1Wg!*ss`8plU#VBr4^9zHdHHIvay;*nKh_(}$eiOXTDfV(JW2dsL}iQ+lyf2S zkJ(yzCJokl8(Rmr&g}jv61XkF=^-wv5YWbt%j_ z%sp<6{X?1Qm3J$Ao>Ru{2#83ENEJqHv?kltCdh}xUu^;QCT%YZN-uGBRN|%H%V5Q; z=QRuy*~>;sHOZSo{u6_-Bzq8)WIB3^xvJG}5bL8!fA0>%0rV#S0R9@x{knOHKKk>Y z%*k^-N=;4I_-91Px+xJvNe)CY$wsRTR_1EB0vn{n-=4Wqi6 zH*3Uv#a<7MbvRM-YI?yu(?qadVS!5RUMO8xrNzHF(L3t2s9CN^vP^ zl`YD_EU~}d_D}RlY z9?b=^pDg`5c9>qi!f-HfrqgEQ!>C7zY51$?d=8qmMstV7T~nn#bKf}IWfG2(Qm>+^ zqaQCyR#!vLu?x-h7 zJ-&#Jukfqe!5FtBg>;12>RwUl<)%wzJeG#(TNENLGRR}qRf}VB3|GQj+`S_%HM;f% ziJN)N+=z#dDDzbNyFjsRal7UjM}bO-g0hlpdx6}ME1cRsT4ejnR=0%A1-Or^RU(h( zs%E$|7Mr{x)j4X&M@>l?NhhU5LV7bDLM2WmlxxA@L)vrOdD2@}T_!`rzG7V_}@BYY;is|Iw(GU{}-d8D+Vl&oNK*xkCvE!O(PZLr(7 z{Vnk;p^@6XaAMLDAn^58!4V%3d1>nsJ8rqqGui8QJ8iT0(--Obzh>~aEin8)cc`8{ zB=HxDKg3k7XJs^YgJp2NqS`DcXfdY^@6@gvwl;Ez3&t_tmxzRUYMXBcrmJ_%lW2u3 z`^ZGJ{gg9YjAYKkwohYhafQaP{gtt9_?+MRP}+|6ej+~=7_{men9?M*df;9=bR$kz zBg&msA}o``q=mNnm9dV0cf&_;r!PX#+#rmJ$s%~ahN;%t7TCbnws=jA;o_FKJ*!s_ zH@hd9wE!~@2@sFj)kz^io8Va1#q3VkTgz&;XAiwW;3ai#CPN;a%coT)_!sjSPkkPK zkJpyxZuez4DS-}O{m<3q@YKTFl39;I+17B}4+*otFnea$wXi^+k5xK#Fe%eD#Ps!ewOBVh!82TnA4W{>t(?{*I?uyB{1+Yo@JEitk*HYV3#U zIog+C{hi?*miA|3viMd9f!g{#jvH%&fW#%tryWG4sEeqJReXn0{hxF_Ur^QRRc3bg z8h#>kjFPVJqnrEhN|j#8GKtFXf< z1?(>2&sao&ebVMpq6uXPY~oU$ku6f7y~dWw;3IL1G%XJjaTJJ4P>+_9_mX)p=@|NM zcGdK4+s&NV{>|LN+iv$~()Q?Ytx3}Q(gY#!lti^wl<{^Zt28O|&dvUnFQoQ6h+Vml zW0rPyJCNPChuDA)^b5S5DK4R{nwt6?>rtfh414Lx^t9T??)HJs-?ojGg8F1Ow?fF~ zrKDmb$|K8Hjhh)!E3}y#N%W8UL2bpJC3UnNt!)kMi!6XSZ*hnbjF(8u<<5zxi y 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́p4`SQkHBW+7$WwW=!=@%*R@0lz96Vv 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

Esi_6Q5WHm& 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�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%0A8e(~M7w)Mn$CrG?Cy=lOUGLzPpgQgVxRSsijMAB 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⁡2?00b}3A=Y&faj$z<8jZ{fV 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℞li2=)eMf^ZT%N2YCvlk!! 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 WhX#Zi2*eG1wE`)No*S40gZ>|sPSME# literal 0 HcmV?d00001