From ef67c82c0f0f7913a8cb0402c41f0b19dd9de123 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Tue, 7 Mar 2017 08:31:18 +0100 Subject: [PATCH 1/5] AI_PATROL_ZONE Added a Stop event and Stopped state to trigger a state transition from * to Stopped when the AI process needs to be stopped. --- Moose Development/Moose/AI/AI_Patrol.lua | 45 ++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/Moose Development/Moose/AI/AI_Patrol.lua b/Moose Development/Moose/AI/AI_Patrol.lua index 8780b3ac0..17e9fc3d0 100644 --- a/Moose Development/Moose/AI/AI_Patrol.lua +++ b/Moose Development/Moose/AI/AI_Patrol.lua @@ -201,6 +201,51 @@ function AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltit self:SetStartState( "None" ) + self:AddTransition( "*", "Stop", "Stopped" ) + +--- OnLeave Transition Handler for State Stopped. +-- @function [parent=#AI_PATROL_ZONE] OnLeaveStopped +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnEnter Transition Handler for State Stopped. +-- @function [parent=#AI_PATROL_ZONE] OnEnterStopped +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- OnBefore Transition Handler for Event Stop. +-- @function [parent=#AI_PATROL_ZONE] OnBeforeStop +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnAfter Transition Handler for Event Stop. +-- @function [parent=#AI_PATROL_ZONE] OnAfterStop +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- Synchronous Event Trigger for Event Stop. +-- @function [parent=#AI_PATROL_ZONE] Stop +-- @param #AI_PATROL_ZONE self + +--- Asynchronous Event Trigger for Event Stop. +-- @function [parent=#AI_PATROL_ZONE] __Stop +-- @param #AI_PATROL_ZONE self +-- @param #number Delay The delay in seconds. + self:AddTransition( "None", "Start", "Patrolling" ) --- OnBefore Transition Handler for Event Start. From af85399975a7fc1c09f910a7757bcad669d550ed Mon Sep 17 00:00:00 2001 From: FlightControl Date: Tue, 7 Mar 2017 09:15:44 +0100 Subject: [PATCH 2/5] Implemented event dispatching for GROUP -- Created EVT-200 test mission -- Documentation --- Moose Development/Moose/AI/AI_Patrol.lua | 5 +- Moose Development/Moose/Core/Event.lua | 149 +- Moose Development/Moose/Wrapper/Group.lua | 25 + .../l10n/DEFAULT/Moose.lua | 33492 +--------------- Moose Mission Setup/Moose.lua | 33492 +--------------- .../EVT-200 - GROUP OnEventShot Example.lua | 28 + .../EVT-200 - GROUP OnEventShot Example.miz | Bin 0 -> 26669 bytes 7 files changed, 201 insertions(+), 66990 deletions(-) create mode 100644 Moose Test Missions/EVT - Event Handling/EVT-200 - GROUP OnEventShot Example/EVT-200 - GROUP OnEventShot Example.lua create mode 100644 Moose Test Missions/EVT - Event Handling/EVT-200 - GROUP OnEventShot Example/EVT-200 - GROUP OnEventShot Example.miz diff --git a/Moose Development/Moose/AI/AI_Patrol.lua b/Moose Development/Moose/AI/AI_Patrol.lua index 17e9fc3d0..b925325a5 100644 --- a/Moose Development/Moose/AI/AI_Patrol.lua +++ b/Moose Development/Moose/AI/AI_Patrol.lua @@ -50,11 +50,14 @@ -- -- * **None** ( Group ): The process is not started yet. -- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. --- * **Returning** ( Group ): The AI is returning to Base.. +-- * **Returning** ( Group ): The AI is returning to Base. +-- * **Stopped** ( Group ): The process is stopped. +-- * **Crashed** ( Group ): The AI has crashed or is dead. -- -- ### 1.2.2) AI_PATROL_ZONE Events -- -- * **Start** ( Group ): Start the process. +-- * **Stop** ( Group ): Stop the process. -- * **Route** ( Group ): Route the AI to a new random 3D point within the Patrol Zone. -- * **RTB** ( Group ): Route the AI to the home base. -- * **Detect** ( Group ): The AI is detecting targets. diff --git a/Moose Development/Moose/Core/Event.lua b/Moose Development/Moose/Core/Event.lua index 42830fee0..9a2d8f8aa 100644 --- a/Moose Development/Moose/Core/Event.lua +++ b/Moose Development/Moose/Core/Event.lua @@ -165,7 +165,9 @@ -- -- Hereby the change log: -- --- * 2016-02-07: Did a complete revision of the Event Handing API and underlying mechanisms. +-- * 2017-03-07: Added the correct event dispatching in case the event is subscribed by a GROUP. +-- +-- * 2017-02-07: Did a complete revision of the Event Handing API and underlying mechanisms. -- -- === -- @@ -179,10 +181,6 @@ -- -- @module Event --- TODO: Need to update the EVENTDATA documentation with IniPlayerName and TgtPlayerName --- TODO: Need to update the EVENTDATA documentation with IniObjectCategory and TgtObjectCategory - - --- The EVENT structure -- @type EVENT @@ -443,8 +441,9 @@ function EVENT:Remove( EventClass, EventID ) self.Events[EventID][EventPriority][EventClass] = nil end ---- Removes an Events entry for a Unit +--- Removes an Events entry for a UNIT. -- @param #EVENT self +-- @param #string UnitName The name of the UNIT. -- @param Core.Base#BASE EventClass The self instance of the class for which the event is. -- @param Dcs.DCSWorld#world.event EventID -- @return #EVENT.Events @@ -457,6 +456,21 @@ function EVENT:RemoveForUnit( UnitName, EventClass, EventID ) Event.IniUnit[UnitName] = nil end +--- Removes an Events entry for a GROUP. +-- @param #EVENT self +-- @param #string GroupName The name of the GROUP. +-- @param Core.Base#BASE EventClass The self instance of the class for which the event is. +-- @param Dcs.DCSWorld#world.event EventID +-- @return #EVENT.Events +function EVENT:RemoveForGroup( GroupName, EventClass, EventID ) + self:F3( { EventClass, _EVENTMETA[EventID].Text } ) + + local EventClass = EventClass + local EventPriority = EventClass:GetEventPriority() + local Event = self.Events[EventID][EventPriority][EventClass] + Event.IniGroup[GroupName] = nil +end + --- Clears all event subscriptions for a @{Base#BASE} derived object. -- @param #EVENT self -- @param Core.Base#BASE EventObject @@ -505,23 +519,43 @@ function EVENT:OnEventGeneric( EventFunction, EventClass, EventID ) end ---- Set a new listener for an S_EVENT_X event +--- Set a new listener for an S_EVENT_X 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 #string UnitName The name of the UNIT. +-- @param #function EventFunction The function to be called when the event occurs for the GROUP. -- @param Core.Base#BASE EventClass The self instance of the class for which the event is. -- @param EventID -- @return #EVENT -function EVENT:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, EventID ) - self:F2( EventDCSUnitName ) +function EVENT:OnEventForUnit( UnitName, EventFunction, EventClass, EventID ) + self:F2( UnitName ) local Event = self:Init( EventID, EventClass ) if not Event.IniUnit then Event.IniUnit = {} end - Event.IniUnit[EventDCSUnitName] = {} - Event.IniUnit[EventDCSUnitName].EventFunction = EventFunction - Event.IniUnit[EventDCSUnitName].EventClass = EventClass + Event.IniUnit[UnitName] = {} + Event.IniUnit[UnitName].EventFunction = EventFunction + Event.IniUnit[UnitName].EventClass = EventClass + return self +end + +--- Set a new listener for an S_EVENT_X event for a GROUP. +-- @param #EVENT self +-- @param #string GroupName The name of the GROUP. +-- @param #function EventFunction The function to be called when the event occurs for the GROUP. +-- @param Core.Base#BASE EventClass The self instance of the class for which the event is. +-- @param EventID +-- @return #EVENT +function EVENT:OnEventForGroup( GroupName, EventFunction, EventClass, EventID ) + self:F2( GroupName ) + + local Event = self:Init( EventID, EventClass ) + if not Event.IniGroup then + Event.IniGroup = {} + end + Event.IniGroup[GroupName] = {} + Event.IniGroup[GroupName].EventFunction = EventFunction + Event.IniGroup[GroupName].EventClass = EventClass return self end @@ -1084,7 +1118,9 @@ function EVENT:onEvent( Event ) if Event.IniDCSGroup and Event.IniDCSGroup:isExist() then Event.IniDCSGroupName = Event.IniDCSGroup:getName() Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) - self:E( { IniGroup = Event.IniGroup } ) + if Event.IniGroup then + Event.IniGroupName = Event.IniDCSGroupName + end end Event.IniPlayerName = Event.IniDCSUnit:getPlayerName() Event.IniCoalition = Event.IniDCSUnit:getCoalition() @@ -1125,6 +1161,10 @@ function EVENT:onEvent( Event ) Event.TgtDCSGroupName = "" if Event.TgtDCSGroup and Event.TgtDCSGroup:isExist() then Event.TgtDCSGroupName = Event.TgtDCSGroup:getName() + Event.TgtGroup = GROUP:FindByName( Event.TgtDCSGroupName ) + if Event.TgtGroup then + Event.TgtGroupName = Event.TgtDCSGroupName + end end Event.TgtPlayerName = Event.TgtDCSUnit:getPlayerName() Event.TgtCoalition = Event.TgtDCSUnit:getCoalition() @@ -1170,6 +1210,8 @@ function EVENT:onEvent( Event ) -- Okay, we got the event from DCS. Now loop the SORTED self.EventSorted[] table for the received Event.id, and for each EventData registered, check if a function needs to be called. for EventClass, EventData in pairs( self.Events[Event.id][EventPriority] ) do + + Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) -- If the EventData is for a UNIT, the call directly the EventClass EventFunction for that UNIT. if Event.IniDCSUnitName and EventData.IniUnit and EventData.IniUnit[Event.IniDCSUnitName] then @@ -1177,8 +1219,7 @@ function EVENT:onEvent( Event ) -- First test if a EventFunction is Set, otherwise search for the default function if EventData.IniUnit[Event.IniDCSUnitName].EventFunction then - self:E( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName, EventPriority } ) - Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) + self:E( { "Calling EventFunction for UNIT ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName, EventPriority } ) local Result, Value = xpcall( function() @@ -1193,7 +1234,6 @@ function EVENT:onEvent( Event ) -- Now call the default event function. self:E( { "Calling " .. _EVENTMETA[Event.id].Event .. " for Class ", EventClass:GetClassNameAndID(), EventPriority } ) - Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) local Result, Value = xpcall( function() @@ -1204,38 +1244,69 @@ function EVENT:onEvent( Event ) end else - - -- If the EventData is not bound to a specific unit, then call the EventClass EventFunction. - -- Note that here the EventFunction will need to implement and determine the logic for the relevant source- or target unit, or weapon. - if Event.IniDCSUnit and not EventData.IniUnit then + + -- If the EventData is for a GROUP, the call directly the EventClass EventFunction for the UNIT in that GROUP. + if Event.IniDCSUnitName and Event.IniDCSGroupName and Event.IniGroupName and EventData.IniGroup and EventData.IniGroup[Event.IniGroupName] then + + -- First test if a EventFunction is Set, otherwise search for the default function + if EventData.IniGroup[Event.IniGroupName].EventFunction then - if EventClass == EventData.EventClass then + self:E( { "Calling EventFunction for GROUP ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName, EventPriority } ) - -- First test if a EventFunction is Set, otherwise search for the default function - if EventData.EventFunction then + local Result, Value = xpcall( + function() + return EventData.IniGroup[Event.IniGroupName].EventFunction( EventClass, Event ) + end, ErrorHandler ) + + else + + -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. + local EventFunction = EventClass[ _EVENTMETA[Event.id].Event ] + if EventFunction and type( EventFunction ) == "function" then + + -- Now call the default event function. + self:E( { "Calling " .. _EVENTMETA[Event.id].Event .. " for GROUP ", EventClass:GetClassNameAndID(), EventPriority } ) - -- There is an EventFunction defined, so call the EventFunction. - self:E( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID(), EventPriority } ) - Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) - local Result, Value = xpcall( function() - return EventData.EventFunction( EventClass, Event ) + return EventFunction( EventClass, Event ) end, ErrorHandler ) - else + end + + end + + else + + -- If the EventData is not bound to a specific unit, then call the EventClass EventFunction. + -- Note that here the EventFunction will need to implement and determine the logic for the relevant source- or target unit, or weapon. + if Event.IniDCSUnit and not EventData.IniUnit then + + if EventClass == EventData.EventClass then - -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. - local EventFunction = EventClass[ _EVENTMETA[Event.id].Event ] - if EventFunction and type( EventFunction ) == "function" then - - -- Now call the default event function. - self:E( { "Calling " .. _EVENTMETA[Event.id].Event .. " for Class ", EventClass:GetClassNameAndID(), EventPriority } ) - Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) + -- First test if a EventFunction is Set, otherwise search for the default function + if EventData.EventFunction then + -- There is an EventFunction defined, so call the EventFunction. + self:E( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID(), EventPriority } ) + local Result, Value = xpcall( function() - return EventFunction( EventClass, Event ) + return EventData.EventFunction( EventClass, Event ) end, ErrorHandler ) + else + + -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. + local EventFunction = EventClass[ _EVENTMETA[Event.id].Event ] + if EventFunction and type( EventFunction ) == "function" then + + -- Now call the default event function. + self:E( { "Calling " .. _EVENTMETA[Event.id].Event .. " for Class ", EventClass:GetClassNameAndID(), EventPriority } ) + + local Result, Value = xpcall( + function() + return EventFunction( EventClass, Event ) + end, ErrorHandler ) + end end end end diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index 998f7ee3e..dccfe2ed6 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -897,4 +897,29 @@ function GROUP:OnReSpawn( ReSpawnFunction ) self.ReSpawnFunction = ReSpawnFunction end +do -- Event Handling + --- Subscribe to a DCS Event. + -- @param #GROUP self + -- @param Core.Event#EVENTS Event + -- @param #function EventFunction (optional) The function to be called when the event occurs for the GROUP. + -- @return #GROUP + function GROUP:HandleEvent( Event, EventFunction ) + + self:EventDispatcher():OnEventForGroup( self:GetName(), EventFunction, self, Event ) + + return self + end + + --- UnSubscribe to a DCS event. + -- @param #GROUP self + -- @param Core.Event#EVENTS Event + -- @return #GROUP + function GROUP:UnHandleEvent( Event ) + + self:EventDispatcher():RemoveForGroup( self:GetName(), self, Event ) + + return self + end + +end 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 8ebe4125c..f73ae2d1c 100644 --- a/Moose Mission Setup/Moose Mission Update/l10n/DEFAULT/Moose.lua +++ b/Moose Mission Setup/Moose Mission Update/l10n/DEFAULT/Moose.lua @@ -1,33489 +1,31 @@ -env.info( '*** MOOSE STATIC INCLUDE START *** ' ) -env.info( 'Moose Generation Timestamp: 20170306_1631' ) +env.info( '*** MOOSE DYNAMIC INCLUDE START *** ' ) +env.info( 'Moose Generation Timestamp: 20170307_0856' ) + local base = _G Include = {} -Include.Files = {} + 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) + 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 + error ("Could not load MOOSE file " .. IncludeFile .. ".lua" ) else - return tostring(tbl) + env.info( "Include:" .. IncludeFile .. " loaded from " .. Include.ProgramPath ) + return f() 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 - - - - - ---3D Vector manipulation -routines.vec = {} - -routines.vec.add = function(vec1, vec2) - return {x = vec1.x + vec2.x, y = vec1.y + vec2.y, z = vec1.z + vec2.z} -end - -routines.vec.sub = function(vec1, vec2) - return {x = vec1.x - vec2.x, y = vec1.y - vec2.y, z = vec1.z - vec2.z} -end - -routines.vec.scalarMult = function(vec, mult) - return {x = vec.x*mult, y = vec.y*mult, z = vec.z*mult} -end - -routines.vec.scalar_mult = routines.vec.scalarMult - -routines.vec.dp = function(vec1, vec2) - return vec1.x*vec2.x + vec1.y*vec2.y + vec1.z*vec2.z -end - -routines.vec.cp = function(vec1, vec2) - return { x = vec1.y*vec2.z - vec1.z*vec2.y, y = vec1.z*vec2.x - vec1.x*vec2.z, z = vec1.x*vec2.y - vec1.y*vec2.x} -end - -routines.vec.mag = function(vec) - return (vec.x^2 + vec.y^2 + vec.z^2)^0.5 -end - -routines.vec.getUnitVec = function(vec) - local mag = routines.vec.mag(vec) - return { x = vec.x/mag, y = vec.y/mag, z = vec.z/mag } -end - -routines.vec.rotateVec2 = function(vec2, theta) - return { x = vec2.x*math.cos(theta) - vec2.y*math.sin(theta), y = vec2.x*math.sin(theta) + vec2.y*math.cos(theta)} -end ---------------------------------------------------------------------------------------------------------------------------- - - - - --- acc- the accuracy of each easting/northing. 0, 1, 2, 3, 4, or 5. -routines.tostringMGRS = function(MGRS, acc) - if acc == 0 then - return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph - else - return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph .. ' ' .. string.format('%0' .. acc .. 'd', routines.utils.round(MGRS.Easting/(10^(5-acc)), 0)) - .. ' ' .. string.format('%0' .. acc .. 'd', routines.utils.round(MGRS.Northing/(10^(5-acc)), 0)) - end -end - ---[[acc: -in DM: decimal point of minutes. -In DMS: decimal point of seconds. -position after the decimal of the least significant digit: -So: -42.32 - acc of 2. -]] -routines.tostringLL = function(lat, lon, acc, DMS) - - local latHemi, lonHemi - if lat > 0 then - latHemi = 'N' - else - latHemi = 'S' - end - - if lon > 0 then - lonHemi = 'E' - else - lonHemi = 'W' - end - - lat = math.abs(lat) - lon = math.abs(lon) - - local latDeg = math.floor(lat) - local latMin = (lat - latDeg)*60 - - local lonDeg = math.floor(lon) - local lonMin = (lon - lonDeg)*60 - - if DMS then -- degrees, minutes, and seconds. - local oldLatMin = latMin - latMin = math.floor(latMin) - local latSec = routines.utils.round((oldLatMin - latMin)*60, acc) - - local oldLonMin = lonMin - lonMin = math.floor(lonMin) - local lonSec = routines.utils.round((oldLonMin - lonMin)*60, acc) - - if latSec == 60 then - latSec = 0 - latMin = latMin + 1 - end - - if lonSec == 60 then - lonSec = 0 - lonMin = lonMin + 1 - end - - local secFrmtStr -- create the formatting string for the seconds place - if acc <= 0 then -- no decimal place. - secFrmtStr = '%02d' - else - local width = 3 + acc -- 01.310 - that's a width of 6, for example. - secFrmtStr = '%0' .. width .. '.' .. acc .. 'f' - end - - return string.format('%02d', latDeg) .. ' ' .. string.format('%02d', latMin) .. '\' ' .. string.format(secFrmtStr, latSec) .. '"' .. latHemi .. ' ' - .. string.format('%02d', lonDeg) .. ' ' .. string.format('%02d', lonMin) .. '\' ' .. string.format(secFrmtStr, lonSec) .. '"' .. lonHemi - - else -- degrees, decimal minutes. - latMin = routines.utils.round(latMin, acc) - lonMin = routines.utils.round(lonMin, acc) - - if latMin == 60 then - latMin = 0 - latDeg = latDeg + 1 - end - - if lonMin == 60 then - lonMin = 0 - lonDeg = lonDeg + 1 - end - - local minFrmtStr -- create the formatting string for the minutes place - if acc <= 0 then -- no decimal place. - minFrmtStr = '%02d' - else - local width = 3 + acc -- 01.310 - that's a width of 6, for example. - minFrmtStr = '%0' .. width .. '.' .. acc .. 'f' - end - - return string.format('%02d', latDeg) .. ' ' .. string.format(minFrmtStr, latMin) .. '\'' .. latHemi .. ' ' - .. string.format('%02d', lonDeg) .. ' ' .. string.format(minFrmtStr, lonMin) .. '\'' .. lonHemi - - end -end - ---[[ required: az - radian - required: dist - meters - optional: alt - meters (set to false or nil if you don't want to use it). - optional: metric - set true to get dist and alt in km and m. - precision will always be nearest degree and NM or km.]] -routines.tostringBR = function(az, dist, alt, metric) - az = routines.utils.round(routines.utils.toDegree(az), 0) - - if metric then - dist = routines.utils.round(dist/1000, 2) - else - dist = routines.utils.round(routines.utils.metersToNM(dist), 2) - end - - local s = string.format('%03d', az) .. ' for ' .. dist - - if alt then - if metric then - s = s .. ' at ' .. routines.utils.round(alt, 0) - else - s = s .. ' at ' .. routines.utils.round(routines.utils.metersToFeet(alt), 0) - end - end - return s -end - -routines.getNorthCorrection = function(point) --gets the correction needed for true north - if not point.z then --Vec2; convert to Vec3 - point.z = point.y - point.y = 0 - end - local lat, lon = coord.LOtoLL(point) - local north_posit = coord.LLtoLO(lat + 1, lon) - return math.atan2(north_posit.z - point.z, north_posit.x - point.x) -end - - -do - local idNum = 0 - - --Simplified event handler - routines.addEventHandler = function(f) --id is optional! - local handler = {} - idNum = idNum + 1 - handler.id = idNum - handler.f = f - handler.onEvent = function(self, event) - self.f(event) - end - world.addEventHandler(handler) - end - - routines.removeEventHandler = function(id) - for key, handler in pairs(world.eventHandlers) do - if handler.id and handler.id == id then - world.eventHandlers[key] = nil - return true - end - end - return false - end -end - --- need to return a Vec3 or Vec2? -function routines.getRandPointInCircle(point, radius, innerRadius) - local theta = 2*math.pi*math.random() - local rad = math.random() + math.random() - if rad > 1 then - rad = 2 - rad - end - - local radMult - if innerRadius and innerRadius <= radius then - radMult = (radius - innerRadius)*rad + innerRadius - else - radMult = radius*rad - end - - if not point.z then --might as well work with vec2/3 - point.z = point.y - end - - local rndCoord - if radius > 0 then - rndCoord = {x = math.cos(theta)*radMult + point.x, y = math.sin(theta)*radMult + point.z} - else - rndCoord = {x = point.x, y = point.z} - end - return rndCoord -end - -routines.goRoute = function(group, path) - local misTask = { - id = 'Mission', - params = { - route = { - points = routines.utils.deepCopy(path), - }, - }, - } - if type(group) == 'string' then - group = Group.getByName(group) - end - local groupCon = group:getController() - if groupCon then - groupCon:setTask(misTask) - return true - end - - Controller.setTask(groupCon, misTask) - return false -end - - --- Useful atomic functions from mist, ported. - -routines.ground = {} -routines.fixedWing = {} -routines.heli = {} - -routines.ground.buildWP = function(point, overRideForm, overRideSpeed) - - local wp = {} - wp.x = point.x - - if point.z then - wp.y = point.z - else - wp.y = point.y - end - local form, speed - - if point.speed and not overRideSpeed then - wp.speed = point.speed - elseif type(overRideSpeed) == 'number' then - wp.speed = overRideSpeed - else - wp.speed = routines.utils.kmphToMps(20) - end - - if point.form and not overRideForm then - form = point.form - else - form = overRideForm - end - - if not form then - wp.action = 'Cone' - else - form = string.lower(form) - if form == 'off_road' or form == 'off road' then - wp.action = 'Off Road' - elseif form == 'on_road' or form == 'on road' then - wp.action = 'On Road' - elseif form == 'rank' or form == 'line_abrest' or form == 'line abrest' or form == 'lineabrest'then - wp.action = 'Rank' - elseif form == 'cone' then - wp.action = 'Cone' - elseif form == 'diamond' then - wp.action = 'Diamond' - elseif form == 'vee' then - wp.action = 'Vee' - elseif form == 'echelon_left' or form == 'echelon left' or form == 'echelonl' then - wp.action = 'EchelonL' - elseif form == 'echelon_right' or form == 'echelon right' or form == 'echelonr' then - wp.action = 'EchelonR' - else - wp.action = 'Cone' -- if nothing matched - end - end - - wp.type = 'Turning Point' - - return wp - -end - -routines.fixedWing.buildWP = function(point, WPtype, speed, alt, altType) - - local wp = {} - wp.x = point.x - - if point.z then - wp.y = point.z - else - wp.y = point.y - end - - if alt and type(alt) == 'number' then - wp.alt = alt - else - wp.alt = 2000 - end - - if altType then - altType = string.lower(altType) - if altType == 'radio' or 'agl' then - wp.alt_type = 'RADIO' - elseif altType == 'baro' or 'asl' then - wp.alt_type = 'BARO' - end - else - wp.alt_type = 'RADIO' - end - - if point.speed then - speed = point.speed - end - - if point.type then - WPtype = point.type - end - - if not speed then - wp.speed = routines.utils.kmphToMps(500) - else - wp.speed = speed - end - - if not WPtype then - wp.action = 'Turning Point' - else - WPtype = string.lower(WPtype) - if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then - wp.action = 'Fly Over Point' - elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then - wp.action = 'Turning Point' - else - wp.action = 'Turning Point' - end - end - - wp.type = 'Turning Point' - return wp -end - -routines.heli.buildWP = function(point, WPtype, speed, alt, altType) - - local wp = {} - wp.x = point.x - - if point.z then - wp.y = point.z - else - wp.y = point.y - end - - if alt and type(alt) == 'number' then - wp.alt = alt - else - wp.alt = 500 - end - - if altType then - altType = string.lower(altType) - if altType == 'radio' or 'agl' then - wp.alt_type = 'RADIO' - elseif altType == 'baro' or 'asl' then - wp.alt_type = 'BARO' - end - else - wp.alt_type = 'RADIO' - end - - if point.speed then - speed = point.speed - end - - if point.type then - WPtype = point.type - end - - if not speed then - wp.speed = routines.utils.kmphToMps(200) - else - wp.speed = speed - end - - if not WPtype then - wp.action = 'Turning Point' - else - WPtype = string.lower(WPtype) - if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then - wp.action = 'Fly Over Point' - elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then - wp.action = 'Turning Point' - else - wp.action = 'Turning Point' - end - end - - wp.type = 'Turning Point' - return wp -end - -routines.groupToRandomPoint = function(vars) - local group = vars.group --Required - local point = vars.point --required - local radius = vars.radius or 0 - local innerRadius = vars.innerRadius - local form = vars.form or 'Cone' - local heading = vars.heading or math.random()*2*math.pi - local headingDegrees = vars.headingDegrees - local speed = vars.speed or routines.utils.kmphToMps(20) - - - local useRoads - if not vars.disableRoads then - useRoads = true - else - useRoads = false - end - - local path = {} - - if headingDegrees then - heading = headingDegrees*math.pi/180 - end - - if heading >= 2*math.pi then - heading = heading - 2*math.pi - end - - local rndCoord = routines.getRandPointInCircle(point, radius, innerRadius) - - local offset = {} - local posStart = routines.getLeadPos(group) - - offset.x = routines.utils.round(math.sin(heading - (math.pi/2)) * 50 + rndCoord.x, 3) - offset.z = routines.utils.round(math.cos(heading + (math.pi/2)) * 50 + rndCoord.y, 3) - path[#path + 1] = routines.ground.buildWP(posStart, form, speed) - - - if useRoads == true and ((point.x - posStart.x)^2 + (point.z - posStart.z)^2)^0.5 > radius * 1.3 then - path[#path + 1] = routines.ground.buildWP({['x'] = posStart.x + 11, ['z'] = posStart.z + 11}, 'off_road', speed) - path[#path + 1] = routines.ground.buildWP(posStart, 'on_road', speed) - path[#path + 1] = routines.ground.buildWP(offset, 'on_road', speed) - else - path[#path + 1] = routines.ground.buildWP({['x'] = posStart.x + 25, ['z'] = posStart.z + 25}, form, speed) - end - - path[#path + 1] = routines.ground.buildWP(offset, form, speed) - path[#path + 1] = routines.ground.buildWP(rndCoord, form, speed) - - routines.goRoute(group, path) - - return -end - -routines.groupRandomDistSelf = function(gpData, dist, form, heading, speed) - local pos = routines.getLeadPos(gpData) - local fakeZone = {} - fakeZone.radius = dist or math.random(300, 1000) - fakeZone.point = {x = pos.x, y, pos.y, z = pos.z} - routines.groupToRandomZone(gpData, fakeZone, form, heading, speed) - - return -end - -routines.groupToRandomZone = function(gpData, zone, form, heading, speed) - if type(gpData) == 'string' then - gpData = Group.getByName(gpData) - end - - if type(zone) == 'string' then - zone = trigger.misc.getZone(zone) - elseif type(zone) == 'table' and not zone.radius then - zone = trigger.misc.getZone(zone[math.random(1, #zone)]) - end - - if speed then - speed = routines.utils.kmphToMps(speed) - end - - local vars = {} - vars.group = gpData - vars.radius = zone.radius - vars.form = form - vars.headingDegrees = heading - vars.speed = speed - vars.point = routines.utils.zoneToVec3(zone) - - routines.groupToRandomPoint(vars) - - return -end - -routines.isTerrainValid = function(coord, terrainTypes) -- vec2/3 and enum or table of acceptable terrain types - if coord.z then - coord.y = coord.z - end - local typeConverted = {} - - if type(terrainTypes) == 'string' then -- if its a string it does this check - for constId, constData in pairs(land.SurfaceType) do - if string.lower(constId) == string.lower(terrainTypes) or string.lower(constData) == string.lower(terrainTypes) then - table.insert(typeConverted, constId) - end - end - elseif type(terrainTypes) == 'table' then -- if its a table it does this check - for typeId, typeData in pairs(terrainTypes) do - for constId, constData in pairs(land.SurfaceType) do - if string.lower(constId) == string.lower(typeData) or string.lower(constData) == string.lower(typeId) then - table.insert(typeConverted, constId) - end - end - end - end - for validIndex, validData in pairs(typeConverted) do - if land.getSurfaceType(coord) == land.SurfaceType[validData] then - return true - end - end - return false -end - -routines.groupToPoint = function(gpData, point, form, heading, speed, useRoads) - if type(point) == 'string' then - point = trigger.misc.getZone(point) - end - if speed then - speed = routines.utils.kmphToMps(speed) - end - - local vars = {} - vars.group = gpData - vars.form = form - vars.headingDegrees = heading - vars.speed = speed - vars.disableRoads = useRoads - vars.point = routines.utils.zoneToVec3(point) - routines.groupToRandomPoint(vars) - - return -end - - -routines.getLeadPos = function(group) - if type(group) == 'string' then -- group name - group = Group.getByName(group) - end - - local units = group:getUnits() - - local leader = units[1] - if not leader then -- SHOULD be good, but if there is a bug, this code future-proofs it then. - local lowestInd = math.huge - for ind, unit in pairs(units) do - if ind < lowestInd then - lowestInd = ind - leader = unit - end - end - end - if leader and Unit.isExist(leader) then -- maybe a little too paranoid now... - return leader:getPosition().p - end -end - ---[[ vars for routines.getMGRSString: -vars.units - table of unit names (NOT unitNameTable- maybe this should change). -vars.acc - integer between 0 and 5, inclusive -]] -routines.getMGRSString = function(vars) - local units = vars.units - local acc = vars.acc or 5 - local avgPos = routines.getAvgPos(units) - if avgPos then - return routines.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(avgPos)), acc) - end -end - ---[[ vars for routines.getLLString -vars.units - table of unit names (NOT unitNameTable- maybe this should change). -vars.acc - integer, number of numbers after decimal place -vars.DMS - if true, output in degrees, minutes, seconds. Otherwise, output in degrees, minutes. - - -]] -routines.getLLString = function(vars) - local units = vars.units - local acc = vars.acc or 3 - local DMS = vars.DMS - local avgPos = routines.getAvgPos(units) - if avgPos then - local lat, lon = coord.LOtoLL(avgPos) - return routines.tostringLL(lat, lon, acc, DMS) - end -end - ---[[ -vars.zone - table of a zone name. -vars.ref - vec3 ref point, maybe overload for vec2 as well? -vars.alt - boolean, if used, includes altitude in string -vars.metric - boolean, gives distance in km instead of NM. -]] -routines.getBRStringZone = function(vars) - local zone = trigger.misc.getZone( vars.zone ) - local ref = routines.utils.makeVec3(vars.ref, 0) -- turn it into Vec3 if it is not already. - local alt = vars.alt - local metric = vars.metric - if zone then - local vec = {x = zone.point.x - ref.x, y = zone.point.y - ref.y, z = zone.point.z - ref.z} - local dir = routines.utils.getDir(vec, ref) - local dist = routines.utils.get2DDist(zone.point, ref) - if alt then - alt = zone.y - end - return routines.tostringBR(dir, dist, alt, metric) - else - env.info( 'routines.getBRStringZone: error: zone is nil' ) - end -end - ---[[ -vars.units- table of unit names (NOT unitNameTable- maybe this should change). -vars.ref - vec3 ref point, maybe overload for vec2 as well? -vars.alt - boolean, if used, includes altitude in string -vars.metric - boolean, gives distance in km instead of NM. -]] -routines.getBRString = function(vars) - local units = vars.units - local ref = routines.utils.makeVec3(vars.ref, 0) -- turn it into Vec3 if it is not already. - local alt = vars.alt - local metric = vars.metric - local avgPos = routines.getAvgPos(units) - if avgPos then - local vec = {x = avgPos.x - ref.x, y = avgPos.y - ref.y, z = avgPos.z - ref.z} - local dir = routines.utils.getDir(vec, ref) - local dist = routines.utils.get2DDist(avgPos, ref) - if alt then - alt = avgPos.y - end - return routines.tostringBR(dir, dist, alt, metric) - end -end - - --- Returns the Vec3 coordinates of the average position of the concentration of units most in the heading direction. ---[[ vars for routines.getLeadingPos: -vars.units - table of unit names -vars.heading - direction -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees -]] -routines.getLeadingPos = function(vars) - local units = vars.units - local heading = vars.heading - local radius = vars.radius - if vars.headingDegrees then - heading = routines.utils.toRadian(vars.headingDegrees) - end - - local unitPosTbl = {} - for i = 1, #units do - local unit = Unit.getByName(units[i]) - if unit and unit:isExist() then - unitPosTbl[#unitPosTbl + 1] = unit:getPosition().p - end - end - if #unitPosTbl > 0 then -- one more more units found. - -- first, find the unit most in the heading direction - local maxPos = -math.huge - - local maxPosInd -- maxPos - the furthest in direction defined by heading; maxPosInd = - for i = 1, #unitPosTbl do - local rotatedVec2 = routines.vec.rotateVec2(routines.utils.makeVec2(unitPosTbl[i]), heading) - if (not maxPos) or maxPos < rotatedVec2.x then - maxPos = rotatedVec2.x - maxPosInd = i - end - end - - --now, get all the units around this unit... - local avgPos - if radius then - local maxUnitPos = unitPosTbl[maxPosInd] - local avgx, avgy, avgz, totNum = 0, 0, 0, 0 - for i = 1, #unitPosTbl do - if routines.utils.get2DDist(maxUnitPos, unitPosTbl[i]) <= radius then - avgx = avgx + unitPosTbl[i].x - avgy = avgy + unitPosTbl[i].y - avgz = avgz + unitPosTbl[i].z - totNum = totNum + 1 - end - end - avgPos = { x = avgx/totNum, y = avgy/totNum, z = avgz/totNum} - else - avgPos = unitPosTbl[maxPosInd] - end - - return avgPos - end -end - - ---[[ vars for routines.getLeadingMGRSString: -vars.units - table of unit names -vars.heading - direction -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees -vars.acc - number, 0 to 5. -]] -routines.getLeadingMGRSString = function(vars) - local pos = routines.getLeadingPos(vars) - if pos then - local acc = vars.acc or 5 - return routines.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(pos)), acc) - end -end - ---[[ vars for routines.getLeadingLLString: -vars.units - table of unit names -vars.heading - direction, number -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees -vars.acc - number of digits after decimal point (can be negative) -vars.DMS - boolean, true if you want DMS. -]] -routines.getLeadingLLString = function(vars) - local pos = routines.getLeadingPos(vars) - if pos then - local acc = vars.acc or 3 - local DMS = vars.DMS - local lat, lon = coord.LOtoLL(pos) - return routines.tostringLL(lat, lon, acc, DMS) - end -end - - - ---[[ vars for routines.getLeadingBRString: -vars.units - table of unit names -vars.heading - direction, number -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees -vars.metric - boolean, if true, use km instead of NM. -vars.alt - boolean, if true, include altitude. -vars.ref - vec3/vec2 reference point. -]] -routines.getLeadingBRString = function(vars) - local pos = routines.getLeadingPos(vars) - if pos then - local ref = vars.ref - local alt = vars.alt - local metric = vars.metric - - local vec = {x = pos.x - ref.x, y = pos.y - ref.y, z = pos.z - ref.z} - local dir = routines.utils.getDir(vec, ref) - local dist = routines.utils.get2DDist(pos, ref) - if alt then - alt = pos.y - end - return routines.tostringBR(dir, dist, alt, metric) - end -end - ---[[ vars for routines.message.add - vars.text = 'Hello World' - vars.displayTime = 20 - vars.msgFor = {coa = {'red'}, countries = {'Ukraine', 'Georgia'}, unitTypes = {'A-10C'}} - -]] - ---[[ vars for routines.msgMGRS -vars.units - table of unit names (NOT unitNameTable- maybe this should change). -vars.acc - integer between 0 and 5, inclusive -vars.text - text in the message -vars.displayTime - self explanatory -vars.msgFor - scope -]] -routines.msgMGRS = function(vars) - local units = vars.units - local acc = vars.acc - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getMGRSString{units = units, acc = acc} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } -end - ---[[ vars for routines.msgLL -vars.units - table of unit names (NOT unitNameTable- maybe this should change) (Yes). -vars.acc - integer, number of numbers after decimal place -vars.DMS - if true, output in degrees, minutes, seconds. Otherwise, output in degrees, minutes. -vars.text - text in the message -vars.displayTime - self explanatory -vars.msgFor - scope -]] -routines.msgLL = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local acc = vars.acc - local DMS = vars.DMS - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getLLString{units = units, acc = acc, DMS = DMS} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } - -end - - ---[[ -vars.units- table of unit names (NOT unitNameTable- maybe this should change). -vars.ref - vec3 ref point, maybe overload for vec2 as well? -vars.alt - boolean, if used, includes altitude in string -vars.metric - boolean, gives distance in km instead of NM. -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] -routines.msgBR = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local ref = vars.ref -- vec2/vec3 will be handled in routines.getBRString - local alt = vars.alt - local metric = vars.metric - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getBRString{units = units, ref = ref, alt = alt, metric = metric} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } - -end - - --------------------------------------------------------------------------------------------- --- basically, just sub-types of routines.msgBR... saves folks the work of getting the ref point. ---[[ -vars.units- table of unit names (NOT unitNameTable- maybe this should change). -vars.ref - string red, blue -vars.alt - boolean, if used, includes altitude in string -vars.metric - boolean, gives distance in km instead of NM. -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] -routines.msgBullseye = function(vars) - if string.lower(vars.ref) == 'red' then - vars.ref = routines.DBs.missionData.bullseye.red - routines.msgBR(vars) - elseif string.lower(vars.ref) == 'blue' then - vars.ref = routines.DBs.missionData.bullseye.blue - routines.msgBR(vars) - end -end - ---[[ -vars.units- table of unit names (NOT unitNameTable- maybe this should change). -vars.ref - unit name of reference point -vars.alt - boolean, if used, includes altitude in string -vars.metric - boolean, gives distance in km instead of NM. -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] - -routines.msgBRA = function(vars) - if Unit.getByName(vars.ref) then - vars.ref = Unit.getByName(vars.ref):getPosition().p - if not vars.alt then - vars.alt = true - end - routines.msgBR(vars) - end -end --------------------------------------------------------------------------------------------- - ---[[ vars for routines.msgLeadingMGRS: -vars.units - table of unit names -vars.heading - direction -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees (optional) -vars.acc - number, 0 to 5. -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] -routines.msgLeadingMGRS = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local heading = vars.heading - local radius = vars.radius - local headingDegrees = vars.headingDegrees - local acc = vars.acc - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getLeadingMGRSString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } - - -end ---[[ vars for routines.msgLeadingLL: -vars.units - table of unit names -vars.heading - direction, number -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees (optional) -vars.acc - number of digits after decimal point (can be negative) -vars.DMS - boolean, true if you want DMS. (optional) -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] -routines.msgLeadingLL = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local heading = vars.heading - local radius = vars.radius - local headingDegrees = vars.headingDegrees - local acc = vars.acc - local DMS = vars.DMS - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getLeadingLLString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc, DMS = DMS} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } - -end - ---[[ -vars.units - table of unit names -vars.heading - direction, number -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees (optional) -vars.metric - boolean, if true, use km instead of NM. (optional) -vars.alt - boolean, if true, include altitude. (optional) -vars.ref - vec3/vec2 reference point. -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] -routines.msgLeadingBR = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local heading = vars.heading - local radius = vars.radius - local headingDegrees = vars.headingDegrees - local metric = vars.metric - local alt = vars.alt - local ref = vars.ref -- vec2/vec3 will be handled in routines.getBRString - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getLeadingBRString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, metric = metric, alt = alt, ref = ref} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } -end - - -function spairs(t, order) - -- collect the keys - local keys = {} - for k in pairs(t) do keys[#keys+1] = k end - - -- if order function given, sort by it by passing the table and keys a, b, - -- otherwise just sort the keys - if order then - table.sort(keys, function(a,b) return order(t, a, b) end) - else - table.sort(keys) - end - - -- return the iterator function - local i = 0 - return function() - i = i + 1 - if keys[i] then - return keys[i], t[keys[i]] - end - end -end - - -function routines.IsPartOfGroupInZones( CargoGroup, LandingZones ) ---trace.f() - - local CurrentZoneID = nil - - if CargoGroup then - local CargoUnits = CargoGroup:getUnits() - for CargoUnitID, CargoUnit in pairs( CargoUnits ) do - if CargoUnit and CargoUnit:getLife() >= 1.0 then - CurrentZoneID = routines.IsUnitInZones( CargoUnit, LandingZones ) - if CurrentZoneID then - break - end - end - end - end - ---trace.r( "", "", { CurrentZoneID } ) - return CurrentZoneID -end - - - -function routines.IsUnitInZones( TransportUnit, LandingZones ) ---trace.f("", "routines.IsUnitInZones" ) - - local TransportZoneResult = nil - local TransportZonePos = nil - local TransportZone = nil - - -- fill-up some local variables to support further calculations to determine location of units within the zone. - if TransportUnit then - local TransportUnitPos = TransportUnit:getPosition().p - if type( LandingZones ) == "table" then - for LandingZoneID, LandingZoneName in pairs( LandingZones ) do - TransportZone = trigger.misc.getZone( LandingZoneName ) - if TransportZone then - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then - TransportZoneResult = LandingZoneID - break - end - end - end - else - TransportZone = trigger.misc.getZone( LandingZones ) - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then - TransportZoneResult = 1 - end - end - if TransportZoneResult then - --trace.i( "routines", "TransportZone:" .. TransportZoneResult ) - else - --trace.i( "routines", "TransportZone:nil logic" ) - end - return TransportZoneResult - else - --trace.i( "routines", "TransportZone:nil hard" ) - return nil - end -end - -function routines.IsUnitNearZonesRadius( TransportUnit, LandingZones, ZoneRadius ) ---trace.f("", "routines.IsUnitInZones" ) - - local TransportZoneResult = nil - local TransportZonePos = nil - local TransportZone = nil - - -- fill-up some local variables to support further calculations to determine location of units within the zone. - if TransportUnit then - local TransportUnitPos = TransportUnit:getPosition().p - if type( LandingZones ) == "table" then - for LandingZoneID, LandingZoneName in pairs( LandingZones ) do - TransportZone = trigger.misc.getZone( LandingZoneName ) - if TransportZone then - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= ZoneRadius ) then - TransportZoneResult = LandingZoneID - break - end - end - end - else - TransportZone = trigger.misc.getZone( LandingZones ) - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= ZoneRadius ) then - TransportZoneResult = 1 - end - end - if TransportZoneResult then - --trace.i( "routines", "TransportZone:" .. TransportZoneResult ) - else - --trace.i( "routines", "TransportZone:nil logic" ) - end - return TransportZoneResult - else - --trace.i( "routines", "TransportZone:nil hard" ) - return nil - end -end - - -function routines.IsStaticInZones( TransportStatic, LandingZones ) ---trace.f() - - local TransportZoneResult = nil - local TransportZonePos = nil - local TransportZone = nil - - -- fill-up some local variables to support further calculations to determine location of units within the zone. - local TransportStaticPos = TransportStatic:getPosition().p - if type( LandingZones ) == "table" then - for LandingZoneID, LandingZoneName in pairs( LandingZones ) do - TransportZone = trigger.misc.getZone( LandingZoneName ) - if TransportZone then - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportStaticPos.x - TransportZonePos.x)^2 + (TransportStaticPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then - TransportZoneResult = LandingZoneID - break - end - end - end - else - TransportZone = trigger.misc.getZone( LandingZones ) - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportStaticPos.x - TransportZonePos.x)^2 + (TransportStaticPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then - TransportZoneResult = 1 - end - end - ---trace.r( "", "", { TransportZoneResult } ) - return TransportZoneResult -end - - -function routines.IsUnitInRadius( CargoUnit, ReferencePosition, Radius ) ---trace.f() - - local Valid = true - - -- fill-up some local variables to support further calculations to determine location of units within the zone. - local CargoPos = CargoUnit:getPosition().p - local ReferenceP = ReferencePosition.p - - if (((CargoPos.x - ReferenceP.x)^2 + (CargoPos.z - ReferenceP.z)^2)^0.5 <= Radius) then - else - Valid = false - end - - return Valid -end - -function routines.IsPartOfGroupInRadius( CargoGroup, ReferencePosition, Radius ) ---trace.f() - - local Valid = true - - Valid = routines.ValidateGroup( CargoGroup, "CargoGroup", Valid ) - - -- fill-up some local variables to support further calculations to determine location of units within the zone - local CargoUnits = CargoGroup:getUnits() - for CargoUnitId, CargoUnit in pairs( CargoUnits ) do - local CargoUnitPos = CargoUnit:getPosition().p --- env.info( 'routines.IsPartOfGroupInRadius: CargoUnitPos.x = ' .. CargoUnitPos.x .. ' CargoUnitPos.z = ' .. CargoUnitPos.z ) - local ReferenceP = ReferencePosition.p --- env.info( 'routines.IsPartOfGroupInRadius: ReferenceGroupPos.x = ' .. ReferenceGroupPos.x .. ' ReferenceGroupPos.z = ' .. ReferenceGroupPos.z ) - - if ((( CargoUnitPos.x - ReferenceP.x)^2 + (CargoUnitPos.z - ReferenceP.z)^2)^0.5 <= Radius) then - else - Valid = false - break - end - end - - return Valid -end - - -function routines.ValidateString( Variable, VariableName, Valid ) ---trace.f() - - if type( Variable ) == "string" then - if Variable == "" then - error( "routines.ValidateString: error: " .. VariableName .. " must be filled out!" ) - Valid = false - end - else - error( "routines.ValidateString: error: " .. VariableName .. " is not a string." ) - Valid = false - end - ---trace.r( "", "", { Valid } ) - return Valid -end - -function routines.ValidateNumber( Variable, VariableName, Valid ) ---trace.f() - - if type( Variable ) == "number" then - else - error( "routines.ValidateNumber: error: " .. VariableName .. " is not a number." ) - Valid = false - end - ---trace.r( "", "", { Valid } ) - return Valid - -end - -function routines.ValidateGroup( Variable, VariableName, Valid ) ---trace.f() - - if Variable == nil then - error( "routines.ValidateGroup: error: " .. VariableName .. " is a nil value!" ) - Valid = false - end - ---trace.r( "", "", { Valid } ) - return Valid -end - -function routines.ValidateZone( LandingZones, VariableName, Valid ) ---trace.f() - - if LandingZones == nil then - error( "routines.ValidateGroup: error: " .. VariableName .. " is a nil value!" ) - Valid = false - end - - if type( LandingZones ) == "table" then - for LandingZoneID, LandingZoneName in pairs( LandingZones ) do - if trigger.misc.getZone( LandingZoneName ) == nil then - error( "routines.ValidateGroup: error: Zone " .. LandingZoneName .. " does not exist!" ) - Valid = false - break - end - end - else - if trigger.misc.getZone( LandingZones ) == nil then - error( "routines.ValidateGroup: error: Zone " .. LandingZones .. " does not exist!" ) - Valid = false - end - end - ---trace.r( "", "", { Valid } ) - return Valid -end - -function routines.ValidateEnumeration( Variable, VariableName, Enum, Valid ) ---trace.f() - - local ValidVariable = false - - for EnumId, EnumData in pairs( Enum ) do - if Variable == EnumData then - ValidVariable = true - break - end - end - - if ValidVariable then - else - error( 'TransportValidateEnum: " .. VariableName .. " is not a valid type.' .. Variable ) - Valid = false - end - ---trace.r( "", "", { Valid } ) - return Valid -end - -function routines.getGroupRoute(groupIdent, task) -- same as getGroupPoints but returns speed and formation type along with vec2 of point} - -- refactor to search by groupId and allow groupId and groupName as inputs - local gpId = groupIdent - if type(groupIdent) == 'string' and not tonumber(groupIdent) then - gpId = _DATABASE.Templates.Groups[groupIdent].groupId - end - - for coa_name, coa_data in pairs(env.mission.coalition) do - if (coa_name == 'red' or coa_name == 'blue') and type(coa_data) == 'table' then - if coa_data.country then --there is a country table - for cntry_id, cntry_data in pairs(coa_data.country) do - for obj_type_name, obj_type_data in pairs(cntry_data) do - if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" then -- only these types have points - if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! - for group_num, group_data in pairs(obj_type_data.group) do - if group_data and group_data.groupId == gpId then -- this is the group we are looking for - if group_data.route and group_data.route.points and #group_data.route.points > 0 then - local points = {} - - for point_num, point in pairs(group_data.route.points) do - local routeData = {} - if not point.point then - routeData.x = point.x - routeData.y = point.y - else - routeData.point = point.point --it's possible that the ME could move to the point = Vec2 notation. - end - routeData.form = point.action - routeData.speed = point.speed - routeData.alt = point.alt - routeData.alt_type = point.alt_type - routeData.airdromeId = point.airdromeId - routeData.helipadId = point.helipadId - routeData.type = point.type - routeData.action = point.action - if task then - routeData.task = point.task - end - points[point_num] = routeData - end - - return points - end - return - end --if group_data and group_data.name and group_data.name == 'groupname' - end --for group_num, group_data in pairs(obj_type_data.group) do - end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then - end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then - end --for obj_type_name, obj_type_data in pairs(cntry_data) do - end --for cntry_id, cntry_data in pairs(coa_data.country) do - end --if coa_data.country then --there is a country table - end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then - end --for coa_name, coa_data in pairs(mission.coalition) do -end - -routines.ground.patrolRoute = function(vars) - - - local tempRoute = {} - local useRoute = {} - local gpData = vars.gpData - if type(gpData) == 'string' then - gpData = Group.getByName(gpData) - end - - local useGroupRoute - if not vars.useGroupRoute then - useGroupRoute = vars.gpData - else - useGroupRoute = vars.useGroupRoute - end - local routeProvided = false - if not vars.route then - if useGroupRoute then - tempRoute = routines.getGroupRoute(useGroupRoute) - end - else - useRoute = vars.route - local posStart = routines.getLeadPos(gpData) - useRoute[1] = routines.ground.buildWP(posStart, useRoute[1].action, useRoute[1].speed) - routeProvided = true - end - - - local overRideSpeed = vars.speed or 'default' - local pType = vars.pType - local offRoadForm = vars.offRoadForm or 'default' - local onRoadForm = vars.onRoadForm or 'default' - - if routeProvided == false and #tempRoute > 0 then - local posStart = routines.getLeadPos(gpData) - - - useRoute[#useRoute + 1] = routines.ground.buildWP(posStart, offRoadForm, overRideSpeed) - for i = 1, #tempRoute do - local tempForm = tempRoute[i].action - local tempSpeed = tempRoute[i].speed - - if offRoadForm == 'default' then - tempForm = tempRoute[i].action - end - if onRoadForm == 'default' then - onRoadForm = 'On Road' - end - if (string.lower(tempRoute[i].action) == 'on road' or string.lower(tempRoute[i].action) == 'onroad' or string.lower(tempRoute[i].action) == 'on_road') then - tempForm = onRoadForm - else - tempForm = offRoadForm - end - - if type(overRideSpeed) == 'number' then - tempSpeed = overRideSpeed - end - - - useRoute[#useRoute + 1] = routines.ground.buildWP(tempRoute[i], tempForm, tempSpeed) - end - - if pType and string.lower(pType) == 'doubleback' then - local curRoute = routines.utils.deepCopy(useRoute) - for i = #curRoute, 2, -1 do - useRoute[#useRoute + 1] = routines.ground.buildWP(curRoute[i], curRoute[i].action, curRoute[i].speed) - end - end - - useRoute[1].action = useRoute[#useRoute].action -- make it so the first WP matches the last WP - end - - local cTask3 = {} - local newPatrol = {} - newPatrol.route = useRoute - newPatrol.gpData = gpData:getName() - cTask3[#cTask3 + 1] = 'routines.ground.patrolRoute(' - cTask3[#cTask3 + 1] = routines.utils.oneLineSerialize(newPatrol) - cTask3[#cTask3 + 1] = ')' - cTask3 = table.concat(cTask3) - local tempTask = { - id = 'WrappedAction', - params = { - action = { - id = 'Script', - params = { - command = cTask3, - - }, - }, - }, - } - - - useRoute[#useRoute].task = tempTask - routines.goRoute(gpData, useRoute) - - return -end - -routines.ground.patrol = function(gpData, pType, form, speed) - local vars = {} - - if type(gpData) == 'table' and gpData:getName() then - gpData = gpData:getName() - end - - vars.useGroupRoute = gpData - vars.gpData = gpData - vars.pType = pType - vars.offRoadForm = form - vars.speed = speed - - routines.ground.patrolRoute(vars) - - return -end - -function routines.GetUnitHeight( CheckUnit ) ---trace.f( "routines" ) - - local UnitPoint = CheckUnit:getPoint() - local UnitPosition = { x = UnitPoint.x, y = UnitPoint.z } - local UnitHeight = UnitPoint.y - - local LandHeight = land.getHeight( UnitPosition ) - - --env.info(( 'CarrierHeight: LandHeight = ' .. LandHeight .. ' CarrierHeight = ' .. CarrierHeight )) - - --trace.f( "routines", "Unit Height = " .. UnitHeight - LandHeight ) - - return UnitHeight - LandHeight - -end - - - -Su34Status = { status = {} } -boardMsgRed = { statusMsg = "" } -boardMsgAll = { timeMsg = "" } -SpawnSettings = {} -Su34MenuPath = {} -Su34Menus = 0 - - -function Su34AttackCarlVinson(groupName) ---trace.menu("", "Su34AttackCarlVinson") - local groupSu34 = Group.getByName( groupName ) - local controllerSu34 = groupSu34.getController(groupSu34) - local groupCarlVinson = Group.getByName("US Carl Vinson #001") - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE ) - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) - if groupCarlVinson ~= nil then - controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupCarlVinson:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = true}}) - end - Su34Status.status[groupName] = 1 - MessageToRed( string.format('%s: ',groupName) .. 'Attacking carrier Carl Vinson. ', 10, 'RedStatus' .. groupName ) -end - -function Su34AttackWest(groupName) ---trace.f("","Su34AttackWest") - local groupSu34 = Group.getByName( groupName ) - local controllerSu34 = groupSu34.getController(groupSu34) - local groupShipWest1 = Group.getByName("US Ship West #001") - local groupShipWest2 = Group.getByName("US Ship West #002") - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE ) - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) - if groupShipWest1 ~= nil then - controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipWest1:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = true}}) - end - if groupShipWest2 ~= nil then - controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipWest2:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = true}}) - end - Su34Status.status[groupName] = 2 - MessageToRed( string.format('%s: ',groupName) .. 'Attacking invading ships in the west. ', 10, 'RedStatus' .. groupName ) -end - -function Su34AttackNorth(groupName) ---trace.menu("","Su34AttackNorth") - local groupSu34 = Group.getByName( groupName ) - local controllerSu34 = groupSu34.getController(groupSu34) - local groupShipNorth1 = Group.getByName("US Ship North #001") - local groupShipNorth2 = Group.getByName("US Ship North #002") - local groupShipNorth3 = Group.getByName("US Ship North #003") - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE ) - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) - if groupShipNorth1 ~= nil then - controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipNorth1:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = false}}) - end - if groupShipNorth2 ~= nil then - controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipNorth2:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = false}}) - end - if groupShipNorth3 ~= nil then - controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipNorth3:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = false}}) - end - Su34Status.status[groupName] = 3 - MessageToRed( string.format('%s: ',groupName) .. 'Attacking invading ships in the north. ', 10, 'RedStatus' .. groupName ) -end - -function Su34Orbit(groupName) ---trace.menu("","Su34Orbit") - local groupSu34 = Group.getByName( groupName ) - local controllerSu34 = groupSu34:getController() - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD ) - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) - controllerSu34:pushTask( {id = 'ControlledTask', params = { task = { id = 'Orbit', params = { pattern = AI.Task.OrbitPattern.RACE_TRACK } }, stopCondition = { duration = 600 } } } ) - Su34Status.status[groupName] = 4 - MessageToRed( string.format('%s: ',groupName) .. 'In orbit and awaiting further instructions. ', 10, 'RedStatus' .. groupName ) -end - -function Su34TakeOff(groupName) ---trace.menu("","Su34TakeOff") - local groupSu34 = Group.getByName( groupName ) - local controllerSu34 = groupSu34:getController() - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD ) - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.BYPASS_AND_ESCAPE ) - Su34Status.status[groupName] = 8 - MessageToRed( string.format('%s: ',groupName) .. 'Take-Off. ', 10, 'RedStatus' .. groupName ) -end - -function Su34Hold(groupName) ---trace.menu("","Su34Hold") - local groupSu34 = Group.getByName( groupName ) - local controllerSu34 = groupSu34:getController() - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD ) - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.BYPASS_AND_ESCAPE ) - Su34Status.status[groupName] = 5 - MessageToRed( string.format('%s: ',groupName) .. 'Holding Weapons. ', 10, 'RedStatus' .. groupName ) -end - -function Su34RTB(groupName) ---trace.menu("","Su34RTB") - Su34Status.status[groupName] = 6 - MessageToRed( string.format('%s: ',groupName) .. 'Return to Krasnodar. ', 10, 'RedStatus' .. groupName ) -end - -function Su34Destroyed(groupName) ---trace.menu("","Su34Destroyed") - Su34Status.status[groupName] = 7 - MessageToRed( string.format('%s: ',groupName) .. 'Destroyed. ', 30, 'RedStatus' .. groupName ) -end - -function GroupAlive( groupName ) ---trace.menu("","GroupAlive") - local groupTest = Group.getByName( groupName ) - - local groupExists = false - - if groupTest then - groupExists = groupTest:isExist() - end - - --trace.r( "", "", { groupExists } ) - return groupExists -end - -function Su34IsDead() ---trace.f() - -end - -function Su34OverviewStatus() ---trace.menu("","Su34OverviewStatus") - local msg = "" - local currentStatus = 0 - local Exists = false - - for groupName, currentStatus in pairs(Su34Status.status) do - - env.info(('Su34 Overview Status: GroupName = ' .. groupName )) - Alive = GroupAlive( groupName ) - - if Alive then - if currentStatus == 1 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Attacking carrier Carl Vinson. " - elseif currentStatus == 2 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Attacking supporting ships in the west. " - elseif currentStatus == 3 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Attacking invading ships in the north. " - elseif currentStatus == 4 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "In orbit and awaiting further instructions. " - elseif currentStatus == 5 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Holding Weapons. " - elseif currentStatus == 6 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Return to Krasnodar. " - elseif currentStatus == 7 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Destroyed. " - elseif currentStatus == 8 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Take-Off. " - end - else - if currentStatus == 7 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Destroyed. " - else - Su34Destroyed(groupName) - end - end - end - - boardMsgRed.statusMsg = msg -end - - -function UpdateBoardMsg() ---trace.f() - Su34OverviewStatus() - MessageToRed( boardMsgRed.statusMsg, 15, 'RedStatus' ) -end - -function MusicReset( flg ) ---trace.f() - trigger.action.setUserFlag(95,flg) -end - -function PlaneActivate(groupNameFormat, flg) ---trace.f() - local groupName = groupNameFormat .. string.format("#%03d", trigger.misc.getUserFlag(flg)) - --trigger.action.outText(groupName,10) - trigger.action.activateGroup(Group.getByName(groupName)) -end - -function Su34Menu(groupName) ---trace.f() - - --env.info(( 'Su34Menu(' .. groupName .. ')' )) - local groupSu34 = Group.getByName( groupName ) - - if Su34Status.status[groupName] == 1 or - Su34Status.status[groupName] == 2 or - Su34Status.status[groupName] == 3 or - Su34Status.status[groupName] == 4 or - Su34Status.status[groupName] == 5 then - if Su34MenuPath[groupName] == nil then - if planeMenuPath == nil then - planeMenuPath = missionCommands.addSubMenuForCoalition( - coalition.side.RED, - "SU-34 anti-ship flights", - nil - ) - end - Su34MenuPath[groupName] = missionCommands.addSubMenuForCoalition( - coalition.side.RED, - "Flight " .. groupName, - planeMenuPath - ) - - missionCommands.addCommandForCoalition( - coalition.side.RED, - "Attack carrier Carl Vinson", - Su34MenuPath[groupName], - Su34AttackCarlVinson, - groupName - ) - - missionCommands.addCommandForCoalition( - coalition.side.RED, - "Attack ships in the west", - Su34MenuPath[groupName], - Su34AttackWest, - groupName - ) - - missionCommands.addCommandForCoalition( - coalition.side.RED, - "Attack ships in the north", - Su34MenuPath[groupName], - Su34AttackNorth, - groupName - ) - - missionCommands.addCommandForCoalition( - coalition.side.RED, - "Hold position and await instructions", - Su34MenuPath[groupName], - Su34Orbit, - groupName - ) - - missionCommands.addCommandForCoalition( - coalition.side.RED, - "Report status", - Su34MenuPath[groupName], - Su34OverviewStatus - ) - end - else - if Su34MenuPath[groupName] then - missionCommands.removeItemForCoalition(coalition.side.RED, Su34MenuPath[groupName]) - end - end -end - ---- Obsolete function, but kept to rework in framework. - -function ChooseInfantry ( TeleportPrefixTable, TeleportMax ) ---trace.f("Spawn") - --env.info(( 'ChooseInfantry: ' )) - - TeleportPrefixTableCount = #TeleportPrefixTable - TeleportPrefixTableIndex = math.random( 1, TeleportPrefixTableCount ) - - --env.info(( 'ChooseInfantry: TeleportPrefixTableIndex = ' .. TeleportPrefixTableIndex .. ' TeleportPrefixTableCount = ' .. TeleportPrefixTableCount .. ' TeleportMax = ' .. TeleportMax )) - - local TeleportFound = false - local TeleportLoop = true - local Index = TeleportPrefixTableIndex - local TeleportPrefix = '' - - while TeleportLoop do - TeleportPrefix = TeleportPrefixTable[Index] - if SpawnSettings[TeleportPrefix] then - if SpawnSettings[TeleportPrefix]['SpawnCount'] - 1 < TeleportMax then - SpawnSettings[TeleportPrefix]['SpawnCount'] = SpawnSettings[TeleportPrefix]['SpawnCount'] + 1 - TeleportFound = true - else - TeleportFound = false - end - else - SpawnSettings[TeleportPrefix] = {} - SpawnSettings[TeleportPrefix]['SpawnCount'] = 0 - TeleportFound = true - end - if TeleportFound then - TeleportLoop = false - else - if Index < TeleportPrefixTableCount then - Index = Index + 1 - else - TeleportLoop = false - end - end - --env.info(( 'ChooseInfantry: Loop 1 - TeleportPrefix = ' .. TeleportPrefix .. ' Index = ' .. Index )) - end - - if TeleportFound == false then - TeleportLoop = true - Index = 1 - while TeleportLoop do - TeleportPrefix = TeleportPrefixTable[Index] - if SpawnSettings[TeleportPrefix] then - if SpawnSettings[TeleportPrefix]['SpawnCount'] - 1 < TeleportMax then - SpawnSettings[TeleportPrefix]['SpawnCount'] = SpawnSettings[TeleportPrefix]['SpawnCount'] + 1 - TeleportFound = true - else - TeleportFound = false - end - else - SpawnSettings[TeleportPrefix] = {} - SpawnSettings[TeleportPrefix]['SpawnCount'] = 0 - TeleportFound = true - end - if TeleportFound then - TeleportLoop = false - else - if Index < TeleportPrefixTableIndex then - Index = Index + 1 - else - TeleportLoop = false - end - end - --env.info(( 'ChooseInfantry: Loop 2 - TeleportPrefix = ' .. TeleportPrefix .. ' Index = ' .. Index )) - end - end - - local TeleportGroupName = '' - if TeleportFound == true then - TeleportGroupName = TeleportPrefix .. string.format("#%03d", SpawnSettings[TeleportPrefix]['SpawnCount'] ) - else - TeleportGroupName = '' - end - - --env.info(('ChooseInfantry: TeleportGroupName = ' .. TeleportGroupName )) - --env.info(('ChooseInfantry: return')) - - return TeleportGroupName -end - -SpawnedInfantry = 0 - -function LandCarrier ( CarrierGroup, LandingZonePrefix ) ---trace.f() - --env.info(( 'LandCarrier: ' )) - --env.info(( 'LandCarrier: CarrierGroup = ' .. CarrierGroup:getName() )) - --env.info(( 'LandCarrier: LandingZone = ' .. LandingZonePrefix )) - - local controllerGroup = CarrierGroup:getController() - - local LandingZone = trigger.misc.getZone(LandingZonePrefix) - local LandingZonePos = {} - LandingZonePos.x = LandingZone.point.x + math.random(LandingZone.radius * -1, LandingZone.radius) - LandingZonePos.y = LandingZone.point.z + math.random(LandingZone.radius * -1, LandingZone.radius) - - controllerGroup:pushTask( { id = 'Land', params = { point = LandingZonePos, durationFlag = true, duration = 10 } } ) - - --env.info(( 'LandCarrier: end' )) -end - -EscortCount = 0 -function EscortCarrier ( CarrierGroup, EscortPrefix, EscortLastWayPoint, EscortEngagementDistanceMax, EscortTargetTypes ) ---trace.f() - --env.info(( 'EscortCarrier: ' )) - --env.info(( 'EscortCarrier: CarrierGroup = ' .. CarrierGroup:getName() )) - --env.info(( 'EscortCarrier: EscortPrefix = ' .. EscortPrefix )) - - local CarrierName = CarrierGroup:getName() - - local EscortMission = {} - local CarrierMission = {} - - local EscortMission = SpawnMissionGroup( EscortPrefix ) - local CarrierMission = SpawnMissionGroup( CarrierGroup:getName() ) - - if EscortMission ~= nil and CarrierMission ~= nil then - - EscortCount = EscortCount + 1 - EscortMissionName = string.format( EscortPrefix .. '#Escort %s', CarrierName ) - EscortMission.name = EscortMissionName - EscortMission.groupId = nil - EscortMission.lateActivation = false - EscortMission.taskSelected = false - - local EscortUnits = #EscortMission.units - for u = 1, EscortUnits do - EscortMission.units[u].name = string.format( EscortPrefix .. '#Escort %s %02d', CarrierName, u ) - EscortMission.units[u].unitId = nil - end - - - EscortMission.route.points[1].task = { id = "ComboTask", - params = - { - tasks = - { - [1] = - { - enabled = true, - auto = false, - id = "Escort", - number = 1, - params = - { - lastWptIndexFlagChangedManually = false, - groupId = CarrierGroup:getID(), - lastWptIndex = nil, - lastWptIndexFlag = false, - engagementDistMax = EscortEngagementDistanceMax, - targetTypes = EscortTargetTypes, - pos = - { - y = 20, - x = 20, - z = 0, - } -- end of ["pos"] - } -- end of ["params"] - } -- end of [1] - } -- end of ["tasks"] - } -- end of ["params"] - } -- end of ["task"] - - SpawnGroupAdd( EscortPrefix, EscortMission ) - - end -end - -function SendMessageToCarrier( CarrierGroup, CarrierMessage ) ---trace.f() - - if CarrierGroup ~= nil then - MessageToGroup( CarrierGroup, CarrierMessage, 30, 'Carrier/' .. CarrierGroup:getName() ) - end - -end - -function MessageToGroup( MsgGroup, MsgText, MsgTime, MsgName ) ---trace.f() - - if type(MsgGroup) == 'string' then - --env.info( 'MessageToGroup: Converted MsgGroup string "' .. MsgGroup .. '" into a Group structure.' ) - MsgGroup = Group.getByName( MsgGroup ) - end - - if MsgGroup ~= nil then - local MsgTable = {} - MsgTable.text = MsgText - MsgTable.displayTime = MsgTime - MsgTable.msgFor = { units = { MsgGroup:getUnits()[1]:getName() } } - MsgTable.name = MsgName - --routines.message.add( MsgTable ) - --env.info(('MessageToGroup: Message sent to ' .. MsgGroup:getUnits()[1]:getName() .. ' -> ' .. MsgText )) - end -end - -function MessageToUnit( UnitName, MsgText, MsgTime, MsgName ) ---trace.f() - - if UnitName ~= nil then - local MsgTable = {} - MsgTable.text = MsgText - MsgTable.displayTime = MsgTime - MsgTable.msgFor = { units = { UnitName } } - MsgTable.name = MsgName - --routines.message.add( MsgTable ) - end -end - -function MessageToAll( MsgText, MsgTime, MsgName ) ---trace.f() - - MESSAGE:New( MsgText, MsgTime, "Message" ):ToCoalition( coalition.side.RED ):ToCoalition( coalition.side.BLUE ) -end - -function MessageToRed( MsgText, MsgTime, MsgName ) ---trace.f() - - MESSAGE:New( MsgText, MsgTime, "To Red Coalition" ):ToCoalition( coalition.side.RED ) -end - -function MessageToBlue( MsgText, MsgTime, MsgName ) ---trace.f() - - MESSAGE:New( MsgText, MsgTime, "To Blue Coalition" ):ToCoalition( coalition.side.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 derived utilities taken from the MIST framework, --- which are excellent tools to be reused in an OO environment!. --- --- ### Authors: --- --- * Grimes : Design & Programming of the MIST framework. --- --- ### Contributions: --- --- * FlightControl : Rework to OO framework --- --- @module Utils - - ---- @type SMOKECOLOR --- @field Green --- @field Red --- @field White --- @field Orange --- @field Blue - -SMOKECOLOR = trigger.smokeColor -- #SMOKECOLOR - ---- @type FLARECOLOR --- @field Green --- @field Red --- @field White --- @field Yellow - -FLARECOLOR = trigger.flareColor -- #FLARECOLOR - ---- Utilities static class. --- @type UTILS -UTILS = {} - - ---from http://lua-users.org/wiki/CopyTable -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 -UTILS.OneLineSerialize = function( tbl ) -- serialization of a table all on a single line, no comments, made to replace old get_table_string function - - lookup_table = {} - - local function _Serialize( tbl ) - - if type(tbl) == 'table' then --function only works for tables! - - if lookup_table[tbl] then - return lookup_table[object] - end - - local tbl_str = {} - - lookup_table[tbl] = tbl_str - - tbl_str[#tbl_str + 1] = '{' - - for ind,val in pairs(tbl) do -- serialize its fields - local ind_str = {} - if type(ind) == "number" then - ind_str[#ind_str + 1] = '[' - ind_str[#ind_str + 1] = tostring(ind) - ind_str[#ind_str + 1] = ']=' - else --must be a string - ind_str[#ind_str + 1] = '[' - ind_str[#ind_str + 1] = routines.utils.basicSerialize(ind) - ind_str[#ind_str + 1] = ']=' - end - - local val_str = {} - if ((type(val) == 'number') or (type(val) == 'boolean')) then - val_str[#val_str + 1] = tostring(val) - val_str[#val_str + 1] = ',' - tbl_str[#tbl_str + 1] = table.concat(ind_str) - tbl_str[#tbl_str + 1] = table.concat(val_str) - elseif type(val) == 'string' then - val_str[#val_str + 1] = routines.utils.basicSerialize(val) - val_str[#val_str + 1] = ',' - tbl_str[#tbl_str + 1] = table.concat(ind_str) - tbl_str[#tbl_str + 1] = table.concat(val_str) - elseif type(val) == 'nil' then -- won't ever happen, right? - val_str[#val_str + 1] = 'nil,' - tbl_str[#tbl_str + 1] = table.concat(ind_str) - tbl_str[#tbl_str + 1] = table.concat(val_str) - elseif type(val) == 'table' then - if ind == "__index" then - -- tbl_str[#tbl_str + 1] = "__index" - -- tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it - else - - val_str[#val_str + 1] = _Serialize(val) - val_str[#val_str + 1] = ',' --I think this is right, I just added it - tbl_str[#tbl_str + 1] = table.concat(ind_str) - tbl_str[#tbl_str + 1] = table.concat(val_str) - end - elseif type(val) == 'function' then - tbl_str[#tbl_str + 1] = "f() " .. tostring(ind) - tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it - else - env.info('unable to serialize value type ' .. routines.utils.basicSerialize(type(val)) .. ' at index ' .. tostring(ind)) - env.info( debug.traceback() ) - end - - end - tbl_str[#tbl_str + 1] = '}' - return table.concat(tbl_str) - else - return tostring(tbl) - end - end - - local objectreturn = _Serialize(tbl) - return objectreturn -end - ---porting in Slmod's "safestring" basic serialize -UTILS.BasicSerialize = function(s) - if s == nil then - return "\"\"" - else - if ((type(s) == 'number') or (type(s) == 'boolean') or (type(s) == 'function') or (type(s) == 'table') or (type(s) == 'userdata') ) then - return tostring(s) - elseif type(s) == 'string' then - s = string.format('%q', s) - return s - end - end -end - - -UTILS.ToDegree = function(angle) - return angle*180/math.pi -end - -UTILS.ToRadian = function(angle) - return angle*math.pi/180 -end - -UTILS.MetersToNM = function(meters) - return meters/1852 -end - -UTILS.MetersToFeet = function(meters) - return meters/0.3048 -end - -UTILS.NMToMeters = function(NM) - return NM*1852 -end - -UTILS.FeetToMeters = function(feet) - return feet*0.3048 -end - -UTILS.MpsToKnots = function(mps) - return mps*3600/1852 -end - -UTILS.MpsToKmph = function(mps) - return mps*3.6 -end - -UTILS.KnotsToMps = function(knots) - return knots*1852/3600 -end - -UTILS.KmphToMps = function(kmph) - return kmph/3.6 -end - ---[[acc: -in DM: decimal point of minutes. -In DMS: decimal point of seconds. -position after the decimal of the least significant digit: -So: -42.32 - acc of 2. -]] -UTILS.tostringLL = function( lat, lon, acc, DMS) - - local latHemi, lonHemi - if lat > 0 then - latHemi = 'N' - else - latHemi = 'S' - end - - if lon > 0 then - lonHemi = 'E' - else - lonHemi = 'W' - end - - lat = math.abs(lat) - lon = math.abs(lon) - - local latDeg = math.floor(lat) - local latMin = (lat - latDeg)*60 - - local lonDeg = math.floor(lon) - local lonMin = (lon - lonDeg)*60 - - if DMS then -- degrees, minutes, and seconds. - local oldLatMin = latMin - latMin = math.floor(latMin) - local latSec = UTILS.Round((oldLatMin - latMin)*60, acc) - - local oldLonMin = lonMin - lonMin = math.floor(lonMin) - local lonSec = UTILS.Round((oldLonMin - lonMin)*60, acc) - - if latSec == 60 then - latSec = 0 - latMin = latMin + 1 - end - - if lonSec == 60 then - lonSec = 0 - lonMin = lonMin + 1 - end - - local secFrmtStr -- create the formatting string for the seconds place - 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 = UTILS.Round(latMin, acc) - lonMin = UTILS.Round(lonMin, acc) - - if latMin == 60 then - latMin = 0 - latDeg = latDeg + 1 - end - - if lonMin == 60 then - lonMin = 0 - lonDeg = lonDeg + 1 - end - - local minFrmtStr -- create the formatting string for the minutes place - if acc <= 0 then -- no decimal place. - minFrmtStr = '%02d' - else - local width = 3 + acc -- 01.310 - that's a width of 6, for example. - minFrmtStr = '%0' .. width .. '.' .. acc .. 'f' - end - - return string.format('%02d', latDeg) .. ' ' .. string.format(minFrmtStr, latMin) .. '\'' .. latHemi .. ' ' - .. string.format('%02d', lonDeg) .. ' ' .. string.format(minFrmtStr, lonMin) .. '\'' .. lonHemi - - end -end - - ---- From http://lua-users.org/wiki/SimpleRound --- use negative idp for rounding ahead of decimal place, positive for rounding after decimal place -function UTILS.Round( num, idp ) - local mult = 10 ^ ( idp or 0 ) - return math.floor( num * mult + 0.5 ) / mult -end - --- porting in Slmod's dostring -function UTILS.DoString( s ) - local f, err = loadstring( s ) - if f then - return true, f() - else - return false, err - end -end ---- 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.2.3) Trace activation. --- --- Tracing can be activated in several ways: --- --- * Switch tracing on or off through the @{#BASE.TraceOnOff}() method. --- * Activate all tracing through the @{#BASE.TraceAll}() method. --- * Activate only the tracing of a certain class (name) through the @{#BASE.TraceClass}() method. --- * Activate only the tracing of a certain method of a certain class through the @{#BASE.TraceClassMethod}() method. --- * Activate only the tracing of a certain level through the @{#BASE.TraceLevel}() method. --- ### 1.2.4) Check if tracing is on. --- --- The method @{#BASE.IsTrace}() will validate if tracing is activated or not. --- --- ## 1.3 DCS simulator Event Handling --- --- The BASE class provides methods to catch DCS Events. These are events that are triggered from within the DCS simulator, --- and handled through lua scripting. MOOSE provides an encapsulation to handle these events more efficiently. --- --- ### 1.3.1 Subscribe / Unsubscribe to DCS Events --- --- At first, the mission designer will need to **Subscribe** to a specific DCS event for the class. --- So, when the DCS event occurs, the class will be notified of that event. --- There are two functions which you use to subscribe to or unsubscribe from an event. --- --- * @{#BASE.HandleEvent}(): Subscribe to a DCS Event. --- * @{#BASE.UnHandleEvent}(): Unsubscribe from a DCS Event. --- --- ### 1.3.2 Event Handling of DCS Events --- --- Once the class is subscribed to the event, an **Event Handling** method on the object or class needs to be written that will be called --- when the DCS event occurs. The Event Handling method receives an @{Event#EVENTDATA} structure, which contains a lot of information --- about the event that occurred. --- --- Find below an example of the prototype how to write an event handling function for two units: --- --- local Tank1 = UNIT:FindByName( "Tank A" ) --- local Tank2 = UNIT:FindByName( "Tank B" ) --- --- -- Here we subscribe to the Dead events. So, if one of these tanks dies, the Tank1 or Tank2 objects will be notified. --- Tank1:HandleEvent( EVENTS.Dead ) --- Tank2:HandleEvent( EVENTS.Dead ) --- --- --- This function is an Event Handling function that will be called when Tank1 is Dead. --- -- @param Wrapper.Unit#UNIT self --- -- @param Core.Event#EVENTDATA EventData --- function Tank1:OnEventDead( EventData ) --- --- self:SmokeGreen() --- end --- --- --- This function is an Event Handling function that will be called when Tank2 is Dead. --- -- @param Wrapper.Unit#UNIT self --- -- @param Core.Event#EVENTDATA EventData --- function Tank2:OnEventDead( EventData ) --- --- self:SmokeBlue() --- end --- --- --- --- See the @{Event} module for more information about event handling. --- --- ## 1.4) Class identification methods --- --- BASE provides methods to get more information of each object: --- --- * @{#BASE.GetClassID}(): Gets the ID (number) of the object. Each object created is assigned a number, that is incremented by one. --- * @{#BASE.GetClassName}(): Gets the name of the object, which is the name of the class the object was instantiated from. --- * @{#BASE.GetClassNameAndID}(): Gets the name and ID of the object. --- --- ## 1.5) All objects derived from BASE can have "States" --- --- A mechanism is in place in MOOSE, that allows to let the objects administer **states**. --- States are essentially properties of objects, which are identified by a **Key** and a **Value**. --- The method @{#BASE.SetState}() can be used to set a Value with a reference Key to the object. --- To **read or retrieve** a state Value based on a Key, use the @{#BASE.GetState} method. --- These two methods provide a very handy way to keep state at long lasting processes. --- Values can be stored within the objects, and later retrieved or changed when needed. --- There is one other important thing to note, the @{#BASE.SetState}() and @{#BASE.GetState} methods --- receive as the **first parameter the object for which the state needs to be set**. --- Thus, if the state is to be set for the same object as the object for which the method is used, then provide the same --- object name to the method. --- --- ## 1.10) BASE Inheritance (tree) support --- --- The following methods are available to support inheritance: --- --- * @{#BASE.Inherit}: Inherits from a class. --- * @{#BASE.GetParent}: Returns the parent object from the object it is handling, or nil if there is no parent object. --- --- ==== --- --- # **API CHANGE HISTORY** --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- YYYY-MM-DD: CLASS:**NewFunction**( Params ) replaces CLASS:_OldFunction_( Params ) --- YYYY-MM-DD: CLASS:**NewFunction( Params )** added --- --- Hereby the change log: --- --- === --- --- # **AUTHORS and CONTRIBUTIONS** --- --- ### Contributions: --- --- * None. --- --- ### Authors: --- --- * **FlightControl**: Design & Programming --- --- @module Base - - - -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, - _Private = {}, - Events = {}, - States = {} -} - ---- The Formation Class --- @type FORMATION --- @field Cone A cone formation. -FORMATION = { - Cone = "Cone" -} - - - --- @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 - - - return self -end - -function BASE:_Destructor() - --self:E("_Destructor") - - --self:EventRemoveAll() -end - -function BASE:_SetDestructor() - - -- TODO: Okay, this is really technical... - -- When you set a proxy to a table to catch __gc, weak tables don't behave like weak... - -- Therefore, I am parking this logic until I've properly discussed all this with the community. - --[[ - local proxy = newproxy(true) - local proxyMeta = getmetatable(proxy) - - proxyMeta.__gc = function () - env.info("In __gc for " .. self:GetClassNameAndID() ) - if self._Destructor then - self:_Destructor() - end - end - - -- keep the userdata from newproxy reachable until the object - -- table is about to be garbage-collected - then the __gc hook - -- will be invoked and the destructor called - rawset( self, '__proxy', proxy ) - --]] -end - ---- 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 - - Child:_SetDestructor() - end - --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:GetParent( 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 string.format( '%s#%09d', self.ClassName, self.ClassID ) -end - ---- Get the ClassName of the class instance. --- @param #BASE self --- @return #string The ClassName of the class instance. -function BASE:GetClassName() - return self.ClassName -end - ---- Get the ClassID of the class instance. --- @param #BASE self --- @return #string The ClassID of the class instance. -function BASE:GetClassID() - return self.ClassID -end - -do -- Event Handling - - --- Returns the event dispatcher - -- @param #BASE self - -- @return Core.Event#EVENT - function BASE:EventDispatcher() - - return _EVENTDISPATCHER - end - - - --- Get the Class @{Event} processing Priority. - -- The Event processing Priority is a number from 1 to 10, - -- reflecting the order of the classes subscribed to the Event to be processed. - -- @param #BASE self - -- @return #number The @{Event} processing Priority. - function BASE:GetEventPriority() - return self._Private.EventPriority or 5 - end - - --- Set the Class @{Event} processing Priority. - -- The Event processing Priority is a number from 1 to 10, - -- reflecting the order of the classes subscribed to the Event to be processed. - -- @param #BASE self - -- @param #number EventPriority The @{Event} processing Priority. - -- @return self - function BASE:SetEventPriority( EventPriority ) - self._Private.EventPriority = EventPriority - end - - --- Remove all subscribed events - -- @param #BASE self - -- @return #BASE - function BASE:EventRemoveAll() - - self:EventDispatcher():RemoveAll( self ) - - return self - end - - --- Subscribe to a DCS Event. - -- @param #BASE self - -- @param Core.Event#EVENTS Event - -- @param #function EventFunction (optional) The function to be called when the event occurs for the unit. - -- @return #BASE - function BASE:HandleEvent( Event, EventFunction ) - - self:EventDispatcher():OnEventGeneric( EventFunction, self, Event ) - - return self - end - - --- UnSubscribe to a DCS event. - -- @param #BASE self - -- @param Core.Event#EVENTS Event - -- @return #BASE - function BASE:UnHandleEvent( Event ) - - self:EventDispatcher():Remove( self, Event ) - - return self - end - - -- Event handling function prototypes - - --- Occurs whenever any unit in a mission fires a weapon. But not any machine gun or autocannon based weapon, those are handled by EVENT.ShootingStart. - -- @function [parent=#BASE] OnEventShot - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs whenever an object is hit by a weapon. - -- initiator : The unit object the fired the weapon - -- weapon: Weapon object that hit the target - -- target: The Object that was hit. - -- @function [parent=#BASE] OnEventHit - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when an aircraft takes off from an airbase, farp, or ship. - -- initiator : The unit that tookoff - -- place: Object from where the AI took-off from. Can be an Airbase Object, FARP, or Ships - -- @function [parent=#BASE] OnEventTakeoff - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when an aircraft lands at an airbase, farp or ship - -- initiator : The unit that has landed - -- place: Object that the unit landed on. Can be an Airbase Object, FARP, or Ships - -- @function [parent=#BASE] OnEventLand - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any aircraft crashes into the ground and is completely destroyed. - -- initiator : The unit that has crashed - -- @function [parent=#BASE] OnEventCrash - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when a pilot ejects from an aircraft - -- initiator : The unit that has ejected - -- @function [parent=#BASE] OnEventEjection - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when an aircraft connects with a tanker and begins taking on fuel. - -- initiator : The unit that is receiving fuel. - -- @function [parent=#BASE] OnEventRefueling - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when an object is completely destroyed. - -- initiator : The unit that is was destroyed. - -- @function [parent=#BASE] OnEvent - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when the pilot of an aircraft is killed. Can occur either if the player is alive and crashes or if a weapon kills the pilot without completely destroying the plane. - -- initiator : The unit that the pilot has died in. - -- @function [parent=#BASE] OnEventPilotDead - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when a ground unit captures either an airbase or a farp. - -- initiator : The unit that captured the base - -- place: The airbase that was captured, can be a FARP or Airbase. When calling place:getCoalition() the faction will already be the new owning faction. - -- @function [parent=#BASE] OnEventBaseCaptured - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when a mission starts - -- @function [parent=#BASE] OnEventMissionStart - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when a mission ends - -- @function [parent=#BASE] OnEventMissionEnd - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when an aircraft is finished taking fuel. - -- initiator : The unit that was receiving fuel. - -- @function [parent=#BASE] OnEventRefuelingStop - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any object is spawned into the mission. - -- initiator : The unit that was spawned - -- @function [parent=#BASE] OnEventBirth - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any system fails on a human controlled aircraft. - -- initiator : The unit that had the failure - -- @function [parent=#BASE] OnEventHumanFailure - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any aircraft starts its engines. - -- initiator : The unit that is starting its engines. - -- @function [parent=#BASE] OnEventEngineStartup - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any aircraft shuts down its engines. - -- initiator : The unit that is stopping its engines. - -- @function [parent=#BASE] OnEventEngineShutdown - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any player assumes direct control of a unit. - -- initiator : The unit that is being taken control of. - -- @function [parent=#BASE] OnEventPlayerEnterUnit - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any player relieves control of a unit to the AI. - -- initiator : The unit that the player left. - -- @function [parent=#BASE] OnEventPlayerLeaveUnit - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any unit begins firing a weapon that has a high rate of fire. Most common with aircraft cannons (GAU-8), autocannons, and machine guns. - -- initiator : The unit that is doing the shooing. - -- target: The unit that is being targeted. - -- @function [parent=#BASE] OnEventShootingStart - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any unit stops firing its weapon. Event will always correspond with a shooting start event. - -- initiator : The unit that was doing the shooing. - -- @function [parent=#BASE] OnEventShootingEnd - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - -end - - ---- Creation of a Birth Event. --- @param #BASE self --- @param Dcs.DCSTypes#Time EventTime The time stamp of the event. --- @param Dcs.DCSWrapper.Object#Object Initiator The initiating object of the event. --- @param #string IniUnitName The initiating unit name. --- @param place --- @param subplace -function BASE:CreateEventBirth( EventTime, Initiator, IniUnitName, place, subplace ) - self:F( { EventTime, Initiator, IniUnitName, place, subplace } ) - - local Event = { - id = world.event.S_EVENT_BIRTH, - time = EventTime, - initiator = Initiator, - IniUnitName = IniUnitName, - place = place, - subplace = subplace - } - - world.onEvent( Event ) -end - ---- Creation of a Crash Event. --- @param #BASE self --- @param Dcs.DCSTypes#Time EventTime The time stamp of the event. --- @param Dcs.DCSWrapper.Object#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 Dcs.DCSTypes#Event structure. ---- The main event handling function... This function captures all events generated for the class. --- @param #BASE self --- @param Dcs.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 - ---- Set a state or property of the Object given a Key and a Value. --- Note that if the Object is destroyed, nillified or garbage collected, then the Values and Keys will also be gone. --- @param #BASE self --- @param Object The object that will hold the Value set by the Key. --- @param Key The key that is used as a reference of the value. Note that the key can be a #string, but it can also be any other type! --- @param Value The value to is stored in the object. --- @return The Value set. --- @return #nil The Key was not found and thus the Value could not be retrieved. -function BASE:SetState( Object, Key, Value ) - - local ClassNameAndID = Object:GetClassNameAndID() - - self.States[ClassNameAndID] = self.States[ClassNameAndID] or {} - self.States[ClassNameAndID][Key] = Value - self:T2( { ClassNameAndID, Key, Value } ) - - return self.States[ClassNameAndID][Key] -end - - ---- Get a Value given a Key from the Object. --- Note that if the Object is destroyed, nillified or garbage collected, then the Values and Keys will also be gone. --- @param #BASE self --- @param Object The object that holds the Value set by the Key. --- @param Key The key that is used to retrieve the value. Note that the key can be a #string, but it can also be any other type! --- @param Value The value to is stored in the Object. --- @return The Value retrieved. -function BASE:GetState( Object, Key ) - - local ClassNameAndID = Object:GetClassNameAndID() - - if self.States[ClassNameAndID] then - local Value = self.States[ClassNameAndID][Key] or false - self:T2( { ClassNameAndID, Key, Value } ) - return Value - 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:TraceOnOff( true ) --- --- -- Switch the tracing Off --- BASE:TraceOnOff( false ) -function BASE:TraceOnOff( TraceOnOff ) - _TraceOnOff = TraceOnOff -end - - ---- Enquires if tracing is on (for the class). --- @param #BASE self --- @return #boolean -function BASE:IsTrace() - - if debug and ( _TraceAll == true ) or ( _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName] ) then - return true - else - return false - end -end - ---- Set trace level --- @param #BASE self --- @param #number Level -function BASE:TraceLevel( Level ) - _TraceLevel = Level - 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 debug and ( _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 debug and _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 debug and _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 debug and _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 debug and ( _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 debug and _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 debug and _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 debug and _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 ) - - if debug then - 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 - -end - - - ---- This module contains the SCHEDULER class. --- --- # 1) @{Scheduler#SCHEDULER} class, extends @{Base#BASE} --- --- The @{Scheduler#SCHEDULER} class creates schedule. --- --- ## 1.1) SCHEDULER constructor --- --- The SCHEDULER class is quite easy to use, but note that the New constructor has variable parameters: --- --- * @{Scheduler#SCHEDULER.New}( nil ): Setup a new SCHEDULER object, which is persistently executed after garbage collection. --- * @{Scheduler#SCHEDULER.New}( Object ): Setup a new SCHEDULER object, which is linked to the Object. When the Object is nillified or destroyed, the SCHEDULER object will also be destroyed and stopped after garbage collection. --- * @{Scheduler#SCHEDULER.New}( nil, Function, FunctionArguments, Start, ... ): Setup a new persistent SCHEDULER object, and start a new schedule for the Function with the defined FunctionArguments according the Start and sequent parameters. --- * @{Scheduler#SCHEDULER.New}( Object, Function, FunctionArguments, Start, ... ): Setup a new SCHEDULER object, linked to Object, and start a new schedule for the Function with the defined FunctionArguments according the Start and sequent parameters. --- --- ## 1.2) SCHEDULER timer stopping and (re-)starting. --- --- The SCHEDULER can be stopped and restarted with the following methods: --- --- * @{Scheduler#SCHEDULER.Start}(): (Re-)Start the schedules within the SCHEDULER object. If a CallID is provided to :Start(), only the schedule referenced by CallID will be (re-)started. --- * @{Scheduler#SCHEDULER.Stop}(): Stop the schedules within the SCHEDULER object. If a CallID is provided to :Stop(), then only the schedule referenced by CallID will be stopped. --- --- ## 1.3) Create a new schedule --- --- With @{Scheduler#SCHEDULER.Schedule}() a new time event can be scheduled. This function is used by the :New() constructor when a new schedule is planned. --- --- === --- --- ### Contributions: --- --- * FlightControl : Concept & Testing --- --- ### Authors: --- --- * FlightControl : Design & Programming --- --- ### Test Missions: --- --- * SCH - Scheduler --- --- === --- --- @module Scheduler - - ---- The SCHEDULER class --- @type SCHEDULER --- @field #number ScheduleID the ID of the scheduler. --- @extends Core.Base#BASE -SCHEDULER = { - ClassName = "SCHEDULER", - Schedules = {}, -} - ---- SCHEDULER constructor. --- @param #SCHEDULER self --- @param #table SchedulerObject Specified for which Moose object the timer is setup. If a value of nil is provided, a scheduler will be setup without an object reference. --- @param #function SchedulerFunction The event function to be called when a timer event occurs. The event function needs to accept the parameters specified in SchedulerArguments. --- @param #table SchedulerArguments Optional arguments that can be given as part of scheduler. The arguments need to be given as a table { param1, param 2, ... }. --- @param #number Start Specifies the amount of seconds that will be waited before the scheduling is started, and the event function is called. --- @param #number Repeat Specifies the interval in seconds when the scheduler will call the event function. --- @param #number RandomizeFactor Specifies a randomization factor between 0 and 1 to randomize the Repeat. --- @param #number Stop Specifies the amount of seconds when the scheduler will be stopped. --- @return #SCHEDULER self. --- @return #number The ScheduleID of the planned schedule. -function SCHEDULER:New( SchedulerObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop ) - local self = BASE:Inherit( self, BASE:New() ) - self:F2( { Start, Repeat, RandomizeFactor, Stop } ) - - local ScheduleID = nil - - self.MasterObject = SchedulerObject - - if SchedulerFunction then - ScheduleID = self:Schedule( SchedulerObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop ) - end - - return self, ScheduleID -end - ---function SCHEDULER:_Destructor() --- --self:E("_Destructor") --- --- _SCHEDULEDISPATCHER:RemoveSchedule( self.CallID ) ---end - ---- Schedule a new time event. Note that the schedule will only take place if the scheduler is *started*. Even for a single schedule event, the scheduler needs to be started also. --- @param #SCHEDULER self --- @param #table SchedulerObject Specified for which Moose object the timer is setup. If a value of nil is provided, a scheduler will be setup without an object reference. --- @param #function SchedulerFunction The event function to be called when a timer event occurs. The event function needs to accept the parameters specified in SchedulerArguments. --- @param #table SchedulerArguments Optional arguments that can be given as part of scheduler. The arguments need to be given as a table { param1, param 2, ... }. --- @param #number Start Specifies the amount of seconds that will be waited before the scheduling is started, and the event function is called. --- @param #number Repeat Specifies the interval in seconds when the scheduler will call the event function. --- @param #number RandomizeFactor Specifies a randomization factor between 0 and 1 to randomize the Repeat. --- @param #number Stop Specifies the amount of seconds when the scheduler will be stopped. --- @return #number The ScheduleID of the planned schedule. -function SCHEDULER:Schedule( SchedulerObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop ) - self:F2( { Start, Repeat, RandomizeFactor, Stop } ) - self:T3( { SchedulerArguments } ) - - local ObjectName = "-" - if SchedulerObject and SchedulerObject.ClassName and SchedulerObject.ClassID then - ObjectName = SchedulerObject.ClassName .. SchedulerObject.ClassID - end - self:F3( { "Schedule :", ObjectName, tostring( SchedulerObject ), Start, Repeat, RandomizeFactor, Stop } ) - self.SchedulerObject = SchedulerObject - - local ScheduleID = _SCHEDULEDISPATCHER:AddSchedule( - self, - SchedulerFunction, - SchedulerArguments, - Start, - Repeat, - RandomizeFactor, - Stop - ) - - self.Schedules[#self.Schedules+1] = ScheduleID - - return ScheduleID -end - ---- (Re-)Starts the schedules or a specific schedule if a valid ScheduleID is provided. --- @param #SCHEDULER self --- @param #number ScheduleID (optional) The ScheduleID of the planned (repeating) schedule. -function SCHEDULER:Start( ScheduleID ) - self:F3( { ScheduleID } ) - - _SCHEDULEDISPATCHER:Start( self, ScheduleID ) -end - ---- Stops the schedules or a specific schedule if a valid ScheduleID is provided. --- @param #SCHEDULER self --- @param #number ScheduleID (optional) The ScheduleID of the planned (repeating) schedule. -function SCHEDULER:Stop( ScheduleID ) - self:F3( { ScheduleID } ) - - _SCHEDULEDISPATCHER:Stop( self, ScheduleID ) -end - ---- Removes a specific schedule if a valid ScheduleID is provided. --- @param #SCHEDULER self --- @param #number ScheduleID (optional) The ScheduleID of the planned (repeating) schedule. -function SCHEDULER:Remove( ScheduleID ) - self:F3( { ScheduleID } ) - - _SCHEDULEDISPATCHER:Remove( self, ScheduleID ) -end - - - - - - - - - - - - - - - ---- This module defines the SCHEDULEDISPATCHER class, which is used by a central object called _SCHEDULEDISPATCHER. --- --- === --- --- Takes care of the creation and dispatching of scheduled functions for SCHEDULER objects. --- --- This class is tricky and needs some thorought explanation. --- SCHEDULE classes are used to schedule functions for objects, or as persistent objects. --- The SCHEDULEDISPATCHER class ensures that: --- --- - Scheduled functions are planned according the SCHEDULER object parameters. --- - Scheduled functions are repeated when requested, according the SCHEDULER object parameters. --- - Scheduled functions are automatically removed when the schedule is finished, according the SCHEDULER object parameters. --- --- The SCHEDULEDISPATCHER class will manage SCHEDULER object in memory during garbage collection: --- - When a SCHEDULER object is not attached to another object (that is, it's first :Schedule() parameter is nil), then the SCHEDULER --- object is _persistent_ within memory. --- - When a SCHEDULER object *is* attached to another object, then the SCHEDULER object is _not persistent_ within memory after a garbage collection! --- The none persistency of SCHEDULERS attached to objects is required to allow SCHEDULER objects to be garbage collectged, when the parent object is also desroyed or nillified and garbage collected. --- Even when there are pending timer scheduled functions to be executed for the SCHEDULER object, --- these will not be executed anymore when the SCHEDULER object has been destroyed. --- --- The SCHEDULEDISPATCHER allows multiple scheduled functions to be planned and executed for one SCHEDULER object. --- The SCHEDULER object therefore keeps a table of "CallID's", which are returned after each planning of a new scheduled function by the SCHEDULEDISPATCHER. --- The SCHEDULER object plans new scheduled functions through the @{Scheduler#SCHEDULER.Schedule}() method. --- The Schedule() method returns the CallID that is the reference ID for each planned schedule. --- --- === --- --- === --- --- ### Contributions: - --- ### Authors: FlightControl : Design & Programming --- --- @module ScheduleDispatcher - ---- The SCHEDULEDISPATCHER structure --- @type SCHEDULEDISPATCHER -SCHEDULEDISPATCHER = { - ClassName = "SCHEDULEDISPATCHER", - CallID = 0, -} - -function SCHEDULEDISPATCHER:New() - local self = BASE:Inherit( self, BASE:New() ) - self:F3() - return self -end - ---- Add a Schedule to the ScheduleDispatcher. --- The development of this method was really tidy. --- It is constructed as such that a garbage collection is executed on the weak tables, when the Scheduler is nillified. --- Nothing of this code should be modified without testing it thoroughly. --- @param #SCHEDULEDISPATCHER self --- @param Core.Scheduler#SCHEDULER Scheduler -function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleArguments, Start, Repeat, Randomize, Stop ) - self:F2( { Scheduler, ScheduleFunction, ScheduleArguments, Start, Repeat, Randomize, Stop } ) - - self.CallID = self.CallID + 1 - - -- Initialize the ObjectSchedulers array, which is a weakly coupled table. - -- If the object used as the key is nil, then the garbage collector will remove the item from the Functions array. - self.PersistentSchedulers = self.PersistentSchedulers or {} - - -- Initialize the ObjectSchedulers array, which is a weakly coupled table. - -- If the object used as the key is nil, then the garbage collector will remove the item from the Functions array. - self.ObjectSchedulers = self.ObjectSchedulers or {} -- setmetatable( {}, { __mode = "v" } ) - - if Scheduler.MasterObject then - self.ObjectSchedulers[self.CallID] = Scheduler - self:F3( { CallID = self.CallID, ObjectScheduler = tostring(self.ObjectSchedulers[self.CallID]), MasterObject = tostring(Scheduler.MasterObject) } ) - else - self.PersistentSchedulers[self.CallID] = Scheduler - self:F3( { CallID = self.CallID, PersistentScheduler = self.PersistentSchedulers[self.CallID] } ) - end - - self.Schedule = self.Schedule or setmetatable( {}, { __mode = "k" } ) - self.Schedule[Scheduler] = self.Schedule[Scheduler] or {} - self.Schedule[Scheduler][self.CallID] = {} - self.Schedule[Scheduler][self.CallID].Function = ScheduleFunction - self.Schedule[Scheduler][self.CallID].Arguments = ScheduleArguments - self.Schedule[Scheduler][self.CallID].StartTime = timer.getTime() + ( Start or 0 ) - self.Schedule[Scheduler][self.CallID].Start = Start + .1 - self.Schedule[Scheduler][self.CallID].Repeat = Repeat - self.Schedule[Scheduler][self.CallID].Randomize = Randomize - self.Schedule[Scheduler][self.CallID].Stop = Stop - - self:T3( self.Schedule[Scheduler][self.CallID] ) - - self.Schedule[Scheduler][self.CallID].CallHandler = function( CallID ) - self:F2( CallID ) - - local ErrorHandler = function( errmsg ) - env.info( "Error in timer function: " .. errmsg ) - if debug ~= nil then - env.info( debug.traceback() ) - end - return errmsg - end - - local Scheduler = self.ObjectSchedulers[CallID] - if not Scheduler then - Scheduler = self.PersistentSchedulers[CallID] - end - - self:T3( { Scheduler = Scheduler } ) - - if Scheduler then - - local Schedule = self.Schedule[Scheduler][CallID] - - self:T3( { Schedule = Schedule } ) - - local ScheduleObject = Scheduler.SchedulerObject - --local ScheduleObjectName = Scheduler.SchedulerObject:GetNameAndClassID() - local ScheduleFunction = Schedule.Function - local ScheduleArguments = Schedule.Arguments - local Start = Schedule.Start - local Repeat = Schedule.Repeat or 0 - local Randomize = Schedule.Randomize or 0 - local Stop = Schedule.Stop or 0 - local ScheduleID = Schedule.ScheduleID - - local Status, Result - if ScheduleObject then - local function Timer() - return ScheduleFunction( ScheduleObject, unpack( ScheduleArguments ) ) - end - Status, Result = xpcall( Timer, ErrorHandler ) - else - local function Timer() - return ScheduleFunction( unpack( ScheduleArguments ) ) - end - Status, Result = xpcall( Timer, ErrorHandler ) - end - - local CurrentTime = timer.getTime() - local StartTime = CurrentTime + Start - - if Status and (( Result == nil ) or ( Result and Result ~= false ) ) then - if Repeat ~= 0 and ( Stop == 0 ) or ( Stop ~= 0 and CurrentTime <= StartTime + Stop ) then - local ScheduleTime = - CurrentTime + - Repeat + - math.random( - - ( Randomize * Repeat / 2 ), - ( Randomize * Repeat / 2 ) - ) + - 0.01 - self:T3( { Repeat = CallID, CurrentTime, ScheduleTime, ScheduleArguments } ) - return ScheduleTime -- returns the next time the function needs to be called. - else - self:Stop( Scheduler, CallID ) - end - else - self:Stop( Scheduler, CallID ) - end - else - self:E( "Scheduled obscolete call for CallID: " .. CallID ) - end - - return nil - end - - self:Start( Scheduler, self.CallID ) - - return self.CallID -end - -function SCHEDULEDISPATCHER:RemoveSchedule( Scheduler, CallID ) - self:F2( { Remove = CallID, Scheduler = Scheduler } ) - - if CallID then - self:Stop( Scheduler, CallID ) - self.Schedule[Scheduler][CallID] = nil - end -end - -function SCHEDULEDISPATCHER:Start( Scheduler, CallID ) - self:F2( { Start = CallID, Scheduler = Scheduler } ) - - if CallID then - local Schedule = self.Schedule[Scheduler] - Schedule[CallID].ScheduleID = timer.scheduleFunction( - Schedule[CallID].CallHandler, - CallID, - timer.getTime() + Schedule[CallID].Start - ) - else - for CallID, Schedule in pairs( self.Schedule[Scheduler] ) do - self:Start( Scheduler, CallID ) -- Recursive - end - end -end - -function SCHEDULEDISPATCHER:Stop( Scheduler, CallID ) - self:F2( { Stop = CallID, Scheduler = Scheduler } ) - - if CallID then - local Schedule = self.Schedule[Scheduler] - timer.removeFunction( Schedule[CallID].ScheduleID ) - else - for CallID, Schedule in pairs( self.Schedule[Scheduler] ) do - self:Stop( Scheduler, CallID ) -- Recursive - end - end -end - - - ---- This core module models the dispatching of DCS Events to subscribed MOOSE classes, --- following a given priority. --- --- ![Banner Image](..\Presentations\EVENT\Dia1.JPG) --- --- === --- --- # 1) Event Handling Overview --- --- ![Objects](..\Presentations\EVENT\Dia2.JPG) --- --- Within a running mission, various DCS events occur. Units are dynamically created, crash, die, shoot stuff, get hit etc. --- This module provides a mechanism to dispatch those events occuring within your running mission, to the different objects orchestrating your mission. --- --- ![Objects](..\Presentations\EVENT\Dia3.JPG) --- --- Objects can subscribe to different events. The Event dispatcher will publish the received DCS events to the subscribed MOOSE objects, in a specified order. --- In this way, the subscribed MOOSE objects are kept in sync with your evolving running mission. --- --- ## 1.1) Event Dispatching --- --- ![Objects](..\Presentations\EVENT\Dia4.JPG) --- --- The _EVENTDISPATCHER object is automatically created within MOOSE, --- and handles the dispatching of DCS Events occurring --- in the simulator to the subscribed objects --- in the correct processing order. --- --- ![Objects](..\Presentations\EVENT\Dia5.JPG) --- --- There are 5 levels of kind of objects that the _EVENTDISPATCHER services: --- --- * _DATABASE object: The core of the MOOSE objects. Any object that is created, deleted or updated, is done in this database. --- * SET_ derived classes: Subsets of the _DATABASE object. These subsets are updated by the _EVENTDISPATCHER as the second priority. --- * UNIT objects: UNIT objects can subscribe to DCS events. Each DCS event will be directly published to teh subscribed UNIT object. --- * GROUP objects: GROUP objects can subscribe to DCS events. Each DCS event will be directly published to the subscribed GROUP object. --- * Any other object: Various other objects can subscribe to DCS events. Each DCS event triggered will be published to each subscribed object. --- --- ![Objects](..\Presentations\EVENT\Dia6.JPG) --- --- For most DCS events, the above order of updating will be followed. --- --- ![Objects](..\Presentations\EVENT\Dia7.JPG) --- --- But for some DCS events, the publishing order is reversed. This is due to the fact that objects need to be **erased** instead of added. --- --- ## 1.2) Event Handling --- --- ![Objects](..\Presentations\EVENT\Dia8.JPG) --- --- The actual event subscribing and handling is not facilitated through the _EVENTDISPATCHER, but it is done through the @{BASE} class, @{UNIT} class and @{GROUP} class. --- The _EVENTDISPATCHER is a component that is quietly working in the background of MOOSE. --- --- ![Objects](..\Presentations\EVENT\Dia9.JPG) --- --- The BASE class provides methods to catch DCS Events. These are events that are triggered from within the DCS simulator, --- and handled through lua scripting. MOOSE provides an encapsulation to handle these events more efficiently. --- --- ### 1.2.1 Subscribe / Unsubscribe to DCS Events --- --- At first, the mission designer will need to **Subscribe** to a specific DCS event for the class. --- So, when the DCS event occurs, the class will be notified of that event. --- There are two functions which you use to subscribe to or unsubscribe from an event. --- --- * @{Base#BASE.HandleEvent}(): Subscribe to a DCS Event. --- * @{Base#BASE.UnHandleEvent}(): Unsubscribe from a DCS Event. --- --- ### 1.3.2 Event Handling of DCS Events --- --- Once the class is subscribed to the event, an **Event Handling** method on the object or class needs to be written that will be called --- when the DCS event occurs. The Event Handling method receives an @{Event#EVENTDATA} structure, which contains a lot of information --- about the event that occurred. --- --- Find below an example of the prototype how to write an event handling function for two units: --- --- local Tank1 = UNIT:FindByName( "Tank A" ) --- local Tank2 = UNIT:FindByName( "Tank B" ) --- --- -- Here we subscribe to the Dead events. So, if one of these tanks dies, the Tank1 or Tank2 objects will be notified. --- Tank1:HandleEvent( EVENTS.Dead ) --- Tank2:HandleEvent( EVENTS.Dead ) --- --- --- This function is an Event Handling function that will be called when Tank1 is Dead. --- -- @param Wrapper.Unit#UNIT self --- -- @param Core.Event#EVENTDATA EventData --- function Tank1:OnEventDead( EventData ) --- --- self:SmokeGreen() --- end --- --- --- This function is an Event Handling function that will be called when Tank2 is Dead. --- -- @param Wrapper.Unit#UNIT self --- -- @param Core.Event#EVENTDATA EventData --- function Tank2:OnEventDead( EventData ) --- --- self:SmokeBlue() --- end --- --- ### 1.3.3 Event Handling methods that are automatically called upon subscribed DCS events --- --- ![Objects](..\Presentations\EVENT\Dia10.JPG) --- --- The following list outlines which EVENTS item in the structure corresponds to which Event Handling method. --- Always ensure that your event handling methods align with the events being subscribed to, or nothing will be executed. --- --- # 2) EVENTS type --- --- The EVENTS structure contains names for all the different DCS events that objects can subscribe to using the --- @{Base#BASE.HandleEvent}() method. --- --- # 3) EVENTDATA type --- --- The @{Event#EVENTDATA} structure contains all the fields that are populated with event information before --- an Event Handler method is being called by the event dispatcher. --- The Event Handler received the EVENTDATA object as a parameter, and can be used to investigate further the different events. --- There are basically 4 main categories of information stored in the EVENTDATA structure: --- --- * Initiator Unit data: Several fields documenting the initiator unit related to the event. --- * Target Unit data: Several fields documenting the target unit related to the event. --- * Weapon data: Certain events populate weapon information. --- * Place data: Certain events populate place information. --- --- --- This function is an Event Handling function that will be called when Tank1 is Dead. --- -- EventData is an EVENTDATA structure. --- -- We use the EventData.IniUnit to smoke the tank Green. --- -- @param Wrapper.Unit#UNIT self --- -- @param Core.Event#EVENTDATA EventData --- function Tank1:OnEventDead( EventData ) --- --- EventData.IniUnit:SmokeGreen() --- end --- --- --- Find below an overview which events populate which information categories: --- --- ![Objects](..\Presentations\EVENT\Dia14.JPG) --- --- **IMPORTANT NOTE:** Some events can involve not just UNIT objects, but also STATIC objects!!! --- In that case the initiator or target unit fields will refer to a STATIC object! --- In case a STATIC object is involved, the documentation indicates which fields will and won't not be populated. --- The fields **IniObjectCategory** and **TgtObjectCategory** contain the indicator which **kind of object is involved** in the event. --- You can use the enumerator **Object.Category.UNIT** and **Object.Category.STATIC** to check on IniObjectCategory and TgtObjectCategory. --- Example code snippet: --- --- if Event.IniObjectCategory == Object.Category.UNIT then --- ... --- end --- if Event.IniObjectCategory == Object.Category.STATIC then --- ... --- end --- --- When a static object is involved in the event, the Group and Player fields won't be populated. --- --- ==== --- --- # **API CHANGE HISTORY** --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- YYYY-MM-DD: CLASS:**NewFunction**( Params ) replaces CLASS:_OldFunction_( Params ) --- YYYY-MM-DD: CLASS:**NewFunction( Params )** added --- --- Hereby the change log: --- --- * 2016-02-07: Did a complete revision of the Event Handing API and underlying mechanisms. --- --- === --- --- # **AUTHORS and CONTRIBUTIONS** --- --- ### Contributions: --- --- ### Authors: --- --- * [**FlightControl**](https://forums.eagle.ru/member.php?u=89536): Design & Programming & documentation. --- --- @module Event - --- TODO: Need to update the EVENTDATA documentation with IniPlayerName and TgtPlayerName --- TODO: Need to update the EVENTDATA documentation with IniObjectCategory and TgtObjectCategory - - - ---- The EVENT structure --- @type EVENT --- @field #EVENT.Events Events --- @extends Core.Base#BASE -EVENT = { - ClassName = "EVENT", - ClassID = 0, -} - ---- The different types of events supported by MOOSE. --- Use this structure to subscribe to events using the @{Base#BASE.HandleEvent}() method. --- @type EVENTS -EVENTS = { - Shot = world.event.S_EVENT_SHOT, - Hit = world.event.S_EVENT_HIT, - Takeoff = world.event.S_EVENT_TAKEOFF, - Land = world.event.S_EVENT_LAND, - Crash = world.event.S_EVENT_CRASH, - Ejection = world.event.S_EVENT_EJECTION, - Refueling = world.event.S_EVENT_REFUELING, - Dead = world.event.S_EVENT_DEAD, - PilotDead = world.event.S_EVENT_PILOT_DEAD, - BaseCaptured = world.event.S_EVENT_BASE_CAPTURED, - MissionStart = world.event.S_EVENT_MISSION_START, - MissionEnd = world.event.S_EVENT_MISSION_END, - TookControl = world.event.S_EVENT_TOOK_CONTROL, - RefuelingStop = world.event.S_EVENT_REFUELING_STOP, - Birth = world.event.S_EVENT_BIRTH, - HumanFailure = world.event.S_EVENT_HUMAN_FAILURE, - EngineStartup = world.event.S_EVENT_ENGINE_STARTUP, - EngineShutdown = world.event.S_EVENT_ENGINE_SHUTDOWN, - PlayerEnterUnit = world.event.S_EVENT_PLAYER_ENTER_UNIT, - PlayerLeaveUnit = world.event.S_EVENT_PLAYER_LEAVE_UNIT, - PlayerComment = world.event.S_EVENT_PLAYER_COMMENT, - ShootingStart = world.event.S_EVENT_SHOOTING_START, - ShootingEnd = world.event.S_EVENT_SHOOTING_END, -} - ---- The Event structure --- Note that at the beginning of each field description, there is an indication which field will be populated depending on the object type involved in the Event: --- --- * A (Object.Category.)UNIT : A UNIT object type is involved in the Event. --- * A (Object.Category.)STATIC : A STATIC object type is involved in the Event.µ --- --- @type EVENTDATA --- @field #number id The identifier of the event. --- --- @field Dcs.DCSUnit#Unit initiator (UNIT/STATIC/SCENERY) The initiating @{Dcs.DCSUnit#Unit} or @{Dcs.DCSStaticObject#StaticObject}. --- @field Dcs.DCSObject#Object.Category IniObjectCategory (UNIT/STATIC/SCENERY) The initiator object category ( Object.Category.UNIT or Object.Category.STATIC ). --- @field Dcs.DCSUnit#Unit IniDCSUnit (UNIT/STATIC) The initiating @{DCSUnit#Unit} or @{DCSStaticObject#StaticObject}. --- @field #string IniDCSUnitName (UNIT/STATIC) The initiating Unit name. --- @field Wrapper.Unit#UNIT IniUnit (UNIT/STATIC) The initiating MOOSE wrapper @{Unit#UNIT} of the initiator Unit object. --- @field #string IniUnitName (UNIT/STATIC) The initiating UNIT name (same as IniDCSUnitName). --- @field Dcs.DCSGroup#Group IniDCSGroup (UNIT) The initiating {DCSGroup#Group}. --- @field #string IniDCSGroupName (UNIT) The initiating Group name. --- @field Wrapper.Group#GROUP IniGroup (UNIT) The initiating MOOSE wrapper @{Group#GROUP} of the initiator Group object. --- @field #string IniGroupName UNIT) The initiating GROUP name (same as IniDCSGroupName). --- @field #string IniPlayerName (UNIT) The name of the initiating player in case the Unit is a client or player slot. --- @field Dcs.DCScoalition#coalition.side IniCoalition (UNIT) The coalition of the initiator. --- @field Dcs.DCSUnit#Unit.Category IniCategory (UNIT) The category of the initiator. --- @field #string IniTypeName (UNIT) The type name of the initiator. --- --- @field Dcs.DCSUnit#Unit target (UNIT/STATIC) The target @{Dcs.DCSUnit#Unit} or @{DCSStaticObject#StaticObject}. --- @field Dcs.DCSObject#Object.Category TgtObjectCategory (UNIT/STATIC) The target object category ( Object.Category.UNIT or Object.Category.STATIC ). --- @field Dcs.DCSUnit#Unit TgtDCSUnit (UNIT/STATIC) The target @{DCSUnit#Unit} or @{DCSStaticObject#StaticObject}. --- @field #string TgtDCSUnitName (UNIT/STATIC) The target Unit name. --- @field Wrapper.Unit#UNIT TgtUnit (UNIT/STATIC) The target MOOSE wrapper @{Unit#UNIT} of the target Unit object. --- @field #string TgtUnitName (UNIT/STATIC) The target UNIT name (same as TgtDCSUnitName). --- @field Dcs.DCSGroup#Group TgtDCSGroup (UNIT) The target {DCSGroup#Group}. --- @field #string TgtDCSGroupName (UNIT) The target Group name. --- @field Wrapper.Group#GROUP TgtGroup (UNIT) The target MOOSE wrapper @{Group#GROUP} of the target Group object. --- @field #string TgtGroupName (UNIT) The target GROUP name (same as TgtDCSGroupName). --- @field #string TgtPlayerName (UNIT) The name of the target player in case the Unit is a client or player slot. --- @field Dcs.DCScoalition#coalition.side TgtCoalition (UNIT) The coalition of the target. --- @field Dcs.DCSUnit#Unit.Category TgtCategory (UNIT) The category of the target. --- @field #string TgtTypeName (UNIT) The type name of the target. --- --- @field weapon The weapon used during the event. --- @field Weapon --- @field WeaponName --- @field WeaponTgtDCSUnit - - -local _EVENTMETA = { - [world.event.S_EVENT_SHOT] = { - Order = 1, - Event = "OnEventShot", - Text = "S_EVENT_SHOT" - }, - [world.event.S_EVENT_HIT] = { - Order = 1, - Event = "OnEventHit", - Text = "S_EVENT_HIT" - }, - [world.event.S_EVENT_TAKEOFF] = { - Order = 1, - Event = "OnEventTakeOff", - Text = "S_EVENT_TAKEOFF" - }, - [world.event.S_EVENT_LAND] = { - Order = 1, - Event = "OnEventLand", - Text = "S_EVENT_LAND" - }, - [world.event.S_EVENT_CRASH] = { - Order = -1, - Event = "OnEventCrash", - Text = "S_EVENT_CRASH" - }, - [world.event.S_EVENT_EJECTION] = { - Order = 1, - Event = "OnEventEjection", - Text = "S_EVENT_EJECTION" - }, - [world.event.S_EVENT_REFUELING] = { - Order = 1, - Event = "OnEventRefueling", - Text = "S_EVENT_REFUELING" - }, - [world.event.S_EVENT_DEAD] = { - Order = -1, - Event = "OnEventDead", - Text = "S_EVENT_DEAD" - }, - [world.event.S_EVENT_PILOT_DEAD] = { - Order = 1, - Event = "OnEventPilotDead", - Text = "S_EVENT_PILOT_DEAD" - }, - [world.event.S_EVENT_BASE_CAPTURED] = { - Order = 1, - Event = "OnEventBaseCaptured", - Text = "S_EVENT_BASE_CAPTURED" - }, - [world.event.S_EVENT_MISSION_START] = { - Order = 1, - Event = "OnEventMissionStart", - Text = "S_EVENT_MISSION_START" - }, - [world.event.S_EVENT_MISSION_END] = { - Order = 1, - Event = "OnEventMissionEnd", - Text = "S_EVENT_MISSION_END" - }, - [world.event.S_EVENT_TOOK_CONTROL] = { - Order = 1, - Event = "OnEventTookControl", - Text = "S_EVENT_TOOK_CONTROL" - }, - [world.event.S_EVENT_REFUELING_STOP] = { - Order = 1, - Event = "OnEventRefuelingStop", - Text = "S_EVENT_REFUELING_STOP" - }, - [world.event.S_EVENT_BIRTH] = { - Order = 1, - Event = "OnEventBirth", - Text = "S_EVENT_BIRTH" - }, - [world.event.S_EVENT_HUMAN_FAILURE] = { - Order = 1, - Event = "OnEventHumanFailure", - Text = "S_EVENT_HUMAN_FAILURE" - }, - [world.event.S_EVENT_ENGINE_STARTUP] = { - Order = 1, - Event = "OnEventEngineStartup", - Text = "S_EVENT_ENGINE_STARTUP" - }, - [world.event.S_EVENT_ENGINE_SHUTDOWN] = { - Order = 1, - Event = "OnEventEngineShutdown", - Text = "S_EVENT_ENGINE_SHUTDOWN" - }, - [world.event.S_EVENT_PLAYER_ENTER_UNIT] = { - Order = 1, - Event = "OnEventPlayerEnterUnit", - Text = "S_EVENT_PLAYER_ENTER_UNIT" - }, - [world.event.S_EVENT_PLAYER_LEAVE_UNIT] = { - Order = -1, - Event = "OnEventPlayerLeaveUnit", - Text = "S_EVENT_PLAYER_LEAVE_UNIT" - }, - [world.event.S_EVENT_PLAYER_COMMENT] = { - Order = 1, - Event = "OnEventPlayerComment", - Text = "S_EVENT_PLAYER_COMMENT" - }, - [world.event.S_EVENT_SHOOTING_START] = { - Order = 1, - Event = "OnEventShootingStart", - Text = "S_EVENT_SHOOTING_START" - }, - [world.event.S_EVENT_SHOOTING_END] = { - Order = 1, - Event = "OnEventShootingEnd", - Text = "S_EVENT_SHOOTING_END" - }, -} - - ---- 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 = _EVENTMETA[EventID].Text - - return EventText -end - - ---- Initializes the Events structure for the event --- @param #EVENT self --- @param Dcs.DCSWorld#world.event EventID --- @param Core.Base#BASE EventClass --- @return #EVENT.Events -function EVENT:Init( EventID, EventClass ) - self:F3( { _EVENTMETA[EventID].Text, EventClass } ) - - if not self.Events[EventID] then - -- Create a WEAK table to ensure that the garbage collector is cleaning the event links when the object usage is cleaned. - self.Events[EventID] = setmetatable( {}, { __mode = "k" } ) - end - - -- Each event has a subtable of EventClasses, ordered by EventPriority. - local EventPriority = EventClass:GetEventPriority() - if not self.Events[EventID][EventPriority] then - self.Events[EventID][EventPriority] = {} - end - - if not self.Events[EventID][EventPriority][EventClass] then - self.Events[EventID][EventPriority][EventClass] = setmetatable( {}, { __mode = "k" } ) - end - return self.Events[EventID][EventPriority][EventClass] -end - ---- Removes an Events entry --- @param #EVENT self --- @param Core.Base#BASE EventClass The self instance of the class for which the event is. --- @param Dcs.DCSWorld#world.event EventID --- @return #EVENT.Events -function EVENT:Remove( EventClass, EventID ) - self:F3( { EventClass, _EVENTMETA[EventID].Text } ) - - local EventClass = EventClass - local EventPriority = EventClass:GetEventPriority() - self.Events[EventID][EventPriority][EventClass] = nil -end - ---- Removes an Events entry for a Unit --- @param #EVENT self --- @param Core.Base#BASE EventClass The self instance of the class for which the event is. --- @param Dcs.DCSWorld#world.event EventID --- @return #EVENT.Events -function EVENT:RemoveForUnit( UnitName, EventClass, EventID ) - self:F3( { EventClass, _EVENTMETA[EventID].Text } ) - - local EventClass = EventClass - local EventPriority = EventClass:GetEventPriority() - local Event = self.Events[EventID][EventPriority][EventClass] - Event.IniUnit[UnitName] = nil -end - ---- Clears all event subscriptions for a @{Base#BASE} derived object. --- @param #EVENT self --- @param Core.Base#BASE EventObject -function EVENT:RemoveAll( EventObject ) - self:F3( { EventObject:GetClassNameAndID() } ) - - local EventClass = EventObject:GetClassNameAndID() - local EventPriority = EventClass:GetEventPriority() - for EventID, EventData in pairs( self.Events ) do - self.Events[EventID][EventPriority][EventClass] = nil - end -end - - - ---- Create an OnDead event handler for a group --- @param #EVENT self --- @param #table EventTemplate --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param EventClass The instance of the class for which the event is. --- @param #function OnEventFunction --- @return #EVENT -function EVENT:OnEventForTemplate( EventTemplate, EventFunction, EventClass, OnEventFunction ) - self:F2( EventTemplate.name ) - - for EventUnitID, EventUnit in pairs( EventTemplate.units ) do - OnEventFunction( self, EventUnit.name, EventFunction, EventClass ) - end - return self -end - ---- Set a new listener for an S_EVENT_X event independent from a unit or a weapon. --- @param #EVENT self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Core.Base#BASE EventClass The self instance of the class for which the event is captured. When the event happens, the event process will be called in this class provided. --- @param EventID --- @return #EVENT -function EVENT:OnEventGeneric( EventFunction, EventClass, EventID ) - self:F2( { EventID } ) - - local Event = self:Init( EventID, EventClass ) - Event.EventFunction = EventFunction - Event.EventClass = EventClass - - 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 Core.Base#BASE EventClass The self instance of the class for which the event is. --- @param EventID --- @return #EVENT -function EVENT:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, EventID ) - self:F2( EventDCSUnitName ) - - local Event = self:Init( EventID, EventClass ) - if not Event.IniUnit then - Event.IniUnit = {} - end - Event.IniUnit[EventDCSUnitName] = {} - Event.IniUnit[EventDCSUnitName].EventFunction = EventFunction - Event.IniUnit[EventDCSUnitName].EventClass = EventClass - return self -end - -do -- OnBirth - - --- Create an OnBirth event handler for a group - -- @param #EVENT self - -- @param Wrapper.Group#GROUP EventGroup - -- @param #function EventFunction The function to be called when the event occurs for the unit. - -- @param EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnBirthForTemplate( EventTemplate, EventFunction, EventClass ) - self:F2( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, 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 EventClass - -- @return #EVENT - function EVENT:OnBirth( EventFunction, EventClass ) - self:F2() - - self:OnEventGeneric( EventFunction, EventClass, 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 EventClass - -- @return #EVENT - function EVENT:OnBirthForUnit( EventDCSUnitName, EventFunction, EventClass ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_BIRTH ) - - return self - end - - --- Stop listening to S_EVENT_BIRTH event. - -- @param #EVENT self - -- @param Base#BASE EventClass - -- @return #EVENT - function EVENT:OnBirthRemove( EventClass ) - self:F2() - - self:Remove( EventClass, world.event.S_EVENT_BIRTH ) - - return self - end - - -end - -do -- OnCrash - - --- Create an OnCrash event handler for a group - -- @param #EVENT self - -- @param Wrapper.Group#GROUP EventGroup - -- @param #function EventFunction The function to be called when the event occurs for the unit. - -- @param EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnCrashForTemplate( EventTemplate, EventFunction, EventClass ) - self:F2( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, 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 EventClass - -- @return #EVENT - function EVENT:OnCrash( EventFunction, EventClass ) - self:F2() - - self:OnEventGeneric( EventFunction, EventClass, 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 EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnCrashForUnit( EventDCSUnitName, EventFunction, EventClass ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_CRASH ) - - return self - end - - --- Stop listening to S_EVENT_CRASH event. - -- @param #EVENT self - -- @param Base#BASE EventClass - -- @return #EVENT - function EVENT:OnCrashRemove( EventClass ) - self:F2() - - self:Remove( EventClass, world.event.S_EVENT_CRASH ) - - return self - end - -end - -do -- OnDead - - --- Create an OnDead event handler for a group - -- @param #EVENT self - -- @param Wrapper.Group#GROUP EventGroup - -- @param #function EventFunction The function to be called when the event occurs for the unit. - -- @param EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnDeadForTemplate( EventTemplate, EventFunction, EventClass ) - self:F2( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, 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 EventClass - -- @return #EVENT - function EVENT:OnDead( EventFunction, EventClass ) - self:F2() - - self:OnEventGeneric( EventFunction, EventClass, 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 EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnDeadForUnit( EventDCSUnitName, EventFunction, EventClass ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_DEAD ) - - return self - end - - --- Stop listening to S_EVENT_DEAD event. - -- @param #EVENT self - -- @param Base#BASE EventClass - -- @return #EVENT - function EVENT:OnDeadRemove( EventClass ) - self:F2() - - self:Remove( EventClass, world.event.S_EVENT_DEAD ) - - return self - end - - -end - -do -- OnPilotDead - - --- Set a new listener for an S_EVENT_PILOT_DEAD event. - -- @param #EVENT self - -- @param #function EventFunction The function to be called when the event occurs for the unit. - -- @param Base#BASE EventClass - -- @return #EVENT - function EVENT:OnPilotDead( EventFunction, EventClass ) - self:F2() - - self:OnEventGeneric( EventFunction, EventClass, world.event.S_EVENT_PILOT_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 EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnPilotDeadForUnit( EventDCSUnitName, EventFunction, EventClass ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_PILOT_DEAD ) - - return self - end - - --- Stop listening to S_EVENT_PILOT_DEAD event. - -- @param #EVENT self - -- @param Base#BASE EventClass - -- @return #EVENT - function EVENT:OnPilotDeadRemove( EventClass ) - self:F2() - - self:Remove( EventClass, world.event.S_EVENT_PILOT_DEAD ) - - return self - end - -end - -do -- OnLand - --- Create an OnLand event handler for a group - -- @param #EVENT self - -- @param #table EventTemplate - -- @param #function EventFunction The function to be called when the event occurs for the unit. - -- @param EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnLandForTemplate( EventTemplate, EventFunction, EventClass ) - self:F2( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, 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 EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnLandForUnit( EventDCSUnitName, EventFunction, EventClass ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_LAND ) - - return self - end - - --- Stop listening to S_EVENT_LAND event. - -- @param #EVENT self - -- @param Base#BASE EventClass - -- @return #EVENT - function EVENT:OnLandRemove( EventClass ) - self:F2() - - self:Remove( EventClass, world.event.S_EVENT_LAND ) - - return self - end - - -end - -do -- OnTakeOff - --- Create an OnTakeOff event handler for a group - -- @param #EVENT self - -- @param #table EventTemplate - -- @param #function EventFunction The function to be called when the event occurs for the unit. - -- @param EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnTakeOffForTemplate( EventTemplate, EventFunction, EventClass ) - self:F2( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, 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 EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnTakeOffForUnit( EventDCSUnitName, EventFunction, EventClass ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_TAKEOFF ) - - return self - end - - --- Stop listening to S_EVENT_TAKEOFF event. - -- @param #EVENT self - -- @param Base#BASE EventClass - -- @return #EVENT - function EVENT:OnTakeOffRemove( EventClass ) - self:F2() - - self:Remove( EventClass, world.event.S_EVENT_TAKEOFF ) - - return self - end - - -end - -do -- OnEngineShutDown - - --- Create an OnDead event handler for a group - -- @param #EVENT self - -- @param #table EventTemplate - -- @param #function EventFunction The function to be called when the event occurs for the unit. - -- @param EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnEngineShutDownForTemplate( EventTemplate, EventFunction, EventClass ) - self:F2( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, 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 EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnEngineShutDownForUnit( EventDCSUnitName, EventFunction, EventClass ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_ENGINE_SHUTDOWN ) - - return self - end - - --- Stop listening to S_EVENT_ENGINE_SHUTDOWN event. - -- @param #EVENT self - -- @param Base#BASE EventClass - -- @return #EVENT - function EVENT:OnEngineShutDownRemove( EventClass ) - self:F2() - - self:Remove( EventClass, world.event.S_EVENT_ENGINE_SHUTDOWN ) - - return self - end - -end - -do -- OnEngineStartUp - - --- 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 EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnEngineStartUpForUnit( EventDCSUnitName, EventFunction, EventClass ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_ENGINE_STARTUP ) - - return self - end - - --- Stop listening to S_EVENT_ENGINE_STARTUP event. - -- @param #EVENT self - -- @param Base#BASE EventClass - -- @return #EVENT - function EVENT:OnEngineStartUpRemove( EventClass ) - self:F2() - - self:Remove( EventClass, world.event.S_EVENT_ENGINE_STARTUP ) - - return self - end - -end - -do -- OnShot - --- 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 EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnShot( EventFunction, EventClass ) - self:F2() - - self:OnEventGeneric( EventFunction, EventClass, 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 EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnShotForUnit( EventDCSUnitName, EventFunction, EventClass ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_SHOT ) - - return self - end - - --- Stop listening to S_EVENT_SHOT event. - -- @param #EVENT self - -- @param Base#BASE EventClass - -- @return #EVENT - function EVENT:OnShotRemove( EventClass ) - self:F2() - - self:Remove( EventClass, world.event.S_EVENT_SHOT ) - - return self - end - - -end - -do -- OnHit - - --- 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 EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnHit( EventFunction, EventClass ) - self:F2() - - self:OnEventGeneric( EventFunction, EventClass, 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 EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnHitForUnit( EventDCSUnitName, EventFunction, EventClass ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_HIT ) - - return self - end - - --- Stop listening to S_EVENT_HIT event. - -- @param #EVENT self - -- @param Base#BASE EventClass - -- @return #EVENT - function EVENT:OnHitRemove( EventClass ) - self:F2() - - self:Remove( EventClass, world.event.S_EVENT_HIT ) - - return self - end - -end - -do -- OnPlayerEnterUnit - - --- 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 EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnPlayerEnterUnit( EventFunction, EventClass ) - self:F2() - - self:OnEventGeneric( EventFunction, EventClass, world.event.S_EVENT_PLAYER_ENTER_UNIT ) - - return self - end - - --- Stop listening to S_EVENT_PLAYER_ENTER_UNIT event. - -- @param #EVENT self - -- @param Base#BASE EventClass - -- @return #EVENT - function EVENT:OnPlayerEnterRemove( EventClass ) - self:F2() - - self:Remove( EventClass, world.event.S_EVENT_PLAYER_ENTER_UNIT ) - - return self - end - -end - -do -- OnPlayerLeaveUnit - --- 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 EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnPlayerLeaveUnit( EventFunction, EventClass ) - self:F2() - - self:OnEventGeneric( EventFunction, EventClass, world.event.S_EVENT_PLAYER_LEAVE_UNIT ) - - return self - end - - --- Stop listening to S_EVENT_PLAYER_LEAVE_UNIT event. - -- @param #EVENT self - -- @param Base#BASE EventClass - -- @return #EVENT - function EVENT:OnPlayerLeaveRemove( EventClass ) - self:F2() - - self:Remove( EventClass, world.event.S_EVENT_PLAYER_LEAVE_UNIT ) - - return self - end - -end - - - ---- @param #EVENT self --- @param #EVENTDATA Event -function EVENT:onEvent( Event ) - - local ErrorHandler = function( errmsg ) - - env.info( "Error in SCHEDULER function:" .. errmsg ) - if debug ~= nil then - env.info( debug.traceback() ) - end - - return errmsg - end - - if self and self.Events and self.Events[Event.id] then - - - if Event.initiator then - - Event.IniObjectCategory = Event.initiator:getCategory() - - if Event.IniObjectCategory == Object.Category.UNIT then - Event.IniDCSUnit = Event.initiator - Event.IniDCSUnitName = Event.IniDCSUnit:getName() - Event.IniUnitName = Event.IniDCSUnitName - Event.IniDCSGroup = Event.IniDCSUnit:getGroup() - Event.IniUnit = UNIT:FindByName( Event.IniDCSUnitName ) - if not Event.IniUnit then - -- Unit can be a CLIENT. Most likely this will be the case ... - Event.IniUnit = CLIENT:FindByName( Event.IniDCSUnitName, '', true ) - end - Event.IniDCSGroupName = "" - if Event.IniDCSGroup and Event.IniDCSGroup:isExist() then - Event.IniDCSGroupName = Event.IniDCSGroup:getName() - Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) - self:E( { IniGroup = Event.IniGroup } ) - end - Event.IniPlayerName = Event.IniDCSUnit:getPlayerName() - Event.IniCoalition = Event.IniDCSUnit:getCoalition() - Event.IniTypeName = Event.IniDCSUnit:getTypeName() - Event.IniCategory = Event.IniDCSUnit:getDesc().category - end - - if Event.IniObjectCategory == Object.Category.STATIC then - Event.IniDCSUnit = Event.initiator - Event.IniDCSUnitName = Event.IniDCSUnit:getName() - Event.IniUnitName = Event.IniDCSUnitName - Event.IniUnit = STATIC:FindByName( Event.IniDCSUnitName, false ) - Event.IniCoalition = Event.IniDCSUnit:getCoalition() - Event.IniCategory = Event.IniDCSUnit:getDesc().category - Event.IniTypeName = Event.IniDCSUnit:getTypeName() - end - - if Event.IniObjectCategory == Object.Category.SCENERY then - Event.IniDCSUnit = Event.initiator - Event.IniDCSUnitName = Event.IniDCSUnit:getName() - Event.IniUnitName = Event.IniDCSUnitName - Event.IniUnit = SCENERY:Register( Event.IniDCSUnitName, Event.initiator ) - Event.IniCategory = Event.IniDCSUnit:getDesc().category - Event.IniTypeName = Event.IniDCSUnit:getTypeName() - end - end - - if Event.target then - - Event.TgtObjectCategory = Event.target:getCategory() - - if Event.TgtObjectCategory == Object.Category.UNIT then - Event.TgtDCSUnit = Event.target - Event.TgtDCSGroup = Event.TgtDCSUnit:getGroup() - Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() - Event.TgtUnitName = Event.TgtDCSUnitName - Event.TgtUnit = UNIT:FindByName( Event.TgtDCSUnitName ) - Event.TgtDCSGroupName = "" - if Event.TgtDCSGroup and Event.TgtDCSGroup:isExist() then - Event.TgtDCSGroupName = Event.TgtDCSGroup:getName() - end - Event.TgtPlayerName = Event.TgtDCSUnit:getPlayerName() - Event.TgtCoalition = Event.TgtDCSUnit:getCoalition() - Event.TgtCategory = Event.TgtDCSUnit:getDesc().category - Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() - end - - if Event.TgtObjectCategory == Object.Category.STATIC then - Event.TgtDCSUnit = Event.target - Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() - Event.TgtUnitName = Event.TgtDCSUnitName - Event.TgtUnit = STATIC:FindByName( Event.TgtDCSUnitName ) - Event.TgtCoalition = Event.TgtDCSUnit:getCoalition() - Event.TgtCategory = Event.TgtDCSUnit:getDesc().category - Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() - end - - if Event.TgtObjectCategory == Object.Category.SCENERY then - Event.TgtDCSUnit = Event.target - Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() - Event.TgtUnitName = Event.TgtDCSUnitName - Event.TgtUnit = SCENERY:Register( Event.TgtDCSUnitName, Event.target ) - Event.TgtCategory = Event.TgtDCSUnit:getDesc().category - Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() - end - end - - if Event.weapon then - Event.Weapon = Event.weapon - Event.WeaponName = Event.Weapon:getTypeName() - --Event.WeaponTgtDCSUnit = Event.Weapon:getTarget() - end - - local PriorityOrder = _EVENTMETA[Event.id].Order - local PriorityBegin = PriorityOrder == -1 and 5 or 1 - local PriorityEnd = PriorityOrder == -1 and 1 or 5 - - self:E( { _EVENTMETA[Event.id].Text, Event, Event.IniDCSUnitName, Event.TgtDCSUnitName, PriorityOrder } ) - - for EventPriority = PriorityBegin, PriorityEnd, PriorityOrder do - - if self.Events[Event.id][EventPriority] then - - -- Okay, we got the event from DCS. Now loop the SORTED self.EventSorted[] table for the received Event.id, and for each EventData registered, check if a function needs to be called. - for EventClass, EventData in pairs( self.Events[Event.id][EventPriority] ) do - - -- If the EventData is for a UNIT, the call directly the EventClass EventFunction for that UNIT. - if Event.IniDCSUnitName and EventData.IniUnit and EventData.IniUnit[Event.IniDCSUnitName] then - - -- First test if a EventFunction is Set, otherwise search for the default function - if EventData.IniUnit[Event.IniDCSUnitName].EventFunction then - - self:E( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName, EventPriority } ) - Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) - - local Result, Value = xpcall( - function() - return EventData.IniUnit[Event.IniDCSUnitName].EventFunction( EventClass, Event ) - end, ErrorHandler ) - - else - - -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. - local EventFunction = EventClass[ _EVENTMETA[Event.id].Event ] - if EventFunction and type( EventFunction ) == "function" then - - -- Now call the default event function. - self:E( { "Calling " .. _EVENTMETA[Event.id].Event .. " for Class ", EventClass:GetClassNameAndID(), EventPriority } ) - Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) - - local Result, Value = xpcall( - function() - return EventFunction( EventClass, Event ) - end, ErrorHandler ) - end - - end - - else - - -- If the EventData is not bound to a specific unit, then call the EventClass EventFunction. - -- Note that here the EventFunction will need to implement and determine the logic for the relevant source- or target unit, or weapon. - if Event.IniDCSUnit and not EventData.IniUnit then - - if EventClass == EventData.EventClass then - - -- First test if a EventFunction is Set, otherwise search for the default function - if EventData.EventFunction then - - -- There is an EventFunction defined, so call the EventFunction. - self:E( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID(), EventPriority } ) - Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) - - local Result, Value = xpcall( - function() - return EventData.EventFunction( EventClass, Event ) - end, ErrorHandler ) - else - - -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. - local EventFunction = EventClass[ _EVENTMETA[Event.id].Event ] - if EventFunction and type( EventFunction ) == "function" then - - -- Now call the default event function. - self:E( { "Calling " .. _EVENTMETA[Event.id].Event .. " for Class ", EventClass:GetClassNameAndID(), EventPriority } ) - Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) - - local Result, Value = xpcall( - function() - return EventFunction( EventClass, Event ) - end, ErrorHandler ) - end - end - end - end - end - end - end - end - else - self:E( { _EVENTMETA[Event.id].Text, Event } ) - end -end - ---- The EVENTHANDLER structure --- @type EVENTHANDLER --- @extends Core.Base#BASE -EVENTHANDLER = { - ClassName = "EVENTHANDLER", - ClassID = 0, -} - ---- The EVENTHANDLER constructor --- @param #EVENTHANDLER self --- @return #EVENTHANDLER -function EVENTHANDLER:New() - self = BASE:Inherit( self, BASE:New() ) -- #EVENTHANDLER - return self -end ---- This module contains the MENU classes. --- --- === --- --- DCS Menus can be managed using the MENU classes. --- The advantage of using MENU classes is that it hides the complexity of dealing with menu management in more advanced scanerios where you need to --- set menus and later remove them, and later set them again. You'll find while using use normal DCS scripting functions, that setting and removing --- menus is not a easy feat if you have complex menu hierarchies defined. --- Using the MOOSE menu classes, the removal and refreshing of menus are nicely being handled within these classes, and becomes much more easy. --- On top, MOOSE implements **variable parameter** passing for command menus. --- --- There are basically two different MENU class types that you need to use: --- --- ### To manage **main menus**, the classes begin with **MENU_**: --- --- * @{Menu#MENU_MISSION}: Manages main menus for whole mission file. --- * @{Menu#MENU_COALITION}: Manages main menus for whole coalition. --- * @{Menu#MENU_GROUP}: Manages main menus for GROUPs. --- * @{Menu#MENU_CLIENT}: Manages main menus for CLIENTs. This manages menus for units with the skill level "Client". --- --- ### To manage **command menus**, which are menus that allow the player to issue **functions**, the classes begin with **MENU_COMMAND_**: --- --- * @{Menu#MENU_MISSION_COMMAND}: Manages command menus for whole mission file. --- * @{Menu#MENU_COALITION_COMMAND}: Manages command menus for whole coalition. --- * @{Menu#MENU_GROUP_COMMAND}: Manages command menus for GROUPs. --- * @{Menu#MENU_CLIENT_COMMAND}: Manages command menus for CLIENTs. This manages menus for units with the skill level "Client". --- --- === --- --- The above menus classes **are derived** from 2 main **abstract** classes defined within the MOOSE framework (so don't use these): --- --- 1) MENU_ BASE abstract base classes (don't use them) --- ==================================================== --- The underlying base menu classes are **NOT** to be used within your missions. --- These are simply abstract base classes defining a couple of fields that are used by the --- derived MENU_ classes to manage menus. --- --- 1.1) @{#MENU_BASE} class, extends @{Base#BASE} --- -------------------------------------------------- --- The @{#MENU_BASE} class defines the main MENU class where other MENU classes are derived from. --- --- 1.2) @{#MENU_COMMAND_BASE} class, extends @{Base#BASE} --- ---------------------------------------------------------- --- The @{#MENU_COMMAND_BASE} class defines the main MENU class where other MENU COMMAND_ classes are derived from, in order to set commands. --- --- === --- --- **The next menus define the MENU classes that you can use within your missions.** --- --- 2) MENU MISSION classes --- ====================== --- The underlying classes manage the menus for a complete mission file. --- --- 2.1) @{#MENU_MISSION} class, extends @{Menu#MENU_BASE} --- --------------------------------------------------------- --- The @{Menu#MENU_MISSION} class manages the main menus for a complete mission. --- You can add menus with the @{#MENU_MISSION.New} method, which constructs a MENU_MISSION object and returns you the object reference. --- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_MISSION.Remove}. --- --- 2.2) @{#MENU_MISSION_COMMAND} class, extends @{Menu#MENU_COMMAND_BASE} --- ------------------------------------------------------------------------- --- The @{Menu#MENU_MISSION_COMMAND} class manages the command menus for a complete mission, which allow players to execute functions during mission execution. --- You can add menus with the @{#MENU_MISSION_COMMAND.New} method, which constructs a MENU_MISSION_COMMAND object and returns you the object reference. --- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_MISSION_COMMAND.Remove}. --- --- === --- --- 3) MENU COALITION classes --- ========================= --- The underlying classes manage the menus for whole coalitions. --- --- 3.1) @{#MENU_COALITION} class, extends @{Menu#MENU_BASE} --- ------------------------------------------------------------ --- The @{Menu#MENU_COALITION} class manages the main menus for coalitions. --- You can add menus with the @{#MENU_COALITION.New} method, which constructs a MENU_COALITION object and returns you the object reference. --- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_COALITION.Remove}. --- --- 3.2) @{Menu#MENU_COALITION_COMMAND} class, extends @{Menu#MENU_COMMAND_BASE} --- ---------------------------------------------------------------------------- --- The @{Menu#MENU_COALITION_COMMAND} class manages the command menus for coalitions, which allow players to execute functions during mission execution. --- You can add menus with the @{#MENU_COALITION_COMMAND.New} method, which constructs a MENU_COALITION_COMMAND object and returns you the object reference. --- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_COALITION_COMMAND.Remove}. --- --- === --- --- 4) MENU GROUP classes --- ===================== --- The underlying classes manage the menus for groups. Note that groups can be inactive, alive or can be destroyed. --- --- 4.1) @{Menu#MENU_GROUP} class, extends @{Menu#MENU_BASE} --- -------------------------------------------------------- --- The @{Menu#MENU_GROUP} class manages the main menus for coalitions. --- You can add menus with the @{#MENU_GROUP.New} method, which constructs a MENU_GROUP object and returns you the object reference. --- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_GROUP.Remove}. --- --- 4.2) @{Menu#MENU_GROUP_COMMAND} class, extends @{Menu#MENU_COMMAND_BASE} --- ------------------------------------------------------------------------ --- The @{Menu#MENU_GROUP_COMMAND} class manages the command menus for coalitions, which allow players to execute functions during mission execution. --- You can add menus with the @{#MENU_GROUP_COMMAND.New} method, which constructs a MENU_GROUP_COMMAND object and returns you the object reference. --- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_GROUP_COMMAND.Remove}. --- --- === --- --- 5) MENU CLIENT classes --- ====================== --- The underlying classes manage the menus for units with skill level client or player. --- --- 5.1) @{Menu#MENU_CLIENT} class, extends @{Menu#MENU_BASE} --- --------------------------------------------------------- --- The @{Menu#MENU_CLIENT} class manages the main menus for coalitions. --- You can add menus with the @{#MENU_CLIENT.New} method, which constructs a MENU_CLIENT object and returns you the object reference. --- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_CLIENT.Remove}. --- --- 5.2) @{Menu#MENU_CLIENT_COMMAND} class, extends @{Menu#MENU_COMMAND_BASE} --- ------------------------------------------------------------------------- --- The @{Menu#MENU_CLIENT_COMMAND} class manages the command menus for coalitions, which allow players to execute functions during mission execution. --- You can add menus with the @{#MENU_CLIENT_COMMAND.New} method, which constructs a MENU_CLIENT_COMMAND object and returns you the object reference. --- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_CLIENT_COMMAND.Remove}. --- --- === --- --- ### Contributions: - --- ### Authors: FlightControl : Design & Programming --- --- @module Menu - - -do -- MENU_BASE - - --- The MENU_BASE class - -- @type MENU_BASE - -- @extends Base#BASE - MENU_BASE = { - ClassName = "MENU_BASE", - MenuPath = nil, - MenuText = "", - MenuParentPath = nil - } - - --- Consructor - function MENU_BASE:New( MenuText, ParentMenu ) - - local MenuParentPath = {} - if ParentMenu ~= nil then - MenuParentPath = ParentMenu.MenuPath - end - - local self = BASE:Inherit( self, BASE:New() ) - - self.MenuPath = nil - self.MenuText = MenuText - self.MenuParentPath = MenuParentPath - - return self - end - -end - -do -- MENU_COMMAND_BASE - - --- The MENU_COMMAND_BASE class - -- @type MENU_COMMAND_BASE - -- @field #function MenuCallHandler - -- @extends Menu#MENU_BASE - MENU_COMMAND_BASE = { - ClassName = "MENU_COMMAND_BASE", - CommandMenuFunction = nil, - CommandMenuArgument = nil, - MenuCallHandler = nil, - } - - --- Constructor - function MENU_COMMAND_BASE:New( MenuText, ParentMenu, CommandMenuFunction, CommandMenuArguments ) - - local self = BASE:Inherit( self, MENU_BASE:New( MenuText, ParentMenu ) ) - - self.CommandMenuFunction = CommandMenuFunction - self.MenuCallHandler = function( CommandMenuArguments ) - self.CommandMenuFunction( unpack( CommandMenuArguments ) ) - end - - return self - end - -end - - -do -- MENU_MISSION - - --- The MENU_MISSION class - -- @type MENU_MISSION - -- @extends Menu#MENU_BASE - MENU_MISSION = { - ClassName = "MENU_MISSION" - } - - --- MENU_MISSION constructor. Creates a new MENU_MISSION object and creates the menu for a complete mission file. - -- @param #MENU_MISSION self - -- @param #string MenuText The text for the menu. - -- @param #table ParentMenu The parent menu. This parameter can be ignored if you want the menu to be located at the perent menu of DCS world (under F10 other). - -- @return #MENU_MISSION self - function MENU_MISSION:New( MenuText, ParentMenu ) - - local self = BASE:Inherit( self, MENU_BASE:New( MenuText, ParentMenu ) ) - - self:F( { MenuText, ParentMenu } ) - - self.MenuText = MenuText - self.ParentMenu = ParentMenu - - self.Menus = {} - - self:T( { MenuText } ) - - self.MenuPath = missionCommands.addSubMenu( MenuText, self.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_MISSION. Note that the main menu is kept! - -- @param #MENU_MISSION self - -- @return #MENU_MISSION self - function MENU_MISSION:RemoveSubMenus() - self:F( self.MenuPath ) - - for MenuID, Menu in pairs( self.Menus ) do - Menu:Remove() - end - - end - - --- Removes the main menu and the sub menus recursively of this MENU_MISSION. - -- @param #MENU_MISSION self - -- @return #nil - function MENU_MISSION:Remove() - self:F( self.MenuPath ) - - self:RemoveSubMenus() - missionCommands.removeItem( self.MenuPath ) - if self.ParentMenu then - self.ParentMenu.Menus[self.MenuPath] = nil - end - - return nil - end - -end - -do -- MENU_MISSION_COMMAND - - --- The MENU_MISSION_COMMAND class - -- @type MENU_MISSION_COMMAND - -- @extends Menu#MENU_COMMAND_BASE - MENU_MISSION_COMMAND = { - ClassName = "MENU_MISSION_COMMAND" - } - - --- MENU_MISSION constructor. Creates a new radio command item for a complete mission file, which can invoke a function with parameters. - -- @param #MENU_MISSION_COMMAND self - -- @param #string MenuText The text for the menu. - -- @param Menu#MENU_MISSION ParentMenu The parent menu. - -- @param CommandMenuFunction A function that is called when the menu key is pressed. - -- @param CommandMenuArgument An argument for the function. There can only be ONE argument given. So multiple arguments must be wrapped into a table. See the below example how to do this. - -- @return #MENU_MISSION_COMMAND self - function MENU_MISSION_COMMAND:New( MenuText, ParentMenu, CommandMenuFunction, ... ) - - local self = BASE:Inherit( self, MENU_COMMAND_BASE:New( MenuText, ParentMenu, CommandMenuFunction, arg ) ) - - self.MenuText = MenuText - self.ParentMenu = ParentMenu - - self:T( { MenuText, CommandMenuFunction, arg } ) - - - self.MenuPath = missionCommands.addCommand( MenuText, self.MenuParentPath, self.MenuCallHandler, arg ) - - ParentMenu.Menus[self.MenuPath] = self - - return self - end - - --- Removes a radio command item for a coalition - -- @param #MENU_MISSION_COMMAND self - -- @return #nil - function MENU_MISSION_COMMAND:Remove() - self:F( self.MenuPath ) - - missionCommands.removeItem( self.MenuPath ) - if self.ParentMenu then - self.ParentMenu.Menus[self.MenuPath] = nil - end - return nil - end - -end - - - -do -- MENU_COALITION - - --- The MENU_COALITION class - -- @type MENU_COALITION - -- @extends Menu#MENU_BASE - -- @usage - -- -- This demo creates a menu structure for the planes within the red coalition. - -- -- To test, join the planes, then look at the other radio menus (Option F10). - -- -- Then switch planes and check if the menu is still there. - -- - -- local Plane1 = CLIENT:FindByName( "Plane 1" ) - -- local Plane2 = CLIENT:FindByName( "Plane 2" ) - -- - -- - -- -- This would create a menu for the red coalition under the main DCS "Others" menu. - -- local MenuCoalitionRed = MENU_COALITION:New( coalition.side.RED, "Manage Menus" ) - -- - -- - -- local function ShowStatus( StatusText, Coalition ) - -- - -- MESSAGE:New( Coalition, 15 ):ToRed() - -- Plane1:Message( StatusText, 15 ) - -- Plane2:Message( StatusText, 15 ) - -- end - -- - -- local MenuStatus -- Menu#MENU_COALITION - -- local MenuStatusShow -- Menu#MENU_COALITION_COMMAND - -- - -- local function RemoveStatusMenu() - -- MenuStatus:Remove() - -- end - -- - -- local function AddStatusMenu() - -- - -- -- This would create a menu for the red coalition under the MenuCoalitionRed menu object. - -- MenuStatus = MENU_COALITION:New( coalition.side.RED, "Status for Planes" ) - -- MenuStatusShow = MENU_COALITION_COMMAND:New( coalition.side.RED, "Show Status", MenuStatus, ShowStatus, "Status of planes is ok!", "Message to Red Coalition" ) - -- end - -- - -- local MenuAdd = MENU_COALITION_COMMAND:New( coalition.side.RED, "Add Status Menu", MenuCoalitionRed, AddStatusMenu ) - -- local MenuRemove = MENU_COALITION_COMMAND:New( coalition.side.RED, "Remove Status Menu", MenuCoalitionRed, RemoveStatusMenu ) - MENU_COALITION = { - ClassName = "MENU_COALITION" - } - - --- MENU_COALITION constructor. Creates a new MENU_COALITION object and creates the menu for a complete coalition. - -- @param #MENU_COALITION self - -- @param Dcs.DCSCoalition#coalition.side Coalition The coalition owning the menu. - -- @param #string MenuText The text for the menu. - -- @param #table ParentMenu The parent menu. This parameter can be ignored if you want the menu to be located at the perent menu of DCS world (under F10 other). - -- @return #MENU_COALITION self - function MENU_COALITION:New( Coalition, MenuText, ParentMenu ) - - local self = BASE:Inherit( self, MENU_BASE:New( MenuText, ParentMenu ) ) - - self:F( { Coalition, MenuText, ParentMenu } ) - - self.Coalition = Coalition - self.MenuText = MenuText - self.ParentMenu = ParentMenu - - self.Menus = {} - - self:T( { MenuText } ) - - self.MenuPath = missionCommands.addSubMenuForCoalition( Coalition, MenuText, self.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. Note that the main menu is kept! - -- @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 main menu and the sub menus recursively of this MENU_COALITION. - -- @param #MENU_COALITION self - -- @return #nil - function MENU_COALITION:Remove() - self:F( self.MenuPath ) - - self:RemoveSubMenus() - missionCommands.removeItemForCoalition( self.Coalition, self.MenuPath ) - if self.ParentMenu then - self.ParentMenu.Menus[self.MenuPath] = nil - end - - return nil - end - -end - -do -- MENU_COALITION_COMMAND - - --- The MENU_COALITION_COMMAND class - -- @type MENU_COALITION_COMMAND - -- @extends Menu#MENU_COMMAND_BASE - MENU_COALITION_COMMAND = { - ClassName = "MENU_COALITION_COMMAND" - } - - --- MENU_COALITION constructor. Creates a new radio command item for a coalition, which can invoke a function with parameters. - -- @param #MENU_COALITION_COMMAND self - -- @param Dcs.DCSCoalition#coalition.side Coalition The coalition owning the menu. - -- @param #string MenuText The text for the menu. - -- @param Menu#MENU_COALITION ParentMenu The parent menu. - -- @param CommandMenuFunction A function that is called when the menu key is pressed. - -- @param CommandMenuArgument An argument for the function. There can only be ONE argument given. So multiple arguments must be wrapped into a table. See the below example how to do this. - -- @return #MENU_COALITION_COMMAND self - function MENU_COALITION_COMMAND:New( Coalition, MenuText, ParentMenu, CommandMenuFunction, ... ) - - local self = BASE:Inherit( self, MENU_COMMAND_BASE:New( MenuText, ParentMenu, CommandMenuFunction, arg ) ) - - self.MenuCoalition = Coalition - self.MenuText = MenuText - self.ParentMenu = ParentMenu - - self:T( { MenuText, CommandMenuFunction, arg } ) - - - self.MenuPath = missionCommands.addCommandForCoalition( self.MenuCoalition, MenuText, self.MenuParentPath, self.MenuCallHandler, arg ) - - ParentMenu.Menus[self.MenuPath] = self - - return self - end - - --- Removes a radio command item for a coalition - -- @param #MENU_COALITION_COMMAND self - -- @return #nil - function MENU_COALITION_COMMAND:Remove() - self:F( self.MenuPath ) - - missionCommands.removeItemForCoalition( self.MenuCoalition, self.MenuPath ) - if self.ParentMenu then - self.ParentMenu.Menus[self.MenuPath] = nil - end - return nil - end - -end - -do -- MENU_CLIENT - - -- 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 = {} - - --- MENU_COALITION constructor. Creates a new radio command item for a coalition, which can invoke a function with parameters. - -- @type MENU_CLIENT - -- @extends Menu#MENU_BASE - -- @usage - -- -- This demo creates a menu structure for the two clients of planes. - -- -- Each client will receive a different menu structure. - -- -- To test, join the planes, then look at the other radio menus (Option F10). - -- -- Then switch planes and check if the menu is still there. - -- -- And play with the Add and Remove menu options. - -- - -- -- Note that in multi player, this will only work after the DCS clients bug is solved. - -- - -- local function ShowStatus( PlaneClient, StatusText, Coalition ) - -- - -- MESSAGE:New( Coalition, 15 ):ToRed() - -- PlaneClient:Message( StatusText, 15 ) - -- end - -- - -- local MenuStatus = {} - -- - -- local function RemoveStatusMenu( MenuClient ) - -- local MenuClientName = MenuClient:GetName() - -- MenuStatus[MenuClientName]:Remove() - -- end - -- - -- --- @param Wrapper.Client#CLIENT MenuClient - -- local function AddStatusMenu( MenuClient ) - -- local MenuClientName = MenuClient:GetName() - -- -- This would create a menu for the red coalition under the MenuCoalitionRed menu object. - -- MenuStatus[MenuClientName] = MENU_CLIENT:New( MenuClient, "Status for Planes" ) - -- MENU_CLIENT_COMMAND:New( MenuClient, "Show Status", MenuStatus[MenuClientName], ShowStatus, MenuClient, "Status of planes is ok!", "Message to Red Coalition" ) - -- end - -- - -- SCHEDULER:New( nil, - -- function() - -- local PlaneClient = CLIENT:FindByName( "Plane 1" ) - -- if PlaneClient and PlaneClient:IsAlive() then - -- local MenuManage = MENU_CLIENT:New( PlaneClient, "Manage Menus" ) - -- MENU_CLIENT_COMMAND:New( PlaneClient, "Add Status Menu Plane 1", MenuManage, AddStatusMenu, PlaneClient ) - -- MENU_CLIENT_COMMAND:New( PlaneClient, "Remove Status Menu Plane 1", MenuManage, RemoveStatusMenu, PlaneClient ) - -- end - -- end, {}, 10, 10 ) - -- - -- SCHEDULER:New( nil, - -- function() - -- local PlaneClient = CLIENT:FindByName( "Plane 2" ) - -- if PlaneClient and PlaneClient:IsAlive() then - -- local MenuManage = MENU_CLIENT:New( PlaneClient, "Manage Menus" ) - -- MENU_CLIENT_COMMAND:New( PlaneClient, "Add Status Menu Plane 2", MenuManage, AddStatusMenu, PlaneClient ) - -- MENU_CLIENT_COMMAND:New( PlaneClient, "Remove Status Menu Plane 2", MenuManage, RemoveStatusMenu, PlaneClient ) - -- end - -- end, {}, 10, 10 ) - MENU_CLIENT = { - ClassName = "MENU_CLIENT" - } - - --- MENU_CLIENT constructor. Creates a new radio menu item for a client. - -- @param #MENU_CLIENT self - -- @param Wrapper.Client#CLIENT Client 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( Client, MenuText, ParentMenu ) - - -- Arrange meta tables - local MenuParentPath = {} - if ParentMenu ~= nil then - MenuParentPath = ParentMenu.MenuPath - end - - local self = BASE:Inherit( self, MENU_BASE:New( MenuText, MenuParentPath ) ) - self:F( { Client, MenuText, ParentMenu } ) - - self.MenuClient = Client - self.MenuClientGroupID = Client: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( { Client: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( { Client: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 #nil - 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_COMMAND - MENU_CLIENT_COMMAND = { - ClassName = "MENU_CLIENT_COMMAND" - } - - --- MENU_CLIENT_COMMAND constructor. Creates a new radio command item for a client, which can invoke a function with parameters. - -- @param #MENU_CLIENT_COMMAND self - -- @param Wrapper.Client#CLIENT Client The Client owning the menu. - -- @param #string MenuText The text for the menu. - -- @param #MENU_BASE ParentMenu The parent menu. - -- @param CommandMenuFunction A function that is called when the menu key is pressed. - -- @return Menu#MENU_CLIENT_COMMAND self - function MENU_CLIENT_COMMAND:New( Client, MenuText, ParentMenu, CommandMenuFunction, ... ) - - -- Arrange meta tables - - local MenuParentPath = {} - if ParentMenu ~= nil then - MenuParentPath = ParentMenu.MenuPath - end - - local self = BASE:Inherit( self, MENU_COMMAND_BASE:New( MenuText, MenuParentPath, CommandMenuFunction, arg ) ) -- Menu#MENU_CLIENT_COMMAND - - self.MenuClient = Client - self.MenuClientGroupID = Client: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( { Client:GetClientGroupName(), MenuPath[table.concat(MenuParentPath)], MenuParentPath, MenuText, CommandMenuFunction, arg } ) - - 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, self.MenuCallHandler, arg ) - MenuPath[MenuPathID] = self.MenuPath - - if ParentMenu and ParentMenu.Menus then - ParentMenu.Menus[self.MenuPath] = self - end - - return self - end - - --- Removes a menu structure for a client. - -- @param #MENU_CLIENT_COMMAND self - -- @return #nil - 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 -end - ---- MENU_GROUP - -do - -- This local variable is used to cache the menus registered under groups. - -- Menus don't dissapear when groups for players are destroyed and restarted. - -- So every menu for a client created must be tracked so that program logic accidentally does not create. - -- the same menus twice during initialization logic. - -- These menu classes are handling this logic with this variable. - local _MENUGROUPS = {} - - --- The MENU_GROUP class - -- @type MENU_GROUP - -- @extends Menu#MENU_BASE - -- @usage - -- -- This demo creates a menu structure for the two groups of planes. - -- -- Each group will receive a different menu structure. - -- -- To test, join the planes, then look at the other radio menus (Option F10). - -- -- Then switch planes and check if the menu is still there. - -- -- And play with the Add and Remove menu options. - -- - -- -- Note that in multi player, this will only work after the DCS groups bug is solved. - -- - -- local function ShowStatus( PlaneGroup, StatusText, Coalition ) - -- - -- MESSAGE:New( Coalition, 15 ):ToRed() - -- PlaneGroup:Message( StatusText, 15 ) - -- end - -- - -- local MenuStatus = {} - -- - -- local function RemoveStatusMenu( MenuGroup ) - -- local MenuGroupName = MenuGroup:GetName() - -- MenuStatus[MenuGroupName]:Remove() - -- end - -- - -- --- @param Wrapper.Group#GROUP MenuGroup - -- local function AddStatusMenu( MenuGroup ) - -- local MenuGroupName = MenuGroup:GetName() - -- -- This would create a menu for the red coalition under the MenuCoalitionRed menu object. - -- MenuStatus[MenuGroupName] = MENU_GROUP:New( MenuGroup, "Status for Planes" ) - -- MENU_GROUP_COMMAND:New( MenuGroup, "Show Status", MenuStatus[MenuGroupName], ShowStatus, MenuGroup, "Status of planes is ok!", "Message to Red Coalition" ) - -- end - -- - -- SCHEDULER:New( nil, - -- function() - -- local PlaneGroup = GROUP:FindByName( "Plane 1" ) - -- if PlaneGroup and PlaneGroup:IsAlive() then - -- local MenuManage = MENU_GROUP:New( PlaneGroup, "Manage Menus" ) - -- MENU_GROUP_COMMAND:New( PlaneGroup, "Add Status Menu Plane 1", MenuManage, AddStatusMenu, PlaneGroup ) - -- MENU_GROUP_COMMAND:New( PlaneGroup, "Remove Status Menu Plane 1", MenuManage, RemoveStatusMenu, PlaneGroup ) - -- end - -- end, {}, 10, 10 ) - -- - -- SCHEDULER:New( nil, - -- function() - -- local PlaneGroup = GROUP:FindByName( "Plane 2" ) - -- if PlaneGroup and PlaneGroup:IsAlive() then - -- local MenuManage = MENU_GROUP:New( PlaneGroup, "Manage Menus" ) - -- MENU_GROUP_COMMAND:New( PlaneGroup, "Add Status Menu Plane 2", MenuManage, AddStatusMenu, PlaneGroup ) - -- MENU_GROUP_COMMAND:New( PlaneGroup, "Remove Status Menu Plane 2", MenuManage, RemoveStatusMenu, PlaneGroup ) - -- end - -- end, {}, 10, 10 ) - -- - MENU_GROUP = { - ClassName = "MENU_GROUP" - } - - --- MENU_GROUP constructor. Creates a new radio menu item for a group. - -- @param #MENU_GROUP self - -- @param Wrapper.Group#GROUP MenuGroup The Group owning the menu. - -- @param #string MenuText The text for the menu. - -- @param #table ParentMenu The parent menu. - -- @return #MENU_GROUP self - function MENU_GROUP:New( MenuGroup, MenuText, ParentMenu ) - - -- Determine if the menu was not already created and already visible at the group. - -- If it is visible, then return the cached self, otherwise, create self and cache it. - - MenuGroup._Menus = MenuGroup._Menus or {} - local Path = ( ParentMenu and ( table.concat( ParentMenu.MenuPath or {}, "@" ) .. "@" .. MenuText ) ) or MenuText - if MenuGroup._Menus[Path] then - self = MenuGroup._Menus[Path] - else - self = BASE:Inherit( self, MENU_BASE:New( MenuText, ParentMenu ) ) - MenuGroup._Menus[Path] = self - - self.Menus = {} - - self.MenuGroup = MenuGroup - self.Path = Path - self.MenuGroupID = MenuGroup:GetID() - self.MenuText = MenuText - self.ParentMenu = ParentMenu - - self:T( { "Adding Menu ", MenuText, self.MenuParentPath } ) - self.MenuPath = missionCommands.addSubMenuForGroup( self.MenuGroupID, MenuText, self.MenuParentPath ) - - if ParentMenu and ParentMenu.Menus then - ParentMenu.Menus[self.MenuPath] = self - end - end - - --self:F( { MenuGroup:GetName(), MenuText, ParentMenu.MenuPath } ) - - return self - end - - --- Removes the sub menus recursively of this MENU_GROUP. - -- @param #MENU_GROUP self - -- @return #MENU_GROUP self - function MENU_GROUP:RemoveSubMenus() - self:F( self.MenuPath ) - - for MenuID, Menu in pairs( self.Menus ) do - Menu:Remove() - end - - end - - --- Removes the main menu and sub menus recursively of this MENU_GROUP. - -- @param #MENU_GROUP self - -- @return #nil - function MENU_GROUP:Remove() - self:F( { self.MenuGroupID, self.MenuPath } ) - - self:RemoveSubMenus() - - if self.MenuGroup._Menus[self.Path] then - self = self.MenuGroup._Menus[self.Path] - - missionCommands.removeItemForGroup( self.MenuGroupID, self.MenuPath ) - if self.ParentMenu then - self.ParentMenu.Menus[self.MenuPath] = nil - end - self:E( self.MenuGroup._Menus[self.Path] ) - self.MenuGroup._Menus[self.Path] = nil - self = nil - end - return nil - end - - - --- The MENU_GROUP_COMMAND class - -- @type MENU_GROUP_COMMAND - -- @extends Menu#MENU_BASE - MENU_GROUP_COMMAND = { - ClassName = "MENU_GROUP_COMMAND" - } - - --- Creates a new radio command item for a group - -- @param #MENU_GROUP_COMMAND self - -- @param Wrapper.Group#GROUP MenuGroup The Group owning the menu. - -- @param MenuText The text for the menu. - -- @param ParentMenu The parent menu. - -- @param CommandMenuFunction A function that is called when the menu key is pressed. - -- @param CommandMenuArgument An argument for the function. - -- @return Menu#MENU_GROUP_COMMAND self - function MENU_GROUP_COMMAND:New( MenuGroup, MenuText, ParentMenu, CommandMenuFunction, ... ) - - MenuGroup._Menus = MenuGroup._Menus or {} - local Path = ( ParentMenu and ( table.concat( ParentMenu.MenuPath or {}, "@" ) .. "@" .. MenuText ) ) or MenuText - if MenuGroup._Menus[Path] then - self = MenuGroup._Menus[Path] - else - self = BASE:Inherit( self, MENU_COMMAND_BASE:New( MenuText, ParentMenu, CommandMenuFunction, arg ) ) - MenuGroup._Menus[Path] = self - - self.Path = Path - self.MenuGroup = MenuGroup - self.MenuGroupID = MenuGroup:GetID() - self.MenuText = MenuText - self.ParentMenu = ParentMenu - - self:T( { "Adding Command Menu ", MenuText, self.MenuParentPath } ) - self.MenuPath = missionCommands.addCommandForGroup( self.MenuGroupID, MenuText, self.MenuParentPath, self.MenuCallHandler, arg ) - - if ParentMenu and ParentMenu.Menus then - ParentMenu.Menus[self.MenuPath] = self - end - end - - --self:F( { MenuGroup:GetName(), MenuText, ParentMenu.MenuPath } ) - - return self - end - - --- Removes a menu structure for a group. - -- @param #MENU_GROUP_COMMAND self - -- @return #nil - function MENU_GROUP_COMMAND:Remove() - self:F( { self.MenuGroupID, self.MenuPath } ) - - if self.MenuGroup._Menus[self.Path] then - self = self.MenuGroup._Menus[self.Path] - - missionCommands.removeItemForGroup( self.MenuGroupID, self.MenuPath ) - self.ParentMenu.Menus[self.MenuPath] = nil - self:E( self.MenuGroup._Menus[self.Path] ) - self.MenuGroup._Menus[self.Path] = nil - self = nil - end - - return nil - end - -end - ---- This core 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 defines by a zone around a @{Unit#UNIT} with a radius. --- * @{Zone#ZONE_GROUP}: The ZONE_GROUP class defines by a zone around a @{Group#GROUP} with a radius. --- * @{Zone#ZONE_POLYGON}: The ZONE_POLYGON class defines by a sequence of @{Group#GROUP} waypoints within the Mission Editor, forming a polygon. --- --- === --- --- # 1) @{Zone#ZONE_BASE} class, extends @{Base#BASE} --- --- This class is an abstract BASE class for derived classes, and is not meant to be instantiated. --- --- ## 1.1) Each zone has a name: --- --- * @{#ZONE_BASE.GetName}(): Returns the name of the zone. --- --- ## 1.2) Each zone implements two polymorphic functions defined in @{Zone#ZONE_BASE}: --- --- * @{#ZONE_BASE.IsVec2InZone}(): Returns if a Vec2 is within the zone. --- * @{#ZONE_BASE.IsVec3InZone}(): Returns if a Vec3 is within the zone. --- --- ## 1.3) A zone has a probability factor that can be set to randomize a selection between zones: --- --- * @{#ZONE_BASE.SetRandomizeProbability}(): Set the randomization probability of a zone to be selected, taking a value between 0 and 1 ( 0 = 0%, 1 = 100% ) --- * @{#ZONE_BASE.GetRandomizeProbability}(): Get the randomization probability of a zone to be selected, passing a value between 0 and 1 ( 0 = 0%, 1 = 100% ) --- * @{#ZONE_BASE.GetZoneMaybe}(): Get the zone taking into account the randomization probability. nil is returned if this zone is not a candidate. --- --- ## 1.4) A zone manages Vectors: --- --- * @{#ZONE_BASE.GetVec2}(): Returns the @{DCSTypes#Vec2} coordinate of the zone. --- * @{#ZONE_BASE.GetRandomVec2}(): Define a random @{DCSTypes#Vec2} within the zone. --- --- ## 1.5) A zone has a bounding square: --- --- * @{#ZONE_BASE.GetBoundingSquare}(): Get the outer most bounding square of the zone. --- --- ## 1.6) A zone can be marked: --- --- * @{#ZONE_BASE.SmokeZone}(): Smokes the zone boundaries in a color. --- * @{#ZONE_BASE.FlareZone}(): Flares the zone boundaries in a color. --- --- === --- --- # 2) @{Zone#ZONE_RADIUS} class, extends @{Zone#ZONE_BASE} --- --- The ZONE_RADIUS class defined by a zone name, a location and a radius. --- This class implements the inherited functions from Core.Zone#ZONE_BASE taking into account the own zone format and properties. --- --- ## 2.1) @{Zone#ZONE_RADIUS} constructor --- --- * @{#ZONE_RADIUS.New}(): Constructor. --- --- ## 2.2) Manage the radius of the zone --- --- * @{#ZONE_RADIUS.SetRadius}(): Sets the radius of the zone. --- * @{#ZONE_RADIUS.GetRadius}(): Returns the radius of the zone. --- --- ## 2.3) Manage the location of the zone --- --- * @{#ZONE_RADIUS.SetVec2}(): Sets the @{DCSTypes#Vec2} of the zone. --- * @{#ZONE_RADIUS.GetVec2}(): Returns the @{DCSTypes#Vec2} of the zone. --- * @{#ZONE_RADIUS.GetVec3}(): Returns the @{DCSTypes#Vec3} of the zone, taking an additional height parameter. --- --- ## 2.4) Zone point randomization --- --- Various functions exist to find random points within the zone. --- --- * @{#ZONE_RADIUS.GetRandomVec2}(): Gets a random 2D point in the zone. --- * @{#ZONE_RADIUS.GetRandomPointVec2}(): Gets a @{Point#POINT_VEC2} object representing a random 2D point in the zone. --- * @{#ZONE_RADIUS.GetRandomPointVec3}(): Gets a @{Point#POINT_VEC3} object representing a random 3D point in the zone. Note that the height of the point is at landheight. --- --- === --- --- # 3) @{Zone#ZONE} class, extends @{Zone#ZONE_RADIUS} --- --- The ZONE class, defined by the zone name as defined within the Mission Editor. --- This class implements the inherited functions from {Core.Zone#ZONE_RADIUS} taking into account the own zone format and properties. --- --- === --- --- # 4) @{Zone#ZONE_UNIT} class, extends @{Zone#ZONE_RADIUS} --- --- The ZONE_UNIT class defined by a zone around a @{Unit#UNIT} with a radius. --- This class implements the inherited functions from @{Zone#ZONE_RADIUS} taking into account the own zone format and properties. --- --- === --- --- # 5) @{Zone#ZONE_GROUP} class, extends @{Zone#ZONE_RADIUS} --- --- The ZONE_GROUP class defines by a zone around a @{Group#GROUP} with a radius. The current leader of the group defines the center of the zone. --- This class implements the inherited functions from @{Zone#ZONE_RADIUS} taking into account the own zone format and properties. --- --- === --- --- # 6) @{Zone#ZONE_POLYGON_BASE} class, extends @{Zone#ZONE_BASE} --- --- The ZONE_POLYGON_BASE class defined by a sequence of @{Group#GROUP} waypoints within the Mission Editor, forming a polygon. --- This class implements the inherited functions from @{Zone#ZONE_RADIUS} taking into account the own zone format and properties. --- This class is an abstract BASE class for derived classes, and is not meant to be instantiated. --- --- ## 6.1) Zone point randomization --- --- Various functions exist to find random points within the zone. --- --- * @{#ZONE_POLYGON_BASE.GetRandomVec2}(): Gets a random 2D point in the zone. --- * @{#ZONE_POLYGON_BASE.GetRandomPointVec2}(): Return a @{Point#POINT_VEC2} object representing a random 2D point within the zone. --- * @{#ZONE_POLYGON_BASE.GetRandomPointVec3}(): Return a @{Point#POINT_VEC3} object representing a random 3D point at landheight within the zone. --- --- --- === --- --- # 7) @{Zone#ZONE_POLYGON} class, extends @{Zone#ZONE_POLYGON_BASE} --- --- The ZONE_POLYGON class defined by a sequence of @{Group#GROUP} waypoints within the Mission Editor, forming a polygon. --- This class implements the inherited functions from @{Zone#ZONE_RADIUS} taking into account the own zone format and properties. --- --- ==== --- --- **API CHANGE HISTORY** --- ====================== --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- Hereby the change log: --- --- 2017-02-28: ZONE\_BASE:**IsVec2InZone()** replaces ZONE\_BASE:_IsPointVec2InZone()_. --- 2017-02-28: ZONE\_BASE:**IsVec3InZone()** replaces ZONE\_BASE:_IsPointVec3InZone()_. --- 2017-02-28: ZONE\_RADIUS:**IsVec2InZone()** replaces ZONE\_RADIUS:_IsPointVec2InZone()_. --- 2017-02-28: ZONE\_RADIUS:**IsVec3InZone()** replaces ZONE\_RADIUS:_IsPointVec3InZone()_. --- 2017-02-28: ZONE\_POLYGON:**IsVec2InZone()** replaces ZONE\_POLYGON:_IsPointVec2InZone()_. --- 2017-02-28: ZONE\_POLYGON:**IsVec3InZone()** replaces ZONE\_POLYGON:_IsPointVec3InZone()_. --- --- 2017-02-18: ZONE\_POLYGON_BASE:**GetRandomPointVec2()** added. --- --- 2017-02-18: ZONE\_POLYGON_BASE:**GetRandomPointVec3()** added. --- --- 2017-02-18: ZONE\_RADIUS:**GetRandomPointVec3( inner, outer )** added. --- --- 2017-02-18: ZONE\_RADIUS:**GetRandomPointVec2( inner, outer )** added. --- --- 2016-08-15: ZONE\_BASE:**GetName()** added. --- --- 2016-08-15: ZONE\_BASE:**SetZoneProbability( ZoneProbability )** added. --- --- 2016-08-15: ZONE\_BASE:**GetZoneProbability()** added. --- --- 2016-08-15: ZONE\_BASE:**GetZoneMaybe()** added. --- --- === --- --- @module Zone - - ---- The ZONE_BASE class --- @type ZONE_BASE --- @field #string ZoneName Name of the zone. --- @field #number ZoneProbability A value between 0 and 1. 0 = 0% and 1 = 100% probability. --- @extends Core.Base#BASE -ZONE_BASE = { - ClassName = "ZONE_BASE", - ZoneName = "", - ZoneProbability = 1, - } - - ---- The ZONE_BASE.BoundingSquare --- @type ZONE_BASE.BoundingSquare --- @field Dcs.DCSTypes#Distance x1 The lower x coordinate (left down) --- @field Dcs.DCSTypes#Distance y1 The lower y coordinate (left down) --- @field Dcs.DCSTypes#Distance x2 The higher x coordinate (right up) --- @field Dcs.DCSTypes#Distance y2 The higher y coordinate (right up) - - ---- 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 the name of the zone. --- @param #ZONE_BASE self --- @return #string The name of the zone. -function ZONE_BASE:GetName() - self:F2() - - return self.ZoneName -end ---- Returns if a location is within the zone. --- @param #ZONE_BASE self --- @param Dcs.DCSTypes#Vec2 Vec2 The location to test. --- @return #boolean true if the location is within the zone. -function ZONE_BASE:IsVec2InZone( Vec2 ) - self:F2( Vec2 ) - - return false -end - ---- Returns if a point is within the zone. --- @param #ZONE_BASE self --- @param Dcs.DCSTypes#Vec3 Vec3 The point to test. --- @return #boolean true if the point is within the zone. -function ZONE_BASE:IsVec3InZone( Vec3 ) - self:F2( Vec3 ) - - local InZone = self:IsVec2InZone( { x = Vec3.x, y = Vec3.z } ) - - return InZone -end - ---- Returns the @{DCSTypes#Vec2} coordinate of the zone. --- @param #ZONE_BASE self --- @return #nil. -function ZONE_BASE:GetVec2() - self:F2( self.ZoneName ) - - return nil -end - ---- Define a random @{DCSTypes#Vec2} within the zone. --- @param #ZONE_BASE self --- @return Dcs.DCSTypes#Vec2 The Vec2 coordinates. -function ZONE_BASE:GetRandomVec2() - return nil -end - ---- Define a random @{Point#POINT_VEC2} within the zone. --- @param #ZONE_BASE self --- @return Core.Point#POINT_VEC2 The PointVec2 coordinates. -function ZONE_BASE:GetRandomPointVec2() - return nil -end - ---- Get the bounding square the zone. --- @param #ZONE_BASE self --- @return #nil The bounding square. -function ZONE_BASE:GetBoundingSquare() - --return { x1 = 0, y1 = 0, x2 = 0, y2 = 0 } - return nil -end - ---- Bound the zone boundaries with a tires. --- @param #ZONE_BASE self -function ZONE_BASE:BoundZone() - self:F2() - -end - ---- Smokes the zone boundaries in a color. --- @param #ZONE_BASE self --- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color. -function ZONE_BASE:SmokeZone( SmokeColor ) - self:F2( SmokeColor ) - -end - ---- Set the randomization probability of a zone to be selected. --- @param #ZONE_BASE self --- @param ZoneProbability A value between 0 and 1. 0 = 0% and 1 = 100% probability. -function ZONE_BASE:SetZoneProbability( ZoneProbability ) - self:F2( ZoneProbability ) - - self.ZoneProbability = ZoneProbability or 1 - return self -end - ---- Get the randomization probability of a zone to be selected. --- @param #ZONE_BASE self --- @return #number A value between 0 and 1. 0 = 0% and 1 = 100% probability. -function ZONE_BASE:GetZoneProbability() - self:F2() - - return self.ZoneProbability -end - ---- Get the zone taking into account the randomization probability of a zone to be selected. --- @param #ZONE_BASE self --- @return #ZONE_BASE The zone is selected taking into account the randomization probability factor. --- @return #nil The zone is not selected taking into account the randomization probability factor. -function ZONE_BASE:GetZoneMaybe() - self:F2() - - local Randomization = math.random() - if Randomization <= self.ZoneProbability then - return self - else - return nil - end -end - - ---- The ZONE_RADIUS class, defined by a zone name, a location and a radius. --- @type ZONE_RADIUS --- @field Dcs.DCSTypes#Vec2 Vec2 The current location of the zone. --- @field Dcs.DCSTypes#Distance Radius The radius of the zone. --- @extends Core.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 Dcs.DCSTypes#Vec2 Vec2 The location of the zone. --- @param Dcs.DCSTypes#Distance Radius The radius of the zone. --- @return #ZONE_RADIUS self -function ZONE_RADIUS:New( ZoneName, Vec2, Radius ) - local self = BASE:Inherit( self, ZONE_BASE:New( ZoneName ) ) -- #ZONE_RADIUS - self:F( { ZoneName, Vec2, Radius } ) - - self.Radius = Radius - self.Vec2 = Vec2 - - return self -end - ---- Bounds the zone with tires. --- @param #ZONE_RADIUS self --- @param #number Points (optional) The amount of points in the circle. --- @return #ZONE_RADIUS self -function ZONE_RADIUS:BoundZone( Points ) - - local Point = {} - local Vec2 = self:GetVec2() - - Points = Points and Points or 360 - - local Angle - local RadialBase = math.pi*2 - - -- - for Angle = 0, 360, (360 / Points ) do - local Radial = Angle * RadialBase / 360 - Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() - Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() - - local Tire = { - ["country"] = "USA", - ["category"] = "Fortifications", - ["canCargo"] = false, - ["shape_name"] = "H-tyre_B_WF", - ["type"] = "Black_Tyre_WF", - --["unitId"] = Angle + 10000, - ["y"] = Point.y, - ["x"] = Point.x, - ["name"] = string.format( "%s-Tire #%0d", self:GetName(), Angle ), - ["heading"] = 0, - } -- end of ["group"] - - coalition.addStaticObject( country.id.USA, Tire ) - end - - return self -end - - ---- Smokes the zone boundaries in a color. --- @param #ZONE_RADIUS self --- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color. --- @param #number Points (optional) The amount of points in the circle. --- @return #ZONE_RADIUS self -function ZONE_RADIUS:SmokeZone( SmokeColor, Points ) - self:F2( SmokeColor ) - - local Point = {} - local Vec2 = self:GetVec2() - - Points = Points and Points or 360 - - local Angle - local RadialBase = math.pi*2 - - for Angle = 0, 360, 360 / Points do - local Radial = Angle * RadialBase / 360 - Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() - Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() - 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 Utilities.Utils#FLARECOLOR FlareColor The flare color. --- @param #number Points (optional) The amount of points in the circle. --- @param Dcs.DCSTypes#Azimuth Azimuth (optional) Azimuth The azimuth of the flare. --- @return #ZONE_RADIUS self -function ZONE_RADIUS:FlareZone( FlareColor, Points, Azimuth ) - self:F2( { FlareColor, Azimuth } ) - - local Point = {} - local Vec2 = self:GetVec2() - - Points = Points and Points or 360 - - local Angle - local RadialBase = math.pi*2 - - for Angle = 0, 360, 360 / Points do - local Radial = Angle * RadialBase / 360 - Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() - Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() - 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 Dcs.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 Dcs.DCSTypes#Distance Radius The radius of the zone. --- @return Dcs.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 @{DCSTypes#Vec2} of the zone. --- @param #ZONE_RADIUS self --- @return Dcs.DCSTypes#Vec2 The location of the zone. -function ZONE_RADIUS:GetVec2() - self:F2( self.ZoneName ) - - self:T2( { self.Vec2 } ) - - return self.Vec2 -end - ---- Sets the @{DCSTypes#Vec2} of the zone. --- @param #ZONE_RADIUS self --- @param Dcs.DCSTypes#Vec2 Vec2 The new location of the zone. --- @return Dcs.DCSTypes#Vec2 The new location of the zone. -function ZONE_RADIUS:SetVec2( Vec2 ) - self:F2( self.ZoneName ) - - self.Vec2 = Vec2 - - self:T2( { self.Vec2 } ) - - return self.Vec2 -end - ---- Returns the @{DCSTypes#Vec3} of the ZONE_RADIUS. --- @param #ZONE_RADIUS self --- @param Dcs.DCSTypes#Distance Height The height to add to the land height where the center of the zone is located. --- @return Dcs.DCSTypes#Vec3 The point of the zone. -function ZONE_RADIUS:GetVec3( Height ) - self:F2( { self.ZoneName, Height } ) - - Height = Height or 0 - local Vec2 = self:GetVec2() - - local Vec3 = { x = Vec2.x, y = land.getHeight( self:GetVec2() ) + Height, z = Vec2.y } - - self:T2( { Vec3 } ) - - return Vec3 -end - - ---- Returns if a location is within the zone. --- @param #ZONE_RADIUS self --- @param Dcs.DCSTypes#Vec2 Vec2 The location to test. --- @return #boolean true if the location is within the zone. -function ZONE_RADIUS:IsVec2InZone( Vec2 ) - self:F2( Vec2 ) - - local ZoneVec2 = self:GetVec2() - - if ZoneVec2 then - if (( Vec2.x - ZoneVec2.x )^2 + ( Vec2.y - ZoneVec2.y ) ^2 ) ^ 0.5 <= self:GetRadius() then - return true - end - end - - return false -end - ---- Returns if a point is within the zone. --- @param #ZONE_RADIUS self --- @param Dcs.DCSTypes#Vec3 Vec3 The point to test. --- @return #boolean true if the point is within the zone. -function ZONE_RADIUS:IsVec3InZone( Vec3 ) - self:F2( Vec3 ) - - local InZone = self:IsVec2InZone( { x = Vec3.x, y = Vec3.z } ) - - return InZone -end - ---- Returns a random Vec2 location within the zone. --- @param #ZONE_RADIUS self --- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. --- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. --- @return Dcs.DCSTypes#Vec2 The random location within the zone. -function ZONE_RADIUS:GetRandomVec2( inner, outer ) - self:F( self.ZoneName, inner, outer ) - - local Point = {} - local Vec2 = self:GetVec2() - local _inner = inner or 0 - local _outer = outer or self:GetRadius() - - local angle = math.random() * math.pi * 2; - Point.x = Vec2.x + math.cos( angle ) * math.random(_inner, _outer); - Point.y = Vec2.y + math.sin( angle ) * math.random(_inner, _outer); - - self:T( { Point } ) - - return Point -end - ---- Returns a @{Point#POINT_VEC2} object reflecting a random 2D location within the zone. --- @param #ZONE_RADIUS self --- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. --- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. --- @return Core.Point#POINT_VEC2 The @{Point#POINT_VEC2} object reflecting the random 3D location within the zone. -function ZONE_RADIUS:GetRandomPointVec2( inner, outer ) - self:F( self.ZoneName, inner, outer ) - - local PointVec2 = POINT_VEC2:NewFromVec2( self:GetRandomVec2() ) - - self:T3( { PointVec2 } ) - - return PointVec2 -end - ---- Returns a @{Point#POINT_VEC3} object reflecting a random 3D location within the zone. --- @param #ZONE_RADIUS self --- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. --- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. --- @return Core.Point#POINT_VEC3 The @{Point#POINT_VEC3} object reflecting the random 3D location within the zone. -function ZONE_RADIUS:GetRandomPointVec3( inner, outer ) - self:F( self.ZoneName, inner, outer ) - - local PointVec3 = POINT_VEC3:NewFromVec2( self:GetRandomVec2() ) - - self:T3( { PointVec3 } ) - - return PointVec3 -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 Core.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 Wrapper.Unit#UNIT ZoneUNIT --- @extends Core.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 Wrapper.Unit#UNIT ZoneUNIT The unit as the center of the zone. --- @param Dcs.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:GetVec2(), Radius ) ) - self:F( { ZoneName, ZoneUNIT:GetVec2(), Radius } ) - - self.ZoneUNIT = ZoneUNIT - self.LastVec2 = ZoneUNIT:GetVec2() - - return self -end - - ---- Returns the current location of the @{Unit#UNIT}. --- @param #ZONE_UNIT self --- @return Dcs.DCSTypes#Vec2 The location of the zone based on the @{Unit#UNIT}location. -function ZONE_UNIT:GetVec2() - self:F( self.ZoneName ) - - local ZoneVec2 = self.ZoneUNIT:GetVec2() - if ZoneVec2 then - self.LastVec2 = ZoneVec2 - return ZoneVec2 - else - return self.LastVec2 - end - - self:T( { ZoneVec2 } ) - - return nil -end - ---- Returns a random location within the zone. --- @param #ZONE_UNIT self --- @return Dcs.DCSTypes#Vec2 The random location within the zone. -function ZONE_UNIT:GetRandomVec2() - self:F( self.ZoneName ) - - local RandomVec2 = {} - local Vec2 = self.ZoneUNIT:GetVec2() - - if not Vec2 then - Vec2 = self.LastVec2 - end - - local angle = math.random() * math.pi*2; - RandomVec2.x = Vec2.x + math.cos( angle ) * math.random() * self:GetRadius(); - RandomVec2.y = Vec2.y + math.sin( angle ) * math.random() * self:GetRadius(); - - self:T( { RandomVec2 } ) - - return RandomVec2 -end - ---- Returns the @{DCSTypes#Vec3} of the ZONE_UNIT. --- @param #ZONE_UNIT self --- @param Dcs.DCSTypes#Distance Height The height to add to the land height where the center of the zone is located. --- @return Dcs.DCSTypes#Vec3 The point of the zone. -function ZONE_UNIT:GetVec3( Height ) - self:F2( self.ZoneName ) - - Height = Height or 0 - - local Vec2 = self:GetVec2() - - local Vec3 = { x = Vec2.x, y = land.getHeight( self:GetVec2() ) + Height, z = Vec2.y } - - self:T2( { Vec3 } ) - - return Vec3 -end - ---- The ZONE_GROUP class defined by a zone around a @{Group}, taking the average center point of all the units within the Group, with a radius. --- @type ZONE_GROUP --- @field Wrapper.Group#GROUP ZoneGROUP --- @extends Core.Zone#ZONE_RADIUS -ZONE_GROUP = { - ClassName="ZONE_GROUP", - } - ---- Constructor to create a ZONE_GROUP instance, taking the zone name, a zone @{Group#GROUP} and a radius. --- @param #ZONE_GROUP self --- @param #string ZoneName Name of the zone. --- @param Wrapper.Group#GROUP ZoneGROUP The @{Group} as the center of the zone. --- @param Dcs.DCSTypes#Distance Radius The radius of the zone. --- @return #ZONE_GROUP self -function ZONE_GROUP:New( ZoneName, ZoneGROUP, Radius ) - local self = BASE:Inherit( self, ZONE_RADIUS:New( ZoneName, ZoneGROUP:GetVec2(), Radius ) ) - self:F( { ZoneName, ZoneGROUP:GetVec2(), Radius } ) - - self.ZoneGROUP = ZoneGROUP - - return self -end - - ---- Returns the current location of the @{Group}. --- @param #ZONE_GROUP self --- @return Dcs.DCSTypes#Vec2 The location of the zone based on the @{Group} location. -function ZONE_GROUP:GetVec2() - self:F( self.ZoneName ) - - local ZoneVec2 = self.ZoneGROUP:GetVec2() - - self:T( { ZoneVec2 } ) - - return ZoneVec2 -end - ---- Returns a random location within the zone of the @{Group}. --- @param #ZONE_GROUP self --- @return Dcs.DCSTypes#Vec2 The random location of the zone based on the @{Group} location. -function ZONE_GROUP:GetRandomVec2() - self:F( self.ZoneName ) - - local Point = {} - local Vec2 = self.ZoneGROUP:GetVec2() - - local angle = math.random() * math.pi*2; - Point.x = Vec2.x + math.cos( angle ) * math.random() * self:GetRadius(); - Point.y = Vec2.y + math.sin( angle ) * math.random() * self:GetRadius(); - - self:T( { Point } ) - - return Point -end - - - --- 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 Core.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 --- @return #ZONE_POLYGON_BASE self -function ZONE_POLYGON_BASE:BoundZone( ) - - local i - local j - local Segments = 10 - - i = 1 - j = #self.Polygon - - while i <= #self.Polygon do - self:T( { i, j, self.Polygon[i], self.Polygon[j] } ) - - local DeltaX = self.Polygon[j].x - self.Polygon[i].x - local DeltaY = self.Polygon[j].y - self.Polygon[i].y - - for Segment = 0, Segments do -- We divide each line in 5 segments and smoke a point on the line. - local PointX = self.Polygon[i].x + ( Segment * DeltaX / Segments ) - local PointY = self.Polygon[i].y + ( Segment * DeltaY / Segments ) - local Tire = { - ["country"] = "USA", - ["category"] = "Fortifications", - ["canCargo"] = false, - ["shape_name"] = "H-tyre_B_WF", - ["type"] = "Black_Tyre_WF", - ["y"] = PointY, - ["x"] = PointX, - ["name"] = string.format( "%s-Tire #%0d", self:GetName(), ((i - 1) * Segments) + Segment ), - ["heading"] = 0, - } -- end of ["group"] - - coalition.addStaticObject( country.id.USA, Tire ) - - end - j = i - i = i + 1 - end - - return self -end - - - ---- Smokes the zone boundaries in a color. --- @param #ZONE_POLYGON_BASE self --- @param Utilities.Utils#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. --- Source learned and taken from: https://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html --- @param #ZONE_POLYGON_BASE self --- @param Dcs.DCSTypes#Vec2 Vec2 The location to test. --- @return #boolean true if the location is within the zone. -function ZONE_POLYGON_BASE:IsVec2InZone( Vec2 ) - self:F2( Vec2 ) - - local Next - local Prev - local InPolygon = false - - Next = 1 - Prev = #self.Polygon - - while Next <= #self.Polygon do - self:T( { Next, Prev, self.Polygon[Next], self.Polygon[Prev] } ) - if ( ( ( self.Polygon[Next].y > Vec2.y ) ~= ( self.Polygon[Prev].y > Vec2.y ) ) and - ( Vec2.x < ( self.Polygon[Prev].x - self.Polygon[Next].x ) * ( Vec2.y - self.Polygon[Next].y ) / ( self.Polygon[Prev].y - self.Polygon[Next].y ) + self.Polygon[Next].x ) - ) then - InPolygon = not InPolygon - end - self:T2( { InPolygon = InPolygon } ) - Prev = Next - Next = Next + 1 - end - - self:T( { InPolygon = InPolygon } ) - return InPolygon -end - ---- Define a random @{DCSTypes#Vec2} within the zone. --- @param #ZONE_POLYGON_BASE self --- @return Dcs.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:IsVec2InZone( Vec2 ) then - Vec2Found = true - end - end - - self:T2( Vec2 ) - - return Vec2 -end - ---- Return a @{Point#POINT_VEC2} object representing a random 2D point at landheight within the zone. --- @param #ZONE_POLYGON_BASE self --- @return @{Point#POINT_VEC2} -function ZONE_POLYGON_BASE:GetRandomPointVec2() - self:F2() - - local PointVec2 = POINT_VEC2:NewFromVec2( self:GetRandomVec2() ) - - self:T2( PointVec2 ) - - return PointVec2 -end - ---- Return a @{Point#POINT_VEC3} object representing a random 3D point at landheight within the zone. --- @param #ZONE_POLYGON_BASE self --- @return @{Point#POINT_VEC3} -function ZONE_POLYGON_BASE:GetRandomPointVec3() - self:F2() - - local PointVec3 = POINT_VEC3:NewFromVec2( self:GetRandomVec2() ) - - self:T2( PointVec3 ) - - return PointVec3 -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 Core.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 Wrapper.Group#GROUP ZoneGroup The GROUP waypoints as defined within the Mission Editor define the polygon shape. --- @return #ZONE_POLYGON self -function ZONE_POLYGON:New( ZoneName, ZoneGroup ) - - local GroupPoints = ZoneGroup:GetTaskRoute() - - local self = BASE:Inherit( self, ZONE_POLYGON_BASE:New( ZoneName, GroupPoints ) ) - self:F( { ZoneName, ZoneGroup, self.Polygon } ) - - return self -end - ---- This module contains the DATABASE class, managing the database of mission objects. --- --- ==== --- --- 1) @{#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 Core.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() ) - - self:SetEventPriority( 1 ) - - self:HandleEvent( EVENTS.Birth, self._EventOnBirth ) - self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) - self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) - - -- Follow alive players and clients - self:HandleEvent( EVENTS.PlayerEnterUnit, self._EventOnPlayerEnterUnit ) - self:HandleEvent( EVENTS.PlayerLeaveUnit, self._EventOnPlayerLeaveUnit ) - - 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 Wrapper.Unit#UNIT The found Unit. -function DATABASE:FindUnit( UnitName ) - - local UnitFound = self.UNITS[UnitName] - return UnitFound -end - - ---- Adds a Unit based on the Unit Name in the DATABASE. --- @param #DATABASE self -function DATABASE:AddUnit( DCSUnitName ) - - if not self.UNITS[DCSUnitName] then - local UnitRegister = UNIT:Register( DCSUnitName ) - 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 Wrapper.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 Wrapper.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 Wrapper.Client#CLIENT The found CLIENT. -function DATABASE:FindClient( ClientName ) - - local ClientFound = self.CLIENTS[ClientName] - return ClientFound -end - - ---- Adds a CLIENT based on the ClientName in the DATABASE. --- @param #DATABASE self -function DATABASE:AddClient( ClientName ) - - if not self.CLIENTS[ClientName] then - self.CLIENTS[ClientName] = CLIENT:Register( ClientName ) - end - - return self.CLIENTS[ClientName] -end - - ---- Finds a GROUP based on the GroupName. --- @param #DATABASE self --- @param #string GroupName --- @return Wrapper.Group#GROUP The found GROUP. -function DATABASE:FindGroup( GroupName ) - - local GroupFound = self.GROUPS[GroupName] - return GroupFound -end - - ---- Adds a GROUP based on the GroupName in the DATABASE. --- @param #DATABASE self -function DATABASE:AddGroup( GroupName ) - - if not self.GROUPS[GroupName] then - self:E( { "Add GROUP:", GroupName } ) - self.GROUPS[GroupName] = GROUP:Register( GroupName ) - end - - return self.GROUPS[GroupName] -end - ---- Adds a player based on the Player Name in the DATABASE. --- @param #DATABASE self -function DATABASE:AddPlayer( UnitName, PlayerName ) - - if PlayerName then - self: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:F( SpawnTemplate.name ) - - self:T( { SpawnTemplate.SpawnCountryID, SpawnTemplate.SpawnCategoryID } ) - - -- Copy the spawn variables of the template in temporary storage, nullify, and restore the spawn variables. - local SpawnCoalitionID = SpawnTemplate.CoalitionID - local SpawnCountryID = SpawnTemplate.CountryID - local SpawnCategoryID = SpawnTemplate.CategoryID - - -- Nullify - SpawnTemplate.CoalitionID = nil - SpawnTemplate.CountryID = nil - SpawnTemplate.CategoryID = nil - - self:_RegisterTemplate( SpawnTemplate, SpawnCoalitionID, SpawnCategoryID, SpawnCountryID ) - - self:T3( SpawnTemplate ) - coalition.addGroup( SpawnCountryID, SpawnCategoryID, SpawnTemplate ) - - -- Restore - SpawnTemplate.CoalitionID = SpawnCoalitionID - SpawnTemplate.CountryID = SpawnCountryID - SpawnTemplate.CategoryID = 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 - - GroupTemplate.CategoryID = CategoryID - GroupTemplate.CoalitionID = CoalitionID - GroupTemplate.CountryID = CountryID - - self.Templates.Groups[GroupTemplateName].GroupName = GroupTemplateName - self.Templates.Groups[GroupTemplateName].Template = GroupTemplate - self.Templates.Groups[GroupTemplateName].groupId = GroupTemplate.groupId - self.Templates.Groups[GroupTemplateName].UnitCount = #GroupTemplate.units - self.Templates.Groups[GroupTemplateName].Units = GroupTemplate.units - self.Templates.Groups[GroupTemplateName].CategoryID = CategoryID - self.Templates.Groups[GroupTemplateName].CoalitionID = 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 - - UnitTemplate.name = env.getValueDictByKey(UnitTemplate.name) - - self.Templates.Units[UnitTemplate.name] = {} - self.Templates.Units[UnitTemplate.name].UnitName = UnitTemplate.name - self.Templates.Units[UnitTemplate.name].Template = UnitTemplate - self.Templates.Units[UnitTemplate.name].GroupName = GroupTemplateName - self.Templates.Units[UnitTemplate.name].GroupTemplate = GroupTemplate - self.Templates.Units[UnitTemplate.name].GroupId = GroupTemplate.groupId - self.Templates.Units[UnitTemplate.name].CategoryID = CategoryID - self.Templates.Units[UnitTemplate.name].CoalitionID = CoalitionID - self.Templates.Units[UnitTemplate.name].CountryID = CountryID - - if UnitTemplate.skill and (UnitTemplate.skill == "Client" or UnitTemplate.skill == "Player") then - self.Templates.ClientsByName[UnitTemplate.name] = UnitTemplate - self.Templates.ClientsByName[UnitTemplate.name].CategoryID = CategoryID - self.Templates.ClientsByName[UnitTemplate.name].CoalitionID = CoalitionID - self.Templates.ClientsByName[UnitTemplate.name].CountryID = CountryID - self.Templates.ClientsByID[UnitTemplate.unitId] = UnitTemplate - end - - TraceTable[#TraceTable+1] = self.Templates.Units[UnitTemplate.name].UnitName - end - - self:E( TraceTable ) -end - -function DATABASE:GetGroupTemplate( GroupName ) - local GroupTemplate = self.Templates.Groups[GroupName].Template - GroupTemplate.SpawnCoalitionID = self.Templates.Groups[GroupName].CoalitionID - GroupTemplate.SpawnCategoryID = self.Templates.Groups[GroupName].CategoryID - GroupTemplate.SpawnCountryID = self.Templates.Groups[GroupName].CountryID - return GroupTemplate -end - -function DATABASE:GetGroupNameFromUnitName( UnitName ) - return self.Templates.Units[UnitName].GroupName -end - -function DATABASE:GetGroupTemplateFromUnitName( UnitName ) - return self.Templates.Units[UnitName].GroupTemplate -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 Core.Event#EVENTDATA Event -function DATABASE:_EventOnBirth( Event ) - self:F2( { Event } ) - - if Event.IniDCSUnit then - if Event.IniObjectCategory == 3 then - self:AddStatic( Event.IniDCSUnitName ) - else - if Event.IniObjectCategory == 1 then - self:AddUnit( Event.IniDCSUnitName ) - self:AddGroup( Event.IniDCSGroupName ) - end - end - self:_EventOnPlayerEnterUnit( Event ) - end -end - - ---- Handles the OnDead or OnCrash event for alive units set. --- @param #DATABASE self --- @param Core.Event#EVENTDATA Event -function DATABASE:_EventOnDeadOrCrash( Event ) - self:F2( { Event } ) - - if Event.IniDCSUnit then - if Event.IniObjectCategory == 3 then - if self.STATICS[Event.IniDCSUnitName] then - self:DeleteStatic( Event.IniDCSUnitName ) - end - else - if Event.IniObjectCategory == 1 then - if self.UNITS[Event.IniDCSUnitName] then - self:DeleteUnit( Event.IniDCSUnitName ) - end - end - end - end -end - - ---- Handles the OnPlayerEnterUnit event to fill the active players table (with the unit filter applied). --- @param #DATABASE self --- @param Core.Event#EVENTDATA Event -function DATABASE:_EventOnPlayerEnterUnit( Event ) - self:F2( { Event } ) - - if Event.IniUnit then - if Event.IniObjectCategory == 1 then - self:AddUnit( Event.IniDCSUnitName ) - self:AddGroup( Event.IniDCSGroupName ) - local PlayerName = Event.IniUnit:GetPlayerName() - if not self.PLAYERS[PlayerName] then - self:AddPlayer( Event.IniUnitName, PlayerName ) - end - end - end -end - - ---- Handles the OnPlayerLeaveUnit event to clean the active players table. --- @param #DATABASE self --- @param Core.Event#EVENTDATA Event -function DATABASE:_EventOnPlayerLeaveUnit( Event ) - self:F2( { Event } ) - - if Event.IniUnit then - if Event.IniObjectCategory == 1 then - local PlayerName = Event.IniUnit:GetPlayerName() - if self.PLAYERS[PlayerName] then - self:DeletePlayer( PlayerName ) - end - end - end -end - ---- Iterators - ---- Iterate the DATABASE and call an iterator function for the given set, providing the Object for each element within the set and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called when there is an alive player in the database. --- @return #DATABASE self -function DATABASE:ForEach( IteratorFunction, FinalizeFunction, arg, Set ) - self:F2( arg ) - - local function CoRoutine() - local Count = 0 - for ObjectID, Object in pairs( Set ) do - self:T2( Object ) - IteratorFunction( Object, unpack( arg ) ) - Count = Count + 1 --- if Count % 100 == 0 then --- coroutine.yield( false ) --- end - end - return true - end - --- local co = coroutine.create( CoRoutine ) - local co = CoRoutine - - local function Schedule() - --- local status, res = coroutine.resume( co ) - local status, res = co() - self:T3( { status, res } ) - - if status == false then - error( res ) - end - if res == false then - return true -- resume next time the loop - end - if FinalizeFunction then - FinalizeFunction( unpack( arg ) ) - end - return false - end - - local Scheduler = SCHEDULER:New( self, Schedule, {}, 0.001, 0.001, 0 ) - - 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] = {} - - local CoalitionSide = coalition.side[string.upper(CoalitionName)] - - ---------------------------------------------- - -- build nav points DB - self.Navpoints[CoalitionName] = {} - if coa_data.nav_points then --navpoints - for nav_ind, nav_data in pairs(coa_data.nav_points) do - - if type(nav_data) == 'table' then - self.Navpoints[CoalitionName][nav_ind] = routines.utils.deepCopy(nav_data) - - self.Navpoints[CoalitionName][nav_ind]['name'] = nav_data.callsignStr -- name is a little bit more self-explanatory. - self.Navpoints[CoalitionName][nav_ind]['point'] = {} -- point is used by SSE, support it. - self.Navpoints[CoalitionName][nav_ind]['point']['x'] = nav_data.x - self.Navpoints[CoalitionName][nav_ind]['point']['y'] = 0 - self.Navpoints[CoalitionName][nav_ind]['point']['z'] = nav_data.y - end - end - end - ------------------------------------------------- - if coa_data.country then --there is a country table - for cntry_id, cntry_data in pairs(coa_data.country) do - - local CountryName = string.upper(cntry_data.name) - local CountryID = cntry_data.id - - --self.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, - CoalitionSide, - _DATABASECategory[string.lower(CategoryName)], - CountryID - ) - end --if GroupTemplate and GroupTemplate.units then - end --for group_num, GroupTemplate in pairs(obj_type_data.group) do - end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then - end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then - end --for obj_type_name, obj_type_data in pairs(cntry_data) do - end --if type(cntry_data) == 'table' then - end --for cntry_id, cntry_data in pairs(coa_data.country) do - end --if coa_data.country then --there is a country table - end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then - end --for coa_name, coa_data in pairs(mission.coalition) do - - return self -end - - - - ---- 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. --- --- ==== --- --- ### Authors: --- --- * FlightControl : Design & Programming --- --- ### Contributions: --- --- --- @module Set - - ---- SET_BASE class --- @type SET_BASE --- @field #table Filter --- @field #table Set --- @field #table List --- @field Core.Scheduler#SCHEDULER CallScheduler --- @extends Core.Base#BASE -SET_BASE = { - ClassName = "SET_BASE", - Filter = {}, - Set = {}, - List = {}, -} - ---- Creates a new SET_BASE object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. --- @param #SET_BASE self --- @return #SET_BASE --- @usage --- -- Define a new SET_BASE Object. This DBObject will contain a reference to all Group and Unit Templates defined within the ME and the DCSRTE. --- DBObject = SET_BASE:New() -function SET_BASE:New( Database ) - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) -- Core.Set#SET_BASE - - self.Database = Database - - self.YieldInterval = 10 - self.TimeInterval = 0.001 - - self.List = {} - self.List.__index = self.List - self.List = setmetatable( { Count = 0 }, self.List ) - - self.CallScheduler = SCHEDULER:New( self ) - - self:SetEventPriority( 2 ) - - return self -end - ---- Finds an @{Base#BASE} object based on the object Name. --- @param #SET_BASE self --- @param #string ObjectName --- @return Core.Base#BASE The Object found. -function SET_BASE:_Find( ObjectName ) - - local ObjectFound = self.Set[ObjectName] - return ObjectFound -end - - ---- Gets the Set. --- @param #SET_BASE self --- @return #SET_BASE self -function SET_BASE:GetSet() - self:F2() - - return self.Set -end - ---- Adds a @{Base#BASE} object in the @{Set#SET_BASE}, using a given ObjectName as the index. --- @param #SET_BASE self --- @param #string ObjectName --- @param Core.Base#BASE Object --- @return Core.Base#BASE The added BASE Object. -function SET_BASE:Add( ObjectName, Object ) - self:F2( ObjectName ) - - local t = { _ = Object } - - if self.List.last then - self.List.last._next = t - t._prev = self.List.last - self.List.last = t - else - -- this is the first node - self.List.first = t - self.List.last = t - end - - self.List.Count = self.List.Count + 1 - - self.Set[ObjectName] = t._ - -end - ---- Adds a @{Base#BASE} object in the @{Set#SET_BASE}, using the Object Name as the index. --- @param #SET_BASE self --- @param Wrapper.Object#OBJECT Object --- @return Core.Base#BASE The added BASE Object. -function SET_BASE:AddObject( Object ) - self:F2( Object.ObjectName ) - - self:T( Object.UnitName ) - self:T( Object.ObjectName ) - self:Add( Object.ObjectName, Object ) - -end - - - ---- 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:F( ObjectName ) - - local t = self.Set[ObjectName] - - self:E( { ObjectName, t } ) - - if t then - if t._next then - if t._prev then - t._next._prev = t._prev - t._prev._next = t._next - else - -- this was the first node - t._next._prev = nil - self.List._first = t._next - end - elseif t._prev then - -- this was the last node - t._prev._next = nil - self.List._last = t._prev - else - -- this was the only node - self.List._first = nil - self.List._last = nil - end - - t._next = nil - t._prev = nil - self.List.Count = self.List.Count - 1 - - self.Set[ObjectName] = nil - end - -end - ---- Gets a @{Base#BASE} object from the @{Set#SET_BASE} and derived classes, based on the Object Name. --- @param #SET_BASE self --- @param #string ObjectName --- @return Core.Base#BASE -function SET_BASE:Get( ObjectName ) - self:F( ObjectName ) - - local t = self.Set[ObjectName] - - self:T3( { ObjectName, t } ) - - return t - -end - ---- Retrieves the amount of objects in the @{Set#SET_BASE} and derived classes. --- @param #SET_BASE self --- @return #number Count -function SET_BASE:Count() - - return self.List.Count -end - - - ---- Copies the Filter criteria from a given Set (for rebuilding a new Set based on an existing Set). --- @param #SET_BASE self --- @param #SET_BASE BaseSet --- @return #SET_BASE -function SET_BASE:SetDatabase( BaseSet ) - - -- Copy the filter criteria of the BaseSet - local OtherFilter = routines.utils.deepCopy( BaseSet.Filter ) - self.Filter = OtherFilter - - -- Now base the new Set on the BaseSet - self.Database = BaseSet:GetSet() - return self -end - - - ---- Define the SET iterator **"yield interval"** and the **"time interval"**. --- @param #SET_BASE self --- @param #number YieldInterval Sets the frequency when the iterator loop will yield after the number of objects processed. The default frequency is 10 objects processed. --- @param #number TimeInterval Sets the time in seconds when the main logic will resume the iterator loop. The default time is 0.001 seconds. --- @return #SET_BASE self -function SET_BASE:SetIteratorIntervals( YieldInterval, TimeInterval ) - - self.YieldInterval = YieldInterval - self.TimeInterval = TimeInterval - - return self -end - - ---- Filters for the defined collection. --- @param #SET_BASE self --- @return #SET_BASE self -function SET_BASE:FilterOnce() - - for ObjectName, Object in pairs( self.Database ) do - - if self:IsIncludeObject( Object ) then - self:Add( ObjectName, Object ) - end - end - - return self -end - ---- Starts the filtering for the defined collection. --- @param #SET_BASE self --- @return #SET_BASE self -function SET_BASE:_FilterStart() - - for ObjectName, Object in pairs( self.Database ) do - - if self:IsIncludeObject( Object ) then - self:E( { "Adding Object:", ObjectName } ) - self:Add( ObjectName, Object ) - end - end - - self:HandleEvent( EVENTS.Birth, self._EventOnBirth ) - self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) - self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) - - -- Follow alive players and clients - self:HandleEvent( EVENTS.PlayerEnterUnit, self._EventOnPlayerEnterUnit ) - self:HandleEvent( EVENTS.PlayerLeaveUnit, self._EventOnPlayerLeaveUnit ) - - - return self -end - ---- Stops the filtering for the defined collection. --- @param #SET_BASE self --- @return #SET_BASE self -function SET_BASE:FilterStop() - - self:UnHandleEvent( EVENTS.Birth ) - self:UnHandleEvent( EVENTS.Dead ) - self:UnHandleEvent( EVENTS.Crash ) - - return self -end - ---- Iterate the SET_BASE while identifying the nearest object from a @{Point#POINT_VEC2}. --- @param #SET_BASE self --- @param Core.Point#POINT_VEC2 PointVec2 A @{Point#POINT_VEC2} object from where to evaluate the closest object in the set. --- @return Core.Base#BASE The closest object. -function SET_BASE:FindNearestObjectFromPointVec2( PointVec2 ) - self:F2( PointVec2 ) - - local NearestObject = nil - local ClosestDistance = nil - - for ObjectID, ObjectData in pairs( self.Set ) do - if NearestObject == nil then - NearestObject = ObjectData - ClosestDistance = PointVec2:DistanceFromVec2( ObjectData:GetVec2() ) - else - local Distance = PointVec2:DistanceFromVec2( ObjectData:GetVec2() ) - if Distance < ClosestDistance then - NearestObject = ObjectData - ClosestDistance = Distance - end - end - end - - return NearestObject -end - - - ------ Private method that registers all alive players in the mission. ----- @param #SET_BASE self ----- @return #SET_BASE self ---function SET_BASE:_RegisterPlayers() --- --- local CoalitionsData = { AlivePlayersRed = coalition.getPlayers( coalition.side.RED ), AlivePlayersBlue = coalition.getPlayers( coalition.side.BLUE ) } --- for CoalitionId, CoalitionData in pairs( CoalitionsData ) do --- for UnitId, UnitData in pairs( CoalitionData ) do --- self:T3( { "UnitData:", UnitData } ) --- if UnitData and UnitData:isExist() then --- local UnitName = UnitData:getName() --- if not self.PlayersAlive[UnitName] then --- self:E( { "Add player for unit:", UnitName, UnitData:getPlayerName() } ) --- self.PlayersAlive[UnitName] = UnitData:getPlayerName() --- end --- end --- end --- end --- --- return self ---end - ---- Events - ---- Handles the OnBirth event for the Set. --- @param #SET_BASE self --- @param Core.Event#EVENTDATA Event -function SET_BASE:_EventOnBirth( Event ) - self:F3( { Event } ) - - if Event.IniDCSUnit then - local ObjectName, Object = self:AddInDatabase( Event ) - self:T3( ObjectName, Object ) - if Object and self:IsIncludeObject( Object ) then - self:Add( ObjectName, Object ) - --self:_EventOnPlayerEnterUnit( Event ) - end - end -end - ---- Handles the OnDead or OnCrash event for alive units set. --- @param #SET_BASE self --- @param Core.Event#EVENTDATA Event -function SET_BASE:_EventOnDeadOrCrash( Event ) - self:F3( { Event } ) - - if Event.IniDCSUnit then - local ObjectName, Object = self:FindInDatabase( Event ) - if ObjectName and Object ~= nil then - self:Remove( ObjectName ) - end - end -end - ---- Handles the OnPlayerEnterUnit event to fill the active players table (with the unit filter applied). --- @param #SET_BASE self --- @param Core.Event#EVENTDATA Event -function SET_BASE:_EventOnPlayerEnterUnit( Event ) - self:F3( { Event } ) - - if Event.IniDCSUnit then - local ObjectName, Object = self:AddInDatabase( Event ) - self:T3( ObjectName, Object ) - if self:IsIncludeObject( Object ) then - self:Add( ObjectName, Object ) - --self:_EventOnPlayerEnterUnit( Event ) - end - end -end - ---- Handles the OnPlayerLeaveUnit event to clean the active players table. --- @param #SET_BASE self --- @param Core.Event#EVENTDATA Event -function SET_BASE:_EventOnPlayerLeaveUnit( Event ) - self:F3( { Event } ) - - local ObjectName = Event.IniDCSUnit - if Event.IniDCSUnit then - if Event.IniDCSGroup then - local GroupUnits = Event.IniDCSGroup:getUnits() - local PlayerCount = 0 - for _, DCSUnit in pairs( GroupUnits ) do - if DCSUnit ~= Event.IniDCSUnit then - if DCSUnit:getPlayer() ~= nil then - PlayerCount = PlayerCount + 1 - end - end - end - self:E(PlayerCount) - if PlayerCount == 0 then - self:Remove( Event.IniDCSGroupName ) - end - end - end -end - --- Iterators - ---- Iterate the SET_BASE and derived classes and call an iterator function for the given SET_BASE, providing the Object for each element within the set and optional parameters. --- @param #SET_BASE self --- @param #function IteratorFunction The function that will be called. --- @return #SET_BASE self -function SET_BASE:ForEach( IteratorFunction, arg, Set, Function, FunctionArguments ) - self:F3( arg ) - - Set = Set or self:GetSet() - arg = arg or {} - - local function CoRoutine() - local Count = 0 - for ObjectID, ObjectData in pairs( Set ) do - local Object = ObjectData - self:T3( Object ) - if Function then - if Function( unpack( FunctionArguments ), Object ) == true then - IteratorFunction( Object, unpack( arg ) ) - end - else - IteratorFunction( Object, unpack( arg ) ) - end - Count = Count + 1 --- if Count % self.YieldInterval == 0 then --- coroutine.yield( false ) --- end - end - return true - end - --- local co = coroutine.create( CoRoutine ) - local co = CoRoutine - - local function Schedule() - --- local status, res = coroutine.resume( co ) - local status, res = co() - self:T3( { status, res } ) - - if status == false then - error( res ) - end - if res == false then - return true -- resume next time the loop - end - - return false - end - - self.CallScheduler:Schedule( self, Schedule, {}, self.TimeInterval, self.TimeInterval, 0 ) - - 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 debugging 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:E( { "Objects in Set:", ObjectNames } ) - - return ObjectNames -end - --- SET_GROUP - ---- SET_GROUP class --- @type SET_GROUP --- @extends #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 Core.Set#SET_GROUP self --- @param #string AddGroupNames A single name or an array of GROUP names. --- @return self -function SET_GROUP:AddGroupsByName( AddGroupNames ) - - local AddGroupNamesArray = ( type( AddGroupNames ) == "table" ) and AddGroupNames or { AddGroupNames } - - for AddGroupID, AddGroupName in pairs( AddGroupNamesArray ) do - self:Add( AddGroupName, GROUP:FindByName( AddGroupName ) ) - end - - return self -end - ---- Remove GROUP(s) from SET_GROUP. --- @param Core.Set#SET_GROUP self --- @param Wrapper.Group#GROUP RemoveGroupNames A single name or an array of GROUP names. --- @return self -function SET_GROUP:RemoveGroupsByName( RemoveGroupNames ) - - local RemoveGroupNamesArray = ( type( RemoveGroupNames ) == "table" ) and RemoveGroupNames or { RemoveGroupNames } - - for RemoveGroupID, RemoveGroupName in pairs( RemoveGroupNamesArray ) do - self:Remove( RemoveGroupName.GroupName ) - end - - return self -end - - - - ---- Finds a Group based on the Group Name. --- @param #SET_GROUP self --- @param #string GroupName --- @return Wrapper.Group#GROUP The found Group. -function SET_GROUP:FindGroup( GroupName ) - - local GroupFound = self.Set[GroupName] - return GroupFound -end - - - ---- 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 Core.Event#EVENTDATA Event --- @return #string The name of the GROUP --- @return #table The GROUP -function SET_GROUP:AddInDatabase( Event ) - self:F3( { Event } ) - - if Event.IniObjectCategory == 1 then - if not self.Database[Event.IniDCSGroupName] then - self.Database[Event.IniDCSGroupName] = GROUP:Register( Event.IniDCSGroupName ) - self:T3( self.Database[Event.IniDCSGroupName] ) - end - end - - return Event.IniDCSGroupName, self.Database[Event.IniDCSGroupName] -end - ---- Handles the Database to check on any event that Object exists in the Database. --- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! --- @param #SET_GROUP self --- @param Core.Event#EVENTDATA Event --- @return #string The name of the GROUP --- @return #table The GROUP -function SET_GROUP:FindInDatabase( Event ) - self:F3( { Event } ) - - return Event.IniDCSGroupName, self.Database[Event.IniDCSGroupName] -end - ---- Iterate the SET_GROUP and call an iterator function for each **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 Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. --- @return #SET_GROUP self -function SET_GROUP:ForEachGroupCompletelyInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set, - --- @param Core.Zone#ZONE_BASE ZoneObject - -- @param Wrapper.Group#GROUP GroupObject - function( ZoneObject, GroupObject ) - if GroupObject:IsCompletelyInZone( ZoneObject ) then - return true - else - return false - end - end, { ZoneObject } ) - - return self -end - ---- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence partly in a @{Zone}, providing the GROUP and optional parameters to the called function. --- @param #SET_GROUP self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. --- @return #SET_GROUP self -function SET_GROUP:ForEachGroupPartlyInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set, - --- @param Core.Zone#ZONE_BASE ZoneObject - -- @param Wrapper.Group#GROUP GroupObject - function( ZoneObject, GroupObject ) - if GroupObject:IsPartlyInZone( ZoneObject ) then - return true - else - return false - end - end, { ZoneObject } ) - - return self -end - ---- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence not in a @{Zone}, providing the GROUP and optional parameters to the called function. --- @param #SET_GROUP self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. --- @return #SET_GROUP self -function SET_GROUP:ForEachGroupNotInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set, - --- @param Core.Zone#ZONE_BASE ZoneObject - -- @param Wrapper.Group#GROUP GroupObject - function( ZoneObject, GroupObject ) - if GroupObject:IsNotInZone( ZoneObject ) then - return true - else - return false - end - end, { ZoneObject } ) - - return self -end - - ------ Iterate the SET_GROUP and call an interator function for each **alive** player, providing the Group of the player and optional parameters. ----- @param #SET_GROUP self ----- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_GROUP. The function needs to accept a GROUP parameter. ----- @return #SET_GROUP self ---function SET_GROUP:ForEachPlayer( IteratorFunction, ... ) --- self:F2( arg ) --- --- self:ForEach( IteratorFunction, arg, self.PlayersAlive ) --- --- return self ---end --- --- ------ Iterate the SET_GROUP and call an interator function for each client, providing the Client to the function and optional parameters. ----- @param #SET_GROUP self ----- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_GROUP. The function needs to accept a CLIENT parameter. ----- @return #SET_GROUP self ---function SET_GROUP:ForEachClient( IteratorFunction, ... ) --- self:F2( arg ) --- --- self:ForEach( IteratorFunction, arg, self.Clients ) --- --- return self ---end - - ---- --- @param #SET_GROUP self --- @param Wrapper.Group#GROUP MooseGroup --- @return #SET_GROUP self -function SET_GROUP:IsIncludeObject( MooseGroup ) - self:F2( MooseGroup ) - local MooseGroupInclude = true - - if self.Filter.Coalitions then - local MooseGroupCoalition = false - for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do - self:T3( { "Coalition:", MooseGroup:GetCoalition(), self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) - if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == MooseGroup:GetCoalition() then - MooseGroupCoalition = true - 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 Core.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 ) ) - - 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 Core.Set#SET_UNIT self --- @param Wrapper.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 ) - end - - return self -end - - ---- Finds a Unit based on the Unit Name. --- @param #SET_UNIT self --- @param #string UnitName --- @return Wrapper.Unit#UNIT The found Unit. -function SET_UNIT:FindUnit( UnitName ) - - local UnitFound = self.Set[UnitName] - return UnitFound -end - - - ---- Builds a set of units of coalitions. --- Possible current coalitions are red, blue and neutral. --- @param #SET_UNIT self --- @param #string Coalitions Can take the following values: "red", "blue", "neutral". --- @return #SET_UNIT self -function SET_UNIT:FilterCoalitions( Coalitions ) - 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 - ---- Builds a set of units having a radar of give types. --- All the units having a radar of a given type will be included within the set. --- @param #SET_UNIT self --- @param #table RadarTypes The radar types. --- @return #SET_UNIT self -function SET_UNIT:FilterHasRadar( RadarTypes ) - - self.Filter.RadarTypes = self.Filter.RadarTypes or {} - if type( RadarTypes ) ~= "table" then - RadarTypes = { RadarTypes } - end - for RadarTypeID, RadarType in pairs( RadarTypes ) do - self.Filter.RadarTypes[RadarType] = RadarType - end - return self -end - ---- Builds a set of SEADable units. --- @param #SET_UNIT self --- @return #SET_UNIT self -function SET_UNIT:FilterHasSEAD() - - self.Filter.SEAD = true - return self -end - - - ---- 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 Core.Event#EVENTDATA Event --- @return #string The name of the UNIT --- @return #table The UNIT -function SET_UNIT:AddInDatabase( Event ) - self:F3( { Event } ) - - if Event.IniObjectCategory == 1 then - if not self.Database[Event.IniDCSUnitName] then - self.Database[Event.IniDCSUnitName] = UNIT:Register( Event.IniDCSUnitName ) - self:T3( self.Database[Event.IniDCSUnitName] ) - end - end - - return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] -end - ---- Handles the Database to check on any event that Object exists in the Database. --- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! --- @param #SET_UNIT self --- @param Core.Event#EVENTDATA Event --- @return #string The name of the UNIT --- @return #table The UNIT -function SET_UNIT:FindInDatabase( Event ) - self:E( { Event.IniDCSUnitName, self.Set[Event.IniDCSUnitName], Event } ) - - - return Event.IniDCSUnitName, self.Set[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 Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the SET_UNIT. The function needs to accept a UNIT parameter. --- @return #SET_UNIT self -function SET_UNIT:ForEachUnitCompletelyInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set, - --- @param Core.Zone#ZONE_BASE ZoneObject - -- @param Wrapper.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 Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the SET_UNIT. The function needs to accept a UNIT parameter. --- @return #SET_UNIT self -function SET_UNIT:ForEachUnitNotInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set, - --- @param Core.Zone#ZONE_BASE ZoneObject - -- @param Wrapper.Unit#UNIT UnitObject - function( ZoneObject, UnitObject ) - if UnitObject:IsNotInZone( ZoneObject ) then - return true - else - return false - end - end, { ZoneObject } ) - - return self -end - ---- Returns map of unit types. --- @param #SET_UNIT self --- @return #map<#string,#number> A map of the unit types found. The key is the UnitTypeName and the value is the amount of unit types found. -function SET_UNIT:GetUnitTypes() - self:F2() - - local MT = {} -- Message Text - local UnitTypes = {} - - for UnitID, UnitData in pairs( self:GetSet() ) do - local TextUnit = UnitData -- Wrapper.Unit#UNIT - if TextUnit:IsAlive() then - local UnitType = TextUnit:GetTypeName() - - if not UnitTypes[UnitType] then - UnitTypes[UnitType] = 1 - else - UnitTypes[UnitType] = UnitTypes[UnitType] + 1 - end - end - end - - for UnitTypeID, UnitType in pairs( UnitTypes ) do - MT[#MT+1] = UnitType .. " of " .. UnitTypeID - end - - return UnitTypes -end - - ---- Returns a comma separated string of the unit types with a count in the @{Set}. --- @param #SET_UNIT self --- @return #string The unit types string -function SET_UNIT:GetUnitTypesText() - self:F2() - - local MT = {} -- Message Text - local UnitTypes = self:GetUnitTypes() - - for UnitTypeID, UnitType in pairs( UnitTypes ) do - MT[#MT+1] = UnitType .. " of " .. UnitTypeID - end - - return table.concat( MT, ", " ) -end - ---- Returns map of unit threat levels. --- @param #SET_UNIT self --- @return #table. -function SET_UNIT:GetUnitThreatLevels() - self:F2() - - local UnitThreatLevels = {} - - for UnitID, UnitData in pairs( self:GetSet() ) do - local ThreatUnit = UnitData -- Wrapper.Unit#UNIT - if ThreatUnit:IsAlive() then - local UnitThreatLevel, UnitThreatLevelText = ThreatUnit:GetThreatLevel() - local ThreatUnitName = ThreatUnit:GetName() - - UnitThreatLevels[UnitThreatLevel] = UnitThreatLevels[UnitThreatLevel] or {} - UnitThreatLevels[UnitThreatLevel].UnitThreatLevelText = UnitThreatLevelText - UnitThreatLevels[UnitThreatLevel].Units = UnitThreatLevels[UnitThreatLevel].Units or {} - UnitThreatLevels[UnitThreatLevel].Units[ThreatUnitName] = ThreatUnit - end - end - - return UnitThreatLevels -end - ---- Calculate the maxium A2G threat level of the SET_UNIT. --- @param #SET_UNIT self -function SET_UNIT:CalculateThreatLevelA2G() - - local MaxThreatLevelA2G = 0 - for UnitName, UnitData in pairs( self:GetSet() ) do - local ThreatUnit = UnitData -- Wrapper.Unit#UNIT - local ThreatLevelA2G = ThreatUnit:GetThreatLevel() - if ThreatLevelA2G > MaxThreatLevelA2G then - MaxThreatLevelA2G = ThreatLevelA2G - end - end - - self:T3( MaxThreatLevelA2G ) - return MaxThreatLevelA2G - -end - - ---- Returns if the @{Set} has targets having a radar (of a given type). --- @param #SET_UNIT self --- @param Dcs.DCSWrapper.Unit#Unit.RadarType RadarType --- @return #number The amount of radars in the Set with the given type -function SET_UNIT:HasRadar( RadarType ) - self:F2( RadarType ) - - local RadarCount = 0 - for UnitID, UnitData in pairs( self:GetSet()) do - local UnitSensorTest = UnitData -- Wrapper.Unit#UNIT - local HasSensors - if RadarType then - HasSensors = UnitSensorTest:HasSensors( Unit.SensorType.RADAR, RadarType ) - else - HasSensors = UnitSensorTest:HasSensors( Unit.SensorType.RADAR ) - end - self:T3(HasSensors) - if HasSensors then - RadarCount = RadarCount + 1 - end - end - - return RadarCount -end - ---- Returns if the @{Set} has targets that can be SEADed. --- @param #SET_UNIT self --- @return #number The amount of SEADable units in the Set -function SET_UNIT:HasSEAD() - self:F2() - - local SEADCount = 0 - for UnitID, UnitData in pairs( self:GetSet()) do - local UnitSEAD = UnitData -- Wrapper.Unit#UNIT - if UnitSEAD:IsAlive() then - local UnitSEADAttributes = UnitSEAD:GetDesc().attributes - - local HasSEAD = UnitSEAD:HasSEAD() - - self:T3(HasSEAD) - if HasSEAD then - SEADCount = SEADCount + 1 - end - end - end - - return SEADCount -end - ---- Returns if the @{Set} has ground targets. --- @param #SET_UNIT self --- @return #number The amount of ground targets in the Set. -function SET_UNIT:HasGroundUnits() - self:F2() - - local GroundUnitCount = 0 - for UnitID, UnitData in pairs( self:GetSet()) do - local UnitTest = UnitData -- Wrapper.Unit#UNIT - if UnitTest:IsGround() then - GroundUnitCount = GroundUnitCount + 1 - end - end - - return GroundUnitCount -end - ---- Returns if the @{Set} has friendly ground units. --- @param #SET_UNIT self --- @return #number The amount of ground targets in the Set. -function SET_UNIT:HasFriendlyUnits( FriendlyCoalition ) - self:F2() - - local FriendlyUnitCount = 0 - for UnitID, UnitData in pairs( self:GetSet()) do - local UnitTest = UnitData -- Wrapper.Unit#UNIT - if UnitTest:IsFriendly( FriendlyCoalition ) then - FriendlyUnitCount = FriendlyUnitCount + 1 - end - end - - return FriendlyUnitCount -end - - - ------ Iterate the SET_UNIT and call an interator function for each **alive** player, providing the Unit of the player and optional parameters. ----- @param #SET_UNIT self ----- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_UNIT. The function needs to accept a UNIT parameter. ----- @return #SET_UNIT self ---function SET_UNIT:ForEachPlayer( IteratorFunction, ... ) --- self:F2( arg ) --- --- self:ForEach( IteratorFunction, arg, self.PlayersAlive ) --- --- return self ---end --- --- ------ Iterate the SET_UNIT and call an interator function for each client, providing the Client to the function and optional parameters. ----- @param #SET_UNIT self ----- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_UNIT. The function needs to accept a CLIENT parameter. ----- @return #SET_UNIT self ---function SET_UNIT:ForEachClient( IteratorFunction, ... ) --- self:F2( arg ) --- --- self:ForEach( IteratorFunction, arg, self.Clients ) --- --- return self ---end - - ---- --- @param #SET_UNIT self --- @param Wrapper.Unit#UNIT MUnit --- @return #SET_UNIT self -function SET_UNIT:IsIncludeObject( MUnit ) - self:F2( MUnit ) - local MUnitInclude = 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 - - if self.Filter.RadarTypes then - local MUnitRadar = false - for RadarTypeID, RadarType in pairs( self.Filter.RadarTypes ) do - self:T3( { "Radar:", RadarType } ) - if MUnit:HasSensors( Unit.SensorType.RADAR, RadarType ) == true then - if MUnit:GetRadar() == true then -- This call is necessary to evaluate the SEAD capability. - self:T3( "RADAR Found" ) - end - MUnitRadar = true - end - end - MUnitInclude = MUnitInclude and MUnitRadar - end - - if self.Filter.SEAD then - local MUnitSEAD = false - if MUnit:HasSEAD() == true then - self:T3( "SEAD Found" ) - MUnitSEAD = true - end - MUnitInclude = MUnitInclude and MUnitSEAD - end - - self:T2( MUnitInclude ) - return MUnitInclude -end - - ---- SET_CLIENT - ---- SET_CLIENT class --- @type SET_CLIENT --- @extends Core.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 Core.Set#SET_CLIENT self --- @param #string AddClientNames A single name or an array of CLIENT names. --- @return self -function SET_CLIENT:AddClientsByName( AddClientNames ) - - local AddClientNamesArray = ( type( AddClientNames ) == "table" ) and AddClientNames or { AddClientNames } - - for AddClientID, AddClientName in pairs( AddClientNamesArray ) do - self:Add( AddClientName, CLIENT:FindByName( AddClientName ) ) - end - - return self -end - ---- Remove CLIENT(s) from SET_CLIENT. --- @param Core.Set#SET_CLIENT self --- @param Wrapper.Client#CLIENT RemoveClientNames A single name or an array of CLIENT names. --- @return self -function SET_CLIENT:RemoveClientsByName( RemoveClientNames ) - - local RemoveClientNamesArray = ( type( RemoveClientNames ) == "table" ) and RemoveClientNames or { RemoveClientNames } - - for RemoveClientID, RemoveClientName in pairs( RemoveClientNamesArray ) do - self:Remove( RemoveClientName.ClientName ) - end - - return self -end - - ---- Finds a Client based on the Client Name. --- @param #SET_CLIENT self --- @param #string ClientName --- @return Wrapper.Client#CLIENT The found Client. -function SET_CLIENT:FindClient( ClientName ) - - local ClientFound = self.Set[ClientName] - return ClientFound -end - - - ---- Builds a set of clients of coalitions. --- Possible current coalitions are red, blue and neutral. --- @param #SET_CLIENT self --- @param #string Coalitions Can take the following values: "red", "blue", "neutral". --- @return #SET_CLIENT self -function SET_CLIENT:FilterCoalitions( Coalitions ) - if not self.Filter.Coalitions then - self.Filter.Coalitions = {} - end - if type( Coalitions ) ~= "table" then - Coalitions = { Coalitions } - end - for CoalitionID, Coalition in pairs( Coalitions ) do - self.Filter.Coalitions[Coalition] = Coalition - end - return self -end - - ---- Builds a set of clients out of categories. --- Possible current categories are plane, helicopter, ground, ship. --- @param #SET_CLIENT self --- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship". --- @return #SET_CLIENT self -function SET_CLIENT:FilterCategories( Categories ) - if not self.Filter.Categories then - self.Filter.Categories = {} - end - if type( Categories ) ~= "table" then - Categories = { Categories } - end - for CategoryID, Category in pairs( Categories ) do - self.Filter.Categories[Category] = Category - end - return self -end - - ---- Builds a set of clients of defined client types. --- Possible current types are those types known within DCS world. --- @param #SET_CLIENT self --- @param #string Types Can take those type strings known within DCS world. --- @return #SET_CLIENT self -function SET_CLIENT:FilterTypes( Types ) - if not self.Filter.Types then - self.Filter.Types = {} - end - if type( Types ) ~= "table" then - Types = { Types } - end - for TypeID, Type in pairs( Types ) do - self.Filter.Types[Type] = Type - end - return self -end - - ---- Builds a set of clients of defined countries. --- Possible current countries are those known within DCS world. --- @param #SET_CLIENT self --- @param #string Countries Can take those country strings known within DCS world. --- @return #SET_CLIENT self -function SET_CLIENT:FilterCountries( Countries ) - if not self.Filter.Countries then - self.Filter.Countries = {} - end - if type( Countries ) ~= "table" then - Countries = { Countries } - end - for CountryID, Country in pairs( Countries ) do - self.Filter.Countries[Country] = Country - end - return self -end - - ---- Builds a set of clients 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 Core.Event#EVENTDATA Event --- @return #string The name of the CLIENT --- @return #table The CLIENT -function SET_CLIENT:AddInDatabase( Event ) - self:F3( { Event } ) - - return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] -end - ---- Handles the Database to check on any event that Object exists in the Database. --- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! --- @param #SET_CLIENT self --- @param Core.Event#EVENTDATA Event --- @return #string The name of the CLIENT --- @return #table The CLIENT -function SET_CLIENT:FindInDatabase( Event ) - self:F3( { Event } ) - - return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] -end - ---- Iterate the SET_CLIENT and call an interator function for each **alive** CLIENT, providing the CLIENT and optional parameters. --- @param #SET_CLIENT self --- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_CLIENT. The function needs to accept a CLIENT parameter. --- @return #SET_CLIENT self -function SET_CLIENT:ForEachClient( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.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 Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_CLIENT. The function needs to accept a CLIENT parameter. --- @return #SET_CLIENT self -function SET_CLIENT:ForEachClientInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set, - --- @param Core.Zone#ZONE_BASE ZoneObject - -- @param Wrapper.Client#CLIENT ClientObject - function( ZoneObject, ClientObject ) - if ClientObject:IsInZone( ZoneObject ) then - return true - else - return false - end - end, { ZoneObject } ) - - return self -end - ---- Iterate the SET_CLIENT and call an iterator function for each **alive** CLIENT presence not in a @{Zone}, providing the CLIENT and optional parameters to the called function. --- @param #SET_CLIENT self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_CLIENT. The function needs to accept a CLIENT parameter. --- @return #SET_CLIENT self -function SET_CLIENT:ForEachClientNotInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set, - --- @param Core.Zone#ZONE_BASE ZoneObject - -- @param Wrapper.Client#CLIENT ClientObject - function( ZoneObject, ClientObject ) - if ClientObject:IsNotInZone( ZoneObject ) then - return true - else - return false - end - end, { ZoneObject } ) - - return self -end - ---- --- @param #SET_CLIENT self --- @param Wrapper.Client#CLIENT MClient --- @return #SET_CLIENT self -function SET_CLIENT:IsIncludeObject( MClient ) - self:F2( MClient ) - - local MClientInclude = true - - if MClient then - local MClientName = MClient.UnitName - - if self.Filter.Coalitions then - local MClientCoalition = false - for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do - local ClientCoalitionID = _DATABASE:GetCoalitionFromClientTemplate( MClientName ) - self:T3( { "Coalition:", ClientCoalitionID, self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) - if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == ClientCoalitionID then - MClientCoalition = true - end - end - self:T( { "Evaluated Coalition", MClientCoalition } ) - MClientInclude = MClientInclude and MClientCoalition - end - - if self.Filter.Categories then - local MClientCategory = false - for CategoryID, CategoryName in pairs( self.Filter.Categories ) do - local ClientCategoryID = _DATABASE:GetCategoryFromClientTemplate( MClientName ) - self:T3( { "Category:", ClientCategoryID, self.FilterMeta.Categories[CategoryName], CategoryName } ) - if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == ClientCategoryID then - MClientCategory = true - end - end - self:T( { "Evaluated Category", MClientCategory } ) - MClientInclude = MClientInclude and MClientCategory - end - - if self.Filter.Types then - local MClientType = false - for TypeID, TypeName in pairs( self.Filter.Types ) do - self:T3( { "Type:", MClient:GetTypeName(), TypeName } ) - if TypeName == MClient:GetTypeName() then - MClientType = true - end - end - self:T( { "Evaluated Type", MClientType } ) - MClientInclude = MClientInclude and MClientType - end - - if self.Filter.Countries then - local MClientCountry = false - for CountryID, CountryName in pairs( self.Filter.Countries ) do - local ClientCountryID = _DATABASE:GetCountryFromClientTemplate(MClientName) - self:T3( { "Country:", ClientCountryID, country.id[CountryName], CountryName } ) - if country.id[CountryName] and country.id[CountryName] == ClientCountryID then - MClientCountry = true - end - end - self:T( { "Evaluated Country", MClientCountry } ) - MClientInclude = MClientInclude and MClientCountry - end - - if self.Filter.ClientPrefixes then - local MClientPrefix = false - for ClientPrefixId, ClientPrefix in pairs( self.Filter.ClientPrefixes ) do - self:T3( { "Prefix:", string.find( MClient.UnitName, ClientPrefix, 1 ), ClientPrefix } ) - if string.find( MClient.UnitName, ClientPrefix, 1 ) then - MClientPrefix = true - end - end - self:T( { "Evaluated Prefix", MClientPrefix } ) - MClientInclude = MClientInclude and MClientPrefix - end - end - - self:T2( MClientInclude ) - return MClientInclude -end - ---- SET_AIRBASE - ---- SET_AIRBASE class --- @type SET_AIRBASE --- @extends Core.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 Core.Set#SET_AIRBASE self --- @param #string AddAirbaseNames A single name or an array of AIRBASE names. --- @return self -function SET_AIRBASE:AddAirbasesByName( AddAirbaseNames ) - - local AddAirbaseNamesArray = ( type( AddAirbaseNames ) == "table" ) and AddAirbaseNames or { AddAirbaseNames } - - for AddAirbaseID, AddAirbaseName in pairs( AddAirbaseNamesArray ) do - self:Add( AddAirbaseName, AIRBASE:FindByName( AddAirbaseName ) ) - end - - return self -end - ---- Remove AIRBASEs from SET_AIRBASE. --- @param Core.Set#SET_AIRBASE self --- @param Wrapper.Airbase#AIRBASE RemoveAirbaseNames A single name or an array of AIRBASE names. --- @return self -function SET_AIRBASE:RemoveAirbasesByName( RemoveAirbaseNames ) - - local RemoveAirbaseNamesArray = ( type( RemoveAirbaseNames ) == "table" ) and RemoveAirbaseNames or { RemoveAirbaseNames } - - for RemoveAirbaseID, RemoveAirbaseName in pairs( RemoveAirbaseNamesArray ) do - self:Remove( RemoveAirbaseName.AirbaseName ) - end - - return self -end - - ---- Finds a Airbase based on the Airbase Name. --- @param #SET_AIRBASE self --- @param #string AirbaseName --- @return Wrapper.Airbase#AIRBASE The found Airbase. -function SET_AIRBASE:FindAirbase( AirbaseName ) - - local AirbaseFound = self.Set[AirbaseName] - return AirbaseFound -end - - - ---- 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 Core.Event#EVENTDATA Event --- @return #string The name of the AIRBASE --- @return #table The AIRBASE -function SET_AIRBASE:AddInDatabase( Event ) - self:F3( { Event } ) - - return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] -end - ---- Handles the Database to check on any event that Object exists in the Database. --- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! --- @param #SET_AIRBASE self --- @param Core.Event#EVENTDATA Event --- @return #string The name of the AIRBASE --- @return #table The AIRBASE -function SET_AIRBASE:FindInDatabase( Event ) - self:F3( { Event } ) - - return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] -end - ---- Iterate the SET_AIRBASE and call an interator function for each AIRBASE, providing the AIRBASE and optional parameters. --- @param #SET_AIRBASE self --- @param #function IteratorFunction The function that will be called when there is an alive AIRBASE in the SET_AIRBASE. The function needs to accept a AIRBASE parameter. --- @return #SET_AIRBASE self -function SET_AIRBASE:ForEachAirbase( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set ) - - return self -end - ---- Iterate the SET_AIRBASE while identifying the nearest @{Airbase#AIRBASE} from a @{Point#POINT_VEC2}. --- @param #SET_AIRBASE self --- @param Core.Point#POINT_VEC2 PointVec2 A @{Point#POINT_VEC2} object from where to evaluate the closest @{Airbase#AIRBASE}. --- @return Wrapper.Airbase#AIRBASE The closest @{Airbase#AIRBASE}. -function SET_AIRBASE:FindNearestAirbaseFromPointVec2( PointVec2 ) - self:F2( PointVec2 ) - - local NearestAirbase = self:FindNearestObjectFromPointVec2( PointVec2 ) - return NearestAirbase -end - - - ---- --- @param #SET_AIRBASE self --- @param Wrapper.Airbase#AIRBASE MAirbase --- @return #SET_AIRBASE self -function SET_AIRBASE:IsIncludeObject( MAirbase ) - self:F2( MAirbase ) - - local MAirbaseInclude = true - - if MAirbase then - local MAirbaseName = MAirbase:GetName() - - if self.Filter.Coalitions then - local MAirbaseCoalition = false - for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do - local AirbaseCoalitionID = _DATABASE:GetCoalitionFromAirbase( MAirbaseName ) - self:T3( { "Coalition:", AirbaseCoalitionID, self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) - if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == AirbaseCoalitionID then - MAirbaseCoalition = true - end - end - self:T( { "Evaluated Coalition", MAirbaseCoalition } ) - MAirbaseInclude = MAirbaseInclude and MAirbaseCoalition - end - - if self.Filter.Categories then - local MAirbaseCategory = false - for CategoryID, CategoryName in pairs( self.Filter.Categories ) do - local AirbaseCategoryID = _DATABASE:GetCategoryFromAirbase( MAirbaseName ) - self:T3( { "Category:", AirbaseCategoryID, self.FilterMeta.Categories[CategoryName], CategoryName } ) - if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == AirbaseCategoryID then - MAirbaseCategory = true - end - end - self:T( { "Evaluated Category", MAirbaseCategory } ) - MAirbaseInclude = MAirbaseInclude and MAirbaseCategory - end - end - - self:T2( MAirbaseInclude ) - return MAirbaseInclude -end ---- 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. --- --- **Important Note:** Most of the functions in this section were taken from MIST, and reworked to OO concepts. --- In order to keep the credibility of the the author, I want to emphasize that the of the MIST framework was created by Grimes, who you can find on the Eagle Dynamics Forums. --- --- ## 1.1) POINT_VEC3 constructor --- --- A new POINT_VEC3 instance can be created with: --- --- * @{Point#POINT_VEC3.New}(): a 3D point. --- * @{Point#POINT_VEC3.NewFromVec3}(): a 3D point created from a @{DCSTypes#Vec3}. --- --- ## 1.2) Manupulate the X, Y, Z coordinates of the point --- --- A POINT_VEC3 class works in 3D space. It contains internally an X, Y, Z coordinate. --- Methods exist to manupulate these coordinates. --- --- The current X, Y, Z axis can be retrieved with the methods @{#POINT_VEC3.GetX}(), @{#POINT_VEC3.GetY}(), @{#POINT_VEC3.GetZ}() respectively. --- The methods @{#POINT_VEC3.SetX}(), @{#POINT_VEC3.SetY}(), @{#POINT_VEC3.SetZ}() change the respective axis with a new value. --- The current axis values can be changed by using the methods @{#POINT_VEC3.AddX}(), @{#POINT_VEC3.AddY}(), @{#POINT_VEC3.AddZ}() --- to add or substract a value from the current respective axis value. --- Note that the Set and Add methods return the current POINT_VEC3 object, so these manipulation methods can be chained... For example: --- --- local Vec3 = PointVec3:AddX( 100 ):AddZ( 150 ):GetVec3() --- --- ## 1.3) Create waypoints for routes --- --- A POINT_VEC3 can prepare waypoints for Ground, Air and Naval groups to be embedded into a Route. --- --- --- ## 1.5) Smoke, flare, explode, illuminate --- --- At the point a smoke, flare, explosion and illumination bomb can be triggered. Use the following methods: --- --- ### 1.5.1) Smoke --- --- * @{#POINT_VEC3.Smoke}(): To smoke the point in a certain color. --- * @{#POINT_VEC3.SmokeBlue}(): To smoke the point in blue. --- * @{#POINT_VEC3.SmokeRed}(): To smoke the point in red. --- * @{#POINT_VEC3.SmokeOrange}(): To smoke the point in orange. --- * @{#POINT_VEC3.SmokeWhite}(): To smoke the point in white. --- * @{#POINT_VEC3.SmokeGreen}(): To smoke the point in green. --- --- ### 1.5.2) Flare --- --- * @{#POINT_VEC3.Flare}(): To flare the point in a certain color. --- * @{#POINT_VEC3.FlareRed}(): To flare the point in red. --- * @{#POINT_VEC3.FlareYellow}(): To flare the point in yellow. --- * @{#POINT_VEC3.FlareWhite}(): To flare the point in white. --- * @{#POINT_VEC3.FlareGreen}(): To flare the point in green. --- --- ### 1.5.3) Explode --- --- * @{#POINT_VEC3.Explosion}(): To explode the point with a certain intensity. --- --- ### 1.5.4) Illuminate --- --- * @{#POINT_VEC3.IlluminationBomb}(): To illuminate the 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_VEC2 instance can be created with: --- --- * @{Point#POINT_VEC2.New}(): a 2D point, taking an additional height parameter. --- * @{Point#POINT_VEC2.NewFromVec2}(): a 2D point created from a @{DCSTypes#Vec2}. --- --- ## 1.2) Manupulate the X, Altitude, Y coordinates of the 2D point --- --- A POINT_VEC2 class works in 2D space, with an altitude setting. It contains internally an X, Altitude, Y coordinate. --- Methods exist to manupulate these coordinates. --- --- The current X, Altitude, Y axis can be retrieved with the methods @{#POINT_VEC2.GetX}(), @{#POINT_VEC2.GetAlt}(), @{#POINT_VEC2.GetY}() respectively. --- The methods @{#POINT_VEC2.SetX}(), @{#POINT_VEC2.SetAlt}(), @{#POINT_VEC2.SetY}() change the respective axis with a new value. --- The current axis values can be changed by using the methods @{#POINT_VEC2.AddX}(), @{#POINT_VEC2.AddAlt}(), @{#POINT_VEC2.AddY}() --- to add or substract a value from the current respective axis value. --- Note that the Set and Add methods return the current POINT_VEC2 object, so these manipulation methods can be chained... For example: --- --- local Vec2 = PointVec2:AddX( 100 ):AddY( 2000 ):GetVec2() --- --- === --- --- **API CHANGE HISTORY** --- ====================== --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- Hereby the change log: --- --- 2017-03-03: POINT\_VEC3:**Explosion( ExplosionIntensity )** added. --- 2017-03-03: POINT\_VEC3:**IlluminationBomb()** added. --- --- 2017-02-18: POINT\_VEC3:**NewFromVec2( Vec2, LandHeightAdd )** added. --- --- 2016-08-12: POINT\_VEC3:**Translate( Distance, Angle )** added. --- --- 2016-08-06: Made PointVec3 and Vec3, PointVec2 and Vec2 terminology used in the code consistent. --- --- * Replaced method _Point_Vec3() to **Vec3**() where the code manages a Vec3. Replaced all references to the method. --- * Replaced method _Point_Vec2() to **Vec2**() where the code manages a Vec2. Replaced all references to the method. --- * Replaced method Random_Point_Vec3() to **RandomVec3**() where the code manages a Vec3. Replaced all references to the method. --- . --- === --- --- ### Authors: --- --- * FlightControl : Design & Programming --- --- ### Contributions: --- --- @module Point - ---- The POINT_VEC3 class --- @type POINT_VEC3 --- @field #number x The x coordinate in 3D space. --- @field #number y The y coordinate in 3D space. --- @field #number z The z coordiante in 3D space. --- @field Utilities.Utils#SMOKECOLOR SmokeColor --- @field Utilities.Utils#FLARECOLOR FlareColor --- @field #POINT_VEC3.RoutePointAltType RoutePointAltType --- @field #POINT_VEC3.RoutePointType RoutePointType --- @field #POINT_VEC3.RoutePointAction RoutePointAction --- @extends Core.Base#BASE -POINT_VEC3 = { - ClassName = "POINT_VEC3", - Metric = true, - RoutePointAltType = { - BARO = "BARO", - }, - RoutePointType = { - TakeOffParking = "TakeOffParking", - TurningPoint = "Turning Point", - }, - RoutePointAction = { - FromParkingArea = "From Parking Area", - TurningPoint = "Turning Point", - }, -} - ---- The POINT_VEC2 class --- @type POINT_VEC2 --- @field Dcs.DCSTypes#Distance x The x coordinate in meters. --- @field Dcs.DCSTypes#Distance y the y coordinate in meters. --- @extends Core.Point#POINT_VEC3 -POINT_VEC2 = { - ClassName = "POINT_VEC2", -} - - -do -- POINT_VEC3 - ---- RoutePoint AltTypes --- @type POINT_VEC3.RoutePointAltType --- @field BARO "BARO" - ---- RoutePoint Types --- @type POINT_VEC3.RoutePointType --- @field TakeOffParking "TakeOffParking" --- @field TurningPoint "Turning Point" - ---- RoutePoint Actions --- @type POINT_VEC3.RoutePointAction --- @field FromParkingArea "From Parking Area" --- @field TurningPoint "Turning Point" - --- Constructor. - ---- Create a new POINT_VEC3 object. --- @param #POINT_VEC3 self --- @param Dcs.DCSTypes#Distance x The x coordinate of the Vec3 point, pointing to the North. --- @param Dcs.DCSTypes#Distance y The y coordinate of the Vec3 point, pointing Upwards. --- @param Dcs.DCSTypes#Distance z The z coordinate of the Vec3 point, pointing to the Right. --- @return Core.Point#POINT_VEC3 self -function POINT_VEC3:New( x, y, z ) - - local self = BASE:Inherit( self, BASE:New() ) - self.x = x - self.y = y - self.z = z - - return self -end - ---- Create a new POINT_VEC3 object from Vec2 coordinates. --- @param #POINT_VEC3 self --- @param Dcs.DCSTypes#Vec2 Vec2 The Vec2 point. --- @return Core.Point#POINT_VEC3 self -function POINT_VEC3:NewFromVec2( Vec2, LandHeightAdd ) - - local LandHeight = land.getHeight( Vec2 ) - - LandHeightAdd = LandHeightAdd or 0 - LandHeight = LandHeight + LandHeightAdd - - self = self:New( Vec2.x, LandHeight, Vec2.y ) - - self:F2( self ) - - return self -end - ---- Create a new POINT_VEC3 object from Vec3 coordinates. --- @param #POINT_VEC3 self --- @param Dcs.DCSTypes#Vec3 Vec3 The Vec3 point. --- @return Core.Point#POINT_VEC3 self -function POINT_VEC3:NewFromVec3( Vec3 ) - - self = self:New( Vec3.x, Vec3.y, Vec3.z ) - self:F2( self ) - return self -end - - ---- Return the coordinates of the POINT_VEC3 in Vec3 format. --- @param #POINT_VEC3 self --- @return Dcs.DCSTypes#Vec3 The Vec3 coodinate. -function POINT_VEC3:GetVec3() - return { x = self.x, y = self.y, z = self.z } -end - ---- Return the coordinates of the POINT_VEC3 in Vec2 format. --- @param #POINT_VEC3 self --- @return Dcs.DCSTypes#Vec2 The Vec2 coodinate. -function POINT_VEC3:GetVec2() - return { x = self.x, y = self.z } -end - - ---- Return the x coordinate of the POINT_VEC3. --- @param #POINT_VEC3 self --- @return #number The x coodinate. -function POINT_VEC3:GetX() - return self.x -end - ---- Return the y coordinate of the POINT_VEC3. --- @param #POINT_VEC3 self --- @return #number The y coodinate. -function POINT_VEC3:GetY() - return self.y -end - ---- Return the z coordinate of the POINT_VEC3. --- @param #POINT_VEC3 self --- @return #number The z coodinate. -function POINT_VEC3:GetZ() - return self.z -end - ---- Set the x coordinate of the POINT_VEC3. --- @param #POINT_VEC3 self --- @param #number x The x coordinate. --- @return #POINT_VEC3 -function POINT_VEC3:SetX( x ) - self.x = x - return self -end - ---- Set the y coordinate of the POINT_VEC3. --- @param #POINT_VEC3 self --- @param #number y The y coordinate. --- @return #POINT_VEC3 -function POINT_VEC3:SetY( y ) - self.y = y - return self -end - ---- Set the z coordinate of the POINT_VEC3. --- @param #POINT_VEC3 self --- @param #number z The z coordinate. --- @return #POINT_VEC3 -function POINT_VEC3:SetZ( z ) - self.z = z - return self -end - ---- Add to the x coordinate of the POINT_VEC3. --- @param #POINT_VEC3 self --- @param #number x The x coordinate value to add to the current x coodinate. --- @return #POINT_VEC3 -function POINT_VEC3:AddX( x ) - self.x = self.x + x - return self -end - ---- Add to the y coordinate of the POINT_VEC3. --- @param #POINT_VEC3 self --- @param #number y The y coordinate value to add to the current y coodinate. --- @return #POINT_VEC3 -function POINT_VEC3:AddY( y ) - self.y = self.y + y - return self -end - ---- Add to the z coordinate of the POINT_VEC3. --- @param #POINT_VEC3 self --- @param #number z The z coordinate value to add to the current z coodinate. --- @return #POINT_VEC3 -function POINT_VEC3:AddZ( z ) - self.z = self.z +z - return self -end - ---- Return a random Vec2 within an Outer Radius and optionally NOT within an Inner Radius of the POINT_VEC3. --- @param #POINT_VEC3 self --- @param Dcs.DCSTypes#Distance OuterRadius --- @param Dcs.DCSTypes#Distance InnerRadius --- @return Dcs.DCSTypes#Vec2 Vec2 -function POINT_VEC3:GetRandomVec2InRadius( OuterRadius, InnerRadius ) - self:F2( { OuterRadius, InnerRadius } ) - - local Theta = 2 * math.pi * math.random() - local Radials = math.random() + math.random() - if Radials > 1 then - Radials = 2 - Radials - end - - local RadialMultiplier - if InnerRadius and InnerRadius <= OuterRadius then - RadialMultiplier = ( OuterRadius - InnerRadius ) * Radials + InnerRadius - else - RadialMultiplier = OuterRadius * Radials - end - - local RandomVec2 - if OuterRadius > 0 then - RandomVec2 = { x = math.cos( Theta ) * RadialMultiplier + self:GetX(), y = math.sin( Theta ) * RadialMultiplier + self:GetZ() } - else - RandomVec2 = { x = self:GetX(), y = self:GetZ() } - end - - return RandomVec2 -end - ---- Return a random POINT_VEC2 within an Outer Radius and optionally NOT within an Inner Radius of the POINT_VEC3. --- @param #POINT_VEC3 self --- @param Dcs.DCSTypes#Distance OuterRadius --- @param Dcs.DCSTypes#Distance InnerRadius --- @return #POINT_VEC2 -function POINT_VEC3:GetRandomPointVec2InRadius( OuterRadius, InnerRadius ) - self:F2( { OuterRadius, InnerRadius } ) - - return POINT_VEC2:NewFromVec2( self:GetRandomVec2InRadius( OuterRadius, InnerRadius ) ) -end - ---- Return a random Vec3 within an Outer Radius and optionally NOT within an Inner Radius of the POINT_VEC3. --- @param #POINT_VEC3 self --- @param Dcs.DCSTypes#Distance OuterRadius --- @param Dcs.DCSTypes#Distance InnerRadius --- @return Dcs.DCSTypes#Vec3 Vec3 -function POINT_VEC3:GetRandomVec3InRadius( OuterRadius, InnerRadius ) - - local RandomVec2 = self:GetRandomVec2InRadius( OuterRadius, InnerRadius ) - local y = self:GetY() + math.random( InnerRadius, OuterRadius ) - local RandomVec3 = { x = RandomVec2.x, y = y, z = RandomVec2.z } - - return RandomVec3 -end - ---- Return a random POINT_VEC3 within an Outer Radius and optionally NOT within an Inner Radius of the POINT_VEC3. --- @param #POINT_VEC3 self --- @param Dcs.DCSTypes#Distance OuterRadius --- @param Dcs.DCSTypes#Distance InnerRadius --- @return #POINT_VEC3 -function POINT_VEC3:GetRandomPointVec3InRadius( OuterRadius, InnerRadius ) - - return POINT_VEC3:NewFromVec3( self:GetRandomVec3InRadius( OuterRadius, InnerRadius ) ) -end - - ---- Return a direction vector Vec3 from POINT_VEC3 to the POINT_VEC3. --- @param #POINT_VEC3 self --- @param #POINT_VEC3 TargetPointVec3 The target POINT_VEC3. --- @return Dcs.DCSTypes#Vec3 DirectionVec3 The direction vector in Vec3 format. -function POINT_VEC3:GetDirectionVec3( TargetPointVec3 ) - return { x = TargetPointVec3:GetX() - self:GetX(), y = TargetPointVec3:GetY() - self:GetY(), z = TargetPointVec3:GetZ() - self:GetZ() } -end - ---- Get a correction in radians of the real magnetic north of the POINT_VEC3. --- @param #POINT_VEC3 self --- @return #number CorrectionRadians The correction in radians. -function POINT_VEC3:GetNorthCorrectionRadians() - local TargetVec3 = self:GetVec3() - local lat, lon = coord.LOtoLL(TargetVec3) - local north_posit = coord.LLtoLO(lat + 1, lon) - return math.atan2( north_posit.z - TargetVec3.z, north_posit.x - TargetVec3.x ) -end - - ---- Return a direction in radians from the POINT_VEC3 using a direction vector in Vec3 format. --- @param #POINT_VEC3 self --- @param Dcs.DCSTypes#Vec3 DirectionVec3 The direction vector in Vec3 format. --- @return #number DirectionRadians The direction in radians. -function POINT_VEC3:GetDirectionRadians( DirectionVec3 ) - local DirectionRadians = math.atan2( DirectionVec3.z, DirectionVec3.x ) - --DirectionRadians = DirectionRadians + self:GetNorthCorrectionRadians() - if DirectionRadians < 0 then - DirectionRadians = DirectionRadians + 2 * math.pi -- put dir in range of 0 to 2*pi ( the full circle ) - end - return DirectionRadians -end - ---- Return the 2D distance in meters between the target POINT_VEC3 and the POINT_VEC3. --- @param #POINT_VEC3 self --- @param #POINT_VEC3 TargetPointVec3 The target POINT_VEC3. --- @return Dcs.DCSTypes#Distance Distance The distance in meters. -function POINT_VEC3:Get2DDistance( TargetPointVec3 ) - local TargetVec3 = TargetPointVec3:GetVec3() - local SourceVec3 = self:GetVec3() - return ( ( TargetVec3.x - SourceVec3.x ) ^ 2 + ( TargetVec3.z - SourceVec3.z ) ^ 2 ) ^ 0.5 -end - ---- Return the 3D distance in meters between the target POINT_VEC3 and the POINT_VEC3. --- @param #POINT_VEC3 self --- @param #POINT_VEC3 TargetPointVec3 The target POINT_VEC3. --- @return Dcs.DCSTypes#Distance Distance The distance in meters. -function POINT_VEC3:Get3DDistance( TargetPointVec3 ) - local TargetVec3 = TargetPointVec3:GetVec3() - local SourceVec3 = self:GetVec3() - return ( ( TargetVec3.x - SourceVec3.x ) ^ 2 + ( TargetVec3.y - SourceVec3.y ) ^ 2 + ( TargetVec3.z - SourceVec3.z ) ^ 2 ) ^ 0.5 -end - ---- Provides a Bearing / Range string --- @param #POINT_VEC3 self --- @param #number AngleRadians The angle in randians --- @param #number Distance The distance --- @return #string The BR Text -function POINT_VEC3:ToStringBR( AngleRadians, Distance ) - - AngleRadians = UTILS.Round( UTILS.ToDegree( AngleRadians ), 0 ) - if self:IsMetric() then - Distance = UTILS.Round( Distance / 1000, 2 ) - else - Distance = UTILS.Round( UTILS.MetersToNM( Distance ), 2 ) - end - - local s = string.format( '%03d', AngleRadians ) .. ' for ' .. Distance - - s = s .. self:GetAltitudeText() -- When the POINT is a VEC2, there will be no altitude shown. - - return s -end - ---- Provides a Bearing / Range string --- @param #POINT_VEC3 self --- @param #number AngleRadians The angle in randians --- @param #number Distance The distance --- @return #string The BR Text -function POINT_VEC3:ToStringLL( acc, DMS ) - - acc = acc or 3 - local lat, lon = coord.LOtoLL( self:GetVec3() ) - return UTILS.tostringLL(lat, lon, acc, DMS) -end - ---- Return the altitude text of the POINT_VEC3. --- @param #POINT_VEC3 self --- @return #string Altitude text. -function POINT_VEC3:GetAltitudeText() - if self:IsMetric() then - return ' at ' .. UTILS.Round( self:GetY(), 0 ) - else - return ' at ' .. UTILS.Round( UTILS.MetersToFeet( self:GetY() ), 0 ) - end -end - ---- Return a BR string from a POINT_VEC3 to the POINT_VEC3. --- @param #POINT_VEC3 self --- @param #POINT_VEC3 TargetPointVec3 The target POINT_VEC3. --- @return #string The BR text. -function POINT_VEC3:GetBRText( TargetPointVec3 ) - local DirectionVec3 = self:GetDirectionVec3( TargetPointVec3 ) - local AngleRadians = self:GetDirectionRadians( DirectionVec3 ) - local Distance = self:Get2DDistance( TargetPointVec3 ) - return self:ToStringBR( AngleRadians, Distance ) -end - ---- Sets the POINT_VEC3 metric or NM. --- @param #POINT_VEC3 self --- @param #boolean Metric true means metric, false means NM. -function POINT_VEC3:SetMetric( Metric ) - self.Metric = Metric -end - ---- Gets if the POINT_VEC3 is metric or NM. --- @param #POINT_VEC3 self --- @return #boolean Metric true means metric, false means NM. -function POINT_VEC3:IsMetric() - return self.Metric -end - ---- Add a Distance in meters from the POINT_VEC3 horizontal plane, with the given angle, and calculate the new POINT_VEC3. --- @param #POINT_VEC3 self --- @param Dcs.DCSTypes#Distance Distance The Distance to be added in meters. --- @param Dcs.DCSTypes#Angle Angle The Angle in degrees. --- @return #POINT_VEC3 The new calculated POINT_VEC3. -function POINT_VEC3:Translate( Distance, Angle ) - local SX = self:GetX() - local SZ = self:GetZ() - local Radians = Angle / 180 * math.pi - local TX = Distance * math.cos( Radians ) + SX - local TZ = Distance * math.sin( Radians ) + SZ - - return POINT_VEC3:New( TX, self:GetY(), TZ ) -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 Dcs.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:GetX() - RoutePoint.y = self:GetZ() - RoutePoint.alt = self:GetY() - 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 - ---- Build an ground type route point. --- @param #POINT_VEC3 self --- @param Dcs.DCSTypes#Speed Speed Speed in km/h. --- @param #POINT_VEC3.RoutePointAction Formation The route point Formation. --- @return #table The route point. -function POINT_VEC3:RoutePointGround( Speed, Formation ) - self:F2( { Formation, Speed } ) - - local RoutePoint = {} - RoutePoint.x = self:GetX() - RoutePoint.y = self:GetZ() - - RoutePoint.action = Formation or "" - - - 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 - ---- Creates an explosion at the point of a certain intensity. --- @param #POINT_VEC3 self --- @param #number ExplosionIntensity -function POINT_VEC3:Explosion( ExplosionIntensity ) - self:F2( { ExplosionIntensity } ) - trigger.action.explosion( self:GetVec3(), ExplosionIntensity ) -end - ---- Creates an illumination bomb at the point. --- @param #POINT_VEC3 self -function POINT_VEC3:IlluminationBomb() - self:F2() - trigger.action.illuminationBomb( self:GetVec3() ) -end - - ---- Smokes the point in a color. --- @param #POINT_VEC3 self --- @param Utilities.Utils#SMOKECOLOR SmokeColor -function POINT_VEC3:Smoke( SmokeColor ) - self:F2( { SmokeColor } ) - trigger.action.smoke( self:GetVec3(), SmokeColor ) -end - ---- Smoke the POINT_VEC3 Green. --- @param #POINT_VEC3 self -function POINT_VEC3:SmokeGreen() - self:F2() - self:Smoke( SMOKECOLOR.Green ) -end - ---- Smoke the POINT_VEC3 Red. --- @param #POINT_VEC3 self -function POINT_VEC3:SmokeRed() - self:F2() - self:Smoke( SMOKECOLOR.Red ) -end - ---- Smoke the POINT_VEC3 White. --- @param #POINT_VEC3 self -function POINT_VEC3:SmokeWhite() - self:F2() - self:Smoke( SMOKECOLOR.White ) -end - ---- Smoke the POINT_VEC3 Orange. --- @param #POINT_VEC3 self -function POINT_VEC3:SmokeOrange() - self:F2() - self:Smoke( SMOKECOLOR.Orange ) -end - ---- Smoke the POINT_VEC3 Blue. --- @param #POINT_VEC3 self -function POINT_VEC3:SmokeBlue() - self:F2() - self:Smoke( SMOKECOLOR.Blue ) -end - ---- Flares the point in a color. --- @param #POINT_VEC3 self --- @param Utilities.Utils#FLARECOLOR FlareColor --- @param Dcs.DCSTypes#Azimuth (optional) Azimuth The azimuth of the flare direction. The default azimuth is 0. -function POINT_VEC3:Flare( FlareColor, Azimuth ) - self:F2( { FlareColor } ) - trigger.action.signalFlare( self:GetVec3(), FlareColor, Azimuth and Azimuth or 0 ) -end - ---- Flare the POINT_VEC3 White. --- @param #POINT_VEC3 self --- @param Dcs.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( FLARECOLOR.White, Azimuth ) -end - ---- Flare the POINT_VEC3 Yellow. --- @param #POINT_VEC3 self --- @param Dcs.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( FLARECOLOR.Yellow, Azimuth ) -end - ---- Flare the POINT_VEC3 Green. --- @param #POINT_VEC3 self --- @param Dcs.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( FLARECOLOR.Green, Azimuth ) -end - ---- Flare the POINT_VEC3 Red. --- @param #POINT_VEC3 self -function POINT_VEC3:FlareRed( Azimuth ) - self:F2( Azimuth ) - self:Flare( FLARECOLOR.Red, Azimuth ) -end - -end - -do -- POINT_VEC2 - - - ---- POINT_VEC2 constructor. --- @param #POINT_VEC2 self --- @param Dcs.DCSTypes#Distance x The x coordinate of the Vec3 point, pointing to the North. --- @param Dcs.DCSTypes#Distance y The y coordinate of the Vec3 point, pointing to the Right. --- @param Dcs.DCSTypes#Distance LandHeightAdd (optional) The default height if required to be evaluated will be the land height of the x, y coordinate. You can specify an extra height to be added to the land height. --- @return Core.Point#POINT_VEC2 -function POINT_VEC2:New( x, y, LandHeightAdd ) - - local LandHeight = land.getHeight( { ["x"] = x, ["y"] = y } ) - - LandHeightAdd = LandHeightAdd or 0 - LandHeight = LandHeight + LandHeightAdd - - self = BASE:Inherit( self, POINT_VEC3:New( x, LandHeight, y ) ) - self:F2( self ) - - return self -end - ---- Create a new POINT_VEC2 object from Vec2 coordinates. --- @param #POINT_VEC2 self --- @param Dcs.DCSTypes#Vec2 Vec2 The Vec2 point. --- @return Core.Point#POINT_VEC2 self -function POINT_VEC2:NewFromVec2( Vec2, LandHeightAdd ) - - local LandHeight = land.getHeight( Vec2 ) - - LandHeightAdd = LandHeightAdd or 0 - LandHeight = LandHeight + LandHeightAdd - - self = BASE:Inherit( self, POINT_VEC3:New( Vec2.x, LandHeight, Vec2.y ) ) - self:F2( self ) - - return self -end - ---- Create a new POINT_VEC2 object from Vec3 coordinates. --- @param #POINT_VEC2 self --- @param Dcs.DCSTypes#Vec3 Vec3 The Vec3 point. --- @return Core.Point#POINT_VEC2 self -function POINT_VEC2:NewFromVec3( Vec3 ) - - local self = BASE:Inherit( self, BASE:New() ) - local Vec2 = { x = Vec3.x, y = Vec3.z } - - local LandHeight = land.getHeight( Vec2 ) - - self = BASE:Inherit( self, POINT_VEC3:New( Vec2.x, LandHeight, Vec2.y ) ) - self:F2( self ) - - return self -end - ---- Return the x coordinate of the POINT_VEC2. --- @param #POINT_VEC2 self --- @return #number The x coodinate. -function POINT_VEC2:GetX() - return self.x -end - ---- Return the y coordinate of the POINT_VEC2. --- @param #POINT_VEC2 self --- @return #number The y coodinate. -function POINT_VEC2:GetY() - return self.z -end - ---- Return the altitude of the land at the POINT_VEC2. --- @param #POINT_VEC2 self --- @return #number The land altitude. -function POINT_VEC2:GetAlt() - return land.getHeight( { x = self.x, y = self.z } ) -end - ---- Set the x coordinate of the POINT_VEC2. --- @param #POINT_VEC2 self --- @param #number x The x coordinate. --- @return #POINT_VEC2 -function POINT_VEC2:SetX( x ) - self.x = x - return self -end - ---- Set the y coordinate of the POINT_VEC2. --- @param #POINT_VEC2 self --- @param #number y The y coordinate. --- @return #POINT_VEC2 -function POINT_VEC2:SetY( y ) - self.z = y - return self -end - ---- Set the altitude of the POINT_VEC2. --- @param #POINT_VEC2 self --- @param #number Altitude The land altitude. If nothing (nil) is given, then the current land altitude is set. --- @return #POINT_VEC2 -function POINT_VEC2:SetAlt( Altitude ) - self.y = Altitude or land.getHeight( { x = self.x, y = self.z } ) - return self -end - ---- Add to the x coordinate of the POINT_VEC2. --- @param #POINT_VEC2 self --- @param #number x The x coordinate. --- @return #POINT_VEC2 -function POINT_VEC2:AddX( x ) - self.x = self.x + x - return self -end - ---- Add to the y coordinate of the POINT_VEC2. --- @param #POINT_VEC2 self --- @param #number y The y coordinate. --- @return #POINT_VEC2 -function POINT_VEC2:AddY( y ) - self.z = self.z + y - return self -end - ---- Add to the current land height an altitude. --- @param #POINT_VEC2 self --- @param #number Altitude The Altitude to add. If nothing (nil) is given, then the current land altitude is set. --- @return #POINT_VEC2 -function POINT_VEC2:AddAlt( Altitude ) - self.y = land.getHeight( { x = self.x, y = self.z } ) + Altitude or 0 - return self -end - - - ---- Calculate the distance from a reference @{#POINT_VEC2}. --- @param #POINT_VEC2 self --- @param #POINT_VEC2 PointVec2Reference The reference @{#POINT_VEC2}. --- @return Dcs.DCSTypes#Distance The distance from the reference @{#POINT_VEC2} in meters. -function POINT_VEC2:DistanceFromPointVec2( PointVec2Reference ) - self:F2( PointVec2Reference ) - - local Distance = ( ( PointVec2Reference:GetX() - self:GetX() ) ^ 2 + ( PointVec2Reference:GetY() - self:GetY() ) ^2 ) ^0.5 - - self:T2( Distance ) - return Distance -end - ---- Calculate the distance from a reference @{DCSTypes#Vec2}. --- @param #POINT_VEC2 self --- @param Dcs.DCSTypes#Vec2 Vec2Reference The reference @{DCSTypes#Vec2}. --- @return Dcs.DCSTypes#Distance The distance from the reference @{DCSTypes#Vec2} in meters. -function POINT_VEC2:DistanceFromVec2( Vec2Reference ) - self:F2( Vec2Reference ) - - local Distance = ( ( Vec2Reference.x - self:GetX() ) ^ 2 + ( Vec2Reference.y - self:GetY() ) ^2 ) ^0.5 - - self:T2( Distance ) - return Distance -end - - ---- Return no text for the altitude of the POINT_VEC2. --- @param #POINT_VEC2 self --- @return #string Empty string. -function POINT_VEC2:GetAltitudeText() - return '' -end - ---- Add a Distance in meters from the POINT_VEC2 orthonormal plane, with the given angle, and calculate the new POINT_VEC2. --- @param #POINT_VEC2 self --- @param Dcs.DCSTypes#Distance Distance The Distance to be added in meters. --- @param Dcs.DCSTypes#Angle Angle The Angle in degrees. --- @return #POINT_VEC2 The new calculated POINT_VEC2. -function POINT_VEC2:Translate( Distance, Angle ) - local SX = self:GetX() - local SY = self:GetY() - local Radians = Angle / 180 * math.pi - local TX = Distance * math.cos( Radians ) + SX - local TY = Distance * math.sin( Radians ) + SY - - return POINT_VEC2:New( TX, TY ) -end - -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 Core.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 - if MessageCategory:sub(-1) ~= "\n" then - self.MessageCategory = MessageCategory .. ": " - else - self.MessageCategory = MessageCategory:sub( 1, -2 ) .. ":\n" - end - else - self.MessageCategory = "" - end - - self.MessageDuration = MessageDuration or 5 - 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 Wrapper.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 a Group. --- @param #MESSAGE self --- @param Wrapper.Group#GROUP Group is the Group. --- @return #MESSAGE -function MESSAGE:ToGroup( Group ) - self:F( Group.GroupName ) - - if Group then - - self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration ) - trigger.action.outTextForGroup( Group:GetID(), self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration ) - 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 a Coalition if the given Condition is true. --- @param #MESSAGE self --- @param CoalitionSide needs to be filled out by the defined structure of the standard scripting engine @{coalition.side}. --- @return #MESSAGE -function MESSAGE:ToCoalitionIf( CoalitionSide, Condition ) - self:F( CoalitionSide ) - - if Condition and Condition == true then - self:ToCoalition( CoalitionSide ) - end - - return self -end - ---- Sends a MESSAGE to all players. --- @param #MESSAGE self --- @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 - - ---- Sends a MESSAGE to all players if the given Condition is true. --- @param #MESSAGE self --- @return #MESSAGE -function MESSAGE:ToAllIf( Condition ) - - if Condition and Condition == true then - self:ToCoalition( coalition.side.RED ) - self:ToCoalition( coalition.side.BLUE ) - end - - 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 ) --- ---- This module contains the **FSM** (**F**inite **S**tate **M**achine) class and derived **FSM\_** classes. --- ## Finite State Machines (FSM) are design patterns allowing efficient (long-lasting) processes and workflows. --- --- ![Banner Image](..\Presentations\FSM\Dia1.JPG) --- --- === --- --- A FSM can only be in one of a finite number of states. --- The machine is in only one state at a time; the state it is in at any given time is called the **current state**. --- It can change from one state to another when initiated by an **__internal__ or __external__ triggering event**, which is called a **transition**. --- An **FSM implementation** is defined by **a list of its states**, **its initial state**, and **the triggering events** for **each possible transition**. --- An FSM implementation is composed out of **two parts**, a set of **state transition rules**, and an implementation set of **state transition handlers**, implementing those transitions. --- --- The FSM class supports a **hierarchical implementation of a Finite State Machine**, --- that is, it allows to **embed existing FSM implementations in a master FSM**. --- FSM hierarchies allow for efficient FSM re-use, **not having to re-invent the wheel every time again** when designing complex processes. --- --- ![Workflow Example](..\Presentations\FSM\Dia2.JPG) --- --- The above diagram shows a graphical representation of a FSM implementation for a **Task**, which guides a Human towards a Zone, --- orders him to destroy x targets and account the results. --- Other examples of ready made FSM could be: --- --- * route a plane to a zone flown by a human --- * detect targets by an AI and report to humans --- * account for destroyed targets by human players --- * handle AI infantry to deploy from or embark to a helicopter or airplane or vehicle --- * let an AI patrol a zone --- --- The **MOOSE framework** uses extensively the FSM class and derived FSM\_ classes, --- because **the goal of MOOSE is to simplify mission design complexity for mission building**. --- By efficiently utilizing the FSM class and derived classes, MOOSE allows mission designers to quickly build processes. --- **Ready made FSM-based implementations classes** exist within the MOOSE framework that **can easily be re-used, --- and tailored** by mission designers through **the implementation of Transition Handlers**. --- Each of these FSM implementation classes start either with: --- --- * an acronym **AI\_**, which indicates an FSM implementation directing **AI controlled** @{GROUP} and/or @{UNIT}. These AI\_ classes derive the @{#FSM_CONTROLLABLE} class. --- * an acronym **TASK\_**, which indicates an FSM implementation executing a @{TASK} executed by Groups of players. These TASK\_ classes derive the @{#FSM_TASK} class. --- * an acronym **ACT\_**, which indicates an Sub-FSM implementation, directing **Humans actions** that need to be done in a @{TASK}, seated in a @{CLIENT} (slot) or a @{UNIT} (CA join). These ACT\_ classes derive the @{#FSM_PROCESS} class. --- --- Detailed explanations and API specifics are further below clarified and FSM derived class specifics are described in those class documentation sections. --- --- ##__Dislaimer:__ --- The FSM class development is based on a finite state machine implementation made by Conroy Kyle. --- The state machine can be found on [github](https://github.com/kyleconroy/lua-state-machine) --- I've reworked this development (taken the concept), and created a **hierarchical state machine** out of it, embedded within the DCS simulator. --- Additionally, I've added extendability and created an API that allows seamless FSM implementation. --- --- === --- --- # 1) @{#FSM} class, extends @{Base#BASE} --- --- ![Transition Rules and Transition Handlers and Event Triggers](..\Presentations\FSM\Dia3.JPG) --- --- The FSM class is the base class of all FSM\_ derived classes. It implements the main functionality to define and execute Finite State Machines. --- The derived FSM\_ classes extend the Finite State Machine functionality to run a workflow process for a specific purpose or component. --- --- Finite State Machines have **Transition Rules**, **Transition Handlers** and **Event Triggers**. --- --- The **Transition Rules** define the "Process Flow Boundaries", that is, --- the path that can be followed hopping from state to state upon triggered events. --- If an event is triggered, and there is no valid path found for that event, --- an error will be raised and the FSM will stop functioning. --- --- The **Transition Handlers** are special methods that can be defined by the mission designer, following a defined syntax. --- If the FSM object finds a method of such a handler, then the method will be called by the FSM, passing specific parameters. --- The method can then define its own custom logic to implement the FSM workflow, and to conduct other actions. --- --- The **Event Triggers** are methods that are defined by the FSM, which the mission designer can use to implement the workflow. --- Most of the time, these Event Triggers are used within the Transition Handler methods, so that a workflow is created running through the state machine. --- --- As explained above, a FSM supports **Linear State Transitions** and **Hierarchical State Transitions**, and both can be mixed to make a comprehensive FSM implementation. --- The below documentation has a seperate chapter explaining both transition modes, taking into account the **Transition Rules**, **Transition Handlers** and **Event Triggers**. --- --- ## 1.1) FSM Linear Transitions --- --- Linear Transitions are Transition Rules allowing an FSM to transition from one or multiple possible **From** state(s) towards a **To** state upon a Triggered **Event**. --- The Lineair transition rule evaluation will always be done from the **current state** of the FSM. --- If no valid Transition Rule can be found in the FSM, the FSM will log an error and stop. --- --- ### 1.1.1) FSM Transition Rules --- --- The FSM has transition rules that it follows and validates, as it walks the process. --- These rules define when an FSM can transition from a specific state towards an other specific state upon a triggered event. --- --- The method @{#FSM.AddTransition}() specifies a new possible Transition Rule for the FSM. --- --- The initial state can be defined using the method @{#FSM.SetStartState}(). The default start state of an FSM is "None". --- --- Find below an example of a Linear Transition Rule definition for an FSM. --- --- local Fsm3Switch = FSM:New() -- #FsmDemo --- FsmSwitch:SetStartState( "Off" ) --- FsmSwitch:AddTransition( "Off", "SwitchOn", "On" ) --- FsmSwitch:AddTransition( "Off", "SwitchMiddle", "Middle" ) --- FsmSwitch:AddTransition( "On", "SwitchOff", "Off" ) --- FsmSwitch:AddTransition( "Middle", "SwitchOff", "Off" ) --- --- The above code snippet models a 3-way switch Linear Transition: --- --- * It can be switched **On** by triggering event **SwitchOn**. --- * It can be switched to the **Middle** position, by triggering event **SwitchMiddle**. --- * It can be switched **Off** by triggering event **SwitchOff**. --- * Note that once the Switch is **On** or **Middle**, it can only be switched **Off**. --- --- ### Some additional comments: --- --- Note that Linear Transition Rules **can be declared in a few variations**: --- --- * The From states can be **a table of strings**, indicating that the transition rule will be valid **if the current state** of the FSM will be **one of the given From states**. --- * The From state can be a **"*"**, indicating that **the transition rule will always be valid**, regardless of the current state of the FSM. --- --- The below code snippet shows how the two last lines can be rewritten and consensed. --- --- FsmSwitch:AddTransition( { "On", "Middle" }, "SwitchOff", "Off" ) --- --- ### 1.1.2) Transition Handling --- --- ![Transition Handlers](..\Presentations\FSM\Dia4.JPG) --- --- An FSM transitions in **4 moments** when an Event is being triggered and processed. --- The mission designer can define for each moment specific logic within methods implementations following a defined API syntax. --- These methods define the flow of the FSM process; because in those methods the FSM Internal Events will be triggered. --- --- * To handle **State** transition moments, create methods starting with OnLeave or OnEnter concatenated with the State name. --- * To handle **Event** transition moments, create methods starting with OnBefore or OnAfter concatenated with the Event name. --- --- **The OnLeave and OnBefore transition methods may return false, which will cancel the transition!** --- --- Transition Handler methods need to follow the above specified naming convention, but are also passed parameters from the FSM. --- These parameters are on the correct order: From, Event, To: --- --- * From = A string containing the From state. --- * Event = A string containing the Event name that was triggered. --- * To = A string containing the To state. --- --- On top, each of these methods can have a variable amount of parameters passed. See the example in section [1.1.3](#1.1.3\)-event-triggers). --- --- ### 1.1.3) Event Triggers --- --- ![Event Triggers](..\Presentations\FSM\Dia5.JPG) --- --- The FSM creates for each Event two **Event Trigger methods**. --- There are two modes how Events can be triggered, which is **synchronous** and **asynchronous**: --- --- * The method **FSM:Event()** triggers an Event that will be processed **synchronously** or **immediately**. --- * The method **FSM:__Event( __seconds__ )** triggers an Event that will be processed **asynchronously** over time, waiting __x seconds__. --- --- The destinction between these 2 Event Trigger methods are important to understand. An asynchronous call will "log" the Event Trigger to be executed at a later time. --- Processing will just continue. Synchronous Event Trigger methods are useful to change states of the FSM immediately, but may have a larger processing impact. --- --- The following example provides a little demonstration on the difference between synchronous and asynchronous Event Triggering. --- --- function FSM:OnAfterEvent( From, Event, To, Amount ) --- self:T( { Amount = Amount } ) --- end --- --- local Amount = 1 --- FSM:__Event( 5, Amount ) --- --- Amount = Amount + 1 --- FSM:Event( Text, Amount ) --- --- In this example, the **:OnAfterEvent**() Transition Handler implementation will get called when **Event** is being triggered. --- Before we go into more detail, let's look at the last 4 lines of the example. --- The last line triggers synchronously the **Event**, and passes Amount as a parameter. --- The 3rd last line of the example triggers asynchronously **Event**. --- Event will be processed after 5 seconds, and Amount is given as a parameter. --- --- The output of this little code fragment will be: --- --- * Amount = 2 --- * Amount = 2 --- --- Because ... When Event was asynchronously processed after 5 seconds, Amount was set to 2. So be careful when processing and passing values and objects in asynchronous processing! --- --- ### 1.1.4) Linear Transition Example --- --- This example is fully implemented in the MOOSE test mission on GITHUB: [FSM-100 - Transition Explanation](https://github.com/FlightControl-Master/MOOSE/blob/master/Moose%20Test%20Missions/FSM%20-%20Finite%20State%20Machine/FSM-100%20-%20Transition%20Explanation/FSM-100%20-%20Transition%20Explanation.lua) --- --- It models a unit standing still near Batumi, and flaring every 5 seconds while switching between a Green flare and a Red flare. --- The purpose of this example is not to show how exciting flaring is, but it demonstrates how a Linear Transition FSM can be build. --- Have a look at the source code. The source code is also further explained below in this section. --- --- The example creates a new FsmDemo object from class FSM. --- It will set the start state of FsmDemo to state **Green**. --- Two Linear Transition Rules are created, where upon the event **Switch**, --- the FsmDemo will transition from state **Green** to **Red** and from **Red** back to **Green**. --- --- ![Transition Example](..\Presentations\FSM\Dia6.JPG) --- --- local FsmDemo = FSM:New() -- #FsmDemo --- FsmDemo:SetStartState( "Green" ) --- FsmDemo:AddTransition( "Green", "Switch", "Red" ) --- FsmDemo:AddTransition( "Red", "Switch", "Green" ) --- --- In the above example, the FsmDemo could flare every 5 seconds a Green or a Red flare into the air. --- The next code implements this through the event handling method **OnAfterSwitch**. --- --- ![Transition Flow](..\Presentations\FSM\Dia7.JPG) --- --- function FsmDemo:OnAfterSwitch( From, Event, To, FsmUnit ) --- self:T( { From, Event, To, FsmUnit } ) --- --- if From == "Green" then --- FsmUnit:Flare(FLARECOLOR.Green) --- else --- if From == "Red" then --- FsmUnit:Flare(FLARECOLOR.Red) --- end --- end --- self:__Switch( 5, FsmUnit ) -- Trigger the next Switch event to happen in 5 seconds. --- end --- --- FsmDemo:__Switch( 5, FsmUnit ) -- Trigger the first Switch event to happen in 5 seconds. --- --- The OnAfterSwitch implements a loop. The last line of the code fragment triggers the Switch Event within 5 seconds. --- Upon the event execution (after 5 seconds), the OnAfterSwitch method is called of FsmDemo (cfr. the double point notation!!! ":"). --- The OnAfterSwitch method receives from the FSM the 3 transition parameter details ( From, Event, To ), --- and one additional parameter that was given when the event was triggered, which is in this case the Unit that is used within OnSwitchAfter. --- --- function FsmDemo:OnAfterSwitch( From, Event, To, FsmUnit ) --- --- For debugging reasons the received parameters are traced within the DCS.log. --- --- self:T( { From, Event, To, FsmUnit } ) --- --- The method will check if the From state received is either "Green" or "Red" and will flare the respective color from the FsmUnit. --- --- if From == "Green" then --- FsmUnit:Flare(FLARECOLOR.Green) --- else --- if From == "Red" then --- FsmUnit:Flare(FLARECOLOR.Red) --- end --- end --- --- It is important that the Switch event is again triggered, otherwise, the FsmDemo would stop working after having the first Event being handled. --- --- FsmDemo:__Switch( 5, FsmUnit ) -- Trigger the next Switch event to happen in 5 seconds. --- --- The below code fragment extends the FsmDemo, demonstrating multiple **From states declared as a table**, adding a **Linear Transition Rule**. --- The new event **Stop** will cancel the Switching process. --- The transition for event Stop can be executed if the current state of the FSM is either "Red" or "Green". --- --- local FsmDemo = FSM:New() -- #FsmDemo --- FsmDemo:SetStartState( "Green" ) --- FsmDemo:AddTransition( "Green", "Switch", "Red" ) --- FsmDemo:AddTransition( "Red", "Switch", "Green" ) --- FsmDemo:AddTransition( { "Red", "Green" }, "Stop", "Stopped" ) --- --- The transition for event Stop can also be simplified, as any current state of the FSM is valid. --- --- FsmDemo:AddTransition( "*", "Stop", "Stopped" ) --- --- So... When FsmDemo:Stop() is being triggered, the state of FsmDemo will transition from Red or Green to Stopped. --- And there is no transition handling method defined for that transition, thus, no new event is being triggered causing the FsmDemo process flow to halt. --- --- ## 1.5) FSM Hierarchical Transitions --- --- Hierarchical Transitions allow to re-use readily available and implemented FSMs. --- This becomes in very useful for mission building, where mission designers build complex processes and workflows, --- combining smaller FSMs to one single FSM. --- --- The FSM can embed **Sub-FSMs** that will execute and return **multiple possible Return (End) States**. --- Depending upon **which state is returned**, the main FSM can continue the flow **triggering specific events**. --- --- The method @{#FSM.AddProcess}() adds a new Sub-FSM to the FSM. --- --- ==== --- --- # **API CHANGE HISTORY** --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- YYYY-MM-DD: CLASS:**NewFunction**( Params ) replaces CLASS:_OldFunction_( Params ) --- YYYY-MM-DD: CLASS:**NewFunction( Params )** added --- --- Hereby the change log: --- --- * 2016-12-18: Released. --- --- === --- --- # **AUTHORS and CONTRIBUTIONS** --- --- ### Contributions: --- --- * [**Pikey**](https://forums.eagle.ru/member.php?u=62835): Review of documentation & advice for improvements. --- --- ### Authors: --- --- * [**FlightControl**](https://forums.eagle.ru/member.php?u=89536): Design & Programming & documentation. --- --- @module Fsm - -do -- FSM - - --- FSM class - -- @type FSM - -- @extends Core.Base#BASE - FSM = { - ClassName = "FSM", - } - - --- Creates a new FSM object. - -- @param #FSM self - -- @return #FSM - function FSM:New( FsmT ) - - -- Inherits from BASE - self = BASE:Inherit( self, BASE:New() ) - - self.options = options or {} - self.options.subs = self.options.subs or {} - self.current = self.options.initial or 'none' - self.Events = {} - self.subs = {} - self.endstates = {} - - self.Scores = {} - - self._StartState = "none" - self._Transitions = {} - self._Processes = {} - self._EndStates = {} - self._Scores = {} - self._EventSchedules = {} - - self.CallScheduler = SCHEDULER:New( self ) - - - return self - end - - - --- Sets the start state of the FSM. - -- @param #FSM self - -- @param #string State A string defining the start state. - function FSM:SetStartState( State ) - - self._StartState = State - self.current = State - end - - - --- Returns the start state of the FSM. - -- @param #FSM self - -- @return #string A string containing the start state. - function FSM:GetStartState() - - return self._StartState or {} - end - - --- Add a new transition rule to the FSM. - -- A transition rule defines when and if the FSM can transition from a state towards another state upon a triggered event. - -- @param #FSM self - -- @param #table From Can contain a string indicating the From state or a table of strings containing multiple From states. - -- @param #string Event The Event name. - -- @param #string To The To state. - function FSM:AddTransition( From, Event, To ) - - local Transition = {} - Transition.From = From - Transition.Event = Event - Transition.To = To - - self:T( Transition ) - - self._Transitions[Transition] = Transition - self:_eventmap( self.Events, Transition ) - end - - - --- Returns a table of the transition rules defined within the FSM. - -- @return #table - function FSM:GetTransitions() - - return self._Transitions or {} - end - - --- Set the default @{Process} template with key ProcessName providing the ProcessClass and the process object when it is assigned to a @{Controllable} by the task. - -- @param #FSM self - -- @param #table From Can contain a string indicating the From state or a table of strings containing multiple From states. - -- @param #string Event The Event name. - -- @param Core.Fsm#FSM_PROCESS Process An sub-process FSM. - -- @param #table ReturnEvents A table indicating for which returned events of the SubFSM which Event must be triggered in the FSM. - -- @return Core.Fsm#FSM_PROCESS The SubFSM. - function FSM:AddProcess( From, Event, Process, ReturnEvents ) - self:T( { From, Event, Process, ReturnEvents } ) - - local Sub = {} - Sub.From = From - Sub.Event = Event - Sub.fsm = Process - Sub.StartEvent = "Start" - Sub.ReturnEvents = ReturnEvents - - self._Processes[Sub] = Sub - - self:_submap( self.subs, Sub, nil ) - - self:AddTransition( From, Event, From ) - - return Process - end - - - --- Returns a table of the SubFSM rules defined within the FSM. - -- @return #table - function FSM:GetProcesses() - - return self._Processes or {} - end - - function FSM:GetProcess( From, Event ) - - for ProcessID, Process in pairs( self:GetProcesses() ) do - if Process.From == From and Process.Event == Event then - self:T( Process ) - return Process.fsm - end - end - - error( "Sub-Process from state " .. From .. " with event " .. Event .. " not found!" ) - end - - --- Adds an End state. - function FSM:AddEndState( State ) - - self._EndStates[State] = State - self.endstates[State] = State - end - - --- Returns the End states. - function FSM:GetEndStates() - - return self._EndStates or {} - end - - - --- Adds a score for the FSM to be achieved. - -- @param #FSM self - -- @param #string State is the state of the process when the score needs to be given. (See the relevant state descriptions of the process). - -- @param #string ScoreText is a text describing the score that is given according the status. - -- @param #number Score is a number providing the score of the status. - -- @return #FSM self - function FSM:AddScore( State, ScoreText, Score ) - self:F2( { State, ScoreText, Score } ) - - self._Scores[State] = self._Scores[State] or {} - self._Scores[State].ScoreText = ScoreText - self._Scores[State].Score = Score - - return self - end - - --- Adds a score for the FSM_PROCESS to be achieved. - -- @param #FSM self - -- @param #string From is the From State of the main process. - -- @param #string Event is the Event of the main process. - -- @param #string State is the state of the process when the score needs to be given. (See the relevant state descriptions of the process). - -- @param #string ScoreText is a text describing the score that is given according the status. - -- @param #number Score is a number providing the score of the status. - -- @return #FSM self - function FSM:AddScoreProcess( From, Event, State, ScoreText, Score ) - self:F2( { Event, State, ScoreText, Score } ) - - local Process = self:GetProcess( From, Event ) - - self:T( { Process = Process._Name, Scores = Process._Scores, State = State, ScoreText = ScoreText, Score = Score } ) - Process._Scores[State] = Process._Scores[State] or {} - Process._Scores[State].ScoreText = ScoreText - Process._Scores[State].Score = Score - - return Process - end - - --- Returns a table with the scores defined. - function FSM:GetScores() - - return self._Scores or {} - end - - --- Returns a table with the Subs defined. - function FSM:GetSubs() - - return self.options.subs - end - - - function FSM:LoadCallBacks( CallBackTable ) - - for name, callback in pairs( CallBackTable or {} ) do - self[name] = callback - end - - end - - function FSM:_eventmap( Events, EventStructure ) - - local Event = EventStructure.Event - local __Event = "__" .. EventStructure.Event - self[Event] = self[Event] or self:_create_transition(Event) - self[__Event] = self[__Event] or self:_delayed_transition(Event) - self:T( "Added methods: " .. Event .. ", " .. __Event ) - Events[Event] = self.Events[Event] or { map = {} } - self:_add_to_map( Events[Event].map, EventStructure ) - - end - - function FSM:_submap( subs, sub, name ) - self:F( { sub = sub, name = name } ) - subs[sub.From] = subs[sub.From] or {} - subs[sub.From][sub.Event] = subs[sub.From][sub.Event] or {} - - -- Make the reference table weak. - -- setmetatable( subs[sub.From][sub.Event], { __mode = "k" } ) - - subs[sub.From][sub.Event][sub] = {} - subs[sub.From][sub.Event][sub].fsm = sub.fsm - subs[sub.From][sub.Event][sub].StartEvent = sub.StartEvent - subs[sub.From][sub.Event][sub].ReturnEvents = sub.ReturnEvents or {} -- these events need to be given to find the correct continue event ... if none given, the processing will stop. - subs[sub.From][sub.Event][sub].name = name - subs[sub.From][sub.Event][sub].fsmparent = self - end - - - function FSM:_call_handler( handler, params, EventName ) - if self[handler] then - self:T( "Calling " .. handler ) - self._EventSchedules[EventName] = nil - local Value = self[handler]( self, unpack(params) ) - return Value - end - end - - function FSM._handler( self, EventName, ... ) - - local Can, to = self:can( EventName ) - - if to == "*" then - to = self.current - end - - if Can then - local from = self.current - local params = { from, EventName, to, ... } - - if self.Controllable then - self:T( "FSM Transition for " .. self.Controllable.ControllableName .. " :" .. self.current .. " --> " .. EventName .. " --> " .. to ) - else - self:T( "FSM Transition:" .. self.current .. " --> " .. EventName .. " --> " .. to ) - end - - if ( self:_call_handler("onbefore" .. EventName, params, EventName ) == false ) - or ( self:_call_handler("OnBefore" .. EventName, params, EventName ) == false ) - or ( self:_call_handler("onleave" .. from, params, EventName ) == false ) - or ( self:_call_handler("OnLeave" .. from, params, EventName ) == false ) then - self:T( "Cancel Transition" ) - return false - end - - self.current = to - - local execute = true - - local subtable = self:_gosub( from, EventName ) - for _, sub in pairs( subtable ) do - --if sub.nextevent then - -- self:F2( "nextevent = " .. sub.nextevent ) - -- self[sub.nextevent]( self ) - --end - self:T( "calling sub start event: " .. sub.StartEvent ) - sub.fsm.fsmparent = self - sub.fsm.ReturnEvents = sub.ReturnEvents - sub.fsm[sub.StartEvent]( sub.fsm ) - execute = false - end - - local fsmparent, Event = self:_isendstate( to ) - if fsmparent and Event then - self:F2( { "end state: ", fsmparent, Event } ) - self:_call_handler("onenter" .. to, params, EventName ) - self:_call_handler("OnEnter" .. to, params, EventName ) - self:_call_handler("onafter" .. EventName, params, EventName ) - self:_call_handler("OnAfter" .. EventName, params, EventName ) - self:_call_handler("onstatechange", params, EventName ) - fsmparent[Event]( fsmparent ) - execute = false - end - - if execute then - -- only execute the call if the From state is not equal to the To state! Otherwise this function should never execute! - --if from ~= to then - self:_call_handler("onenter" .. to, params, EventName ) - self:_call_handler("OnEnter" .. to, params, EventName ) - --end - - self:_call_handler("onafter" .. EventName, params, EventName ) - self:_call_handler("OnAfter" .. EventName, params, EventName ) - - self:_call_handler("onstatechange", params, EventName ) - end - else - self:T( "Cannot execute transition." ) - self:T( { From = self.current, Event = EventName, To = to, Can = Can } ) - end - - return nil - end - - function FSM:_delayed_transition( EventName ) - return function( self, DelaySeconds, ... ) - self:T2( "Delayed Event: " .. EventName ) - local CallID = 0 - if DelaySeconds ~= nil then - if DelaySeconds < 0 then -- Only call the event ONCE! - DelaySeconds = math.abs( DelaySeconds ) - if not self._EventSchedules[EventName] then - CallID = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1 ) - self._EventSchedules[EventName] = CallID - else - -- reschedule - end - else - CallID = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1 ) - end - else - error( "FSM: An asynchronous event trigger requires a DelaySeconds parameter!!! This can be positive or negative! Sorry, but will not process this." ) - end - self:T2( { CallID = CallID } ) - end - end - - function FSM:_create_transition( EventName ) - return function( self, ... ) return self._handler( self, EventName , ... ) end - end - - function FSM:_gosub( ParentFrom, ParentEvent ) - local fsmtable = {} - if self.subs[ParentFrom] and self.subs[ParentFrom][ParentEvent] then - self:T( { ParentFrom, ParentEvent, self.subs[ParentFrom], self.subs[ParentFrom][ParentEvent] } ) - return self.subs[ParentFrom][ParentEvent] - else - return {} - end - end - - function FSM:_isendstate( Current ) - local FSMParent = self.fsmparent - if FSMParent and self.endstates[Current] then - self:T( { state = Current, endstates = self.endstates, endstate = self.endstates[Current] } ) - FSMParent.current = Current - local ParentFrom = FSMParent.current - self:T( ParentFrom ) - self:T( self.ReturnEvents ) - local Event = self.ReturnEvents[Current] - self:T( { ParentFrom, Event, self.ReturnEvents } ) - if Event then - return FSMParent, Event - else - self:T( { "Could not find parent event name for state ", ParentFrom } ) - end - end - - return nil - end - - function FSM:_add_to_map( Map, Event ) - self:F3( { Map, Event } ) - if type(Event.From) == 'string' then - Map[Event.From] = Event.To - else - for _, From in ipairs(Event.From) do - Map[From] = Event.To - end - end - self:T3( { Map, Event } ) - end - - function FSM:GetState() - return self.current - end - - - function FSM:Is( State ) - return self.current == State - end - - function FSM:is(state) - return self.current == state - end - - function FSM:can(e) - local Event = self.Events[e] - self:F3( { self.current, Event } ) - local To = Event and Event.map[self.current] or Event.map['*'] - return To ~= nil, To - end - - function FSM:cannot(e) - return not self:can(e) - end - -end - -do -- FSM_CONTROLLABLE - - --- FSM_CONTROLLABLE class - -- @type FSM_CONTROLLABLE - -- @field Wrapper.Controllable#CONTROLLABLE Controllable - -- @extends Core.Fsm#FSM - FSM_CONTROLLABLE = { - ClassName = "FSM_CONTROLLABLE", - } - - --- Creates a new FSM_CONTROLLABLE object. - -- @param #FSM_CONTROLLABLE self - -- @param #table FSMT Finite State Machine Table - -- @param Wrapper.Controllable#CONTROLLABLE Controllable (optional) The CONTROLLABLE object that the FSM_CONTROLLABLE governs. - -- @return #FSM_CONTROLLABLE - function FSM_CONTROLLABLE:New( FSMT, Controllable ) - - -- Inherits from BASE - local self = BASE:Inherit( self, FSM:New( FSMT ) ) -- Core.Fsm#FSM_CONTROLLABLE - - if Controllable then - self:SetControllable( Controllable ) - end - - return self - end - - --- Sets the CONTROLLABLE object that the FSM_CONTROLLABLE governs. - -- @param #FSM_CONTROLLABLE self - -- @param Wrapper.Controllable#CONTROLLABLE FSMControllable - -- @return #FSM_CONTROLLABLE - function FSM_CONTROLLABLE:SetControllable( FSMControllable ) - self:F( FSMControllable ) - self.Controllable = FSMControllable - end - - --- Gets the CONTROLLABLE object that the FSM_CONTROLLABLE governs. - -- @param #FSM_CONTROLLABLE self - -- @return Wrapper.Controllable#CONTROLLABLE - function FSM_CONTROLLABLE:GetControllable() - return self.Controllable - end - - function FSM_CONTROLLABLE:_call_handler( handler, params, EventName ) - - local ErrorHandler = function( errmsg ) - - env.info( "Error in SCHEDULER function:" .. errmsg ) - if debug ~= nil then - env.info( debug.traceback() ) - end - - return errmsg - end - - if self[handler] then - self:F3( "Calling " .. handler ) - self._EventSchedules[EventName] = nil - local Result, Value = xpcall( function() return self[handler]( self, self.Controllable, unpack( params ) ) end, ErrorHandler ) - return Value - --return self[handler]( self, self.Controllable, unpack( params ) ) - end - end - -end - -do -- FSM_PROCESS - - --- FSM_PROCESS class - -- @type FSM_PROCESS - -- @field Tasking.Task#TASK Task - -- @extends Core.Fsm#FSM_CONTROLLABLE - FSM_PROCESS = { - ClassName = "FSM_PROCESS", - } - - --- Creates a new FSM_PROCESS object. - -- @param #FSM_PROCESS self - -- @return #FSM_PROCESS - function FSM_PROCESS:New( Controllable, Task ) - - local self = BASE:Inherit( self, FSM_CONTROLLABLE:New() ) -- Core.Fsm#FSM_PROCESS - - self:F( Controllable, Task ) - - self:Assign( Controllable, Task ) - - return self - end - - function FSM_PROCESS:Init( FsmProcess ) - self:T( "No Initialisation" ) - end - - --- Creates a new FSM_PROCESS object based on this FSM_PROCESS. - -- @param #FSM_PROCESS self - -- @return #FSM_PROCESS - function FSM_PROCESS:Copy( Controllable, Task ) - self:T( { self:GetClassNameAndID() } ) - - local NewFsm = self:New( Controllable, Task ) -- Core.Fsm#FSM_PROCESS - - NewFsm:Assign( Controllable, Task ) - - -- Polymorphic call to initialize the new FSM_PROCESS based on self FSM_PROCESS - NewFsm:Init( self ) - - -- Set Start State - NewFsm:SetStartState( self:GetStartState() ) - - -- Copy Transitions - for TransitionID, Transition in pairs( self:GetTransitions() ) do - NewFsm:AddTransition( Transition.From, Transition.Event, Transition.To ) - end - - -- Copy Processes - for ProcessID, Process in pairs( self:GetProcesses() ) do - self:T( { Process} ) - local FsmProcess = NewFsm:AddProcess( Process.From, Process.Event, Process.fsm:Copy( Controllable, Task ), Process.ReturnEvents ) - end - - -- Copy End States - for EndStateID, EndState in pairs( self:GetEndStates() ) do - self:T( EndState ) - NewFsm:AddEndState( EndState ) - end - - -- Copy the score tables - for ScoreID, Score in pairs( self:GetScores() ) do - self:T( Score ) - NewFsm:AddScore( ScoreID, Score.ScoreText, Score.Score ) - end - - return NewFsm - end - - --- Sets the task of the process. - -- @param #FSM_PROCESS self - -- @param Tasking.Task#TASK Task - -- @return #FSM_PROCESS - function FSM_PROCESS:SetTask( Task ) - - self.Task = Task - - return self - end - - --- Gets the task of the process. - -- @param #FSM_PROCESS self - -- @return Tasking.Task#TASK - function FSM_PROCESS:GetTask() - - return self.Task - end - - --- Gets the mission of the process. - -- @param #FSM_PROCESS self - -- @return Tasking.Mission#MISSION - function FSM_PROCESS:GetMission() - - return self.Task.Mission - end - - --- Gets the mission of the process. - -- @param #FSM_PROCESS self - -- @return Tasking.CommandCenter#COMMANDCENTER - function FSM_PROCESS:GetCommandCenter() - - return self:GetTask():GetMission():GetCommandCenter() - end - --- TODO: Need to check and fix that an FSM_PROCESS is only for a UNIT. Not for a GROUP. - - --- Send a message of the @{Task} to the Group of the Unit. --- @param #FSM_PROCESS self -function FSM_PROCESS:Message( Message ) - self:F( { Message = Message } ) - - local CC = self:GetCommandCenter() - local TaskGroup = self.Controllable:GetGroup() - - local PlayerName = self.Controllable:GetPlayerName() -- Only for a unit - PlayerName = PlayerName and " (" .. PlayerName .. ")" or "" -- If PlayerName is nil, then keep it nil, otherwise add brackets. - local Callsign = self.Controllable:GetCallsign() - local Prefix = Callsign and " @ " .. Callsign .. PlayerName or "" - - Message = Prefix .. ": " .. Message - CC:MessageToGroup( Message, TaskGroup ) -end - - - - - --- Assign the process to a @{Unit} and activate the process. - -- @param #FSM_PROCESS self - -- @param Task.Tasking#TASK Task - -- @param Wrapper.Unit#UNIT ProcessUnit - -- @return #FSM_PROCESS self - function FSM_PROCESS:Assign( ProcessUnit, Task ) - self:T( { Task, ProcessUnit } ) - - self:SetControllable( ProcessUnit ) - self:SetTask( Task ) - - --self.ProcessGroup = ProcessUnit:GetGroup() - - return self - end - - function FSM_PROCESS:onenterAssigned( ProcessUnit ) - self:T( "Assign" ) - - self.Task:Assign() - end - - function FSM_PROCESS:onenterFailed( ProcessUnit ) - self:T( "Failed" ) - - self.Task:Fail() - end - - function FSM_PROCESS:onenterSuccess( ProcessUnit ) - self:T( "Success" ) - - self.Task:Success() - end - - --- StateMachine callback function for a FSM_PROCESS - -- @param #FSM_PROCESS self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function FSM_PROCESS:onstatechange( ProcessUnit, From, Event, To, Dummy ) - self:T( { ProcessUnit, From, Event, To, Dummy, self:IsTrace() } ) - - if self:IsTrace() then - MESSAGE:New( "@ Process " .. self:GetClassNameAndID() .. " : " .. Event .. " changed to state " .. To, 2 ):ToAll() - end - - self:T( self._Scores[To] ) - -- TODO: This needs to be reworked with a callback functions allocated within Task, and set within the mission script from the Task Objects... - if self._Scores[To] then - - local Task = self.Task - local Scoring = Task:GetScoring() - if Scoring then - Scoring:_AddMissionTaskScore( Task.Mission, ProcessUnit, self._Scores[To].ScoreText, self._Scores[To].Score ) - end - end - end - -end - -do -- FSM_TASK - - --- FSM_TASK class - -- @type FSM_TASK - -- @field Tasking.Task#TASK Task - -- @extends Core.Fsm#FSM - FSM_TASK = { - ClassName = "FSM_TASK", - } - - --- Creates a new FSM_TASK object. - -- @param #FSM_TASK self - -- @param #table FSMT - -- @param Tasking.Task#TASK Task - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #FSM_TASK - function FSM_TASK:New( FSMT ) - - local self = BASE:Inherit( self, FSM_CONTROLLABLE:New( FSMT ) ) -- Core.Fsm#FSM_TASK - - self["onstatechange"] = self.OnStateChange - - return self - end - - function FSM_TASK:_call_handler( handler, params, EventName ) - if self[handler] then - self:T( "Calling " .. handler ) - self._EventSchedules[EventName] = nil - return self[handler]( self, unpack( params ) ) - end - end - -end -- FSM_TASK - -do -- FSM_SET - - --- FSM_SET class - -- @type FSM_SET - -- @field Core.Set#SET_BASE Set - -- @extends Core.Fsm#FSM - FSM_SET = { - ClassName = "FSM_SET", - } - - --- Creates a new FSM_SET object. - -- @param #FSM_SET self - -- @param #table FSMT Finite State Machine Table - -- @param Set_SET_BASE FSMSet (optional) The Set object that the FSM_SET governs. - -- @return #FSM_SET - function FSM_SET:New( FSMSet ) - - -- Inherits from BASE - self = BASE:Inherit( self, FSM:New() ) -- Core.Fsm#FSM_SET - - if FSMSet then - self:Set( FSMSet ) - end - - return self - end - - --- Sets the SET_BASE object that the FSM_SET governs. - -- @param #FSM_SET self - -- @param Core.Set#SET_BASE FSMSet - -- @return #FSM_SET - function FSM_SET:Set( FSMSet ) - self:F( FSMSet ) - self.Set = FSMSet - end - - --- Gets the SET_BASE object that the FSM_SET governs. - -- @param #FSM_SET self - -- @return Core.Set#SET_BASE - function FSM_SET:Get() - return self.Controllable - end - - function FSM_SET:_call_handler( handler, params, EventName ) - if self[handler] then - self:T( "Calling " .. handler ) - self._EventSchedules[EventName] = nil - return self[handler]( self, self.Set, unpack( params ) ) - end - end - -end -- FSM_SET - ---- 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 - ---- The OBJECT class --- @type OBJECT --- @extends Core.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 Dcs.DCSWrapper.Object#Object ObjectName The Object name --- @return #OBJECT self -function OBJECT:New( ObjectName, Test ) - local self = BASE:Inherit( self, BASE:New() ) - self:F2( ObjectName ) - self.ObjectName = ObjectName - - return self -end - - ---- Returns the unit's unique identifier. --- @param Wrapper.Object#OBJECT self --- @return Dcs.DCSWrapper.Object#Object.ID ObjectID --- @return #nil The DCS Object is not existing or alive. -function OBJECT:GetID() - self:F2( self.ObjectName ) - - local DCSObject = self:GetDCSObject() - - if DCSObject then - local ObjectID = DCSObject:getID() - return ObjectID - end - - return nil -end - ---- Destroys the OBJECT. --- @param #OBJECT self --- @return #nil The DCS Unit is not existing or alive. -function OBJECT:Destroy() - self:F2( self.ObjectName ) - - local DCSObject = self:GetDCSObject() - - if DCSObject then - - DCSObject:destroy() - end - - return nil -end - - - - ---- This module contains the IDENTIFIABLE class. --- --- 1) @{#IDENTIFIABLE} class, extends @{Object#OBJECT} --- =============================================================== --- The @{#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.New}(): Create a IDENTIFIABLE instance. --- --- 1.2) IDENTIFIABLE methods: --- -------------------------- --- The following methods can be used to identify an identifiable object: --- --- * @{#IDENTIFIABLE.GetName}(): Returns the name of the Identifiable. --- * @{#IDENTIFIABLE.IsAlive}(): Returns if the Identifiable is alive. --- * @{#IDENTIFIABLE.GetTypeName}(): Returns the type name of the Identifiable. --- * @{#IDENTIFIABLE.GetCoalition}(): Returns the coalition of the Identifiable. --- * @{#IDENTIFIABLE.GetCountry}(): Returns the country of the Identifiable. --- * @{#IDENTIFIABLE.GetDesc}(): Returns the descriptor structure of the Identifiable. --- --- --- === --- --- @module Identifiable - ---- The IDENTIFIABLE class --- @type IDENTIFIABLE --- @extends Wrapper.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 Dcs.DCSWrapper.Identifiable#Identifiable IdentifiableName The DCS Identifiable name --- @return #IDENTIFIABLE self -function IDENTIFIABLE:New( IdentifiableName ) - local self = BASE:Inherit( self, OBJECT:New( IdentifiableName ) ) - self:F2( IdentifiableName ) - self.IdentifiableName = IdentifiableName - return self -end - ---- Returns if the Identifiable is alive. --- @param #IDENTIFIABLE self --- @return #boolean true if Identifiable is alive. --- @return #nil The DCS Identifiable is not existing or alive. -function IDENTIFIABLE:IsAlive() - self:F3( 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 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 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 category of the DCS Identifiable. --- @param #IDENTIFIABLE self --- @return Dcs.DCSWrapper.Object#Object.Category The category ID -function IDENTIFIABLE:GetCategory() - self:F2( self.ObjectName ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - local ObjectCategory = DCSObject:getCategory() - self:T3( ObjectCategory ) - return ObjectCategory - end - - return nil -end - - ---- Returns the DCS Identifiable category name as defined within the DCS Identifiable Descriptor. --- @param #IDENTIFIABLE self --- @return #string The DCS Identifiable Category Name -function IDENTIFIABLE:GetCategoryName() - local DCSIdentifiable = self:GetDCSObject() - - if DCSIdentifiable then - local IdentifiableCategoryName = _CategoryName[ self:GetDesc().category ] - return IdentifiableCategoryName - end - - self:E( self.ClassName .. " " .. self.IdentifiableName .. " not found!" ) - return nil -end - ---- Returns coalition of the Identifiable. --- @param #IDENTIFIABLE self --- @return Dcs.DCSCoalitionWrapper.Object#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 self --- @return Dcs.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 self --- @return Dcs.DCSWrapper.Identifiable#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 - ---- Gets the CallSign of the IDENTIFIABLE, which is a blank by default. --- @param #IDENTIFIABLE self --- @return #string The CallSign of the IDENTIFIABLE. -function IDENTIFIABLE:GetCallsign() - return '' -end - - -function IDENTIFIABLE:GetThreatLevel() - - return 0, "Scenery" -end ---- This module contains the POSITIONABLE class. --- --- 1) @{Positionable#POSITIONABLE} class, extends @{Identifiable#IDENTIFIABLE} --- =========================================================== --- The @{Positionable#POSITIONABLE} class is a wrapper class to handle the POSITIONABLE objects: --- --- * Support all DCS APIs. --- * Enhance with POSITIONABLE specific APIs not in the DCS API set. --- * Manage the "state" of the POSITIONABLE. --- --- 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 - ---- The POSITIONABLE class --- @type POSITIONABLE --- @extends Wrapper.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 Dcs.DCSWrapper.Positionable#Positionable PositionableName The POSITIONABLE name --- @return #POSITIONABLE self -function POSITIONABLE:New( PositionableName ) - local self = BASE:Inherit( self, IDENTIFIABLE:New( PositionableName ) ) - - self.PositionableName = PositionableName - return self -end - ---- Returns the @{DCSTypes#Position3} position vectors indicating the point and direction vectors in 3D of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Position The 3D position vectors of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetPositionVec3() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionablePosition = DCSPositionable:getPosition().p - self:T3( PositionablePosition ) - return PositionablePosition - end - - return nil -end - ---- Returns the @{DCSTypes#Vec2} vector indicating the point in 2D of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Vec2 The 2D point vector of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetVec2() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionableVec3 = DCSPositionable:getPosition().p - - local PositionableVec2 = {} - PositionableVec2.x = PositionableVec3.x - PositionableVec2.y = PositionableVec3.z - - self:T2( PositionableVec2 ) - return PositionableVec2 - end - - return nil -end - ---- Returns a POINT_VEC2 object indicating the point in 2D of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Core.Point#POINT_VEC2 The 2D point vector of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetPointVec2() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionableVec3 = DCSPositionable:getPosition().p - - local PositionablePointVec2 = POINT_VEC2:NewFromVec3( PositionableVec3 ) - - self:T2( PositionablePointVec2 ) - return PositionablePointVec2 - end - - return nil -end - ---- Returns a POINT_VEC3 object indicating the point in 3D of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Core.Point#POINT_VEC3 The 3D point vector of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetPointVec3() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionableVec3 = self:GetPositionVec3() - - local PositionablePointVec3 = POINT_VEC3:NewFromVec3( PositionableVec3 ) - - self:T2( PositionablePointVec3 ) - return PositionablePointVec3 - end - - return nil -end - - ---- Returns a random @{DCSTypes#Vec3} vector within a range, indicating the point in 3D of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Vec3 The 3D point vector of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetRandomVec3( Radius ) - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionablePointVec3 = DCSPositionable:getPosition().p - local PositionableRandomVec3 = {} - local angle = math.random() * math.pi*2; - PositionableRandomVec3.x = PositionablePointVec3.x + math.cos( angle ) * math.random() * Radius; - PositionableRandomVec3.y = PositionablePointVec3.y - PositionableRandomVec3.z = PositionablePointVec3.z + math.sin( angle ) * math.random() * Radius; - - self:T3( PositionableRandomVec3 ) - return PositionableRandomVec3 - end - - return nil -end - ---- Returns the @{DCSTypes#Vec3} vector indicating the 3D vector of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Vec3 The 3D point vector of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetVec3() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionableVec3 = DCSPositionable:getPosition().p - self:T3( PositionableVec3 ) - return PositionableVec3 - end - - return nil -end - ---- Returns the altitude of the POSITIONABLE. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Distance The altitude of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetAltitude() - self:F2() - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionablePointVec3 = DCSPositionable:getPoint() --Dcs.DCSTypes#Vec3 - return PositionablePointVec3.y - end - - return nil -end - ---- Returns if the Positionable is located above a runway. --- @param Wrapper.Positionable#POSITIONABLE self --- @return #boolean true if Positionable is above a runway. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:IsAboveRunway() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - - local Vec2 = self:GetVec2() - local SurfaceType = land.getSurfaceType( Vec2 ) - local IsAboveRunway = SurfaceType == land.SurfaceType.RUNWAY - - self:T2( IsAboveRunway ) - return IsAboveRunway - end - - return nil -end - - - ---- Returns the POSITIONABLE heading in degrees. --- @param Wrapper.Positionable#POSITIONABLE self --- @return #number The POSTIONABLE 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 - PositionableHeading = PositionableHeading * 180 / math.pi - self:T2( PositionableHeading ) - return PositionableHeading - end - end - - return nil -end - - ---- Returns true if the POSITIONABLE is in the air. --- Polymorphic, is overridden in GROUP and UNIT. --- @param Wrapper.Positionable#POSITIONABLE self --- @return #boolean true if in the air. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:InAir() - self:F2( self.PositionableName ) - - return nil -end - - ---- Returns the POSITIONABLE velocity vector. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Vec3 The velocity vector --- @return #nil The 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 - ---- Returns the POSITIONABLE velocity in km/h. --- @param Wrapper.Positionable#POSITIONABLE self --- @return #number The velocity in km/h --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetVelocityKMH() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local VelocityVec3 = self:GetVelocity() - local Velocity = ( VelocityVec3.x ^ 2 + VelocityVec3.y ^ 2 + VelocityVec3.z ^ 2 ) ^ 0.5 -- in meters / sec - local Velocity = Velocity * 3.6 -- now it is in km/h. - self:T3( Velocity ) - return Velocity - end - - return nil -end - ---- Returns a message with the callsign embedded (if there is one). --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTypes#Duration Duration The duration of the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. --- @return Core.Message#MESSAGE -function POSITIONABLE:GetMessage( Message, Duration, Name ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - Name = Name or self:GetTypeName() - return MESSAGE:New( Message, Duration, self:GetCallsign() .. " (" .. Name .. ")" ) - end - - return nil -end - ---- Send a message to all coalitions. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTypes#Duration Duration The duration of the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. -function POSITIONABLE:MessageToAll( Message, Duration, Name ) - self:F2( { Message, Duration } ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - self:GetMessage( Message, Duration, Name ):ToAll() - end - - return nil -end - ---- Send a message to a coalition. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTYpes#Duration Duration The duration of the message. --- @param Dcs.DCScoalition#coalition MessageCoalition The Coalition receiving the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. -function POSITIONABLE:MessageToCoalition( Message, Duration, MessageCoalition, Name ) - self:F2( { Message, Duration } ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - self:GetMessage( Message, Duration, Name ):ToCoalition( MessageCoalition ) - end - - return nil -end - - ---- Send a message to the red coalition. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTYpes#Duration Duration The duration of the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. -function POSITIONABLE:MessageToRed( Message, Duration, Name ) - self:F2( { Message, Duration } ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - self:GetMessage( Message, Duration, Name ):ToRed() - end - - return nil -end - ---- Send a message to the blue coalition. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTypes#Duration Duration The duration of the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. -function POSITIONABLE:MessageToBlue( Message, Duration, Name ) - self:F2( { Message, Duration } ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - self:GetMessage( Message, Duration, Name ):ToBlue() - end - - return nil -end - ---- Send a message to a client. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTypes#Duration Duration The duration of the message. --- @param Wrapper.Client#CLIENT Client The client object receiving the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. -function POSITIONABLE:MessageToClient( Message, Duration, Client, Name ) - self:F2( { Message, Duration } ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - self:GetMessage( Message, Duration, Name ):ToClient( Client ) - end - - return nil -end - ---- Send a message to a @{Group}. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTypes#Duration Duration The duration of the message. --- @param Wrapper.Group#GROUP MessageGroup The GROUP object receiving the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. -function POSITIONABLE:MessageToGroup( Message, Duration, MessageGroup, Name ) - self:F2( { Message, Duration } ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - if DCSObject:isExist() then - self:GetMessage( Message, Duration, Name ):ToGroup( MessageGroup ) - end - end - - return nil -end - ---- Send a message to the players in the @{Group}. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTypes#Duration Duration The duration of the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. -function POSITIONABLE:Message( Message, Duration, Name ) - self:F2( { Message, Duration } ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - self:GetMessage( Message, Duration, Name ):ToGroup( self ) - end - - return nil -end - - - - - ---- This module contains the CONTROLLABLE class. --- --- 1) @{Controllable#CONTROLLABLE} class, extends @{Positionable#POSITIONABLE} --- =========================================================== --- 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 some or all ammunition at a VEC2 point. --- * @{#CONTROLLABLE.TaskFollow}: (AIR) Following another airborne controllable. --- * @{#CONTROLLABLE.TaskHold}: (GROUND) Hold ground controllable from moving. --- * @{#CONTROLLABLE.TaskHoldPosition}: (AIR) Hold position at the current position of the first unit of the controllable. --- * @{#CONTROLLABLE.TaskLand}: (AIR HELICOPTER) Landing at the ground. For helicopters only. --- * @{#CONTROLLABLE.TaskLandAtZone}: (AIR) Land the controllable at a @{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 - ---- The CONTROLLABLE class --- @type CONTROLLABLE --- @extends Wrapper.Positionable#POSITIONABLE --- @field Dcs.DCSWrapper.Controllable#Controllable DCSControllable The DCS controllable class. --- @field #string ControllableName The name of the controllable. -CONTROLLABLE = { - ClassName = "CONTROLLABLE", - ControllableName = "", - WayPointFunctions = {}, -} - ---- Create a new CONTROLLABLE from a DCSControllable --- @param #CONTROLLABLE self --- @param Dcs.DCSWrapper.Controllable#Controllable ControllableName The DCS Controllable name --- @return #CONTROLLABLE self -function CONTROLLABLE:New( ControllableName ) - local self = BASE:Inherit( self, POSITIONABLE:New( ControllableName ) ) - self:F2( ControllableName ) - self.ControllableName = ControllableName - - self.TaskScheduler = SCHEDULER:New( self ) - return self -end - --- DCS Controllable methods support. - ---- Get the controller for the CONTROLLABLE. --- @param #CONTROLLABLE self --- @return Dcs.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 - --- Get methods - ---- Returns the UNITs wrappers of the DCS Units of the Controllable (default is a GROUP). --- @param #CONTROLLABLE self --- @return #list The UNITs wrappers. -function CONTROLLABLE:GetUnits() - self:F2( { self.ControllableName } ) - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - local DCSUnits = DCSControllable:getUnits() - local Units = {} - for Index, UnitData in pairs( DCSUnits ) do - Units[#Units+1] = UNIT:Find( UnitData ) - end - self:T3( Units ) - return Units - end - - return nil -end - - ---- Returns the health. Dead controllables have health <= 1.0. --- @param #CONTROLLABLE self --- @return #number The controllable health value (unit or group average). --- @return #nil The controllable is not existing or alive. -function CONTROLLABLE:GetLife() - self:F2( self.ControllableName ) - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - local UnitLife = 0 - local Units = self:GetUnits() - if #Units == 1 then - local Unit = Units[1] -- Wrapper.Unit#UNIT - UnitLife = Unit:GetLife() - else - local UnitLifeTotal = 0 - for UnitID, Unit in pairs( Units ) do - local Unit = Unit -- Wrapper.Unit#UNIT - UnitLifeTotal = UnitLifeTotal + Unit:GetLife() - end - UnitLife = UnitLifeTotal / #Units - end - return UnitLife - end - - return nil -end - - - --- Tasks - ---- Popping current Task from the controllable. --- @param #CONTROLLABLE self --- @return Wrapper.Controllable#CONTROLLABLE self -function CONTROLLABLE:PopCurrentTask() - self:F2() - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - local Controller = self:_GetController() - Controller:popTask() - return self - end - - return nil -end - ---- Pushing Task on the queue from the controllable. --- @param #CONTROLLABLE self --- @return Wrapper.Controllable#CONTROLLABLE self -function CONTROLLABLE:PushTask( DCSTask, WaitTime ) - self:F2() - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - local 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 - self.TaskScheduler:Schedule( 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 Wrapper.Controllable#CONTROLLABLE self -function CONTROLLABLE:SetTask( DCSTask, WaitTime ) - self:F2( { DCSTask } ) - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - - local Controller = self:_GetController() - self:T3( Controller ) - - -- 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 - Controller:setTask( DCSTask ) - else - self.TaskScheduler:Schedule( Controller, Controller.setTask, { DCSTask }, WaitTime ) - end - - return self - end - - return nil -end - - ---- Return a condition section for a controlled task. --- @param #CONTROLLABLE self --- @param Dcs.DCSTime#Time time --- @param #string userFlag --- @param #boolean userFlagValue --- @param #string condition --- @param Dcs.DCSTime#Time duration --- @param #number lastWayPoint --- return Dcs.DCSTasking.Task#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 Dcs.DCSTasking.Task#Task DCSTask --- @param #DCSStopCondition DCSStopCondition --- @return Dcs.DCSTasking.Task#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 Dcs.DCSTasking.Task#TaskArray DCSTasks Array of @{DCSTasking.Task#Task} --- @return Dcs.DCSTasking.Task#Task -function CONTROLLABLE:TaskCombo( DCSTasks ) - self:F2( { DCSTasks } ) - - local DCSTaskCombo - - DCSTaskCombo = { - id = 'ComboTask', - params = { - tasks = DCSTasks - } - } - - for TaskID, Task in ipairs( DCSTasks ) do - self:E( Task ) - end - - self:T3( { DCSTaskCombo } ) - return DCSTaskCombo -end - ---- Return a WrappedAction Task taking a Command. --- @param #CONTROLLABLE self --- @param Dcs.DCSCommand#Command DCSCommand --- @return Dcs.DCSTasking.Task#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 Dcs.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 Dcs.DCSTasking.Task#Task --- @usage --- --- This test demonstrates the use(s) of the SwitchWayPoint method of the GROUP class. --- HeliGroup = GROUP:FindByName( "Helicopter" ) --- --- --- Route the helicopter back to the FARP after 60 seconds. --- -- We use the SCHEDULER class to do this. --- SCHEDULER:New( nil, --- function( HeliGroup ) --- local CommandRTB = HeliGroup:CommandSwitchWayPoint( 2, 8 ) --- HeliGroup:SetCommand( CommandRTB ) --- end, { HeliGroup }, 90 --- ) -function CONTROLLABLE:CommandSwitchWayPoint( FromWayPoint, ToWayPoint ) - self:F2( { FromWayPoint, ToWayPoint } ) - - local CommandSwitchWayPoint = { - id = 'SwitchWaypoint', - params = { - fromWaypointIndex = FromWayPoint, - goToWaypointIndex = ToWayPoint, - }, - } - - self:T3( { CommandSwitchWayPoint } ) - return CommandSwitchWayPoint -end - ---- Perform stop route command --- @param #CONTROLLABLE self --- @param #boolean StopRoute --- @return Dcs.DCSTasking.Task#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 Wrapper.Controllable#CONTROLLABLE AttackGroup The Controllable to be attacked. --- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. --- @param Dcs.DCSTypes#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. --- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. --- @param Dcs.DCSTypes#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. --- @param Dcs.DCSTypes#Distance Altitude (optional) Desired attack start altitude. Controllable/aircraft will make its attacks from the altitude. If the altitude is too low or too high to use weapon aircraft/controllable will choose closest altitude to the desired attack start altitude. If the desired altitude is defined controllable/aircraft will not attack from safe altitude. --- @param #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 Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:TaskAttackGroup( AttackGroup, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit ) - self:F2( { self.ControllableName, AttackGroup, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit } ) - - -- AttackControllable = { - -- id = 'AttackControllable', - -- params = { - -- groupId = Group.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 = { - groupId = AttackGroup: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 Wrapper.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 Dcs.DCSTypes#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. --- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. --- @param Dcs.DCSTypes#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. --- @param #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 Dcs.DCSTasking.Task#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 = { - altitudeEnabled = true, - unitId = AttackUnit:GetID(), - attackQtyLimit = AttackQtyLimit or false, - attackQty = AttackQty or 2, - expend = WeaponExpend or "Auto", - altitude = 2000, - directionEnabled = true, - groupAttack = true, - --weaponType = WeaponType or 1073741822, - direction = Direction or 0, - } - } - - self:E( DCSTask ) - - return DCSTask -end - - ---- (AIR) Delivering weapon at the point on the ground. --- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Vec2 Vec2 2D-coordinates of the point to deliver weapon at. --- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. --- @param Dcs.DCSTypes#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. --- @param #number AttackQty (optional) Desired quantity of passes. The parameter is not the same in AttackControllable and AttackUnit tasks. --- @param Dcs.DCSTypes#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. --- @param #boolean ControllableAttack (optional) Flag indicates that the target must be engaged by all aircrafts of the controllable. Has effect only if the task is assigned to a controllable, not to a single aircraft. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:TaskBombing( Vec2, WeaponType, WeaponExpend, AttackQty, Direction, ControllableAttack ) - self:F2( { self.ControllableName, Vec2, 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 = Vec2, - 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 Dcs.DCSTypes#Vec2 Point The point to hold the position. --- @param #number Altitude The altitude to hold the position. --- @param #number Speed The speed flying when holding the position. --- @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:GetVec2() - 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 Dcs.DCSTypes#Vec2 Vec2 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 Dcs.DCSTypes#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. --- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. --- @param Dcs.DCSTypes#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. --- @param #boolean ControllableAttack (optional) Flag indicates that the target must be engaged by all aircrafts of the controllable. Has effect only if the task is assigned to a controllable, not to a single aircraft. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:TaskAttackMapObject( Vec2, WeaponType, WeaponExpend, AttackQty, Direction, ControllableAttack ) - self:F2( { self.ControllableName, Vec2, 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 = Vec2, - 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 Wrapper.Airbase#AIRBASE Airbase Airbase to attack. --- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. --- @param Dcs.DCSTypes#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. --- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. --- @param Dcs.DCSTypes#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. --- @param #boolean ControllableAttack (optional) Flag indicates that the target must be engaged by all aircrafts of the controllable. Has effect only if the task is assigned to a controllable, not to a single aircraft. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -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 Dcs.DCSTasking.Task#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 Dcs.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 Core.Zone#ZONE Zone The zone where to land. --- @param #number Duration The duration in seconds to stay on the ground. --- @return #CONTROLLABLE self -function CONTROLLABLE:TaskLandAtZone( Zone, Duration, RandomPoint ) - self:F2( { self.ControllableName, Zone, Duration, RandomPoint } ) - - local Point - if RandomPoint then - Point = Zone:GetRandomVec2() - else - Point = Zone:GetVec2() - 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 Wrapper.Controllable#CONTROLLABLE FollowControllable The controllable to be followed. --- @param Dcs.DCSTypes#Vec3 Vec3 Position of the unit / lead unit of the controllable relative lead unit of another controllable in frame reference oriented by course of lead unit of another controllable. If another controllable is on land the unit / controllable will orbit around. --- @param #number LastWaypointIndex Detach waypoint of another controllable. Once reached the unit / controllable Follow task is finished. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:TaskFollow( FollowControllable, Vec3, LastWaypointIndex ) - self:F2( { self.ControllableName, FollowControllable, Vec3, LastWaypointIndex } ) - --- Follow = { --- id = 'Follow', --- params = { --- groupId = Group.ID, --- pos = Vec3, --- lastWptIndexFlag = boolean, --- lastWptIndex = number --- } --- } - - local LastWaypointIndexFlag = false - if LastWaypointIndex then - LastWaypointIndexFlag = true - end - - local DCSTask - DCSTask = { - id = 'Follow', - params = { - groupId = FollowControllable:GetID(), - pos = Vec3, - 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 Wrapper.Controllable#CONTROLLABLE EscortControllable The controllable to be escorted. --- @param Dcs.DCSTypes#Vec3 Vec3 Position of the unit / lead unit of the controllable relative lead unit of another controllable in frame reference oriented by course of lead unit of another controllable. If another controllable is on land the unit / controllable will orbit around. --- @param #number LastWaypointIndex Detach waypoint of another controllable. Once reached the unit / controllable Follow task is finished. --- @param #number EngagementDistanceMax Maximal distance from escorted controllable to threat. If the threat is already engaged by escort escort will disengage if the distance becomes greater than 1.5 * engagementDistMax. --- @param Dcs.DCSTypes#AttributeNameArray TargetTypes Array of AttributeName that is contains threat categories allowed to engage. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:TaskEscort( FollowControllable, Vec3, LastWaypointIndex, EngagementDistance, TargetTypes ) - self:F2( { self.ControllableName, FollowControllable, Vec3, LastWaypointIndex, EngagementDistance, TargetTypes } ) - --- Escort = { --- id = 'Escort', --- params = { --- groupId = Group.ID, --- pos = Vec3, --- lastWptIndexFlag = boolean, --- lastWptIndex = number, --- engagementDistMax = Distance, --- targetTypes = array of AttributeName, --- } --- } - - local LastWaypointIndexFlag = false - if LastWaypointIndex then - LastWaypointIndexFlag = true - end - - local DCSTask - DCSTask = { id = 'Escort', - params = { - groupId = FollowControllable:GetID(), - pos = Vec3, - 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 Dcs.DCSTypes#Vec2 Vec2 The point to fire at. --- @param Dcs.DCSTypes#Distance Radius The radius of the zone to deploy the fire at. --- @param #number AmmoCount (optional) Quantity of ammunition to expand (omit to fire until ammunition is depleted). --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:TaskFireAtPoint( Vec2, Radius, AmmoCount ) - self:F2( { self.ControllableName, Vec2, Radius, AmmoCount } ) - - -- FireAtPoint = { - -- id = 'FireAtPoint', - -- params = { - -- point = Vec2, - -- radius = Distance, - -- expendQty = number, - -- expendQtyEnabled = boolean, - -- } - -- } - - local DCSTask - DCSTask = { id = 'FireAtPoint', - params = { - point = Vec2, - radius = Radius, - expendQty = 100, -- dummy value - expendQtyEnabled = false, - } - } - - if AmmoCount then - DCSTask.params.expendQty = AmmoCount - DCSTask.params.expendQtyEnabled = true - end - - self:T3( { DCSTask } ) - return DCSTask -end - ---- (GROUND) Hold ground controllable from moving. --- @param #CONTROLLABLE self --- @return Dcs.DCSTasking.Task#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 Wrapper.Controllable#CONTROLLABLE AttackGroup Target CONTROLLABLE. --- @param #number WeaponType Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. --- @param Dcs.DCSTypes#AI.Task.Designation Designation (optional) Designation type. --- @param #boolean Datalink (optional) Allows to use datalink to send the target information to attack aircraft. Enabled by default. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:TaskFAC_AttackGroup( AttackGroup, WeaponType, Designation, Datalink ) - self:F2( { self.ControllableName, AttackGroup, WeaponType, Designation, Datalink } ) - --- FAC_AttackControllable = { --- id = 'FAC_AttackControllable', --- params = { --- groupId = Group.ID, --- weaponType = number, --- designation = enum AI.Task.Designation, --- datalink = boolean --- } --- } - - local DCSTask - DCSTask = { id = 'FAC_AttackControllable', - params = { - groupId = AttackGroup:GetID(), - weaponType = WeaponType, - designation = Designation, - datalink = Datalink, - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - --- EN-ACT_ROUTE TASKS FOR AIRBORNE CONTROLLABLES - ---- (AIR) Engaging targets of defined types. --- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Distance Distance Maximal distance from the target to a route leg. If the target is on a greater distance it will be ignored. --- @param Dcs.DCSTypes#AttributeNameArray TargetTypes Array of target categories allowed to engage. --- @param #number Priority All enroute tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -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 Dcs.DCSTypes#Vec2 Vec2 2D-coordinates of the zone. --- @param Dcs.DCSTypes#Distance Radius Radius of the zone. --- @param Dcs.DCSTypes#AttributeNameArray TargetTypes Array of target categories allowed to engage. --- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskEngageTargets( Vec2, Radius, TargetTypes, Priority ) - self:F2( { self.ControllableName, Vec2, Radius, TargetTypes, Priority } ) - --- EngageTargetsInZone = { --- id = 'EngageTargetsInZone', --- params = { --- point = Vec2, --- zoneRadius = Distance, --- targetTypes = array of AttributeName, --- priority = number --- } --- } - - local DCSTask - DCSTask = { id = 'EngageTargetsInZone', - params = { - point = Vec2, - 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 Wrapper.Controllable#CONTROLLABLE AttackGroup The Controllable to be attacked. --- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. --- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. --- @param Dcs.DCSTypes#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. --- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. --- @param Dcs.DCSTypes#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. --- @param Dcs.DCSTypes#Distance Altitude (optional) Desired attack start altitude. Controllable/aircraft will make its attacks from the altitude. If the altitude is too low or too high to use weapon aircraft/controllable will choose closest altitude to the desired attack start altitude. If the desired altitude is defined controllable/aircraft will not attack from safe altitude. --- @param #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 Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskEngageGroup( AttackGroup, Priority, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit ) - self:F2( { self.ControllableName, AttackGroup, Priority, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit } ) - - -- EngageControllable = { - -- id = 'EngageControllable ', - -- params = { - -- groupId = Group.ID, - -- weaponType = number, - -- expend = enum AI.Task.WeaponExpend, - -- attackQty = number, - -- directionEnabled = boolean, - -- direction = Azimuth, - -- altitudeEnabled = boolean, - -- altitude = Distance, - -- attackQtyLimit = boolean, - -- priority = number, - -- } - -- } - - local DirectionEnabled = nil - if Direction then - DirectionEnabled = true - end - - local AltitudeEnabled = nil - if Altitude then - AltitudeEnabled = true - end - - local DCSTask - DCSTask = { id = 'EngageControllable', - params = { - groupId = AttackGroup:GetID(), - weaponType = WeaponType, - expend = WeaponExpend, - attackQty = AttackQty, - directionEnabled = DirectionEnabled, - direction = Direction, - altitudeEnabled = AltitudeEnabled, - altitude = Altitude, - attackQtyLimit = AttackQtyLimit, - priority = Priority, - }, - }, - - self:T3( { DCSTask } ) - return DCSTask -end - - ---- (AIR) Attack the Unit. --- @param #CONTROLLABLE self --- @param Wrapper.Unit#UNIT EngageUnit The UNIT. --- @param #number Priority (optional) All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. --- @param #boolean GroupAttack (optional) If true, all units in the group will attack the Unit when found. --- @param Dcs.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 Dcs.DCSTypes#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. --- @param Dcs.DCSTypes#Distance Altitude (optional) Desired altitude to perform the unit engagement. --- @param #boolean Visible (optional) Unit must be visible. --- @param #boolean ControllableAttack (optional) Flag indicates that the target must be engaged by all aircrafts of the controllable. Has effect only if the task is assigned to a controllable, not to a single aircraft. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskEngageUnit( EngageUnit, Priority, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, Visible, ControllableAttack ) - self:F2( { self.ControllableName, EngageUnit, Priority, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, Visible, ControllableAttack } ) - - -- EngageUnit = { - -- id = 'EngageUnit', - -- params = { - -- unitId = Unit.ID, - -- weaponType = number, - -- expend = enum AI.Task.WeaponExpend - -- attackQty = number, - -- direction = Azimuth, - -- attackQtyLimit = boolean, - -- controllableAttack = boolean, - -- priority = number, - -- } - -- } - - local DCSTask - DCSTask = { id = 'EngageUnit', - params = { - unitId = EngageUnit:GetID(), - priority = Priority or 1, - groupAttack = GroupAttack or false, - visible = Visible or false, - expend = WeaponExpend or "Auto", - directionEnabled = Direction and true or false, - direction = Direction, - altitudeEnabled = Altitude and true or false, - altitude = Altitude, - attackQtyLimit = AttackQty and true or false, - attackQty = AttackQty, - controllableAttack = ControllableAttack, - }, - }, - - 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 Dcs.DCSTasking.Task#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 Dcs.DCSTasking.Task#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 Dcs.DCSTasking.Task#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 Wrapper.Controllable#CONTROLLABLE AttackGroup Target CONTROLLABLE. --- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. --- @param #number WeaponType Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. --- @param Dcs.DCSTypes#AI.Task.Designation Designation (optional) Designation type. --- @param #boolean Datalink (optional) Allows to use datalink to send the target information to attack aircraft. Enabled by default. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskFAC_EngageGroup( AttackGroup, Priority, WeaponType, Designation, Datalink ) - self:F2( { self.ControllableName, AttackGroup, WeaponType, Priority, Designation, Datalink } ) - --- FAC_EngageControllable = { --- id = 'FAC_EngageControllable', --- params = { --- groupId = Group.ID, --- weaponType = number, --- designation = enum AI.Task.Designation, --- datalink = boolean, --- priority = number, --- } --- } - - local DCSTask - DCSTask = { id = 'FAC_EngageControllable', - params = { - groupId = AttackGroup: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 Dcs.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 Dcs.DCSTasking.Task#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 Dcs.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 Dcs.DCSTasking.Task#Task The DCS task structure -function CONTROLLABLE:TaskEmbarking( Point, Duration, EmbarkingControllable ) - self:F2( { self.ControllableName, Point, Duration, EmbarkingControllable.DCSControllable } ) - - local DCSTask - DCSTask = { id = 'Embarking', - params = { x = Point.x, - y = Point.y, - duration = Duration, - controllablesForEmbarking = { EmbarkingControllable.ControllableID }, - durationFlag = true, - distributionFlag = false, - distribution = {}, - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - ---- (GROUND) Embark to a Transport landed at a location. - ---- Move to a defined Vec2 Point, and embark to a controllable when arrived within a defined Radius. --- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Vec2 Point The point where to wait. --- @param #number Radius The radius of the embarking zone around the Point. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:TaskEmbarkToTransport( Point, Radius ) - self:F2( { self.ControllableName, Point, Radius } ) - - local DCSTask --Dcs.DCSTasking.Task#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 Dcs.DCSTasking.Task#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 Dcs.DCSTasking.Task#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 Dcs.DCSTypes#Vec3 Point The destination point in Vec3 format. --- @param #number Speed The speed to travel. --- @return #CONTROLLABLE self -function CONTROLLABLE:RouteToVec2( Point, Speed ) - self:F2( { Point, Speed } ) - - local ControllablePoint = self:GetUnit( 1 ):GetVec2() - - local PointFrom = {} - PointFrom.x = ControllablePoint.x - PointFrom.y = ControllablePoint.y - PointFrom.type = "Turning Point" - PointFrom.action = "Turning Point" - PointFrom.speed = Speed - PointFrom.speed_locked = true - PointFrom.properties = { - ["vnav"] = 1, - ["scale"] = 0, - ["angle"] = 0, - ["vangle"] = 0, - ["steer"] = 2, - } - - - local PointTo = {} - PointTo.x = Point.x - PointTo.y = Point.y - PointTo.type = "Turning Point" - PointTo.action = "Fly Over Point" - PointTo.speed = Speed - PointTo.speed_locked = true - PointTo.properties = { - ["vnav"] = 1, - ["scale"] = 0, - ["angle"] = 0, - ["vangle"] = 0, - ["steer"] = 2, - } - - - local Points = { PointFrom, PointTo } - - self:T3( Points ) - - self:Route( Points ) - - return self -end - ---- (AIR + GROUND) Make the Controllable move to a given point. --- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Vec3 Point The destination point in Vec3 format. --- @param #number Speed The speed to travel. --- @return #CONTROLLABLE self -function CONTROLLABLE:RouteToVec3( Point, Speed ) - self:F2( { Point, Speed } ) - - local ControllableVec3 = self:GetUnit( 1 ):GetVec3() - - local PointFrom = {} - PointFrom.x = ControllableVec3.x - PointFrom.y = ControllableVec3.z - PointFrom.alt = ControllableVec3.y - PointFrom.alt_type = "BARO" - PointFrom.type = "Turning Point" - PointFrom.action = "Turning Point" - PointFrom.speed = Speed - PointFrom.speed_locked = true - PointFrom.properties = { - ["vnav"] = 1, - ["scale"] = 0, - ["angle"] = 0, - ["vangle"] = 0, - ["steer"] = 2, - } - - - local PointTo = {} - PointTo.x = Point.x - PointTo.y = Point.z - PointTo.alt = Point.y - PointTo.alt_type = "BARO" - PointTo.type = "Turning Point" - PointTo.action = "Fly Over Point" - PointTo.speed = Speed - PointTo.speed_locked = true - PointTo.properties = { - ["vnav"] = 1, - ["scale"] = 0, - ["angle"] = 0, - ["vangle"] = 0, - ["steer"] = 2, - } - - - local Points = { PointFrom, PointTo } - - self:T3( Points ) - - self:Route( Points ) - - return self -end - - - ---- Make the controllable to follow a given route. --- @param #CONTROLLABLE self --- @param #table 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 ) - self.TaskScheduler:Schedule( 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 Core.Zone#ZONE Zone The zone where to route to. --- @param #boolean Randomize Defines whether to target point gets randomized within the Zone. --- @param #number Speed The speed. --- @param Base#FORMATION Formation The formation string. -function CONTROLLABLE:TaskRouteToZone( Zone, Randomize, Speed, Formation ) - self:F2( Zone ) - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - - local ControllablePoint = self:GetVec2() - - local PointFrom = {} - PointFrom.x = ControllablePoint.x - PointFrom.y = ControllablePoint.y - PointFrom.type = "Turning Point" - PointFrom.action = "Cone" - PointFrom.speed = 20 / 1.6 - - - local PointTo = {} - local ZonePoint - - if Randomize then - ZonePoint = Zone:GetRandomVec2() - else - ZonePoint = Zone:GetVec2() - end - - PointTo.x = ZonePoint.x - PointTo.y = ZonePoint.y - PointTo.type = "Turning Point" - - if Formation then - PointTo.action = Formation - else - PointTo.action = "Cone" - end - - if Speed then - PointTo.speed = Speed - else - PointTo.speed = 20 / 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 Wrapper.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:GetVec2() - 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:GetVec2() - - 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 Wrapper.Controllable#CONTROLLABLE self --- @param #boolean DetectVisual (optional) --- @param #boolean DetectOptical (optional) --- @param #boolean DetectRadar (optional) --- @param #boolean DetectIRST (optional) --- @param #boolean DetectRWR (optional) --- @param #boolean DetectDLINK (optional) --- @return #table DetectedTargets -function CONTROLLABLE:GetDetectedTargets( DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK ) - self:F2( self.ControllableName ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local DetectionVisual = ( DetectVisual and DetectVisual == true ) and Controller.Detection.VISUAL or nil - local DetectionOptical = ( DetectOptical and DetectOptical == true ) and Controller.Detection.OPTICAL or nil - local DetectionRadar = ( DetectRadar and DetectRadar == true ) and Controller.Detection.RADAR or nil - local DetectionIRST = ( DetectIRST and DetectIRST == true ) and Controller.Detection.IRST or nil - local DetectionRWR = ( DetectRWR and DetectRWR == true ) and Controller.Detection.RWR or nil - local DetectionDLINK = ( DetectDLINK and DetectDLINK == true ) and Controller.Detection.DLINK or nil - - - 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 Wrapper.Controllable#CONTROLLABLE self --- @return Wrapper.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 ) - self:F( { WayPoints } ) - - if WayPoints then - self.WayPoints = WayPoints - else - self.WayPoints = self:GetTaskRoute() - end - - return self -end - ---- Get the current WayPoints set with the WayPoint functions( Note that the WayPoints can be nil, although there ARE waypoints). --- @param #CONTROLLABLE self --- @return #table WayPoints If WayPoints is given, then return the WayPoints structure. -function CONTROLLABLE:GetWayPoints() - self:F( ) - - if self.WayPoints then - return self.WayPoints - end - - return nil -end - ---- Registers a waypoint function that will be executed when the controllable moves over the WayPoint. --- @param #CONTROLLABLE self --- @param #number WayPoint The waypoint number. Note that the start waypoint on the route is WayPoint 1! --- @param #number WayPointIndex When defining multiple WayPoint functions for one WayPoint, use WayPointIndex to set the sequence of actions. --- @param #function WayPointFunction The waypoint function to be called when the controllable moves over the waypoint. The waypoint function takes variable parameters. --- @return #CONTROLLABLE -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 = GROUP: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 ) - self:F( { WayPoint, WaitTime } ) - - if not WayPoint then - WayPoint = 1 - end - - -- When starting the mission from a certain point, the TaskPoints need to be deleted before the given WayPoint. - for TaskPointID = 1, WayPoint - 1 do - table.remove( self.WayPoints, 1 ) - end - - self:T3( self.WayPoints ) - - self:SetTask( self:TaskRoute( self.WayPoints ), WaitTime ) - - return self -end - --- Message APIs--- 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 --- --- A GROUP is a @{Controllable}. See the @{Controllable} task methods section for a description of the task methods. --- --- ### 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: --- --- * @{Controllable#CONTROLLABLE.TaskMission}: (AIR + GROUND) Return a mission task from a mission template. --- --- ## 1.3) GROUP Command methods --- --- A GROUP is a @{Controllable}. See the @{Controllable} command methods section for a description of the command methods. --- --- ## 1.4) GROUP option methods --- --- A GROUP is a @{Controllable}. See the @{Controllable} option methods section for a description of the option methods. --- --- ## 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. --- --- ## 1.6) GROUP AI methods --- --- A GROUP has AI methods to control the AI activation. --- --- * @{#GROUP.SetAIOnOff}(): Turns the GROUP AI On or Off. --- * @{#GROUP.SetAIOn}(): Turns the GROUP AI On. --- * @{#GROUP.SetAIOff}(): Turns the GROUP AI Off. --- --- ==== --- --- # **API CHANGE HISTORY** --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- Hereby the change log: --- --- 2017-01-24: GROUP:**SetAIOnOff( AIOnOff )** added. --- --- 2017-01-24: GROUP:**SetAIOn()** added. --- --- 2017-01-24: GROUP:**SetAIOff()** added. --- --- === --- --- # **AUTHORS and CONTRIBUTIONS** --- --- ### Contributions: --- --- * [**Entropy**](https://forums.eagle.ru/member.php?u=111471), **Afinegan**: Came up with the requirement for AIOnOff(). --- --- ### Authors: --- --- * **FlightControl**: Design & Programming --- --- @module Group --- @author FlightControl - ---- The GROUP class --- @type GROUP --- @extends Wrapper.Controllable#CONTROLLABLE --- @field #string GroupName The name of the group. -GROUP = { - ClassName = "GROUP", -} - ---- Create a new GROUP from a DCSGroup --- @param #GROUP self --- @param Dcs.DCSWrapper.Group#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 - - self:SetEventPriority( 4 ) - return self -end - --- Reference methods. - ---- Find the GROUP wrapper class instance using the DCS Group. --- @param #GROUP self --- @param Dcs.DCSWrapper.Group#Group DCSGroup The DCS Group. --- @return #GROUP The GROUP. -function GROUP:Find( DCSGroup ) - - local GroupName = DCSGroup:getName() -- Wrapper.Group#GROUP - local GroupFound = _DATABASE:FindGroup( GroupName ) - return GroupFound -end - ---- Find the created GROUP using the DCS Group Name. --- @param #GROUP self --- @param #string GroupName The DCS Group Name. --- @return #GROUP The GROUP. -function GROUP:FindByName( GroupName ) - - local GroupFound = _DATABASE:FindGroup( GroupName ) - return GroupFound -end - --- DCS Group methods support. - ---- Returns the DCS Group. --- @param #GROUP self --- @return Dcs.DCSWrapper.Group#Group The DCS Group. -function GROUP:GetDCSObject() - local DCSGroup = Group.getByName( self.GroupName ) - - if DCSGroup then - return DCSGroup - end - - return nil -end - ---- Returns the @{DCSTypes#Position3} position vectors indicating the point and direction vectors in 3D of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Position The 3D position vectors of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function GROUP:GetPositionVec3() -- Overridden from POSITIONABLE:GetPositionVec3() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionablePosition = DCSPositionable:getUnits()[1]:getPosition().p - self:T3( PositionablePosition ) - return PositionablePosition - 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() and DCSGroup:getUnit(1) ~= nil - 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 Dcs.DCSWrapper.Group#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 Dcs.DCSCoalitionWrapper.Object#coalition.side The coalition side of the DCS Group. -function GROUP:GetCoalition() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - if DCSGroup then - local GroupCoalition = DCSGroup:getCoalition() - self:T3( GroupCoalition ) - return GroupCoalition - end - - return nil -end - ---- Returns the country of the DCS Group. --- @param #GROUP self --- @return Dcs.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 UNIT wrapper class with number UnitNumber. --- If the underlying DCS Unit does not exist, the method will return nil. . --- @param #GROUP self --- @param #number UnitNumber The number of the UNIT wrapper class to be returned. --- @return Wrapper.Unit#UNIT The UNIT wrapper class. -function GROUP:GetUnit( UnitNumber ) - self:F2( { self.GroupName, UnitNumber } ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local UnitFound = UNIT:Find( DCSGroup:getUnit( UnitNumber ) ) - 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 Dcs.DCSWrapper.Unit#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 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. --- @param #GROUP self --- @return Dcs.DCSTypes#Vec2 Current Vec2 point of the first DCS Unit of the DCS Group. -function GROUP:GetVec2() - self:F2( self.GroupName ) - - local UnitPoint = self:GetUnit(1) - UnitPoint:GetVec2() - local GroupPointVec2 = UnitPoint:GetVec2() - self:T3( GroupPointVec2 ) - return GroupPointVec2 -end - ---- Returns the current Vec3 vector of the first DCS Unit in the GROUP. --- @return Dcs.DCSTypes#Vec3 Current Vec3 of the first DCS Unit of the GROUP. -function GROUP:GetVec3() - self:F2( self.GroupName ) - - local GroupVec3 = self:GetUnit(1):GetVec3() - self:T3( GroupVec3 ) - return GroupVec3 -end - - - -do -- Is Zone methods - ---- Returns true if all units of the group are within a @{Zone}. --- @param #GROUP self --- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the Group is completely within the @{Zone#ZONE_BASE} -function GROUP:IsCompletelyInZone( Zone ) - self:F2( { self.GroupName, Zone } ) - - for UnitID, UnitData in pairs( self:GetUnits() ) do - local Unit = UnitData -- Wrapper.Unit#UNIT - if Zone:IsVec3InZone( Unit:GetVec3() ) then - else - return false - end - end - - return true -end - ---- Returns true if some units of the group are within a @{Zone}. --- @param #GROUP self --- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the Group is completely within the @{Zone#ZONE_BASE} -function GROUP:IsPartlyInZone( Zone ) - self:F2( { self.GroupName, Zone } ) - - for UnitID, UnitData in pairs( self:GetUnits() ) do - local Unit = UnitData -- Wrapper.Unit#UNIT - if Zone:IsVec3InZone( Unit:GetVec3() ) then - return true - end - end - - return false -end - ---- Returns true if none of the group units of the group are within a @{Zone}. --- @param #GROUP self --- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the Group is 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 -- Wrapper.Unit#UNIT - if Zone:IsVec3InZone( Unit:GetVec3() ) 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 - -end - -do -- AI methods - - --- Turns the AI On or Off for the GROUP. - -- @param #GROUP self - -- @param #boolean AIOnOff The value true turns the AI On, the value false turns the AI Off. - -- @return #GROUP The GROUP. - function GROUP:SetAIOnOff( AIOnOff ) - - local DCSGroup = self:GetDCSObject() -- Dcs.DCSGroup#Group - - if DCSGroup then - local DCSController = DCSGroup:getController() -- Dcs.DCSController#Controller - if DCSController then - DCSController:setOnOff( AIOnOff ) - return self - end - end - - return nil - end - - --- Turns the AI On for the GROUP. - -- @param #GROUP self - -- @return #GROUP The GROUP. - function GROUP:SetAIOn() - - return self:SetAIOnOff( true ) - end - - --- Turns the AI Off for the GROUP. - -- @param #GROUP self - -- @return #GROUP The GROUP. - function GROUP:SetAIOff() - - return self:SetAIOnOff( false ) - end - -end - - - ---- Returns the current maximum velocity of the group. --- Each unit within the group gets evaluated, and the maximum velocity (= the unit which is going the fastest) is returned. --- @param #GROUP self --- @return #number Maximum velocity found. -function GROUP:GetMaxVelocity() - self:F2() - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupVelocityMax = 0 - - for Index, UnitData in pairs( DCSGroup:getUnits() ) do - - local UnitVelocityVec3 = UnitData:getVelocity() - local UnitVelocity = math.abs( UnitVelocityVec3.x ) + math.abs( UnitVelocityVec3.y ) + math.abs( UnitVelocityVec3.z ) - - if UnitVelocity > GroupVelocityMax then - GroupVelocityMax = UnitVelocity - end - end - - return GroupVelocityMax - end - - return nil -end - ---- Returns the current minimum height of the group. --- Each unit within the group gets evaluated, and the minimum height (= the unit which is the lowest elevated) is returned. --- @param #GROUP self --- @return #number Minimum height found. -function GROUP:GetMinHeight() - self:F2() - -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 - --- SPAWNING - ---- Respawn the @{GROUP} using a (tweaked) template of the Group. --- The template must be retrieved with the @{Group#GROUP.GetTemplate}() function. --- The template contains all the definitions as declared within the mission file. --- To understand templates, do the following: --- --- * unpack your .miz file into a directory using 7-zip. --- * browse in the directory created to the file **mission**. --- * open the file and search for the country group definitions. --- --- Your group template will contain the fields as described within the mission file. --- --- This function will: --- --- * Get the current position and heading of the group. --- * When the group is alive, it will tweak the template x, y and heading coordinates of the group and the embedded units to the current units positions. --- * Then it will destroy the current alive group. --- * And it will respawn the group using your new template definition. --- @param Wrapper.Group#GROUP self --- @param #table Template The template of the Group retrieved with GROUP:GetTemplate() -function GROUP:Respawn( Template ) - - local Vec3 = self:GetVec3() - 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 -- Wrapper.Unit#UNIT - self:E( GroupUnit:GetName() ) - if GroupUnit:IsAlive() then - local GroupUnitVec3 = GroupUnit:GetVec3() - 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 - - self:Destroy() - _DATABASE:Spawn( Template ) -end - ---- Returns the group template from the @{DATABASE} (_DATABASE object). --- @param #GROUP self --- @return #table -function GROUP:GetTemplate() - local GroupName = self:GetName() - self:E( GroupName ) - return _DATABASE:GetGroupTemplate( GroupName ) -end - ---- Sets the controlled status in a Template. --- @param #GROUP self --- @param #boolean Controlled true is controlled, false is uncontrolled. --- @return #table -function GROUP:SetTemplateControlled( Template, Controlled ) - Template.uncontrolled = not Controlled - return Template -end - ---- Sets the CountryID of the group in a Template. --- @param #GROUP self --- @param Dcs.DCScountry#country.id CountryID The country ID. --- @return #table -function GROUP:SetTemplateCountry( Template, CountryID ) - Template.CountryID = CountryID - return Template -end - ---- Sets the CoalitionID of the group in a Template. --- @param #GROUP self --- @param Dcs.DCSCoalitionWrapper.Object#coalition.side CoalitionID The coalition ID. --- @return #table -function GROUP:SetTemplateCoalition( Template, CoalitionID ) - Template.CoalitionID = CoalitionID - return 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 - ---- Calculate the maxium A2G threat level of the Group. --- @param #GROUP self -function GROUP:CalculateThreatLevelA2G() - - local MaxThreatLevelA2G = 0 - for UnitName, UnitData in pairs( self:GetUnits() ) do - local ThreatUnit = UnitData -- Wrapper.Unit#UNIT - local ThreatLevelA2G = ThreatUnit:GetThreatLevel() - if ThreatLevelA2G > MaxThreatLevelA2G then - MaxThreatLevelA2G = ThreatLevelA2G - end - end - - self:T3( MaxThreatLevelA2G ) - return MaxThreatLevelA2G -end - ---- Returns true if the first unit of the GROUP is in the air. --- @param Wrapper.Group#GROUP self --- @return #boolean true if in the first unit of the group is in the air. --- @return #nil The GROUP is not existing or not alive. -function GROUP:InAir() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local DCSUnit = DCSGroup:getUnit(1) - if DCSUnit then - local GroupInAir = DCSGroup:getUnit(1):inAir() - self:T3( GroupInAir ) - return GroupInAir - end - end - - return nil -end - -function GROUP:OnReSpawn( ReSpawnFunction ) - - self.ReSpawnFunction = ReSpawnFunction -end - - ---- This module contains the UNIT class. --- --- 1) @{#UNIT} class, extends @{Controllable#CONTROLLABLE} --- =========================================================== --- The @{#UNIT} class is a wrapper class to handle the DCS Unit objects: --- --- * Support all DCS Unit APIs. --- * Enhance with Unit specific APIs not in the DCS Unit API set. --- * Handle local Unit Controller. --- * Manage the "state" of the DCS Unit. --- --- --- 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 @{DCSWrapper.Unit#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.GetVec3}() will obtain the current **location** of the DCS Unit in a Vec2 (2D) or a **point** in a Vec3 (3D) vector respectively. --- If you want to obtain the complete **3D position** including 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 Wrapper.Controllable#CONTROLLABLE -UNIT = { - ClassName="UNIT", -} - - ---- Unit.SensorType --- @type Unit.SensorType --- @field OPTIC --- @field RADAR --- @field IRST --- @field RWR - - --- Registration. - ---- Create a new UNIT from DCSUnit. --- @param #UNIT self --- @param #string UnitName The name of the DCS unit. --- @return #UNIT -function UNIT:Register( UnitName ) - local self = BASE:Inherit( self, CONTROLLABLE:New( UnitName ) ) - self.UnitName = UnitName - - self:SetEventPriority( 3 ) - return self -end - --- Reference methods. - ---- Finds a UNIT from the _DATABASE using a DCSUnit object. --- @param #UNIT self --- @param Dcs.DCSWrapper.Unit#Unit DCSUnit An existing DCS Unit object reference. --- @return #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 self -function UNIT:FindByName( UnitName ) - - local UnitFound = _DATABASE:FindUnit( UnitName ) - return UnitFound -end - ---- Return the name of the UNIT. --- @param #UNIT self --- @return #string The UNIT name. -function UNIT:Name() - - return self.UnitName -end - - ---- @param #UNIT self --- @return Dcs.DCSWrapper.Unit#Unit -function UNIT:GetDCSObject() - - local DCSUnit = Unit.getByName( self.UnitName ) - - if DCSUnit then - return DCSUnit - end - - return nil -end - ---- Respawn the @{Unit} using a (tweaked) template of the parent Group. --- --- This function will: --- --- * Get the current position and heading of the group. --- * When the unit is alive, it will tweak the template x, y and heading coordinates of the group and the embedded units to the current units positions. --- * Then it will respawn the re-modelled group. --- --- @param #UNIT self --- @param Dcs.DCSTypes#Vec3 SpawnVec3 The position where to Spawn the new Unit at. --- @param #number Heading The heading of the unit respawn. -function UNIT:ReSpawn( SpawnVec3, Heading ) - - local SpawnGroupTemplate = UTILS.DeepCopy( _DATABASE:GetGroupTemplateFromUnitName( self:Name() ) ) - self:T( SpawnGroupTemplate ) - - local SpawnGroup = self:GetGroup() - - if SpawnGroup then - - local Vec3 = SpawnGroup:GetVec3() - SpawnGroupTemplate.x = SpawnVec3.x - SpawnGroupTemplate.y = SpawnVec3.z - - self:E( #SpawnGroupTemplate.units ) - for UnitID, UnitData in pairs( SpawnGroup:GetUnits() ) do - local GroupUnit = UnitData -- #UNIT - self:E( GroupUnit:GetName() ) - if GroupUnit:IsAlive() then - local GroupUnitVec3 = GroupUnit:GetVec3() - local GroupUnitHeading = GroupUnit:GetHeading() - SpawnGroupTemplate.units[UnitID].alt = GroupUnitVec3.y - SpawnGroupTemplate.units[UnitID].x = GroupUnitVec3.x - SpawnGroupTemplate.units[UnitID].y = GroupUnitVec3.z - SpawnGroupTemplate.units[UnitID].heading = GroupUnitHeading - self:E( { UnitID, SpawnGroupTemplate.units[UnitID], SpawnGroupTemplate.units[UnitID] } ) - end - end - end - - for UnitTemplateID, UnitTemplateData in pairs( SpawnGroupTemplate.units ) do - self:T( UnitTemplateData.name ) - if UnitTemplateData.name == self:Name() then - self:T("Adjusting") - SpawnGroupTemplate.units[UnitTemplateID].alt = SpawnVec3.y - SpawnGroupTemplate.units[UnitTemplateID].x = SpawnVec3.x - SpawnGroupTemplate.units[UnitTemplateID].y = SpawnVec3.z - SpawnGroupTemplate.units[UnitTemplateID].heading = Heading - self:E( { UnitTemplateID, SpawnGroupTemplate.units[UnitTemplateID], SpawnGroupTemplate.units[UnitTemplateID] } ) - else - self:E( SpawnGroupTemplate.units[UnitTemplateID].name ) - local GroupUnit = UNIT:FindByName( SpawnGroupTemplate.units[UnitTemplateID].name ) -- #UNIT - if GroupUnit and GroupUnit:IsAlive() then - local GroupUnitVec3 = GroupUnit:GetVec3() - local GroupUnitHeading = GroupUnit:GetHeading() - UnitTemplateData.alt = GroupUnitVec3.y - UnitTemplateData.x = GroupUnitVec3.x - UnitTemplateData.y = GroupUnitVec3.z - UnitTemplateData.heading = GroupUnitHeading - else - if SpawnGroupTemplate.units[UnitTemplateID].name ~= self:Name() then - self:T("nilling") - SpawnGroupTemplate.units[UnitTemplateID].delete = true - end - end - end - end - - -- Remove obscolete units from the group structure - i = 1 - while i <= #SpawnGroupTemplate.units do - - local UnitTemplateData = SpawnGroupTemplate.units[i] - self:T( UnitTemplateData.name ) - - if UnitTemplateData.delete then - table.remove( SpawnGroupTemplate.units, i ) - else - i = i + 1 - end - end - - _DATABASE:Spawn( SpawnGroupTemplate ) -end - - - ---- Returns if the unit is activated. --- @param #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 the Unit's callsign - the localized string. --- @param #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 - - 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 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 number in the group. --- The number is the same number the unit has in ME. --- It may not be changed during the mission. --- If any unit in the group is destroyed, the numbers of another units will not be changed. --- @param #UNIT self --- @return #number The Unit number. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetNumber() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitNumber = DCSUnit:getNumber() - return UnitNumber - end - - return nil -end - ---- Returns the unit's group if it exist and nil otherwise. --- @param Wrapper.Unit#UNIT self --- @return Wrapper.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 - - --- Need to add here functions to check if radar is on and which object etc. - ---- Returns the prefix name of the DCS Unit. A prefix name is a part of the name before a '#'-sign. --- DCS Units spawned with the @{SPAWN} class contain a '#'-sign to indicate the end of the (base) DCS Unit name. --- The spawn sequence number and unit number are contained within the name after the '#' sign. --- @param #UNIT self --- @return #string The name of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetPrefix() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitPrefix = string.match( self.UnitName, ".*#" ):sub( 1, -2 ) - self:T3( UnitPrefix ) - return UnitPrefix - end - - return nil -end - ---- Returns the Unit's ammunition. --- @param #UNIT self --- @return Dcs.DCSWrapper.Unit#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 self --- @return Dcs.DCSWrapper.Unit#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 if the unit has sensors of a certain type. --- @param #UNIT self --- @return #boolean returns true if the unit has specified types of sensors. This function is more preferable than Unit.getSensors() if you don't want to get information about all the unit's sensors, and just want to check if the unit has specified types of sensors. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:HasSensors( ... ) - self:F2( arg ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local HasSensors = DCSUnit:hasSensors( unpack( arg ) ) - return HasSensors - end - - return nil -end - ---- Returns if the unit is SEADable. --- @param #UNIT self --- @return #boolean returns true if the unit is SEADable. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:HasSEAD() - self:F2() - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitSEADAttributes = DCSUnit:getDesc().attributes - - local HasSEAD = false - if UnitSEADAttributes["RADAR_BAND1_FOR_ARM"] and UnitSEADAttributes["RADAR_BAND1_FOR_ARM"] == true or - UnitSEADAttributes["RADAR_BAND2_FOR_ARM"] and UnitSEADAttributes["RADAR_BAND2_FOR_ARM"] == true then - HasSEAD = true - end - return HasSEAD - end - - return nil -end - ---- Returns two values: --- --- * First value indicates if at least one of the unit's radar(s) is on. --- * Second value is the object of the radar's interest. Not nil only if at least one radar of the unit is tracking a target. --- @param #UNIT self --- @return #boolean Indicates if at least one of the unit's radar(s) is on. --- @return Dcs.DCSWrapper.Object#Object The object of the radar's interest. Not nil only if at least one radar of the unit is tracking a target. --- @return #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 - ---- Returns relative amount of fuel (from 0.0 to 1.0) the unit has in its internal tanks. If there are additional fuel tanks the value may be greater than 1.0. --- @param #UNIT self --- @return #number The relative amount of fuel (from 0.0 to 1.0). --- @return #nil The DCS Unit is not existing or alive. -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 in a UNIT list of one element. --- @param #UNIT self --- @return #list The UNITs wrappers. -function UNIT:GetUnits() - self:F2( { self.UnitName } ) - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local DCSUnits = DCSUnit:getUnits() - local Units = {} - Units[1] = UNIT:Find( DCSUnit ) - self:T3( Units ) - return Units - end - - return nil -end - - ---- Returns the unit's health. Dead units has health <= 1.0. --- @param #UNIT self --- @return #number The Unit's health value. --- @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 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 the Unit's A2G threat level on a scale from 1 to 10 ... --- The following threat levels are foreseen: --- --- * Threat level 0: Unit is unarmed. --- * Threat level 1: Unit is infantry. --- * Threat level 2: Unit is an infantry vehicle. --- * Threat level 3: Unit is ground artillery. --- * Threat level 4: Unit is a tank. --- * Threat level 5: Unit is a modern tank or ifv with ATGM. --- * Threat level 6: Unit is a AAA. --- * Threat level 7: Unit is a SAM or manpad, IR guided. --- * Threat level 8: Unit is a Short Range SAM, radar guided. --- * Threat level 9: Unit is a Medium Range SAM, radar guided. --- * Threat level 10: Unit is a Long Range SAM, radar guided. --- @param #UNIT self -function UNIT:GetThreatLevel() - - local Attributes = self:GetDesc().attributes - self:E( Attributes ) - - local ThreatLevel = 0 - local ThreatText = "" - - if self:IsGround() then - - self:E( "Ground" ) - - local ThreatLevels = { - "Unarmed", - "Infantry", - "Old Tanks & APCs", - "Tanks & IFVs without ATGM", - "Tanks & IFV with ATGM", - "Modern Tanks", - "AAA", - "IR Guided SAMs", - "SR SAMs", - "MR SAMs", - "LR SAMs" - } - - - if Attributes["LR SAM"] then ThreatLevel = 10 - elseif Attributes["MR SAM"] then ThreatLevel = 9 - elseif Attributes["SR SAM"] and - not Attributes["IR Guided SAM"] then ThreatLevel = 8 - elseif ( Attributes["SR SAM"] or Attributes["MANPADS"] ) and - Attributes["IR Guided SAM"] then ThreatLevel = 7 - elseif Attributes["AAA"] then ThreatLevel = 6 - elseif Attributes["Modern Tanks"] then ThreatLevel = 5 - elseif ( Attributes["Tanks"] or Attributes["IFV"] ) and - Attributes["ATGM"] then ThreatLevel = 4 - elseif ( Attributes["Tanks"] or Attributes["IFV"] ) and - not Attributes["ATGM"] then ThreatLevel = 3 - elseif Attributes["Old Tanks"] or Attributes["APC"] or Attributes["Artillery"] then ThreatLevel = 2 - elseif Attributes["Infantry"] then ThreatLevel = 1 - end - - ThreatText = ThreatLevels[ThreatLevel+1] - end - - if self:IsAir() then - - self:E( "Air" ) - - local ThreatLevels = { - "Unarmed", - "Tanker", - "AWACS", - "Transport Helicpter", - "UAV", - "Bomber", - "Strategic Bomber", - "Attack Helicopter", - "Interceptor", - "Multirole Fighter", - "Fighter" - } - - - if Attributes["Fighters"] then ThreatLevel = 10 - elseif Attributes["Multirole fighters"] then ThreatLevel = 9 - elseif Attributes["Battleplanes"] then ThreatLevel = 8 - elseif Attributes["Attack helicopters"] then ThreatLevel = 7 - elseif Attributes["Strategic bombers"] then ThreatLevel = 6 - elseif Attributes["Bombers"] then ThreatLevel = 5 - elseif Attributes["UAVs"] then ThreatLevel = 4 - elseif Attributes["Transport helicopters"] then ThreatLevel = 3 - elseif Attributes["AWACS"] then ThreatLevel = 2 - elseif Attributes["Tankers"] then ThreatLevel = 1 - end - - ThreatText = ThreatLevels[ThreatLevel+1] - end - - if self:IsShip() then - - self:E( "Ship" ) - ---["Aircraft Carriers"] = {"Heavy armed ships",}, ---["Cruisers"] = {"Heavy armed ships",}, ---["Destroyers"] = {"Heavy armed ships",}, ---["Frigates"] = {"Heavy armed ships",}, ---["Corvettes"] = {"Heavy armed ships",}, ---["Heavy armed ships"] = {"Armed ships", "Armed Air Defence", "HeavyArmoredUnits",}, ---["Light armed ships"] = {"Armed ships","NonArmoredUnits"}, ---["Armed ships"] = {"Ships"}, ---["Unarmed ships"] = {"Ships","HeavyArmoredUnits",}, - - local ThreatLevels = { - "Unarmed ship", - "Light armed ships", - "Corvettes", - "", - "Frigates", - "", - "Cruiser", - "", - "Destroyer", - "", - "Aircraft Carrier" - } - - - if Attributes["Aircraft Carriers"] then ThreatLevel = 10 - elseif Attributes["Destroyers"] then ThreatLevel = 8 - elseif Attributes["Cruisers"] then ThreatLevel = 6 - elseif Attributes["Frigates"] then ThreatLevel = 4 - elseif Attributes["Corvettes"] then ThreatLevel = 2 - elseif Attributes["Light armed ships"] then ThreatLevel = 1 - end - - ThreatText = ThreatLevels[ThreatLevel+1] - end - - self:T2( ThreatLevel ) - return ThreatLevel, ThreatText - -end - - --- Is functions - ---- Returns true if the unit is within a @{Zone}. --- @param #UNIT self --- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the unit is within the @{Zone#ZONE_BASE} -function UNIT:IsInZone( Zone ) - self:F2( { self.UnitName, Zone } ) - - if self:IsAlive() then - local IsInZone = Zone:IsVec3InZone( self:GetVec3() ) - - self:T( { IsInZone } ) - return IsInZone - end - - return false -end - ---- Returns true if the unit is not within a @{Zone}. --- @param #UNIT self --- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the unit is not within the @{Zone#ZONE_BASE} -function UNIT:IsNotInZone( Zone ) - self:F2( { self.UnitName, Zone } ) - - if self:IsAlive() then - local IsInZone = not Zone:IsVec3InZone( self:GetVec3() ) - - self:T( { IsInZone } ) - return IsInZone - else - return false - end -end - - ---- Returns true if there is an **other** DCS Unit within a radius of the current 2D point of the DCS Unit. --- @param #UNIT self --- @param #UNIT AwaitUnit The other UNIT wrapper object. --- @param Radius The radius in meters with the DCS Unit in the centre. --- @return true If the other DCS Unit is within the radius of the 2D point of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:OtherUnitInRadius( AwaitUnit, Radius ) - self:F2( { self.UnitName, AwaitUnit.UnitName, Radius } ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitVec3 = self:GetVec3() - local AwaitUnitVec3 = AwaitUnit:GetVec3() - - if (((UnitVec3.x - AwaitUnitVec3.x)^2 + (UnitVec3.z - AwaitUnitVec3.z)^2)^0.5 <= Radius) then - self:T3( "true" ) - return true - else - self:T3( "false" ) - return false - end - end - - return nil -end - - - ---- Signal a flare at the position of the UNIT. --- @param #UNIT self --- @param Utilities.Utils#FLARECOLOR FlareColor -function UNIT:Flare( FlareColor ) - self:F2() - trigger.action.signalFlare( self:GetVec3(), 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:GetVec3(), 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:GetVec3(), 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:GetVec3(), trigger.flareColor.Green , 0 ) -end - ---- Signal a red flare at the position of the UNIT. --- @param #UNIT self -function UNIT:FlareRed() - self:F2() - local Vec3 = self:GetVec3() - if Vec3 then - trigger.action.signalFlare( Vec3, trigger.flareColor.Red, 0 ) - end -end - ---- Smoke the UNIT. --- @param #UNIT self -function UNIT:Smoke( SmokeColor, Range ) - self:F2() - if Range then - trigger.action.smoke( self:GetRandomVec3( Range ), SmokeColor ) - else - trigger.action.smoke( self:GetVec3(), SmokeColor ) - end - -end - ---- Smoke the UNIT Green. --- @param #UNIT self -function UNIT:SmokeGreen() - self:F2() - trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Green ) -end - ---- Smoke the UNIT Red. --- @param #UNIT self -function UNIT:SmokeRed() - self:F2() - trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Red ) -end - ---- Smoke the UNIT White. --- @param #UNIT self -function UNIT:SmokeWhite() - self:F2() - trigger.action.smoke( self:GetVec3(), trigger.smokeColor.White ) -end - ---- Smoke the UNIT Orange. --- @param #UNIT self -function UNIT:SmokeOrange() - self:F2() - trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Orange ) -end - ---- Smoke the UNIT Blue. --- @param #UNIT self -function UNIT:SmokeBlue() - self:F2() - trigger.action.smoke( self:GetVec3(), 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 DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitDescriptor = DCSUnit:getDesc() - self:T3( { UnitDescriptor.category, Unit.Category.AIRPLANE, Unit.Category.HELICOPTER } ) - - local IsAirResult = ( UnitDescriptor.category == Unit.Category.AIRPLANE ) or ( UnitDescriptor.category == Unit.Category.HELICOPTER ) - - self:T3( IsAirResult ) - return IsAirResult - end - - return nil -end - ---- Returns if the unit is of an ground category. --- If the unit is a ground vehicle or infantry, this method will return true, otherwise false. --- @param #UNIT self --- @return #boolean Ground category evaluation result. -function UNIT:IsGround() - self:F2() - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitDescriptor = DCSUnit:getDesc() - self:T3( { UnitDescriptor.category, Unit.Category.GROUND_UNIT } ) - - local IsGroundResult = ( UnitDescriptor.category == Unit.Category.GROUND_UNIT ) - - self:T3( IsGroundResult ) - return IsGroundResult - end - - return nil -end - ---- Returns if the unit is a friendly unit. --- @param #UNIT self --- @return #boolean IsFriendly evaluation result. -function UNIT:IsFriendly( FriendlyCoalition ) - self:F2() - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitCoalition = DCSUnit:getCoalition() - self:T3( { UnitCoalition, FriendlyCoalition } ) - - local IsFriendlyResult = ( UnitCoalition == FriendlyCoalition ) - - self:E( IsFriendlyResult ) - return IsFriendlyResult - end - - return nil -end - ---- Returns if the unit is of a ship category. --- If the unit is a ship, this method will return true, otherwise false. --- @param #UNIT self --- @return #boolean Ship category evaluation result. -function UNIT:IsShip() - self:F2() - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitDescriptor = DCSUnit:getDesc() - self:T3( { UnitDescriptor.category, Unit.Category.SHIP } ) - - local IsShipResult = ( UnitDescriptor.category == Unit.Category.SHIP ) - - self:T3( IsShipResult ) - return IsShipResult - end - - return nil -end - ---- Returns true if the UNIT is in the air. --- @param Wrapper.Positionable#UNIT self --- @return #boolean true if in the air. --- @return #nil The UNIT is not existing or alive. -function UNIT:InAir() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitInAir = DCSUnit:inAir() - self:T3( UnitInAir ) - return UnitInAir - end - - return nil -end - -do -- Event Handling - - --- Subscribe to a DCS Event. - -- @param #UNIT self - -- @param Core.Event#EVENTS Event - -- @param #function EventFunction (optional) The function to be called when the event occurs for the unit. - -- @return #UNIT - function UNIT:HandleEvent( Event, EventFunction ) - - self:EventDispatcher():OnEventForUnit( self:GetName(), EventFunction, self, Event ) - - return self - end - - --- UnSubscribe to a DCS event. - -- @param #UNIT self - -- @param Core.Event#EVENTS Event - -- @return #UNIT - function UNIT:UnHandleEvent( Event ) - - self:EventDispatcher():RemoveForUnit( self:GetName(), self, Event ) - - return self - end - -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 - ---- The CLIENT class --- @type CLIENT --- @extends Wrapper.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. --- @param #boolean Error A flag that indicates whether an error should be raised if the CLIENT cannot be found. By default an error will be raised. --- @return #CLIENT --- @usage --- -- Create new Clients. --- local Mission = MISSIONSCHEDULER.AddMission( 'Russia Transport Troops SA-6', 'Operational', 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', 'Russia' ) --- Mission:AddGoal( DeploySA6TroopsGoal ) --- --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 1' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 3' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 2' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 4' ):Transport() ) -function CLIENT:FindByName( ClientName, ClientBriefing, Error ) - local ClientFound = _DATABASE:FindClient( ClientName ) - - if ClientFound then - ClientFound:F( { ClientName, ClientBriefing } ) - ClientFound:AddBriefing( ClientBriefing ) - ClientFound.MessageSwitch = true - - return ClientFound - end - - if not Error then - error( "CLIENT not found for: " .. ClientName ) - end -end - -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 CallBackFunction Create a function that will be called when a player joins the slot. --- @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:F3( { 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 Dcs.DCSWrapper.Group#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 Dcs.DCSTypes#Group.ID ---- Get the group ID of the client. --- @param #CLIENT self --- @return Dcs.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 Wrapper.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 Dcs.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 @{AI_Cargo#CARGO} contained within the CLIENT to the player as a message. --- The @{AI_Cargo#CARGO} is shown using the @{Message#MESSAGE} distribution system. --- @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 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 @{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: --- --- * 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 Wrapper.Positionable#POSITIONABLE -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. --- @param #boolean RaiseError Raise an error if not found. --- @return #STATIC -function STATIC:FindByName( StaticName, RaiseError ) - local StaticFound = _DATABASE:FindStatic( StaticName ) - - self.StaticName = StaticName - - if StaticFound then - StaticFound:F( { StaticName } ) - - return StaticFound - end - - if RaiseError == nil or RaiseError == true then - error( "STATIC not found for: " .. StaticName ) - end - - return nil -end - -function STATIC:Register( StaticName ) - local self = BASE:Inherit( self, POSITIONABLE:New( StaticName ) ) - self.StaticName = StaticName - return self -end - - -function STATIC:GetDCSObject() - local DCSStatic = StaticObject.getByName( self.StaticName ) - - if DCSStatic then - return DCSStatic - end - - return nil -end - -function STATIC:GetThreatLevel() - - return 1, "Static" -end--- This module contains the AIRBASE classes. --- --- === --- --- 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. --- * 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 @{DCSWrapper.Airbase#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 Wrapper.Positionable#POSITIONABLE -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 #string AirbaseName The name of the airbase. --- @return Wrapper.Airbase#AIRBASE -function AIRBASE:Register( AirbaseName ) - - local self = BASE:Inherit( self, POSITIONABLE:New( AirbaseName ) ) - self.AirbaseName = AirbaseName - return self -end - --- Reference methods. - ---- Finds a AIRBASE from the _DATABASE using a DCSAirbase object. --- @param #AIRBASE self --- @param Dcs.DCSWrapper.Airbase#Airbase DCSAirbase An existing DCS Airbase object reference. --- @return Wrapper.Airbase#AIRBASE self -function AIRBASE:Find( DCSAirbase ) - - local AirbaseName = DCSAirbase:getName() - local AirbaseFound = _DATABASE:FindAirbase( AirbaseName ) - return AirbaseFound -end - ---- Find a AIRBASE in the _DATABASE using the name of an existing DCS Airbase. --- @param #AIRBASE self --- @param #string AirbaseName The Airbase Name. --- @return Wrapper.Airbase#AIRBASE self -function AIRBASE:FindByName( AirbaseName ) - - local AirbaseFound = _DATABASE:FindAirbase( AirbaseName ) - return AirbaseFound -end - -function AIRBASE:GetDCSObject() - local DCSAirbase = Airbase.getByName( self.AirbaseName ) - - if DCSAirbase then - return DCSAirbase - end - - return nil -end - - - ---- This module contains the SCENERY class. --- --- 1) @{Scenery#SCENERY} class, extends @{Positionable#POSITIONABLE} --- =============================================================== --- Scenery objects are defined on the map. --- The @{Scenery#SCENERY} class is a wrapper class to handle the DCS Scenery objects: --- --- * Wraps the DCS Scenery objects. --- * Support all DCS Scenery APIs. --- * Enhance with Scenery specific APIs not in the DCS API set. --- --- @module Scenery --- @author FlightControl - - - ---- The SCENERY class --- @type SCENERY --- @extends Wrapper.Positionable#POSITIONABLE -SCENERY = { - ClassName = "SCENERY", -} - - -function SCENERY:Register( SceneryName, SceneryObject ) - local self = BASE:Inherit( self, POSITIONABLE:New( SceneryName ) ) - self.SceneryName = SceneryName - self.SceneryObject = SceneryObject - return self -end - -function SCENERY:GetDCSObject() - return self.SceneryObject -end - -function SCENERY:GetThreatLevel() - - return 0, "Scenery" -end ---- Single-Player:**Yes** / Multi-Player:**Yes** / Core:**Yes** -- **Administer the scoring of player achievements, --- and create a CSV file logging the scoring events for use at team or squadron websites.** --- --- ![Banner Image](..\Presentations\SCORING\Dia1.JPG) --- --- === --- --- # 1) @{Scoring#SCORING} class, extends @{Base#BASE} --- --- The @{#SCORING} class administers the scoring of player achievements, --- and creates a CSV file logging the scoring events and results for use at team or squadron websites. --- --- SCORING automatically calculates the threat level of the objects hit and destroyed by players, --- which can be @{Unit}, @{Static) and @{Scenery} objects. --- --- Positive score points are granted when enemy or neutral targets are destroyed. --- Negative score points or penalties are given when a friendly target is hit or destroyed. --- This brings a lot of dynamism in the scoring, where players need to take care to inflict damage on the right target. --- By default, penalties weight heavier in the scoring, to ensure that players don't commit fratricide. --- The total score of the player is calculated by **adding the scores minus the penalties**. --- --- ![Banner Image](..\Presentations\SCORING\Dia4.JPG) --- --- The score value is calculated based on the **threat level of the player** and the **threat level of the target**. --- A calculated score takes the threat level of the target divided by a balanced threat level of the player unit. --- As such, if the threat level of the target is high, and the player threat level is low, a higher score will be given than --- if the threat level of the player would be high too. --- --- ![Banner Image](..\Presentations\SCORING\Dia5.JPG) --- --- When multiple players hit the same target, and finally succeed in destroying the target, then each player who contributed to the target --- destruction, will receive a score. This is important for targets that require significant damage before it can be destroyed, like --- ships or heavy planes. --- --- ![Banner Image](..\Presentations\SCORING\Dia13.JPG) --- --- Optionally, the score values can be **scaled** by a **scale**. Specific scales can be set for positive cores or negative penalties. --- The default range of the scores granted is a value between 0 and 10. The default range of penalties given is a value between 0 and 30. --- --- ![Banner Image](..\Presentations\SCORING\Dia7.JPG) --- --- **Additional scores** can be granted to **specific objects**, when the player(s) destroy these objects. --- --- ![Banner Image](..\Presentations\SCORING\Dia9.JPG) --- --- Various @{Zone}s can be defined for which scores are also granted when objects in that @{Zone} are destroyed. --- This is **specifically useful** to designate **scenery targets on the map** that will generate points when destroyed. --- --- With a small change in MissionScripting.lua, the scoring results can also be logged in a **CSV file**. --- These CSV files can be used to: --- --- * Upload scoring to a database or a BI tool to publish the scoring results to the player community. --- * Upload scoring in an (online) Excel like tool, using pivot tables and pivot charts to show mission results. --- * Share scoring amoung players after the mission to discuss mission results. --- --- Scores can be **reported**. **Menu options** are automatically added to **each player group** when a player joins a client slot or a CA unit. --- Use the radio menu F10 to consult the scores while running the mission. --- Scores can be reported for your user, or an overall score can be reported of all players currently active in the mission. --- --- ## 1.1) Set the destroy score or penalty scale --- --- Score scales can be set for scores granted when enemies or friendlies are destroyed. --- Use the method @{#SCORING.SetScaleDestroyScore}() to set the scale of enemy destroys (positive destroys). --- Use the method @{#SCORING.SetScaleDestroyPenalty}() to set the scale of friendly destroys (negative destroys). --- --- local Scoring = SCORING:New( "Scoring File" ) --- Scoring:SetScaleDestroyScore( 10 ) --- Scoring:SetScaleDestroyPenalty( 40 ) --- --- The above sets the scale for valid scores to 10. So scores will be given in a scale from 0 to 10. --- The penalties will be given in a scale from 0 to 40. --- --- ## 1.2) Define special targets that will give extra scores. --- --- Special targets can be set that will give extra scores to the players when these are destroyed. --- Use the methods @{#SCORING.AddUnitScore}() and @{#SCORING.RemoveUnitScore}() to specify a special additional score for a specific @{Unit}s. --- Use the methods @{#SCORING.AddStaticScore}() and @{#SCORING.RemoveStaticScore}() to specify a special additional score for a specific @{Static}s. --- Use the method @{#SCORING.SetGroupGroup}() to specify a special additional score for a specific @{Group}s. --- --- local Scoring = SCORING:New( "Scoring File" ) --- Scoring:AddUnitScore( UNIT:FindByName( "Unit #001" ), 200 ) --- Scoring:AddStaticScore( STATIC:FindByName( "Static #1" ), 100 ) --- --- The above grants an additional score of 200 points for Unit #001 and an additional 100 points of Static #1 if these are destroyed. --- Note that later in the mission, one can remove these scores set, for example, when the a goal achievement time limit is over. --- For example, this can be done as follows: --- --- Scoring:RemoveUnitScore( UNIT:FindByName( "Unit #001" ) ) --- --- --- --- ## 1.3) Define destruction zones that will give extra scores. --- --- Define zones of destruction. Any object destroyed within the zone of the given category will give extra points. --- Use the method @{#SCORING.AddZoneScore}() to add a @{Zone} for additional scoring. --- Use the method @{#SCORING.RemoveZoneScore}() to remove a @{Zone} for additional scoring. --- There are interesting variations that can be achieved with this functionality. For example, if the @{Zone} is a @{Zone#ZONE_UNIT}, --- then the zone is a moving zone, and anything destroyed within that @{Zone} will generate points. --- The other implementation could be to designate a scenery target (a building) in the mission editor surrounded by a @{Zone}, --- just large enough around that building. --- --- ## 1.4) Add extra Goal scores upon an event or a condition. --- --- A mission has goals and achievements. The scoring system provides an API to set additional scores when a goal or achievement event happens. --- Use the method @{#SCORING.AddGoalScore}() to add a score for a Player at any time in your mission. --- --- ## 1.5) Configure fratricide level. --- --- When a player commits too much damage to friendlies, his penalty score will reach a certain level. --- Use the method @{#SCORING.SetFratricide}() to define the level when a player gets kicked. --- By default, the fratricide level is the default penalty mutiplier * 2 for the penalty score. --- --- ## 1.6) Penalty score when a player changes the coalition. --- --- When a player changes the coalition, he can receive a penalty score. --- Use the method @{#SCORING.SetCoalitionChangePenalty}() to define the penalty when a player changes coalition. --- By default, the penalty for changing coalition is the default penalty scale. --- --- ## 1.8) Define output CSV files. --- --- The CSV file is given the name of the string given in the @{#SCORING.New}{} constructor, followed by the .csv extension. --- The file is incrementally saved in the **\\Saved Games\\DCS\\Logs** folder, and has a time stamp indicating each mission run. --- See the following example: --- --- local ScoringFirstMission = SCORING:New( "FirstMission" ) --- local ScoringSecondMission = SCORING:New( "SecondMission" ) --- --- The above documents that 2 Scoring objects are created, ScoringFirstMission and ScoringSecondMission. --- --- ## 1.9) Configure messages. --- --- When players hit or destroy targets, messages are sent. --- Various methods exist to configure: --- --- * Which messages are sent upon the event. --- * Which audience receives the message. --- --- ### 1.9.1) Configure the messages sent upon the event. --- --- Use the following methods to configure when to send messages. By default, all messages are sent. --- --- * @{#SCORING.SetMessagesHit}(): Configure to send messages after a target has been hit. --- * @{#SCORING.SetMessagesDestroy}(): Configure to send messages after a target has been destroyed. --- * @{#SCORING.SetMessagesAddon}(): Configure to send messages for additional score, after a target has been destroyed. --- * @{#SCORING.SetMessagesZone}(): Configure to send messages for additional score, after a target has been destroyed within a given zone. --- --- ### 1.9.2) Configure the audience of the messages. --- --- Use the following methods to configure the audience of the messages. By default, the messages are sent to all players in the mission. --- --- * @{#SCORING.SetMessagesToAll}(): Configure to send messages to all players. --- * @{#SCORING.SetMessagesToCoalition}(): Configure to send messages to only those players within the same coalition as the player. --- --- --- ==== --- --- # **API CHANGE HISTORY** --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- Hereby the change log: --- --- 2017-02-26: Initial class and API. --- --- === --- --- # **AUTHORS and CONTRIBUTIONS** --- --- ### Contributions: --- --- * **Wingthor (TAW)**: Testing & Advice. --- * **Dutch-Baron (TAW)**: Testing & Advice. --- * **[Whisper](http://forums.eagle.ru/member.php?u=3829): Testing and Advice. --- --- ### Authors: --- --- * **FlightControl**: Concept, Design & Programming. --- --- @module Scoring - - ---- The Scoring class --- @type SCORING --- @field Players A collection of the current players that have joined the game. --- @extends Core.Base#BASE -SCORING = { - ClassName = "SCORING", - ClassID = 0, - Players = {}, -} - -local _SCORINGCoalition = - { - [1] = "Red", - [2] = "Blue", - } - -local _SCORINGCategory = - { - [Unit.Category.AIRPLANE] = "Plane", - [Unit.Category.HELICOPTER] = "Helicopter", - [Unit.Category.GROUND_UNIT] = "Vehicle", - [Unit.Category.SHIP] = "Ship", - [Unit.Category.STRUCTURE] = "Structure", - } - ---- Creates a new SCORING object to administer the scoring achieved by players. --- @param #SCORING self --- @param #string GameName The name of the game. This name is also logged in the CSV score file. --- @return #SCORING self --- @usage --- -- Define a new scoring object for the mission Gori Valley. --- ScoringObject = SCORING:New( "Gori Valley" ) -function SCORING:New( GameName ) - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) -- #SCORING - - if GameName then - self.GameName = GameName - else - error( "A game name must be given to register the scoring results" ) - end - - - -- Additional Object scores - self.ScoringObjects = {} - - -- Additional Zone scores. - self.ScoringZones = {} - - -- Configure Messages - self:SetMessagesToAll() - self:SetMessagesHit( true ) - self:SetMessagesDestroy( true ) - self:SetMessagesScore( true ) - self:SetMessagesZone( true ) - - -- Scales - self:SetScaleDestroyScore( 10 ) - self:SetScaleDestroyPenalty( 30 ) - - -- Default fratricide penalty level (maximum penalty that can be assigned to a player before he gets kicked). - self:SetFratricide( self.ScaleDestroyPenalty * 3 ) - - -- Default penalty when a player changes coalition. - self:SetCoalitionChangePenalty( self.ScaleDestroyPenalty ) - - -- Event handlers - self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) - self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) - self:HandleEvent( EVENTS.Hit, self._EventOnHit ) - self:HandleEvent( EVENTS.PlayerEnterUnit ) - self:HandleEvent( EVENTS.PlayerLeaveUnit ) - - -- Create the CSV file. - self:OpenCSV( GameName ) - - return self - -end - ---- Set the scale for scoring valid destroys (enemy destroys). --- A default calculated score is a value between 1 and 10. --- The scale magnifies the scores given to the players. --- @param #SCORING self --- @param #number Scale The scale of the score given. -function SCORING:SetScaleDestroyScore( Scale ) - - self.ScaleDestroyScore = Scale - - return self -end - ---- Set the scale for scoring penalty destroys (friendly destroys). --- A default calculated penalty is a value between 1 and 10. --- The scale magnifies the scores given to the players. --- @param #SCORING self --- @param #number Scale The scale of the score given. --- @return #SCORING -function SCORING:SetScaleDestroyPenalty( Scale ) - - self.ScaleDestroyPenalty = Scale - - return self -end - ---- Add a @{Unit} for additional scoring when the @{Unit} is destroyed. --- Note that if there was already a @{Unit} declared within the scoring with the same name, --- then the old @{Unit} will be replaced with the new @{Unit}. --- @param #SCORING self --- @param Wrapper.Unit#UNIT ScoreUnit The @{Unit} for which the Score needs to be given. --- @param #number Score The Score value. --- @return #SCORING -function SCORING:AddUnitScore( ScoreUnit, Score ) - - local UnitName = ScoreUnit:GetName() - - self.ScoringObjects[UnitName] = Score - - return self -end - ---- Removes a @{Unit} for additional scoring when the @{Unit} is destroyed. --- @param #SCORING self --- @param Wrapper.Unit#UNIT ScoreUnit The @{Unit} for which the Score needs to be given. --- @return #SCORING -function SCORING:RemoveUnitScore( ScoreUnit ) - - local UnitName = ScoreUnit:GetName() - - self.ScoringObjects[UnitName] = nil - - return self -end - ---- Add a @{Static} for additional scoring when the @{Static} is destroyed. --- Note that if there was already a @{Static} declared within the scoring with the same name, --- then the old @{Static} will be replaced with the new @{Static}. --- @param #SCORING self --- @param Wrapper.Static#UNIT ScoreStatic The @{Static} for which the Score needs to be given. --- @param #number Score The Score value. --- @return #SCORING -function SCORING:AddStaticScore( ScoreStatic, Score ) - - local StaticName = ScoreStatic:GetName() - - self.ScoringObjects[StaticName] = Score - - return self -end - ---- Removes a @{Static} for additional scoring when the @{Static} is destroyed. --- @param #SCORING self --- @param Wrapper.Static#UNIT ScoreStatic The @{Static} for which the Score needs to be given. --- @return #SCORING -function SCORING:RemoveStaticScore( ScoreStatic ) - - local StaticName = ScoreStatic:GetName() - - self.ScoringObjects[StaticName] = nil - - return self -end - - ---- Specify a special additional score for a @{Group}. --- @param #SCORING self --- @param Wrapper.Group#GROUP ScoreGroup The @{Group} for which each @{Unit} a Score is given. --- @param #number Score The Score value. --- @return #SCORING -function SCORING:AddScoreGroup( ScoreGroup, Score ) - - local ScoreUnits = ScoreGroup:GetUnits() - - for ScoreUnitID, ScoreUnit in pairs( ScoreUnits ) do - local UnitName = ScoreUnit:GetName() - self.ScoringObjects[UnitName] = Score - end - - return self -end - ---- Add a @{Zone} to define additional scoring when any object is destroyed in that zone. --- Note that if a @{Zone} with the same name is already within the scoring added, the @{Zone} (type) and Score will be replaced! --- This allows for a dynamic destruction zone evolution within your mission. --- @param #SCORING self --- @param Core.Zone#ZONE_BASE ScoreZone The @{Zone} which defines the destruction score perimeters. --- Note that a zone can be a polygon or a moving zone. --- @param #number Score The Score value. --- @return #SCORING -function SCORING:AddZoneScore( ScoreZone, Score ) - - local ZoneName = ScoreZone:GetName() - - self.ScoringZones[ZoneName] = {} - self.ScoringZones[ZoneName].ScoreZone = ScoreZone - self.ScoringZones[ZoneName].Score = Score - - return self -end - ---- Remove a @{Zone} for additional scoring. --- The scoring will search if any @{Zone} is added with the given name, and will remove that zone from the scoring. --- This allows for a dynamic destruction zone evolution within your mission. --- @param #SCORING self --- @param Core.Zone#ZONE_BASE ScoreZone The @{Zone} which defines the destruction score perimeters. --- Note that a zone can be a polygon or a moving zone. --- @return #SCORING -function SCORING:RemoveZoneScore( ScoreZone ) - - local ZoneName = ScoreZone:GetName() - - self.ScoringZones[ZoneName] = nil - - return self -end - - ---- Configure to send messages after a target has been hit. --- @param #SCORING self --- @param #boolean OnOff If true is given, the messages are sent. --- @return #SCORING -function SCORING:SetMessagesHit( OnOff ) - - self.MessagesHit = OnOff - return self -end - ---- If to send messages after a target has been hit. --- @param #SCORING self --- @return #boolean -function SCORING:IfMessagesHit() - - return self.MessagesHit -end - ---- Configure to send messages after a target has been destroyed. --- @param #SCORING self --- @param #boolean OnOff If true is given, the messages are sent. --- @return #SCORING -function SCORING:SetMessagesDestroy( OnOff ) - - self.MessagesDestroy = OnOff - return self -end - ---- If to send messages after a target has been destroyed. --- @param #SCORING self --- @return #boolean -function SCORING:IfMessagesDestroy() - - return self.MessagesDestroy -end - ---- Configure to send messages after a target has been destroyed and receives additional scores. --- @param #SCORING self --- @param #boolean OnOff If true is given, the messages are sent. --- @return #SCORING -function SCORING:SetMessagesScore( OnOff ) - - self.MessagesScore = OnOff - return self -end - ---- If to send messages after a target has been destroyed and receives additional scores. --- @param #SCORING self --- @return #boolean -function SCORING:IfMessagesScore() - - return self.MessagesScore -end - ---- Configure to send messages after a target has been hit in a zone, and additional score is received. --- @param #SCORING self --- @param #boolean OnOff If true is given, the messages are sent. --- @return #SCORING -function SCORING:SetMessagesZone( OnOff ) - - self.MessagesZone = OnOff - return self -end - ---- If to send messages after a target has been hit in a zone, and additional score is received. --- @param #SCORING self --- @return #boolean -function SCORING:IfMessagesZone() - - return self.MessagesZone -end - ---- Configure to send messages to all players. --- @param #SCORING self --- @return #SCORING -function SCORING:SetMessagesToAll() - - self.MessagesAudience = 1 - return self -end - ---- If to send messages to all players. --- @param #SCORING self --- @return #boolean -function SCORING:IfMessagesToAll() - - return self.MessagesAudience == 1 -end - ---- Configure to send messages to only those players within the same coalition as the player. --- @param #SCORING self --- @return #SCORING -function SCORING:SetMessagesToCoalition() - - self.MessagesAudience = 2 - return self -end - ---- If to send messages to only those players within the same coalition as the player. --- @param #SCORING self --- @return #boolean -function SCORING:IfMessagesToCoalition() - - return self.MessagesAudience == 2 -end - - ---- When a player commits too much damage to friendlies, his penalty score will reach a certain level. --- Use this method to define the level when a player gets kicked. --- By default, the fratricide level is the default penalty mutiplier * 2 for the penalty score. --- @param #SCORING self --- @param #number Fratricide The amount of maximum penalty that may be inflicted by a friendly player before he gets kicked. --- @return #SCORING -function SCORING:SetFratricide( Fratricide ) - - self.Fratricide = Fratricide - return self -end - - ---- When a player changes the coalition, he can receive a penalty score. --- Use the method @{#SCORING.SetCoalitionChangePenalty}() to define the penalty when a player changes coalition. --- By default, the penalty for changing coalition is the default penalty scale. --- @param #SCORING self --- @param #number CoalitionChangePenalty The amount of penalty that is given. --- @return #SCORING -function SCORING:SetCoalitionChangePenalty( CoalitionChangePenalty ) - - self.CoalitionChangePenalty = CoalitionChangePenalty - return self -end - - ---- Add a new player entering a Unit. --- @param #SCORING self --- @param Wrapper.Unit#UNIT UnitData -function SCORING:_AddPlayerFromUnit( UnitData ) - self:F( UnitData ) - - if UnitData:IsAlive() then - local UnitName = UnitData:GetName() - local PlayerName = UnitData:GetPlayerName() - local UnitDesc = UnitData:GetDesc() - local UnitCategory = UnitDesc.category - local UnitCoalition = UnitData:GetCoalition() - local UnitTypeName = UnitData:GetTypeName() - - self:T( { PlayerName, UnitName, UnitCategory, UnitCoalition, UnitTypeName } ) - - if self.Players[PlayerName] == nil then -- I believe this is the place where a Player gets a life in a mission when he enters a unit ... - self.Players[PlayerName] = {} - self.Players[PlayerName].Hit = {} - self.Players[PlayerName].Destroy = {} - self.Players[PlayerName].Goals = {} - self.Players[PlayerName].Mission = {} - - -- for CategoryID, CategoryName in pairs( SCORINGCategory ) do - -- self.Players[PlayerName].Hit[CategoryID] = {} - -- self.Players[PlayerName].Destroy[CategoryID] = {} - -- end - self.Players[PlayerName].HitPlayers = {} - self.Players[PlayerName].Score = 0 - self.Players[PlayerName].Penalty = 0 - self.Players[PlayerName].PenaltyCoalition = 0 - self.Players[PlayerName].PenaltyWarning = 0 - end - - if not self.Players[PlayerName].UnitCoalition then - self.Players[PlayerName].UnitCoalition = UnitCoalition - else - if self.Players[PlayerName].UnitCoalition ~= UnitCoalition then - self.Players[PlayerName].Penalty = self.Players[PlayerName].Penalty + 50 - self.Players[PlayerName].PenaltyCoalition = self.Players[PlayerName].PenaltyCoalition + 1 - MESSAGE: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 - self.Players[PlayerName].UNIT = UnitData - - if self.Players[PlayerName].Penalty > self.Fratricide * 0.50 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 " .. self.Fratricide .. ", 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 > self.Fratricide then - UnitData:Destroy() - MESSAGE:New( "Player '" .. PlayerName .. "' committed FRATRICIDE, he will be COURT MARTIALED and is DISMISSED from this mission!", - 10 - ):ToAll() - end - - end -end - - ---- Add a goal score for a player. --- The method takes the PlayerUnit for which the Goal score needs to be set. --- The GoalTag is a string or identifier that is taken into the CSV file scoring log to identify the goal. --- A free text can be given that is shown to the players. --- The Score can be both positive and negative. --- @param #SCORING self --- @param Wrapper.Unit#UNIT PlayerUnit The @{Unit} of the Player. Other Properties for the scoring are taken from this PlayerUnit, like coalition, type etc. --- @param #string GoalTag The string or identifier that is used in the CSV file to identify the goal (sort or group later in Excel). --- @param #string Text A free text that is shown to the players. --- @param #number Score The score can be both positive or negative ( Penalty ). -function SCORING:AddGoalScore( PlayerUnit, GoalTag, Text, Score ) - - local PlayerName = PlayerUnit:GetPlayerName() - - self:E( { PlayerUnit.UnitName, PlayerName, GoalTag, Text, Score } ) - - -- PlayerName can be nil, if the Unit with the player crashed or due to another reason. - if PlayerName then - local PlayerData = self.Players[PlayerName] - - PlayerData.Goals[GoalTag] = PlayerData.Goals[GoalTag] or { Score = 0 } - PlayerData.Goals[GoalTag].Score = PlayerData.Goals[GoalTag].Score + Score - PlayerData.Score = PlayerData.Score + Score - - MESSAGE:New( Text, 30 ):ToAll() - - self:ScoreCSV( PlayerName, "GOAL_" .. string.upper( GoalTag ), 1, Score, PlayerUnit:GetName() ) - end -end - - ---- Registers Scores the players completing a Mission Task. --- @param #SCORING self --- @param Tasking.Mission#MISSION Mission --- @param Wrapper.Unit#UNIT PlayerUnit --- @param #string Text --- @param #number Score -function SCORING:_AddMissionTaskScore( Mission, PlayerUnit, Text, Score ) - - local PlayerName = PlayerUnit:GetPlayerName() - local MissionName = Mission:GetName() - - self:E( { Mission:GetName(), PlayerUnit.UnitName, PlayerName, Text, Score } ) - - -- PlayerName can be nil, if the Unit with the player crashed or due to another reason. - if PlayerName then - local PlayerData = self.Players[PlayerName] - - if not PlayerData.Mission[MissionName] then - PlayerData.Mission[MissionName] = {} - PlayerData.Mission[MissionName].ScoreTask = 0 - PlayerData.Mission[MissionName].ScoreMission = 0 - end - - self:T( PlayerName ) - self:T( PlayerData.Mission[MissionName] ) - - PlayerData.Score = self.Players[PlayerName].Score + Score - PlayerData.Mission[MissionName].ScoreTask = self.Players[PlayerName].Mission[MissionName].ScoreTask + Score - - MESSAGE:New( "Player '" .. PlayerName .. "' has " .. Text .. " in Mission '" .. MissionName .. "'. " .. - Score .. " task score!", - 30 ):ToAll() - - self:ScoreCSV( PlayerName, "TASK_" .. MissionName:gsub( ' ', '_' ), 1, Score, PlayerUnit:GetName() ) - end -end - - ---- Registers Mission Scores for possible multiple players that contributed in the Mission. --- @param #SCORING self --- @param Tasking.Mission#MISSION Mission --- @param Wrapper.Unit#UNIT PlayerUnit --- @param #string Text --- @param #number Score -function SCORING:_AddMissionScore( Mission, Text, Score ) - - local MissionName = Mission:GetName() - - self:E( { Mission, Text, Score } ) - self:E( self.Players ) - - for PlayerName, PlayerData in pairs( self.Players ) do - - self:E( PlayerData ) - if PlayerData.Mission[MissionName] then - - PlayerData.Score = PlayerData.Score + Score - PlayerData.Mission[MissionName].ScoreMission = PlayerData.Mission[MissionName].ScoreMission + Score - - MESSAGE:New( "Player '" .. PlayerName .. "' has " .. Text .. " in Mission '" .. MissionName .. "'. " .. - Score .. " mission score!", - 60 ):ToAll() - - self:ScoreCSV( PlayerName, "MISSION_" .. MissionName:gsub( ' ', '_' ), 1, Score ) - end - end -end - - ---- Handles the OnPlayerEnterUnit event for the scoring. --- @param #SCORING self --- @param Core.Event#EVENTDATA Event -function SCORING:OnEventPlayerEnterUnit( Event ) - if Event.IniUnit then - self:_AddPlayerFromUnit( Event.IniUnit ) - local Menu = MENU_GROUP:New( Event.IniGroup, 'Scoring' ) - local ReportGroupSummary = MENU_GROUP_COMMAND:New( Event.IniGroup, 'Summary report players in group', Menu, SCORING.ReportScoreGroupSummary, self, Event.IniGroup ) - local ReportGroupDetailed = MENU_GROUP_COMMAND:New( Event.IniGroup, 'Detailed report players in group', Menu, SCORING.ReportScoreGroupDetailed, self, Event.IniGroup ) - local ReportToAllSummary = MENU_GROUP_COMMAND:New( Event.IniGroup, 'Summary report all players', Menu, SCORING.ReportScoreAllSummary, self, Event.IniGroup ) - self:SetState( Event.IniUnit, "ScoringMenu", Menu ) - end -end - ---- Handles the OnPlayerLeaveUnit event for the scoring. --- @param #SCORING self --- @param Core.Event#EVENTDATA Event -function SCORING:OnEventPlayerLeaveUnit( Event ) - if Event.IniUnit then - local Menu = self:GetState( Event.IniUnit, "ScoringMenu" ) -- Core.Menu#MENU_GROUP - if Menu then - Menu:Remove() - end - end -end - - ---- Handles the OnHit event for the scoring. --- @param #SCORING self --- @param Core.Event#EVENTDATA Event -function SCORING:_EventOnHit( Event ) - self:F( { Event } ) - - local InitUnit = nil - local InitUNIT = nil - local InitUnitName = "" - local InitGroup = nil - local InitGroupName = "" - local InitPlayerName = nil - - local InitCoalition = nil - local InitCategory = nil - local InitType = nil - local InitUnitCoalition = nil - local InitUnitCategory = nil - local InitUnitType = nil - - local TargetUnit = nil - local TargetUNIT = nil - local TargetUnitName = "" - local TargetGroup = nil - local TargetGroupName = "" - local TargetPlayerName = nil - - local TargetCoalition = nil - local TargetCategory = nil - local TargetType = nil - local TargetUnitCoalition = nil - local TargetUnitCategory = nil - local TargetUnitType = nil - - if Event.IniDCSUnit then - - InitUnit = Event.IniDCSUnit - InitUNIT = Event.IniUnit - InitUnitName = Event.IniDCSUnitName - InitGroup = Event.IniDCSGroup - InitGroupName = Event.IniDCSGroupName - InitPlayerName = Event.IniPlayerName - - InitCoalition = Event.IniCoalition - --TODO: Workaround Client DCS Bug - --InitCategory = InitUnit:getCategory() - --InitCategory = InitUnit:getDesc().category - InitCategory = Event.IniCategory - InitType = Event.IniTypeName - - InitUnitCoalition = _SCORINGCoalition[InitCoalition] - InitUnitCategory = _SCORINGCategory[InitCategory] - InitUnitType = InitType - - self:T( { InitUnitName, InitGroupName, InitPlayerName, InitCoalition, InitCategory, InitType , InitUnitCoalition, InitUnitCategory, InitUnitType } ) - end - - - if Event.TgtDCSUnit then - - TargetUnit = Event.TgtDCSUnit - TargetUNIT = Event.TgtUnit - TargetUnitName = Event.TgtDCSUnitName - TargetGroup = Event.TgtDCSGroup - TargetGroupName = Event.TgtDCSGroupName - TargetPlayerName = Event.TgtPlayerName - - TargetCoalition = Event.TgtCoalition - --TODO: Workaround Client DCS Bug - --TargetCategory = TargetUnit:getCategory() - --TargetCategory = TargetUnit:getDesc().category - TargetCategory = Event.TgtCategory - TargetType = Event.TgtTypeName - - TargetUnitCoalition = _SCORINGCoalition[TargetCoalition] - TargetUnitCategory = _SCORINGCategory[TargetCategory] - TargetUnitType = TargetType - - self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType, TargetUnitCoalition, TargetUnitCategory, TargetUnitType } ) - end - - if InitPlayerName ~= nil then -- It is a player that is hitting something - self:_AddPlayerFromUnit( InitUNIT ) - if self.Players[InitPlayerName] then -- This should normally not happen, but i'll test it anyway. - if TargetPlayerName ~= nil then -- It is a player hitting another player ... - self:_AddPlayerFromUnit( TargetUNIT ) - end - - self:T( "Hitting Something" ) - - -- What is he hitting? - if TargetCategory then - - -- A target got hit, score it. - -- Player contains the score data from self.Players[InitPlayerName] - local Player = self.Players[InitPlayerName] - - -- Ensure there is a hit table per TargetCategory and TargetUnitName. - Player.Hit[TargetCategory] = Player.Hit[TargetCategory] or {} - Player.Hit[TargetCategory][TargetUnitName] = Player.Hit[TargetCategory][TargetUnitName] or {} - - -- PlayerHit contains the score counters and data per unit that was hit. - local PlayerHit = Player.Hit[TargetCategory][TargetUnitName] - - PlayerHit.Score = PlayerHit.Score or 0 - PlayerHit.Penalty = PlayerHit.Penalty or 0 - PlayerHit.ScoreHit = PlayerHit.ScoreHit or 0 - PlayerHit.PenaltyHit = PlayerHit.PenaltyHit or 0 - PlayerHit.TimeStamp = PlayerHit.TimeStamp or 0 - PlayerHit.UNIT = PlayerHit.UNIT or TargetUNIT - - -- Only grant hit scores if there was more than one second between the last hit. - if timer.getTime() - PlayerHit.TimeStamp > 1 then - PlayerHit.TimeStamp = timer.getTime() - - if TargetPlayerName ~= nil then -- It is a player hitting another player ... - - -- Ensure there is a Player to Player hit reference table. - Player.HitPlayers[TargetPlayerName] = true - end - - local Score = 0 - - if InitCoalition then -- A coalition object was hit. - if InitCoalition == TargetCoalition then - Player.Penalty = Player.Penalty + 10 - PlayerHit.Penalty = PlayerHit.Penalty + 10 - PlayerHit.PenaltyHit = PlayerHit.PenaltyHit + 1 - - if TargetPlayerName ~= nil then -- It is a player hitting another player ... - MESSAGE - :New( "Player '" .. InitPlayerName .. "' hit friendly player '" .. TargetPlayerName .. "' " .. - TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.PenaltyHit .. " times. " .. - "Penalty: -" .. PlayerHit.Penalty .. ". Score Total:" .. Player.Score - Player.Penalty, - 2 - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) - else - MESSAGE - :New( "Player '" .. InitPlayerName .. "' hit a friendly target " .. - TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.PenaltyHit .. " times. " .. - "Penalty: -" .. PlayerHit.Penalty .. ". Score Total:" .. Player.Score - Player.Penalty, - 2 - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) - end - self:ScoreCSV( InitPlayerName, "HIT_PENALTY", 1, -25, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - else - Player.Score = Player.Score + 1 - PlayerHit.Score = PlayerHit.Score + 1 - PlayerHit.ScoreHit = PlayerHit.ScoreHit + 1 - if TargetPlayerName ~= nil then -- It is a player hitting another player ... - MESSAGE - :New( "Player '" .. InitPlayerName .. "' hit enemy player '" .. TargetPlayerName .. "' " .. - TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.ScoreHit .. " times. " .. - "Score: " .. PlayerHit.Score .. ". Score Total:" .. Player.Score - Player.Penalty, - 2 - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) - else - MESSAGE - :New( "Player '" .. InitPlayerName .. "' hit an enemy target " .. - TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.ScoreHit .. " times. " .. - "Score: " .. PlayerHit.Score .. ". Score Total:" .. Player.Score - Player.Penalty, - 2 - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) - end - self:ScoreCSV( InitPlayerName, "HIT_SCORE", 1, 1, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - end - else -- A scenery object was hit. - MESSAGE - :New( "Player '" .. InitPlayerName .. "' hit a scenery object.", - 2 - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) - self:ScoreCSV( InitPlayerName, "HIT_SCORE", 1, 1, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, "", "Scenery", TargetUnitType ) - end - end - end - end - elseif InitPlayerName == nil then -- It is an AI hitting a player??? - - end -end - ---- Track DEAD or CRASH events for the scoring. --- @param #SCORING self --- @param Core.Event#EVENTDATA Event -function SCORING:_EventOnDeadOrCrash( Event ) - self:F( { Event } ) - - local TargetUnit = nil - local TargetGroup = nil - local TargetUnitName = "" - local TargetGroupName = "" - local TargetPlayerName = "" - local TargetCoalition = nil - local TargetCategory = nil - local TargetType = nil - local TargetUnitCoalition = nil - local TargetUnitCategory = nil - local TargetUnitType = nil - - if Event.IniDCSUnit then - - TargetUnit = Event.IniUnit - TargetUnitName = Event.IniDCSUnitName - TargetGroup = Event.IniDCSGroup - TargetGroupName = Event.IniDCSGroupName - TargetPlayerName = Event.IniPlayerName - - TargetCoalition = Event.IniCoalition - --TargetCategory = TargetUnit:getCategory() - --TargetCategory = TargetUnit:getDesc().category -- Workaround - TargetCategory = Event.IniCategory - TargetType = Event.IniTypeName - - TargetUnitCoalition = _SCORINGCoalition[TargetCoalition] - TargetUnitCategory = _SCORINGCategory[TargetCategory] - TargetUnitType = TargetType - - self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType } ) - end - - -- Player contains the score and reference data for the player. - for PlayerName, Player in pairs( self.Players ) do - if Player then -- This should normally not happen, but i'll test it anyway. - self:T( "Something got destroyed" ) - - -- Some variables - local InitUnitName = Player.UnitName - local InitUnitType = Player.UnitType - local InitCoalition = Player.UnitCoalition - local InitCategory = Player.UnitCategory - local InitUnitCoalition = _SCORINGCoalition[InitCoalition] - local InitUnitCategory = _SCORINGCategory[InitCategory] - - self:T( { InitUnitName, InitUnitType, InitUnitCoalition, InitCoalition, InitUnitCategory, InitCategory } ) - - -- What is the player destroying? - if Player and Player.Hit and Player.Hit[TargetCategory] and Player.Hit[TargetCategory][TargetUnitName] then -- Was there a hit for this unit for this player before registered??? - - Player.Destroy[TargetCategory] = Player.Destroy[TargetCategory] or {} - Player.Destroy[TargetCategory][TargetType] = Player.Destroy[TargetCategory][TargetType] or {} - - -- PlayerDestroy contains the destroy score data per category and target type of the player. - local TargetDestroy = Player.Destroy[TargetCategory][TargetType] - TargetDestroy.Score = TargetDestroy.Score or 0 - TargetDestroy.ScoreDestroy = TargetDestroy.ScoreDestroy or 0 - TargetDestroy.Penalty = TargetDestroy.Penalty or 0 - TargetDestroy.PenaltyDestroy = TargetDestroy.PenaltyDestroy or 0 - - if TargetCoalition then - if InitCoalition == TargetCoalition then - local ThreatLevelTarget, ThreatTypeTarget = TargetUnit:GetThreatLevel() - local ThreatLevelPlayer = Player.UNIT:GetThreatLevel() / 10 + 1 - local ThreatPenalty = math.ceil( ( ThreatLevelTarget / ThreatLevelPlayer ) * self.ScaleDestroyPenalty / 10 ) - self:E( { ThreatLevel = ThreatPenalty, ThreatLevelTarget = ThreatLevelTarget, ThreatTypeTarget = ThreatTypeTarget, ThreatLevelPlayer = ThreatLevelPlayer } ) - - Player.Penalty = Player.Penalty + ThreatPenalty - TargetDestroy.Penalty = TargetDestroy.Penalty + ThreatPenalty - TargetDestroy.PenaltyDestroy = TargetDestroy.PenaltyDestroy + 1 - - if Player.HitPlayers[TargetPlayerName] then -- A player destroyed another player - MESSAGE - :New( "Player '" .. PlayerName .. "' destroyed friendly player '" .. TargetPlayerName .. "' " .. - TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. TargetDestroy.PenaltyDestroy .. " times. " .. - "Penalty: -" .. TargetDestroy.Penalty .. ". Score Total:" .. Player.Score - Player.Penalty, - 15 - ) - :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) - else - MESSAGE - :New( "Player '" .. PlayerName .. "' destroyed a friendly target " .. - TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. TargetDestroy.PenaltyDestroy .. " times. " .. - "Penalty: -" .. TargetDestroy.Penalty .. ". Score Total:" .. Player.Score - Player.Penalty, - 15 - ) - :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) - end - self:ScoreCSV( PlayerName, "DESTROY_PENALTY", 1, ThreatPenalty, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - else - - local ThreatLevelTarget, ThreatTypeTarget = TargetUnit:GetThreatLevel() - local ThreatLevelPlayer = Player.UNIT:GetThreatLevel() / 10 + 1 - local ThreatScore = math.ceil( ( ThreatLevelTarget / ThreatLevelPlayer ) * self.ScaleDestroyScore / 10 ) - - self:E( { ThreatLevel = ThreatScore, ThreatLevelTarget = ThreatLevelTarget, ThreatTypeTarget = ThreatTypeTarget, ThreatLevelPlayer = ThreatLevelPlayer } ) - - Player.Score = Player.Score + ThreatScore - TargetDestroy.Score = TargetDestroy.Score + ThreatScore - TargetDestroy.ScoreDestroy = TargetDestroy.ScoreDestroy + 1 - if Player.HitPlayers[TargetPlayerName] then -- A player destroyed another player - MESSAGE - :New( "Player '" .. PlayerName .. "' destroyed enemy player '" .. TargetPlayerName .. "' " .. - TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. TargetDestroy.ScoreDestroy .. " times. " .. - "Score: " .. TargetDestroy.Score .. ". Score Total:" .. Player.Score - Player.Penalty, - 15 - ) - :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) - else - MESSAGE - :New( "Player '" .. PlayerName .. "' destroyed an enemy " .. - TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. TargetDestroy.ScoreDestroy .. " times. " .. - "Score: " .. TargetDestroy.Score .. ". Total:" .. Player.Score - Player.Penalty, - 15 - ) - :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) - end - self:ScoreCSV( PlayerName, "DESTROY_SCORE", 1, ThreatScore, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - - local UnitName = TargetUnit:GetName() - local Score = self.ScoringObjects[UnitName] - if Score then - Player.Score = Player.Score + Score - TargetDestroy.Score = TargetDestroy.Score + Score - MESSAGE - :New( "Special target '" .. TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. " destroyed! " .. - "Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! Total: " .. Player.Score - Player.Penalty, - 15 - ) - :ToAllIf( self:IfMessagesScore() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesScore() and self:IfMessagesToCoalition() ) - self:ScoreCSV( PlayerName, "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - end - - -- Check if there are Zones where the destruction happened. - for ZoneName, ScoreZoneData in pairs( self.ScoringZones ) do - self:E( { ScoringZone = ScoreZoneData } ) - local ScoreZone = ScoreZoneData.ScoreZone -- Core.Zone#ZONE_BASE - local Score = ScoreZoneData.Score - if ScoreZone:IsVec2InZone( TargetUnit:GetVec2() ) then - Player.Score = Player.Score + Score - TargetDestroy.Score = TargetDestroy.Score + Score - MESSAGE - :New( "Target destroyed in zone '" .. ScoreZone:GetName() .. "'." .. - "Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! " .. - "Total: " .. Player.Score - Player.Penalty, - 15 ) - :ToAllIf( self:IfMessagesZone() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesZone() and self:IfMessagesToCoalition() ) - self:ScoreCSV( PlayerName, "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - end - end - - end - else - -- Check if there are Zones where the destruction happened. - for ZoneName, ScoreZoneData in pairs( self.ScoringZones ) do - self:E( { ScoringZone = ScoreZoneData } ) - local ScoreZone = ScoreZoneData.ScoreZone -- Core.Zone#ZONE_BASE - local Score = ScoreZoneData.Score - if ScoreZone:IsVec2InZone( TargetUnit:GetVec2() ) then - Player.Score = Player.Score + Score - TargetDestroy.Score = TargetDestroy.Score + Score - MESSAGE - :New( "Scenery destroyed in zone '" .. ScoreZone:GetName() .. "'." .. - "Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! " .. - "Total: " .. Player.Score - Player.Penalty, - 15 - ) - :ToAllIf( self:IfMessagesZone() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesZone() and self:IfMessagesToCoalition() ) - self:ScoreCSV( PlayerName, "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, "", "Scenery", TargetUnitType ) - end - end - end - end - end - end -end - - ---- Produce detailed report of player hit scores. --- @param #SCORING self --- @param #string PlayerName The name of the player. --- @return #string The report. -function SCORING:ReportDetailedPlayerHits( PlayerName ) - - local ScoreMessage = "" - local PlayerScore = 0 - local PlayerPenalty = 0 - - local PlayerData = self.Players[PlayerName] - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) - - -- Some variables - local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] - local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local ScoreMessageHits = "" - for CategoryID, CategoryName in pairs( _SCORINGCategory ) do - self:T( CategoryName ) - if PlayerData.Hit[CategoryID] then - self:T( "Hit scores exist for player " .. PlayerName ) - local Score = 0 - local ScoreHit = 0 - local Penalty = 0 - local PenaltyHit = 0 - for UnitName, UnitData in pairs( PlayerData.Hit[CategoryID] ) do - Score = Score + UnitData.Score - ScoreHit = ScoreHit + UnitData.ScoreHit - Penalty = Penalty + UnitData.Penalty - PenaltyHit = UnitData.PenaltyHit - end - local ScoreMessageHit = string.format( "%s:%d ", CategoryName, Score - Penalty ) - self:T( ScoreMessageHit ) - ScoreMessageHits = ScoreMessageHits .. ScoreMessageHit - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageHits ~= "" then - ScoreMessage = "Hits: " .. ScoreMessageHits - end - end - - return ScoreMessage, PlayerScore, PlayerPenalty -end - - ---- Produce detailed report of player destroy scores. --- @param #SCORING self --- @param #string PlayerName The name of the player. --- @return #string The report. -function SCORING:ReportDetailedPlayerDestroys( PlayerName ) - - local ScoreMessage = "" - local PlayerScore = 0 - local PlayerPenalty = 0 - - local PlayerData = self.Players[PlayerName] - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) - - -- Some variables - local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] - local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local ScoreMessageDestroys = "" - for CategoryID, CategoryName in pairs( _SCORINGCategory ) do - if PlayerData.Destroy[CategoryID] then - self:T( "Destroy scores exist for player " .. PlayerName ) - local Score = 0 - local ScoreDestroy = 0 - local Penalty = 0 - local PenaltyDestroy = 0 - - for UnitName, UnitData in pairs( PlayerData.Destroy[CategoryID] ) do - self:E( { UnitData = UnitData } ) - if UnitData ~= {} then - Score = Score + UnitData.Score - ScoreDestroy = ScoreDestroy + UnitData.ScoreDestroy - Penalty = Penalty + UnitData.Penalty - PenaltyDestroy = PenaltyDestroy + UnitData.PenaltyDestroy - end - end - - local ScoreMessageDestroy = string.format( " %s:%d ", CategoryName, Score - Penalty ) - self:T( ScoreMessageDestroy ) - ScoreMessageDestroys = ScoreMessageDestroys .. ScoreMessageDestroy - - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageDestroys = ScoreMessageDestroys .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageDestroys ~= "" then - ScoreMessage = "Destroys: " .. ScoreMessageDestroys - end - end - - return ScoreMessage, PlayerScore, PlayerPenalty -end - ---- Produce detailed report of player penalty scores because of changing the coalition. --- @param #SCORING self --- @param #string PlayerName The name of the player. --- @return #string The report. -function SCORING:ReportDetailedPlayerCoalitionChanges( PlayerName ) - - local ScoreMessage = "" - local PlayerScore = 0 - local PlayerPenalty = 0 - - local PlayerData = self.Players[PlayerName] - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) - - -- Some variables - local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] - local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local ScoreMessageCoalitionChangePenalties = "" - if PlayerData.PenaltyCoalition ~= 0 then - ScoreMessageCoalitionChangePenalties = ScoreMessageCoalitionChangePenalties .. string.format( " -%d (%d changed)", PlayerData.Penalty, PlayerData.PenaltyCoalition ) - PlayerPenalty = PlayerPenalty + PlayerData.Penalty - end - if ScoreMessageCoalitionChangePenalties ~= "" then - ScoreMessage = ScoreMessage .. "Coalition Penalties: " .. ScoreMessageCoalitionChangePenalties - end - end - - return ScoreMessage, PlayerScore, PlayerPenalty -end - ---- Produce detailed report of player goal scores. --- @param #SCORING self --- @param #string PlayerName The name of the player. --- @return #string The report. -function SCORING:ReportDetailedPlayerGoals( PlayerName ) - - local ScoreMessage = "" - local PlayerScore = 0 - local PlayerPenalty = 0 - - local PlayerData = self.Players[PlayerName] - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) - - -- Some variables - local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] - local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local ScoreMessageGoal = "" - local ScoreGoal = 0 - local ScoreTask = 0 - for GoalName, GoalData in pairs( PlayerData.Goals ) do - ScoreGoal = ScoreGoal + GoalData.Score - ScoreMessageGoal = ScoreMessageGoal .. "'" .. GoalName .. "':" .. GoalData.Score .. "; " - end - PlayerScore = PlayerScore + ScoreGoal - - if ScoreMessageGoal ~= "" then - ScoreMessage = "Goals: " .. ScoreMessageGoal - end - end - - return ScoreMessage, PlayerScore, PlayerPenalty -end - ---- Produce detailed report of player penalty scores because of changing the coalition. --- @param #SCORING self --- @param #string PlayerName The name of the player. --- @return #string The report. -function SCORING:ReportDetailedPlayerMissions( PlayerName ) - - local ScoreMessage = "" - local PlayerScore = 0 - local PlayerPenalty = 0 - - local PlayerData = self.Players[PlayerName] - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) - - -- Some variables - local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] - local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local ScoreMessageMission = "" - local ScoreMission = 0 - local ScoreTask = 0 - for MissionName, MissionData in pairs( PlayerData.Mission ) do - ScoreMission = ScoreMission + MissionData.ScoreMission - ScoreTask = ScoreTask + MissionData.ScoreTask - ScoreMessageMission = ScoreMessageMission .. "'" .. MissionName .. "'; " - end - PlayerScore = PlayerScore + ScoreMission + ScoreTask - - if ScoreMessageMission ~= "" then - ScoreMessage = "Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ")" - end - end - - return ScoreMessage, PlayerScore, PlayerPenalty -end - - ---- Report Group Score Summary --- @param #SCORING self --- @param Wrapper.Group#GROUP PlayerGroup The player group. -function SCORING:ReportScoreGroupSummary( PlayerGroup ) - - local PlayerMessage = "" - - self:T( "Report Score Group Summary" ) - - local PlayerUnits = PlayerGroup:GetUnits() - for UnitID, PlayerUnit in pairs( PlayerUnits ) do - local PlayerUnit = PlayerUnit -- Wrapper.Unit#UNIT - local PlayerName = PlayerUnit:GetPlayerName() - - if PlayerName then - - local ReportHits, ScoreHits, PenaltyHits = self:ReportDetailedPlayerHits( PlayerName ) - ReportHits = ReportHits ~= "" and "\n- " .. ReportHits or ReportHits - self:E( { ReportHits, ScoreHits, PenaltyHits } ) - - local ReportDestroys, ScoreDestroys, PenaltyDestroys = self:ReportDetailedPlayerDestroys( PlayerName ) - ReportDestroys = ReportDestroys ~= "" and "\n- " .. ReportDestroys or ReportDestroys - self:E( { ReportDestroys, ScoreDestroys, PenaltyDestroys } ) - - local ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges = self:ReportDetailedPlayerCoalitionChanges( PlayerName ) - ReportCoalitionChanges = ReportCoalitionChanges ~= "" and "\n- " .. ReportCoalitionChanges or ReportCoalitionChanges - self:E( { ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges } ) - - local ReportGoals, ScoreGoals, PenaltyGoals = self:ReportDetailedPlayerGoals( PlayerName ) - ReportGoals = ReportGoals ~= "" and "\n- " .. ReportGoals or ReportGoals - self:E( { ReportGoals, ScoreGoals, PenaltyGoals } ) - - local ReportMissions, ScoreMissions, PenaltyMissions = self:ReportDetailedPlayerMissions( PlayerName ) - ReportMissions = ReportMissions ~= "" and "\n- " .. ReportMissions or ReportMissions - self:E( { ReportMissions, ScoreMissions, PenaltyMissions } ) - - local PlayerScore = ScoreHits + ScoreDestroys + ScoreCoalitionChanges + ScoreGoals + ScoreMissions - local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + ScoreGoals + PenaltyMissions - - PlayerMessage = - string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )", - PlayerName, - PlayerScore - PlayerPenalty, - PlayerScore, - PlayerPenalty - ) - MESSAGE:New( PlayerMessage, 30, "Player '" .. PlayerName .. "'" ):ToGroup( PlayerGroup ) - end - end - -end - ---- Report Group Score Detailed --- @param #SCORING self --- @param Wrapper.Group#GROUP PlayerGroup The player group. -function SCORING:ReportScoreGroupDetailed( PlayerGroup ) - - local PlayerMessage = "" - - self:T( "Report Score Group Detailed" ) - - local PlayerUnits = PlayerGroup:GetUnits() - for UnitID, PlayerUnit in pairs( PlayerUnits ) do - local PlayerUnit = PlayerUnit -- Wrapper.Unit#UNIT - local PlayerName = PlayerUnit:GetPlayerName() - - if PlayerName then - - local ReportHits, ScoreHits, PenaltyHits = self:ReportDetailedPlayerHits( PlayerName ) - ReportHits = ReportHits ~= "" and "\n- " .. ReportHits or ReportHits - self:E( { ReportHits, ScoreHits, PenaltyHits } ) - - local ReportDestroys, ScoreDestroys, PenaltyDestroys = self:ReportDetailedPlayerDestroys( PlayerName ) - ReportDestroys = ReportDestroys ~= "" and "\n- " .. ReportDestroys or ReportDestroys - self:E( { ReportDestroys, ScoreDestroys, PenaltyDestroys } ) - - local ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges = self:ReportDetailedPlayerCoalitionChanges( PlayerName ) - ReportCoalitionChanges = ReportCoalitionChanges ~= "" and "\n- " .. ReportCoalitionChanges or ReportCoalitionChanges - self:E( { ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges } ) - - local ReportGoals, ScoreGoals, PenaltyGoals = self:ReportDetailedPlayerGoals( PlayerName ) - ReportGoals = ReportGoals ~= "" and "\n- " .. ReportGoals or ReportGoals - self:E( { ReportGoals, ScoreGoals, PenaltyGoals } ) - - local ReportMissions, ScoreMissions, PenaltyMissions = self:ReportDetailedPlayerMissions( PlayerName ) - ReportMissions = ReportMissions ~= "" and "\n- " .. ReportMissions or ReportMissions - self:E( { ReportMissions, ScoreMissions, PenaltyMissions } ) - - local PlayerScore = ScoreHits + ScoreDestroys + ScoreCoalitionChanges + ScoreGoals + ScoreMissions - local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + ScoreGoals + PenaltyMissions - - PlayerMessage = - string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )%s%s%s%s%s", - PlayerName, - PlayerScore - PlayerPenalty, - PlayerScore, - PlayerPenalty, - ReportHits, - ReportDestroys, - ReportCoalitionChanges, - ReportGoals, - ReportMissions - ) - MESSAGE:New( PlayerMessage, 30, "Player '" .. PlayerName .. "'" ):ToGroup( PlayerGroup ) - end - end - -end - ---- Report all players score --- @param #SCORING self --- @param Wrapper.Group#GROUP PlayerGroup The player group. -function SCORING:ReportScoreAllSummary( PlayerGroup ) - - local PlayerMessage = "" - - self:T( "Report Score All Players" ) - - for PlayerName, PlayerData in pairs( self.Players ) do - - if PlayerName then - - local ReportHits, ScoreHits, PenaltyHits = self:ReportDetailedPlayerHits( PlayerName ) - ReportHits = ReportHits ~= "" and "\n- " .. ReportHits or ReportHits - self:E( { ReportHits, ScoreHits, PenaltyHits } ) - - local ReportDestroys, ScoreDestroys, PenaltyDestroys = self:ReportDetailedPlayerDestroys( PlayerName ) - ReportDestroys = ReportDestroys ~= "" and "\n- " .. ReportDestroys or ReportDestroys - self:E( { ReportDestroys, ScoreDestroys, PenaltyDestroys } ) - - local ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges = self:ReportDetailedPlayerCoalitionChanges( PlayerName ) - ReportCoalitionChanges = ReportCoalitionChanges ~= "" and "\n- " .. ReportCoalitionChanges or ReportCoalitionChanges - self:E( { ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges } ) - - local ReportGoals, ScoreGoals, PenaltyGoals = self:ReportDetailedPlayerGoals( PlayerName ) - ReportGoals = ReportGoals ~= "" and "\n- " .. ReportGoals or ReportGoals - self:E( { ReportGoals, ScoreGoals, PenaltyGoals } ) - - local ReportMissions, ScoreMissions, PenaltyMissions = self:ReportDetailedPlayerMissions( PlayerName ) - ReportMissions = ReportMissions ~= "" and "\n- " .. ReportMissions or ReportMissions - self:E( { ReportMissions, ScoreMissions, PenaltyMissions } ) - - local PlayerScore = ScoreHits + ScoreDestroys + ScoreCoalitionChanges + ScoreGoals + ScoreMissions - local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + ScoreGoals + PenaltyMissions - - PlayerMessage = - string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )", - PlayerName, - PlayerScore - PlayerPenalty, - PlayerScore, - PlayerPenalty - ) - MESSAGE:New( PlayerMessage, 30, "Player '" .. PlayerName .. "'" ):ToGroup( PlayerGroup ) - end - end - -end - - -function SCORING:SecondsToClock(sSeconds) - local nSeconds = sSeconds - if nSeconds == 0 then - --return nil; - return "00:00:00"; - else - nHours = string.format("%02.f", math.floor(nSeconds/3600)); - nMins = string.format("%02.f", math.floor(nSeconds/60 - (nHours*60))); - nSecs = string.format("%02.f", math.floor(nSeconds - nHours*3600 - nMins *60)); - return nHours..":"..nMins..":"..nSecs - end -end - ---- Opens a score CSV file to log the scores. --- @param #SCORING self --- @param #string ScoringCSV --- @return #SCORING self --- @usage --- -- Open a new CSV file to log the scores of the game Gori Valley. Let the name of the CSV file begin with "Player Scores". --- ScoringObject = SCORING:New( "Gori Valley" ) --- ScoringObject:OpenCSV( "Player Scores" ) -function SCORING:OpenCSV( ScoringCSV ) - self:F( ScoringCSV ) - - if lfs and io and os then - if ScoringCSV then - self.ScoringCSV = ScoringCSV - local fdir = lfs.writedir() .. [[Logs\]] .. self.ScoringCSV .. " " .. os.date( "%Y-%m-%d %H-%M-%S" ) .. ".csv" - - self.CSVFile, self.err = io.open( fdir, "w+" ) - if not self.CSVFile then - error( "Error: Cannot open CSV file in " .. lfs.writedir() ) - end - - self.CSVFile:write( '"GameName","RunTime","Time","PlayerName","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 - - TargetUnitCoalition = TargetUnitCoalition or "" - TargetUnitCategory = TargetUnitCategory or "" - TargetUnitType = TargetUnitType or "" - TargetUnitName = TargetUnitName or "" - - if lfs and io and os then - self.CSVFile:write( - '"' .. self.GameName .. '"' .. ',' .. - '"' .. self.RunTime .. '"' .. ',' .. - '' .. ScoreTime .. '' .. ',' .. - '"' .. PlayerName .. '"' .. ',' .. - '"' .. 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 - ---- 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 Core.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 = 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 Dcs.DCSWrapper.Group#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 @{DCSWrapper.Unit#Unit} from the simulator, but checks first if it is still existing! --- @param #CLEANUP self --- @param Dcs.DCSWrapper.Unit#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 Dcs.DCSTypes#Weapon ---- Destroys a missile from the simulator, but checks first if it is still existing! --- @param #CLEANUP self --- @param Dcs.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 Dcs.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 Dcs.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() - 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 Dcs.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 ) - 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 ) - SCHEDULER:New( self, CLEANUP._DestroyUnit, { Event.TgtDCSUnit }, 0.1 ) - end - end - end -end - ---- Add the @{DCSWrapper.Unit#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 Dcs.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 - ---- Single-Player:**Yes** / Multi-Player:**Yes** / AI:**Yes** / Human:**No** / Types:**All** -- --- **Spawn groups of units dynamically in your missions.** --- --- ![Banner Image](..\Presentations\SPAWN\SPAWN.JPG) --- --- === --- --- # 1) @{#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 methods (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 represents the GROUP Template (definition). --- * @{#SPAWN.NewWithAlias}(): Creates a new SPAWN object taking the name of the group that represents the GROUP Template (definition), and gives each spawned @{Group} an different name. --- --- It is important to understand how the SPAWN class works internally. The SPAWN object created will contain internally a list of groups that will be spawned and that are already spawned. --- The initialization methods will modify this list of groups so that when a group gets spawned, ALL information is already prepared when spawning. This is done for performance reasons. --- So in principle, the group list will contain all parameters and configurations after initialization, and when groups get actually spawned, this spawning can be done quickly and efficient. --- --- ## 1.2) SPAWN initialization methods --- --- A spawn object will behave differently based on the usage of **initialization** methods, which all start with the **Init** prefix: --- --- * @{#SPAWN.InitLimit}(): Limits the amount of groups that can be alive at the same time and that can be dynamically spawned. --- * @{#SPAWN.InitRandomizeRoute}(): Randomize the routes of spawned groups, and for air groups also optionally the height. --- * @{#SPAWN.InitRandomizeTemplate}(): Randomize the group templates so that when a new group is spawned, a random group template is selected from one of the templates defined. --- * @{#SPAWN.InitUnControlled}(): Spawn plane groups uncontrolled. --- * @{#SPAWN.InitArray}(): 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 methods are @{#SPAWN.InitRepeatOnLanding} and @{#SPAWN.InitRepeatOnEngineShutDown}. --- * @{#SPAWN.InitRandomizeUnits}(): Randomizes the @{Unit}s in the @{Group} that is spawned within a **radius band**, given an Outer and Inner radius. --- * @{#SPAWN.InitRandomizeZones}(): Randomizes the spawning between a predefined list of @{Zone}s that are declared using this function. Each zone can be given a probability factor. --- * @{#SPAWN.InitAIOn}(): Turns the AI On when spawning the new @{Group} object. --- * @{#SPAWN.InitAIOff}(): Turns the AI Off when spawning the new @{Group} object. --- * @{#SPAWN.InitAIOnOff}(): Turns the AI On or Off when spawning the new @{Group} object. --- --- ## 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.SpawnFromVec3}(): Spawn a new group from a Vec3 coordinate. (The group will can be spawned at a point in the air). --- * @{#SPAWN.SpawnFromVec2}(): Spawn a new group from a Vec2 coordinate. (The group will be spawned at land height ). --- * @{#SPAWN.SpawnFromStatic}(): Spawn a new group from a structure, taking the position of a @{Static}. --- * @{#SPAWN.SpawnFromUnit}(): Spawn a new group taking the position of a @{Unit}. --- * @{#SPAWN.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) Retrieve alive GROUPs spawned by the SPAWN object --- --- The SPAWN class administers which GROUPS it has reserved (in stock) or has created during mission execution. --- Every time a SPAWN object spawns a new GROUP object, a reference to the GROUP object is added to an internal table of GROUPS. --- SPAWN provides methods to iterate through that internal GROUP object reference table: --- --- * @{#SPAWN.GetFirstAliveGroup}(): Will find the first alive GROUP it has spawned, and return the alive GROUP object and the first Index where the first alive GROUP object has been found. --- * @{#SPAWN.GetNextAliveGroup}(): Will find the next alive GROUP object from a given Index, and return a reference to the alive GROUP object and the next Index where the alive GROUP has been found. --- * @{#SPAWN.GetLastAliveGroup}(): Will find the last alive GROUP object, and will return a reference to the last live GROUP object and the last Index where the last alive GROUP object has been found. --- --- You can use the methods @{#SPAWN.GetFirstAliveGroup}() and sequently @{#SPAWN.GetNextAliveGroup}() to iterate through the alive GROUPS within the SPAWN object, and to actions... See the respective methods for an example. --- The method @{#SPAWN.GetGroupFromIndex}() will return the GROUP object reference from the given Index, dead or alive... --- --- ## 1.5) 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.InitCleanUp}() initialization method has been defined that will silently monitor the status of each spawned group. --- Once a group has a velocity = 0, and has been waiting for a defined interval, that group will be cleaned or removed from run-time. --- There is a catch however :-) If a damaged group has returned to an airbase within the coalition, that group will not be considered as "lost"... --- In such a case, when the inactive group is cleaned, a new group will Re-spawned automatically. --- This models AI that has succesfully returned to their airbase, to restart their combat activities. --- Check the @{#SPAWN.InitCleanUp}() for further info. --- --- ## 1.6) Catch the @{Group} spawn event in a callback function! --- --- When using the SpawnScheduled method, new @{Group}s are created following the schedule timing parameters. --- When a new @{Group} is spawned, you maybe want to execute actions with that group spawned at the spawn event. --- To SPAWN class supports this functionality through the @{#SPAWN.OnSpawnGroup}( **function( SpawnedGroup ) end ** ) method, which takes a function as a parameter that you can define locally. --- Whenever a new @{Group} is spawned, the given function is called, and the @{Group} that was just spawned, is given as a parameter. --- As a result, your spawn event handling function requires one parameter to be declared, which will contain the spawned @{Group} object. --- A coding example is provided at the description of the @{#SPAWN.OnSpawnGroup}( **function( SpawnedGroup ) end ** ) method. --- --- ==== --- --- # **API CHANGE HISTORY** --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- Hereby the change log: --- --- 2017-02-04: SPAWN:InitUnControlled( **UnControlled** ) replaces SPAWN:InitUnControlled(). --- --- 2017-01-24: SPAWN:**InitAIOnOff( AIOnOff )** added. --- --- 2017-01-24: SPAWN:**InitAIOn()** added. --- --- 2017-01-24: SPAWN:**InitAIOff()** added. --- --- 2016-08-15: SPAWN:**InitCleanUp**( SpawnCleanUpInterval ) replaces SPAWN:_CleanUp_( SpawnCleanUpInterval ). --- --- 2016-08-15: SPAWN:**InitRandomizeZones( SpawnZones )** added. --- --- 2016-08-14: SPAWN:**OnSpawnGroup**( SpawnCallBackFunction, ... ) replaces SPAWN:_SpawnFunction_( SpawnCallBackFunction, ... ). --- --- 2016-08-14: SPAWN.SpawnInZone( Zone, __RandomizeGroup__, SpawnIndex ) replaces SpawnInZone( Zone, _RandomizeUnits, OuterRadius, InnerRadius,_ SpawnIndex ). --- --- 2016-08-14: SPAWN.SpawnFromVec3( Vec3, SpawnIndex ) replaces SpawnFromVec3( Vec3, _RandomizeUnits, OuterRadius, InnerRadius,_ SpawnIndex ): --- --- 2016-08-14: SPAWN.SpawnFromVec2( Vec2, SpawnIndex ) replaces SpawnFromVec2( Vec2, _RandomizeUnits, OuterRadius, InnerRadius,_ SpawnIndex ): --- --- 2016-08-14: SPAWN.SpawnFromUnit( SpawnUnit, SpawnIndex ) replaces SpawnFromUnit( SpawnUnit, _RandomizeUnits, OuterRadius, InnerRadius,_ SpawnIndex ): --- --- 2016-08-14: SPAWN.SpawnFromUnit( SpawnUnit, SpawnIndex ) replaces SpawnFromStatic( SpawnStatic, _RandomizeUnits, OuterRadius, InnerRadius,_ SpawnIndex ): --- --- 2016-08-14: SPAWN.**InitRandomizeUnits( RandomizeUnits, OuterRadius, InnerRadius )** added: --- --- 2016-08-14: SPAWN.**Init**Limit( SpawnMaxUnitsAlive, SpawnMaxGroups ) replaces SPAWN._Limit_( SpawnMaxUnitsAlive, SpawnMaxGroups ): --- --- 2016-08-14: SPAWN.**Init**Array( SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY ) replaces SPAWN._Array_( SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY ). --- --- 2016-08-14: SPAWN.**Init**RandomizeRoute( SpawnStartPoint, SpawnEndPoint, SpawnRadius, SpawnHeight ) replaces SPAWN._RandomizeRoute_( SpawnStartPoint, SpawnEndPoint, SpawnRadius, SpawnHeight ). --- --- 2016-08-14: SPAWN.**Init**RandomizeTemplate( SpawnTemplatePrefixTable ) replaces SPAWN._RandomizeTemplate_( SpawnTemplatePrefixTable ). --- --- 2016-08-14: SPAWN.**Init**UnControlled() replaces SPAWN._UnControlled_(). --- --- === --- --- # **AUTHORS and CONTRIBUTIONS** --- --- ### Contributions: --- --- * **Aaron**: Posed the idea for Group position randomization at SpawnInZone and make the Unit randomization separate from the Group randomization. --- * [**Entropy**](https://forums.eagle.ru/member.php?u=111471), **Afinegan**: Came up with the requirement for AIOnOff(). --- --- ### Authors: --- --- * **FlightControl**: Design & Programming --- --- @module Spawn - - - ---- SPAWN Class --- @type SPAWN --- @extends Core.Base#BASE --- @field ClassName --- @field #string SpawnTemplatePrefix --- @field #string SpawnAliasPrefix --- @field #number AliveUnits --- @field #number MaxAliveUnits --- @field #number SpawnIndex --- @field #number MaxAliveGroups --- @field #SPAWN.SpawnZoneTable SpawnZoneTable -SPAWN = { - ClassName = "SPAWN", - SpawnTemplatePrefix = nil, - SpawnAliasPrefix = nil, -} - - ---- @type SPAWN.SpawnZoneTable --- @list SpawnZone - - ---- 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() ) -- #SPAWN - 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.AIOnOff = true -- The AI is on by default when spawning a group. - self.SpawnUnControlled = false - - 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.AIOnOff = true -- The AI is on by default when spawning a group. - self.SpawnUnControlled = false - - 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 method should be used... --- When a @{#SPAWN.New} is executed and the limit of the amount of units alive is reached, then no new spawn will happen of the group, until some of these units of the spawn object will be destroyed. --- @param #SPAWN self --- @param #number SpawnMaxUnitsAlive The maximum amount of units that can be alive at runtime. --- @param #number SpawnMaxGroups The maximum amount of groups that can be spawned. When the limit is reached, then no more actual spawns will happen of the group. --- This parameter is useful to define a maximum amount of airplanes, ground troops, helicopters, ships etc within a supply area. --- This parameter accepts the value 0, which defines that there are no maximum group limits, but there are limits on the maximum of units that can be alive at the same time. --- @return #SPAWN self --- @usage --- -- NATO helicopters engaging in the battle field. --- -- This helicopter group consists of one Unit. So, this group will SPAWN maximum 2 groups simultaneously within the DCSRTE. --- -- There will be maximum 24 groups spawned during the whole mission lifetime. --- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):InitLimit( 2, 24 ) -function SPAWN:InitLimit( SpawnMaxUnitsAlive, SpawnMaxGroups ) - self:F( { self.SpawnTemplatePrefix, SpawnMaxUnitsAlive, SpawnMaxGroups } ) - - self.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 ... --- @param #number SpawnHeight (optional) Specifies the **additional** height in meters that can be added to the base height specified at each waypoint in the ME. --- @return #SPAWN --- @usage --- -- NATO helicopters engaging in the battle field. --- -- The KA-50 has waypoints Start point ( =0 or SP ), 1, 2, 3, 4, End point (= 5 or DP). --- -- Waypoints 2 and 3 will only be randomized. The others will remain on their original position with each new spawn of the helicopter. --- -- The randomization of waypoint 2 and 3 will take place within a radius of 2000 meters. --- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):InitRandomizeRoute( 2, 2, 2000 ) -function SPAWN:InitRandomizeRoute( SpawnStartPoint, SpawnEndPoint, SpawnRadius, SpawnHeight ) - self:F( { self.SpawnTemplatePrefix, SpawnStartPoint, SpawnEndPoint, SpawnRadius, SpawnHeight } ) - - self.SpawnRandomizeRoute = true - self.SpawnRandomizeRouteStartPoint = SpawnStartPoint - self.SpawnRandomizeRouteEndPoint = SpawnEndPoint - self.SpawnRandomizeRouteRadius = SpawnRadius - self.SpawnRandomizeRouteHeight = SpawnHeight - - for GroupID = 1, self.SpawnMaxGroups do - self:_RandomizeRoute( GroupID ) - end - - return self -end - ---- Randomizes the UNITs that are spawned within a radius band given an Outer and Inner radius. --- @param #SPAWN self --- @param #boolean RandomizeUnits If true, SPAWN will perform the randomization of the @{UNIT}s position within the group between a given outer and inner radius. --- @param Dcs.DCSTypes#Distance OuterRadius (optional) The outer radius in meters where the new group will be spawned. --- @param Dcs.DCSTypes#Distance InnerRadius (optional) The inner radius in meters where the new group will NOT be spawned. --- @return #SPAWN --- @usage --- -- NATO helicopters engaging in the battle field. --- -- The KA-50 has waypoints Start point ( =0 or SP ), 1, 2, 3, 4, End point (= 5 or DP). --- -- Waypoints 2 and 3 will only be randomized. The others will remain on their original position with each new spawn of the helicopter. --- -- The randomization of waypoint 2 and 3 will take place within a radius of 2000 meters. --- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):InitRandomizeRoute( 2, 2, 2000 ) -function SPAWN:InitRandomizeUnits( RandomizeUnits, OuterRadius, InnerRadius ) - self:F( { self.SpawnTemplatePrefix, RandomizeUnits, OuterRadius, InnerRadius } ) - - self.SpawnRandomizeUnits = RandomizeUnits or false - self.SpawnOuterRadius = OuterRadius or 0 - self.SpawnInnerRadius = InnerRadius or 0 - - for GroupID = 1, self.SpawnMaxGroups do - self:_RandomizeRoute( GroupID ) - end - - return self -end - ---- This method is rather complicated to understand. But I'll try to explain. --- This method becomes useful when you need to spawn groups with random templates of groups defined within the mission editor, --- but they will all follow the same Template route and have the same prefix name. --- In other words, this method randomizes between a defined set of groups the template to be used for each new spawn of a group. --- @param #SPAWN self --- @param #string SpawnTemplatePrefixTable A table with the names of the groups defined within the mission editor, from which one will be choosen when a new group will be spawned. --- @return #SPAWN --- @usage --- -- NATO Tank Platoons invading Gori. --- -- Choose between 13 different 'US Tank Platoon' configurations for each new SPAWN the Group to be spawned for the --- -- 'US Tank Platoon Left', 'US Tank Platoon Middle' and 'US Tank Platoon Right' SpawnTemplatePrefixes. --- -- Each new SPAWN will randomize the route, with a defined time interval of 200 seconds with 40% time variation (randomization) and --- -- with a limit set of maximum 12 Units alive simulteneously and 150 Groups to be spawned during the whole mission. --- Spawn_US_Platoon = { 'US Tank Platoon 1', 'US Tank Platoon 2', 'US Tank Platoon 3', 'US Tank Platoon 4', 'US Tank Platoon 5', --- 'US Tank Platoon 6', 'US Tank Platoon 7', 'US Tank Platoon 8', 'US Tank Platoon 9', 'US Tank Platoon 10', --- 'US Tank Platoon 11', 'US Tank Platoon 12', 'US Tank Platoon 13' } --- Spawn_US_Platoon_Left = SPAWN:New( 'US Tank Platoon Left' ):InitLimit( 12, 150 ):Schedule( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 ) --- Spawn_US_Platoon_Middle = SPAWN:New( 'US Tank Platoon Middle' ):InitLimit( 12, 150 ):Schedule( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 ) --- Spawn_US_Platoon_Right = SPAWN:New( 'US Tank Platoon Right' ):InitLimit( 12, 150 ):Schedule( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 ) -function SPAWN:InitRandomizeTemplate( SpawnTemplatePrefixTable ) - self:F( { self.SpawnTemplatePrefix, SpawnTemplatePrefixTable } ) - - self.SpawnTemplatePrefixTable = SpawnTemplatePrefixTable - self.SpawnRandomizeTemplate = true - - for SpawnGroupID = 1, self.SpawnMaxGroups do - self:_RandomizeTemplate( SpawnGroupID ) - end - - return self -end - ---TODO: Add example. ---- This method provides the functionality to randomize the spawning of the Groups at a given list of zones of different types. --- @param #SPAWN self --- @param #table SpawnZoneTable A table with @{Zone} objects. If this table is given, then each spawn will be executed within the given list of @{Zone}s objects. --- @return #SPAWN --- @usage --- -- NATO Tank Platoons invading Gori. --- -- Choose between 3 different zones for each new SPAWN the Group to be executed, regardless of the zone type. -function SPAWN:InitRandomizeZones( SpawnZoneTable ) - self:F( { self.SpawnTemplatePrefix, SpawnZoneTable } ) - - self.SpawnZoneTable = SpawnZoneTable - self.SpawnRandomizeZones = true - - for SpawnGroupID = 1, self.SpawnMaxGroups do - self:_RandomizeZones( SpawnGroupID ) - end - - return self -end - - - - - ---- For planes and helicopters, when these groups go home and land on their home airbases and farps, they normally would taxi to the parking spot, shut-down their engines and wait forever until the Group is removed by the runtime environment. --- This method is used to re-spawn automatically (so no extra call is needed anymore) the same group after it has landed. --- This will enable a spawned group to be re-spawned after it lands, until it is destroyed... --- Note: When the group is respawned, it will re-spawn from the original airbase where it took off. --- So ensure that the routes for groups that respawn, always return to the original airbase, or players may get confused ... --- @param #SPAWN self --- @return #SPAWN self --- @usage --- -- RU Su-34 - AI Ship Attack --- -- Re-SPAWN the Group(s) after each landing and Engine Shut-Down automatically. --- SpawnRU_SU34 = SPAWN:New( 'TF1 RU Su-34 Krymsk@AI - Attack Ships' ):Schedule( 2, 3, 1800, 0.4 ):SpawnUncontrolled():InitRandomizeRoute( 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:InitCleanUp( SpawnCleanUpInterval ) - self:F( { self.SpawnTemplatePrefix, SpawnCleanUpInterval } ) - - self.SpawnCleanUpInterval = SpawnCleanUpInterval - self.SpawnCleanUpTimeStamps = {} - - local SpawnGroup, SpawnCursor = self:GetFirstAliveGroup() - self:T( { "CleanUp Scheduler:", SpawnGroup } ) - - --self.CleanUpFunction = routines.scheduleFunction( self._SpawnCleanUpScheduler, { self }, timer.getTime() + 1, SpawnCleanUpInterval ) - self.CleanUpScheduler = SCHEDULER:New( self, self._SpawnCleanUpScheduler, {}, 1, SpawnCleanUpInterval, 0.2 ) - return self -end - - - ---- Makes the groups visible before start (like a batallion). --- The method will take the position of the group as the first position in the array. --- @param #SPAWN self --- @param #number SpawnAngle The angle in degrees how the groups and each unit of the group will be positioned. --- @param #number SpawnWidth The amount of Groups that will be positioned on the X axis. --- @param #number SpawnDeltaX The space between each Group on the X-axis. --- @param #number SpawnDeltaY The space between each Group on the Y-axis. --- @return #SPAWN self --- @usage --- -- Define an array of Groups. --- Spawn_BE_Ground = SPAWN:New( 'BE Ground' ):InitLimit( 2, 24 ):InitArray( 90, "Diamond", 10, 100, 50 ) -function SPAWN:InitArray( SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY ) - self:F( { self.SpawnTemplatePrefix, SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY } ) - - self.SpawnVisible = true -- When the first Spawn executes, all the Groups need to be made visible before start. - - local SpawnX = 0 - local SpawnY = 0 - local SpawnXIndex = 0 - local SpawnYIndex = 0 - - for SpawnGroupID = 1, self.SpawnMaxGroups do - self:T( { SpawnX, SpawnY, SpawnXIndex, SpawnYIndex } ) - - self.SpawnGroups[SpawnGroupID].Visible = true - self.SpawnGroups[SpawnGroupID].Spawned = false - - SpawnXIndex = SpawnXIndex + 1 - if SpawnWidth and SpawnWidth ~= 0 then - if SpawnXIndex >= SpawnWidth then - SpawnXIndex = 0 - SpawnYIndex = SpawnYIndex + 1 - end - end - - local SpawnRootX = self.SpawnGroups[SpawnGroupID].SpawnTemplate.x - local SpawnRootY = self.SpawnGroups[SpawnGroupID].SpawnTemplate.y - - self:_TranslateRotate( SpawnGroupID, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle ) - - self.SpawnGroups[SpawnGroupID].SpawnTemplate.lateActivation = true - self.SpawnGroups[SpawnGroupID].SpawnTemplate.visible = true - - self.SpawnGroups[SpawnGroupID].Visible = true - - _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 - -do -- AI methods - --- Turns the AI On or Off for the @{Group} when spawning. - -- @param #SPAWN self - -- @param #boolean AIOnOff A value of true sets the AI On, a value of false sets the AI Off. - -- @return #SPAWN The SPAWN object - function SPAWN:InitAIOnOff( AIOnOff ) - - self.AIOnOff = AIOnOff - return self - end - - --- Turns the AI On for the @{Group} when spawning. - -- @param #SPAWN self - -- @return #SPAWN The SPAWN object - function SPAWN:InitAIOn() - - return self:InitAIOnOff( true ) - end - - --- Turns the AI Off for the @{Group} when spawning. - -- @param #SPAWN self - -- @return #SPAWN The SPAWN object - function SPAWN:InitAIOff() - - return self:InitAIOnOff( false ) - end - -end -- AI methods - ---- Will spawn a group based on the internal index. --- Note: Uses @{DATABASE} module defined in MOOSE. --- @param #SPAWN self --- @return Wrapper.Group#GROUP The group that was spawned. You can use this group for further actions. -function SPAWN:Spawn() - self:F( { self.SpawnTemplatePrefix, self.SpawnIndex, self.AliveUnits } ) - - 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 Wrapper.Group#GROUP The group that was spawned. You can use this group for further actions. -function SPAWN:ReSpawn( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex } ) - - if not SpawnIndex then - SpawnIndex = 1 - end - --- TODO: This logic makes DCS crash and i don't know why (yet). - local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) - local WayPoints = SpawnGroup and SpawnGroup.WayPoints or nil - if SpawnGroup then - local SpawnDCSGroup = SpawnGroup:GetDCSObject() - if SpawnDCSGroup then - SpawnGroup:Destroy() - end - end - - local SpawnGroup = self:SpawnWithIndex( SpawnIndex ) - if SpawnGroup and WayPoints then - -- If there were WayPoints set, then Re-Execute those WayPoints! - SpawnGroup:WayPointInitialize( WayPoints ) - SpawnGroup:WayPointExecute( 1, 5 ) - end - - if SpawnGroup.ReSpawnFunction then - SpawnGroup:ReSpawnFunction() - end - - return SpawnGroup -end - ---- Will spawn a group with a specified index number. --- Uses @{DATABASE} global object defined in MOOSE. --- @param #SPAWN self --- @param #string SpawnIndex The index of the group to be spawned. --- @return Wrapper.Group#GROUP The group that was spawned. You can use this group for further actions. -function SPAWN:SpawnWithIndex( SpawnIndex ) - self:F2( { SpawnTemplatePrefix = self.SpawnTemplatePrefix, SpawnIndex = SpawnIndex, AliveUnits = self.AliveUnits, SpawnMaxGroups = self.SpawnMaxGroups } ) - - if self:_GetSpawnIndex( SpawnIndex ) then - - if self.SpawnGroups[self.SpawnIndex].Visible then - self.SpawnGroups[self.SpawnIndex].Group:Activate() - else - - local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate - self:T( SpawnTemplate.name ) - - if SpawnTemplate then - - local PointVec3 = POINT_VEC3:New( SpawnTemplate.route.points[1].x, SpawnTemplate.route.points[1].alt, SpawnTemplate.route.points[1].y ) - self:T( { "Current point of ", self.SpawnTemplatePrefix, PointVec3 } ) - - -- If RandomizeUnits, then Randomize the formation at the start point. - if self.SpawnRandomizeUnits then - for UnitID = 1, #SpawnTemplate.units do - local RandomVec2 = PointVec3:GetRandomVec2InRadius( self.SpawnOuterRadius, self.SpawnInnerRadius ) - SpawnTemplate.units[UnitID].x = RandomVec2.x - SpawnTemplate.units[UnitID].y = RandomVec2.y - self:T( 'SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) - end - end - - if SpawnTemplate.CategoryID == Group.Category.HELICOPTER or SpawnTemplate.CategoryID == Group.Category.AIRPLANE then - if SpawnTemplate.route.points[1].type == "TakeOffParking" then - SpawnTemplate.uncontrolled = self.SpawnUnControlled - end - end - end - - _EVENTDISPATCHER:OnBirthForTemplate( SpawnTemplate, self._OnBirth, self ) - _EVENTDISPATCHER:OnCrashForTemplate( SpawnTemplate, self._OnDeadOrCrash, self ) - _EVENTDISPATCHER:OnDeadForTemplate( SpawnTemplate, self._OnDeadOrCrash, self ) - - if self.Repeat then - _EVENTDISPATCHER:OnTakeOffForTemplate( SpawnTemplate, self._OnTakeOff, self ) - _EVENTDISPATCHER:OnLandForTemplate( SpawnTemplate, self._OnLand, self ) - end - if self.RepeatOnEngineShutDown then - _EVENTDISPATCHER:OnEngineShutDownForTemplate( SpawnTemplate, self._OnEngineShutDown, self ) - end - self:T3( SpawnTemplate.name ) - - self.SpawnGroups[self.SpawnIndex].Group = _DATABASE:Spawn( SpawnTemplate ) - - local SpawnGroup = self.SpawnGroups[self.SpawnIndex].Group -- Wrapper.Group#GROUP - - --TODO: Need to check if this function doesn't need to be scheduled, as the group may not be immediately there! - if SpawnGroup then - - SpawnGroup:SetAIOnOff( self.AIOnOff ) - end - - -- 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 method 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 method will be called when a new group is spawned, including its given parameters. --- The first parameter of the SpawnFunction is the @{Group#GROUP} that was spawned. --- @param #SPAWN self --- @param #function SpawnCallBackFunction The function to be called when a group spawns. --- @param SpawnFunctionArguments A random amount of arguments to be provided to the function when the group spawns. --- @return #SPAWN --- @usage --- -- Declare SpawnObject and call a function when a new Group is spawned. --- local SpawnObject = SPAWN --- :New( "SpawnObject" ) --- :InitLimit( 2, 10 ) --- :OnSpawnGroup( --- function( SpawnGroup ) --- SpawnGroup:E( "I am spawned" ) --- end --- ) --- :SpawnScheduled( 300, 0.3 ) -function SPAWN:OnSpawnGroup( SpawnCallBackFunction, ... ) - self:F( "OnSpawnGroup" ) - - self.SpawnFunctionHook = SpawnCallBackFunction - self.SpawnFunctionArguments = {} - if arg then - self.SpawnFunctionArguments = arg - end - - return self -end - - ---- Will spawn a group from a Vec3 in 3D space. --- This method is mostly advisable to be used if you want to simulate spawning units in the air, like helicopters or airplanes. --- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. --- You can use the returned group to further define the route to be followed. --- @param #SPAWN self --- @param Dcs.DCSTypes#Vec3 Vec3 The Vec3 coordinates where to spawn the group. --- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. --- @return Wrapper.Group#GROUP that was spawned. --- @return #nil Nothing was spawned. -function SPAWN:SpawnFromVec3( Vec3, SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, Vec3, SpawnIndex } ) - - local PointVec3 = POINT_VEC3:NewFromVec3( Vec3 ) - self:T2(PointVec3) - - if SpawnIndex then - else - SpawnIndex = self.SpawnIndex + 1 - end - - if self:_GetSpawnIndex( SpawnIndex ) then - - local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate - - if SpawnTemplate then - - self:T( { "Current point of ", self.SpawnTemplatePrefix, Vec3 } ) - - -- Translate the position of the Group Template to the Vec3. - for UnitID = 1, #SpawnTemplate.units do - self:T( 'Before Translation SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) - local UnitTemplate = SpawnTemplate.units[UnitID] - local SX = UnitTemplate.x - local SY = UnitTemplate.y - local BX = SpawnTemplate.route.points[1].x - local BY = SpawnTemplate.route.points[1].y - local TX = Vec3.x + ( SX - BX ) - local TY = Vec3.z + ( SY - BY ) - SpawnTemplate.units[UnitID].x = TX - SpawnTemplate.units[UnitID].y = TY - SpawnTemplate.units[UnitID].alt = Vec3.y - self:T( 'After Translation SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) - end - - SpawnTemplate.route.points[1].x = Vec3.x - SpawnTemplate.route.points[1].y = Vec3.z - SpawnTemplate.route.points[1].alt = Vec3.y - - SpawnTemplate.x = Vec3.x - SpawnTemplate.y = Vec3.z - - return self:SpawnWithIndex( self.SpawnIndex ) - end - end - - return nil -end - ---- Will spawn a group from a Vec2 in 3D space. --- This method is mostly advisable to be used if you want to simulate spawning groups on the ground from air units, like vehicles. --- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. --- You can use the returned group to further define the route to be followed. --- @param #SPAWN self --- @param Dcs.DCSTypes#Vec2 Vec2 The Vec2 coordinates where to spawn the group. --- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. --- @return Wrapper.Group#GROUP that was spawned. --- @return #nil Nothing was spawned. -function SPAWN:SpawnFromVec2( Vec2, SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, Vec2, SpawnIndex } ) - - local PointVec2 = POINT_VEC2:NewFromVec2( Vec2 ) - return self:SpawnFromVec3( PointVec2:GetVec3(), SpawnIndex ) -end - - ---- Will spawn a group from a hosting unit. This method is mostly advisable to be used if you want to simulate spawning from air units, like helicopters, which are dropping infantry into a defined Landing Zone. --- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. --- You can use the returned group to further define the route to be followed. --- @param #SPAWN self --- @param Wrapper.Unit#UNIT HostUnit The air or ground unit dropping or unloading the group. --- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. --- @return Wrapper.Group#GROUP that was spawned. --- @return #nil Nothing was spawned. -function SPAWN:SpawnFromUnit( HostUnit, SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, HostUnit, SpawnIndex } ) - - if HostUnit and HostUnit:IsAlive() then -- and HostUnit:getUnit(1):inAir() == false then - return self:SpawnFromVec3( HostUnit:GetVec3(), SpawnIndex ) - end - - return nil -end - ---- Will spawn a group from a hosting static. This method is mostly advisable to be used if you want to simulate spawning from buldings and structures (static buildings). --- You can use the returned group to further define the route to be followed. --- @param #SPAWN self --- @param Wrapper.Static#STATIC HostStatic The static dropping or unloading the group. --- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. --- @return Wrapper.Group#GROUP that was spawned. --- @return #nil Nothing was spawned. -function SPAWN:SpawnFromStatic( HostStatic, SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, HostStatic, SpawnIndex } ) - - if HostStatic and HostStatic:IsAlive() then - return self:SpawnFromVec3( HostStatic:GetVec3(), SpawnIndex ) - end - - return nil -end - ---- Will spawn a Group within a given @{Zone}. --- The @{Zone} can be of any type derived from @{Zone#ZONE_BASE}. --- Once the @{Group} is spawned within the zone, the @{Group} will continue on its route. --- The **first waypoint** (where the group is spawned) is replaced with the zone location coordinates. --- @param #SPAWN self --- @param Core.Zone#ZONE Zone The zone where the group is to be spawned. --- @param #boolean RandomizeGroup (optional) Randomization of the @{Group} position in the zone. --- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. --- @return Wrapper.Group#GROUP that was spawned. --- @return #nil when nothing was spawned. -function SPAWN:SpawnInZone( Zone, RandomizeGroup, SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, Zone, RandomizeGroup, SpawnIndex } ) - - if Zone then - if RandomizeGroup then - return self:SpawnFromVec2( Zone:GetRandomVec2(), SpawnIndex ) - else - return self:SpawnFromVec2( Zone:GetVec2(), SpawnIndex ) - end - end - - return nil -end - ---- (**AIR**) Will spawn a plane group in UnControlled or Controlled mode... --- This will be similar to the uncontrolled flag setting in the ME. --- You can use UnControlled mode to simulate planes startup and ready for take-off but aren't moving (yet). --- ReSpawn the plane in Controlled mode, and the plane will move... --- @param #SPAWN self --- @param #boolean UnControlled true if UnControlled, false if Controlled. --- @return #SPAWN self -function SPAWN:InitUnControlled( UnControlled ) - self:F2( { self.SpawnTemplatePrefix, UnControlled } ) - - self.SpawnUnControlled = UnControlled - - for SpawnGroupID = 1, self.SpawnMaxGroups do - self.SpawnGroups[SpawnGroupID].UnControlled = UnControlled - 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 - ---- Will find the first alive @{Group} it has spawned, and return the alive @{Group} object and the first Index where the first alive @{Group} object has been found. --- @param #SPAWN self --- @return Wrapper.Group#GROUP, #number The @{Group} object found, the new Index where the group was found. --- @return #nil, #nil When no group is found, #nil is returned. --- @usage --- -- Find the first alive @{Group} object of the SpawnPlanes SPAWN object @{Group} collection that it has spawned during the mission. --- local GroupPlane, Index = SpawnPlanes:GetFirstAliveGroup() --- while GroupPlane ~= nil do --- -- Do actions with the GroupPlane object. --- GroupPlane, Index = SpawnPlanes:GetNextAliveGroup( Index ) --- end -function SPAWN:GetFirstAliveGroup() - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) - - for SpawnIndex = 1, self.SpawnCount do - local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) - if SpawnGroup and SpawnGroup:IsAlive() then - return SpawnGroup, SpawnIndex - end - end - - return nil, nil -end - - ---- Will find the next alive @{Group} object from a given Index, and return a reference to the alive @{Group} object and the next Index where the alive @{Group} has been found. --- @param #SPAWN self --- @param #number SpawnIndexStart A Index holding the start position to search from. This method can also be used to find the first alive @{Group} object from the given Index. --- @return Wrapper.Group#GROUP, #number The next alive @{Group} object found, the next Index where the next alive @{Group} object was found. --- @return #nil, #nil When no alive @{Group} object is found from the start Index position, #nil is returned. --- @usage --- -- Find the first alive @{Group} object of the SpawnPlanes SPAWN object @{Group} collection that it has spawned during the mission. --- local GroupPlane, Index = SpawnPlanes:GetFirstAliveGroup() --- while GroupPlane ~= nil do --- -- Do actions with the GroupPlane object. --- GroupPlane, Index = SpawnPlanes:GetNextAliveGroup( Index ) --- end -function SPAWN:GetNextAliveGroup( SpawnIndexStart ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnIndexStart } ) - - SpawnIndexStart = SpawnIndexStart + 1 - for SpawnIndex = SpawnIndexStart, self.SpawnCount do - local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) - if SpawnGroup and SpawnGroup:IsAlive() then - return SpawnGroup, SpawnIndex - end - end - - return nil, nil -end - ---- Will find the last alive @{Group} object, and will return a reference to the last live @{Group} object and the last Index where the last alive @{Group} object has been found. --- @param #SPAWN self --- @return Wrapper.Group#GROUP, #number The last alive @{Group} object found, the last Index where the last alive @{Group} object was found. --- @return #nil, #nil When no alive @{Group} object is found, #nil is returned. --- @usage --- -- Find the last alive @{Group} object of the SpawnPlanes SPAWN object @{Group} collection that it has spawned during the mission. --- local GroupPlane, Index = SpawnPlanes:GetLastAliveGroup() --- if GroupPlane then -- GroupPlane can be nil!!! --- -- Do actions with the GroupPlane object. --- end -function SPAWN:GetLastAliveGroup() - self:F( { self.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 Wrapper.Group#GROUP self -function SPAWN:GetGroupFromIndex( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnIndex } ) - - if not SpawnIndex then - SpawnIndex = 1 - end - - if self.SpawnGroups and self.SpawnGroups[SpawnIndex] then - local SpawnGroup = self.SpawnGroups[SpawnIndex].Group - return SpawnGroup - else - return nil - end -end - ---- 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 Dcs.DCSWrapper.Unit#Unit DCSUnit The @{DCSUnit} to be searched. --- @return #string The prefix --- @return #nil Nothing found -function SPAWN:_GetGroupIndexFromDCSUnit( DCSUnit ) - self:F3( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, DCSUnit } ) - - local SpawnUnitName = ( DCSUnit and DCSUnit:getName() ) or nil - if SpawnUnitName then - local IndexString = string.match( SpawnUnitName, "#.*-" ):sub( 2, -2 ) - if IndexString then - local Index = tonumber( IndexString ) - return Index - end - end - - return nil -end - ---- Return the prefix of a SpawnUnit. --- The method will search for a #-mark, and will return the text before the #-mark. --- It will return nil of no prefix was found. --- @param #SPAWN self --- @param Dcs.DCSWrapper.Unit#UNIT DCSUnit The @{DCSUnit} to be searched. --- @return #string The prefix --- @return #nil Nothing found -function SPAWN:_GetPrefixFromDCSUnit( DCSUnit ) - self:F3( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, DCSUnit } ) - - local DCSUnitName = ( DCSUnit and DCSUnit:getName() ) or nil - if DCSUnitName then - local SpawnPrefix = string.match( DCSUnitName, ".*#" ) - if SpawnPrefix then - SpawnPrefix = SpawnPrefix:sub( 1, -2 ) - end - return SpawnPrefix - end - - return nil -end - ---- Return the group within the SpawnGroups collection with input a DCSUnit. --- @param #SPAWN self --- @param Dcs.DCSWrapper.Unit#Unit DCSUnit The @{DCSUnit} to be searched. --- @return Wrapper.Group#GROUP The Group --- @return #nil Nothing found -function SPAWN:_GetGroupFromDCSUnit( DCSUnit ) - self:F3( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, DCSUnit } ) - - 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 - - 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:F3( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnGroup } ) - - local IndexString = string.match( SpawnGroup:GetName(), "#.*$" ):sub( 2 ) - local Index = tonumber( IndexString ) - - self:T3( IndexString, Index ) - return Index - -end - ---- Return the last maximum index that can be used. -function SPAWN:_GetLastIndex() - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) - - return self.SpawnMaxGroups -end - ---- Initalize the SpawnGroups collection. -function SPAWN:_InitializeSpawnGroups( SpawnIndex ) - self:F3( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnIndex } ) - - if not self.SpawnGroups[SpawnIndex] then - self.SpawnGroups[SpawnIndex] = {} - self.SpawnGroups[SpawnIndex].Visible = false - self.SpawnGroups[SpawnIndex].Spawned = false - self.SpawnGroups[SpawnIndex].UnControlled = false - self.SpawnGroups[SpawnIndex].SpawnTime = 0 - - self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix = self.SpawnTemplatePrefix - self.SpawnGroups[SpawnIndex].SpawnTemplate = self:_Prepare( self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix, SpawnIndex ) - end - - self:_RandomizeTemplate( SpawnIndex ) - self:_RandomizeRoute( SpawnIndex ) - --self:_TranslateRotate( SpawnIndex ) - - return self.SpawnGroups[SpawnIndex] -end - - - ---- Gets the CategoryID of the Group with the given SpawnPrefix -function SPAWN:_GetGroupCategoryID( SpawnPrefix ) - local TemplateGroup = Group.getByName( SpawnPrefix ) - - if TemplateGroup then - return TemplateGroup:getCategory() - else - return nil - end -end - ---- Gets the CoalitionID of the Group with the given SpawnPrefix -function SPAWN:_GetGroupCoalitionID( SpawnPrefix ) - local TemplateGroup = Group.getByName( SpawnPrefix ) - - if TemplateGroup then - return TemplateGroup:getCoalition() - else - return nil - end -end - ---- Gets the CountryID of the Group with the given SpawnPrefix -function SPAWN:_GetGroupCountryID( SpawnPrefix ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnPrefix } ) - - local TemplateGroup = Group.getByName( SpawnPrefix ) - - if TemplateGroup then - local TemplateUnits = TemplateGroup:getUnits() - return TemplateUnits[1]:getCountry() - else - return nil - end -end - ---- Gets the Group Template from the ME environment definition. --- This method used the @{DATABASE} object, which contains ALL initial and new spawned object in MOOSE. --- @param #SPAWN self --- @param #string SpawnTemplatePrefix --- @return @SPAWN self -function SPAWN:_GetTemplate( SpawnTemplatePrefix ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnTemplatePrefix } ) - - local SpawnTemplate = nil - - 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:T3( { SpawnTemplate } ) - return SpawnTemplate -end - ---- Prepares the new Group Template. --- @param #SPAWN self --- @param #string SpawnTemplatePrefix --- @param #number SpawnIndex --- @return #SPAWN self -function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) - 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 - - if SpawnTemplate.CategoryID == Group.Category.GROUND then - self:T3( "For ground units, visible needs to be false..." ) - SpawnTemplate.visible = false - end - - - for UnitID = 1, #SpawnTemplate.units do - SpawnTemplate.units[UnitID].name = string.format( SpawnTemplate.name .. '-%02d', UnitID ) - SpawnTemplate.units[UnitID].unitId = nil - end - - self:T3( { "Template:", SpawnTemplate } ) - return SpawnTemplate - -end - ---- Private method randomizing the routes. --- @param #SPAWN self --- @param #number SpawnIndex The index of the group to be spawned. --- @return #SPAWN -function SPAWN:_RandomizeRoute( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeRoute, self.SpawnRandomizeRouteStartPoint, self.SpawnRandomizeRouteEndPoint, self.SpawnRandomizeRouteRadius } ) - - if self.SpawnRandomizeRoute then - local SpawnTemplate = self.SpawnGroups[SpawnIndex].SpawnTemplate - local RouteCount = #SpawnTemplate.route.points - - for t = self.SpawnRandomizeRouteStartPoint + 1, ( RouteCount - self.SpawnRandomizeRouteEndPoint ) do - - SpawnTemplate.route.points[t].x = SpawnTemplate.route.points[t].x + math.random( self.SpawnRandomizeRouteRadius * -1, self.SpawnRandomizeRouteRadius ) - SpawnTemplate.route.points[t].y = SpawnTemplate.route.points[t].y + math.random( self.SpawnRandomizeRouteRadius * -1, self.SpawnRandomizeRouteRadius ) - - -- Manage randomization of altitude for airborne units ... - if SpawnTemplate.CategoryID == Group.Category.AIRPLANE or SpawnTemplate.CategoryID == Group.Category.HELICOPTER then - if SpawnTemplate.route.points[t].alt and self.SpawnRandomizeRouteHeight then - SpawnTemplate.route.points[t].alt = SpawnTemplate.route.points[t].alt + math.random( 1, self.SpawnRandomizeRouteHeight ) - end - else - SpawnTemplate.route.points[t].alt = nil - end - - self:T( 'SpawnTemplate.route.points[' .. t .. '].x = ' .. SpawnTemplate.route.points[t].x .. ', SpawnTemplate.route.points[' .. t .. '].y = ' .. SpawnTemplate.route.points[t].y ) - end - end - - self:_RandomizeZones( SpawnIndex ) - - return self -end - ---- Private method that randomizes the template of the group. --- @param #SPAWN self --- @param #number SpawnIndex --- @return #SPAWN self -function SPAWN:_RandomizeTemplate( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeTemplate } ) - - if self.SpawnRandomizeTemplate then - self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix = self.SpawnTemplatePrefixTable[ math.random( 1, #self.SpawnTemplatePrefixTable ) ] - self.SpawnGroups[SpawnIndex].SpawnTemplate = self:_Prepare( self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix, SpawnIndex ) - self.SpawnGroups[SpawnIndex].SpawnTemplate.route = 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 - local OldX = self.SpawnGroups[SpawnIndex].SpawnTemplate.units[1].x - local OldY = self.SpawnGroups[SpawnIndex].SpawnTemplate.units[1].y - for UnitID = 1, #self.SpawnGroups[SpawnIndex].SpawnTemplate.units do - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].heading = self.SpawnTemplate.units[1].heading - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].x = self.SpawnTemplate.units[1].x + ( self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].x - OldX ) - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].y = self.SpawnTemplate.units[1].y + ( self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].y - OldY ) - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].alt = self.SpawnTemplate.units[1].alt - end - end - - self:_RandomizeRoute( SpawnIndex ) - - return self -end - ---- Private method that randomizes the @{Zone}s where the Group will be spawned. --- @param #SPAWN self --- @param #number SpawnIndex --- @return #SPAWN self -function SPAWN:_RandomizeZones( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeZones } ) - - if self.SpawnRandomizeZones then - local SpawnZone = nil -- Core.Zone#ZONE_BASE - while not SpawnZone do - self:T( { SpawnZoneTableCount = #self.SpawnZoneTable, self.SpawnZoneTable } ) - local ZoneID = math.random( #self.SpawnZoneTable ) - self:T( ZoneID ) - SpawnZone = self.SpawnZoneTable[ ZoneID ]:GetZoneMaybe() - end - - self:T( "Preparing Spawn in Zone", SpawnZone:GetName() ) - - local SpawnVec2 = SpawnZone:GetRandomVec2() - - self:T( { SpawnVec2 = SpawnVec2 } ) - - local SpawnTemplate = self.SpawnGroups[SpawnIndex].SpawnTemplate - - self:T( { Route = SpawnTemplate.route } ) - - for UnitID = 1, #SpawnTemplate.units do - local UnitTemplate = SpawnTemplate.units[UnitID] - self:T( 'Before Translation SpawnTemplate.units['..UnitID..'].x = ' .. UnitTemplate.x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. UnitTemplate.y ) - local SX = UnitTemplate.x - local SY = UnitTemplate.y - local BX = SpawnTemplate.route.points[1].x - local BY = SpawnTemplate.route.points[1].y - local TX = SpawnVec2.x + ( SX - BX ) - local TY = SpawnVec2.y + ( SY - BY ) - UnitTemplate.x = TX - UnitTemplate.y = TY - -- TODO: Manage altitude based on landheight... - --SpawnTemplate.units[UnitID].alt = SpawnVec2: - self:T( 'After Translation SpawnTemplate.units['..UnitID..'].x = ' .. UnitTemplate.x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. UnitTemplate.y ) - end - SpawnTemplate.x = SpawnVec2.x - SpawnTemplate.y = SpawnVec2.y - SpawnTemplate.route.points[1].x = SpawnVec2.x - SpawnTemplate.route.points[1].y = SpawnVec2.y - end - - return self - -end - -function SPAWN:_TranslateRotate( SpawnIndex, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle } ) - - -- Translate - local TranslatedX = SpawnX - local TranslatedY = SpawnY - - -- Rotate - -- From Wikipedia: https://en.wikipedia.org/wiki/Rotation_matrix#Common_rotations - -- x' = x \cos \theta - y \sin \theta\ - -- y' = x \sin \theta + y \cos \theta\ - local RotatedX = - TranslatedX * math.cos( math.rad( SpawnAngle ) ) - + TranslatedY * math.sin( math.rad( SpawnAngle ) ) - local RotatedY = TranslatedX * math.sin( math.rad( SpawnAngle ) ) - + TranslatedY * math.cos( math.rad( SpawnAngle ) ) - - -- Assign - self.SpawnGroups[SpawnIndex].SpawnTemplate.x = SpawnRootX - RotatedX - self.SpawnGroups[SpawnIndex].SpawnTemplate.y = SpawnRootY + RotatedY - - - local SpawnUnitCount = table.getn( self.SpawnGroups[SpawnIndex].SpawnTemplate.units ) - for u = 1, SpawnUnitCount do - - -- Translate - local TranslatedX = SpawnX - local TranslatedY = SpawnY - 10 * ( u - 1 ) - - -- Rotate - local RotatedX = - TranslatedX * math.cos( math.rad( SpawnAngle ) ) - + TranslatedY * math.sin( math.rad( SpawnAngle ) ) - local RotatedY = TranslatedX * math.sin( math.rad( SpawnAngle ) ) - + TranslatedY * math.cos( math.rad( SpawnAngle ) ) - - -- Assign - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].x = SpawnRootX - RotatedX - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].y = SpawnRootY + RotatedY - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].heading = self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].heading + math.rad( SpawnAngle ) - end - - return self -end - ---- Get the next index of the groups to be spawned. This method is complicated, as it is used at several spaces. -function SPAWN:_GetSpawnIndex( SpawnIndex ) - self:F2( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnMaxGroups, self.SpawnMaxUnitsAlive, self.AliveUnits, #self.SpawnTemplate.units } ) - - if ( self.SpawnMaxGroups == 0 ) or ( SpawnIndex <= self.SpawnMaxGroups ) then - if ( self.SpawnMaxUnitsAlive == 0 ) or ( self.AliveUnits + #self.SpawnTemplate.units <= self.SpawnMaxUnitsAlive ) or self.UnControlled == true then - if SpawnIndex and SpawnIndex >= self.SpawnCount + 1 then - self.SpawnCount = self.SpawnCount + 1 - SpawnIndex = self.SpawnCount - end - self.SpawnIndex = SpawnIndex - if not self.SpawnGroups[self.SpawnIndex] then - self:_InitializeSpawnGroups( self.SpawnIndex ) - end - else - return nil - end - else - return nil - end - - return self.SpawnIndex -end - - --- TODO Need to delete this... _DATABASE does this now ... - ---- @param #SPAWN self --- @param Core.Event#EVENTDATA Event -function SPAWN:_OnBirth( Event ) - - if timer.getTime0() < timer.getAbsTime() then - if Event.IniDCSUnit then - local EventPrefix = self:_GetPrefixFromDCSUnit( Event.IniDCSUnit ) - self:T( { "Birth Event:", EventPrefix, self.SpawnTemplatePrefix } ) - if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then - self.AliveUnits = self.AliveUnits + 1 - self:T( "Alive Units: " .. self.AliveUnits ) - end - end - end - -end - ---- Obscolete --- @todo Need to delete this... _DATABASE does this now ... - ---- @param #SPAWN self --- @param Core.Event#EVENTDATA Event -function SPAWN:_OnDeadOrCrash( Event ) - self:F( self.SpawnTemplatePrefix, Event ) - - if Event.IniDCSUnit then - local EventPrefix = self:_GetPrefixFromDCSUnit( Event.IniDCSUnit ) - self:T( { "Dead event: " .. EventPrefix, self.SpawnTemplatePrefix } ) - if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then - self.AliveUnits = self.AliveUnits - 1 - self:T( "Alive Units: " .. self.AliveUnits ) - end - end -end - ---- 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:F2( { "_Scheduler", self.SpawnTemplatePrefix, self.SpawnAliasPrefix, self.SpawnIndex, self.SpawnMaxGroups, self.SpawnMaxUnitsAlive } ) - - -- Validate if there are still groups left in the batch... - self:Spawn() - - return true -end - ---- Schedules the CleanUp of Groups --- @param #SPAWN self --- @return #boolean True = Continue Scheduler -function SPAWN:_SpawnCleanUpScheduler() - self:F( { "CleanUp Scheduler:", self.SpawnTemplatePrefix } ) - - local SpawnGroup, SpawnCursor = self:GetFirstAliveGroup() - self:T( { "CleanUp Scheduler:", SpawnGroup, SpawnCursor } ) - - while SpawnGroup do - - local SpawnUnits = SpawnGroup:GetUnits() - - for UnitID, UnitData in pairs( SpawnUnits ) do - - local SpawnUnit = UnitData -- Wrapper.Unit#UNIT - local SpawnUnitName = SpawnUnit:GetName() - - - self.SpawnCleanUpTimeStamps[SpawnUnitName] = self.SpawnCleanUpTimeStamps[SpawnUnitName] or {} - local Stamp = self.SpawnCleanUpTimeStamps[SpawnUnitName] - self:T( { SpawnUnitName, Stamp } ) - - if Stamp.Vec2 then - if SpawnUnit:InAir() == false and SpawnUnit:GetVelocityKMH() < 1 then - local NewVec2 = SpawnUnit:GetVec2() - if Stamp.Vec2.x == NewVec2.x and Stamp.Vec2.y == NewVec2.y then - -- If the plane is not moving, and is on the ground, assign it with a timestamp... - if Stamp.Time + self.SpawnCleanUpInterval < timer.getTime() then - self:T( { "CleanUp Scheduler:", "ReSpawning:", SpawnGroup:GetName() } ) - self:ReSpawn( SpawnCursor ) - Stamp.Vec2 = nil - Stamp.Time = nil - end - else - Stamp.Time = timer.getTime() - Stamp.Vec2 = SpawnUnit:GetVec2() - end - else - Stamp.Vec2 = nil - Stamp.Time = nil - end - else - if SpawnUnit:InAir() == false then - Stamp.Vec2 = SpawnUnit:GetVec2() - if SpawnUnit:GetVelocityKMH() < 1 then - Stamp.Time = timer.getTime() - end - else - Stamp.Time = nil - Stamp.Vec2 = nil - end - end - end - - SpawnGroup, SpawnCursor = self:GetNextAliveGroup( SpawnCursor ) - - self:T( { "CleanUp Scheduler:", SpawnGroup, SpawnCursor } ) - - end - - return true -- Repeat - -end ---- 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 Core.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 - -- 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 ) - if self.TargetSkill[_targetskill] then - if (_evade > self.TargetSkill[_targetskill].Evade) then - self:T( string.format("Evading, target skill " ..string.format(_targetskill)) ) - local _targetMim = Weapon.getTarget(SEADWeapon) - local _targetMimname = Unit.getName(_targetMim) - local _targetMimgroup = Unit.getGroup(Weapon.getTarget(SEADWeapon)) - local _targetMimcont= _targetMimgroup:getController() - routines.groupRandomDistSelf(_targetMimgroup,300,'Diamond',250,20) -- move randomly - local SuppressedGroups1 = {} -- unit suppressed radar off for a random time - local function SuppressionEnd1(id) - id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN) - SuppressedGroups1[id.groupName] = nil - end - local id = { - groupName = _targetMimgroup, - ctrl = _targetMimcont - } - local delay1 = math.random(self.TargetSkill[_targetskill].DelayOff[1], self.TargetSkill[_targetskill].DelayOff[2]) - if SuppressedGroups1[id.groupName] == nil then - SuppressedGroups1[id.groupName] = { - SuppressionEndTime1 = timer.getTime() + delay1, - SuppressionEndN1 = SuppressionEndCounter1 --Store instance of SuppressionEnd() scheduled function - } - Controller.setOption(_targetMimcont, AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN) - timer.scheduleFunction(SuppressionEnd1, id, SuppressedGroups1[id.groupName].SuppressionEndTime1) --Schedule the SuppressionEnd() function - --trigger.action.outText( string.format("Radar Off " ..string.format(delay1)), 20) - end - - local SuppressedGroups = {} - local function SuppressionEnd(id) - id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.RED) - SuppressedGroups[id.groupName] = nil - end - local id = { - groupName = _targetMimgroup, - ctrl = _targetMimcont - } - local delay = math.random(self.TargetSkill[_targetskill].DelayOn[1], self.TargetSkill[_targetskill].DelayOn[2]) - if SuppressedGroups[id.groupName] == nil then - SuppressedGroups[id.groupName] = { - SuppressionEndTime = timer.getTime() + delay, - SuppressionEndN = SuppressionEndCounter --Store instance of SuppressionEnd() scheduled function - } - timer.scheduleFunction(SuppressionEnd, id, SuppressedGroups[id.groupName].SuppressionEndTime) --Schedule the SuppressionEnd() function - --trigger.action.outText( string.format("Radar On " ..string.format(delay)), 20) - end - 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 Core.Base#BASE --- @field Wrapper.Client#CLIENT EscortClient --- @field Wrapper.Group#GROUP EscortGroup --- @field #string EscortName --- @field #ESCORT.MODE EscortMode The mode the escort is in. --- @field Core.Scheduler#SCHEDULER FollowScheduler The instance of the SCHEDULER class. --- @field #number FollowDistance The current follow distance. --- @field #boolean ReportTargets If true, nearby targets are reported. --- @Field Dcs.DCSTypes#AI.Option.Air.val.ROE OptionROE Which ROE is set to the EscortGroup. --- @field Dcs.DCSTypes#AI.Option.Air.val.REACTION_ON_THREAT OptionReactionOnThreat Which REACTION_ON_THREAT is set to the EscortGroup. --- @field Core.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 Wrapper.Client#CLIENT EscortClient The client escorted by the EscortGroup. --- @param Wrapper.Group#GROUP EscortGroup The group AI escorting the EscortClient. --- @param #string EscortName Name of the escort. --- @param #string EscortBriefing A text showing the ESCORT briefing to the player. Note that if no EscortBriefing is provided, the default briefing will be shown. --- @return #ESCORT self --- @usage --- -- Declare a new EscortPlanes object as follows: --- --- -- First find the GROUP object and the CLIENT object. --- local EscortClient = CLIENT:FindByName( "Unit Name" ) -- The Unit Name is the name of the unit flagged with the skill Client in the mission editor. --- local EscortGroup = GROUP:FindByName( "Group Name" ) -- The Group Name is the name of the group that will escort the Escort Client. --- --- -- Now use these 2 objects to construct the new EscortPlanes object. --- EscortPlanes = ESCORT:New( EscortClient, EscortGroup, "Desert", "Welcome to the mission. You are escorted by a plane with code name 'Desert', which can be instructed through the F10 radio menu." ) -function ESCORT:New( EscortClient, EscortGroup, EscortName, EscortBriefing ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( { EscortClient, EscortGroup, EscortName } ) - - self.EscortClient = EscortClient -- Wrapper.Client#CLIENT - self.EscortGroup = EscortGroup -- Wrapper.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() - - if not EscortBriefing then - EscortGroup:MessageToClient( EscortGroup:GetCategoryName() .. " '" .. EscortName .. "' (" .. EscortGroup:GetCallsign() .. ") reporting! " .. - "We're escorting your flight. " .. - "Use the Radio Menu and F10 and use the options under + " .. EscortName .. "\n", - 60, EscortClient - ) - else - EscortGroup:MessageToClient( EscortGroup:GetCategoryName() .. " '" .. EscortName .. "' (" .. EscortGroup:GetCallsign() .. ") " .. EscortBriefing, - 60, EscortClient - ) - end - - self.FollowDistance = 100 - self.CT1 = 0 - self.GT1 = 0 - self.FollowScheduler = 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 Dcs.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 Dcs.DCSTypes#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. --- @param Dcs.DCSTypes#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. --- @param #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 Dcs.DCSTypes#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. --- @param Dcs.DCSTypes#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. --- @param #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 Dcs.DCSTypes#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. --- @param Dcs.DCSTypes#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. --- @param #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 = 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 = 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 = 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 = 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 Dcs.DCSTypes#Time Seconds Optional parameter that lets the escort report their current detected targets after specified time interval in seconds. The default time is 30 seconds. --- @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 = 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 -- Wrapper.Group#GROUP - local OrbitUnit = OrbitGroup:GetUnit(1) -- Wrapper.Unit#UNIT - local OrbitHeight = MenuParam.ParamHeight - local OrbitSeconds = MenuParam.ParamSeconds -- Not implemented yet - - self.FollowScheduler:Stop() - - local PointFrom = {} - local GroupVec3 = EscortGroup:GetUnit(1):GetVec3() - PointFrom = {} - PointFrom.x = GroupVec3.x - PointFrom.y = GroupVec3.z - PointFrom.speed = 250 - PointFrom.type = AI.Task.WaypointType.TURNING_POINT - PointFrom.alt = GroupVec3.y - PointFrom.alt_type = AI.Task.AltitudeType.BARO - - local OrbitPoint = OrbitUnit:GetVec2() - local PointTo = {} - PointTo.x = OrbitPoint.x - PointTo.y = OrbitPoint.y - PointTo.speed = 250 - PointTo.type = AI.Task.WaypointType.TURNING_POINT - PointTo.alt = OrbitHeight - PointTo.alt_type = AI.Task.AltitudeType.BARO - PointTo.task = EscortGroup:TaskOrbitCircleAtVec2( OrbitPoint, OrbitHeight, 0 ) - - local Points = { PointFrom, PointTo } - - EscortGroup:OptionROEHoldFire() - EscortGroup:OptionROTPassiveDefense() - - EscortGroup:SetTask( EscortGroup:TaskRoute( Points ) ) - EscortGroup:MessageToClient( "Orbiting at location.", 10, EscortClient ) - -end - ---- @param #MENUPARAM MenuParam -function ESCORT._JoinUpAndFollow( 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 Functional.Escort#ESCORT self --- @param Wrapper.Group#GROUP EscortGroup --- @param Wrapper.Client#CLIENT EscortClient --- @param Dcs.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 = 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 Wrapper.Group#GROUP EscortGroup -function _Resume( EscortGroup ) - env.info( '_Resume' ) - - local Escort = EscortGroup:GetState( EscortGroup, "Escort" ) - env.info( "EscortMode = " .. Escort.EscortMode ) - if Escort.EscortMode == ESCORT.MODE.FOLLOW then - Escort:JoinUpAndFollow( EscortGroup, Escort.EscortClient, Escort.Distance ) - end - -end - ---- @param #MENUPARAM MenuParam -function ESCORT._AttackTarget( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - - local EscortClient = self.EscortClient - local AttackUnit = MenuParam.ParamUnit -- Wrapper.Unit#UNIT - - self.FollowScheduler:Stop() - - self:T( AttackUnit ) - - if EscortGroup:IsAir() then - EscortGroup:OptionROEOpenFire() - EscortGroup:OptionROTPassiveDefense() - EscortGroup:SetState( EscortGroup, "Escort", self ) - SCHEDULER:New( EscortGroup, - EscortGroup.PushTask, - { EscortGroup:TaskCombo( - { EscortGroup:TaskAttackUnit( AttackUnit ), - EscortGroup:TaskFunction( 1, 2, "_Resume", { "''" } ) - } - ) - }, 10 - ) - else - SCHEDULER:New( EscortGroup, - EscortGroup.PushTask, - { EscortGroup:TaskCombo( - { EscortGroup:TaskFireAtPoint( AttackUnit:GetVec2(), 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 -- Wrapper.Unit#UNIT - - self.FollowScheduler:Stop() - - self:T( AttackUnit ) - - if EscortGroupAttack:IsAir() then - EscortGroupAttack:OptionROEOpenFire() - EscortGroupAttack:OptionROTVertical() - SCHDULER:New( EscortGroupAttack, - EscortGroupAttack.PushTask, - { EscortGroupAttack:TaskCombo( - { EscortGroupAttack:TaskAttackUnit( AttackUnit ), - EscortGroupAttack:TaskOrbitCircle( 500, 350 ) - } - ) - }, 10 - ) - else - SCHEDULER:New( EscortGroupAttack, - EscortGroupAttack.PushTask, - { EscortGroupAttack:TaskCombo( - { EscortGroupAttack:TaskFireAtPoint( AttackUnit:GetVec2(), 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 - - SCHEDULER:New( EscortGroup, EscortGroup.SetTask, { EscortGroup:TaskRoute( WayPoints ) }, 1 ) - - EscortGroup:MessageToClient( "Resuming mission from waypoint " .. WayPoint .. ".", 10, EscortClient ) -end - ---- Registers the waypoints --- @param #ESCORT self --- @return #table -function ESCORT:RegisterRoute() - self:F() - - local EscortGroup = self.EscortGroup -- Wrapper.Group#GROUP - - local TaskPoints = EscortGroup:GetTaskRoute() - - self:T( TaskPoints ) - - return TaskPoints -end - ---- @param Functional.Escort#ESCORT self -function ESCORT:_FollowScheduler() - self:F( { self.FollowDistance } ) - - self:T( {self.EscortClient.UnitName, self.EscortGroup.GroupName } ) - if self.EscortGroup:IsAlive() and self.EscortClient:IsAlive() then - - local ClientUnit = self.EscortClient:GetClientGroupUnit() - local GroupUnit = self.EscortGroup:GetUnit( 1 ) - local FollowDistance = self.FollowDistance - - self:T( {ClientUnit.UnitName, GroupUnit.UnitName } ) - - if self.CT1 == 0 and self.GT1 == 0 then - self.CV1 = ClientUnit:GetVec3() - self:T( { "self.CV1", self.CV1 } ) - self.CT1 = timer.getTime() - self.GV1 = GroupUnit:GetVec3() - self.GT1 = timer.getTime() - else - local CT1 = self.CT1 - local CT2 = timer.getTime() - local CV1 = self.CV1 - local CV2 = ClientUnit:GetVec3() - self.CT1 = CT2 - self.CV1 = CV2 - - local CD = ( ( CV2.x - CV1.x )^2 + ( CV2.y - CV1.y )^2 + ( CV2.z - CV1.z )^2 ) ^ 0.5 - local CT = CT2 - CT1 - - local CS = ( 3600 / CT ) * ( CD / 1000 ) - - self:T2( { "Client:", CS, CD, CT, CV2, CV1, CT2, CT1 } ) - - local GT1 = self.GT1 - local GT2 = timer.getTime() - local GV1 = self.GV1 - local GV2 = GroupUnit:GetVec3() - self.GT1 = GT2 - self.GV1 = GV2 - - local GD = ( ( GV2.x - GV1.x )^2 + ( GV2.y - GV1.y )^2 + ( GV2.z - GV1.z )^2 ) ^ 0.5 - local GT = GT2 - GT1 - - local GS = ( 3600 / GT ) * ( GD / 1000 ) - - self:T2( { "Group:", GS, GD, GT, GV2, GV1, GT2, GT1 } ) - - -- Calculate the group direction vector - local GV = { x = GV2.x - CV2.x, y = GV2.y - CV2.y, z = GV2.z - CV2.z } - - -- Calculate GH2, GH2 with the same height as CV2. - local GH2 = { x = GV2.x, y = CV2.y, z = GV2.z } - - -- Calculate the angle of GV to the orthonormal plane - local alpha = math.atan2( GV.z, GV.x ) - - -- Now we calculate the intersecting vector between the circle around CV2 with radius FollowDistance and GH2. - -- From the GeoGebra model: CVI = (x(CV2) + FollowDistance cos(alpha), y(GH2) + FollowDistance sin(alpha), z(CV2)) - local CVI = { x = CV2.x + FollowDistance * math.cos(alpha), - y = GH2.y, - z = CV2.z + FollowDistance * math.sin(alpha), - } - - -- Calculate the direction vector DV of the escort group. We use CVI as the base and CV2 as the direction. - local DV = { x = CV2.x - CVI.x, y = CV2.y - CVI.y, z = CV2.z - CVI.z } - - -- We now calculate the unary direction vector DVu, so that we can multiply DVu with the speed, which is expressed in meters / s. - -- We need to calculate this vector to predict the point the escort group needs to fly to according its speed. - -- The distance of the destination point should be far enough not to have the aircraft starting to swipe left to right... - local DVu = { x = DV.x / FollowDistance, y = DV.y / FollowDistance, z = DV.z / FollowDistance } - - -- Now we can calculate the group destination vector GDV. - local GDV = { x = DVu.x * CS * 8 + CVI.x, y = CVI.y, z = DVu.z * CS * 8 + CVI.z } - - if self.SmokeDirectionVector == true then - trigger.action.smoke( GDV, trigger.smokeColor.Red ) - end - - self:T2( { "CV2:", CV2 } ) - self:T2( { "CVI:", CVI } ) - self:T2( { "GDV:", GDV } ) - - -- Measure distance between client and group - local CatchUpDistance = ( ( GDV.x - GV2.x )^2 + ( GDV.y - GV2.y )^2 + ( GDV.z - GV2.z )^2 ) ^ 0.5 - - -- The calculation of the Speed would simulate that the group would take 30 seconds to overcome - -- the requested Distance). - local Time = 10 - local CatchUpSpeed = ( CatchUpDistance - ( CS * 8.4 ) ) / Time - - local Speed = CS + CatchUpSpeed - if Speed < 0 then - Speed = 0 - end - - self:T( { "Client Speed, Escort Speed, Speed, FollowDistance, Time:", CS, GS, Speed, FollowDistance, Time } ) - - -- Now route the escort to the desired point with the desired speed. - self.EscortGroup:RouteToVec3( GDV, Speed / 3.6 ) -- DCS models speed in Mps (Miles per second) - end - - return true - end - - return false -end - - ---- Report Targets Scheduler. --- @param #ESCORT self -function ESCORT:_ReportTargetsScheduler() - self:F( self.EscortGroup:GetName() ) - - if self.EscortGroup:IsAlive() and self.EscortClient:IsAlive() then - local EscortGroupName = self.EscortGroup:GetName() - local EscortTargets = self.EscortGroup:GetDetectedTargets() - - local ClientEscortTargets = self.EscortClient._EscortGroups[EscortGroupName].Targets - - local EscortTargetMessages = "" - for EscortTargetID, EscortTarget in pairs( EscortTargets ) do - local EscortObject = EscortTarget.object - self:T( EscortObject ) - if EscortObject and EscortObject:isExist() and EscortObject.id_ < 50000000 then - - local EscortTargetUnit = UNIT:Find( EscortObject ) - local EscortTargetUnitName = EscortTargetUnit:GetName() - - - - -- local EscortTargetIsDetected, - -- EscortTargetIsVisible, - -- EscortTargetLastTime, - -- EscortTargetKnowType, - -- EscortTargetKnowDistance, - -- EscortTargetLastPos, - -- EscortTargetLastVelocity - -- = self.EscortGroup:IsTargetDetected( EscortObject ) - -- - -- self:T( { EscortTargetIsDetected, - -- EscortTargetIsVisible, - -- EscortTargetLastTime, - -- EscortTargetKnowType, - -- EscortTargetKnowDistance, - -- EscortTargetLastPos, - -- EscortTargetLastVelocity } ) - - - local EscortTargetUnitVec3 = EscortTargetUnit:GetVec3() - local EscortVec3 = self.EscortGroup:GetVec3() - local Distance = ( ( EscortTargetUnitVec3.x - EscortVec3.x )^2 + - ( EscortTargetUnitVec3.y - EscortVec3.y )^2 + - ( EscortTargetUnitVec3.z - EscortVec3.z )^2 - ) ^ 0.5 / 1000 - - self:T( { self.EscortGroup:GetName(), EscortTargetUnit:GetName(), Distance, EscortTarget } ) - - if Distance <= 15 then - - if not ClientEscortTargets[EscortTargetUnitName] then - ClientEscortTargets[EscortTargetUnitName] = {} - end - ClientEscortTargets[EscortTargetUnitName].AttackUnit = EscortTargetUnit - ClientEscortTargets[EscortTargetUnitName].visible = EscortTarget.visible - ClientEscortTargets[EscortTargetUnitName].type = EscortTarget.type - ClientEscortTargets[EscortTargetUnitName].distance = EscortTarget.distance - else - if ClientEscortTargets[EscortTargetUnitName] then - ClientEscortTargets[EscortTargetUnitName] = nil - end - end - end - end - - self:T( { "Sorting Targets Table:", ClientEscortTargets } ) - table.sort( ClientEscortTargets, function( a, b ) return a.Distance < b.Distance end ) - self:T( { "Sorted Targets Table:", ClientEscortTargets } ) - - -- Remove the sub menus of the Attack menu of the Escort for the EscortGroup. - self.EscortMenuAttackNearbyTargets:RemoveSubMenus() - - if self.EscortMenuTargetAssistance then - self.EscortMenuTargetAssistance:RemoveSubMenus() - end - - --for MenuIndex = 1, #self.EscortMenuAttackTargets do - -- self:T( { "Remove Menu:", self.EscortMenuAttackTargets[MenuIndex] } ) - -- self.EscortMenuAttackTargets[MenuIndex] = self.EscortMenuAttackTargets[MenuIndex]:Remove() - --end - - - if ClientEscortTargets then - for ClientEscortTargetUnitName, ClientEscortTargetData in pairs( ClientEscortTargets ) do - - for ClientEscortGroupName, EscortGroupData in pairs( self.EscortClient._EscortGroups ) do - - if ClientEscortTargetData and ClientEscortTargetData.AttackUnit:IsAlive() then - - local EscortTargetMessage = "" - local EscortTargetCategoryName = ClientEscortTargetData.AttackUnit:GetCategoryName() - local EscortTargetCategoryType = ClientEscortTargetData.AttackUnit:GetTypeName() - if ClientEscortTargetData.type then - EscortTargetMessage = EscortTargetMessage .. EscortTargetCategoryName .. " (" .. EscortTargetCategoryType .. ") at " - else - EscortTargetMessage = EscortTargetMessage .. "Unknown target at " - end - - local EscortTargetUnitVec3 = ClientEscortTargetData.AttackUnit:GetVec3() - local EscortVec3 = self.EscortGroup:GetVec3() - local Distance = ( ( EscortTargetUnitVec3.x - EscortVec3.x )^2 + - ( EscortTargetUnitVec3.y - EscortVec3.y )^2 + - ( EscortTargetUnitVec3.z - EscortVec3.z )^2 - ) ^ 0.5 / 1000 - - self:T( { self.EscortGroup:GetName(), ClientEscortTargetData.AttackUnit:GetName(), Distance, ClientEscortTargetData.AttackUnit } ) - if ClientEscortTargetData.visible == false then - EscortTargetMessage = EscortTargetMessage .. string.format( "%.2f", Distance ) .. " estimated km" - else - EscortTargetMessage = EscortTargetMessage .. string.format( "%.2f", Distance ) .. " km" - end - - if ClientEscortTargetData.visible then - EscortTargetMessage = EscortTargetMessage .. ", visual" - end - - if ClientEscortGroupName == EscortGroupName then - - MENU_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 EscortVec3 = self.EscortGroup:GetVec3() - local Distance = ( ( WayPoint.x - EscortVec3.x )^2 + - ( WayPoint.y - EscortVec3.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 Core.Set#SET_CLIENT DBClients --- @extends Core.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 Wrapper.Client#CLIENT Client --- function( Client ) --- --- ... actions ... --- --- end --- ) - - self.MessagesOnOff = true - - self.TrackingToAll = false - self.TrackingOnOff = true - self.TrackingFrequency = 3 - - self.AlertsToAll = true - self.AlertsHitsOnOff = true - self.AlertsLaunchesOnOff = true - - self.DetailsRangeOnOff = true - self.DetailsBearingOnOff = true - - self.MenusOnOff = true - - self.TrackingMissiles = {} - - self.TrackingScheduler = SCHEDULER:New( self, self._TrackMissiles, {}, 0.5, 0.05, 0 ) - - return self -end - --- Initialization methods. - - - ---- Sets by default the display of any message to be ON or OFF. --- @param #MISSILETRAINER self --- @param #boolean MessagesOnOff true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitMessagesOnOff( MessagesOnOff ) - self:F( MessagesOnOff ) - - self.MessagesOnOff = MessagesOnOff - if self.MessagesOnOff == true then - MESSAGE:New( "Messages ON", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Messages OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Sets by default the missile tracking report for all players or only for those missiles targetted to you. --- @param #MISSILETRAINER self --- @param #boolean TrackingToAll true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitTrackingToAll( TrackingToAll ) - self:F( TrackingToAll ) - - self.TrackingToAll = TrackingToAll - if self.TrackingToAll == true then - MESSAGE:New( "Missile tracking to all players ON", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Missile tracking to all players OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Sets by default the display of missile tracking report to be ON or OFF. --- @param #MISSILETRAINER self --- @param #boolean TrackingOnOff true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitTrackingOnOff( TrackingOnOff ) - self:F( TrackingOnOff ) - - self.TrackingOnOff = TrackingOnOff - if self.TrackingOnOff == true then - MESSAGE:New( "Missile tracking ON", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Missile tracking OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Increases, decreases the missile tracking message display frequency with the provided time interval in seconds. --- The default frequency is a 3 second interval, so the Tracking Frequency parameter specifies the increase or decrease from the default 3 seconds or the last frequency update. --- @param #MISSILETRAINER self --- @param #number TrackingFrequency Provide a negative or positive value in seconds to incraese or decrease the display frequency. --- @return #MISSILETRAINER self -function MISSILETRAINER:InitTrackingFrequency( TrackingFrequency ) - self:F( TrackingFrequency ) - - self.TrackingFrequency = self.TrackingFrequency + TrackingFrequency - if self.TrackingFrequency < 0.5 then - self.TrackingFrequency = 0.5 - end - if self.TrackingFrequency then - MESSAGE:New( "Missile tracking frequency is " .. self.TrackingFrequency .. " seconds.", 15, "Menu" ):ToAll() - end - - return self -end - ---- Sets by default the display of alerts to be shown to all players or only to you. --- @param #MISSILETRAINER self --- @param #boolean AlertsToAll true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitAlertsToAll( AlertsToAll ) - self:F( AlertsToAll ) - - self.AlertsToAll = AlertsToAll - if self.AlertsToAll == true then - MESSAGE:New( "Alerts to all players ON", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Alerts to all players OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Sets by default the display of hit alerts ON or OFF. --- @param #MISSILETRAINER self --- @param #boolean AlertsHitsOnOff true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitAlertsHitsOnOff( AlertsHitsOnOff ) - self:F( AlertsHitsOnOff ) - - self.AlertsHitsOnOff = AlertsHitsOnOff - if self.AlertsHitsOnOff == true then - MESSAGE:New( "Alerts Hits ON", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Alerts Hits OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Sets by default the display of launch alerts ON or OFF. --- @param #MISSILETRAINER self --- @param #boolean AlertsLaunchesOnOff true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitAlertsLaunchesOnOff( AlertsLaunchesOnOff ) - self:F( AlertsLaunchesOnOff ) - - self.AlertsLaunchesOnOff = AlertsLaunchesOnOff - if self.AlertsLaunchesOnOff == true then - MESSAGE:New( "Alerts Launches ON", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Alerts Launches OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Sets by default the display of range information of missiles ON of OFF. --- @param #MISSILETRAINER self --- @param #boolean DetailsRangeOnOff true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitRangeOnOff( DetailsRangeOnOff ) - self:F( DetailsRangeOnOff ) - - self.DetailsRangeOnOff = DetailsRangeOnOff - if self.DetailsRangeOnOff == true then - MESSAGE:New( "Range display ON", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Range display OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Sets by default the display of bearing information of missiles ON of OFF. --- @param #MISSILETRAINER self --- @param #boolean DetailsBearingOnOff true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitBearingOnOff( DetailsBearingOnOff ) - self:F( DetailsBearingOnOff ) - - self.DetailsBearingOnOff = DetailsBearingOnOff - if self.DetailsBearingOnOff == true then - MESSAGE:New( "Bearing display OFF", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Bearing display OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Enables / Disables the menus. --- @param #MISSILETRAINER self --- @param #boolean MenusOnOff true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitMenusOnOff( MenusOnOff ) - self:F( MenusOnOff ) - - self.MenusOnOff = MenusOnOff - if self.MenusOnOff == true then - MESSAGE:New( "Menus are ENABLED (only when a player rejoins a slot)", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Menus are DISABLED", 15, "Menu" ):ToAll() - end - - return self -end - - --- Menu functions - -function MISSILETRAINER._MenuMessages( MenuParameters ) - - local self = MenuParameters.MenuSelf - - if MenuParameters.MessagesOnOff ~= nil then - self:InitMessagesOnOff( MenuParameters.MessagesOnOff ) - end - - if MenuParameters.TrackingToAll ~= nil then - self:InitTrackingToAll( MenuParameters.TrackingToAll ) - end - - if MenuParameters.TrackingOnOff ~= nil then - self:InitTrackingOnOff( MenuParameters.TrackingOnOff ) - end - - if MenuParameters.TrackingFrequency ~= nil then - self:InitTrackingFrequency( MenuParameters.TrackingFrequency ) - end - - if MenuParameters.AlertsToAll ~= nil then - self:InitAlertsToAll( MenuParameters.AlertsToAll ) - end - - if MenuParameters.AlertsHitsOnOff ~= nil then - self:InitAlertsHitsOnOff( MenuParameters.AlertsHitsOnOff ) - end - - if MenuParameters.AlertsLaunchesOnOff ~= nil then - self:InitAlertsLaunchesOnOff( MenuParameters.AlertsLaunchesOnOff ) - end - - if MenuParameters.DetailsRangeOnOff ~= nil then - self:InitRangeOnOff( MenuParameters.DetailsRangeOnOff ) - end - - if MenuParameters.DetailsBearingOnOff ~= nil then - self:InitBearingOnOff( MenuParameters.DetailsBearingOnOff ) - end - - if MenuParameters.Distance ~= nil then - self.Distance = MenuParameters.Distance - MESSAGE:New( "Hit detection distance set to " .. self.Distance .. " meters", 15, "Menu" ):ToAll() - end - -end - ---- Detects if an SA site was shot with an anti radiation missile. In this case, take evasive actions based on the skill level set within the ME. --- @param #MISSILETRAINER self --- @param Core.Event#EVENTDATA 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 - if TrainerTargetDCSUnit then - local TrainerTargetDCSUnitName = Unit.getName( TrainerTargetDCSUnit ) - local TrainerTargetSkill = _DATABASE.Templates.Units[TrainerTargetDCSUnitName].Template.skill - - self:T(TrainerTargetDCSUnitName ) - - local Client = self.DBClients:FindClient( TrainerTargetDCSUnitName ) - if Client then - - local TrainerSourceUnit = UNIT:Find( TrainerSourceDCSUnit ) - local TrainerTargetUnit = UNIT:Find( TrainerTargetDCSUnit ) - - if self.MessagesOnOff == true and self.AlertsLaunchesOnOff == true then - - local Message = MESSAGE:New( - string.format( "%s launched a %s", - TrainerSourceUnit:GetTypeName(), - TrainerWeaponName - ) .. self:_AddRange( Client, TrainerWeapon ) .. self:_AddBearing( Client, TrainerWeapon ), 5, "Launch Alert" ) - - if self.AlertsToAll then - Message:ToAll() - else - Message:ToClient( Client ) - end - end - - local ClientID = Client:GetID() - self:T( ClientID ) - local MissileData = {} - MissileData.TrainerSourceUnit = TrainerSourceUnit - MissileData.TrainerWeapon = TrainerWeapon - MissileData.TrainerTargetUnit = TrainerTargetUnit - MissileData.TrainerWeaponTypeName = TrainerWeapon:getTypeName() - MissileData.TrainerWeaponLaunched = true - table.insert( self.TrackingMissiles[ClientID].MissileData, MissileData ) - --self:T( self.TrackingMissiles ) - end - else - -- TODO: some weapons don't know the target unit... Need to develop a workaround for this. - if ( TrainerWeapon:getTypeName() == "9M311" ) then - SCHEDULER:New( TrainerWeapon, TrainerWeapon.destroy, {}, 1 ) - else - end - end -end - -function MISSILETRAINER:_AddRange( Client, TrainerWeapon ) - - local RangeText = "" - - if self.DetailsRangeOnOff then - - local PositionMissile = TrainerWeapon:getPoint() - local TargetVec3 = Client:GetVec3() - - local Range = ( ( PositionMissile.x - TargetVec3.x )^2 + - ( PositionMissile.y - TargetVec3.y )^2 + - ( PositionMissile.z - TargetVec3.z )^2 - ) ^ 0.5 / 1000 - - RangeText = string.format( ", at %4.2fkm", Range ) - end - - return RangeText -end - -function MISSILETRAINER:_AddBearing( Client, TrainerWeapon ) - - local BearingText = "" - - if self.DetailsBearingOnOff then - - local PositionMissile = TrainerWeapon:getPoint() - local TargetVec3 = Client:GetVec3() - - self:T2( { TargetVec3, PositionMissile }) - - local DirectionVector = { x = PositionMissile.x - TargetVec3.x, y = PositionMissile.y - TargetVec3.y, z = PositionMissile.z - TargetVec3.z } - local DirectionRadians = math.atan2( DirectionVector.z, DirectionVector.x ) - --DirectionRadians = DirectionRadians + routines.getNorthCorrection( PositionTarget ) - if DirectionRadians < 0 then - DirectionRadians = DirectionRadians + 2 * math.pi - end - local DirectionDegrees = DirectionRadians * 180 / math.pi - - BearingText = string.format( ", %d degrees", DirectionDegrees ) - end - - return BearingText -end - - -function MISSILETRAINER:_TrackMissiles() - self:F2() - - - local ShowMessages = false - if self.MessagesOnOff and self.MessageLastTime + self.TrackingFrequency <= timer.getTime() then - self.MessageLastTime = timer.getTime() - ShowMessages = true - end - - -- ALERTS PART - - -- Loop for all Player Clients to check the alerts and deletion of missiles. - for ClientDataID, ClientData in pairs( self.TrackingMissiles ) do - - local Client = ClientData.Client - 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 TargetVec3 = Client:GetVec3() - - local Distance = ( ( PositionMissile.x - TargetVec3.x )^2 + - ( PositionMissile.y - TargetVec3.y )^2 + - ( PositionMissile.z - TargetVec3.z )^2 - ) ^ 0.5 / 1000 - - if Distance <= self.Distance then - -- Hit alert - TrainerWeapon:destroy() - if self.MessagesOnOff == true and self.AlertsHitsOnOff == true then - - self:T( "killed" ) - - local Message = MESSAGE:New( - string.format( "%s launched by %s killed %s", - TrainerWeapon:getTypeName(), - TrainerSourceUnit:GetTypeName(), - TrainerTargetUnit:GetPlayerName() - ), 15, "Hit Alert" ) - - if self.AlertsToAll == true then - Message:ToAll() - else - Message:ToClient( Client ) - end - - MissileData = nil - table.remove( ClientData.MissileData, MissileDataID ) - self:T(ClientData.MissileData) - end - end - else - if not ( TrainerWeapon and TrainerWeapon:isExist() ) then - if self.MessagesOnOff == true and self.AlertsLaunchesOnOff == true then - -- Weapon does not exist anymore. Delete from Table - local Message = MESSAGE:New( - string.format( "%s launched by %s self destructed!", - TrainerWeaponTypeName, - TrainerSourceUnit:GetTypeName() - ), 5, "Tracking" ) - - if self.AlertsToAll == true then - Message:ToAll() - else - Message:ToClient( Client ) - end - end - MissileData = nil - table.remove( ClientData.MissileData, MissileDataID ) - self:T( ClientData.MissileData ) - end - end - end - 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 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 --- --- 3) @{AirbasePolice#AIRBASEPOLICE_NEVADA} class, extends @{AirbasePolice#AIRBASEPOLICE_BASE} --- ============================================================================================= --- All the airbases on the NEVADA 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: --- * Nellis --- * McCarran --- * Creech --- * Groom Lake --- --- ### Contributions: Dutch Baron - Concept & Testing --- ### Author: FlightControl - Framework Design & Programming --- --- @module AirbasePolice - - - - - ---- @type AIRBASEPOLICE_BASE --- @field Core.Set#SET_CLIENT SetClient --- @extends Core.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(SMOKECOLOR.White):Flush() - for PointsRunwayID, PointsRunway in pairs( Airbase.PointsRunways ) do - Airbase.ZoneRunways[PointsRunwayID] = ZONE_POLYGON_BASE:New( "Runway " .. PointsRunwayID, PointsRunway ):SmokeZone(SMOKECOLOR.Red):Flush() - end - end - --- -- Template --- local TemplateBoundary = GROUP:FindByName( "Template Boundary" ) --- self.Airbases.Template.ZoneBoundary = ZONE_POLYGON:New( "Template Boundary", TemplateBoundary ):SmokeZone(SMOKECOLOR.White):Flush() --- --- local TemplateRunway1 = GROUP:FindByName( "Template Runway 1" ) --- self.Airbases.Template.ZoneRunways[1] = ZONE_POLYGON:New( "Template Runway 1", TemplateRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - - self.SetClient:ForEachClient( - --- @param Wrapper.Client#CLIENT Client - function( Client ) - Client:SetState( self, "Speeding", false ) - Client:SetState( self, "Warnings", 0) - Client:SetState( self, "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 Wrapper.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 - - -- TODO: GetVelocityKMH function usage - local VelocityVec3 = Client:GetVelocity() - local Velocity = ( VelocityVec3.x ^ 2 + VelocityVec3.y ^ 2 + VelocityVec3.z ^ 2 ) ^ 0.5 -- in meters / sec - local Velocity = Velocity * 3.6 -- now it is in km/h. - -- MESSAGE:New( "Velocity = " .. Velocity, 1 ):ToAll() - 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 <= 3 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 .. " / 3" ) - 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:Destroy() - trigger.action.setUserFlag( "AIRCRAFT_"..Client:GetID(), 100) - Client:SetState( self, "Speeding", false ) - Client:SetState( self, "Warnings", 0 ) - end - - else - Client:Message( "You are speeding on the taxiway, slow down now! 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 Core.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] = { - [1]={["y"]=242140.57142858,["x"]=-6478.8571428583,}, - [2]={["y"]=242188.57142858,["x"]=-6522.0000000011,}, - [3]={["y"]=244124.2857143,["x"]=-4344.0000000011,}, - [4]={["y"]=244068.2857143,["x"]=-4296.5714285726,}, - [5]={["y"]=242140.57142858,["x"]=-6480.0000000011,} - }, - }, - 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,}, - }, - [2] = { - [1]={["y"]=895605.71428572,["x"]=-314724.57142857,}, - [2]={["y"]=897639.71428572,["x"]=-316148,}, - [3]={["y"]=897683.42857143,["x"]=-316087.14285714,}, - [4]={["y"]=895650,["x"]=-314660,}, - [5]={["y"]=895606,["x"]=-314724.85714286,} - }, - }, - 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(SMOKECOLOR.White):Flush() - -- - -- local AnapaVityazevoRunway1 = GROUP:FindByName( "AnapaVityazevo Runway 1" ) - -- self.Airbases.AnapaVityazevo.ZoneRunways[1] = ZONE_POLYGON:New( "AnapaVityazevo Runway 1", AnapaVityazevoRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Batumi - -- local BatumiBoundary = GROUP:FindByName( "Batumi Boundary" ) - -- self.Airbases.Batumi.ZoneBoundary = ZONE_POLYGON:New( "Batumi Boundary", BatumiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local BatumiRunway1 = GROUP:FindByName( "Batumi Runway 1" ) - -- self.Airbases.Batumi.ZoneRunways[1] = ZONE_POLYGON:New( "Batumi Runway 1", BatumiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Beslan - -- local BeslanBoundary = GROUP:FindByName( "Beslan Boundary" ) - -- self.Airbases.Beslan.ZoneBoundary = ZONE_POLYGON:New( "Beslan Boundary", BeslanBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local BeslanRunway1 = GROUP:FindByName( "Beslan Runway 1" ) - -- self.Airbases.Beslan.ZoneRunways[1] = ZONE_POLYGON:New( "Beslan Runway 1", BeslanRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Gelendzhik - -- local GelendzhikBoundary = GROUP:FindByName( "Gelendzhik Boundary" ) - -- self.Airbases.Gelendzhik.ZoneBoundary = ZONE_POLYGON:New( "Gelendzhik Boundary", GelendzhikBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local GelendzhikRunway1 = GROUP:FindByName( "Gelendzhik Runway 1" ) - -- self.Airbases.Gelendzhik.ZoneRunways[1] = ZONE_POLYGON:New( "Gelendzhik Runway 1", GelendzhikRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Gudauta - -- local GudautaBoundary = GROUP:FindByName( "Gudauta Boundary" ) - -- self.Airbases.Gudauta.ZoneBoundary = ZONE_POLYGON:New( "Gudauta Boundary", GudautaBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local GudautaRunway1 = GROUP:FindByName( "Gudauta Runway 1" ) - -- self.Airbases.Gudauta.ZoneRunways[1] = ZONE_POLYGON:New( "Gudauta Runway 1", GudautaRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Kobuleti - -- local KobuletiBoundary = GROUP:FindByName( "Kobuleti Boundary" ) - -- self.Airbases.Kobuleti.ZoneBoundary = ZONE_POLYGON:New( "Kobuleti Boundary", KobuletiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local KobuletiRunway1 = GROUP:FindByName( "Kobuleti Runway 1" ) - -- self.Airbases.Kobuleti.ZoneRunways[1] = ZONE_POLYGON:New( "Kobuleti Runway 1", KobuletiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- KrasnodarCenter - -- local KrasnodarCenterBoundary = GROUP:FindByName( "KrasnodarCenter Boundary" ) - -- self.Airbases.KrasnodarCenter.ZoneBoundary = ZONE_POLYGON:New( "KrasnodarCenter Boundary", KrasnodarCenterBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local KrasnodarCenterRunway1 = GROUP:FindByName( "KrasnodarCenter Runway 1" ) - -- self.Airbases.KrasnodarCenter.ZoneRunways[1] = ZONE_POLYGON:New( "KrasnodarCenter Runway 1", KrasnodarCenterRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- KrasnodarPashkovsky - -- local KrasnodarPashkovskyBoundary = GROUP:FindByName( "KrasnodarPashkovsky Boundary" ) - -- self.Airbases.KrasnodarPashkovsky.ZoneBoundary = ZONE_POLYGON:New( "KrasnodarPashkovsky Boundary", KrasnodarPashkovskyBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local KrasnodarPashkovskyRunway1 = GROUP:FindByName( "KrasnodarPashkovsky Runway 1" ) - -- self.Airbases.KrasnodarPashkovsky.ZoneRunways[1] = ZONE_POLYGON:New( "KrasnodarPashkovsky Runway 1", KrasnodarPashkovskyRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- local KrasnodarPashkovskyRunway2 = GROUP:FindByName( "KrasnodarPashkovsky Runway 2" ) - -- self.Airbases.KrasnodarPashkovsky.ZoneRunways[2] = ZONE_POLYGON:New( "KrasnodarPashkovsky Runway 2", KrasnodarPashkovskyRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Krymsk - -- local KrymskBoundary = GROUP:FindByName( "Krymsk Boundary" ) - -- self.Airbases.Krymsk.ZoneBoundary = ZONE_POLYGON:New( "Krymsk Boundary", KrymskBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local KrymskRunway1 = GROUP:FindByName( "Krymsk Runway 1" ) - -- self.Airbases.Krymsk.ZoneRunways[1] = ZONE_POLYGON:New( "Krymsk Runway 1", KrymskRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Kutaisi - -- local KutaisiBoundary = GROUP:FindByName( "Kutaisi Boundary" ) - -- self.Airbases.Kutaisi.ZoneBoundary = ZONE_POLYGON:New( "Kutaisi Boundary", KutaisiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local KutaisiRunway1 = GROUP:FindByName( "Kutaisi Runway 1" ) - -- self.Airbases.Kutaisi.ZoneRunways[1] = ZONE_POLYGON:New( "Kutaisi Runway 1", KutaisiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- MaykopKhanskaya - -- local MaykopKhanskayaBoundary = GROUP:FindByName( "MaykopKhanskaya Boundary" ) - -- self.Airbases.MaykopKhanskaya.ZoneBoundary = ZONE_POLYGON:New( "MaykopKhanskaya Boundary", MaykopKhanskayaBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local MaykopKhanskayaRunway1 = GROUP:FindByName( "MaykopKhanskaya Runway 1" ) - -- self.Airbases.MaykopKhanskaya.ZoneRunways[1] = ZONE_POLYGON:New( "MaykopKhanskaya Runway 1", MaykopKhanskayaRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- MineralnyeVody - -- local MineralnyeVodyBoundary = GROUP:FindByName( "MineralnyeVody Boundary" ) - -- self.Airbases.MineralnyeVody.ZoneBoundary = ZONE_POLYGON:New( "MineralnyeVody Boundary", MineralnyeVodyBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local MineralnyeVodyRunway1 = GROUP:FindByName( "MineralnyeVody Runway 1" ) - -- self.Airbases.MineralnyeVody.ZoneRunways[1] = ZONE_POLYGON:New( "MineralnyeVody Runway 1", MineralnyeVodyRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Mozdok - -- local MozdokBoundary = GROUP:FindByName( "Mozdok Boundary" ) - -- self.Airbases.Mozdok.ZoneBoundary = ZONE_POLYGON:New( "Mozdok Boundary", MozdokBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local MozdokRunway1 = GROUP:FindByName( "Mozdok Runway 1" ) - -- self.Airbases.Mozdok.ZoneRunways[1] = ZONE_POLYGON:New( "Mozdok Runway 1", MozdokRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Nalchik - -- local NalchikBoundary = GROUP:FindByName( "Nalchik Boundary" ) - -- self.Airbases.Nalchik.ZoneBoundary = ZONE_POLYGON:New( "Nalchik Boundary", NalchikBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local NalchikRunway1 = GROUP:FindByName( "Nalchik Runway 1" ) - -- self.Airbases.Nalchik.ZoneRunways[1] = ZONE_POLYGON:New( "Nalchik Runway 1", NalchikRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Novorossiysk - -- local NovorossiyskBoundary = GROUP:FindByName( "Novorossiysk Boundary" ) - -- self.Airbases.Novorossiysk.ZoneBoundary = ZONE_POLYGON:New( "Novorossiysk Boundary", NovorossiyskBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local NovorossiyskRunway1 = GROUP:FindByName( "Novorossiysk Runway 1" ) - -- self.Airbases.Novorossiysk.ZoneRunways[1] = ZONE_POLYGON:New( "Novorossiysk Runway 1", NovorossiyskRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- SenakiKolkhi - -- local SenakiKolkhiBoundary = GROUP:FindByName( "SenakiKolkhi Boundary" ) - -- self.Airbases.SenakiKolkhi.ZoneBoundary = ZONE_POLYGON:New( "SenakiKolkhi Boundary", SenakiKolkhiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local SenakiKolkhiRunway1 = GROUP:FindByName( "SenakiKolkhi Runway 1" ) - -- self.Airbases.SenakiKolkhi.ZoneRunways[1] = ZONE_POLYGON:New( "SenakiKolkhi Runway 1", SenakiKolkhiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- SochiAdler - -- local SochiAdlerBoundary = GROUP:FindByName( "SochiAdler Boundary" ) - -- self.Airbases.SochiAdler.ZoneBoundary = ZONE_POLYGON:New( "SochiAdler Boundary", SochiAdlerBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local SochiAdlerRunway1 = GROUP:FindByName( "SochiAdler Runway 1" ) - -- self.Airbases.SochiAdler.ZoneRunways[1] = ZONE_POLYGON:New( "SochiAdler Runway 1", SochiAdlerRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- local SochiAdlerRunway2 = GROUP:FindByName( "SochiAdler Runway 2" ) - -- self.Airbases.SochiAdler.ZoneRunways[2] = ZONE_POLYGON:New( "SochiAdler Runway 2", SochiAdlerRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Soganlug - -- local SoganlugBoundary = GROUP:FindByName( "Soganlug Boundary" ) - -- self.Airbases.Soganlug.ZoneBoundary = ZONE_POLYGON:New( "Soganlug Boundary", SoganlugBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local SoganlugRunway1 = GROUP:FindByName( "Soganlug Runway 1" ) - -- self.Airbases.Soganlug.ZoneRunways[1] = ZONE_POLYGON:New( "Soganlug Runway 1", SoganlugRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- SukhumiBabushara - -- local SukhumiBabusharaBoundary = GROUP:FindByName( "SukhumiBabushara Boundary" ) - -- self.Airbases.SukhumiBabushara.ZoneBoundary = ZONE_POLYGON:New( "SukhumiBabushara Boundary", SukhumiBabusharaBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local SukhumiBabusharaRunway1 = GROUP:FindByName( "SukhumiBabushara Runway 1" ) - -- self.Airbases.SukhumiBabushara.ZoneRunways[1] = ZONE_POLYGON:New( "SukhumiBabushara Runway 1", SukhumiBabusharaRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- TbilisiLochini - -- local TbilisiLochiniBoundary = GROUP:FindByName( "TbilisiLochini Boundary" ) - -- self.Airbases.TbilisiLochini.ZoneBoundary = ZONE_POLYGON:New( "TbilisiLochini Boundary", TbilisiLochiniBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local TbilisiLochiniRunway1 = GROUP:FindByName( "TbilisiLochini Runway 1" ) - -- self.Airbases.TbilisiLochini.ZoneRunways[1] = ZONE_POLYGON:New( "TbilisiLochini Runway 1", TbilisiLochiniRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- local TbilisiLochiniRunway2 = GROUP:FindByName( "TbilisiLochini Runway 2" ) - -- self.Airbases.TbilisiLochini.ZoneRunways[2] = ZONE_POLYGON:New( "TbilisiLochini Runway 2", TbilisiLochiniRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Vaziani - -- local VazianiBoundary = GROUP:FindByName( "Vaziani Boundary" ) - -- self.Airbases.Vaziani.ZoneBoundary = ZONE_POLYGON:New( "Vaziani Boundary", VazianiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local VazianiRunway1 = GROUP:FindByName( "Vaziani Runway 1" ) - -- self.Airbases.Vaziani.ZoneRunways[1] = ZONE_POLYGON:New( "Vaziani Runway 1", VazianiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - - - -- Template - -- local TemplateBoundary = GROUP:FindByName( "Template Boundary" ) - -- self.Airbases.Template.ZoneBoundary = ZONE_POLYGON:New( "Template Boundary", TemplateBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local TemplateRunway1 = GROUP:FindByName( "Template Runway 1" ) - -- self.Airbases.Template.ZoneRunways[1] = ZONE_POLYGON:New( "Template Runway 1", TemplateRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - - return self - -end - - - - ---- @type AIRBASEPOLICE_NEVADA --- @extends Functional.AirbasePolice#AIRBASEPOLICE_BASE -AIRBASEPOLICE_NEVADA = { - ClassName = "AIRBASEPOLICE_NEVADA", - Airbases = { - Nellis = { - PointsBoundary = { - [1]={["y"]=-17814.714285714,["x"]=-399823.14285714,}, - [2]={["y"]=-16875.857142857,["x"]=-398763.14285714,}, - [3]={["y"]=-16251.571428571,["x"]=-398988.85714286,}, - [4]={["y"]=-16163,["x"]=-398693.14285714,}, - [5]={["y"]=-16328.714285714,["x"]=-398034.57142857,}, - [6]={["y"]=-15943,["x"]=-397571.71428571,}, - [7]={["y"]=-15711.571428571,["x"]=-397551.71428571,}, - [8]={["y"]=-15748.714285714,["x"]=-396806,}, - [9]={["y"]=-16288.714285714,["x"]=-396517.42857143,}, - [10]={["y"]=-16751.571428571,["x"]=-396308.85714286,}, - [11]={["y"]=-17263,["x"]=-396234.57142857,}, - [12]={["y"]=-17577.285714286,["x"]=-396640.28571429,}, - [13]={["y"]=-17614.428571429,["x"]=-397400.28571429,}, - [14]={["y"]=-19405.857142857,["x"]=-399428.85714286,}, - [15]={["y"]=-19234.428571429,["x"]=-399683.14285714,}, - [16]={["y"]=-18708.714285714,["x"]=-399408.85714286,}, - [17]={["y"]=-18397.285714286,["x"]=-399657.42857143,}, - [18]={["y"]=-17814.428571429,["x"]=-399823.42857143,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=-18687,["x"]=-399380.28571429,}, - [2]={["y"]=-18620.714285714,["x"]=-399436.85714286,}, - [3]={["y"]=-16217.857142857,["x"]=-396596.85714286,}, - [4]={["y"]=-16300.142857143,["x"]=-396530,}, - [5]={["y"]=-18687,["x"]=-399380.85714286,}, - }, - [2] = { - [1]={["y"]=-18451.571428572,["x"]=-399580.57142857,}, - [2]={["y"]=-18392.142857143,["x"]=-399628.57142857,}, - [3]={["y"]=-16011,["x"]=-396806.85714286,}, - [4]={["y"]=-16074.714285714,["x"]=-396751.71428572,}, - [5]={["y"]=-18451.571428572,["x"]=-399580.85714285,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - McCarran = { - PointsBoundary = { - [1]={["y"]=-29455.285714286,["x"]=-416277.42857142,}, - [2]={["y"]=-28860.142857143,["x"]=-416492,}, - [3]={["y"]=-25044.428571429,["x"]=-416344.85714285,}, - [4]={["y"]=-24580.142857143,["x"]=-415959.14285714,}, - [5]={["y"]=-25073,["x"]=-415630.57142857,}, - [6]={["y"]=-25087.285714286,["x"]=-415130.57142857,}, - [7]={["y"]=-25830.142857143,["x"]=-414866.28571428,}, - [8]={["y"]=-26658.714285715,["x"]=-414880.57142857,}, - [9]={["y"]=-26973,["x"]=-415273.42857142,}, - [10]={["y"]=-27380.142857143,["x"]=-415187.71428571,}, - [11]={["y"]=-27715.857142857,["x"]=-414144.85714285,}, - [12]={["y"]=-27551.571428572,["x"]=-413473.42857142,}, - [13]={["y"]=-28630.142857143,["x"]=-413201.99999999,}, - [14]={["y"]=-29494.428571429,["x"]=-415437.71428571,}, - [15]={["y"]=-29455.571428572,["x"]=-416277.71428571,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=-29408.428571429,["x"]=-416016.28571428,}, - [2]={["y"]=-29408.142857144,["x"]=-416105.42857142,}, - [3]={["y"]=-24680.714285715,["x"]=-416003.14285713,}, - [4]={["y"]=-24681.857142858,["x"]=-415926.57142856,}, - [5]={["y"]=-29408.42857143,["x"]=-416016.57142856,}, - }, - [2] = { - [1]={["y"]=-28575.571428572,["x"]=-416303.14285713,}, - [2]={["y"]=-28575.571428572,["x"]=-416382.57142856,}, - [3]={["y"]=-25111.000000001,["x"]=-416309.7142857,}, - [4]={["y"]=-25111.000000001,["x"]=-416249.14285713,}, - [5]={["y"]=-28575.571428572,["x"]=-416303.7142857,}, - }, - [3] = { - [1]={["y"]=-29331.000000001,["x"]=-416275.42857141,}, - [2]={["y"]=-29259.000000001,["x"]=-416306.85714284,}, - [3]={["y"]=-28005.571428572,["x"]=-413449.7142857,}, - [4]={["y"]=-28068.714285715,["x"]=-413422.85714284,}, - [5]={["y"]=-29331.000000001,["x"]=-416275.7142857,}, - }, - [4] = { - [1]={["y"]=-29073.285714286,["x"]=-416386.57142856,}, - [2]={["y"]=-28997.285714286,["x"]=-416417.42857141,}, - [3]={["y"]=-27697.571428572,["x"]=-413464.57142856,}, - [4]={["y"]=-27767.857142858,["x"]=-413434.28571427,}, - [5]={["y"]=-29073.000000001,["x"]=-416386.85714284,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Creech = { - PointsBoundary = { - [1]={["y"]=-74522.714285715,["x"]=-360887.99999998,}, - [2]={["y"]=-74197,["x"]=-360556.57142855,}, - [3]={["y"]=-74402.714285715,["x"]=-359639.42857141,}, - [4]={["y"]=-74637,["x"]=-359279.42857141,}, - [5]={["y"]=-75759.857142857,["x"]=-359005.14285712,}, - [6]={["y"]=-75834.142857143,["x"]=-359045.14285712,}, - [7]={["y"]=-75902.714285714,["x"]=-359782.28571427,}, - [8]={["y"]=-76099.857142857,["x"]=-360399.42857141,}, - [9]={["y"]=-77314.142857143,["x"]=-360219.42857141,}, - [10]={["y"]=-77728.428571429,["x"]=-360445.14285713,}, - [11]={["y"]=-77585.571428571,["x"]=-360585.14285713,}, - [12]={["y"]=-76471.285714286,["x"]=-360819.42857141,}, - [13]={["y"]=-76325.571428571,["x"]=-360942.28571427,}, - [14]={["y"]=-74671.857142857,["x"]=-360927.7142857,}, - [15]={["y"]=-74522.714285714,["x"]=-360888.85714284,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=-74237.571428571,["x"]=-360591.7142857,}, - [2]={["y"]=-74234.428571429,["x"]=-360493.71428571,}, - [3]={["y"]=-77605.285714286,["x"]=-360399.14285713,}, - [4]={["y"]=-77608.714285715,["x"]=-360498.85714285,}, - [5]={["y"]=-74237.857142857,["x"]=-360591.7142857,}, - }, - [2] = { - [1]={["y"]=-75807.571428572,["x"]=-359073.42857142,}, - [2]={["y"]=-74770.142857144,["x"]=-360581.71428571,}, - [3]={["y"]=-74641.285714287,["x"]=-360585.42857142,}, - [4]={["y"]=-75734.142857144,["x"]=-359023.14285714,}, - [5]={["y"]=-75807.285714287,["x"]=-359073.42857142,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - GroomLake = { - PointsBoundary = { - [1]={["y"]=-88916.714285714,["x"]=-289102.28571425,}, - [2]={["y"]=-87023.571428572,["x"]=-290388.57142857,}, - [3]={["y"]=-85916.428571429,["x"]=-290674.28571428,}, - [4]={["y"]=-87645.000000001,["x"]=-286567.14285714,}, - [5]={["y"]=-88380.714285715,["x"]=-286388.57142857,}, - [6]={["y"]=-89670.714285715,["x"]=-283524.28571428,}, - [7]={["y"]=-89797.857142858,["x"]=-283567.14285714,}, - [8]={["y"]=-88635.000000001,["x"]=-286749.99999999,}, - [9]={["y"]=-89177.857142858,["x"]=-287207.14285714,}, - [10]={["y"]=-89092.142857144,["x"]=-288892.85714285,}, - [11]={["y"]=-88917.000000001,["x"]=-289102.85714285,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=-86039.000000001,["x"]=-290606.28571428,}, - [2]={["y"]=-85965.285714287,["x"]=-290573.99999999,}, - [3]={["y"]=-87692.714285715,["x"]=-286634.85714285,}, - [4]={["y"]=-87756.714285715,["x"]=-286663.99999999,}, - [5]={["y"]=-86038.714285715,["x"]=-290606.85714285,}, - }, - [2] = { - [1]={["y"]=-86808.428571429,["x"]=-290375.7142857,}, - [2]={["y"]=-86732.714285715,["x"]=-290344.28571427,}, - [3]={["y"]=-89672.714285714,["x"]=-283546.57142855,}, - [4]={["y"]=-89772.142857143,["x"]=-283587.71428569,}, - [5]={["y"]=-86808.142857143,["x"]=-290375.7142857,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - }, -} - ---- Creates a new AIRBASEPOLICE_NEVADA object. --- @param #AIRBASEPOLICE_NEVADA 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_NEVADA self -function AIRBASEPOLICE_NEVADA:New( SetClient ) - - -- Inherits from BASE - local self = BASE:Inherit( self, AIRBASEPOLICE_BASE:New( SetClient, self.Airbases ) ) - --- -- Nellis --- local NellisBoundary = GROUP:FindByName( "Nellis Boundary" ) --- self.Airbases.Nellis.ZoneBoundary = ZONE_POLYGON:New( "Nellis Boundary", NellisBoundary ):SmokeZone(SMOKECOLOR.White):Flush() --- --- local NellisRunway1 = GROUP:FindByName( "Nellis Runway 1" ) --- self.Airbases.Nellis.ZoneRunways[1] = ZONE_POLYGON:New( "Nellis Runway 1", NellisRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- local NellisRunway2 = GROUP:FindByName( "Nellis Runway 2" ) --- self.Airbases.Nellis.ZoneRunways[2] = ZONE_POLYGON:New( "Nellis Runway 2", NellisRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- -- McCarran --- local McCarranBoundary = GROUP:FindByName( "McCarran Boundary" ) --- self.Airbases.McCarran.ZoneBoundary = ZONE_POLYGON:New( "McCarran Boundary", McCarranBoundary ):SmokeZone(SMOKECOLOR.White):Flush() --- --- local McCarranRunway1 = GROUP:FindByName( "McCarran Runway 1" ) --- self.Airbases.McCarran.ZoneRunways[1] = ZONE_POLYGON:New( "McCarran Runway 1", McCarranRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- local McCarranRunway2 = GROUP:FindByName( "McCarran Runway 2" ) --- self.Airbases.McCarran.ZoneRunways[2] = ZONE_POLYGON:New( "McCarran Runway 2", McCarranRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- local McCarranRunway3 = GROUP:FindByName( "McCarran Runway 3" ) --- self.Airbases.McCarran.ZoneRunways[3] = ZONE_POLYGON:New( "McCarran Runway 3", McCarranRunway3 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- local McCarranRunway4 = GROUP:FindByName( "McCarran Runway 4" ) --- self.Airbases.McCarran.ZoneRunways[4] = ZONE_POLYGON:New( "McCarran Runway 4", McCarranRunway4 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- -- Creech --- local CreechBoundary = GROUP:FindByName( "Creech Boundary" ) --- self.Airbases.Creech.ZoneBoundary = ZONE_POLYGON:New( "Creech Boundary", CreechBoundary ):SmokeZone(SMOKECOLOR.White):Flush() --- --- local CreechRunway1 = GROUP:FindByName( "Creech Runway 1" ) --- self.Airbases.Creech.ZoneRunways[1] = ZONE_POLYGON:New( "Creech Runway 1", CreechRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- local CreechRunway2 = GROUP:FindByName( "Creech Runway 2" ) --- self.Airbases.Creech.ZoneRunways[2] = ZONE_POLYGON:New( "Creech Runway 2", CreechRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- -- Groom Lake --- local GroomLakeBoundary = GROUP:FindByName( "GroomLake Boundary" ) --- self.Airbases.GroomLake.ZoneBoundary = ZONE_POLYGON:New( "GroomLake Boundary", GroomLakeBoundary ):SmokeZone(SMOKECOLOR.White):Flush() --- --- local GroomLakeRunway1 = GROUP:FindByName( "GroomLake Runway 1" ) --- self.Airbases.GroomLake.ZoneRunways[1] = ZONE_POLYGON:New( "GroomLake Runway 1", GroomLakeRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- local GroomLakeRunway2 = GROUP:FindByName( "GroomLake Runway 2" ) --- self.Airbases.GroomLake.ZoneRunways[2] = ZONE_POLYGON:New( "GroomLake Runway 2", GroomLakeRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - -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. --- The @{Detection#DETECTION_BASE} class will detect objects within the battle zone for a list of @{Group}s detecting targets following (a) detection method(s). --- --- 1.1) DETECTION_BASE constructor --- ------------------------------- --- Construct a new DETECTION_BASE instance using the @{Detection#DETECTION_BASE.New}() method. --- --- 1.2) DETECTION_BASE initialization --- ---------------------------------- --- By default, detection will return detected objects with all the detection sensors available. --- However, you can ask how the objects were found with specific detection methods. --- If you use one of the below methods, the detection will work with the detection method specified. --- You can specify to apply multiple detection methods. --- --- Use the following functions to report the objects it detected using the methods Visual, Optical, Radar, IRST, RWR, DLINK: --- --- * @{Detection#DETECTION_BASE.InitDetectVisual}(): Detected using Visual. --- * @{Detection#DETECTION_BASE.InitDetectOptical}(): Detected using Optical. --- * @{Detection#DETECTION_BASE.InitDetectRadar}(): Detected using Radar. --- * @{Detection#DETECTION_BASE.InitDetectIRST}(): Detected using IRST. --- * @{Detection#DETECTION_BASE.InitDetectRWR}(): Detected using RWR. --- * @{Detection#DETECTION_BASE.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_AREAS} class, extends @{Detection#DETECTION_BASE} --- =============================================================================== --- The @{Detection#DETECTION_AREAS} class will detect units within the battle zone for a list of @{Group}s detecting targets following (a) detection method(s), --- and will build a list (table) of @{Set#SET_UNIT}s containing the @{Unit#UNIT}s detected. --- The class is group the detected units within zones given a DetectedZoneRange parameter. --- A set with multiple detected zones will be created as there are groups of units detected. --- --- 2.1) Retrieve the Detected Unit sets and Detected Zones --- ------------------------------------------------------- --- The DetectedUnitSets methods are implemented in @{Detection#DECTECTION_BASE} and the DetectedZones methods is implemented in @{Detection#DETECTION_AREAS}. --- --- Retrieve the DetectedUnitSets with the method @{Detection#DETECTION_BASE.GetDetectedSets}(). A table will be return of @{Set#SET_UNIT}s. --- To understand the amount of sets created, use the method @{Detection#DETECTION_BASE.GetDetectedSetCount}(). --- If you want to obtain a specific set from the DetectedSets, use the method @{Detection#DETECTION_BASE.GetDetectedSet}() with a given index. --- --- Retrieve the formed @{Zone@ZONE_UNIT}s as a result of the grouping the detected units within the DetectionZoneRange, use the method @{Detection#DETECTION_BASE.GetDetectionZones}(). --- To understand the amount of zones created, use the method @{Detection#DETECTION_BASE.GetDetectionZoneCount}(). --- If you want to obtain a specific zone from the DetectedZones, use the method @{Detection#DETECTION_BASE.GetDetectionZone}() with a given index. --- --- 1.4) Flare or Smoke detected units --- ---------------------------------- --- Use the methods @{Detection#DETECTION_AREAS.FlareDetectedUnits}() or @{Detection#DETECTION_AREAS.SmokeDetectedUnits}() to flare or smoke the detected units when a new detection has taken place. --- --- 1.5) Flare or Smoke detected zones --- ---------------------------------- --- Use the methods @{Detection#DETECTION_AREAS.FlareDetectedZones}() or @{Detection#DETECTION_AREAS.SmokeDetectedZones}() to flare or smoke the detected zones when a new detection has taken place. --- --- === --- --- ### Contributions: --- --- * Mechanist : Concept & Testing --- --- ### Authors: --- --- * FlightControl : Design & Programming --- --- @module Detection - - - ---- DETECTION_BASE class --- @type DETECTION_BASE --- @field Core.Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Forward Air Controller role. --- @field Dcs.DCSTypes#Distance DetectionRange The range till which targets are accepted to be detected. --- @field #DETECTION_BASE.DetectedObjects DetectedObjects The list of detected objects. --- @field #table DetectedObjectsIdentified Map of the DetectedObjects identified. --- @field #number DetectionRun --- @extends Core.Base#BASE -DETECTION_BASE = { - ClassName = "DETECTION_BASE", - DetectionSetGroup = nil, - DetectionRange = nil, - DetectedObjects = {}, - DetectionRun = 0, - DetectedObjectsIdentified = {}, -} - ---- @type DETECTION_BASE.DetectedObjects --- @list <#DETECTION_BASE.DetectedObject> - ---- @type DETECTION_BASE.DetectedObject --- @field #string Name --- @field #boolean Visible --- @field #string Type --- @field #number Distance --- @field #boolean Identified - ---- DETECTION constructor. --- @param #DETECTION_BASE self --- @param Core.Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Forward Air Controller role. --- @param Dcs.DCSTypes#Distance DetectionRange The range till which targets are accepted to be detected. --- @return #DETECTION_BASE self -function DETECTION_BASE:New( DetectionSetGroup, DetectionRange ) - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) - - self.DetectionSetGroup = DetectionSetGroup - self.DetectionRange = DetectionRange - - self:InitDetectVisual( false ) - self:InitDetectOptical( false ) - self:InitDetectRadar( false ) - self:InitDetectRWR( false ) - self:InitDetectIRST( false ) - self:InitDetectDLINK( false ) - - 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 - ---- Determines if a detected object has already been identified during detection processing. --- @param #DETECTION_BASE self --- @param #DETECTION_BASE.DetectedObject DetectedObject --- @return #boolean true if already identified. -function DETECTION_BASE:IsDetectedObjectIdentified( DetectedObject ) - self:F3( DetectedObject.Name ) - - local DetectedObjectName = DetectedObject.Name - local DetectedObjectIdentified = self.DetectedObjectsIdentified[DetectedObjectName] == true - self:T3( DetectedObjectIdentified ) - return DetectedObjectIdentified -end - ---- Identifies a detected object during detection processing. --- @param #DETECTION_BASE self --- @param #DETECTION_BASE.DetectedObject DetectedObject -function DETECTION_BASE:IdentifyDetectedObject( DetectedObject ) - self:F( DetectedObject.Name ) - - local DetectedObjectName = DetectedObject.Name - self.DetectedObjectsIdentified[DetectedObjectName] = true -end - ---- UnIdentify a detected object during detection processing. --- @param #DETECTION_BASE self --- @param #DETECTION_BASE.DetectedObject DetectedObject -function DETECTION_BASE:UnIdentifyDetectedObject( DetectedObject ) - - local DetectedObjectName = DetectedObject.Name - self.DetectedObjectsIdentified[DetectedObjectName] = false -end - ---- UnIdentify all detected objects during detection processing. --- @param #DETECTION_BASE self -function DETECTION_BASE:UnIdentifyAllDetectedObjects() - - self.DetectedObjectsIdentified = {} -- Table will be garbage collected. -end - ---- Gets a detected object with a given name. --- @param #DETECTION_BASE self --- @param #string ObjectName --- @return #DETECTION_BASE.DetectedObject -function DETECTION_BASE:GetDetectedObject( ObjectName ) - self:F3( ObjectName ) - - if ObjectName then - local DetectedObject = self.DetectedObjects[ObjectName] - - -- Only return detected objects that are alive! - local DetectedUnit = UNIT:FindByName( ObjectName ) - if DetectedUnit and DetectedUnit:IsAlive() then - if self:IsDetectedObjectIdentified( DetectedObject ) == false then - return DetectedObject - end - end - end - - return nil -end - ---- Get the detected @{Set#SET_BASE}s. --- @param #DETECTION_BASE self --- @return #DETECTION_BASE.DetectedSets DetectedSets -function DETECTION_BASE:GetDetectedSets() - - local DetectionSets = self.DetectedSets - return DetectionSets -end - ---- Get the amount of SETs with detected objects. --- @param #DETECTION_BASE self --- @return #number Count -function DETECTION_BASE:GetDetectedSetCount() - - local DetectionSetCount = #self.DetectedSets - return DetectionSetCount -end - ---- Get a SET of detected objects using a given numeric index. --- @param #DETECTION_BASE self --- @param #number Index --- @return Core.Set#SET_BASE -function DETECTION_BASE:GetDetectedSet( Index ) - - local DetectionSet = self.DetectedSets[Index] - if DetectionSet then - return DetectionSet - end - - return nil -end - ---- Get the detection Groups. --- @param #DETECTION_BASE self --- @return Wrapper.Group#GROUP -function DETECTION_BASE:GetDetectionSetGroup() - - local DetectionSetGroup = self.DetectionSetGroup - return DetectionSetGroup -end - ---- 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 - - ---- Schedule the DETECTION construction. --- @param #DETECTION_BASE self --- @param #number DelayTime The delay in seconds to wait the reporting. --- @param #number RepeatInterval The repeat interval in seconds for the reporting to happen repeatedly. --- @return #DETECTION_BASE self -function DETECTION_BASE:Schedule( DelayTime, RepeatInterval ) - self:F2() - - self.ScheduleDelayTime = DelayTime - self.ScheduleRepeatInterval = RepeatInterval - - self.DetectionScheduler = SCHEDULER:New( self, self._DetectionScheduler, { self, "Detection" }, DelayTime, RepeatInterval ) - return self -end - - ---- 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.DetectionRun = self.DetectionRun + 1 - - self:UnIdentifyAllDetectedObjects() -- Resets the DetectedObjectsIdentified table - - for DetectionGroupID, DetectionGroupData in pairs( self.DetectionSetGroup:GetSet() ) do - local DetectionGroup = DetectionGroupData -- Wrapper.Group#GROUP - - if DetectionGroup:IsAlive() then - - local DetectionGroupName = DetectionGroup:GetName() - - local DetectionDetectedTargets = DetectionGroup:GetDetectedTargets( - self.DetectVisual, - self.DetectOptical, - self.DetectRadar, - self.DetectIRST, - self.DetectRWR, - self.DetectDLINK - ) - - for DetectionDetectedTargetID, DetectionDetectedTarget in pairs( DetectionDetectedTargets ) do - local DetectionObject = DetectionDetectedTarget.object -- Dcs.DCSWrapper.Object#Object - self:T2( DetectionObject ) - - if DetectionObject and DetectionObject:isExist() and DetectionObject.id_ < 50000000 then - - local DetectionDetectedObjectName = DetectionObject:getName() - - local DetectionDetectedObjectPositionVec3 = DetectionObject:getPoint() - local DetectionGroupVec3 = DetectionGroup:GetVec3() - - local Distance = ( ( DetectionDetectedObjectPositionVec3.x - DetectionGroupVec3.x )^2 + - ( DetectionDetectedObjectPositionVec3.y - DetectionGroupVec3.y )^2 + - ( DetectionDetectedObjectPositionVec3.z - DetectionGroupVec3.z )^2 - ) ^ 0.5 / 1000 - - self:T2( { DetectionGroupName, DetectionDetectedObjectName, Distance } ) - - if Distance <= self.DetectionRange then - - if not self.DetectedObjects[DetectionDetectedObjectName] then - self.DetectedObjects[DetectionDetectedObjectName] = {} - end - self.DetectedObjects[DetectionDetectedObjectName].Name = DetectionDetectedObjectName - self.DetectedObjects[DetectionDetectedObjectName].Visible = DetectionDetectedTarget.visible - self.DetectedObjects[DetectionDetectedObjectName].Type = DetectionDetectedTarget.type - self.DetectedObjects[DetectionDetectedObjectName].Distance = DetectionDetectedTarget.distance - else - -- if beyond the DetectionRange then nullify... - if self.DetectedObjects[DetectionDetectedObjectName] then - self.DetectedObjects[DetectionDetectedObjectName] = nil - end - end - end - end - - self:T2( self.DetectedObjects ) - - -- okay, now we have a list of detected object names ... - -- Sort the table based on distance ... - table.sort( self.DetectedObjects, function( a, b ) return a.Distance < b.Distance end ) - end - end - - if self.DetectedObjects then - self:CreateDetectionSets() - end - - return true -end - - - ---- DETECTION_AREAS class --- @type DETECTION_AREAS --- @field Dcs.DCSTypes#Distance DetectionZoneRange The range till which targets are grouped upon the first detected target. --- @field #DETECTION_AREAS.DetectedAreas DetectedAreas A list of areas containing the set of @{Unit}s, @{Zone}s, the center @{Unit} within the zone, and ID of each area that was detected within a DetectionZoneRange. --- @extends Functional.Detection#DETECTION_BASE -DETECTION_AREAS = { - ClassName = "DETECTION_AREAS", - DetectedAreas = { n = 0 }, - DetectionZoneRange = nil, -} - ---- @type DETECTION_AREAS.DetectedAreas --- @list <#DETECTION_AREAS.DetectedArea> - ---- @type DETECTION_AREAS.DetectedArea --- @field Core.Set#SET_UNIT Set -- The Set of Units in the detected area. --- @field Core.Zone#ZONE_UNIT Zone -- The Zone of the detected area. --- @field #boolean Changed Documents if the detected area has changes. --- @field #table Changes A list of the changes reported on the detected area. (It is up to the user of the detected area to consume those changes). --- @field #number AreaID -- The identifier of the detected area. --- @field #boolean FriendliesNearBy Indicates if there are friendlies within the detected area. --- @field Wrapper.Unit#UNIT NearestFAC The nearest FAC near the Area. - - ---- DETECTION_AREAS constructor. --- @param Functional.Detection#DETECTION_AREAS self --- @param Core.Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Forward Air Controller role. --- @param Dcs.DCSTypes#Distance DetectionRange The range till which targets are accepted to be detected. --- @param Dcs.DCSTypes#Distance DetectionZoneRange The range till which targets are grouped upon the first detected target. --- @return Functional.Detection#DETECTION_AREAS self -function DETECTION_AREAS:New( DetectionSetGroup, DetectionRange, DetectionZoneRange ) - - -- Inherits from DETECTION_BASE - local self = BASE:Inherit( self, DETECTION_BASE:New( DetectionSetGroup, DetectionRange ) ) - - self.DetectionZoneRange = DetectionZoneRange - - self._SmokeDetectedUnits = false - self._FlareDetectedUnits = false - self._SmokeDetectedZones = false - self._FlareDetectedZones = false - - self:Schedule( 10, 10 ) - - return self -end - ---- Add a detected @{#DETECTION_AREAS.DetectedArea}. --- @param Core.Set#SET_UNIT Set -- The Set of Units in the detected area. --- @param Core.Zone#ZONE_UNIT Zone -- The Zone of the detected area. --- @return #DETECTION_AREAS.DetectedArea DetectedArea -function DETECTION_AREAS:AddDetectedArea( Set, Zone ) - local DetectedAreas = self:GetDetectedAreas() - DetectedAreas.n = self:GetDetectedAreaCount() + 1 - DetectedAreas[DetectedAreas.n] = {} - local DetectedArea = DetectedAreas[DetectedAreas.n] - DetectedArea.Set = Set - DetectedArea.Zone = Zone - DetectedArea.Removed = false - DetectedArea.AreaID = DetectedAreas.n - - return DetectedArea -end - ---- Remove a detected @{#DETECTION_AREAS.DetectedArea} with a given Index. --- @param #DETECTION_AREAS self --- @param #number Index The Index of the detection are to be removed. --- @return #nil -function DETECTION_AREAS:RemoveDetectedArea( Index ) - local DetectedAreas = self:GetDetectedAreas() - local DetectedAreaCount = self:GetDetectedAreaCount() - local DetectedArea = DetectedAreas[Index] - local DetectedAreaSet = DetectedArea.Set - DetectedArea[Index] = nil - return nil -end - - ---- Get the detected @{#DETECTION_AREAS.DetectedAreas}. --- @param #DETECTION_AREAS self --- @return #DETECTION_AREAS.DetectedAreas DetectedAreas -function DETECTION_AREAS:GetDetectedAreas() - - local DetectedAreas = self.DetectedAreas - return DetectedAreas -end - ---- Get the amount of @{#DETECTION_AREAS.DetectedAreas}. --- @param #DETECTION_AREAS self --- @return #number DetectedAreaCount -function DETECTION_AREAS:GetDetectedAreaCount() - - local DetectedAreaCount = self.DetectedAreas.n - return DetectedAreaCount -end - ---- Get the @{Set#SET_UNIT} of a detecttion area using a given numeric index. --- @param #DETECTION_AREAS self --- @param #number Index --- @return Core.Set#SET_UNIT DetectedSet -function DETECTION_AREAS:GetDetectedSet( Index ) - - local DetectedSetUnit = self.DetectedAreas[Index].Set - if DetectedSetUnit then - return DetectedSetUnit - end - - return nil -end - ---- Get the @{Zone#ZONE_UNIT} of a detection area using a given numeric index. --- @param #DETECTION_AREAS self --- @param #number Index --- @return Core.Zone#ZONE_UNIT DetectedZone -function DETECTION_AREAS:GetDetectedZone( Index ) - - local DetectedZone = self.DetectedAreas[Index].Zone - if DetectedZone then - return DetectedZone - end - - return nil -end - ---- Background worker function to determine if there are friendlies nearby ... --- @param #DETECTION_AREAS self --- @param Wrapper.Unit#UNIT ReportUnit -function DETECTION_AREAS:ReportFriendliesNearBy( ReportGroupData ) - self:F2() - - local DetectedArea = ReportGroupData.DetectedArea -- Functional.Detection#DETECTION_AREAS.DetectedArea - local DetectedSet = ReportGroupData.DetectedArea.Set - local DetectedZone = ReportGroupData.DetectedArea.Zone - local DetectedZoneUnit = DetectedZone.ZoneUNIT - - DetectedArea.FriendliesNearBy = false - - local SphereSearch = { - id = world.VolumeType.SPHERE, - params = { - point = DetectedZoneUnit:GetVec3(), - radius = 6000, - } - - } - - --- @param Dcs.DCSWrapper.Unit#Unit FoundDCSUnit - -- @param Wrapper.Group#GROUP ReportGroup - -- @param Set#SET_GROUP ReportSetGroup - local FindNearByFriendlies = function( FoundDCSUnit, ReportGroupData ) - - local DetectedArea = ReportGroupData.DetectedArea -- Functional.Detection#DETECTION_AREAS.DetectedArea - local DetectedSet = ReportGroupData.DetectedArea.Set - local DetectedZone = ReportGroupData.DetectedArea.Zone - local DetectedZoneUnit = DetectedZone.ZoneUNIT -- Wrapper.Unit#UNIT - local ReportSetGroup = ReportGroupData.ReportSetGroup - - local EnemyCoalition = DetectedZoneUnit:GetCoalition() - - local FoundUnitCoalition = FoundDCSUnit:getCoalition() - local FoundUnitName = FoundDCSUnit:getName() - local FoundUnitGroupName = FoundDCSUnit:getGroup():getName() - local EnemyUnitName = DetectedZoneUnit:GetName() - local FoundUnitInReportSetGroup = ReportSetGroup:FindGroup( FoundUnitGroupName ) ~= nil - - self:T3( { "Friendlies search:", FoundUnitName, FoundUnitCoalition, EnemyUnitName, EnemyCoalition, FoundUnitInReportSetGroup } ) - - if FoundUnitCoalition ~= EnemyCoalition and FoundUnitInReportSetGroup == false then - DetectedArea.FriendliesNearBy = true - return false - end - - return true - end - - world.searchObjects( Object.Category.UNIT, SphereSearch, FindNearByFriendlies, ReportGroupData ) - -end - - - ---- Returns if there are friendlies nearby the FAC units ... --- @param #DETECTION_AREAS self --- @return #boolean trhe if there are friendlies nearby -function DETECTION_AREAS:IsFriendliesNearBy( DetectedArea ) - - self:T3( DetectedArea.FriendliesNearBy ) - return DetectedArea.FriendliesNearBy or false -end - ---- Calculate the maxium A2G threat level of the DetectedArea. --- @param #DETECTION_AREAS self --- @param #DETECTION_AREAS.DetectedArea DetectedArea -function DETECTION_AREAS:CalculateThreatLevelA2G( DetectedArea ) - - local MaxThreatLevelA2G = 0 - for UnitName, UnitData in pairs( DetectedArea.Set:GetSet() ) do - local ThreatUnit = UnitData -- Wrapper.Unit#UNIT - local ThreatLevelA2G = ThreatUnit:GetThreatLevel() - if ThreatLevelA2G > MaxThreatLevelA2G then - MaxThreatLevelA2G = ThreatLevelA2G - end - end - - self:T3( MaxThreatLevelA2G ) - DetectedArea.MaxThreatLevelA2G = MaxThreatLevelA2G - -end - ---- Find the nearest FAC of the DetectedArea. --- @param #DETECTION_AREAS self --- @param #DETECTION_AREAS.DetectedArea DetectedArea --- @return Wrapper.Unit#UNIT The nearest FAC unit -function DETECTION_AREAS:NearestFAC( DetectedArea ) - - local NearestFAC = nil - local MinDistance = 1000000000 -- Units are not further than 1000000 km away from an area :-) - - for FACGroupName, FACGroupData in pairs( self.DetectionSetGroup:GetSet() ) do - for FACUnit, FACUnitData in pairs( FACGroupData:GetUnits() ) do - local FACUnit = FACUnitData -- Wrapper.Unit#UNIT - if FACUnit:IsActive() then - local Vec3 = FACUnit:GetVec3() - local PointVec3 = POINT_VEC3:NewFromVec3( Vec3 ) - local Distance = PointVec3:Get2DDistance(POINT_VEC3:NewFromVec3( FACUnit:GetVec3() ) ) - if Distance < MinDistance then - MinDistance = Distance - NearestFAC = FACUnit - end - end - end - end - - DetectedArea.NearestFAC = NearestFAC - -end - ---- Returns the A2G threat level of the units in the DetectedArea --- @param #DETECTION_AREAS self --- @param #DETECTION_AREAS.DetectedArea DetectedArea --- @return #number a scale from 0 to 10. -function DETECTION_AREAS:GetTreatLevelA2G( DetectedArea ) - - self:T3( DetectedArea.MaxThreatLevelA2G ) - return DetectedArea.MaxThreatLevelA2G -end - - - ---- Smoke the detected units --- @param #DETECTION_AREAS self --- @return #DETECTION_AREAS self -function DETECTION_AREAS:SmokeDetectedUnits() - self:F2() - - self._SmokeDetectedUnits = true - return self -end - ---- Flare the detected units --- @param #DETECTION_AREAS self --- @return #DETECTION_AREAS self -function DETECTION_AREAS:FlareDetectedUnits() - self:F2() - - self._FlareDetectedUnits = true - return self -end - ---- Smoke the detected zones --- @param #DETECTION_AREAS self --- @return #DETECTION_AREAS self -function DETECTION_AREAS:SmokeDetectedZones() - self:F2() - - self._SmokeDetectedZones = true - return self -end - ---- Flare the detected zones --- @param #DETECTION_AREAS self --- @return #DETECTION_AREAS self -function DETECTION_AREAS:FlareDetectedZones() - self:F2() - - self._FlareDetectedZones = true - return self -end - ---- Add a change to the detected zone. --- @param #DETECTION_AREAS self --- @param #DETECTION_AREAS.DetectedArea DetectedArea --- @param #string ChangeCode --- @return #DETECTION_AREAS self -function DETECTION_AREAS:AddChangeArea( DetectedArea, ChangeCode, AreaUnitType ) - - DetectedArea.Changed = true - local AreaID = DetectedArea.AreaID - - DetectedArea.Changes = DetectedArea.Changes or {} - DetectedArea.Changes[ChangeCode] = DetectedArea.Changes[ChangeCode] or {} - DetectedArea.Changes[ChangeCode].AreaID = AreaID - DetectedArea.Changes[ChangeCode].AreaUnitType = AreaUnitType - - self:T( { "Change on Detection Area:", DetectedArea.AreaID, ChangeCode, AreaUnitType } ) - - return self -end - - ---- Add a change to the detected zone. --- @param #DETECTION_AREAS self --- @param #DETECTION_AREAS.DetectedArea DetectedArea --- @param #string ChangeCode --- @param #string ChangeUnitType --- @return #DETECTION_AREAS self -function DETECTION_AREAS:AddChangeUnit( DetectedArea, ChangeCode, ChangeUnitType ) - - DetectedArea.Changed = true - local AreaID = DetectedArea.AreaID - - DetectedArea.Changes = DetectedArea.Changes or {} - DetectedArea.Changes[ChangeCode] = DetectedArea.Changes[ChangeCode] or {} - DetectedArea.Changes[ChangeCode][ChangeUnitType] = DetectedArea.Changes[ChangeCode][ChangeUnitType] or 0 - DetectedArea.Changes[ChangeCode][ChangeUnitType] = DetectedArea.Changes[ChangeCode][ChangeUnitType] + 1 - DetectedArea.Changes[ChangeCode].AreaID = AreaID - - self:T( { "Change on Detection Area:", DetectedArea.AreaID, ChangeCode, ChangeUnitType } ) - - return self -end - ---- Make text documenting the changes of the detected zone. --- @param #DETECTION_AREAS self --- @param #DETECTION_AREAS.DetectedArea DetectedArea --- @return #string The Changes text -function DETECTION_AREAS:GetChangeText( DetectedArea ) - self:F( DetectedArea ) - - local MT = {} - - for ChangeCode, ChangeData in pairs( DetectedArea.Changes ) do - - if ChangeCode == "AA" then - MT[#MT+1] = "Detected new area " .. ChangeData.AreaID .. ". The center target is a " .. ChangeData.AreaUnitType .. "." - end - - if ChangeCode == "RAU" then - MT[#MT+1] = "Changed area " .. ChangeData.AreaID .. ". Removed the center target." - end - - if ChangeCode == "AAU" then - MT[#MT+1] = "Changed area " .. ChangeData.AreaID .. ". The new center target is a " .. ChangeData.AreaUnitType "." - end - - if ChangeCode == "RA" then - MT[#MT+1] = "Removed old area " .. ChangeData.AreaID .. ". No more targets in this area." - end - - if ChangeCode == "AU" then - local MTUT = {} - for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do - if ChangeUnitType ~= "AreaID" then - MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType - end - end - MT[#MT+1] = "Detected for area " .. ChangeData.AreaID .. " new target(s) " .. table.concat( MTUT, ", " ) .. "." - end - - if ChangeCode == "RU" then - local MTUT = {} - for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do - if ChangeUnitType ~= "AreaID" then - MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType - end - end - MT[#MT+1] = "Removed for area " .. ChangeData.AreaID .. " invisible or destroyed target(s) " .. table.concat( MTUT, ", " ) .. "." - end - - end - - return table.concat( MT, "\n" ) - -end - - ---- Accepts changes from the detected zone. --- @param #DETECTION_AREAS self --- @param #DETECTION_AREAS.DetectedArea DetectedArea --- @return #DETECTION_AREAS self -function DETECTION_AREAS:AcceptChanges( DetectedArea ) - - DetectedArea.Changed = false - DetectedArea.Changes = {} - - return self -end - - ---- Make a DetectionSet table. This function will be overridden in the derived clsses. --- @param #DETECTION_AREAS self --- @return #DETECTION_AREAS self -function DETECTION_AREAS:CreateDetectionSets() - self:F2() - - -- First go through all detected sets, and check if there are new detected units, match all existing detected units and identify undetected units. - -- Regroup when needed, split groups when needed. - for DetectedAreaID, DetectedAreaData in ipairs( self.DetectedAreas ) do - - local DetectedArea = DetectedAreaData -- #DETECTION_AREAS.DetectedArea - if DetectedArea then - - local DetectedSet = DetectedArea.Set - - local AreaExists = false -- This flag will determine of the detected area is still existing. - - -- First test if the center unit is detected in the detection area. - self:T3( DetectedArea.Zone.ZoneUNIT.UnitName ) - local DetectedZoneObject = self:GetDetectedObject( DetectedArea.Zone.ZoneUNIT.UnitName ) - self:T3( { "Detecting Zone Object", DetectedArea.AreaID, DetectedArea.Zone, DetectedZoneObject } ) - - if DetectedZoneObject then - - --self:IdentifyDetectedObject( DetectedZoneObject ) - AreaExists = true - - - - else - -- The center object of the detected area has not been detected. Find an other unit of the set to become the center of the area. - -- First remove the center unit from the set. - DetectedSet:RemoveUnitsByName( DetectedArea.Zone.ZoneUNIT.UnitName ) - - self:AddChangeArea( DetectedArea, 'RAU', "Dummy" ) - - -- Then search for a new center area unit within the set. Note that the new area unit candidate must be within the area range. - for DetectedUnitName, DetectedUnitData in pairs( DetectedSet:GetSet() ) do - - local DetectedUnit = DetectedUnitData -- Wrapper.Unit#UNIT - local DetectedObject = self:GetDetectedObject( DetectedUnit.UnitName ) - - -- The DetectedObject can be nil when the DetectedUnit is not alive anymore or it is not in the DetectedObjects map. - -- If the DetectedUnit was already identified, DetectedObject will be nil. - if DetectedObject then - self:IdentifyDetectedObject( DetectedObject ) - AreaExists = true - - -- Assign the Unit as the new center unit of the detected area. - DetectedArea.Zone = ZONE_UNIT:New( DetectedUnit:GetName(), DetectedUnit, self.DetectionZoneRange ) - - self:AddChangeArea( DetectedArea, "AAU", DetectedArea.Zone.ZoneUNIT:GetTypeName() ) - - -- We don't need to add the DetectedObject to the area set, because it is already there ... - break - end - end - end - - -- Now we've determined the center unit of the area, now we can iterate the units in the detected area. - -- Note that the position of the area may have moved due to the center unit repositioning. - -- If no center unit was identified, then the detected area does not exist anymore and should be deleted, as there are no valid units that can be the center unit. - if AreaExists then - - -- ok, we found the center unit of the area, now iterate through the detected area set and see which units are still within the center unit zone ... - -- Those units within the zone are flagged as Identified. - -- If a unit was not found in the set, remove it from the set. This may be added later to other existing or new sets. - for DetectedUnitName, DetectedUnitData in pairs( DetectedSet:GetSet() ) do - - local DetectedUnit = DetectedUnitData -- Wrapper.Unit#UNIT - local DetectedObject = nil - if DetectedUnit:IsAlive() then - --self:E(DetectedUnit:GetName()) - DetectedObject = self:GetDetectedObject( DetectedUnit:GetName() ) - end - if DetectedObject then - - -- Check if the DetectedUnit is within the DetectedArea.Zone - if DetectedUnit:IsInZone( DetectedArea.Zone ) then - - -- Yes, the DetectedUnit is within the DetectedArea.Zone, no changes, DetectedUnit can be kept within the Set. - self:IdentifyDetectedObject( DetectedObject ) - - else - -- No, the DetectedUnit is not within the DetectedArea.Zone, remove DetectedUnit from the Set. - DetectedSet:Remove( DetectedUnitName ) - self:AddChangeUnit( DetectedArea, "RU", DetectedUnit:GetTypeName() ) - end - - else - -- There was no DetectedObject, remove DetectedUnit from the Set. - self:AddChangeUnit( DetectedArea, "RU", "destroyed target" ) - DetectedSet:Remove( DetectedUnitName ) - - -- The DetectedObject has been identified, because it does not exist ... - -- self:IdentifyDetectedObject( DetectedObject ) - end - end - else - self:RemoveDetectedArea( DetectedAreaID ) - self:AddChangeArea( DetectedArea, "RA" ) - end - end - end - - -- We iterated through the existing detection areas and: - -- - We checked which units are still detected in each detection area. Those units were flagged as Identified. - -- - We recentered the detection area to new center units where it was needed. - -- - -- Now we need to loop through the unidentified detected units and see where they belong: - -- - They can be added to a new detection area and become the new center unit. - -- - They can be added to a new detection area. - for DetectedUnitName, DetectedObjectData in pairs( self.DetectedObjects ) do - - local DetectedObject = self:GetDetectedObject( DetectedUnitName ) - - if DetectedObject then - - -- We found an unidentified unit outside of any existing detection area. - local DetectedUnit = UNIT:FindByName( DetectedUnitName ) -- Wrapper.Unit#UNIT - - local AddedToDetectionArea = false - - for DetectedAreaID, DetectedAreaData in ipairs( self.DetectedAreas ) do - - local DetectedArea = DetectedAreaData -- #DETECTION_AREAS.DetectedArea - if DetectedArea then - self:T( "Detection Area #" .. DetectedArea.AreaID ) - local DetectedSet = DetectedArea.Set - if not self:IsDetectedObjectIdentified( DetectedObject ) and DetectedUnit:IsInZone( DetectedArea.Zone ) then - self:IdentifyDetectedObject( DetectedObject ) - DetectedSet:AddUnit( DetectedUnit ) - AddedToDetectionArea = true - self:AddChangeUnit( DetectedArea, "AU", DetectedUnit:GetTypeName() ) - end - end - end - - if AddedToDetectionArea == false then - - -- New detection area - local DetectedArea = self:AddDetectedArea( - SET_UNIT:New(), - ZONE_UNIT:New( DetectedUnitName, DetectedUnit, self.DetectionZoneRange ) - ) - --self:E( DetectedArea.Zone.ZoneUNIT.UnitName ) - DetectedArea.Set:AddUnit( DetectedUnit ) - self:AddChangeArea( DetectedArea, "AA", DetectedUnit:GetTypeName() ) - end - end - end - - -- Now all the tests should have been build, now make some smoke and flares... - -- We also report here the friendlies within the detected areas. - - for DetectedAreaID, DetectedAreaData in ipairs( self.DetectedAreas ) do - - local DetectedArea = DetectedAreaData -- #DETECTION_AREAS.DetectedArea - local DetectedSet = DetectedArea.Set - local DetectedZone = DetectedArea.Zone - - self:ReportFriendliesNearBy( { DetectedArea = DetectedArea, ReportSetGroup = self.DetectionSetGroup } ) -- Fill the Friendlies table - self:CalculateThreatLevelA2G( DetectedArea ) -- Calculate A2G threat level - self:NearestFAC( DetectedArea ) - - if DETECTION_AREAS._SmokeDetectedUnits or self._SmokeDetectedUnits then - DetectedZone.ZoneUNIT:SmokeRed() - end - DetectedSet:ForEachUnit( - --- @param Wrapper.Unit#UNIT DetectedUnit - function( DetectedUnit ) - if DetectedUnit:IsAlive() then - self:T( "Detected Set #" .. DetectedArea.AreaID .. ":" .. DetectedUnit:GetName() ) - if DETECTION_AREAS._FlareDetectedUnits or self._FlareDetectedUnits then - DetectedUnit:FlareGreen() - end - if DETECTION_AREAS._SmokeDetectedUnits or self._SmokeDetectedUnits then - DetectedUnit:SmokeGreen() - end - end - end - ) - if DETECTION_AREAS._FlareDetectedZones or self._FlareDetectedZones then - DetectedZone:FlareZone( SMOKECOLOR.White, 30, math.random( 0,90 ) ) - end - if DETECTION_AREAS._SmokeDetectedZones or self._SmokeDetectedZones then - DetectedZone:SmokeZone( SMOKECOLOR.White, 30 ) - end - end - -end - - ---- Single-Player:**No** / Multi-Player:**Yes** / AI:**Yes** / Human:**No** / Types:**All** -- **AI Balancing will replace in multi player missions --- non-occupied human slots with AI groups, in order to provide an engaging simulation environment, --- even when there are hardly any players in the mission.** --- --- ![Banner Image](..\Presentations\AI_Balancer\Dia1.JPG) --- --- === --- --- # 1) @{AI_Balancer#AI_BALANCER} class, extends @{Fsm#FSM_SET} --- --- The @{AI_Balancer#AI_BALANCER} class monitors and manages as many replacement AI groups as there are --- CLIENTS in a SET_CLIENT collection, which are not occupied by human players. --- In other words, use AI_BALANCER to simulate human behaviour by spawning in replacement AI in multi player missions. --- --- The parent class @{Fsm#FSM_SET} manages the functionality to control the Finite State Machine (FSM). --- The mission designer can tailor the behaviour of the AI_BALANCER, by defining event and state transition methods. --- An explanation about state and event transition methods can be found in the @{FSM} module documentation. --- --- The mission designer can tailor the AI_BALANCER behaviour, by implementing a state or event handling method for the following: --- --- * **@{#AI_BALANCER.OnAfterSpawned}**( AISet, From, Event, To, AIGroup ): Define to add extra logic when an AI is spawned. --- --- ## 1.1) AI_BALANCER construction --- --- Create a new AI_BALANCER object with the @{#AI_BALANCER.New}() method: --- --- ## 1.2) AI_BALANCER is a FSM --- --- ![Process](..\Presentations\AI_Balancer\Dia13.JPG) --- --- ### 1.2.1) AI_BALANCER States --- --- * **Monitoring** ( Set ): Monitoring the Set if all AI is spawned for the Clients. --- * **Spawning** ( Set, ClientName ): There is a new AI group spawned with ClientName as the name of reference. --- * **Spawned** ( Set, AIGroup ): A new AI has been spawned. You can handle this event to customize the AI behaviour with other AI FSMs or own processes. --- * **Destroying** ( Set, AIGroup ): The AI is being destroyed. --- * **Returning** ( Set, AIGroup ): The AI is returning to the airbase specified by the ReturnToAirbase methods. Handle this state to customize the return behaviour of the AI, if any. --- --- ### 1.2.2) AI_BALANCER Events --- --- * **Monitor** ( Set ): Every 10 seconds, the Monitor event is triggered to monitor the Set. --- * **Spawn** ( Set, ClientName ): Triggers when there is a new AI group to be spawned with ClientName as the name of reference. --- * **Spawned** ( Set, AIGroup ): Triggers when a new AI has been spawned. You can handle this event to customize the AI behaviour with other AI FSMs or own processes. --- * **Destroy** ( Set, AIGroup ): The AI is being destroyed. --- * **Return** ( Set, AIGroup ): The AI is returning to the airbase specified by the ReturnToAirbase methods. --- --- ## 1.3) AI_BALANCER spawn interval for replacement AI --- --- Use the method @{#AI_BALANCER.InitSpawnInterval}() to set the earliest and latest interval in seconds that is waited until a new replacement AI is spawned. --- --- ## 1.4) AI_BALANCER returns AI to Airbases --- --- By default, When a human player joins a slot that is AI_BALANCED, the AI group will be destroyed by default. --- However, there are 2 additional options that you can use to customize the destroy behaviour. --- When a human player joins a slot, you can configure to let the AI return to: --- --- * @{#AI_BALANCER.ReturnToHomeAirbase}: Returns the AI to the **home** @{Airbase#AIRBASE}. --- * @{#AI_BALANCER.ReturnToNearestAirbases}: Returns the AI to the **nearest friendly** @{Airbase#AIRBASE}. --- --- Note that when AI returns to an airbase, the AI_BALANCER will trigger the **Return** event and the AI will return, --- otherwise the AI_BALANCER will trigger a **Destroy** event, and the AI will be destroyed. --- --- === --- --- # **API CHANGE HISTORY** --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- Hereby the change log: --- --- 2017-01-17: There is still a problem with AI being destroyed, but not respawned. Need to check further upon that. --- --- 2017-01-08: AI_BALANCER:**InitSpawnInterval( Earliest, Latest )** added. --- --- === --- --- # **AUTHORS and CONTRIBUTIONS** --- --- ### Contributions: --- --- * **[Dutch_Baron](https://forums.eagle.ru/member.php?u=112075)**: Working together with James has resulted in the creation of the AI_BALANCER class. James has shared his ideas on balancing AI with air units, and together we made a first design which you can use now :-) --- * **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 AI_BALANCER moose class. --- --- ### Authors: --- --- * FlightControl: Framework Design & Programming and Documentation. --- --- @module AI_Balancer - ---- AI_BALANCER class --- @type AI_BALANCER --- @field Core.Set#SET_CLIENT SetClient --- @field Functional.Spawn#SPAWN SpawnAI --- @field Wrapper.Group#GROUP Test --- @extends Core.Fsm#FSM_SET -AI_BALANCER = { - ClassName = "AI_BALANCER", - PatrolZones = {}, - AIGroups = {}, - Earliest = 5, -- Earliest a new AI can be spawned is in 5 seconds. - Latest = 60, -- Latest a new AI can be spawned is in 60 seconds. -} - - - ---- Creates a new AI_BALANCER object --- @param #AI_BALANCER self --- @param Core.Set#SET_CLIENT SetClient A SET\_CLIENT object that will contain the CLIENT objects to be monitored if they are alive or not (joined by a player). --- @param Functional.Spawn#SPAWN SpawnAI The default Spawn object to spawn new AI Groups when needed. --- @return #AI_BALANCER -function AI_BALANCER:New( SetClient, SpawnAI ) - - -- Inherits from BASE - local self = BASE:Inherit( self, FSM_SET:New( SET_GROUP:New() ) ) -- AI.AI_Balancer#AI_BALANCER - - -- TODO: Define the OnAfterSpawned event - self:SetStartState( "None" ) - self:AddTransition( "*", "Monitor", "Monitoring" ) - self:AddTransition( "*", "Spawn", "Spawning" ) - self:AddTransition( "Spawning", "Spawned", "Spawned" ) - self:AddTransition( "*", "Destroy", "Destroying" ) - self:AddTransition( "*", "Return", "Returning" ) - - self.SetClient = SetClient - self.SetClient:FilterOnce() - self.SpawnAI = SpawnAI - - self.SpawnQueue = {} - - self.ToNearestAirbase = false - self.ToHomeAirbase = false - - self:__Monitor( 1 ) - - return self -end - ---- Sets the earliest to the latest interval in seconds how long AI_BALANCER will wait to spawn a new AI. --- Provide 2 identical seconds if the interval should be a fixed amount of seconds. --- @param #AI_BALANCER self --- @param #number Earliest The earliest a new AI can be spawned in seconds. --- @param #number Latest The latest a new AI can be spawned in seconds. --- @return self -function AI_BALANCER:InitSpawnInterval( Earliest, Latest ) - - self.Earliest = Earliest - self.Latest = Latest - - return self -end - ---- Returns the AI to the nearest friendly @{Airbase#AIRBASE}. --- @param #AI_BALANCER self --- @param Dcs.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 Core.Set#SET_AIRBASE ReturnAirbaseSet The SET of @{Set#SET_AIRBASE}s to evaluate where to return to. -function AI_BALANCER:ReturnToNearestAirbases( ReturnTresholdRange, ReturnAirbaseSet ) - - self.ToNearestAirbase = true - self.ReturnTresholdRange = ReturnTresholdRange - self.ReturnAirbaseSet = ReturnAirbaseSet -end - ---- Returns the AI to the home @{Airbase#AIRBASE}. --- @param #AI_BALANCER self --- @param Dcs.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 AI_BALANCER:ReturnToHomeAirbase( ReturnTresholdRange ) - - self.ToHomeAirbase = true - self.ReturnTresholdRange = ReturnTresholdRange -end - ---- @param #AI_BALANCER self --- @param Core.Set#SET_GROUP SetGroup --- @param #string ClientName --- @param Wrapper.Group#GROUP AIGroup -function AI_BALANCER:onenterSpawning( SetGroup, From, Event, To, ClientName ) - - -- OK, Spawn a new group from the default SpawnAI object provided. - local AIGroup = self.SpawnAI:Spawn() -- Wrapper.Group#GROUP - if AIGroup then - AIGroup:E( "Spawning new AIGroup" ) - --TODO: need to rework UnitName thing ... - - SetGroup:Add( ClientName, AIGroup ) - self.SpawnQueue[ClientName] = nil - - -- Fire the Spawned event. The first parameter is the AIGroup just Spawned. - -- Mission designers can catch this event to bind further actions to the AIGroup. - self:Spawned( AIGroup ) - end -end - ---- @param #AI_BALANCER self --- @param Core.Set#SET_GROUP SetGroup --- @param Wrapper.Group#GROUP AIGroup -function AI_BALANCER:onenterDestroying( SetGroup, From, Event, To, ClientName, AIGroup ) - - AIGroup:Destroy() - SetGroup:Flush() - SetGroup:Remove( ClientName ) - SetGroup:Flush() -end - ---- @param #AI_BALANCER self --- @param Core.Set#SET_GROUP SetGroup --- @param Wrapper.Group#GROUP AIGroup -function AI_BALANCER:onenterReturning( SetGroup, From, Event, To, AIGroup ) - - local AIGroupTemplate = AIGroup:GetTemplate() - if self.ToHomeAirbase == true then - local WayPointCount = #AIGroupTemplate.route.points - local SwitchWayPointCommand = AIGroup:CommandSwitchWayPoint( 1, WayPointCount, 1 ) - AIGroup:SetCommand( SwitchWayPointCommand ) - AIGroup:MessageToRed( "Returning to home base ...", 30 ) - else - -- Okay, we need to send this Group back to the nearest base of the Coalition of the AI. - --TODO: i need to rework the POINT_VEC2 thing. - local PointVec2 = POINT_VEC2:New( AIGroup:GetVec2().x, AIGroup:GetVec2().y ) - local ClosestAirbase = self.ReturnAirbaseSet:FindNearestAirbaseFromPointVec2( PointVec2 ) - self:T( ClosestAirbase.AirbaseName ) - AIGroup:MessageToRed( "Returning to " .. ClosestAirbase:GetName().. " ...", 30 ) - local RTBRoute = AIGroup:RouteReturnToAirbase( ClosestAirbase ) - AIGroupTemplate.route = RTBRoute - AIGroup:Respawn( AIGroupTemplate ) - end - -end - - ---- @param #AI_BALANCER self -function AI_BALANCER:onenterMonitoring( SetGroup ) - - self:T2( { self.SetClient:Count() } ) - --self.SetClient:Flush() - - self.SetClient:ForEachClient( - --- @param Wrapper.Client#CLIENT Client - function( Client ) - self:T3(Client.ClientName) - - local AIGroup = self.Set:Get( Client.UnitName ) -- Wrapper.Group#GROUP - if Client:IsAlive() then - - if AIGroup and AIGroup:IsAlive() == true then - - if self.ToNearestAirbase == false and self.ToHomeAirbase == false then - self:Destroy( Client.UnitName, AIGroup ) - else - -- We test if there is no other CLIENT within the self.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:GetVec2(), self.ReturnTresholdRange ) - - self:T2( RangeZone ) - - _DATABASE:ForEachPlayer( - --- @param Wrapper.Unit#UNIT RangeTestUnit - function( RangeTestUnit, RangeZone, AIGroup, PlayerInRange ) - self:T2( { PlayerInRange, RangeTestUnit.UnitName, RangeZone.ZoneName } ) - if RangeTestUnit:IsInZone( RangeZone ) == true then - self:T2( "in zone" ) - if RangeTestUnit:GetCoalition() ~= AIGroup:GetCoalition() then - self:T2( "in range" ) - PlayerInRange.Value = true - end - end - end, - - --- @param Core.Zone#ZONE_RADIUS RangeZone - -- @param Wrapper.Group#GROUP AIGroup - function( RangeZone, AIGroup, PlayerInRange ) - if PlayerInRange.Value == false then - self:Return( AIGroup ) - end - end - , RangeZone, AIGroup, PlayerInRange - ) - - end - self.Set:Remove( Client.UnitName ) - end - else - if not AIGroup or not AIGroup:IsAlive() == true then - self:T( "Client " .. Client.UnitName .. " not alive." ) - if not self.SpawnQueue[Client.UnitName] then - -- Spawn a new AI taking into account the spawn interval Earliest, Latest - self:__Spawn( math.random( self.Earliest, self.Latest ), Client.UnitName ) - self.SpawnQueue[Client.UnitName] = true - self:E( "New AI Spawned for Client " .. Client.UnitName ) - end - end - end - return true - end - ) - - self:__Monitor( 10 ) -end - - - ---- Single-Player:**Yes** / Multi-Player:**Yes** / AI:**Yes** / Human:**No** / Types:**Air** -- --- **Air Patrolling or Staging.** --- --- ![Banner Image](..\Presentations\AI_PATROL\Dia1.JPG) --- --- === --- --- # 1) @{#AI_PATROL_ZONE} class, extends @{Fsm#FSM_CONTROLLABLE} --- --- The @{#AI_PATROL_ZONE} class implements the core functions to patrol a @{Zone} by an AI @{Controllable} or @{Group}. --- --- ![Process](..\Presentations\AI_PATROL\Dia3.JPG) --- --- The AI_PATROL_ZONE is assigned a @{Group} and this must be done before the AI_PATROL_ZONE process can be started using the **Start** event. --- --- ![Process](..\Presentations\AI_PATROL\Dia4.JPG) --- --- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits. --- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. --- --- ![Process](..\Presentations\AI_PATROL\Dia5.JPG) --- --- This cycle will continue. --- --- ![Process](..\Presentations\AI_PATROL\Dia6.JPG) --- --- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event. --- --- ![Process](..\Presentations\AI_PATROL\Dia9.JPG) --- ----- Note that the enemy is not engaged! To model enemy engagement, either tailor the **Detected** event, or --- use derived AI_ classes to model AI offensive or defensive behaviour. --- --- ![Process](..\Presentations\AI_PATROL\Dia10.JPG) --- --- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. --- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. --- --- ![Process](..\Presentations\AI_PATROL\Dia11.JPG) --- --- ## 1.1) AI_PATROL_ZONE constructor --- --- * @{#AI_PATROL_ZONE.New}(): Creates a new AI_PATROL_ZONE object. --- --- ## 1.2) AI_PATROL_ZONE is a FSM --- --- ![Process](..\Presentations\AI_PATROL\Dia2.JPG) --- --- ### 1.2.1) AI_PATROL_ZONE States --- --- * **None** ( Group ): The process is not started yet. --- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. --- * **Returning** ( Group ): The AI is returning to Base.. --- --- ### 1.2.2) AI_PATROL_ZONE Events --- --- * **Start** ( Group ): Start the process. --- * **Route** ( Group ): Route the AI to a new random 3D point within the Patrol Zone. --- * **RTB** ( Group ): Route the AI to the home base. --- * **Detect** ( Group ): The AI is detecting targets. --- * **Detected** ( Group ): The AI has detected new targets. --- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. --- --- ## 1.3) Set or Get the AI controllable --- --- * @{#AI_PATROL_ZONE.SetControllable}(): Set the AIControllable. --- * @{#AI_PATROL_ZONE.GetControllable}(): Get the AIControllable. --- --- ## 1.4) Set the Speed and Altitude boundaries of the AI controllable --- --- * @{#AI_PATROL_ZONE.SetSpeed}(): Set the patrol speed boundaries of the AI, for the next patrol. --- * @{#AI_PATROL_ZONE.SetAltitude}(): Set altitude boundaries of the AI, for the next patrol. --- --- ## 1.5) Manage the detection process of the AI controllable --- --- The detection process of the AI controllable can be manipulated. --- Detection requires an amount of CPU power, which has an impact on your mission performance. --- Only put detection on when absolutely necessary, and the frequency of the detection can also be set. --- --- * @{#AI_PATROL_ZONE.SetDetectionOn}(): Set the detection on. The AI will detect for targets. --- * @{#AI_PATROL_ZONE.SetDetectionOff}(): Set the detection off, the AI will not detect for targets. The existing target list will NOT be erased. --- --- The detection frequency can be set with @{#AI_PATROL_ZONE.SetDetectionInterval}( seconds ), where the amount of seconds specify how much seconds will be waited before the next detection. --- Use the method @{#AI_PATROL_ZONE.GetDetectedUnits}() to obtain a list of the @{Unit}s detected by the AI. --- --- The detection can be filtered to potential targets in a specific zone. --- Use the method @{#AI_PATROL_ZONE.SetDetectionZone}() to set the zone where targets need to be detected. --- Note that when the zone is too far away, or the AI is not heading towards the zone, or the AI is too high, no targets may be detected --- according the weather conditions. --- --- ## 1.6) Manage the "out of fuel" in the AI_PATROL_ZONE --- --- When the AI is out of fuel, it is required that a new AI is started, before the old AI can return to the home base. --- Therefore, with a parameter and a calculation of the distance to the home base, the fuel treshold is calculated. --- When the fuel treshold is reached, the AI will continue for a given time its patrol task in orbit, --- while a new AI is targetted to the AI_PATROL_ZONE. --- Once the time is finished, the old AI will return to the base. --- Use the method @{#AI_PATROL_ZONE.ManageFuel}() to have this proces in place. --- --- ## 1.7) Manage "damage" behaviour of the AI in the AI_PATROL_ZONE --- --- When the AI is damaged, it is required that a new AIControllable is started. However, damage cannon be foreseen early on. --- Therefore, when the damage treshold is reached, the AI will return immediately to the home base (RTB). --- Use the method @{#AI_PATROL_ZONE.ManageDamage}() to have this proces in place. --- --- ==== --- --- # **OPEN ISSUES** --- --- 2017-01-17: When Spawned AI is located at an airbase, it will be routed first back to the airbase after take-off. --- --- 2016-01-17: --- -- Fixed problem with AI returning to base too early and unexpected. --- -- ReSpawning of AI will reset the AI_PATROL and derived classes. --- -- Checked the correct workings of SCHEDULER, and it DOES work correctly. --- --- ==== --- --- # **API CHANGE HISTORY** --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- Hereby the change log: --- --- 2017-01-17: Rename of class: **AI\_PATROL\_ZONE** is the new name for the old _AI\_PATROLZONE_. --- --- 2017-01-15: Complete revision. AI_PATROL_ZONE is the base class for other AI_PATROL like classes. --- --- 2016-09-01: Initial class and API. --- --- === --- --- # **AUTHORS and CONTRIBUTIONS** --- --- ### Contributions: --- --- * **[Dutch_Baron](https://forums.eagle.ru/member.php?u=112075)**: Working together with James has resulted in the creation of the AI_BALANCER class. James has shared his ideas on balancing AI with air units, and together we made a first design which you can use now :-) --- * **[Pikey](https://forums.eagle.ru/member.php?u=62835)**: Testing and API concept review. --- --- ### Authors: --- --- * **FlightControl**: Design & Programming. --- --- @module AI_Patrol - ---- AI_PATROL_ZONE class --- @type AI_PATROL_ZONE --- @field Wrapper.Controllable#CONTROLLABLE AIControllable The @{Controllable} patrolling. --- @field Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. --- @field Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @field Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @field Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. --- @field Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h. --- @field Functional.Spawn#SPAWN CoordTest --- @extends Core.Fsm#FSM_CONTROLLABLE -AI_PATROL_ZONE = { - ClassName = "AI_PATROL_ZONE", -} - ---- Creates a new AI_PATROL_ZONE object --- @param #AI_PATROL_ZONE self --- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. --- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. --- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h. --- @param Dcs.DCSTypes#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO --- @return #AI_PATROL_ZONE self --- @usage --- -- Define a new AI_PATROL_ZONE Object. This PatrolArea will patrol an AIControllable within PatrolZone between 3000 and 6000 meters, with a variying speed between 600 and 900 km/h. --- PatrolZone = ZONE:New( 'PatrolZone' ) --- PatrolSpawn = SPAWN:New( 'Patrol Group' ) --- PatrolArea = AI_PATROL_ZONE:New( PatrolZone, 3000, 6000, 600, 900 ) -function AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) - - -- Inherits from BASE - local self = BASE:Inherit( self, FSM_CONTROLLABLE:New() ) -- #AI_PATROL_ZONE - - - self.PatrolZone = PatrolZone - self.PatrolFloorAltitude = PatrolFloorAltitude - self.PatrolCeilingAltitude = PatrolCeilingAltitude - self.PatrolMinSpeed = PatrolMinSpeed - self.PatrolMaxSpeed = PatrolMaxSpeed - - -- defafult PatrolAltType to "RADIO" if not specified - self.PatrolAltType = PatrolAltType or "RADIO" - - self:SetDetectionInterval( 30 ) - - self.CheckStatus = true - - self:ManageFuel( .2, 60 ) - self:ManageDamage( 1 ) - - - self.DetectedUnits = {} -- This table contains the targets detected during patrol. - - self:SetStartState( "None" ) - - self:AddTransition( "None", "Start", "Patrolling" ) - ---- OnBefore Transition Handler for Event Start. --- @function [parent=#AI_PATROL_ZONE] OnBeforeStart --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event Start. --- @function [parent=#AI_PATROL_ZONE] OnAfterStart --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event Start. --- @function [parent=#AI_PATROL_ZONE] Start --- @param #AI_PATROL_ZONE self - ---- Asynchronous Event Trigger for Event Start. --- @function [parent=#AI_PATROL_ZONE] __Start --- @param #AI_PATROL_ZONE self --- @param #number Delay The delay in seconds. - ---- OnLeave Transition Handler for State Patrolling. --- @function [parent=#AI_PATROL_ZONE] OnLeavePatrolling --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnEnter Transition Handler for State Patrolling. --- @function [parent=#AI_PATROL_ZONE] OnEnterPatrolling --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - - self:AddTransition( "Patrolling", "Route", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. - ---- OnBefore Transition Handler for Event Route. --- @function [parent=#AI_PATROL_ZONE] OnBeforeRoute --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event Route. --- @function [parent=#AI_PATROL_ZONE] OnAfterRoute --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event Route. --- @function [parent=#AI_PATROL_ZONE] Route --- @param #AI_PATROL_ZONE self - ---- Asynchronous Event Trigger for Event Route. --- @function [parent=#AI_PATROL_ZONE] __Route --- @param #AI_PATROL_ZONE self --- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "Status", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. - ---- OnBefore Transition Handler for Event Status. --- @function [parent=#AI_PATROL_ZONE] OnBeforeStatus --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event Status. --- @function [parent=#AI_PATROL_ZONE] OnAfterStatus --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event Status. --- @function [parent=#AI_PATROL_ZONE] Status --- @param #AI_PATROL_ZONE self - ---- Asynchronous Event Trigger for Event Status. --- @function [parent=#AI_PATROL_ZONE] __Status --- @param #AI_PATROL_ZONE self --- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "Detect", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. - ---- OnBefore Transition Handler for Event Detect. --- @function [parent=#AI_PATROL_ZONE] OnBeforeDetect --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event Detect. --- @function [parent=#AI_PATROL_ZONE] OnAfterDetect --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event Detect. --- @function [parent=#AI_PATROL_ZONE] Detect --- @param #AI_PATROL_ZONE self - ---- Asynchronous Event Trigger for Event Detect. --- @function [parent=#AI_PATROL_ZONE] __Detect --- @param #AI_PATROL_ZONE self --- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "Detected", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. - ---- OnBefore Transition Handler for Event Detected. --- @function [parent=#AI_PATROL_ZONE] OnBeforeDetected --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event Detected. --- @function [parent=#AI_PATROL_ZONE] OnAfterDetected --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event Detected. --- @function [parent=#AI_PATROL_ZONE] Detected --- @param #AI_PATROL_ZONE self - ---- Asynchronous Event Trigger for Event Detected. --- @function [parent=#AI_PATROL_ZONE] __Detected --- @param #AI_PATROL_ZONE self --- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "RTB", "Returning" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. - ---- OnBefore Transition Handler for Event RTB. --- @function [parent=#AI_PATROL_ZONE] OnBeforeRTB --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event RTB. --- @function [parent=#AI_PATROL_ZONE] OnAfterRTB --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event RTB. --- @function [parent=#AI_PATROL_ZONE] RTB --- @param #AI_PATROL_ZONE self - ---- Asynchronous Event Trigger for Event RTB. --- @function [parent=#AI_PATROL_ZONE] __RTB --- @param #AI_PATROL_ZONE self --- @param #number Delay The delay in seconds. - ---- OnLeave Transition Handler for State Returning. --- @function [parent=#AI_PATROL_ZONE] OnLeaveReturning --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnEnter Transition Handler for State Returning. --- @function [parent=#AI_PATROL_ZONE] OnEnterReturning --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - - self:AddTransition( "*", "Reset", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. - - self:AddTransition( "*", "Eject", "*" ) - self:AddTransition( "*", "Crash", "Crashed" ) - self:AddTransition( "*", "PilotDead", "*" ) - - return self -end - - - - ---- Sets (modifies) the minimum and maximum speed of the patrol. --- @param #AI_PATROL_ZONE self --- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. --- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h. --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetSpeed( PatrolMinSpeed, PatrolMaxSpeed ) - self:F2( { PatrolMinSpeed, PatrolMaxSpeed } ) - - self.PatrolMinSpeed = PatrolMinSpeed - self.PatrolMaxSpeed = PatrolMaxSpeed -end - - - ---- Sets the floor and ceiling altitude of the patrol. --- @param #AI_PATROL_ZONE self --- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetAltitude( PatrolFloorAltitude, PatrolCeilingAltitude ) - self:F2( { PatrolFloorAltitude, PatrolCeilingAltitude } ) - - self.PatrolFloorAltitude = PatrolFloorAltitude - self.PatrolCeilingAltitude = PatrolCeilingAltitude -end - --- * @{#AI_PATROL_ZONE.SetDetectionOn}(): Set the detection on. The AI will detect for targets. --- * @{#AI_PATROL_ZONE.SetDetectionOff}(): Set the detection off, the AI will not detect for targets. The existing target list will NOT be erased. - ---- Set the detection on. The AI will detect for targets. --- @param #AI_PATROL_ZONE self --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetDetectionOn() - self:F2() - - self.DetectOn = true -end - ---- Set the detection off. The AI will NOT detect for targets. --- However, the list of already detected targets will be kept and can be enquired! --- @param #AI_PATROL_ZONE self --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetDetectionOff() - self:F2() - - self.DetectOn = false -end - ---- Set the status checking off. --- @param #AI_PATROL_ZONE self --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetStatusOff() - self:F2() - - self.CheckStatus = false -end - ---- Activate the detection. The AI will detect for targets if the Detection is switched On. --- @param #AI_PATROL_ZONE self --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetDetectionActivated() - self:F2() - - self:ClearDetectedUnits() - self.DetectActivated = true - self:__Detect( -self.DetectInterval ) -end - ---- Deactivate the detection. The AI will NOT detect for targets. --- @param #AI_PATROL_ZONE self --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetDetectionDeactivated() - self:F2() - - self:ClearDetectedUnits() - self.DetectActivated = false -end - ---- Set the interval in seconds between each detection executed by the AI. --- The list of already detected targets will be kept and updated. --- Newly detected targets will be added, but already detected targets that were --- not detected in this cycle, will NOT be removed! --- The default interval is 30 seconds. --- @param #AI_PATROL_ZONE self --- @param #number Seconds The interval in seconds. --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetDetectionInterval( Seconds ) - self:F2() - - if Seconds then - self.DetectInterval = Seconds - else - self.DetectInterval = 30 - end -end - ---- Set the detection zone where the AI is detecting targets. --- @param #AI_PATROL_ZONE self --- @param Core.Zone#ZONE DetectionZone The zone where to detect targets. --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetDetectionZone( DetectionZone ) - self:F2() - - if DetectionZone then - self.DetectZone = DetectionZone - else - self.DetectZone = nil - end -end - ---- Gets a list of @{Unit#UNIT}s that were detected by the AI. --- No filtering is applied, so, ANY detected UNIT can be in this list. --- It is up to the mission designer to use the @{Unit} class and methods to filter the targets. --- @param #AI_PATROL_ZONE self --- @return #table The list of @{Unit#UNIT}s -function AI_PATROL_ZONE:GetDetectedUnits() - self:F2() - - return self.DetectedUnits -end - ---- Clears the list of @{Unit#UNIT}s that were detected by the AI. --- @param #AI_PATROL_ZONE self -function AI_PATROL_ZONE:ClearDetectedUnits() - self:F2() - self.DetectedUnits = {} -end - ---- When the AI is out of fuel, it is required that a new AI is started, before the old AI can return to the home base. --- Therefore, with a parameter and a calculation of the distance to the home base, the fuel treshold is calculated. --- When the fuel treshold is reached, the AI will continue for a given time its patrol task in orbit, while a new AIControllable is targetted to the AI_PATROL_ZONE. --- Once the time is finished, the old AI will return to the base. --- @param #AI_PATROL_ZONE self --- @param #number PatrolFuelTresholdPercentage The treshold in percentage (between 0 and 1) when the AIControllable is considered to get out of fuel. --- @param #number PatrolOutOfFuelOrbitTime The amount of seconds the out of fuel AIControllable will orbit before returning to the base. --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:ManageFuel( PatrolFuelTresholdPercentage, PatrolOutOfFuelOrbitTime ) - - self.PatrolManageFuel = true - self.PatrolFuelTresholdPercentage = PatrolFuelTresholdPercentage - self.PatrolOutOfFuelOrbitTime = PatrolOutOfFuelOrbitTime - - return self -end - ---- When the AI is damaged beyond a certain treshold, it is required that the AI returns to the home base. --- However, damage cannot be foreseen early on. --- Therefore, when the damage treshold is reached, --- the AI will return immediately to the home base (RTB). --- Note that for groups, the average damage of the complete group will be calculated. --- So, in a group of 4 airplanes, 2 lost and 2 with damage 0.2, the damage treshold will be 0.25. --- @param #AI_PATROL_ZONE self --- @param #number PatrolDamageTreshold The treshold in percentage (between 0 and 1) when the AI is considered to be damaged. --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:ManageDamage( PatrolDamageTreshold ) - - self.PatrolManageDamage = true - self.PatrolDamageTreshold = PatrolDamageTreshold - - return self -end - ---- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. --- @param #AI_PATROL_ZONE self --- @return #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_PATROL_ZONE:onafterStart( Controllable, From, Event, To ) - self:F2() - - self:__Route( 1 ) -- Route to the patrol point. The asynchronous trigger is important, because a spawned group and units takes at least one second to come live. - self:__Status( 60 ) -- Check status status every 30 seconds. - self:SetDetectionActivated() - - self:HandleEvent( EVENTS.PilotDead, self.OnPilotDead ) - self:HandleEvent( EVENTS.Crash, self.OnCrash ) - self:HandleEvent( EVENTS.Ejection, self.OnEjection ) - - Controllable:OptionROEHoldFire() - Controllable:OptionROTVertical() - - self.Controllable:OnReSpawn( - function( PatrolGroup ) - self:E( "ReSpawn" ) - self:__Reset( 1 ) - self:__Route( 5 ) - end - ) - - self:SetDetectionOn() - -end - - ---- @param #AI_PATROL_ZONE self ---- @param Wrapper.Controllable#CONTROLLABLE Controllable -function AI_PATROL_ZONE:onbeforeDetect( Controllable, From, Event, To ) - - return self.DetectOn and self.DetectActivated -end - ---- @param #AI_PATROL_ZONE self ---- @param Wrapper.Controllable#CONTROLLABLE Controllable -function AI_PATROL_ZONE:onafterDetect( Controllable, From, Event, To ) - - local Detected = false - - local DetectedTargets = Controllable:GetDetectedTargets() - for TargetID, Target in pairs( DetectedTargets or {} ) do - local TargetObject = Target.object - - if TargetObject and TargetObject:isExist() and TargetObject.id_ < 50000000 then - - local TargetUnit = UNIT:Find( TargetObject ) - local TargetUnitName = TargetUnit:GetName() - - if self.DetectionZone then - if TargetUnit:IsInZone( self.DetectionZone ) then - self:T( {"Detected ", TargetUnit } ) - if self.DetectedUnits[TargetUnit] == nil then - self.DetectedUnits[TargetUnit] = true - end - Detected = true - end - else - if self.DetectedUnits[TargetUnit] == nil then - self.DetectedUnits[TargetUnit] = true - end - Detected = true - end - end - end - - self:__Detect( -self.DetectInterval ) - - if Detected == true then - self:__Detected( 1.5 ) - end - -end - ---- @param Wrapper.Controllable#CONTROLLABLE AIControllable --- This statis method is called from the route path within the last task at the last waaypoint of the Controllable. --- Note that this method is required, as triggers the next route when patrolling for the Controllable. -function AI_PATROL_ZONE:_NewPatrolRoute( AIControllable ) - - local PatrolZone = AIControllable:GetState( AIControllable, "PatrolZone" ) -- PatrolCore.Zone#AI_PATROL_ZONE - PatrolZone:__Route( 1 ) -end - - ---- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_PATROL_ZONE:onafterRoute( Controllable, From, Event, To ) - - self:F2() - - -- When RTB, don't allow anymore the routing. - if From == "RTB" then - return - end - - - if self.Controllable:IsAlive() then - -- Determine if the AIControllable is within the PatrolZone. - -- If not, make a waypoint within the to that the AIControllable will fly at maximum speed to that point. - - local PatrolRoute = {} - - -- Calculate the current route point of the controllable as the start point of the route. - -- However, when the controllable is not in the air, - -- the controllable current waypoint is probably the airbase... - -- Thus, if we would take the current waypoint as the startpoint, upon take-off, the controllable flies - -- immediately back to the airbase, and this is not correct. - -- Therefore, when on a runway, get as the current route point a random point within the PatrolZone. - -- This will make the plane fly immediately to the patrol zone. - - if self.Controllable:InAir() == false then - self:E( "Not in the air, finding route path within PatrolZone" ) - local CurrentVec2 = self.Controllable:GetVec2() - --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). - local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() - local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) - local ToPatrolZoneSpeed = self.PatrolMaxSpeed - local CurrentRoutePoint = CurrentPointVec3:RoutePointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TakeOffParking, - POINT_VEC3.RoutePointAction.FromParkingArea, - ToPatrolZoneSpeed, - true - ) - PatrolRoute[#PatrolRoute+1] = CurrentRoutePoint - else - self:E( "In the air, finding route path within PatrolZone" ) - local CurrentVec2 = self.Controllable:GetVec2() - --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). - local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() - local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) - local ToPatrolZoneSpeed = self.PatrolMaxSpeed - local CurrentRoutePoint = CurrentPointVec3:RoutePointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToPatrolZoneSpeed, - true - ) - PatrolRoute[#PatrolRoute+1] = CurrentRoutePoint - end - - - --- Define a random point in the @{Zone}. The AI will fly to that point within the zone. - - --- Find a random 2D point in PatrolZone. - local ToTargetVec2 = self.PatrolZone:GetRandomVec2() - self:T2( ToTargetVec2 ) - - --- Define Speed and Altitude. - local ToTargetAltitude = math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ) - local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) - self:T2( { self.PatrolMinSpeed, self.PatrolMaxSpeed, ToTargetSpeed } ) - - --- Obtain a 3D @{Point} from the 2D point + altitude. - local ToTargetPointVec3 = POINT_VEC3:New( ToTargetVec2.x, ToTargetAltitude, ToTargetVec2.y ) - - --- Create a route point of type air. - local ToTargetRoutePoint = ToTargetPointVec3:RoutePointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, - true - ) - - --self.CoordTest:SpawnFromVec3( ToTargetPointVec3:GetVec3() ) - - --ToTargetPointVec3:SmokeRed() - - PatrolRoute[#PatrolRoute+1] = ToTargetRoutePoint - - --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... - self.Controllable:WayPointInitialize( PatrolRoute ) - - --- Do a trick, link the NewPatrolRoute function of the PATROLGROUP object to the AIControllable in a temporary variable ... - self.Controllable:SetState( self.Controllable, "PatrolZone", self ) - self.Controllable:WayPointFunction( #PatrolRoute, 1, "AI_PATROL_ZONE:_NewPatrolRoute" ) - - --- NOW ROUTE THE GROUP! - self.Controllable:WayPointExecute( 1, 2 ) - end - -end - ---- @param #AI_PATROL_ZONE self -function AI_PATROL_ZONE:onbeforeStatus() - - return self.CheckStatus -end - ---- @param #AI_PATROL_ZONE self -function AI_PATROL_ZONE:onafterStatus() - self:F2() - - if self.Controllable and self.Controllable:IsAlive() then - - local RTB = false - - local Fuel = self.Controllable:GetUnit(1):GetFuel() - if Fuel < self.PatrolFuelTresholdPercentage then - self:E( self.Controllable:GetName() .. " is out of fuel:" .. Fuel .. ", RTB!" ) - local OldAIControllable = self.Controllable - local AIControllableTemplate = self.Controllable:GetTemplate() - - local OrbitTask = OldAIControllable:TaskOrbitCircle( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ), self.PatrolMinSpeed ) - local TimedOrbitTask = OldAIControllable:TaskControlled( OrbitTask, OldAIControllable:TaskCondition(nil,nil,nil,nil,self.PatrolOutOfFuelOrbitTime,nil ) ) - OldAIControllable:SetTask( TimedOrbitTask, 10 ) - - RTB = true - else - end - - -- TODO: Check GROUP damage function. - local Damage = self.Controllable:GetLife() - if Damage <= self.PatrolDamageTreshold then - self:E( self.Controllable:GetName() .. " is damaged:" .. Damage .. ", RTB!" ) - RTB = true - end - - if RTB == true then - self:RTB() - else - self:__Status( 60 ) -- Execute the Patrol event after 30 seconds. - end - end -end - ---- @param #AI_PATROL_ZONE self -function AI_PATROL_ZONE:onafterRTB() - self:F2() - - if self.Controllable and self.Controllable:IsAlive() then - - self:SetDetectionOff() - self.CheckStatus = false - - local PatrolRoute = {} - - --- Calculate the current route point. - local CurrentVec2 = self.Controllable:GetVec2() - - --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). - local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() - local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) - local ToPatrolZoneSpeed = self.PatrolMaxSpeed - local CurrentRoutePoint = CurrentPointVec3:RoutePointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToPatrolZoneSpeed, - true - ) - - PatrolRoute[#PatrolRoute+1] = CurrentRoutePoint - - --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... - self.Controllable:WayPointInitialize( PatrolRoute ) - - --- NOW ROUTE THE GROUP! - self.Controllable:WayPointExecute( 1, 1 ) - - end - -end - ---- @param #AI_PATROL_ZONE self -function AI_PATROL_ZONE:onafterDead() - self:SetDetectionOff() - self:SetStatusOff() -end - ---- @param #AI_PATROL_ZONE self --- @param Core.Event#EVENTDATA EventData -function AI_PATROL_ZONE:OnCrash( EventData ) - - if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then - self:E( self.Controllable:GetUnits() ) - if #self.Controllable:GetUnits() == 1 then - self:__Crash( 1, EventData ) - end - end -end - ---- @param #AI_PATROL_ZONE self --- @param Core.Event#EVENTDATA EventData -function AI_PATROL_ZONE:OnEjection( EventData ) - - if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then - self:__Eject( 1, EventData ) - end -end - ---- @param #AI_PATROL_ZONE self --- @param Core.Event#EVENTDATA EventData -function AI_PATROL_ZONE:OnPilotDead( EventData ) - - if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then - self:__PilotDead( 1, EventData ) - end -end ---- Single-Player:**Yes** / Multi-Player:**Yes** / AI:**Yes** / Human:**No** / Types:**Air** -- --- **Provide Close Air Support to friendly ground troops.** --- --- ![Banner Image](..\Presentations\AI_CAS\Dia1.JPG) --- --- === --- --- # 1) @{#AI_CAS_ZONE} class, extends @{AI_Patrol#AI_PATROL_ZONE} --- --- @{#AI_CAS_ZONE} derives from the @{AI_Patrol#AI_PATROL_ZONE}, inheriting its methods and behaviour. --- --- The @{#AI_CAS_ZONE} class implements the core functions to provide Close Air Support in an Engage @{Zone} by an AIR @{Controllable} or @{Group}. --- The AI_CAS_ZONE runs a process. It holds an AI in a Patrol Zone and when the AI is commanded to engage, it will fly to an Engage Zone. --- --- ![HoldAndEngage](..\Presentations\AI_CAS\Dia3.JPG) --- --- The AI_CAS_ZONE is assigned a @{Group} and this must be done before the AI_CAS_ZONE process can be started through the **Start** event. --- --- ![Start Event](..\Presentations\AI_CAS\Dia4.JPG) --- --- Upon started, The AI will **Route** itself towards the random 3D point within a patrol zone, --- using a random speed within the given altitude and speed limits. --- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. --- This cycle will continue until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. --- --- ![Route Event](..\Presentations\AI_CAS\Dia5.JPG) --- --- When the AI is commanded to provide Close Air Support (through the event **Engage**), the AI will fly towards the Engage Zone. --- Any target that is detected in the Engage Zone will be reported and will be destroyed by the AI. --- --- ![Engage Event](..\Presentations\AI_CAS\Dia6.JPG) --- --- The AI will detect the targets and will only destroy the targets within the Engage Zone. --- --- ![Engage Event](..\Presentations\AI_CAS\Dia7.JPG) --- --- Every target that is destroyed, is reported< by the AI. --- --- ![Engage Event](..\Presentations\AI_CAS\Dia8.JPG) --- --- Note that the AI does not know when the Engage Zone is cleared, and therefore will keep circling in the zone. --- --- ![Engage Event](..\Presentations\AI_CAS\Dia9.JPG) --- --- Until it is notified through the event **Accomplish**, which is to be triggered by an observing party: --- --- * a FAC --- * a timed event --- * a menu option selected by a human --- * a condition --- * others ... --- --- ![Engage Event](..\Presentations\AI_CAS\Dia10.JPG) --- --- When the AI has accomplished the CAS, it will fly back to the Patrol Zone. --- --- ![Engage Event](..\Presentations\AI_CAS\Dia11.JPG) --- --- It will keep patrolling there, until it is notified to RTB or move to another CAS Zone. --- It can be notified to go RTB through the **RTB** event. --- --- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. --- --- ![Engage Event](..\Presentations\AI_CAS\Dia12.JPG) --- --- # 1.1) AI_CAS_ZONE constructor --- --- * @{#AI_CAS_ZONE.New}(): Creates a new AI_CAS_ZONE object. --- --- ## 1.2) AI_CAS_ZONE is a FSM --- --- ![Process](..\Presentations\AI_CAS\Dia2.JPG) --- --- ### 1.2.1) AI_CAS_ZONE States --- --- * **None** ( Group ): The process is not started yet. --- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. --- * **Engaging** ( Group ): The AI is engaging the targets in the Engage Zone, executing CAS. --- * **Returning** ( Group ): The AI is returning to Base.. --- --- ### 1.2.2) AI_CAS_ZONE Events --- --- * **Start** ( Group ): Start the process. --- * **Route** ( Group ): Route the AI to a new random 3D point within the Patrol Zone. --- * **Engage** ( Group ): Engage the AI to provide CAS in the Engage Zone, destroying any target it finds. --- * **RTB** ( Group ): Route the AI to the home base. --- * **Detect** ( Group ): The AI is detecting targets. --- * **Detected** ( Group ): The AI has detected new targets. --- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. --- --- ==== --- --- # **API CHANGE HISTORY** --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- Hereby the change log: --- --- 2017-01-15: Initial class and API. --- --- === --- --- # **AUTHORS and CONTRIBUTIONS** --- --- ### Contributions: --- --- * **[Quax](https://forums.eagle.ru/member.php?u=90530)**: Concept, Advice & Testing. --- * **[Pikey](https://forums.eagle.ru/member.php?u=62835)**: Concept, Advice & Testing. --- * **[Gunterlund](http://forums.eagle.ru:8080/member.php?u=75036)**: Test case revision. --- --- ### Authors: --- --- * **FlightControl**: Concept, Design & Programming. --- --- @module AI_Cas - - ---- AI_CAS_ZONE class --- @type AI_CAS_ZONE --- @field Wrapper.Controllable#CONTROLLABLE AIControllable The @{Controllable} patrolling. --- @field Core.Zone#ZONE_BASE TargetZone The @{Zone} where the patrol needs to be executed. --- @extends AI.AI_Patrol#AI_PATROL_ZONE -AI_CAS_ZONE = { - ClassName = "AI_CAS_ZONE", -} - - - ---- Creates a new AI_CAS_ZONE object --- @param #AI_CAS_ZONE self --- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. --- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. --- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h. --- @param Core.Zone#ZONE_BASE EngageZone The zone where the engage will happen. --- @param Dcs.DCSTypes#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO --- @return #AI_CAS_ZONE self -function AI_CAS_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageZone, PatrolAltType ) - - -- Inherits from BASE - local self = BASE:Inherit( self, AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_CAS_ZONE - - self.EngageZone = EngageZone - self.Accomplished = false - - self:SetDetectionZone( self.EngageZone ) - - self:AddTransition( { "Patrolling", "Engaging" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. - - --- OnBefore Transition Handler for Event Engage. - -- @function [parent=#AI_CAS_ZONE] OnBeforeEngage - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone. - -- @param Dcs.DCSTypes#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. - -- @param Dcs.DCSTypes#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. - -- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. - -- @param Dcs.DCSTypes#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. - - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Engage. - -- @function [parent=#AI_CAS_ZONE] OnAfterEngage - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone. - -- @param Dcs.DCSTypes#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. - -- @param Dcs.DCSTypes#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. - -- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. - -- @param Dcs.DCSTypes#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. - - --- Synchronous Event Trigger for Event Engage. - -- @function [parent=#AI_CAS_ZONE] Engage - -- @param #AI_CAS_ZONE self - - --- Asynchronous Event Trigger for Event Engage. - -- @function [parent=#AI_CAS_ZONE] __Engage - -- @param #AI_CAS_ZONE self - -- @param #number Delay The delay in seconds. - ---- OnLeave Transition Handler for State Engaging. --- @function [parent=#AI_CAS_ZONE] OnLeaveEngaging --- @param #AI_CAS_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnEnter Transition Handler for State Engaging. --- @function [parent=#AI_CAS_ZONE] OnEnterEngaging --- @param #AI_CAS_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - - self:AddTransition( "Engaging", "Target", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. - - self:AddTransition( "Engaging", "Fired", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. - - --- OnBefore Transition Handler for Event Fired. - -- @function [parent=#AI_CAS_ZONE] OnBeforeFired - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Fired. - -- @function [parent=#AI_CAS_ZONE] OnAfterFired - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Fired. - -- @function [parent=#AI_CAS_ZONE] Fired - -- @param #AI_CAS_ZONE self - - --- Asynchronous Event Trigger for Event Fired. - -- @function [parent=#AI_CAS_ZONE] __Fired - -- @param #AI_CAS_ZONE self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. - - --- OnBefore Transition Handler for Event Destroy. - -- @function [parent=#AI_CAS_ZONE] OnBeforeDestroy - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Destroy. - -- @function [parent=#AI_CAS_ZONE] OnAfterDestroy - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Destroy. - -- @function [parent=#AI_CAS_ZONE] Destroy - -- @param #AI_CAS_ZONE self - - --- Asynchronous Event Trigger for Event Destroy. - -- @function [parent=#AI_CAS_ZONE] __Destroy - -- @param #AI_CAS_ZONE self - -- @param #number Delay The delay in seconds. - - - self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. - - --- OnBefore Transition Handler for Event Abort. - -- @function [parent=#AI_CAS_ZONE] OnBeforeAbort - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Abort. - -- @function [parent=#AI_CAS_ZONE] OnAfterAbort - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Abort. - -- @function [parent=#AI_CAS_ZONE] Abort - -- @param #AI_CAS_ZONE self - - --- Asynchronous Event Trigger for Event Abort. - -- @function [parent=#AI_CAS_ZONE] __Abort - -- @param #AI_CAS_ZONE self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. - - --- OnBefore Transition Handler for Event Accomplish. - -- @function [parent=#AI_CAS_ZONE] OnBeforeAccomplish - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Accomplish. - -- @function [parent=#AI_CAS_ZONE] OnAfterAccomplish - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Accomplish. - -- @function [parent=#AI_CAS_ZONE] Accomplish - -- @param #AI_CAS_ZONE self - - --- Asynchronous Event Trigger for Event Accomplish. - -- @function [parent=#AI_CAS_ZONE] __Accomplish - -- @param #AI_CAS_ZONE self - -- @param #number Delay The delay in seconds. - - return self -end - - ---- Set the Engage Zone where the AI is performing CAS. Note that if the EngageZone is changed, the AI needs to re-detect targets. --- @param #AI_CAS_ZONE self --- @param Core.Zone#ZONE EngageZone The zone where the AI is performing CAS. --- @return #AI_CAS_ZONE self -function AI_CAS_ZONE:SetEngageZone( EngageZone ) - self:F2() - - if EngageZone then - self.EngageZone = EngageZone - else - self.EngageZone = nil - end -end - - - ---- onafter State Transition for Event Start. --- @param #AI_CAS_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAS_ZONE:onafterStart( Controllable, From, Event, To ) - - -- Call the parent Start event handler - self:GetParent(self).onafterStart( self, Controllable, From, Event, To ) - self:HandleEvent( EVENTS.Dead, self.OnDead ) - - self:SetDetectionDeactivated() -- When not engaging, set the detection off. -end - ---- @param Wrapper.Controllable#CONTROLLABLE AIControllable -function _NewEngageRoute( AIControllable ) - - AIControllable:T( "NewEngageRoute" ) - local EngageZone = AIControllable:GetState( AIControllable, "EngageZone" ) -- AI.AI_Cas#AI_CAS_ZONE - EngageZone:__Engage( 1 ) -end - ---- @param #AI_CAS_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAS_ZONE:onbeforeEngage( Controllable, From, Event, To ) - - if self.Accomplished == true then - return false - end -end - ---- @param #AI_CAS_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAS_ZONE:onafterTarget( Controllable, From, Event, To ) - self:E("onafterTarget") - - if Controllable:IsAlive() then - - local AttackTasks = {} - - for DetectedUnit, Detected in pairs( self.DetectedUnits ) do - local DetectedUnit = DetectedUnit -- Wrapper.Unit#UNIT - if DetectedUnit:IsAlive() then - if DetectedUnit:IsInZone( self.EngageZone ) then - if Detected == true then - self:E( {"Target: ", DetectedUnit } ) - self.DetectedUnits[DetectedUnit] = false - local AttackTask = Controllable:EnRouteTaskEngageUnit( DetectedUnit, 1, true, self.EngageWeaponExpend, self.EngageAttackQty, self.EngageDirection, self.EngageAltitude, nil ) - self.Controllable:PushTask( AttackTask, 1 ) - end - end - else - self.DetectedUnits[DetectedUnit] = nil - end - end - - self:__Target( -10 ) - - end -end - - - ---- @param #AI_CAS_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone. --- @param Dcs.DCSTypes#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. --- @param Dcs.DCSTypes#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. --- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. --- @param Dcs.DCSTypes#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. -function AI_CAS_ZONE:onafterEngage( Controllable, From, Event, To, EngageSpeed, EngageAltitude, EngageWeaponExpend, EngageAttackQty, EngageDirection ) - self:E("onafterEngage") - - self.EngageSpeed = EngageSpeed or 400 - self.EngageAltitude = EngageAltitude or 2000 - self.EngageWeaponExpend = EngageWeaponExpend - self.EngageAttackQty = EngageAttackQty - self.EngageDirection = EngageDirection - - if Controllable:IsAlive() then - - - local EngageRoute = {} - - --- Calculate the current route point. - local CurrentVec2 = self.Controllable:GetVec2() - - --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). - local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() - local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) - local ToEngageZoneSpeed = self.PatrolMaxSpeed - local CurrentRoutePoint = CurrentPointVec3:RoutePointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - self.EngageSpeed, - true - ) - - EngageRoute[#EngageRoute+1] = CurrentRoutePoint - - - if self.Controllable:IsNotInZone( self.EngageZone ) then - - -- Find a random 2D point in EngageZone. - local ToEngageZoneVec2 = self.EngageZone:GetRandomVec2() - self:T2( ToEngageZoneVec2 ) - - -- Obtain a 3D @{Point} from the 2D point + altitude. - local ToEngageZonePointVec3 = POINT_VEC3:New( ToEngageZoneVec2.x, self.EngageAltitude, ToEngageZoneVec2.y ) - - -- Create a route point of type air. - local ToEngageZoneRoutePoint = ToEngageZonePointVec3:RoutePointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - self.EngageSpeed, - true - ) - - EngageRoute[#EngageRoute+1] = ToEngageZoneRoutePoint - - end - - --- Define a random point in the @{Zone}. The AI will fly to that point within the zone. - - --- Find a random 2D point in EngageZone. - local ToTargetVec2 = self.EngageZone:GetRandomVec2() - self:T2( ToTargetVec2 ) - - --- Obtain a 3D @{Point} from the 2D point + altitude. - local ToTargetPointVec3 = POINT_VEC3:New( ToTargetVec2.x, self.EngageAltitude, ToTargetVec2.y ) - - --- Create a route point of type air. - local ToTargetRoutePoint = ToTargetPointVec3:RoutePointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - self.EngageSpeed, - true - ) - - --ToTargetPointVec3:SmokeBlue() - - EngageRoute[#EngageRoute+1] = ToTargetRoutePoint - - - Controllable:OptionROEOpenFire() - Controllable:OptionROTVertical() - --- local AttackTasks = {} --- --- for DetectedUnitID, DetectedUnit in pairs( self.DetectedUnits ) do --- local DetectedUnit = DetectedUnit -- Wrapper.Unit#UNIT --- self:T( DetectedUnit ) --- if DetectedUnit:IsAlive() then --- if DetectedUnit:IsInZone( self.EngageZone ) then --- self:E( {"Engaging ", DetectedUnit } ) --- AttackTasks[#AttackTasks+1] = Controllable:TaskAttackUnit( DetectedUnit ) --- end --- else --- self.DetectedUnits[DetectedUnit] = nil --- end --- end --- --- EngageRoute[1].task = Controllable:TaskCombo( AttackTasks ) - - --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... - self.Controllable:WayPointInitialize( EngageRoute ) - - --- Do a trick, link the NewEngageRoute function of the object to the AIControllable in a temporary variable ... - self.Controllable:SetState( self.Controllable, "EngageZone", self ) - - self.Controllable:WayPointFunction( #EngageRoute, 1, "_NewEngageRoute" ) - - --- NOW ROUTE THE GROUP! - self.Controllable:WayPointExecute( 1 ) - - self:SetDetectionInterval( 10 ) - self:SetDetectionActivated() - self:__Target( -10 ) -- Start Targetting - end -end - ---- @param #AI_CAS_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @param Core.Event#EVENTDATA EventData -function AI_CAS_ZONE:onafterDestroy( Controllable, From, Event, To, EventData ) - - if EventData.IniUnit then - self.DetectedUnits[EventData.IniUnit] = nil - end - - Controllable:MessageToAll( "Destroyed a target", 15 , "Destroyed!" ) -end - ---- @param #AI_CAS_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAS_ZONE:onafterAccomplish( Controllable, From, Event, To ) - self.Accomplished = true - self:SetDetectionDeactivated() -end - ---- @param #AI_CAS_ZONE self --- @param Core.Event#EVENTDATA EventData -function AI_CAS_ZONE:OnDead( EventData ) - self:T( { "EventDead", EventData } ) - - if EventData.IniDCSUnit then - self:__Destroy( 1, EventData ) - end -end - - ---- Single-Player:**Yes** / Multi-Player:**Yes** / AI:**Yes** / Human:**No** / Types:**Air** -- **Execute Combat Air Patrol (CAP).** --- --- ![Banner Image](..\Presentations\AI_CAP\Dia1.JPG) --- --- === --- --- # 1) @{#AI_CAP_ZONE} class, extends @{AI_CAP#AI_PATROL_ZONE} --- --- The @{#AI_CAP_ZONE} class implements the core functions to patrol a @{Zone} by an AI @{Controllable} or @{Group} --- and automatically engage any airborne enemies that are within a certain range or within a certain zone. --- --- ![Process](..\Presentations\AI_CAP\Dia3.JPG) --- --- The AI_CAP_ZONE is assigned a @{Group} and this must be done before the AI_CAP_ZONE process can be started using the **Start** event. --- --- ![Process](..\Presentations\AI_CAP\Dia4.JPG) --- --- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits. --- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. --- --- ![Process](..\Presentations\AI_CAP\Dia5.JPG) --- --- This cycle will continue. --- --- ![Process](..\Presentations\AI_CAP\Dia6.JPG) --- --- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event. --- --- ![Process](..\Presentations\AI_CAP\Dia9.JPG) --- --- When enemies are detected, the AI will automatically engage the enemy. --- --- ![Process](..\Presentations\AI_CAP\Dia10.JPG) --- --- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. --- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. --- --- ![Process](..\Presentations\AI_CAP\Dia13.JPG) --- --- ## 1.1) AI_CAP_ZONE constructor --- --- * @{#AI_CAP_ZONE.New}(): Creates a new AI_CAP_ZONE object. --- --- ## 1.2) AI_CAP_ZONE is a FSM --- --- ![Process](..\Presentations\AI_CAP\Dia2.JPG) --- --- ### 1.2.1) AI_CAP_ZONE States --- --- * **None** ( Group ): The process is not started yet. --- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. --- * **Engaging** ( Group ): The AI is engaging the bogeys. --- * **Returning** ( Group ): The AI is returning to Base.. --- --- ### 1.2.2) AI_CAP_ZONE Events --- --- * **Start** ( Group ): Start the process. --- * **Route** ( Group ): Route the AI to a new random 3D point within the Patrol Zone. --- * **Engage** ( Group ): Let the AI engage the bogeys. --- * **RTB** ( Group ): Route the AI to the home base. --- * **Detect** ( Group ): The AI is detecting targets. --- * **Detected** ( Group ): The AI has detected new targets. --- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. --- --- ## 1.3) Set the Range of Engagement --- --- ![Range](..\Presentations\AI_CAP\Dia11.JPG) --- --- An optional range can be set in meters, --- that will define when the AI will engage with the detected airborne enemy targets. --- The range can be beyond or smaller than the range of the Patrol Zone. --- The range is applied at the position of the AI. --- Use the method @{AI_CAP#AI_CAP_ZONE.SetEngageRange}() to define that range. --- --- ## 1.4) Set the Zone of Engagement --- --- ![Zone](..\Presentations\AI_CAP\Dia12.JPG) --- --- An optional @{Zone} can be set, --- that will define when the AI will engage with the detected airborne enemy targets. --- Use the method @{AI_Cap#AI_CAP_ZONE.SetEngageZone}() to define that Zone. --- --- ==== --- --- # **API CHANGE HISTORY** --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- Hereby the change log: --- --- 2017-01-15: Initial class and API. --- --- === --- --- # **AUTHORS and CONTRIBUTIONS** --- --- ### Contributions: --- --- * **[Quax](https://forums.eagle.ru/member.php?u=90530)**: Concept, Advice & Testing. --- * **[Pikey](https://forums.eagle.ru/member.php?u=62835)**: Concept, Advice & Testing. --- * **[Gunterlund](http://forums.eagle.ru:8080/member.php?u=75036)**: Test case revision. --- * **[Whisper](http://forums.eagle.ru/member.php?u=3829): Testing. --- * **[Delta99](https://forums.eagle.ru/member.php?u=125166): Testing. --- --- ### Authors: --- --- * **FlightControl**: Concept, Design & Programming. --- --- @module AI_Cap - - ---- AI_CAP_ZONE class --- @type AI_CAP_ZONE --- @field Wrapper.Controllable#CONTROLLABLE AIControllable The @{Controllable} patrolling. --- @field Core.Zone#ZONE_BASE TargetZone The @{Zone} where the patrol needs to be executed. --- @extends AI.AI_Patrol#AI_PATROL_ZONE -AI_CAP_ZONE = { - ClassName = "AI_CAP_ZONE", -} - - - ---- Creates a new AI_CAP_ZONE object --- @param #AI_CAP_ZONE self --- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. --- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. --- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h. --- @param Dcs.DCSTypes#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO --- @return #AI_CAP_ZONE self -function AI_CAP_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) - - -- Inherits from BASE - local self = BASE:Inherit( self, AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_CAP_ZONE - - self.Accomplished = false - self.Engaging = false - - self:AddTransition( { "Patrolling", "Engaging" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAP_ZONE. - - --- OnBefore Transition Handler for Event Engage. - -- @function [parent=#AI_CAP_ZONE] OnBeforeEngage - -- @param #AI_CAP_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Engage. - -- @function [parent=#AI_CAP_ZONE] OnAfterEngage - -- @param #AI_CAP_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Engage. - -- @function [parent=#AI_CAP_ZONE] Engage - -- @param #AI_CAP_ZONE self - - --- Asynchronous Event Trigger for Event Engage. - -- @function [parent=#AI_CAP_ZONE] __Engage - -- @param #AI_CAP_ZONE self - -- @param #number Delay The delay in seconds. - ---- OnLeave Transition Handler for State Engaging. --- @function [parent=#AI_CAP_ZONE] OnLeaveEngaging --- @param #AI_CAP_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnEnter Transition Handler for State Engaging. --- @function [parent=#AI_CAP_ZONE] OnEnterEngaging --- @param #AI_CAP_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - - self:AddTransition( "Engaging", "Fired", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAP_ZONE. - - --- OnBefore Transition Handler for Event Fired. - -- @function [parent=#AI_CAP_ZONE] OnBeforeFired - -- @param #AI_CAP_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Fired. - -- @function [parent=#AI_CAP_ZONE] OnAfterFired - -- @param #AI_CAP_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Fired. - -- @function [parent=#AI_CAP_ZONE] Fired - -- @param #AI_CAP_ZONE self - - --- Asynchronous Event Trigger for Event Fired. - -- @function [parent=#AI_CAP_ZONE] __Fired - -- @param #AI_CAP_ZONE self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_CAP_ZONE. - - --- OnBefore Transition Handler for Event Destroy. - -- @function [parent=#AI_CAP_ZONE] OnBeforeDestroy - -- @param #AI_CAP_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Destroy. - -- @function [parent=#AI_CAP_ZONE] OnAfterDestroy - -- @param #AI_CAP_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Destroy. - -- @function [parent=#AI_CAP_ZONE] Destroy - -- @param #AI_CAP_ZONE self - - --- Asynchronous Event Trigger for Event Destroy. - -- @function [parent=#AI_CAP_ZONE] __Destroy - -- @param #AI_CAP_ZONE self - -- @param #number Delay The delay in seconds. - - - self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_CAP_ZONE. - - --- OnBefore Transition Handler for Event Abort. - -- @function [parent=#AI_CAP_ZONE] OnBeforeAbort - -- @param #AI_CAP_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Abort. - -- @function [parent=#AI_CAP_ZONE] OnAfterAbort - -- @param #AI_CAP_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Abort. - -- @function [parent=#AI_CAP_ZONE] Abort - -- @param #AI_CAP_ZONE self - - --- Asynchronous Event Trigger for Event Abort. - -- @function [parent=#AI_CAP_ZONE] __Abort - -- @param #AI_CAP_ZONE self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_CAP_ZONE. - - --- OnBefore Transition Handler for Event Accomplish. - -- @function [parent=#AI_CAP_ZONE] OnBeforeAccomplish - -- @param #AI_CAP_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Accomplish. - -- @function [parent=#AI_CAP_ZONE] OnAfterAccomplish - -- @param #AI_CAP_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Accomplish. - -- @function [parent=#AI_CAP_ZONE] Accomplish - -- @param #AI_CAP_ZONE self - - --- Asynchronous Event Trigger for Event Accomplish. - -- @function [parent=#AI_CAP_ZONE] __Accomplish - -- @param #AI_CAP_ZONE self - -- @param #number Delay The delay in seconds. - - return self -end - - ---- Set the Engage Zone which defines where the AI will engage bogies. --- @param #AI_CAP_ZONE self --- @param Core.Zone#ZONE EngageZone The zone where the AI is performing CAP. --- @return #AI_CAP_ZONE self -function AI_CAP_ZONE:SetEngageZone( EngageZone ) - self:F2() - - if EngageZone then - self.EngageZone = EngageZone - else - self.EngageZone = nil - end -end - ---- Set the Engage Range when the AI will engage with airborne enemies. --- @param #AI_CAP_ZONE self --- @param #number EngageRange The Engage Range. --- @return #AI_CAP_ZONE self -function AI_CAP_ZONE:SetEngageRange( EngageRange ) - self:F2() - - if EngageRange then - self.EngageRange = EngageRange - else - self.EngageRange = nil - end -end - ---- onafter State Transition for Event Start. --- @param #AI_CAP_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAP_ZONE:onafterStart( Controllable, From, Event, To ) - - -- Call the parent Start event handler - self:GetParent(self).onafterStart( self, Controllable, From, Event, To ) - -end - ---- @param Wrapper.Controllable#CONTROLLABLE AIControllable -function _NewEngageCapRoute( AIControllable ) - - AIControllable:T( "NewEngageRoute" ) - local EngageZone = AIControllable:GetState( AIControllable, "EngageZone" ) -- AI.AI_Cap#AI_CAP_ZONE - EngageZone:__Engage( 1 ) -end - ---- @param #AI_CAP_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAP_ZONE:onbeforeEngage( Controllable, From, Event, To ) - - if self.Accomplished == true then - return false - end -end - ---- @param #AI_CAP_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAP_ZONE:onafterDetected( Controllable, From, Event, To ) - - if From ~= "Engaging" then - - local Engage = false - - for DetectedUnit, Detected in pairs( self.DetectedUnits ) do - - local DetectedUnit = DetectedUnit -- Wrapper.Unit#UNIT - self:T( DetectedUnit ) - if DetectedUnit:IsAlive() and DetectedUnit:IsAir() then - Engage = true - break - end - end - - if Engage == true then - self:E( 'Detected -> Engaging' ) - self:__Engage( 1 ) - end - end -end - - - ---- @param #AI_CAP_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAP_ZONE:onafterEngage( Controllable, From, Event, To ) - - if Controllable:IsAlive() then - - local EngageRoute = {} - - --- Calculate the current route point. - local CurrentVec2 = self.Controllable:GetVec2() - - --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). - local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() - local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) - local ToEngageZoneSpeed = self.PatrolMaxSpeed - local CurrentRoutePoint = CurrentPointVec3:RoutePointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToEngageZoneSpeed, - true - ) - - EngageRoute[#EngageRoute+1] = CurrentRoutePoint - - - --- Find a random 2D point in PatrolZone. - local ToTargetVec2 = self.PatrolZone:GetRandomVec2() - self:T2( ToTargetVec2 ) - - --- Define Speed and Altitude. - local ToTargetAltitude = math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) - local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) - self:T2( { self.PatrolMinSpeed, self.PatrolMaxSpeed, ToTargetSpeed } ) - - --- Obtain a 3D @{Point} from the 2D point + altitude. - local ToTargetPointVec3 = POINT_VEC3:New( ToTargetVec2.x, ToTargetAltitude, ToTargetVec2.y ) - - --- Create a route point of type air. - local ToPatrolRoutePoint = ToTargetPointVec3:RoutePointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, - true - ) - - EngageRoute[#EngageRoute+1] = ToPatrolRoutePoint - - Controllable:OptionROEOpenFire() - Controllable:OptionROTPassiveDefense() - - local AttackTasks = {} - - for DetectedUnit, Detected in pairs( self.DetectedUnits ) do - local DetectedUnit = DetectedUnit -- Wrapper.Unit#UNIT - self:T( { DetectedUnit, DetectedUnit:IsAlive(), DetectedUnit:IsAir() } ) - if DetectedUnit:IsAlive() and DetectedUnit:IsAir() then - if self.EngageZone then - if DetectedUnit:IsInZone( self.EngageZone ) then - self:E( {"Within Zone and Engaging ", DetectedUnit } ) - AttackTasks[#AttackTasks+1] = Controllable:TaskAttackUnit( DetectedUnit ) - end - else - if self.EngageRange then - if DetectedUnit:GetPointVec3():Get2DDistance(Controllable:GetPointVec3() ) <= self.EngageRange then - self:E( {"Within Range and Engaging", DetectedUnit } ) - AttackTasks[#AttackTasks+1] = Controllable:TaskAttackUnit( DetectedUnit ) - end - else - AttackTasks[#AttackTasks+1] = Controllable:TaskAttackUnit( DetectedUnit ) - end - end - else - self.DetectedUnits[DetectedUnit] = nil - end - end - - --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... - self.Controllable:WayPointInitialize( EngageRoute ) - - - if #AttackTasks == 0 then - self:E("No targets found -> Going back to Patrolling") - self:__Abort( 1 ) - self:__Route( 1 ) - self:SetDetectionActivated() - else - EngageRoute[1].task = Controllable:TaskCombo( AttackTasks ) - - --- Do a trick, link the NewEngageRoute function of the object to the AIControllable in a temporary variable ... - self.Controllable:SetState( self.Controllable, "EngageZone", self ) - - self.Controllable:WayPointFunction( #EngageRoute, 1, "_NewEngageCapRoute" ) - - self:SetDetectionDeactivated() - end - - --- NOW ROUTE THE GROUP! - self.Controllable:WayPointExecute( 1, 2 ) - - end -end - ---- @param #AI_CAP_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @param Core.Event#EVENTDATA EventData -function AI_CAP_ZONE:onafterDestroy( Controllable, From, Event, To, EventData ) - - if EventData.IniUnit then - self.DetectedUnits[EventData.IniUnit] = nil - end - - Controllable:MessageToAll( "Destroyed a target", 15 , "Destroyed!" ) -end - ---- @param #AI_CAP_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAP_ZONE:onafterAccomplish( Controllable, From, Event, To ) - self.Accomplished = true - self:SetDetectionOff() -end - - ----Single-Player:**Yes** / Multi-Player:**Yes** / AI:**Yes** / Human:**No** / Types:**Ground** -- --- **Management of logical cargo objects, that can be transported from and to transportation carriers.** --- --- ![Banner Image](..\Presentations\AI_CARGO\CARGO.JPG) --- --- === --- --- Cargo can be of various forms, always are composed out of ONE object ( one unit or one static or one slingload crate ): --- --- * AI_CARGO_UNIT, represented by a @{Unit} in a @{Group}: Cargo can be represented by a Unit in a Group. Destruction of the Unit will mean that the cargo is lost. --- * CARGO_STATIC, represented by a @{Static}: Cargo can be represented by a Static. Destruction of the Static will mean that the cargo is lost. --- * AI_CARGO_PACKAGE, contained in a @{Unit} of a @{Group}: Cargo can be contained within a Unit of a Group. The cargo can be **delivered** by the @{Unit}. If the Unit is destroyed, the cargo will be destroyed also. --- * AI_CARGO_PACKAGE, Contained in a @{Static}: Cargo can be contained within a Static. The cargo can be **collected** from the @Static. If the @{Static} is destroyed, the cargo will be destroyed. --- * CARGO_SLINGLOAD, represented by a @{Cargo} that is transportable: Cargo can be represented by a Cargo object that is transportable. Destruction of the Cargo will mean that the cargo is lost. --- --- * AI_CARGO_GROUPED, represented by a Group of CARGO_UNITs. --- --- # 1) @{#AI_CARGO} class, extends @{Fsm#FSM_PROCESS} --- --- The @{#AI_CARGO} class defines the core functions that defines a cargo object within MOOSE. --- A cargo is a logical object defined that is available for transport, and has a life status within a simulation. --- --- The AI_CARGO is a state machine: it manages the different events and states of the cargo. --- All derived classes from AI_CARGO follow the same state machine, expose the same cargo event functions, and provide the same cargo states. --- --- ## 1.2.1) AI_CARGO Events: --- --- * @{#AI_CARGO.Board}( ToCarrier ): Boards the cargo to a carrier. --- * @{#AI_CARGO.Load}( ToCarrier ): Loads the cargo into a carrier, regardless of its position. --- * @{#AI_CARGO.UnBoard}( ToPointVec2 ): UnBoard the cargo from a carrier. This will trigger a movement of the cargo to the option ToPointVec2. --- * @{#AI_CARGO.UnLoad}( ToPointVec2 ): UnLoads the cargo from a carrier. --- * @{#AI_CARGO.Dead}( Controllable ): The cargo is dead. The cargo process will be ended. --- --- ## 1.2.2) AI_CARGO States: --- --- * **UnLoaded**: The cargo is unloaded from a carrier. --- * **Boarding**: The cargo is currently boarding (= running) into a carrier. --- * **Loaded**: The cargo is loaded into a carrier. --- * **UnBoarding**: The cargo is currently unboarding (=running) from a carrier. --- * **Dead**: The cargo is dead ... --- * **End**: The process has come to an end. --- --- ## 1.2.3) AI_CARGO state transition methods: --- --- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. --- There are 2 moments when state transition methods will be called by the state machine: --- --- * **Leaving** the state. --- The state transition method needs to start with the name **OnLeave + the name of the state**. --- If the state transition method returns false, then the processing of the state transition will not be done! --- If you want to change the behaviour of the AIControllable at this event, return false, --- but then you'll need to specify your own logic using the AIControllable! --- --- * **Entering** the state. --- The state transition method needs to start with the name **OnEnter + the name of the state**. --- These state transition methods need to provide a return value, which is specified at the function description. --- --- # 2) #AI_CARGO_UNIT class --- --- The AI_CARGO_UNIT class defines a cargo that is represented by a UNIT object within the simulator, and can be transported by a carrier. --- Use the event functions as described above to Load, UnLoad, Board, UnBoard the AI_CARGO_UNIT objects to and from carriers. --- --- # 5) #AI_CARGO_GROUPED class --- --- The AI_CARGO_GROUPED class defines a cargo that is represented by a group of UNIT objects within the simulator, and can be transported by a carrier. --- Use the event functions as described above to Load, UnLoad, Board, UnBoard the AI_CARGO_UNIT objects to and from carriers. --- --- This module is still under construction, but is described above works already, and will keep working ... --- --- @module Cargo - --- Events - --- Board - ---- Boards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo to the Carrier. --- The cargo must be in the **UnLoaded** state. --- @function [parent=#AI_CARGO] Board --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. - ---- Boards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo to the Carrier. --- The cargo must be in the **UnLoaded** state. --- @function [parent=#AI_CARGO] __Board --- @param #AI_CARGO self --- @param #number DelaySeconds The amount of seconds to delay the action. --- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. - - --- UnBoard - ---- UnBoards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo from the Carrier. --- The cargo must be in the **Loaded** state. --- @function [parent=#AI_CARGO] UnBoard --- @param #AI_CARGO self --- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Point#POINT_VEC2) to where the cargo should run after onboarding. If not provided, the cargo will run to 60 meters behind the Carrier location. - ---- UnBoards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo from the Carrier. --- The cargo must be in the **Loaded** state. --- @function [parent=#AI_CARGO] __UnBoard --- @param #AI_CARGO self --- @param #number DelaySeconds The amount of seconds to delay the action. --- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Point#POINT_VEC2) to where the cargo should run after onboarding. If not provided, the cargo will run to 60 meters behind the Carrier location. - - --- Load - ---- Loads the cargo to a Carrier. The event will load the cargo into the Carrier regardless of its position. There will be no movement simulated of the cargo loading. --- The cargo must be in the **UnLoaded** state. --- @function [parent=#AI_CARGO] Load --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. - ---- Loads the cargo to a Carrier. The event will load the cargo into the Carrier regardless of its position. There will be no movement simulated of the cargo loading. --- The cargo must be in the **UnLoaded** state. --- @function [parent=#AI_CARGO] __Load --- @param #AI_CARGO self --- @param #number DelaySeconds The amount of seconds to delay the action. --- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. - - --- UnLoad - ---- UnLoads the cargo to a Carrier. The event will unload the cargo from the Carrier. There will be no movement simulated of the cargo loading. --- The cargo must be in the **Loaded** state. --- @function [parent=#AI_CARGO] UnLoad --- @param #AI_CARGO self --- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Point#POINT_VEC2) to where the cargo will be placed after unloading. If not provided, the cargo will be placed 60 meters behind the Carrier location. - ---- UnLoads the cargo to a Carrier. The event will unload the cargo from the Carrier. There will be no movement simulated of the cargo loading. --- The cargo must be in the **Loaded** state. --- @function [parent=#AI_CARGO] __UnLoad --- @param #AI_CARGO self --- @param #number DelaySeconds The amount of seconds to delay the action. --- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Point#POINT_VEC2) to where the cargo will be placed after unloading. If not provided, the cargo will be placed 60 meters behind the Carrier location. - --- State Transition Functions - --- UnLoaded - ---- @function [parent=#AI_CARGO] OnLeaveUnLoaded --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable --- @return #boolean - ---- @function [parent=#AI_CARGO] OnEnterUnLoaded --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable - --- Loaded - ---- @function [parent=#AI_CARGO] OnLeaveLoaded --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable --- @return #boolean - ---- @function [parent=#AI_CARGO] OnEnterLoaded --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable - --- Boarding - ---- @function [parent=#AI_CARGO] OnLeaveBoarding --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable --- @return #boolean - ---- @function [parent=#AI_CARGO] OnEnterBoarding --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable - --- UnBoarding - ---- @function [parent=#AI_CARGO] OnLeaveUnBoarding --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable --- @return #boolean - ---- @function [parent=#AI_CARGO] OnEnterUnBoarding --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable - - --- TODO: Find all Carrier objects and make the type of the Carriers Wrapper.Unit#UNIT in the documentation. - -CARGOS = {} - -do -- AI_CARGO - - --- @type AI_CARGO - -- @extends Core.Fsm#FSM_PROCESS - -- @field #string Type A string defining the type of the cargo. eg. Engineers, Equipment, Screwdrivers. - -- @field #string Name A string defining the name of the cargo. The name is the unique identifier of the cargo. - -- @field #number Weight A number defining the weight of the cargo. The weight is expressed in kg. - -- @field #number ReportRadius (optional) A number defining the radius in meters when the cargo is signalling or reporting to a Carrier. - -- @field #number NearRadius (optional) A number defining the radius in meters when the cargo is near to a Carrier, so that it can be loaded. - -- @field Wrapper.Controllable#CONTROLLABLE CargoObject The alive DCS object representing the cargo. This value can be nil, meaning, that the cargo is not represented anywhere... - -- @field Wrapper.Controllable#CONTROLLABLE CargoCarrier The alive DCS object carrying the cargo. This value can be nil, meaning, that the cargo is not contained anywhere... - -- @field #boolean Slingloadable This flag defines if the cargo can be slingloaded. - -- @field #boolean Moveable This flag defines if the cargo is moveable. - -- @field #boolean Representable This flag defines if the cargo can be represented by a DCS Unit. - -- @field #boolean Containable This flag defines if the cargo can be contained within a DCS Unit. - AI_CARGO = { - ClassName = "AI_CARGO", - Type = nil, - Name = nil, - Weight = nil, - CargoObject = nil, - CargoCarrier = nil, - Representable = false, - Slingloadable = false, - Moveable = false, - Containable = false, - } - ---- @type AI_CARGO.CargoObjects --- @map < #string, Wrapper.Positionable#POSITIONABLE > The alive POSITIONABLE objects representing the the cargo. - - ---- AI_CARGO Constructor. This class is an abstract class and should not be instantiated. --- @param #AI_CARGO self --- @param #string Type --- @param #string Name --- @param #number Weight --- @param #number ReportRadius (optional) --- @param #number NearRadius (optional) --- @return #AI_CARGO -function AI_CARGO:New( Type, Name, Weight, ReportRadius, NearRadius ) - - local self = BASE:Inherit( self, FSM:New() ) -- Core.Fsm#FSM_CONTROLLABLE - self:F( { Type, Name, Weight, ReportRadius, NearRadius } ) - - self:SetStartState( "UnLoaded" ) - self:AddTransition( "UnLoaded", "Board", "Boarding" ) - self:AddTransition( "Boarding", "Boarding", "Boarding" ) - self:AddTransition( "Boarding", "Load", "Loaded" ) - self:AddTransition( "UnLoaded", "Load", "Loaded" ) - self:AddTransition( "Loaded", "UnBoard", "UnBoarding" ) - self:AddTransition( "UnBoarding", "UnBoarding", "UnBoarding" ) - self:AddTransition( "UnBoarding", "UnLoad", "UnLoaded" ) - self:AddTransition( "Loaded", "UnLoad", "UnLoaded" ) - - - self.Type = Type - self.Name = Name - self.Weight = Weight - self.ReportRadius = ReportRadius - self.NearRadius = NearRadius - self.CargoObject = nil - self.CargoCarrier = nil - self.Representable = false - self.Slingloadable = false - self.Moveable = false - self.Containable = false - - - self.CargoScheduler = SCHEDULER:New() - - CARGOS[self.Name] = self - - return self -end - - ---- Template method to spawn a new representation of the AI_CARGO in the simulator. --- @param #AI_CARGO self --- @return #AI_CARGO -function AI_CARGO:Spawn( PointVec2 ) - self:F() - -end - - ---- Check if CargoCarrier is near the Cargo to be Loaded. --- @param #AI_CARGO self --- @param Core.Point#POINT_VEC2 PointVec2 --- @return #boolean -function AI_CARGO:IsNear( PointVec2 ) - self:F( { PointVec2 } ) - - local Distance = PointVec2:DistanceFromPointVec2( self.CargoObject:GetPointVec2() ) - self:T( Distance ) - - if Distance <= self.NearRadius then - return true - else - return false - end -end - -end - -do -- AI_CARGO_REPRESENTABLE - - --- @type AI_CARGO_REPRESENTABLE - -- @extends #AI_CARGO - AI_CARGO_REPRESENTABLE = { - ClassName = "AI_CARGO_REPRESENTABLE" - } - ---- AI_CARGO_REPRESENTABLE Constructor. --- @param #AI_CARGO_REPRESENTABLE self --- @param Wrapper.Controllable#Controllable CargoObject --- @param #string Type --- @param #string Name --- @param #number Weight --- @param #number ReportRadius (optional) --- @param #number NearRadius (optional) --- @return #AI_CARGO_REPRESENTABLE -function AI_CARGO_REPRESENTABLE:New( CargoObject, Type, Name, Weight, ReportRadius, NearRadius ) - local self = BASE:Inherit( self, AI_CARGO:New( Type, Name, Weight, ReportRadius, NearRadius ) ) -- #AI_CARGO - self:F( { Type, Name, Weight, ReportRadius, NearRadius } ) - - return self -end - ---- Route a cargo unit to a PointVec2. --- @param #AI_CARGO_REPRESENTABLE self --- @param Core.Point#POINT_VEC2 ToPointVec2 --- @param #number Speed --- @return #AI_CARGO_REPRESENTABLE -function AI_CARGO_REPRESENTABLE:RouteTo( ToPointVec2, Speed ) - self:F2( ToPointVec2 ) - - local Points = {} - - local PointStartVec2 = self.CargoObject:GetPointVec2() - - Points[#Points+1] = PointStartVec2:RoutePointGround( Speed ) - Points[#Points+1] = ToPointVec2:RoutePointGround( Speed ) - - local TaskRoute = self.CargoObject:TaskRoute( Points ) - self.CargoObject:SetTask( TaskRoute, 2 ) - return self -end - -end -- AI_CARGO - -do -- AI_CARGO_UNIT - - --- @type AI_CARGO_UNIT - -- @extends #AI_CARGO_REPRESENTABLE - AI_CARGO_UNIT = { - ClassName = "AI_CARGO_UNIT" - } - ---- AI_CARGO_UNIT Constructor. --- @param #AI_CARGO_UNIT self --- @param Wrapper.Unit#UNIT CargoUnit --- @param #string Type --- @param #string Name --- @param #number Weight --- @param #number ReportRadius (optional) --- @param #number NearRadius (optional) --- @return #AI_CARGO_UNIT -function AI_CARGO_UNIT:New( CargoUnit, Type, Name, Weight, ReportRadius, NearRadius ) - local self = BASE:Inherit( self, AI_CARGO_REPRESENTABLE:New( CargoUnit, Type, Name, Weight, ReportRadius, NearRadius ) ) -- #AI_CARGO_UNIT - self:F( { Type, Name, Weight, ReportRadius, NearRadius } ) - - self:T( CargoUnit ) - self.CargoObject = CargoUnit - - self:T( self.ClassName ) - - return self -end - ---- Enter UnBoarding State. --- @param #AI_CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To --- @param Core.Point#POINT_VEC2 ToPointVec2 -function AI_CARGO_UNIT:onenterUnBoarding( From, Event, To, ToPointVec2 ) - self:F() - - local Angle = 180 - local Speed = 10 - local DeployDistance = 5 - local RouteDistance = 60 - - if From == "Loaded" then - - local CargoCarrierPointVec2 = self.CargoCarrier:GetPointVec2() - local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - local CargoDeployPointVec2 = CargoCarrierPointVec2:Translate( DeployDistance, CargoDeployHeading ) - local CargoRoutePointVec2 = CargoCarrierPointVec2:Translate( RouteDistance, CargoDeployHeading ) - - -- if there is no ToPointVec2 given, then use the CargoRoutePointVec2 - ToPointVec2 = ToPointVec2 or CargoRoutePointVec2 - - local FromPointVec2 = CargoCarrierPointVec2 - - -- Respawn the group... - if self.CargoObject then - self.CargoObject:ReSpawn( CargoDeployPointVec2:GetVec3(), CargoDeployHeading ) - self.CargoCarrier = nil - - local Points = {} - Points[#Points+1] = FromPointVec2:RoutePointGround( Speed ) - Points[#Points+1] = ToPointVec2:RoutePointGround( Speed ) - - local TaskRoute = self.CargoObject:TaskRoute( Points ) - self.CargoObject:SetTask( TaskRoute, 1 ) - - self:__UnBoarding( 1, ToPointVec2 ) - end - end - -end - ---- Leave UnBoarding State. --- @param #AI_CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To --- @param Core.Point#POINT_VEC2 ToPointVec2 -function AI_CARGO_UNIT:onleaveUnBoarding( From, Event, To, ToPointVec2 ) - self:F( { ToPointVec2, From, Event, To } ) - - local Angle = 180 - local Speed = 10 - local Distance = 5 - - if From == "UnBoarding" then - if self:IsNear( ToPointVec2 ) then - return true - else - self:__UnBoarding( 1, ToPointVec2 ) - end - return false - end - -end - ---- UnBoard Event. --- @param #AI_CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To --- @param Core.Point#POINT_VEC2 ToPointVec2 -function AI_CARGO_UNIT:onafterUnBoarding( From, Event, To, ToPointVec2 ) - self:F( { ToPointVec2, From, Event, To } ) - - self.CargoInAir = self.CargoObject:InAir() - - self:T( self.CargoInAir ) - - -- Only unboard the cargo when the carrier is not in the air. - -- (eg. cargo can be on a oil derrick, moving the cargo on the oil derrick will drop the cargo on the sea). - if not self.CargoInAir then - - end - - self:__UnLoad( 1, ToPointVec2 ) - -end - - - ---- Enter UnLoaded State. --- @param #AI_CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To --- @param Core.Point#POINT_VEC2 -function AI_CARGO_UNIT:onenterUnLoaded( From, Event, To, ToPointVec2 ) - self:F( { ToPointVec2, From, Event, To } ) - - local Angle = 180 - local Speed = 10 - local Distance = 5 - - if From == "Loaded" then - local StartPointVec2 = self.CargoCarrier:GetPointVec2() - local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - local CargoDeployPointVec2 = StartPointVec2:Translate( Distance, CargoDeployHeading ) - - ToPointVec2 = ToPointVec2 or POINT_VEC2:New( CargoDeployPointVec2:GetX(), CargoDeployPointVec2:GetY() ) - - -- Respawn the group... - if self.CargoObject then - self.CargoObject:ReSpawn( ToPointVec2:GetVec3(), 0 ) - self.CargoCarrier = nil - end - - end - - if self.OnUnLoadedCallBack then - self.OnUnLoadedCallBack( self, unpack( self.OnUnLoadedParameters ) ) - self.OnUnLoadedCallBack = nil - end - -end - - - ---- Enter Boarding State. --- @param #AI_CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT CargoCarrier -function AI_CARGO_UNIT:onenterBoarding( From, Event, To, CargoCarrier ) - self:F( { CargoCarrier.UnitName, From, Event, To } ) - - local Speed = 10 - local Angle = 180 - local Distance = 5 - - if From == "UnLoaded" then - local CargoCarrierPointVec2 = CargoCarrier:GetPointVec2() - local CargoCarrierHeading = CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - local CargoDeployPointVec2 = CargoCarrierPointVec2:Translate( Distance, CargoDeployHeading ) - - local Points = {} - - local PointStartVec2 = self.CargoObject:GetPointVec2() - - Points[#Points+1] = PointStartVec2:RoutePointGround( Speed ) - Points[#Points+1] = CargoDeployPointVec2:RoutePointGround( Speed ) - - local TaskRoute = self.CargoObject:TaskRoute( Points ) - self.CargoObject:SetTask( TaskRoute, 2 ) - end - -end - ---- Leave Boarding State. --- @param #AI_CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT CargoCarrier -function AI_CARGO_UNIT:onleaveBoarding( From, Event, To, CargoCarrier ) - self:F( { CargoCarrier.UnitName, From, Event, To } ) - - if self:IsNear( CargoCarrier:GetPointVec2() ) then - self:__Load( 1, CargoCarrier ) - return true - else - self:__Boarding( 1, CargoCarrier ) - end - return false -end - ---- Loaded State. --- @param #AI_CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT CargoCarrier -function AI_CARGO_UNIT:onenterLoaded( From, Event, To, CargoCarrier ) - self:F() - - self.CargoCarrier = CargoCarrier - - -- Only destroy the CargoObject is if there is a CargoObject (packages don't have CargoObjects). - if self.CargoObject then - self:T("Destroying") - self.CargoObject:Destroy() - end -end - - ---- Board Event. --- @param #AI_CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To -function AI_CARGO_UNIT:onafterBoard( From, Event, To, CargoCarrier ) - self:F() - - self.CargoInAir = self.CargoObject:InAir() - - self:T( self.CargoInAir ) - - -- Only move the group to the carrier when the cargo is not in the air - -- (eg. cargo can be on a oil derrick, moving the cargo on the oil derrick will drop the cargo on the sea). - if not self.CargoInAir then - self:Load( CargoCarrier ) - end - -end - -end - -do -- AI_CARGO_PACKAGE - - --- @type AI_CARGO_PACKAGE - -- @extends #AI_CARGO_REPRESENTABLE - AI_CARGO_PACKAGE = { - ClassName = "AI_CARGO_PACKAGE" - } - ---- AI_CARGO_PACKAGE Constructor. --- @param #AI_CARGO_PACKAGE self --- @param Wrapper.Unit#UNIT CargoCarrier The UNIT carrying the package. --- @param #string Type --- @param #string Name --- @param #number Weight --- @param #number ReportRadius (optional) --- @param #number NearRadius (optional) --- @return #AI_CARGO_PACKAGE -function AI_CARGO_PACKAGE:New( CargoCarrier, Type, Name, Weight, ReportRadius, NearRadius ) - local self = BASE:Inherit( self, AI_CARGO_REPRESENTABLE:New( CargoCarrier, Type, Name, Weight, ReportRadius, NearRadius ) ) -- #AI_CARGO_PACKAGE - self:F( { Type, Name, Weight, ReportRadius, NearRadius } ) - - self:T( CargoCarrier ) - self.CargoCarrier = CargoCarrier - - return self -end - ---- Board Event. --- @param #AI_CARGO_PACKAGE self --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT CargoCarrier --- @param #number Speed --- @param #number BoardDistance --- @param #number Angle -function AI_CARGO_PACKAGE:onafterOnBoard( From, Event, To, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) - self:F() - - self.CargoInAir = self.CargoCarrier:InAir() - - self:T( self.CargoInAir ) - - -- Only move the CargoCarrier to the New CargoCarrier when the New CargoCarrier is not in the air. - if not self.CargoInAir then - - local Points = {} - - local StartPointVec2 = self.CargoCarrier:GetPointVec2() - local CargoCarrierHeading = CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - self:T( { CargoCarrierHeading, CargoDeployHeading } ) - local CargoDeployPointVec2 = CargoCarrier:GetPointVec2():Translate( BoardDistance, CargoDeployHeading ) - - Points[#Points+1] = StartPointVec2:RoutePointGround( Speed ) - Points[#Points+1] = CargoDeployPointVec2:RoutePointGround( Speed ) - - local TaskRoute = self.CargoCarrier:TaskRoute( Points ) - self.CargoCarrier:SetTask( TaskRoute, 1 ) - end - - self:Boarded( CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) - -end - ---- Check if CargoCarrier is near the Cargo to be Loaded. --- @param #AI_CARGO_PACKAGE self --- @param Wrapper.Unit#UNIT CargoCarrier --- @return #boolean -function AI_CARGO_PACKAGE:IsNear( CargoCarrier ) - self:F() - - local CargoCarrierPoint = CargoCarrier:GetPointVec2() - - local Distance = CargoCarrierPoint:DistanceFromPointVec2( self.CargoCarrier:GetPointVec2() ) - self:T( Distance ) - - if Distance <= self.NearRadius then - return true - else - return false - end -end - ---- Boarded Event. --- @param #AI_CARGO_PACKAGE self --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT CargoCarrier -function AI_CARGO_PACKAGE:onafterOnBoarded( From, Event, To, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) - self:F() - - if self:IsNear( CargoCarrier ) then - self:__Load( 1, CargoCarrier, Speed, LoadDistance, Angle ) - else - self:__Boarded( 1, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) - end -end - ---- UnBoard Event. --- @param #AI_CARGO_PACKAGE self --- @param #string Event --- @param #string From --- @param #string To --- @param #number Speed --- @param #number UnLoadDistance --- @param #number UnBoardDistance --- @param #number Radius --- @param #number Angle -function AI_CARGO_PACKAGE:onafterUnBoard( From, Event, To, CargoCarrier, Speed, UnLoadDistance, UnBoardDistance, Radius, Angle ) - self:F() - - self.CargoInAir = self.CargoCarrier:InAir() - - self:T( self.CargoInAir ) - - -- Only unboard the cargo when the carrier is not in the air. - -- (eg. cargo can be on a oil derrick, moving the cargo on the oil derrick will drop the cargo on the sea). - if not self.CargoInAir then - - self:_Next( self.FsmP.UnLoad, UnLoadDistance, Angle ) - - local Points = {} - - local StartPointVec2 = CargoCarrier:GetPointVec2() - local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - self:T( { CargoCarrierHeading, CargoDeployHeading } ) - local CargoDeployPointVec2 = StartPointVec2:Translate( UnBoardDistance, CargoDeployHeading ) - - Points[#Points+1] = StartPointVec2:RoutePointGround( Speed ) - Points[#Points+1] = CargoDeployPointVec2:RoutePointGround( Speed ) - - local TaskRoute = CargoCarrier:TaskRoute( Points ) - CargoCarrier:SetTask( TaskRoute, 1 ) - end - - self:__UnBoarded( 1 , CargoCarrier, Speed ) - -end - ---- UnBoarded Event. --- @param #AI_CARGO_PACKAGE self --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT CargoCarrier -function AI_CARGO_PACKAGE:onafterUnBoarded( From, Event, To, CargoCarrier, Speed ) - self:F() - - if self:IsNear( CargoCarrier ) then - self:__UnLoad( 1, CargoCarrier, Speed ) - else - self:__UnBoarded( 1, CargoCarrier, Speed ) - end -end - ---- Load Event. --- @param #AI_CARGO_PACKAGE self --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT CargoCarrier --- @param #number Speed --- @param #number LoadDistance --- @param #number Angle -function AI_CARGO_PACKAGE:onafterLoad( From, Event, To, CargoCarrier, Speed, LoadDistance, Angle ) - self:F() - - self.CargoCarrier = CargoCarrier - - local StartPointVec2 = self.CargoCarrier:GetPointVec2() - local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - local CargoDeployPointVec2 = StartPointVec2:Translate( LoadDistance, CargoDeployHeading ) - - local Points = {} - Points[#Points+1] = StartPointVec2:RoutePointGround( Speed ) - Points[#Points+1] = CargoDeployPointVec2:RoutePointGround( Speed ) - - local TaskRoute = self.CargoCarrier:TaskRoute( Points ) - self.CargoCarrier:SetTask( TaskRoute, 1 ) - -end - ---- UnLoad Event. --- @param #AI_CARGO_PACKAGE self --- @param #string Event --- @param #string From --- @param #string To --- @param #number Distance --- @param #number Angle -function AI_CARGO_PACKAGE:onafterUnLoad( From, Event, To, CargoCarrier, Speed, Distance, Angle ) - self:F() - - local StartPointVec2 = self.CargoCarrier:GetPointVec2() - local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - local CargoDeployPointVec2 = StartPointVec2:Translate( Distance, CargoDeployHeading ) - - self.CargoCarrier = CargoCarrier - - local Points = {} - Points[#Points+1] = StartPointVec2:RoutePointGround( Speed ) - Points[#Points+1] = CargoDeployPointVec2:RoutePointGround( Speed ) - - local TaskRoute = self.CargoCarrier:TaskRoute( Points ) - self.CargoCarrier:SetTask( TaskRoute, 1 ) - -end - - -end - -do -- AI_CARGO_GROUP - - --- @type AI_CARGO_GROUP - -- @extends AI.AI_Cargo#AI_CARGO - -- @field Set#SET_BASE CargoSet A set of cargo objects. - -- @field #string Name A string defining the name of the cargo group. The name is the unique identifier of the cargo. - AI_CARGO_GROUP = { - ClassName = "AI_CARGO_GROUP", - } - ---- AI_CARGO_GROUP constructor. --- @param #AI_CARGO_GROUP self --- @param Core.Set#Set_BASE CargoSet --- @param #string Type --- @param #string Name --- @param #number Weight --- @param #number ReportRadius (optional) --- @param #number NearRadius (optional) --- @return #AI_CARGO_GROUP -function AI_CARGO_GROUP:New( CargoSet, Type, Name, ReportRadius, NearRadius ) - local self = BASE:Inherit( self, AI_CARGO:New( Type, Name, 0, ReportRadius, NearRadius ) ) -- #AI_CARGO_GROUP - self:F( { Type, Name, ReportRadius, NearRadius } ) - - self.CargoSet = CargoSet - - - return self -end - -end -- AI_CARGO_GROUP - -do -- AI_CARGO_GROUPED - - --- @type AI_CARGO_GROUPED - -- @extends AI.AI_Cargo#AI_CARGO_GROUP - AI_CARGO_GROUPED = { - ClassName = "AI_CARGO_GROUPED", - } - ---- AI_CARGO_GROUPED constructor. --- @param #AI_CARGO_GROUPED self --- @param Core.Set#Set_BASE CargoSet --- @param #string Type --- @param #string Name --- @param #number Weight --- @param #number ReportRadius (optional) --- @param #number NearRadius (optional) --- @return #AI_CARGO_GROUPED -function AI_CARGO_GROUPED:New( CargoSet, Type, Name, ReportRadius, NearRadius ) - local self = BASE:Inherit( self, AI_CARGO_GROUP:New( CargoSet, Type, Name, ReportRadius, NearRadius ) ) -- #AI_CARGO_GROUPED - self:F( { Type, Name, ReportRadius, NearRadius } ) - - return self -end - ---- Enter Boarding State. --- @param #AI_CARGO_GROUPED self --- @param Wrapper.Unit#UNIT CargoCarrier --- @param #string Event --- @param #string From --- @param #string To -function AI_CARGO_GROUPED:onenterBoarding( From, Event, To, CargoCarrier ) - self:F( { CargoCarrier.UnitName, From, Event, To } ) - - if From == "UnLoaded" then - - -- For each Cargo object within the AI_CARGO_GROUPED, route each object to the CargoLoadPointVec2 - self.CargoSet:ForEach( - function( Cargo ) - Cargo:__Board( 1, CargoCarrier ) - end - ) - - self:__Boarding( 1, CargoCarrier ) - end - -end - ---- Enter Loaded State. --- @param #AI_CARGO_GROUPED self --- @param Wrapper.Unit#UNIT CargoCarrier --- @param #string Event --- @param #string From --- @param #string To -function AI_CARGO_GROUPED:onenterLoaded( From, Event, To, CargoCarrier ) - self:F( { CargoCarrier.UnitName, From, Event, To } ) - - if From == "UnLoaded" then - -- For each Cargo object within the AI_CARGO_GROUPED, load each cargo to the CargoCarrier. - for CargoID, Cargo in pairs( self.CargoSet:GetSet() ) do - Cargo:Load( CargoCarrier ) - end - end -end - ---- Leave Boarding State. --- @param #AI_CARGO_GROUPED self --- @param Wrapper.Unit#UNIT CargoCarrier --- @param #string Event --- @param #string From --- @param #string To -function AI_CARGO_GROUPED:onleaveBoarding( From, Event, To, CargoCarrier ) - self:F( { CargoCarrier.UnitName, From, Event, To } ) - - local Boarded = true - - -- For each Cargo object within the AI_CARGO_GROUPED, route each object to the CargoLoadPointVec2 - for CargoID, Cargo in pairs( self.CargoSet:GetSet() ) do - self:T( Cargo.current ) - if not Cargo:is( "Loaded" ) then - Boarded = false - end - end - - if not Boarded then - self:__Boarding( 1, CargoCarrier ) - else - self:__Load( 1, CargoCarrier ) - end - return Boarded -end - ---- Enter UnBoarding State. --- @param #AI_CARGO_GROUPED self --- @param Core.Point#POINT_VEC2 ToPointVec2 --- @param #string Event --- @param #string From --- @param #string To -function AI_CARGO_GROUPED:onenterUnBoarding( From, Event, To, ToPointVec2 ) - self:F() - - local Timer = 1 - - if From == "Loaded" then - - -- For each Cargo object within the AI_CARGO_GROUPED, route each object to the CargoLoadPointVec2 - self.CargoSet:ForEach( - function( Cargo ) - Cargo:__UnBoard( Timer, ToPointVec2 ) - Timer = Timer + 10 - end - ) - - self:__UnBoarding( 1, ToPointVec2 ) - end - -end - ---- Leave UnBoarding State. --- @param #AI_CARGO_GROUPED self --- @param Core.Point#POINT_VEC2 ToPointVec2 --- @param #string Event --- @param #string From --- @param #string To -function AI_CARGO_GROUPED:onleaveUnBoarding( From, Event, To, ToPointVec2 ) - self:F( { ToPointVec2, From, Event, To } ) - - local Angle = 180 - local Speed = 10 - local Distance = 5 - - if From == "UnBoarding" then - local UnBoarded = true - - -- For each Cargo object within the AI_CARGO_GROUPED, route each object to the CargoLoadPointVec2 - for CargoID, Cargo in pairs( self.CargoSet:GetSet() ) do - self:T( Cargo.current ) - if not Cargo:is( "UnLoaded" ) then - UnBoarded = false - end - end - - if UnBoarded then - return true - else - self:__UnBoarding( 1, ToPointVec2 ) - end - - return false - end - -end - ---- UnBoard Event. --- @param #AI_CARGO_GROUPED self --- @param Core.Point#POINT_VEC2 ToPointVec2 --- @param #string Event --- @param #string From --- @param #string To -function AI_CARGO_GROUPED:onafterUnBoarding( From, Event, To, ToPointVec2 ) - self:F( { ToPointVec2, From, Event, To } ) - - self:__UnLoad( 1, ToPointVec2 ) -end - - - ---- Enter UnLoaded State. --- @param #AI_CARGO_GROUPED self --- @param Core.Point#POINT_VEC2 --- @param #string Event --- @param #string From --- @param #string To -function AI_CARGO_GROUPED:onenterUnLoaded( From, Event, To, ToPointVec2 ) - self:F( { ToPointVec2, From, Event, To } ) - - if From == "Loaded" then - - -- For each Cargo object within the AI_CARGO_GROUPED, route each object to the CargoLoadPointVec2 - self.CargoSet:ForEach( - function( Cargo ) - Cargo:UnLoad( ToPointVec2 ) - end - ) - - end - -end - -end -- AI_CARGO_GROUPED - - - ---- (SP) (MP) (FSM) Accept or reject process for player (task) assignments. --- --- === --- --- # @{#ACT_ASSIGN} FSM template class, extends @{Fsm#FSM_PROCESS} --- --- ## ACT_ASSIGN state machine: --- --- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur. --- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below. --- Each derived class follows exactly the same process, using the same events and following the same state transitions, --- but will have **different implementation behaviour** upon each event or state transition. --- --- ### ACT_ASSIGN **Events**: --- --- These are the events defined in this class: --- --- * **Start**: Start the tasking acceptance process. --- * **Assign**: Assign the task. --- * **Reject**: Reject the task.. --- --- ### ACT_ASSIGN **Event methods**: --- --- Event methods are available (dynamically allocated by the state machine), that accomodate for state transitions occurring in the process. --- There are two types of event methods, which you can use to influence the normal mechanisms in the state machine: --- --- * **Immediate**: The event method has exactly the name of the event. --- * **Delayed**: The event method starts with a __ + the name of the event. The first parameter of the event method is a number value, expressing the delay in seconds when the event will be executed. --- --- ### ACT_ASSIGN **States**: --- --- * **UnAssigned**: The player has not accepted the task. --- * **Assigned (*)**: The player has accepted the task. --- * **Rejected (*)**: The player has not accepted the task. --- * **Waiting**: The process is awaiting player feedback. --- * **Failed (*)**: The process has failed. --- --- (*) End states of the process. --- --- ### ACT_ASSIGN state transition methods: --- --- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. --- There are 2 moments when state transition methods will be called by the state machine: --- --- * **Before** the state transition. --- The state transition method needs to start with the name **OnBefore + the name of the state**. --- If the state transition method returns false, then the processing of the state transition will not be done! --- If you want to change the behaviour of the AIControllable at this event, return false, --- but then you'll need to specify your own logic using the AIControllable! --- --- * **After** the state transition. --- The state transition method needs to start with the name **OnAfter + the name of the state**. --- These state transition methods need to provide a return value, which is specified at the function description. --- --- === --- --- # 1) @{#ACT_ASSIGN_ACCEPT} class, extends @{Fsm.Assign#ACT_ASSIGN} --- --- The ACT_ASSIGN_ACCEPT class accepts by default a task for a player. No player intervention is allowed to reject the task. --- --- ## 1.1) ACT_ASSIGN_ACCEPT constructor: --- --- * @{#ACT_ASSIGN_ACCEPT.New}(): Creates a new ACT_ASSIGN_ACCEPT object. --- --- === --- --- # 2) @{#ACT_ASSIGN_MENU_ACCEPT} class, extends @{Fsm.Assign#ACT_ASSIGN} --- --- The ACT_ASSIGN_MENU_ACCEPT class accepts a task when the player accepts the task through an added menu option. --- This assignment type is useful to conditionally allow the player to choose whether or not he would accept the task. --- The assignment type also allows to reject the task. --- --- ## 2.1) ACT_ASSIGN_MENU_ACCEPT constructor: --- ----------------------------------------- --- --- * @{#ACT_ASSIGN_MENU_ACCEPT.New}(): Creates a new ACT_ASSIGN_MENU_ACCEPT object. --- --- === --- --- @module Assign - - -do -- ACT_ASSIGN - - --- ACT_ASSIGN class - -- @type ACT_ASSIGN - -- @field Tasking.Task#TASK Task - -- @field Wrapper.Unit#UNIT ProcessUnit - -- @field Core.Zone#ZONE_BASE TargetZone - -- @extends Core.Fsm#FSM_PROCESS - ACT_ASSIGN = { - ClassName = "ACT_ASSIGN", - } - - - --- Creates a new task assignment state machine. The process will accept the task by default, no player intervention accepted. - -- @param #ACT_ASSIGN self - -- @return #ACT_ASSIGN The task acceptance process. - function ACT_ASSIGN:New() - - -- Inherits from BASE - local self = BASE:Inherit( self, FSM_PROCESS:New( "ACT_ASSIGN" ) ) -- Core.Fsm#FSM_PROCESS - - self:AddTransition( "UnAssigned", "Start", "Waiting" ) - self:AddTransition( "Waiting", "Assign", "Assigned" ) - self:AddTransition( "Waiting", "Reject", "Rejected" ) - self:AddTransition( "*", "Fail", "Failed" ) - - self:AddEndState( "Assigned" ) - self:AddEndState( "Rejected" ) - self:AddEndState( "Failed" ) - - self:SetStartState( "UnAssigned" ) - - return self - end - -end -- ACT_ASSIGN - - - -do -- ACT_ASSIGN_ACCEPT - - --- ACT_ASSIGN_ACCEPT class - -- @type ACT_ASSIGN_ACCEPT - -- @field Tasking.Task#TASK Task - -- @field Wrapper.Unit#UNIT ProcessUnit - -- @field Core.Zone#ZONE_BASE TargetZone - -- @extends #ACT_ASSIGN - ACT_ASSIGN_ACCEPT = { - ClassName = "ACT_ASSIGN_ACCEPT", - } - - - --- Creates a new task assignment state machine. The process will accept the task by default, no player intervention accepted. - -- @param #ACT_ASSIGN_ACCEPT self - -- @param #string TaskBriefing - function ACT_ASSIGN_ACCEPT:New( TaskBriefing ) - - local self = BASE:Inherit( self, ACT_ASSIGN:New() ) -- #ACT_ASSIGN_ACCEPT - - self.TaskBriefing = TaskBriefing - - return self - end - - function ACT_ASSIGN_ACCEPT:Init( FsmAssign ) - - self.TaskBriefing = FsmAssign.TaskBriefing - end - - --- StateMachine callback function - -- @param #ACT_ASSIGN_ACCEPT self - -- @param Wrapper.Unit#UNIT ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ASSIGN_ACCEPT:onafterStart( ProcessUnit, From, Event, To ) - self:E( { ProcessUnit, From, Event, To } ) - - self:__Assign( 1 ) - end - - --- StateMachine callback function - -- @param #ACT_ASSIGN_ACCEPT self - -- @param Wrapper.Unit#UNIT ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ASSIGN_ACCEPT:onenterAssigned( ProcessUnit, From, Event, To ) - env.info( "in here" ) - self:E( { ProcessUnit, From, Event, To } ) - - local ProcessGroup = ProcessUnit:GetGroup() - - self:Message( "You are assigned to the task " .. self.Task:GetName() ) - - self.Task:Assign() - end - -end -- ACT_ASSIGN_ACCEPT - - -do -- ACT_ASSIGN_MENU_ACCEPT - - --- ACT_ASSIGN_MENU_ACCEPT class - -- @type ACT_ASSIGN_MENU_ACCEPT - -- @field Tasking.Task#TASK Task - -- @field Wrapper.Unit#UNIT ProcessUnit - -- @field Core.Zone#ZONE_BASE TargetZone - -- @extends #ACT_ASSIGN - ACT_ASSIGN_MENU_ACCEPT = { - ClassName = "ACT_ASSIGN_MENU_ACCEPT", - } - - --- Init. - -- @param #ACT_ASSIGN_MENU_ACCEPT self - -- @param #string TaskName - -- @param #string TaskBriefing - -- @return #ACT_ASSIGN_MENU_ACCEPT self - function ACT_ASSIGN_MENU_ACCEPT:New( TaskName, TaskBriefing ) - - -- Inherits from BASE - local self = BASE:Inherit( self, ACT_ASSIGN:New() ) -- #ACT_ASSIGN_MENU_ACCEPT - - self.TaskName = TaskName - self.TaskBriefing = TaskBriefing - - return self - end - - function ACT_ASSIGN_MENU_ACCEPT:Init( FsmAssign ) - - self.TaskName = FsmAssign.TaskName - self.TaskBriefing = FsmAssign.TaskBriefing - end - - - --- Creates a new task assignment state machine. The process will request from the menu if it accepts the task, if not, the unit is removed from the simulator. - -- @param #ACT_ASSIGN_MENU_ACCEPT self - -- @param #string TaskName - -- @param #string TaskBriefing - -- @return #ACT_ASSIGN_MENU_ACCEPT self - function ACT_ASSIGN_MENU_ACCEPT:Init( TaskName, TaskBriefing ) - - self.TaskBriefing = TaskBriefing - self.TaskName = TaskName - - return self - end - - --- StateMachine callback function - -- @param #ACT_ASSIGN_MENU_ACCEPT self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ASSIGN_MENU_ACCEPT:onafterStart( ProcessUnit, From, Event, To ) - self:E( { ProcessUnit, From, Event, To } ) - - self:Message( "Access the radio menu to accept the task. You have 30 seconds or the assignment will be cancelled." ) - - local ProcessGroup = ProcessUnit:GetGroup() - - self.Menu = MENU_GROUP:New( ProcessGroup, "Task " .. self.TaskName .. " acceptance" ) - self.MenuAcceptTask = MENU_GROUP_COMMAND:New( ProcessGroup, "Accept task " .. self.TaskName, self.Menu, self.MenuAssign, self ) - self.MenuRejectTask = MENU_GROUP_COMMAND:New( ProcessGroup, "Reject task " .. self.TaskName, self.Menu, self.MenuReject, self ) - end - - --- Menu function. - -- @param #ACT_ASSIGN_MENU_ACCEPT self - function ACT_ASSIGN_MENU_ACCEPT:MenuAssign() - self:E( ) - - self:__Assign( 1 ) - end - - --- Menu function. - -- @param #ACT_ASSIGN_MENU_ACCEPT self - function ACT_ASSIGN_MENU_ACCEPT:MenuReject() - self:E( ) - - self:__Reject( 1 ) - end - - --- StateMachine callback function - -- @param #ACT_ASSIGN_MENU_ACCEPT self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ASSIGN_MENU_ACCEPT:onafterAssign( ProcessUnit, From, Event, To ) - self:E( { ProcessUnit.UnitNameFrom, Event, To } ) - - self.Menu:Remove() - end - - --- StateMachine callback function - -- @param #ACT_ASSIGN_MENU_ACCEPT self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ASSIGN_MENU_ACCEPT:onafterReject( ProcessUnit, From, Event, To ) - self:E( { ProcessUnit.UnitName, From, Event, To } ) - - self.Menu:Remove() - --TODO: need to resolve this problem ... it has to do with the events ... - --self.Task:UnAssignFromUnit( ProcessUnit )needs to become a callback funtion call upon the event - ProcessUnit:Destroy() - end - -end -- ACT_ASSIGN_MENU_ACCEPT ---- (SP) (MP) (FSM) Route AI or players through waypoints or to zones. --- --- === --- --- # @{#ACT_ROUTE} FSM class, extends @{Fsm#FSM_PROCESS} --- --- ## ACT_ROUTE state machine: --- --- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur. --- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below. --- Each derived class follows exactly the same process, using the same events and following the same state transitions, --- but will have **different implementation behaviour** upon each event or state transition. --- --- ### ACT_ROUTE **Events**: --- --- These are the events defined in this class: --- --- * **Start**: The process is started. The process will go into the Report state. --- * **Report**: The process is reporting to the player the route to be followed. --- * **Route**: The process is routing the controllable. --- * **Pause**: The process is pausing the route of the controllable. --- * **Arrive**: The controllable has arrived at a route point. --- * **More**: There are more route points that need to be followed. The process will go back into the Report state. --- * **NoMore**: There are no more route points that need to be followed. The process will go into the Success state. --- --- ### ACT_ROUTE **Event methods**: --- --- Event methods are available (dynamically allocated by the state machine), that accomodate for state transitions occurring in the process. --- There are two types of event methods, which you can use to influence the normal mechanisms in the state machine: --- --- * **Immediate**: The event method has exactly the name of the event. --- * **Delayed**: The event method starts with a __ + the name of the event. The first parameter of the event method is a number value, expressing the delay in seconds when the event will be executed. --- --- ### ACT_ROUTE **States**: --- --- * **None**: The controllable did not receive route commands. --- * **Arrived (*)**: The controllable has arrived at a route point. --- * **Aborted (*)**: The controllable has aborted the route path. --- * **Routing**: The controllable is understay to the route point. --- * **Pausing**: The process is pausing the routing. AI air will go into hover, AI ground will stop moving. Players can fly around. --- * **Success (*)**: All route points were reached. --- * **Failed (*)**: The process has failed. --- --- (*) End states of the process. --- --- ### ACT_ROUTE state transition methods: --- --- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. --- There are 2 moments when state transition methods will be called by the state machine: --- --- * **Before** the state transition. --- The state transition method needs to start with the name **OnBefore + the name of the state**. --- If the state transition method returns false, then the processing of the state transition will not be done! --- If you want to change the behaviour of the AIControllable at this event, return false, --- but then you'll need to specify your own logic using the AIControllable! --- --- * **After** the state transition. --- The state transition method needs to start with the name **OnAfter + the name of the state**. --- These state transition methods need to provide a return value, which is specified at the function description. --- --- === --- --- # 1) @{#ACT_ROUTE_ZONE} class, extends @{Fsm.Route#ACT_ROUTE} --- --- The ACT_ROUTE_ZONE class implements the core functions to route an AIR @{Controllable} player @{Unit} to a @{Zone}. --- The player receives on perioding times messages with the coordinates of the route to follow. --- Upon arrival at the zone, a confirmation of arrival is sent, and the process will be ended. --- --- # 1.1) ACT_ROUTE_ZONE constructor: --- --- * @{#ACT_ROUTE_ZONE.New}(): Creates a new ACT_ROUTE_ZONE object. --- --- === --- --- @module Route - - -do -- ACT_ROUTE - - --- ACT_ROUTE class - -- @type ACT_ROUTE - -- @field Tasking.Task#TASK TASK - -- @field Wrapper.Unit#UNIT ProcessUnit - -- @field Core.Zone#ZONE_BASE TargetZone - -- @extends Core.Fsm#FSM_PROCESS - ACT_ROUTE = { - ClassName = "ACT_ROUTE", - } - - - --- Creates a new routing state machine. The process will route a CLIENT to a ZONE until the CLIENT is within that ZONE. - -- @param #ACT_ROUTE self - -- @return #ACT_ROUTE self - function ACT_ROUTE:New() - - -- Inherits from BASE - local self = BASE:Inherit( self, FSM_PROCESS:New( "ACT_ROUTE" ) ) -- Core.Fsm#FSM_PROCESS - - self:AddTransition( "None", "Start", "Routing" ) - self:AddTransition( "*", "Report", "Reporting" ) - self:AddTransition( "*", "Route", "Routing" ) - self:AddTransition( "Routing", "Pause", "Pausing" ) - self:AddTransition( "*", "Abort", "Aborted" ) - self:AddTransition( "Routing", "Arrive", "Arrived" ) - self:AddTransition( "Arrived", "Success", "Success" ) - self:AddTransition( "*", "Fail", "Failed" ) - self:AddTransition( "", "", "" ) - self:AddTransition( "", "", "" ) - - self:AddEndState( "Arrived" ) - self:AddEndState( "Failed" ) - - self:SetStartState( "None" ) - - return self - end - - --- Task Events - - --- StateMachine callback function - -- @param #ACT_ROUTE self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ROUTE:onafterStart( ProcessUnit, From, Event, To ) - - - self:__Route( 1 ) - end - - --- Check if the controllable has arrived. - -- @param #ACT_ROUTE self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @return #boolean - function ACT_ROUTE:onfuncHasArrived( ProcessUnit ) - return false - end - - --- StateMachine callback function - -- @param #ACT_ROUTE self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ROUTE:onbeforeRoute( ProcessUnit, From, Event, To ) - self:F( { "BeforeRoute 1", self.DisplayCount, self.DisplayInterval } ) - - if ProcessUnit:IsAlive() then - self:F( "BeforeRoute 2" ) - local HasArrived = self:onfuncHasArrived( ProcessUnit ) -- Polymorphic - if self.DisplayCount >= self.DisplayInterval then - self:T( { HasArrived = HasArrived } ) - if not HasArrived then - self:Report() - end - self.DisplayCount = 1 - else - self.DisplayCount = self.DisplayCount + 1 - end - - self:T( { DisplayCount = self.DisplayCount } ) - - if HasArrived then - self:__Arrive( 1 ) - else - self:__Route( 1 ) - end - - return HasArrived -- if false, then the event will not be executed... - end - - return false - - end - -end -- ACT_ROUTE - - - -do -- ACT_ROUTE_ZONE - - --- ACT_ROUTE_ZONE class - -- @type ACT_ROUTE_ZONE - -- @field Tasking.Task#TASK TASK - -- @field Wrapper.Unit#UNIT ProcessUnit - -- @field Core.Zone#ZONE_BASE TargetZone - -- @extends #ACT_ROUTE - ACT_ROUTE_ZONE = { - ClassName = "ACT_ROUTE_ZONE", - } - - - --- Creates a new routing state machine. The task will route a controllable to a ZONE until the controllable is within that ZONE. - -- @param #ACT_ROUTE_ZONE self - -- @param Core.Zone#ZONE_BASE TargetZone - function ACT_ROUTE_ZONE:New( TargetZone ) - local self = BASE:Inherit( self, ACT_ROUTE:New() ) -- #ACT_ROUTE_ZONE - - self.TargetZone = TargetZone - - self.DisplayInterval = 30 - self.DisplayCount = 30 - self.DisplayMessage = true - self.DisplayTime = 10 -- 10 seconds is the default - - return self - end - - function ACT_ROUTE_ZONE:Init( FsmRoute ) - - self.TargetZone = FsmRoute.TargetZone - - self.DisplayInterval = 30 - self.DisplayCount = 30 - self.DisplayMessage = true - self.DisplayTime = 10 -- 10 seconds is the default - end - - --- Method override to check if the controllable has arrived. - -- @param #ACT_ROUTE self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @return #boolean - function ACT_ROUTE_ZONE:onfuncHasArrived( ProcessUnit ) - - if ProcessUnit:IsInZone( self.TargetZone ) then - local RouteText = "You have arrived within the zone." - self:Message( RouteText ) - end - - return ProcessUnit:IsInZone( self.TargetZone ) - end - - --- Task Events - - --- StateMachine callback function - -- @param #ACT_ROUTE_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ROUTE_ZONE:onenterReporting( ProcessUnit, From, Event, To ) - - local ZoneVec2 = self.TargetZone:GetVec2() - local ZonePointVec2 = POINT_VEC2:New( ZoneVec2.x, ZoneVec2.y ) - local TaskUnitVec2 = ProcessUnit:GetVec2() - local TaskUnitPointVec2 = POINT_VEC2:New( TaskUnitVec2.x, TaskUnitVec2.y ) - local RouteText = "Route to " .. TaskUnitPointVec2:GetBRText( ZonePointVec2 ) .. " km to target." - self:Message( RouteText ) - end - -end -- ACT_ROUTE_ZONE ---- (SP) (MP) (FSM) Account for (Detect, count and report) DCS events occuring on DCS objects (units). --- --- === --- --- # @{#ACT_ACCOUNT} FSM class, extends @{Fsm#FSM_PROCESS} --- --- ## ACT_ACCOUNT state machine: --- --- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur. --- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below. --- Each derived class follows exactly the same process, using the same events and following the same state transitions, --- but will have **different implementation behaviour** upon each event or state transition. --- --- ### ACT_ACCOUNT **Events**: --- --- These are the events defined in this class: --- --- * **Start**: The process is started. The process will go into the Report state. --- * **Event**: A relevant event has occured that needs to be accounted for. The process will go into the Account state. --- * **Report**: The process is reporting to the player the accounting status of the DCS events. --- * **More**: There are more DCS events that need to be accounted for. The process will go back into the Report state. --- * **NoMore**: There are no more DCS events that need to be accounted for. The process will go into the Success state. --- --- ### ACT_ACCOUNT **Event methods**: --- --- Event methods are available (dynamically allocated by the state machine), that accomodate for state transitions occurring in the process. --- There are two types of event methods, which you can use to influence the normal mechanisms in the state machine: --- --- * **Immediate**: The event method has exactly the name of the event. --- * **Delayed**: The event method starts with a __ + the name of the event. The first parameter of the event method is a number value, expressing the delay in seconds when the event will be executed. --- --- ### ACT_ACCOUNT **States**: --- --- * **Assigned**: The player is assigned to the task. This is the initialization state for the process. --- * **Waiting**: the process is waiting for a DCS event to occur within the simulator. This state is set automatically. --- * **Report**: The process is Reporting to the players in the group of the unit. This state is set automatically every 30 seconds. --- * **Account**: The relevant DCS event has occurred, and is accounted for. --- * **Success (*)**: All DCS events were accounted for. --- * **Failed (*)**: The process has failed. --- --- (*) End states of the process. --- --- ### ACT_ACCOUNT state transition methods: --- --- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. --- There are 2 moments when state transition methods will be called by the state machine: --- --- * **Before** the state transition. --- The state transition method needs to start with the name **OnBefore + the name of the state**. --- If the state transition method returns false, then the processing of the state transition will not be done! --- If you want to change the behaviour of the AIControllable at this event, return false, --- but then you'll need to specify your own logic using the AIControllable! --- --- * **After** the state transition. --- The state transition method needs to start with the name **OnAfter + the name of the state**. --- These state transition methods need to provide a return value, which is specified at the function description. --- --- # 1) @{#ACT_ACCOUNT_DEADS} FSM class, extends @{Fsm.Account#ACT_ACCOUNT} --- --- The ACT_ACCOUNT_DEADS class accounts (detects, counts and reports) successful kills of DCS units. --- The process is given a @{Set} of units that will be tracked upon successful destruction. --- The process will end after each target has been successfully destroyed. --- Each successful dead will trigger an Account state transition that can be scored, modified or administered. --- --- --- ## ACT_ACCOUNT_DEADS constructor: --- --- * @{#ACT_ACCOUNT_DEADS.New}(): Creates a new ACT_ACCOUNT_DEADS object. --- --- === --- --- @module Account - - -do -- ACT_ACCOUNT - - --- ACT_ACCOUNT class - -- @type ACT_ACCOUNT - -- @field Set#SET_UNIT TargetSetUnit - -- @extends Core.Fsm#FSM_PROCESS - ACT_ACCOUNT = { - ClassName = "ACT_ACCOUNT", - TargetSetUnit = nil, - } - - --- Creates a new DESTROY process. - -- @param #ACT_ACCOUNT self - -- @return #ACT_ACCOUNT - function ACT_ACCOUNT:New() - - -- Inherits from BASE - local self = BASE:Inherit( self, FSM_PROCESS:New() ) -- Core.Fsm#FSM_PROCESS - - self:AddTransition( "Assigned", "Start", "Waiting") - self:AddTransition( "*", "Wait", "Waiting") - self:AddTransition( "*", "Report", "Report") - self:AddTransition( "*", "Event", "Account") - self:AddTransition( "Account", "More", "Wait") - self:AddTransition( "Account", "NoMore", "Accounted") - self:AddTransition( "*", "Fail", "Failed") - - self:AddEndState( "Accounted" ) - self:AddEndState( "Failed" ) - - self:SetStartState( "Assigned" ) - - return self - end - - --- Process Events - - --- StateMachine callback function - -- @param #ACT_ACCOUNT self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ACCOUNT:onafterStart( ProcessUnit, From, Event, To ) - - self:HandleEvent( EVENTS.Dead, self.onfuncEventDead ) - - self:__Wait( 1 ) - end - - - --- StateMachine callback function - -- @param #ACT_ACCOUNT self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ACCOUNT:onenterWaiting( ProcessUnit, From, Event, To ) - - if self.DisplayCount >= self.DisplayInterval then - self:Report() - self.DisplayCount = 1 - else - self.DisplayCount = self.DisplayCount + 1 - end - - return true -- Process always the event. - end - - --- StateMachine callback function - -- @param #ACT_ACCOUNT self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ACCOUNT:onafterEvent( ProcessUnit, From, Event, To, Event ) - - self:__NoMore( 1 ) - end - -end -- ACT_ACCOUNT - -do -- ACT_ACCOUNT_DEADS - - --- ACT_ACCOUNT_DEADS class - -- @type ACT_ACCOUNT_DEADS - -- @field Set#SET_UNIT TargetSetUnit - -- @extends #ACT_ACCOUNT - ACT_ACCOUNT_DEADS = { - ClassName = "ACT_ACCOUNT_DEADS", - TargetSetUnit = nil, - } - - - --- Creates a new DESTROY process. - -- @param #ACT_ACCOUNT_DEADS self - -- @param Set#SET_UNIT TargetSetUnit - -- @param #string TaskName - function ACT_ACCOUNT_DEADS:New( TargetSetUnit, TaskName ) - -- Inherits from BASE - local self = BASE:Inherit( self, ACT_ACCOUNT:New() ) -- #ACT_ACCOUNT_DEADS - - self.TargetSetUnit = TargetSetUnit - self.TaskName = TaskName - - self.DisplayInterval = 30 - self.DisplayCount = 30 - self.DisplayMessage = true - self.DisplayTime = 10 -- 10 seconds is the default - self.DisplayCategory = "HQ" -- Targets is the default display category - - return self - end - - function ACT_ACCOUNT_DEADS:Init( FsmAccount ) - - self.TargetSetUnit = FsmAccount.TargetSetUnit - self.TaskName = FsmAccount.TaskName - end - - - - function ACT_ACCOUNT_DEADS:_Destructor() - self:E("_Destructor") - - self:EventRemoveAll() - - end - - --- Process Events - - --- StateMachine callback function - -- @param #ACT_ACCOUNT_DEADS self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ACCOUNT_DEADS:onenterReport( ProcessUnit, From, Event, To ) - self:E( { ProcessUnit, From, Event, To } ) - - self:Message( "Your group with assigned " .. self.TaskName .. " task has " .. self.TargetSetUnit:GetUnitTypesText() .. " targets left to be destroyed." ) - end - - - --- StateMachine callback function - -- @param #ACT_ACCOUNT_DEADS self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ACCOUNT_DEADS:onenterAccount( ProcessUnit, From, Event, To, EventData ) - self:T( { ProcessUnit, EventData, From, Event, To } ) - - self:T({self.Controllable}) - - self.TargetSetUnit:Flush() - - if self.TargetSetUnit:FindUnit( EventData.IniUnitName ) then - local TaskGroup = ProcessUnit:GetGroup() - self.TargetSetUnit:RemoveUnitsByName( EventData.IniUnitName ) - self:Message( "You hit a target. Your group with assigned " .. self.TaskName .. " task has " .. self.TargetSetUnit:Count() .. " targets ( " .. self.TargetSetUnit:GetUnitTypesText() .. " ) left to be destroyed." ) - end - end - - --- StateMachine callback function - -- @param #ACT_ACCOUNT_DEADS self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ACCOUNT_DEADS:onafterEvent( ProcessUnit, From, Event, To, EventData ) - - if self.TargetSetUnit:Count() > 0 then - self:__More( 1 ) - else - self:__NoMore( 1 ) - end - end - - --- DCS Events - - --- @param #ACT_ACCOUNT_DEADS self - -- @param Event#EVENTDATA EventData - function ACT_ACCOUNT_DEADS:onfuncEventDead( EventData ) - self:T( { "EventDead", EventData } ) - - if EventData.IniDCSUnit then - self:__Event( 1, EventData ) - end - end - -end -- ACT_ACCOUNT DEADS ---- (SP) (MP) (FSM) Route AI or players through waypoints or to zones. --- --- === --- --- # @{#ACT_ASSIST} FSM class, extends @{Fsm#FSM_PROCESS} --- --- ## ACT_ASSIST state machine: --- --- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur. --- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below. --- Each derived class follows exactly the same process, using the same events and following the same state transitions, --- but will have **different implementation behaviour** upon each event or state transition. --- --- ### ACT_ASSIST **Events**: --- --- These are the events defined in this class: --- --- * **Start**: The process is started. --- * **Next**: The process is smoking the targets in the given zone. --- --- ### ACT_ASSIST **Event methods**: --- --- Event methods are available (dynamically allocated by the state machine), that accomodate for state transitions occurring in the process. --- There are two types of event methods, which you can use to influence the normal mechanisms in the state machine: --- --- * **Immediate**: The event method has exactly the name of the event. --- * **Delayed**: The event method starts with a __ + the name of the event. The first parameter of the event method is a number value, expressing the delay in seconds when the event will be executed. --- --- ### ACT_ASSIST **States**: --- --- * **None**: The controllable did not receive route commands. --- * **AwaitSmoke (*)**: The process is awaiting to smoke the targets in the zone. --- * **Smoking (*)**: The process is smoking the targets in the zone. --- * **Failed (*)**: The process has failed. --- --- (*) End states of the process. --- --- ### ACT_ASSIST state transition methods: --- --- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. --- There are 2 moments when state transition methods will be called by the state machine: --- --- * **Before** the state transition. --- The state transition method needs to start with the name **OnBefore + the name of the state**. --- If the state transition method returns false, then the processing of the state transition will not be done! --- If you want to change the behaviour of the AIControllable at this event, return false, --- but then you'll need to specify your own logic using the AIControllable! --- --- * **After** the state transition. --- The state transition method needs to start with the name **OnAfter + the name of the state**. --- These state transition methods need to provide a return value, which is specified at the function description. --- --- === --- --- # 1) @{#ACT_ASSIST_SMOKE_TARGETS_ZONE} class, extends @{Fsm.Route#ACT_ASSIST} --- --- The ACT_ASSIST_SMOKE_TARGETS_ZONE class implements the core functions to smoke targets in a @{Zone}. --- The targets are smoked within a certain range around each target, simulating a realistic smoking behaviour. --- At random intervals, a new target is smoked. --- --- # 1.1) ACT_ASSIST_SMOKE_TARGETS_ZONE constructor: --- --- * @{#ACT_ASSIST_SMOKE_TARGETS_ZONE.New}(): Creates a new ACT_ASSIST_SMOKE_TARGETS_ZONE object. --- --- === --- --- @module Smoke - -do -- ACT_ASSIST - - --- ACT_ASSIST class - -- @type ACT_ASSIST - -- @extends Core.Fsm#FSM_PROCESS - ACT_ASSIST = { - ClassName = "ACT_ASSIST", - } - - --- Creates a new target smoking state machine. The process will request from the menu if it accepts the task, if not, the unit is removed from the simulator. - -- @param #ACT_ASSIST self - -- @return #ACT_ASSIST - function ACT_ASSIST:New() - - -- Inherits from BASE - local self = BASE:Inherit( self, FSM_PROCESS:New( "ACT_ASSIST" ) ) -- Core.Fsm#FSM_PROCESS - - self:AddTransition( "None", "Start", "AwaitSmoke" ) - self:AddTransition( "AwaitSmoke", "Next", "Smoking" ) - self:AddTransition( "Smoking", "Next", "AwaitSmoke" ) - self:AddTransition( "*", "Stop", "Success" ) - self:AddTransition( "*", "Fail", "Failed" ) - - self:AddEndState( "Failed" ) - self:AddEndState( "Success" ) - - self:SetStartState( "None" ) - - return self - end - - --- Task Events - - --- StateMachine callback function - -- @param #ACT_ASSIST self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ASSIST:onafterStart( ProcessUnit, From, Event, To ) - - local ProcessGroup = ProcessUnit:GetGroup() - local MissionMenu = self:GetMission():GetMissionMenu( ProcessGroup ) - - local function MenuSmoke( MenuParam ) - self:E( MenuParam ) - local self = MenuParam.self - local SmokeColor = MenuParam.SmokeColor - self.SmokeColor = SmokeColor - self:__Next( 1 ) - end - - self.Menu = MENU_GROUP:New( ProcessGroup, "Target acquisition", MissionMenu ) - self.MenuSmokeBlue = MENU_GROUP_COMMAND:New( ProcessGroup, "Drop blue smoke on targets", self.Menu, MenuSmoke, { self = self, SmokeColor = SMOKECOLOR.Blue } ) - self.MenuSmokeGreen = MENU_GROUP_COMMAND:New( ProcessGroup, "Drop green smoke on targets", self.Menu, MenuSmoke, { self = self, SmokeColor = SMOKECOLOR.Green } ) - self.MenuSmokeOrange = MENU_GROUP_COMMAND:New( ProcessGroup, "Drop Orange smoke on targets", self.Menu, MenuSmoke, { self = self, SmokeColor = SMOKECOLOR.Orange } ) - self.MenuSmokeRed = MENU_GROUP_COMMAND:New( ProcessGroup, "Drop Red smoke on targets", self.Menu, MenuSmoke, { self = self, SmokeColor = SMOKECOLOR.Red } ) - self.MenuSmokeWhite = MENU_GROUP_COMMAND:New( ProcessGroup, "Drop White smoke on targets", self.Menu, MenuSmoke, { self = self, SmokeColor = SMOKECOLOR.White } ) - end - -end - -do -- ACT_ASSIST_SMOKE_TARGETS_ZONE - - --- ACT_ASSIST_SMOKE_TARGETS_ZONE class - -- @type ACT_ASSIST_SMOKE_TARGETS_ZONE - -- @field Set#SET_UNIT TargetSetUnit - -- @field Core.Zone#ZONE_BASE TargetZone - -- @extends #ACT_ASSIST - ACT_ASSIST_SMOKE_TARGETS_ZONE = { - ClassName = "ACT_ASSIST_SMOKE_TARGETS_ZONE", - } - --- function ACT_ASSIST_SMOKE_TARGETS_ZONE:_Destructor() --- self:E("_Destructor") --- --- self.Menu:Remove() --- self:EventRemoveAll() --- end - - --- Creates a new target smoking state machine. The process will request from the menu if it accepts the task, if not, the unit is removed from the simulator. - -- @param #ACT_ASSIST_SMOKE_TARGETS_ZONE self - -- @param Set#SET_UNIT TargetSetUnit - -- @param Core.Zone#ZONE_BASE TargetZone - function ACT_ASSIST_SMOKE_TARGETS_ZONE:New( TargetSetUnit, TargetZone ) - local self = BASE:Inherit( self, ACT_ASSIST:New() ) -- #ACT_ASSIST - - self.TargetSetUnit = TargetSetUnit - self.TargetZone = TargetZone - - return self - end - - function ACT_ASSIST_SMOKE_TARGETS_ZONE:Init( FsmSmoke ) - - self.TargetSetUnit = FsmSmoke.TargetSetUnit - self.TargetZone = FsmSmoke.TargetZone - end - - --- Creates a new target smoking state machine. The process will request from the menu if it accepts the task, if not, the unit is removed from the simulator. - -- @param #ACT_ASSIST_SMOKE_TARGETS_ZONE self - -- @param Set#SET_UNIT TargetSetUnit - -- @param Core.Zone#ZONE_BASE TargetZone - -- @return #ACT_ASSIST_SMOKE_TARGETS_ZONE self - function ACT_ASSIST_SMOKE_TARGETS_ZONE:Init( TargetSetUnit, TargetZone ) - - self.TargetSetUnit = TargetSetUnit - self.TargetZone = TargetZone - - return self - end - - --- StateMachine callback function - -- @param #ACT_ASSIST_SMOKE_TARGETS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ASSIST_SMOKE_TARGETS_ZONE:onenterSmoking( ProcessUnit, From, Event, To ) - - self.TargetSetUnit:ForEachUnit( - --- @param Wrapper.Unit#UNIT SmokeUnit - function( SmokeUnit ) - if math.random( 1, ( 100 * self.TargetSetUnit:Count() ) / 4 ) <= 100 then - SCHEDULER:New( self, - function() - if SmokeUnit:IsAlive() then - SmokeUnit:Smoke( self.SmokeColor, 150 ) - end - end, {}, math.random( 10, 60 ) - ) - end - end - ) - - end - -end--- A COMMANDCENTER is the owner of multiple missions within MOOSE. --- A COMMANDCENTER governs multiple missions, the tasking and the reporting. --- @module CommandCenter - - - ---- The REPORT class --- @type REPORT --- @extends Core.Base#BASE -REPORT = { - ClassName = "REPORT", -} - ---- Create a new REPORT. --- @param #REPORT self --- @param #string Title --- @return #REPORT -function REPORT:New( Title ) - - local self = BASE:Inherit( self, BASE:New() ) - - self.Report = {} - self.Report[#self.Report+1] = Title - - return self -end - ---- Add a new line to a REPORT. --- @param #REPORT self --- @param #string Text --- @return #REPORT -function REPORT:Add( Text ) - self.Report[#self.Report+1] = Text - return self.Report[#self.Report+1] -end - -function REPORT:Text() - return table.concat( self.Report, "\n" ) -end - ---- The COMMANDCENTER class --- @type COMMANDCENTER --- @field Wrapper.Group#GROUP HQ --- @field Dcs.DCSCoalitionWrapper.Object#coalition CommandCenterCoalition --- @list Missions --- @extends Core.Base#BASE -COMMANDCENTER = { - ClassName = "COMMANDCENTER", - CommandCenterName = "", - CommandCenterCoalition = nil, - CommandCenterPositionable = nil, - Name = "", -} ---- The constructor takes an IDENTIFIABLE as the HQ command center. --- @param #COMMANDCENTER self --- @param Wrapper.Positionable#POSITIONABLE CommandCenterPositionable --- @param #string CommandCenterName --- @return #COMMANDCENTER -function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName ) - - local self = BASE:Inherit( self, BASE:New() ) - - self.CommandCenterPositionable = CommandCenterPositionable - self.CommandCenterName = CommandCenterName or CommandCenterPositionable:GetName() - self.CommandCenterCoalition = CommandCenterPositionable:GetCoalition() - - self.Missions = {} - - self:HandleEvent( EVENTS.Birth, - --- @param #COMMANDCENTER self - --- @param Core.Event#EVENTDATA EventData - function( self, EventData ) - self:E( { EventData } ) - local EventGroup = GROUP:Find( EventData.IniDCSGroup ) - if EventGroup and self:HasGroup( EventGroup ) then - local MenuReporting = MENU_GROUP:New( EventGroup, "Reporting", self.CommandCenterMenu ) - local MenuMissionsSummary = MENU_GROUP_COMMAND:New( EventGroup, "Missions Summary Report", MenuReporting, self.ReportSummary, self, EventGroup ) - local MenuMissionsDetails = MENU_GROUP_COMMAND:New( EventGroup, "Missions Details Report", MenuReporting, self.ReportDetails, self, EventGroup ) - self:ReportSummary( EventGroup ) - end - local PlayerUnit = EventData.IniUnit - for MissionID, Mission in pairs( self:GetMissions() ) do - local Mission = Mission -- Tasking.Mission#MISSION - local PlayerGroup = EventData.IniGroup -- The GROUP object should be filled! - Mission:JoinUnit( PlayerUnit, PlayerGroup ) - Mission:ReportDetails() - end - - end - ) - - -- When a player enters a client or a unit, the CommandCenter will check for each Mission and each Task in the Mission if the player has things to do. - -- For these elements, it will= - -- - Set the correct menu. - -- - Assign the PlayerUnit to the Task if required. - -- - Send a message to the other players in the group that this player has joined. - self:HandleEvent( EVENTS.PlayerEnterUnit, - --- @param #COMMANDCENTER self - -- @param Core.Event#EVENTDATA EventData - function( self, EventData ) - local PlayerUnit = EventData.IniUnit - for MissionID, Mission in pairs( self:GetMissions() ) do - local Mission = Mission -- Tasking.Mission#MISSION - local PlayerGroup = EventData.IniGroup -- The GROUP object should be filled! - Mission:JoinUnit( PlayerUnit, PlayerGroup ) - Mission:ReportDetails() - end - end - ) - - -- Handle when a player leaves a slot and goes back to spectators ... - -- The PlayerUnit will be UnAssigned from the Task. - -- When there is no Unit left running the Task, the Task goes into Abort... - self:HandleEvent( EVENTS.PlayerLeaveUnit, - --- @param #TASK self - -- @param Core.Event#EVENTDATA EventData - function( self, EventData ) - local PlayerUnit = EventData.IniUnit - for MissionID, Mission in pairs( self:GetMissions() ) do - local Mission = Mission -- Tasking.Mission#MISSION - Mission:AbortUnit( PlayerUnit ) - end - end - ) - - -- Handle when a player leaves a slot and goes back to spectators ... - -- The PlayerUnit will be UnAssigned from the Task. - -- When there is no Unit left running the Task, the Task goes into Abort... - self:HandleEvent( EVENTS.Crash, - --- @param #TASK self - -- @param Core.Event#EVENTDATA EventData - function( self, EventData ) - local PlayerUnit = EventData.IniUnit - for MissionID, Mission in pairs( self:GetMissions() ) do - Mission:CrashUnit( PlayerUnit ) - end - end - ) - - return self -end - ---- Gets the name of the HQ command center. --- @param #COMMANDCENTER self --- @return #string -function COMMANDCENTER:GetName() - - return self.CommandCenterName -end - ---- Gets the POSITIONABLE of the HQ command center. --- @param #COMMANDCENTER self --- @return Wrapper.Positionable#POSITIONABLE -function COMMANDCENTER:GetPositionable() - return self.CommandCenterPositionable -end - ---- Get the Missions governed by the HQ command center. --- @param #COMMANDCENTER self --- @return #list -function COMMANDCENTER:GetMissions() - - return self.Missions -end - ---- Add a MISSION to be governed by the HQ command center. --- @param #COMMANDCENTER self --- @param Tasking.Mission#MISSION Mission --- @return Tasking.Mission#MISSION -function COMMANDCENTER:AddMission( Mission ) - - self.Missions[Mission] = Mission - - return Mission -end - ---- Removes a MISSION to be governed by the HQ command center. --- The given Mission is not nilified. --- @param #COMMANDCENTER self --- @param Tasking.Mission#MISSION Mission --- @return Tasking.Mission#MISSION -function COMMANDCENTER:RemoveMission( Mission ) - - self.Missions[Mission] = nil - - return Mission -end - ---- Sets the menu structure of the Missions governed by the HQ command center. --- @param #COMMANDCENTER self -function COMMANDCENTER:SetMenu() - self:F() - - self.CommandCenterMenu = self.CommandCenterMenu or MENU_COALITION:New( self.CommandCenterCoalition, "Command Center (" .. self:GetName() .. ")" ) - - for MissionID, Mission in pairs( self:GetMissions() ) do - local Mission = Mission -- Tasking.Mission#MISSION - Mission:RemoveMenu() - end - - for MissionID, Mission in pairs( self:GetMissions() ) do - local Mission = Mission -- Tasking.Mission#MISSION - Mission:SetMenu() - end -end - - ---- Checks of the COMMANDCENTER has a GROUP. --- @param #COMMANDCENTER self --- @param Wrapper.Group#GROUP --- @return #boolean -function COMMANDCENTER:HasGroup( MissionGroup ) - - local Has = false - - for MissionID, Mission in pairs( self.Missions ) do - local Mission = Mission -- Tasking.Mission#MISSION - if Mission:HasGroup( MissionGroup ) then - Has = true - break - end - end - - return Has -end - ---- Send a CC message to a GROUP. --- @param #COMMANDCENTER self --- @param #string Message --- @param Wrapper.Group#GROUP TaskGroup --- @param #sring Name (optional) The name of the Group used as a prefix for the message to the Group. If not provided, there will be nothing shown. -function COMMANDCENTER:MessageToGroup( Message, TaskGroup, Name ) - - local Prefix = Name and "@ Group (" .. Name .. "): " or '' - Message = Prefix .. Message - self:GetPositionable():MessageToGroup( Message , 20, TaskGroup, self:GetName() ) - -end - ---- Send a CC message to the coalition of the CC. --- @param #COMMANDCENTER self -function COMMANDCENTER:MessageToCoalition( Message ) - - local CCCoalition = self:GetPositionable():GetCoalition() - --TODO: Fix coalition bug! - self:GetPositionable():MessageToCoalition( Message, 20, CCCoalition, self:GetName() ) - -end - ---- Report the status of all MISSIONs to a GROUP. --- Each Mission is listed, with an indication how many Tasks are still to be completed. --- @param #COMMANDCENTER self -function COMMANDCENTER:ReportSummary( ReportGroup ) - self:E( ReportGroup ) - - local Report = REPORT:New() - - for MissionID, Mission in pairs( self.Missions ) do - local Mission = Mission -- Tasking.Mission#MISSION - Report:Add( " - " .. Mission:ReportOverview() ) - end - - self:GetPositionable():MessageToGroup( Report:Text(), 30, ReportGroup ) - -end - ---- Report the status of a Task to a Group. --- Report the details of a Mission, listing the Mission, and all the Task details. --- @param #COMMANDCENTER self -function COMMANDCENTER:ReportDetails( ReportGroup, Task ) - self:E( ReportGroup ) - - local Report = REPORT:New() - - for MissionID, Mission in pairs( self.Missions ) do - local Mission = Mission -- Tasking.Mission#MISSION - Report:Add( " - " .. Mission:ReportDetails() ) - end - - self:GetPositionable():MessageToGroup( Report:Text(), 30, ReportGroup ) -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 --- @field #MISSION.Clients _Clients --- @field Core.Menu#MENU_COALITION MissionMenu --- @field #string MissionBriefing --- @extends Core.Fsm#FSM -MISSION = { - ClassName = "MISSION", - Name = "", - MissionStatus = "PENDING", - _Clients = {}, - TaskMenus = {}, - TaskCategoryMenus = {}, - TaskTypeMenus = {}, - _ActiveTasks = {}, - GoalFunction = nil, - MissionReportTrigger = 0, - MissionProgressTrigger = 0, - MissionReportShow = false, - MissionReportFlash = false, - MissionTimeInterval = 0, - MissionCoalition = "", - SUCCESS = 1, - FAILED = 2, - REPEAT = 3, - _GoalTasks = {} -} - ---- This is the main MISSION declaration method. Each Mission is like the master or a Mission orchestration between, Clients, Tasks, Stages etc. --- @param #MISSION self --- @param Tasking.CommandCenter#COMMANDCENTER CommandCenter --- @param #string MissionName is the name of the mission. This name will be used to reference the status of each mission by the players. --- @param #string MissionPriority is a string indicating the "priority" of the Mission. f.e. "Primary", "Secondary" or "First", "Second". It is free format and up to the Mission designer to choose. There are no rules behind this field. --- @param #string MissionBriefing is a string indicating the mission briefing to be shown when a player joins a @{CLIENT}. --- @param Dcs.DCSCoalitionWrapper.Object#coalition MissionCoalition is a string indicating the coalition or party to which this mission belongs to. It is free format and can be chosen freely by the mission designer. Note that this field is not to be confused with the coalition concept of the ME. Examples of a Mission Coalition could be "NATO", "CCCP", "Intruders", "Terrorists"... --- @return #MISSION self -function MISSION:New( CommandCenter, MissionName, MissionPriority, MissionBriefing, MissionCoalition ) - - local self = BASE:Inherit( self, FSM:New() ) -- Core.Fsm#FSM - - self:SetStartState( "Idle" ) - - self:AddTransition( "Idle", "Start", "Ongoing" ) - self:AddTransition( "Ongoing", "Stop", "Idle" ) - self:AddTransition( "Ongoing", "Complete", "Completed" ) - self:AddTransition( "*", "Fail", "Failed" ) - - self:T( { MissionName, MissionPriority, MissionBriefing, MissionCoalition } ) - - self.CommandCenter = CommandCenter - CommandCenter:AddMission( self ) - - self.Name = MissionName - self.MissionPriority = MissionPriority - self.MissionBriefing = MissionBriefing - self.MissionCoalition = MissionCoalition - - self.Tasks = {} - - return self -end - ---- FSM function for a MISSION --- @param #MISSION self --- @param #string Event --- @param #string From --- @param #string To -function MISSION:onbeforeComplete( From, Event, To ) - - for TaskID, Task in pairs( self:GetTasks() ) do - local Task = Task -- Tasking.Task#TASK - if not Task:IsStateSuccess() and not Task:IsStateFailed() and not Task:IsStateAborted() and not Task:IsStateCancelled() then - return false -- Mission cannot be completed. Other Tasks are still active. - end - end - return true -- Allow Mission completion. -end - ---- FSM function for a MISSION --- @param #MISSION self --- @param #string Event --- @param #string From --- @param #string To -function MISSION:onenterCompleted( From, Event, To ) - - self:GetCommandCenter():MessageToCoalition( "Mission " .. self:GetName() .. " has been completed! Good job guys!" ) -end - ---- Gets the mission name. --- @param #MISSION self --- @return #MISSION self -function MISSION:GetName() - return self.Name -end - ---- Add a Unit to join the Mission. --- For each Task within the Mission, the Unit is joined with the Task. --- If the Unit was not part of a Task in the Mission, false is returned. --- If the Unit is part of a Task in the Mission, true is returned. --- @param #MISSION self --- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player joining the Mission. --- @param Wrapper.Group#GROUP PlayerGroup The GROUP of the player joining the Mission. --- @return #boolean true if Unit is part of a Task in the Mission. -function MISSION:JoinUnit( PlayerUnit, PlayerGroup ) - self:F( { PlayerUnit = PlayerUnit, PlayerGroup = PlayerGroup } ) - - local PlayerUnitAdded = false - - for TaskID, Task in pairs( self:GetTasks() ) do - local Task = Task -- Tasking.Task#TASK - if Task:JoinUnit( PlayerUnit, PlayerGroup ) then - PlayerUnitAdded = true - end - end - - return PlayerUnitAdded -end - ---- Aborts a PlayerUnit from the Mission. --- For each Task within the Mission, the PlayerUnit is removed from Task where it is assigned. --- If the Unit was not part of a Task in the Mission, false is returned. --- If the Unit is part of a Task in the Mission, true is returned. --- @param #MISSION self --- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player joining the Mission. --- @return #boolean true if Unit is part of a Task in the Mission. -function MISSION:AbortUnit( PlayerUnit ) - self:F( { PlayerUnit = PlayerUnit } ) - - local PlayerUnitRemoved = false - - for TaskID, Task in pairs( self:GetTasks() ) do - if Task:AbortUnit( PlayerUnit ) then - PlayerUnitRemoved = true - end - end - - return PlayerUnitRemoved -end - ---- Handles a crash of a PlayerUnit from the Mission. --- For each Task within the Mission, the PlayerUnit is removed from Task where it is assigned. --- If the Unit was not part of a Task in the Mission, false is returned. --- If the Unit is part of a Task in the Mission, true is returned. --- @param #MISSION self --- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player crashing. --- @return #boolean true if Unit is part of a Task in the Mission. -function MISSION:CrashUnit( PlayerUnit ) - self:F( { PlayerUnit = PlayerUnit } ) - - local PlayerUnitRemoved = false - - for TaskID, Task in pairs( self:GetTasks() ) do - if Task:CrashUnit( PlayerUnit ) then - PlayerUnitRemoved = true - end - end - - return PlayerUnitRemoved -end - ---- Add a scoring to the mission. --- @param #MISSION self --- @return #MISSION self -function MISSION:AddScoring( Scoring ) - self.Scoring = Scoring - return self -end - ---- Get the scoring object of a mission. --- @param #MISSION self --- @return #SCORING Scoring -function MISSION:GetScoring() - return self.Scoring -end - ---- Get the groups for which TASKS are given in the mission --- @param #MISSION self --- @return Core.Set#SET_GROUP -function MISSION:GetGroups() - - local SetGroup = SET_GROUP:New() - - for TaskID, Task in pairs( self:GetTasks() ) do - local Task = Task -- Tasking.Task#TASK - local GroupSet = Task:GetGroups() - GroupSet:ForEachGroup( - function( TaskGroup ) - SetGroup:Add( TaskGroup, TaskGroup ) - end - ) - end - - return SetGroup - -end - - ---- Sets the Planned Task menu. --- @param #MISSION self -function MISSION:SetMenu() - self:F() - - for _, Task in pairs( self:GetTasks() ) do - local Task = Task -- Tasking.Task#TASK - Task:SetMenu() - end -end - ---- Removes the Planned Task menu. --- @param #MISSION self -function MISSION:RemoveMenu() - self:F() - - for _, Task in pairs( self:GetTasks() ) do - local Task = Task -- Tasking.Task#TASK - Task:RemoveMenu() - end -end - - ---- Gets the COMMANDCENTER. --- @param #MISSION self --- @return Tasking.CommandCenter#COMMANDCENTER -function MISSION:GetCommandCenter() - return self.CommandCenter -end - ---- Sets the Assigned Task menu. --- @param #MISSION self --- @param Tasking.Task#TASK Task --- @param #string MenuText The menu text. --- @return #MISSION self -function MISSION:SetAssignedMenu( Task ) - - for _, Task in pairs( self.Tasks ) do - local Task = Task -- Tasking.Task#TASK - Task:RemoveMenu() - Task:SetAssignedMenu() - end - -end - ---- Removes a Task menu. --- @param #MISSION self --- @param Tasking.Task#TASK Task --- @return #MISSION self -function MISSION:RemoveTaskMenu( Task ) - - Task:RemoveMenu() -end - - ---- Gets the mission menu for the coalition. --- @param #MISSION self --- @param Wrapper.Group#GROUP TaskGroup --- @return Core.Menu#MENU_COALITION self -function MISSION:GetMissionMenu( TaskGroup ) - - local CommandCenter = self:GetCommandCenter() - local CommandCenterMenu = CommandCenter.CommandCenterMenu - - local MissionName = self:GetName() - - local TaskGroupName = TaskGroup:GetName() - local MissionMenu = MENU_GROUP:New( TaskGroup, MissionName, CommandCenterMenu ) - - return MissionMenu -end - - ---- Clears the mission menu for the coalition. --- @param #MISSION self --- @return #MISSION self -function MISSION:ClearMissionMenu() - self.MissionMenu:Remove() - self.MissionMenu = nil -end - ---- Get the TASK identified by the TaskNumber from the Mission. This function is useful in GoalFunctions. --- @param #string TaskName The Name of the @{Task} within the @{Mission}. --- @return Tasking.Task#TASK The Task --- @return #nil Returns nil if no task was found. -function MISSION:GetTask( TaskName ) - self:F( { TaskName } ) - - return self.Tasks[TaskName] -end - - ---- Register a @{Task} to be completed within the @{Mission}. --- Note that there can be multiple @{Task}s registered to be completed. --- Each Task can be set a certain Goals. The Mission will not be completed until all Goals are reached. --- @param #MISSION self --- @param Tasking.Task#TASK Task is the @{Task} object. --- @return Tasking.Task#TASK The task added. -function MISSION:AddTask( Task ) - - local TaskName = Task:GetTaskName() - self:F( TaskName ) - - self.Tasks[TaskName] = self.Tasks[TaskName] or { n = 0 } - - self.Tasks[TaskName] = Task - - self:GetCommandCenter():SetMenu() - - return Task -end - ---- Removes a @{Task} to be completed within the @{Mission}. --- Note that there can be multiple @{Task}s registered to be completed. --- Each Task can be set a certain Goals. The Mission will not be completed until all Goals are reached. --- @param #MISSION self --- @param Tasking.Task#TASK Task is the @{Task} object. --- @return #nil The cleaned Task reference. -function MISSION:RemoveTask( Task ) - - local TaskName = Task:GetTaskName() - - self:F( TaskName ) - self.Tasks[TaskName] = self.Tasks[TaskName] or { n = 0 } - - -- Ensure everything gets garbarge collected. - self.Tasks[TaskName] = nil - Task = nil - - collectgarbage() - - self:GetCommandCenter():SetMenu() - - return nil -end - ---- Return the next @{Task} ID to be completed within the @{Mission}. --- @param #MISSION self --- @param Tasking.Task#TASK Task is the @{Task} object. --- @return Tasking.Task#TASK The task added. -function MISSION:GetNextTaskID( Task ) - - local TaskName = Task:GetTaskName() - self:F( TaskName ) - self.Tasks[TaskName] = self.Tasks[TaskName] or { n = 0 } - - self.Tasks[TaskName].n = self.Tasks[TaskName].n + 1 - - return self.Tasks[TaskName].n -end - - - ---- old stuff - ---- 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 - -function MISSION:HasGroup( TaskGroup ) - local Has = false - - for TaskID, Task in pairs( self:GetTasks() ) do - local Task = Task -- Tasking.Task#TASK - if Task:HasGroup( TaskGroup ) then - Has = true - break - end - end - - return Has -end - ---- Create a summary report of the Mission (one line). --- @param #MISSION self --- @return #string -function MISSION:ReportSummary() - - local Report = REPORT:New() - - -- List the name of the mission. - local Name = self:GetName() - - -- Determine the status of the mission. - local Status = self:GetState() - - -- Determine how many tasks are remaining. - local TasksRemaining = 0 - for TaskID, Task in pairs( self:GetTasks() ) do - local Task = Task -- Tasking.Task#TASK - if Task:IsStateSuccess() or Task:IsStateFailed() then - else - TasksRemaining = TasksRemaining + 1 - end - end - - Report:Add( "Mission " .. Name .. " - " .. Status .. " - " .. TasksRemaining .. " tasks remaining." ) - - return Report:Text() -end - ---- Create a overview report of the Mission (multiple lines). --- @param #MISSION self --- @return #string -function MISSION:ReportOverview() - - local Report = REPORT:New() - - -- List the name of the mission. - local Name = self:GetName() - - -- Determine the status of the mission. - local Status = self:GetState() - - Report:Add( "Mission " .. Name .. " - State '" .. Status .. "'" ) - - -- Determine how many tasks are remaining. - local TasksRemaining = 0 - for TaskID, Task in pairs( self:GetTasks() ) do - local Task = Task -- Tasking.Task#TASK - Report:Add( "- " .. Task:ReportSummary() ) - end - - return Report:Text() -end - ---- Create a detailed report of the Mission, listing all the details of the Task. --- @param #MISSION self --- @return #string -function MISSION:ReportDetails() - - local Report = REPORT:New() - - -- List the name of the mission. - local Name = self:GetName() - - -- Determine the status of the mission. - local Status = self:GetState() - - Report:Add( "Mission " .. Name .. " - State '" .. Status .. "'" ) - - -- Determine how many tasks are remaining. - local TasksRemaining = 0 - for TaskID, Task in pairs( self:GetTasks() ) do - local Task = Task -- Tasking.Task#TASK - Report:Add( Task:ReportDetails() ) - end - - return Report:Text() -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 - - ---- 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 -- Wrapper.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 - ---- This module contains the TASK class. --- --- 1) @{#TASK} class, extends @{Base#BASE} --- ============================================ --- 1.1) The @{#TASK} class implements the methods for task orchestration within MOOSE. --- ---------------------------------------------------------------------------------------- --- The class provides a couple of methods to: --- --- * @{#TASK.AssignToGroup}():Assign a task to a group (of players). --- * @{#TASK.AddProcess}():Add a @{Process} to a task. --- * @{#TASK.RemoveProcesses}():Remove a running @{Process} from a running task. --- * @{#TASK.SetStateMachine}():Set a @{Fsm} to a task. --- * @{#TASK.RemoveStateMachine}():Remove @{Fsm} from a task. --- * @{#TASK.HasStateMachine}():Enquire if the task has a @{Fsm} --- * @{#TASK.AssignToUnit}(): Assign a task to a unit. (Needs to be implemented in the derived classes from @{#TASK}. --- * @{#TASK.UnAssignFromUnit}(): Unassign the task from a unit. --- * @{#TASK.SetTimeOut}(): Set timer in seconds before task gets cancelled if not assigned. --- --- 1.2) Set and enquire task status (beyond the task state machine processing). --- ---------------------------------------------------------------------------- --- A task needs to implement as a minimum the following task states: --- --- * **Success**: Expresses the successful execution and finalization of the task. --- * **Failed**: Expresses the failure of a task. --- * **Planned**: Expresses that the task is created, but not yet in execution and is not assigned yet. --- * **Assigned**: Expresses that the task is assigned to a Group of players, and that the task is in execution mode. --- --- A task may also implement the following task states: --- --- * **Rejected**: Expresses that the task is rejected by a player, who was requested to accept the task. --- * **Cancelled**: Expresses that the task is cancelled by HQ or through a logical situation where a cancellation of the task is required. --- --- A task can implement more statusses than the ones outlined above. Please consult the documentation of the specific tasks to understand the different status modelled. --- --- The status of tasks can be set by the methods **State** followed by the task status. An example is `StateAssigned()`. --- The status of tasks can be enquired by the methods **IsState** followed by the task status name. An example is `if IsStateAssigned() then`. --- --- 1.3) Add scoring when reaching a certain task status: --- ----------------------------------------------------- --- Upon reaching a certain task status in a task, additional scoring can be given. If the Mission has a scoring system attached, the scores will be added to the mission scoring. --- Use the method @{#TASK.AddScore}() to add scores when a status is reached. --- --- 1.4) Task briefing: --- ------------------- --- A task briefing can be given that is shown to the player when he is assigned to the task. --- --- === --- --- ### Authors: FlightControl - Design and Programming --- --- @module Task - ---- The TASK class --- @type TASK --- @field Core.Scheduler#SCHEDULER TaskScheduler --- @field Tasking.Mission#MISSION Mission --- @field Core.Set#SET_GROUP SetGroup The Set of Groups assigned to the Task --- @field Core.Fsm#FSM_PROCESS FsmTemplate --- @field Tasking.Mission#MISSION Mission --- @field Tasking.CommandCenter#COMMANDCENTER CommandCenter --- @extends Core.Fsm#FSM_TASK -TASK = { - ClassName = "TASK", - TaskScheduler = nil, - ProcessClasses = {}, -- The container of the Process classes that will be used to create and assign new processes for the task to ProcessUnits. - Processes = {}, -- The container of actual process objects instantiated and assigned to ProcessUnits. - Players = nil, - Scores = {}, - Menu = {}, - SetGroup = nil, - FsmTemplate = nil, - Mission = nil, - CommandCenter = nil, - TimeOut = 0, -} - ---- FSM PlayerAborted event handler prototype for TASK. --- @function [parent=#TASK] OnAfterPlayerAborted --- @param #TASK self --- @param Wrapper.Unit#UNIT PlayerUnit The Unit of the Player when he went back to spectators or left the mission. --- @param #string PlayerName The name of the Player. - ---- FSM PlayerCrashed event handler prototype for TASK. --- @function [parent=#TASK] OnAfterPlayerCrashed --- @param #TASK self --- @param Wrapper.Unit#UNIT PlayerUnit The Unit of the Player when he crashed in the mission. --- @param #string PlayerName The name of the Player. - ---- FSM PlayerDead event handler prototype for TASK. --- @function [parent=#TASK] OnAfterPlayerDead --- @param #TASK self --- @param Wrapper.Unit#UNIT PlayerUnit The Unit of the Player when he died in the mission. --- @param #string PlayerName The name of the Player. - ---- FSM Fail synchronous event function for TASK. --- Use this event to Fail the Task. --- @function [parent=#TASK] Fail --- @param #TASK self - ---- FSM Fail asynchronous event function for TASK. --- Use this event to Fail the Task. --- @function [parent=#TASK] __Fail --- @param #TASK self - ---- FSM Abort synchronous event function for TASK. --- Use this event to Abort the Task. --- @function [parent=#TASK] Abort --- @param #TASK self - ---- FSM Abort asynchronous event function for TASK. --- Use this event to Abort the Task. --- @function [parent=#TASK] __Abort --- @param #TASK self - ---- FSM Success synchronous event function for TASK. --- Use this event to make the Task a Success. --- @function [parent=#TASK] Success --- @param #TASK self - ---- FSM Success asynchronous event function for TASK. --- Use this event to make the Task a Success. --- @function [parent=#TASK] __Success --- @param #TASK self - ---- FSM Cancel synchronous event function for TASK. --- Use this event to Cancel the Task. --- @function [parent=#TASK] Cancel --- @param #TASK self - ---- FSM Cancel asynchronous event function for TASK. --- Use this event to Cancel the Task. --- @function [parent=#TASK] __Cancel --- @param #TASK self - ---- FSM Replan synchronous event function for TASK. --- Use this event to Replan the Task. --- @function [parent=#TASK] Replan --- @param #TASK self - ---- FSM Replan asynchronous event function for TASK. --- Use this event to Replan the Task. --- @function [parent=#TASK] __Replan --- @param #TASK self - - ---- Instantiates a new TASK. Should never be used. Interface Class. --- @param #TASK self --- @param Tasking.Mission#MISSION Mission The mission wherein the Task is registered. --- @param Core.Set#SET_GROUP SetGroupAssign The set of groups for which the Task can be assigned. --- @param #string TaskName The name of the Task --- @param #string TaskType The type of the Task --- @return #TASK self -function TASK:New( Mission, SetGroupAssign, TaskName, TaskType ) - - local self = BASE:Inherit( self, FSM_TASK:New() ) -- Core.Fsm#FSM_TASK - - self:SetStartState( "Planned" ) - self:AddTransition( "Planned", "Assign", "Assigned" ) - self:AddTransition( "Assigned", "AssignUnit", "Assigned" ) - self:AddTransition( "Assigned", "Success", "Success" ) - self:AddTransition( "Assigned", "Fail", "Failed" ) - self:AddTransition( "Assigned", "Abort", "Aborted" ) - self:AddTransition( "Assigned", "Cancel", "Cancelled" ) - self:AddTransition( "*", "PlayerCrashed", "*" ) - self:AddTransition( "*", "PlayerAborted", "*" ) - self:AddTransition( "*", "PlayerDead", "*" ) - self:AddTransition( { "Failed", "Aborted", "Cancelled" }, "Replan", "Planned" ) - self:AddTransition( "*", "TimeOut", "Cancelled" ) - - self:E( "New TASK " .. TaskName ) - - self.Processes = {} - self.Fsm = {} - - self.Mission = Mission - self.CommandCenter = Mission:GetCommandCenter() - - self.SetGroup = SetGroupAssign - - self:SetType( TaskType ) - self:SetName( TaskName ) - self:SetID( Mission:GetNextTaskID( self ) ) -- The Mission orchestrates the task sequences .. - - self.TaskBriefing = "You are invited for the task: " .. self.TaskName .. "." - - self.FsmTemplate = self.FsmTemplate or FSM_PROCESS:New() - - Mission:AddTask( self ) - - return self -end - ---- Get the Task FSM Process Template --- @param #TASK self --- @return Core.Fsm#FSM_PROCESS -function TASK:GetUnitProcess() - - return self.FsmTemplate -end - ---- Sets the Task FSM Process Template --- @param #TASK self --- @param Core.Fsm#FSM_PROCESS -function TASK:SetUnitProcess( FsmTemplate ) - - self.FsmTemplate = FsmTemplate -end - ---- Add a PlayerUnit to join the Task. --- For each Group within the Task, the Unit is check if it can join the Task. --- If the Unit was not part of the Task, false is returned. --- If the Unit is part of the Task, true is returned. --- @param #TASK self --- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player joining the Mission. --- @param Wrapper.Group#GROUP PlayerGroup The GROUP of the player joining the Mission. --- @return #boolean true if Unit is part of the Task. -function TASK:JoinUnit( PlayerUnit, PlayerGroup ) - self:F( { PlayerUnit = PlayerUnit, PlayerGroup = PlayerGroup } ) - - local PlayerUnitAdded = false - - local PlayerGroups = self:GetGroups() - - -- Is the PlayerGroup part of the PlayerGroups? - if PlayerGroups:IsIncludeObject( PlayerGroup ) then - - -- Check if the PlayerGroup is already assigned to the Task. If yes, the PlayerGroup is added to the Task. - -- If the PlayerGroup is not assigned to the Task, the menu needs to be set. In that case, the PlayerUnit will become the GroupPlayer leader. - if self:IsStatePlanned() or self:IsStateReplanned() then - self:SetMenuForGroup( PlayerGroup ) - self:MessageToGroups( PlayerUnit:GetPlayerName() .. " is planning to join Task " .. self:GetName() ) - end - if self:IsStateAssigned() then - local IsAssignedToGroup = self:IsAssignedToGroup( PlayerGroup ) - self:E( { IsAssignedToGroup = IsAssignedToGroup } ) - if IsAssignedToGroup then - self:AssignToUnit( PlayerUnit ) - self:MessageToGroups( PlayerUnit:GetPlayerName() .. " joined Task " .. self:GetName() ) - end - end - end - - return PlayerUnitAdded -end - ---- Abort a PlayerUnit from a Task. --- If the Unit was not part of the Task, false is returned. --- If the Unit is part of the Task, true is returned. --- @param #TASK self --- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player aborting the Task. --- @return #boolean true if Unit is part of the Task. -function TASK:AbortUnit( PlayerUnit ) - self:F( { PlayerUnit = PlayerUnit } ) - - local PlayerUnitAborted = false - - local PlayerGroups = self:GetGroups() - local PlayerGroup = PlayerUnit:GetGroup() - - -- Is the PlayerGroup part of the PlayerGroups? - if PlayerGroups:IsIncludeObject( PlayerGroup ) then - - -- Check if the PlayerGroup is already assigned to the Task. If yes, the PlayerGroup is aborted from the Task. - -- If the PlayerUnit was the last unit of the PlayerGroup, the menu needs to be removed from the Group. - if self:IsStateAssigned() then - local IsAssignedToGroup = self:IsAssignedToGroup( PlayerGroup ) - self:E( { IsAssignedToGroup = IsAssignedToGroup } ) - if IsAssignedToGroup then - self:UnAssignFromUnit( PlayerUnit ) - self:MessageToGroups( PlayerUnit:GetPlayerName() .. " aborted Task " .. self:GetName() ) - self:E( { TaskGroup = PlayerGroup:GetName(), GetUnits = PlayerGroup:GetUnits() } ) - if #PlayerGroup:GetUnits() == 1 then - PlayerGroup:SetState( PlayerGroup, "Assigned", nil ) - self:RemoveMenuForGroup( PlayerGroup ) - end - self:PlayerAborted( PlayerUnit ) - end - end - end - - return PlayerUnitAborted -end - ---- A PlayerUnit crashed in a Task. Abort the Player. --- If the Unit was not part of the Task, false is returned. --- If the Unit is part of the Task, true is returned. --- @param #TASK self --- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player aborting the Task. --- @return #boolean true if Unit is part of the Task. -function TASK:CrashUnit( PlayerUnit ) - self:F( { PlayerUnit = PlayerUnit } ) - - local PlayerUnitCrashed = false - - local PlayerGroups = self:GetGroups() - local PlayerGroup = PlayerUnit:GetGroup() - - -- Is the PlayerGroup part of the PlayerGroups? - if PlayerGroups:IsIncludeObject( PlayerGroup ) then - - -- Check if the PlayerGroup is already assigned to the Task. If yes, the PlayerGroup is aborted from the Task. - -- If the PlayerUnit was the last unit of the PlayerGroup, the menu needs to be removed from the Group. - if self:IsStateAssigned() then - local IsAssignedToGroup = self:IsAssignedToGroup( PlayerGroup ) - self:E( { IsAssignedToGroup = IsAssignedToGroup } ) - if IsAssignedToGroup then - self:UnAssignFromUnit( PlayerUnit ) - self:MessageToGroups( PlayerUnit:GetPlayerName() .. " crashed in Task " .. self:GetName() ) - self:E( { TaskGroup = PlayerGroup:GetName(), GetUnits = PlayerGroup:GetUnits() } ) - if #PlayerGroup:GetUnits() == 1 then - PlayerGroup:SetState( PlayerGroup, "Assigned", nil ) - self:RemoveMenuForGroup( PlayerGroup ) - end - self:PlayerCrashed( PlayerUnit ) - end - end - end - - return PlayerUnitCrashed -end - - - ---- Gets the Mission to where the TASK belongs. --- @param #TASK self --- @return Tasking.Mission#MISSION -function TASK:GetMission() - - return self.Mission -end - - ---- Gets the SET_GROUP assigned to the TASK. --- @param #TASK self --- @return Core.Set#SET_GROUP -function TASK:GetGroups() - return self.SetGroup -end - - - ---- Assign the @{Task}to a @{Group}. --- @param #TASK self --- @param Wrapper.Group#GROUP TaskGroup --- @return #TASK -function TASK:AssignToGroup( TaskGroup ) - self:F2( TaskGroup:GetName() ) - - local TaskGroupName = TaskGroup:GetName() - - TaskGroup:SetState( TaskGroup, "Assigned", self ) - - self:RemoveMenuForGroup( TaskGroup ) - self:SetAssignedMenuForGroup( TaskGroup ) - - local TaskUnits = TaskGroup:GetUnits() - for UnitID, UnitData in pairs( TaskUnits ) do - local TaskUnit = UnitData -- Wrapper.Unit#UNIT - local PlayerName = TaskUnit:GetPlayerName() - self:E(PlayerName) - if PlayerName ~= nil or PlayerName ~= "" then - self:AssignToUnit( TaskUnit ) - end - end - - return self -end - ---- --- @param #TASK self --- @param Wrapper.Group#GROUP FindGroup --- @return #boolean -function TASK:HasGroup( FindGroup ) - - return self:GetGroups():IsIncludeObject( FindGroup ) - -end - ---- Assign the @{Task} to an alive @{Unit}. --- @param #TASK self --- @param Wrapper.Unit#UNIT TaskUnit --- @return #TASK self -function TASK:AssignToUnit( TaskUnit ) - self:F( TaskUnit:GetName() ) - - local FsmTemplate = self:GetUnitProcess() - - -- Assign a new FsmUnit to TaskUnit. - local FsmUnit = self:SetStateMachine( TaskUnit, FsmTemplate:Copy( TaskUnit, self ) ) -- Core.Fsm#FSM_PROCESS - self:E({"Address FsmUnit", tostring( FsmUnit ) } ) - - FsmUnit:SetStartState( "Planned" ) - FsmUnit:Accept() -- Each Task needs to start with an Accept event to start the flow. - - return self -end - ---- UnAssign the @{Task} from an alive @{Unit}. --- @param #TASK self --- @param Wrapper.Unit#UNIT TaskUnit --- @return #TASK self -function TASK:UnAssignFromUnit( TaskUnit ) - self:F( TaskUnit ) - - self:RemoveStateMachine( TaskUnit ) - - return self -end - ---- Sets the TimeOut for the @{Task}. If @{Task} stayed planned for longer than TimeOut, it gets into Cancelled status. --- @param #TASK self --- @param #integer Timer in seconds --- @return #TASK self -function TASK:SetTimeOut ( Timer ) - self:F( Timer ) - self.TimeOut = Timer - self:__TimeOut( self.TimeOut ) - return self -end - ---- Send a message of the @{Task} to the assigned @{Group}s. --- @param #TASK self -function TASK:MessageToGroups( Message ) - self:F( { Message = Message } ) - - local Mission = self:GetMission() - local CC = Mission:GetCommandCenter() - - for TaskGroupName, TaskGroup in pairs( self.SetGroup:GetSet() ) do - local TaskGroup = TaskGroup -- Wrapper.Group#GROUP - CC:MessageToGroup( Message, TaskGroup, TaskGroup:GetName() ) - end -end - - ---- Send the briefng message of the @{Task} to the assigned @{Group}s. --- @param #TASK self -function TASK:SendBriefingToAssignedGroups() - self:F2() - - for TaskGroupName, TaskGroup in pairs( self.SetGroup:GetSet() ) do - - if self:IsAssignedToGroup( TaskGroup ) then - TaskGroup:Message( self.TaskBriefing, 60 ) - end - end -end - - ---- Assign the @{Task} from the @{Group}s. --- @param #TASK self -function TASK:UnAssignFromGroups() - self:F2() - - for TaskGroupName, TaskGroup in pairs( self.SetGroup:GetSet() ) do - - TaskGroup:SetState( TaskGroup, "Assigned", nil ) - - self:RemoveMenuForGroup( TaskGroup ) - - local TaskUnits = TaskGroup:GetUnits() - for UnitID, UnitData in pairs( TaskUnits ) do - local TaskUnit = UnitData -- Wrapper.Unit#UNIT - local PlayerName = TaskUnit:GetPlayerName() - if PlayerName ~= nil or PlayerName ~= "" then - self:UnAssignFromUnit( TaskUnit ) - end - end - end -end - ---- Returns if the @{Task} is assigned to the Group. --- @param #TASK self --- @param Wrapper.Group#GROUP TaskGroup --- @return #boolean -function TASK:IsAssignedToGroup( TaskGroup ) - - local TaskGroupName = TaskGroup:GetName() - - if self:IsStateAssigned() then - if TaskGroup:GetState( TaskGroup, "Assigned" ) == self then - return true - end - end - - return false -end - ---- Returns if the @{Task} has still alive and assigned Units. --- @param #TASK self --- @return #boolean -function TASK:HasAliveUnits() - self:F() - - for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do - if self:IsStateAssigned() then - if self:IsAssignedToGroup( TaskGroup ) then - for TaskUnitID, TaskUnit in pairs( TaskGroup:GetUnits() ) do - if TaskUnit:IsAlive() then - self:T( { HasAliveUnits = true } ) - return true - end - end - end - end - end - - self:T( { HasAliveUnits = false } ) - return false -end - ---- Set the menu options of the @{Task} to all the groups in the SetGroup. --- @param #TASK self -function TASK:SetMenu() - self:F() - - self.SetGroup:Flush() - for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do - if self:IsStatePlanned() or self:IsStateReplanned() then - self:SetMenuForGroup( TaskGroup ) - end - end -end - - ---- Remove the menu options of the @{Task} to all the groups in the SetGroup. --- @param #TASK self --- @return #TASK self -function TASK:RemoveMenu() - - for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do - self:RemoveMenuForGroup( TaskGroup ) - end -end - - ---- Set the Menu for a Group --- @param #TASK self -function TASK:SetMenuForGroup( TaskGroup ) - - if not self:IsAssignedToGroup( TaskGroup ) then - self:SetPlannedMenuForGroup( TaskGroup, self:GetTaskName() ) - else - self:SetAssignedMenuForGroup( TaskGroup ) - end -end - - ---- Set the planned menu option of the @{Task}. --- @param #TASK self --- @param Wrapper.Group#GROUP TaskGroup --- @param #string MenuText The menu text. --- @return #TASK self -function TASK:SetPlannedMenuForGroup( TaskGroup, MenuText ) - self:E( TaskGroup:GetName() ) - - local Mission = self:GetMission() - local MissionMenu = Mission:GetMissionMenu( TaskGroup ) - - local TaskType = self:GetType() - local TaskTypeMenu = MENU_GROUP:New( TaskGroup, TaskType, MissionMenu ) - local TaskMenu = MENU_GROUP_COMMAND:New( TaskGroup, MenuText, TaskTypeMenu, self.MenuAssignToGroup, { self = self, TaskGroup = TaskGroup } ) - - return self -end - ---- Set the assigned menu options of the @{Task}. --- @param #TASK self --- @param Wrapper.Group#GROUP TaskGroup --- @return #TASK self -function TASK:SetAssignedMenuForGroup( TaskGroup ) - self:E( TaskGroup:GetName() ) - - local Mission = self:GetMission() - local MissionMenu = Mission:GetMissionMenu( TaskGroup ) - - self:E( { MissionMenu = MissionMenu } ) - - local TaskTypeMenu = MENU_GROUP_COMMAND:New( TaskGroup, "Task Status", MissionMenu, self.MenuTaskStatus, { self = self, TaskGroup = TaskGroup } ) - local TaskMenu = MENU_GROUP_COMMAND:New( TaskGroup, "Abort Task", MissionMenu, self.MenuTaskAbort, { self = self, TaskGroup = TaskGroup } ) - - return self -end - ---- Remove the menu option of the @{Task} for a @{Group}. --- @param #TASK self --- @param Wrapper.Group#GROUP TaskGroup --- @return #TASK self -function TASK:RemoveMenuForGroup( TaskGroup ) - - local Mission = self:GetMission() - local MissionName = Mission:GetName() - - local MissionMenu = Mission:GetMissionMenu( TaskGroup ) - MissionMenu:Remove() -end - -function TASK.MenuAssignToGroup( MenuParam ) - - local self = MenuParam.self - local TaskGroup = MenuParam.TaskGroup - - self:E( "Assigned menu selected") - - self:AssignToGroup( TaskGroup ) -end - -function TASK.MenuTaskStatus( MenuParam ) - - local self = MenuParam.self - local TaskGroup = MenuParam.TaskGroup - - --self:AssignToGroup( TaskGroup ) -end - -function TASK.MenuTaskAbort( MenuParam ) - - local self = MenuParam.self - local TaskGroup = MenuParam.TaskGroup - - self:Abort() -end - - - ---- Returns the @{Task} name. --- @param #TASK self --- @return #string TaskName -function TASK:GetTaskName() - return self.TaskName -end - - - - ---- Get the default or currently assigned @{Process} template with key ProcessName. --- @param #TASK self --- @param #string ProcessName --- @return Core.Fsm#FSM_PROCESS -function TASK:GetProcessTemplate( ProcessName ) - - local ProcessTemplate = self.ProcessClasses[ProcessName] - - return ProcessTemplate -end - - - --- TODO: Obscolete? ---- Fail processes from @{Task} with key @{Unit} --- @param #TASK self --- @param #string TaskUnitName --- @return #TASK self -function TASK:FailProcesses( TaskUnitName ) - - for ProcessID, ProcessData in pairs( self.Processes[TaskUnitName] ) do - local Process = ProcessData - Process.Fsm:Fail() - end -end - ---- Add a FiniteStateMachine to @{Task} with key Task@{Unit} --- @param #TASK self --- @param Wrapper.Unit#UNIT TaskUnit --- @return #TASK self -function TASK:SetStateMachine( TaskUnit, Fsm ) - self:F( { TaskUnit, self.Fsm[TaskUnit] ~= nil } ) - - self.Fsm[TaskUnit] = Fsm - - return Fsm -end - ---- Remove FiniteStateMachines from @{Task} with key Task@{Unit} --- @param #TASK self --- @param Wrapper.Unit#UNIT TaskUnit --- @return #TASK self -function TASK:RemoveStateMachine( TaskUnit ) - self:F( { TaskUnit, self.Fsm[TaskUnit] ~= nil } ) - - self.Fsm[TaskUnit] = nil - collectgarbage() - self:T( "Garbage Collected, Processes should be finalized now ...") -end - ---- Checks if there is a FiniteStateMachine assigned to Task@{Unit} for @{Task} --- @param #TASK self --- @param Wrapper.Unit#UNIT TaskUnit --- @return #TASK self -function TASK:HasStateMachine( TaskUnit ) - self:F( { TaskUnit, self.Fsm[TaskUnit] ~= nil } ) - - return ( self.Fsm[TaskUnit] ~= nil ) -end - - ---- Gets the Scoring of the task --- @param #TASK self --- @return Functional.Scoring#SCORING Scoring -function TASK:GetScoring() - return self.Mission:GetScoring() -end - - ---- Gets the Task Index, which is a combination of the Task type, the Task name. --- @param #TASK self --- @return #string The Task ID -function TASK:GetTaskIndex() - - local TaskType = self:GetType() - local TaskName = self:GetName() - - return TaskType .. "." .. TaskName -end - ---- Sets the Name of the Task --- @param #TASK self --- @param #string TaskName -function TASK:SetName( TaskName ) - self.TaskName = TaskName -end - ---- Gets the Name of the Task --- @param #TASK self --- @return #string The Task Name -function TASK:GetName() - return self.TaskName -end - ---- Sets the Type of the Task --- @param #TASK self --- @param #string TaskType -function TASK:SetType( TaskType ) - self.TaskType = TaskType -end - ---- Gets the Type of the Task --- @param #TASK self --- @return #string TaskType -function TASK:GetType() - return self.TaskType -end - ---- Sets the ID of the Task --- @param #TASK self --- @param #string TaskID -function TASK:SetID( TaskID ) - self.TaskID = TaskID -end - ---- Gets the ID of the Task --- @param #TASK self --- @return #string TaskID -function TASK:GetID() - return self.TaskID -end - - ---- Sets a @{Task} to status **Success**. --- @param #TASK self -function TASK:StateSuccess() - self:SetState( self, "State", "Success" ) - return self -end - ---- Is the @{Task} status **Success**. --- @param #TASK self -function TASK:IsStateSuccess() - return self:Is( "Success" ) -end - ---- Sets a @{Task} to status **Failed**. --- @param #TASK self -function TASK:StateFailed() - self:SetState( self, "State", "Failed" ) - return self -end - ---- Is the @{Task} status **Failed**. --- @param #TASK self -function TASK:IsStateFailed() - return self:Is( "Failed" ) -end - ---- Sets a @{Task} to status **Planned**. --- @param #TASK self -function TASK:StatePlanned() - self:SetState( self, "State", "Planned" ) - return self -end - ---- Is the @{Task} status **Planned**. --- @param #TASK self -function TASK:IsStatePlanned() - return self:Is( "Planned" ) -end - ---- Sets a @{Task} to status **Assigned**. --- @param #TASK self -function TASK:StateAssigned() - self:SetState( self, "State", "Assigned" ) - return self -end - ---- Is the @{Task} status **Assigned**. --- @param #TASK self -function TASK:IsStateAssigned() - return self:Is( "Assigned" ) -end - ---- Sets a @{Task} to status **Hold**. --- @param #TASK self -function TASK:StateHold() - self:SetState( self, "State", "Hold" ) - return self -end - ---- Is the @{Task} status **Hold**. --- @param #TASK self -function TASK:IsStateHold() - return self:Is( "Hold" ) -end - ---- Sets a @{Task} to status **Replanned**. --- @param #TASK self -function TASK:StateReplanned() - self:SetState( self, "State", "Replanned" ) - return self -end - ---- Is the @{Task} status **Replanned**. --- @param #TASK self -function TASK:IsStateReplanned() - return self:Is( "Replanned" ) -end - ---- Gets the @{Task} status. --- @param #TASK self -function TASK:GetStateString() - return self:GetState( self, "State" ) -end - ---- Sets a @{Task} briefing. --- @param #TASK self --- @param #string TaskBriefing --- @return #TASK self -function TASK:SetBriefing( TaskBriefing ) - self.TaskBriefing = TaskBriefing - return self -end - - - - ---- FSM function for a TASK --- @param #TASK self --- @param #string Event --- @param #string From --- @param #string To -function TASK:onenterAssigned( From, Event, To ) - - self:E("Task Assigned") - - self:MessageToGroups( "Task " .. self:GetName() .. " has been assigned to your group." ) - self:GetMission():__Start( 1 ) -end - - ---- FSM function for a TASK --- @param #TASK self --- @param #string Event --- @param #string From --- @param #string To -function TASK:onenterSuccess( From, Event, To ) - - self:E( "Task Success" ) - - self:MessageToGroups( "Task " .. self:GetName() .. " is successful! Good job!" ) - self:UnAssignFromGroups() - - self:GetMission():__Complete( 1 ) - -end - - ---- FSM function for a TASK --- @param #TASK self --- @param #string From --- @param #string Event --- @param #string To -function TASK:onenterAborted( From, Event, To ) - - self:E( "Task Aborted" ) - - self:GetMission():GetCommandCenter():MessageToCoalition( "Task " .. self:GetName() .. " has been aborted! Task may be replanned." ) - - self:UnAssignFromGroups() - - self:__Replan( 5 ) -end - ---- FSM function for a TASK --- @param #TASK self --- @param #string From --- @param #string Event --- @param #string To -function TASK:onafterReplan( From, Event, To ) - - self:E( "Task Replanned" ) - - self:GetMission():GetCommandCenter():MessageToCoalition( "Replanning Task " .. self:GetName() .. "." ) - - self:SetMenu() - -end - ---- FSM function for a TASK --- @param #TASK self --- @param #string From --- @param #string Event --- @param #string To -function TASK:onenterFailed( From, Event, To ) - - self:E( "Task Failed" ) - - self:GetMission():GetCommandCenter():MessageToCoalition( "Task " .. self:GetName() .. " has failed!" ) - - self:UnAssignFromGroups() -end - ---- FSM function for a TASK --- @param #TASK self --- @param #string Event --- @param #string From --- @param #string To -function TASK:onstatechange( From, Event, To ) - - if self:IsTrace() then - MESSAGE:New( "@ Task " .. self.TaskName .. " : " .. Event .. " changed to state " .. To, 2 ):ToAll() - end - - if self.Scores[To] then - local Scoring = self:GetScoring() - if Scoring then - self:E( { self.Scores[To].ScoreText, self.Scores[To].Score } ) - Scoring:_AddMissionScore( self.Mission, self.Scores[To].ScoreText, self.Scores[To].Score ) - end - end - -end - ---- FSM function for a TASK --- @param #TASK self --- @param #string Event --- @param #string From --- @param #string To -function TASK:onenterPlanned( From, Event, To) - if not self.TimeOut == 0 then - self.__TimeOut( self.TimeOut ) - end -end - ---- FSM function for a TASK --- @param #TASK self --- @param #string Event --- @param #string From --- @param #string To -function TASK:onbeforeTimeOut( From, Event, To ) - if From == "Planned" then - self:RemoveMenu() - return true - end - return false -end - -do -- Reporting - ---- Create a summary report of the Task. --- List the Task Name and Status --- @param #TASK self --- @return #string -function TASK:ReportSummary() - - local Report = REPORT:New() - - -- List the name of the Task. - local Name = self:GetName() - - -- Determine the status of the Task. - local State = self:GetState() - - Report:Add( "Task " .. Name .. " - State '" .. State ) - - return Report:Text() -end - - ---- Create a detailed report of the Task. --- List the Task Status, and the Players assigned to the Task. --- @param #TASK self --- @return #string -function TASK:ReportDetails() - - local Report = REPORT:New() - - -- List the name of the Task. - local Name = self:GetName() - - -- Determine the status of the Task. - local State = self:GetState() - - - -- Loop each Unit active in the Task, and find Player Names. - local PlayerNames = {} - for PlayerGroupID, PlayerGroup in pairs( self:GetGroups():GetSet() ) do - local Player = PlayerGroup -- Wrapper.Group#GROUP - for PlayerUnitID, PlayerUnit in pairs( PlayerGroup:GetUnits() ) do - local PlayerUnit = PlayerUnit -- Wrapper.Unit#UNIT - if PlayerUnit and PlayerUnit:IsAlive() then - local PlayerName = PlayerUnit:GetPlayerName() - PlayerNames[#PlayerNames+1] = PlayerName - end - end - local PlayerNameText = table.concat( PlayerNames, ", " ) - Report:Add( "Task " .. Name .. " - State '" .. State .. "' - Players " .. PlayerNameText ) - end - - -- Loop each Process in the Task, and find Reporting Details. - - return Report:Text() -end - - -end -- Reporting ---- This module contains the DETECTION_MANAGER class and derived classes. --- --- === --- --- 1) @{DetectionManager#DETECTION_MANAGER} class, extends @{Base#BASE} --- ==================================================================== --- The @{DetectionManager#DETECTION_MANAGER} class defines the core functions to report detected objects to groups. --- Reportings can be done in several manners, and it is up to the derived classes if DETECTION_MANAGER to model the reporting behaviour. --- --- 1.1) DETECTION_MANAGER constructor: --- ----------------------------------- --- * @{DetectionManager#DETECTION_MANAGER.New}(): Create a new DETECTION_MANAGER instance. --- --- 1.2) DETECTION_MANAGER reporting: --- --------------------------------- --- Derived DETECTION_MANAGER classes will reports detected units using the method @{DetectionManager#DETECTION_MANAGER.ReportDetected}(). This method implements polymorphic behaviour. --- --- The time interval in seconds of the reporting can be changed using the methods @{DetectionManager#DETECTION_MANAGER.SetReportInterval}(). --- To control how long a reporting message is displayed, use @{DetectionManager#DETECTION_MANAGER.SetReportDisplayTime}(). --- Derived classes need to implement the method @{DetectionManager#DETECTION_MANAGER.GetReportDisplayTime}() to use the correct display time for displayed messages during a report. --- --- Reporting can be started and stopped using the methods @{DetectionManager#DETECTION_MANAGER.StartReporting}() and @{DetectionManager#DETECTION_MANAGER.StopReporting}() respectively. --- If an ad-hoc report is requested, use the method @{DetectionManager#DETECTION_MANAGER#ReportNow}(). --- --- The default reporting interval is every 60 seconds. The reporting messages are displayed 15 seconds. --- --- === --- --- 2) @{DetectionManager#DETECTION_REPORTING} class, extends @{DetectionManager#DETECTION_MANAGER} --- ========================================================================================= --- The @{DetectionManager#DETECTION_REPORTING} class implements detected units reporting. Reporting can be controlled using the reporting methods available in the @{DetectionManager#DETECTION_MANAGER} class. --- --- 2.1) DETECTION_REPORTING constructor: --- ------------------------------- --- The @{DetectionManager#DETECTION_REPORTING.New}() method creates a new DETECTION_REPORTING instance. --- --- === --- --- 3) @{#DETECTION_DISPATCHER} class, extends @{#DETECTION_MANAGER} --- ================================================================ --- The @{#DETECTION_DISPATCHER} class implements the dynamic dispatching of tasks upon groups of detected units determined a @{Set} of FAC (groups). --- The FAC will detect units, will group them, and will dispatch @{Task}s to groups. Depending on the type of target detected, different tasks will be dispatched. --- Find a summary below describing for which situation a task type is created: --- --- * **CAS Task**: Is created when there are enemy ground units within range of the FAC, while there are friendly units in the FAC perimeter. --- * **BAI Task**: Is created when there are enemy ground units within range of the FAC, while there are NO other friendly units within the FAC perimeter. --- * **SEAD Task**: Is created when there are enemy ground units wihtin range of the FAC, with air search radars. --- --- Other task types will follow... --- --- 3.1) DETECTION_DISPATCHER constructor: --- -------------------------------------- --- The @{#DETECTION_DISPATCHER.New}() method creates a new DETECTION_DISPATCHER instance. --- --- === --- --- ### Contributions: Mechanist, Prof_Hilactic, FlightControl - Concept & Testing --- ### Author: FlightControl - Framework Design & Programming --- --- @module DetectionManager - -do -- DETECTION MANAGER - - --- DETECTION_MANAGER class. - -- @type DETECTION_MANAGER - -- @field Set#SET_GROUP SetGroup The groups to which the FAC will report to. - -- @field Functional.Detection#DETECTION_BASE Detection The DETECTION_BASE object that is used to report the detected objects. - -- @extends Base#BASE - DETECTION_MANAGER = { - ClassName = "DETECTION_MANAGER", - SetGroup = nil, - Detection = nil, - } - - --- FAC constructor. - -- @param #DETECTION_MANAGER self - -- @param Set#SET_GROUP SetGroup - -- @param Functional.Detection#DETECTION_BASE Detection - -- @return #DETECTION_MANAGER self - function DETECTION_MANAGER:New( SetGroup, Detection ) - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) -- Functional.Detection#DETECTION_MANAGER - - self.SetGroup = SetGroup - self.Detection = Detection - - self:SetReportInterval( 30 ) - self:SetReportDisplayTime( 25 ) - - return self - end - - --- Set the reporting time interval. - -- @param #DETECTION_MANAGER self - -- @param #number ReportInterval The interval in seconds when a report needs to be done. - -- @return #DETECTION_MANAGER self - function DETECTION_MANAGER:SetReportInterval( ReportInterval ) - self:F2() - - self._ReportInterval = ReportInterval - end - - - --- Set the reporting message display time. - -- @param #DETECTION_MANAGER self - -- @param #number ReportDisplayTime The display time in seconds when a report needs to be done. - -- @return #DETECTION_MANAGER self - function DETECTION_MANAGER:SetReportDisplayTime( ReportDisplayTime ) - self:F2() - - self._ReportDisplayTime = ReportDisplayTime - end - - --- Get the reporting message display time. - -- @param #DETECTION_MANAGER self - -- @return #number ReportDisplayTime The display time in seconds when a report needs to be done. - function DETECTION_MANAGER:GetReportDisplayTime() - self:F2() - - return self._ReportDisplayTime - end - - - - --- Reports the detected items to the @{Set#SET_GROUP}. - -- @param #DETECTION_MANAGER self - -- @param Functional.Detection#DETECTION_BASE Detection - -- @return #DETECTION_MANAGER self - function DETECTION_MANAGER:ReportDetected( Detection ) - self:F2() - - end - - --- Schedule the FAC reporting. - -- @param #DETECTION_MANAGER self - -- @param #number DelayTime The delay in seconds to wait the reporting. - -- @param #number ReportInterval The repeat interval in seconds for the reporting to happen repeatedly. - -- @return #DETECTION_MANAGER self - function DETECTION_MANAGER:Schedule( DelayTime, ReportInterval ) - self:F2() - - self._ScheduleDelayTime = DelayTime - - self:SetReportInterval( ReportInterval ) - - self.FacScheduler = SCHEDULER:New(self, self._FacScheduler, { self, "DetectionManager" }, self._ScheduleDelayTime, self._ReportInterval ) - return self - end - - --- Report the detected @{Unit#UNIT}s detected within the @{Detection#DETECTION_BASE} object to the @{Set#SET_GROUP}s. - -- @param #DETECTION_MANAGER self - function DETECTION_MANAGER:_FacScheduler( SchedulerName ) - self:F2( { SchedulerName } ) - - return self:ProcessDetected( self.Detection ) - --- self.SetGroup:ForEachGroup( --- --- @param Wrapper.Group#GROUP Group --- function( Group ) --- if Group:IsAlive() then --- return self:ProcessDetected( self.Detection ) --- end --- end --- ) - --- return true - end - -end - - -do -- DETECTION_REPORTING - - --- DETECTION_REPORTING class. - -- @type DETECTION_REPORTING - -- @field Set#SET_GROUP SetGroup The groups to which the FAC will report to. - -- @field Functional.Detection#DETECTION_BASE Detection The DETECTION_BASE object that is used to report the detected objects. - -- @extends #DETECTION_MANAGER - DETECTION_REPORTING = { - ClassName = "DETECTION_REPORTING", - } - - - --- DETECTION_REPORTING constructor. - -- @param #DETECTION_REPORTING self - -- @param Set#SET_GROUP SetGroup - -- @param Functional.Detection#DETECTION_AREAS Detection - -- @return #DETECTION_REPORTING self - function DETECTION_REPORTING:New( SetGroup, Detection ) - - -- Inherits from DETECTION_MANAGER - local self = BASE:Inherit( self, DETECTION_MANAGER:New( SetGroup, Detection ) ) -- #DETECTION_REPORTING - - self:Schedule( 1, 30 ) - return self - end - - --- Creates a string of the detected items in a @{Detection}. - -- @param #DETECTION_MANAGER self - -- @param Set#SET_UNIT DetectedSet The detected Set created by the @{Detection#DETECTION_BASE} object. - -- @return #DETECTION_MANAGER self - function DETECTION_REPORTING:GetDetectedItemsText( DetectedSet ) - self:F2() - - local MT = {} -- Message Text - local UnitTypes = {} - - for DetectedUnitID, DetectedUnitData in pairs( DetectedSet:GetSet() ) do - local DetectedUnit = DetectedUnitData -- Wrapper.Unit#UNIT - if DetectedUnit:IsAlive() then - local UnitType = DetectedUnit:GetTypeName() - - if not UnitTypes[UnitType] then - UnitTypes[UnitType] = 1 - else - UnitTypes[UnitType] = UnitTypes[UnitType] + 1 - end - end - end - - for UnitTypeID, UnitType in pairs( UnitTypes ) do - MT[#MT+1] = UnitType .. " of " .. UnitTypeID - end - - return table.concat( MT, ", " ) - end - - - - --- Reports the detected items to the @{Set#SET_GROUP}. - -- @param #DETECTION_REPORTING self - -- @param Wrapper.Group#GROUP Group The @{Group} object to where the report needs to go. - -- @param Functional.Detection#DETECTION_AREAS Detection The detection created by the @{Detection#DETECTION_BASE} object. - -- @return #boolean Return true if you want the reporting to continue... false will cancel the reporting loop. - function DETECTION_REPORTING:ProcessDetected( Group, Detection ) - self:F2( Group ) - - self:E( Group ) - local DetectedMsg = {} - for DetectedAreaID, DetectedAreaData in pairs( Detection:GetDetectedAreas() ) do - local DetectedArea = DetectedAreaData -- Functional.Detection#DETECTION_AREAS.DetectedArea - DetectedMsg[#DetectedMsg+1] = " - Group #" .. DetectedAreaID .. ": " .. self:GetDetectedItemsText( DetectedArea.Set ) - end - local FACGroup = Detection:GetDetectionGroups() - FACGroup:MessageToGroup( "Reporting detected target groups:\n" .. table.concat( DetectedMsg, "\n" ), self:GetReportDisplayTime(), Group ) - - return true - end - -end - -do -- DETECTION_DISPATCHER - - --- DETECTION_DISPATCHER class. - -- @type DETECTION_DISPATCHER - -- @field Set#SET_GROUP SetGroup The groups to which the FAC will report to. - -- @field Functional.Detection#DETECTION_BASE Detection The DETECTION_BASE object that is used to report the detected objects. - -- @field Tasking.Mission#MISSION Mission - -- @field Wrapper.Group#GROUP CommandCenter - -- @extends Tasking.DetectionManager#DETECTION_MANAGER - DETECTION_DISPATCHER = { - ClassName = "DETECTION_DISPATCHER", - Mission = nil, - CommandCenter = nil, - Detection = nil, - } - - - --- DETECTION_DISPATCHER constructor. - -- @param #DETECTION_DISPATCHER self - -- @param Set#SET_GROUP SetGroup - -- @param Functional.Detection#DETECTION_BASE Detection - -- @return #DETECTION_DISPATCHER self - function DETECTION_DISPATCHER:New( Mission, CommandCenter, SetGroup, Detection ) - - -- Inherits from DETECTION_MANAGER - local self = BASE:Inherit( self, DETECTION_MANAGER:New( SetGroup, Detection ) ) -- #DETECTION_DISPATCHER - - self.Detection = Detection - self.CommandCenter = CommandCenter - self.Mission = Mission - - self:Schedule( 30 ) - return self - end - - - --- Creates a SEAD task when there are targets for it. - -- @param #DETECTION_DISPATCHER self - -- @param Functional.Detection#DETECTION_AREAS.DetectedArea DetectedArea - -- @return Set#SET_UNIT TargetSetUnit: The target set of units. - -- @return #nil If there are no targets to be set. - function DETECTION_DISPATCHER:EvaluateSEAD( DetectedArea ) - self:F( { DetectedArea.AreaID } ) - - local DetectedSet = DetectedArea.Set - local DetectedZone = DetectedArea.Zone - - -- Determine if the set has radar targets. If it does, construct a SEAD task. - local RadarCount = DetectedSet:HasSEAD() - - if RadarCount > 0 then - - -- Here we're doing something advanced... We're copying the DetectedSet, but making a new Set only with SEADable Radar units in it. - local TargetSetUnit = SET_UNIT:New() - TargetSetUnit:SetDatabase( DetectedSet ) - TargetSetUnit:FilterHasSEAD() - TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection. - - return TargetSetUnit - end - - return nil - end - - --- Creates a CAS task when there are targets for it. - -- @param #DETECTION_DISPATCHER self - -- @param Functional.Detection#DETECTION_AREAS.DetectedArea DetectedArea - -- @return Tasking.Task#TASK - function DETECTION_DISPATCHER:EvaluateCAS( DetectedArea ) - self:F( { DetectedArea.AreaID } ) - - local DetectedSet = DetectedArea.Set - local DetectedZone = DetectedArea.Zone - - - -- Determine if the set has radar targets. If it does, construct a SEAD task. - local GroundUnitCount = DetectedSet:HasGroundUnits() - local FriendliesNearBy = self.Detection:IsFriendliesNearBy( DetectedArea ) - - if GroundUnitCount > 0 and FriendliesNearBy == true then - - -- Copy the Set - local TargetSetUnit = SET_UNIT:New() - TargetSetUnit:SetDatabase( DetectedSet ) - TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection. - - return TargetSetUnit - end - - return nil - end - - --- Creates a BAI task when there are targets for it. - -- @param #DETECTION_DISPATCHER self - -- @param Functional.Detection#DETECTION_AREAS.DetectedArea DetectedArea - -- @return Tasking.Task#TASK - function DETECTION_DISPATCHER:EvaluateBAI( DetectedArea, FriendlyCoalition ) - self:F( { DetectedArea.AreaID } ) - - local DetectedSet = DetectedArea.Set - local DetectedZone = DetectedArea.Zone - - - -- Determine if the set has radar targets. If it does, construct a SEAD task. - local GroundUnitCount = DetectedSet:HasGroundUnits() - local FriendliesNearBy = self.Detection:IsFriendliesNearBy( DetectedArea ) - - if GroundUnitCount > 0 and FriendliesNearBy == false then - - -- Copy the Set - local TargetSetUnit = SET_UNIT:New() - TargetSetUnit:SetDatabase( DetectedSet ) - TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection. - - return TargetSetUnit - end - - return nil - end - - --- Evaluates the removal of the Task from the Mission. - -- Can only occur when the DetectedArea is Changed AND the state of the Task is "Planned". - -- @param #DETECTION_DISPATCHER self - -- @param Tasking.Mission#MISSION Mission - -- @param Tasking.Task#TASK Task - -- @param Functional.Detection#DETECTION_AREAS.DetectedArea DetectedArea - -- @return Tasking.Task#TASK - function DETECTION_DISPATCHER:EvaluateRemoveTask( Mission, Task, DetectedArea ) - - if Task then - if Task:IsStatePlanned() and DetectedArea.Changed == true then - self:E( "Removing Tasking: " .. Task:GetTaskName() ) - Task = Mission:RemoveTask( Task ) - end - end - - return Task - end - - - --- Assigns tasks in relation to the detected items to the @{Set#SET_GROUP}. - -- @param #DETECTION_DISPATCHER self - -- @param Functional.Detection#DETECTION_AREAS Detection The detection created by the @{Detection#DETECTION_AREAS} object. - -- @return #boolean Return true if you want the task assigning to continue... false will cancel the loop. - function DETECTION_DISPATCHER:ProcessDetected( Detection ) - self:F2() - - local AreaMsg = {} - local TaskMsg = {} - local ChangeMsg = {} - - local Mission = self.Mission - - --- First we need to the detected targets. - for DetectedAreaID, DetectedAreaData in ipairs( Detection:GetDetectedAreas() ) do - - local DetectedArea = DetectedAreaData -- Functional.Detection#DETECTION_AREAS.DetectedArea - local DetectedSet = DetectedArea.Set - local DetectedZone = DetectedArea.Zone - self:E( { "Targets in DetectedArea", DetectedArea.AreaID, DetectedSet:Count(), tostring( DetectedArea ) } ) - DetectedSet:Flush() - - local AreaID = DetectedArea.AreaID - - -- Evaluate SEAD Tasking - local SEADTask = Mission:GetTask( "SEAD." .. AreaID ) - SEADTask = self:EvaluateRemoveTask( Mission, SEADTask, DetectedArea ) - if not SEADTask then - local TargetSetUnit = self:EvaluateSEAD( DetectedArea ) -- Returns a SetUnit if there are targets to be SEADed... - if TargetSetUnit then - SEADTask = Mission:AddTask( TASK_SEAD:New( Mission, self.SetGroup, "SEAD." .. AreaID, TargetSetUnit , DetectedZone ) ) - end - end - if SEADTask and SEADTask:IsStatePlanned() then - self:E( "Planned" ) - --SEADTask:SetPlannedMenu() - TaskMsg[#TaskMsg+1] = " - " .. SEADTask:GetStateString() .. " SEAD " .. AreaID .. " - " .. SEADTask.TargetSetUnit:GetUnitTypesText() - end - - -- Evaluate CAS Tasking - local CASTask = Mission:GetTask( "CAS." .. AreaID ) - CASTask = self:EvaluateRemoveTask( Mission, CASTask, DetectedArea ) - if not CASTask then - local TargetSetUnit = self:EvaluateCAS( DetectedArea ) -- Returns a SetUnit if there are targets to be SEADed... - if TargetSetUnit then - CASTask = Mission:AddTask( TASK_A2G:New( Mission, self.SetGroup, "CAS." .. AreaID, "CAS", TargetSetUnit , DetectedZone, DetectedArea.NearestFAC ) ) - end - end - if CASTask and CASTask:IsStatePlanned() then - --CASTask:SetPlannedMenu() - TaskMsg[#TaskMsg+1] = " - " .. CASTask:GetStateString() .. " CAS " .. AreaID .. " - " .. CASTask.TargetSetUnit:GetUnitTypesText() - end - - -- Evaluate BAI Tasking - local BAITask = Mission:GetTask( "BAI." .. AreaID ) - BAITask = self:EvaluateRemoveTask( Mission, BAITask, DetectedArea ) - if not BAITask then - local TargetSetUnit = self:EvaluateBAI( DetectedArea, self.CommandCenter:GetCoalition() ) -- Returns a SetUnit if there are targets to be SEADed... - if TargetSetUnit then - BAITask = Mission:AddTask( TASK_A2G:New( Mission, self.SetGroup, "BAI." .. AreaID, "BAI", TargetSetUnit , DetectedZone, DetectedArea.NearestFAC ) ) - end - end - if BAITask and BAITask:IsStatePlanned() then - --BAITask:SetPlannedMenu() - TaskMsg[#TaskMsg+1] = " - " .. BAITask:GetStateString() .. " BAI " .. AreaID .. " - " .. BAITask.TargetSetUnit:GetUnitTypesText() - end - - if #TaskMsg > 0 then - - local ThreatLevel = Detection:GetTreatLevelA2G( DetectedArea ) - - local DetectedAreaVec3 = DetectedZone:GetVec3() - local DetectedAreaPointVec3 = POINT_VEC3:New( DetectedAreaVec3.x, DetectedAreaVec3.y, DetectedAreaVec3.z ) - local DetectedAreaPointLL = DetectedAreaPointVec3:ToStringLL( 3, true ) - AreaMsg[#AreaMsg+1] = string.format( " - Area #%d - %s - Threat Level [%s] (%2d)", - DetectedAreaID, - DetectedAreaPointLL, - string.rep( "â– ", ThreatLevel ), - ThreatLevel - ) - - -- Loop through the changes ... - local ChangeText = Detection:GetChangeText( DetectedArea ) - - if ChangeText ~= "" then - ChangeMsg[#ChangeMsg+1] = string.gsub( string.gsub( ChangeText, "\n", "%1 - " ), "^.", " - %1" ) - end - end - - -- OK, so the tasking has been done, now delete the changes reported for the area. - Detection:AcceptChanges( DetectedArea ) - - end - - -- TODO set menus using the HQ coordinator - Mission:GetCommandCenter():SetMenu() - - if #AreaMsg > 0 then - for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do - if not TaskGroup:GetState( TaskGroup, "Assigned" ) then - self.CommandCenter:MessageToGroup( - string.format( "HQ Reporting - Target areas for mission '%s':\nAreas:\n%s\n\nTasks:\n%s\n\nChanges:\n%s ", - self.Mission:GetName(), - table.concat( AreaMsg, "\n" ), - table.concat( TaskMsg, "\n" ), - table.concat( ChangeMsg, "\n" ) - ), self:GetReportDisplayTime(), TaskGroup - ) - end - end - end - - return true - end - -end--- This module contains the TASK_SEAD classes. --- --- 1) @{#TASK_SEAD} class, extends @{Task#TASK} --- ================================================= --- The @{#TASK_SEAD} class defines a SEAD task for a @{Set} of Target Units, located at a Target Zone, --- based on the tasking capabilities defined in @{Task#TASK}. --- The TASK_SEAD is implemented using a @{Statemachine#FSM_TASK}, and has the following statuses: --- --- * **None**: Start of the process --- * **Planned**: The SEAD task is planned. Upon Planned, the sub-process @{Process_Fsm.Assign#ACT_ASSIGN_ACCEPT} is started to accept the task. --- * **Assigned**: The SEAD task is assigned to a @{Group#GROUP}. Upon Assigned, the sub-process @{Process_Fsm.Route#ACT_ROUTE} is started to route the active Units in the Group to the attack zone. --- * **Success**: The SEAD task is successfully completed. Upon Success, the sub-process @{Process_SEAD#PROCESS_SEAD} is started to follow-up successful SEADing of the targets assigned in the task. --- * **Failed**: The SEAD task has failed. This will happen if the player exists the task early, without communicating a possible cancellation to HQ. --- --- === --- --- ### Authors: FlightControl - Design and Programming --- --- @module Task_SEAD - - - -do -- TASK_SEAD - - --- The TASK_SEAD class - -- @type TASK_SEAD - -- @field Set#SET_UNIT TargetSetUnit - -- @extends Tasking.Task#TASK - TASK_SEAD = { - ClassName = "TASK_SEAD", - } - - --- Instantiates a new TASK_SEAD. - -- @param #TASK_SEAD self - -- @param Tasking.Mission#MISSION Mission - -- @param Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. - -- @param #string TaskName The name of the Task. - -- @param Set#SET_UNIT UnitSetTargets - -- @param Core.Zone#ZONE_BASE TargetZone - -- @return #TASK_SEAD self - function TASK_SEAD:New( Mission, SetGroup, TaskName, TargetSetUnit, TargetZone ) - local self = BASE:Inherit( self, TASK:New( Mission, SetGroup, TaskName, "SEAD" ) ) -- Tasking.Task_SEAD#TASK_SEAD - self:F() - - self.TargetSetUnit = TargetSetUnit - self.TargetZone = TargetZone - - local Fsm = self:GetUnitProcess() - - Fsm:AddProcess ( "Planned", "Accept", ACT_ASSIGN_ACCEPT:New( self.TaskBriefing ), { Assigned = "Route", Rejected = "Eject" } ) - Fsm:AddProcess ( "Assigned", "Route", ACT_ROUTE_ZONE:New( self.TargetZone ), { Arrived = "Update" } ) - Fsm:AddTransition( "Rejected", "Eject", "Planned" ) - Fsm:AddTransition( "Arrived", "Update", "Updated" ) - Fsm:AddProcess ( "Updated", "Account", ACT_ACCOUNT_DEADS:New( self.TargetSetUnit, "SEAD" ), { Accounted = "Success" } ) - Fsm:AddProcess ( "Updated", "Smoke", ACT_ASSIST_SMOKE_TARGETS_ZONE:New( self.TargetSetUnit, self.TargetZone ) ) - Fsm:AddTransition( "Accounted", "Success", "Success" ) - Fsm:AddTransition( "Failed", "Fail", "Failed" ) - - function Fsm:onenterUpdated( TaskUnit ) - self:E( { self } ) - self:Account() - self:Smoke() - end - --- _EVENTDISPATCHER:OnPlayerLeaveUnit( self._EventPlayerLeaveUnit, self ) --- _EVENTDISPATCHER:OnDead( self._EventDead, self ) --- _EVENTDISPATCHER:OnCrash( self._EventDead, self ) --- _EVENTDISPATCHER:OnPilotDead( self._EventDead, self ) - - return self - end - - --- @param #TASK_SEAD self - function TASK_SEAD:GetPlannedMenuText() - return self:GetStateString() .. " - " .. self:GetTaskName() .. " ( " .. self.TargetSetUnit:GetUnitTypesText() .. " )" - end - -end ---- (AI) (SP) (MP) Tasking for Air to Ground Processes. --- --- 1) @{#TASK_A2G} class, extends @{Task#TASK} --- ================================================= --- The @{#TASK_A2G} class defines a CAS or BAI task of a @{Set} of Target Units, --- located at a Target Zone, based on the tasking capabilities defined in @{Task#TASK}. --- The TASK_A2G is implemented using a @{Statemachine#FSM_TASK}, and has the following statuses: --- --- * **None**: Start of the process --- * **Planned**: The SEAD task is planned. Upon Planned, the sub-process @{Process_Fsm.Assign#ACT_ASSIGN_ACCEPT} is started to accept the task. --- * **Assigned**: The SEAD task is assigned to a @{Group#GROUP}. Upon Assigned, the sub-process @{Process_Fsm.Route#ACT_ROUTE} is started to route the active Units in the Group to the attack zone. --- * **Success**: The SEAD task is successfully completed. Upon Success, the sub-process @{Process_SEAD#PROCESS_SEAD} is started to follow-up successful SEADing of the targets assigned in the task. --- * **Failed**: The SEAD task has failed. This will happen if the player exists the task early, without communicating a possible cancellation to HQ. --- --- === --- --- ### Authors: FlightControl - Design and Programming --- --- @module Task_A2G - - -do -- TASK_A2G - - --- The TASK_A2G class - -- @type TASK_A2G - -- @extends Tasking.Task#TASK - TASK_A2G = { - ClassName = "TASK_A2G", - } - - --- Instantiates a new TASK_A2G. - -- @param #TASK_A2G self - -- @param Tasking.Mission#MISSION Mission - -- @param Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. - -- @param #string TaskName The name of the Task. - -- @param #string TaskType BAI or CAS - -- @param Set#SET_UNIT UnitSetTargets - -- @param Core.Zone#ZONE_BASE TargetZone - -- @return #TASK_A2G self - function TASK_A2G:New( Mission, SetGroup, TaskName, TaskType, TargetSetUnit, TargetZone, FACUnit ) - local self = BASE:Inherit( self, TASK:New( Mission, SetGroup, TaskName, TaskType ) ) - self:F() - - self.TargetSetUnit = TargetSetUnit - self.TargetZone = TargetZone - self.FACUnit = FACUnit - - local A2GUnitProcess = self:GetUnitProcess() - - A2GUnitProcess:AddProcess ( "Planned", "Accept", ACT_ASSIGN_ACCEPT:New( "Attack the Area" ), { Assigned = "Route", Rejected = "Eject" } ) - A2GUnitProcess:AddProcess ( "Assigned", "Route", ACT_ROUTE_ZONE:New( self.TargetZone ), { Arrived = "Update" } ) - A2GUnitProcess:AddTransition( "Rejected", "Eject", "Planned" ) - A2GUnitProcess:AddTransition( "Arrived", "Update", "Updated" ) - A2GUnitProcess:AddProcess ( "Updated", "Account", ACT_ACCOUNT_DEADS:New( self.TargetSetUnit, "Attack" ), { Accounted = "Success" } ) - A2GUnitProcess:AddProcess ( "Updated", "Smoke", ACT_ASSIST_SMOKE_TARGETS_ZONE:New( self.TargetSetUnit, self.TargetZone ) ) - --Fsm:AddProcess ( "Updated", "JTAC", PROCESS_JTAC:New( self, TaskUnit, self.TargetSetUnit, self.FACUnit ) ) - A2GUnitProcess:AddTransition( "Accounted", "Success", "Success" ) - A2GUnitProcess:AddTransition( "Failed", "Fail", "Failed" ) - - function A2GUnitProcess:onenterUpdated( TaskUnit ) - self:E( { self } ) - self:Account() - self:Smoke() - end - - - - --_EVENTDISPATCHER:OnPlayerLeaveUnit( self._EventPlayerLeaveUnit, self ) - --_EVENTDISPATCHER:OnDead( self._EventDead, self ) - --_EVENTDISPATCHER:OnCrash( self._EventDead, self ) - --_EVENTDISPATCHER:OnPilotDead( self._EventDead, self ) - - return self - end - - --- @param #TASK_A2G self - function TASK_A2G:GetPlannedMenuText() - return self:GetStateString() .. " - " .. self:GetTaskName() .. " ( " .. self.TargetSetUnit:GetUnitTypesText() .. " )" - end - - end - - - ---- The main include file for the MOOSE system. - ---- Core Routines -Include.File( "Utilities/Routines" ) -Include.File( "Utilities/Utils" ) - ---- Core Classes -Include.File( "Core/Base" ) -Include.File( "Core/Scheduler" ) -Include.File( "Core/ScheduleDispatcher") -Include.File( "Core/Event" ) -Include.File( "Core/Menu" ) -Include.File( "Core/Zone" ) -Include.File( "Core/Database" ) -Include.File( "Core/Set" ) -Include.File( "Core/Point" ) -Include.File( "Core/Message" ) -Include.File( "Core/Fsm" ) - ---- Wrapper Classes -Include.File( "Wrapper/Object" ) -Include.File( "Wrapper/Identifiable" ) -Include.File( "Wrapper/Positionable" ) -Include.File( "Wrapper/Controllable" ) -Include.File( "Wrapper/Group" ) -Include.File( "Wrapper/Unit" ) -Include.File( "Wrapper/Client" ) -Include.File( "Wrapper/Static" ) -Include.File( "Wrapper/Airbase" ) -Include.File( "Wrapper/Scenery" ) - ---- Functional Classes -Include.File( "Functional/Scoring" ) -Include.File( "Functional/CleanUp" ) -Include.File( "Functional/Spawn" ) -Include.File( "Functional/Movement" ) -Include.File( "Functional/Sead" ) -Include.File( "Functional/Escort" ) -Include.File( "Functional/MissileTrainer" ) -Include.File( "Functional/AirbasePolice" ) -Include.File( "Functional/Detection" ) - ---- AI Classes -Include.File( "AI/AI_Balancer" ) -Include.File( "AI/AI_Patrol" ) -Include.File( "AI/AI_Cap" ) -Include.File( "AI/AI_Cas" ) -Include.File( "AI/AI_Cargo" ) - ---- Actions -Include.File( "Actions/Act_Assign" ) -Include.File( "Actions/Act_Route" ) -Include.File( "Actions/Act_Account" ) -Include.File( "Actions/Act_Assist" ) - ---- Task Handling Classes -Include.File( "Tasking/CommandCenter" ) -Include.File( "Tasking/Mission" ) -Include.File( "Tasking/Task" ) -Include.File( "Tasking/DetectionManager" ) -Include.File( "Tasking/Task_SEAD" ) -Include.File( "Tasking/Task_A2G" ) - - --- The order of the declarations is important here. Don't touch it. - ---- Declare the event dispatcher based on the EVENT class -_EVENTDISPATCHER = EVENT:New() -- Core.Event#EVENT - ---- Declare the timer dispatcher based on the SCHEDULEDISPATCHER class -_SCHEDULEDISPATCHER = SCHEDULEDISPATCHER:New() -- Core.Timer#SCHEDULEDISPATCHER - ---- Declare the main database object, which is used internally by the MOOSE classes. -_DATABASE = DATABASE:New() -- Database#DATABASE +Include.ProgramPath = "Scripts/Moose/" +env.info( "Include.ProgramPath = " .. Include.ProgramPath) +Include.Files = {} +Include.File( "Moose" ) -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 8ebe4125c..f73ae2d1c 100644 --- a/Moose Mission Setup/Moose.lua +++ b/Moose Mission Setup/Moose.lua @@ -1,33489 +1,31 @@ -env.info( '*** MOOSE STATIC INCLUDE START *** ' ) -env.info( 'Moose Generation Timestamp: 20170306_1631' ) +env.info( '*** MOOSE DYNAMIC INCLUDE START *** ' ) +env.info( 'Moose Generation Timestamp: 20170307_0856' ) + local base = _G Include = {} -Include.Files = {} + 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) + 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 + error ("Could not load MOOSE file " .. IncludeFile .. ".lua" ) else - return tostring(tbl) + env.info( "Include:" .. IncludeFile .. " loaded from " .. Include.ProgramPath ) + return f() 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 - - - - - ---3D Vector manipulation -routines.vec = {} - -routines.vec.add = function(vec1, vec2) - return {x = vec1.x + vec2.x, y = vec1.y + vec2.y, z = vec1.z + vec2.z} -end - -routines.vec.sub = function(vec1, vec2) - return {x = vec1.x - vec2.x, y = vec1.y - vec2.y, z = vec1.z - vec2.z} -end - -routines.vec.scalarMult = function(vec, mult) - return {x = vec.x*mult, y = vec.y*mult, z = vec.z*mult} -end - -routines.vec.scalar_mult = routines.vec.scalarMult - -routines.vec.dp = function(vec1, vec2) - return vec1.x*vec2.x + vec1.y*vec2.y + vec1.z*vec2.z -end - -routines.vec.cp = function(vec1, vec2) - return { x = vec1.y*vec2.z - vec1.z*vec2.y, y = vec1.z*vec2.x - vec1.x*vec2.z, z = vec1.x*vec2.y - vec1.y*vec2.x} -end - -routines.vec.mag = function(vec) - return (vec.x^2 + vec.y^2 + vec.z^2)^0.5 -end - -routines.vec.getUnitVec = function(vec) - local mag = routines.vec.mag(vec) - return { x = vec.x/mag, y = vec.y/mag, z = vec.z/mag } -end - -routines.vec.rotateVec2 = function(vec2, theta) - return { x = vec2.x*math.cos(theta) - vec2.y*math.sin(theta), y = vec2.x*math.sin(theta) + vec2.y*math.cos(theta)} -end ---------------------------------------------------------------------------------------------------------------------------- - - - - --- acc- the accuracy of each easting/northing. 0, 1, 2, 3, 4, or 5. -routines.tostringMGRS = function(MGRS, acc) - if acc == 0 then - return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph - else - return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph .. ' ' .. string.format('%0' .. acc .. 'd', routines.utils.round(MGRS.Easting/(10^(5-acc)), 0)) - .. ' ' .. string.format('%0' .. acc .. 'd', routines.utils.round(MGRS.Northing/(10^(5-acc)), 0)) - end -end - ---[[acc: -in DM: decimal point of minutes. -In DMS: decimal point of seconds. -position after the decimal of the least significant digit: -So: -42.32 - acc of 2. -]] -routines.tostringLL = function(lat, lon, acc, DMS) - - local latHemi, lonHemi - if lat > 0 then - latHemi = 'N' - else - latHemi = 'S' - end - - if lon > 0 then - lonHemi = 'E' - else - lonHemi = 'W' - end - - lat = math.abs(lat) - lon = math.abs(lon) - - local latDeg = math.floor(lat) - local latMin = (lat - latDeg)*60 - - local lonDeg = math.floor(lon) - local lonMin = (lon - lonDeg)*60 - - if DMS then -- degrees, minutes, and seconds. - local oldLatMin = latMin - latMin = math.floor(latMin) - local latSec = routines.utils.round((oldLatMin - latMin)*60, acc) - - local oldLonMin = lonMin - lonMin = math.floor(lonMin) - local lonSec = routines.utils.round((oldLonMin - lonMin)*60, acc) - - if latSec == 60 then - latSec = 0 - latMin = latMin + 1 - end - - if lonSec == 60 then - lonSec = 0 - lonMin = lonMin + 1 - end - - local secFrmtStr -- create the formatting string for the seconds place - if acc <= 0 then -- no decimal place. - secFrmtStr = '%02d' - else - local width = 3 + acc -- 01.310 - that's a width of 6, for example. - secFrmtStr = '%0' .. width .. '.' .. acc .. 'f' - end - - return string.format('%02d', latDeg) .. ' ' .. string.format('%02d', latMin) .. '\' ' .. string.format(secFrmtStr, latSec) .. '"' .. latHemi .. ' ' - .. string.format('%02d', lonDeg) .. ' ' .. string.format('%02d', lonMin) .. '\' ' .. string.format(secFrmtStr, lonSec) .. '"' .. lonHemi - - else -- degrees, decimal minutes. - latMin = routines.utils.round(latMin, acc) - lonMin = routines.utils.round(lonMin, acc) - - if latMin == 60 then - latMin = 0 - latDeg = latDeg + 1 - end - - if lonMin == 60 then - lonMin = 0 - lonDeg = lonDeg + 1 - end - - local minFrmtStr -- create the formatting string for the minutes place - if acc <= 0 then -- no decimal place. - minFrmtStr = '%02d' - else - local width = 3 + acc -- 01.310 - that's a width of 6, for example. - minFrmtStr = '%0' .. width .. '.' .. acc .. 'f' - end - - return string.format('%02d', latDeg) .. ' ' .. string.format(minFrmtStr, latMin) .. '\'' .. latHemi .. ' ' - .. string.format('%02d', lonDeg) .. ' ' .. string.format(minFrmtStr, lonMin) .. '\'' .. lonHemi - - end -end - ---[[ required: az - radian - required: dist - meters - optional: alt - meters (set to false or nil if you don't want to use it). - optional: metric - set true to get dist and alt in km and m. - precision will always be nearest degree and NM or km.]] -routines.tostringBR = function(az, dist, alt, metric) - az = routines.utils.round(routines.utils.toDegree(az), 0) - - if metric then - dist = routines.utils.round(dist/1000, 2) - else - dist = routines.utils.round(routines.utils.metersToNM(dist), 2) - end - - local s = string.format('%03d', az) .. ' for ' .. dist - - if alt then - if metric then - s = s .. ' at ' .. routines.utils.round(alt, 0) - else - s = s .. ' at ' .. routines.utils.round(routines.utils.metersToFeet(alt), 0) - end - end - return s -end - -routines.getNorthCorrection = function(point) --gets the correction needed for true north - if not point.z then --Vec2; convert to Vec3 - point.z = point.y - point.y = 0 - end - local lat, lon = coord.LOtoLL(point) - local north_posit = coord.LLtoLO(lat + 1, lon) - return math.atan2(north_posit.z - point.z, north_posit.x - point.x) -end - - -do - local idNum = 0 - - --Simplified event handler - routines.addEventHandler = function(f) --id is optional! - local handler = {} - idNum = idNum + 1 - handler.id = idNum - handler.f = f - handler.onEvent = function(self, event) - self.f(event) - end - world.addEventHandler(handler) - end - - routines.removeEventHandler = function(id) - for key, handler in pairs(world.eventHandlers) do - if handler.id and handler.id == id then - world.eventHandlers[key] = nil - return true - end - end - return false - end -end - --- need to return a Vec3 or Vec2? -function routines.getRandPointInCircle(point, radius, innerRadius) - local theta = 2*math.pi*math.random() - local rad = math.random() + math.random() - if rad > 1 then - rad = 2 - rad - end - - local radMult - if innerRadius and innerRadius <= radius then - radMult = (radius - innerRadius)*rad + innerRadius - else - radMult = radius*rad - end - - if not point.z then --might as well work with vec2/3 - point.z = point.y - end - - local rndCoord - if radius > 0 then - rndCoord = {x = math.cos(theta)*radMult + point.x, y = math.sin(theta)*radMult + point.z} - else - rndCoord = {x = point.x, y = point.z} - end - return rndCoord -end - -routines.goRoute = function(group, path) - local misTask = { - id = 'Mission', - params = { - route = { - points = routines.utils.deepCopy(path), - }, - }, - } - if type(group) == 'string' then - group = Group.getByName(group) - end - local groupCon = group:getController() - if groupCon then - groupCon:setTask(misTask) - return true - end - - Controller.setTask(groupCon, misTask) - return false -end - - --- Useful atomic functions from mist, ported. - -routines.ground = {} -routines.fixedWing = {} -routines.heli = {} - -routines.ground.buildWP = function(point, overRideForm, overRideSpeed) - - local wp = {} - wp.x = point.x - - if point.z then - wp.y = point.z - else - wp.y = point.y - end - local form, speed - - if point.speed and not overRideSpeed then - wp.speed = point.speed - elseif type(overRideSpeed) == 'number' then - wp.speed = overRideSpeed - else - wp.speed = routines.utils.kmphToMps(20) - end - - if point.form and not overRideForm then - form = point.form - else - form = overRideForm - end - - if not form then - wp.action = 'Cone' - else - form = string.lower(form) - if form == 'off_road' or form == 'off road' then - wp.action = 'Off Road' - elseif form == 'on_road' or form == 'on road' then - wp.action = 'On Road' - elseif form == 'rank' or form == 'line_abrest' or form == 'line abrest' or form == 'lineabrest'then - wp.action = 'Rank' - elseif form == 'cone' then - wp.action = 'Cone' - elseif form == 'diamond' then - wp.action = 'Diamond' - elseif form == 'vee' then - wp.action = 'Vee' - elseif form == 'echelon_left' or form == 'echelon left' or form == 'echelonl' then - wp.action = 'EchelonL' - elseif form == 'echelon_right' or form == 'echelon right' or form == 'echelonr' then - wp.action = 'EchelonR' - else - wp.action = 'Cone' -- if nothing matched - end - end - - wp.type = 'Turning Point' - - return wp - -end - -routines.fixedWing.buildWP = function(point, WPtype, speed, alt, altType) - - local wp = {} - wp.x = point.x - - if point.z then - wp.y = point.z - else - wp.y = point.y - end - - if alt and type(alt) == 'number' then - wp.alt = alt - else - wp.alt = 2000 - end - - if altType then - altType = string.lower(altType) - if altType == 'radio' or 'agl' then - wp.alt_type = 'RADIO' - elseif altType == 'baro' or 'asl' then - wp.alt_type = 'BARO' - end - else - wp.alt_type = 'RADIO' - end - - if point.speed then - speed = point.speed - end - - if point.type then - WPtype = point.type - end - - if not speed then - wp.speed = routines.utils.kmphToMps(500) - else - wp.speed = speed - end - - if not WPtype then - wp.action = 'Turning Point' - else - WPtype = string.lower(WPtype) - if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then - wp.action = 'Fly Over Point' - elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then - wp.action = 'Turning Point' - else - wp.action = 'Turning Point' - end - end - - wp.type = 'Turning Point' - return wp -end - -routines.heli.buildWP = function(point, WPtype, speed, alt, altType) - - local wp = {} - wp.x = point.x - - if point.z then - wp.y = point.z - else - wp.y = point.y - end - - if alt and type(alt) == 'number' then - wp.alt = alt - else - wp.alt = 500 - end - - if altType then - altType = string.lower(altType) - if altType == 'radio' or 'agl' then - wp.alt_type = 'RADIO' - elseif altType == 'baro' or 'asl' then - wp.alt_type = 'BARO' - end - else - wp.alt_type = 'RADIO' - end - - if point.speed then - speed = point.speed - end - - if point.type then - WPtype = point.type - end - - if not speed then - wp.speed = routines.utils.kmphToMps(200) - else - wp.speed = speed - end - - if not WPtype then - wp.action = 'Turning Point' - else - WPtype = string.lower(WPtype) - if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then - wp.action = 'Fly Over Point' - elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then - wp.action = 'Turning Point' - else - wp.action = 'Turning Point' - end - end - - wp.type = 'Turning Point' - return wp -end - -routines.groupToRandomPoint = function(vars) - local group = vars.group --Required - local point = vars.point --required - local radius = vars.radius or 0 - local innerRadius = vars.innerRadius - local form = vars.form or 'Cone' - local heading = vars.heading or math.random()*2*math.pi - local headingDegrees = vars.headingDegrees - local speed = vars.speed or routines.utils.kmphToMps(20) - - - local useRoads - if not vars.disableRoads then - useRoads = true - else - useRoads = false - end - - local path = {} - - if headingDegrees then - heading = headingDegrees*math.pi/180 - end - - if heading >= 2*math.pi then - heading = heading - 2*math.pi - end - - local rndCoord = routines.getRandPointInCircle(point, radius, innerRadius) - - local offset = {} - local posStart = routines.getLeadPos(group) - - offset.x = routines.utils.round(math.sin(heading - (math.pi/2)) * 50 + rndCoord.x, 3) - offset.z = routines.utils.round(math.cos(heading + (math.pi/2)) * 50 + rndCoord.y, 3) - path[#path + 1] = routines.ground.buildWP(posStart, form, speed) - - - if useRoads == true and ((point.x - posStart.x)^2 + (point.z - posStart.z)^2)^0.5 > radius * 1.3 then - path[#path + 1] = routines.ground.buildWP({['x'] = posStart.x + 11, ['z'] = posStart.z + 11}, 'off_road', speed) - path[#path + 1] = routines.ground.buildWP(posStart, 'on_road', speed) - path[#path + 1] = routines.ground.buildWP(offset, 'on_road', speed) - else - path[#path + 1] = routines.ground.buildWP({['x'] = posStart.x + 25, ['z'] = posStart.z + 25}, form, speed) - end - - path[#path + 1] = routines.ground.buildWP(offset, form, speed) - path[#path + 1] = routines.ground.buildWP(rndCoord, form, speed) - - routines.goRoute(group, path) - - return -end - -routines.groupRandomDistSelf = function(gpData, dist, form, heading, speed) - local pos = routines.getLeadPos(gpData) - local fakeZone = {} - fakeZone.radius = dist or math.random(300, 1000) - fakeZone.point = {x = pos.x, y, pos.y, z = pos.z} - routines.groupToRandomZone(gpData, fakeZone, form, heading, speed) - - return -end - -routines.groupToRandomZone = function(gpData, zone, form, heading, speed) - if type(gpData) == 'string' then - gpData = Group.getByName(gpData) - end - - if type(zone) == 'string' then - zone = trigger.misc.getZone(zone) - elseif type(zone) == 'table' and not zone.radius then - zone = trigger.misc.getZone(zone[math.random(1, #zone)]) - end - - if speed then - speed = routines.utils.kmphToMps(speed) - end - - local vars = {} - vars.group = gpData - vars.radius = zone.radius - vars.form = form - vars.headingDegrees = heading - vars.speed = speed - vars.point = routines.utils.zoneToVec3(zone) - - routines.groupToRandomPoint(vars) - - return -end - -routines.isTerrainValid = function(coord, terrainTypes) -- vec2/3 and enum or table of acceptable terrain types - if coord.z then - coord.y = coord.z - end - local typeConverted = {} - - if type(terrainTypes) == 'string' then -- if its a string it does this check - for constId, constData in pairs(land.SurfaceType) do - if string.lower(constId) == string.lower(terrainTypes) or string.lower(constData) == string.lower(terrainTypes) then - table.insert(typeConverted, constId) - end - end - elseif type(terrainTypes) == 'table' then -- if its a table it does this check - for typeId, typeData in pairs(terrainTypes) do - for constId, constData in pairs(land.SurfaceType) do - if string.lower(constId) == string.lower(typeData) or string.lower(constData) == string.lower(typeId) then - table.insert(typeConverted, constId) - end - end - end - end - for validIndex, validData in pairs(typeConverted) do - if land.getSurfaceType(coord) == land.SurfaceType[validData] then - return true - end - end - return false -end - -routines.groupToPoint = function(gpData, point, form, heading, speed, useRoads) - if type(point) == 'string' then - point = trigger.misc.getZone(point) - end - if speed then - speed = routines.utils.kmphToMps(speed) - end - - local vars = {} - vars.group = gpData - vars.form = form - vars.headingDegrees = heading - vars.speed = speed - vars.disableRoads = useRoads - vars.point = routines.utils.zoneToVec3(point) - routines.groupToRandomPoint(vars) - - return -end - - -routines.getLeadPos = function(group) - if type(group) == 'string' then -- group name - group = Group.getByName(group) - end - - local units = group:getUnits() - - local leader = units[1] - if not leader then -- SHOULD be good, but if there is a bug, this code future-proofs it then. - local lowestInd = math.huge - for ind, unit in pairs(units) do - if ind < lowestInd then - lowestInd = ind - leader = unit - end - end - end - if leader and Unit.isExist(leader) then -- maybe a little too paranoid now... - return leader:getPosition().p - end -end - ---[[ vars for routines.getMGRSString: -vars.units - table of unit names (NOT unitNameTable- maybe this should change). -vars.acc - integer between 0 and 5, inclusive -]] -routines.getMGRSString = function(vars) - local units = vars.units - local acc = vars.acc or 5 - local avgPos = routines.getAvgPos(units) - if avgPos then - return routines.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(avgPos)), acc) - end -end - ---[[ vars for routines.getLLString -vars.units - table of unit names (NOT unitNameTable- maybe this should change). -vars.acc - integer, number of numbers after decimal place -vars.DMS - if true, output in degrees, minutes, seconds. Otherwise, output in degrees, minutes. - - -]] -routines.getLLString = function(vars) - local units = vars.units - local acc = vars.acc or 3 - local DMS = vars.DMS - local avgPos = routines.getAvgPos(units) - if avgPos then - local lat, lon = coord.LOtoLL(avgPos) - return routines.tostringLL(lat, lon, acc, DMS) - end -end - ---[[ -vars.zone - table of a zone name. -vars.ref - vec3 ref point, maybe overload for vec2 as well? -vars.alt - boolean, if used, includes altitude in string -vars.metric - boolean, gives distance in km instead of NM. -]] -routines.getBRStringZone = function(vars) - local zone = trigger.misc.getZone( vars.zone ) - local ref = routines.utils.makeVec3(vars.ref, 0) -- turn it into Vec3 if it is not already. - local alt = vars.alt - local metric = vars.metric - if zone then - local vec = {x = zone.point.x - ref.x, y = zone.point.y - ref.y, z = zone.point.z - ref.z} - local dir = routines.utils.getDir(vec, ref) - local dist = routines.utils.get2DDist(zone.point, ref) - if alt then - alt = zone.y - end - return routines.tostringBR(dir, dist, alt, metric) - else - env.info( 'routines.getBRStringZone: error: zone is nil' ) - end -end - ---[[ -vars.units- table of unit names (NOT unitNameTable- maybe this should change). -vars.ref - vec3 ref point, maybe overload for vec2 as well? -vars.alt - boolean, if used, includes altitude in string -vars.metric - boolean, gives distance in km instead of NM. -]] -routines.getBRString = function(vars) - local units = vars.units - local ref = routines.utils.makeVec3(vars.ref, 0) -- turn it into Vec3 if it is not already. - local alt = vars.alt - local metric = vars.metric - local avgPos = routines.getAvgPos(units) - if avgPos then - local vec = {x = avgPos.x - ref.x, y = avgPos.y - ref.y, z = avgPos.z - ref.z} - local dir = routines.utils.getDir(vec, ref) - local dist = routines.utils.get2DDist(avgPos, ref) - if alt then - alt = avgPos.y - end - return routines.tostringBR(dir, dist, alt, metric) - end -end - - --- Returns the Vec3 coordinates of the average position of the concentration of units most in the heading direction. ---[[ vars for routines.getLeadingPos: -vars.units - table of unit names -vars.heading - direction -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees -]] -routines.getLeadingPos = function(vars) - local units = vars.units - local heading = vars.heading - local radius = vars.radius - if vars.headingDegrees then - heading = routines.utils.toRadian(vars.headingDegrees) - end - - local unitPosTbl = {} - for i = 1, #units do - local unit = Unit.getByName(units[i]) - if unit and unit:isExist() then - unitPosTbl[#unitPosTbl + 1] = unit:getPosition().p - end - end - if #unitPosTbl > 0 then -- one more more units found. - -- first, find the unit most in the heading direction - local maxPos = -math.huge - - local maxPosInd -- maxPos - the furthest in direction defined by heading; maxPosInd = - for i = 1, #unitPosTbl do - local rotatedVec2 = routines.vec.rotateVec2(routines.utils.makeVec2(unitPosTbl[i]), heading) - if (not maxPos) or maxPos < rotatedVec2.x then - maxPos = rotatedVec2.x - maxPosInd = i - end - end - - --now, get all the units around this unit... - local avgPos - if radius then - local maxUnitPos = unitPosTbl[maxPosInd] - local avgx, avgy, avgz, totNum = 0, 0, 0, 0 - for i = 1, #unitPosTbl do - if routines.utils.get2DDist(maxUnitPos, unitPosTbl[i]) <= radius then - avgx = avgx + unitPosTbl[i].x - avgy = avgy + unitPosTbl[i].y - avgz = avgz + unitPosTbl[i].z - totNum = totNum + 1 - end - end - avgPos = { x = avgx/totNum, y = avgy/totNum, z = avgz/totNum} - else - avgPos = unitPosTbl[maxPosInd] - end - - return avgPos - end -end - - ---[[ vars for routines.getLeadingMGRSString: -vars.units - table of unit names -vars.heading - direction -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees -vars.acc - number, 0 to 5. -]] -routines.getLeadingMGRSString = function(vars) - local pos = routines.getLeadingPos(vars) - if pos then - local acc = vars.acc or 5 - return routines.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(pos)), acc) - end -end - ---[[ vars for routines.getLeadingLLString: -vars.units - table of unit names -vars.heading - direction, number -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees -vars.acc - number of digits after decimal point (can be negative) -vars.DMS - boolean, true if you want DMS. -]] -routines.getLeadingLLString = function(vars) - local pos = routines.getLeadingPos(vars) - if pos then - local acc = vars.acc or 3 - local DMS = vars.DMS - local lat, lon = coord.LOtoLL(pos) - return routines.tostringLL(lat, lon, acc, DMS) - end -end - - - ---[[ vars for routines.getLeadingBRString: -vars.units - table of unit names -vars.heading - direction, number -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees -vars.metric - boolean, if true, use km instead of NM. -vars.alt - boolean, if true, include altitude. -vars.ref - vec3/vec2 reference point. -]] -routines.getLeadingBRString = function(vars) - local pos = routines.getLeadingPos(vars) - if pos then - local ref = vars.ref - local alt = vars.alt - local metric = vars.metric - - local vec = {x = pos.x - ref.x, y = pos.y - ref.y, z = pos.z - ref.z} - local dir = routines.utils.getDir(vec, ref) - local dist = routines.utils.get2DDist(pos, ref) - if alt then - alt = pos.y - end - return routines.tostringBR(dir, dist, alt, metric) - end -end - ---[[ vars for routines.message.add - vars.text = 'Hello World' - vars.displayTime = 20 - vars.msgFor = {coa = {'red'}, countries = {'Ukraine', 'Georgia'}, unitTypes = {'A-10C'}} - -]] - ---[[ vars for routines.msgMGRS -vars.units - table of unit names (NOT unitNameTable- maybe this should change). -vars.acc - integer between 0 and 5, inclusive -vars.text - text in the message -vars.displayTime - self explanatory -vars.msgFor - scope -]] -routines.msgMGRS = function(vars) - local units = vars.units - local acc = vars.acc - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getMGRSString{units = units, acc = acc} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } -end - ---[[ vars for routines.msgLL -vars.units - table of unit names (NOT unitNameTable- maybe this should change) (Yes). -vars.acc - integer, number of numbers after decimal place -vars.DMS - if true, output in degrees, minutes, seconds. Otherwise, output in degrees, minutes. -vars.text - text in the message -vars.displayTime - self explanatory -vars.msgFor - scope -]] -routines.msgLL = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local acc = vars.acc - local DMS = vars.DMS - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getLLString{units = units, acc = acc, DMS = DMS} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } - -end - - ---[[ -vars.units- table of unit names (NOT unitNameTable- maybe this should change). -vars.ref - vec3 ref point, maybe overload for vec2 as well? -vars.alt - boolean, if used, includes altitude in string -vars.metric - boolean, gives distance in km instead of NM. -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] -routines.msgBR = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local ref = vars.ref -- vec2/vec3 will be handled in routines.getBRString - local alt = vars.alt - local metric = vars.metric - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getBRString{units = units, ref = ref, alt = alt, metric = metric} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } - -end - - --------------------------------------------------------------------------------------------- --- basically, just sub-types of routines.msgBR... saves folks the work of getting the ref point. ---[[ -vars.units- table of unit names (NOT unitNameTable- maybe this should change). -vars.ref - string red, blue -vars.alt - boolean, if used, includes altitude in string -vars.metric - boolean, gives distance in km instead of NM. -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] -routines.msgBullseye = function(vars) - if string.lower(vars.ref) == 'red' then - vars.ref = routines.DBs.missionData.bullseye.red - routines.msgBR(vars) - elseif string.lower(vars.ref) == 'blue' then - vars.ref = routines.DBs.missionData.bullseye.blue - routines.msgBR(vars) - end -end - ---[[ -vars.units- table of unit names (NOT unitNameTable- maybe this should change). -vars.ref - unit name of reference point -vars.alt - boolean, if used, includes altitude in string -vars.metric - boolean, gives distance in km instead of NM. -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] - -routines.msgBRA = function(vars) - if Unit.getByName(vars.ref) then - vars.ref = Unit.getByName(vars.ref):getPosition().p - if not vars.alt then - vars.alt = true - end - routines.msgBR(vars) - end -end --------------------------------------------------------------------------------------------- - ---[[ vars for routines.msgLeadingMGRS: -vars.units - table of unit names -vars.heading - direction -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees (optional) -vars.acc - number, 0 to 5. -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] -routines.msgLeadingMGRS = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local heading = vars.heading - local radius = vars.radius - local headingDegrees = vars.headingDegrees - local acc = vars.acc - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getLeadingMGRSString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } - - -end ---[[ vars for routines.msgLeadingLL: -vars.units - table of unit names -vars.heading - direction, number -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees (optional) -vars.acc - number of digits after decimal point (can be negative) -vars.DMS - boolean, true if you want DMS. (optional) -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] -routines.msgLeadingLL = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local heading = vars.heading - local radius = vars.radius - local headingDegrees = vars.headingDegrees - local acc = vars.acc - local DMS = vars.DMS - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getLeadingLLString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc, DMS = DMS} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } - -end - ---[[ -vars.units - table of unit names -vars.heading - direction, number -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees (optional) -vars.metric - boolean, if true, use km instead of NM. (optional) -vars.alt - boolean, if true, include altitude. (optional) -vars.ref - vec3/vec2 reference point. -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] -routines.msgLeadingBR = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local heading = vars.heading - local radius = vars.radius - local headingDegrees = vars.headingDegrees - local metric = vars.metric - local alt = vars.alt - local ref = vars.ref -- vec2/vec3 will be handled in routines.getBRString - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getLeadingBRString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, metric = metric, alt = alt, ref = ref} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } -end - - -function spairs(t, order) - -- collect the keys - local keys = {} - for k in pairs(t) do keys[#keys+1] = k end - - -- if order function given, sort by it by passing the table and keys a, b, - -- otherwise just sort the keys - if order then - table.sort(keys, function(a,b) return order(t, a, b) end) - else - table.sort(keys) - end - - -- return the iterator function - local i = 0 - return function() - i = i + 1 - if keys[i] then - return keys[i], t[keys[i]] - end - end -end - - -function routines.IsPartOfGroupInZones( CargoGroup, LandingZones ) ---trace.f() - - local CurrentZoneID = nil - - if CargoGroup then - local CargoUnits = CargoGroup:getUnits() - for CargoUnitID, CargoUnit in pairs( CargoUnits ) do - if CargoUnit and CargoUnit:getLife() >= 1.0 then - CurrentZoneID = routines.IsUnitInZones( CargoUnit, LandingZones ) - if CurrentZoneID then - break - end - end - end - end - ---trace.r( "", "", { CurrentZoneID } ) - return CurrentZoneID -end - - - -function routines.IsUnitInZones( TransportUnit, LandingZones ) ---trace.f("", "routines.IsUnitInZones" ) - - local TransportZoneResult = nil - local TransportZonePos = nil - local TransportZone = nil - - -- fill-up some local variables to support further calculations to determine location of units within the zone. - if TransportUnit then - local TransportUnitPos = TransportUnit:getPosition().p - if type( LandingZones ) == "table" then - for LandingZoneID, LandingZoneName in pairs( LandingZones ) do - TransportZone = trigger.misc.getZone( LandingZoneName ) - if TransportZone then - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then - TransportZoneResult = LandingZoneID - break - end - end - end - else - TransportZone = trigger.misc.getZone( LandingZones ) - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then - TransportZoneResult = 1 - end - end - if TransportZoneResult then - --trace.i( "routines", "TransportZone:" .. TransportZoneResult ) - else - --trace.i( "routines", "TransportZone:nil logic" ) - end - return TransportZoneResult - else - --trace.i( "routines", "TransportZone:nil hard" ) - return nil - end -end - -function routines.IsUnitNearZonesRadius( TransportUnit, LandingZones, ZoneRadius ) ---trace.f("", "routines.IsUnitInZones" ) - - local TransportZoneResult = nil - local TransportZonePos = nil - local TransportZone = nil - - -- fill-up some local variables to support further calculations to determine location of units within the zone. - if TransportUnit then - local TransportUnitPos = TransportUnit:getPosition().p - if type( LandingZones ) == "table" then - for LandingZoneID, LandingZoneName in pairs( LandingZones ) do - TransportZone = trigger.misc.getZone( LandingZoneName ) - if TransportZone then - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= ZoneRadius ) then - TransportZoneResult = LandingZoneID - break - end - end - end - else - TransportZone = trigger.misc.getZone( LandingZones ) - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= ZoneRadius ) then - TransportZoneResult = 1 - end - end - if TransportZoneResult then - --trace.i( "routines", "TransportZone:" .. TransportZoneResult ) - else - --trace.i( "routines", "TransportZone:nil logic" ) - end - return TransportZoneResult - else - --trace.i( "routines", "TransportZone:nil hard" ) - return nil - end -end - - -function routines.IsStaticInZones( TransportStatic, LandingZones ) ---trace.f() - - local TransportZoneResult = nil - local TransportZonePos = nil - local TransportZone = nil - - -- fill-up some local variables to support further calculations to determine location of units within the zone. - local TransportStaticPos = TransportStatic:getPosition().p - if type( LandingZones ) == "table" then - for LandingZoneID, LandingZoneName in pairs( LandingZones ) do - TransportZone = trigger.misc.getZone( LandingZoneName ) - if TransportZone then - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportStaticPos.x - TransportZonePos.x)^2 + (TransportStaticPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then - TransportZoneResult = LandingZoneID - break - end - end - end - else - TransportZone = trigger.misc.getZone( LandingZones ) - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportStaticPos.x - TransportZonePos.x)^2 + (TransportStaticPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then - TransportZoneResult = 1 - end - end - ---trace.r( "", "", { TransportZoneResult } ) - return TransportZoneResult -end - - -function routines.IsUnitInRadius( CargoUnit, ReferencePosition, Radius ) ---trace.f() - - local Valid = true - - -- fill-up some local variables to support further calculations to determine location of units within the zone. - local CargoPos = CargoUnit:getPosition().p - local ReferenceP = ReferencePosition.p - - if (((CargoPos.x - ReferenceP.x)^2 + (CargoPos.z - ReferenceP.z)^2)^0.5 <= Radius) then - else - Valid = false - end - - return Valid -end - -function routines.IsPartOfGroupInRadius( CargoGroup, ReferencePosition, Radius ) ---trace.f() - - local Valid = true - - Valid = routines.ValidateGroup( CargoGroup, "CargoGroup", Valid ) - - -- fill-up some local variables to support further calculations to determine location of units within the zone - local CargoUnits = CargoGroup:getUnits() - for CargoUnitId, CargoUnit in pairs( CargoUnits ) do - local CargoUnitPos = CargoUnit:getPosition().p --- env.info( 'routines.IsPartOfGroupInRadius: CargoUnitPos.x = ' .. CargoUnitPos.x .. ' CargoUnitPos.z = ' .. CargoUnitPos.z ) - local ReferenceP = ReferencePosition.p --- env.info( 'routines.IsPartOfGroupInRadius: ReferenceGroupPos.x = ' .. ReferenceGroupPos.x .. ' ReferenceGroupPos.z = ' .. ReferenceGroupPos.z ) - - if ((( CargoUnitPos.x - ReferenceP.x)^2 + (CargoUnitPos.z - ReferenceP.z)^2)^0.5 <= Radius) then - else - Valid = false - break - end - end - - return Valid -end - - -function routines.ValidateString( Variable, VariableName, Valid ) ---trace.f() - - if type( Variable ) == "string" then - if Variable == "" then - error( "routines.ValidateString: error: " .. VariableName .. " must be filled out!" ) - Valid = false - end - else - error( "routines.ValidateString: error: " .. VariableName .. " is not a string." ) - Valid = false - end - ---trace.r( "", "", { Valid } ) - return Valid -end - -function routines.ValidateNumber( Variable, VariableName, Valid ) ---trace.f() - - if type( Variable ) == "number" then - else - error( "routines.ValidateNumber: error: " .. VariableName .. " is not a number." ) - Valid = false - end - ---trace.r( "", "", { Valid } ) - return Valid - -end - -function routines.ValidateGroup( Variable, VariableName, Valid ) ---trace.f() - - if Variable == nil then - error( "routines.ValidateGroup: error: " .. VariableName .. " is a nil value!" ) - Valid = false - end - ---trace.r( "", "", { Valid } ) - return Valid -end - -function routines.ValidateZone( LandingZones, VariableName, Valid ) ---trace.f() - - if LandingZones == nil then - error( "routines.ValidateGroup: error: " .. VariableName .. " is a nil value!" ) - Valid = false - end - - if type( LandingZones ) == "table" then - for LandingZoneID, LandingZoneName in pairs( LandingZones ) do - if trigger.misc.getZone( LandingZoneName ) == nil then - error( "routines.ValidateGroup: error: Zone " .. LandingZoneName .. " does not exist!" ) - Valid = false - break - end - end - else - if trigger.misc.getZone( LandingZones ) == nil then - error( "routines.ValidateGroup: error: Zone " .. LandingZones .. " does not exist!" ) - Valid = false - end - end - ---trace.r( "", "", { Valid } ) - return Valid -end - -function routines.ValidateEnumeration( Variable, VariableName, Enum, Valid ) ---trace.f() - - local ValidVariable = false - - for EnumId, EnumData in pairs( Enum ) do - if Variable == EnumData then - ValidVariable = true - break - end - end - - if ValidVariable then - else - error( 'TransportValidateEnum: " .. VariableName .. " is not a valid type.' .. Variable ) - Valid = false - end - ---trace.r( "", "", { Valid } ) - return Valid -end - -function routines.getGroupRoute(groupIdent, task) -- same as getGroupPoints but returns speed and formation type along with vec2 of point} - -- refactor to search by groupId and allow groupId and groupName as inputs - local gpId = groupIdent - if type(groupIdent) == 'string' and not tonumber(groupIdent) then - gpId = _DATABASE.Templates.Groups[groupIdent].groupId - end - - for coa_name, coa_data in pairs(env.mission.coalition) do - if (coa_name == 'red' or coa_name == 'blue') and type(coa_data) == 'table' then - if coa_data.country then --there is a country table - for cntry_id, cntry_data in pairs(coa_data.country) do - for obj_type_name, obj_type_data in pairs(cntry_data) do - if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" then -- only these types have points - if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! - for group_num, group_data in pairs(obj_type_data.group) do - if group_data and group_data.groupId == gpId then -- this is the group we are looking for - if group_data.route and group_data.route.points and #group_data.route.points > 0 then - local points = {} - - for point_num, point in pairs(group_data.route.points) do - local routeData = {} - if not point.point then - routeData.x = point.x - routeData.y = point.y - else - routeData.point = point.point --it's possible that the ME could move to the point = Vec2 notation. - end - routeData.form = point.action - routeData.speed = point.speed - routeData.alt = point.alt - routeData.alt_type = point.alt_type - routeData.airdromeId = point.airdromeId - routeData.helipadId = point.helipadId - routeData.type = point.type - routeData.action = point.action - if task then - routeData.task = point.task - end - points[point_num] = routeData - end - - return points - end - return - end --if group_data and group_data.name and group_data.name == 'groupname' - end --for group_num, group_data in pairs(obj_type_data.group) do - end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then - end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then - end --for obj_type_name, obj_type_data in pairs(cntry_data) do - end --for cntry_id, cntry_data in pairs(coa_data.country) do - end --if coa_data.country then --there is a country table - end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then - end --for coa_name, coa_data in pairs(mission.coalition) do -end - -routines.ground.patrolRoute = function(vars) - - - local tempRoute = {} - local useRoute = {} - local gpData = vars.gpData - if type(gpData) == 'string' then - gpData = Group.getByName(gpData) - end - - local useGroupRoute - if not vars.useGroupRoute then - useGroupRoute = vars.gpData - else - useGroupRoute = vars.useGroupRoute - end - local routeProvided = false - if not vars.route then - if useGroupRoute then - tempRoute = routines.getGroupRoute(useGroupRoute) - end - else - useRoute = vars.route - local posStart = routines.getLeadPos(gpData) - useRoute[1] = routines.ground.buildWP(posStart, useRoute[1].action, useRoute[1].speed) - routeProvided = true - end - - - local overRideSpeed = vars.speed or 'default' - local pType = vars.pType - local offRoadForm = vars.offRoadForm or 'default' - local onRoadForm = vars.onRoadForm or 'default' - - if routeProvided == false and #tempRoute > 0 then - local posStart = routines.getLeadPos(gpData) - - - useRoute[#useRoute + 1] = routines.ground.buildWP(posStart, offRoadForm, overRideSpeed) - for i = 1, #tempRoute do - local tempForm = tempRoute[i].action - local tempSpeed = tempRoute[i].speed - - if offRoadForm == 'default' then - tempForm = tempRoute[i].action - end - if onRoadForm == 'default' then - onRoadForm = 'On Road' - end - if (string.lower(tempRoute[i].action) == 'on road' or string.lower(tempRoute[i].action) == 'onroad' or string.lower(tempRoute[i].action) == 'on_road') then - tempForm = onRoadForm - else - tempForm = offRoadForm - end - - if type(overRideSpeed) == 'number' then - tempSpeed = overRideSpeed - end - - - useRoute[#useRoute + 1] = routines.ground.buildWP(tempRoute[i], tempForm, tempSpeed) - end - - if pType and string.lower(pType) == 'doubleback' then - local curRoute = routines.utils.deepCopy(useRoute) - for i = #curRoute, 2, -1 do - useRoute[#useRoute + 1] = routines.ground.buildWP(curRoute[i], curRoute[i].action, curRoute[i].speed) - end - end - - useRoute[1].action = useRoute[#useRoute].action -- make it so the first WP matches the last WP - end - - local cTask3 = {} - local newPatrol = {} - newPatrol.route = useRoute - newPatrol.gpData = gpData:getName() - cTask3[#cTask3 + 1] = 'routines.ground.patrolRoute(' - cTask3[#cTask3 + 1] = routines.utils.oneLineSerialize(newPatrol) - cTask3[#cTask3 + 1] = ')' - cTask3 = table.concat(cTask3) - local tempTask = { - id = 'WrappedAction', - params = { - action = { - id = 'Script', - params = { - command = cTask3, - - }, - }, - }, - } - - - useRoute[#useRoute].task = tempTask - routines.goRoute(gpData, useRoute) - - return -end - -routines.ground.patrol = function(gpData, pType, form, speed) - local vars = {} - - if type(gpData) == 'table' and gpData:getName() then - gpData = gpData:getName() - end - - vars.useGroupRoute = gpData - vars.gpData = gpData - vars.pType = pType - vars.offRoadForm = form - vars.speed = speed - - routines.ground.patrolRoute(vars) - - return -end - -function routines.GetUnitHeight( CheckUnit ) ---trace.f( "routines" ) - - local UnitPoint = CheckUnit:getPoint() - local UnitPosition = { x = UnitPoint.x, y = UnitPoint.z } - local UnitHeight = UnitPoint.y - - local LandHeight = land.getHeight( UnitPosition ) - - --env.info(( 'CarrierHeight: LandHeight = ' .. LandHeight .. ' CarrierHeight = ' .. CarrierHeight )) - - --trace.f( "routines", "Unit Height = " .. UnitHeight - LandHeight ) - - return UnitHeight - LandHeight - -end - - - -Su34Status = { status = {} } -boardMsgRed = { statusMsg = "" } -boardMsgAll = { timeMsg = "" } -SpawnSettings = {} -Su34MenuPath = {} -Su34Menus = 0 - - -function Su34AttackCarlVinson(groupName) ---trace.menu("", "Su34AttackCarlVinson") - local groupSu34 = Group.getByName( groupName ) - local controllerSu34 = groupSu34.getController(groupSu34) - local groupCarlVinson = Group.getByName("US Carl Vinson #001") - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE ) - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) - if groupCarlVinson ~= nil then - controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupCarlVinson:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = true}}) - end - Su34Status.status[groupName] = 1 - MessageToRed( string.format('%s: ',groupName) .. 'Attacking carrier Carl Vinson. ', 10, 'RedStatus' .. groupName ) -end - -function Su34AttackWest(groupName) ---trace.f("","Su34AttackWest") - local groupSu34 = Group.getByName( groupName ) - local controllerSu34 = groupSu34.getController(groupSu34) - local groupShipWest1 = Group.getByName("US Ship West #001") - local groupShipWest2 = Group.getByName("US Ship West #002") - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE ) - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) - if groupShipWest1 ~= nil then - controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipWest1:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = true}}) - end - if groupShipWest2 ~= nil then - controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipWest2:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = true}}) - end - Su34Status.status[groupName] = 2 - MessageToRed( string.format('%s: ',groupName) .. 'Attacking invading ships in the west. ', 10, 'RedStatus' .. groupName ) -end - -function Su34AttackNorth(groupName) ---trace.menu("","Su34AttackNorth") - local groupSu34 = Group.getByName( groupName ) - local controllerSu34 = groupSu34.getController(groupSu34) - local groupShipNorth1 = Group.getByName("US Ship North #001") - local groupShipNorth2 = Group.getByName("US Ship North #002") - local groupShipNorth3 = Group.getByName("US Ship North #003") - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE ) - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) - if groupShipNorth1 ~= nil then - controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipNorth1:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = false}}) - end - if groupShipNorth2 ~= nil then - controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipNorth2:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = false}}) - end - if groupShipNorth3 ~= nil then - controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipNorth3:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = false}}) - end - Su34Status.status[groupName] = 3 - MessageToRed( string.format('%s: ',groupName) .. 'Attacking invading ships in the north. ', 10, 'RedStatus' .. groupName ) -end - -function Su34Orbit(groupName) ---trace.menu("","Su34Orbit") - local groupSu34 = Group.getByName( groupName ) - local controllerSu34 = groupSu34:getController() - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD ) - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) - controllerSu34:pushTask( {id = 'ControlledTask', params = { task = { id = 'Orbit', params = { pattern = AI.Task.OrbitPattern.RACE_TRACK } }, stopCondition = { duration = 600 } } } ) - Su34Status.status[groupName] = 4 - MessageToRed( string.format('%s: ',groupName) .. 'In orbit and awaiting further instructions. ', 10, 'RedStatus' .. groupName ) -end - -function Su34TakeOff(groupName) ---trace.menu("","Su34TakeOff") - local groupSu34 = Group.getByName( groupName ) - local controllerSu34 = groupSu34:getController() - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD ) - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.BYPASS_AND_ESCAPE ) - Su34Status.status[groupName] = 8 - MessageToRed( string.format('%s: ',groupName) .. 'Take-Off. ', 10, 'RedStatus' .. groupName ) -end - -function Su34Hold(groupName) ---trace.menu("","Su34Hold") - local groupSu34 = Group.getByName( groupName ) - local controllerSu34 = groupSu34:getController() - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD ) - controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.BYPASS_AND_ESCAPE ) - Su34Status.status[groupName] = 5 - MessageToRed( string.format('%s: ',groupName) .. 'Holding Weapons. ', 10, 'RedStatus' .. groupName ) -end - -function Su34RTB(groupName) ---trace.menu("","Su34RTB") - Su34Status.status[groupName] = 6 - MessageToRed( string.format('%s: ',groupName) .. 'Return to Krasnodar. ', 10, 'RedStatus' .. groupName ) -end - -function Su34Destroyed(groupName) ---trace.menu("","Su34Destroyed") - Su34Status.status[groupName] = 7 - MessageToRed( string.format('%s: ',groupName) .. 'Destroyed. ', 30, 'RedStatus' .. groupName ) -end - -function GroupAlive( groupName ) ---trace.menu("","GroupAlive") - local groupTest = Group.getByName( groupName ) - - local groupExists = false - - if groupTest then - groupExists = groupTest:isExist() - end - - --trace.r( "", "", { groupExists } ) - return groupExists -end - -function Su34IsDead() ---trace.f() - -end - -function Su34OverviewStatus() ---trace.menu("","Su34OverviewStatus") - local msg = "" - local currentStatus = 0 - local Exists = false - - for groupName, currentStatus in pairs(Su34Status.status) do - - env.info(('Su34 Overview Status: GroupName = ' .. groupName )) - Alive = GroupAlive( groupName ) - - if Alive then - if currentStatus == 1 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Attacking carrier Carl Vinson. " - elseif currentStatus == 2 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Attacking supporting ships in the west. " - elseif currentStatus == 3 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Attacking invading ships in the north. " - elseif currentStatus == 4 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "In orbit and awaiting further instructions. " - elseif currentStatus == 5 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Holding Weapons. " - elseif currentStatus == 6 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Return to Krasnodar. " - elseif currentStatus == 7 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Destroyed. " - elseif currentStatus == 8 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Take-Off. " - end - else - if currentStatus == 7 then - msg = msg .. string.format("%s: ",groupName) - msg = msg .. "Destroyed. " - else - Su34Destroyed(groupName) - end - end - end - - boardMsgRed.statusMsg = msg -end - - -function UpdateBoardMsg() ---trace.f() - Su34OverviewStatus() - MessageToRed( boardMsgRed.statusMsg, 15, 'RedStatus' ) -end - -function MusicReset( flg ) ---trace.f() - trigger.action.setUserFlag(95,flg) -end - -function PlaneActivate(groupNameFormat, flg) ---trace.f() - local groupName = groupNameFormat .. string.format("#%03d", trigger.misc.getUserFlag(flg)) - --trigger.action.outText(groupName,10) - trigger.action.activateGroup(Group.getByName(groupName)) -end - -function Su34Menu(groupName) ---trace.f() - - --env.info(( 'Su34Menu(' .. groupName .. ')' )) - local groupSu34 = Group.getByName( groupName ) - - if Su34Status.status[groupName] == 1 or - Su34Status.status[groupName] == 2 or - Su34Status.status[groupName] == 3 or - Su34Status.status[groupName] == 4 or - Su34Status.status[groupName] == 5 then - if Su34MenuPath[groupName] == nil then - if planeMenuPath == nil then - planeMenuPath = missionCommands.addSubMenuForCoalition( - coalition.side.RED, - "SU-34 anti-ship flights", - nil - ) - end - Su34MenuPath[groupName] = missionCommands.addSubMenuForCoalition( - coalition.side.RED, - "Flight " .. groupName, - planeMenuPath - ) - - missionCommands.addCommandForCoalition( - coalition.side.RED, - "Attack carrier Carl Vinson", - Su34MenuPath[groupName], - Su34AttackCarlVinson, - groupName - ) - - missionCommands.addCommandForCoalition( - coalition.side.RED, - "Attack ships in the west", - Su34MenuPath[groupName], - Su34AttackWest, - groupName - ) - - missionCommands.addCommandForCoalition( - coalition.side.RED, - "Attack ships in the north", - Su34MenuPath[groupName], - Su34AttackNorth, - groupName - ) - - missionCommands.addCommandForCoalition( - coalition.side.RED, - "Hold position and await instructions", - Su34MenuPath[groupName], - Su34Orbit, - groupName - ) - - missionCommands.addCommandForCoalition( - coalition.side.RED, - "Report status", - Su34MenuPath[groupName], - Su34OverviewStatus - ) - end - else - if Su34MenuPath[groupName] then - missionCommands.removeItemForCoalition(coalition.side.RED, Su34MenuPath[groupName]) - end - end -end - ---- Obsolete function, but kept to rework in framework. - -function ChooseInfantry ( TeleportPrefixTable, TeleportMax ) ---trace.f("Spawn") - --env.info(( 'ChooseInfantry: ' )) - - TeleportPrefixTableCount = #TeleportPrefixTable - TeleportPrefixTableIndex = math.random( 1, TeleportPrefixTableCount ) - - --env.info(( 'ChooseInfantry: TeleportPrefixTableIndex = ' .. TeleportPrefixTableIndex .. ' TeleportPrefixTableCount = ' .. TeleportPrefixTableCount .. ' TeleportMax = ' .. TeleportMax )) - - local TeleportFound = false - local TeleportLoop = true - local Index = TeleportPrefixTableIndex - local TeleportPrefix = '' - - while TeleportLoop do - TeleportPrefix = TeleportPrefixTable[Index] - if SpawnSettings[TeleportPrefix] then - if SpawnSettings[TeleportPrefix]['SpawnCount'] - 1 < TeleportMax then - SpawnSettings[TeleportPrefix]['SpawnCount'] = SpawnSettings[TeleportPrefix]['SpawnCount'] + 1 - TeleportFound = true - else - TeleportFound = false - end - else - SpawnSettings[TeleportPrefix] = {} - SpawnSettings[TeleportPrefix]['SpawnCount'] = 0 - TeleportFound = true - end - if TeleportFound then - TeleportLoop = false - else - if Index < TeleportPrefixTableCount then - Index = Index + 1 - else - TeleportLoop = false - end - end - --env.info(( 'ChooseInfantry: Loop 1 - TeleportPrefix = ' .. TeleportPrefix .. ' Index = ' .. Index )) - end - - if TeleportFound == false then - TeleportLoop = true - Index = 1 - while TeleportLoop do - TeleportPrefix = TeleportPrefixTable[Index] - if SpawnSettings[TeleportPrefix] then - if SpawnSettings[TeleportPrefix]['SpawnCount'] - 1 < TeleportMax then - SpawnSettings[TeleportPrefix]['SpawnCount'] = SpawnSettings[TeleportPrefix]['SpawnCount'] + 1 - TeleportFound = true - else - TeleportFound = false - end - else - SpawnSettings[TeleportPrefix] = {} - SpawnSettings[TeleportPrefix]['SpawnCount'] = 0 - TeleportFound = true - end - if TeleportFound then - TeleportLoop = false - else - if Index < TeleportPrefixTableIndex then - Index = Index + 1 - else - TeleportLoop = false - end - end - --env.info(( 'ChooseInfantry: Loop 2 - TeleportPrefix = ' .. TeleportPrefix .. ' Index = ' .. Index )) - end - end - - local TeleportGroupName = '' - if TeleportFound == true then - TeleportGroupName = TeleportPrefix .. string.format("#%03d", SpawnSettings[TeleportPrefix]['SpawnCount'] ) - else - TeleportGroupName = '' - end - - --env.info(('ChooseInfantry: TeleportGroupName = ' .. TeleportGroupName )) - --env.info(('ChooseInfantry: return')) - - return TeleportGroupName -end - -SpawnedInfantry = 0 - -function LandCarrier ( CarrierGroup, LandingZonePrefix ) ---trace.f() - --env.info(( 'LandCarrier: ' )) - --env.info(( 'LandCarrier: CarrierGroup = ' .. CarrierGroup:getName() )) - --env.info(( 'LandCarrier: LandingZone = ' .. LandingZonePrefix )) - - local controllerGroup = CarrierGroup:getController() - - local LandingZone = trigger.misc.getZone(LandingZonePrefix) - local LandingZonePos = {} - LandingZonePos.x = LandingZone.point.x + math.random(LandingZone.radius * -1, LandingZone.radius) - LandingZonePos.y = LandingZone.point.z + math.random(LandingZone.radius * -1, LandingZone.radius) - - controllerGroup:pushTask( { id = 'Land', params = { point = LandingZonePos, durationFlag = true, duration = 10 } } ) - - --env.info(( 'LandCarrier: end' )) -end - -EscortCount = 0 -function EscortCarrier ( CarrierGroup, EscortPrefix, EscortLastWayPoint, EscortEngagementDistanceMax, EscortTargetTypes ) ---trace.f() - --env.info(( 'EscortCarrier: ' )) - --env.info(( 'EscortCarrier: CarrierGroup = ' .. CarrierGroup:getName() )) - --env.info(( 'EscortCarrier: EscortPrefix = ' .. EscortPrefix )) - - local CarrierName = CarrierGroup:getName() - - local EscortMission = {} - local CarrierMission = {} - - local EscortMission = SpawnMissionGroup( EscortPrefix ) - local CarrierMission = SpawnMissionGroup( CarrierGroup:getName() ) - - if EscortMission ~= nil and CarrierMission ~= nil then - - EscortCount = EscortCount + 1 - EscortMissionName = string.format( EscortPrefix .. '#Escort %s', CarrierName ) - EscortMission.name = EscortMissionName - EscortMission.groupId = nil - EscortMission.lateActivation = false - EscortMission.taskSelected = false - - local EscortUnits = #EscortMission.units - for u = 1, EscortUnits do - EscortMission.units[u].name = string.format( EscortPrefix .. '#Escort %s %02d', CarrierName, u ) - EscortMission.units[u].unitId = nil - end - - - EscortMission.route.points[1].task = { id = "ComboTask", - params = - { - tasks = - { - [1] = - { - enabled = true, - auto = false, - id = "Escort", - number = 1, - params = - { - lastWptIndexFlagChangedManually = false, - groupId = CarrierGroup:getID(), - lastWptIndex = nil, - lastWptIndexFlag = false, - engagementDistMax = EscortEngagementDistanceMax, - targetTypes = EscortTargetTypes, - pos = - { - y = 20, - x = 20, - z = 0, - } -- end of ["pos"] - } -- end of ["params"] - } -- end of [1] - } -- end of ["tasks"] - } -- end of ["params"] - } -- end of ["task"] - - SpawnGroupAdd( EscortPrefix, EscortMission ) - - end -end - -function SendMessageToCarrier( CarrierGroup, CarrierMessage ) ---trace.f() - - if CarrierGroup ~= nil then - MessageToGroup( CarrierGroup, CarrierMessage, 30, 'Carrier/' .. CarrierGroup:getName() ) - end - -end - -function MessageToGroup( MsgGroup, MsgText, MsgTime, MsgName ) ---trace.f() - - if type(MsgGroup) == 'string' then - --env.info( 'MessageToGroup: Converted MsgGroup string "' .. MsgGroup .. '" into a Group structure.' ) - MsgGroup = Group.getByName( MsgGroup ) - end - - if MsgGroup ~= nil then - local MsgTable = {} - MsgTable.text = MsgText - MsgTable.displayTime = MsgTime - MsgTable.msgFor = { units = { MsgGroup:getUnits()[1]:getName() } } - MsgTable.name = MsgName - --routines.message.add( MsgTable ) - --env.info(('MessageToGroup: Message sent to ' .. MsgGroup:getUnits()[1]:getName() .. ' -> ' .. MsgText )) - end -end - -function MessageToUnit( UnitName, MsgText, MsgTime, MsgName ) ---trace.f() - - if UnitName ~= nil then - local MsgTable = {} - MsgTable.text = MsgText - MsgTable.displayTime = MsgTime - MsgTable.msgFor = { units = { UnitName } } - MsgTable.name = MsgName - --routines.message.add( MsgTable ) - end -end - -function MessageToAll( MsgText, MsgTime, MsgName ) ---trace.f() - - MESSAGE:New( MsgText, MsgTime, "Message" ):ToCoalition( coalition.side.RED ):ToCoalition( coalition.side.BLUE ) -end - -function MessageToRed( MsgText, MsgTime, MsgName ) ---trace.f() - - MESSAGE:New( MsgText, MsgTime, "To Red Coalition" ):ToCoalition( coalition.side.RED ) -end - -function MessageToBlue( MsgText, MsgTime, MsgName ) ---trace.f() - - MESSAGE:New( MsgText, MsgTime, "To Blue Coalition" ):ToCoalition( coalition.side.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 derived utilities taken from the MIST framework, --- which are excellent tools to be reused in an OO environment!. --- --- ### Authors: --- --- * Grimes : Design & Programming of the MIST framework. --- --- ### Contributions: --- --- * FlightControl : Rework to OO framework --- --- @module Utils - - ---- @type SMOKECOLOR --- @field Green --- @field Red --- @field White --- @field Orange --- @field Blue - -SMOKECOLOR = trigger.smokeColor -- #SMOKECOLOR - ---- @type FLARECOLOR --- @field Green --- @field Red --- @field White --- @field Yellow - -FLARECOLOR = trigger.flareColor -- #FLARECOLOR - ---- Utilities static class. --- @type UTILS -UTILS = {} - - ---from http://lua-users.org/wiki/CopyTable -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 -UTILS.OneLineSerialize = function( tbl ) -- serialization of a table all on a single line, no comments, made to replace old get_table_string function - - lookup_table = {} - - local function _Serialize( tbl ) - - if type(tbl) == 'table' then --function only works for tables! - - if lookup_table[tbl] then - return lookup_table[object] - end - - local tbl_str = {} - - lookup_table[tbl] = tbl_str - - tbl_str[#tbl_str + 1] = '{' - - for ind,val in pairs(tbl) do -- serialize its fields - local ind_str = {} - if type(ind) == "number" then - ind_str[#ind_str + 1] = '[' - ind_str[#ind_str + 1] = tostring(ind) - ind_str[#ind_str + 1] = ']=' - else --must be a string - ind_str[#ind_str + 1] = '[' - ind_str[#ind_str + 1] = routines.utils.basicSerialize(ind) - ind_str[#ind_str + 1] = ']=' - end - - local val_str = {} - if ((type(val) == 'number') or (type(val) == 'boolean')) then - val_str[#val_str + 1] = tostring(val) - val_str[#val_str + 1] = ',' - tbl_str[#tbl_str + 1] = table.concat(ind_str) - tbl_str[#tbl_str + 1] = table.concat(val_str) - elseif type(val) == 'string' then - val_str[#val_str + 1] = routines.utils.basicSerialize(val) - val_str[#val_str + 1] = ',' - tbl_str[#tbl_str + 1] = table.concat(ind_str) - tbl_str[#tbl_str + 1] = table.concat(val_str) - elseif type(val) == 'nil' then -- won't ever happen, right? - val_str[#val_str + 1] = 'nil,' - tbl_str[#tbl_str + 1] = table.concat(ind_str) - tbl_str[#tbl_str + 1] = table.concat(val_str) - elseif type(val) == 'table' then - if ind == "__index" then - -- tbl_str[#tbl_str + 1] = "__index" - -- tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it - else - - val_str[#val_str + 1] = _Serialize(val) - val_str[#val_str + 1] = ',' --I think this is right, I just added it - tbl_str[#tbl_str + 1] = table.concat(ind_str) - tbl_str[#tbl_str + 1] = table.concat(val_str) - end - elseif type(val) == 'function' then - tbl_str[#tbl_str + 1] = "f() " .. tostring(ind) - tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it - else - env.info('unable to serialize value type ' .. routines.utils.basicSerialize(type(val)) .. ' at index ' .. tostring(ind)) - env.info( debug.traceback() ) - end - - end - tbl_str[#tbl_str + 1] = '}' - return table.concat(tbl_str) - else - return tostring(tbl) - end - end - - local objectreturn = _Serialize(tbl) - return objectreturn -end - ---porting in Slmod's "safestring" basic serialize -UTILS.BasicSerialize = function(s) - if s == nil then - return "\"\"" - else - if ((type(s) == 'number') or (type(s) == 'boolean') or (type(s) == 'function') or (type(s) == 'table') or (type(s) == 'userdata') ) then - return tostring(s) - elseif type(s) == 'string' then - s = string.format('%q', s) - return s - end - end -end - - -UTILS.ToDegree = function(angle) - return angle*180/math.pi -end - -UTILS.ToRadian = function(angle) - return angle*math.pi/180 -end - -UTILS.MetersToNM = function(meters) - return meters/1852 -end - -UTILS.MetersToFeet = function(meters) - return meters/0.3048 -end - -UTILS.NMToMeters = function(NM) - return NM*1852 -end - -UTILS.FeetToMeters = function(feet) - return feet*0.3048 -end - -UTILS.MpsToKnots = function(mps) - return mps*3600/1852 -end - -UTILS.MpsToKmph = function(mps) - return mps*3.6 -end - -UTILS.KnotsToMps = function(knots) - return knots*1852/3600 -end - -UTILS.KmphToMps = function(kmph) - return kmph/3.6 -end - ---[[acc: -in DM: decimal point of minutes. -In DMS: decimal point of seconds. -position after the decimal of the least significant digit: -So: -42.32 - acc of 2. -]] -UTILS.tostringLL = function( lat, lon, acc, DMS) - - local latHemi, lonHemi - if lat > 0 then - latHemi = 'N' - else - latHemi = 'S' - end - - if lon > 0 then - lonHemi = 'E' - else - lonHemi = 'W' - end - - lat = math.abs(lat) - lon = math.abs(lon) - - local latDeg = math.floor(lat) - local latMin = (lat - latDeg)*60 - - local lonDeg = math.floor(lon) - local lonMin = (lon - lonDeg)*60 - - if DMS then -- degrees, minutes, and seconds. - local oldLatMin = latMin - latMin = math.floor(latMin) - local latSec = UTILS.Round((oldLatMin - latMin)*60, acc) - - local oldLonMin = lonMin - lonMin = math.floor(lonMin) - local lonSec = UTILS.Round((oldLonMin - lonMin)*60, acc) - - if latSec == 60 then - latSec = 0 - latMin = latMin + 1 - end - - if lonSec == 60 then - lonSec = 0 - lonMin = lonMin + 1 - end - - local secFrmtStr -- create the formatting string for the seconds place - 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 = UTILS.Round(latMin, acc) - lonMin = UTILS.Round(lonMin, acc) - - if latMin == 60 then - latMin = 0 - latDeg = latDeg + 1 - end - - if lonMin == 60 then - lonMin = 0 - lonDeg = lonDeg + 1 - end - - local minFrmtStr -- create the formatting string for the minutes place - if acc <= 0 then -- no decimal place. - minFrmtStr = '%02d' - else - local width = 3 + acc -- 01.310 - that's a width of 6, for example. - minFrmtStr = '%0' .. width .. '.' .. acc .. 'f' - end - - return string.format('%02d', latDeg) .. ' ' .. string.format(minFrmtStr, latMin) .. '\'' .. latHemi .. ' ' - .. string.format('%02d', lonDeg) .. ' ' .. string.format(minFrmtStr, lonMin) .. '\'' .. lonHemi - - end -end - - ---- From http://lua-users.org/wiki/SimpleRound --- use negative idp for rounding ahead of decimal place, positive for rounding after decimal place -function UTILS.Round( num, idp ) - local mult = 10 ^ ( idp or 0 ) - return math.floor( num * mult + 0.5 ) / mult -end - --- porting in Slmod's dostring -function UTILS.DoString( s ) - local f, err = loadstring( s ) - if f then - return true, f() - else - return false, err - end -end ---- 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.2.3) Trace activation. --- --- Tracing can be activated in several ways: --- --- * Switch tracing on or off through the @{#BASE.TraceOnOff}() method. --- * Activate all tracing through the @{#BASE.TraceAll}() method. --- * Activate only the tracing of a certain class (name) through the @{#BASE.TraceClass}() method. --- * Activate only the tracing of a certain method of a certain class through the @{#BASE.TraceClassMethod}() method. --- * Activate only the tracing of a certain level through the @{#BASE.TraceLevel}() method. --- ### 1.2.4) Check if tracing is on. --- --- The method @{#BASE.IsTrace}() will validate if tracing is activated or not. --- --- ## 1.3 DCS simulator Event Handling --- --- The BASE class provides methods to catch DCS Events. These are events that are triggered from within the DCS simulator, --- and handled through lua scripting. MOOSE provides an encapsulation to handle these events more efficiently. --- --- ### 1.3.1 Subscribe / Unsubscribe to DCS Events --- --- At first, the mission designer will need to **Subscribe** to a specific DCS event for the class. --- So, when the DCS event occurs, the class will be notified of that event. --- There are two functions which you use to subscribe to or unsubscribe from an event. --- --- * @{#BASE.HandleEvent}(): Subscribe to a DCS Event. --- * @{#BASE.UnHandleEvent}(): Unsubscribe from a DCS Event. --- --- ### 1.3.2 Event Handling of DCS Events --- --- Once the class is subscribed to the event, an **Event Handling** method on the object or class needs to be written that will be called --- when the DCS event occurs. The Event Handling method receives an @{Event#EVENTDATA} structure, which contains a lot of information --- about the event that occurred. --- --- Find below an example of the prototype how to write an event handling function for two units: --- --- local Tank1 = UNIT:FindByName( "Tank A" ) --- local Tank2 = UNIT:FindByName( "Tank B" ) --- --- -- Here we subscribe to the Dead events. So, if one of these tanks dies, the Tank1 or Tank2 objects will be notified. --- Tank1:HandleEvent( EVENTS.Dead ) --- Tank2:HandleEvent( EVENTS.Dead ) --- --- --- This function is an Event Handling function that will be called when Tank1 is Dead. --- -- @param Wrapper.Unit#UNIT self --- -- @param Core.Event#EVENTDATA EventData --- function Tank1:OnEventDead( EventData ) --- --- self:SmokeGreen() --- end --- --- --- This function is an Event Handling function that will be called when Tank2 is Dead. --- -- @param Wrapper.Unit#UNIT self --- -- @param Core.Event#EVENTDATA EventData --- function Tank2:OnEventDead( EventData ) --- --- self:SmokeBlue() --- end --- --- --- --- See the @{Event} module for more information about event handling. --- --- ## 1.4) Class identification methods --- --- BASE provides methods to get more information of each object: --- --- * @{#BASE.GetClassID}(): Gets the ID (number) of the object. Each object created is assigned a number, that is incremented by one. --- * @{#BASE.GetClassName}(): Gets the name of the object, which is the name of the class the object was instantiated from. --- * @{#BASE.GetClassNameAndID}(): Gets the name and ID of the object. --- --- ## 1.5) All objects derived from BASE can have "States" --- --- A mechanism is in place in MOOSE, that allows to let the objects administer **states**. --- States are essentially properties of objects, which are identified by a **Key** and a **Value**. --- The method @{#BASE.SetState}() can be used to set a Value with a reference Key to the object. --- To **read or retrieve** a state Value based on a Key, use the @{#BASE.GetState} method. --- These two methods provide a very handy way to keep state at long lasting processes. --- Values can be stored within the objects, and later retrieved or changed when needed. --- There is one other important thing to note, the @{#BASE.SetState}() and @{#BASE.GetState} methods --- receive as the **first parameter the object for which the state needs to be set**. --- Thus, if the state is to be set for the same object as the object for which the method is used, then provide the same --- object name to the method. --- --- ## 1.10) BASE Inheritance (tree) support --- --- The following methods are available to support inheritance: --- --- * @{#BASE.Inherit}: Inherits from a class. --- * @{#BASE.GetParent}: Returns the parent object from the object it is handling, or nil if there is no parent object. --- --- ==== --- --- # **API CHANGE HISTORY** --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- YYYY-MM-DD: CLASS:**NewFunction**( Params ) replaces CLASS:_OldFunction_( Params ) --- YYYY-MM-DD: CLASS:**NewFunction( Params )** added --- --- Hereby the change log: --- --- === --- --- # **AUTHORS and CONTRIBUTIONS** --- --- ### Contributions: --- --- * None. --- --- ### Authors: --- --- * **FlightControl**: Design & Programming --- --- @module Base - - - -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, - _Private = {}, - Events = {}, - States = {} -} - ---- The Formation Class --- @type FORMATION --- @field Cone A cone formation. -FORMATION = { - Cone = "Cone" -} - - - --- @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 - - - return self -end - -function BASE:_Destructor() - --self:E("_Destructor") - - --self:EventRemoveAll() -end - -function BASE:_SetDestructor() - - -- TODO: Okay, this is really technical... - -- When you set a proxy to a table to catch __gc, weak tables don't behave like weak... - -- Therefore, I am parking this logic until I've properly discussed all this with the community. - --[[ - local proxy = newproxy(true) - local proxyMeta = getmetatable(proxy) - - proxyMeta.__gc = function () - env.info("In __gc for " .. self:GetClassNameAndID() ) - if self._Destructor then - self:_Destructor() - end - end - - -- keep the userdata from newproxy reachable until the object - -- table is about to be garbage-collected - then the __gc hook - -- will be invoked and the destructor called - rawset( self, '__proxy', proxy ) - --]] -end - ---- 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 - - Child:_SetDestructor() - end - --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:GetParent( 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 string.format( '%s#%09d', self.ClassName, self.ClassID ) -end - ---- Get the ClassName of the class instance. --- @param #BASE self --- @return #string The ClassName of the class instance. -function BASE:GetClassName() - return self.ClassName -end - ---- Get the ClassID of the class instance. --- @param #BASE self --- @return #string The ClassID of the class instance. -function BASE:GetClassID() - return self.ClassID -end - -do -- Event Handling - - --- Returns the event dispatcher - -- @param #BASE self - -- @return Core.Event#EVENT - function BASE:EventDispatcher() - - return _EVENTDISPATCHER - end - - - --- Get the Class @{Event} processing Priority. - -- The Event processing Priority is a number from 1 to 10, - -- reflecting the order of the classes subscribed to the Event to be processed. - -- @param #BASE self - -- @return #number The @{Event} processing Priority. - function BASE:GetEventPriority() - return self._Private.EventPriority or 5 - end - - --- Set the Class @{Event} processing Priority. - -- The Event processing Priority is a number from 1 to 10, - -- reflecting the order of the classes subscribed to the Event to be processed. - -- @param #BASE self - -- @param #number EventPriority The @{Event} processing Priority. - -- @return self - function BASE:SetEventPriority( EventPriority ) - self._Private.EventPriority = EventPriority - end - - --- Remove all subscribed events - -- @param #BASE self - -- @return #BASE - function BASE:EventRemoveAll() - - self:EventDispatcher():RemoveAll( self ) - - return self - end - - --- Subscribe to a DCS Event. - -- @param #BASE self - -- @param Core.Event#EVENTS Event - -- @param #function EventFunction (optional) The function to be called when the event occurs for the unit. - -- @return #BASE - function BASE:HandleEvent( Event, EventFunction ) - - self:EventDispatcher():OnEventGeneric( EventFunction, self, Event ) - - return self - end - - --- UnSubscribe to a DCS event. - -- @param #BASE self - -- @param Core.Event#EVENTS Event - -- @return #BASE - function BASE:UnHandleEvent( Event ) - - self:EventDispatcher():Remove( self, Event ) - - return self - end - - -- Event handling function prototypes - - --- Occurs whenever any unit in a mission fires a weapon. But not any machine gun or autocannon based weapon, those are handled by EVENT.ShootingStart. - -- @function [parent=#BASE] OnEventShot - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs whenever an object is hit by a weapon. - -- initiator : The unit object the fired the weapon - -- weapon: Weapon object that hit the target - -- target: The Object that was hit. - -- @function [parent=#BASE] OnEventHit - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when an aircraft takes off from an airbase, farp, or ship. - -- initiator : The unit that tookoff - -- place: Object from where the AI took-off from. Can be an Airbase Object, FARP, or Ships - -- @function [parent=#BASE] OnEventTakeoff - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when an aircraft lands at an airbase, farp or ship - -- initiator : The unit that has landed - -- place: Object that the unit landed on. Can be an Airbase Object, FARP, or Ships - -- @function [parent=#BASE] OnEventLand - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any aircraft crashes into the ground and is completely destroyed. - -- initiator : The unit that has crashed - -- @function [parent=#BASE] OnEventCrash - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when a pilot ejects from an aircraft - -- initiator : The unit that has ejected - -- @function [parent=#BASE] OnEventEjection - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when an aircraft connects with a tanker and begins taking on fuel. - -- initiator : The unit that is receiving fuel. - -- @function [parent=#BASE] OnEventRefueling - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when an object is completely destroyed. - -- initiator : The unit that is was destroyed. - -- @function [parent=#BASE] OnEvent - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when the pilot of an aircraft is killed. Can occur either if the player is alive and crashes or if a weapon kills the pilot without completely destroying the plane. - -- initiator : The unit that the pilot has died in. - -- @function [parent=#BASE] OnEventPilotDead - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when a ground unit captures either an airbase or a farp. - -- initiator : The unit that captured the base - -- place: The airbase that was captured, can be a FARP or Airbase. When calling place:getCoalition() the faction will already be the new owning faction. - -- @function [parent=#BASE] OnEventBaseCaptured - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when a mission starts - -- @function [parent=#BASE] OnEventMissionStart - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when a mission ends - -- @function [parent=#BASE] OnEventMissionEnd - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when an aircraft is finished taking fuel. - -- initiator : The unit that was receiving fuel. - -- @function [parent=#BASE] OnEventRefuelingStop - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any object is spawned into the mission. - -- initiator : The unit that was spawned - -- @function [parent=#BASE] OnEventBirth - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any system fails on a human controlled aircraft. - -- initiator : The unit that had the failure - -- @function [parent=#BASE] OnEventHumanFailure - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any aircraft starts its engines. - -- initiator : The unit that is starting its engines. - -- @function [parent=#BASE] OnEventEngineStartup - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any aircraft shuts down its engines. - -- initiator : The unit that is stopping its engines. - -- @function [parent=#BASE] OnEventEngineShutdown - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any player assumes direct control of a unit. - -- initiator : The unit that is being taken control of. - -- @function [parent=#BASE] OnEventPlayerEnterUnit - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any player relieves control of a unit to the AI. - -- initiator : The unit that the player left. - -- @function [parent=#BASE] OnEventPlayerLeaveUnit - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any unit begins firing a weapon that has a high rate of fire. Most common with aircraft cannons (GAU-8), autocannons, and machine guns. - -- initiator : The unit that is doing the shooing. - -- target: The unit that is being targeted. - -- @function [parent=#BASE] OnEventShootingStart - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any unit stops firing its weapon. Event will always correspond with a shooting start event. - -- initiator : The unit that was doing the shooing. - -- @function [parent=#BASE] OnEventShootingEnd - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - -end - - ---- Creation of a Birth Event. --- @param #BASE self --- @param Dcs.DCSTypes#Time EventTime The time stamp of the event. --- @param Dcs.DCSWrapper.Object#Object Initiator The initiating object of the event. --- @param #string IniUnitName The initiating unit name. --- @param place --- @param subplace -function BASE:CreateEventBirth( EventTime, Initiator, IniUnitName, place, subplace ) - self:F( { EventTime, Initiator, IniUnitName, place, subplace } ) - - local Event = { - id = world.event.S_EVENT_BIRTH, - time = EventTime, - initiator = Initiator, - IniUnitName = IniUnitName, - place = place, - subplace = subplace - } - - world.onEvent( Event ) -end - ---- Creation of a Crash Event. --- @param #BASE self --- @param Dcs.DCSTypes#Time EventTime The time stamp of the event. --- @param Dcs.DCSWrapper.Object#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 Dcs.DCSTypes#Event structure. ---- The main event handling function... This function captures all events generated for the class. --- @param #BASE self --- @param Dcs.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 - ---- Set a state or property of the Object given a Key and a Value. --- Note that if the Object is destroyed, nillified or garbage collected, then the Values and Keys will also be gone. --- @param #BASE self --- @param Object The object that will hold the Value set by the Key. --- @param Key The key that is used as a reference of the value. Note that the key can be a #string, but it can also be any other type! --- @param Value The value to is stored in the object. --- @return The Value set. --- @return #nil The Key was not found and thus the Value could not be retrieved. -function BASE:SetState( Object, Key, Value ) - - local ClassNameAndID = Object:GetClassNameAndID() - - self.States[ClassNameAndID] = self.States[ClassNameAndID] or {} - self.States[ClassNameAndID][Key] = Value - self:T2( { ClassNameAndID, Key, Value } ) - - return self.States[ClassNameAndID][Key] -end - - ---- Get a Value given a Key from the Object. --- Note that if the Object is destroyed, nillified or garbage collected, then the Values and Keys will also be gone. --- @param #BASE self --- @param Object The object that holds the Value set by the Key. --- @param Key The key that is used to retrieve the value. Note that the key can be a #string, but it can also be any other type! --- @param Value The value to is stored in the Object. --- @return The Value retrieved. -function BASE:GetState( Object, Key ) - - local ClassNameAndID = Object:GetClassNameAndID() - - if self.States[ClassNameAndID] then - local Value = self.States[ClassNameAndID][Key] or false - self:T2( { ClassNameAndID, Key, Value } ) - return Value - 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:TraceOnOff( true ) --- --- -- Switch the tracing Off --- BASE:TraceOnOff( false ) -function BASE:TraceOnOff( TraceOnOff ) - _TraceOnOff = TraceOnOff -end - - ---- Enquires if tracing is on (for the class). --- @param #BASE self --- @return #boolean -function BASE:IsTrace() - - if debug and ( _TraceAll == true ) or ( _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName] ) then - return true - else - return false - end -end - ---- Set trace level --- @param #BASE self --- @param #number Level -function BASE:TraceLevel( Level ) - _TraceLevel = Level - 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 debug and ( _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 debug and _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 debug and _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 debug and _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 debug and ( _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 debug and _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 debug and _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 debug and _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 ) - - if debug then - 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 - -end - - - ---- This module contains the SCHEDULER class. --- --- # 1) @{Scheduler#SCHEDULER} class, extends @{Base#BASE} --- --- The @{Scheduler#SCHEDULER} class creates schedule. --- --- ## 1.1) SCHEDULER constructor --- --- The SCHEDULER class is quite easy to use, but note that the New constructor has variable parameters: --- --- * @{Scheduler#SCHEDULER.New}( nil ): Setup a new SCHEDULER object, which is persistently executed after garbage collection. --- * @{Scheduler#SCHEDULER.New}( Object ): Setup a new SCHEDULER object, which is linked to the Object. When the Object is nillified or destroyed, the SCHEDULER object will also be destroyed and stopped after garbage collection. --- * @{Scheduler#SCHEDULER.New}( nil, Function, FunctionArguments, Start, ... ): Setup a new persistent SCHEDULER object, and start a new schedule for the Function with the defined FunctionArguments according the Start and sequent parameters. --- * @{Scheduler#SCHEDULER.New}( Object, Function, FunctionArguments, Start, ... ): Setup a new SCHEDULER object, linked to Object, and start a new schedule for the Function with the defined FunctionArguments according the Start and sequent parameters. --- --- ## 1.2) SCHEDULER timer stopping and (re-)starting. --- --- The SCHEDULER can be stopped and restarted with the following methods: --- --- * @{Scheduler#SCHEDULER.Start}(): (Re-)Start the schedules within the SCHEDULER object. If a CallID is provided to :Start(), only the schedule referenced by CallID will be (re-)started. --- * @{Scheduler#SCHEDULER.Stop}(): Stop the schedules within the SCHEDULER object. If a CallID is provided to :Stop(), then only the schedule referenced by CallID will be stopped. --- --- ## 1.3) Create a new schedule --- --- With @{Scheduler#SCHEDULER.Schedule}() a new time event can be scheduled. This function is used by the :New() constructor when a new schedule is planned. --- --- === --- --- ### Contributions: --- --- * FlightControl : Concept & Testing --- --- ### Authors: --- --- * FlightControl : Design & Programming --- --- ### Test Missions: --- --- * SCH - Scheduler --- --- === --- --- @module Scheduler - - ---- The SCHEDULER class --- @type SCHEDULER --- @field #number ScheduleID the ID of the scheduler. --- @extends Core.Base#BASE -SCHEDULER = { - ClassName = "SCHEDULER", - Schedules = {}, -} - ---- SCHEDULER constructor. --- @param #SCHEDULER self --- @param #table SchedulerObject Specified for which Moose object the timer is setup. If a value of nil is provided, a scheduler will be setup without an object reference. --- @param #function SchedulerFunction The event function to be called when a timer event occurs. The event function needs to accept the parameters specified in SchedulerArguments. --- @param #table SchedulerArguments Optional arguments that can be given as part of scheduler. The arguments need to be given as a table { param1, param 2, ... }. --- @param #number Start Specifies the amount of seconds that will be waited before the scheduling is started, and the event function is called. --- @param #number Repeat Specifies the interval in seconds when the scheduler will call the event function. --- @param #number RandomizeFactor Specifies a randomization factor between 0 and 1 to randomize the Repeat. --- @param #number Stop Specifies the amount of seconds when the scheduler will be stopped. --- @return #SCHEDULER self. --- @return #number The ScheduleID of the planned schedule. -function SCHEDULER:New( SchedulerObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop ) - local self = BASE:Inherit( self, BASE:New() ) - self:F2( { Start, Repeat, RandomizeFactor, Stop } ) - - local ScheduleID = nil - - self.MasterObject = SchedulerObject - - if SchedulerFunction then - ScheduleID = self:Schedule( SchedulerObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop ) - end - - return self, ScheduleID -end - ---function SCHEDULER:_Destructor() --- --self:E("_Destructor") --- --- _SCHEDULEDISPATCHER:RemoveSchedule( self.CallID ) ---end - ---- Schedule a new time event. Note that the schedule will only take place if the scheduler is *started*. Even for a single schedule event, the scheduler needs to be started also. --- @param #SCHEDULER self --- @param #table SchedulerObject Specified for which Moose object the timer is setup. If a value of nil is provided, a scheduler will be setup without an object reference. --- @param #function SchedulerFunction The event function to be called when a timer event occurs. The event function needs to accept the parameters specified in SchedulerArguments. --- @param #table SchedulerArguments Optional arguments that can be given as part of scheduler. The arguments need to be given as a table { param1, param 2, ... }. --- @param #number Start Specifies the amount of seconds that will be waited before the scheduling is started, and the event function is called. --- @param #number Repeat Specifies the interval in seconds when the scheduler will call the event function. --- @param #number RandomizeFactor Specifies a randomization factor between 0 and 1 to randomize the Repeat. --- @param #number Stop Specifies the amount of seconds when the scheduler will be stopped. --- @return #number The ScheduleID of the planned schedule. -function SCHEDULER:Schedule( SchedulerObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop ) - self:F2( { Start, Repeat, RandomizeFactor, Stop } ) - self:T3( { SchedulerArguments } ) - - local ObjectName = "-" - if SchedulerObject and SchedulerObject.ClassName and SchedulerObject.ClassID then - ObjectName = SchedulerObject.ClassName .. SchedulerObject.ClassID - end - self:F3( { "Schedule :", ObjectName, tostring( SchedulerObject ), Start, Repeat, RandomizeFactor, Stop } ) - self.SchedulerObject = SchedulerObject - - local ScheduleID = _SCHEDULEDISPATCHER:AddSchedule( - self, - SchedulerFunction, - SchedulerArguments, - Start, - Repeat, - RandomizeFactor, - Stop - ) - - self.Schedules[#self.Schedules+1] = ScheduleID - - return ScheduleID -end - ---- (Re-)Starts the schedules or a specific schedule if a valid ScheduleID is provided. --- @param #SCHEDULER self --- @param #number ScheduleID (optional) The ScheduleID of the planned (repeating) schedule. -function SCHEDULER:Start( ScheduleID ) - self:F3( { ScheduleID } ) - - _SCHEDULEDISPATCHER:Start( self, ScheduleID ) -end - ---- Stops the schedules or a specific schedule if a valid ScheduleID is provided. --- @param #SCHEDULER self --- @param #number ScheduleID (optional) The ScheduleID of the planned (repeating) schedule. -function SCHEDULER:Stop( ScheduleID ) - self:F3( { ScheduleID } ) - - _SCHEDULEDISPATCHER:Stop( self, ScheduleID ) -end - ---- Removes a specific schedule if a valid ScheduleID is provided. --- @param #SCHEDULER self --- @param #number ScheduleID (optional) The ScheduleID of the planned (repeating) schedule. -function SCHEDULER:Remove( ScheduleID ) - self:F3( { ScheduleID } ) - - _SCHEDULEDISPATCHER:Remove( self, ScheduleID ) -end - - - - - - - - - - - - - - - ---- This module defines the SCHEDULEDISPATCHER class, which is used by a central object called _SCHEDULEDISPATCHER. --- --- === --- --- Takes care of the creation and dispatching of scheduled functions for SCHEDULER objects. --- --- This class is tricky and needs some thorought explanation. --- SCHEDULE classes are used to schedule functions for objects, or as persistent objects. --- The SCHEDULEDISPATCHER class ensures that: --- --- - Scheduled functions are planned according the SCHEDULER object parameters. --- - Scheduled functions are repeated when requested, according the SCHEDULER object parameters. --- - Scheduled functions are automatically removed when the schedule is finished, according the SCHEDULER object parameters. --- --- The SCHEDULEDISPATCHER class will manage SCHEDULER object in memory during garbage collection: --- - When a SCHEDULER object is not attached to another object (that is, it's first :Schedule() parameter is nil), then the SCHEDULER --- object is _persistent_ within memory. --- - When a SCHEDULER object *is* attached to another object, then the SCHEDULER object is _not persistent_ within memory after a garbage collection! --- The none persistency of SCHEDULERS attached to objects is required to allow SCHEDULER objects to be garbage collectged, when the parent object is also desroyed or nillified and garbage collected. --- Even when there are pending timer scheduled functions to be executed for the SCHEDULER object, --- these will not be executed anymore when the SCHEDULER object has been destroyed. --- --- The SCHEDULEDISPATCHER allows multiple scheduled functions to be planned and executed for one SCHEDULER object. --- The SCHEDULER object therefore keeps a table of "CallID's", which are returned after each planning of a new scheduled function by the SCHEDULEDISPATCHER. --- The SCHEDULER object plans new scheduled functions through the @{Scheduler#SCHEDULER.Schedule}() method. --- The Schedule() method returns the CallID that is the reference ID for each planned schedule. --- --- === --- --- === --- --- ### Contributions: - --- ### Authors: FlightControl : Design & Programming --- --- @module ScheduleDispatcher - ---- The SCHEDULEDISPATCHER structure --- @type SCHEDULEDISPATCHER -SCHEDULEDISPATCHER = { - ClassName = "SCHEDULEDISPATCHER", - CallID = 0, -} - -function SCHEDULEDISPATCHER:New() - local self = BASE:Inherit( self, BASE:New() ) - self:F3() - return self -end - ---- Add a Schedule to the ScheduleDispatcher. --- The development of this method was really tidy. --- It is constructed as such that a garbage collection is executed on the weak tables, when the Scheduler is nillified. --- Nothing of this code should be modified without testing it thoroughly. --- @param #SCHEDULEDISPATCHER self --- @param Core.Scheduler#SCHEDULER Scheduler -function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleArguments, Start, Repeat, Randomize, Stop ) - self:F2( { Scheduler, ScheduleFunction, ScheduleArguments, Start, Repeat, Randomize, Stop } ) - - self.CallID = self.CallID + 1 - - -- Initialize the ObjectSchedulers array, which is a weakly coupled table. - -- If the object used as the key is nil, then the garbage collector will remove the item from the Functions array. - self.PersistentSchedulers = self.PersistentSchedulers or {} - - -- Initialize the ObjectSchedulers array, which is a weakly coupled table. - -- If the object used as the key is nil, then the garbage collector will remove the item from the Functions array. - self.ObjectSchedulers = self.ObjectSchedulers or {} -- setmetatable( {}, { __mode = "v" } ) - - if Scheduler.MasterObject then - self.ObjectSchedulers[self.CallID] = Scheduler - self:F3( { CallID = self.CallID, ObjectScheduler = tostring(self.ObjectSchedulers[self.CallID]), MasterObject = tostring(Scheduler.MasterObject) } ) - else - self.PersistentSchedulers[self.CallID] = Scheduler - self:F3( { CallID = self.CallID, PersistentScheduler = self.PersistentSchedulers[self.CallID] } ) - end - - self.Schedule = self.Schedule or setmetatable( {}, { __mode = "k" } ) - self.Schedule[Scheduler] = self.Schedule[Scheduler] or {} - self.Schedule[Scheduler][self.CallID] = {} - self.Schedule[Scheduler][self.CallID].Function = ScheduleFunction - self.Schedule[Scheduler][self.CallID].Arguments = ScheduleArguments - self.Schedule[Scheduler][self.CallID].StartTime = timer.getTime() + ( Start or 0 ) - self.Schedule[Scheduler][self.CallID].Start = Start + .1 - self.Schedule[Scheduler][self.CallID].Repeat = Repeat - self.Schedule[Scheduler][self.CallID].Randomize = Randomize - self.Schedule[Scheduler][self.CallID].Stop = Stop - - self:T3( self.Schedule[Scheduler][self.CallID] ) - - self.Schedule[Scheduler][self.CallID].CallHandler = function( CallID ) - self:F2( CallID ) - - local ErrorHandler = function( errmsg ) - env.info( "Error in timer function: " .. errmsg ) - if debug ~= nil then - env.info( debug.traceback() ) - end - return errmsg - end - - local Scheduler = self.ObjectSchedulers[CallID] - if not Scheduler then - Scheduler = self.PersistentSchedulers[CallID] - end - - self:T3( { Scheduler = Scheduler } ) - - if Scheduler then - - local Schedule = self.Schedule[Scheduler][CallID] - - self:T3( { Schedule = Schedule } ) - - local ScheduleObject = Scheduler.SchedulerObject - --local ScheduleObjectName = Scheduler.SchedulerObject:GetNameAndClassID() - local ScheduleFunction = Schedule.Function - local ScheduleArguments = Schedule.Arguments - local Start = Schedule.Start - local Repeat = Schedule.Repeat or 0 - local Randomize = Schedule.Randomize or 0 - local Stop = Schedule.Stop or 0 - local ScheduleID = Schedule.ScheduleID - - local Status, Result - if ScheduleObject then - local function Timer() - return ScheduleFunction( ScheduleObject, unpack( ScheduleArguments ) ) - end - Status, Result = xpcall( Timer, ErrorHandler ) - else - local function Timer() - return ScheduleFunction( unpack( ScheduleArguments ) ) - end - Status, Result = xpcall( Timer, ErrorHandler ) - end - - local CurrentTime = timer.getTime() - local StartTime = CurrentTime + Start - - if Status and (( Result == nil ) or ( Result and Result ~= false ) ) then - if Repeat ~= 0 and ( Stop == 0 ) or ( Stop ~= 0 and CurrentTime <= StartTime + Stop ) then - local ScheduleTime = - CurrentTime + - Repeat + - math.random( - - ( Randomize * Repeat / 2 ), - ( Randomize * Repeat / 2 ) - ) + - 0.01 - self:T3( { Repeat = CallID, CurrentTime, ScheduleTime, ScheduleArguments } ) - return ScheduleTime -- returns the next time the function needs to be called. - else - self:Stop( Scheduler, CallID ) - end - else - self:Stop( Scheduler, CallID ) - end - else - self:E( "Scheduled obscolete call for CallID: " .. CallID ) - end - - return nil - end - - self:Start( Scheduler, self.CallID ) - - return self.CallID -end - -function SCHEDULEDISPATCHER:RemoveSchedule( Scheduler, CallID ) - self:F2( { Remove = CallID, Scheduler = Scheduler } ) - - if CallID then - self:Stop( Scheduler, CallID ) - self.Schedule[Scheduler][CallID] = nil - end -end - -function SCHEDULEDISPATCHER:Start( Scheduler, CallID ) - self:F2( { Start = CallID, Scheduler = Scheduler } ) - - if CallID then - local Schedule = self.Schedule[Scheduler] - Schedule[CallID].ScheduleID = timer.scheduleFunction( - Schedule[CallID].CallHandler, - CallID, - timer.getTime() + Schedule[CallID].Start - ) - else - for CallID, Schedule in pairs( self.Schedule[Scheduler] ) do - self:Start( Scheduler, CallID ) -- Recursive - end - end -end - -function SCHEDULEDISPATCHER:Stop( Scheduler, CallID ) - self:F2( { Stop = CallID, Scheduler = Scheduler } ) - - if CallID then - local Schedule = self.Schedule[Scheduler] - timer.removeFunction( Schedule[CallID].ScheduleID ) - else - for CallID, Schedule in pairs( self.Schedule[Scheduler] ) do - self:Stop( Scheduler, CallID ) -- Recursive - end - end -end - - - ---- This core module models the dispatching of DCS Events to subscribed MOOSE classes, --- following a given priority. --- --- ![Banner Image](..\Presentations\EVENT\Dia1.JPG) --- --- === --- --- # 1) Event Handling Overview --- --- ![Objects](..\Presentations\EVENT\Dia2.JPG) --- --- Within a running mission, various DCS events occur. Units are dynamically created, crash, die, shoot stuff, get hit etc. --- This module provides a mechanism to dispatch those events occuring within your running mission, to the different objects orchestrating your mission. --- --- ![Objects](..\Presentations\EVENT\Dia3.JPG) --- --- Objects can subscribe to different events. The Event dispatcher will publish the received DCS events to the subscribed MOOSE objects, in a specified order. --- In this way, the subscribed MOOSE objects are kept in sync with your evolving running mission. --- --- ## 1.1) Event Dispatching --- --- ![Objects](..\Presentations\EVENT\Dia4.JPG) --- --- The _EVENTDISPATCHER object is automatically created within MOOSE, --- and handles the dispatching of DCS Events occurring --- in the simulator to the subscribed objects --- in the correct processing order. --- --- ![Objects](..\Presentations\EVENT\Dia5.JPG) --- --- There are 5 levels of kind of objects that the _EVENTDISPATCHER services: --- --- * _DATABASE object: The core of the MOOSE objects. Any object that is created, deleted or updated, is done in this database. --- * SET_ derived classes: Subsets of the _DATABASE object. These subsets are updated by the _EVENTDISPATCHER as the second priority. --- * UNIT objects: UNIT objects can subscribe to DCS events. Each DCS event will be directly published to teh subscribed UNIT object. --- * GROUP objects: GROUP objects can subscribe to DCS events. Each DCS event will be directly published to the subscribed GROUP object. --- * Any other object: Various other objects can subscribe to DCS events. Each DCS event triggered will be published to each subscribed object. --- --- ![Objects](..\Presentations\EVENT\Dia6.JPG) --- --- For most DCS events, the above order of updating will be followed. --- --- ![Objects](..\Presentations\EVENT\Dia7.JPG) --- --- But for some DCS events, the publishing order is reversed. This is due to the fact that objects need to be **erased** instead of added. --- --- ## 1.2) Event Handling --- --- ![Objects](..\Presentations\EVENT\Dia8.JPG) --- --- The actual event subscribing and handling is not facilitated through the _EVENTDISPATCHER, but it is done through the @{BASE} class, @{UNIT} class and @{GROUP} class. --- The _EVENTDISPATCHER is a component that is quietly working in the background of MOOSE. --- --- ![Objects](..\Presentations\EVENT\Dia9.JPG) --- --- The BASE class provides methods to catch DCS Events. These are events that are triggered from within the DCS simulator, --- and handled through lua scripting. MOOSE provides an encapsulation to handle these events more efficiently. --- --- ### 1.2.1 Subscribe / Unsubscribe to DCS Events --- --- At first, the mission designer will need to **Subscribe** to a specific DCS event for the class. --- So, when the DCS event occurs, the class will be notified of that event. --- There are two functions which you use to subscribe to or unsubscribe from an event. --- --- * @{Base#BASE.HandleEvent}(): Subscribe to a DCS Event. --- * @{Base#BASE.UnHandleEvent}(): Unsubscribe from a DCS Event. --- --- ### 1.3.2 Event Handling of DCS Events --- --- Once the class is subscribed to the event, an **Event Handling** method on the object or class needs to be written that will be called --- when the DCS event occurs. The Event Handling method receives an @{Event#EVENTDATA} structure, which contains a lot of information --- about the event that occurred. --- --- Find below an example of the prototype how to write an event handling function for two units: --- --- local Tank1 = UNIT:FindByName( "Tank A" ) --- local Tank2 = UNIT:FindByName( "Tank B" ) --- --- -- Here we subscribe to the Dead events. So, if one of these tanks dies, the Tank1 or Tank2 objects will be notified. --- Tank1:HandleEvent( EVENTS.Dead ) --- Tank2:HandleEvent( EVENTS.Dead ) --- --- --- This function is an Event Handling function that will be called when Tank1 is Dead. --- -- @param Wrapper.Unit#UNIT self --- -- @param Core.Event#EVENTDATA EventData --- function Tank1:OnEventDead( EventData ) --- --- self:SmokeGreen() --- end --- --- --- This function is an Event Handling function that will be called when Tank2 is Dead. --- -- @param Wrapper.Unit#UNIT self --- -- @param Core.Event#EVENTDATA EventData --- function Tank2:OnEventDead( EventData ) --- --- self:SmokeBlue() --- end --- --- ### 1.3.3 Event Handling methods that are automatically called upon subscribed DCS events --- --- ![Objects](..\Presentations\EVENT\Dia10.JPG) --- --- The following list outlines which EVENTS item in the structure corresponds to which Event Handling method. --- Always ensure that your event handling methods align with the events being subscribed to, or nothing will be executed. --- --- # 2) EVENTS type --- --- The EVENTS structure contains names for all the different DCS events that objects can subscribe to using the --- @{Base#BASE.HandleEvent}() method. --- --- # 3) EVENTDATA type --- --- The @{Event#EVENTDATA} structure contains all the fields that are populated with event information before --- an Event Handler method is being called by the event dispatcher. --- The Event Handler received the EVENTDATA object as a parameter, and can be used to investigate further the different events. --- There are basically 4 main categories of information stored in the EVENTDATA structure: --- --- * Initiator Unit data: Several fields documenting the initiator unit related to the event. --- * Target Unit data: Several fields documenting the target unit related to the event. --- * Weapon data: Certain events populate weapon information. --- * Place data: Certain events populate place information. --- --- --- This function is an Event Handling function that will be called when Tank1 is Dead. --- -- EventData is an EVENTDATA structure. --- -- We use the EventData.IniUnit to smoke the tank Green. --- -- @param Wrapper.Unit#UNIT self --- -- @param Core.Event#EVENTDATA EventData --- function Tank1:OnEventDead( EventData ) --- --- EventData.IniUnit:SmokeGreen() --- end --- --- --- Find below an overview which events populate which information categories: --- --- ![Objects](..\Presentations\EVENT\Dia14.JPG) --- --- **IMPORTANT NOTE:** Some events can involve not just UNIT objects, but also STATIC objects!!! --- In that case the initiator or target unit fields will refer to a STATIC object! --- In case a STATIC object is involved, the documentation indicates which fields will and won't not be populated. --- The fields **IniObjectCategory** and **TgtObjectCategory** contain the indicator which **kind of object is involved** in the event. --- You can use the enumerator **Object.Category.UNIT** and **Object.Category.STATIC** to check on IniObjectCategory and TgtObjectCategory. --- Example code snippet: --- --- if Event.IniObjectCategory == Object.Category.UNIT then --- ... --- end --- if Event.IniObjectCategory == Object.Category.STATIC then --- ... --- end --- --- When a static object is involved in the event, the Group and Player fields won't be populated. --- --- ==== --- --- # **API CHANGE HISTORY** --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- YYYY-MM-DD: CLASS:**NewFunction**( Params ) replaces CLASS:_OldFunction_( Params ) --- YYYY-MM-DD: CLASS:**NewFunction( Params )** added --- --- Hereby the change log: --- --- * 2016-02-07: Did a complete revision of the Event Handing API and underlying mechanisms. --- --- === --- --- # **AUTHORS and CONTRIBUTIONS** --- --- ### Contributions: --- --- ### Authors: --- --- * [**FlightControl**](https://forums.eagle.ru/member.php?u=89536): Design & Programming & documentation. --- --- @module Event - --- TODO: Need to update the EVENTDATA documentation with IniPlayerName and TgtPlayerName --- TODO: Need to update the EVENTDATA documentation with IniObjectCategory and TgtObjectCategory - - - ---- The EVENT structure --- @type EVENT --- @field #EVENT.Events Events --- @extends Core.Base#BASE -EVENT = { - ClassName = "EVENT", - ClassID = 0, -} - ---- The different types of events supported by MOOSE. --- Use this structure to subscribe to events using the @{Base#BASE.HandleEvent}() method. --- @type EVENTS -EVENTS = { - Shot = world.event.S_EVENT_SHOT, - Hit = world.event.S_EVENT_HIT, - Takeoff = world.event.S_EVENT_TAKEOFF, - Land = world.event.S_EVENT_LAND, - Crash = world.event.S_EVENT_CRASH, - Ejection = world.event.S_EVENT_EJECTION, - Refueling = world.event.S_EVENT_REFUELING, - Dead = world.event.S_EVENT_DEAD, - PilotDead = world.event.S_EVENT_PILOT_DEAD, - BaseCaptured = world.event.S_EVENT_BASE_CAPTURED, - MissionStart = world.event.S_EVENT_MISSION_START, - MissionEnd = world.event.S_EVENT_MISSION_END, - TookControl = world.event.S_EVENT_TOOK_CONTROL, - RefuelingStop = world.event.S_EVENT_REFUELING_STOP, - Birth = world.event.S_EVENT_BIRTH, - HumanFailure = world.event.S_EVENT_HUMAN_FAILURE, - EngineStartup = world.event.S_EVENT_ENGINE_STARTUP, - EngineShutdown = world.event.S_EVENT_ENGINE_SHUTDOWN, - PlayerEnterUnit = world.event.S_EVENT_PLAYER_ENTER_UNIT, - PlayerLeaveUnit = world.event.S_EVENT_PLAYER_LEAVE_UNIT, - PlayerComment = world.event.S_EVENT_PLAYER_COMMENT, - ShootingStart = world.event.S_EVENT_SHOOTING_START, - ShootingEnd = world.event.S_EVENT_SHOOTING_END, -} - ---- The Event structure --- Note that at the beginning of each field description, there is an indication which field will be populated depending on the object type involved in the Event: --- --- * A (Object.Category.)UNIT : A UNIT object type is involved in the Event. --- * A (Object.Category.)STATIC : A STATIC object type is involved in the Event.µ --- --- @type EVENTDATA --- @field #number id The identifier of the event. --- --- @field Dcs.DCSUnit#Unit initiator (UNIT/STATIC/SCENERY) The initiating @{Dcs.DCSUnit#Unit} or @{Dcs.DCSStaticObject#StaticObject}. --- @field Dcs.DCSObject#Object.Category IniObjectCategory (UNIT/STATIC/SCENERY) The initiator object category ( Object.Category.UNIT or Object.Category.STATIC ). --- @field Dcs.DCSUnit#Unit IniDCSUnit (UNIT/STATIC) The initiating @{DCSUnit#Unit} or @{DCSStaticObject#StaticObject}. --- @field #string IniDCSUnitName (UNIT/STATIC) The initiating Unit name. --- @field Wrapper.Unit#UNIT IniUnit (UNIT/STATIC) The initiating MOOSE wrapper @{Unit#UNIT} of the initiator Unit object. --- @field #string IniUnitName (UNIT/STATIC) The initiating UNIT name (same as IniDCSUnitName). --- @field Dcs.DCSGroup#Group IniDCSGroup (UNIT) The initiating {DCSGroup#Group}. --- @field #string IniDCSGroupName (UNIT) The initiating Group name. --- @field Wrapper.Group#GROUP IniGroup (UNIT) The initiating MOOSE wrapper @{Group#GROUP} of the initiator Group object. --- @field #string IniGroupName UNIT) The initiating GROUP name (same as IniDCSGroupName). --- @field #string IniPlayerName (UNIT) The name of the initiating player in case the Unit is a client or player slot. --- @field Dcs.DCScoalition#coalition.side IniCoalition (UNIT) The coalition of the initiator. --- @field Dcs.DCSUnit#Unit.Category IniCategory (UNIT) The category of the initiator. --- @field #string IniTypeName (UNIT) The type name of the initiator. --- --- @field Dcs.DCSUnit#Unit target (UNIT/STATIC) The target @{Dcs.DCSUnit#Unit} or @{DCSStaticObject#StaticObject}. --- @field Dcs.DCSObject#Object.Category TgtObjectCategory (UNIT/STATIC) The target object category ( Object.Category.UNIT or Object.Category.STATIC ). --- @field Dcs.DCSUnit#Unit TgtDCSUnit (UNIT/STATIC) The target @{DCSUnit#Unit} or @{DCSStaticObject#StaticObject}. --- @field #string TgtDCSUnitName (UNIT/STATIC) The target Unit name. --- @field Wrapper.Unit#UNIT TgtUnit (UNIT/STATIC) The target MOOSE wrapper @{Unit#UNIT} of the target Unit object. --- @field #string TgtUnitName (UNIT/STATIC) The target UNIT name (same as TgtDCSUnitName). --- @field Dcs.DCSGroup#Group TgtDCSGroup (UNIT) The target {DCSGroup#Group}. --- @field #string TgtDCSGroupName (UNIT) The target Group name. --- @field Wrapper.Group#GROUP TgtGroup (UNIT) The target MOOSE wrapper @{Group#GROUP} of the target Group object. --- @field #string TgtGroupName (UNIT) The target GROUP name (same as TgtDCSGroupName). --- @field #string TgtPlayerName (UNIT) The name of the target player in case the Unit is a client or player slot. --- @field Dcs.DCScoalition#coalition.side TgtCoalition (UNIT) The coalition of the target. --- @field Dcs.DCSUnit#Unit.Category TgtCategory (UNIT) The category of the target. --- @field #string TgtTypeName (UNIT) The type name of the target. --- --- @field weapon The weapon used during the event. --- @field Weapon --- @field WeaponName --- @field WeaponTgtDCSUnit - - -local _EVENTMETA = { - [world.event.S_EVENT_SHOT] = { - Order = 1, - Event = "OnEventShot", - Text = "S_EVENT_SHOT" - }, - [world.event.S_EVENT_HIT] = { - Order = 1, - Event = "OnEventHit", - Text = "S_EVENT_HIT" - }, - [world.event.S_EVENT_TAKEOFF] = { - Order = 1, - Event = "OnEventTakeOff", - Text = "S_EVENT_TAKEOFF" - }, - [world.event.S_EVENT_LAND] = { - Order = 1, - Event = "OnEventLand", - Text = "S_EVENT_LAND" - }, - [world.event.S_EVENT_CRASH] = { - Order = -1, - Event = "OnEventCrash", - Text = "S_EVENT_CRASH" - }, - [world.event.S_EVENT_EJECTION] = { - Order = 1, - Event = "OnEventEjection", - Text = "S_EVENT_EJECTION" - }, - [world.event.S_EVENT_REFUELING] = { - Order = 1, - Event = "OnEventRefueling", - Text = "S_EVENT_REFUELING" - }, - [world.event.S_EVENT_DEAD] = { - Order = -1, - Event = "OnEventDead", - Text = "S_EVENT_DEAD" - }, - [world.event.S_EVENT_PILOT_DEAD] = { - Order = 1, - Event = "OnEventPilotDead", - Text = "S_EVENT_PILOT_DEAD" - }, - [world.event.S_EVENT_BASE_CAPTURED] = { - Order = 1, - Event = "OnEventBaseCaptured", - Text = "S_EVENT_BASE_CAPTURED" - }, - [world.event.S_EVENT_MISSION_START] = { - Order = 1, - Event = "OnEventMissionStart", - Text = "S_EVENT_MISSION_START" - }, - [world.event.S_EVENT_MISSION_END] = { - Order = 1, - Event = "OnEventMissionEnd", - Text = "S_EVENT_MISSION_END" - }, - [world.event.S_EVENT_TOOK_CONTROL] = { - Order = 1, - Event = "OnEventTookControl", - Text = "S_EVENT_TOOK_CONTROL" - }, - [world.event.S_EVENT_REFUELING_STOP] = { - Order = 1, - Event = "OnEventRefuelingStop", - Text = "S_EVENT_REFUELING_STOP" - }, - [world.event.S_EVENT_BIRTH] = { - Order = 1, - Event = "OnEventBirth", - Text = "S_EVENT_BIRTH" - }, - [world.event.S_EVENT_HUMAN_FAILURE] = { - Order = 1, - Event = "OnEventHumanFailure", - Text = "S_EVENT_HUMAN_FAILURE" - }, - [world.event.S_EVENT_ENGINE_STARTUP] = { - Order = 1, - Event = "OnEventEngineStartup", - Text = "S_EVENT_ENGINE_STARTUP" - }, - [world.event.S_EVENT_ENGINE_SHUTDOWN] = { - Order = 1, - Event = "OnEventEngineShutdown", - Text = "S_EVENT_ENGINE_SHUTDOWN" - }, - [world.event.S_EVENT_PLAYER_ENTER_UNIT] = { - Order = 1, - Event = "OnEventPlayerEnterUnit", - Text = "S_EVENT_PLAYER_ENTER_UNIT" - }, - [world.event.S_EVENT_PLAYER_LEAVE_UNIT] = { - Order = -1, - Event = "OnEventPlayerLeaveUnit", - Text = "S_EVENT_PLAYER_LEAVE_UNIT" - }, - [world.event.S_EVENT_PLAYER_COMMENT] = { - Order = 1, - Event = "OnEventPlayerComment", - Text = "S_EVENT_PLAYER_COMMENT" - }, - [world.event.S_EVENT_SHOOTING_START] = { - Order = 1, - Event = "OnEventShootingStart", - Text = "S_EVENT_SHOOTING_START" - }, - [world.event.S_EVENT_SHOOTING_END] = { - Order = 1, - Event = "OnEventShootingEnd", - Text = "S_EVENT_SHOOTING_END" - }, -} - - ---- 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 = _EVENTMETA[EventID].Text - - return EventText -end - - ---- Initializes the Events structure for the event --- @param #EVENT self --- @param Dcs.DCSWorld#world.event EventID --- @param Core.Base#BASE EventClass --- @return #EVENT.Events -function EVENT:Init( EventID, EventClass ) - self:F3( { _EVENTMETA[EventID].Text, EventClass } ) - - if not self.Events[EventID] then - -- Create a WEAK table to ensure that the garbage collector is cleaning the event links when the object usage is cleaned. - self.Events[EventID] = setmetatable( {}, { __mode = "k" } ) - end - - -- Each event has a subtable of EventClasses, ordered by EventPriority. - local EventPriority = EventClass:GetEventPriority() - if not self.Events[EventID][EventPriority] then - self.Events[EventID][EventPriority] = {} - end - - if not self.Events[EventID][EventPriority][EventClass] then - self.Events[EventID][EventPriority][EventClass] = setmetatable( {}, { __mode = "k" } ) - end - return self.Events[EventID][EventPriority][EventClass] -end - ---- Removes an Events entry --- @param #EVENT self --- @param Core.Base#BASE EventClass The self instance of the class for which the event is. --- @param Dcs.DCSWorld#world.event EventID --- @return #EVENT.Events -function EVENT:Remove( EventClass, EventID ) - self:F3( { EventClass, _EVENTMETA[EventID].Text } ) - - local EventClass = EventClass - local EventPriority = EventClass:GetEventPriority() - self.Events[EventID][EventPriority][EventClass] = nil -end - ---- Removes an Events entry for a Unit --- @param #EVENT self --- @param Core.Base#BASE EventClass The self instance of the class for which the event is. --- @param Dcs.DCSWorld#world.event EventID --- @return #EVENT.Events -function EVENT:RemoveForUnit( UnitName, EventClass, EventID ) - self:F3( { EventClass, _EVENTMETA[EventID].Text } ) - - local EventClass = EventClass - local EventPriority = EventClass:GetEventPriority() - local Event = self.Events[EventID][EventPriority][EventClass] - Event.IniUnit[UnitName] = nil -end - ---- Clears all event subscriptions for a @{Base#BASE} derived object. --- @param #EVENT self --- @param Core.Base#BASE EventObject -function EVENT:RemoveAll( EventObject ) - self:F3( { EventObject:GetClassNameAndID() } ) - - local EventClass = EventObject:GetClassNameAndID() - local EventPriority = EventClass:GetEventPriority() - for EventID, EventData in pairs( self.Events ) do - self.Events[EventID][EventPriority][EventClass] = nil - end -end - - - ---- Create an OnDead event handler for a group --- @param #EVENT self --- @param #table EventTemplate --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param EventClass The instance of the class for which the event is. --- @param #function OnEventFunction --- @return #EVENT -function EVENT:OnEventForTemplate( EventTemplate, EventFunction, EventClass, OnEventFunction ) - self:F2( EventTemplate.name ) - - for EventUnitID, EventUnit in pairs( EventTemplate.units ) do - OnEventFunction( self, EventUnit.name, EventFunction, EventClass ) - end - return self -end - ---- Set a new listener for an S_EVENT_X event independent from a unit or a weapon. --- @param #EVENT self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Core.Base#BASE EventClass The self instance of the class for which the event is captured. When the event happens, the event process will be called in this class provided. --- @param EventID --- @return #EVENT -function EVENT:OnEventGeneric( EventFunction, EventClass, EventID ) - self:F2( { EventID } ) - - local Event = self:Init( EventID, EventClass ) - Event.EventFunction = EventFunction - Event.EventClass = EventClass - - 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 Core.Base#BASE EventClass The self instance of the class for which the event is. --- @param EventID --- @return #EVENT -function EVENT:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, EventID ) - self:F2( EventDCSUnitName ) - - local Event = self:Init( EventID, EventClass ) - if not Event.IniUnit then - Event.IniUnit = {} - end - Event.IniUnit[EventDCSUnitName] = {} - Event.IniUnit[EventDCSUnitName].EventFunction = EventFunction - Event.IniUnit[EventDCSUnitName].EventClass = EventClass - return self -end - -do -- OnBirth - - --- Create an OnBirth event handler for a group - -- @param #EVENT self - -- @param Wrapper.Group#GROUP EventGroup - -- @param #function EventFunction The function to be called when the event occurs for the unit. - -- @param EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnBirthForTemplate( EventTemplate, EventFunction, EventClass ) - self:F2( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, 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 EventClass - -- @return #EVENT - function EVENT:OnBirth( EventFunction, EventClass ) - self:F2() - - self:OnEventGeneric( EventFunction, EventClass, 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 EventClass - -- @return #EVENT - function EVENT:OnBirthForUnit( EventDCSUnitName, EventFunction, EventClass ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_BIRTH ) - - return self - end - - --- Stop listening to S_EVENT_BIRTH event. - -- @param #EVENT self - -- @param Base#BASE EventClass - -- @return #EVENT - function EVENT:OnBirthRemove( EventClass ) - self:F2() - - self:Remove( EventClass, world.event.S_EVENT_BIRTH ) - - return self - end - - -end - -do -- OnCrash - - --- Create an OnCrash event handler for a group - -- @param #EVENT self - -- @param Wrapper.Group#GROUP EventGroup - -- @param #function EventFunction The function to be called when the event occurs for the unit. - -- @param EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnCrashForTemplate( EventTemplate, EventFunction, EventClass ) - self:F2( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, 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 EventClass - -- @return #EVENT - function EVENT:OnCrash( EventFunction, EventClass ) - self:F2() - - self:OnEventGeneric( EventFunction, EventClass, 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 EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnCrashForUnit( EventDCSUnitName, EventFunction, EventClass ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_CRASH ) - - return self - end - - --- Stop listening to S_EVENT_CRASH event. - -- @param #EVENT self - -- @param Base#BASE EventClass - -- @return #EVENT - function EVENT:OnCrashRemove( EventClass ) - self:F2() - - self:Remove( EventClass, world.event.S_EVENT_CRASH ) - - return self - end - -end - -do -- OnDead - - --- Create an OnDead event handler for a group - -- @param #EVENT self - -- @param Wrapper.Group#GROUP EventGroup - -- @param #function EventFunction The function to be called when the event occurs for the unit. - -- @param EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnDeadForTemplate( EventTemplate, EventFunction, EventClass ) - self:F2( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, 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 EventClass - -- @return #EVENT - function EVENT:OnDead( EventFunction, EventClass ) - self:F2() - - self:OnEventGeneric( EventFunction, EventClass, 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 EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnDeadForUnit( EventDCSUnitName, EventFunction, EventClass ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_DEAD ) - - return self - end - - --- Stop listening to S_EVENT_DEAD event. - -- @param #EVENT self - -- @param Base#BASE EventClass - -- @return #EVENT - function EVENT:OnDeadRemove( EventClass ) - self:F2() - - self:Remove( EventClass, world.event.S_EVENT_DEAD ) - - return self - end - - -end - -do -- OnPilotDead - - --- Set a new listener for an S_EVENT_PILOT_DEAD event. - -- @param #EVENT self - -- @param #function EventFunction The function to be called when the event occurs for the unit. - -- @param Base#BASE EventClass - -- @return #EVENT - function EVENT:OnPilotDead( EventFunction, EventClass ) - self:F2() - - self:OnEventGeneric( EventFunction, EventClass, world.event.S_EVENT_PILOT_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 EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnPilotDeadForUnit( EventDCSUnitName, EventFunction, EventClass ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_PILOT_DEAD ) - - return self - end - - --- Stop listening to S_EVENT_PILOT_DEAD event. - -- @param #EVENT self - -- @param Base#BASE EventClass - -- @return #EVENT - function EVENT:OnPilotDeadRemove( EventClass ) - self:F2() - - self:Remove( EventClass, world.event.S_EVENT_PILOT_DEAD ) - - return self - end - -end - -do -- OnLand - --- Create an OnLand event handler for a group - -- @param #EVENT self - -- @param #table EventTemplate - -- @param #function EventFunction The function to be called when the event occurs for the unit. - -- @param EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnLandForTemplate( EventTemplate, EventFunction, EventClass ) - self:F2( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, 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 EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnLandForUnit( EventDCSUnitName, EventFunction, EventClass ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_LAND ) - - return self - end - - --- Stop listening to S_EVENT_LAND event. - -- @param #EVENT self - -- @param Base#BASE EventClass - -- @return #EVENT - function EVENT:OnLandRemove( EventClass ) - self:F2() - - self:Remove( EventClass, world.event.S_EVENT_LAND ) - - return self - end - - -end - -do -- OnTakeOff - --- Create an OnTakeOff event handler for a group - -- @param #EVENT self - -- @param #table EventTemplate - -- @param #function EventFunction The function to be called when the event occurs for the unit. - -- @param EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnTakeOffForTemplate( EventTemplate, EventFunction, EventClass ) - self:F2( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, 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 EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnTakeOffForUnit( EventDCSUnitName, EventFunction, EventClass ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_TAKEOFF ) - - return self - end - - --- Stop listening to S_EVENT_TAKEOFF event. - -- @param #EVENT self - -- @param Base#BASE EventClass - -- @return #EVENT - function EVENT:OnTakeOffRemove( EventClass ) - self:F2() - - self:Remove( EventClass, world.event.S_EVENT_TAKEOFF ) - - return self - end - - -end - -do -- OnEngineShutDown - - --- Create an OnDead event handler for a group - -- @param #EVENT self - -- @param #table EventTemplate - -- @param #function EventFunction The function to be called when the event occurs for the unit. - -- @param EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnEngineShutDownForTemplate( EventTemplate, EventFunction, EventClass ) - self:F2( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, 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 EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnEngineShutDownForUnit( EventDCSUnitName, EventFunction, EventClass ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_ENGINE_SHUTDOWN ) - - return self - end - - --- Stop listening to S_EVENT_ENGINE_SHUTDOWN event. - -- @param #EVENT self - -- @param Base#BASE EventClass - -- @return #EVENT - function EVENT:OnEngineShutDownRemove( EventClass ) - self:F2() - - self:Remove( EventClass, world.event.S_EVENT_ENGINE_SHUTDOWN ) - - return self - end - -end - -do -- OnEngineStartUp - - --- 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 EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnEngineStartUpForUnit( EventDCSUnitName, EventFunction, EventClass ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_ENGINE_STARTUP ) - - return self - end - - --- Stop listening to S_EVENT_ENGINE_STARTUP event. - -- @param #EVENT self - -- @param Base#BASE EventClass - -- @return #EVENT - function EVENT:OnEngineStartUpRemove( EventClass ) - self:F2() - - self:Remove( EventClass, world.event.S_EVENT_ENGINE_STARTUP ) - - return self - end - -end - -do -- OnShot - --- 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 EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnShot( EventFunction, EventClass ) - self:F2() - - self:OnEventGeneric( EventFunction, EventClass, 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 EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnShotForUnit( EventDCSUnitName, EventFunction, EventClass ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_SHOT ) - - return self - end - - --- Stop listening to S_EVENT_SHOT event. - -- @param #EVENT self - -- @param Base#BASE EventClass - -- @return #EVENT - function EVENT:OnShotRemove( EventClass ) - self:F2() - - self:Remove( EventClass, world.event.S_EVENT_SHOT ) - - return self - end - - -end - -do -- OnHit - - --- 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 EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnHit( EventFunction, EventClass ) - self:F2() - - self:OnEventGeneric( EventFunction, EventClass, 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 EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnHitForUnit( EventDCSUnitName, EventFunction, EventClass ) - self:F2( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_HIT ) - - return self - end - - --- Stop listening to S_EVENT_HIT event. - -- @param #EVENT self - -- @param Base#BASE EventClass - -- @return #EVENT - function EVENT:OnHitRemove( EventClass ) - self:F2() - - self:Remove( EventClass, world.event.S_EVENT_HIT ) - - return self - end - -end - -do -- OnPlayerEnterUnit - - --- 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 EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnPlayerEnterUnit( EventFunction, EventClass ) - self:F2() - - self:OnEventGeneric( EventFunction, EventClass, world.event.S_EVENT_PLAYER_ENTER_UNIT ) - - return self - end - - --- Stop listening to S_EVENT_PLAYER_ENTER_UNIT event. - -- @param #EVENT self - -- @param Base#BASE EventClass - -- @return #EVENT - function EVENT:OnPlayerEnterRemove( EventClass ) - self:F2() - - self:Remove( EventClass, world.event.S_EVENT_PLAYER_ENTER_UNIT ) - - return self - end - -end - -do -- OnPlayerLeaveUnit - --- 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 EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnPlayerLeaveUnit( EventFunction, EventClass ) - self:F2() - - self:OnEventGeneric( EventFunction, EventClass, world.event.S_EVENT_PLAYER_LEAVE_UNIT ) - - return self - end - - --- Stop listening to S_EVENT_PLAYER_LEAVE_UNIT event. - -- @param #EVENT self - -- @param Base#BASE EventClass - -- @return #EVENT - function EVENT:OnPlayerLeaveRemove( EventClass ) - self:F2() - - self:Remove( EventClass, world.event.S_EVENT_PLAYER_LEAVE_UNIT ) - - return self - end - -end - - - ---- @param #EVENT self --- @param #EVENTDATA Event -function EVENT:onEvent( Event ) - - local ErrorHandler = function( errmsg ) - - env.info( "Error in SCHEDULER function:" .. errmsg ) - if debug ~= nil then - env.info( debug.traceback() ) - end - - return errmsg - end - - if self and self.Events and self.Events[Event.id] then - - - if Event.initiator then - - Event.IniObjectCategory = Event.initiator:getCategory() - - if Event.IniObjectCategory == Object.Category.UNIT then - Event.IniDCSUnit = Event.initiator - Event.IniDCSUnitName = Event.IniDCSUnit:getName() - Event.IniUnitName = Event.IniDCSUnitName - Event.IniDCSGroup = Event.IniDCSUnit:getGroup() - Event.IniUnit = UNIT:FindByName( Event.IniDCSUnitName ) - if not Event.IniUnit then - -- Unit can be a CLIENT. Most likely this will be the case ... - Event.IniUnit = CLIENT:FindByName( Event.IniDCSUnitName, '', true ) - end - Event.IniDCSGroupName = "" - if Event.IniDCSGroup and Event.IniDCSGroup:isExist() then - Event.IniDCSGroupName = Event.IniDCSGroup:getName() - Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) - self:E( { IniGroup = Event.IniGroup } ) - end - Event.IniPlayerName = Event.IniDCSUnit:getPlayerName() - Event.IniCoalition = Event.IniDCSUnit:getCoalition() - Event.IniTypeName = Event.IniDCSUnit:getTypeName() - Event.IniCategory = Event.IniDCSUnit:getDesc().category - end - - if Event.IniObjectCategory == Object.Category.STATIC then - Event.IniDCSUnit = Event.initiator - Event.IniDCSUnitName = Event.IniDCSUnit:getName() - Event.IniUnitName = Event.IniDCSUnitName - Event.IniUnit = STATIC:FindByName( Event.IniDCSUnitName, false ) - Event.IniCoalition = Event.IniDCSUnit:getCoalition() - Event.IniCategory = Event.IniDCSUnit:getDesc().category - Event.IniTypeName = Event.IniDCSUnit:getTypeName() - end - - if Event.IniObjectCategory == Object.Category.SCENERY then - Event.IniDCSUnit = Event.initiator - Event.IniDCSUnitName = Event.IniDCSUnit:getName() - Event.IniUnitName = Event.IniDCSUnitName - Event.IniUnit = SCENERY:Register( Event.IniDCSUnitName, Event.initiator ) - Event.IniCategory = Event.IniDCSUnit:getDesc().category - Event.IniTypeName = Event.IniDCSUnit:getTypeName() - end - end - - if Event.target then - - Event.TgtObjectCategory = Event.target:getCategory() - - if Event.TgtObjectCategory == Object.Category.UNIT then - Event.TgtDCSUnit = Event.target - Event.TgtDCSGroup = Event.TgtDCSUnit:getGroup() - Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() - Event.TgtUnitName = Event.TgtDCSUnitName - Event.TgtUnit = UNIT:FindByName( Event.TgtDCSUnitName ) - Event.TgtDCSGroupName = "" - if Event.TgtDCSGroup and Event.TgtDCSGroup:isExist() then - Event.TgtDCSGroupName = Event.TgtDCSGroup:getName() - end - Event.TgtPlayerName = Event.TgtDCSUnit:getPlayerName() - Event.TgtCoalition = Event.TgtDCSUnit:getCoalition() - Event.TgtCategory = Event.TgtDCSUnit:getDesc().category - Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() - end - - if Event.TgtObjectCategory == Object.Category.STATIC then - Event.TgtDCSUnit = Event.target - Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() - Event.TgtUnitName = Event.TgtDCSUnitName - Event.TgtUnit = STATIC:FindByName( Event.TgtDCSUnitName ) - Event.TgtCoalition = Event.TgtDCSUnit:getCoalition() - Event.TgtCategory = Event.TgtDCSUnit:getDesc().category - Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() - end - - if Event.TgtObjectCategory == Object.Category.SCENERY then - Event.TgtDCSUnit = Event.target - Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() - Event.TgtUnitName = Event.TgtDCSUnitName - Event.TgtUnit = SCENERY:Register( Event.TgtDCSUnitName, Event.target ) - Event.TgtCategory = Event.TgtDCSUnit:getDesc().category - Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() - end - end - - if Event.weapon then - Event.Weapon = Event.weapon - Event.WeaponName = Event.Weapon:getTypeName() - --Event.WeaponTgtDCSUnit = Event.Weapon:getTarget() - end - - local PriorityOrder = _EVENTMETA[Event.id].Order - local PriorityBegin = PriorityOrder == -1 and 5 or 1 - local PriorityEnd = PriorityOrder == -1 and 1 or 5 - - self:E( { _EVENTMETA[Event.id].Text, Event, Event.IniDCSUnitName, Event.TgtDCSUnitName, PriorityOrder } ) - - for EventPriority = PriorityBegin, PriorityEnd, PriorityOrder do - - if self.Events[Event.id][EventPriority] then - - -- Okay, we got the event from DCS. Now loop the SORTED self.EventSorted[] table for the received Event.id, and for each EventData registered, check if a function needs to be called. - for EventClass, EventData in pairs( self.Events[Event.id][EventPriority] ) do - - -- If the EventData is for a UNIT, the call directly the EventClass EventFunction for that UNIT. - if Event.IniDCSUnitName and EventData.IniUnit and EventData.IniUnit[Event.IniDCSUnitName] then - - -- First test if a EventFunction is Set, otherwise search for the default function - if EventData.IniUnit[Event.IniDCSUnitName].EventFunction then - - self:E( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName, EventPriority } ) - Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) - - local Result, Value = xpcall( - function() - return EventData.IniUnit[Event.IniDCSUnitName].EventFunction( EventClass, Event ) - end, ErrorHandler ) - - else - - -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. - local EventFunction = EventClass[ _EVENTMETA[Event.id].Event ] - if EventFunction and type( EventFunction ) == "function" then - - -- Now call the default event function. - self:E( { "Calling " .. _EVENTMETA[Event.id].Event .. " for Class ", EventClass:GetClassNameAndID(), EventPriority } ) - Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) - - local Result, Value = xpcall( - function() - return EventFunction( EventClass, Event ) - end, ErrorHandler ) - end - - end - - else - - -- If the EventData is not bound to a specific unit, then call the EventClass EventFunction. - -- Note that here the EventFunction will need to implement and determine the logic for the relevant source- or target unit, or weapon. - if Event.IniDCSUnit and not EventData.IniUnit then - - if EventClass == EventData.EventClass then - - -- First test if a EventFunction is Set, otherwise search for the default function - if EventData.EventFunction then - - -- There is an EventFunction defined, so call the EventFunction. - self:E( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID(), EventPriority } ) - Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) - - local Result, Value = xpcall( - function() - return EventData.EventFunction( EventClass, Event ) - end, ErrorHandler ) - else - - -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. - local EventFunction = EventClass[ _EVENTMETA[Event.id].Event ] - if EventFunction and type( EventFunction ) == "function" then - - -- Now call the default event function. - self:E( { "Calling " .. _EVENTMETA[Event.id].Event .. " for Class ", EventClass:GetClassNameAndID(), EventPriority } ) - Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) - - local Result, Value = xpcall( - function() - return EventFunction( EventClass, Event ) - end, ErrorHandler ) - end - end - end - end - end - end - end - end - else - self:E( { _EVENTMETA[Event.id].Text, Event } ) - end -end - ---- The EVENTHANDLER structure --- @type EVENTHANDLER --- @extends Core.Base#BASE -EVENTHANDLER = { - ClassName = "EVENTHANDLER", - ClassID = 0, -} - ---- The EVENTHANDLER constructor --- @param #EVENTHANDLER self --- @return #EVENTHANDLER -function EVENTHANDLER:New() - self = BASE:Inherit( self, BASE:New() ) -- #EVENTHANDLER - return self -end ---- This module contains the MENU classes. --- --- === --- --- DCS Menus can be managed using the MENU classes. --- The advantage of using MENU classes is that it hides the complexity of dealing with menu management in more advanced scanerios where you need to --- set menus and later remove them, and later set them again. You'll find while using use normal DCS scripting functions, that setting and removing --- menus is not a easy feat if you have complex menu hierarchies defined. --- Using the MOOSE menu classes, the removal and refreshing of menus are nicely being handled within these classes, and becomes much more easy. --- On top, MOOSE implements **variable parameter** passing for command menus. --- --- There are basically two different MENU class types that you need to use: --- --- ### To manage **main menus**, the classes begin with **MENU_**: --- --- * @{Menu#MENU_MISSION}: Manages main menus for whole mission file. --- * @{Menu#MENU_COALITION}: Manages main menus for whole coalition. --- * @{Menu#MENU_GROUP}: Manages main menus for GROUPs. --- * @{Menu#MENU_CLIENT}: Manages main menus for CLIENTs. This manages menus for units with the skill level "Client". --- --- ### To manage **command menus**, which are menus that allow the player to issue **functions**, the classes begin with **MENU_COMMAND_**: --- --- * @{Menu#MENU_MISSION_COMMAND}: Manages command menus for whole mission file. --- * @{Menu#MENU_COALITION_COMMAND}: Manages command menus for whole coalition. --- * @{Menu#MENU_GROUP_COMMAND}: Manages command menus for GROUPs. --- * @{Menu#MENU_CLIENT_COMMAND}: Manages command menus for CLIENTs. This manages menus for units with the skill level "Client". --- --- === --- --- The above menus classes **are derived** from 2 main **abstract** classes defined within the MOOSE framework (so don't use these): --- --- 1) MENU_ BASE abstract base classes (don't use them) --- ==================================================== --- The underlying base menu classes are **NOT** to be used within your missions. --- These are simply abstract base classes defining a couple of fields that are used by the --- derived MENU_ classes to manage menus. --- --- 1.1) @{#MENU_BASE} class, extends @{Base#BASE} --- -------------------------------------------------- --- The @{#MENU_BASE} class defines the main MENU class where other MENU classes are derived from. --- --- 1.2) @{#MENU_COMMAND_BASE} class, extends @{Base#BASE} --- ---------------------------------------------------------- --- The @{#MENU_COMMAND_BASE} class defines the main MENU class where other MENU COMMAND_ classes are derived from, in order to set commands. --- --- === --- --- **The next menus define the MENU classes that you can use within your missions.** --- --- 2) MENU MISSION classes --- ====================== --- The underlying classes manage the menus for a complete mission file. --- --- 2.1) @{#MENU_MISSION} class, extends @{Menu#MENU_BASE} --- --------------------------------------------------------- --- The @{Menu#MENU_MISSION} class manages the main menus for a complete mission. --- You can add menus with the @{#MENU_MISSION.New} method, which constructs a MENU_MISSION object and returns you the object reference. --- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_MISSION.Remove}. --- --- 2.2) @{#MENU_MISSION_COMMAND} class, extends @{Menu#MENU_COMMAND_BASE} --- ------------------------------------------------------------------------- --- The @{Menu#MENU_MISSION_COMMAND} class manages the command menus for a complete mission, which allow players to execute functions during mission execution. --- You can add menus with the @{#MENU_MISSION_COMMAND.New} method, which constructs a MENU_MISSION_COMMAND object and returns you the object reference. --- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_MISSION_COMMAND.Remove}. --- --- === --- --- 3) MENU COALITION classes --- ========================= --- The underlying classes manage the menus for whole coalitions. --- --- 3.1) @{#MENU_COALITION} class, extends @{Menu#MENU_BASE} --- ------------------------------------------------------------ --- The @{Menu#MENU_COALITION} class manages the main menus for coalitions. --- You can add menus with the @{#MENU_COALITION.New} method, which constructs a MENU_COALITION object and returns you the object reference. --- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_COALITION.Remove}. --- --- 3.2) @{Menu#MENU_COALITION_COMMAND} class, extends @{Menu#MENU_COMMAND_BASE} --- ---------------------------------------------------------------------------- --- The @{Menu#MENU_COALITION_COMMAND} class manages the command menus for coalitions, which allow players to execute functions during mission execution. --- You can add menus with the @{#MENU_COALITION_COMMAND.New} method, which constructs a MENU_COALITION_COMMAND object and returns you the object reference. --- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_COALITION_COMMAND.Remove}. --- --- === --- --- 4) MENU GROUP classes --- ===================== --- The underlying classes manage the menus for groups. Note that groups can be inactive, alive or can be destroyed. --- --- 4.1) @{Menu#MENU_GROUP} class, extends @{Menu#MENU_BASE} --- -------------------------------------------------------- --- The @{Menu#MENU_GROUP} class manages the main menus for coalitions. --- You can add menus with the @{#MENU_GROUP.New} method, which constructs a MENU_GROUP object and returns you the object reference. --- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_GROUP.Remove}. --- --- 4.2) @{Menu#MENU_GROUP_COMMAND} class, extends @{Menu#MENU_COMMAND_BASE} --- ------------------------------------------------------------------------ --- The @{Menu#MENU_GROUP_COMMAND} class manages the command menus for coalitions, which allow players to execute functions during mission execution. --- You can add menus with the @{#MENU_GROUP_COMMAND.New} method, which constructs a MENU_GROUP_COMMAND object and returns you the object reference. --- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_GROUP_COMMAND.Remove}. --- --- === --- --- 5) MENU CLIENT classes --- ====================== --- The underlying classes manage the menus for units with skill level client or player. --- --- 5.1) @{Menu#MENU_CLIENT} class, extends @{Menu#MENU_BASE} --- --------------------------------------------------------- --- The @{Menu#MENU_CLIENT} class manages the main menus for coalitions. --- You can add menus with the @{#MENU_CLIENT.New} method, which constructs a MENU_CLIENT object and returns you the object reference. --- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_CLIENT.Remove}. --- --- 5.2) @{Menu#MENU_CLIENT_COMMAND} class, extends @{Menu#MENU_COMMAND_BASE} --- ------------------------------------------------------------------------- --- The @{Menu#MENU_CLIENT_COMMAND} class manages the command menus for coalitions, which allow players to execute functions during mission execution. --- You can add menus with the @{#MENU_CLIENT_COMMAND.New} method, which constructs a MENU_CLIENT_COMMAND object and returns you the object reference. --- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_CLIENT_COMMAND.Remove}. --- --- === --- --- ### Contributions: - --- ### Authors: FlightControl : Design & Programming --- --- @module Menu - - -do -- MENU_BASE - - --- The MENU_BASE class - -- @type MENU_BASE - -- @extends Base#BASE - MENU_BASE = { - ClassName = "MENU_BASE", - MenuPath = nil, - MenuText = "", - MenuParentPath = nil - } - - --- Consructor - function MENU_BASE:New( MenuText, ParentMenu ) - - local MenuParentPath = {} - if ParentMenu ~= nil then - MenuParentPath = ParentMenu.MenuPath - end - - local self = BASE:Inherit( self, BASE:New() ) - - self.MenuPath = nil - self.MenuText = MenuText - self.MenuParentPath = MenuParentPath - - return self - end - -end - -do -- MENU_COMMAND_BASE - - --- The MENU_COMMAND_BASE class - -- @type MENU_COMMAND_BASE - -- @field #function MenuCallHandler - -- @extends Menu#MENU_BASE - MENU_COMMAND_BASE = { - ClassName = "MENU_COMMAND_BASE", - CommandMenuFunction = nil, - CommandMenuArgument = nil, - MenuCallHandler = nil, - } - - --- Constructor - function MENU_COMMAND_BASE:New( MenuText, ParentMenu, CommandMenuFunction, CommandMenuArguments ) - - local self = BASE:Inherit( self, MENU_BASE:New( MenuText, ParentMenu ) ) - - self.CommandMenuFunction = CommandMenuFunction - self.MenuCallHandler = function( CommandMenuArguments ) - self.CommandMenuFunction( unpack( CommandMenuArguments ) ) - end - - return self - end - -end - - -do -- MENU_MISSION - - --- The MENU_MISSION class - -- @type MENU_MISSION - -- @extends Menu#MENU_BASE - MENU_MISSION = { - ClassName = "MENU_MISSION" - } - - --- MENU_MISSION constructor. Creates a new MENU_MISSION object and creates the menu for a complete mission file. - -- @param #MENU_MISSION self - -- @param #string MenuText The text for the menu. - -- @param #table ParentMenu The parent menu. This parameter can be ignored if you want the menu to be located at the perent menu of DCS world (under F10 other). - -- @return #MENU_MISSION self - function MENU_MISSION:New( MenuText, ParentMenu ) - - local self = BASE:Inherit( self, MENU_BASE:New( MenuText, ParentMenu ) ) - - self:F( { MenuText, ParentMenu } ) - - self.MenuText = MenuText - self.ParentMenu = ParentMenu - - self.Menus = {} - - self:T( { MenuText } ) - - self.MenuPath = missionCommands.addSubMenu( MenuText, self.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_MISSION. Note that the main menu is kept! - -- @param #MENU_MISSION self - -- @return #MENU_MISSION self - function MENU_MISSION:RemoveSubMenus() - self:F( self.MenuPath ) - - for MenuID, Menu in pairs( self.Menus ) do - Menu:Remove() - end - - end - - --- Removes the main menu and the sub menus recursively of this MENU_MISSION. - -- @param #MENU_MISSION self - -- @return #nil - function MENU_MISSION:Remove() - self:F( self.MenuPath ) - - self:RemoveSubMenus() - missionCommands.removeItem( self.MenuPath ) - if self.ParentMenu then - self.ParentMenu.Menus[self.MenuPath] = nil - end - - return nil - end - -end - -do -- MENU_MISSION_COMMAND - - --- The MENU_MISSION_COMMAND class - -- @type MENU_MISSION_COMMAND - -- @extends Menu#MENU_COMMAND_BASE - MENU_MISSION_COMMAND = { - ClassName = "MENU_MISSION_COMMAND" - } - - --- MENU_MISSION constructor. Creates a new radio command item for a complete mission file, which can invoke a function with parameters. - -- @param #MENU_MISSION_COMMAND self - -- @param #string MenuText The text for the menu. - -- @param Menu#MENU_MISSION ParentMenu The parent menu. - -- @param CommandMenuFunction A function that is called when the menu key is pressed. - -- @param CommandMenuArgument An argument for the function. There can only be ONE argument given. So multiple arguments must be wrapped into a table. See the below example how to do this. - -- @return #MENU_MISSION_COMMAND self - function MENU_MISSION_COMMAND:New( MenuText, ParentMenu, CommandMenuFunction, ... ) - - local self = BASE:Inherit( self, MENU_COMMAND_BASE:New( MenuText, ParentMenu, CommandMenuFunction, arg ) ) - - self.MenuText = MenuText - self.ParentMenu = ParentMenu - - self:T( { MenuText, CommandMenuFunction, arg } ) - - - self.MenuPath = missionCommands.addCommand( MenuText, self.MenuParentPath, self.MenuCallHandler, arg ) - - ParentMenu.Menus[self.MenuPath] = self - - return self - end - - --- Removes a radio command item for a coalition - -- @param #MENU_MISSION_COMMAND self - -- @return #nil - function MENU_MISSION_COMMAND:Remove() - self:F( self.MenuPath ) - - missionCommands.removeItem( self.MenuPath ) - if self.ParentMenu then - self.ParentMenu.Menus[self.MenuPath] = nil - end - return nil - end - -end - - - -do -- MENU_COALITION - - --- The MENU_COALITION class - -- @type MENU_COALITION - -- @extends Menu#MENU_BASE - -- @usage - -- -- This demo creates a menu structure for the planes within the red coalition. - -- -- To test, join the planes, then look at the other radio menus (Option F10). - -- -- Then switch planes and check if the menu is still there. - -- - -- local Plane1 = CLIENT:FindByName( "Plane 1" ) - -- local Plane2 = CLIENT:FindByName( "Plane 2" ) - -- - -- - -- -- This would create a menu for the red coalition under the main DCS "Others" menu. - -- local MenuCoalitionRed = MENU_COALITION:New( coalition.side.RED, "Manage Menus" ) - -- - -- - -- local function ShowStatus( StatusText, Coalition ) - -- - -- MESSAGE:New( Coalition, 15 ):ToRed() - -- Plane1:Message( StatusText, 15 ) - -- Plane2:Message( StatusText, 15 ) - -- end - -- - -- local MenuStatus -- Menu#MENU_COALITION - -- local MenuStatusShow -- Menu#MENU_COALITION_COMMAND - -- - -- local function RemoveStatusMenu() - -- MenuStatus:Remove() - -- end - -- - -- local function AddStatusMenu() - -- - -- -- This would create a menu for the red coalition under the MenuCoalitionRed menu object. - -- MenuStatus = MENU_COALITION:New( coalition.side.RED, "Status for Planes" ) - -- MenuStatusShow = MENU_COALITION_COMMAND:New( coalition.side.RED, "Show Status", MenuStatus, ShowStatus, "Status of planes is ok!", "Message to Red Coalition" ) - -- end - -- - -- local MenuAdd = MENU_COALITION_COMMAND:New( coalition.side.RED, "Add Status Menu", MenuCoalitionRed, AddStatusMenu ) - -- local MenuRemove = MENU_COALITION_COMMAND:New( coalition.side.RED, "Remove Status Menu", MenuCoalitionRed, RemoveStatusMenu ) - MENU_COALITION = { - ClassName = "MENU_COALITION" - } - - --- MENU_COALITION constructor. Creates a new MENU_COALITION object and creates the menu for a complete coalition. - -- @param #MENU_COALITION self - -- @param Dcs.DCSCoalition#coalition.side Coalition The coalition owning the menu. - -- @param #string MenuText The text for the menu. - -- @param #table ParentMenu The parent menu. This parameter can be ignored if you want the menu to be located at the perent menu of DCS world (under F10 other). - -- @return #MENU_COALITION self - function MENU_COALITION:New( Coalition, MenuText, ParentMenu ) - - local self = BASE:Inherit( self, MENU_BASE:New( MenuText, ParentMenu ) ) - - self:F( { Coalition, MenuText, ParentMenu } ) - - self.Coalition = Coalition - self.MenuText = MenuText - self.ParentMenu = ParentMenu - - self.Menus = {} - - self:T( { MenuText } ) - - self.MenuPath = missionCommands.addSubMenuForCoalition( Coalition, MenuText, self.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. Note that the main menu is kept! - -- @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 main menu and the sub menus recursively of this MENU_COALITION. - -- @param #MENU_COALITION self - -- @return #nil - function MENU_COALITION:Remove() - self:F( self.MenuPath ) - - self:RemoveSubMenus() - missionCommands.removeItemForCoalition( self.Coalition, self.MenuPath ) - if self.ParentMenu then - self.ParentMenu.Menus[self.MenuPath] = nil - end - - return nil - end - -end - -do -- MENU_COALITION_COMMAND - - --- The MENU_COALITION_COMMAND class - -- @type MENU_COALITION_COMMAND - -- @extends Menu#MENU_COMMAND_BASE - MENU_COALITION_COMMAND = { - ClassName = "MENU_COALITION_COMMAND" - } - - --- MENU_COALITION constructor. Creates a new radio command item for a coalition, which can invoke a function with parameters. - -- @param #MENU_COALITION_COMMAND self - -- @param Dcs.DCSCoalition#coalition.side Coalition The coalition owning the menu. - -- @param #string MenuText The text for the menu. - -- @param Menu#MENU_COALITION ParentMenu The parent menu. - -- @param CommandMenuFunction A function that is called when the menu key is pressed. - -- @param CommandMenuArgument An argument for the function. There can only be ONE argument given. So multiple arguments must be wrapped into a table. See the below example how to do this. - -- @return #MENU_COALITION_COMMAND self - function MENU_COALITION_COMMAND:New( Coalition, MenuText, ParentMenu, CommandMenuFunction, ... ) - - local self = BASE:Inherit( self, MENU_COMMAND_BASE:New( MenuText, ParentMenu, CommandMenuFunction, arg ) ) - - self.MenuCoalition = Coalition - self.MenuText = MenuText - self.ParentMenu = ParentMenu - - self:T( { MenuText, CommandMenuFunction, arg } ) - - - self.MenuPath = missionCommands.addCommandForCoalition( self.MenuCoalition, MenuText, self.MenuParentPath, self.MenuCallHandler, arg ) - - ParentMenu.Menus[self.MenuPath] = self - - return self - end - - --- Removes a radio command item for a coalition - -- @param #MENU_COALITION_COMMAND self - -- @return #nil - function MENU_COALITION_COMMAND:Remove() - self:F( self.MenuPath ) - - missionCommands.removeItemForCoalition( self.MenuCoalition, self.MenuPath ) - if self.ParentMenu then - self.ParentMenu.Menus[self.MenuPath] = nil - end - return nil - end - -end - -do -- MENU_CLIENT - - -- 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 = {} - - --- MENU_COALITION constructor. Creates a new radio command item for a coalition, which can invoke a function with parameters. - -- @type MENU_CLIENT - -- @extends Menu#MENU_BASE - -- @usage - -- -- This demo creates a menu structure for the two clients of planes. - -- -- Each client will receive a different menu structure. - -- -- To test, join the planes, then look at the other radio menus (Option F10). - -- -- Then switch planes and check if the menu is still there. - -- -- And play with the Add and Remove menu options. - -- - -- -- Note that in multi player, this will only work after the DCS clients bug is solved. - -- - -- local function ShowStatus( PlaneClient, StatusText, Coalition ) - -- - -- MESSAGE:New( Coalition, 15 ):ToRed() - -- PlaneClient:Message( StatusText, 15 ) - -- end - -- - -- local MenuStatus = {} - -- - -- local function RemoveStatusMenu( MenuClient ) - -- local MenuClientName = MenuClient:GetName() - -- MenuStatus[MenuClientName]:Remove() - -- end - -- - -- --- @param Wrapper.Client#CLIENT MenuClient - -- local function AddStatusMenu( MenuClient ) - -- local MenuClientName = MenuClient:GetName() - -- -- This would create a menu for the red coalition under the MenuCoalitionRed menu object. - -- MenuStatus[MenuClientName] = MENU_CLIENT:New( MenuClient, "Status for Planes" ) - -- MENU_CLIENT_COMMAND:New( MenuClient, "Show Status", MenuStatus[MenuClientName], ShowStatus, MenuClient, "Status of planes is ok!", "Message to Red Coalition" ) - -- end - -- - -- SCHEDULER:New( nil, - -- function() - -- local PlaneClient = CLIENT:FindByName( "Plane 1" ) - -- if PlaneClient and PlaneClient:IsAlive() then - -- local MenuManage = MENU_CLIENT:New( PlaneClient, "Manage Menus" ) - -- MENU_CLIENT_COMMAND:New( PlaneClient, "Add Status Menu Plane 1", MenuManage, AddStatusMenu, PlaneClient ) - -- MENU_CLIENT_COMMAND:New( PlaneClient, "Remove Status Menu Plane 1", MenuManage, RemoveStatusMenu, PlaneClient ) - -- end - -- end, {}, 10, 10 ) - -- - -- SCHEDULER:New( nil, - -- function() - -- local PlaneClient = CLIENT:FindByName( "Plane 2" ) - -- if PlaneClient and PlaneClient:IsAlive() then - -- local MenuManage = MENU_CLIENT:New( PlaneClient, "Manage Menus" ) - -- MENU_CLIENT_COMMAND:New( PlaneClient, "Add Status Menu Plane 2", MenuManage, AddStatusMenu, PlaneClient ) - -- MENU_CLIENT_COMMAND:New( PlaneClient, "Remove Status Menu Plane 2", MenuManage, RemoveStatusMenu, PlaneClient ) - -- end - -- end, {}, 10, 10 ) - MENU_CLIENT = { - ClassName = "MENU_CLIENT" - } - - --- MENU_CLIENT constructor. Creates a new radio menu item for a client. - -- @param #MENU_CLIENT self - -- @param Wrapper.Client#CLIENT Client 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( Client, MenuText, ParentMenu ) - - -- Arrange meta tables - local MenuParentPath = {} - if ParentMenu ~= nil then - MenuParentPath = ParentMenu.MenuPath - end - - local self = BASE:Inherit( self, MENU_BASE:New( MenuText, MenuParentPath ) ) - self:F( { Client, MenuText, ParentMenu } ) - - self.MenuClient = Client - self.MenuClientGroupID = Client: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( { Client: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( { Client: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 #nil - 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_COMMAND - MENU_CLIENT_COMMAND = { - ClassName = "MENU_CLIENT_COMMAND" - } - - --- MENU_CLIENT_COMMAND constructor. Creates a new radio command item for a client, which can invoke a function with parameters. - -- @param #MENU_CLIENT_COMMAND self - -- @param Wrapper.Client#CLIENT Client The Client owning the menu. - -- @param #string MenuText The text for the menu. - -- @param #MENU_BASE ParentMenu The parent menu. - -- @param CommandMenuFunction A function that is called when the menu key is pressed. - -- @return Menu#MENU_CLIENT_COMMAND self - function MENU_CLIENT_COMMAND:New( Client, MenuText, ParentMenu, CommandMenuFunction, ... ) - - -- Arrange meta tables - - local MenuParentPath = {} - if ParentMenu ~= nil then - MenuParentPath = ParentMenu.MenuPath - end - - local self = BASE:Inherit( self, MENU_COMMAND_BASE:New( MenuText, MenuParentPath, CommandMenuFunction, arg ) ) -- Menu#MENU_CLIENT_COMMAND - - self.MenuClient = Client - self.MenuClientGroupID = Client: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( { Client:GetClientGroupName(), MenuPath[table.concat(MenuParentPath)], MenuParentPath, MenuText, CommandMenuFunction, arg } ) - - 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, self.MenuCallHandler, arg ) - MenuPath[MenuPathID] = self.MenuPath - - if ParentMenu and ParentMenu.Menus then - ParentMenu.Menus[self.MenuPath] = self - end - - return self - end - - --- Removes a menu structure for a client. - -- @param #MENU_CLIENT_COMMAND self - -- @return #nil - 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 -end - ---- MENU_GROUP - -do - -- This local variable is used to cache the menus registered under groups. - -- Menus don't dissapear when groups for players are destroyed and restarted. - -- So every menu for a client created must be tracked so that program logic accidentally does not create. - -- the same menus twice during initialization logic. - -- These menu classes are handling this logic with this variable. - local _MENUGROUPS = {} - - --- The MENU_GROUP class - -- @type MENU_GROUP - -- @extends Menu#MENU_BASE - -- @usage - -- -- This demo creates a menu structure for the two groups of planes. - -- -- Each group will receive a different menu structure. - -- -- To test, join the planes, then look at the other radio menus (Option F10). - -- -- Then switch planes and check if the menu is still there. - -- -- And play with the Add and Remove menu options. - -- - -- -- Note that in multi player, this will only work after the DCS groups bug is solved. - -- - -- local function ShowStatus( PlaneGroup, StatusText, Coalition ) - -- - -- MESSAGE:New( Coalition, 15 ):ToRed() - -- PlaneGroup:Message( StatusText, 15 ) - -- end - -- - -- local MenuStatus = {} - -- - -- local function RemoveStatusMenu( MenuGroup ) - -- local MenuGroupName = MenuGroup:GetName() - -- MenuStatus[MenuGroupName]:Remove() - -- end - -- - -- --- @param Wrapper.Group#GROUP MenuGroup - -- local function AddStatusMenu( MenuGroup ) - -- local MenuGroupName = MenuGroup:GetName() - -- -- This would create a menu for the red coalition under the MenuCoalitionRed menu object. - -- MenuStatus[MenuGroupName] = MENU_GROUP:New( MenuGroup, "Status for Planes" ) - -- MENU_GROUP_COMMAND:New( MenuGroup, "Show Status", MenuStatus[MenuGroupName], ShowStatus, MenuGroup, "Status of planes is ok!", "Message to Red Coalition" ) - -- end - -- - -- SCHEDULER:New( nil, - -- function() - -- local PlaneGroup = GROUP:FindByName( "Plane 1" ) - -- if PlaneGroup and PlaneGroup:IsAlive() then - -- local MenuManage = MENU_GROUP:New( PlaneGroup, "Manage Menus" ) - -- MENU_GROUP_COMMAND:New( PlaneGroup, "Add Status Menu Plane 1", MenuManage, AddStatusMenu, PlaneGroup ) - -- MENU_GROUP_COMMAND:New( PlaneGroup, "Remove Status Menu Plane 1", MenuManage, RemoveStatusMenu, PlaneGroup ) - -- end - -- end, {}, 10, 10 ) - -- - -- SCHEDULER:New( nil, - -- function() - -- local PlaneGroup = GROUP:FindByName( "Plane 2" ) - -- if PlaneGroup and PlaneGroup:IsAlive() then - -- local MenuManage = MENU_GROUP:New( PlaneGroup, "Manage Menus" ) - -- MENU_GROUP_COMMAND:New( PlaneGroup, "Add Status Menu Plane 2", MenuManage, AddStatusMenu, PlaneGroup ) - -- MENU_GROUP_COMMAND:New( PlaneGroup, "Remove Status Menu Plane 2", MenuManage, RemoveStatusMenu, PlaneGroup ) - -- end - -- end, {}, 10, 10 ) - -- - MENU_GROUP = { - ClassName = "MENU_GROUP" - } - - --- MENU_GROUP constructor. Creates a new radio menu item for a group. - -- @param #MENU_GROUP self - -- @param Wrapper.Group#GROUP MenuGroup The Group owning the menu. - -- @param #string MenuText The text for the menu. - -- @param #table ParentMenu The parent menu. - -- @return #MENU_GROUP self - function MENU_GROUP:New( MenuGroup, MenuText, ParentMenu ) - - -- Determine if the menu was not already created and already visible at the group. - -- If it is visible, then return the cached self, otherwise, create self and cache it. - - MenuGroup._Menus = MenuGroup._Menus or {} - local Path = ( ParentMenu and ( table.concat( ParentMenu.MenuPath or {}, "@" ) .. "@" .. MenuText ) ) or MenuText - if MenuGroup._Menus[Path] then - self = MenuGroup._Menus[Path] - else - self = BASE:Inherit( self, MENU_BASE:New( MenuText, ParentMenu ) ) - MenuGroup._Menus[Path] = self - - self.Menus = {} - - self.MenuGroup = MenuGroup - self.Path = Path - self.MenuGroupID = MenuGroup:GetID() - self.MenuText = MenuText - self.ParentMenu = ParentMenu - - self:T( { "Adding Menu ", MenuText, self.MenuParentPath } ) - self.MenuPath = missionCommands.addSubMenuForGroup( self.MenuGroupID, MenuText, self.MenuParentPath ) - - if ParentMenu and ParentMenu.Menus then - ParentMenu.Menus[self.MenuPath] = self - end - end - - --self:F( { MenuGroup:GetName(), MenuText, ParentMenu.MenuPath } ) - - return self - end - - --- Removes the sub menus recursively of this MENU_GROUP. - -- @param #MENU_GROUP self - -- @return #MENU_GROUP self - function MENU_GROUP:RemoveSubMenus() - self:F( self.MenuPath ) - - for MenuID, Menu in pairs( self.Menus ) do - Menu:Remove() - end - - end - - --- Removes the main menu and sub menus recursively of this MENU_GROUP. - -- @param #MENU_GROUP self - -- @return #nil - function MENU_GROUP:Remove() - self:F( { self.MenuGroupID, self.MenuPath } ) - - self:RemoveSubMenus() - - if self.MenuGroup._Menus[self.Path] then - self = self.MenuGroup._Menus[self.Path] - - missionCommands.removeItemForGroup( self.MenuGroupID, self.MenuPath ) - if self.ParentMenu then - self.ParentMenu.Menus[self.MenuPath] = nil - end - self:E( self.MenuGroup._Menus[self.Path] ) - self.MenuGroup._Menus[self.Path] = nil - self = nil - end - return nil - end - - - --- The MENU_GROUP_COMMAND class - -- @type MENU_GROUP_COMMAND - -- @extends Menu#MENU_BASE - MENU_GROUP_COMMAND = { - ClassName = "MENU_GROUP_COMMAND" - } - - --- Creates a new radio command item for a group - -- @param #MENU_GROUP_COMMAND self - -- @param Wrapper.Group#GROUP MenuGroup The Group owning the menu. - -- @param MenuText The text for the menu. - -- @param ParentMenu The parent menu. - -- @param CommandMenuFunction A function that is called when the menu key is pressed. - -- @param CommandMenuArgument An argument for the function. - -- @return Menu#MENU_GROUP_COMMAND self - function MENU_GROUP_COMMAND:New( MenuGroup, MenuText, ParentMenu, CommandMenuFunction, ... ) - - MenuGroup._Menus = MenuGroup._Menus or {} - local Path = ( ParentMenu and ( table.concat( ParentMenu.MenuPath or {}, "@" ) .. "@" .. MenuText ) ) or MenuText - if MenuGroup._Menus[Path] then - self = MenuGroup._Menus[Path] - else - self = BASE:Inherit( self, MENU_COMMAND_BASE:New( MenuText, ParentMenu, CommandMenuFunction, arg ) ) - MenuGroup._Menus[Path] = self - - self.Path = Path - self.MenuGroup = MenuGroup - self.MenuGroupID = MenuGroup:GetID() - self.MenuText = MenuText - self.ParentMenu = ParentMenu - - self:T( { "Adding Command Menu ", MenuText, self.MenuParentPath } ) - self.MenuPath = missionCommands.addCommandForGroup( self.MenuGroupID, MenuText, self.MenuParentPath, self.MenuCallHandler, arg ) - - if ParentMenu and ParentMenu.Menus then - ParentMenu.Menus[self.MenuPath] = self - end - end - - --self:F( { MenuGroup:GetName(), MenuText, ParentMenu.MenuPath } ) - - return self - end - - --- Removes a menu structure for a group. - -- @param #MENU_GROUP_COMMAND self - -- @return #nil - function MENU_GROUP_COMMAND:Remove() - self:F( { self.MenuGroupID, self.MenuPath } ) - - if self.MenuGroup._Menus[self.Path] then - self = self.MenuGroup._Menus[self.Path] - - missionCommands.removeItemForGroup( self.MenuGroupID, self.MenuPath ) - self.ParentMenu.Menus[self.MenuPath] = nil - self:E( self.MenuGroup._Menus[self.Path] ) - self.MenuGroup._Menus[self.Path] = nil - self = nil - end - - return nil - end - -end - ---- This core 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 defines by a zone around a @{Unit#UNIT} with a radius. --- * @{Zone#ZONE_GROUP}: The ZONE_GROUP class defines by a zone around a @{Group#GROUP} with a radius. --- * @{Zone#ZONE_POLYGON}: The ZONE_POLYGON class defines by a sequence of @{Group#GROUP} waypoints within the Mission Editor, forming a polygon. --- --- === --- --- # 1) @{Zone#ZONE_BASE} class, extends @{Base#BASE} --- --- This class is an abstract BASE class for derived classes, and is not meant to be instantiated. --- --- ## 1.1) Each zone has a name: --- --- * @{#ZONE_BASE.GetName}(): Returns the name of the zone. --- --- ## 1.2) Each zone implements two polymorphic functions defined in @{Zone#ZONE_BASE}: --- --- * @{#ZONE_BASE.IsVec2InZone}(): Returns if a Vec2 is within the zone. --- * @{#ZONE_BASE.IsVec3InZone}(): Returns if a Vec3 is within the zone. --- --- ## 1.3) A zone has a probability factor that can be set to randomize a selection between zones: --- --- * @{#ZONE_BASE.SetRandomizeProbability}(): Set the randomization probability of a zone to be selected, taking a value between 0 and 1 ( 0 = 0%, 1 = 100% ) --- * @{#ZONE_BASE.GetRandomizeProbability}(): Get the randomization probability of a zone to be selected, passing a value between 0 and 1 ( 0 = 0%, 1 = 100% ) --- * @{#ZONE_BASE.GetZoneMaybe}(): Get the zone taking into account the randomization probability. nil is returned if this zone is not a candidate. --- --- ## 1.4) A zone manages Vectors: --- --- * @{#ZONE_BASE.GetVec2}(): Returns the @{DCSTypes#Vec2} coordinate of the zone. --- * @{#ZONE_BASE.GetRandomVec2}(): Define a random @{DCSTypes#Vec2} within the zone. --- --- ## 1.5) A zone has a bounding square: --- --- * @{#ZONE_BASE.GetBoundingSquare}(): Get the outer most bounding square of the zone. --- --- ## 1.6) A zone can be marked: --- --- * @{#ZONE_BASE.SmokeZone}(): Smokes the zone boundaries in a color. --- * @{#ZONE_BASE.FlareZone}(): Flares the zone boundaries in a color. --- --- === --- --- # 2) @{Zone#ZONE_RADIUS} class, extends @{Zone#ZONE_BASE} --- --- The ZONE_RADIUS class defined by a zone name, a location and a radius. --- This class implements the inherited functions from Core.Zone#ZONE_BASE taking into account the own zone format and properties. --- --- ## 2.1) @{Zone#ZONE_RADIUS} constructor --- --- * @{#ZONE_RADIUS.New}(): Constructor. --- --- ## 2.2) Manage the radius of the zone --- --- * @{#ZONE_RADIUS.SetRadius}(): Sets the radius of the zone. --- * @{#ZONE_RADIUS.GetRadius}(): Returns the radius of the zone. --- --- ## 2.3) Manage the location of the zone --- --- * @{#ZONE_RADIUS.SetVec2}(): Sets the @{DCSTypes#Vec2} of the zone. --- * @{#ZONE_RADIUS.GetVec2}(): Returns the @{DCSTypes#Vec2} of the zone. --- * @{#ZONE_RADIUS.GetVec3}(): Returns the @{DCSTypes#Vec3} of the zone, taking an additional height parameter. --- --- ## 2.4) Zone point randomization --- --- Various functions exist to find random points within the zone. --- --- * @{#ZONE_RADIUS.GetRandomVec2}(): Gets a random 2D point in the zone. --- * @{#ZONE_RADIUS.GetRandomPointVec2}(): Gets a @{Point#POINT_VEC2} object representing a random 2D point in the zone. --- * @{#ZONE_RADIUS.GetRandomPointVec3}(): Gets a @{Point#POINT_VEC3} object representing a random 3D point in the zone. Note that the height of the point is at landheight. --- --- === --- --- # 3) @{Zone#ZONE} class, extends @{Zone#ZONE_RADIUS} --- --- The ZONE class, defined by the zone name as defined within the Mission Editor. --- This class implements the inherited functions from {Core.Zone#ZONE_RADIUS} taking into account the own zone format and properties. --- --- === --- --- # 4) @{Zone#ZONE_UNIT} class, extends @{Zone#ZONE_RADIUS} --- --- The ZONE_UNIT class defined by a zone around a @{Unit#UNIT} with a radius. --- This class implements the inherited functions from @{Zone#ZONE_RADIUS} taking into account the own zone format and properties. --- --- === --- --- # 5) @{Zone#ZONE_GROUP} class, extends @{Zone#ZONE_RADIUS} --- --- The ZONE_GROUP class defines by a zone around a @{Group#GROUP} with a radius. The current leader of the group defines the center of the zone. --- This class implements the inherited functions from @{Zone#ZONE_RADIUS} taking into account the own zone format and properties. --- --- === --- --- # 6) @{Zone#ZONE_POLYGON_BASE} class, extends @{Zone#ZONE_BASE} --- --- The ZONE_POLYGON_BASE class defined by a sequence of @{Group#GROUP} waypoints within the Mission Editor, forming a polygon. --- This class implements the inherited functions from @{Zone#ZONE_RADIUS} taking into account the own zone format and properties. --- This class is an abstract BASE class for derived classes, and is not meant to be instantiated. --- --- ## 6.1) Zone point randomization --- --- Various functions exist to find random points within the zone. --- --- * @{#ZONE_POLYGON_BASE.GetRandomVec2}(): Gets a random 2D point in the zone. --- * @{#ZONE_POLYGON_BASE.GetRandomPointVec2}(): Return a @{Point#POINT_VEC2} object representing a random 2D point within the zone. --- * @{#ZONE_POLYGON_BASE.GetRandomPointVec3}(): Return a @{Point#POINT_VEC3} object representing a random 3D point at landheight within the zone. --- --- --- === --- --- # 7) @{Zone#ZONE_POLYGON} class, extends @{Zone#ZONE_POLYGON_BASE} --- --- The ZONE_POLYGON class defined by a sequence of @{Group#GROUP} waypoints within the Mission Editor, forming a polygon. --- This class implements the inherited functions from @{Zone#ZONE_RADIUS} taking into account the own zone format and properties. --- --- ==== --- --- **API CHANGE HISTORY** --- ====================== --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- Hereby the change log: --- --- 2017-02-28: ZONE\_BASE:**IsVec2InZone()** replaces ZONE\_BASE:_IsPointVec2InZone()_. --- 2017-02-28: ZONE\_BASE:**IsVec3InZone()** replaces ZONE\_BASE:_IsPointVec3InZone()_. --- 2017-02-28: ZONE\_RADIUS:**IsVec2InZone()** replaces ZONE\_RADIUS:_IsPointVec2InZone()_. --- 2017-02-28: ZONE\_RADIUS:**IsVec3InZone()** replaces ZONE\_RADIUS:_IsPointVec3InZone()_. --- 2017-02-28: ZONE\_POLYGON:**IsVec2InZone()** replaces ZONE\_POLYGON:_IsPointVec2InZone()_. --- 2017-02-28: ZONE\_POLYGON:**IsVec3InZone()** replaces ZONE\_POLYGON:_IsPointVec3InZone()_. --- --- 2017-02-18: ZONE\_POLYGON_BASE:**GetRandomPointVec2()** added. --- --- 2017-02-18: ZONE\_POLYGON_BASE:**GetRandomPointVec3()** added. --- --- 2017-02-18: ZONE\_RADIUS:**GetRandomPointVec3( inner, outer )** added. --- --- 2017-02-18: ZONE\_RADIUS:**GetRandomPointVec2( inner, outer )** added. --- --- 2016-08-15: ZONE\_BASE:**GetName()** added. --- --- 2016-08-15: ZONE\_BASE:**SetZoneProbability( ZoneProbability )** added. --- --- 2016-08-15: ZONE\_BASE:**GetZoneProbability()** added. --- --- 2016-08-15: ZONE\_BASE:**GetZoneMaybe()** added. --- --- === --- --- @module Zone - - ---- The ZONE_BASE class --- @type ZONE_BASE --- @field #string ZoneName Name of the zone. --- @field #number ZoneProbability A value between 0 and 1. 0 = 0% and 1 = 100% probability. --- @extends Core.Base#BASE -ZONE_BASE = { - ClassName = "ZONE_BASE", - ZoneName = "", - ZoneProbability = 1, - } - - ---- The ZONE_BASE.BoundingSquare --- @type ZONE_BASE.BoundingSquare --- @field Dcs.DCSTypes#Distance x1 The lower x coordinate (left down) --- @field Dcs.DCSTypes#Distance y1 The lower y coordinate (left down) --- @field Dcs.DCSTypes#Distance x2 The higher x coordinate (right up) --- @field Dcs.DCSTypes#Distance y2 The higher y coordinate (right up) - - ---- 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 the name of the zone. --- @param #ZONE_BASE self --- @return #string The name of the zone. -function ZONE_BASE:GetName() - self:F2() - - return self.ZoneName -end ---- Returns if a location is within the zone. --- @param #ZONE_BASE self --- @param Dcs.DCSTypes#Vec2 Vec2 The location to test. --- @return #boolean true if the location is within the zone. -function ZONE_BASE:IsVec2InZone( Vec2 ) - self:F2( Vec2 ) - - return false -end - ---- Returns if a point is within the zone. --- @param #ZONE_BASE self --- @param Dcs.DCSTypes#Vec3 Vec3 The point to test. --- @return #boolean true if the point is within the zone. -function ZONE_BASE:IsVec3InZone( Vec3 ) - self:F2( Vec3 ) - - local InZone = self:IsVec2InZone( { x = Vec3.x, y = Vec3.z } ) - - return InZone -end - ---- Returns the @{DCSTypes#Vec2} coordinate of the zone. --- @param #ZONE_BASE self --- @return #nil. -function ZONE_BASE:GetVec2() - self:F2( self.ZoneName ) - - return nil -end - ---- Define a random @{DCSTypes#Vec2} within the zone. --- @param #ZONE_BASE self --- @return Dcs.DCSTypes#Vec2 The Vec2 coordinates. -function ZONE_BASE:GetRandomVec2() - return nil -end - ---- Define a random @{Point#POINT_VEC2} within the zone. --- @param #ZONE_BASE self --- @return Core.Point#POINT_VEC2 The PointVec2 coordinates. -function ZONE_BASE:GetRandomPointVec2() - return nil -end - ---- Get the bounding square the zone. --- @param #ZONE_BASE self --- @return #nil The bounding square. -function ZONE_BASE:GetBoundingSquare() - --return { x1 = 0, y1 = 0, x2 = 0, y2 = 0 } - return nil -end - ---- Bound the zone boundaries with a tires. --- @param #ZONE_BASE self -function ZONE_BASE:BoundZone() - self:F2() - -end - ---- Smokes the zone boundaries in a color. --- @param #ZONE_BASE self --- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color. -function ZONE_BASE:SmokeZone( SmokeColor ) - self:F2( SmokeColor ) - -end - ---- Set the randomization probability of a zone to be selected. --- @param #ZONE_BASE self --- @param ZoneProbability A value between 0 and 1. 0 = 0% and 1 = 100% probability. -function ZONE_BASE:SetZoneProbability( ZoneProbability ) - self:F2( ZoneProbability ) - - self.ZoneProbability = ZoneProbability or 1 - return self -end - ---- Get the randomization probability of a zone to be selected. --- @param #ZONE_BASE self --- @return #number A value between 0 and 1. 0 = 0% and 1 = 100% probability. -function ZONE_BASE:GetZoneProbability() - self:F2() - - return self.ZoneProbability -end - ---- Get the zone taking into account the randomization probability of a zone to be selected. --- @param #ZONE_BASE self --- @return #ZONE_BASE The zone is selected taking into account the randomization probability factor. --- @return #nil The zone is not selected taking into account the randomization probability factor. -function ZONE_BASE:GetZoneMaybe() - self:F2() - - local Randomization = math.random() - if Randomization <= self.ZoneProbability then - return self - else - return nil - end -end - - ---- The ZONE_RADIUS class, defined by a zone name, a location and a radius. --- @type ZONE_RADIUS --- @field Dcs.DCSTypes#Vec2 Vec2 The current location of the zone. --- @field Dcs.DCSTypes#Distance Radius The radius of the zone. --- @extends Core.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 Dcs.DCSTypes#Vec2 Vec2 The location of the zone. --- @param Dcs.DCSTypes#Distance Radius The radius of the zone. --- @return #ZONE_RADIUS self -function ZONE_RADIUS:New( ZoneName, Vec2, Radius ) - local self = BASE:Inherit( self, ZONE_BASE:New( ZoneName ) ) -- #ZONE_RADIUS - self:F( { ZoneName, Vec2, Radius } ) - - self.Radius = Radius - self.Vec2 = Vec2 - - return self -end - ---- Bounds the zone with tires. --- @param #ZONE_RADIUS self --- @param #number Points (optional) The amount of points in the circle. --- @return #ZONE_RADIUS self -function ZONE_RADIUS:BoundZone( Points ) - - local Point = {} - local Vec2 = self:GetVec2() - - Points = Points and Points or 360 - - local Angle - local RadialBase = math.pi*2 - - -- - for Angle = 0, 360, (360 / Points ) do - local Radial = Angle * RadialBase / 360 - Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() - Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() - - local Tire = { - ["country"] = "USA", - ["category"] = "Fortifications", - ["canCargo"] = false, - ["shape_name"] = "H-tyre_B_WF", - ["type"] = "Black_Tyre_WF", - --["unitId"] = Angle + 10000, - ["y"] = Point.y, - ["x"] = Point.x, - ["name"] = string.format( "%s-Tire #%0d", self:GetName(), Angle ), - ["heading"] = 0, - } -- end of ["group"] - - coalition.addStaticObject( country.id.USA, Tire ) - end - - return self -end - - ---- Smokes the zone boundaries in a color. --- @param #ZONE_RADIUS self --- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color. --- @param #number Points (optional) The amount of points in the circle. --- @return #ZONE_RADIUS self -function ZONE_RADIUS:SmokeZone( SmokeColor, Points ) - self:F2( SmokeColor ) - - local Point = {} - local Vec2 = self:GetVec2() - - Points = Points and Points or 360 - - local Angle - local RadialBase = math.pi*2 - - for Angle = 0, 360, 360 / Points do - local Radial = Angle * RadialBase / 360 - Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() - Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() - 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 Utilities.Utils#FLARECOLOR FlareColor The flare color. --- @param #number Points (optional) The amount of points in the circle. --- @param Dcs.DCSTypes#Azimuth Azimuth (optional) Azimuth The azimuth of the flare. --- @return #ZONE_RADIUS self -function ZONE_RADIUS:FlareZone( FlareColor, Points, Azimuth ) - self:F2( { FlareColor, Azimuth } ) - - local Point = {} - local Vec2 = self:GetVec2() - - Points = Points and Points or 360 - - local Angle - local RadialBase = math.pi*2 - - for Angle = 0, 360, 360 / Points do - local Radial = Angle * RadialBase / 360 - Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() - Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() - 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 Dcs.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 Dcs.DCSTypes#Distance Radius The radius of the zone. --- @return Dcs.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 @{DCSTypes#Vec2} of the zone. --- @param #ZONE_RADIUS self --- @return Dcs.DCSTypes#Vec2 The location of the zone. -function ZONE_RADIUS:GetVec2() - self:F2( self.ZoneName ) - - self:T2( { self.Vec2 } ) - - return self.Vec2 -end - ---- Sets the @{DCSTypes#Vec2} of the zone. --- @param #ZONE_RADIUS self --- @param Dcs.DCSTypes#Vec2 Vec2 The new location of the zone. --- @return Dcs.DCSTypes#Vec2 The new location of the zone. -function ZONE_RADIUS:SetVec2( Vec2 ) - self:F2( self.ZoneName ) - - self.Vec2 = Vec2 - - self:T2( { self.Vec2 } ) - - return self.Vec2 -end - ---- Returns the @{DCSTypes#Vec3} of the ZONE_RADIUS. --- @param #ZONE_RADIUS self --- @param Dcs.DCSTypes#Distance Height The height to add to the land height where the center of the zone is located. --- @return Dcs.DCSTypes#Vec3 The point of the zone. -function ZONE_RADIUS:GetVec3( Height ) - self:F2( { self.ZoneName, Height } ) - - Height = Height or 0 - local Vec2 = self:GetVec2() - - local Vec3 = { x = Vec2.x, y = land.getHeight( self:GetVec2() ) + Height, z = Vec2.y } - - self:T2( { Vec3 } ) - - return Vec3 -end - - ---- Returns if a location is within the zone. --- @param #ZONE_RADIUS self --- @param Dcs.DCSTypes#Vec2 Vec2 The location to test. --- @return #boolean true if the location is within the zone. -function ZONE_RADIUS:IsVec2InZone( Vec2 ) - self:F2( Vec2 ) - - local ZoneVec2 = self:GetVec2() - - if ZoneVec2 then - if (( Vec2.x - ZoneVec2.x )^2 + ( Vec2.y - ZoneVec2.y ) ^2 ) ^ 0.5 <= self:GetRadius() then - return true - end - end - - return false -end - ---- Returns if a point is within the zone. --- @param #ZONE_RADIUS self --- @param Dcs.DCSTypes#Vec3 Vec3 The point to test. --- @return #boolean true if the point is within the zone. -function ZONE_RADIUS:IsVec3InZone( Vec3 ) - self:F2( Vec3 ) - - local InZone = self:IsVec2InZone( { x = Vec3.x, y = Vec3.z } ) - - return InZone -end - ---- Returns a random Vec2 location within the zone. --- @param #ZONE_RADIUS self --- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. --- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. --- @return Dcs.DCSTypes#Vec2 The random location within the zone. -function ZONE_RADIUS:GetRandomVec2( inner, outer ) - self:F( self.ZoneName, inner, outer ) - - local Point = {} - local Vec2 = self:GetVec2() - local _inner = inner or 0 - local _outer = outer or self:GetRadius() - - local angle = math.random() * math.pi * 2; - Point.x = Vec2.x + math.cos( angle ) * math.random(_inner, _outer); - Point.y = Vec2.y + math.sin( angle ) * math.random(_inner, _outer); - - self:T( { Point } ) - - return Point -end - ---- Returns a @{Point#POINT_VEC2} object reflecting a random 2D location within the zone. --- @param #ZONE_RADIUS self --- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. --- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. --- @return Core.Point#POINT_VEC2 The @{Point#POINT_VEC2} object reflecting the random 3D location within the zone. -function ZONE_RADIUS:GetRandomPointVec2( inner, outer ) - self:F( self.ZoneName, inner, outer ) - - local PointVec2 = POINT_VEC2:NewFromVec2( self:GetRandomVec2() ) - - self:T3( { PointVec2 } ) - - return PointVec2 -end - ---- Returns a @{Point#POINT_VEC3} object reflecting a random 3D location within the zone. --- @param #ZONE_RADIUS self --- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. --- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. --- @return Core.Point#POINT_VEC3 The @{Point#POINT_VEC3} object reflecting the random 3D location within the zone. -function ZONE_RADIUS:GetRandomPointVec3( inner, outer ) - self:F( self.ZoneName, inner, outer ) - - local PointVec3 = POINT_VEC3:NewFromVec2( self:GetRandomVec2() ) - - self:T3( { PointVec3 } ) - - return PointVec3 -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 Core.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 Wrapper.Unit#UNIT ZoneUNIT --- @extends Core.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 Wrapper.Unit#UNIT ZoneUNIT The unit as the center of the zone. --- @param Dcs.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:GetVec2(), Radius ) ) - self:F( { ZoneName, ZoneUNIT:GetVec2(), Radius } ) - - self.ZoneUNIT = ZoneUNIT - self.LastVec2 = ZoneUNIT:GetVec2() - - return self -end - - ---- Returns the current location of the @{Unit#UNIT}. --- @param #ZONE_UNIT self --- @return Dcs.DCSTypes#Vec2 The location of the zone based on the @{Unit#UNIT}location. -function ZONE_UNIT:GetVec2() - self:F( self.ZoneName ) - - local ZoneVec2 = self.ZoneUNIT:GetVec2() - if ZoneVec2 then - self.LastVec2 = ZoneVec2 - return ZoneVec2 - else - return self.LastVec2 - end - - self:T( { ZoneVec2 } ) - - return nil -end - ---- Returns a random location within the zone. --- @param #ZONE_UNIT self --- @return Dcs.DCSTypes#Vec2 The random location within the zone. -function ZONE_UNIT:GetRandomVec2() - self:F( self.ZoneName ) - - local RandomVec2 = {} - local Vec2 = self.ZoneUNIT:GetVec2() - - if not Vec2 then - Vec2 = self.LastVec2 - end - - local angle = math.random() * math.pi*2; - RandomVec2.x = Vec2.x + math.cos( angle ) * math.random() * self:GetRadius(); - RandomVec2.y = Vec2.y + math.sin( angle ) * math.random() * self:GetRadius(); - - self:T( { RandomVec2 } ) - - return RandomVec2 -end - ---- Returns the @{DCSTypes#Vec3} of the ZONE_UNIT. --- @param #ZONE_UNIT self --- @param Dcs.DCSTypes#Distance Height The height to add to the land height where the center of the zone is located. --- @return Dcs.DCSTypes#Vec3 The point of the zone. -function ZONE_UNIT:GetVec3( Height ) - self:F2( self.ZoneName ) - - Height = Height or 0 - - local Vec2 = self:GetVec2() - - local Vec3 = { x = Vec2.x, y = land.getHeight( self:GetVec2() ) + Height, z = Vec2.y } - - self:T2( { Vec3 } ) - - return Vec3 -end - ---- The ZONE_GROUP class defined by a zone around a @{Group}, taking the average center point of all the units within the Group, with a radius. --- @type ZONE_GROUP --- @field Wrapper.Group#GROUP ZoneGROUP --- @extends Core.Zone#ZONE_RADIUS -ZONE_GROUP = { - ClassName="ZONE_GROUP", - } - ---- Constructor to create a ZONE_GROUP instance, taking the zone name, a zone @{Group#GROUP} and a radius. --- @param #ZONE_GROUP self --- @param #string ZoneName Name of the zone. --- @param Wrapper.Group#GROUP ZoneGROUP The @{Group} as the center of the zone. --- @param Dcs.DCSTypes#Distance Radius The radius of the zone. --- @return #ZONE_GROUP self -function ZONE_GROUP:New( ZoneName, ZoneGROUP, Radius ) - local self = BASE:Inherit( self, ZONE_RADIUS:New( ZoneName, ZoneGROUP:GetVec2(), Radius ) ) - self:F( { ZoneName, ZoneGROUP:GetVec2(), Radius } ) - - self.ZoneGROUP = ZoneGROUP - - return self -end - - ---- Returns the current location of the @{Group}. --- @param #ZONE_GROUP self --- @return Dcs.DCSTypes#Vec2 The location of the zone based on the @{Group} location. -function ZONE_GROUP:GetVec2() - self:F( self.ZoneName ) - - local ZoneVec2 = self.ZoneGROUP:GetVec2() - - self:T( { ZoneVec2 } ) - - return ZoneVec2 -end - ---- Returns a random location within the zone of the @{Group}. --- @param #ZONE_GROUP self --- @return Dcs.DCSTypes#Vec2 The random location of the zone based on the @{Group} location. -function ZONE_GROUP:GetRandomVec2() - self:F( self.ZoneName ) - - local Point = {} - local Vec2 = self.ZoneGROUP:GetVec2() - - local angle = math.random() * math.pi*2; - Point.x = Vec2.x + math.cos( angle ) * math.random() * self:GetRadius(); - Point.y = Vec2.y + math.sin( angle ) * math.random() * self:GetRadius(); - - self:T( { Point } ) - - return Point -end - - - --- 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 Core.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 --- @return #ZONE_POLYGON_BASE self -function ZONE_POLYGON_BASE:BoundZone( ) - - local i - local j - local Segments = 10 - - i = 1 - j = #self.Polygon - - while i <= #self.Polygon do - self:T( { i, j, self.Polygon[i], self.Polygon[j] } ) - - local DeltaX = self.Polygon[j].x - self.Polygon[i].x - local DeltaY = self.Polygon[j].y - self.Polygon[i].y - - for Segment = 0, Segments do -- We divide each line in 5 segments and smoke a point on the line. - local PointX = self.Polygon[i].x + ( Segment * DeltaX / Segments ) - local PointY = self.Polygon[i].y + ( Segment * DeltaY / Segments ) - local Tire = { - ["country"] = "USA", - ["category"] = "Fortifications", - ["canCargo"] = false, - ["shape_name"] = "H-tyre_B_WF", - ["type"] = "Black_Tyre_WF", - ["y"] = PointY, - ["x"] = PointX, - ["name"] = string.format( "%s-Tire #%0d", self:GetName(), ((i - 1) * Segments) + Segment ), - ["heading"] = 0, - } -- end of ["group"] - - coalition.addStaticObject( country.id.USA, Tire ) - - end - j = i - i = i + 1 - end - - return self -end - - - ---- Smokes the zone boundaries in a color. --- @param #ZONE_POLYGON_BASE self --- @param Utilities.Utils#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. --- Source learned and taken from: https://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html --- @param #ZONE_POLYGON_BASE self --- @param Dcs.DCSTypes#Vec2 Vec2 The location to test. --- @return #boolean true if the location is within the zone. -function ZONE_POLYGON_BASE:IsVec2InZone( Vec2 ) - self:F2( Vec2 ) - - local Next - local Prev - local InPolygon = false - - Next = 1 - Prev = #self.Polygon - - while Next <= #self.Polygon do - self:T( { Next, Prev, self.Polygon[Next], self.Polygon[Prev] } ) - if ( ( ( self.Polygon[Next].y > Vec2.y ) ~= ( self.Polygon[Prev].y > Vec2.y ) ) and - ( Vec2.x < ( self.Polygon[Prev].x - self.Polygon[Next].x ) * ( Vec2.y - self.Polygon[Next].y ) / ( self.Polygon[Prev].y - self.Polygon[Next].y ) + self.Polygon[Next].x ) - ) then - InPolygon = not InPolygon - end - self:T2( { InPolygon = InPolygon } ) - Prev = Next - Next = Next + 1 - end - - self:T( { InPolygon = InPolygon } ) - return InPolygon -end - ---- Define a random @{DCSTypes#Vec2} within the zone. --- @param #ZONE_POLYGON_BASE self --- @return Dcs.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:IsVec2InZone( Vec2 ) then - Vec2Found = true - end - end - - self:T2( Vec2 ) - - return Vec2 -end - ---- Return a @{Point#POINT_VEC2} object representing a random 2D point at landheight within the zone. --- @param #ZONE_POLYGON_BASE self --- @return @{Point#POINT_VEC2} -function ZONE_POLYGON_BASE:GetRandomPointVec2() - self:F2() - - local PointVec2 = POINT_VEC2:NewFromVec2( self:GetRandomVec2() ) - - self:T2( PointVec2 ) - - return PointVec2 -end - ---- Return a @{Point#POINT_VEC3} object representing a random 3D point at landheight within the zone. --- @param #ZONE_POLYGON_BASE self --- @return @{Point#POINT_VEC3} -function ZONE_POLYGON_BASE:GetRandomPointVec3() - self:F2() - - local PointVec3 = POINT_VEC3:NewFromVec2( self:GetRandomVec2() ) - - self:T2( PointVec3 ) - - return PointVec3 -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 Core.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 Wrapper.Group#GROUP ZoneGroup The GROUP waypoints as defined within the Mission Editor define the polygon shape. --- @return #ZONE_POLYGON self -function ZONE_POLYGON:New( ZoneName, ZoneGroup ) - - local GroupPoints = ZoneGroup:GetTaskRoute() - - local self = BASE:Inherit( self, ZONE_POLYGON_BASE:New( ZoneName, GroupPoints ) ) - self:F( { ZoneName, ZoneGroup, self.Polygon } ) - - return self -end - ---- This module contains the DATABASE class, managing the database of mission objects. --- --- ==== --- --- 1) @{#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 Core.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() ) - - self:SetEventPriority( 1 ) - - self:HandleEvent( EVENTS.Birth, self._EventOnBirth ) - self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) - self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) - - -- Follow alive players and clients - self:HandleEvent( EVENTS.PlayerEnterUnit, self._EventOnPlayerEnterUnit ) - self:HandleEvent( EVENTS.PlayerLeaveUnit, self._EventOnPlayerLeaveUnit ) - - 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 Wrapper.Unit#UNIT The found Unit. -function DATABASE:FindUnit( UnitName ) - - local UnitFound = self.UNITS[UnitName] - return UnitFound -end - - ---- Adds a Unit based on the Unit Name in the DATABASE. --- @param #DATABASE self -function DATABASE:AddUnit( DCSUnitName ) - - if not self.UNITS[DCSUnitName] then - local UnitRegister = UNIT:Register( DCSUnitName ) - 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 Wrapper.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 Wrapper.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 Wrapper.Client#CLIENT The found CLIENT. -function DATABASE:FindClient( ClientName ) - - local ClientFound = self.CLIENTS[ClientName] - return ClientFound -end - - ---- Adds a CLIENT based on the ClientName in the DATABASE. --- @param #DATABASE self -function DATABASE:AddClient( ClientName ) - - if not self.CLIENTS[ClientName] then - self.CLIENTS[ClientName] = CLIENT:Register( ClientName ) - end - - return self.CLIENTS[ClientName] -end - - ---- Finds a GROUP based on the GroupName. --- @param #DATABASE self --- @param #string GroupName --- @return Wrapper.Group#GROUP The found GROUP. -function DATABASE:FindGroup( GroupName ) - - local GroupFound = self.GROUPS[GroupName] - return GroupFound -end - - ---- Adds a GROUP based on the GroupName in the DATABASE. --- @param #DATABASE self -function DATABASE:AddGroup( GroupName ) - - if not self.GROUPS[GroupName] then - self:E( { "Add GROUP:", GroupName } ) - self.GROUPS[GroupName] = GROUP:Register( GroupName ) - end - - return self.GROUPS[GroupName] -end - ---- Adds a player based on the Player Name in the DATABASE. --- @param #DATABASE self -function DATABASE:AddPlayer( UnitName, PlayerName ) - - if PlayerName then - self: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:F( SpawnTemplate.name ) - - self:T( { SpawnTemplate.SpawnCountryID, SpawnTemplate.SpawnCategoryID } ) - - -- Copy the spawn variables of the template in temporary storage, nullify, and restore the spawn variables. - local SpawnCoalitionID = SpawnTemplate.CoalitionID - local SpawnCountryID = SpawnTemplate.CountryID - local SpawnCategoryID = SpawnTemplate.CategoryID - - -- Nullify - SpawnTemplate.CoalitionID = nil - SpawnTemplate.CountryID = nil - SpawnTemplate.CategoryID = nil - - self:_RegisterTemplate( SpawnTemplate, SpawnCoalitionID, SpawnCategoryID, SpawnCountryID ) - - self:T3( SpawnTemplate ) - coalition.addGroup( SpawnCountryID, SpawnCategoryID, SpawnTemplate ) - - -- Restore - SpawnTemplate.CoalitionID = SpawnCoalitionID - SpawnTemplate.CountryID = SpawnCountryID - SpawnTemplate.CategoryID = 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 - - GroupTemplate.CategoryID = CategoryID - GroupTemplate.CoalitionID = CoalitionID - GroupTemplate.CountryID = CountryID - - self.Templates.Groups[GroupTemplateName].GroupName = GroupTemplateName - self.Templates.Groups[GroupTemplateName].Template = GroupTemplate - self.Templates.Groups[GroupTemplateName].groupId = GroupTemplate.groupId - self.Templates.Groups[GroupTemplateName].UnitCount = #GroupTemplate.units - self.Templates.Groups[GroupTemplateName].Units = GroupTemplate.units - self.Templates.Groups[GroupTemplateName].CategoryID = CategoryID - self.Templates.Groups[GroupTemplateName].CoalitionID = 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 - - UnitTemplate.name = env.getValueDictByKey(UnitTemplate.name) - - self.Templates.Units[UnitTemplate.name] = {} - self.Templates.Units[UnitTemplate.name].UnitName = UnitTemplate.name - self.Templates.Units[UnitTemplate.name].Template = UnitTemplate - self.Templates.Units[UnitTemplate.name].GroupName = GroupTemplateName - self.Templates.Units[UnitTemplate.name].GroupTemplate = GroupTemplate - self.Templates.Units[UnitTemplate.name].GroupId = GroupTemplate.groupId - self.Templates.Units[UnitTemplate.name].CategoryID = CategoryID - self.Templates.Units[UnitTemplate.name].CoalitionID = CoalitionID - self.Templates.Units[UnitTemplate.name].CountryID = CountryID - - if UnitTemplate.skill and (UnitTemplate.skill == "Client" or UnitTemplate.skill == "Player") then - self.Templates.ClientsByName[UnitTemplate.name] = UnitTemplate - self.Templates.ClientsByName[UnitTemplate.name].CategoryID = CategoryID - self.Templates.ClientsByName[UnitTemplate.name].CoalitionID = CoalitionID - self.Templates.ClientsByName[UnitTemplate.name].CountryID = CountryID - self.Templates.ClientsByID[UnitTemplate.unitId] = UnitTemplate - end - - TraceTable[#TraceTable+1] = self.Templates.Units[UnitTemplate.name].UnitName - end - - self:E( TraceTable ) -end - -function DATABASE:GetGroupTemplate( GroupName ) - local GroupTemplate = self.Templates.Groups[GroupName].Template - GroupTemplate.SpawnCoalitionID = self.Templates.Groups[GroupName].CoalitionID - GroupTemplate.SpawnCategoryID = self.Templates.Groups[GroupName].CategoryID - GroupTemplate.SpawnCountryID = self.Templates.Groups[GroupName].CountryID - return GroupTemplate -end - -function DATABASE:GetGroupNameFromUnitName( UnitName ) - return self.Templates.Units[UnitName].GroupName -end - -function DATABASE:GetGroupTemplateFromUnitName( UnitName ) - return self.Templates.Units[UnitName].GroupTemplate -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 Core.Event#EVENTDATA Event -function DATABASE:_EventOnBirth( Event ) - self:F2( { Event } ) - - if Event.IniDCSUnit then - if Event.IniObjectCategory == 3 then - self:AddStatic( Event.IniDCSUnitName ) - else - if Event.IniObjectCategory == 1 then - self:AddUnit( Event.IniDCSUnitName ) - self:AddGroup( Event.IniDCSGroupName ) - end - end - self:_EventOnPlayerEnterUnit( Event ) - end -end - - ---- Handles the OnDead or OnCrash event for alive units set. --- @param #DATABASE self --- @param Core.Event#EVENTDATA Event -function DATABASE:_EventOnDeadOrCrash( Event ) - self:F2( { Event } ) - - if Event.IniDCSUnit then - if Event.IniObjectCategory == 3 then - if self.STATICS[Event.IniDCSUnitName] then - self:DeleteStatic( Event.IniDCSUnitName ) - end - else - if Event.IniObjectCategory == 1 then - if self.UNITS[Event.IniDCSUnitName] then - self:DeleteUnit( Event.IniDCSUnitName ) - end - end - end - end -end - - ---- Handles the OnPlayerEnterUnit event to fill the active players table (with the unit filter applied). --- @param #DATABASE self --- @param Core.Event#EVENTDATA Event -function DATABASE:_EventOnPlayerEnterUnit( Event ) - self:F2( { Event } ) - - if Event.IniUnit then - if Event.IniObjectCategory == 1 then - self:AddUnit( Event.IniDCSUnitName ) - self:AddGroup( Event.IniDCSGroupName ) - local PlayerName = Event.IniUnit:GetPlayerName() - if not self.PLAYERS[PlayerName] then - self:AddPlayer( Event.IniUnitName, PlayerName ) - end - end - end -end - - ---- Handles the OnPlayerLeaveUnit event to clean the active players table. --- @param #DATABASE self --- @param Core.Event#EVENTDATA Event -function DATABASE:_EventOnPlayerLeaveUnit( Event ) - self:F2( { Event } ) - - if Event.IniUnit then - if Event.IniObjectCategory == 1 then - local PlayerName = Event.IniUnit:GetPlayerName() - if self.PLAYERS[PlayerName] then - self:DeletePlayer( PlayerName ) - end - end - end -end - ---- Iterators - ---- Iterate the DATABASE and call an iterator function for the given set, providing the Object for each element within the set and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called when there is an alive player in the database. --- @return #DATABASE self -function DATABASE:ForEach( IteratorFunction, FinalizeFunction, arg, Set ) - self:F2( arg ) - - local function CoRoutine() - local Count = 0 - for ObjectID, Object in pairs( Set ) do - self:T2( Object ) - IteratorFunction( Object, unpack( arg ) ) - Count = Count + 1 --- if Count % 100 == 0 then --- coroutine.yield( false ) --- end - end - return true - end - --- local co = coroutine.create( CoRoutine ) - local co = CoRoutine - - local function Schedule() - --- local status, res = coroutine.resume( co ) - local status, res = co() - self:T3( { status, res } ) - - if status == false then - error( res ) - end - if res == false then - return true -- resume next time the loop - end - if FinalizeFunction then - FinalizeFunction( unpack( arg ) ) - end - return false - end - - local Scheduler = SCHEDULER:New( self, Schedule, {}, 0.001, 0.001, 0 ) - - 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] = {} - - local CoalitionSide = coalition.side[string.upper(CoalitionName)] - - ---------------------------------------------- - -- build nav points DB - self.Navpoints[CoalitionName] = {} - if coa_data.nav_points then --navpoints - for nav_ind, nav_data in pairs(coa_data.nav_points) do - - if type(nav_data) == 'table' then - self.Navpoints[CoalitionName][nav_ind] = routines.utils.deepCopy(nav_data) - - self.Navpoints[CoalitionName][nav_ind]['name'] = nav_data.callsignStr -- name is a little bit more self-explanatory. - self.Navpoints[CoalitionName][nav_ind]['point'] = {} -- point is used by SSE, support it. - self.Navpoints[CoalitionName][nav_ind]['point']['x'] = nav_data.x - self.Navpoints[CoalitionName][nav_ind]['point']['y'] = 0 - self.Navpoints[CoalitionName][nav_ind]['point']['z'] = nav_data.y - end - end - end - ------------------------------------------------- - if coa_data.country then --there is a country table - for cntry_id, cntry_data in pairs(coa_data.country) do - - local CountryName = string.upper(cntry_data.name) - local CountryID = cntry_data.id - - --self.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, - CoalitionSide, - _DATABASECategory[string.lower(CategoryName)], - CountryID - ) - end --if GroupTemplate and GroupTemplate.units then - end --for group_num, GroupTemplate in pairs(obj_type_data.group) do - end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then - end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then - end --for obj_type_name, obj_type_data in pairs(cntry_data) do - end --if type(cntry_data) == 'table' then - end --for cntry_id, cntry_data in pairs(coa_data.country) do - end --if coa_data.country then --there is a country table - end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then - end --for coa_name, coa_data in pairs(mission.coalition) do - - return self -end - - - - ---- 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. --- --- ==== --- --- ### Authors: --- --- * FlightControl : Design & Programming --- --- ### Contributions: --- --- --- @module Set - - ---- SET_BASE class --- @type SET_BASE --- @field #table Filter --- @field #table Set --- @field #table List --- @field Core.Scheduler#SCHEDULER CallScheduler --- @extends Core.Base#BASE -SET_BASE = { - ClassName = "SET_BASE", - Filter = {}, - Set = {}, - List = {}, -} - ---- Creates a new SET_BASE object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. --- @param #SET_BASE self --- @return #SET_BASE --- @usage --- -- Define a new SET_BASE Object. This DBObject will contain a reference to all Group and Unit Templates defined within the ME and the DCSRTE. --- DBObject = SET_BASE:New() -function SET_BASE:New( Database ) - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) -- Core.Set#SET_BASE - - self.Database = Database - - self.YieldInterval = 10 - self.TimeInterval = 0.001 - - self.List = {} - self.List.__index = self.List - self.List = setmetatable( { Count = 0 }, self.List ) - - self.CallScheduler = SCHEDULER:New( self ) - - self:SetEventPriority( 2 ) - - return self -end - ---- Finds an @{Base#BASE} object based on the object Name. --- @param #SET_BASE self --- @param #string ObjectName --- @return Core.Base#BASE The Object found. -function SET_BASE:_Find( ObjectName ) - - local ObjectFound = self.Set[ObjectName] - return ObjectFound -end - - ---- Gets the Set. --- @param #SET_BASE self --- @return #SET_BASE self -function SET_BASE:GetSet() - self:F2() - - return self.Set -end - ---- Adds a @{Base#BASE} object in the @{Set#SET_BASE}, using a given ObjectName as the index. --- @param #SET_BASE self --- @param #string ObjectName --- @param Core.Base#BASE Object --- @return Core.Base#BASE The added BASE Object. -function SET_BASE:Add( ObjectName, Object ) - self:F2( ObjectName ) - - local t = { _ = Object } - - if self.List.last then - self.List.last._next = t - t._prev = self.List.last - self.List.last = t - else - -- this is the first node - self.List.first = t - self.List.last = t - end - - self.List.Count = self.List.Count + 1 - - self.Set[ObjectName] = t._ - -end - ---- Adds a @{Base#BASE} object in the @{Set#SET_BASE}, using the Object Name as the index. --- @param #SET_BASE self --- @param Wrapper.Object#OBJECT Object --- @return Core.Base#BASE The added BASE Object. -function SET_BASE:AddObject( Object ) - self:F2( Object.ObjectName ) - - self:T( Object.UnitName ) - self:T( Object.ObjectName ) - self:Add( Object.ObjectName, Object ) - -end - - - ---- 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:F( ObjectName ) - - local t = self.Set[ObjectName] - - self:E( { ObjectName, t } ) - - if t then - if t._next then - if t._prev then - t._next._prev = t._prev - t._prev._next = t._next - else - -- this was the first node - t._next._prev = nil - self.List._first = t._next - end - elseif t._prev then - -- this was the last node - t._prev._next = nil - self.List._last = t._prev - else - -- this was the only node - self.List._first = nil - self.List._last = nil - end - - t._next = nil - t._prev = nil - self.List.Count = self.List.Count - 1 - - self.Set[ObjectName] = nil - end - -end - ---- Gets a @{Base#BASE} object from the @{Set#SET_BASE} and derived classes, based on the Object Name. --- @param #SET_BASE self --- @param #string ObjectName --- @return Core.Base#BASE -function SET_BASE:Get( ObjectName ) - self:F( ObjectName ) - - local t = self.Set[ObjectName] - - self:T3( { ObjectName, t } ) - - return t - -end - ---- Retrieves the amount of objects in the @{Set#SET_BASE} and derived classes. --- @param #SET_BASE self --- @return #number Count -function SET_BASE:Count() - - return self.List.Count -end - - - ---- Copies the Filter criteria from a given Set (for rebuilding a new Set based on an existing Set). --- @param #SET_BASE self --- @param #SET_BASE BaseSet --- @return #SET_BASE -function SET_BASE:SetDatabase( BaseSet ) - - -- Copy the filter criteria of the BaseSet - local OtherFilter = routines.utils.deepCopy( BaseSet.Filter ) - self.Filter = OtherFilter - - -- Now base the new Set on the BaseSet - self.Database = BaseSet:GetSet() - return self -end - - - ---- Define the SET iterator **"yield interval"** and the **"time interval"**. --- @param #SET_BASE self --- @param #number YieldInterval Sets the frequency when the iterator loop will yield after the number of objects processed. The default frequency is 10 objects processed. --- @param #number TimeInterval Sets the time in seconds when the main logic will resume the iterator loop. The default time is 0.001 seconds. --- @return #SET_BASE self -function SET_BASE:SetIteratorIntervals( YieldInterval, TimeInterval ) - - self.YieldInterval = YieldInterval - self.TimeInterval = TimeInterval - - return self -end - - ---- Filters for the defined collection. --- @param #SET_BASE self --- @return #SET_BASE self -function SET_BASE:FilterOnce() - - for ObjectName, Object in pairs( self.Database ) do - - if self:IsIncludeObject( Object ) then - self:Add( ObjectName, Object ) - end - end - - return self -end - ---- Starts the filtering for the defined collection. --- @param #SET_BASE self --- @return #SET_BASE self -function SET_BASE:_FilterStart() - - for ObjectName, Object in pairs( self.Database ) do - - if self:IsIncludeObject( Object ) then - self:E( { "Adding Object:", ObjectName } ) - self:Add( ObjectName, Object ) - end - end - - self:HandleEvent( EVENTS.Birth, self._EventOnBirth ) - self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) - self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) - - -- Follow alive players and clients - self:HandleEvent( EVENTS.PlayerEnterUnit, self._EventOnPlayerEnterUnit ) - self:HandleEvent( EVENTS.PlayerLeaveUnit, self._EventOnPlayerLeaveUnit ) - - - return self -end - ---- Stops the filtering for the defined collection. --- @param #SET_BASE self --- @return #SET_BASE self -function SET_BASE:FilterStop() - - self:UnHandleEvent( EVENTS.Birth ) - self:UnHandleEvent( EVENTS.Dead ) - self:UnHandleEvent( EVENTS.Crash ) - - return self -end - ---- Iterate the SET_BASE while identifying the nearest object from a @{Point#POINT_VEC2}. --- @param #SET_BASE self --- @param Core.Point#POINT_VEC2 PointVec2 A @{Point#POINT_VEC2} object from where to evaluate the closest object in the set. --- @return Core.Base#BASE The closest object. -function SET_BASE:FindNearestObjectFromPointVec2( PointVec2 ) - self:F2( PointVec2 ) - - local NearestObject = nil - local ClosestDistance = nil - - for ObjectID, ObjectData in pairs( self.Set ) do - if NearestObject == nil then - NearestObject = ObjectData - ClosestDistance = PointVec2:DistanceFromVec2( ObjectData:GetVec2() ) - else - local Distance = PointVec2:DistanceFromVec2( ObjectData:GetVec2() ) - if Distance < ClosestDistance then - NearestObject = ObjectData - ClosestDistance = Distance - end - end - end - - return NearestObject -end - - - ------ Private method that registers all alive players in the mission. ----- @param #SET_BASE self ----- @return #SET_BASE self ---function SET_BASE:_RegisterPlayers() --- --- local CoalitionsData = { AlivePlayersRed = coalition.getPlayers( coalition.side.RED ), AlivePlayersBlue = coalition.getPlayers( coalition.side.BLUE ) } --- for CoalitionId, CoalitionData in pairs( CoalitionsData ) do --- for UnitId, UnitData in pairs( CoalitionData ) do --- self:T3( { "UnitData:", UnitData } ) --- if UnitData and UnitData:isExist() then --- local UnitName = UnitData:getName() --- if not self.PlayersAlive[UnitName] then --- self:E( { "Add player for unit:", UnitName, UnitData:getPlayerName() } ) --- self.PlayersAlive[UnitName] = UnitData:getPlayerName() --- end --- end --- end --- end --- --- return self ---end - ---- Events - ---- Handles the OnBirth event for the Set. --- @param #SET_BASE self --- @param Core.Event#EVENTDATA Event -function SET_BASE:_EventOnBirth( Event ) - self:F3( { Event } ) - - if Event.IniDCSUnit then - local ObjectName, Object = self:AddInDatabase( Event ) - self:T3( ObjectName, Object ) - if Object and self:IsIncludeObject( Object ) then - self:Add( ObjectName, Object ) - --self:_EventOnPlayerEnterUnit( Event ) - end - end -end - ---- Handles the OnDead or OnCrash event for alive units set. --- @param #SET_BASE self --- @param Core.Event#EVENTDATA Event -function SET_BASE:_EventOnDeadOrCrash( Event ) - self:F3( { Event } ) - - if Event.IniDCSUnit then - local ObjectName, Object = self:FindInDatabase( Event ) - if ObjectName and Object ~= nil then - self:Remove( ObjectName ) - end - end -end - ---- Handles the OnPlayerEnterUnit event to fill the active players table (with the unit filter applied). --- @param #SET_BASE self --- @param Core.Event#EVENTDATA Event -function SET_BASE:_EventOnPlayerEnterUnit( Event ) - self:F3( { Event } ) - - if Event.IniDCSUnit then - local ObjectName, Object = self:AddInDatabase( Event ) - self:T3( ObjectName, Object ) - if self:IsIncludeObject( Object ) then - self:Add( ObjectName, Object ) - --self:_EventOnPlayerEnterUnit( Event ) - end - end -end - ---- Handles the OnPlayerLeaveUnit event to clean the active players table. --- @param #SET_BASE self --- @param Core.Event#EVENTDATA Event -function SET_BASE:_EventOnPlayerLeaveUnit( Event ) - self:F3( { Event } ) - - local ObjectName = Event.IniDCSUnit - if Event.IniDCSUnit then - if Event.IniDCSGroup then - local GroupUnits = Event.IniDCSGroup:getUnits() - local PlayerCount = 0 - for _, DCSUnit in pairs( GroupUnits ) do - if DCSUnit ~= Event.IniDCSUnit then - if DCSUnit:getPlayer() ~= nil then - PlayerCount = PlayerCount + 1 - end - end - end - self:E(PlayerCount) - if PlayerCount == 0 then - self:Remove( Event.IniDCSGroupName ) - end - end - end -end - --- Iterators - ---- Iterate the SET_BASE and derived classes and call an iterator function for the given SET_BASE, providing the Object for each element within the set and optional parameters. --- @param #SET_BASE self --- @param #function IteratorFunction The function that will be called. --- @return #SET_BASE self -function SET_BASE:ForEach( IteratorFunction, arg, Set, Function, FunctionArguments ) - self:F3( arg ) - - Set = Set or self:GetSet() - arg = arg or {} - - local function CoRoutine() - local Count = 0 - for ObjectID, ObjectData in pairs( Set ) do - local Object = ObjectData - self:T3( Object ) - if Function then - if Function( unpack( FunctionArguments ), Object ) == true then - IteratorFunction( Object, unpack( arg ) ) - end - else - IteratorFunction( Object, unpack( arg ) ) - end - Count = Count + 1 --- if Count % self.YieldInterval == 0 then --- coroutine.yield( false ) --- end - end - return true - end - --- local co = coroutine.create( CoRoutine ) - local co = CoRoutine - - local function Schedule() - --- local status, res = coroutine.resume( co ) - local status, res = co() - self:T3( { status, res } ) - - if status == false then - error( res ) - end - if res == false then - return true -- resume next time the loop - end - - return false - end - - self.CallScheduler:Schedule( self, Schedule, {}, self.TimeInterval, self.TimeInterval, 0 ) - - 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 debugging 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:E( { "Objects in Set:", ObjectNames } ) - - return ObjectNames -end - --- SET_GROUP - ---- SET_GROUP class --- @type SET_GROUP --- @extends #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 Core.Set#SET_GROUP self --- @param #string AddGroupNames A single name or an array of GROUP names. --- @return self -function SET_GROUP:AddGroupsByName( AddGroupNames ) - - local AddGroupNamesArray = ( type( AddGroupNames ) == "table" ) and AddGroupNames or { AddGroupNames } - - for AddGroupID, AddGroupName in pairs( AddGroupNamesArray ) do - self:Add( AddGroupName, GROUP:FindByName( AddGroupName ) ) - end - - return self -end - ---- Remove GROUP(s) from SET_GROUP. --- @param Core.Set#SET_GROUP self --- @param Wrapper.Group#GROUP RemoveGroupNames A single name or an array of GROUP names. --- @return self -function SET_GROUP:RemoveGroupsByName( RemoveGroupNames ) - - local RemoveGroupNamesArray = ( type( RemoveGroupNames ) == "table" ) and RemoveGroupNames or { RemoveGroupNames } - - for RemoveGroupID, RemoveGroupName in pairs( RemoveGroupNamesArray ) do - self:Remove( RemoveGroupName.GroupName ) - end - - return self -end - - - - ---- Finds a Group based on the Group Name. --- @param #SET_GROUP self --- @param #string GroupName --- @return Wrapper.Group#GROUP The found Group. -function SET_GROUP:FindGroup( GroupName ) - - local GroupFound = self.Set[GroupName] - return GroupFound -end - - - ---- 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 Core.Event#EVENTDATA Event --- @return #string The name of the GROUP --- @return #table The GROUP -function SET_GROUP:AddInDatabase( Event ) - self:F3( { Event } ) - - if Event.IniObjectCategory == 1 then - if not self.Database[Event.IniDCSGroupName] then - self.Database[Event.IniDCSGroupName] = GROUP:Register( Event.IniDCSGroupName ) - self:T3( self.Database[Event.IniDCSGroupName] ) - end - end - - return Event.IniDCSGroupName, self.Database[Event.IniDCSGroupName] -end - ---- Handles the Database to check on any event that Object exists in the Database. --- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! --- @param #SET_GROUP self --- @param Core.Event#EVENTDATA Event --- @return #string The name of the GROUP --- @return #table The GROUP -function SET_GROUP:FindInDatabase( Event ) - self:F3( { Event } ) - - return Event.IniDCSGroupName, self.Database[Event.IniDCSGroupName] -end - ---- Iterate the SET_GROUP and call an iterator function for each **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 Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. --- @return #SET_GROUP self -function SET_GROUP:ForEachGroupCompletelyInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set, - --- @param Core.Zone#ZONE_BASE ZoneObject - -- @param Wrapper.Group#GROUP GroupObject - function( ZoneObject, GroupObject ) - if GroupObject:IsCompletelyInZone( ZoneObject ) then - return true - else - return false - end - end, { ZoneObject } ) - - return self -end - ---- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence partly in a @{Zone}, providing the GROUP and optional parameters to the called function. --- @param #SET_GROUP self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. --- @return #SET_GROUP self -function SET_GROUP:ForEachGroupPartlyInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set, - --- @param Core.Zone#ZONE_BASE ZoneObject - -- @param Wrapper.Group#GROUP GroupObject - function( ZoneObject, GroupObject ) - if GroupObject:IsPartlyInZone( ZoneObject ) then - return true - else - return false - end - end, { ZoneObject } ) - - return self -end - ---- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence not in a @{Zone}, providing the GROUP and optional parameters to the called function. --- @param #SET_GROUP self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. --- @return #SET_GROUP self -function SET_GROUP:ForEachGroupNotInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set, - --- @param Core.Zone#ZONE_BASE ZoneObject - -- @param Wrapper.Group#GROUP GroupObject - function( ZoneObject, GroupObject ) - if GroupObject:IsNotInZone( ZoneObject ) then - return true - else - return false - end - end, { ZoneObject } ) - - return self -end - - ------ Iterate the SET_GROUP and call an interator function for each **alive** player, providing the Group of the player and optional parameters. ----- @param #SET_GROUP self ----- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_GROUP. The function needs to accept a GROUP parameter. ----- @return #SET_GROUP self ---function SET_GROUP:ForEachPlayer( IteratorFunction, ... ) --- self:F2( arg ) --- --- self:ForEach( IteratorFunction, arg, self.PlayersAlive ) --- --- return self ---end --- --- ------ Iterate the SET_GROUP and call an interator function for each client, providing the Client to the function and optional parameters. ----- @param #SET_GROUP self ----- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_GROUP. The function needs to accept a CLIENT parameter. ----- @return #SET_GROUP self ---function SET_GROUP:ForEachClient( IteratorFunction, ... ) --- self:F2( arg ) --- --- self:ForEach( IteratorFunction, arg, self.Clients ) --- --- return self ---end - - ---- --- @param #SET_GROUP self --- @param Wrapper.Group#GROUP MooseGroup --- @return #SET_GROUP self -function SET_GROUP:IsIncludeObject( MooseGroup ) - self:F2( MooseGroup ) - local MooseGroupInclude = true - - if self.Filter.Coalitions then - local MooseGroupCoalition = false - for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do - self:T3( { "Coalition:", MooseGroup:GetCoalition(), self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) - if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == MooseGroup:GetCoalition() then - MooseGroupCoalition = true - 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 Core.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 ) ) - - 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 Core.Set#SET_UNIT self --- @param Wrapper.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 ) - end - - return self -end - - ---- Finds a Unit based on the Unit Name. --- @param #SET_UNIT self --- @param #string UnitName --- @return Wrapper.Unit#UNIT The found Unit. -function SET_UNIT:FindUnit( UnitName ) - - local UnitFound = self.Set[UnitName] - return UnitFound -end - - - ---- Builds a set of units of coalitions. --- Possible current coalitions are red, blue and neutral. --- @param #SET_UNIT self --- @param #string Coalitions Can take the following values: "red", "blue", "neutral". --- @return #SET_UNIT self -function SET_UNIT:FilterCoalitions( Coalitions ) - 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 - ---- Builds a set of units having a radar of give types. --- All the units having a radar of a given type will be included within the set. --- @param #SET_UNIT self --- @param #table RadarTypes The radar types. --- @return #SET_UNIT self -function SET_UNIT:FilterHasRadar( RadarTypes ) - - self.Filter.RadarTypes = self.Filter.RadarTypes or {} - if type( RadarTypes ) ~= "table" then - RadarTypes = { RadarTypes } - end - for RadarTypeID, RadarType in pairs( RadarTypes ) do - self.Filter.RadarTypes[RadarType] = RadarType - end - return self -end - ---- Builds a set of SEADable units. --- @param #SET_UNIT self --- @return #SET_UNIT self -function SET_UNIT:FilterHasSEAD() - - self.Filter.SEAD = true - return self -end - - - ---- 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 Core.Event#EVENTDATA Event --- @return #string The name of the UNIT --- @return #table The UNIT -function SET_UNIT:AddInDatabase( Event ) - self:F3( { Event } ) - - if Event.IniObjectCategory == 1 then - if not self.Database[Event.IniDCSUnitName] then - self.Database[Event.IniDCSUnitName] = UNIT:Register( Event.IniDCSUnitName ) - self:T3( self.Database[Event.IniDCSUnitName] ) - end - end - - return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] -end - ---- Handles the Database to check on any event that Object exists in the Database. --- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! --- @param #SET_UNIT self --- @param Core.Event#EVENTDATA Event --- @return #string The name of the UNIT --- @return #table The UNIT -function SET_UNIT:FindInDatabase( Event ) - self:E( { Event.IniDCSUnitName, self.Set[Event.IniDCSUnitName], Event } ) - - - return Event.IniDCSUnitName, self.Set[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 Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the SET_UNIT. The function needs to accept a UNIT parameter. --- @return #SET_UNIT self -function SET_UNIT:ForEachUnitCompletelyInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set, - --- @param Core.Zone#ZONE_BASE ZoneObject - -- @param Wrapper.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 Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the SET_UNIT. The function needs to accept a UNIT parameter. --- @return #SET_UNIT self -function SET_UNIT:ForEachUnitNotInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set, - --- @param Core.Zone#ZONE_BASE ZoneObject - -- @param Wrapper.Unit#UNIT UnitObject - function( ZoneObject, UnitObject ) - if UnitObject:IsNotInZone( ZoneObject ) then - return true - else - return false - end - end, { ZoneObject } ) - - return self -end - ---- Returns map of unit types. --- @param #SET_UNIT self --- @return #map<#string,#number> A map of the unit types found. The key is the UnitTypeName and the value is the amount of unit types found. -function SET_UNIT:GetUnitTypes() - self:F2() - - local MT = {} -- Message Text - local UnitTypes = {} - - for UnitID, UnitData in pairs( self:GetSet() ) do - local TextUnit = UnitData -- Wrapper.Unit#UNIT - if TextUnit:IsAlive() then - local UnitType = TextUnit:GetTypeName() - - if not UnitTypes[UnitType] then - UnitTypes[UnitType] = 1 - else - UnitTypes[UnitType] = UnitTypes[UnitType] + 1 - end - end - end - - for UnitTypeID, UnitType in pairs( UnitTypes ) do - MT[#MT+1] = UnitType .. " of " .. UnitTypeID - end - - return UnitTypes -end - - ---- Returns a comma separated string of the unit types with a count in the @{Set}. --- @param #SET_UNIT self --- @return #string The unit types string -function SET_UNIT:GetUnitTypesText() - self:F2() - - local MT = {} -- Message Text - local UnitTypes = self:GetUnitTypes() - - for UnitTypeID, UnitType in pairs( UnitTypes ) do - MT[#MT+1] = UnitType .. " of " .. UnitTypeID - end - - return table.concat( MT, ", " ) -end - ---- Returns map of unit threat levels. --- @param #SET_UNIT self --- @return #table. -function SET_UNIT:GetUnitThreatLevels() - self:F2() - - local UnitThreatLevels = {} - - for UnitID, UnitData in pairs( self:GetSet() ) do - local ThreatUnit = UnitData -- Wrapper.Unit#UNIT - if ThreatUnit:IsAlive() then - local UnitThreatLevel, UnitThreatLevelText = ThreatUnit:GetThreatLevel() - local ThreatUnitName = ThreatUnit:GetName() - - UnitThreatLevels[UnitThreatLevel] = UnitThreatLevels[UnitThreatLevel] or {} - UnitThreatLevels[UnitThreatLevel].UnitThreatLevelText = UnitThreatLevelText - UnitThreatLevels[UnitThreatLevel].Units = UnitThreatLevels[UnitThreatLevel].Units or {} - UnitThreatLevels[UnitThreatLevel].Units[ThreatUnitName] = ThreatUnit - end - end - - return UnitThreatLevels -end - ---- Calculate the maxium A2G threat level of the SET_UNIT. --- @param #SET_UNIT self -function SET_UNIT:CalculateThreatLevelA2G() - - local MaxThreatLevelA2G = 0 - for UnitName, UnitData in pairs( self:GetSet() ) do - local ThreatUnit = UnitData -- Wrapper.Unit#UNIT - local ThreatLevelA2G = ThreatUnit:GetThreatLevel() - if ThreatLevelA2G > MaxThreatLevelA2G then - MaxThreatLevelA2G = ThreatLevelA2G - end - end - - self:T3( MaxThreatLevelA2G ) - return MaxThreatLevelA2G - -end - - ---- Returns if the @{Set} has targets having a radar (of a given type). --- @param #SET_UNIT self --- @param Dcs.DCSWrapper.Unit#Unit.RadarType RadarType --- @return #number The amount of radars in the Set with the given type -function SET_UNIT:HasRadar( RadarType ) - self:F2( RadarType ) - - local RadarCount = 0 - for UnitID, UnitData in pairs( self:GetSet()) do - local UnitSensorTest = UnitData -- Wrapper.Unit#UNIT - local HasSensors - if RadarType then - HasSensors = UnitSensorTest:HasSensors( Unit.SensorType.RADAR, RadarType ) - else - HasSensors = UnitSensorTest:HasSensors( Unit.SensorType.RADAR ) - end - self:T3(HasSensors) - if HasSensors then - RadarCount = RadarCount + 1 - end - end - - return RadarCount -end - ---- Returns if the @{Set} has targets that can be SEADed. --- @param #SET_UNIT self --- @return #number The amount of SEADable units in the Set -function SET_UNIT:HasSEAD() - self:F2() - - local SEADCount = 0 - for UnitID, UnitData in pairs( self:GetSet()) do - local UnitSEAD = UnitData -- Wrapper.Unit#UNIT - if UnitSEAD:IsAlive() then - local UnitSEADAttributes = UnitSEAD:GetDesc().attributes - - local HasSEAD = UnitSEAD:HasSEAD() - - self:T3(HasSEAD) - if HasSEAD then - SEADCount = SEADCount + 1 - end - end - end - - return SEADCount -end - ---- Returns if the @{Set} has ground targets. --- @param #SET_UNIT self --- @return #number The amount of ground targets in the Set. -function SET_UNIT:HasGroundUnits() - self:F2() - - local GroundUnitCount = 0 - for UnitID, UnitData in pairs( self:GetSet()) do - local UnitTest = UnitData -- Wrapper.Unit#UNIT - if UnitTest:IsGround() then - GroundUnitCount = GroundUnitCount + 1 - end - end - - return GroundUnitCount -end - ---- Returns if the @{Set} has friendly ground units. --- @param #SET_UNIT self --- @return #number The amount of ground targets in the Set. -function SET_UNIT:HasFriendlyUnits( FriendlyCoalition ) - self:F2() - - local FriendlyUnitCount = 0 - for UnitID, UnitData in pairs( self:GetSet()) do - local UnitTest = UnitData -- Wrapper.Unit#UNIT - if UnitTest:IsFriendly( FriendlyCoalition ) then - FriendlyUnitCount = FriendlyUnitCount + 1 - end - end - - return FriendlyUnitCount -end - - - ------ Iterate the SET_UNIT and call an interator function for each **alive** player, providing the Unit of the player and optional parameters. ----- @param #SET_UNIT self ----- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_UNIT. The function needs to accept a UNIT parameter. ----- @return #SET_UNIT self ---function SET_UNIT:ForEachPlayer( IteratorFunction, ... ) --- self:F2( arg ) --- --- self:ForEach( IteratorFunction, arg, self.PlayersAlive ) --- --- return self ---end --- --- ------ Iterate the SET_UNIT and call an interator function for each client, providing the Client to the function and optional parameters. ----- @param #SET_UNIT self ----- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_UNIT. The function needs to accept a CLIENT parameter. ----- @return #SET_UNIT self ---function SET_UNIT:ForEachClient( IteratorFunction, ... ) --- self:F2( arg ) --- --- self:ForEach( IteratorFunction, arg, self.Clients ) --- --- return self ---end - - ---- --- @param #SET_UNIT self --- @param Wrapper.Unit#UNIT MUnit --- @return #SET_UNIT self -function SET_UNIT:IsIncludeObject( MUnit ) - self:F2( MUnit ) - local MUnitInclude = 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 - - if self.Filter.RadarTypes then - local MUnitRadar = false - for RadarTypeID, RadarType in pairs( self.Filter.RadarTypes ) do - self:T3( { "Radar:", RadarType } ) - if MUnit:HasSensors( Unit.SensorType.RADAR, RadarType ) == true then - if MUnit:GetRadar() == true then -- This call is necessary to evaluate the SEAD capability. - self:T3( "RADAR Found" ) - end - MUnitRadar = true - end - end - MUnitInclude = MUnitInclude and MUnitRadar - end - - if self.Filter.SEAD then - local MUnitSEAD = false - if MUnit:HasSEAD() == true then - self:T3( "SEAD Found" ) - MUnitSEAD = true - end - MUnitInclude = MUnitInclude and MUnitSEAD - end - - self:T2( MUnitInclude ) - return MUnitInclude -end - - ---- SET_CLIENT - ---- SET_CLIENT class --- @type SET_CLIENT --- @extends Core.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 Core.Set#SET_CLIENT self --- @param #string AddClientNames A single name or an array of CLIENT names. --- @return self -function SET_CLIENT:AddClientsByName( AddClientNames ) - - local AddClientNamesArray = ( type( AddClientNames ) == "table" ) and AddClientNames or { AddClientNames } - - for AddClientID, AddClientName in pairs( AddClientNamesArray ) do - self:Add( AddClientName, CLIENT:FindByName( AddClientName ) ) - end - - return self -end - ---- Remove CLIENT(s) from SET_CLIENT. --- @param Core.Set#SET_CLIENT self --- @param Wrapper.Client#CLIENT RemoveClientNames A single name or an array of CLIENT names. --- @return self -function SET_CLIENT:RemoveClientsByName( RemoveClientNames ) - - local RemoveClientNamesArray = ( type( RemoveClientNames ) == "table" ) and RemoveClientNames or { RemoveClientNames } - - for RemoveClientID, RemoveClientName in pairs( RemoveClientNamesArray ) do - self:Remove( RemoveClientName.ClientName ) - end - - return self -end - - ---- Finds a Client based on the Client Name. --- @param #SET_CLIENT self --- @param #string ClientName --- @return Wrapper.Client#CLIENT The found Client. -function SET_CLIENT:FindClient( ClientName ) - - local ClientFound = self.Set[ClientName] - return ClientFound -end - - - ---- Builds a set of clients of coalitions. --- Possible current coalitions are red, blue and neutral. --- @param #SET_CLIENT self --- @param #string Coalitions Can take the following values: "red", "blue", "neutral". --- @return #SET_CLIENT self -function SET_CLIENT:FilterCoalitions( Coalitions ) - if not self.Filter.Coalitions then - self.Filter.Coalitions = {} - end - if type( Coalitions ) ~= "table" then - Coalitions = { Coalitions } - end - for CoalitionID, Coalition in pairs( Coalitions ) do - self.Filter.Coalitions[Coalition] = Coalition - end - return self -end - - ---- Builds a set of clients out of categories. --- Possible current categories are plane, helicopter, ground, ship. --- @param #SET_CLIENT self --- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship". --- @return #SET_CLIENT self -function SET_CLIENT:FilterCategories( Categories ) - if not self.Filter.Categories then - self.Filter.Categories = {} - end - if type( Categories ) ~= "table" then - Categories = { Categories } - end - for CategoryID, Category in pairs( Categories ) do - self.Filter.Categories[Category] = Category - end - return self -end - - ---- Builds a set of clients of defined client types. --- Possible current types are those types known within DCS world. --- @param #SET_CLIENT self --- @param #string Types Can take those type strings known within DCS world. --- @return #SET_CLIENT self -function SET_CLIENT:FilterTypes( Types ) - if not self.Filter.Types then - self.Filter.Types = {} - end - if type( Types ) ~= "table" then - Types = { Types } - end - for TypeID, Type in pairs( Types ) do - self.Filter.Types[Type] = Type - end - return self -end - - ---- Builds a set of clients of defined countries. --- Possible current countries are those known within DCS world. --- @param #SET_CLIENT self --- @param #string Countries Can take those country strings known within DCS world. --- @return #SET_CLIENT self -function SET_CLIENT:FilterCountries( Countries ) - if not self.Filter.Countries then - self.Filter.Countries = {} - end - if type( Countries ) ~= "table" then - Countries = { Countries } - end - for CountryID, Country in pairs( Countries ) do - self.Filter.Countries[Country] = Country - end - return self -end - - ---- Builds a set of clients 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 Core.Event#EVENTDATA Event --- @return #string The name of the CLIENT --- @return #table The CLIENT -function SET_CLIENT:AddInDatabase( Event ) - self:F3( { Event } ) - - return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] -end - ---- Handles the Database to check on any event that Object exists in the Database. --- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! --- @param #SET_CLIENT self --- @param Core.Event#EVENTDATA Event --- @return #string The name of the CLIENT --- @return #table The CLIENT -function SET_CLIENT:FindInDatabase( Event ) - self:F3( { Event } ) - - return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] -end - ---- Iterate the SET_CLIENT and call an interator function for each **alive** CLIENT, providing the CLIENT and optional parameters. --- @param #SET_CLIENT self --- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_CLIENT. The function needs to accept a CLIENT parameter. --- @return #SET_CLIENT self -function SET_CLIENT:ForEachClient( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.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 Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_CLIENT. The function needs to accept a CLIENT parameter. --- @return #SET_CLIENT self -function SET_CLIENT:ForEachClientInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set, - --- @param Core.Zone#ZONE_BASE ZoneObject - -- @param Wrapper.Client#CLIENT ClientObject - function( ZoneObject, ClientObject ) - if ClientObject:IsInZone( ZoneObject ) then - return true - else - return false - end - end, { ZoneObject } ) - - return self -end - ---- Iterate the SET_CLIENT and call an iterator function for each **alive** CLIENT presence not in a @{Zone}, providing the CLIENT and optional parameters to the called function. --- @param #SET_CLIENT self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_CLIENT. The function needs to accept a CLIENT parameter. --- @return #SET_CLIENT self -function SET_CLIENT:ForEachClientNotInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set, - --- @param Core.Zone#ZONE_BASE ZoneObject - -- @param Wrapper.Client#CLIENT ClientObject - function( ZoneObject, ClientObject ) - if ClientObject:IsNotInZone( ZoneObject ) then - return true - else - return false - end - end, { ZoneObject } ) - - return self -end - ---- --- @param #SET_CLIENT self --- @param Wrapper.Client#CLIENT MClient --- @return #SET_CLIENT self -function SET_CLIENT:IsIncludeObject( MClient ) - self:F2( MClient ) - - local MClientInclude = true - - if MClient then - local MClientName = MClient.UnitName - - if self.Filter.Coalitions then - local MClientCoalition = false - for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do - local ClientCoalitionID = _DATABASE:GetCoalitionFromClientTemplate( MClientName ) - self:T3( { "Coalition:", ClientCoalitionID, self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) - if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == ClientCoalitionID then - MClientCoalition = true - end - end - self:T( { "Evaluated Coalition", MClientCoalition } ) - MClientInclude = MClientInclude and MClientCoalition - end - - if self.Filter.Categories then - local MClientCategory = false - for CategoryID, CategoryName in pairs( self.Filter.Categories ) do - local ClientCategoryID = _DATABASE:GetCategoryFromClientTemplate( MClientName ) - self:T3( { "Category:", ClientCategoryID, self.FilterMeta.Categories[CategoryName], CategoryName } ) - if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == ClientCategoryID then - MClientCategory = true - end - end - self:T( { "Evaluated Category", MClientCategory } ) - MClientInclude = MClientInclude and MClientCategory - end - - if self.Filter.Types then - local MClientType = false - for TypeID, TypeName in pairs( self.Filter.Types ) do - self:T3( { "Type:", MClient:GetTypeName(), TypeName } ) - if TypeName == MClient:GetTypeName() then - MClientType = true - end - end - self:T( { "Evaluated Type", MClientType } ) - MClientInclude = MClientInclude and MClientType - end - - if self.Filter.Countries then - local MClientCountry = false - for CountryID, CountryName in pairs( self.Filter.Countries ) do - local ClientCountryID = _DATABASE:GetCountryFromClientTemplate(MClientName) - self:T3( { "Country:", ClientCountryID, country.id[CountryName], CountryName } ) - if country.id[CountryName] and country.id[CountryName] == ClientCountryID then - MClientCountry = true - end - end - self:T( { "Evaluated Country", MClientCountry } ) - MClientInclude = MClientInclude and MClientCountry - end - - if self.Filter.ClientPrefixes then - local MClientPrefix = false - for ClientPrefixId, ClientPrefix in pairs( self.Filter.ClientPrefixes ) do - self:T3( { "Prefix:", string.find( MClient.UnitName, ClientPrefix, 1 ), ClientPrefix } ) - if string.find( MClient.UnitName, ClientPrefix, 1 ) then - MClientPrefix = true - end - end - self:T( { "Evaluated Prefix", MClientPrefix } ) - MClientInclude = MClientInclude and MClientPrefix - end - end - - self:T2( MClientInclude ) - return MClientInclude -end - ---- SET_AIRBASE - ---- SET_AIRBASE class --- @type SET_AIRBASE --- @extends Core.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 Core.Set#SET_AIRBASE self --- @param #string AddAirbaseNames A single name or an array of AIRBASE names. --- @return self -function SET_AIRBASE:AddAirbasesByName( AddAirbaseNames ) - - local AddAirbaseNamesArray = ( type( AddAirbaseNames ) == "table" ) and AddAirbaseNames or { AddAirbaseNames } - - for AddAirbaseID, AddAirbaseName in pairs( AddAirbaseNamesArray ) do - self:Add( AddAirbaseName, AIRBASE:FindByName( AddAirbaseName ) ) - end - - return self -end - ---- Remove AIRBASEs from SET_AIRBASE. --- @param Core.Set#SET_AIRBASE self --- @param Wrapper.Airbase#AIRBASE RemoveAirbaseNames A single name or an array of AIRBASE names. --- @return self -function SET_AIRBASE:RemoveAirbasesByName( RemoveAirbaseNames ) - - local RemoveAirbaseNamesArray = ( type( RemoveAirbaseNames ) == "table" ) and RemoveAirbaseNames or { RemoveAirbaseNames } - - for RemoveAirbaseID, RemoveAirbaseName in pairs( RemoveAirbaseNamesArray ) do - self:Remove( RemoveAirbaseName.AirbaseName ) - end - - return self -end - - ---- Finds a Airbase based on the Airbase Name. --- @param #SET_AIRBASE self --- @param #string AirbaseName --- @return Wrapper.Airbase#AIRBASE The found Airbase. -function SET_AIRBASE:FindAirbase( AirbaseName ) - - local AirbaseFound = self.Set[AirbaseName] - return AirbaseFound -end - - - ---- 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 Core.Event#EVENTDATA Event --- @return #string The name of the AIRBASE --- @return #table The AIRBASE -function SET_AIRBASE:AddInDatabase( Event ) - self:F3( { Event } ) - - return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] -end - ---- Handles the Database to check on any event that Object exists in the Database. --- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! --- @param #SET_AIRBASE self --- @param Core.Event#EVENTDATA Event --- @return #string The name of the AIRBASE --- @return #table The AIRBASE -function SET_AIRBASE:FindInDatabase( Event ) - self:F3( { Event } ) - - return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] -end - ---- Iterate the SET_AIRBASE and call an interator function for each AIRBASE, providing the AIRBASE and optional parameters. --- @param #SET_AIRBASE self --- @param #function IteratorFunction The function that will be called when there is an alive AIRBASE in the SET_AIRBASE. The function needs to accept a AIRBASE parameter. --- @return #SET_AIRBASE self -function SET_AIRBASE:ForEachAirbase( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set ) - - return self -end - ---- Iterate the SET_AIRBASE while identifying the nearest @{Airbase#AIRBASE} from a @{Point#POINT_VEC2}. --- @param #SET_AIRBASE self --- @param Core.Point#POINT_VEC2 PointVec2 A @{Point#POINT_VEC2} object from where to evaluate the closest @{Airbase#AIRBASE}. --- @return Wrapper.Airbase#AIRBASE The closest @{Airbase#AIRBASE}. -function SET_AIRBASE:FindNearestAirbaseFromPointVec2( PointVec2 ) - self:F2( PointVec2 ) - - local NearestAirbase = self:FindNearestObjectFromPointVec2( PointVec2 ) - return NearestAirbase -end - - - ---- --- @param #SET_AIRBASE self --- @param Wrapper.Airbase#AIRBASE MAirbase --- @return #SET_AIRBASE self -function SET_AIRBASE:IsIncludeObject( MAirbase ) - self:F2( MAirbase ) - - local MAirbaseInclude = true - - if MAirbase then - local MAirbaseName = MAirbase:GetName() - - if self.Filter.Coalitions then - local MAirbaseCoalition = false - for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do - local AirbaseCoalitionID = _DATABASE:GetCoalitionFromAirbase( MAirbaseName ) - self:T3( { "Coalition:", AirbaseCoalitionID, self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) - if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == AirbaseCoalitionID then - MAirbaseCoalition = true - end - end - self:T( { "Evaluated Coalition", MAirbaseCoalition } ) - MAirbaseInclude = MAirbaseInclude and MAirbaseCoalition - end - - if self.Filter.Categories then - local MAirbaseCategory = false - for CategoryID, CategoryName in pairs( self.Filter.Categories ) do - local AirbaseCategoryID = _DATABASE:GetCategoryFromAirbase( MAirbaseName ) - self:T3( { "Category:", AirbaseCategoryID, self.FilterMeta.Categories[CategoryName], CategoryName } ) - if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == AirbaseCategoryID then - MAirbaseCategory = true - end - end - self:T( { "Evaluated Category", MAirbaseCategory } ) - MAirbaseInclude = MAirbaseInclude and MAirbaseCategory - end - end - - self:T2( MAirbaseInclude ) - return MAirbaseInclude -end ---- 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. --- --- **Important Note:** Most of the functions in this section were taken from MIST, and reworked to OO concepts. --- In order to keep the credibility of the the author, I want to emphasize that the of the MIST framework was created by Grimes, who you can find on the Eagle Dynamics Forums. --- --- ## 1.1) POINT_VEC3 constructor --- --- A new POINT_VEC3 instance can be created with: --- --- * @{Point#POINT_VEC3.New}(): a 3D point. --- * @{Point#POINT_VEC3.NewFromVec3}(): a 3D point created from a @{DCSTypes#Vec3}. --- --- ## 1.2) Manupulate the X, Y, Z coordinates of the point --- --- A POINT_VEC3 class works in 3D space. It contains internally an X, Y, Z coordinate. --- Methods exist to manupulate these coordinates. --- --- The current X, Y, Z axis can be retrieved with the methods @{#POINT_VEC3.GetX}(), @{#POINT_VEC3.GetY}(), @{#POINT_VEC3.GetZ}() respectively. --- The methods @{#POINT_VEC3.SetX}(), @{#POINT_VEC3.SetY}(), @{#POINT_VEC3.SetZ}() change the respective axis with a new value. --- The current axis values can be changed by using the methods @{#POINT_VEC3.AddX}(), @{#POINT_VEC3.AddY}(), @{#POINT_VEC3.AddZ}() --- to add or substract a value from the current respective axis value. --- Note that the Set and Add methods return the current POINT_VEC3 object, so these manipulation methods can be chained... For example: --- --- local Vec3 = PointVec3:AddX( 100 ):AddZ( 150 ):GetVec3() --- --- ## 1.3) Create waypoints for routes --- --- A POINT_VEC3 can prepare waypoints for Ground, Air and Naval groups to be embedded into a Route. --- --- --- ## 1.5) Smoke, flare, explode, illuminate --- --- At the point a smoke, flare, explosion and illumination bomb can be triggered. Use the following methods: --- --- ### 1.5.1) Smoke --- --- * @{#POINT_VEC3.Smoke}(): To smoke the point in a certain color. --- * @{#POINT_VEC3.SmokeBlue}(): To smoke the point in blue. --- * @{#POINT_VEC3.SmokeRed}(): To smoke the point in red. --- * @{#POINT_VEC3.SmokeOrange}(): To smoke the point in orange. --- * @{#POINT_VEC3.SmokeWhite}(): To smoke the point in white. --- * @{#POINT_VEC3.SmokeGreen}(): To smoke the point in green. --- --- ### 1.5.2) Flare --- --- * @{#POINT_VEC3.Flare}(): To flare the point in a certain color. --- * @{#POINT_VEC3.FlareRed}(): To flare the point in red. --- * @{#POINT_VEC3.FlareYellow}(): To flare the point in yellow. --- * @{#POINT_VEC3.FlareWhite}(): To flare the point in white. --- * @{#POINT_VEC3.FlareGreen}(): To flare the point in green. --- --- ### 1.5.3) Explode --- --- * @{#POINT_VEC3.Explosion}(): To explode the point with a certain intensity. --- --- ### 1.5.4) Illuminate --- --- * @{#POINT_VEC3.IlluminationBomb}(): To illuminate the 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_VEC2 instance can be created with: --- --- * @{Point#POINT_VEC2.New}(): a 2D point, taking an additional height parameter. --- * @{Point#POINT_VEC2.NewFromVec2}(): a 2D point created from a @{DCSTypes#Vec2}. --- --- ## 1.2) Manupulate the X, Altitude, Y coordinates of the 2D point --- --- A POINT_VEC2 class works in 2D space, with an altitude setting. It contains internally an X, Altitude, Y coordinate. --- Methods exist to manupulate these coordinates. --- --- The current X, Altitude, Y axis can be retrieved with the methods @{#POINT_VEC2.GetX}(), @{#POINT_VEC2.GetAlt}(), @{#POINT_VEC2.GetY}() respectively. --- The methods @{#POINT_VEC2.SetX}(), @{#POINT_VEC2.SetAlt}(), @{#POINT_VEC2.SetY}() change the respective axis with a new value. --- The current axis values can be changed by using the methods @{#POINT_VEC2.AddX}(), @{#POINT_VEC2.AddAlt}(), @{#POINT_VEC2.AddY}() --- to add or substract a value from the current respective axis value. --- Note that the Set and Add methods return the current POINT_VEC2 object, so these manipulation methods can be chained... For example: --- --- local Vec2 = PointVec2:AddX( 100 ):AddY( 2000 ):GetVec2() --- --- === --- --- **API CHANGE HISTORY** --- ====================== --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- Hereby the change log: --- --- 2017-03-03: POINT\_VEC3:**Explosion( ExplosionIntensity )** added. --- 2017-03-03: POINT\_VEC3:**IlluminationBomb()** added. --- --- 2017-02-18: POINT\_VEC3:**NewFromVec2( Vec2, LandHeightAdd )** added. --- --- 2016-08-12: POINT\_VEC3:**Translate( Distance, Angle )** added. --- --- 2016-08-06: Made PointVec3 and Vec3, PointVec2 and Vec2 terminology used in the code consistent. --- --- * Replaced method _Point_Vec3() to **Vec3**() where the code manages a Vec3. Replaced all references to the method. --- * Replaced method _Point_Vec2() to **Vec2**() where the code manages a Vec2. Replaced all references to the method. --- * Replaced method Random_Point_Vec3() to **RandomVec3**() where the code manages a Vec3. Replaced all references to the method. --- . --- === --- --- ### Authors: --- --- * FlightControl : Design & Programming --- --- ### Contributions: --- --- @module Point - ---- The POINT_VEC3 class --- @type POINT_VEC3 --- @field #number x The x coordinate in 3D space. --- @field #number y The y coordinate in 3D space. --- @field #number z The z coordiante in 3D space. --- @field Utilities.Utils#SMOKECOLOR SmokeColor --- @field Utilities.Utils#FLARECOLOR FlareColor --- @field #POINT_VEC3.RoutePointAltType RoutePointAltType --- @field #POINT_VEC3.RoutePointType RoutePointType --- @field #POINT_VEC3.RoutePointAction RoutePointAction --- @extends Core.Base#BASE -POINT_VEC3 = { - ClassName = "POINT_VEC3", - Metric = true, - RoutePointAltType = { - BARO = "BARO", - }, - RoutePointType = { - TakeOffParking = "TakeOffParking", - TurningPoint = "Turning Point", - }, - RoutePointAction = { - FromParkingArea = "From Parking Area", - TurningPoint = "Turning Point", - }, -} - ---- The POINT_VEC2 class --- @type POINT_VEC2 --- @field Dcs.DCSTypes#Distance x The x coordinate in meters. --- @field Dcs.DCSTypes#Distance y the y coordinate in meters. --- @extends Core.Point#POINT_VEC3 -POINT_VEC2 = { - ClassName = "POINT_VEC2", -} - - -do -- POINT_VEC3 - ---- RoutePoint AltTypes --- @type POINT_VEC3.RoutePointAltType --- @field BARO "BARO" - ---- RoutePoint Types --- @type POINT_VEC3.RoutePointType --- @field TakeOffParking "TakeOffParking" --- @field TurningPoint "Turning Point" - ---- RoutePoint Actions --- @type POINT_VEC3.RoutePointAction --- @field FromParkingArea "From Parking Area" --- @field TurningPoint "Turning Point" - --- Constructor. - ---- Create a new POINT_VEC3 object. --- @param #POINT_VEC3 self --- @param Dcs.DCSTypes#Distance x The x coordinate of the Vec3 point, pointing to the North. --- @param Dcs.DCSTypes#Distance y The y coordinate of the Vec3 point, pointing Upwards. --- @param Dcs.DCSTypes#Distance z The z coordinate of the Vec3 point, pointing to the Right. --- @return Core.Point#POINT_VEC3 self -function POINT_VEC3:New( x, y, z ) - - local self = BASE:Inherit( self, BASE:New() ) - self.x = x - self.y = y - self.z = z - - return self -end - ---- Create a new POINT_VEC3 object from Vec2 coordinates. --- @param #POINT_VEC3 self --- @param Dcs.DCSTypes#Vec2 Vec2 The Vec2 point. --- @return Core.Point#POINT_VEC3 self -function POINT_VEC3:NewFromVec2( Vec2, LandHeightAdd ) - - local LandHeight = land.getHeight( Vec2 ) - - LandHeightAdd = LandHeightAdd or 0 - LandHeight = LandHeight + LandHeightAdd - - self = self:New( Vec2.x, LandHeight, Vec2.y ) - - self:F2( self ) - - return self -end - ---- Create a new POINT_VEC3 object from Vec3 coordinates. --- @param #POINT_VEC3 self --- @param Dcs.DCSTypes#Vec3 Vec3 The Vec3 point. --- @return Core.Point#POINT_VEC3 self -function POINT_VEC3:NewFromVec3( Vec3 ) - - self = self:New( Vec3.x, Vec3.y, Vec3.z ) - self:F2( self ) - return self -end - - ---- Return the coordinates of the POINT_VEC3 in Vec3 format. --- @param #POINT_VEC3 self --- @return Dcs.DCSTypes#Vec3 The Vec3 coodinate. -function POINT_VEC3:GetVec3() - return { x = self.x, y = self.y, z = self.z } -end - ---- Return the coordinates of the POINT_VEC3 in Vec2 format. --- @param #POINT_VEC3 self --- @return Dcs.DCSTypes#Vec2 The Vec2 coodinate. -function POINT_VEC3:GetVec2() - return { x = self.x, y = self.z } -end - - ---- Return the x coordinate of the POINT_VEC3. --- @param #POINT_VEC3 self --- @return #number The x coodinate. -function POINT_VEC3:GetX() - return self.x -end - ---- Return the y coordinate of the POINT_VEC3. --- @param #POINT_VEC3 self --- @return #number The y coodinate. -function POINT_VEC3:GetY() - return self.y -end - ---- Return the z coordinate of the POINT_VEC3. --- @param #POINT_VEC3 self --- @return #number The z coodinate. -function POINT_VEC3:GetZ() - return self.z -end - ---- Set the x coordinate of the POINT_VEC3. --- @param #POINT_VEC3 self --- @param #number x The x coordinate. --- @return #POINT_VEC3 -function POINT_VEC3:SetX( x ) - self.x = x - return self -end - ---- Set the y coordinate of the POINT_VEC3. --- @param #POINT_VEC3 self --- @param #number y The y coordinate. --- @return #POINT_VEC3 -function POINT_VEC3:SetY( y ) - self.y = y - return self -end - ---- Set the z coordinate of the POINT_VEC3. --- @param #POINT_VEC3 self --- @param #number z The z coordinate. --- @return #POINT_VEC3 -function POINT_VEC3:SetZ( z ) - self.z = z - return self -end - ---- Add to the x coordinate of the POINT_VEC3. --- @param #POINT_VEC3 self --- @param #number x The x coordinate value to add to the current x coodinate. --- @return #POINT_VEC3 -function POINT_VEC3:AddX( x ) - self.x = self.x + x - return self -end - ---- Add to the y coordinate of the POINT_VEC3. --- @param #POINT_VEC3 self --- @param #number y The y coordinate value to add to the current y coodinate. --- @return #POINT_VEC3 -function POINT_VEC3:AddY( y ) - self.y = self.y + y - return self -end - ---- Add to the z coordinate of the POINT_VEC3. --- @param #POINT_VEC3 self --- @param #number z The z coordinate value to add to the current z coodinate. --- @return #POINT_VEC3 -function POINT_VEC3:AddZ( z ) - self.z = self.z +z - return self -end - ---- Return a random Vec2 within an Outer Radius and optionally NOT within an Inner Radius of the POINT_VEC3. --- @param #POINT_VEC3 self --- @param Dcs.DCSTypes#Distance OuterRadius --- @param Dcs.DCSTypes#Distance InnerRadius --- @return Dcs.DCSTypes#Vec2 Vec2 -function POINT_VEC3:GetRandomVec2InRadius( OuterRadius, InnerRadius ) - self:F2( { OuterRadius, InnerRadius } ) - - local Theta = 2 * math.pi * math.random() - local Radials = math.random() + math.random() - if Radials > 1 then - Radials = 2 - Radials - end - - local RadialMultiplier - if InnerRadius and InnerRadius <= OuterRadius then - RadialMultiplier = ( OuterRadius - InnerRadius ) * Radials + InnerRadius - else - RadialMultiplier = OuterRadius * Radials - end - - local RandomVec2 - if OuterRadius > 0 then - RandomVec2 = { x = math.cos( Theta ) * RadialMultiplier + self:GetX(), y = math.sin( Theta ) * RadialMultiplier + self:GetZ() } - else - RandomVec2 = { x = self:GetX(), y = self:GetZ() } - end - - return RandomVec2 -end - ---- Return a random POINT_VEC2 within an Outer Radius and optionally NOT within an Inner Radius of the POINT_VEC3. --- @param #POINT_VEC3 self --- @param Dcs.DCSTypes#Distance OuterRadius --- @param Dcs.DCSTypes#Distance InnerRadius --- @return #POINT_VEC2 -function POINT_VEC3:GetRandomPointVec2InRadius( OuterRadius, InnerRadius ) - self:F2( { OuterRadius, InnerRadius } ) - - return POINT_VEC2:NewFromVec2( self:GetRandomVec2InRadius( OuterRadius, InnerRadius ) ) -end - ---- Return a random Vec3 within an Outer Radius and optionally NOT within an Inner Radius of the POINT_VEC3. --- @param #POINT_VEC3 self --- @param Dcs.DCSTypes#Distance OuterRadius --- @param Dcs.DCSTypes#Distance InnerRadius --- @return Dcs.DCSTypes#Vec3 Vec3 -function POINT_VEC3:GetRandomVec3InRadius( OuterRadius, InnerRadius ) - - local RandomVec2 = self:GetRandomVec2InRadius( OuterRadius, InnerRadius ) - local y = self:GetY() + math.random( InnerRadius, OuterRadius ) - local RandomVec3 = { x = RandomVec2.x, y = y, z = RandomVec2.z } - - return RandomVec3 -end - ---- Return a random POINT_VEC3 within an Outer Radius and optionally NOT within an Inner Radius of the POINT_VEC3. --- @param #POINT_VEC3 self --- @param Dcs.DCSTypes#Distance OuterRadius --- @param Dcs.DCSTypes#Distance InnerRadius --- @return #POINT_VEC3 -function POINT_VEC3:GetRandomPointVec3InRadius( OuterRadius, InnerRadius ) - - return POINT_VEC3:NewFromVec3( self:GetRandomVec3InRadius( OuterRadius, InnerRadius ) ) -end - - ---- Return a direction vector Vec3 from POINT_VEC3 to the POINT_VEC3. --- @param #POINT_VEC3 self --- @param #POINT_VEC3 TargetPointVec3 The target POINT_VEC3. --- @return Dcs.DCSTypes#Vec3 DirectionVec3 The direction vector in Vec3 format. -function POINT_VEC3:GetDirectionVec3( TargetPointVec3 ) - return { x = TargetPointVec3:GetX() - self:GetX(), y = TargetPointVec3:GetY() - self:GetY(), z = TargetPointVec3:GetZ() - self:GetZ() } -end - ---- Get a correction in radians of the real magnetic north of the POINT_VEC3. --- @param #POINT_VEC3 self --- @return #number CorrectionRadians The correction in radians. -function POINT_VEC3:GetNorthCorrectionRadians() - local TargetVec3 = self:GetVec3() - local lat, lon = coord.LOtoLL(TargetVec3) - local north_posit = coord.LLtoLO(lat + 1, lon) - return math.atan2( north_posit.z - TargetVec3.z, north_posit.x - TargetVec3.x ) -end - - ---- Return a direction in radians from the POINT_VEC3 using a direction vector in Vec3 format. --- @param #POINT_VEC3 self --- @param Dcs.DCSTypes#Vec3 DirectionVec3 The direction vector in Vec3 format. --- @return #number DirectionRadians The direction in radians. -function POINT_VEC3:GetDirectionRadians( DirectionVec3 ) - local DirectionRadians = math.atan2( DirectionVec3.z, DirectionVec3.x ) - --DirectionRadians = DirectionRadians + self:GetNorthCorrectionRadians() - if DirectionRadians < 0 then - DirectionRadians = DirectionRadians + 2 * math.pi -- put dir in range of 0 to 2*pi ( the full circle ) - end - return DirectionRadians -end - ---- Return the 2D distance in meters between the target POINT_VEC3 and the POINT_VEC3. --- @param #POINT_VEC3 self --- @param #POINT_VEC3 TargetPointVec3 The target POINT_VEC3. --- @return Dcs.DCSTypes#Distance Distance The distance in meters. -function POINT_VEC3:Get2DDistance( TargetPointVec3 ) - local TargetVec3 = TargetPointVec3:GetVec3() - local SourceVec3 = self:GetVec3() - return ( ( TargetVec3.x - SourceVec3.x ) ^ 2 + ( TargetVec3.z - SourceVec3.z ) ^ 2 ) ^ 0.5 -end - ---- Return the 3D distance in meters between the target POINT_VEC3 and the POINT_VEC3. --- @param #POINT_VEC3 self --- @param #POINT_VEC3 TargetPointVec3 The target POINT_VEC3. --- @return Dcs.DCSTypes#Distance Distance The distance in meters. -function POINT_VEC3:Get3DDistance( TargetPointVec3 ) - local TargetVec3 = TargetPointVec3:GetVec3() - local SourceVec3 = self:GetVec3() - return ( ( TargetVec3.x - SourceVec3.x ) ^ 2 + ( TargetVec3.y - SourceVec3.y ) ^ 2 + ( TargetVec3.z - SourceVec3.z ) ^ 2 ) ^ 0.5 -end - ---- Provides a Bearing / Range string --- @param #POINT_VEC3 self --- @param #number AngleRadians The angle in randians --- @param #number Distance The distance --- @return #string The BR Text -function POINT_VEC3:ToStringBR( AngleRadians, Distance ) - - AngleRadians = UTILS.Round( UTILS.ToDegree( AngleRadians ), 0 ) - if self:IsMetric() then - Distance = UTILS.Round( Distance / 1000, 2 ) - else - Distance = UTILS.Round( UTILS.MetersToNM( Distance ), 2 ) - end - - local s = string.format( '%03d', AngleRadians ) .. ' for ' .. Distance - - s = s .. self:GetAltitudeText() -- When the POINT is a VEC2, there will be no altitude shown. - - return s -end - ---- Provides a Bearing / Range string --- @param #POINT_VEC3 self --- @param #number AngleRadians The angle in randians --- @param #number Distance The distance --- @return #string The BR Text -function POINT_VEC3:ToStringLL( acc, DMS ) - - acc = acc or 3 - local lat, lon = coord.LOtoLL( self:GetVec3() ) - return UTILS.tostringLL(lat, lon, acc, DMS) -end - ---- Return the altitude text of the POINT_VEC3. --- @param #POINT_VEC3 self --- @return #string Altitude text. -function POINT_VEC3:GetAltitudeText() - if self:IsMetric() then - return ' at ' .. UTILS.Round( self:GetY(), 0 ) - else - return ' at ' .. UTILS.Round( UTILS.MetersToFeet( self:GetY() ), 0 ) - end -end - ---- Return a BR string from a POINT_VEC3 to the POINT_VEC3. --- @param #POINT_VEC3 self --- @param #POINT_VEC3 TargetPointVec3 The target POINT_VEC3. --- @return #string The BR text. -function POINT_VEC3:GetBRText( TargetPointVec3 ) - local DirectionVec3 = self:GetDirectionVec3( TargetPointVec3 ) - local AngleRadians = self:GetDirectionRadians( DirectionVec3 ) - local Distance = self:Get2DDistance( TargetPointVec3 ) - return self:ToStringBR( AngleRadians, Distance ) -end - ---- Sets the POINT_VEC3 metric or NM. --- @param #POINT_VEC3 self --- @param #boolean Metric true means metric, false means NM. -function POINT_VEC3:SetMetric( Metric ) - self.Metric = Metric -end - ---- Gets if the POINT_VEC3 is metric or NM. --- @param #POINT_VEC3 self --- @return #boolean Metric true means metric, false means NM. -function POINT_VEC3:IsMetric() - return self.Metric -end - ---- Add a Distance in meters from the POINT_VEC3 horizontal plane, with the given angle, and calculate the new POINT_VEC3. --- @param #POINT_VEC3 self --- @param Dcs.DCSTypes#Distance Distance The Distance to be added in meters. --- @param Dcs.DCSTypes#Angle Angle The Angle in degrees. --- @return #POINT_VEC3 The new calculated POINT_VEC3. -function POINT_VEC3:Translate( Distance, Angle ) - local SX = self:GetX() - local SZ = self:GetZ() - local Radians = Angle / 180 * math.pi - local TX = Distance * math.cos( Radians ) + SX - local TZ = Distance * math.sin( Radians ) + SZ - - return POINT_VEC3:New( TX, self:GetY(), TZ ) -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 Dcs.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:GetX() - RoutePoint.y = self:GetZ() - RoutePoint.alt = self:GetY() - 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 - ---- Build an ground type route point. --- @param #POINT_VEC3 self --- @param Dcs.DCSTypes#Speed Speed Speed in km/h. --- @param #POINT_VEC3.RoutePointAction Formation The route point Formation. --- @return #table The route point. -function POINT_VEC3:RoutePointGround( Speed, Formation ) - self:F2( { Formation, Speed } ) - - local RoutePoint = {} - RoutePoint.x = self:GetX() - RoutePoint.y = self:GetZ() - - RoutePoint.action = Formation or "" - - - 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 - ---- Creates an explosion at the point of a certain intensity. --- @param #POINT_VEC3 self --- @param #number ExplosionIntensity -function POINT_VEC3:Explosion( ExplosionIntensity ) - self:F2( { ExplosionIntensity } ) - trigger.action.explosion( self:GetVec3(), ExplosionIntensity ) -end - ---- Creates an illumination bomb at the point. --- @param #POINT_VEC3 self -function POINT_VEC3:IlluminationBomb() - self:F2() - trigger.action.illuminationBomb( self:GetVec3() ) -end - - ---- Smokes the point in a color. --- @param #POINT_VEC3 self --- @param Utilities.Utils#SMOKECOLOR SmokeColor -function POINT_VEC3:Smoke( SmokeColor ) - self:F2( { SmokeColor } ) - trigger.action.smoke( self:GetVec3(), SmokeColor ) -end - ---- Smoke the POINT_VEC3 Green. --- @param #POINT_VEC3 self -function POINT_VEC3:SmokeGreen() - self:F2() - self:Smoke( SMOKECOLOR.Green ) -end - ---- Smoke the POINT_VEC3 Red. --- @param #POINT_VEC3 self -function POINT_VEC3:SmokeRed() - self:F2() - self:Smoke( SMOKECOLOR.Red ) -end - ---- Smoke the POINT_VEC3 White. --- @param #POINT_VEC3 self -function POINT_VEC3:SmokeWhite() - self:F2() - self:Smoke( SMOKECOLOR.White ) -end - ---- Smoke the POINT_VEC3 Orange. --- @param #POINT_VEC3 self -function POINT_VEC3:SmokeOrange() - self:F2() - self:Smoke( SMOKECOLOR.Orange ) -end - ---- Smoke the POINT_VEC3 Blue. --- @param #POINT_VEC3 self -function POINT_VEC3:SmokeBlue() - self:F2() - self:Smoke( SMOKECOLOR.Blue ) -end - ---- Flares the point in a color. --- @param #POINT_VEC3 self --- @param Utilities.Utils#FLARECOLOR FlareColor --- @param Dcs.DCSTypes#Azimuth (optional) Azimuth The azimuth of the flare direction. The default azimuth is 0. -function POINT_VEC3:Flare( FlareColor, Azimuth ) - self:F2( { FlareColor } ) - trigger.action.signalFlare( self:GetVec3(), FlareColor, Azimuth and Azimuth or 0 ) -end - ---- Flare the POINT_VEC3 White. --- @param #POINT_VEC3 self --- @param Dcs.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( FLARECOLOR.White, Azimuth ) -end - ---- Flare the POINT_VEC3 Yellow. --- @param #POINT_VEC3 self --- @param Dcs.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( FLARECOLOR.Yellow, Azimuth ) -end - ---- Flare the POINT_VEC3 Green. --- @param #POINT_VEC3 self --- @param Dcs.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( FLARECOLOR.Green, Azimuth ) -end - ---- Flare the POINT_VEC3 Red. --- @param #POINT_VEC3 self -function POINT_VEC3:FlareRed( Azimuth ) - self:F2( Azimuth ) - self:Flare( FLARECOLOR.Red, Azimuth ) -end - -end - -do -- POINT_VEC2 - - - ---- POINT_VEC2 constructor. --- @param #POINT_VEC2 self --- @param Dcs.DCSTypes#Distance x The x coordinate of the Vec3 point, pointing to the North. --- @param Dcs.DCSTypes#Distance y The y coordinate of the Vec3 point, pointing to the Right. --- @param Dcs.DCSTypes#Distance LandHeightAdd (optional) The default height if required to be evaluated will be the land height of the x, y coordinate. You can specify an extra height to be added to the land height. --- @return Core.Point#POINT_VEC2 -function POINT_VEC2:New( x, y, LandHeightAdd ) - - local LandHeight = land.getHeight( { ["x"] = x, ["y"] = y } ) - - LandHeightAdd = LandHeightAdd or 0 - LandHeight = LandHeight + LandHeightAdd - - self = BASE:Inherit( self, POINT_VEC3:New( x, LandHeight, y ) ) - self:F2( self ) - - return self -end - ---- Create a new POINT_VEC2 object from Vec2 coordinates. --- @param #POINT_VEC2 self --- @param Dcs.DCSTypes#Vec2 Vec2 The Vec2 point. --- @return Core.Point#POINT_VEC2 self -function POINT_VEC2:NewFromVec2( Vec2, LandHeightAdd ) - - local LandHeight = land.getHeight( Vec2 ) - - LandHeightAdd = LandHeightAdd or 0 - LandHeight = LandHeight + LandHeightAdd - - self = BASE:Inherit( self, POINT_VEC3:New( Vec2.x, LandHeight, Vec2.y ) ) - self:F2( self ) - - return self -end - ---- Create a new POINT_VEC2 object from Vec3 coordinates. --- @param #POINT_VEC2 self --- @param Dcs.DCSTypes#Vec3 Vec3 The Vec3 point. --- @return Core.Point#POINT_VEC2 self -function POINT_VEC2:NewFromVec3( Vec3 ) - - local self = BASE:Inherit( self, BASE:New() ) - local Vec2 = { x = Vec3.x, y = Vec3.z } - - local LandHeight = land.getHeight( Vec2 ) - - self = BASE:Inherit( self, POINT_VEC3:New( Vec2.x, LandHeight, Vec2.y ) ) - self:F2( self ) - - return self -end - ---- Return the x coordinate of the POINT_VEC2. --- @param #POINT_VEC2 self --- @return #number The x coodinate. -function POINT_VEC2:GetX() - return self.x -end - ---- Return the y coordinate of the POINT_VEC2. --- @param #POINT_VEC2 self --- @return #number The y coodinate. -function POINT_VEC2:GetY() - return self.z -end - ---- Return the altitude of the land at the POINT_VEC2. --- @param #POINT_VEC2 self --- @return #number The land altitude. -function POINT_VEC2:GetAlt() - return land.getHeight( { x = self.x, y = self.z } ) -end - ---- Set the x coordinate of the POINT_VEC2. --- @param #POINT_VEC2 self --- @param #number x The x coordinate. --- @return #POINT_VEC2 -function POINT_VEC2:SetX( x ) - self.x = x - return self -end - ---- Set the y coordinate of the POINT_VEC2. --- @param #POINT_VEC2 self --- @param #number y The y coordinate. --- @return #POINT_VEC2 -function POINT_VEC2:SetY( y ) - self.z = y - return self -end - ---- Set the altitude of the POINT_VEC2. --- @param #POINT_VEC2 self --- @param #number Altitude The land altitude. If nothing (nil) is given, then the current land altitude is set. --- @return #POINT_VEC2 -function POINT_VEC2:SetAlt( Altitude ) - self.y = Altitude or land.getHeight( { x = self.x, y = self.z } ) - return self -end - ---- Add to the x coordinate of the POINT_VEC2. --- @param #POINT_VEC2 self --- @param #number x The x coordinate. --- @return #POINT_VEC2 -function POINT_VEC2:AddX( x ) - self.x = self.x + x - return self -end - ---- Add to the y coordinate of the POINT_VEC2. --- @param #POINT_VEC2 self --- @param #number y The y coordinate. --- @return #POINT_VEC2 -function POINT_VEC2:AddY( y ) - self.z = self.z + y - return self -end - ---- Add to the current land height an altitude. --- @param #POINT_VEC2 self --- @param #number Altitude The Altitude to add. If nothing (nil) is given, then the current land altitude is set. --- @return #POINT_VEC2 -function POINT_VEC2:AddAlt( Altitude ) - self.y = land.getHeight( { x = self.x, y = self.z } ) + Altitude or 0 - return self -end - - - ---- Calculate the distance from a reference @{#POINT_VEC2}. --- @param #POINT_VEC2 self --- @param #POINT_VEC2 PointVec2Reference The reference @{#POINT_VEC2}. --- @return Dcs.DCSTypes#Distance The distance from the reference @{#POINT_VEC2} in meters. -function POINT_VEC2:DistanceFromPointVec2( PointVec2Reference ) - self:F2( PointVec2Reference ) - - local Distance = ( ( PointVec2Reference:GetX() - self:GetX() ) ^ 2 + ( PointVec2Reference:GetY() - self:GetY() ) ^2 ) ^0.5 - - self:T2( Distance ) - return Distance -end - ---- Calculate the distance from a reference @{DCSTypes#Vec2}. --- @param #POINT_VEC2 self --- @param Dcs.DCSTypes#Vec2 Vec2Reference The reference @{DCSTypes#Vec2}. --- @return Dcs.DCSTypes#Distance The distance from the reference @{DCSTypes#Vec2} in meters. -function POINT_VEC2:DistanceFromVec2( Vec2Reference ) - self:F2( Vec2Reference ) - - local Distance = ( ( Vec2Reference.x - self:GetX() ) ^ 2 + ( Vec2Reference.y - self:GetY() ) ^2 ) ^0.5 - - self:T2( Distance ) - return Distance -end - - ---- Return no text for the altitude of the POINT_VEC2. --- @param #POINT_VEC2 self --- @return #string Empty string. -function POINT_VEC2:GetAltitudeText() - return '' -end - ---- Add a Distance in meters from the POINT_VEC2 orthonormal plane, with the given angle, and calculate the new POINT_VEC2. --- @param #POINT_VEC2 self --- @param Dcs.DCSTypes#Distance Distance The Distance to be added in meters. --- @param Dcs.DCSTypes#Angle Angle The Angle in degrees. --- @return #POINT_VEC2 The new calculated POINT_VEC2. -function POINT_VEC2:Translate( Distance, Angle ) - local SX = self:GetX() - local SY = self:GetY() - local Radians = Angle / 180 * math.pi - local TX = Distance * math.cos( Radians ) + SX - local TY = Distance * math.sin( Radians ) + SY - - return POINT_VEC2:New( TX, TY ) -end - -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 Core.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 - if MessageCategory:sub(-1) ~= "\n" then - self.MessageCategory = MessageCategory .. ": " - else - self.MessageCategory = MessageCategory:sub( 1, -2 ) .. ":\n" - end - else - self.MessageCategory = "" - end - - self.MessageDuration = MessageDuration or 5 - 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 Wrapper.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 a Group. --- @param #MESSAGE self --- @param Wrapper.Group#GROUP Group is the Group. --- @return #MESSAGE -function MESSAGE:ToGroup( Group ) - self:F( Group.GroupName ) - - if Group then - - self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration ) - trigger.action.outTextForGroup( Group:GetID(), self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration ) - 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 a Coalition if the given Condition is true. --- @param #MESSAGE self --- @param CoalitionSide needs to be filled out by the defined structure of the standard scripting engine @{coalition.side}. --- @return #MESSAGE -function MESSAGE:ToCoalitionIf( CoalitionSide, Condition ) - self:F( CoalitionSide ) - - if Condition and Condition == true then - self:ToCoalition( CoalitionSide ) - end - - return self -end - ---- Sends a MESSAGE to all players. --- @param #MESSAGE self --- @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 - - ---- Sends a MESSAGE to all players if the given Condition is true. --- @param #MESSAGE self --- @return #MESSAGE -function MESSAGE:ToAllIf( Condition ) - - if Condition and Condition == true then - self:ToCoalition( coalition.side.RED ) - self:ToCoalition( coalition.side.BLUE ) - end - - 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 ) --- ---- This module contains the **FSM** (**F**inite **S**tate **M**achine) class and derived **FSM\_** classes. --- ## Finite State Machines (FSM) are design patterns allowing efficient (long-lasting) processes and workflows. --- --- ![Banner Image](..\Presentations\FSM\Dia1.JPG) --- --- === --- --- A FSM can only be in one of a finite number of states. --- The machine is in only one state at a time; the state it is in at any given time is called the **current state**. --- It can change from one state to another when initiated by an **__internal__ or __external__ triggering event**, which is called a **transition**. --- An **FSM implementation** is defined by **a list of its states**, **its initial state**, and **the triggering events** for **each possible transition**. --- An FSM implementation is composed out of **two parts**, a set of **state transition rules**, and an implementation set of **state transition handlers**, implementing those transitions. --- --- The FSM class supports a **hierarchical implementation of a Finite State Machine**, --- that is, it allows to **embed existing FSM implementations in a master FSM**. --- FSM hierarchies allow for efficient FSM re-use, **not having to re-invent the wheel every time again** when designing complex processes. --- --- ![Workflow Example](..\Presentations\FSM\Dia2.JPG) --- --- The above diagram shows a graphical representation of a FSM implementation for a **Task**, which guides a Human towards a Zone, --- orders him to destroy x targets and account the results. --- Other examples of ready made FSM could be: --- --- * route a plane to a zone flown by a human --- * detect targets by an AI and report to humans --- * account for destroyed targets by human players --- * handle AI infantry to deploy from or embark to a helicopter or airplane or vehicle --- * let an AI patrol a zone --- --- The **MOOSE framework** uses extensively the FSM class and derived FSM\_ classes, --- because **the goal of MOOSE is to simplify mission design complexity for mission building**. --- By efficiently utilizing the FSM class and derived classes, MOOSE allows mission designers to quickly build processes. --- **Ready made FSM-based implementations classes** exist within the MOOSE framework that **can easily be re-used, --- and tailored** by mission designers through **the implementation of Transition Handlers**. --- Each of these FSM implementation classes start either with: --- --- * an acronym **AI\_**, which indicates an FSM implementation directing **AI controlled** @{GROUP} and/or @{UNIT}. These AI\_ classes derive the @{#FSM_CONTROLLABLE} class. --- * an acronym **TASK\_**, which indicates an FSM implementation executing a @{TASK} executed by Groups of players. These TASK\_ classes derive the @{#FSM_TASK} class. --- * an acronym **ACT\_**, which indicates an Sub-FSM implementation, directing **Humans actions** that need to be done in a @{TASK}, seated in a @{CLIENT} (slot) or a @{UNIT} (CA join). These ACT\_ classes derive the @{#FSM_PROCESS} class. --- --- Detailed explanations and API specifics are further below clarified and FSM derived class specifics are described in those class documentation sections. --- --- ##__Dislaimer:__ --- The FSM class development is based on a finite state machine implementation made by Conroy Kyle. --- The state machine can be found on [github](https://github.com/kyleconroy/lua-state-machine) --- I've reworked this development (taken the concept), and created a **hierarchical state machine** out of it, embedded within the DCS simulator. --- Additionally, I've added extendability and created an API that allows seamless FSM implementation. --- --- === --- --- # 1) @{#FSM} class, extends @{Base#BASE} --- --- ![Transition Rules and Transition Handlers and Event Triggers](..\Presentations\FSM\Dia3.JPG) --- --- The FSM class is the base class of all FSM\_ derived classes. It implements the main functionality to define and execute Finite State Machines. --- The derived FSM\_ classes extend the Finite State Machine functionality to run a workflow process for a specific purpose or component. --- --- Finite State Machines have **Transition Rules**, **Transition Handlers** and **Event Triggers**. --- --- The **Transition Rules** define the "Process Flow Boundaries", that is, --- the path that can be followed hopping from state to state upon triggered events. --- If an event is triggered, and there is no valid path found for that event, --- an error will be raised and the FSM will stop functioning. --- --- The **Transition Handlers** are special methods that can be defined by the mission designer, following a defined syntax. --- If the FSM object finds a method of such a handler, then the method will be called by the FSM, passing specific parameters. --- The method can then define its own custom logic to implement the FSM workflow, and to conduct other actions. --- --- The **Event Triggers** are methods that are defined by the FSM, which the mission designer can use to implement the workflow. --- Most of the time, these Event Triggers are used within the Transition Handler methods, so that a workflow is created running through the state machine. --- --- As explained above, a FSM supports **Linear State Transitions** and **Hierarchical State Transitions**, and both can be mixed to make a comprehensive FSM implementation. --- The below documentation has a seperate chapter explaining both transition modes, taking into account the **Transition Rules**, **Transition Handlers** and **Event Triggers**. --- --- ## 1.1) FSM Linear Transitions --- --- Linear Transitions are Transition Rules allowing an FSM to transition from one or multiple possible **From** state(s) towards a **To** state upon a Triggered **Event**. --- The Lineair transition rule evaluation will always be done from the **current state** of the FSM. --- If no valid Transition Rule can be found in the FSM, the FSM will log an error and stop. --- --- ### 1.1.1) FSM Transition Rules --- --- The FSM has transition rules that it follows and validates, as it walks the process. --- These rules define when an FSM can transition from a specific state towards an other specific state upon a triggered event. --- --- The method @{#FSM.AddTransition}() specifies a new possible Transition Rule for the FSM. --- --- The initial state can be defined using the method @{#FSM.SetStartState}(). The default start state of an FSM is "None". --- --- Find below an example of a Linear Transition Rule definition for an FSM. --- --- local Fsm3Switch = FSM:New() -- #FsmDemo --- FsmSwitch:SetStartState( "Off" ) --- FsmSwitch:AddTransition( "Off", "SwitchOn", "On" ) --- FsmSwitch:AddTransition( "Off", "SwitchMiddle", "Middle" ) --- FsmSwitch:AddTransition( "On", "SwitchOff", "Off" ) --- FsmSwitch:AddTransition( "Middle", "SwitchOff", "Off" ) --- --- The above code snippet models a 3-way switch Linear Transition: --- --- * It can be switched **On** by triggering event **SwitchOn**. --- * It can be switched to the **Middle** position, by triggering event **SwitchMiddle**. --- * It can be switched **Off** by triggering event **SwitchOff**. --- * Note that once the Switch is **On** or **Middle**, it can only be switched **Off**. --- --- ### Some additional comments: --- --- Note that Linear Transition Rules **can be declared in a few variations**: --- --- * The From states can be **a table of strings**, indicating that the transition rule will be valid **if the current state** of the FSM will be **one of the given From states**. --- * The From state can be a **"*"**, indicating that **the transition rule will always be valid**, regardless of the current state of the FSM. --- --- The below code snippet shows how the two last lines can be rewritten and consensed. --- --- FsmSwitch:AddTransition( { "On", "Middle" }, "SwitchOff", "Off" ) --- --- ### 1.1.2) Transition Handling --- --- ![Transition Handlers](..\Presentations\FSM\Dia4.JPG) --- --- An FSM transitions in **4 moments** when an Event is being triggered and processed. --- The mission designer can define for each moment specific logic within methods implementations following a defined API syntax. --- These methods define the flow of the FSM process; because in those methods the FSM Internal Events will be triggered. --- --- * To handle **State** transition moments, create methods starting with OnLeave or OnEnter concatenated with the State name. --- * To handle **Event** transition moments, create methods starting with OnBefore or OnAfter concatenated with the Event name. --- --- **The OnLeave and OnBefore transition methods may return false, which will cancel the transition!** --- --- Transition Handler methods need to follow the above specified naming convention, but are also passed parameters from the FSM. --- These parameters are on the correct order: From, Event, To: --- --- * From = A string containing the From state. --- * Event = A string containing the Event name that was triggered. --- * To = A string containing the To state. --- --- On top, each of these methods can have a variable amount of parameters passed. See the example in section [1.1.3](#1.1.3\)-event-triggers). --- --- ### 1.1.3) Event Triggers --- --- ![Event Triggers](..\Presentations\FSM\Dia5.JPG) --- --- The FSM creates for each Event two **Event Trigger methods**. --- There are two modes how Events can be triggered, which is **synchronous** and **asynchronous**: --- --- * The method **FSM:Event()** triggers an Event that will be processed **synchronously** or **immediately**. --- * The method **FSM:__Event( __seconds__ )** triggers an Event that will be processed **asynchronously** over time, waiting __x seconds__. --- --- The destinction between these 2 Event Trigger methods are important to understand. An asynchronous call will "log" the Event Trigger to be executed at a later time. --- Processing will just continue. Synchronous Event Trigger methods are useful to change states of the FSM immediately, but may have a larger processing impact. --- --- The following example provides a little demonstration on the difference between synchronous and asynchronous Event Triggering. --- --- function FSM:OnAfterEvent( From, Event, To, Amount ) --- self:T( { Amount = Amount } ) --- end --- --- local Amount = 1 --- FSM:__Event( 5, Amount ) --- --- Amount = Amount + 1 --- FSM:Event( Text, Amount ) --- --- In this example, the **:OnAfterEvent**() Transition Handler implementation will get called when **Event** is being triggered. --- Before we go into more detail, let's look at the last 4 lines of the example. --- The last line triggers synchronously the **Event**, and passes Amount as a parameter. --- The 3rd last line of the example triggers asynchronously **Event**. --- Event will be processed after 5 seconds, and Amount is given as a parameter. --- --- The output of this little code fragment will be: --- --- * Amount = 2 --- * Amount = 2 --- --- Because ... When Event was asynchronously processed after 5 seconds, Amount was set to 2. So be careful when processing and passing values and objects in asynchronous processing! --- --- ### 1.1.4) Linear Transition Example --- --- This example is fully implemented in the MOOSE test mission on GITHUB: [FSM-100 - Transition Explanation](https://github.com/FlightControl-Master/MOOSE/blob/master/Moose%20Test%20Missions/FSM%20-%20Finite%20State%20Machine/FSM-100%20-%20Transition%20Explanation/FSM-100%20-%20Transition%20Explanation.lua) --- --- It models a unit standing still near Batumi, and flaring every 5 seconds while switching between a Green flare and a Red flare. --- The purpose of this example is not to show how exciting flaring is, but it demonstrates how a Linear Transition FSM can be build. --- Have a look at the source code. The source code is also further explained below in this section. --- --- The example creates a new FsmDemo object from class FSM. --- It will set the start state of FsmDemo to state **Green**. --- Two Linear Transition Rules are created, where upon the event **Switch**, --- the FsmDemo will transition from state **Green** to **Red** and from **Red** back to **Green**. --- --- ![Transition Example](..\Presentations\FSM\Dia6.JPG) --- --- local FsmDemo = FSM:New() -- #FsmDemo --- FsmDemo:SetStartState( "Green" ) --- FsmDemo:AddTransition( "Green", "Switch", "Red" ) --- FsmDemo:AddTransition( "Red", "Switch", "Green" ) --- --- In the above example, the FsmDemo could flare every 5 seconds a Green or a Red flare into the air. --- The next code implements this through the event handling method **OnAfterSwitch**. --- --- ![Transition Flow](..\Presentations\FSM\Dia7.JPG) --- --- function FsmDemo:OnAfterSwitch( From, Event, To, FsmUnit ) --- self:T( { From, Event, To, FsmUnit } ) --- --- if From == "Green" then --- FsmUnit:Flare(FLARECOLOR.Green) --- else --- if From == "Red" then --- FsmUnit:Flare(FLARECOLOR.Red) --- end --- end --- self:__Switch( 5, FsmUnit ) -- Trigger the next Switch event to happen in 5 seconds. --- end --- --- FsmDemo:__Switch( 5, FsmUnit ) -- Trigger the first Switch event to happen in 5 seconds. --- --- The OnAfterSwitch implements a loop. The last line of the code fragment triggers the Switch Event within 5 seconds. --- Upon the event execution (after 5 seconds), the OnAfterSwitch method is called of FsmDemo (cfr. the double point notation!!! ":"). --- The OnAfterSwitch method receives from the FSM the 3 transition parameter details ( From, Event, To ), --- and one additional parameter that was given when the event was triggered, which is in this case the Unit that is used within OnSwitchAfter. --- --- function FsmDemo:OnAfterSwitch( From, Event, To, FsmUnit ) --- --- For debugging reasons the received parameters are traced within the DCS.log. --- --- self:T( { From, Event, To, FsmUnit } ) --- --- The method will check if the From state received is either "Green" or "Red" and will flare the respective color from the FsmUnit. --- --- if From == "Green" then --- FsmUnit:Flare(FLARECOLOR.Green) --- else --- if From == "Red" then --- FsmUnit:Flare(FLARECOLOR.Red) --- end --- end --- --- It is important that the Switch event is again triggered, otherwise, the FsmDemo would stop working after having the first Event being handled. --- --- FsmDemo:__Switch( 5, FsmUnit ) -- Trigger the next Switch event to happen in 5 seconds. --- --- The below code fragment extends the FsmDemo, demonstrating multiple **From states declared as a table**, adding a **Linear Transition Rule**. --- The new event **Stop** will cancel the Switching process. --- The transition for event Stop can be executed if the current state of the FSM is either "Red" or "Green". --- --- local FsmDemo = FSM:New() -- #FsmDemo --- FsmDemo:SetStartState( "Green" ) --- FsmDemo:AddTransition( "Green", "Switch", "Red" ) --- FsmDemo:AddTransition( "Red", "Switch", "Green" ) --- FsmDemo:AddTransition( { "Red", "Green" }, "Stop", "Stopped" ) --- --- The transition for event Stop can also be simplified, as any current state of the FSM is valid. --- --- FsmDemo:AddTransition( "*", "Stop", "Stopped" ) --- --- So... When FsmDemo:Stop() is being triggered, the state of FsmDemo will transition from Red or Green to Stopped. --- And there is no transition handling method defined for that transition, thus, no new event is being triggered causing the FsmDemo process flow to halt. --- --- ## 1.5) FSM Hierarchical Transitions --- --- Hierarchical Transitions allow to re-use readily available and implemented FSMs. --- This becomes in very useful for mission building, where mission designers build complex processes and workflows, --- combining smaller FSMs to one single FSM. --- --- The FSM can embed **Sub-FSMs** that will execute and return **multiple possible Return (End) States**. --- Depending upon **which state is returned**, the main FSM can continue the flow **triggering specific events**. --- --- The method @{#FSM.AddProcess}() adds a new Sub-FSM to the FSM. --- --- ==== --- --- # **API CHANGE HISTORY** --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- YYYY-MM-DD: CLASS:**NewFunction**( Params ) replaces CLASS:_OldFunction_( Params ) --- YYYY-MM-DD: CLASS:**NewFunction( Params )** added --- --- Hereby the change log: --- --- * 2016-12-18: Released. --- --- === --- --- # **AUTHORS and CONTRIBUTIONS** --- --- ### Contributions: --- --- * [**Pikey**](https://forums.eagle.ru/member.php?u=62835): Review of documentation & advice for improvements. --- --- ### Authors: --- --- * [**FlightControl**](https://forums.eagle.ru/member.php?u=89536): Design & Programming & documentation. --- --- @module Fsm - -do -- FSM - - --- FSM class - -- @type FSM - -- @extends Core.Base#BASE - FSM = { - ClassName = "FSM", - } - - --- Creates a new FSM object. - -- @param #FSM self - -- @return #FSM - function FSM:New( FsmT ) - - -- Inherits from BASE - self = BASE:Inherit( self, BASE:New() ) - - self.options = options or {} - self.options.subs = self.options.subs or {} - self.current = self.options.initial or 'none' - self.Events = {} - self.subs = {} - self.endstates = {} - - self.Scores = {} - - self._StartState = "none" - self._Transitions = {} - self._Processes = {} - self._EndStates = {} - self._Scores = {} - self._EventSchedules = {} - - self.CallScheduler = SCHEDULER:New( self ) - - - return self - end - - - --- Sets the start state of the FSM. - -- @param #FSM self - -- @param #string State A string defining the start state. - function FSM:SetStartState( State ) - - self._StartState = State - self.current = State - end - - - --- Returns the start state of the FSM. - -- @param #FSM self - -- @return #string A string containing the start state. - function FSM:GetStartState() - - return self._StartState or {} - end - - --- Add a new transition rule to the FSM. - -- A transition rule defines when and if the FSM can transition from a state towards another state upon a triggered event. - -- @param #FSM self - -- @param #table From Can contain a string indicating the From state or a table of strings containing multiple From states. - -- @param #string Event The Event name. - -- @param #string To The To state. - function FSM:AddTransition( From, Event, To ) - - local Transition = {} - Transition.From = From - Transition.Event = Event - Transition.To = To - - self:T( Transition ) - - self._Transitions[Transition] = Transition - self:_eventmap( self.Events, Transition ) - end - - - --- Returns a table of the transition rules defined within the FSM. - -- @return #table - function FSM:GetTransitions() - - return self._Transitions or {} - end - - --- Set the default @{Process} template with key ProcessName providing the ProcessClass and the process object when it is assigned to a @{Controllable} by the task. - -- @param #FSM self - -- @param #table From Can contain a string indicating the From state or a table of strings containing multiple From states. - -- @param #string Event The Event name. - -- @param Core.Fsm#FSM_PROCESS Process An sub-process FSM. - -- @param #table ReturnEvents A table indicating for which returned events of the SubFSM which Event must be triggered in the FSM. - -- @return Core.Fsm#FSM_PROCESS The SubFSM. - function FSM:AddProcess( From, Event, Process, ReturnEvents ) - self:T( { From, Event, Process, ReturnEvents } ) - - local Sub = {} - Sub.From = From - Sub.Event = Event - Sub.fsm = Process - Sub.StartEvent = "Start" - Sub.ReturnEvents = ReturnEvents - - self._Processes[Sub] = Sub - - self:_submap( self.subs, Sub, nil ) - - self:AddTransition( From, Event, From ) - - return Process - end - - - --- Returns a table of the SubFSM rules defined within the FSM. - -- @return #table - function FSM:GetProcesses() - - return self._Processes or {} - end - - function FSM:GetProcess( From, Event ) - - for ProcessID, Process in pairs( self:GetProcesses() ) do - if Process.From == From and Process.Event == Event then - self:T( Process ) - return Process.fsm - end - end - - error( "Sub-Process from state " .. From .. " with event " .. Event .. " not found!" ) - end - - --- Adds an End state. - function FSM:AddEndState( State ) - - self._EndStates[State] = State - self.endstates[State] = State - end - - --- Returns the End states. - function FSM:GetEndStates() - - return self._EndStates or {} - end - - - --- Adds a score for the FSM to be achieved. - -- @param #FSM self - -- @param #string State is the state of the process when the score needs to be given. (See the relevant state descriptions of the process). - -- @param #string ScoreText is a text describing the score that is given according the status. - -- @param #number Score is a number providing the score of the status. - -- @return #FSM self - function FSM:AddScore( State, ScoreText, Score ) - self:F2( { State, ScoreText, Score } ) - - self._Scores[State] = self._Scores[State] or {} - self._Scores[State].ScoreText = ScoreText - self._Scores[State].Score = Score - - return self - end - - --- Adds a score for the FSM_PROCESS to be achieved. - -- @param #FSM self - -- @param #string From is the From State of the main process. - -- @param #string Event is the Event of the main process. - -- @param #string State is the state of the process when the score needs to be given. (See the relevant state descriptions of the process). - -- @param #string ScoreText is a text describing the score that is given according the status. - -- @param #number Score is a number providing the score of the status. - -- @return #FSM self - function FSM:AddScoreProcess( From, Event, State, ScoreText, Score ) - self:F2( { Event, State, ScoreText, Score } ) - - local Process = self:GetProcess( From, Event ) - - self:T( { Process = Process._Name, Scores = Process._Scores, State = State, ScoreText = ScoreText, Score = Score } ) - Process._Scores[State] = Process._Scores[State] or {} - Process._Scores[State].ScoreText = ScoreText - Process._Scores[State].Score = Score - - return Process - end - - --- Returns a table with the scores defined. - function FSM:GetScores() - - return self._Scores or {} - end - - --- Returns a table with the Subs defined. - function FSM:GetSubs() - - return self.options.subs - end - - - function FSM:LoadCallBacks( CallBackTable ) - - for name, callback in pairs( CallBackTable or {} ) do - self[name] = callback - end - - end - - function FSM:_eventmap( Events, EventStructure ) - - local Event = EventStructure.Event - local __Event = "__" .. EventStructure.Event - self[Event] = self[Event] or self:_create_transition(Event) - self[__Event] = self[__Event] or self:_delayed_transition(Event) - self:T( "Added methods: " .. Event .. ", " .. __Event ) - Events[Event] = self.Events[Event] or { map = {} } - self:_add_to_map( Events[Event].map, EventStructure ) - - end - - function FSM:_submap( subs, sub, name ) - self:F( { sub = sub, name = name } ) - subs[sub.From] = subs[sub.From] or {} - subs[sub.From][sub.Event] = subs[sub.From][sub.Event] or {} - - -- Make the reference table weak. - -- setmetatable( subs[sub.From][sub.Event], { __mode = "k" } ) - - subs[sub.From][sub.Event][sub] = {} - subs[sub.From][sub.Event][sub].fsm = sub.fsm - subs[sub.From][sub.Event][sub].StartEvent = sub.StartEvent - subs[sub.From][sub.Event][sub].ReturnEvents = sub.ReturnEvents or {} -- these events need to be given to find the correct continue event ... if none given, the processing will stop. - subs[sub.From][sub.Event][sub].name = name - subs[sub.From][sub.Event][sub].fsmparent = self - end - - - function FSM:_call_handler( handler, params, EventName ) - if self[handler] then - self:T( "Calling " .. handler ) - self._EventSchedules[EventName] = nil - local Value = self[handler]( self, unpack(params) ) - return Value - end - end - - function FSM._handler( self, EventName, ... ) - - local Can, to = self:can( EventName ) - - if to == "*" then - to = self.current - end - - if Can then - local from = self.current - local params = { from, EventName, to, ... } - - if self.Controllable then - self:T( "FSM Transition for " .. self.Controllable.ControllableName .. " :" .. self.current .. " --> " .. EventName .. " --> " .. to ) - else - self:T( "FSM Transition:" .. self.current .. " --> " .. EventName .. " --> " .. to ) - end - - if ( self:_call_handler("onbefore" .. EventName, params, EventName ) == false ) - or ( self:_call_handler("OnBefore" .. EventName, params, EventName ) == false ) - or ( self:_call_handler("onleave" .. from, params, EventName ) == false ) - or ( self:_call_handler("OnLeave" .. from, params, EventName ) == false ) then - self:T( "Cancel Transition" ) - return false - end - - self.current = to - - local execute = true - - local subtable = self:_gosub( from, EventName ) - for _, sub in pairs( subtable ) do - --if sub.nextevent then - -- self:F2( "nextevent = " .. sub.nextevent ) - -- self[sub.nextevent]( self ) - --end - self:T( "calling sub start event: " .. sub.StartEvent ) - sub.fsm.fsmparent = self - sub.fsm.ReturnEvents = sub.ReturnEvents - sub.fsm[sub.StartEvent]( sub.fsm ) - execute = false - end - - local fsmparent, Event = self:_isendstate( to ) - if fsmparent and Event then - self:F2( { "end state: ", fsmparent, Event } ) - self:_call_handler("onenter" .. to, params, EventName ) - self:_call_handler("OnEnter" .. to, params, EventName ) - self:_call_handler("onafter" .. EventName, params, EventName ) - self:_call_handler("OnAfter" .. EventName, params, EventName ) - self:_call_handler("onstatechange", params, EventName ) - fsmparent[Event]( fsmparent ) - execute = false - end - - if execute then - -- only execute the call if the From state is not equal to the To state! Otherwise this function should never execute! - --if from ~= to then - self:_call_handler("onenter" .. to, params, EventName ) - self:_call_handler("OnEnter" .. to, params, EventName ) - --end - - self:_call_handler("onafter" .. EventName, params, EventName ) - self:_call_handler("OnAfter" .. EventName, params, EventName ) - - self:_call_handler("onstatechange", params, EventName ) - end - else - self:T( "Cannot execute transition." ) - self:T( { From = self.current, Event = EventName, To = to, Can = Can } ) - end - - return nil - end - - function FSM:_delayed_transition( EventName ) - return function( self, DelaySeconds, ... ) - self:T2( "Delayed Event: " .. EventName ) - local CallID = 0 - if DelaySeconds ~= nil then - if DelaySeconds < 0 then -- Only call the event ONCE! - DelaySeconds = math.abs( DelaySeconds ) - if not self._EventSchedules[EventName] then - CallID = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1 ) - self._EventSchedules[EventName] = CallID - else - -- reschedule - end - else - CallID = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1 ) - end - else - error( "FSM: An asynchronous event trigger requires a DelaySeconds parameter!!! This can be positive or negative! Sorry, but will not process this." ) - end - self:T2( { CallID = CallID } ) - end - end - - function FSM:_create_transition( EventName ) - return function( self, ... ) return self._handler( self, EventName , ... ) end - end - - function FSM:_gosub( ParentFrom, ParentEvent ) - local fsmtable = {} - if self.subs[ParentFrom] and self.subs[ParentFrom][ParentEvent] then - self:T( { ParentFrom, ParentEvent, self.subs[ParentFrom], self.subs[ParentFrom][ParentEvent] } ) - return self.subs[ParentFrom][ParentEvent] - else - return {} - end - end - - function FSM:_isendstate( Current ) - local FSMParent = self.fsmparent - if FSMParent and self.endstates[Current] then - self:T( { state = Current, endstates = self.endstates, endstate = self.endstates[Current] } ) - FSMParent.current = Current - local ParentFrom = FSMParent.current - self:T( ParentFrom ) - self:T( self.ReturnEvents ) - local Event = self.ReturnEvents[Current] - self:T( { ParentFrom, Event, self.ReturnEvents } ) - if Event then - return FSMParent, Event - else - self:T( { "Could not find parent event name for state ", ParentFrom } ) - end - end - - return nil - end - - function FSM:_add_to_map( Map, Event ) - self:F3( { Map, Event } ) - if type(Event.From) == 'string' then - Map[Event.From] = Event.To - else - for _, From in ipairs(Event.From) do - Map[From] = Event.To - end - end - self:T3( { Map, Event } ) - end - - function FSM:GetState() - return self.current - end - - - function FSM:Is( State ) - return self.current == State - end - - function FSM:is(state) - return self.current == state - end - - function FSM:can(e) - local Event = self.Events[e] - self:F3( { self.current, Event } ) - local To = Event and Event.map[self.current] or Event.map['*'] - return To ~= nil, To - end - - function FSM:cannot(e) - return not self:can(e) - end - -end - -do -- FSM_CONTROLLABLE - - --- FSM_CONTROLLABLE class - -- @type FSM_CONTROLLABLE - -- @field Wrapper.Controllable#CONTROLLABLE Controllable - -- @extends Core.Fsm#FSM - FSM_CONTROLLABLE = { - ClassName = "FSM_CONTROLLABLE", - } - - --- Creates a new FSM_CONTROLLABLE object. - -- @param #FSM_CONTROLLABLE self - -- @param #table FSMT Finite State Machine Table - -- @param Wrapper.Controllable#CONTROLLABLE Controllable (optional) The CONTROLLABLE object that the FSM_CONTROLLABLE governs. - -- @return #FSM_CONTROLLABLE - function FSM_CONTROLLABLE:New( FSMT, Controllable ) - - -- Inherits from BASE - local self = BASE:Inherit( self, FSM:New( FSMT ) ) -- Core.Fsm#FSM_CONTROLLABLE - - if Controllable then - self:SetControllable( Controllable ) - end - - return self - end - - --- Sets the CONTROLLABLE object that the FSM_CONTROLLABLE governs. - -- @param #FSM_CONTROLLABLE self - -- @param Wrapper.Controllable#CONTROLLABLE FSMControllable - -- @return #FSM_CONTROLLABLE - function FSM_CONTROLLABLE:SetControllable( FSMControllable ) - self:F( FSMControllable ) - self.Controllable = FSMControllable - end - - --- Gets the CONTROLLABLE object that the FSM_CONTROLLABLE governs. - -- @param #FSM_CONTROLLABLE self - -- @return Wrapper.Controllable#CONTROLLABLE - function FSM_CONTROLLABLE:GetControllable() - return self.Controllable - end - - function FSM_CONTROLLABLE:_call_handler( handler, params, EventName ) - - local ErrorHandler = function( errmsg ) - - env.info( "Error in SCHEDULER function:" .. errmsg ) - if debug ~= nil then - env.info( debug.traceback() ) - end - - return errmsg - end - - if self[handler] then - self:F3( "Calling " .. handler ) - self._EventSchedules[EventName] = nil - local Result, Value = xpcall( function() return self[handler]( self, self.Controllable, unpack( params ) ) end, ErrorHandler ) - return Value - --return self[handler]( self, self.Controllable, unpack( params ) ) - end - end - -end - -do -- FSM_PROCESS - - --- FSM_PROCESS class - -- @type FSM_PROCESS - -- @field Tasking.Task#TASK Task - -- @extends Core.Fsm#FSM_CONTROLLABLE - FSM_PROCESS = { - ClassName = "FSM_PROCESS", - } - - --- Creates a new FSM_PROCESS object. - -- @param #FSM_PROCESS self - -- @return #FSM_PROCESS - function FSM_PROCESS:New( Controllable, Task ) - - local self = BASE:Inherit( self, FSM_CONTROLLABLE:New() ) -- Core.Fsm#FSM_PROCESS - - self:F( Controllable, Task ) - - self:Assign( Controllable, Task ) - - return self - end - - function FSM_PROCESS:Init( FsmProcess ) - self:T( "No Initialisation" ) - end - - --- Creates a new FSM_PROCESS object based on this FSM_PROCESS. - -- @param #FSM_PROCESS self - -- @return #FSM_PROCESS - function FSM_PROCESS:Copy( Controllable, Task ) - self:T( { self:GetClassNameAndID() } ) - - local NewFsm = self:New( Controllable, Task ) -- Core.Fsm#FSM_PROCESS - - NewFsm:Assign( Controllable, Task ) - - -- Polymorphic call to initialize the new FSM_PROCESS based on self FSM_PROCESS - NewFsm:Init( self ) - - -- Set Start State - NewFsm:SetStartState( self:GetStartState() ) - - -- Copy Transitions - for TransitionID, Transition in pairs( self:GetTransitions() ) do - NewFsm:AddTransition( Transition.From, Transition.Event, Transition.To ) - end - - -- Copy Processes - for ProcessID, Process in pairs( self:GetProcesses() ) do - self:T( { Process} ) - local FsmProcess = NewFsm:AddProcess( Process.From, Process.Event, Process.fsm:Copy( Controllable, Task ), Process.ReturnEvents ) - end - - -- Copy End States - for EndStateID, EndState in pairs( self:GetEndStates() ) do - self:T( EndState ) - NewFsm:AddEndState( EndState ) - end - - -- Copy the score tables - for ScoreID, Score in pairs( self:GetScores() ) do - self:T( Score ) - NewFsm:AddScore( ScoreID, Score.ScoreText, Score.Score ) - end - - return NewFsm - end - - --- Sets the task of the process. - -- @param #FSM_PROCESS self - -- @param Tasking.Task#TASK Task - -- @return #FSM_PROCESS - function FSM_PROCESS:SetTask( Task ) - - self.Task = Task - - return self - end - - --- Gets the task of the process. - -- @param #FSM_PROCESS self - -- @return Tasking.Task#TASK - function FSM_PROCESS:GetTask() - - return self.Task - end - - --- Gets the mission of the process. - -- @param #FSM_PROCESS self - -- @return Tasking.Mission#MISSION - function FSM_PROCESS:GetMission() - - return self.Task.Mission - end - - --- Gets the mission of the process. - -- @param #FSM_PROCESS self - -- @return Tasking.CommandCenter#COMMANDCENTER - function FSM_PROCESS:GetCommandCenter() - - return self:GetTask():GetMission():GetCommandCenter() - end - --- TODO: Need to check and fix that an FSM_PROCESS is only for a UNIT. Not for a GROUP. - - --- Send a message of the @{Task} to the Group of the Unit. --- @param #FSM_PROCESS self -function FSM_PROCESS:Message( Message ) - self:F( { Message = Message } ) - - local CC = self:GetCommandCenter() - local TaskGroup = self.Controllable:GetGroup() - - local PlayerName = self.Controllable:GetPlayerName() -- Only for a unit - PlayerName = PlayerName and " (" .. PlayerName .. ")" or "" -- If PlayerName is nil, then keep it nil, otherwise add brackets. - local Callsign = self.Controllable:GetCallsign() - local Prefix = Callsign and " @ " .. Callsign .. PlayerName or "" - - Message = Prefix .. ": " .. Message - CC:MessageToGroup( Message, TaskGroup ) -end - - - - - --- Assign the process to a @{Unit} and activate the process. - -- @param #FSM_PROCESS self - -- @param Task.Tasking#TASK Task - -- @param Wrapper.Unit#UNIT ProcessUnit - -- @return #FSM_PROCESS self - function FSM_PROCESS:Assign( ProcessUnit, Task ) - self:T( { Task, ProcessUnit } ) - - self:SetControllable( ProcessUnit ) - self:SetTask( Task ) - - --self.ProcessGroup = ProcessUnit:GetGroup() - - return self - end - - function FSM_PROCESS:onenterAssigned( ProcessUnit ) - self:T( "Assign" ) - - self.Task:Assign() - end - - function FSM_PROCESS:onenterFailed( ProcessUnit ) - self:T( "Failed" ) - - self.Task:Fail() - end - - function FSM_PROCESS:onenterSuccess( ProcessUnit ) - self:T( "Success" ) - - self.Task:Success() - end - - --- StateMachine callback function for a FSM_PROCESS - -- @param #FSM_PROCESS self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function FSM_PROCESS:onstatechange( ProcessUnit, From, Event, To, Dummy ) - self:T( { ProcessUnit, From, Event, To, Dummy, self:IsTrace() } ) - - if self:IsTrace() then - MESSAGE:New( "@ Process " .. self:GetClassNameAndID() .. " : " .. Event .. " changed to state " .. To, 2 ):ToAll() - end - - self:T( self._Scores[To] ) - -- TODO: This needs to be reworked with a callback functions allocated within Task, and set within the mission script from the Task Objects... - if self._Scores[To] then - - local Task = self.Task - local Scoring = Task:GetScoring() - if Scoring then - Scoring:_AddMissionTaskScore( Task.Mission, ProcessUnit, self._Scores[To].ScoreText, self._Scores[To].Score ) - end - end - end - -end - -do -- FSM_TASK - - --- FSM_TASK class - -- @type FSM_TASK - -- @field Tasking.Task#TASK Task - -- @extends Core.Fsm#FSM - FSM_TASK = { - ClassName = "FSM_TASK", - } - - --- Creates a new FSM_TASK object. - -- @param #FSM_TASK self - -- @param #table FSMT - -- @param Tasking.Task#TASK Task - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #FSM_TASK - function FSM_TASK:New( FSMT ) - - local self = BASE:Inherit( self, FSM_CONTROLLABLE:New( FSMT ) ) -- Core.Fsm#FSM_TASK - - self["onstatechange"] = self.OnStateChange - - return self - end - - function FSM_TASK:_call_handler( handler, params, EventName ) - if self[handler] then - self:T( "Calling " .. handler ) - self._EventSchedules[EventName] = nil - return self[handler]( self, unpack( params ) ) - end - end - -end -- FSM_TASK - -do -- FSM_SET - - --- FSM_SET class - -- @type FSM_SET - -- @field Core.Set#SET_BASE Set - -- @extends Core.Fsm#FSM - FSM_SET = { - ClassName = "FSM_SET", - } - - --- Creates a new FSM_SET object. - -- @param #FSM_SET self - -- @param #table FSMT Finite State Machine Table - -- @param Set_SET_BASE FSMSet (optional) The Set object that the FSM_SET governs. - -- @return #FSM_SET - function FSM_SET:New( FSMSet ) - - -- Inherits from BASE - self = BASE:Inherit( self, FSM:New() ) -- Core.Fsm#FSM_SET - - if FSMSet then - self:Set( FSMSet ) - end - - return self - end - - --- Sets the SET_BASE object that the FSM_SET governs. - -- @param #FSM_SET self - -- @param Core.Set#SET_BASE FSMSet - -- @return #FSM_SET - function FSM_SET:Set( FSMSet ) - self:F( FSMSet ) - self.Set = FSMSet - end - - --- Gets the SET_BASE object that the FSM_SET governs. - -- @param #FSM_SET self - -- @return Core.Set#SET_BASE - function FSM_SET:Get() - return self.Controllable - end - - function FSM_SET:_call_handler( handler, params, EventName ) - if self[handler] then - self:T( "Calling " .. handler ) - self._EventSchedules[EventName] = nil - return self[handler]( self, self.Set, unpack( params ) ) - end - end - -end -- FSM_SET - ---- 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 - ---- The OBJECT class --- @type OBJECT --- @extends Core.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 Dcs.DCSWrapper.Object#Object ObjectName The Object name --- @return #OBJECT self -function OBJECT:New( ObjectName, Test ) - local self = BASE:Inherit( self, BASE:New() ) - self:F2( ObjectName ) - self.ObjectName = ObjectName - - return self -end - - ---- Returns the unit's unique identifier. --- @param Wrapper.Object#OBJECT self --- @return Dcs.DCSWrapper.Object#Object.ID ObjectID --- @return #nil The DCS Object is not existing or alive. -function OBJECT:GetID() - self:F2( self.ObjectName ) - - local DCSObject = self:GetDCSObject() - - if DCSObject then - local ObjectID = DCSObject:getID() - return ObjectID - end - - return nil -end - ---- Destroys the OBJECT. --- @param #OBJECT self --- @return #nil The DCS Unit is not existing or alive. -function OBJECT:Destroy() - self:F2( self.ObjectName ) - - local DCSObject = self:GetDCSObject() - - if DCSObject then - - DCSObject:destroy() - end - - return nil -end - - - - ---- This module contains the IDENTIFIABLE class. --- --- 1) @{#IDENTIFIABLE} class, extends @{Object#OBJECT} --- =============================================================== --- The @{#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.New}(): Create a IDENTIFIABLE instance. --- --- 1.2) IDENTIFIABLE methods: --- -------------------------- --- The following methods can be used to identify an identifiable object: --- --- * @{#IDENTIFIABLE.GetName}(): Returns the name of the Identifiable. --- * @{#IDENTIFIABLE.IsAlive}(): Returns if the Identifiable is alive. --- * @{#IDENTIFIABLE.GetTypeName}(): Returns the type name of the Identifiable. --- * @{#IDENTIFIABLE.GetCoalition}(): Returns the coalition of the Identifiable. --- * @{#IDENTIFIABLE.GetCountry}(): Returns the country of the Identifiable. --- * @{#IDENTIFIABLE.GetDesc}(): Returns the descriptor structure of the Identifiable. --- --- --- === --- --- @module Identifiable - ---- The IDENTIFIABLE class --- @type IDENTIFIABLE --- @extends Wrapper.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 Dcs.DCSWrapper.Identifiable#Identifiable IdentifiableName The DCS Identifiable name --- @return #IDENTIFIABLE self -function IDENTIFIABLE:New( IdentifiableName ) - local self = BASE:Inherit( self, OBJECT:New( IdentifiableName ) ) - self:F2( IdentifiableName ) - self.IdentifiableName = IdentifiableName - return self -end - ---- Returns if the Identifiable is alive. --- @param #IDENTIFIABLE self --- @return #boolean true if Identifiable is alive. --- @return #nil The DCS Identifiable is not existing or alive. -function IDENTIFIABLE:IsAlive() - self:F3( 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 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 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 category of the DCS Identifiable. --- @param #IDENTIFIABLE self --- @return Dcs.DCSWrapper.Object#Object.Category The category ID -function IDENTIFIABLE:GetCategory() - self:F2( self.ObjectName ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - local ObjectCategory = DCSObject:getCategory() - self:T3( ObjectCategory ) - return ObjectCategory - end - - return nil -end - - ---- Returns the DCS Identifiable category name as defined within the DCS Identifiable Descriptor. --- @param #IDENTIFIABLE self --- @return #string The DCS Identifiable Category Name -function IDENTIFIABLE:GetCategoryName() - local DCSIdentifiable = self:GetDCSObject() - - if DCSIdentifiable then - local IdentifiableCategoryName = _CategoryName[ self:GetDesc().category ] - return IdentifiableCategoryName - end - - self:E( self.ClassName .. " " .. self.IdentifiableName .. " not found!" ) - return nil -end - ---- Returns coalition of the Identifiable. --- @param #IDENTIFIABLE self --- @return Dcs.DCSCoalitionWrapper.Object#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 self --- @return Dcs.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 self --- @return Dcs.DCSWrapper.Identifiable#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 - ---- Gets the CallSign of the IDENTIFIABLE, which is a blank by default. --- @param #IDENTIFIABLE self --- @return #string The CallSign of the IDENTIFIABLE. -function IDENTIFIABLE:GetCallsign() - return '' -end - - -function IDENTIFIABLE:GetThreatLevel() - - return 0, "Scenery" -end ---- This module contains the POSITIONABLE class. --- --- 1) @{Positionable#POSITIONABLE} class, extends @{Identifiable#IDENTIFIABLE} --- =========================================================== --- The @{Positionable#POSITIONABLE} class is a wrapper class to handle the POSITIONABLE objects: --- --- * Support all DCS APIs. --- * Enhance with POSITIONABLE specific APIs not in the DCS API set. --- * Manage the "state" of the POSITIONABLE. --- --- 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 - ---- The POSITIONABLE class --- @type POSITIONABLE --- @extends Wrapper.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 Dcs.DCSWrapper.Positionable#Positionable PositionableName The POSITIONABLE name --- @return #POSITIONABLE self -function POSITIONABLE:New( PositionableName ) - local self = BASE:Inherit( self, IDENTIFIABLE:New( PositionableName ) ) - - self.PositionableName = PositionableName - return self -end - ---- Returns the @{DCSTypes#Position3} position vectors indicating the point and direction vectors in 3D of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Position The 3D position vectors of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetPositionVec3() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionablePosition = DCSPositionable:getPosition().p - self:T3( PositionablePosition ) - return PositionablePosition - end - - return nil -end - ---- Returns the @{DCSTypes#Vec2} vector indicating the point in 2D of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Vec2 The 2D point vector of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetVec2() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionableVec3 = DCSPositionable:getPosition().p - - local PositionableVec2 = {} - PositionableVec2.x = PositionableVec3.x - PositionableVec2.y = PositionableVec3.z - - self:T2( PositionableVec2 ) - return PositionableVec2 - end - - return nil -end - ---- Returns a POINT_VEC2 object indicating the point in 2D of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Core.Point#POINT_VEC2 The 2D point vector of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetPointVec2() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionableVec3 = DCSPositionable:getPosition().p - - local PositionablePointVec2 = POINT_VEC2:NewFromVec3( PositionableVec3 ) - - self:T2( PositionablePointVec2 ) - return PositionablePointVec2 - end - - return nil -end - ---- Returns a POINT_VEC3 object indicating the point in 3D of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Core.Point#POINT_VEC3 The 3D point vector of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetPointVec3() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionableVec3 = self:GetPositionVec3() - - local PositionablePointVec3 = POINT_VEC3:NewFromVec3( PositionableVec3 ) - - self:T2( PositionablePointVec3 ) - return PositionablePointVec3 - end - - return nil -end - - ---- Returns a random @{DCSTypes#Vec3} vector within a range, indicating the point in 3D of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Vec3 The 3D point vector of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetRandomVec3( Radius ) - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionablePointVec3 = DCSPositionable:getPosition().p - local PositionableRandomVec3 = {} - local angle = math.random() * math.pi*2; - PositionableRandomVec3.x = PositionablePointVec3.x + math.cos( angle ) * math.random() * Radius; - PositionableRandomVec3.y = PositionablePointVec3.y - PositionableRandomVec3.z = PositionablePointVec3.z + math.sin( angle ) * math.random() * Radius; - - self:T3( PositionableRandomVec3 ) - return PositionableRandomVec3 - end - - return nil -end - ---- Returns the @{DCSTypes#Vec3} vector indicating the 3D vector of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Vec3 The 3D point vector of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetVec3() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionableVec3 = DCSPositionable:getPosition().p - self:T3( PositionableVec3 ) - return PositionableVec3 - end - - return nil -end - ---- Returns the altitude of the POSITIONABLE. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Distance The altitude of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetAltitude() - self:F2() - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionablePointVec3 = DCSPositionable:getPoint() --Dcs.DCSTypes#Vec3 - return PositionablePointVec3.y - end - - return nil -end - ---- Returns if the Positionable is located above a runway. --- @param Wrapper.Positionable#POSITIONABLE self --- @return #boolean true if Positionable is above a runway. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:IsAboveRunway() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - - local Vec2 = self:GetVec2() - local SurfaceType = land.getSurfaceType( Vec2 ) - local IsAboveRunway = SurfaceType == land.SurfaceType.RUNWAY - - self:T2( IsAboveRunway ) - return IsAboveRunway - end - - return nil -end - - - ---- Returns the POSITIONABLE heading in degrees. --- @param Wrapper.Positionable#POSITIONABLE self --- @return #number The POSTIONABLE 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 - PositionableHeading = PositionableHeading * 180 / math.pi - self:T2( PositionableHeading ) - return PositionableHeading - end - end - - return nil -end - - ---- Returns true if the POSITIONABLE is in the air. --- Polymorphic, is overridden in GROUP and UNIT. --- @param Wrapper.Positionable#POSITIONABLE self --- @return #boolean true if in the air. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:InAir() - self:F2( self.PositionableName ) - - return nil -end - - ---- Returns the POSITIONABLE velocity vector. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Vec3 The velocity vector --- @return #nil The 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 - ---- Returns the POSITIONABLE velocity in km/h. --- @param Wrapper.Positionable#POSITIONABLE self --- @return #number The velocity in km/h --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetVelocityKMH() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local VelocityVec3 = self:GetVelocity() - local Velocity = ( VelocityVec3.x ^ 2 + VelocityVec3.y ^ 2 + VelocityVec3.z ^ 2 ) ^ 0.5 -- in meters / sec - local Velocity = Velocity * 3.6 -- now it is in km/h. - self:T3( Velocity ) - return Velocity - end - - return nil -end - ---- Returns a message with the callsign embedded (if there is one). --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTypes#Duration Duration The duration of the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. --- @return Core.Message#MESSAGE -function POSITIONABLE:GetMessage( Message, Duration, Name ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - Name = Name or self:GetTypeName() - return MESSAGE:New( Message, Duration, self:GetCallsign() .. " (" .. Name .. ")" ) - end - - return nil -end - ---- Send a message to all coalitions. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTypes#Duration Duration The duration of the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. -function POSITIONABLE:MessageToAll( Message, Duration, Name ) - self:F2( { Message, Duration } ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - self:GetMessage( Message, Duration, Name ):ToAll() - end - - return nil -end - ---- Send a message to a coalition. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTYpes#Duration Duration The duration of the message. --- @param Dcs.DCScoalition#coalition MessageCoalition The Coalition receiving the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. -function POSITIONABLE:MessageToCoalition( Message, Duration, MessageCoalition, Name ) - self:F2( { Message, Duration } ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - self:GetMessage( Message, Duration, Name ):ToCoalition( MessageCoalition ) - end - - return nil -end - - ---- Send a message to the red coalition. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTYpes#Duration Duration The duration of the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. -function POSITIONABLE:MessageToRed( Message, Duration, Name ) - self:F2( { Message, Duration } ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - self:GetMessage( Message, Duration, Name ):ToRed() - end - - return nil -end - ---- Send a message to the blue coalition. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTypes#Duration Duration The duration of the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. -function POSITIONABLE:MessageToBlue( Message, Duration, Name ) - self:F2( { Message, Duration } ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - self:GetMessage( Message, Duration, Name ):ToBlue() - end - - return nil -end - ---- Send a message to a client. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTypes#Duration Duration The duration of the message. --- @param Wrapper.Client#CLIENT Client The client object receiving the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. -function POSITIONABLE:MessageToClient( Message, Duration, Client, Name ) - self:F2( { Message, Duration } ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - self:GetMessage( Message, Duration, Name ):ToClient( Client ) - end - - return nil -end - ---- Send a message to a @{Group}. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTypes#Duration Duration The duration of the message. --- @param Wrapper.Group#GROUP MessageGroup The GROUP object receiving the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. -function POSITIONABLE:MessageToGroup( Message, Duration, MessageGroup, Name ) - self:F2( { Message, Duration } ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - if DCSObject:isExist() then - self:GetMessage( Message, Duration, Name ):ToGroup( MessageGroup ) - end - end - - return nil -end - ---- Send a message to the players in the @{Group}. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTypes#Duration Duration The duration of the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. -function POSITIONABLE:Message( Message, Duration, Name ) - self:F2( { Message, Duration } ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - self:GetMessage( Message, Duration, Name ):ToGroup( self ) - end - - return nil -end - - - - - ---- This module contains the CONTROLLABLE class. --- --- 1) @{Controllable#CONTROLLABLE} class, extends @{Positionable#POSITIONABLE} --- =========================================================== --- 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 some or all ammunition at a VEC2 point. --- * @{#CONTROLLABLE.TaskFollow}: (AIR) Following another airborne controllable. --- * @{#CONTROLLABLE.TaskHold}: (GROUND) Hold ground controllable from moving. --- * @{#CONTROLLABLE.TaskHoldPosition}: (AIR) Hold position at the current position of the first unit of the controllable. --- * @{#CONTROLLABLE.TaskLand}: (AIR HELICOPTER) Landing at the ground. For helicopters only. --- * @{#CONTROLLABLE.TaskLandAtZone}: (AIR) Land the controllable at a @{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 - ---- The CONTROLLABLE class --- @type CONTROLLABLE --- @extends Wrapper.Positionable#POSITIONABLE --- @field Dcs.DCSWrapper.Controllable#Controllable DCSControllable The DCS controllable class. --- @field #string ControllableName The name of the controllable. -CONTROLLABLE = { - ClassName = "CONTROLLABLE", - ControllableName = "", - WayPointFunctions = {}, -} - ---- Create a new CONTROLLABLE from a DCSControllable --- @param #CONTROLLABLE self --- @param Dcs.DCSWrapper.Controllable#Controllable ControllableName The DCS Controllable name --- @return #CONTROLLABLE self -function CONTROLLABLE:New( ControllableName ) - local self = BASE:Inherit( self, POSITIONABLE:New( ControllableName ) ) - self:F2( ControllableName ) - self.ControllableName = ControllableName - - self.TaskScheduler = SCHEDULER:New( self ) - return self -end - --- DCS Controllable methods support. - ---- Get the controller for the CONTROLLABLE. --- @param #CONTROLLABLE self --- @return Dcs.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 - --- Get methods - ---- Returns the UNITs wrappers of the DCS Units of the Controllable (default is a GROUP). --- @param #CONTROLLABLE self --- @return #list The UNITs wrappers. -function CONTROLLABLE:GetUnits() - self:F2( { self.ControllableName } ) - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - local DCSUnits = DCSControllable:getUnits() - local Units = {} - for Index, UnitData in pairs( DCSUnits ) do - Units[#Units+1] = UNIT:Find( UnitData ) - end - self:T3( Units ) - return Units - end - - return nil -end - - ---- Returns the health. Dead controllables have health <= 1.0. --- @param #CONTROLLABLE self --- @return #number The controllable health value (unit or group average). --- @return #nil The controllable is not existing or alive. -function CONTROLLABLE:GetLife() - self:F2( self.ControllableName ) - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - local UnitLife = 0 - local Units = self:GetUnits() - if #Units == 1 then - local Unit = Units[1] -- Wrapper.Unit#UNIT - UnitLife = Unit:GetLife() - else - local UnitLifeTotal = 0 - for UnitID, Unit in pairs( Units ) do - local Unit = Unit -- Wrapper.Unit#UNIT - UnitLifeTotal = UnitLifeTotal + Unit:GetLife() - end - UnitLife = UnitLifeTotal / #Units - end - return UnitLife - end - - return nil -end - - - --- Tasks - ---- Popping current Task from the controllable. --- @param #CONTROLLABLE self --- @return Wrapper.Controllable#CONTROLLABLE self -function CONTROLLABLE:PopCurrentTask() - self:F2() - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - local Controller = self:_GetController() - Controller:popTask() - return self - end - - return nil -end - ---- Pushing Task on the queue from the controllable. --- @param #CONTROLLABLE self --- @return Wrapper.Controllable#CONTROLLABLE self -function CONTROLLABLE:PushTask( DCSTask, WaitTime ) - self:F2() - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - local 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 - self.TaskScheduler:Schedule( 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 Wrapper.Controllable#CONTROLLABLE self -function CONTROLLABLE:SetTask( DCSTask, WaitTime ) - self:F2( { DCSTask } ) - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - - local Controller = self:_GetController() - self:T3( Controller ) - - -- 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 - Controller:setTask( DCSTask ) - else - self.TaskScheduler:Schedule( Controller, Controller.setTask, { DCSTask }, WaitTime ) - end - - return self - end - - return nil -end - - ---- Return a condition section for a controlled task. --- @param #CONTROLLABLE self --- @param Dcs.DCSTime#Time time --- @param #string userFlag --- @param #boolean userFlagValue --- @param #string condition --- @param Dcs.DCSTime#Time duration --- @param #number lastWayPoint --- return Dcs.DCSTasking.Task#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 Dcs.DCSTasking.Task#Task DCSTask --- @param #DCSStopCondition DCSStopCondition --- @return Dcs.DCSTasking.Task#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 Dcs.DCSTasking.Task#TaskArray DCSTasks Array of @{DCSTasking.Task#Task} --- @return Dcs.DCSTasking.Task#Task -function CONTROLLABLE:TaskCombo( DCSTasks ) - self:F2( { DCSTasks } ) - - local DCSTaskCombo - - DCSTaskCombo = { - id = 'ComboTask', - params = { - tasks = DCSTasks - } - } - - for TaskID, Task in ipairs( DCSTasks ) do - self:E( Task ) - end - - self:T3( { DCSTaskCombo } ) - return DCSTaskCombo -end - ---- Return a WrappedAction Task taking a Command. --- @param #CONTROLLABLE self --- @param Dcs.DCSCommand#Command DCSCommand --- @return Dcs.DCSTasking.Task#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 Dcs.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 Dcs.DCSTasking.Task#Task --- @usage --- --- This test demonstrates the use(s) of the SwitchWayPoint method of the GROUP class. --- HeliGroup = GROUP:FindByName( "Helicopter" ) --- --- --- Route the helicopter back to the FARP after 60 seconds. --- -- We use the SCHEDULER class to do this. --- SCHEDULER:New( nil, --- function( HeliGroup ) --- local CommandRTB = HeliGroup:CommandSwitchWayPoint( 2, 8 ) --- HeliGroup:SetCommand( CommandRTB ) --- end, { HeliGroup }, 90 --- ) -function CONTROLLABLE:CommandSwitchWayPoint( FromWayPoint, ToWayPoint ) - self:F2( { FromWayPoint, ToWayPoint } ) - - local CommandSwitchWayPoint = { - id = 'SwitchWaypoint', - params = { - fromWaypointIndex = FromWayPoint, - goToWaypointIndex = ToWayPoint, - }, - } - - self:T3( { CommandSwitchWayPoint } ) - return CommandSwitchWayPoint -end - ---- Perform stop route command --- @param #CONTROLLABLE self --- @param #boolean StopRoute --- @return Dcs.DCSTasking.Task#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 Wrapper.Controllable#CONTROLLABLE AttackGroup The Controllable to be attacked. --- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. --- @param Dcs.DCSTypes#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. --- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. --- @param Dcs.DCSTypes#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. --- @param Dcs.DCSTypes#Distance Altitude (optional) Desired attack start altitude. Controllable/aircraft will make its attacks from the altitude. If the altitude is too low or too high to use weapon aircraft/controllable will choose closest altitude to the desired attack start altitude. If the desired altitude is defined controllable/aircraft will not attack from safe altitude. --- @param #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 Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:TaskAttackGroup( AttackGroup, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit ) - self:F2( { self.ControllableName, AttackGroup, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit } ) - - -- AttackControllable = { - -- id = 'AttackControllable', - -- params = { - -- groupId = Group.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 = { - groupId = AttackGroup: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 Wrapper.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 Dcs.DCSTypes#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. --- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. --- @param Dcs.DCSTypes#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. --- @param #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 Dcs.DCSTasking.Task#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 = { - altitudeEnabled = true, - unitId = AttackUnit:GetID(), - attackQtyLimit = AttackQtyLimit or false, - attackQty = AttackQty or 2, - expend = WeaponExpend or "Auto", - altitude = 2000, - directionEnabled = true, - groupAttack = true, - --weaponType = WeaponType or 1073741822, - direction = Direction or 0, - } - } - - self:E( DCSTask ) - - return DCSTask -end - - ---- (AIR) Delivering weapon at the point on the ground. --- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Vec2 Vec2 2D-coordinates of the point to deliver weapon at. --- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. --- @param Dcs.DCSTypes#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. --- @param #number AttackQty (optional) Desired quantity of passes. The parameter is not the same in AttackControllable and AttackUnit tasks. --- @param Dcs.DCSTypes#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. --- @param #boolean ControllableAttack (optional) Flag indicates that the target must be engaged by all aircrafts of the controllable. Has effect only if the task is assigned to a controllable, not to a single aircraft. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:TaskBombing( Vec2, WeaponType, WeaponExpend, AttackQty, Direction, ControllableAttack ) - self:F2( { self.ControllableName, Vec2, 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 = Vec2, - 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 Dcs.DCSTypes#Vec2 Point The point to hold the position. --- @param #number Altitude The altitude to hold the position. --- @param #number Speed The speed flying when holding the position. --- @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:GetVec2() - 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 Dcs.DCSTypes#Vec2 Vec2 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 Dcs.DCSTypes#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. --- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. --- @param Dcs.DCSTypes#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. --- @param #boolean ControllableAttack (optional) Flag indicates that the target must be engaged by all aircrafts of the controllable. Has effect only if the task is assigned to a controllable, not to a single aircraft. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:TaskAttackMapObject( Vec2, WeaponType, WeaponExpend, AttackQty, Direction, ControllableAttack ) - self:F2( { self.ControllableName, Vec2, 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 = Vec2, - 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 Wrapper.Airbase#AIRBASE Airbase Airbase to attack. --- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. --- @param Dcs.DCSTypes#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. --- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. --- @param Dcs.DCSTypes#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. --- @param #boolean ControllableAttack (optional) Flag indicates that the target must be engaged by all aircrafts of the controllable. Has effect only if the task is assigned to a controllable, not to a single aircraft. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -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 Dcs.DCSTasking.Task#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 Dcs.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 Core.Zone#ZONE Zone The zone where to land. --- @param #number Duration The duration in seconds to stay on the ground. --- @return #CONTROLLABLE self -function CONTROLLABLE:TaskLandAtZone( Zone, Duration, RandomPoint ) - self:F2( { self.ControllableName, Zone, Duration, RandomPoint } ) - - local Point - if RandomPoint then - Point = Zone:GetRandomVec2() - else - Point = Zone:GetVec2() - 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 Wrapper.Controllable#CONTROLLABLE FollowControllable The controllable to be followed. --- @param Dcs.DCSTypes#Vec3 Vec3 Position of the unit / lead unit of the controllable relative lead unit of another controllable in frame reference oriented by course of lead unit of another controllable. If another controllable is on land the unit / controllable will orbit around. --- @param #number LastWaypointIndex Detach waypoint of another controllable. Once reached the unit / controllable Follow task is finished. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:TaskFollow( FollowControllable, Vec3, LastWaypointIndex ) - self:F2( { self.ControllableName, FollowControllable, Vec3, LastWaypointIndex } ) - --- Follow = { --- id = 'Follow', --- params = { --- groupId = Group.ID, --- pos = Vec3, --- lastWptIndexFlag = boolean, --- lastWptIndex = number --- } --- } - - local LastWaypointIndexFlag = false - if LastWaypointIndex then - LastWaypointIndexFlag = true - end - - local DCSTask - DCSTask = { - id = 'Follow', - params = { - groupId = FollowControllable:GetID(), - pos = Vec3, - 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 Wrapper.Controllable#CONTROLLABLE EscortControllable The controllable to be escorted. --- @param Dcs.DCSTypes#Vec3 Vec3 Position of the unit / lead unit of the controllable relative lead unit of another controllable in frame reference oriented by course of lead unit of another controllable. If another controllable is on land the unit / controllable will orbit around. --- @param #number LastWaypointIndex Detach waypoint of another controllable. Once reached the unit / controllable Follow task is finished. --- @param #number EngagementDistanceMax Maximal distance from escorted controllable to threat. If the threat is already engaged by escort escort will disengage if the distance becomes greater than 1.5 * engagementDistMax. --- @param Dcs.DCSTypes#AttributeNameArray TargetTypes Array of AttributeName that is contains threat categories allowed to engage. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:TaskEscort( FollowControllable, Vec3, LastWaypointIndex, EngagementDistance, TargetTypes ) - self:F2( { self.ControllableName, FollowControllable, Vec3, LastWaypointIndex, EngagementDistance, TargetTypes } ) - --- Escort = { --- id = 'Escort', --- params = { --- groupId = Group.ID, --- pos = Vec3, --- lastWptIndexFlag = boolean, --- lastWptIndex = number, --- engagementDistMax = Distance, --- targetTypes = array of AttributeName, --- } --- } - - local LastWaypointIndexFlag = false - if LastWaypointIndex then - LastWaypointIndexFlag = true - end - - local DCSTask - DCSTask = { id = 'Escort', - params = { - groupId = FollowControllable:GetID(), - pos = Vec3, - 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 Dcs.DCSTypes#Vec2 Vec2 The point to fire at. --- @param Dcs.DCSTypes#Distance Radius The radius of the zone to deploy the fire at. --- @param #number AmmoCount (optional) Quantity of ammunition to expand (omit to fire until ammunition is depleted). --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:TaskFireAtPoint( Vec2, Radius, AmmoCount ) - self:F2( { self.ControllableName, Vec2, Radius, AmmoCount } ) - - -- FireAtPoint = { - -- id = 'FireAtPoint', - -- params = { - -- point = Vec2, - -- radius = Distance, - -- expendQty = number, - -- expendQtyEnabled = boolean, - -- } - -- } - - local DCSTask - DCSTask = { id = 'FireAtPoint', - params = { - point = Vec2, - radius = Radius, - expendQty = 100, -- dummy value - expendQtyEnabled = false, - } - } - - if AmmoCount then - DCSTask.params.expendQty = AmmoCount - DCSTask.params.expendQtyEnabled = true - end - - self:T3( { DCSTask } ) - return DCSTask -end - ---- (GROUND) Hold ground controllable from moving. --- @param #CONTROLLABLE self --- @return Dcs.DCSTasking.Task#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 Wrapper.Controllable#CONTROLLABLE AttackGroup Target CONTROLLABLE. --- @param #number WeaponType Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. --- @param Dcs.DCSTypes#AI.Task.Designation Designation (optional) Designation type. --- @param #boolean Datalink (optional) Allows to use datalink to send the target information to attack aircraft. Enabled by default. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:TaskFAC_AttackGroup( AttackGroup, WeaponType, Designation, Datalink ) - self:F2( { self.ControllableName, AttackGroup, WeaponType, Designation, Datalink } ) - --- FAC_AttackControllable = { --- id = 'FAC_AttackControllable', --- params = { --- groupId = Group.ID, --- weaponType = number, --- designation = enum AI.Task.Designation, --- datalink = boolean --- } --- } - - local DCSTask - DCSTask = { id = 'FAC_AttackControllable', - params = { - groupId = AttackGroup:GetID(), - weaponType = WeaponType, - designation = Designation, - datalink = Datalink, - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - --- EN-ACT_ROUTE TASKS FOR AIRBORNE CONTROLLABLES - ---- (AIR) Engaging targets of defined types. --- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Distance Distance Maximal distance from the target to a route leg. If the target is on a greater distance it will be ignored. --- @param Dcs.DCSTypes#AttributeNameArray TargetTypes Array of target categories allowed to engage. --- @param #number Priority All enroute tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -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 Dcs.DCSTypes#Vec2 Vec2 2D-coordinates of the zone. --- @param Dcs.DCSTypes#Distance Radius Radius of the zone. --- @param Dcs.DCSTypes#AttributeNameArray TargetTypes Array of target categories allowed to engage. --- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskEngageTargets( Vec2, Radius, TargetTypes, Priority ) - self:F2( { self.ControllableName, Vec2, Radius, TargetTypes, Priority } ) - --- EngageTargetsInZone = { --- id = 'EngageTargetsInZone', --- params = { --- point = Vec2, --- zoneRadius = Distance, --- targetTypes = array of AttributeName, --- priority = number --- } --- } - - local DCSTask - DCSTask = { id = 'EngageTargetsInZone', - params = { - point = Vec2, - 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 Wrapper.Controllable#CONTROLLABLE AttackGroup The Controllable to be attacked. --- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. --- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. --- @param Dcs.DCSTypes#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. --- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. --- @param Dcs.DCSTypes#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. --- @param Dcs.DCSTypes#Distance Altitude (optional) Desired attack start altitude. Controllable/aircraft will make its attacks from the altitude. If the altitude is too low or too high to use weapon aircraft/controllable will choose closest altitude to the desired attack start altitude. If the desired altitude is defined controllable/aircraft will not attack from safe altitude. --- @param #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 Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskEngageGroup( AttackGroup, Priority, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit ) - self:F2( { self.ControllableName, AttackGroup, Priority, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit } ) - - -- EngageControllable = { - -- id = 'EngageControllable ', - -- params = { - -- groupId = Group.ID, - -- weaponType = number, - -- expend = enum AI.Task.WeaponExpend, - -- attackQty = number, - -- directionEnabled = boolean, - -- direction = Azimuth, - -- altitudeEnabled = boolean, - -- altitude = Distance, - -- attackQtyLimit = boolean, - -- priority = number, - -- } - -- } - - local DirectionEnabled = nil - if Direction then - DirectionEnabled = true - end - - local AltitudeEnabled = nil - if Altitude then - AltitudeEnabled = true - end - - local DCSTask - DCSTask = { id = 'EngageControllable', - params = { - groupId = AttackGroup:GetID(), - weaponType = WeaponType, - expend = WeaponExpend, - attackQty = AttackQty, - directionEnabled = DirectionEnabled, - direction = Direction, - altitudeEnabled = AltitudeEnabled, - altitude = Altitude, - attackQtyLimit = AttackQtyLimit, - priority = Priority, - }, - }, - - self:T3( { DCSTask } ) - return DCSTask -end - - ---- (AIR) Attack the Unit. --- @param #CONTROLLABLE self --- @param Wrapper.Unit#UNIT EngageUnit The UNIT. --- @param #number Priority (optional) All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. --- @param #boolean GroupAttack (optional) If true, all units in the group will attack the Unit when found. --- @param Dcs.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 Dcs.DCSTypes#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. --- @param Dcs.DCSTypes#Distance Altitude (optional) Desired altitude to perform the unit engagement. --- @param #boolean Visible (optional) Unit must be visible. --- @param #boolean ControllableAttack (optional) Flag indicates that the target must be engaged by all aircrafts of the controllable. Has effect only if the task is assigned to a controllable, not to a single aircraft. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskEngageUnit( EngageUnit, Priority, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, Visible, ControllableAttack ) - self:F2( { self.ControllableName, EngageUnit, Priority, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, Visible, ControllableAttack } ) - - -- EngageUnit = { - -- id = 'EngageUnit', - -- params = { - -- unitId = Unit.ID, - -- weaponType = number, - -- expend = enum AI.Task.WeaponExpend - -- attackQty = number, - -- direction = Azimuth, - -- attackQtyLimit = boolean, - -- controllableAttack = boolean, - -- priority = number, - -- } - -- } - - local DCSTask - DCSTask = { id = 'EngageUnit', - params = { - unitId = EngageUnit:GetID(), - priority = Priority or 1, - groupAttack = GroupAttack or false, - visible = Visible or false, - expend = WeaponExpend or "Auto", - directionEnabled = Direction and true or false, - direction = Direction, - altitudeEnabled = Altitude and true or false, - altitude = Altitude, - attackQtyLimit = AttackQty and true or false, - attackQty = AttackQty, - controllableAttack = ControllableAttack, - }, - }, - - 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 Dcs.DCSTasking.Task#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 Dcs.DCSTasking.Task#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 Dcs.DCSTasking.Task#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 Wrapper.Controllable#CONTROLLABLE AttackGroup Target CONTROLLABLE. --- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. --- @param #number WeaponType Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. --- @param Dcs.DCSTypes#AI.Task.Designation Designation (optional) Designation type. --- @param #boolean Datalink (optional) Allows to use datalink to send the target information to attack aircraft. Enabled by default. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskFAC_EngageGroup( AttackGroup, Priority, WeaponType, Designation, Datalink ) - self:F2( { self.ControllableName, AttackGroup, WeaponType, Priority, Designation, Datalink } ) - --- FAC_EngageControllable = { --- id = 'FAC_EngageControllable', --- params = { --- groupId = Group.ID, --- weaponType = number, --- designation = enum AI.Task.Designation, --- datalink = boolean, --- priority = number, --- } --- } - - local DCSTask - DCSTask = { id = 'FAC_EngageControllable', - params = { - groupId = AttackGroup: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 Dcs.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 Dcs.DCSTasking.Task#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 Dcs.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 Dcs.DCSTasking.Task#Task The DCS task structure -function CONTROLLABLE:TaskEmbarking( Point, Duration, EmbarkingControllable ) - self:F2( { self.ControllableName, Point, Duration, EmbarkingControllable.DCSControllable } ) - - local DCSTask - DCSTask = { id = 'Embarking', - params = { x = Point.x, - y = Point.y, - duration = Duration, - controllablesForEmbarking = { EmbarkingControllable.ControllableID }, - durationFlag = true, - distributionFlag = false, - distribution = {}, - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - ---- (GROUND) Embark to a Transport landed at a location. - ---- Move to a defined Vec2 Point, and embark to a controllable when arrived within a defined Radius. --- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Vec2 Point The point where to wait. --- @param #number Radius The radius of the embarking zone around the Point. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:TaskEmbarkToTransport( Point, Radius ) - self:F2( { self.ControllableName, Point, Radius } ) - - local DCSTask --Dcs.DCSTasking.Task#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 Dcs.DCSTasking.Task#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 Dcs.DCSTasking.Task#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 Dcs.DCSTypes#Vec3 Point The destination point in Vec3 format. --- @param #number Speed The speed to travel. --- @return #CONTROLLABLE self -function CONTROLLABLE:RouteToVec2( Point, Speed ) - self:F2( { Point, Speed } ) - - local ControllablePoint = self:GetUnit( 1 ):GetVec2() - - local PointFrom = {} - PointFrom.x = ControllablePoint.x - PointFrom.y = ControllablePoint.y - PointFrom.type = "Turning Point" - PointFrom.action = "Turning Point" - PointFrom.speed = Speed - PointFrom.speed_locked = true - PointFrom.properties = { - ["vnav"] = 1, - ["scale"] = 0, - ["angle"] = 0, - ["vangle"] = 0, - ["steer"] = 2, - } - - - local PointTo = {} - PointTo.x = Point.x - PointTo.y = Point.y - PointTo.type = "Turning Point" - PointTo.action = "Fly Over Point" - PointTo.speed = Speed - PointTo.speed_locked = true - PointTo.properties = { - ["vnav"] = 1, - ["scale"] = 0, - ["angle"] = 0, - ["vangle"] = 0, - ["steer"] = 2, - } - - - local Points = { PointFrom, PointTo } - - self:T3( Points ) - - self:Route( Points ) - - return self -end - ---- (AIR + GROUND) Make the Controllable move to a given point. --- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Vec3 Point The destination point in Vec3 format. --- @param #number Speed The speed to travel. --- @return #CONTROLLABLE self -function CONTROLLABLE:RouteToVec3( Point, Speed ) - self:F2( { Point, Speed } ) - - local ControllableVec3 = self:GetUnit( 1 ):GetVec3() - - local PointFrom = {} - PointFrom.x = ControllableVec3.x - PointFrom.y = ControllableVec3.z - PointFrom.alt = ControllableVec3.y - PointFrom.alt_type = "BARO" - PointFrom.type = "Turning Point" - PointFrom.action = "Turning Point" - PointFrom.speed = Speed - PointFrom.speed_locked = true - PointFrom.properties = { - ["vnav"] = 1, - ["scale"] = 0, - ["angle"] = 0, - ["vangle"] = 0, - ["steer"] = 2, - } - - - local PointTo = {} - PointTo.x = Point.x - PointTo.y = Point.z - PointTo.alt = Point.y - PointTo.alt_type = "BARO" - PointTo.type = "Turning Point" - PointTo.action = "Fly Over Point" - PointTo.speed = Speed - PointTo.speed_locked = true - PointTo.properties = { - ["vnav"] = 1, - ["scale"] = 0, - ["angle"] = 0, - ["vangle"] = 0, - ["steer"] = 2, - } - - - local Points = { PointFrom, PointTo } - - self:T3( Points ) - - self:Route( Points ) - - return self -end - - - ---- Make the controllable to follow a given route. --- @param #CONTROLLABLE self --- @param #table 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 ) - self.TaskScheduler:Schedule( 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 Core.Zone#ZONE Zone The zone where to route to. --- @param #boolean Randomize Defines whether to target point gets randomized within the Zone. --- @param #number Speed The speed. --- @param Base#FORMATION Formation The formation string. -function CONTROLLABLE:TaskRouteToZone( Zone, Randomize, Speed, Formation ) - self:F2( Zone ) - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - - local ControllablePoint = self:GetVec2() - - local PointFrom = {} - PointFrom.x = ControllablePoint.x - PointFrom.y = ControllablePoint.y - PointFrom.type = "Turning Point" - PointFrom.action = "Cone" - PointFrom.speed = 20 / 1.6 - - - local PointTo = {} - local ZonePoint - - if Randomize then - ZonePoint = Zone:GetRandomVec2() - else - ZonePoint = Zone:GetVec2() - end - - PointTo.x = ZonePoint.x - PointTo.y = ZonePoint.y - PointTo.type = "Turning Point" - - if Formation then - PointTo.action = Formation - else - PointTo.action = "Cone" - end - - if Speed then - PointTo.speed = Speed - else - PointTo.speed = 20 / 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 Wrapper.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:GetVec2() - 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:GetVec2() - - 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 Wrapper.Controllable#CONTROLLABLE self --- @param #boolean DetectVisual (optional) --- @param #boolean DetectOptical (optional) --- @param #boolean DetectRadar (optional) --- @param #boolean DetectIRST (optional) --- @param #boolean DetectRWR (optional) --- @param #boolean DetectDLINK (optional) --- @return #table DetectedTargets -function CONTROLLABLE:GetDetectedTargets( DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK ) - self:F2( self.ControllableName ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local DetectionVisual = ( DetectVisual and DetectVisual == true ) and Controller.Detection.VISUAL or nil - local DetectionOptical = ( DetectOptical and DetectOptical == true ) and Controller.Detection.OPTICAL or nil - local DetectionRadar = ( DetectRadar and DetectRadar == true ) and Controller.Detection.RADAR or nil - local DetectionIRST = ( DetectIRST and DetectIRST == true ) and Controller.Detection.IRST or nil - local DetectionRWR = ( DetectRWR and DetectRWR == true ) and Controller.Detection.RWR or nil - local DetectionDLINK = ( DetectDLINK and DetectDLINK == true ) and Controller.Detection.DLINK or nil - - - 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 Wrapper.Controllable#CONTROLLABLE self --- @return Wrapper.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 ) - self:F( { WayPoints } ) - - if WayPoints then - self.WayPoints = WayPoints - else - self.WayPoints = self:GetTaskRoute() - end - - return self -end - ---- Get the current WayPoints set with the WayPoint functions( Note that the WayPoints can be nil, although there ARE waypoints). --- @param #CONTROLLABLE self --- @return #table WayPoints If WayPoints is given, then return the WayPoints structure. -function CONTROLLABLE:GetWayPoints() - self:F( ) - - if self.WayPoints then - return self.WayPoints - end - - return nil -end - ---- Registers a waypoint function that will be executed when the controllable moves over the WayPoint. --- @param #CONTROLLABLE self --- @param #number WayPoint The waypoint number. Note that the start waypoint on the route is WayPoint 1! --- @param #number WayPointIndex When defining multiple WayPoint functions for one WayPoint, use WayPointIndex to set the sequence of actions. --- @param #function WayPointFunction The waypoint function to be called when the controllable moves over the waypoint. The waypoint function takes variable parameters. --- @return #CONTROLLABLE -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 = GROUP: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 ) - self:F( { WayPoint, WaitTime } ) - - if not WayPoint then - WayPoint = 1 - end - - -- When starting the mission from a certain point, the TaskPoints need to be deleted before the given WayPoint. - for TaskPointID = 1, WayPoint - 1 do - table.remove( self.WayPoints, 1 ) - end - - self:T3( self.WayPoints ) - - self:SetTask( self:TaskRoute( self.WayPoints ), WaitTime ) - - return self -end - --- Message APIs--- 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 --- --- A GROUP is a @{Controllable}. See the @{Controllable} task methods section for a description of the task methods. --- --- ### 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: --- --- * @{Controllable#CONTROLLABLE.TaskMission}: (AIR + GROUND) Return a mission task from a mission template. --- --- ## 1.3) GROUP Command methods --- --- A GROUP is a @{Controllable}. See the @{Controllable} command methods section for a description of the command methods. --- --- ## 1.4) GROUP option methods --- --- A GROUP is a @{Controllable}. See the @{Controllable} option methods section for a description of the option methods. --- --- ## 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. --- --- ## 1.6) GROUP AI methods --- --- A GROUP has AI methods to control the AI activation. --- --- * @{#GROUP.SetAIOnOff}(): Turns the GROUP AI On or Off. --- * @{#GROUP.SetAIOn}(): Turns the GROUP AI On. --- * @{#GROUP.SetAIOff}(): Turns the GROUP AI Off. --- --- ==== --- --- # **API CHANGE HISTORY** --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- Hereby the change log: --- --- 2017-01-24: GROUP:**SetAIOnOff( AIOnOff )** added. --- --- 2017-01-24: GROUP:**SetAIOn()** added. --- --- 2017-01-24: GROUP:**SetAIOff()** added. --- --- === --- --- # **AUTHORS and CONTRIBUTIONS** --- --- ### Contributions: --- --- * [**Entropy**](https://forums.eagle.ru/member.php?u=111471), **Afinegan**: Came up with the requirement for AIOnOff(). --- --- ### Authors: --- --- * **FlightControl**: Design & Programming --- --- @module Group --- @author FlightControl - ---- The GROUP class --- @type GROUP --- @extends Wrapper.Controllable#CONTROLLABLE --- @field #string GroupName The name of the group. -GROUP = { - ClassName = "GROUP", -} - ---- Create a new GROUP from a DCSGroup --- @param #GROUP self --- @param Dcs.DCSWrapper.Group#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 - - self:SetEventPriority( 4 ) - return self -end - --- Reference methods. - ---- Find the GROUP wrapper class instance using the DCS Group. --- @param #GROUP self --- @param Dcs.DCSWrapper.Group#Group DCSGroup The DCS Group. --- @return #GROUP The GROUP. -function GROUP:Find( DCSGroup ) - - local GroupName = DCSGroup:getName() -- Wrapper.Group#GROUP - local GroupFound = _DATABASE:FindGroup( GroupName ) - return GroupFound -end - ---- Find the created GROUP using the DCS Group Name. --- @param #GROUP self --- @param #string GroupName The DCS Group Name. --- @return #GROUP The GROUP. -function GROUP:FindByName( GroupName ) - - local GroupFound = _DATABASE:FindGroup( GroupName ) - return GroupFound -end - --- DCS Group methods support. - ---- Returns the DCS Group. --- @param #GROUP self --- @return Dcs.DCSWrapper.Group#Group The DCS Group. -function GROUP:GetDCSObject() - local DCSGroup = Group.getByName( self.GroupName ) - - if DCSGroup then - return DCSGroup - end - - return nil -end - ---- Returns the @{DCSTypes#Position3} position vectors indicating the point and direction vectors in 3D of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Position The 3D position vectors of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function GROUP:GetPositionVec3() -- Overridden from POSITIONABLE:GetPositionVec3() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionablePosition = DCSPositionable:getUnits()[1]:getPosition().p - self:T3( PositionablePosition ) - return PositionablePosition - 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() and DCSGroup:getUnit(1) ~= nil - 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 Dcs.DCSWrapper.Group#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 Dcs.DCSCoalitionWrapper.Object#coalition.side The coalition side of the DCS Group. -function GROUP:GetCoalition() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - if DCSGroup then - local GroupCoalition = DCSGroup:getCoalition() - self:T3( GroupCoalition ) - return GroupCoalition - end - - return nil -end - ---- Returns the country of the DCS Group. --- @param #GROUP self --- @return Dcs.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 UNIT wrapper class with number UnitNumber. --- If the underlying DCS Unit does not exist, the method will return nil. . --- @param #GROUP self --- @param #number UnitNumber The number of the UNIT wrapper class to be returned. --- @return Wrapper.Unit#UNIT The UNIT wrapper class. -function GROUP:GetUnit( UnitNumber ) - self:F2( { self.GroupName, UnitNumber } ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local UnitFound = UNIT:Find( DCSGroup:getUnit( UnitNumber ) ) - 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 Dcs.DCSWrapper.Unit#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 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. --- @param #GROUP self --- @return Dcs.DCSTypes#Vec2 Current Vec2 point of the first DCS Unit of the DCS Group. -function GROUP:GetVec2() - self:F2( self.GroupName ) - - local UnitPoint = self:GetUnit(1) - UnitPoint:GetVec2() - local GroupPointVec2 = UnitPoint:GetVec2() - self:T3( GroupPointVec2 ) - return GroupPointVec2 -end - ---- Returns the current Vec3 vector of the first DCS Unit in the GROUP. --- @return Dcs.DCSTypes#Vec3 Current Vec3 of the first DCS Unit of the GROUP. -function GROUP:GetVec3() - self:F2( self.GroupName ) - - local GroupVec3 = self:GetUnit(1):GetVec3() - self:T3( GroupVec3 ) - return GroupVec3 -end - - - -do -- Is Zone methods - ---- Returns true if all units of the group are within a @{Zone}. --- @param #GROUP self --- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the Group is completely within the @{Zone#ZONE_BASE} -function GROUP:IsCompletelyInZone( Zone ) - self:F2( { self.GroupName, Zone } ) - - for UnitID, UnitData in pairs( self:GetUnits() ) do - local Unit = UnitData -- Wrapper.Unit#UNIT - if Zone:IsVec3InZone( Unit:GetVec3() ) then - else - return false - end - end - - return true -end - ---- Returns true if some units of the group are within a @{Zone}. --- @param #GROUP self --- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the Group is completely within the @{Zone#ZONE_BASE} -function GROUP:IsPartlyInZone( Zone ) - self:F2( { self.GroupName, Zone } ) - - for UnitID, UnitData in pairs( self:GetUnits() ) do - local Unit = UnitData -- Wrapper.Unit#UNIT - if Zone:IsVec3InZone( Unit:GetVec3() ) then - return true - end - end - - return false -end - ---- Returns true if none of the group units of the group are within a @{Zone}. --- @param #GROUP self --- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the Group is 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 -- Wrapper.Unit#UNIT - if Zone:IsVec3InZone( Unit:GetVec3() ) 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 - -end - -do -- AI methods - - --- Turns the AI On or Off for the GROUP. - -- @param #GROUP self - -- @param #boolean AIOnOff The value true turns the AI On, the value false turns the AI Off. - -- @return #GROUP The GROUP. - function GROUP:SetAIOnOff( AIOnOff ) - - local DCSGroup = self:GetDCSObject() -- Dcs.DCSGroup#Group - - if DCSGroup then - local DCSController = DCSGroup:getController() -- Dcs.DCSController#Controller - if DCSController then - DCSController:setOnOff( AIOnOff ) - return self - end - end - - return nil - end - - --- Turns the AI On for the GROUP. - -- @param #GROUP self - -- @return #GROUP The GROUP. - function GROUP:SetAIOn() - - return self:SetAIOnOff( true ) - end - - --- Turns the AI Off for the GROUP. - -- @param #GROUP self - -- @return #GROUP The GROUP. - function GROUP:SetAIOff() - - return self:SetAIOnOff( false ) - end - -end - - - ---- Returns the current maximum velocity of the group. --- Each unit within the group gets evaluated, and the maximum velocity (= the unit which is going the fastest) is returned. --- @param #GROUP self --- @return #number Maximum velocity found. -function GROUP:GetMaxVelocity() - self:F2() - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupVelocityMax = 0 - - for Index, UnitData in pairs( DCSGroup:getUnits() ) do - - local UnitVelocityVec3 = UnitData:getVelocity() - local UnitVelocity = math.abs( UnitVelocityVec3.x ) + math.abs( UnitVelocityVec3.y ) + math.abs( UnitVelocityVec3.z ) - - if UnitVelocity > GroupVelocityMax then - GroupVelocityMax = UnitVelocity - end - end - - return GroupVelocityMax - end - - return nil -end - ---- Returns the current minimum height of the group. --- Each unit within the group gets evaluated, and the minimum height (= the unit which is the lowest elevated) is returned. --- @param #GROUP self --- @return #number Minimum height found. -function GROUP:GetMinHeight() - self:F2() - -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 - --- SPAWNING - ---- Respawn the @{GROUP} using a (tweaked) template of the Group. --- The template must be retrieved with the @{Group#GROUP.GetTemplate}() function. --- The template contains all the definitions as declared within the mission file. --- To understand templates, do the following: --- --- * unpack your .miz file into a directory using 7-zip. --- * browse in the directory created to the file **mission**. --- * open the file and search for the country group definitions. --- --- Your group template will contain the fields as described within the mission file. --- --- This function will: --- --- * Get the current position and heading of the group. --- * When the group is alive, it will tweak the template x, y and heading coordinates of the group and the embedded units to the current units positions. --- * Then it will destroy the current alive group. --- * And it will respawn the group using your new template definition. --- @param Wrapper.Group#GROUP self --- @param #table Template The template of the Group retrieved with GROUP:GetTemplate() -function GROUP:Respawn( Template ) - - local Vec3 = self:GetVec3() - 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 -- Wrapper.Unit#UNIT - self:E( GroupUnit:GetName() ) - if GroupUnit:IsAlive() then - local GroupUnitVec3 = GroupUnit:GetVec3() - 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 - - self:Destroy() - _DATABASE:Spawn( Template ) -end - ---- Returns the group template from the @{DATABASE} (_DATABASE object). --- @param #GROUP self --- @return #table -function GROUP:GetTemplate() - local GroupName = self:GetName() - self:E( GroupName ) - return _DATABASE:GetGroupTemplate( GroupName ) -end - ---- Sets the controlled status in a Template. --- @param #GROUP self --- @param #boolean Controlled true is controlled, false is uncontrolled. --- @return #table -function GROUP:SetTemplateControlled( Template, Controlled ) - Template.uncontrolled = not Controlled - return Template -end - ---- Sets the CountryID of the group in a Template. --- @param #GROUP self --- @param Dcs.DCScountry#country.id CountryID The country ID. --- @return #table -function GROUP:SetTemplateCountry( Template, CountryID ) - Template.CountryID = CountryID - return Template -end - ---- Sets the CoalitionID of the group in a Template. --- @param #GROUP self --- @param Dcs.DCSCoalitionWrapper.Object#coalition.side CoalitionID The coalition ID. --- @return #table -function GROUP:SetTemplateCoalition( Template, CoalitionID ) - Template.CoalitionID = CoalitionID - return 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 - ---- Calculate the maxium A2G threat level of the Group. --- @param #GROUP self -function GROUP:CalculateThreatLevelA2G() - - local MaxThreatLevelA2G = 0 - for UnitName, UnitData in pairs( self:GetUnits() ) do - local ThreatUnit = UnitData -- Wrapper.Unit#UNIT - local ThreatLevelA2G = ThreatUnit:GetThreatLevel() - if ThreatLevelA2G > MaxThreatLevelA2G then - MaxThreatLevelA2G = ThreatLevelA2G - end - end - - self:T3( MaxThreatLevelA2G ) - return MaxThreatLevelA2G -end - ---- Returns true if the first unit of the GROUP is in the air. --- @param Wrapper.Group#GROUP self --- @return #boolean true if in the first unit of the group is in the air. --- @return #nil The GROUP is not existing or not alive. -function GROUP:InAir() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local DCSUnit = DCSGroup:getUnit(1) - if DCSUnit then - local GroupInAir = DCSGroup:getUnit(1):inAir() - self:T3( GroupInAir ) - return GroupInAir - end - end - - return nil -end - -function GROUP:OnReSpawn( ReSpawnFunction ) - - self.ReSpawnFunction = ReSpawnFunction -end - - ---- This module contains the UNIT class. --- --- 1) @{#UNIT} class, extends @{Controllable#CONTROLLABLE} --- =========================================================== --- The @{#UNIT} class is a wrapper class to handle the DCS Unit objects: --- --- * Support all DCS Unit APIs. --- * Enhance with Unit specific APIs not in the DCS Unit API set. --- * Handle local Unit Controller. --- * Manage the "state" of the DCS Unit. --- --- --- 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 @{DCSWrapper.Unit#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.GetVec3}() will obtain the current **location** of the DCS Unit in a Vec2 (2D) or a **point** in a Vec3 (3D) vector respectively. --- If you want to obtain the complete **3D position** including 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 Wrapper.Controllable#CONTROLLABLE -UNIT = { - ClassName="UNIT", -} - - ---- Unit.SensorType --- @type Unit.SensorType --- @field OPTIC --- @field RADAR --- @field IRST --- @field RWR - - --- Registration. - ---- Create a new UNIT from DCSUnit. --- @param #UNIT self --- @param #string UnitName The name of the DCS unit. --- @return #UNIT -function UNIT:Register( UnitName ) - local self = BASE:Inherit( self, CONTROLLABLE:New( UnitName ) ) - self.UnitName = UnitName - - self:SetEventPriority( 3 ) - return self -end - --- Reference methods. - ---- Finds a UNIT from the _DATABASE using a DCSUnit object. --- @param #UNIT self --- @param Dcs.DCSWrapper.Unit#Unit DCSUnit An existing DCS Unit object reference. --- @return #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 self -function UNIT:FindByName( UnitName ) - - local UnitFound = _DATABASE:FindUnit( UnitName ) - return UnitFound -end - ---- Return the name of the UNIT. --- @param #UNIT self --- @return #string The UNIT name. -function UNIT:Name() - - return self.UnitName -end - - ---- @param #UNIT self --- @return Dcs.DCSWrapper.Unit#Unit -function UNIT:GetDCSObject() - - local DCSUnit = Unit.getByName( self.UnitName ) - - if DCSUnit then - return DCSUnit - end - - return nil -end - ---- Respawn the @{Unit} using a (tweaked) template of the parent Group. --- --- This function will: --- --- * Get the current position and heading of the group. --- * When the unit is alive, it will tweak the template x, y and heading coordinates of the group and the embedded units to the current units positions. --- * Then it will respawn the re-modelled group. --- --- @param #UNIT self --- @param Dcs.DCSTypes#Vec3 SpawnVec3 The position where to Spawn the new Unit at. --- @param #number Heading The heading of the unit respawn. -function UNIT:ReSpawn( SpawnVec3, Heading ) - - local SpawnGroupTemplate = UTILS.DeepCopy( _DATABASE:GetGroupTemplateFromUnitName( self:Name() ) ) - self:T( SpawnGroupTemplate ) - - local SpawnGroup = self:GetGroup() - - if SpawnGroup then - - local Vec3 = SpawnGroup:GetVec3() - SpawnGroupTemplate.x = SpawnVec3.x - SpawnGroupTemplate.y = SpawnVec3.z - - self:E( #SpawnGroupTemplate.units ) - for UnitID, UnitData in pairs( SpawnGroup:GetUnits() ) do - local GroupUnit = UnitData -- #UNIT - self:E( GroupUnit:GetName() ) - if GroupUnit:IsAlive() then - local GroupUnitVec3 = GroupUnit:GetVec3() - local GroupUnitHeading = GroupUnit:GetHeading() - SpawnGroupTemplate.units[UnitID].alt = GroupUnitVec3.y - SpawnGroupTemplate.units[UnitID].x = GroupUnitVec3.x - SpawnGroupTemplate.units[UnitID].y = GroupUnitVec3.z - SpawnGroupTemplate.units[UnitID].heading = GroupUnitHeading - self:E( { UnitID, SpawnGroupTemplate.units[UnitID], SpawnGroupTemplate.units[UnitID] } ) - end - end - end - - for UnitTemplateID, UnitTemplateData in pairs( SpawnGroupTemplate.units ) do - self:T( UnitTemplateData.name ) - if UnitTemplateData.name == self:Name() then - self:T("Adjusting") - SpawnGroupTemplate.units[UnitTemplateID].alt = SpawnVec3.y - SpawnGroupTemplate.units[UnitTemplateID].x = SpawnVec3.x - SpawnGroupTemplate.units[UnitTemplateID].y = SpawnVec3.z - SpawnGroupTemplate.units[UnitTemplateID].heading = Heading - self:E( { UnitTemplateID, SpawnGroupTemplate.units[UnitTemplateID], SpawnGroupTemplate.units[UnitTemplateID] } ) - else - self:E( SpawnGroupTemplate.units[UnitTemplateID].name ) - local GroupUnit = UNIT:FindByName( SpawnGroupTemplate.units[UnitTemplateID].name ) -- #UNIT - if GroupUnit and GroupUnit:IsAlive() then - local GroupUnitVec3 = GroupUnit:GetVec3() - local GroupUnitHeading = GroupUnit:GetHeading() - UnitTemplateData.alt = GroupUnitVec3.y - UnitTemplateData.x = GroupUnitVec3.x - UnitTemplateData.y = GroupUnitVec3.z - UnitTemplateData.heading = GroupUnitHeading - else - if SpawnGroupTemplate.units[UnitTemplateID].name ~= self:Name() then - self:T("nilling") - SpawnGroupTemplate.units[UnitTemplateID].delete = true - end - end - end - end - - -- Remove obscolete units from the group structure - i = 1 - while i <= #SpawnGroupTemplate.units do - - local UnitTemplateData = SpawnGroupTemplate.units[i] - self:T( UnitTemplateData.name ) - - if UnitTemplateData.delete then - table.remove( SpawnGroupTemplate.units, i ) - else - i = i + 1 - end - end - - _DATABASE:Spawn( SpawnGroupTemplate ) -end - - - ---- Returns if the unit is activated. --- @param #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 the Unit's callsign - the localized string. --- @param #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 - - 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 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 number in the group. --- The number is the same number the unit has in ME. --- It may not be changed during the mission. --- If any unit in the group is destroyed, the numbers of another units will not be changed. --- @param #UNIT self --- @return #number The Unit number. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetNumber() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitNumber = DCSUnit:getNumber() - return UnitNumber - end - - return nil -end - ---- Returns the unit's group if it exist and nil otherwise. --- @param Wrapper.Unit#UNIT self --- @return Wrapper.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 - - --- Need to add here functions to check if radar is on and which object etc. - ---- Returns the prefix name of the DCS Unit. A prefix name is a part of the name before a '#'-sign. --- DCS Units spawned with the @{SPAWN} class contain a '#'-sign to indicate the end of the (base) DCS Unit name. --- The spawn sequence number and unit number are contained within the name after the '#' sign. --- @param #UNIT self --- @return #string The name of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetPrefix() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitPrefix = string.match( self.UnitName, ".*#" ):sub( 1, -2 ) - self:T3( UnitPrefix ) - return UnitPrefix - end - - return nil -end - ---- Returns the Unit's ammunition. --- @param #UNIT self --- @return Dcs.DCSWrapper.Unit#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 self --- @return Dcs.DCSWrapper.Unit#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 if the unit has sensors of a certain type. --- @param #UNIT self --- @return #boolean returns true if the unit has specified types of sensors. This function is more preferable than Unit.getSensors() if you don't want to get information about all the unit's sensors, and just want to check if the unit has specified types of sensors. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:HasSensors( ... ) - self:F2( arg ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local HasSensors = DCSUnit:hasSensors( unpack( arg ) ) - return HasSensors - end - - return nil -end - ---- Returns if the unit is SEADable. --- @param #UNIT self --- @return #boolean returns true if the unit is SEADable. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:HasSEAD() - self:F2() - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitSEADAttributes = DCSUnit:getDesc().attributes - - local HasSEAD = false - if UnitSEADAttributes["RADAR_BAND1_FOR_ARM"] and UnitSEADAttributes["RADAR_BAND1_FOR_ARM"] == true or - UnitSEADAttributes["RADAR_BAND2_FOR_ARM"] and UnitSEADAttributes["RADAR_BAND2_FOR_ARM"] == true then - HasSEAD = true - end - return HasSEAD - end - - return nil -end - ---- Returns two values: --- --- * First value indicates if at least one of the unit's radar(s) is on. --- * Second value is the object of the radar's interest. Not nil only if at least one radar of the unit is tracking a target. --- @param #UNIT self --- @return #boolean Indicates if at least one of the unit's radar(s) is on. --- @return Dcs.DCSWrapper.Object#Object The object of the radar's interest. Not nil only if at least one radar of the unit is tracking a target. --- @return #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 - ---- Returns relative amount of fuel (from 0.0 to 1.0) the unit has in its internal tanks. If there are additional fuel tanks the value may be greater than 1.0. --- @param #UNIT self --- @return #number The relative amount of fuel (from 0.0 to 1.0). --- @return #nil The DCS Unit is not existing or alive. -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 in a UNIT list of one element. --- @param #UNIT self --- @return #list The UNITs wrappers. -function UNIT:GetUnits() - self:F2( { self.UnitName } ) - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local DCSUnits = DCSUnit:getUnits() - local Units = {} - Units[1] = UNIT:Find( DCSUnit ) - self:T3( Units ) - return Units - end - - return nil -end - - ---- Returns the unit's health. Dead units has health <= 1.0. --- @param #UNIT self --- @return #number The Unit's health value. --- @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 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 the Unit's A2G threat level on a scale from 1 to 10 ... --- The following threat levels are foreseen: --- --- * Threat level 0: Unit is unarmed. --- * Threat level 1: Unit is infantry. --- * Threat level 2: Unit is an infantry vehicle. --- * Threat level 3: Unit is ground artillery. --- * Threat level 4: Unit is a tank. --- * Threat level 5: Unit is a modern tank or ifv with ATGM. --- * Threat level 6: Unit is a AAA. --- * Threat level 7: Unit is a SAM or manpad, IR guided. --- * Threat level 8: Unit is a Short Range SAM, radar guided. --- * Threat level 9: Unit is a Medium Range SAM, radar guided. --- * Threat level 10: Unit is a Long Range SAM, radar guided. --- @param #UNIT self -function UNIT:GetThreatLevel() - - local Attributes = self:GetDesc().attributes - self:E( Attributes ) - - local ThreatLevel = 0 - local ThreatText = "" - - if self:IsGround() then - - self:E( "Ground" ) - - local ThreatLevels = { - "Unarmed", - "Infantry", - "Old Tanks & APCs", - "Tanks & IFVs without ATGM", - "Tanks & IFV with ATGM", - "Modern Tanks", - "AAA", - "IR Guided SAMs", - "SR SAMs", - "MR SAMs", - "LR SAMs" - } - - - if Attributes["LR SAM"] then ThreatLevel = 10 - elseif Attributes["MR SAM"] then ThreatLevel = 9 - elseif Attributes["SR SAM"] and - not Attributes["IR Guided SAM"] then ThreatLevel = 8 - elseif ( Attributes["SR SAM"] or Attributes["MANPADS"] ) and - Attributes["IR Guided SAM"] then ThreatLevel = 7 - elseif Attributes["AAA"] then ThreatLevel = 6 - elseif Attributes["Modern Tanks"] then ThreatLevel = 5 - elseif ( Attributes["Tanks"] or Attributes["IFV"] ) and - Attributes["ATGM"] then ThreatLevel = 4 - elseif ( Attributes["Tanks"] or Attributes["IFV"] ) and - not Attributes["ATGM"] then ThreatLevel = 3 - elseif Attributes["Old Tanks"] or Attributes["APC"] or Attributes["Artillery"] then ThreatLevel = 2 - elseif Attributes["Infantry"] then ThreatLevel = 1 - end - - ThreatText = ThreatLevels[ThreatLevel+1] - end - - if self:IsAir() then - - self:E( "Air" ) - - local ThreatLevels = { - "Unarmed", - "Tanker", - "AWACS", - "Transport Helicpter", - "UAV", - "Bomber", - "Strategic Bomber", - "Attack Helicopter", - "Interceptor", - "Multirole Fighter", - "Fighter" - } - - - if Attributes["Fighters"] then ThreatLevel = 10 - elseif Attributes["Multirole fighters"] then ThreatLevel = 9 - elseif Attributes["Battleplanes"] then ThreatLevel = 8 - elseif Attributes["Attack helicopters"] then ThreatLevel = 7 - elseif Attributes["Strategic bombers"] then ThreatLevel = 6 - elseif Attributes["Bombers"] then ThreatLevel = 5 - elseif Attributes["UAVs"] then ThreatLevel = 4 - elseif Attributes["Transport helicopters"] then ThreatLevel = 3 - elseif Attributes["AWACS"] then ThreatLevel = 2 - elseif Attributes["Tankers"] then ThreatLevel = 1 - end - - ThreatText = ThreatLevels[ThreatLevel+1] - end - - if self:IsShip() then - - self:E( "Ship" ) - ---["Aircraft Carriers"] = {"Heavy armed ships",}, ---["Cruisers"] = {"Heavy armed ships",}, ---["Destroyers"] = {"Heavy armed ships",}, ---["Frigates"] = {"Heavy armed ships",}, ---["Corvettes"] = {"Heavy armed ships",}, ---["Heavy armed ships"] = {"Armed ships", "Armed Air Defence", "HeavyArmoredUnits",}, ---["Light armed ships"] = {"Armed ships","NonArmoredUnits"}, ---["Armed ships"] = {"Ships"}, ---["Unarmed ships"] = {"Ships","HeavyArmoredUnits",}, - - local ThreatLevels = { - "Unarmed ship", - "Light armed ships", - "Corvettes", - "", - "Frigates", - "", - "Cruiser", - "", - "Destroyer", - "", - "Aircraft Carrier" - } - - - if Attributes["Aircraft Carriers"] then ThreatLevel = 10 - elseif Attributes["Destroyers"] then ThreatLevel = 8 - elseif Attributes["Cruisers"] then ThreatLevel = 6 - elseif Attributes["Frigates"] then ThreatLevel = 4 - elseif Attributes["Corvettes"] then ThreatLevel = 2 - elseif Attributes["Light armed ships"] then ThreatLevel = 1 - end - - ThreatText = ThreatLevels[ThreatLevel+1] - end - - self:T2( ThreatLevel ) - return ThreatLevel, ThreatText - -end - - --- Is functions - ---- Returns true if the unit is within a @{Zone}. --- @param #UNIT self --- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the unit is within the @{Zone#ZONE_BASE} -function UNIT:IsInZone( Zone ) - self:F2( { self.UnitName, Zone } ) - - if self:IsAlive() then - local IsInZone = Zone:IsVec3InZone( self:GetVec3() ) - - self:T( { IsInZone } ) - return IsInZone - end - - return false -end - ---- Returns true if the unit is not within a @{Zone}. --- @param #UNIT self --- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the unit is not within the @{Zone#ZONE_BASE} -function UNIT:IsNotInZone( Zone ) - self:F2( { self.UnitName, Zone } ) - - if self:IsAlive() then - local IsInZone = not Zone:IsVec3InZone( self:GetVec3() ) - - self:T( { IsInZone } ) - return IsInZone - else - return false - end -end - - ---- Returns true if there is an **other** DCS Unit within a radius of the current 2D point of the DCS Unit. --- @param #UNIT self --- @param #UNIT AwaitUnit The other UNIT wrapper object. --- @param Radius The radius in meters with the DCS Unit in the centre. --- @return true If the other DCS Unit is within the radius of the 2D point of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:OtherUnitInRadius( AwaitUnit, Radius ) - self:F2( { self.UnitName, AwaitUnit.UnitName, Radius } ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitVec3 = self:GetVec3() - local AwaitUnitVec3 = AwaitUnit:GetVec3() - - if (((UnitVec3.x - AwaitUnitVec3.x)^2 + (UnitVec3.z - AwaitUnitVec3.z)^2)^0.5 <= Radius) then - self:T3( "true" ) - return true - else - self:T3( "false" ) - return false - end - end - - return nil -end - - - ---- Signal a flare at the position of the UNIT. --- @param #UNIT self --- @param Utilities.Utils#FLARECOLOR FlareColor -function UNIT:Flare( FlareColor ) - self:F2() - trigger.action.signalFlare( self:GetVec3(), 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:GetVec3(), 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:GetVec3(), 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:GetVec3(), trigger.flareColor.Green , 0 ) -end - ---- Signal a red flare at the position of the UNIT. --- @param #UNIT self -function UNIT:FlareRed() - self:F2() - local Vec3 = self:GetVec3() - if Vec3 then - trigger.action.signalFlare( Vec3, trigger.flareColor.Red, 0 ) - end -end - ---- Smoke the UNIT. --- @param #UNIT self -function UNIT:Smoke( SmokeColor, Range ) - self:F2() - if Range then - trigger.action.smoke( self:GetRandomVec3( Range ), SmokeColor ) - else - trigger.action.smoke( self:GetVec3(), SmokeColor ) - end - -end - ---- Smoke the UNIT Green. --- @param #UNIT self -function UNIT:SmokeGreen() - self:F2() - trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Green ) -end - ---- Smoke the UNIT Red. --- @param #UNIT self -function UNIT:SmokeRed() - self:F2() - trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Red ) -end - ---- Smoke the UNIT White. --- @param #UNIT self -function UNIT:SmokeWhite() - self:F2() - trigger.action.smoke( self:GetVec3(), trigger.smokeColor.White ) -end - ---- Smoke the UNIT Orange. --- @param #UNIT self -function UNIT:SmokeOrange() - self:F2() - trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Orange ) -end - ---- Smoke the UNIT Blue. --- @param #UNIT self -function UNIT:SmokeBlue() - self:F2() - trigger.action.smoke( self:GetVec3(), 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 DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitDescriptor = DCSUnit:getDesc() - self:T3( { UnitDescriptor.category, Unit.Category.AIRPLANE, Unit.Category.HELICOPTER } ) - - local IsAirResult = ( UnitDescriptor.category == Unit.Category.AIRPLANE ) or ( UnitDescriptor.category == Unit.Category.HELICOPTER ) - - self:T3( IsAirResult ) - return IsAirResult - end - - return nil -end - ---- Returns if the unit is of an ground category. --- If the unit is a ground vehicle or infantry, this method will return true, otherwise false. --- @param #UNIT self --- @return #boolean Ground category evaluation result. -function UNIT:IsGround() - self:F2() - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitDescriptor = DCSUnit:getDesc() - self:T3( { UnitDescriptor.category, Unit.Category.GROUND_UNIT } ) - - local IsGroundResult = ( UnitDescriptor.category == Unit.Category.GROUND_UNIT ) - - self:T3( IsGroundResult ) - return IsGroundResult - end - - return nil -end - ---- Returns if the unit is a friendly unit. --- @param #UNIT self --- @return #boolean IsFriendly evaluation result. -function UNIT:IsFriendly( FriendlyCoalition ) - self:F2() - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitCoalition = DCSUnit:getCoalition() - self:T3( { UnitCoalition, FriendlyCoalition } ) - - local IsFriendlyResult = ( UnitCoalition == FriendlyCoalition ) - - self:E( IsFriendlyResult ) - return IsFriendlyResult - end - - return nil -end - ---- Returns if the unit is of a ship category. --- If the unit is a ship, this method will return true, otherwise false. --- @param #UNIT self --- @return #boolean Ship category evaluation result. -function UNIT:IsShip() - self:F2() - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitDescriptor = DCSUnit:getDesc() - self:T3( { UnitDescriptor.category, Unit.Category.SHIP } ) - - local IsShipResult = ( UnitDescriptor.category == Unit.Category.SHIP ) - - self:T3( IsShipResult ) - return IsShipResult - end - - return nil -end - ---- Returns true if the UNIT is in the air. --- @param Wrapper.Positionable#UNIT self --- @return #boolean true if in the air. --- @return #nil The UNIT is not existing or alive. -function UNIT:InAir() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitInAir = DCSUnit:inAir() - self:T3( UnitInAir ) - return UnitInAir - end - - return nil -end - -do -- Event Handling - - --- Subscribe to a DCS Event. - -- @param #UNIT self - -- @param Core.Event#EVENTS Event - -- @param #function EventFunction (optional) The function to be called when the event occurs for the unit. - -- @return #UNIT - function UNIT:HandleEvent( Event, EventFunction ) - - self:EventDispatcher():OnEventForUnit( self:GetName(), EventFunction, self, Event ) - - return self - end - - --- UnSubscribe to a DCS event. - -- @param #UNIT self - -- @param Core.Event#EVENTS Event - -- @return #UNIT - function UNIT:UnHandleEvent( Event ) - - self:EventDispatcher():RemoveForUnit( self:GetName(), self, Event ) - - return self - end - -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 - ---- The CLIENT class --- @type CLIENT --- @extends Wrapper.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. --- @param #boolean Error A flag that indicates whether an error should be raised if the CLIENT cannot be found. By default an error will be raised. --- @return #CLIENT --- @usage --- -- Create new Clients. --- local Mission = MISSIONSCHEDULER.AddMission( 'Russia Transport Troops SA-6', 'Operational', 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', 'Russia' ) --- Mission:AddGoal( DeploySA6TroopsGoal ) --- --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 1' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 3' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 2' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 4' ):Transport() ) -function CLIENT:FindByName( ClientName, ClientBriefing, Error ) - local ClientFound = _DATABASE:FindClient( ClientName ) - - if ClientFound then - ClientFound:F( { ClientName, ClientBriefing } ) - ClientFound:AddBriefing( ClientBriefing ) - ClientFound.MessageSwitch = true - - return ClientFound - end - - if not Error then - error( "CLIENT not found for: " .. ClientName ) - end -end - -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 CallBackFunction Create a function that will be called when a player joins the slot. --- @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:F3( { 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 Dcs.DCSWrapper.Group#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 Dcs.DCSTypes#Group.ID ---- Get the group ID of the client. --- @param #CLIENT self --- @return Dcs.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 Wrapper.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 Dcs.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 @{AI_Cargo#CARGO} contained within the CLIENT to the player as a message. --- The @{AI_Cargo#CARGO} is shown using the @{Message#MESSAGE} distribution system. --- @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 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 @{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: --- --- * 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 Wrapper.Positionable#POSITIONABLE -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. --- @param #boolean RaiseError Raise an error if not found. --- @return #STATIC -function STATIC:FindByName( StaticName, RaiseError ) - local StaticFound = _DATABASE:FindStatic( StaticName ) - - self.StaticName = StaticName - - if StaticFound then - StaticFound:F( { StaticName } ) - - return StaticFound - end - - if RaiseError == nil or RaiseError == true then - error( "STATIC not found for: " .. StaticName ) - end - - return nil -end - -function STATIC:Register( StaticName ) - local self = BASE:Inherit( self, POSITIONABLE:New( StaticName ) ) - self.StaticName = StaticName - return self -end - - -function STATIC:GetDCSObject() - local DCSStatic = StaticObject.getByName( self.StaticName ) - - if DCSStatic then - return DCSStatic - end - - return nil -end - -function STATIC:GetThreatLevel() - - return 1, "Static" -end--- This module contains the AIRBASE classes. --- --- === --- --- 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. --- * 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 @{DCSWrapper.Airbase#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 Wrapper.Positionable#POSITIONABLE -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 #string AirbaseName The name of the airbase. --- @return Wrapper.Airbase#AIRBASE -function AIRBASE:Register( AirbaseName ) - - local self = BASE:Inherit( self, POSITIONABLE:New( AirbaseName ) ) - self.AirbaseName = AirbaseName - return self -end - --- Reference methods. - ---- Finds a AIRBASE from the _DATABASE using a DCSAirbase object. --- @param #AIRBASE self --- @param Dcs.DCSWrapper.Airbase#Airbase DCSAirbase An existing DCS Airbase object reference. --- @return Wrapper.Airbase#AIRBASE self -function AIRBASE:Find( DCSAirbase ) - - local AirbaseName = DCSAirbase:getName() - local AirbaseFound = _DATABASE:FindAirbase( AirbaseName ) - return AirbaseFound -end - ---- Find a AIRBASE in the _DATABASE using the name of an existing DCS Airbase. --- @param #AIRBASE self --- @param #string AirbaseName The Airbase Name. --- @return Wrapper.Airbase#AIRBASE self -function AIRBASE:FindByName( AirbaseName ) - - local AirbaseFound = _DATABASE:FindAirbase( AirbaseName ) - return AirbaseFound -end - -function AIRBASE:GetDCSObject() - local DCSAirbase = Airbase.getByName( self.AirbaseName ) - - if DCSAirbase then - return DCSAirbase - end - - return nil -end - - - ---- This module contains the SCENERY class. --- --- 1) @{Scenery#SCENERY} class, extends @{Positionable#POSITIONABLE} --- =============================================================== --- Scenery objects are defined on the map. --- The @{Scenery#SCENERY} class is a wrapper class to handle the DCS Scenery objects: --- --- * Wraps the DCS Scenery objects. --- * Support all DCS Scenery APIs. --- * Enhance with Scenery specific APIs not in the DCS API set. --- --- @module Scenery --- @author FlightControl - - - ---- The SCENERY class --- @type SCENERY --- @extends Wrapper.Positionable#POSITIONABLE -SCENERY = { - ClassName = "SCENERY", -} - - -function SCENERY:Register( SceneryName, SceneryObject ) - local self = BASE:Inherit( self, POSITIONABLE:New( SceneryName ) ) - self.SceneryName = SceneryName - self.SceneryObject = SceneryObject - return self -end - -function SCENERY:GetDCSObject() - return self.SceneryObject -end - -function SCENERY:GetThreatLevel() - - return 0, "Scenery" -end ---- Single-Player:**Yes** / Multi-Player:**Yes** / Core:**Yes** -- **Administer the scoring of player achievements, --- and create a CSV file logging the scoring events for use at team or squadron websites.** --- --- ![Banner Image](..\Presentations\SCORING\Dia1.JPG) --- --- === --- --- # 1) @{Scoring#SCORING} class, extends @{Base#BASE} --- --- The @{#SCORING} class administers the scoring of player achievements, --- and creates a CSV file logging the scoring events and results for use at team or squadron websites. --- --- SCORING automatically calculates the threat level of the objects hit and destroyed by players, --- which can be @{Unit}, @{Static) and @{Scenery} objects. --- --- Positive score points are granted when enemy or neutral targets are destroyed. --- Negative score points or penalties are given when a friendly target is hit or destroyed. --- This brings a lot of dynamism in the scoring, where players need to take care to inflict damage on the right target. --- By default, penalties weight heavier in the scoring, to ensure that players don't commit fratricide. --- The total score of the player is calculated by **adding the scores minus the penalties**. --- --- ![Banner Image](..\Presentations\SCORING\Dia4.JPG) --- --- The score value is calculated based on the **threat level of the player** and the **threat level of the target**. --- A calculated score takes the threat level of the target divided by a balanced threat level of the player unit. --- As such, if the threat level of the target is high, and the player threat level is low, a higher score will be given than --- if the threat level of the player would be high too. --- --- ![Banner Image](..\Presentations\SCORING\Dia5.JPG) --- --- When multiple players hit the same target, and finally succeed in destroying the target, then each player who contributed to the target --- destruction, will receive a score. This is important for targets that require significant damage before it can be destroyed, like --- ships or heavy planes. --- --- ![Banner Image](..\Presentations\SCORING\Dia13.JPG) --- --- Optionally, the score values can be **scaled** by a **scale**. Specific scales can be set for positive cores or negative penalties. --- The default range of the scores granted is a value between 0 and 10. The default range of penalties given is a value between 0 and 30. --- --- ![Banner Image](..\Presentations\SCORING\Dia7.JPG) --- --- **Additional scores** can be granted to **specific objects**, when the player(s) destroy these objects. --- --- ![Banner Image](..\Presentations\SCORING\Dia9.JPG) --- --- Various @{Zone}s can be defined for which scores are also granted when objects in that @{Zone} are destroyed. --- This is **specifically useful** to designate **scenery targets on the map** that will generate points when destroyed. --- --- With a small change in MissionScripting.lua, the scoring results can also be logged in a **CSV file**. --- These CSV files can be used to: --- --- * Upload scoring to a database or a BI tool to publish the scoring results to the player community. --- * Upload scoring in an (online) Excel like tool, using pivot tables and pivot charts to show mission results. --- * Share scoring amoung players after the mission to discuss mission results. --- --- Scores can be **reported**. **Menu options** are automatically added to **each player group** when a player joins a client slot or a CA unit. --- Use the radio menu F10 to consult the scores while running the mission. --- Scores can be reported for your user, or an overall score can be reported of all players currently active in the mission. --- --- ## 1.1) Set the destroy score or penalty scale --- --- Score scales can be set for scores granted when enemies or friendlies are destroyed. --- Use the method @{#SCORING.SetScaleDestroyScore}() to set the scale of enemy destroys (positive destroys). --- Use the method @{#SCORING.SetScaleDestroyPenalty}() to set the scale of friendly destroys (negative destroys). --- --- local Scoring = SCORING:New( "Scoring File" ) --- Scoring:SetScaleDestroyScore( 10 ) --- Scoring:SetScaleDestroyPenalty( 40 ) --- --- The above sets the scale for valid scores to 10. So scores will be given in a scale from 0 to 10. --- The penalties will be given in a scale from 0 to 40. --- --- ## 1.2) Define special targets that will give extra scores. --- --- Special targets can be set that will give extra scores to the players when these are destroyed. --- Use the methods @{#SCORING.AddUnitScore}() and @{#SCORING.RemoveUnitScore}() to specify a special additional score for a specific @{Unit}s. --- Use the methods @{#SCORING.AddStaticScore}() and @{#SCORING.RemoveStaticScore}() to specify a special additional score for a specific @{Static}s. --- Use the method @{#SCORING.SetGroupGroup}() to specify a special additional score for a specific @{Group}s. --- --- local Scoring = SCORING:New( "Scoring File" ) --- Scoring:AddUnitScore( UNIT:FindByName( "Unit #001" ), 200 ) --- Scoring:AddStaticScore( STATIC:FindByName( "Static #1" ), 100 ) --- --- The above grants an additional score of 200 points for Unit #001 and an additional 100 points of Static #1 if these are destroyed. --- Note that later in the mission, one can remove these scores set, for example, when the a goal achievement time limit is over. --- For example, this can be done as follows: --- --- Scoring:RemoveUnitScore( UNIT:FindByName( "Unit #001" ) ) --- --- --- --- ## 1.3) Define destruction zones that will give extra scores. --- --- Define zones of destruction. Any object destroyed within the zone of the given category will give extra points. --- Use the method @{#SCORING.AddZoneScore}() to add a @{Zone} for additional scoring. --- Use the method @{#SCORING.RemoveZoneScore}() to remove a @{Zone} for additional scoring. --- There are interesting variations that can be achieved with this functionality. For example, if the @{Zone} is a @{Zone#ZONE_UNIT}, --- then the zone is a moving zone, and anything destroyed within that @{Zone} will generate points. --- The other implementation could be to designate a scenery target (a building) in the mission editor surrounded by a @{Zone}, --- just large enough around that building. --- --- ## 1.4) Add extra Goal scores upon an event or a condition. --- --- A mission has goals and achievements. The scoring system provides an API to set additional scores when a goal or achievement event happens. --- Use the method @{#SCORING.AddGoalScore}() to add a score for a Player at any time in your mission. --- --- ## 1.5) Configure fratricide level. --- --- When a player commits too much damage to friendlies, his penalty score will reach a certain level. --- Use the method @{#SCORING.SetFratricide}() to define the level when a player gets kicked. --- By default, the fratricide level is the default penalty mutiplier * 2 for the penalty score. --- --- ## 1.6) Penalty score when a player changes the coalition. --- --- When a player changes the coalition, he can receive a penalty score. --- Use the method @{#SCORING.SetCoalitionChangePenalty}() to define the penalty when a player changes coalition. --- By default, the penalty for changing coalition is the default penalty scale. --- --- ## 1.8) Define output CSV files. --- --- The CSV file is given the name of the string given in the @{#SCORING.New}{} constructor, followed by the .csv extension. --- The file is incrementally saved in the **\\Saved Games\\DCS\\Logs** folder, and has a time stamp indicating each mission run. --- See the following example: --- --- local ScoringFirstMission = SCORING:New( "FirstMission" ) --- local ScoringSecondMission = SCORING:New( "SecondMission" ) --- --- The above documents that 2 Scoring objects are created, ScoringFirstMission and ScoringSecondMission. --- --- ## 1.9) Configure messages. --- --- When players hit or destroy targets, messages are sent. --- Various methods exist to configure: --- --- * Which messages are sent upon the event. --- * Which audience receives the message. --- --- ### 1.9.1) Configure the messages sent upon the event. --- --- Use the following methods to configure when to send messages. By default, all messages are sent. --- --- * @{#SCORING.SetMessagesHit}(): Configure to send messages after a target has been hit. --- * @{#SCORING.SetMessagesDestroy}(): Configure to send messages after a target has been destroyed. --- * @{#SCORING.SetMessagesAddon}(): Configure to send messages for additional score, after a target has been destroyed. --- * @{#SCORING.SetMessagesZone}(): Configure to send messages for additional score, after a target has been destroyed within a given zone. --- --- ### 1.9.2) Configure the audience of the messages. --- --- Use the following methods to configure the audience of the messages. By default, the messages are sent to all players in the mission. --- --- * @{#SCORING.SetMessagesToAll}(): Configure to send messages to all players. --- * @{#SCORING.SetMessagesToCoalition}(): Configure to send messages to only those players within the same coalition as the player. --- --- --- ==== --- --- # **API CHANGE HISTORY** --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- Hereby the change log: --- --- 2017-02-26: Initial class and API. --- --- === --- --- # **AUTHORS and CONTRIBUTIONS** --- --- ### Contributions: --- --- * **Wingthor (TAW)**: Testing & Advice. --- * **Dutch-Baron (TAW)**: Testing & Advice. --- * **[Whisper](http://forums.eagle.ru/member.php?u=3829): Testing and Advice. --- --- ### Authors: --- --- * **FlightControl**: Concept, Design & Programming. --- --- @module Scoring - - ---- The Scoring class --- @type SCORING --- @field Players A collection of the current players that have joined the game. --- @extends Core.Base#BASE -SCORING = { - ClassName = "SCORING", - ClassID = 0, - Players = {}, -} - -local _SCORINGCoalition = - { - [1] = "Red", - [2] = "Blue", - } - -local _SCORINGCategory = - { - [Unit.Category.AIRPLANE] = "Plane", - [Unit.Category.HELICOPTER] = "Helicopter", - [Unit.Category.GROUND_UNIT] = "Vehicle", - [Unit.Category.SHIP] = "Ship", - [Unit.Category.STRUCTURE] = "Structure", - } - ---- Creates a new SCORING object to administer the scoring achieved by players. --- @param #SCORING self --- @param #string GameName The name of the game. This name is also logged in the CSV score file. --- @return #SCORING self --- @usage --- -- Define a new scoring object for the mission Gori Valley. --- ScoringObject = SCORING:New( "Gori Valley" ) -function SCORING:New( GameName ) - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) -- #SCORING - - if GameName then - self.GameName = GameName - else - error( "A game name must be given to register the scoring results" ) - end - - - -- Additional Object scores - self.ScoringObjects = {} - - -- Additional Zone scores. - self.ScoringZones = {} - - -- Configure Messages - self:SetMessagesToAll() - self:SetMessagesHit( true ) - self:SetMessagesDestroy( true ) - self:SetMessagesScore( true ) - self:SetMessagesZone( true ) - - -- Scales - self:SetScaleDestroyScore( 10 ) - self:SetScaleDestroyPenalty( 30 ) - - -- Default fratricide penalty level (maximum penalty that can be assigned to a player before he gets kicked). - self:SetFratricide( self.ScaleDestroyPenalty * 3 ) - - -- Default penalty when a player changes coalition. - self:SetCoalitionChangePenalty( self.ScaleDestroyPenalty ) - - -- Event handlers - self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) - self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) - self:HandleEvent( EVENTS.Hit, self._EventOnHit ) - self:HandleEvent( EVENTS.PlayerEnterUnit ) - self:HandleEvent( EVENTS.PlayerLeaveUnit ) - - -- Create the CSV file. - self:OpenCSV( GameName ) - - return self - -end - ---- Set the scale for scoring valid destroys (enemy destroys). --- A default calculated score is a value between 1 and 10. --- The scale magnifies the scores given to the players. --- @param #SCORING self --- @param #number Scale The scale of the score given. -function SCORING:SetScaleDestroyScore( Scale ) - - self.ScaleDestroyScore = Scale - - return self -end - ---- Set the scale for scoring penalty destroys (friendly destroys). --- A default calculated penalty is a value between 1 and 10. --- The scale magnifies the scores given to the players. --- @param #SCORING self --- @param #number Scale The scale of the score given. --- @return #SCORING -function SCORING:SetScaleDestroyPenalty( Scale ) - - self.ScaleDestroyPenalty = Scale - - return self -end - ---- Add a @{Unit} for additional scoring when the @{Unit} is destroyed. --- Note that if there was already a @{Unit} declared within the scoring with the same name, --- then the old @{Unit} will be replaced with the new @{Unit}. --- @param #SCORING self --- @param Wrapper.Unit#UNIT ScoreUnit The @{Unit} for which the Score needs to be given. --- @param #number Score The Score value. --- @return #SCORING -function SCORING:AddUnitScore( ScoreUnit, Score ) - - local UnitName = ScoreUnit:GetName() - - self.ScoringObjects[UnitName] = Score - - return self -end - ---- Removes a @{Unit} for additional scoring when the @{Unit} is destroyed. --- @param #SCORING self --- @param Wrapper.Unit#UNIT ScoreUnit The @{Unit} for which the Score needs to be given. --- @return #SCORING -function SCORING:RemoveUnitScore( ScoreUnit ) - - local UnitName = ScoreUnit:GetName() - - self.ScoringObjects[UnitName] = nil - - return self -end - ---- Add a @{Static} for additional scoring when the @{Static} is destroyed. --- Note that if there was already a @{Static} declared within the scoring with the same name, --- then the old @{Static} will be replaced with the new @{Static}. --- @param #SCORING self --- @param Wrapper.Static#UNIT ScoreStatic The @{Static} for which the Score needs to be given. --- @param #number Score The Score value. --- @return #SCORING -function SCORING:AddStaticScore( ScoreStatic, Score ) - - local StaticName = ScoreStatic:GetName() - - self.ScoringObjects[StaticName] = Score - - return self -end - ---- Removes a @{Static} for additional scoring when the @{Static} is destroyed. --- @param #SCORING self --- @param Wrapper.Static#UNIT ScoreStatic The @{Static} for which the Score needs to be given. --- @return #SCORING -function SCORING:RemoveStaticScore( ScoreStatic ) - - local StaticName = ScoreStatic:GetName() - - self.ScoringObjects[StaticName] = nil - - return self -end - - ---- Specify a special additional score for a @{Group}. --- @param #SCORING self --- @param Wrapper.Group#GROUP ScoreGroup The @{Group} for which each @{Unit} a Score is given. --- @param #number Score The Score value. --- @return #SCORING -function SCORING:AddScoreGroup( ScoreGroup, Score ) - - local ScoreUnits = ScoreGroup:GetUnits() - - for ScoreUnitID, ScoreUnit in pairs( ScoreUnits ) do - local UnitName = ScoreUnit:GetName() - self.ScoringObjects[UnitName] = Score - end - - return self -end - ---- Add a @{Zone} to define additional scoring when any object is destroyed in that zone. --- Note that if a @{Zone} with the same name is already within the scoring added, the @{Zone} (type) and Score will be replaced! --- This allows for a dynamic destruction zone evolution within your mission. --- @param #SCORING self --- @param Core.Zone#ZONE_BASE ScoreZone The @{Zone} which defines the destruction score perimeters. --- Note that a zone can be a polygon or a moving zone. --- @param #number Score The Score value. --- @return #SCORING -function SCORING:AddZoneScore( ScoreZone, Score ) - - local ZoneName = ScoreZone:GetName() - - self.ScoringZones[ZoneName] = {} - self.ScoringZones[ZoneName].ScoreZone = ScoreZone - self.ScoringZones[ZoneName].Score = Score - - return self -end - ---- Remove a @{Zone} for additional scoring. --- The scoring will search if any @{Zone} is added with the given name, and will remove that zone from the scoring. --- This allows for a dynamic destruction zone evolution within your mission. --- @param #SCORING self --- @param Core.Zone#ZONE_BASE ScoreZone The @{Zone} which defines the destruction score perimeters. --- Note that a zone can be a polygon or a moving zone. --- @return #SCORING -function SCORING:RemoveZoneScore( ScoreZone ) - - local ZoneName = ScoreZone:GetName() - - self.ScoringZones[ZoneName] = nil - - return self -end - - ---- Configure to send messages after a target has been hit. --- @param #SCORING self --- @param #boolean OnOff If true is given, the messages are sent. --- @return #SCORING -function SCORING:SetMessagesHit( OnOff ) - - self.MessagesHit = OnOff - return self -end - ---- If to send messages after a target has been hit. --- @param #SCORING self --- @return #boolean -function SCORING:IfMessagesHit() - - return self.MessagesHit -end - ---- Configure to send messages after a target has been destroyed. --- @param #SCORING self --- @param #boolean OnOff If true is given, the messages are sent. --- @return #SCORING -function SCORING:SetMessagesDestroy( OnOff ) - - self.MessagesDestroy = OnOff - return self -end - ---- If to send messages after a target has been destroyed. --- @param #SCORING self --- @return #boolean -function SCORING:IfMessagesDestroy() - - return self.MessagesDestroy -end - ---- Configure to send messages after a target has been destroyed and receives additional scores. --- @param #SCORING self --- @param #boolean OnOff If true is given, the messages are sent. --- @return #SCORING -function SCORING:SetMessagesScore( OnOff ) - - self.MessagesScore = OnOff - return self -end - ---- If to send messages after a target has been destroyed and receives additional scores. --- @param #SCORING self --- @return #boolean -function SCORING:IfMessagesScore() - - return self.MessagesScore -end - ---- Configure to send messages after a target has been hit in a zone, and additional score is received. --- @param #SCORING self --- @param #boolean OnOff If true is given, the messages are sent. --- @return #SCORING -function SCORING:SetMessagesZone( OnOff ) - - self.MessagesZone = OnOff - return self -end - ---- If to send messages after a target has been hit in a zone, and additional score is received. --- @param #SCORING self --- @return #boolean -function SCORING:IfMessagesZone() - - return self.MessagesZone -end - ---- Configure to send messages to all players. --- @param #SCORING self --- @return #SCORING -function SCORING:SetMessagesToAll() - - self.MessagesAudience = 1 - return self -end - ---- If to send messages to all players. --- @param #SCORING self --- @return #boolean -function SCORING:IfMessagesToAll() - - return self.MessagesAudience == 1 -end - ---- Configure to send messages to only those players within the same coalition as the player. --- @param #SCORING self --- @return #SCORING -function SCORING:SetMessagesToCoalition() - - self.MessagesAudience = 2 - return self -end - ---- If to send messages to only those players within the same coalition as the player. --- @param #SCORING self --- @return #boolean -function SCORING:IfMessagesToCoalition() - - return self.MessagesAudience == 2 -end - - ---- When a player commits too much damage to friendlies, his penalty score will reach a certain level. --- Use this method to define the level when a player gets kicked. --- By default, the fratricide level is the default penalty mutiplier * 2 for the penalty score. --- @param #SCORING self --- @param #number Fratricide The amount of maximum penalty that may be inflicted by a friendly player before he gets kicked. --- @return #SCORING -function SCORING:SetFratricide( Fratricide ) - - self.Fratricide = Fratricide - return self -end - - ---- When a player changes the coalition, he can receive a penalty score. --- Use the method @{#SCORING.SetCoalitionChangePenalty}() to define the penalty when a player changes coalition. --- By default, the penalty for changing coalition is the default penalty scale. --- @param #SCORING self --- @param #number CoalitionChangePenalty The amount of penalty that is given. --- @return #SCORING -function SCORING:SetCoalitionChangePenalty( CoalitionChangePenalty ) - - self.CoalitionChangePenalty = CoalitionChangePenalty - return self -end - - ---- Add a new player entering a Unit. --- @param #SCORING self --- @param Wrapper.Unit#UNIT UnitData -function SCORING:_AddPlayerFromUnit( UnitData ) - self:F( UnitData ) - - if UnitData:IsAlive() then - local UnitName = UnitData:GetName() - local PlayerName = UnitData:GetPlayerName() - local UnitDesc = UnitData:GetDesc() - local UnitCategory = UnitDesc.category - local UnitCoalition = UnitData:GetCoalition() - local UnitTypeName = UnitData:GetTypeName() - - self:T( { PlayerName, UnitName, UnitCategory, UnitCoalition, UnitTypeName } ) - - if self.Players[PlayerName] == nil then -- I believe this is the place where a Player gets a life in a mission when he enters a unit ... - self.Players[PlayerName] = {} - self.Players[PlayerName].Hit = {} - self.Players[PlayerName].Destroy = {} - self.Players[PlayerName].Goals = {} - self.Players[PlayerName].Mission = {} - - -- for CategoryID, CategoryName in pairs( SCORINGCategory ) do - -- self.Players[PlayerName].Hit[CategoryID] = {} - -- self.Players[PlayerName].Destroy[CategoryID] = {} - -- end - self.Players[PlayerName].HitPlayers = {} - self.Players[PlayerName].Score = 0 - self.Players[PlayerName].Penalty = 0 - self.Players[PlayerName].PenaltyCoalition = 0 - self.Players[PlayerName].PenaltyWarning = 0 - end - - if not self.Players[PlayerName].UnitCoalition then - self.Players[PlayerName].UnitCoalition = UnitCoalition - else - if self.Players[PlayerName].UnitCoalition ~= UnitCoalition then - self.Players[PlayerName].Penalty = self.Players[PlayerName].Penalty + 50 - self.Players[PlayerName].PenaltyCoalition = self.Players[PlayerName].PenaltyCoalition + 1 - MESSAGE: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 - self.Players[PlayerName].UNIT = UnitData - - if self.Players[PlayerName].Penalty > self.Fratricide * 0.50 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 " .. self.Fratricide .. ", 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 > self.Fratricide then - UnitData:Destroy() - MESSAGE:New( "Player '" .. PlayerName .. "' committed FRATRICIDE, he will be COURT MARTIALED and is DISMISSED from this mission!", - 10 - ):ToAll() - end - - end -end - - ---- Add a goal score for a player. --- The method takes the PlayerUnit for which the Goal score needs to be set. --- The GoalTag is a string or identifier that is taken into the CSV file scoring log to identify the goal. --- A free text can be given that is shown to the players. --- The Score can be both positive and negative. --- @param #SCORING self --- @param Wrapper.Unit#UNIT PlayerUnit The @{Unit} of the Player. Other Properties for the scoring are taken from this PlayerUnit, like coalition, type etc. --- @param #string GoalTag The string or identifier that is used in the CSV file to identify the goal (sort or group later in Excel). --- @param #string Text A free text that is shown to the players. --- @param #number Score The score can be both positive or negative ( Penalty ). -function SCORING:AddGoalScore( PlayerUnit, GoalTag, Text, Score ) - - local PlayerName = PlayerUnit:GetPlayerName() - - self:E( { PlayerUnit.UnitName, PlayerName, GoalTag, Text, Score } ) - - -- PlayerName can be nil, if the Unit with the player crashed or due to another reason. - if PlayerName then - local PlayerData = self.Players[PlayerName] - - PlayerData.Goals[GoalTag] = PlayerData.Goals[GoalTag] or { Score = 0 } - PlayerData.Goals[GoalTag].Score = PlayerData.Goals[GoalTag].Score + Score - PlayerData.Score = PlayerData.Score + Score - - MESSAGE:New( Text, 30 ):ToAll() - - self:ScoreCSV( PlayerName, "GOAL_" .. string.upper( GoalTag ), 1, Score, PlayerUnit:GetName() ) - end -end - - ---- Registers Scores the players completing a Mission Task. --- @param #SCORING self --- @param Tasking.Mission#MISSION Mission --- @param Wrapper.Unit#UNIT PlayerUnit --- @param #string Text --- @param #number Score -function SCORING:_AddMissionTaskScore( Mission, PlayerUnit, Text, Score ) - - local PlayerName = PlayerUnit:GetPlayerName() - local MissionName = Mission:GetName() - - self:E( { Mission:GetName(), PlayerUnit.UnitName, PlayerName, Text, Score } ) - - -- PlayerName can be nil, if the Unit with the player crashed or due to another reason. - if PlayerName then - local PlayerData = self.Players[PlayerName] - - if not PlayerData.Mission[MissionName] then - PlayerData.Mission[MissionName] = {} - PlayerData.Mission[MissionName].ScoreTask = 0 - PlayerData.Mission[MissionName].ScoreMission = 0 - end - - self:T( PlayerName ) - self:T( PlayerData.Mission[MissionName] ) - - PlayerData.Score = self.Players[PlayerName].Score + Score - PlayerData.Mission[MissionName].ScoreTask = self.Players[PlayerName].Mission[MissionName].ScoreTask + Score - - MESSAGE:New( "Player '" .. PlayerName .. "' has " .. Text .. " in Mission '" .. MissionName .. "'. " .. - Score .. " task score!", - 30 ):ToAll() - - self:ScoreCSV( PlayerName, "TASK_" .. MissionName:gsub( ' ', '_' ), 1, Score, PlayerUnit:GetName() ) - end -end - - ---- Registers Mission Scores for possible multiple players that contributed in the Mission. --- @param #SCORING self --- @param Tasking.Mission#MISSION Mission --- @param Wrapper.Unit#UNIT PlayerUnit --- @param #string Text --- @param #number Score -function SCORING:_AddMissionScore( Mission, Text, Score ) - - local MissionName = Mission:GetName() - - self:E( { Mission, Text, Score } ) - self:E( self.Players ) - - for PlayerName, PlayerData in pairs( self.Players ) do - - self:E( PlayerData ) - if PlayerData.Mission[MissionName] then - - PlayerData.Score = PlayerData.Score + Score - PlayerData.Mission[MissionName].ScoreMission = PlayerData.Mission[MissionName].ScoreMission + Score - - MESSAGE:New( "Player '" .. PlayerName .. "' has " .. Text .. " in Mission '" .. MissionName .. "'. " .. - Score .. " mission score!", - 60 ):ToAll() - - self:ScoreCSV( PlayerName, "MISSION_" .. MissionName:gsub( ' ', '_' ), 1, Score ) - end - end -end - - ---- Handles the OnPlayerEnterUnit event for the scoring. --- @param #SCORING self --- @param Core.Event#EVENTDATA Event -function SCORING:OnEventPlayerEnterUnit( Event ) - if Event.IniUnit then - self:_AddPlayerFromUnit( Event.IniUnit ) - local Menu = MENU_GROUP:New( Event.IniGroup, 'Scoring' ) - local ReportGroupSummary = MENU_GROUP_COMMAND:New( Event.IniGroup, 'Summary report players in group', Menu, SCORING.ReportScoreGroupSummary, self, Event.IniGroup ) - local ReportGroupDetailed = MENU_GROUP_COMMAND:New( Event.IniGroup, 'Detailed report players in group', Menu, SCORING.ReportScoreGroupDetailed, self, Event.IniGroup ) - local ReportToAllSummary = MENU_GROUP_COMMAND:New( Event.IniGroup, 'Summary report all players', Menu, SCORING.ReportScoreAllSummary, self, Event.IniGroup ) - self:SetState( Event.IniUnit, "ScoringMenu", Menu ) - end -end - ---- Handles the OnPlayerLeaveUnit event for the scoring. --- @param #SCORING self --- @param Core.Event#EVENTDATA Event -function SCORING:OnEventPlayerLeaveUnit( Event ) - if Event.IniUnit then - local Menu = self:GetState( Event.IniUnit, "ScoringMenu" ) -- Core.Menu#MENU_GROUP - if Menu then - Menu:Remove() - end - end -end - - ---- Handles the OnHit event for the scoring. --- @param #SCORING self --- @param Core.Event#EVENTDATA Event -function SCORING:_EventOnHit( Event ) - self:F( { Event } ) - - local InitUnit = nil - local InitUNIT = nil - local InitUnitName = "" - local InitGroup = nil - local InitGroupName = "" - local InitPlayerName = nil - - local InitCoalition = nil - local InitCategory = nil - local InitType = nil - local InitUnitCoalition = nil - local InitUnitCategory = nil - local InitUnitType = nil - - local TargetUnit = nil - local TargetUNIT = nil - local TargetUnitName = "" - local TargetGroup = nil - local TargetGroupName = "" - local TargetPlayerName = nil - - local TargetCoalition = nil - local TargetCategory = nil - local TargetType = nil - local TargetUnitCoalition = nil - local TargetUnitCategory = nil - local TargetUnitType = nil - - if Event.IniDCSUnit then - - InitUnit = Event.IniDCSUnit - InitUNIT = Event.IniUnit - InitUnitName = Event.IniDCSUnitName - InitGroup = Event.IniDCSGroup - InitGroupName = Event.IniDCSGroupName - InitPlayerName = Event.IniPlayerName - - InitCoalition = Event.IniCoalition - --TODO: Workaround Client DCS Bug - --InitCategory = InitUnit:getCategory() - --InitCategory = InitUnit:getDesc().category - InitCategory = Event.IniCategory - InitType = Event.IniTypeName - - InitUnitCoalition = _SCORINGCoalition[InitCoalition] - InitUnitCategory = _SCORINGCategory[InitCategory] - InitUnitType = InitType - - self:T( { InitUnitName, InitGroupName, InitPlayerName, InitCoalition, InitCategory, InitType , InitUnitCoalition, InitUnitCategory, InitUnitType } ) - end - - - if Event.TgtDCSUnit then - - TargetUnit = Event.TgtDCSUnit - TargetUNIT = Event.TgtUnit - TargetUnitName = Event.TgtDCSUnitName - TargetGroup = Event.TgtDCSGroup - TargetGroupName = Event.TgtDCSGroupName - TargetPlayerName = Event.TgtPlayerName - - TargetCoalition = Event.TgtCoalition - --TODO: Workaround Client DCS Bug - --TargetCategory = TargetUnit:getCategory() - --TargetCategory = TargetUnit:getDesc().category - TargetCategory = Event.TgtCategory - TargetType = Event.TgtTypeName - - TargetUnitCoalition = _SCORINGCoalition[TargetCoalition] - TargetUnitCategory = _SCORINGCategory[TargetCategory] - TargetUnitType = TargetType - - self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType, TargetUnitCoalition, TargetUnitCategory, TargetUnitType } ) - end - - if InitPlayerName ~= nil then -- It is a player that is hitting something - self:_AddPlayerFromUnit( InitUNIT ) - if self.Players[InitPlayerName] then -- This should normally not happen, but i'll test it anyway. - if TargetPlayerName ~= nil then -- It is a player hitting another player ... - self:_AddPlayerFromUnit( TargetUNIT ) - end - - self:T( "Hitting Something" ) - - -- What is he hitting? - if TargetCategory then - - -- A target got hit, score it. - -- Player contains the score data from self.Players[InitPlayerName] - local Player = self.Players[InitPlayerName] - - -- Ensure there is a hit table per TargetCategory and TargetUnitName. - Player.Hit[TargetCategory] = Player.Hit[TargetCategory] or {} - Player.Hit[TargetCategory][TargetUnitName] = Player.Hit[TargetCategory][TargetUnitName] or {} - - -- PlayerHit contains the score counters and data per unit that was hit. - local PlayerHit = Player.Hit[TargetCategory][TargetUnitName] - - PlayerHit.Score = PlayerHit.Score or 0 - PlayerHit.Penalty = PlayerHit.Penalty or 0 - PlayerHit.ScoreHit = PlayerHit.ScoreHit or 0 - PlayerHit.PenaltyHit = PlayerHit.PenaltyHit or 0 - PlayerHit.TimeStamp = PlayerHit.TimeStamp or 0 - PlayerHit.UNIT = PlayerHit.UNIT or TargetUNIT - - -- Only grant hit scores if there was more than one second between the last hit. - if timer.getTime() - PlayerHit.TimeStamp > 1 then - PlayerHit.TimeStamp = timer.getTime() - - if TargetPlayerName ~= nil then -- It is a player hitting another player ... - - -- Ensure there is a Player to Player hit reference table. - Player.HitPlayers[TargetPlayerName] = true - end - - local Score = 0 - - if InitCoalition then -- A coalition object was hit. - if InitCoalition == TargetCoalition then - Player.Penalty = Player.Penalty + 10 - PlayerHit.Penalty = PlayerHit.Penalty + 10 - PlayerHit.PenaltyHit = PlayerHit.PenaltyHit + 1 - - if TargetPlayerName ~= nil then -- It is a player hitting another player ... - MESSAGE - :New( "Player '" .. InitPlayerName .. "' hit friendly player '" .. TargetPlayerName .. "' " .. - TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.PenaltyHit .. " times. " .. - "Penalty: -" .. PlayerHit.Penalty .. ". Score Total:" .. Player.Score - Player.Penalty, - 2 - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) - else - MESSAGE - :New( "Player '" .. InitPlayerName .. "' hit a friendly target " .. - TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.PenaltyHit .. " times. " .. - "Penalty: -" .. PlayerHit.Penalty .. ". Score Total:" .. Player.Score - Player.Penalty, - 2 - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) - end - self:ScoreCSV( InitPlayerName, "HIT_PENALTY", 1, -25, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - else - Player.Score = Player.Score + 1 - PlayerHit.Score = PlayerHit.Score + 1 - PlayerHit.ScoreHit = PlayerHit.ScoreHit + 1 - if TargetPlayerName ~= nil then -- It is a player hitting another player ... - MESSAGE - :New( "Player '" .. InitPlayerName .. "' hit enemy player '" .. TargetPlayerName .. "' " .. - TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.ScoreHit .. " times. " .. - "Score: " .. PlayerHit.Score .. ". Score Total:" .. Player.Score - Player.Penalty, - 2 - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) - else - MESSAGE - :New( "Player '" .. InitPlayerName .. "' hit an enemy target " .. - TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.ScoreHit .. " times. " .. - "Score: " .. PlayerHit.Score .. ". Score Total:" .. Player.Score - Player.Penalty, - 2 - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) - end - self:ScoreCSV( InitPlayerName, "HIT_SCORE", 1, 1, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - end - else -- A scenery object was hit. - MESSAGE - :New( "Player '" .. InitPlayerName .. "' hit a scenery object.", - 2 - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) - self:ScoreCSV( InitPlayerName, "HIT_SCORE", 1, 1, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, "", "Scenery", TargetUnitType ) - end - end - end - end - elseif InitPlayerName == nil then -- It is an AI hitting a player??? - - end -end - ---- Track DEAD or CRASH events for the scoring. --- @param #SCORING self --- @param Core.Event#EVENTDATA Event -function SCORING:_EventOnDeadOrCrash( Event ) - self:F( { Event } ) - - local TargetUnit = nil - local TargetGroup = nil - local TargetUnitName = "" - local TargetGroupName = "" - local TargetPlayerName = "" - local TargetCoalition = nil - local TargetCategory = nil - local TargetType = nil - local TargetUnitCoalition = nil - local TargetUnitCategory = nil - local TargetUnitType = nil - - if Event.IniDCSUnit then - - TargetUnit = Event.IniUnit - TargetUnitName = Event.IniDCSUnitName - TargetGroup = Event.IniDCSGroup - TargetGroupName = Event.IniDCSGroupName - TargetPlayerName = Event.IniPlayerName - - TargetCoalition = Event.IniCoalition - --TargetCategory = TargetUnit:getCategory() - --TargetCategory = TargetUnit:getDesc().category -- Workaround - TargetCategory = Event.IniCategory - TargetType = Event.IniTypeName - - TargetUnitCoalition = _SCORINGCoalition[TargetCoalition] - TargetUnitCategory = _SCORINGCategory[TargetCategory] - TargetUnitType = TargetType - - self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType } ) - end - - -- Player contains the score and reference data for the player. - for PlayerName, Player in pairs( self.Players ) do - if Player then -- This should normally not happen, but i'll test it anyway. - self:T( "Something got destroyed" ) - - -- Some variables - local InitUnitName = Player.UnitName - local InitUnitType = Player.UnitType - local InitCoalition = Player.UnitCoalition - local InitCategory = Player.UnitCategory - local InitUnitCoalition = _SCORINGCoalition[InitCoalition] - local InitUnitCategory = _SCORINGCategory[InitCategory] - - self:T( { InitUnitName, InitUnitType, InitUnitCoalition, InitCoalition, InitUnitCategory, InitCategory } ) - - -- What is the player destroying? - if Player and Player.Hit and Player.Hit[TargetCategory] and Player.Hit[TargetCategory][TargetUnitName] then -- Was there a hit for this unit for this player before registered??? - - Player.Destroy[TargetCategory] = Player.Destroy[TargetCategory] or {} - Player.Destroy[TargetCategory][TargetType] = Player.Destroy[TargetCategory][TargetType] or {} - - -- PlayerDestroy contains the destroy score data per category and target type of the player. - local TargetDestroy = Player.Destroy[TargetCategory][TargetType] - TargetDestroy.Score = TargetDestroy.Score or 0 - TargetDestroy.ScoreDestroy = TargetDestroy.ScoreDestroy or 0 - TargetDestroy.Penalty = TargetDestroy.Penalty or 0 - TargetDestroy.PenaltyDestroy = TargetDestroy.PenaltyDestroy or 0 - - if TargetCoalition then - if InitCoalition == TargetCoalition then - local ThreatLevelTarget, ThreatTypeTarget = TargetUnit:GetThreatLevel() - local ThreatLevelPlayer = Player.UNIT:GetThreatLevel() / 10 + 1 - local ThreatPenalty = math.ceil( ( ThreatLevelTarget / ThreatLevelPlayer ) * self.ScaleDestroyPenalty / 10 ) - self:E( { ThreatLevel = ThreatPenalty, ThreatLevelTarget = ThreatLevelTarget, ThreatTypeTarget = ThreatTypeTarget, ThreatLevelPlayer = ThreatLevelPlayer } ) - - Player.Penalty = Player.Penalty + ThreatPenalty - TargetDestroy.Penalty = TargetDestroy.Penalty + ThreatPenalty - TargetDestroy.PenaltyDestroy = TargetDestroy.PenaltyDestroy + 1 - - if Player.HitPlayers[TargetPlayerName] then -- A player destroyed another player - MESSAGE - :New( "Player '" .. PlayerName .. "' destroyed friendly player '" .. TargetPlayerName .. "' " .. - TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. TargetDestroy.PenaltyDestroy .. " times. " .. - "Penalty: -" .. TargetDestroy.Penalty .. ". Score Total:" .. Player.Score - Player.Penalty, - 15 - ) - :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) - else - MESSAGE - :New( "Player '" .. PlayerName .. "' destroyed a friendly target " .. - TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. TargetDestroy.PenaltyDestroy .. " times. " .. - "Penalty: -" .. TargetDestroy.Penalty .. ". Score Total:" .. Player.Score - Player.Penalty, - 15 - ) - :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) - end - self:ScoreCSV( PlayerName, "DESTROY_PENALTY", 1, ThreatPenalty, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - else - - local ThreatLevelTarget, ThreatTypeTarget = TargetUnit:GetThreatLevel() - local ThreatLevelPlayer = Player.UNIT:GetThreatLevel() / 10 + 1 - local ThreatScore = math.ceil( ( ThreatLevelTarget / ThreatLevelPlayer ) * self.ScaleDestroyScore / 10 ) - - self:E( { ThreatLevel = ThreatScore, ThreatLevelTarget = ThreatLevelTarget, ThreatTypeTarget = ThreatTypeTarget, ThreatLevelPlayer = ThreatLevelPlayer } ) - - Player.Score = Player.Score + ThreatScore - TargetDestroy.Score = TargetDestroy.Score + ThreatScore - TargetDestroy.ScoreDestroy = TargetDestroy.ScoreDestroy + 1 - if Player.HitPlayers[TargetPlayerName] then -- A player destroyed another player - MESSAGE - :New( "Player '" .. PlayerName .. "' destroyed enemy player '" .. TargetPlayerName .. "' " .. - TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. TargetDestroy.ScoreDestroy .. " times. " .. - "Score: " .. TargetDestroy.Score .. ". Score Total:" .. Player.Score - Player.Penalty, - 15 - ) - :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) - else - MESSAGE - :New( "Player '" .. PlayerName .. "' destroyed an enemy " .. - TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. TargetDestroy.ScoreDestroy .. " times. " .. - "Score: " .. TargetDestroy.Score .. ". Total:" .. Player.Score - Player.Penalty, - 15 - ) - :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) - end - self:ScoreCSV( PlayerName, "DESTROY_SCORE", 1, ThreatScore, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - - local UnitName = TargetUnit:GetName() - local Score = self.ScoringObjects[UnitName] - if Score then - Player.Score = Player.Score + Score - TargetDestroy.Score = TargetDestroy.Score + Score - MESSAGE - :New( "Special target '" .. TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. " destroyed! " .. - "Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! Total: " .. Player.Score - Player.Penalty, - 15 - ) - :ToAllIf( self:IfMessagesScore() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesScore() and self:IfMessagesToCoalition() ) - self:ScoreCSV( PlayerName, "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - end - - -- Check if there are Zones where the destruction happened. - for ZoneName, ScoreZoneData in pairs( self.ScoringZones ) do - self:E( { ScoringZone = ScoreZoneData } ) - local ScoreZone = ScoreZoneData.ScoreZone -- Core.Zone#ZONE_BASE - local Score = ScoreZoneData.Score - if ScoreZone:IsVec2InZone( TargetUnit:GetVec2() ) then - Player.Score = Player.Score + Score - TargetDestroy.Score = TargetDestroy.Score + Score - MESSAGE - :New( "Target destroyed in zone '" .. ScoreZone:GetName() .. "'." .. - "Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! " .. - "Total: " .. Player.Score - Player.Penalty, - 15 ) - :ToAllIf( self:IfMessagesZone() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesZone() and self:IfMessagesToCoalition() ) - self:ScoreCSV( PlayerName, "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - end - end - - end - else - -- Check if there are Zones where the destruction happened. - for ZoneName, ScoreZoneData in pairs( self.ScoringZones ) do - self:E( { ScoringZone = ScoreZoneData } ) - local ScoreZone = ScoreZoneData.ScoreZone -- Core.Zone#ZONE_BASE - local Score = ScoreZoneData.Score - if ScoreZone:IsVec2InZone( TargetUnit:GetVec2() ) then - Player.Score = Player.Score + Score - TargetDestroy.Score = TargetDestroy.Score + Score - MESSAGE - :New( "Scenery destroyed in zone '" .. ScoreZone:GetName() .. "'." .. - "Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! " .. - "Total: " .. Player.Score - Player.Penalty, - 15 - ) - :ToAllIf( self:IfMessagesZone() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesZone() and self:IfMessagesToCoalition() ) - self:ScoreCSV( PlayerName, "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, "", "Scenery", TargetUnitType ) - end - end - end - end - end - end -end - - ---- Produce detailed report of player hit scores. --- @param #SCORING self --- @param #string PlayerName The name of the player. --- @return #string The report. -function SCORING:ReportDetailedPlayerHits( PlayerName ) - - local ScoreMessage = "" - local PlayerScore = 0 - local PlayerPenalty = 0 - - local PlayerData = self.Players[PlayerName] - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) - - -- Some variables - local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] - local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local ScoreMessageHits = "" - for CategoryID, CategoryName in pairs( _SCORINGCategory ) do - self:T( CategoryName ) - if PlayerData.Hit[CategoryID] then - self:T( "Hit scores exist for player " .. PlayerName ) - local Score = 0 - local ScoreHit = 0 - local Penalty = 0 - local PenaltyHit = 0 - for UnitName, UnitData in pairs( PlayerData.Hit[CategoryID] ) do - Score = Score + UnitData.Score - ScoreHit = ScoreHit + UnitData.ScoreHit - Penalty = Penalty + UnitData.Penalty - PenaltyHit = UnitData.PenaltyHit - end - local ScoreMessageHit = string.format( "%s:%d ", CategoryName, Score - Penalty ) - self:T( ScoreMessageHit ) - ScoreMessageHits = ScoreMessageHits .. ScoreMessageHit - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageHits ~= "" then - ScoreMessage = "Hits: " .. ScoreMessageHits - end - end - - return ScoreMessage, PlayerScore, PlayerPenalty -end - - ---- Produce detailed report of player destroy scores. --- @param #SCORING self --- @param #string PlayerName The name of the player. --- @return #string The report. -function SCORING:ReportDetailedPlayerDestroys( PlayerName ) - - local ScoreMessage = "" - local PlayerScore = 0 - local PlayerPenalty = 0 - - local PlayerData = self.Players[PlayerName] - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) - - -- Some variables - local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] - local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local ScoreMessageDestroys = "" - for CategoryID, CategoryName in pairs( _SCORINGCategory ) do - if PlayerData.Destroy[CategoryID] then - self:T( "Destroy scores exist for player " .. PlayerName ) - local Score = 0 - local ScoreDestroy = 0 - local Penalty = 0 - local PenaltyDestroy = 0 - - for UnitName, UnitData in pairs( PlayerData.Destroy[CategoryID] ) do - self:E( { UnitData = UnitData } ) - if UnitData ~= {} then - Score = Score + UnitData.Score - ScoreDestroy = ScoreDestroy + UnitData.ScoreDestroy - Penalty = Penalty + UnitData.Penalty - PenaltyDestroy = PenaltyDestroy + UnitData.PenaltyDestroy - end - end - - local ScoreMessageDestroy = string.format( " %s:%d ", CategoryName, Score - Penalty ) - self:T( ScoreMessageDestroy ) - ScoreMessageDestroys = ScoreMessageDestroys .. ScoreMessageDestroy - - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageDestroys = ScoreMessageDestroys .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageDestroys ~= "" then - ScoreMessage = "Destroys: " .. ScoreMessageDestroys - end - end - - return ScoreMessage, PlayerScore, PlayerPenalty -end - ---- Produce detailed report of player penalty scores because of changing the coalition. --- @param #SCORING self --- @param #string PlayerName The name of the player. --- @return #string The report. -function SCORING:ReportDetailedPlayerCoalitionChanges( PlayerName ) - - local ScoreMessage = "" - local PlayerScore = 0 - local PlayerPenalty = 0 - - local PlayerData = self.Players[PlayerName] - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) - - -- Some variables - local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] - local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local ScoreMessageCoalitionChangePenalties = "" - if PlayerData.PenaltyCoalition ~= 0 then - ScoreMessageCoalitionChangePenalties = ScoreMessageCoalitionChangePenalties .. string.format( " -%d (%d changed)", PlayerData.Penalty, PlayerData.PenaltyCoalition ) - PlayerPenalty = PlayerPenalty + PlayerData.Penalty - end - if ScoreMessageCoalitionChangePenalties ~= "" then - ScoreMessage = ScoreMessage .. "Coalition Penalties: " .. ScoreMessageCoalitionChangePenalties - end - end - - return ScoreMessage, PlayerScore, PlayerPenalty -end - ---- Produce detailed report of player goal scores. --- @param #SCORING self --- @param #string PlayerName The name of the player. --- @return #string The report. -function SCORING:ReportDetailedPlayerGoals( PlayerName ) - - local ScoreMessage = "" - local PlayerScore = 0 - local PlayerPenalty = 0 - - local PlayerData = self.Players[PlayerName] - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) - - -- Some variables - local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] - local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local ScoreMessageGoal = "" - local ScoreGoal = 0 - local ScoreTask = 0 - for GoalName, GoalData in pairs( PlayerData.Goals ) do - ScoreGoal = ScoreGoal + GoalData.Score - ScoreMessageGoal = ScoreMessageGoal .. "'" .. GoalName .. "':" .. GoalData.Score .. "; " - end - PlayerScore = PlayerScore + ScoreGoal - - if ScoreMessageGoal ~= "" then - ScoreMessage = "Goals: " .. ScoreMessageGoal - end - end - - return ScoreMessage, PlayerScore, PlayerPenalty -end - ---- Produce detailed report of player penalty scores because of changing the coalition. --- @param #SCORING self --- @param #string PlayerName The name of the player. --- @return #string The report. -function SCORING:ReportDetailedPlayerMissions( PlayerName ) - - local ScoreMessage = "" - local PlayerScore = 0 - local PlayerPenalty = 0 - - local PlayerData = self.Players[PlayerName] - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) - - -- Some variables - local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] - local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local ScoreMessageMission = "" - local ScoreMission = 0 - local ScoreTask = 0 - for MissionName, MissionData in pairs( PlayerData.Mission ) do - ScoreMission = ScoreMission + MissionData.ScoreMission - ScoreTask = ScoreTask + MissionData.ScoreTask - ScoreMessageMission = ScoreMessageMission .. "'" .. MissionName .. "'; " - end - PlayerScore = PlayerScore + ScoreMission + ScoreTask - - if ScoreMessageMission ~= "" then - ScoreMessage = "Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ")" - end - end - - return ScoreMessage, PlayerScore, PlayerPenalty -end - - ---- Report Group Score Summary --- @param #SCORING self --- @param Wrapper.Group#GROUP PlayerGroup The player group. -function SCORING:ReportScoreGroupSummary( PlayerGroup ) - - local PlayerMessage = "" - - self:T( "Report Score Group Summary" ) - - local PlayerUnits = PlayerGroup:GetUnits() - for UnitID, PlayerUnit in pairs( PlayerUnits ) do - local PlayerUnit = PlayerUnit -- Wrapper.Unit#UNIT - local PlayerName = PlayerUnit:GetPlayerName() - - if PlayerName then - - local ReportHits, ScoreHits, PenaltyHits = self:ReportDetailedPlayerHits( PlayerName ) - ReportHits = ReportHits ~= "" and "\n- " .. ReportHits or ReportHits - self:E( { ReportHits, ScoreHits, PenaltyHits } ) - - local ReportDestroys, ScoreDestroys, PenaltyDestroys = self:ReportDetailedPlayerDestroys( PlayerName ) - ReportDestroys = ReportDestroys ~= "" and "\n- " .. ReportDestroys or ReportDestroys - self:E( { ReportDestroys, ScoreDestroys, PenaltyDestroys } ) - - local ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges = self:ReportDetailedPlayerCoalitionChanges( PlayerName ) - ReportCoalitionChanges = ReportCoalitionChanges ~= "" and "\n- " .. ReportCoalitionChanges or ReportCoalitionChanges - self:E( { ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges } ) - - local ReportGoals, ScoreGoals, PenaltyGoals = self:ReportDetailedPlayerGoals( PlayerName ) - ReportGoals = ReportGoals ~= "" and "\n- " .. ReportGoals or ReportGoals - self:E( { ReportGoals, ScoreGoals, PenaltyGoals } ) - - local ReportMissions, ScoreMissions, PenaltyMissions = self:ReportDetailedPlayerMissions( PlayerName ) - ReportMissions = ReportMissions ~= "" and "\n- " .. ReportMissions or ReportMissions - self:E( { ReportMissions, ScoreMissions, PenaltyMissions } ) - - local PlayerScore = ScoreHits + ScoreDestroys + ScoreCoalitionChanges + ScoreGoals + ScoreMissions - local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + ScoreGoals + PenaltyMissions - - PlayerMessage = - string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )", - PlayerName, - PlayerScore - PlayerPenalty, - PlayerScore, - PlayerPenalty - ) - MESSAGE:New( PlayerMessage, 30, "Player '" .. PlayerName .. "'" ):ToGroup( PlayerGroup ) - end - end - -end - ---- Report Group Score Detailed --- @param #SCORING self --- @param Wrapper.Group#GROUP PlayerGroup The player group. -function SCORING:ReportScoreGroupDetailed( PlayerGroup ) - - local PlayerMessage = "" - - self:T( "Report Score Group Detailed" ) - - local PlayerUnits = PlayerGroup:GetUnits() - for UnitID, PlayerUnit in pairs( PlayerUnits ) do - local PlayerUnit = PlayerUnit -- Wrapper.Unit#UNIT - local PlayerName = PlayerUnit:GetPlayerName() - - if PlayerName then - - local ReportHits, ScoreHits, PenaltyHits = self:ReportDetailedPlayerHits( PlayerName ) - ReportHits = ReportHits ~= "" and "\n- " .. ReportHits or ReportHits - self:E( { ReportHits, ScoreHits, PenaltyHits } ) - - local ReportDestroys, ScoreDestroys, PenaltyDestroys = self:ReportDetailedPlayerDestroys( PlayerName ) - ReportDestroys = ReportDestroys ~= "" and "\n- " .. ReportDestroys or ReportDestroys - self:E( { ReportDestroys, ScoreDestroys, PenaltyDestroys } ) - - local ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges = self:ReportDetailedPlayerCoalitionChanges( PlayerName ) - ReportCoalitionChanges = ReportCoalitionChanges ~= "" and "\n- " .. ReportCoalitionChanges or ReportCoalitionChanges - self:E( { ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges } ) - - local ReportGoals, ScoreGoals, PenaltyGoals = self:ReportDetailedPlayerGoals( PlayerName ) - ReportGoals = ReportGoals ~= "" and "\n- " .. ReportGoals or ReportGoals - self:E( { ReportGoals, ScoreGoals, PenaltyGoals } ) - - local ReportMissions, ScoreMissions, PenaltyMissions = self:ReportDetailedPlayerMissions( PlayerName ) - ReportMissions = ReportMissions ~= "" and "\n- " .. ReportMissions or ReportMissions - self:E( { ReportMissions, ScoreMissions, PenaltyMissions } ) - - local PlayerScore = ScoreHits + ScoreDestroys + ScoreCoalitionChanges + ScoreGoals + ScoreMissions - local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + ScoreGoals + PenaltyMissions - - PlayerMessage = - string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )%s%s%s%s%s", - PlayerName, - PlayerScore - PlayerPenalty, - PlayerScore, - PlayerPenalty, - ReportHits, - ReportDestroys, - ReportCoalitionChanges, - ReportGoals, - ReportMissions - ) - MESSAGE:New( PlayerMessage, 30, "Player '" .. PlayerName .. "'" ):ToGroup( PlayerGroup ) - end - end - -end - ---- Report all players score --- @param #SCORING self --- @param Wrapper.Group#GROUP PlayerGroup The player group. -function SCORING:ReportScoreAllSummary( PlayerGroup ) - - local PlayerMessage = "" - - self:T( "Report Score All Players" ) - - for PlayerName, PlayerData in pairs( self.Players ) do - - if PlayerName then - - local ReportHits, ScoreHits, PenaltyHits = self:ReportDetailedPlayerHits( PlayerName ) - ReportHits = ReportHits ~= "" and "\n- " .. ReportHits or ReportHits - self:E( { ReportHits, ScoreHits, PenaltyHits } ) - - local ReportDestroys, ScoreDestroys, PenaltyDestroys = self:ReportDetailedPlayerDestroys( PlayerName ) - ReportDestroys = ReportDestroys ~= "" and "\n- " .. ReportDestroys or ReportDestroys - self:E( { ReportDestroys, ScoreDestroys, PenaltyDestroys } ) - - local ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges = self:ReportDetailedPlayerCoalitionChanges( PlayerName ) - ReportCoalitionChanges = ReportCoalitionChanges ~= "" and "\n- " .. ReportCoalitionChanges or ReportCoalitionChanges - self:E( { ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges } ) - - local ReportGoals, ScoreGoals, PenaltyGoals = self:ReportDetailedPlayerGoals( PlayerName ) - ReportGoals = ReportGoals ~= "" and "\n- " .. ReportGoals or ReportGoals - self:E( { ReportGoals, ScoreGoals, PenaltyGoals } ) - - local ReportMissions, ScoreMissions, PenaltyMissions = self:ReportDetailedPlayerMissions( PlayerName ) - ReportMissions = ReportMissions ~= "" and "\n- " .. ReportMissions or ReportMissions - self:E( { ReportMissions, ScoreMissions, PenaltyMissions } ) - - local PlayerScore = ScoreHits + ScoreDestroys + ScoreCoalitionChanges + ScoreGoals + ScoreMissions - local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + ScoreGoals + PenaltyMissions - - PlayerMessage = - string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )", - PlayerName, - PlayerScore - PlayerPenalty, - PlayerScore, - PlayerPenalty - ) - MESSAGE:New( PlayerMessage, 30, "Player '" .. PlayerName .. "'" ):ToGroup( PlayerGroup ) - end - end - -end - - -function SCORING:SecondsToClock(sSeconds) - local nSeconds = sSeconds - if nSeconds == 0 then - --return nil; - return "00:00:00"; - else - nHours = string.format("%02.f", math.floor(nSeconds/3600)); - nMins = string.format("%02.f", math.floor(nSeconds/60 - (nHours*60))); - nSecs = string.format("%02.f", math.floor(nSeconds - nHours*3600 - nMins *60)); - return nHours..":"..nMins..":"..nSecs - end -end - ---- Opens a score CSV file to log the scores. --- @param #SCORING self --- @param #string ScoringCSV --- @return #SCORING self --- @usage --- -- Open a new CSV file to log the scores of the game Gori Valley. Let the name of the CSV file begin with "Player Scores". --- ScoringObject = SCORING:New( "Gori Valley" ) --- ScoringObject:OpenCSV( "Player Scores" ) -function SCORING:OpenCSV( ScoringCSV ) - self:F( ScoringCSV ) - - if lfs and io and os then - if ScoringCSV then - self.ScoringCSV = ScoringCSV - local fdir = lfs.writedir() .. [[Logs\]] .. self.ScoringCSV .. " " .. os.date( "%Y-%m-%d %H-%M-%S" ) .. ".csv" - - self.CSVFile, self.err = io.open( fdir, "w+" ) - if not self.CSVFile then - error( "Error: Cannot open CSV file in " .. lfs.writedir() ) - end - - self.CSVFile:write( '"GameName","RunTime","Time","PlayerName","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 - - TargetUnitCoalition = TargetUnitCoalition or "" - TargetUnitCategory = TargetUnitCategory or "" - TargetUnitType = TargetUnitType or "" - TargetUnitName = TargetUnitName or "" - - if lfs and io and os then - self.CSVFile:write( - '"' .. self.GameName .. '"' .. ',' .. - '"' .. self.RunTime .. '"' .. ',' .. - '' .. ScoreTime .. '' .. ',' .. - '"' .. PlayerName .. '"' .. ',' .. - '"' .. 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 - ---- 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 Core.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 = 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 Dcs.DCSWrapper.Group#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 @{DCSWrapper.Unit#Unit} from the simulator, but checks first if it is still existing! --- @param #CLEANUP self --- @param Dcs.DCSWrapper.Unit#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 Dcs.DCSTypes#Weapon ---- Destroys a missile from the simulator, but checks first if it is still existing! --- @param #CLEANUP self --- @param Dcs.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 Dcs.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 Dcs.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() - 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 Dcs.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 ) - 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 ) - SCHEDULER:New( self, CLEANUP._DestroyUnit, { Event.TgtDCSUnit }, 0.1 ) - end - end - end -end - ---- Add the @{DCSWrapper.Unit#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 Dcs.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 - ---- Single-Player:**Yes** / Multi-Player:**Yes** / AI:**Yes** / Human:**No** / Types:**All** -- --- **Spawn groups of units dynamically in your missions.** --- --- ![Banner Image](..\Presentations\SPAWN\SPAWN.JPG) --- --- === --- --- # 1) @{#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 methods (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 represents the GROUP Template (definition). --- * @{#SPAWN.NewWithAlias}(): Creates a new SPAWN object taking the name of the group that represents the GROUP Template (definition), and gives each spawned @{Group} an different name. --- --- It is important to understand how the SPAWN class works internally. The SPAWN object created will contain internally a list of groups that will be spawned and that are already spawned. --- The initialization methods will modify this list of groups so that when a group gets spawned, ALL information is already prepared when spawning. This is done for performance reasons. --- So in principle, the group list will contain all parameters and configurations after initialization, and when groups get actually spawned, this spawning can be done quickly and efficient. --- --- ## 1.2) SPAWN initialization methods --- --- A spawn object will behave differently based on the usage of **initialization** methods, which all start with the **Init** prefix: --- --- * @{#SPAWN.InitLimit}(): Limits the amount of groups that can be alive at the same time and that can be dynamically spawned. --- * @{#SPAWN.InitRandomizeRoute}(): Randomize the routes of spawned groups, and for air groups also optionally the height. --- * @{#SPAWN.InitRandomizeTemplate}(): Randomize the group templates so that when a new group is spawned, a random group template is selected from one of the templates defined. --- * @{#SPAWN.InitUnControlled}(): Spawn plane groups uncontrolled. --- * @{#SPAWN.InitArray}(): 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 methods are @{#SPAWN.InitRepeatOnLanding} and @{#SPAWN.InitRepeatOnEngineShutDown}. --- * @{#SPAWN.InitRandomizeUnits}(): Randomizes the @{Unit}s in the @{Group} that is spawned within a **radius band**, given an Outer and Inner radius. --- * @{#SPAWN.InitRandomizeZones}(): Randomizes the spawning between a predefined list of @{Zone}s that are declared using this function. Each zone can be given a probability factor. --- * @{#SPAWN.InitAIOn}(): Turns the AI On when spawning the new @{Group} object. --- * @{#SPAWN.InitAIOff}(): Turns the AI Off when spawning the new @{Group} object. --- * @{#SPAWN.InitAIOnOff}(): Turns the AI On or Off when spawning the new @{Group} object. --- --- ## 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.SpawnFromVec3}(): Spawn a new group from a Vec3 coordinate. (The group will can be spawned at a point in the air). --- * @{#SPAWN.SpawnFromVec2}(): Spawn a new group from a Vec2 coordinate. (The group will be spawned at land height ). --- * @{#SPAWN.SpawnFromStatic}(): Spawn a new group from a structure, taking the position of a @{Static}. --- * @{#SPAWN.SpawnFromUnit}(): Spawn a new group taking the position of a @{Unit}. --- * @{#SPAWN.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) Retrieve alive GROUPs spawned by the SPAWN object --- --- The SPAWN class administers which GROUPS it has reserved (in stock) or has created during mission execution. --- Every time a SPAWN object spawns a new GROUP object, a reference to the GROUP object is added to an internal table of GROUPS. --- SPAWN provides methods to iterate through that internal GROUP object reference table: --- --- * @{#SPAWN.GetFirstAliveGroup}(): Will find the first alive GROUP it has spawned, and return the alive GROUP object and the first Index where the first alive GROUP object has been found. --- * @{#SPAWN.GetNextAliveGroup}(): Will find the next alive GROUP object from a given Index, and return a reference to the alive GROUP object and the next Index where the alive GROUP has been found. --- * @{#SPAWN.GetLastAliveGroup}(): Will find the last alive GROUP object, and will return a reference to the last live GROUP object and the last Index where the last alive GROUP object has been found. --- --- You can use the methods @{#SPAWN.GetFirstAliveGroup}() and sequently @{#SPAWN.GetNextAliveGroup}() to iterate through the alive GROUPS within the SPAWN object, and to actions... See the respective methods for an example. --- The method @{#SPAWN.GetGroupFromIndex}() will return the GROUP object reference from the given Index, dead or alive... --- --- ## 1.5) 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.InitCleanUp}() initialization method has been defined that will silently monitor the status of each spawned group. --- Once a group has a velocity = 0, and has been waiting for a defined interval, that group will be cleaned or removed from run-time. --- There is a catch however :-) If a damaged group has returned to an airbase within the coalition, that group will not be considered as "lost"... --- In such a case, when the inactive group is cleaned, a new group will Re-spawned automatically. --- This models AI that has succesfully returned to their airbase, to restart their combat activities. --- Check the @{#SPAWN.InitCleanUp}() for further info. --- --- ## 1.6) Catch the @{Group} spawn event in a callback function! --- --- When using the SpawnScheduled method, new @{Group}s are created following the schedule timing parameters. --- When a new @{Group} is spawned, you maybe want to execute actions with that group spawned at the spawn event. --- To SPAWN class supports this functionality through the @{#SPAWN.OnSpawnGroup}( **function( SpawnedGroup ) end ** ) method, which takes a function as a parameter that you can define locally. --- Whenever a new @{Group} is spawned, the given function is called, and the @{Group} that was just spawned, is given as a parameter. --- As a result, your spawn event handling function requires one parameter to be declared, which will contain the spawned @{Group} object. --- A coding example is provided at the description of the @{#SPAWN.OnSpawnGroup}( **function( SpawnedGroup ) end ** ) method. --- --- ==== --- --- # **API CHANGE HISTORY** --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- Hereby the change log: --- --- 2017-02-04: SPAWN:InitUnControlled( **UnControlled** ) replaces SPAWN:InitUnControlled(). --- --- 2017-01-24: SPAWN:**InitAIOnOff( AIOnOff )** added. --- --- 2017-01-24: SPAWN:**InitAIOn()** added. --- --- 2017-01-24: SPAWN:**InitAIOff()** added. --- --- 2016-08-15: SPAWN:**InitCleanUp**( SpawnCleanUpInterval ) replaces SPAWN:_CleanUp_( SpawnCleanUpInterval ). --- --- 2016-08-15: SPAWN:**InitRandomizeZones( SpawnZones )** added. --- --- 2016-08-14: SPAWN:**OnSpawnGroup**( SpawnCallBackFunction, ... ) replaces SPAWN:_SpawnFunction_( SpawnCallBackFunction, ... ). --- --- 2016-08-14: SPAWN.SpawnInZone( Zone, __RandomizeGroup__, SpawnIndex ) replaces SpawnInZone( Zone, _RandomizeUnits, OuterRadius, InnerRadius,_ SpawnIndex ). --- --- 2016-08-14: SPAWN.SpawnFromVec3( Vec3, SpawnIndex ) replaces SpawnFromVec3( Vec3, _RandomizeUnits, OuterRadius, InnerRadius,_ SpawnIndex ): --- --- 2016-08-14: SPAWN.SpawnFromVec2( Vec2, SpawnIndex ) replaces SpawnFromVec2( Vec2, _RandomizeUnits, OuterRadius, InnerRadius,_ SpawnIndex ): --- --- 2016-08-14: SPAWN.SpawnFromUnit( SpawnUnit, SpawnIndex ) replaces SpawnFromUnit( SpawnUnit, _RandomizeUnits, OuterRadius, InnerRadius,_ SpawnIndex ): --- --- 2016-08-14: SPAWN.SpawnFromUnit( SpawnUnit, SpawnIndex ) replaces SpawnFromStatic( SpawnStatic, _RandomizeUnits, OuterRadius, InnerRadius,_ SpawnIndex ): --- --- 2016-08-14: SPAWN.**InitRandomizeUnits( RandomizeUnits, OuterRadius, InnerRadius )** added: --- --- 2016-08-14: SPAWN.**Init**Limit( SpawnMaxUnitsAlive, SpawnMaxGroups ) replaces SPAWN._Limit_( SpawnMaxUnitsAlive, SpawnMaxGroups ): --- --- 2016-08-14: SPAWN.**Init**Array( SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY ) replaces SPAWN._Array_( SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY ). --- --- 2016-08-14: SPAWN.**Init**RandomizeRoute( SpawnStartPoint, SpawnEndPoint, SpawnRadius, SpawnHeight ) replaces SPAWN._RandomizeRoute_( SpawnStartPoint, SpawnEndPoint, SpawnRadius, SpawnHeight ). --- --- 2016-08-14: SPAWN.**Init**RandomizeTemplate( SpawnTemplatePrefixTable ) replaces SPAWN._RandomizeTemplate_( SpawnTemplatePrefixTable ). --- --- 2016-08-14: SPAWN.**Init**UnControlled() replaces SPAWN._UnControlled_(). --- --- === --- --- # **AUTHORS and CONTRIBUTIONS** --- --- ### Contributions: --- --- * **Aaron**: Posed the idea for Group position randomization at SpawnInZone and make the Unit randomization separate from the Group randomization. --- * [**Entropy**](https://forums.eagle.ru/member.php?u=111471), **Afinegan**: Came up with the requirement for AIOnOff(). --- --- ### Authors: --- --- * **FlightControl**: Design & Programming --- --- @module Spawn - - - ---- SPAWN Class --- @type SPAWN --- @extends Core.Base#BASE --- @field ClassName --- @field #string SpawnTemplatePrefix --- @field #string SpawnAliasPrefix --- @field #number AliveUnits --- @field #number MaxAliveUnits --- @field #number SpawnIndex --- @field #number MaxAliveGroups --- @field #SPAWN.SpawnZoneTable SpawnZoneTable -SPAWN = { - ClassName = "SPAWN", - SpawnTemplatePrefix = nil, - SpawnAliasPrefix = nil, -} - - ---- @type SPAWN.SpawnZoneTable --- @list SpawnZone - - ---- 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() ) -- #SPAWN - 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.AIOnOff = true -- The AI is on by default when spawning a group. - self.SpawnUnControlled = false - - 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.AIOnOff = true -- The AI is on by default when spawning a group. - self.SpawnUnControlled = false - - 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 method should be used... --- When a @{#SPAWN.New} is executed and the limit of the amount of units alive is reached, then no new spawn will happen of the group, until some of these units of the spawn object will be destroyed. --- @param #SPAWN self --- @param #number SpawnMaxUnitsAlive The maximum amount of units that can be alive at runtime. --- @param #number SpawnMaxGroups The maximum amount of groups that can be spawned. When the limit is reached, then no more actual spawns will happen of the group. --- This parameter is useful to define a maximum amount of airplanes, ground troops, helicopters, ships etc within a supply area. --- This parameter accepts the value 0, which defines that there are no maximum group limits, but there are limits on the maximum of units that can be alive at the same time. --- @return #SPAWN self --- @usage --- -- NATO helicopters engaging in the battle field. --- -- This helicopter group consists of one Unit. So, this group will SPAWN maximum 2 groups simultaneously within the DCSRTE. --- -- There will be maximum 24 groups spawned during the whole mission lifetime. --- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):InitLimit( 2, 24 ) -function SPAWN:InitLimit( SpawnMaxUnitsAlive, SpawnMaxGroups ) - self:F( { self.SpawnTemplatePrefix, SpawnMaxUnitsAlive, SpawnMaxGroups } ) - - self.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 ... --- @param #number SpawnHeight (optional) Specifies the **additional** height in meters that can be added to the base height specified at each waypoint in the ME. --- @return #SPAWN --- @usage --- -- NATO helicopters engaging in the battle field. --- -- The KA-50 has waypoints Start point ( =0 or SP ), 1, 2, 3, 4, End point (= 5 or DP). --- -- Waypoints 2 and 3 will only be randomized. The others will remain on their original position with each new spawn of the helicopter. --- -- The randomization of waypoint 2 and 3 will take place within a radius of 2000 meters. --- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):InitRandomizeRoute( 2, 2, 2000 ) -function SPAWN:InitRandomizeRoute( SpawnStartPoint, SpawnEndPoint, SpawnRadius, SpawnHeight ) - self:F( { self.SpawnTemplatePrefix, SpawnStartPoint, SpawnEndPoint, SpawnRadius, SpawnHeight } ) - - self.SpawnRandomizeRoute = true - self.SpawnRandomizeRouteStartPoint = SpawnStartPoint - self.SpawnRandomizeRouteEndPoint = SpawnEndPoint - self.SpawnRandomizeRouteRadius = SpawnRadius - self.SpawnRandomizeRouteHeight = SpawnHeight - - for GroupID = 1, self.SpawnMaxGroups do - self:_RandomizeRoute( GroupID ) - end - - return self -end - ---- Randomizes the UNITs that are spawned within a radius band given an Outer and Inner radius. --- @param #SPAWN self --- @param #boolean RandomizeUnits If true, SPAWN will perform the randomization of the @{UNIT}s position within the group between a given outer and inner radius. --- @param Dcs.DCSTypes#Distance OuterRadius (optional) The outer radius in meters where the new group will be spawned. --- @param Dcs.DCSTypes#Distance InnerRadius (optional) The inner radius in meters where the new group will NOT be spawned. --- @return #SPAWN --- @usage --- -- NATO helicopters engaging in the battle field. --- -- The KA-50 has waypoints Start point ( =0 or SP ), 1, 2, 3, 4, End point (= 5 or DP). --- -- Waypoints 2 and 3 will only be randomized. The others will remain on their original position with each new spawn of the helicopter. --- -- The randomization of waypoint 2 and 3 will take place within a radius of 2000 meters. --- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):InitRandomizeRoute( 2, 2, 2000 ) -function SPAWN:InitRandomizeUnits( RandomizeUnits, OuterRadius, InnerRadius ) - self:F( { self.SpawnTemplatePrefix, RandomizeUnits, OuterRadius, InnerRadius } ) - - self.SpawnRandomizeUnits = RandomizeUnits or false - self.SpawnOuterRadius = OuterRadius or 0 - self.SpawnInnerRadius = InnerRadius or 0 - - for GroupID = 1, self.SpawnMaxGroups do - self:_RandomizeRoute( GroupID ) - end - - return self -end - ---- This method is rather complicated to understand. But I'll try to explain. --- This method becomes useful when you need to spawn groups with random templates of groups defined within the mission editor, --- but they will all follow the same Template route and have the same prefix name. --- In other words, this method randomizes between a defined set of groups the template to be used for each new spawn of a group. --- @param #SPAWN self --- @param #string SpawnTemplatePrefixTable A table with the names of the groups defined within the mission editor, from which one will be choosen when a new group will be spawned. --- @return #SPAWN --- @usage --- -- NATO Tank Platoons invading Gori. --- -- Choose between 13 different 'US Tank Platoon' configurations for each new SPAWN the Group to be spawned for the --- -- 'US Tank Platoon Left', 'US Tank Platoon Middle' and 'US Tank Platoon Right' SpawnTemplatePrefixes. --- -- Each new SPAWN will randomize the route, with a defined time interval of 200 seconds with 40% time variation (randomization) and --- -- with a limit set of maximum 12 Units alive simulteneously and 150 Groups to be spawned during the whole mission. --- Spawn_US_Platoon = { 'US Tank Platoon 1', 'US Tank Platoon 2', 'US Tank Platoon 3', 'US Tank Platoon 4', 'US Tank Platoon 5', --- 'US Tank Platoon 6', 'US Tank Platoon 7', 'US Tank Platoon 8', 'US Tank Platoon 9', 'US Tank Platoon 10', --- 'US Tank Platoon 11', 'US Tank Platoon 12', 'US Tank Platoon 13' } --- Spawn_US_Platoon_Left = SPAWN:New( 'US Tank Platoon Left' ):InitLimit( 12, 150 ):Schedule( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 ) --- Spawn_US_Platoon_Middle = SPAWN:New( 'US Tank Platoon Middle' ):InitLimit( 12, 150 ):Schedule( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 ) --- Spawn_US_Platoon_Right = SPAWN:New( 'US Tank Platoon Right' ):InitLimit( 12, 150 ):Schedule( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 ) -function SPAWN:InitRandomizeTemplate( SpawnTemplatePrefixTable ) - self:F( { self.SpawnTemplatePrefix, SpawnTemplatePrefixTable } ) - - self.SpawnTemplatePrefixTable = SpawnTemplatePrefixTable - self.SpawnRandomizeTemplate = true - - for SpawnGroupID = 1, self.SpawnMaxGroups do - self:_RandomizeTemplate( SpawnGroupID ) - end - - return self -end - ---TODO: Add example. ---- This method provides the functionality to randomize the spawning of the Groups at a given list of zones of different types. --- @param #SPAWN self --- @param #table SpawnZoneTable A table with @{Zone} objects. If this table is given, then each spawn will be executed within the given list of @{Zone}s objects. --- @return #SPAWN --- @usage --- -- NATO Tank Platoons invading Gori. --- -- Choose between 3 different zones for each new SPAWN the Group to be executed, regardless of the zone type. -function SPAWN:InitRandomizeZones( SpawnZoneTable ) - self:F( { self.SpawnTemplatePrefix, SpawnZoneTable } ) - - self.SpawnZoneTable = SpawnZoneTable - self.SpawnRandomizeZones = true - - for SpawnGroupID = 1, self.SpawnMaxGroups do - self:_RandomizeZones( SpawnGroupID ) - end - - return self -end - - - - - ---- For planes and helicopters, when these groups go home and land on their home airbases and farps, they normally would taxi to the parking spot, shut-down their engines and wait forever until the Group is removed by the runtime environment. --- This method is used to re-spawn automatically (so no extra call is needed anymore) the same group after it has landed. --- This will enable a spawned group to be re-spawned after it lands, until it is destroyed... --- Note: When the group is respawned, it will re-spawn from the original airbase where it took off. --- So ensure that the routes for groups that respawn, always return to the original airbase, or players may get confused ... --- @param #SPAWN self --- @return #SPAWN self --- @usage --- -- RU Su-34 - AI Ship Attack --- -- Re-SPAWN the Group(s) after each landing and Engine Shut-Down automatically. --- SpawnRU_SU34 = SPAWN:New( 'TF1 RU Su-34 Krymsk@AI - Attack Ships' ):Schedule( 2, 3, 1800, 0.4 ):SpawnUncontrolled():InitRandomizeRoute( 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:InitCleanUp( SpawnCleanUpInterval ) - self:F( { self.SpawnTemplatePrefix, SpawnCleanUpInterval } ) - - self.SpawnCleanUpInterval = SpawnCleanUpInterval - self.SpawnCleanUpTimeStamps = {} - - local SpawnGroup, SpawnCursor = self:GetFirstAliveGroup() - self:T( { "CleanUp Scheduler:", SpawnGroup } ) - - --self.CleanUpFunction = routines.scheduleFunction( self._SpawnCleanUpScheduler, { self }, timer.getTime() + 1, SpawnCleanUpInterval ) - self.CleanUpScheduler = SCHEDULER:New( self, self._SpawnCleanUpScheduler, {}, 1, SpawnCleanUpInterval, 0.2 ) - return self -end - - - ---- Makes the groups visible before start (like a batallion). --- The method will take the position of the group as the first position in the array. --- @param #SPAWN self --- @param #number SpawnAngle The angle in degrees how the groups and each unit of the group will be positioned. --- @param #number SpawnWidth The amount of Groups that will be positioned on the X axis. --- @param #number SpawnDeltaX The space between each Group on the X-axis. --- @param #number SpawnDeltaY The space between each Group on the Y-axis. --- @return #SPAWN self --- @usage --- -- Define an array of Groups. --- Spawn_BE_Ground = SPAWN:New( 'BE Ground' ):InitLimit( 2, 24 ):InitArray( 90, "Diamond", 10, 100, 50 ) -function SPAWN:InitArray( SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY ) - self:F( { self.SpawnTemplatePrefix, SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY } ) - - self.SpawnVisible = true -- When the first Spawn executes, all the Groups need to be made visible before start. - - local SpawnX = 0 - local SpawnY = 0 - local SpawnXIndex = 0 - local SpawnYIndex = 0 - - for SpawnGroupID = 1, self.SpawnMaxGroups do - self:T( { SpawnX, SpawnY, SpawnXIndex, SpawnYIndex } ) - - self.SpawnGroups[SpawnGroupID].Visible = true - self.SpawnGroups[SpawnGroupID].Spawned = false - - SpawnXIndex = SpawnXIndex + 1 - if SpawnWidth and SpawnWidth ~= 0 then - if SpawnXIndex >= SpawnWidth then - SpawnXIndex = 0 - SpawnYIndex = SpawnYIndex + 1 - end - end - - local SpawnRootX = self.SpawnGroups[SpawnGroupID].SpawnTemplate.x - local SpawnRootY = self.SpawnGroups[SpawnGroupID].SpawnTemplate.y - - self:_TranslateRotate( SpawnGroupID, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle ) - - self.SpawnGroups[SpawnGroupID].SpawnTemplate.lateActivation = true - self.SpawnGroups[SpawnGroupID].SpawnTemplate.visible = true - - self.SpawnGroups[SpawnGroupID].Visible = true - - _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 - -do -- AI methods - --- Turns the AI On or Off for the @{Group} when spawning. - -- @param #SPAWN self - -- @param #boolean AIOnOff A value of true sets the AI On, a value of false sets the AI Off. - -- @return #SPAWN The SPAWN object - function SPAWN:InitAIOnOff( AIOnOff ) - - self.AIOnOff = AIOnOff - return self - end - - --- Turns the AI On for the @{Group} when spawning. - -- @param #SPAWN self - -- @return #SPAWN The SPAWN object - function SPAWN:InitAIOn() - - return self:InitAIOnOff( true ) - end - - --- Turns the AI Off for the @{Group} when spawning. - -- @param #SPAWN self - -- @return #SPAWN The SPAWN object - function SPAWN:InitAIOff() - - return self:InitAIOnOff( false ) - end - -end -- AI methods - ---- Will spawn a group based on the internal index. --- Note: Uses @{DATABASE} module defined in MOOSE. --- @param #SPAWN self --- @return Wrapper.Group#GROUP The group that was spawned. You can use this group for further actions. -function SPAWN:Spawn() - self:F( { self.SpawnTemplatePrefix, self.SpawnIndex, self.AliveUnits } ) - - 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 Wrapper.Group#GROUP The group that was spawned. You can use this group for further actions. -function SPAWN:ReSpawn( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex } ) - - if not SpawnIndex then - SpawnIndex = 1 - end - --- TODO: This logic makes DCS crash and i don't know why (yet). - local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) - local WayPoints = SpawnGroup and SpawnGroup.WayPoints or nil - if SpawnGroup then - local SpawnDCSGroup = SpawnGroup:GetDCSObject() - if SpawnDCSGroup then - SpawnGroup:Destroy() - end - end - - local SpawnGroup = self:SpawnWithIndex( SpawnIndex ) - if SpawnGroup and WayPoints then - -- If there were WayPoints set, then Re-Execute those WayPoints! - SpawnGroup:WayPointInitialize( WayPoints ) - SpawnGroup:WayPointExecute( 1, 5 ) - end - - if SpawnGroup.ReSpawnFunction then - SpawnGroup:ReSpawnFunction() - end - - return SpawnGroup -end - ---- Will spawn a group with a specified index number. --- Uses @{DATABASE} global object defined in MOOSE. --- @param #SPAWN self --- @param #string SpawnIndex The index of the group to be spawned. --- @return Wrapper.Group#GROUP The group that was spawned. You can use this group for further actions. -function SPAWN:SpawnWithIndex( SpawnIndex ) - self:F2( { SpawnTemplatePrefix = self.SpawnTemplatePrefix, SpawnIndex = SpawnIndex, AliveUnits = self.AliveUnits, SpawnMaxGroups = self.SpawnMaxGroups } ) - - if self:_GetSpawnIndex( SpawnIndex ) then - - if self.SpawnGroups[self.SpawnIndex].Visible then - self.SpawnGroups[self.SpawnIndex].Group:Activate() - else - - local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate - self:T( SpawnTemplate.name ) - - if SpawnTemplate then - - local PointVec3 = POINT_VEC3:New( SpawnTemplate.route.points[1].x, SpawnTemplate.route.points[1].alt, SpawnTemplate.route.points[1].y ) - self:T( { "Current point of ", self.SpawnTemplatePrefix, PointVec3 } ) - - -- If RandomizeUnits, then Randomize the formation at the start point. - if self.SpawnRandomizeUnits then - for UnitID = 1, #SpawnTemplate.units do - local RandomVec2 = PointVec3:GetRandomVec2InRadius( self.SpawnOuterRadius, self.SpawnInnerRadius ) - SpawnTemplate.units[UnitID].x = RandomVec2.x - SpawnTemplate.units[UnitID].y = RandomVec2.y - self:T( 'SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) - end - end - - if SpawnTemplate.CategoryID == Group.Category.HELICOPTER or SpawnTemplate.CategoryID == Group.Category.AIRPLANE then - if SpawnTemplate.route.points[1].type == "TakeOffParking" then - SpawnTemplate.uncontrolled = self.SpawnUnControlled - end - end - end - - _EVENTDISPATCHER:OnBirthForTemplate( SpawnTemplate, self._OnBirth, self ) - _EVENTDISPATCHER:OnCrashForTemplate( SpawnTemplate, self._OnDeadOrCrash, self ) - _EVENTDISPATCHER:OnDeadForTemplate( SpawnTemplate, self._OnDeadOrCrash, self ) - - if self.Repeat then - _EVENTDISPATCHER:OnTakeOffForTemplate( SpawnTemplate, self._OnTakeOff, self ) - _EVENTDISPATCHER:OnLandForTemplate( SpawnTemplate, self._OnLand, self ) - end - if self.RepeatOnEngineShutDown then - _EVENTDISPATCHER:OnEngineShutDownForTemplate( SpawnTemplate, self._OnEngineShutDown, self ) - end - self:T3( SpawnTemplate.name ) - - self.SpawnGroups[self.SpawnIndex].Group = _DATABASE:Spawn( SpawnTemplate ) - - local SpawnGroup = self.SpawnGroups[self.SpawnIndex].Group -- Wrapper.Group#GROUP - - --TODO: Need to check if this function doesn't need to be scheduled, as the group may not be immediately there! - if SpawnGroup then - - SpawnGroup:SetAIOnOff( self.AIOnOff ) - end - - -- 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 method 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 method will be called when a new group is spawned, including its given parameters. --- The first parameter of the SpawnFunction is the @{Group#GROUP} that was spawned. --- @param #SPAWN self --- @param #function SpawnCallBackFunction The function to be called when a group spawns. --- @param SpawnFunctionArguments A random amount of arguments to be provided to the function when the group spawns. --- @return #SPAWN --- @usage --- -- Declare SpawnObject and call a function when a new Group is spawned. --- local SpawnObject = SPAWN --- :New( "SpawnObject" ) --- :InitLimit( 2, 10 ) --- :OnSpawnGroup( --- function( SpawnGroup ) --- SpawnGroup:E( "I am spawned" ) --- end --- ) --- :SpawnScheduled( 300, 0.3 ) -function SPAWN:OnSpawnGroup( SpawnCallBackFunction, ... ) - self:F( "OnSpawnGroup" ) - - self.SpawnFunctionHook = SpawnCallBackFunction - self.SpawnFunctionArguments = {} - if arg then - self.SpawnFunctionArguments = arg - end - - return self -end - - ---- Will spawn a group from a Vec3 in 3D space. --- This method is mostly advisable to be used if you want to simulate spawning units in the air, like helicopters or airplanes. --- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. --- You can use the returned group to further define the route to be followed. --- @param #SPAWN self --- @param Dcs.DCSTypes#Vec3 Vec3 The Vec3 coordinates where to spawn the group. --- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. --- @return Wrapper.Group#GROUP that was spawned. --- @return #nil Nothing was spawned. -function SPAWN:SpawnFromVec3( Vec3, SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, Vec3, SpawnIndex } ) - - local PointVec3 = POINT_VEC3:NewFromVec3( Vec3 ) - self:T2(PointVec3) - - if SpawnIndex then - else - SpawnIndex = self.SpawnIndex + 1 - end - - if self:_GetSpawnIndex( SpawnIndex ) then - - local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate - - if SpawnTemplate then - - self:T( { "Current point of ", self.SpawnTemplatePrefix, Vec3 } ) - - -- Translate the position of the Group Template to the Vec3. - for UnitID = 1, #SpawnTemplate.units do - self:T( 'Before Translation SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) - local UnitTemplate = SpawnTemplate.units[UnitID] - local SX = UnitTemplate.x - local SY = UnitTemplate.y - local BX = SpawnTemplate.route.points[1].x - local BY = SpawnTemplate.route.points[1].y - local TX = Vec3.x + ( SX - BX ) - local TY = Vec3.z + ( SY - BY ) - SpawnTemplate.units[UnitID].x = TX - SpawnTemplate.units[UnitID].y = TY - SpawnTemplate.units[UnitID].alt = Vec3.y - self:T( 'After Translation SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) - end - - SpawnTemplate.route.points[1].x = Vec3.x - SpawnTemplate.route.points[1].y = Vec3.z - SpawnTemplate.route.points[1].alt = Vec3.y - - SpawnTemplate.x = Vec3.x - SpawnTemplate.y = Vec3.z - - return self:SpawnWithIndex( self.SpawnIndex ) - end - end - - return nil -end - ---- Will spawn a group from a Vec2 in 3D space. --- This method is mostly advisable to be used if you want to simulate spawning groups on the ground from air units, like vehicles. --- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. --- You can use the returned group to further define the route to be followed. --- @param #SPAWN self --- @param Dcs.DCSTypes#Vec2 Vec2 The Vec2 coordinates where to spawn the group. --- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. --- @return Wrapper.Group#GROUP that was spawned. --- @return #nil Nothing was spawned. -function SPAWN:SpawnFromVec2( Vec2, SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, Vec2, SpawnIndex } ) - - local PointVec2 = POINT_VEC2:NewFromVec2( Vec2 ) - return self:SpawnFromVec3( PointVec2:GetVec3(), SpawnIndex ) -end - - ---- Will spawn a group from a hosting unit. This method is mostly advisable to be used if you want to simulate spawning from air units, like helicopters, which are dropping infantry into a defined Landing Zone. --- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. --- You can use the returned group to further define the route to be followed. --- @param #SPAWN self --- @param Wrapper.Unit#UNIT HostUnit The air or ground unit dropping or unloading the group. --- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. --- @return Wrapper.Group#GROUP that was spawned. --- @return #nil Nothing was spawned. -function SPAWN:SpawnFromUnit( HostUnit, SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, HostUnit, SpawnIndex } ) - - if HostUnit and HostUnit:IsAlive() then -- and HostUnit:getUnit(1):inAir() == false then - return self:SpawnFromVec3( HostUnit:GetVec3(), SpawnIndex ) - end - - return nil -end - ---- Will spawn a group from a hosting static. This method is mostly advisable to be used if you want to simulate spawning from buldings and structures (static buildings). --- You can use the returned group to further define the route to be followed. --- @param #SPAWN self --- @param Wrapper.Static#STATIC HostStatic The static dropping or unloading the group. --- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. --- @return Wrapper.Group#GROUP that was spawned. --- @return #nil Nothing was spawned. -function SPAWN:SpawnFromStatic( HostStatic, SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, HostStatic, SpawnIndex } ) - - if HostStatic and HostStatic:IsAlive() then - return self:SpawnFromVec3( HostStatic:GetVec3(), SpawnIndex ) - end - - return nil -end - ---- Will spawn a Group within a given @{Zone}. --- The @{Zone} can be of any type derived from @{Zone#ZONE_BASE}. --- Once the @{Group} is spawned within the zone, the @{Group} will continue on its route. --- The **first waypoint** (where the group is spawned) is replaced with the zone location coordinates. --- @param #SPAWN self --- @param Core.Zone#ZONE Zone The zone where the group is to be spawned. --- @param #boolean RandomizeGroup (optional) Randomization of the @{Group} position in the zone. --- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. --- @return Wrapper.Group#GROUP that was spawned. --- @return #nil when nothing was spawned. -function SPAWN:SpawnInZone( Zone, RandomizeGroup, SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, Zone, RandomizeGroup, SpawnIndex } ) - - if Zone then - if RandomizeGroup then - return self:SpawnFromVec2( Zone:GetRandomVec2(), SpawnIndex ) - else - return self:SpawnFromVec2( Zone:GetVec2(), SpawnIndex ) - end - end - - return nil -end - ---- (**AIR**) Will spawn a plane group in UnControlled or Controlled mode... --- This will be similar to the uncontrolled flag setting in the ME. --- You can use UnControlled mode to simulate planes startup and ready for take-off but aren't moving (yet). --- ReSpawn the plane in Controlled mode, and the plane will move... --- @param #SPAWN self --- @param #boolean UnControlled true if UnControlled, false if Controlled. --- @return #SPAWN self -function SPAWN:InitUnControlled( UnControlled ) - self:F2( { self.SpawnTemplatePrefix, UnControlled } ) - - self.SpawnUnControlled = UnControlled - - for SpawnGroupID = 1, self.SpawnMaxGroups do - self.SpawnGroups[SpawnGroupID].UnControlled = UnControlled - 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 - ---- Will find the first alive @{Group} it has spawned, and return the alive @{Group} object and the first Index where the first alive @{Group} object has been found. --- @param #SPAWN self --- @return Wrapper.Group#GROUP, #number The @{Group} object found, the new Index where the group was found. --- @return #nil, #nil When no group is found, #nil is returned. --- @usage --- -- Find the first alive @{Group} object of the SpawnPlanes SPAWN object @{Group} collection that it has spawned during the mission. --- local GroupPlane, Index = SpawnPlanes:GetFirstAliveGroup() --- while GroupPlane ~= nil do --- -- Do actions with the GroupPlane object. --- GroupPlane, Index = SpawnPlanes:GetNextAliveGroup( Index ) --- end -function SPAWN:GetFirstAliveGroup() - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) - - for SpawnIndex = 1, self.SpawnCount do - local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) - if SpawnGroup and SpawnGroup:IsAlive() then - return SpawnGroup, SpawnIndex - end - end - - return nil, nil -end - - ---- Will find the next alive @{Group} object from a given Index, and return a reference to the alive @{Group} object and the next Index where the alive @{Group} has been found. --- @param #SPAWN self --- @param #number SpawnIndexStart A Index holding the start position to search from. This method can also be used to find the first alive @{Group} object from the given Index. --- @return Wrapper.Group#GROUP, #number The next alive @{Group} object found, the next Index where the next alive @{Group} object was found. --- @return #nil, #nil When no alive @{Group} object is found from the start Index position, #nil is returned. --- @usage --- -- Find the first alive @{Group} object of the SpawnPlanes SPAWN object @{Group} collection that it has spawned during the mission. --- local GroupPlane, Index = SpawnPlanes:GetFirstAliveGroup() --- while GroupPlane ~= nil do --- -- Do actions with the GroupPlane object. --- GroupPlane, Index = SpawnPlanes:GetNextAliveGroup( Index ) --- end -function SPAWN:GetNextAliveGroup( SpawnIndexStart ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnIndexStart } ) - - SpawnIndexStart = SpawnIndexStart + 1 - for SpawnIndex = SpawnIndexStart, self.SpawnCount do - local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) - if SpawnGroup and SpawnGroup:IsAlive() then - return SpawnGroup, SpawnIndex - end - end - - return nil, nil -end - ---- Will find the last alive @{Group} object, and will return a reference to the last live @{Group} object and the last Index where the last alive @{Group} object has been found. --- @param #SPAWN self --- @return Wrapper.Group#GROUP, #number The last alive @{Group} object found, the last Index where the last alive @{Group} object was found. --- @return #nil, #nil When no alive @{Group} object is found, #nil is returned. --- @usage --- -- Find the last alive @{Group} object of the SpawnPlanes SPAWN object @{Group} collection that it has spawned during the mission. --- local GroupPlane, Index = SpawnPlanes:GetLastAliveGroup() --- if GroupPlane then -- GroupPlane can be nil!!! --- -- Do actions with the GroupPlane object. --- end -function SPAWN:GetLastAliveGroup() - self:F( { self.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 Wrapper.Group#GROUP self -function SPAWN:GetGroupFromIndex( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnIndex } ) - - if not SpawnIndex then - SpawnIndex = 1 - end - - if self.SpawnGroups and self.SpawnGroups[SpawnIndex] then - local SpawnGroup = self.SpawnGroups[SpawnIndex].Group - return SpawnGroup - else - return nil - end -end - ---- 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 Dcs.DCSWrapper.Unit#Unit DCSUnit The @{DCSUnit} to be searched. --- @return #string The prefix --- @return #nil Nothing found -function SPAWN:_GetGroupIndexFromDCSUnit( DCSUnit ) - self:F3( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, DCSUnit } ) - - local SpawnUnitName = ( DCSUnit and DCSUnit:getName() ) or nil - if SpawnUnitName then - local IndexString = string.match( SpawnUnitName, "#.*-" ):sub( 2, -2 ) - if IndexString then - local Index = tonumber( IndexString ) - return Index - end - end - - return nil -end - ---- Return the prefix of a SpawnUnit. --- The method will search for a #-mark, and will return the text before the #-mark. --- It will return nil of no prefix was found. --- @param #SPAWN self --- @param Dcs.DCSWrapper.Unit#UNIT DCSUnit The @{DCSUnit} to be searched. --- @return #string The prefix --- @return #nil Nothing found -function SPAWN:_GetPrefixFromDCSUnit( DCSUnit ) - self:F3( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, DCSUnit } ) - - local DCSUnitName = ( DCSUnit and DCSUnit:getName() ) or nil - if DCSUnitName then - local SpawnPrefix = string.match( DCSUnitName, ".*#" ) - if SpawnPrefix then - SpawnPrefix = SpawnPrefix:sub( 1, -2 ) - end - return SpawnPrefix - end - - return nil -end - ---- Return the group within the SpawnGroups collection with input a DCSUnit. --- @param #SPAWN self --- @param Dcs.DCSWrapper.Unit#Unit DCSUnit The @{DCSUnit} to be searched. --- @return Wrapper.Group#GROUP The Group --- @return #nil Nothing found -function SPAWN:_GetGroupFromDCSUnit( DCSUnit ) - self:F3( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, DCSUnit } ) - - 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 - - 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:F3( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnGroup } ) - - local IndexString = string.match( SpawnGroup:GetName(), "#.*$" ):sub( 2 ) - local Index = tonumber( IndexString ) - - self:T3( IndexString, Index ) - return Index - -end - ---- Return the last maximum index that can be used. -function SPAWN:_GetLastIndex() - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) - - return self.SpawnMaxGroups -end - ---- Initalize the SpawnGroups collection. -function SPAWN:_InitializeSpawnGroups( SpawnIndex ) - self:F3( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnIndex } ) - - if not self.SpawnGroups[SpawnIndex] then - self.SpawnGroups[SpawnIndex] = {} - self.SpawnGroups[SpawnIndex].Visible = false - self.SpawnGroups[SpawnIndex].Spawned = false - self.SpawnGroups[SpawnIndex].UnControlled = false - self.SpawnGroups[SpawnIndex].SpawnTime = 0 - - self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix = self.SpawnTemplatePrefix - self.SpawnGroups[SpawnIndex].SpawnTemplate = self:_Prepare( self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix, SpawnIndex ) - end - - self:_RandomizeTemplate( SpawnIndex ) - self:_RandomizeRoute( SpawnIndex ) - --self:_TranslateRotate( SpawnIndex ) - - return self.SpawnGroups[SpawnIndex] -end - - - ---- Gets the CategoryID of the Group with the given SpawnPrefix -function SPAWN:_GetGroupCategoryID( SpawnPrefix ) - local TemplateGroup = Group.getByName( SpawnPrefix ) - - if TemplateGroup then - return TemplateGroup:getCategory() - else - return nil - end -end - ---- Gets the CoalitionID of the Group with the given SpawnPrefix -function SPAWN:_GetGroupCoalitionID( SpawnPrefix ) - local TemplateGroup = Group.getByName( SpawnPrefix ) - - if TemplateGroup then - return TemplateGroup:getCoalition() - else - return nil - end -end - ---- Gets the CountryID of the Group with the given SpawnPrefix -function SPAWN:_GetGroupCountryID( SpawnPrefix ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnPrefix } ) - - local TemplateGroup = Group.getByName( SpawnPrefix ) - - if TemplateGroup then - local TemplateUnits = TemplateGroup:getUnits() - return TemplateUnits[1]:getCountry() - else - return nil - end -end - ---- Gets the Group Template from the ME environment definition. --- This method used the @{DATABASE} object, which contains ALL initial and new spawned object in MOOSE. --- @param #SPAWN self --- @param #string SpawnTemplatePrefix --- @return @SPAWN self -function SPAWN:_GetTemplate( SpawnTemplatePrefix ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnTemplatePrefix } ) - - local SpawnTemplate = nil - - 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:T3( { SpawnTemplate } ) - return SpawnTemplate -end - ---- Prepares the new Group Template. --- @param #SPAWN self --- @param #string SpawnTemplatePrefix --- @param #number SpawnIndex --- @return #SPAWN self -function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) - 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 - - if SpawnTemplate.CategoryID == Group.Category.GROUND then - self:T3( "For ground units, visible needs to be false..." ) - SpawnTemplate.visible = false - end - - - for UnitID = 1, #SpawnTemplate.units do - SpawnTemplate.units[UnitID].name = string.format( SpawnTemplate.name .. '-%02d', UnitID ) - SpawnTemplate.units[UnitID].unitId = nil - end - - self:T3( { "Template:", SpawnTemplate } ) - return SpawnTemplate - -end - ---- Private method randomizing the routes. --- @param #SPAWN self --- @param #number SpawnIndex The index of the group to be spawned. --- @return #SPAWN -function SPAWN:_RandomizeRoute( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeRoute, self.SpawnRandomizeRouteStartPoint, self.SpawnRandomizeRouteEndPoint, self.SpawnRandomizeRouteRadius } ) - - if self.SpawnRandomizeRoute then - local SpawnTemplate = self.SpawnGroups[SpawnIndex].SpawnTemplate - local RouteCount = #SpawnTemplate.route.points - - for t = self.SpawnRandomizeRouteStartPoint + 1, ( RouteCount - self.SpawnRandomizeRouteEndPoint ) do - - SpawnTemplate.route.points[t].x = SpawnTemplate.route.points[t].x + math.random( self.SpawnRandomizeRouteRadius * -1, self.SpawnRandomizeRouteRadius ) - SpawnTemplate.route.points[t].y = SpawnTemplate.route.points[t].y + math.random( self.SpawnRandomizeRouteRadius * -1, self.SpawnRandomizeRouteRadius ) - - -- Manage randomization of altitude for airborne units ... - if SpawnTemplate.CategoryID == Group.Category.AIRPLANE or SpawnTemplate.CategoryID == Group.Category.HELICOPTER then - if SpawnTemplate.route.points[t].alt and self.SpawnRandomizeRouteHeight then - SpawnTemplate.route.points[t].alt = SpawnTemplate.route.points[t].alt + math.random( 1, self.SpawnRandomizeRouteHeight ) - end - else - SpawnTemplate.route.points[t].alt = nil - end - - self:T( 'SpawnTemplate.route.points[' .. t .. '].x = ' .. SpawnTemplate.route.points[t].x .. ', SpawnTemplate.route.points[' .. t .. '].y = ' .. SpawnTemplate.route.points[t].y ) - end - end - - self:_RandomizeZones( SpawnIndex ) - - return self -end - ---- Private method that randomizes the template of the group. --- @param #SPAWN self --- @param #number SpawnIndex --- @return #SPAWN self -function SPAWN:_RandomizeTemplate( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeTemplate } ) - - if self.SpawnRandomizeTemplate then - self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix = self.SpawnTemplatePrefixTable[ math.random( 1, #self.SpawnTemplatePrefixTable ) ] - self.SpawnGroups[SpawnIndex].SpawnTemplate = self:_Prepare( self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix, SpawnIndex ) - self.SpawnGroups[SpawnIndex].SpawnTemplate.route = 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 - local OldX = self.SpawnGroups[SpawnIndex].SpawnTemplate.units[1].x - local OldY = self.SpawnGroups[SpawnIndex].SpawnTemplate.units[1].y - for UnitID = 1, #self.SpawnGroups[SpawnIndex].SpawnTemplate.units do - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].heading = self.SpawnTemplate.units[1].heading - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].x = self.SpawnTemplate.units[1].x + ( self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].x - OldX ) - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].y = self.SpawnTemplate.units[1].y + ( self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].y - OldY ) - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].alt = self.SpawnTemplate.units[1].alt - end - end - - self:_RandomizeRoute( SpawnIndex ) - - return self -end - ---- Private method that randomizes the @{Zone}s where the Group will be spawned. --- @param #SPAWN self --- @param #number SpawnIndex --- @return #SPAWN self -function SPAWN:_RandomizeZones( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeZones } ) - - if self.SpawnRandomizeZones then - local SpawnZone = nil -- Core.Zone#ZONE_BASE - while not SpawnZone do - self:T( { SpawnZoneTableCount = #self.SpawnZoneTable, self.SpawnZoneTable } ) - local ZoneID = math.random( #self.SpawnZoneTable ) - self:T( ZoneID ) - SpawnZone = self.SpawnZoneTable[ ZoneID ]:GetZoneMaybe() - end - - self:T( "Preparing Spawn in Zone", SpawnZone:GetName() ) - - local SpawnVec2 = SpawnZone:GetRandomVec2() - - self:T( { SpawnVec2 = SpawnVec2 } ) - - local SpawnTemplate = self.SpawnGroups[SpawnIndex].SpawnTemplate - - self:T( { Route = SpawnTemplate.route } ) - - for UnitID = 1, #SpawnTemplate.units do - local UnitTemplate = SpawnTemplate.units[UnitID] - self:T( 'Before Translation SpawnTemplate.units['..UnitID..'].x = ' .. UnitTemplate.x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. UnitTemplate.y ) - local SX = UnitTemplate.x - local SY = UnitTemplate.y - local BX = SpawnTemplate.route.points[1].x - local BY = SpawnTemplate.route.points[1].y - local TX = SpawnVec2.x + ( SX - BX ) - local TY = SpawnVec2.y + ( SY - BY ) - UnitTemplate.x = TX - UnitTemplate.y = TY - -- TODO: Manage altitude based on landheight... - --SpawnTemplate.units[UnitID].alt = SpawnVec2: - self:T( 'After Translation SpawnTemplate.units['..UnitID..'].x = ' .. UnitTemplate.x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. UnitTemplate.y ) - end - SpawnTemplate.x = SpawnVec2.x - SpawnTemplate.y = SpawnVec2.y - SpawnTemplate.route.points[1].x = SpawnVec2.x - SpawnTemplate.route.points[1].y = SpawnVec2.y - end - - return self - -end - -function SPAWN:_TranslateRotate( SpawnIndex, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle } ) - - -- Translate - local TranslatedX = SpawnX - local TranslatedY = SpawnY - - -- Rotate - -- From Wikipedia: https://en.wikipedia.org/wiki/Rotation_matrix#Common_rotations - -- x' = x \cos \theta - y \sin \theta\ - -- y' = x \sin \theta + y \cos \theta\ - local RotatedX = - TranslatedX * math.cos( math.rad( SpawnAngle ) ) - + TranslatedY * math.sin( math.rad( SpawnAngle ) ) - local RotatedY = TranslatedX * math.sin( math.rad( SpawnAngle ) ) - + TranslatedY * math.cos( math.rad( SpawnAngle ) ) - - -- Assign - self.SpawnGroups[SpawnIndex].SpawnTemplate.x = SpawnRootX - RotatedX - self.SpawnGroups[SpawnIndex].SpawnTemplate.y = SpawnRootY + RotatedY - - - local SpawnUnitCount = table.getn( self.SpawnGroups[SpawnIndex].SpawnTemplate.units ) - for u = 1, SpawnUnitCount do - - -- Translate - local TranslatedX = SpawnX - local TranslatedY = SpawnY - 10 * ( u - 1 ) - - -- Rotate - local RotatedX = - TranslatedX * math.cos( math.rad( SpawnAngle ) ) - + TranslatedY * math.sin( math.rad( SpawnAngle ) ) - local RotatedY = TranslatedX * math.sin( math.rad( SpawnAngle ) ) - + TranslatedY * math.cos( math.rad( SpawnAngle ) ) - - -- Assign - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].x = SpawnRootX - RotatedX - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].y = SpawnRootY + RotatedY - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].heading = self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].heading + math.rad( SpawnAngle ) - end - - return self -end - ---- Get the next index of the groups to be spawned. This method is complicated, as it is used at several spaces. -function SPAWN:_GetSpawnIndex( SpawnIndex ) - self:F2( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnMaxGroups, self.SpawnMaxUnitsAlive, self.AliveUnits, #self.SpawnTemplate.units } ) - - if ( self.SpawnMaxGroups == 0 ) or ( SpawnIndex <= self.SpawnMaxGroups ) then - if ( self.SpawnMaxUnitsAlive == 0 ) or ( self.AliveUnits + #self.SpawnTemplate.units <= self.SpawnMaxUnitsAlive ) or self.UnControlled == true then - if SpawnIndex and SpawnIndex >= self.SpawnCount + 1 then - self.SpawnCount = self.SpawnCount + 1 - SpawnIndex = self.SpawnCount - end - self.SpawnIndex = SpawnIndex - if not self.SpawnGroups[self.SpawnIndex] then - self:_InitializeSpawnGroups( self.SpawnIndex ) - end - else - return nil - end - else - return nil - end - - return self.SpawnIndex -end - - --- TODO Need to delete this... _DATABASE does this now ... - ---- @param #SPAWN self --- @param Core.Event#EVENTDATA Event -function SPAWN:_OnBirth( Event ) - - if timer.getTime0() < timer.getAbsTime() then - if Event.IniDCSUnit then - local EventPrefix = self:_GetPrefixFromDCSUnit( Event.IniDCSUnit ) - self:T( { "Birth Event:", EventPrefix, self.SpawnTemplatePrefix } ) - if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then - self.AliveUnits = self.AliveUnits + 1 - self:T( "Alive Units: " .. self.AliveUnits ) - end - end - end - -end - ---- Obscolete --- @todo Need to delete this... _DATABASE does this now ... - ---- @param #SPAWN self --- @param Core.Event#EVENTDATA Event -function SPAWN:_OnDeadOrCrash( Event ) - self:F( self.SpawnTemplatePrefix, Event ) - - if Event.IniDCSUnit then - local EventPrefix = self:_GetPrefixFromDCSUnit( Event.IniDCSUnit ) - self:T( { "Dead event: " .. EventPrefix, self.SpawnTemplatePrefix } ) - if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then - self.AliveUnits = self.AliveUnits - 1 - self:T( "Alive Units: " .. self.AliveUnits ) - end - end -end - ---- 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:F2( { "_Scheduler", self.SpawnTemplatePrefix, self.SpawnAliasPrefix, self.SpawnIndex, self.SpawnMaxGroups, self.SpawnMaxUnitsAlive } ) - - -- Validate if there are still groups left in the batch... - self:Spawn() - - return true -end - ---- Schedules the CleanUp of Groups --- @param #SPAWN self --- @return #boolean True = Continue Scheduler -function SPAWN:_SpawnCleanUpScheduler() - self:F( { "CleanUp Scheduler:", self.SpawnTemplatePrefix } ) - - local SpawnGroup, SpawnCursor = self:GetFirstAliveGroup() - self:T( { "CleanUp Scheduler:", SpawnGroup, SpawnCursor } ) - - while SpawnGroup do - - local SpawnUnits = SpawnGroup:GetUnits() - - for UnitID, UnitData in pairs( SpawnUnits ) do - - local SpawnUnit = UnitData -- Wrapper.Unit#UNIT - local SpawnUnitName = SpawnUnit:GetName() - - - self.SpawnCleanUpTimeStamps[SpawnUnitName] = self.SpawnCleanUpTimeStamps[SpawnUnitName] or {} - local Stamp = self.SpawnCleanUpTimeStamps[SpawnUnitName] - self:T( { SpawnUnitName, Stamp } ) - - if Stamp.Vec2 then - if SpawnUnit:InAir() == false and SpawnUnit:GetVelocityKMH() < 1 then - local NewVec2 = SpawnUnit:GetVec2() - if Stamp.Vec2.x == NewVec2.x and Stamp.Vec2.y == NewVec2.y then - -- If the plane is not moving, and is on the ground, assign it with a timestamp... - if Stamp.Time + self.SpawnCleanUpInterval < timer.getTime() then - self:T( { "CleanUp Scheduler:", "ReSpawning:", SpawnGroup:GetName() } ) - self:ReSpawn( SpawnCursor ) - Stamp.Vec2 = nil - Stamp.Time = nil - end - else - Stamp.Time = timer.getTime() - Stamp.Vec2 = SpawnUnit:GetVec2() - end - else - Stamp.Vec2 = nil - Stamp.Time = nil - end - else - if SpawnUnit:InAir() == false then - Stamp.Vec2 = SpawnUnit:GetVec2() - if SpawnUnit:GetVelocityKMH() < 1 then - Stamp.Time = timer.getTime() - end - else - Stamp.Time = nil - Stamp.Vec2 = nil - end - end - end - - SpawnGroup, SpawnCursor = self:GetNextAliveGroup( SpawnCursor ) - - self:T( { "CleanUp Scheduler:", SpawnGroup, SpawnCursor } ) - - end - - return true -- Repeat - -end ---- 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 Core.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 - -- 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 ) - if self.TargetSkill[_targetskill] then - if (_evade > self.TargetSkill[_targetskill].Evade) then - self:T( string.format("Evading, target skill " ..string.format(_targetskill)) ) - local _targetMim = Weapon.getTarget(SEADWeapon) - local _targetMimname = Unit.getName(_targetMim) - local _targetMimgroup = Unit.getGroup(Weapon.getTarget(SEADWeapon)) - local _targetMimcont= _targetMimgroup:getController() - routines.groupRandomDistSelf(_targetMimgroup,300,'Diamond',250,20) -- move randomly - local SuppressedGroups1 = {} -- unit suppressed radar off for a random time - local function SuppressionEnd1(id) - id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN) - SuppressedGroups1[id.groupName] = nil - end - local id = { - groupName = _targetMimgroup, - ctrl = _targetMimcont - } - local delay1 = math.random(self.TargetSkill[_targetskill].DelayOff[1], self.TargetSkill[_targetskill].DelayOff[2]) - if SuppressedGroups1[id.groupName] == nil then - SuppressedGroups1[id.groupName] = { - SuppressionEndTime1 = timer.getTime() + delay1, - SuppressionEndN1 = SuppressionEndCounter1 --Store instance of SuppressionEnd() scheduled function - } - Controller.setOption(_targetMimcont, AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN) - timer.scheduleFunction(SuppressionEnd1, id, SuppressedGroups1[id.groupName].SuppressionEndTime1) --Schedule the SuppressionEnd() function - --trigger.action.outText( string.format("Radar Off " ..string.format(delay1)), 20) - end - - local SuppressedGroups = {} - local function SuppressionEnd(id) - id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.RED) - SuppressedGroups[id.groupName] = nil - end - local id = { - groupName = _targetMimgroup, - ctrl = _targetMimcont - } - local delay = math.random(self.TargetSkill[_targetskill].DelayOn[1], self.TargetSkill[_targetskill].DelayOn[2]) - if SuppressedGroups[id.groupName] == nil then - SuppressedGroups[id.groupName] = { - SuppressionEndTime = timer.getTime() + delay, - SuppressionEndN = SuppressionEndCounter --Store instance of SuppressionEnd() scheduled function - } - timer.scheduleFunction(SuppressionEnd, id, SuppressedGroups[id.groupName].SuppressionEndTime) --Schedule the SuppressionEnd() function - --trigger.action.outText( string.format("Radar On " ..string.format(delay)), 20) - end - 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 Core.Base#BASE --- @field Wrapper.Client#CLIENT EscortClient --- @field Wrapper.Group#GROUP EscortGroup --- @field #string EscortName --- @field #ESCORT.MODE EscortMode The mode the escort is in. --- @field Core.Scheduler#SCHEDULER FollowScheduler The instance of the SCHEDULER class. --- @field #number FollowDistance The current follow distance. --- @field #boolean ReportTargets If true, nearby targets are reported. --- @Field Dcs.DCSTypes#AI.Option.Air.val.ROE OptionROE Which ROE is set to the EscortGroup. --- @field Dcs.DCSTypes#AI.Option.Air.val.REACTION_ON_THREAT OptionReactionOnThreat Which REACTION_ON_THREAT is set to the EscortGroup. --- @field Core.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 Wrapper.Client#CLIENT EscortClient The client escorted by the EscortGroup. --- @param Wrapper.Group#GROUP EscortGroup The group AI escorting the EscortClient. --- @param #string EscortName Name of the escort. --- @param #string EscortBriefing A text showing the ESCORT briefing to the player. Note that if no EscortBriefing is provided, the default briefing will be shown. --- @return #ESCORT self --- @usage --- -- Declare a new EscortPlanes object as follows: --- --- -- First find the GROUP object and the CLIENT object. --- local EscortClient = CLIENT:FindByName( "Unit Name" ) -- The Unit Name is the name of the unit flagged with the skill Client in the mission editor. --- local EscortGroup = GROUP:FindByName( "Group Name" ) -- The Group Name is the name of the group that will escort the Escort Client. --- --- -- Now use these 2 objects to construct the new EscortPlanes object. --- EscortPlanes = ESCORT:New( EscortClient, EscortGroup, "Desert", "Welcome to the mission. You are escorted by a plane with code name 'Desert', which can be instructed through the F10 radio menu." ) -function ESCORT:New( EscortClient, EscortGroup, EscortName, EscortBriefing ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( { EscortClient, EscortGroup, EscortName } ) - - self.EscortClient = EscortClient -- Wrapper.Client#CLIENT - self.EscortGroup = EscortGroup -- Wrapper.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() - - if not EscortBriefing then - EscortGroup:MessageToClient( EscortGroup:GetCategoryName() .. " '" .. EscortName .. "' (" .. EscortGroup:GetCallsign() .. ") reporting! " .. - "We're escorting your flight. " .. - "Use the Radio Menu and F10 and use the options under + " .. EscortName .. "\n", - 60, EscortClient - ) - else - EscortGroup:MessageToClient( EscortGroup:GetCategoryName() .. " '" .. EscortName .. "' (" .. EscortGroup:GetCallsign() .. ") " .. EscortBriefing, - 60, EscortClient - ) - end - - self.FollowDistance = 100 - self.CT1 = 0 - self.GT1 = 0 - self.FollowScheduler = 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 Dcs.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 Dcs.DCSTypes#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. --- @param Dcs.DCSTypes#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. --- @param #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 Dcs.DCSTypes#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. --- @param Dcs.DCSTypes#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. --- @param #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 Dcs.DCSTypes#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. --- @param Dcs.DCSTypes#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. --- @param #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 = 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 = 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 = 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 = 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 Dcs.DCSTypes#Time Seconds Optional parameter that lets the escort report their current detected targets after specified time interval in seconds. The default time is 30 seconds. --- @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 = 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 -- Wrapper.Group#GROUP - local OrbitUnit = OrbitGroup:GetUnit(1) -- Wrapper.Unit#UNIT - local OrbitHeight = MenuParam.ParamHeight - local OrbitSeconds = MenuParam.ParamSeconds -- Not implemented yet - - self.FollowScheduler:Stop() - - local PointFrom = {} - local GroupVec3 = EscortGroup:GetUnit(1):GetVec3() - PointFrom = {} - PointFrom.x = GroupVec3.x - PointFrom.y = GroupVec3.z - PointFrom.speed = 250 - PointFrom.type = AI.Task.WaypointType.TURNING_POINT - PointFrom.alt = GroupVec3.y - PointFrom.alt_type = AI.Task.AltitudeType.BARO - - local OrbitPoint = OrbitUnit:GetVec2() - local PointTo = {} - PointTo.x = OrbitPoint.x - PointTo.y = OrbitPoint.y - PointTo.speed = 250 - PointTo.type = AI.Task.WaypointType.TURNING_POINT - PointTo.alt = OrbitHeight - PointTo.alt_type = AI.Task.AltitudeType.BARO - PointTo.task = EscortGroup:TaskOrbitCircleAtVec2( OrbitPoint, OrbitHeight, 0 ) - - local Points = { PointFrom, PointTo } - - EscortGroup:OptionROEHoldFire() - EscortGroup:OptionROTPassiveDefense() - - EscortGroup:SetTask( EscortGroup:TaskRoute( Points ) ) - EscortGroup:MessageToClient( "Orbiting at location.", 10, EscortClient ) - -end - ---- @param #MENUPARAM MenuParam -function ESCORT._JoinUpAndFollow( 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 Functional.Escort#ESCORT self --- @param Wrapper.Group#GROUP EscortGroup --- @param Wrapper.Client#CLIENT EscortClient --- @param Dcs.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 = 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 Wrapper.Group#GROUP EscortGroup -function _Resume( EscortGroup ) - env.info( '_Resume' ) - - local Escort = EscortGroup:GetState( EscortGroup, "Escort" ) - env.info( "EscortMode = " .. Escort.EscortMode ) - if Escort.EscortMode == ESCORT.MODE.FOLLOW then - Escort:JoinUpAndFollow( EscortGroup, Escort.EscortClient, Escort.Distance ) - end - -end - ---- @param #MENUPARAM MenuParam -function ESCORT._AttackTarget( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - - local EscortClient = self.EscortClient - local AttackUnit = MenuParam.ParamUnit -- Wrapper.Unit#UNIT - - self.FollowScheduler:Stop() - - self:T( AttackUnit ) - - if EscortGroup:IsAir() then - EscortGroup:OptionROEOpenFire() - EscortGroup:OptionROTPassiveDefense() - EscortGroup:SetState( EscortGroup, "Escort", self ) - SCHEDULER:New( EscortGroup, - EscortGroup.PushTask, - { EscortGroup:TaskCombo( - { EscortGroup:TaskAttackUnit( AttackUnit ), - EscortGroup:TaskFunction( 1, 2, "_Resume", { "''" } ) - } - ) - }, 10 - ) - else - SCHEDULER:New( EscortGroup, - EscortGroup.PushTask, - { EscortGroup:TaskCombo( - { EscortGroup:TaskFireAtPoint( AttackUnit:GetVec2(), 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 -- Wrapper.Unit#UNIT - - self.FollowScheduler:Stop() - - self:T( AttackUnit ) - - if EscortGroupAttack:IsAir() then - EscortGroupAttack:OptionROEOpenFire() - EscortGroupAttack:OptionROTVertical() - SCHDULER:New( EscortGroupAttack, - EscortGroupAttack.PushTask, - { EscortGroupAttack:TaskCombo( - { EscortGroupAttack:TaskAttackUnit( AttackUnit ), - EscortGroupAttack:TaskOrbitCircle( 500, 350 ) - } - ) - }, 10 - ) - else - SCHEDULER:New( EscortGroupAttack, - EscortGroupAttack.PushTask, - { EscortGroupAttack:TaskCombo( - { EscortGroupAttack:TaskFireAtPoint( AttackUnit:GetVec2(), 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 - - SCHEDULER:New( EscortGroup, EscortGroup.SetTask, { EscortGroup:TaskRoute( WayPoints ) }, 1 ) - - EscortGroup:MessageToClient( "Resuming mission from waypoint " .. WayPoint .. ".", 10, EscortClient ) -end - ---- Registers the waypoints --- @param #ESCORT self --- @return #table -function ESCORT:RegisterRoute() - self:F() - - local EscortGroup = self.EscortGroup -- Wrapper.Group#GROUP - - local TaskPoints = EscortGroup:GetTaskRoute() - - self:T( TaskPoints ) - - return TaskPoints -end - ---- @param Functional.Escort#ESCORT self -function ESCORT:_FollowScheduler() - self:F( { self.FollowDistance } ) - - self:T( {self.EscortClient.UnitName, self.EscortGroup.GroupName } ) - if self.EscortGroup:IsAlive() and self.EscortClient:IsAlive() then - - local ClientUnit = self.EscortClient:GetClientGroupUnit() - local GroupUnit = self.EscortGroup:GetUnit( 1 ) - local FollowDistance = self.FollowDistance - - self:T( {ClientUnit.UnitName, GroupUnit.UnitName } ) - - if self.CT1 == 0 and self.GT1 == 0 then - self.CV1 = ClientUnit:GetVec3() - self:T( { "self.CV1", self.CV1 } ) - self.CT1 = timer.getTime() - self.GV1 = GroupUnit:GetVec3() - self.GT1 = timer.getTime() - else - local CT1 = self.CT1 - local CT2 = timer.getTime() - local CV1 = self.CV1 - local CV2 = ClientUnit:GetVec3() - self.CT1 = CT2 - self.CV1 = CV2 - - local CD = ( ( CV2.x - CV1.x )^2 + ( CV2.y - CV1.y )^2 + ( CV2.z - CV1.z )^2 ) ^ 0.5 - local CT = CT2 - CT1 - - local CS = ( 3600 / CT ) * ( CD / 1000 ) - - self:T2( { "Client:", CS, CD, CT, CV2, CV1, CT2, CT1 } ) - - local GT1 = self.GT1 - local GT2 = timer.getTime() - local GV1 = self.GV1 - local GV2 = GroupUnit:GetVec3() - self.GT1 = GT2 - self.GV1 = GV2 - - local GD = ( ( GV2.x - GV1.x )^2 + ( GV2.y - GV1.y )^2 + ( GV2.z - GV1.z )^2 ) ^ 0.5 - local GT = GT2 - GT1 - - local GS = ( 3600 / GT ) * ( GD / 1000 ) - - self:T2( { "Group:", GS, GD, GT, GV2, GV1, GT2, GT1 } ) - - -- Calculate the group direction vector - local GV = { x = GV2.x - CV2.x, y = GV2.y - CV2.y, z = GV2.z - CV2.z } - - -- Calculate GH2, GH2 with the same height as CV2. - local GH2 = { x = GV2.x, y = CV2.y, z = GV2.z } - - -- Calculate the angle of GV to the orthonormal plane - local alpha = math.atan2( GV.z, GV.x ) - - -- Now we calculate the intersecting vector between the circle around CV2 with radius FollowDistance and GH2. - -- From the GeoGebra model: CVI = (x(CV2) + FollowDistance cos(alpha), y(GH2) + FollowDistance sin(alpha), z(CV2)) - local CVI = { x = CV2.x + FollowDistance * math.cos(alpha), - y = GH2.y, - z = CV2.z + FollowDistance * math.sin(alpha), - } - - -- Calculate the direction vector DV of the escort group. We use CVI as the base and CV2 as the direction. - local DV = { x = CV2.x - CVI.x, y = CV2.y - CVI.y, z = CV2.z - CVI.z } - - -- We now calculate the unary direction vector DVu, so that we can multiply DVu with the speed, which is expressed in meters / s. - -- We need to calculate this vector to predict the point the escort group needs to fly to according its speed. - -- The distance of the destination point should be far enough not to have the aircraft starting to swipe left to right... - local DVu = { x = DV.x / FollowDistance, y = DV.y / FollowDistance, z = DV.z / FollowDistance } - - -- Now we can calculate the group destination vector GDV. - local GDV = { x = DVu.x * CS * 8 + CVI.x, y = CVI.y, z = DVu.z * CS * 8 + CVI.z } - - if self.SmokeDirectionVector == true then - trigger.action.smoke( GDV, trigger.smokeColor.Red ) - end - - self:T2( { "CV2:", CV2 } ) - self:T2( { "CVI:", CVI } ) - self:T2( { "GDV:", GDV } ) - - -- Measure distance between client and group - local CatchUpDistance = ( ( GDV.x - GV2.x )^2 + ( GDV.y - GV2.y )^2 + ( GDV.z - GV2.z )^2 ) ^ 0.5 - - -- The calculation of the Speed would simulate that the group would take 30 seconds to overcome - -- the requested Distance). - local Time = 10 - local CatchUpSpeed = ( CatchUpDistance - ( CS * 8.4 ) ) / Time - - local Speed = CS + CatchUpSpeed - if Speed < 0 then - Speed = 0 - end - - self:T( { "Client Speed, Escort Speed, Speed, FollowDistance, Time:", CS, GS, Speed, FollowDistance, Time } ) - - -- Now route the escort to the desired point with the desired speed. - self.EscortGroup:RouteToVec3( GDV, Speed / 3.6 ) -- DCS models speed in Mps (Miles per second) - end - - return true - end - - return false -end - - ---- Report Targets Scheduler. --- @param #ESCORT self -function ESCORT:_ReportTargetsScheduler() - self:F( self.EscortGroup:GetName() ) - - if self.EscortGroup:IsAlive() and self.EscortClient:IsAlive() then - local EscortGroupName = self.EscortGroup:GetName() - local EscortTargets = self.EscortGroup:GetDetectedTargets() - - local ClientEscortTargets = self.EscortClient._EscortGroups[EscortGroupName].Targets - - local EscortTargetMessages = "" - for EscortTargetID, EscortTarget in pairs( EscortTargets ) do - local EscortObject = EscortTarget.object - self:T( EscortObject ) - if EscortObject and EscortObject:isExist() and EscortObject.id_ < 50000000 then - - local EscortTargetUnit = UNIT:Find( EscortObject ) - local EscortTargetUnitName = EscortTargetUnit:GetName() - - - - -- local EscortTargetIsDetected, - -- EscortTargetIsVisible, - -- EscortTargetLastTime, - -- EscortTargetKnowType, - -- EscortTargetKnowDistance, - -- EscortTargetLastPos, - -- EscortTargetLastVelocity - -- = self.EscortGroup:IsTargetDetected( EscortObject ) - -- - -- self:T( { EscortTargetIsDetected, - -- EscortTargetIsVisible, - -- EscortTargetLastTime, - -- EscortTargetKnowType, - -- EscortTargetKnowDistance, - -- EscortTargetLastPos, - -- EscortTargetLastVelocity } ) - - - local EscortTargetUnitVec3 = EscortTargetUnit:GetVec3() - local EscortVec3 = self.EscortGroup:GetVec3() - local Distance = ( ( EscortTargetUnitVec3.x - EscortVec3.x )^2 + - ( EscortTargetUnitVec3.y - EscortVec3.y )^2 + - ( EscortTargetUnitVec3.z - EscortVec3.z )^2 - ) ^ 0.5 / 1000 - - self:T( { self.EscortGroup:GetName(), EscortTargetUnit:GetName(), Distance, EscortTarget } ) - - if Distance <= 15 then - - if not ClientEscortTargets[EscortTargetUnitName] then - ClientEscortTargets[EscortTargetUnitName] = {} - end - ClientEscortTargets[EscortTargetUnitName].AttackUnit = EscortTargetUnit - ClientEscortTargets[EscortTargetUnitName].visible = EscortTarget.visible - ClientEscortTargets[EscortTargetUnitName].type = EscortTarget.type - ClientEscortTargets[EscortTargetUnitName].distance = EscortTarget.distance - else - if ClientEscortTargets[EscortTargetUnitName] then - ClientEscortTargets[EscortTargetUnitName] = nil - end - end - end - end - - self:T( { "Sorting Targets Table:", ClientEscortTargets } ) - table.sort( ClientEscortTargets, function( a, b ) return a.Distance < b.Distance end ) - self:T( { "Sorted Targets Table:", ClientEscortTargets } ) - - -- Remove the sub menus of the Attack menu of the Escort for the EscortGroup. - self.EscortMenuAttackNearbyTargets:RemoveSubMenus() - - if self.EscortMenuTargetAssistance then - self.EscortMenuTargetAssistance:RemoveSubMenus() - end - - --for MenuIndex = 1, #self.EscortMenuAttackTargets do - -- self:T( { "Remove Menu:", self.EscortMenuAttackTargets[MenuIndex] } ) - -- self.EscortMenuAttackTargets[MenuIndex] = self.EscortMenuAttackTargets[MenuIndex]:Remove() - --end - - - if ClientEscortTargets then - for ClientEscortTargetUnitName, ClientEscortTargetData in pairs( ClientEscortTargets ) do - - for ClientEscortGroupName, EscortGroupData in pairs( self.EscortClient._EscortGroups ) do - - if ClientEscortTargetData and ClientEscortTargetData.AttackUnit:IsAlive() then - - local EscortTargetMessage = "" - local EscortTargetCategoryName = ClientEscortTargetData.AttackUnit:GetCategoryName() - local EscortTargetCategoryType = ClientEscortTargetData.AttackUnit:GetTypeName() - if ClientEscortTargetData.type then - EscortTargetMessage = EscortTargetMessage .. EscortTargetCategoryName .. " (" .. EscortTargetCategoryType .. ") at " - else - EscortTargetMessage = EscortTargetMessage .. "Unknown target at " - end - - local EscortTargetUnitVec3 = ClientEscortTargetData.AttackUnit:GetVec3() - local EscortVec3 = self.EscortGroup:GetVec3() - local Distance = ( ( EscortTargetUnitVec3.x - EscortVec3.x )^2 + - ( EscortTargetUnitVec3.y - EscortVec3.y )^2 + - ( EscortTargetUnitVec3.z - EscortVec3.z )^2 - ) ^ 0.5 / 1000 - - self:T( { self.EscortGroup:GetName(), ClientEscortTargetData.AttackUnit:GetName(), Distance, ClientEscortTargetData.AttackUnit } ) - if ClientEscortTargetData.visible == false then - EscortTargetMessage = EscortTargetMessage .. string.format( "%.2f", Distance ) .. " estimated km" - else - EscortTargetMessage = EscortTargetMessage .. string.format( "%.2f", Distance ) .. " km" - end - - if ClientEscortTargetData.visible then - EscortTargetMessage = EscortTargetMessage .. ", visual" - end - - if ClientEscortGroupName == EscortGroupName then - - MENU_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 EscortVec3 = self.EscortGroup:GetVec3() - local Distance = ( ( WayPoint.x - EscortVec3.x )^2 + - ( WayPoint.y - EscortVec3.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 Core.Set#SET_CLIENT DBClients --- @extends Core.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 Wrapper.Client#CLIENT Client --- function( Client ) --- --- ... actions ... --- --- end --- ) - - self.MessagesOnOff = true - - self.TrackingToAll = false - self.TrackingOnOff = true - self.TrackingFrequency = 3 - - self.AlertsToAll = true - self.AlertsHitsOnOff = true - self.AlertsLaunchesOnOff = true - - self.DetailsRangeOnOff = true - self.DetailsBearingOnOff = true - - self.MenusOnOff = true - - self.TrackingMissiles = {} - - self.TrackingScheduler = SCHEDULER:New( self, self._TrackMissiles, {}, 0.5, 0.05, 0 ) - - return self -end - --- Initialization methods. - - - ---- Sets by default the display of any message to be ON or OFF. --- @param #MISSILETRAINER self --- @param #boolean MessagesOnOff true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitMessagesOnOff( MessagesOnOff ) - self:F( MessagesOnOff ) - - self.MessagesOnOff = MessagesOnOff - if self.MessagesOnOff == true then - MESSAGE:New( "Messages ON", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Messages OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Sets by default the missile tracking report for all players or only for those missiles targetted to you. --- @param #MISSILETRAINER self --- @param #boolean TrackingToAll true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitTrackingToAll( TrackingToAll ) - self:F( TrackingToAll ) - - self.TrackingToAll = TrackingToAll - if self.TrackingToAll == true then - MESSAGE:New( "Missile tracking to all players ON", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Missile tracking to all players OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Sets by default the display of missile tracking report to be ON or OFF. --- @param #MISSILETRAINER self --- @param #boolean TrackingOnOff true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitTrackingOnOff( TrackingOnOff ) - self:F( TrackingOnOff ) - - self.TrackingOnOff = TrackingOnOff - if self.TrackingOnOff == true then - MESSAGE:New( "Missile tracking ON", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Missile tracking OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Increases, decreases the missile tracking message display frequency with the provided time interval in seconds. --- The default frequency is a 3 second interval, so the Tracking Frequency parameter specifies the increase or decrease from the default 3 seconds or the last frequency update. --- @param #MISSILETRAINER self --- @param #number TrackingFrequency Provide a negative or positive value in seconds to incraese or decrease the display frequency. --- @return #MISSILETRAINER self -function MISSILETRAINER:InitTrackingFrequency( TrackingFrequency ) - self:F( TrackingFrequency ) - - self.TrackingFrequency = self.TrackingFrequency + TrackingFrequency - if self.TrackingFrequency < 0.5 then - self.TrackingFrequency = 0.5 - end - if self.TrackingFrequency then - MESSAGE:New( "Missile tracking frequency is " .. self.TrackingFrequency .. " seconds.", 15, "Menu" ):ToAll() - end - - return self -end - ---- Sets by default the display of alerts to be shown to all players or only to you. --- @param #MISSILETRAINER self --- @param #boolean AlertsToAll true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitAlertsToAll( AlertsToAll ) - self:F( AlertsToAll ) - - self.AlertsToAll = AlertsToAll - if self.AlertsToAll == true then - MESSAGE:New( "Alerts to all players ON", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Alerts to all players OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Sets by default the display of hit alerts ON or OFF. --- @param #MISSILETRAINER self --- @param #boolean AlertsHitsOnOff true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitAlertsHitsOnOff( AlertsHitsOnOff ) - self:F( AlertsHitsOnOff ) - - self.AlertsHitsOnOff = AlertsHitsOnOff - if self.AlertsHitsOnOff == true then - MESSAGE:New( "Alerts Hits ON", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Alerts Hits OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Sets by default the display of launch alerts ON or OFF. --- @param #MISSILETRAINER self --- @param #boolean AlertsLaunchesOnOff true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitAlertsLaunchesOnOff( AlertsLaunchesOnOff ) - self:F( AlertsLaunchesOnOff ) - - self.AlertsLaunchesOnOff = AlertsLaunchesOnOff - if self.AlertsLaunchesOnOff == true then - MESSAGE:New( "Alerts Launches ON", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Alerts Launches OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Sets by default the display of range information of missiles ON of OFF. --- @param #MISSILETRAINER self --- @param #boolean DetailsRangeOnOff true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitRangeOnOff( DetailsRangeOnOff ) - self:F( DetailsRangeOnOff ) - - self.DetailsRangeOnOff = DetailsRangeOnOff - if self.DetailsRangeOnOff == true then - MESSAGE:New( "Range display ON", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Range display OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Sets by default the display of bearing information of missiles ON of OFF. --- @param #MISSILETRAINER self --- @param #boolean DetailsBearingOnOff true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitBearingOnOff( DetailsBearingOnOff ) - self:F( DetailsBearingOnOff ) - - self.DetailsBearingOnOff = DetailsBearingOnOff - if self.DetailsBearingOnOff == true then - MESSAGE:New( "Bearing display OFF", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Bearing display OFF", 15, "Menu" ):ToAll() - end - - return self -end - ---- Enables / Disables the menus. --- @param #MISSILETRAINER self --- @param #boolean MenusOnOff true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitMenusOnOff( MenusOnOff ) - self:F( MenusOnOff ) - - self.MenusOnOff = MenusOnOff - if self.MenusOnOff == true then - MESSAGE:New( "Menus are ENABLED (only when a player rejoins a slot)", 15, "Menu" ):ToAll() - else - MESSAGE:New( "Menus are DISABLED", 15, "Menu" ):ToAll() - end - - return self -end - - --- Menu functions - -function MISSILETRAINER._MenuMessages( MenuParameters ) - - local self = MenuParameters.MenuSelf - - if MenuParameters.MessagesOnOff ~= nil then - self:InitMessagesOnOff( MenuParameters.MessagesOnOff ) - end - - if MenuParameters.TrackingToAll ~= nil then - self:InitTrackingToAll( MenuParameters.TrackingToAll ) - end - - if MenuParameters.TrackingOnOff ~= nil then - self:InitTrackingOnOff( MenuParameters.TrackingOnOff ) - end - - if MenuParameters.TrackingFrequency ~= nil then - self:InitTrackingFrequency( MenuParameters.TrackingFrequency ) - end - - if MenuParameters.AlertsToAll ~= nil then - self:InitAlertsToAll( MenuParameters.AlertsToAll ) - end - - if MenuParameters.AlertsHitsOnOff ~= nil then - self:InitAlertsHitsOnOff( MenuParameters.AlertsHitsOnOff ) - end - - if MenuParameters.AlertsLaunchesOnOff ~= nil then - self:InitAlertsLaunchesOnOff( MenuParameters.AlertsLaunchesOnOff ) - end - - if MenuParameters.DetailsRangeOnOff ~= nil then - self:InitRangeOnOff( MenuParameters.DetailsRangeOnOff ) - end - - if MenuParameters.DetailsBearingOnOff ~= nil then - self:InitBearingOnOff( MenuParameters.DetailsBearingOnOff ) - end - - if MenuParameters.Distance ~= nil then - self.Distance = MenuParameters.Distance - MESSAGE:New( "Hit detection distance set to " .. self.Distance .. " meters", 15, "Menu" ):ToAll() - end - -end - ---- Detects if an SA site was shot with an anti radiation missile. In this case, take evasive actions based on the skill level set within the ME. --- @param #MISSILETRAINER self --- @param Core.Event#EVENTDATA 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 - if TrainerTargetDCSUnit then - local TrainerTargetDCSUnitName = Unit.getName( TrainerTargetDCSUnit ) - local TrainerTargetSkill = _DATABASE.Templates.Units[TrainerTargetDCSUnitName].Template.skill - - self:T(TrainerTargetDCSUnitName ) - - local Client = self.DBClients:FindClient( TrainerTargetDCSUnitName ) - if Client then - - local TrainerSourceUnit = UNIT:Find( TrainerSourceDCSUnit ) - local TrainerTargetUnit = UNIT:Find( TrainerTargetDCSUnit ) - - if self.MessagesOnOff == true and self.AlertsLaunchesOnOff == true then - - local Message = MESSAGE:New( - string.format( "%s launched a %s", - TrainerSourceUnit:GetTypeName(), - TrainerWeaponName - ) .. self:_AddRange( Client, TrainerWeapon ) .. self:_AddBearing( Client, TrainerWeapon ), 5, "Launch Alert" ) - - if self.AlertsToAll then - Message:ToAll() - else - Message:ToClient( Client ) - end - end - - local ClientID = Client:GetID() - self:T( ClientID ) - local MissileData = {} - MissileData.TrainerSourceUnit = TrainerSourceUnit - MissileData.TrainerWeapon = TrainerWeapon - MissileData.TrainerTargetUnit = TrainerTargetUnit - MissileData.TrainerWeaponTypeName = TrainerWeapon:getTypeName() - MissileData.TrainerWeaponLaunched = true - table.insert( self.TrackingMissiles[ClientID].MissileData, MissileData ) - --self:T( self.TrackingMissiles ) - end - else - -- TODO: some weapons don't know the target unit... Need to develop a workaround for this. - if ( TrainerWeapon:getTypeName() == "9M311" ) then - SCHEDULER:New( TrainerWeapon, TrainerWeapon.destroy, {}, 1 ) - else - end - end -end - -function MISSILETRAINER:_AddRange( Client, TrainerWeapon ) - - local RangeText = "" - - if self.DetailsRangeOnOff then - - local PositionMissile = TrainerWeapon:getPoint() - local TargetVec3 = Client:GetVec3() - - local Range = ( ( PositionMissile.x - TargetVec3.x )^2 + - ( PositionMissile.y - TargetVec3.y )^2 + - ( PositionMissile.z - TargetVec3.z )^2 - ) ^ 0.5 / 1000 - - RangeText = string.format( ", at %4.2fkm", Range ) - end - - return RangeText -end - -function MISSILETRAINER:_AddBearing( Client, TrainerWeapon ) - - local BearingText = "" - - if self.DetailsBearingOnOff then - - local PositionMissile = TrainerWeapon:getPoint() - local TargetVec3 = Client:GetVec3() - - self:T2( { TargetVec3, PositionMissile }) - - local DirectionVector = { x = PositionMissile.x - TargetVec3.x, y = PositionMissile.y - TargetVec3.y, z = PositionMissile.z - TargetVec3.z } - local DirectionRadians = math.atan2( DirectionVector.z, DirectionVector.x ) - --DirectionRadians = DirectionRadians + routines.getNorthCorrection( PositionTarget ) - if DirectionRadians < 0 then - DirectionRadians = DirectionRadians + 2 * math.pi - end - local DirectionDegrees = DirectionRadians * 180 / math.pi - - BearingText = string.format( ", %d degrees", DirectionDegrees ) - end - - return BearingText -end - - -function MISSILETRAINER:_TrackMissiles() - self:F2() - - - local ShowMessages = false - if self.MessagesOnOff and self.MessageLastTime + self.TrackingFrequency <= timer.getTime() then - self.MessageLastTime = timer.getTime() - ShowMessages = true - end - - -- ALERTS PART - - -- Loop for all Player Clients to check the alerts and deletion of missiles. - for ClientDataID, ClientData in pairs( self.TrackingMissiles ) do - - local Client = ClientData.Client - 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 TargetVec3 = Client:GetVec3() - - local Distance = ( ( PositionMissile.x - TargetVec3.x )^2 + - ( PositionMissile.y - TargetVec3.y )^2 + - ( PositionMissile.z - TargetVec3.z )^2 - ) ^ 0.5 / 1000 - - if Distance <= self.Distance then - -- Hit alert - TrainerWeapon:destroy() - if self.MessagesOnOff == true and self.AlertsHitsOnOff == true then - - self:T( "killed" ) - - local Message = MESSAGE:New( - string.format( "%s launched by %s killed %s", - TrainerWeapon:getTypeName(), - TrainerSourceUnit:GetTypeName(), - TrainerTargetUnit:GetPlayerName() - ), 15, "Hit Alert" ) - - if self.AlertsToAll == true then - Message:ToAll() - else - Message:ToClient( Client ) - end - - MissileData = nil - table.remove( ClientData.MissileData, MissileDataID ) - self:T(ClientData.MissileData) - end - end - else - if not ( TrainerWeapon and TrainerWeapon:isExist() ) then - if self.MessagesOnOff == true and self.AlertsLaunchesOnOff == true then - -- Weapon does not exist anymore. Delete from Table - local Message = MESSAGE:New( - string.format( "%s launched by %s self destructed!", - TrainerWeaponTypeName, - TrainerSourceUnit:GetTypeName() - ), 5, "Tracking" ) - - if self.AlertsToAll == true then - Message:ToAll() - else - Message:ToClient( Client ) - end - end - MissileData = nil - table.remove( ClientData.MissileData, MissileDataID ) - self:T( ClientData.MissileData ) - end - end - end - 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 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 --- --- 3) @{AirbasePolice#AIRBASEPOLICE_NEVADA} class, extends @{AirbasePolice#AIRBASEPOLICE_BASE} --- ============================================================================================= --- All the airbases on the NEVADA 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: --- * Nellis --- * McCarran --- * Creech --- * Groom Lake --- --- ### Contributions: Dutch Baron - Concept & Testing --- ### Author: FlightControl - Framework Design & Programming --- --- @module AirbasePolice - - - - - ---- @type AIRBASEPOLICE_BASE --- @field Core.Set#SET_CLIENT SetClient --- @extends Core.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(SMOKECOLOR.White):Flush() - for PointsRunwayID, PointsRunway in pairs( Airbase.PointsRunways ) do - Airbase.ZoneRunways[PointsRunwayID] = ZONE_POLYGON_BASE:New( "Runway " .. PointsRunwayID, PointsRunway ):SmokeZone(SMOKECOLOR.Red):Flush() - end - end - --- -- Template --- local TemplateBoundary = GROUP:FindByName( "Template Boundary" ) --- self.Airbases.Template.ZoneBoundary = ZONE_POLYGON:New( "Template Boundary", TemplateBoundary ):SmokeZone(SMOKECOLOR.White):Flush() --- --- local TemplateRunway1 = GROUP:FindByName( "Template Runway 1" ) --- self.Airbases.Template.ZoneRunways[1] = ZONE_POLYGON:New( "Template Runway 1", TemplateRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - - self.SetClient:ForEachClient( - --- @param Wrapper.Client#CLIENT Client - function( Client ) - Client:SetState( self, "Speeding", false ) - Client:SetState( self, "Warnings", 0) - Client:SetState( self, "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 Wrapper.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 - - -- TODO: GetVelocityKMH function usage - local VelocityVec3 = Client:GetVelocity() - local Velocity = ( VelocityVec3.x ^ 2 + VelocityVec3.y ^ 2 + VelocityVec3.z ^ 2 ) ^ 0.5 -- in meters / sec - local Velocity = Velocity * 3.6 -- now it is in km/h. - -- MESSAGE:New( "Velocity = " .. Velocity, 1 ):ToAll() - 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 <= 3 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 .. " / 3" ) - 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:Destroy() - trigger.action.setUserFlag( "AIRCRAFT_"..Client:GetID(), 100) - Client:SetState( self, "Speeding", false ) - Client:SetState( self, "Warnings", 0 ) - end - - else - Client:Message( "You are speeding on the taxiway, slow down now! 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 Core.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] = { - [1]={["y"]=242140.57142858,["x"]=-6478.8571428583,}, - [2]={["y"]=242188.57142858,["x"]=-6522.0000000011,}, - [3]={["y"]=244124.2857143,["x"]=-4344.0000000011,}, - [4]={["y"]=244068.2857143,["x"]=-4296.5714285726,}, - [5]={["y"]=242140.57142858,["x"]=-6480.0000000011,} - }, - }, - 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,}, - }, - [2] = { - [1]={["y"]=895605.71428572,["x"]=-314724.57142857,}, - [2]={["y"]=897639.71428572,["x"]=-316148,}, - [3]={["y"]=897683.42857143,["x"]=-316087.14285714,}, - [4]={["y"]=895650,["x"]=-314660,}, - [5]={["y"]=895606,["x"]=-314724.85714286,} - }, - }, - 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(SMOKECOLOR.White):Flush() - -- - -- local AnapaVityazevoRunway1 = GROUP:FindByName( "AnapaVityazevo Runway 1" ) - -- self.Airbases.AnapaVityazevo.ZoneRunways[1] = ZONE_POLYGON:New( "AnapaVityazevo Runway 1", AnapaVityazevoRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Batumi - -- local BatumiBoundary = GROUP:FindByName( "Batumi Boundary" ) - -- self.Airbases.Batumi.ZoneBoundary = ZONE_POLYGON:New( "Batumi Boundary", BatumiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local BatumiRunway1 = GROUP:FindByName( "Batumi Runway 1" ) - -- self.Airbases.Batumi.ZoneRunways[1] = ZONE_POLYGON:New( "Batumi Runway 1", BatumiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Beslan - -- local BeslanBoundary = GROUP:FindByName( "Beslan Boundary" ) - -- self.Airbases.Beslan.ZoneBoundary = ZONE_POLYGON:New( "Beslan Boundary", BeslanBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local BeslanRunway1 = GROUP:FindByName( "Beslan Runway 1" ) - -- self.Airbases.Beslan.ZoneRunways[1] = ZONE_POLYGON:New( "Beslan Runway 1", BeslanRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Gelendzhik - -- local GelendzhikBoundary = GROUP:FindByName( "Gelendzhik Boundary" ) - -- self.Airbases.Gelendzhik.ZoneBoundary = ZONE_POLYGON:New( "Gelendzhik Boundary", GelendzhikBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local GelendzhikRunway1 = GROUP:FindByName( "Gelendzhik Runway 1" ) - -- self.Airbases.Gelendzhik.ZoneRunways[1] = ZONE_POLYGON:New( "Gelendzhik Runway 1", GelendzhikRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Gudauta - -- local GudautaBoundary = GROUP:FindByName( "Gudauta Boundary" ) - -- self.Airbases.Gudauta.ZoneBoundary = ZONE_POLYGON:New( "Gudauta Boundary", GudautaBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local GudautaRunway1 = GROUP:FindByName( "Gudauta Runway 1" ) - -- self.Airbases.Gudauta.ZoneRunways[1] = ZONE_POLYGON:New( "Gudauta Runway 1", GudautaRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Kobuleti - -- local KobuletiBoundary = GROUP:FindByName( "Kobuleti Boundary" ) - -- self.Airbases.Kobuleti.ZoneBoundary = ZONE_POLYGON:New( "Kobuleti Boundary", KobuletiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local KobuletiRunway1 = GROUP:FindByName( "Kobuleti Runway 1" ) - -- self.Airbases.Kobuleti.ZoneRunways[1] = ZONE_POLYGON:New( "Kobuleti Runway 1", KobuletiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- KrasnodarCenter - -- local KrasnodarCenterBoundary = GROUP:FindByName( "KrasnodarCenter Boundary" ) - -- self.Airbases.KrasnodarCenter.ZoneBoundary = ZONE_POLYGON:New( "KrasnodarCenter Boundary", KrasnodarCenterBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local KrasnodarCenterRunway1 = GROUP:FindByName( "KrasnodarCenter Runway 1" ) - -- self.Airbases.KrasnodarCenter.ZoneRunways[1] = ZONE_POLYGON:New( "KrasnodarCenter Runway 1", KrasnodarCenterRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- KrasnodarPashkovsky - -- local KrasnodarPashkovskyBoundary = GROUP:FindByName( "KrasnodarPashkovsky Boundary" ) - -- self.Airbases.KrasnodarPashkovsky.ZoneBoundary = ZONE_POLYGON:New( "KrasnodarPashkovsky Boundary", KrasnodarPashkovskyBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local KrasnodarPashkovskyRunway1 = GROUP:FindByName( "KrasnodarPashkovsky Runway 1" ) - -- self.Airbases.KrasnodarPashkovsky.ZoneRunways[1] = ZONE_POLYGON:New( "KrasnodarPashkovsky Runway 1", KrasnodarPashkovskyRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- local KrasnodarPashkovskyRunway2 = GROUP:FindByName( "KrasnodarPashkovsky Runway 2" ) - -- self.Airbases.KrasnodarPashkovsky.ZoneRunways[2] = ZONE_POLYGON:New( "KrasnodarPashkovsky Runway 2", KrasnodarPashkovskyRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Krymsk - -- local KrymskBoundary = GROUP:FindByName( "Krymsk Boundary" ) - -- self.Airbases.Krymsk.ZoneBoundary = ZONE_POLYGON:New( "Krymsk Boundary", KrymskBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local KrymskRunway1 = GROUP:FindByName( "Krymsk Runway 1" ) - -- self.Airbases.Krymsk.ZoneRunways[1] = ZONE_POLYGON:New( "Krymsk Runway 1", KrymskRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Kutaisi - -- local KutaisiBoundary = GROUP:FindByName( "Kutaisi Boundary" ) - -- self.Airbases.Kutaisi.ZoneBoundary = ZONE_POLYGON:New( "Kutaisi Boundary", KutaisiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local KutaisiRunway1 = GROUP:FindByName( "Kutaisi Runway 1" ) - -- self.Airbases.Kutaisi.ZoneRunways[1] = ZONE_POLYGON:New( "Kutaisi Runway 1", KutaisiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- MaykopKhanskaya - -- local MaykopKhanskayaBoundary = GROUP:FindByName( "MaykopKhanskaya Boundary" ) - -- self.Airbases.MaykopKhanskaya.ZoneBoundary = ZONE_POLYGON:New( "MaykopKhanskaya Boundary", MaykopKhanskayaBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local MaykopKhanskayaRunway1 = GROUP:FindByName( "MaykopKhanskaya Runway 1" ) - -- self.Airbases.MaykopKhanskaya.ZoneRunways[1] = ZONE_POLYGON:New( "MaykopKhanskaya Runway 1", MaykopKhanskayaRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- MineralnyeVody - -- local MineralnyeVodyBoundary = GROUP:FindByName( "MineralnyeVody Boundary" ) - -- self.Airbases.MineralnyeVody.ZoneBoundary = ZONE_POLYGON:New( "MineralnyeVody Boundary", MineralnyeVodyBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local MineralnyeVodyRunway1 = GROUP:FindByName( "MineralnyeVody Runway 1" ) - -- self.Airbases.MineralnyeVody.ZoneRunways[1] = ZONE_POLYGON:New( "MineralnyeVody Runway 1", MineralnyeVodyRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Mozdok - -- local MozdokBoundary = GROUP:FindByName( "Mozdok Boundary" ) - -- self.Airbases.Mozdok.ZoneBoundary = ZONE_POLYGON:New( "Mozdok Boundary", MozdokBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local MozdokRunway1 = GROUP:FindByName( "Mozdok Runway 1" ) - -- self.Airbases.Mozdok.ZoneRunways[1] = ZONE_POLYGON:New( "Mozdok Runway 1", MozdokRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Nalchik - -- local NalchikBoundary = GROUP:FindByName( "Nalchik Boundary" ) - -- self.Airbases.Nalchik.ZoneBoundary = ZONE_POLYGON:New( "Nalchik Boundary", NalchikBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local NalchikRunway1 = GROUP:FindByName( "Nalchik Runway 1" ) - -- self.Airbases.Nalchik.ZoneRunways[1] = ZONE_POLYGON:New( "Nalchik Runway 1", NalchikRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Novorossiysk - -- local NovorossiyskBoundary = GROUP:FindByName( "Novorossiysk Boundary" ) - -- self.Airbases.Novorossiysk.ZoneBoundary = ZONE_POLYGON:New( "Novorossiysk Boundary", NovorossiyskBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local NovorossiyskRunway1 = GROUP:FindByName( "Novorossiysk Runway 1" ) - -- self.Airbases.Novorossiysk.ZoneRunways[1] = ZONE_POLYGON:New( "Novorossiysk Runway 1", NovorossiyskRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- SenakiKolkhi - -- local SenakiKolkhiBoundary = GROUP:FindByName( "SenakiKolkhi Boundary" ) - -- self.Airbases.SenakiKolkhi.ZoneBoundary = ZONE_POLYGON:New( "SenakiKolkhi Boundary", SenakiKolkhiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local SenakiKolkhiRunway1 = GROUP:FindByName( "SenakiKolkhi Runway 1" ) - -- self.Airbases.SenakiKolkhi.ZoneRunways[1] = ZONE_POLYGON:New( "SenakiKolkhi Runway 1", SenakiKolkhiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- SochiAdler - -- local SochiAdlerBoundary = GROUP:FindByName( "SochiAdler Boundary" ) - -- self.Airbases.SochiAdler.ZoneBoundary = ZONE_POLYGON:New( "SochiAdler Boundary", SochiAdlerBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local SochiAdlerRunway1 = GROUP:FindByName( "SochiAdler Runway 1" ) - -- self.Airbases.SochiAdler.ZoneRunways[1] = ZONE_POLYGON:New( "SochiAdler Runway 1", SochiAdlerRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- local SochiAdlerRunway2 = GROUP:FindByName( "SochiAdler Runway 2" ) - -- self.Airbases.SochiAdler.ZoneRunways[2] = ZONE_POLYGON:New( "SochiAdler Runway 2", SochiAdlerRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Soganlug - -- local SoganlugBoundary = GROUP:FindByName( "Soganlug Boundary" ) - -- self.Airbases.Soganlug.ZoneBoundary = ZONE_POLYGON:New( "Soganlug Boundary", SoganlugBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local SoganlugRunway1 = GROUP:FindByName( "Soganlug Runway 1" ) - -- self.Airbases.Soganlug.ZoneRunways[1] = ZONE_POLYGON:New( "Soganlug Runway 1", SoganlugRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- SukhumiBabushara - -- local SukhumiBabusharaBoundary = GROUP:FindByName( "SukhumiBabushara Boundary" ) - -- self.Airbases.SukhumiBabushara.ZoneBoundary = ZONE_POLYGON:New( "SukhumiBabushara Boundary", SukhumiBabusharaBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local SukhumiBabusharaRunway1 = GROUP:FindByName( "SukhumiBabushara Runway 1" ) - -- self.Airbases.SukhumiBabushara.ZoneRunways[1] = ZONE_POLYGON:New( "SukhumiBabushara Runway 1", SukhumiBabusharaRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- TbilisiLochini - -- local TbilisiLochiniBoundary = GROUP:FindByName( "TbilisiLochini Boundary" ) - -- self.Airbases.TbilisiLochini.ZoneBoundary = ZONE_POLYGON:New( "TbilisiLochini Boundary", TbilisiLochiniBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local TbilisiLochiniRunway1 = GROUP:FindByName( "TbilisiLochini Runway 1" ) - -- self.Airbases.TbilisiLochini.ZoneRunways[1] = ZONE_POLYGON:New( "TbilisiLochini Runway 1", TbilisiLochiniRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- local TbilisiLochiniRunway2 = GROUP:FindByName( "TbilisiLochini Runway 2" ) - -- self.Airbases.TbilisiLochini.ZoneRunways[2] = ZONE_POLYGON:New( "TbilisiLochini Runway 2", TbilisiLochiniRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Vaziani - -- local VazianiBoundary = GROUP:FindByName( "Vaziani Boundary" ) - -- self.Airbases.Vaziani.ZoneBoundary = ZONE_POLYGON:New( "Vaziani Boundary", VazianiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local VazianiRunway1 = GROUP:FindByName( "Vaziani Runway 1" ) - -- self.Airbases.Vaziani.ZoneRunways[1] = ZONE_POLYGON:New( "Vaziani Runway 1", VazianiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - - - -- Template - -- local TemplateBoundary = GROUP:FindByName( "Template Boundary" ) - -- self.Airbases.Template.ZoneBoundary = ZONE_POLYGON:New( "Template Boundary", TemplateBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local TemplateRunway1 = GROUP:FindByName( "Template Runway 1" ) - -- self.Airbases.Template.ZoneRunways[1] = ZONE_POLYGON:New( "Template Runway 1", TemplateRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - - return self - -end - - - - ---- @type AIRBASEPOLICE_NEVADA --- @extends Functional.AirbasePolice#AIRBASEPOLICE_BASE -AIRBASEPOLICE_NEVADA = { - ClassName = "AIRBASEPOLICE_NEVADA", - Airbases = { - Nellis = { - PointsBoundary = { - [1]={["y"]=-17814.714285714,["x"]=-399823.14285714,}, - [2]={["y"]=-16875.857142857,["x"]=-398763.14285714,}, - [3]={["y"]=-16251.571428571,["x"]=-398988.85714286,}, - [4]={["y"]=-16163,["x"]=-398693.14285714,}, - [5]={["y"]=-16328.714285714,["x"]=-398034.57142857,}, - [6]={["y"]=-15943,["x"]=-397571.71428571,}, - [7]={["y"]=-15711.571428571,["x"]=-397551.71428571,}, - [8]={["y"]=-15748.714285714,["x"]=-396806,}, - [9]={["y"]=-16288.714285714,["x"]=-396517.42857143,}, - [10]={["y"]=-16751.571428571,["x"]=-396308.85714286,}, - [11]={["y"]=-17263,["x"]=-396234.57142857,}, - [12]={["y"]=-17577.285714286,["x"]=-396640.28571429,}, - [13]={["y"]=-17614.428571429,["x"]=-397400.28571429,}, - [14]={["y"]=-19405.857142857,["x"]=-399428.85714286,}, - [15]={["y"]=-19234.428571429,["x"]=-399683.14285714,}, - [16]={["y"]=-18708.714285714,["x"]=-399408.85714286,}, - [17]={["y"]=-18397.285714286,["x"]=-399657.42857143,}, - [18]={["y"]=-17814.428571429,["x"]=-399823.42857143,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=-18687,["x"]=-399380.28571429,}, - [2]={["y"]=-18620.714285714,["x"]=-399436.85714286,}, - [3]={["y"]=-16217.857142857,["x"]=-396596.85714286,}, - [4]={["y"]=-16300.142857143,["x"]=-396530,}, - [5]={["y"]=-18687,["x"]=-399380.85714286,}, - }, - [2] = { - [1]={["y"]=-18451.571428572,["x"]=-399580.57142857,}, - [2]={["y"]=-18392.142857143,["x"]=-399628.57142857,}, - [3]={["y"]=-16011,["x"]=-396806.85714286,}, - [4]={["y"]=-16074.714285714,["x"]=-396751.71428572,}, - [5]={["y"]=-18451.571428572,["x"]=-399580.85714285,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - McCarran = { - PointsBoundary = { - [1]={["y"]=-29455.285714286,["x"]=-416277.42857142,}, - [2]={["y"]=-28860.142857143,["x"]=-416492,}, - [3]={["y"]=-25044.428571429,["x"]=-416344.85714285,}, - [4]={["y"]=-24580.142857143,["x"]=-415959.14285714,}, - [5]={["y"]=-25073,["x"]=-415630.57142857,}, - [6]={["y"]=-25087.285714286,["x"]=-415130.57142857,}, - [7]={["y"]=-25830.142857143,["x"]=-414866.28571428,}, - [8]={["y"]=-26658.714285715,["x"]=-414880.57142857,}, - [9]={["y"]=-26973,["x"]=-415273.42857142,}, - [10]={["y"]=-27380.142857143,["x"]=-415187.71428571,}, - [11]={["y"]=-27715.857142857,["x"]=-414144.85714285,}, - [12]={["y"]=-27551.571428572,["x"]=-413473.42857142,}, - [13]={["y"]=-28630.142857143,["x"]=-413201.99999999,}, - [14]={["y"]=-29494.428571429,["x"]=-415437.71428571,}, - [15]={["y"]=-29455.571428572,["x"]=-416277.71428571,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=-29408.428571429,["x"]=-416016.28571428,}, - [2]={["y"]=-29408.142857144,["x"]=-416105.42857142,}, - [3]={["y"]=-24680.714285715,["x"]=-416003.14285713,}, - [4]={["y"]=-24681.857142858,["x"]=-415926.57142856,}, - [5]={["y"]=-29408.42857143,["x"]=-416016.57142856,}, - }, - [2] = { - [1]={["y"]=-28575.571428572,["x"]=-416303.14285713,}, - [2]={["y"]=-28575.571428572,["x"]=-416382.57142856,}, - [3]={["y"]=-25111.000000001,["x"]=-416309.7142857,}, - [4]={["y"]=-25111.000000001,["x"]=-416249.14285713,}, - [5]={["y"]=-28575.571428572,["x"]=-416303.7142857,}, - }, - [3] = { - [1]={["y"]=-29331.000000001,["x"]=-416275.42857141,}, - [2]={["y"]=-29259.000000001,["x"]=-416306.85714284,}, - [3]={["y"]=-28005.571428572,["x"]=-413449.7142857,}, - [4]={["y"]=-28068.714285715,["x"]=-413422.85714284,}, - [5]={["y"]=-29331.000000001,["x"]=-416275.7142857,}, - }, - [4] = { - [1]={["y"]=-29073.285714286,["x"]=-416386.57142856,}, - [2]={["y"]=-28997.285714286,["x"]=-416417.42857141,}, - [3]={["y"]=-27697.571428572,["x"]=-413464.57142856,}, - [4]={["y"]=-27767.857142858,["x"]=-413434.28571427,}, - [5]={["y"]=-29073.000000001,["x"]=-416386.85714284,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Creech = { - PointsBoundary = { - [1]={["y"]=-74522.714285715,["x"]=-360887.99999998,}, - [2]={["y"]=-74197,["x"]=-360556.57142855,}, - [3]={["y"]=-74402.714285715,["x"]=-359639.42857141,}, - [4]={["y"]=-74637,["x"]=-359279.42857141,}, - [5]={["y"]=-75759.857142857,["x"]=-359005.14285712,}, - [6]={["y"]=-75834.142857143,["x"]=-359045.14285712,}, - [7]={["y"]=-75902.714285714,["x"]=-359782.28571427,}, - [8]={["y"]=-76099.857142857,["x"]=-360399.42857141,}, - [9]={["y"]=-77314.142857143,["x"]=-360219.42857141,}, - [10]={["y"]=-77728.428571429,["x"]=-360445.14285713,}, - [11]={["y"]=-77585.571428571,["x"]=-360585.14285713,}, - [12]={["y"]=-76471.285714286,["x"]=-360819.42857141,}, - [13]={["y"]=-76325.571428571,["x"]=-360942.28571427,}, - [14]={["y"]=-74671.857142857,["x"]=-360927.7142857,}, - [15]={["y"]=-74522.714285714,["x"]=-360888.85714284,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=-74237.571428571,["x"]=-360591.7142857,}, - [2]={["y"]=-74234.428571429,["x"]=-360493.71428571,}, - [3]={["y"]=-77605.285714286,["x"]=-360399.14285713,}, - [4]={["y"]=-77608.714285715,["x"]=-360498.85714285,}, - [5]={["y"]=-74237.857142857,["x"]=-360591.7142857,}, - }, - [2] = { - [1]={["y"]=-75807.571428572,["x"]=-359073.42857142,}, - [2]={["y"]=-74770.142857144,["x"]=-360581.71428571,}, - [3]={["y"]=-74641.285714287,["x"]=-360585.42857142,}, - [4]={["y"]=-75734.142857144,["x"]=-359023.14285714,}, - [5]={["y"]=-75807.285714287,["x"]=-359073.42857142,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - GroomLake = { - PointsBoundary = { - [1]={["y"]=-88916.714285714,["x"]=-289102.28571425,}, - [2]={["y"]=-87023.571428572,["x"]=-290388.57142857,}, - [3]={["y"]=-85916.428571429,["x"]=-290674.28571428,}, - [4]={["y"]=-87645.000000001,["x"]=-286567.14285714,}, - [5]={["y"]=-88380.714285715,["x"]=-286388.57142857,}, - [6]={["y"]=-89670.714285715,["x"]=-283524.28571428,}, - [7]={["y"]=-89797.857142858,["x"]=-283567.14285714,}, - [8]={["y"]=-88635.000000001,["x"]=-286749.99999999,}, - [9]={["y"]=-89177.857142858,["x"]=-287207.14285714,}, - [10]={["y"]=-89092.142857144,["x"]=-288892.85714285,}, - [11]={["y"]=-88917.000000001,["x"]=-289102.85714285,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=-86039.000000001,["x"]=-290606.28571428,}, - [2]={["y"]=-85965.285714287,["x"]=-290573.99999999,}, - [3]={["y"]=-87692.714285715,["x"]=-286634.85714285,}, - [4]={["y"]=-87756.714285715,["x"]=-286663.99999999,}, - [5]={["y"]=-86038.714285715,["x"]=-290606.85714285,}, - }, - [2] = { - [1]={["y"]=-86808.428571429,["x"]=-290375.7142857,}, - [2]={["y"]=-86732.714285715,["x"]=-290344.28571427,}, - [3]={["y"]=-89672.714285714,["x"]=-283546.57142855,}, - [4]={["y"]=-89772.142857143,["x"]=-283587.71428569,}, - [5]={["y"]=-86808.142857143,["x"]=-290375.7142857,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - }, -} - ---- Creates a new AIRBASEPOLICE_NEVADA object. --- @param #AIRBASEPOLICE_NEVADA 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_NEVADA self -function AIRBASEPOLICE_NEVADA:New( SetClient ) - - -- Inherits from BASE - local self = BASE:Inherit( self, AIRBASEPOLICE_BASE:New( SetClient, self.Airbases ) ) - --- -- Nellis --- local NellisBoundary = GROUP:FindByName( "Nellis Boundary" ) --- self.Airbases.Nellis.ZoneBoundary = ZONE_POLYGON:New( "Nellis Boundary", NellisBoundary ):SmokeZone(SMOKECOLOR.White):Flush() --- --- local NellisRunway1 = GROUP:FindByName( "Nellis Runway 1" ) --- self.Airbases.Nellis.ZoneRunways[1] = ZONE_POLYGON:New( "Nellis Runway 1", NellisRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- local NellisRunway2 = GROUP:FindByName( "Nellis Runway 2" ) --- self.Airbases.Nellis.ZoneRunways[2] = ZONE_POLYGON:New( "Nellis Runway 2", NellisRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- -- McCarran --- local McCarranBoundary = GROUP:FindByName( "McCarran Boundary" ) --- self.Airbases.McCarran.ZoneBoundary = ZONE_POLYGON:New( "McCarran Boundary", McCarranBoundary ):SmokeZone(SMOKECOLOR.White):Flush() --- --- local McCarranRunway1 = GROUP:FindByName( "McCarran Runway 1" ) --- self.Airbases.McCarran.ZoneRunways[1] = ZONE_POLYGON:New( "McCarran Runway 1", McCarranRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- local McCarranRunway2 = GROUP:FindByName( "McCarran Runway 2" ) --- self.Airbases.McCarran.ZoneRunways[2] = ZONE_POLYGON:New( "McCarran Runway 2", McCarranRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- local McCarranRunway3 = GROUP:FindByName( "McCarran Runway 3" ) --- self.Airbases.McCarran.ZoneRunways[3] = ZONE_POLYGON:New( "McCarran Runway 3", McCarranRunway3 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- local McCarranRunway4 = GROUP:FindByName( "McCarran Runway 4" ) --- self.Airbases.McCarran.ZoneRunways[4] = ZONE_POLYGON:New( "McCarran Runway 4", McCarranRunway4 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- -- Creech --- local CreechBoundary = GROUP:FindByName( "Creech Boundary" ) --- self.Airbases.Creech.ZoneBoundary = ZONE_POLYGON:New( "Creech Boundary", CreechBoundary ):SmokeZone(SMOKECOLOR.White):Flush() --- --- local CreechRunway1 = GROUP:FindByName( "Creech Runway 1" ) --- self.Airbases.Creech.ZoneRunways[1] = ZONE_POLYGON:New( "Creech Runway 1", CreechRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- local CreechRunway2 = GROUP:FindByName( "Creech Runway 2" ) --- self.Airbases.Creech.ZoneRunways[2] = ZONE_POLYGON:New( "Creech Runway 2", CreechRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- -- Groom Lake --- local GroomLakeBoundary = GROUP:FindByName( "GroomLake Boundary" ) --- self.Airbases.GroomLake.ZoneBoundary = ZONE_POLYGON:New( "GroomLake Boundary", GroomLakeBoundary ):SmokeZone(SMOKECOLOR.White):Flush() --- --- local GroomLakeRunway1 = GROUP:FindByName( "GroomLake Runway 1" ) --- self.Airbases.GroomLake.ZoneRunways[1] = ZONE_POLYGON:New( "GroomLake Runway 1", GroomLakeRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- local GroomLakeRunway2 = GROUP:FindByName( "GroomLake Runway 2" ) --- self.Airbases.GroomLake.ZoneRunways[2] = ZONE_POLYGON:New( "GroomLake Runway 2", GroomLakeRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - -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. --- The @{Detection#DETECTION_BASE} class will detect objects within the battle zone for a list of @{Group}s detecting targets following (a) detection method(s). --- --- 1.1) DETECTION_BASE constructor --- ------------------------------- --- Construct a new DETECTION_BASE instance using the @{Detection#DETECTION_BASE.New}() method. --- --- 1.2) DETECTION_BASE initialization --- ---------------------------------- --- By default, detection will return detected objects with all the detection sensors available. --- However, you can ask how the objects were found with specific detection methods. --- If you use one of the below methods, the detection will work with the detection method specified. --- You can specify to apply multiple detection methods. --- --- Use the following functions to report the objects it detected using the methods Visual, Optical, Radar, IRST, RWR, DLINK: --- --- * @{Detection#DETECTION_BASE.InitDetectVisual}(): Detected using Visual. --- * @{Detection#DETECTION_BASE.InitDetectOptical}(): Detected using Optical. --- * @{Detection#DETECTION_BASE.InitDetectRadar}(): Detected using Radar. --- * @{Detection#DETECTION_BASE.InitDetectIRST}(): Detected using IRST. --- * @{Detection#DETECTION_BASE.InitDetectRWR}(): Detected using RWR. --- * @{Detection#DETECTION_BASE.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_AREAS} class, extends @{Detection#DETECTION_BASE} --- =============================================================================== --- The @{Detection#DETECTION_AREAS} class will detect units within the battle zone for a list of @{Group}s detecting targets following (a) detection method(s), --- and will build a list (table) of @{Set#SET_UNIT}s containing the @{Unit#UNIT}s detected. --- The class is group the detected units within zones given a DetectedZoneRange parameter. --- A set with multiple detected zones will be created as there are groups of units detected. --- --- 2.1) Retrieve the Detected Unit sets and Detected Zones --- ------------------------------------------------------- --- The DetectedUnitSets methods are implemented in @{Detection#DECTECTION_BASE} and the DetectedZones methods is implemented in @{Detection#DETECTION_AREAS}. --- --- Retrieve the DetectedUnitSets with the method @{Detection#DETECTION_BASE.GetDetectedSets}(). A table will be return of @{Set#SET_UNIT}s. --- To understand the amount of sets created, use the method @{Detection#DETECTION_BASE.GetDetectedSetCount}(). --- If you want to obtain a specific set from the DetectedSets, use the method @{Detection#DETECTION_BASE.GetDetectedSet}() with a given index. --- --- Retrieve the formed @{Zone@ZONE_UNIT}s as a result of the grouping the detected units within the DetectionZoneRange, use the method @{Detection#DETECTION_BASE.GetDetectionZones}(). --- To understand the amount of zones created, use the method @{Detection#DETECTION_BASE.GetDetectionZoneCount}(). --- If you want to obtain a specific zone from the DetectedZones, use the method @{Detection#DETECTION_BASE.GetDetectionZone}() with a given index. --- --- 1.4) Flare or Smoke detected units --- ---------------------------------- --- Use the methods @{Detection#DETECTION_AREAS.FlareDetectedUnits}() or @{Detection#DETECTION_AREAS.SmokeDetectedUnits}() to flare or smoke the detected units when a new detection has taken place. --- --- 1.5) Flare or Smoke detected zones --- ---------------------------------- --- Use the methods @{Detection#DETECTION_AREAS.FlareDetectedZones}() or @{Detection#DETECTION_AREAS.SmokeDetectedZones}() to flare or smoke the detected zones when a new detection has taken place. --- --- === --- --- ### Contributions: --- --- * Mechanist : Concept & Testing --- --- ### Authors: --- --- * FlightControl : Design & Programming --- --- @module Detection - - - ---- DETECTION_BASE class --- @type DETECTION_BASE --- @field Core.Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Forward Air Controller role. --- @field Dcs.DCSTypes#Distance DetectionRange The range till which targets are accepted to be detected. --- @field #DETECTION_BASE.DetectedObjects DetectedObjects The list of detected objects. --- @field #table DetectedObjectsIdentified Map of the DetectedObjects identified. --- @field #number DetectionRun --- @extends Core.Base#BASE -DETECTION_BASE = { - ClassName = "DETECTION_BASE", - DetectionSetGroup = nil, - DetectionRange = nil, - DetectedObjects = {}, - DetectionRun = 0, - DetectedObjectsIdentified = {}, -} - ---- @type DETECTION_BASE.DetectedObjects --- @list <#DETECTION_BASE.DetectedObject> - ---- @type DETECTION_BASE.DetectedObject --- @field #string Name --- @field #boolean Visible --- @field #string Type --- @field #number Distance --- @field #boolean Identified - ---- DETECTION constructor. --- @param #DETECTION_BASE self --- @param Core.Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Forward Air Controller role. --- @param Dcs.DCSTypes#Distance DetectionRange The range till which targets are accepted to be detected. --- @return #DETECTION_BASE self -function DETECTION_BASE:New( DetectionSetGroup, DetectionRange ) - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) - - self.DetectionSetGroup = DetectionSetGroup - self.DetectionRange = DetectionRange - - self:InitDetectVisual( false ) - self:InitDetectOptical( false ) - self:InitDetectRadar( false ) - self:InitDetectRWR( false ) - self:InitDetectIRST( false ) - self:InitDetectDLINK( false ) - - 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 - ---- Determines if a detected object has already been identified during detection processing. --- @param #DETECTION_BASE self --- @param #DETECTION_BASE.DetectedObject DetectedObject --- @return #boolean true if already identified. -function DETECTION_BASE:IsDetectedObjectIdentified( DetectedObject ) - self:F3( DetectedObject.Name ) - - local DetectedObjectName = DetectedObject.Name - local DetectedObjectIdentified = self.DetectedObjectsIdentified[DetectedObjectName] == true - self:T3( DetectedObjectIdentified ) - return DetectedObjectIdentified -end - ---- Identifies a detected object during detection processing. --- @param #DETECTION_BASE self --- @param #DETECTION_BASE.DetectedObject DetectedObject -function DETECTION_BASE:IdentifyDetectedObject( DetectedObject ) - self:F( DetectedObject.Name ) - - local DetectedObjectName = DetectedObject.Name - self.DetectedObjectsIdentified[DetectedObjectName] = true -end - ---- UnIdentify a detected object during detection processing. --- @param #DETECTION_BASE self --- @param #DETECTION_BASE.DetectedObject DetectedObject -function DETECTION_BASE:UnIdentifyDetectedObject( DetectedObject ) - - local DetectedObjectName = DetectedObject.Name - self.DetectedObjectsIdentified[DetectedObjectName] = false -end - ---- UnIdentify all detected objects during detection processing. --- @param #DETECTION_BASE self -function DETECTION_BASE:UnIdentifyAllDetectedObjects() - - self.DetectedObjectsIdentified = {} -- Table will be garbage collected. -end - ---- Gets a detected object with a given name. --- @param #DETECTION_BASE self --- @param #string ObjectName --- @return #DETECTION_BASE.DetectedObject -function DETECTION_BASE:GetDetectedObject( ObjectName ) - self:F3( ObjectName ) - - if ObjectName then - local DetectedObject = self.DetectedObjects[ObjectName] - - -- Only return detected objects that are alive! - local DetectedUnit = UNIT:FindByName( ObjectName ) - if DetectedUnit and DetectedUnit:IsAlive() then - if self:IsDetectedObjectIdentified( DetectedObject ) == false then - return DetectedObject - end - end - end - - return nil -end - ---- Get the detected @{Set#SET_BASE}s. --- @param #DETECTION_BASE self --- @return #DETECTION_BASE.DetectedSets DetectedSets -function DETECTION_BASE:GetDetectedSets() - - local DetectionSets = self.DetectedSets - return DetectionSets -end - ---- Get the amount of SETs with detected objects. --- @param #DETECTION_BASE self --- @return #number Count -function DETECTION_BASE:GetDetectedSetCount() - - local DetectionSetCount = #self.DetectedSets - return DetectionSetCount -end - ---- Get a SET of detected objects using a given numeric index. --- @param #DETECTION_BASE self --- @param #number Index --- @return Core.Set#SET_BASE -function DETECTION_BASE:GetDetectedSet( Index ) - - local DetectionSet = self.DetectedSets[Index] - if DetectionSet then - return DetectionSet - end - - return nil -end - ---- Get the detection Groups. --- @param #DETECTION_BASE self --- @return Wrapper.Group#GROUP -function DETECTION_BASE:GetDetectionSetGroup() - - local DetectionSetGroup = self.DetectionSetGroup - return DetectionSetGroup -end - ---- 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 - - ---- Schedule the DETECTION construction. --- @param #DETECTION_BASE self --- @param #number DelayTime The delay in seconds to wait the reporting. --- @param #number RepeatInterval The repeat interval in seconds for the reporting to happen repeatedly. --- @return #DETECTION_BASE self -function DETECTION_BASE:Schedule( DelayTime, RepeatInterval ) - self:F2() - - self.ScheduleDelayTime = DelayTime - self.ScheduleRepeatInterval = RepeatInterval - - self.DetectionScheduler = SCHEDULER:New( self, self._DetectionScheduler, { self, "Detection" }, DelayTime, RepeatInterval ) - return self -end - - ---- 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.DetectionRun = self.DetectionRun + 1 - - self:UnIdentifyAllDetectedObjects() -- Resets the DetectedObjectsIdentified table - - for DetectionGroupID, DetectionGroupData in pairs( self.DetectionSetGroup:GetSet() ) do - local DetectionGroup = DetectionGroupData -- Wrapper.Group#GROUP - - if DetectionGroup:IsAlive() then - - local DetectionGroupName = DetectionGroup:GetName() - - local DetectionDetectedTargets = DetectionGroup:GetDetectedTargets( - self.DetectVisual, - self.DetectOptical, - self.DetectRadar, - self.DetectIRST, - self.DetectRWR, - self.DetectDLINK - ) - - for DetectionDetectedTargetID, DetectionDetectedTarget in pairs( DetectionDetectedTargets ) do - local DetectionObject = DetectionDetectedTarget.object -- Dcs.DCSWrapper.Object#Object - self:T2( DetectionObject ) - - if DetectionObject and DetectionObject:isExist() and DetectionObject.id_ < 50000000 then - - local DetectionDetectedObjectName = DetectionObject:getName() - - local DetectionDetectedObjectPositionVec3 = DetectionObject:getPoint() - local DetectionGroupVec3 = DetectionGroup:GetVec3() - - local Distance = ( ( DetectionDetectedObjectPositionVec3.x - DetectionGroupVec3.x )^2 + - ( DetectionDetectedObjectPositionVec3.y - DetectionGroupVec3.y )^2 + - ( DetectionDetectedObjectPositionVec3.z - DetectionGroupVec3.z )^2 - ) ^ 0.5 / 1000 - - self:T2( { DetectionGroupName, DetectionDetectedObjectName, Distance } ) - - if Distance <= self.DetectionRange then - - if not self.DetectedObjects[DetectionDetectedObjectName] then - self.DetectedObjects[DetectionDetectedObjectName] = {} - end - self.DetectedObjects[DetectionDetectedObjectName].Name = DetectionDetectedObjectName - self.DetectedObjects[DetectionDetectedObjectName].Visible = DetectionDetectedTarget.visible - self.DetectedObjects[DetectionDetectedObjectName].Type = DetectionDetectedTarget.type - self.DetectedObjects[DetectionDetectedObjectName].Distance = DetectionDetectedTarget.distance - else - -- if beyond the DetectionRange then nullify... - if self.DetectedObjects[DetectionDetectedObjectName] then - self.DetectedObjects[DetectionDetectedObjectName] = nil - end - end - end - end - - self:T2( self.DetectedObjects ) - - -- okay, now we have a list of detected object names ... - -- Sort the table based on distance ... - table.sort( self.DetectedObjects, function( a, b ) return a.Distance < b.Distance end ) - end - end - - if self.DetectedObjects then - self:CreateDetectionSets() - end - - return true -end - - - ---- DETECTION_AREAS class --- @type DETECTION_AREAS --- @field Dcs.DCSTypes#Distance DetectionZoneRange The range till which targets are grouped upon the first detected target. --- @field #DETECTION_AREAS.DetectedAreas DetectedAreas A list of areas containing the set of @{Unit}s, @{Zone}s, the center @{Unit} within the zone, and ID of each area that was detected within a DetectionZoneRange. --- @extends Functional.Detection#DETECTION_BASE -DETECTION_AREAS = { - ClassName = "DETECTION_AREAS", - DetectedAreas = { n = 0 }, - DetectionZoneRange = nil, -} - ---- @type DETECTION_AREAS.DetectedAreas --- @list <#DETECTION_AREAS.DetectedArea> - ---- @type DETECTION_AREAS.DetectedArea --- @field Core.Set#SET_UNIT Set -- The Set of Units in the detected area. --- @field Core.Zone#ZONE_UNIT Zone -- The Zone of the detected area. --- @field #boolean Changed Documents if the detected area has changes. --- @field #table Changes A list of the changes reported on the detected area. (It is up to the user of the detected area to consume those changes). --- @field #number AreaID -- The identifier of the detected area. --- @field #boolean FriendliesNearBy Indicates if there are friendlies within the detected area. --- @field Wrapper.Unit#UNIT NearestFAC The nearest FAC near the Area. - - ---- DETECTION_AREAS constructor. --- @param Functional.Detection#DETECTION_AREAS self --- @param Core.Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Forward Air Controller role. --- @param Dcs.DCSTypes#Distance DetectionRange The range till which targets are accepted to be detected. --- @param Dcs.DCSTypes#Distance DetectionZoneRange The range till which targets are grouped upon the first detected target. --- @return Functional.Detection#DETECTION_AREAS self -function DETECTION_AREAS:New( DetectionSetGroup, DetectionRange, DetectionZoneRange ) - - -- Inherits from DETECTION_BASE - local self = BASE:Inherit( self, DETECTION_BASE:New( DetectionSetGroup, DetectionRange ) ) - - self.DetectionZoneRange = DetectionZoneRange - - self._SmokeDetectedUnits = false - self._FlareDetectedUnits = false - self._SmokeDetectedZones = false - self._FlareDetectedZones = false - - self:Schedule( 10, 10 ) - - return self -end - ---- Add a detected @{#DETECTION_AREAS.DetectedArea}. --- @param Core.Set#SET_UNIT Set -- The Set of Units in the detected area. --- @param Core.Zone#ZONE_UNIT Zone -- The Zone of the detected area. --- @return #DETECTION_AREAS.DetectedArea DetectedArea -function DETECTION_AREAS:AddDetectedArea( Set, Zone ) - local DetectedAreas = self:GetDetectedAreas() - DetectedAreas.n = self:GetDetectedAreaCount() + 1 - DetectedAreas[DetectedAreas.n] = {} - local DetectedArea = DetectedAreas[DetectedAreas.n] - DetectedArea.Set = Set - DetectedArea.Zone = Zone - DetectedArea.Removed = false - DetectedArea.AreaID = DetectedAreas.n - - return DetectedArea -end - ---- Remove a detected @{#DETECTION_AREAS.DetectedArea} with a given Index. --- @param #DETECTION_AREAS self --- @param #number Index The Index of the detection are to be removed. --- @return #nil -function DETECTION_AREAS:RemoveDetectedArea( Index ) - local DetectedAreas = self:GetDetectedAreas() - local DetectedAreaCount = self:GetDetectedAreaCount() - local DetectedArea = DetectedAreas[Index] - local DetectedAreaSet = DetectedArea.Set - DetectedArea[Index] = nil - return nil -end - - ---- Get the detected @{#DETECTION_AREAS.DetectedAreas}. --- @param #DETECTION_AREAS self --- @return #DETECTION_AREAS.DetectedAreas DetectedAreas -function DETECTION_AREAS:GetDetectedAreas() - - local DetectedAreas = self.DetectedAreas - return DetectedAreas -end - ---- Get the amount of @{#DETECTION_AREAS.DetectedAreas}. --- @param #DETECTION_AREAS self --- @return #number DetectedAreaCount -function DETECTION_AREAS:GetDetectedAreaCount() - - local DetectedAreaCount = self.DetectedAreas.n - return DetectedAreaCount -end - ---- Get the @{Set#SET_UNIT} of a detecttion area using a given numeric index. --- @param #DETECTION_AREAS self --- @param #number Index --- @return Core.Set#SET_UNIT DetectedSet -function DETECTION_AREAS:GetDetectedSet( Index ) - - local DetectedSetUnit = self.DetectedAreas[Index].Set - if DetectedSetUnit then - return DetectedSetUnit - end - - return nil -end - ---- Get the @{Zone#ZONE_UNIT} of a detection area using a given numeric index. --- @param #DETECTION_AREAS self --- @param #number Index --- @return Core.Zone#ZONE_UNIT DetectedZone -function DETECTION_AREAS:GetDetectedZone( Index ) - - local DetectedZone = self.DetectedAreas[Index].Zone - if DetectedZone then - return DetectedZone - end - - return nil -end - ---- Background worker function to determine if there are friendlies nearby ... --- @param #DETECTION_AREAS self --- @param Wrapper.Unit#UNIT ReportUnit -function DETECTION_AREAS:ReportFriendliesNearBy( ReportGroupData ) - self:F2() - - local DetectedArea = ReportGroupData.DetectedArea -- Functional.Detection#DETECTION_AREAS.DetectedArea - local DetectedSet = ReportGroupData.DetectedArea.Set - local DetectedZone = ReportGroupData.DetectedArea.Zone - local DetectedZoneUnit = DetectedZone.ZoneUNIT - - DetectedArea.FriendliesNearBy = false - - local SphereSearch = { - id = world.VolumeType.SPHERE, - params = { - point = DetectedZoneUnit:GetVec3(), - radius = 6000, - } - - } - - --- @param Dcs.DCSWrapper.Unit#Unit FoundDCSUnit - -- @param Wrapper.Group#GROUP ReportGroup - -- @param Set#SET_GROUP ReportSetGroup - local FindNearByFriendlies = function( FoundDCSUnit, ReportGroupData ) - - local DetectedArea = ReportGroupData.DetectedArea -- Functional.Detection#DETECTION_AREAS.DetectedArea - local DetectedSet = ReportGroupData.DetectedArea.Set - local DetectedZone = ReportGroupData.DetectedArea.Zone - local DetectedZoneUnit = DetectedZone.ZoneUNIT -- Wrapper.Unit#UNIT - local ReportSetGroup = ReportGroupData.ReportSetGroup - - local EnemyCoalition = DetectedZoneUnit:GetCoalition() - - local FoundUnitCoalition = FoundDCSUnit:getCoalition() - local FoundUnitName = FoundDCSUnit:getName() - local FoundUnitGroupName = FoundDCSUnit:getGroup():getName() - local EnemyUnitName = DetectedZoneUnit:GetName() - local FoundUnitInReportSetGroup = ReportSetGroup:FindGroup( FoundUnitGroupName ) ~= nil - - self:T3( { "Friendlies search:", FoundUnitName, FoundUnitCoalition, EnemyUnitName, EnemyCoalition, FoundUnitInReportSetGroup } ) - - if FoundUnitCoalition ~= EnemyCoalition and FoundUnitInReportSetGroup == false then - DetectedArea.FriendliesNearBy = true - return false - end - - return true - end - - world.searchObjects( Object.Category.UNIT, SphereSearch, FindNearByFriendlies, ReportGroupData ) - -end - - - ---- Returns if there are friendlies nearby the FAC units ... --- @param #DETECTION_AREAS self --- @return #boolean trhe if there are friendlies nearby -function DETECTION_AREAS:IsFriendliesNearBy( DetectedArea ) - - self:T3( DetectedArea.FriendliesNearBy ) - return DetectedArea.FriendliesNearBy or false -end - ---- Calculate the maxium A2G threat level of the DetectedArea. --- @param #DETECTION_AREAS self --- @param #DETECTION_AREAS.DetectedArea DetectedArea -function DETECTION_AREAS:CalculateThreatLevelA2G( DetectedArea ) - - local MaxThreatLevelA2G = 0 - for UnitName, UnitData in pairs( DetectedArea.Set:GetSet() ) do - local ThreatUnit = UnitData -- Wrapper.Unit#UNIT - local ThreatLevelA2G = ThreatUnit:GetThreatLevel() - if ThreatLevelA2G > MaxThreatLevelA2G then - MaxThreatLevelA2G = ThreatLevelA2G - end - end - - self:T3( MaxThreatLevelA2G ) - DetectedArea.MaxThreatLevelA2G = MaxThreatLevelA2G - -end - ---- Find the nearest FAC of the DetectedArea. --- @param #DETECTION_AREAS self --- @param #DETECTION_AREAS.DetectedArea DetectedArea --- @return Wrapper.Unit#UNIT The nearest FAC unit -function DETECTION_AREAS:NearestFAC( DetectedArea ) - - local NearestFAC = nil - local MinDistance = 1000000000 -- Units are not further than 1000000 km away from an area :-) - - for FACGroupName, FACGroupData in pairs( self.DetectionSetGroup:GetSet() ) do - for FACUnit, FACUnitData in pairs( FACGroupData:GetUnits() ) do - local FACUnit = FACUnitData -- Wrapper.Unit#UNIT - if FACUnit:IsActive() then - local Vec3 = FACUnit:GetVec3() - local PointVec3 = POINT_VEC3:NewFromVec3( Vec3 ) - local Distance = PointVec3:Get2DDistance(POINT_VEC3:NewFromVec3( FACUnit:GetVec3() ) ) - if Distance < MinDistance then - MinDistance = Distance - NearestFAC = FACUnit - end - end - end - end - - DetectedArea.NearestFAC = NearestFAC - -end - ---- Returns the A2G threat level of the units in the DetectedArea --- @param #DETECTION_AREAS self --- @param #DETECTION_AREAS.DetectedArea DetectedArea --- @return #number a scale from 0 to 10. -function DETECTION_AREAS:GetTreatLevelA2G( DetectedArea ) - - self:T3( DetectedArea.MaxThreatLevelA2G ) - return DetectedArea.MaxThreatLevelA2G -end - - - ---- Smoke the detected units --- @param #DETECTION_AREAS self --- @return #DETECTION_AREAS self -function DETECTION_AREAS:SmokeDetectedUnits() - self:F2() - - self._SmokeDetectedUnits = true - return self -end - ---- Flare the detected units --- @param #DETECTION_AREAS self --- @return #DETECTION_AREAS self -function DETECTION_AREAS:FlareDetectedUnits() - self:F2() - - self._FlareDetectedUnits = true - return self -end - ---- Smoke the detected zones --- @param #DETECTION_AREAS self --- @return #DETECTION_AREAS self -function DETECTION_AREAS:SmokeDetectedZones() - self:F2() - - self._SmokeDetectedZones = true - return self -end - ---- Flare the detected zones --- @param #DETECTION_AREAS self --- @return #DETECTION_AREAS self -function DETECTION_AREAS:FlareDetectedZones() - self:F2() - - self._FlareDetectedZones = true - return self -end - ---- Add a change to the detected zone. --- @param #DETECTION_AREAS self --- @param #DETECTION_AREAS.DetectedArea DetectedArea --- @param #string ChangeCode --- @return #DETECTION_AREAS self -function DETECTION_AREAS:AddChangeArea( DetectedArea, ChangeCode, AreaUnitType ) - - DetectedArea.Changed = true - local AreaID = DetectedArea.AreaID - - DetectedArea.Changes = DetectedArea.Changes or {} - DetectedArea.Changes[ChangeCode] = DetectedArea.Changes[ChangeCode] or {} - DetectedArea.Changes[ChangeCode].AreaID = AreaID - DetectedArea.Changes[ChangeCode].AreaUnitType = AreaUnitType - - self:T( { "Change on Detection Area:", DetectedArea.AreaID, ChangeCode, AreaUnitType } ) - - return self -end - - ---- Add a change to the detected zone. --- @param #DETECTION_AREAS self --- @param #DETECTION_AREAS.DetectedArea DetectedArea --- @param #string ChangeCode --- @param #string ChangeUnitType --- @return #DETECTION_AREAS self -function DETECTION_AREAS:AddChangeUnit( DetectedArea, ChangeCode, ChangeUnitType ) - - DetectedArea.Changed = true - local AreaID = DetectedArea.AreaID - - DetectedArea.Changes = DetectedArea.Changes or {} - DetectedArea.Changes[ChangeCode] = DetectedArea.Changes[ChangeCode] or {} - DetectedArea.Changes[ChangeCode][ChangeUnitType] = DetectedArea.Changes[ChangeCode][ChangeUnitType] or 0 - DetectedArea.Changes[ChangeCode][ChangeUnitType] = DetectedArea.Changes[ChangeCode][ChangeUnitType] + 1 - DetectedArea.Changes[ChangeCode].AreaID = AreaID - - self:T( { "Change on Detection Area:", DetectedArea.AreaID, ChangeCode, ChangeUnitType } ) - - return self -end - ---- Make text documenting the changes of the detected zone. --- @param #DETECTION_AREAS self --- @param #DETECTION_AREAS.DetectedArea DetectedArea --- @return #string The Changes text -function DETECTION_AREAS:GetChangeText( DetectedArea ) - self:F( DetectedArea ) - - local MT = {} - - for ChangeCode, ChangeData in pairs( DetectedArea.Changes ) do - - if ChangeCode == "AA" then - MT[#MT+1] = "Detected new area " .. ChangeData.AreaID .. ". The center target is a " .. ChangeData.AreaUnitType .. "." - end - - if ChangeCode == "RAU" then - MT[#MT+1] = "Changed area " .. ChangeData.AreaID .. ". Removed the center target." - end - - if ChangeCode == "AAU" then - MT[#MT+1] = "Changed area " .. ChangeData.AreaID .. ". The new center target is a " .. ChangeData.AreaUnitType "." - end - - if ChangeCode == "RA" then - MT[#MT+1] = "Removed old area " .. ChangeData.AreaID .. ". No more targets in this area." - end - - if ChangeCode == "AU" then - local MTUT = {} - for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do - if ChangeUnitType ~= "AreaID" then - MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType - end - end - MT[#MT+1] = "Detected for area " .. ChangeData.AreaID .. " new target(s) " .. table.concat( MTUT, ", " ) .. "." - end - - if ChangeCode == "RU" then - local MTUT = {} - for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do - if ChangeUnitType ~= "AreaID" then - MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType - end - end - MT[#MT+1] = "Removed for area " .. ChangeData.AreaID .. " invisible or destroyed target(s) " .. table.concat( MTUT, ", " ) .. "." - end - - end - - return table.concat( MT, "\n" ) - -end - - ---- Accepts changes from the detected zone. --- @param #DETECTION_AREAS self --- @param #DETECTION_AREAS.DetectedArea DetectedArea --- @return #DETECTION_AREAS self -function DETECTION_AREAS:AcceptChanges( DetectedArea ) - - DetectedArea.Changed = false - DetectedArea.Changes = {} - - return self -end - - ---- Make a DetectionSet table. This function will be overridden in the derived clsses. --- @param #DETECTION_AREAS self --- @return #DETECTION_AREAS self -function DETECTION_AREAS:CreateDetectionSets() - self:F2() - - -- First go through all detected sets, and check if there are new detected units, match all existing detected units and identify undetected units. - -- Regroup when needed, split groups when needed. - for DetectedAreaID, DetectedAreaData in ipairs( self.DetectedAreas ) do - - local DetectedArea = DetectedAreaData -- #DETECTION_AREAS.DetectedArea - if DetectedArea then - - local DetectedSet = DetectedArea.Set - - local AreaExists = false -- This flag will determine of the detected area is still existing. - - -- First test if the center unit is detected in the detection area. - self:T3( DetectedArea.Zone.ZoneUNIT.UnitName ) - local DetectedZoneObject = self:GetDetectedObject( DetectedArea.Zone.ZoneUNIT.UnitName ) - self:T3( { "Detecting Zone Object", DetectedArea.AreaID, DetectedArea.Zone, DetectedZoneObject } ) - - if DetectedZoneObject then - - --self:IdentifyDetectedObject( DetectedZoneObject ) - AreaExists = true - - - - else - -- The center object of the detected area has not been detected. Find an other unit of the set to become the center of the area. - -- First remove the center unit from the set. - DetectedSet:RemoveUnitsByName( DetectedArea.Zone.ZoneUNIT.UnitName ) - - self:AddChangeArea( DetectedArea, 'RAU', "Dummy" ) - - -- Then search for a new center area unit within the set. Note that the new area unit candidate must be within the area range. - for DetectedUnitName, DetectedUnitData in pairs( DetectedSet:GetSet() ) do - - local DetectedUnit = DetectedUnitData -- Wrapper.Unit#UNIT - local DetectedObject = self:GetDetectedObject( DetectedUnit.UnitName ) - - -- The DetectedObject can be nil when the DetectedUnit is not alive anymore or it is not in the DetectedObjects map. - -- If the DetectedUnit was already identified, DetectedObject will be nil. - if DetectedObject then - self:IdentifyDetectedObject( DetectedObject ) - AreaExists = true - - -- Assign the Unit as the new center unit of the detected area. - DetectedArea.Zone = ZONE_UNIT:New( DetectedUnit:GetName(), DetectedUnit, self.DetectionZoneRange ) - - self:AddChangeArea( DetectedArea, "AAU", DetectedArea.Zone.ZoneUNIT:GetTypeName() ) - - -- We don't need to add the DetectedObject to the area set, because it is already there ... - break - end - end - end - - -- Now we've determined the center unit of the area, now we can iterate the units in the detected area. - -- Note that the position of the area may have moved due to the center unit repositioning. - -- If no center unit was identified, then the detected area does not exist anymore and should be deleted, as there are no valid units that can be the center unit. - if AreaExists then - - -- ok, we found the center unit of the area, now iterate through the detected area set and see which units are still within the center unit zone ... - -- Those units within the zone are flagged as Identified. - -- If a unit was not found in the set, remove it from the set. This may be added later to other existing or new sets. - for DetectedUnitName, DetectedUnitData in pairs( DetectedSet:GetSet() ) do - - local DetectedUnit = DetectedUnitData -- Wrapper.Unit#UNIT - local DetectedObject = nil - if DetectedUnit:IsAlive() then - --self:E(DetectedUnit:GetName()) - DetectedObject = self:GetDetectedObject( DetectedUnit:GetName() ) - end - if DetectedObject then - - -- Check if the DetectedUnit is within the DetectedArea.Zone - if DetectedUnit:IsInZone( DetectedArea.Zone ) then - - -- Yes, the DetectedUnit is within the DetectedArea.Zone, no changes, DetectedUnit can be kept within the Set. - self:IdentifyDetectedObject( DetectedObject ) - - else - -- No, the DetectedUnit is not within the DetectedArea.Zone, remove DetectedUnit from the Set. - DetectedSet:Remove( DetectedUnitName ) - self:AddChangeUnit( DetectedArea, "RU", DetectedUnit:GetTypeName() ) - end - - else - -- There was no DetectedObject, remove DetectedUnit from the Set. - self:AddChangeUnit( DetectedArea, "RU", "destroyed target" ) - DetectedSet:Remove( DetectedUnitName ) - - -- The DetectedObject has been identified, because it does not exist ... - -- self:IdentifyDetectedObject( DetectedObject ) - end - end - else - self:RemoveDetectedArea( DetectedAreaID ) - self:AddChangeArea( DetectedArea, "RA" ) - end - end - end - - -- We iterated through the existing detection areas and: - -- - We checked which units are still detected in each detection area. Those units were flagged as Identified. - -- - We recentered the detection area to new center units where it was needed. - -- - -- Now we need to loop through the unidentified detected units and see where they belong: - -- - They can be added to a new detection area and become the new center unit. - -- - They can be added to a new detection area. - for DetectedUnitName, DetectedObjectData in pairs( self.DetectedObjects ) do - - local DetectedObject = self:GetDetectedObject( DetectedUnitName ) - - if DetectedObject then - - -- We found an unidentified unit outside of any existing detection area. - local DetectedUnit = UNIT:FindByName( DetectedUnitName ) -- Wrapper.Unit#UNIT - - local AddedToDetectionArea = false - - for DetectedAreaID, DetectedAreaData in ipairs( self.DetectedAreas ) do - - local DetectedArea = DetectedAreaData -- #DETECTION_AREAS.DetectedArea - if DetectedArea then - self:T( "Detection Area #" .. DetectedArea.AreaID ) - local DetectedSet = DetectedArea.Set - if not self:IsDetectedObjectIdentified( DetectedObject ) and DetectedUnit:IsInZone( DetectedArea.Zone ) then - self:IdentifyDetectedObject( DetectedObject ) - DetectedSet:AddUnit( DetectedUnit ) - AddedToDetectionArea = true - self:AddChangeUnit( DetectedArea, "AU", DetectedUnit:GetTypeName() ) - end - end - end - - if AddedToDetectionArea == false then - - -- New detection area - local DetectedArea = self:AddDetectedArea( - SET_UNIT:New(), - ZONE_UNIT:New( DetectedUnitName, DetectedUnit, self.DetectionZoneRange ) - ) - --self:E( DetectedArea.Zone.ZoneUNIT.UnitName ) - DetectedArea.Set:AddUnit( DetectedUnit ) - self:AddChangeArea( DetectedArea, "AA", DetectedUnit:GetTypeName() ) - end - end - end - - -- Now all the tests should have been build, now make some smoke and flares... - -- We also report here the friendlies within the detected areas. - - for DetectedAreaID, DetectedAreaData in ipairs( self.DetectedAreas ) do - - local DetectedArea = DetectedAreaData -- #DETECTION_AREAS.DetectedArea - local DetectedSet = DetectedArea.Set - local DetectedZone = DetectedArea.Zone - - self:ReportFriendliesNearBy( { DetectedArea = DetectedArea, ReportSetGroup = self.DetectionSetGroup } ) -- Fill the Friendlies table - self:CalculateThreatLevelA2G( DetectedArea ) -- Calculate A2G threat level - self:NearestFAC( DetectedArea ) - - if DETECTION_AREAS._SmokeDetectedUnits or self._SmokeDetectedUnits then - DetectedZone.ZoneUNIT:SmokeRed() - end - DetectedSet:ForEachUnit( - --- @param Wrapper.Unit#UNIT DetectedUnit - function( DetectedUnit ) - if DetectedUnit:IsAlive() then - self:T( "Detected Set #" .. DetectedArea.AreaID .. ":" .. DetectedUnit:GetName() ) - if DETECTION_AREAS._FlareDetectedUnits or self._FlareDetectedUnits then - DetectedUnit:FlareGreen() - end - if DETECTION_AREAS._SmokeDetectedUnits or self._SmokeDetectedUnits then - DetectedUnit:SmokeGreen() - end - end - end - ) - if DETECTION_AREAS._FlareDetectedZones or self._FlareDetectedZones then - DetectedZone:FlareZone( SMOKECOLOR.White, 30, math.random( 0,90 ) ) - end - if DETECTION_AREAS._SmokeDetectedZones or self._SmokeDetectedZones then - DetectedZone:SmokeZone( SMOKECOLOR.White, 30 ) - end - end - -end - - ---- Single-Player:**No** / Multi-Player:**Yes** / AI:**Yes** / Human:**No** / Types:**All** -- **AI Balancing will replace in multi player missions --- non-occupied human slots with AI groups, in order to provide an engaging simulation environment, --- even when there are hardly any players in the mission.** --- --- ![Banner Image](..\Presentations\AI_Balancer\Dia1.JPG) --- --- === --- --- # 1) @{AI_Balancer#AI_BALANCER} class, extends @{Fsm#FSM_SET} --- --- The @{AI_Balancer#AI_BALANCER} class monitors and manages as many replacement AI groups as there are --- CLIENTS in a SET_CLIENT collection, which are not occupied by human players. --- In other words, use AI_BALANCER to simulate human behaviour by spawning in replacement AI in multi player missions. --- --- The parent class @{Fsm#FSM_SET} manages the functionality to control the Finite State Machine (FSM). --- The mission designer can tailor the behaviour of the AI_BALANCER, by defining event and state transition methods. --- An explanation about state and event transition methods can be found in the @{FSM} module documentation. --- --- The mission designer can tailor the AI_BALANCER behaviour, by implementing a state or event handling method for the following: --- --- * **@{#AI_BALANCER.OnAfterSpawned}**( AISet, From, Event, To, AIGroup ): Define to add extra logic when an AI is spawned. --- --- ## 1.1) AI_BALANCER construction --- --- Create a new AI_BALANCER object with the @{#AI_BALANCER.New}() method: --- --- ## 1.2) AI_BALANCER is a FSM --- --- ![Process](..\Presentations\AI_Balancer\Dia13.JPG) --- --- ### 1.2.1) AI_BALANCER States --- --- * **Monitoring** ( Set ): Monitoring the Set if all AI is spawned for the Clients. --- * **Spawning** ( Set, ClientName ): There is a new AI group spawned with ClientName as the name of reference. --- * **Spawned** ( Set, AIGroup ): A new AI has been spawned. You can handle this event to customize the AI behaviour with other AI FSMs or own processes. --- * **Destroying** ( Set, AIGroup ): The AI is being destroyed. --- * **Returning** ( Set, AIGroup ): The AI is returning to the airbase specified by the ReturnToAirbase methods. Handle this state to customize the return behaviour of the AI, if any. --- --- ### 1.2.2) AI_BALANCER Events --- --- * **Monitor** ( Set ): Every 10 seconds, the Monitor event is triggered to monitor the Set. --- * **Spawn** ( Set, ClientName ): Triggers when there is a new AI group to be spawned with ClientName as the name of reference. --- * **Spawned** ( Set, AIGroup ): Triggers when a new AI has been spawned. You can handle this event to customize the AI behaviour with other AI FSMs or own processes. --- * **Destroy** ( Set, AIGroup ): The AI is being destroyed. --- * **Return** ( Set, AIGroup ): The AI is returning to the airbase specified by the ReturnToAirbase methods. --- --- ## 1.3) AI_BALANCER spawn interval for replacement AI --- --- Use the method @{#AI_BALANCER.InitSpawnInterval}() to set the earliest and latest interval in seconds that is waited until a new replacement AI is spawned. --- --- ## 1.4) AI_BALANCER returns AI to Airbases --- --- By default, When a human player joins a slot that is AI_BALANCED, the AI group will be destroyed by default. --- However, there are 2 additional options that you can use to customize the destroy behaviour. --- When a human player joins a slot, you can configure to let the AI return to: --- --- * @{#AI_BALANCER.ReturnToHomeAirbase}: Returns the AI to the **home** @{Airbase#AIRBASE}. --- * @{#AI_BALANCER.ReturnToNearestAirbases}: Returns the AI to the **nearest friendly** @{Airbase#AIRBASE}. --- --- Note that when AI returns to an airbase, the AI_BALANCER will trigger the **Return** event and the AI will return, --- otherwise the AI_BALANCER will trigger a **Destroy** event, and the AI will be destroyed. --- --- === --- --- # **API CHANGE HISTORY** --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- Hereby the change log: --- --- 2017-01-17: There is still a problem with AI being destroyed, but not respawned. Need to check further upon that. --- --- 2017-01-08: AI_BALANCER:**InitSpawnInterval( Earliest, Latest )** added. --- --- === --- --- # **AUTHORS and CONTRIBUTIONS** --- --- ### Contributions: --- --- * **[Dutch_Baron](https://forums.eagle.ru/member.php?u=112075)**: Working together with James has resulted in the creation of the AI_BALANCER class. James has shared his ideas on balancing AI with air units, and together we made a first design which you can use now :-) --- * **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 AI_BALANCER moose class. --- --- ### Authors: --- --- * FlightControl: Framework Design & Programming and Documentation. --- --- @module AI_Balancer - ---- AI_BALANCER class --- @type AI_BALANCER --- @field Core.Set#SET_CLIENT SetClient --- @field Functional.Spawn#SPAWN SpawnAI --- @field Wrapper.Group#GROUP Test --- @extends Core.Fsm#FSM_SET -AI_BALANCER = { - ClassName = "AI_BALANCER", - PatrolZones = {}, - AIGroups = {}, - Earliest = 5, -- Earliest a new AI can be spawned is in 5 seconds. - Latest = 60, -- Latest a new AI can be spawned is in 60 seconds. -} - - - ---- Creates a new AI_BALANCER object --- @param #AI_BALANCER self --- @param Core.Set#SET_CLIENT SetClient A SET\_CLIENT object that will contain the CLIENT objects to be monitored if they are alive or not (joined by a player). --- @param Functional.Spawn#SPAWN SpawnAI The default Spawn object to spawn new AI Groups when needed. --- @return #AI_BALANCER -function AI_BALANCER:New( SetClient, SpawnAI ) - - -- Inherits from BASE - local self = BASE:Inherit( self, FSM_SET:New( SET_GROUP:New() ) ) -- AI.AI_Balancer#AI_BALANCER - - -- TODO: Define the OnAfterSpawned event - self:SetStartState( "None" ) - self:AddTransition( "*", "Monitor", "Monitoring" ) - self:AddTransition( "*", "Spawn", "Spawning" ) - self:AddTransition( "Spawning", "Spawned", "Spawned" ) - self:AddTransition( "*", "Destroy", "Destroying" ) - self:AddTransition( "*", "Return", "Returning" ) - - self.SetClient = SetClient - self.SetClient:FilterOnce() - self.SpawnAI = SpawnAI - - self.SpawnQueue = {} - - self.ToNearestAirbase = false - self.ToHomeAirbase = false - - self:__Monitor( 1 ) - - return self -end - ---- Sets the earliest to the latest interval in seconds how long AI_BALANCER will wait to spawn a new AI. --- Provide 2 identical seconds if the interval should be a fixed amount of seconds. --- @param #AI_BALANCER self --- @param #number Earliest The earliest a new AI can be spawned in seconds. --- @param #number Latest The latest a new AI can be spawned in seconds. --- @return self -function AI_BALANCER:InitSpawnInterval( Earliest, Latest ) - - self.Earliest = Earliest - self.Latest = Latest - - return self -end - ---- Returns the AI to the nearest friendly @{Airbase#AIRBASE}. --- @param #AI_BALANCER self --- @param Dcs.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 Core.Set#SET_AIRBASE ReturnAirbaseSet The SET of @{Set#SET_AIRBASE}s to evaluate where to return to. -function AI_BALANCER:ReturnToNearestAirbases( ReturnTresholdRange, ReturnAirbaseSet ) - - self.ToNearestAirbase = true - self.ReturnTresholdRange = ReturnTresholdRange - self.ReturnAirbaseSet = ReturnAirbaseSet -end - ---- Returns the AI to the home @{Airbase#AIRBASE}. --- @param #AI_BALANCER self --- @param Dcs.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 AI_BALANCER:ReturnToHomeAirbase( ReturnTresholdRange ) - - self.ToHomeAirbase = true - self.ReturnTresholdRange = ReturnTresholdRange -end - ---- @param #AI_BALANCER self --- @param Core.Set#SET_GROUP SetGroup --- @param #string ClientName --- @param Wrapper.Group#GROUP AIGroup -function AI_BALANCER:onenterSpawning( SetGroup, From, Event, To, ClientName ) - - -- OK, Spawn a new group from the default SpawnAI object provided. - local AIGroup = self.SpawnAI:Spawn() -- Wrapper.Group#GROUP - if AIGroup then - AIGroup:E( "Spawning new AIGroup" ) - --TODO: need to rework UnitName thing ... - - SetGroup:Add( ClientName, AIGroup ) - self.SpawnQueue[ClientName] = nil - - -- Fire the Spawned event. The first parameter is the AIGroup just Spawned. - -- Mission designers can catch this event to bind further actions to the AIGroup. - self:Spawned( AIGroup ) - end -end - ---- @param #AI_BALANCER self --- @param Core.Set#SET_GROUP SetGroup --- @param Wrapper.Group#GROUP AIGroup -function AI_BALANCER:onenterDestroying( SetGroup, From, Event, To, ClientName, AIGroup ) - - AIGroup:Destroy() - SetGroup:Flush() - SetGroup:Remove( ClientName ) - SetGroup:Flush() -end - ---- @param #AI_BALANCER self --- @param Core.Set#SET_GROUP SetGroup --- @param Wrapper.Group#GROUP AIGroup -function AI_BALANCER:onenterReturning( SetGroup, From, Event, To, AIGroup ) - - local AIGroupTemplate = AIGroup:GetTemplate() - if self.ToHomeAirbase == true then - local WayPointCount = #AIGroupTemplate.route.points - local SwitchWayPointCommand = AIGroup:CommandSwitchWayPoint( 1, WayPointCount, 1 ) - AIGroup:SetCommand( SwitchWayPointCommand ) - AIGroup:MessageToRed( "Returning to home base ...", 30 ) - else - -- Okay, we need to send this Group back to the nearest base of the Coalition of the AI. - --TODO: i need to rework the POINT_VEC2 thing. - local PointVec2 = POINT_VEC2:New( AIGroup:GetVec2().x, AIGroup:GetVec2().y ) - local ClosestAirbase = self.ReturnAirbaseSet:FindNearestAirbaseFromPointVec2( PointVec2 ) - self:T( ClosestAirbase.AirbaseName ) - AIGroup:MessageToRed( "Returning to " .. ClosestAirbase:GetName().. " ...", 30 ) - local RTBRoute = AIGroup:RouteReturnToAirbase( ClosestAirbase ) - AIGroupTemplate.route = RTBRoute - AIGroup:Respawn( AIGroupTemplate ) - end - -end - - ---- @param #AI_BALANCER self -function AI_BALANCER:onenterMonitoring( SetGroup ) - - self:T2( { self.SetClient:Count() } ) - --self.SetClient:Flush() - - self.SetClient:ForEachClient( - --- @param Wrapper.Client#CLIENT Client - function( Client ) - self:T3(Client.ClientName) - - local AIGroup = self.Set:Get( Client.UnitName ) -- Wrapper.Group#GROUP - if Client:IsAlive() then - - if AIGroup and AIGroup:IsAlive() == true then - - if self.ToNearestAirbase == false and self.ToHomeAirbase == false then - self:Destroy( Client.UnitName, AIGroup ) - else - -- We test if there is no other CLIENT within the self.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:GetVec2(), self.ReturnTresholdRange ) - - self:T2( RangeZone ) - - _DATABASE:ForEachPlayer( - --- @param Wrapper.Unit#UNIT RangeTestUnit - function( RangeTestUnit, RangeZone, AIGroup, PlayerInRange ) - self:T2( { PlayerInRange, RangeTestUnit.UnitName, RangeZone.ZoneName } ) - if RangeTestUnit:IsInZone( RangeZone ) == true then - self:T2( "in zone" ) - if RangeTestUnit:GetCoalition() ~= AIGroup:GetCoalition() then - self:T2( "in range" ) - PlayerInRange.Value = true - end - end - end, - - --- @param Core.Zone#ZONE_RADIUS RangeZone - -- @param Wrapper.Group#GROUP AIGroup - function( RangeZone, AIGroup, PlayerInRange ) - if PlayerInRange.Value == false then - self:Return( AIGroup ) - end - end - , RangeZone, AIGroup, PlayerInRange - ) - - end - self.Set:Remove( Client.UnitName ) - end - else - if not AIGroup or not AIGroup:IsAlive() == true then - self:T( "Client " .. Client.UnitName .. " not alive." ) - if not self.SpawnQueue[Client.UnitName] then - -- Spawn a new AI taking into account the spawn interval Earliest, Latest - self:__Spawn( math.random( self.Earliest, self.Latest ), Client.UnitName ) - self.SpawnQueue[Client.UnitName] = true - self:E( "New AI Spawned for Client " .. Client.UnitName ) - end - end - end - return true - end - ) - - self:__Monitor( 10 ) -end - - - ---- Single-Player:**Yes** / Multi-Player:**Yes** / AI:**Yes** / Human:**No** / Types:**Air** -- --- **Air Patrolling or Staging.** --- --- ![Banner Image](..\Presentations\AI_PATROL\Dia1.JPG) --- --- === --- --- # 1) @{#AI_PATROL_ZONE} class, extends @{Fsm#FSM_CONTROLLABLE} --- --- The @{#AI_PATROL_ZONE} class implements the core functions to patrol a @{Zone} by an AI @{Controllable} or @{Group}. --- --- ![Process](..\Presentations\AI_PATROL\Dia3.JPG) --- --- The AI_PATROL_ZONE is assigned a @{Group} and this must be done before the AI_PATROL_ZONE process can be started using the **Start** event. --- --- ![Process](..\Presentations\AI_PATROL\Dia4.JPG) --- --- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits. --- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. --- --- ![Process](..\Presentations\AI_PATROL\Dia5.JPG) --- --- This cycle will continue. --- --- ![Process](..\Presentations\AI_PATROL\Dia6.JPG) --- --- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event. --- --- ![Process](..\Presentations\AI_PATROL\Dia9.JPG) --- ----- Note that the enemy is not engaged! To model enemy engagement, either tailor the **Detected** event, or --- use derived AI_ classes to model AI offensive or defensive behaviour. --- --- ![Process](..\Presentations\AI_PATROL\Dia10.JPG) --- --- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. --- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. --- --- ![Process](..\Presentations\AI_PATROL\Dia11.JPG) --- --- ## 1.1) AI_PATROL_ZONE constructor --- --- * @{#AI_PATROL_ZONE.New}(): Creates a new AI_PATROL_ZONE object. --- --- ## 1.2) AI_PATROL_ZONE is a FSM --- --- ![Process](..\Presentations\AI_PATROL\Dia2.JPG) --- --- ### 1.2.1) AI_PATROL_ZONE States --- --- * **None** ( Group ): The process is not started yet. --- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. --- * **Returning** ( Group ): The AI is returning to Base.. --- --- ### 1.2.2) AI_PATROL_ZONE Events --- --- * **Start** ( Group ): Start the process. --- * **Route** ( Group ): Route the AI to a new random 3D point within the Patrol Zone. --- * **RTB** ( Group ): Route the AI to the home base. --- * **Detect** ( Group ): The AI is detecting targets. --- * **Detected** ( Group ): The AI has detected new targets. --- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. --- --- ## 1.3) Set or Get the AI controllable --- --- * @{#AI_PATROL_ZONE.SetControllable}(): Set the AIControllable. --- * @{#AI_PATROL_ZONE.GetControllable}(): Get the AIControllable. --- --- ## 1.4) Set the Speed and Altitude boundaries of the AI controllable --- --- * @{#AI_PATROL_ZONE.SetSpeed}(): Set the patrol speed boundaries of the AI, for the next patrol. --- * @{#AI_PATROL_ZONE.SetAltitude}(): Set altitude boundaries of the AI, for the next patrol. --- --- ## 1.5) Manage the detection process of the AI controllable --- --- The detection process of the AI controllable can be manipulated. --- Detection requires an amount of CPU power, which has an impact on your mission performance. --- Only put detection on when absolutely necessary, and the frequency of the detection can also be set. --- --- * @{#AI_PATROL_ZONE.SetDetectionOn}(): Set the detection on. The AI will detect for targets. --- * @{#AI_PATROL_ZONE.SetDetectionOff}(): Set the detection off, the AI will not detect for targets. The existing target list will NOT be erased. --- --- The detection frequency can be set with @{#AI_PATROL_ZONE.SetDetectionInterval}( seconds ), where the amount of seconds specify how much seconds will be waited before the next detection. --- Use the method @{#AI_PATROL_ZONE.GetDetectedUnits}() to obtain a list of the @{Unit}s detected by the AI. --- --- The detection can be filtered to potential targets in a specific zone. --- Use the method @{#AI_PATROL_ZONE.SetDetectionZone}() to set the zone where targets need to be detected. --- Note that when the zone is too far away, or the AI is not heading towards the zone, or the AI is too high, no targets may be detected --- according the weather conditions. --- --- ## 1.6) Manage the "out of fuel" in the AI_PATROL_ZONE --- --- When the AI is out of fuel, it is required that a new AI is started, before the old AI can return to the home base. --- Therefore, with a parameter and a calculation of the distance to the home base, the fuel treshold is calculated. --- When the fuel treshold is reached, the AI will continue for a given time its patrol task in orbit, --- while a new AI is targetted to the AI_PATROL_ZONE. --- Once the time is finished, the old AI will return to the base. --- Use the method @{#AI_PATROL_ZONE.ManageFuel}() to have this proces in place. --- --- ## 1.7) Manage "damage" behaviour of the AI in the AI_PATROL_ZONE --- --- When the AI is damaged, it is required that a new AIControllable is started. However, damage cannon be foreseen early on. --- Therefore, when the damage treshold is reached, the AI will return immediately to the home base (RTB). --- Use the method @{#AI_PATROL_ZONE.ManageDamage}() to have this proces in place. --- --- ==== --- --- # **OPEN ISSUES** --- --- 2017-01-17: When Spawned AI is located at an airbase, it will be routed first back to the airbase after take-off. --- --- 2016-01-17: --- -- Fixed problem with AI returning to base too early and unexpected. --- -- ReSpawning of AI will reset the AI_PATROL and derived classes. --- -- Checked the correct workings of SCHEDULER, and it DOES work correctly. --- --- ==== --- --- # **API CHANGE HISTORY** --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- Hereby the change log: --- --- 2017-01-17: Rename of class: **AI\_PATROL\_ZONE** is the new name for the old _AI\_PATROLZONE_. --- --- 2017-01-15: Complete revision. AI_PATROL_ZONE is the base class for other AI_PATROL like classes. --- --- 2016-09-01: Initial class and API. --- --- === --- --- # **AUTHORS and CONTRIBUTIONS** --- --- ### Contributions: --- --- * **[Dutch_Baron](https://forums.eagle.ru/member.php?u=112075)**: Working together with James has resulted in the creation of the AI_BALANCER class. James has shared his ideas on balancing AI with air units, and together we made a first design which you can use now :-) --- * **[Pikey](https://forums.eagle.ru/member.php?u=62835)**: Testing and API concept review. --- --- ### Authors: --- --- * **FlightControl**: Design & Programming. --- --- @module AI_Patrol - ---- AI_PATROL_ZONE class --- @type AI_PATROL_ZONE --- @field Wrapper.Controllable#CONTROLLABLE AIControllable The @{Controllable} patrolling. --- @field Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. --- @field Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @field Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @field Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. --- @field Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h. --- @field Functional.Spawn#SPAWN CoordTest --- @extends Core.Fsm#FSM_CONTROLLABLE -AI_PATROL_ZONE = { - ClassName = "AI_PATROL_ZONE", -} - ---- Creates a new AI_PATROL_ZONE object --- @param #AI_PATROL_ZONE self --- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. --- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. --- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h. --- @param Dcs.DCSTypes#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO --- @return #AI_PATROL_ZONE self --- @usage --- -- Define a new AI_PATROL_ZONE Object. This PatrolArea will patrol an AIControllable within PatrolZone between 3000 and 6000 meters, with a variying speed between 600 and 900 km/h. --- PatrolZone = ZONE:New( 'PatrolZone' ) --- PatrolSpawn = SPAWN:New( 'Patrol Group' ) --- PatrolArea = AI_PATROL_ZONE:New( PatrolZone, 3000, 6000, 600, 900 ) -function AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) - - -- Inherits from BASE - local self = BASE:Inherit( self, FSM_CONTROLLABLE:New() ) -- #AI_PATROL_ZONE - - - self.PatrolZone = PatrolZone - self.PatrolFloorAltitude = PatrolFloorAltitude - self.PatrolCeilingAltitude = PatrolCeilingAltitude - self.PatrolMinSpeed = PatrolMinSpeed - self.PatrolMaxSpeed = PatrolMaxSpeed - - -- defafult PatrolAltType to "RADIO" if not specified - self.PatrolAltType = PatrolAltType or "RADIO" - - self:SetDetectionInterval( 30 ) - - self.CheckStatus = true - - self:ManageFuel( .2, 60 ) - self:ManageDamage( 1 ) - - - self.DetectedUnits = {} -- This table contains the targets detected during patrol. - - self:SetStartState( "None" ) - - self:AddTransition( "None", "Start", "Patrolling" ) - ---- OnBefore Transition Handler for Event Start. --- @function [parent=#AI_PATROL_ZONE] OnBeforeStart --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event Start. --- @function [parent=#AI_PATROL_ZONE] OnAfterStart --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event Start. --- @function [parent=#AI_PATROL_ZONE] Start --- @param #AI_PATROL_ZONE self - ---- Asynchronous Event Trigger for Event Start. --- @function [parent=#AI_PATROL_ZONE] __Start --- @param #AI_PATROL_ZONE self --- @param #number Delay The delay in seconds. - ---- OnLeave Transition Handler for State Patrolling. --- @function [parent=#AI_PATROL_ZONE] OnLeavePatrolling --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnEnter Transition Handler for State Patrolling. --- @function [parent=#AI_PATROL_ZONE] OnEnterPatrolling --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - - self:AddTransition( "Patrolling", "Route", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. - ---- OnBefore Transition Handler for Event Route. --- @function [parent=#AI_PATROL_ZONE] OnBeforeRoute --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event Route. --- @function [parent=#AI_PATROL_ZONE] OnAfterRoute --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event Route. --- @function [parent=#AI_PATROL_ZONE] Route --- @param #AI_PATROL_ZONE self - ---- Asynchronous Event Trigger for Event Route. --- @function [parent=#AI_PATROL_ZONE] __Route --- @param #AI_PATROL_ZONE self --- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "Status", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. - ---- OnBefore Transition Handler for Event Status. --- @function [parent=#AI_PATROL_ZONE] OnBeforeStatus --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event Status. --- @function [parent=#AI_PATROL_ZONE] OnAfterStatus --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event Status. --- @function [parent=#AI_PATROL_ZONE] Status --- @param #AI_PATROL_ZONE self - ---- Asynchronous Event Trigger for Event Status. --- @function [parent=#AI_PATROL_ZONE] __Status --- @param #AI_PATROL_ZONE self --- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "Detect", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. - ---- OnBefore Transition Handler for Event Detect. --- @function [parent=#AI_PATROL_ZONE] OnBeforeDetect --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event Detect. --- @function [parent=#AI_PATROL_ZONE] OnAfterDetect --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event Detect. --- @function [parent=#AI_PATROL_ZONE] Detect --- @param #AI_PATROL_ZONE self - ---- Asynchronous Event Trigger for Event Detect. --- @function [parent=#AI_PATROL_ZONE] __Detect --- @param #AI_PATROL_ZONE self --- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "Detected", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. - ---- OnBefore Transition Handler for Event Detected. --- @function [parent=#AI_PATROL_ZONE] OnBeforeDetected --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event Detected. --- @function [parent=#AI_PATROL_ZONE] OnAfterDetected --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event Detected. --- @function [parent=#AI_PATROL_ZONE] Detected --- @param #AI_PATROL_ZONE self - ---- Asynchronous Event Trigger for Event Detected. --- @function [parent=#AI_PATROL_ZONE] __Detected --- @param #AI_PATROL_ZONE self --- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "RTB", "Returning" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. - ---- OnBefore Transition Handler for Event RTB. --- @function [parent=#AI_PATROL_ZONE] OnBeforeRTB --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event RTB. --- @function [parent=#AI_PATROL_ZONE] OnAfterRTB --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event RTB. --- @function [parent=#AI_PATROL_ZONE] RTB --- @param #AI_PATROL_ZONE self - ---- Asynchronous Event Trigger for Event RTB. --- @function [parent=#AI_PATROL_ZONE] __RTB --- @param #AI_PATROL_ZONE self --- @param #number Delay The delay in seconds. - ---- OnLeave Transition Handler for State Returning. --- @function [parent=#AI_PATROL_ZONE] OnLeaveReturning --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnEnter Transition Handler for State Returning. --- @function [parent=#AI_PATROL_ZONE] OnEnterReturning --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - - self:AddTransition( "*", "Reset", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. - - self:AddTransition( "*", "Eject", "*" ) - self:AddTransition( "*", "Crash", "Crashed" ) - self:AddTransition( "*", "PilotDead", "*" ) - - return self -end - - - - ---- Sets (modifies) the minimum and maximum speed of the patrol. --- @param #AI_PATROL_ZONE self --- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. --- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h. --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetSpeed( PatrolMinSpeed, PatrolMaxSpeed ) - self:F2( { PatrolMinSpeed, PatrolMaxSpeed } ) - - self.PatrolMinSpeed = PatrolMinSpeed - self.PatrolMaxSpeed = PatrolMaxSpeed -end - - - ---- Sets the floor and ceiling altitude of the patrol. --- @param #AI_PATROL_ZONE self --- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetAltitude( PatrolFloorAltitude, PatrolCeilingAltitude ) - self:F2( { PatrolFloorAltitude, PatrolCeilingAltitude } ) - - self.PatrolFloorAltitude = PatrolFloorAltitude - self.PatrolCeilingAltitude = PatrolCeilingAltitude -end - --- * @{#AI_PATROL_ZONE.SetDetectionOn}(): Set the detection on. The AI will detect for targets. --- * @{#AI_PATROL_ZONE.SetDetectionOff}(): Set the detection off, the AI will not detect for targets. The existing target list will NOT be erased. - ---- Set the detection on. The AI will detect for targets. --- @param #AI_PATROL_ZONE self --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetDetectionOn() - self:F2() - - self.DetectOn = true -end - ---- Set the detection off. The AI will NOT detect for targets. --- However, the list of already detected targets will be kept and can be enquired! --- @param #AI_PATROL_ZONE self --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetDetectionOff() - self:F2() - - self.DetectOn = false -end - ---- Set the status checking off. --- @param #AI_PATROL_ZONE self --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetStatusOff() - self:F2() - - self.CheckStatus = false -end - ---- Activate the detection. The AI will detect for targets if the Detection is switched On. --- @param #AI_PATROL_ZONE self --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetDetectionActivated() - self:F2() - - self:ClearDetectedUnits() - self.DetectActivated = true - self:__Detect( -self.DetectInterval ) -end - ---- Deactivate the detection. The AI will NOT detect for targets. --- @param #AI_PATROL_ZONE self --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetDetectionDeactivated() - self:F2() - - self:ClearDetectedUnits() - self.DetectActivated = false -end - ---- Set the interval in seconds between each detection executed by the AI. --- The list of already detected targets will be kept and updated. --- Newly detected targets will be added, but already detected targets that were --- not detected in this cycle, will NOT be removed! --- The default interval is 30 seconds. --- @param #AI_PATROL_ZONE self --- @param #number Seconds The interval in seconds. --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetDetectionInterval( Seconds ) - self:F2() - - if Seconds then - self.DetectInterval = Seconds - else - self.DetectInterval = 30 - end -end - ---- Set the detection zone where the AI is detecting targets. --- @param #AI_PATROL_ZONE self --- @param Core.Zone#ZONE DetectionZone The zone where to detect targets. --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetDetectionZone( DetectionZone ) - self:F2() - - if DetectionZone then - self.DetectZone = DetectionZone - else - self.DetectZone = nil - end -end - ---- Gets a list of @{Unit#UNIT}s that were detected by the AI. --- No filtering is applied, so, ANY detected UNIT can be in this list. --- It is up to the mission designer to use the @{Unit} class and methods to filter the targets. --- @param #AI_PATROL_ZONE self --- @return #table The list of @{Unit#UNIT}s -function AI_PATROL_ZONE:GetDetectedUnits() - self:F2() - - return self.DetectedUnits -end - ---- Clears the list of @{Unit#UNIT}s that were detected by the AI. --- @param #AI_PATROL_ZONE self -function AI_PATROL_ZONE:ClearDetectedUnits() - self:F2() - self.DetectedUnits = {} -end - ---- When the AI is out of fuel, it is required that a new AI is started, before the old AI can return to the home base. --- Therefore, with a parameter and a calculation of the distance to the home base, the fuel treshold is calculated. --- When the fuel treshold is reached, the AI will continue for a given time its patrol task in orbit, while a new AIControllable is targetted to the AI_PATROL_ZONE. --- Once the time is finished, the old AI will return to the base. --- @param #AI_PATROL_ZONE self --- @param #number PatrolFuelTresholdPercentage The treshold in percentage (between 0 and 1) when the AIControllable is considered to get out of fuel. --- @param #number PatrolOutOfFuelOrbitTime The amount of seconds the out of fuel AIControllable will orbit before returning to the base. --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:ManageFuel( PatrolFuelTresholdPercentage, PatrolOutOfFuelOrbitTime ) - - self.PatrolManageFuel = true - self.PatrolFuelTresholdPercentage = PatrolFuelTresholdPercentage - self.PatrolOutOfFuelOrbitTime = PatrolOutOfFuelOrbitTime - - return self -end - ---- When the AI is damaged beyond a certain treshold, it is required that the AI returns to the home base. --- However, damage cannot be foreseen early on. --- Therefore, when the damage treshold is reached, --- the AI will return immediately to the home base (RTB). --- Note that for groups, the average damage of the complete group will be calculated. --- So, in a group of 4 airplanes, 2 lost and 2 with damage 0.2, the damage treshold will be 0.25. --- @param #AI_PATROL_ZONE self --- @param #number PatrolDamageTreshold The treshold in percentage (between 0 and 1) when the AI is considered to be damaged. --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:ManageDamage( PatrolDamageTreshold ) - - self.PatrolManageDamage = true - self.PatrolDamageTreshold = PatrolDamageTreshold - - return self -end - ---- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. --- @param #AI_PATROL_ZONE self --- @return #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_PATROL_ZONE:onafterStart( Controllable, From, Event, To ) - self:F2() - - self:__Route( 1 ) -- Route to the patrol point. The asynchronous trigger is important, because a spawned group and units takes at least one second to come live. - self:__Status( 60 ) -- Check status status every 30 seconds. - self:SetDetectionActivated() - - self:HandleEvent( EVENTS.PilotDead, self.OnPilotDead ) - self:HandleEvent( EVENTS.Crash, self.OnCrash ) - self:HandleEvent( EVENTS.Ejection, self.OnEjection ) - - Controllable:OptionROEHoldFire() - Controllable:OptionROTVertical() - - self.Controllable:OnReSpawn( - function( PatrolGroup ) - self:E( "ReSpawn" ) - self:__Reset( 1 ) - self:__Route( 5 ) - end - ) - - self:SetDetectionOn() - -end - - ---- @param #AI_PATROL_ZONE self ---- @param Wrapper.Controllable#CONTROLLABLE Controllable -function AI_PATROL_ZONE:onbeforeDetect( Controllable, From, Event, To ) - - return self.DetectOn and self.DetectActivated -end - ---- @param #AI_PATROL_ZONE self ---- @param Wrapper.Controllable#CONTROLLABLE Controllable -function AI_PATROL_ZONE:onafterDetect( Controllable, From, Event, To ) - - local Detected = false - - local DetectedTargets = Controllable:GetDetectedTargets() - for TargetID, Target in pairs( DetectedTargets or {} ) do - local TargetObject = Target.object - - if TargetObject and TargetObject:isExist() and TargetObject.id_ < 50000000 then - - local TargetUnit = UNIT:Find( TargetObject ) - local TargetUnitName = TargetUnit:GetName() - - if self.DetectionZone then - if TargetUnit:IsInZone( self.DetectionZone ) then - self:T( {"Detected ", TargetUnit } ) - if self.DetectedUnits[TargetUnit] == nil then - self.DetectedUnits[TargetUnit] = true - end - Detected = true - end - else - if self.DetectedUnits[TargetUnit] == nil then - self.DetectedUnits[TargetUnit] = true - end - Detected = true - end - end - end - - self:__Detect( -self.DetectInterval ) - - if Detected == true then - self:__Detected( 1.5 ) - end - -end - ---- @param Wrapper.Controllable#CONTROLLABLE AIControllable --- This statis method is called from the route path within the last task at the last waaypoint of the Controllable. --- Note that this method is required, as triggers the next route when patrolling for the Controllable. -function AI_PATROL_ZONE:_NewPatrolRoute( AIControllable ) - - local PatrolZone = AIControllable:GetState( AIControllable, "PatrolZone" ) -- PatrolCore.Zone#AI_PATROL_ZONE - PatrolZone:__Route( 1 ) -end - - ---- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_PATROL_ZONE:onafterRoute( Controllable, From, Event, To ) - - self:F2() - - -- When RTB, don't allow anymore the routing. - if From == "RTB" then - return - end - - - if self.Controllable:IsAlive() then - -- Determine if the AIControllable is within the PatrolZone. - -- If not, make a waypoint within the to that the AIControllable will fly at maximum speed to that point. - - local PatrolRoute = {} - - -- Calculate the current route point of the controllable as the start point of the route. - -- However, when the controllable is not in the air, - -- the controllable current waypoint is probably the airbase... - -- Thus, if we would take the current waypoint as the startpoint, upon take-off, the controllable flies - -- immediately back to the airbase, and this is not correct. - -- Therefore, when on a runway, get as the current route point a random point within the PatrolZone. - -- This will make the plane fly immediately to the patrol zone. - - if self.Controllable:InAir() == false then - self:E( "Not in the air, finding route path within PatrolZone" ) - local CurrentVec2 = self.Controllable:GetVec2() - --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). - local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() - local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) - local ToPatrolZoneSpeed = self.PatrolMaxSpeed - local CurrentRoutePoint = CurrentPointVec3:RoutePointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TakeOffParking, - POINT_VEC3.RoutePointAction.FromParkingArea, - ToPatrolZoneSpeed, - true - ) - PatrolRoute[#PatrolRoute+1] = CurrentRoutePoint - else - self:E( "In the air, finding route path within PatrolZone" ) - local CurrentVec2 = self.Controllable:GetVec2() - --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). - local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() - local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) - local ToPatrolZoneSpeed = self.PatrolMaxSpeed - local CurrentRoutePoint = CurrentPointVec3:RoutePointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToPatrolZoneSpeed, - true - ) - PatrolRoute[#PatrolRoute+1] = CurrentRoutePoint - end - - - --- Define a random point in the @{Zone}. The AI will fly to that point within the zone. - - --- Find a random 2D point in PatrolZone. - local ToTargetVec2 = self.PatrolZone:GetRandomVec2() - self:T2( ToTargetVec2 ) - - --- Define Speed and Altitude. - local ToTargetAltitude = math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ) - local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) - self:T2( { self.PatrolMinSpeed, self.PatrolMaxSpeed, ToTargetSpeed } ) - - --- Obtain a 3D @{Point} from the 2D point + altitude. - local ToTargetPointVec3 = POINT_VEC3:New( ToTargetVec2.x, ToTargetAltitude, ToTargetVec2.y ) - - --- Create a route point of type air. - local ToTargetRoutePoint = ToTargetPointVec3:RoutePointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, - true - ) - - --self.CoordTest:SpawnFromVec3( ToTargetPointVec3:GetVec3() ) - - --ToTargetPointVec3:SmokeRed() - - PatrolRoute[#PatrolRoute+1] = ToTargetRoutePoint - - --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... - self.Controllable:WayPointInitialize( PatrolRoute ) - - --- Do a trick, link the NewPatrolRoute function of the PATROLGROUP object to the AIControllable in a temporary variable ... - self.Controllable:SetState( self.Controllable, "PatrolZone", self ) - self.Controllable:WayPointFunction( #PatrolRoute, 1, "AI_PATROL_ZONE:_NewPatrolRoute" ) - - --- NOW ROUTE THE GROUP! - self.Controllable:WayPointExecute( 1, 2 ) - end - -end - ---- @param #AI_PATROL_ZONE self -function AI_PATROL_ZONE:onbeforeStatus() - - return self.CheckStatus -end - ---- @param #AI_PATROL_ZONE self -function AI_PATROL_ZONE:onafterStatus() - self:F2() - - if self.Controllable and self.Controllable:IsAlive() then - - local RTB = false - - local Fuel = self.Controllable:GetUnit(1):GetFuel() - if Fuel < self.PatrolFuelTresholdPercentage then - self:E( self.Controllable:GetName() .. " is out of fuel:" .. Fuel .. ", RTB!" ) - local OldAIControllable = self.Controllable - local AIControllableTemplate = self.Controllable:GetTemplate() - - local OrbitTask = OldAIControllable:TaskOrbitCircle( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ), self.PatrolMinSpeed ) - local TimedOrbitTask = OldAIControllable:TaskControlled( OrbitTask, OldAIControllable:TaskCondition(nil,nil,nil,nil,self.PatrolOutOfFuelOrbitTime,nil ) ) - OldAIControllable:SetTask( TimedOrbitTask, 10 ) - - RTB = true - else - end - - -- TODO: Check GROUP damage function. - local Damage = self.Controllable:GetLife() - if Damage <= self.PatrolDamageTreshold then - self:E( self.Controllable:GetName() .. " is damaged:" .. Damage .. ", RTB!" ) - RTB = true - end - - if RTB == true then - self:RTB() - else - self:__Status( 60 ) -- Execute the Patrol event after 30 seconds. - end - end -end - ---- @param #AI_PATROL_ZONE self -function AI_PATROL_ZONE:onafterRTB() - self:F2() - - if self.Controllable and self.Controllable:IsAlive() then - - self:SetDetectionOff() - self.CheckStatus = false - - local PatrolRoute = {} - - --- Calculate the current route point. - local CurrentVec2 = self.Controllable:GetVec2() - - --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). - local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() - local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) - local ToPatrolZoneSpeed = self.PatrolMaxSpeed - local CurrentRoutePoint = CurrentPointVec3:RoutePointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToPatrolZoneSpeed, - true - ) - - PatrolRoute[#PatrolRoute+1] = CurrentRoutePoint - - --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... - self.Controllable:WayPointInitialize( PatrolRoute ) - - --- NOW ROUTE THE GROUP! - self.Controllable:WayPointExecute( 1, 1 ) - - end - -end - ---- @param #AI_PATROL_ZONE self -function AI_PATROL_ZONE:onafterDead() - self:SetDetectionOff() - self:SetStatusOff() -end - ---- @param #AI_PATROL_ZONE self --- @param Core.Event#EVENTDATA EventData -function AI_PATROL_ZONE:OnCrash( EventData ) - - if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then - self:E( self.Controllable:GetUnits() ) - if #self.Controllable:GetUnits() == 1 then - self:__Crash( 1, EventData ) - end - end -end - ---- @param #AI_PATROL_ZONE self --- @param Core.Event#EVENTDATA EventData -function AI_PATROL_ZONE:OnEjection( EventData ) - - if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then - self:__Eject( 1, EventData ) - end -end - ---- @param #AI_PATROL_ZONE self --- @param Core.Event#EVENTDATA EventData -function AI_PATROL_ZONE:OnPilotDead( EventData ) - - if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then - self:__PilotDead( 1, EventData ) - end -end ---- Single-Player:**Yes** / Multi-Player:**Yes** / AI:**Yes** / Human:**No** / Types:**Air** -- --- **Provide Close Air Support to friendly ground troops.** --- --- ![Banner Image](..\Presentations\AI_CAS\Dia1.JPG) --- --- === --- --- # 1) @{#AI_CAS_ZONE} class, extends @{AI_Patrol#AI_PATROL_ZONE} --- --- @{#AI_CAS_ZONE} derives from the @{AI_Patrol#AI_PATROL_ZONE}, inheriting its methods and behaviour. --- --- The @{#AI_CAS_ZONE} class implements the core functions to provide Close Air Support in an Engage @{Zone} by an AIR @{Controllable} or @{Group}. --- The AI_CAS_ZONE runs a process. It holds an AI in a Patrol Zone and when the AI is commanded to engage, it will fly to an Engage Zone. --- --- ![HoldAndEngage](..\Presentations\AI_CAS\Dia3.JPG) --- --- The AI_CAS_ZONE is assigned a @{Group} and this must be done before the AI_CAS_ZONE process can be started through the **Start** event. --- --- ![Start Event](..\Presentations\AI_CAS\Dia4.JPG) --- --- Upon started, The AI will **Route** itself towards the random 3D point within a patrol zone, --- using a random speed within the given altitude and speed limits. --- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. --- This cycle will continue until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. --- --- ![Route Event](..\Presentations\AI_CAS\Dia5.JPG) --- --- When the AI is commanded to provide Close Air Support (through the event **Engage**), the AI will fly towards the Engage Zone. --- Any target that is detected in the Engage Zone will be reported and will be destroyed by the AI. --- --- ![Engage Event](..\Presentations\AI_CAS\Dia6.JPG) --- --- The AI will detect the targets and will only destroy the targets within the Engage Zone. --- --- ![Engage Event](..\Presentations\AI_CAS\Dia7.JPG) --- --- Every target that is destroyed, is reported< by the AI. --- --- ![Engage Event](..\Presentations\AI_CAS\Dia8.JPG) --- --- Note that the AI does not know when the Engage Zone is cleared, and therefore will keep circling in the zone. --- --- ![Engage Event](..\Presentations\AI_CAS\Dia9.JPG) --- --- Until it is notified through the event **Accomplish**, which is to be triggered by an observing party: --- --- * a FAC --- * a timed event --- * a menu option selected by a human --- * a condition --- * others ... --- --- ![Engage Event](..\Presentations\AI_CAS\Dia10.JPG) --- --- When the AI has accomplished the CAS, it will fly back to the Patrol Zone. --- --- ![Engage Event](..\Presentations\AI_CAS\Dia11.JPG) --- --- It will keep patrolling there, until it is notified to RTB or move to another CAS Zone. --- It can be notified to go RTB through the **RTB** event. --- --- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. --- --- ![Engage Event](..\Presentations\AI_CAS\Dia12.JPG) --- --- # 1.1) AI_CAS_ZONE constructor --- --- * @{#AI_CAS_ZONE.New}(): Creates a new AI_CAS_ZONE object. --- --- ## 1.2) AI_CAS_ZONE is a FSM --- --- ![Process](..\Presentations\AI_CAS\Dia2.JPG) --- --- ### 1.2.1) AI_CAS_ZONE States --- --- * **None** ( Group ): The process is not started yet. --- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. --- * **Engaging** ( Group ): The AI is engaging the targets in the Engage Zone, executing CAS. --- * **Returning** ( Group ): The AI is returning to Base.. --- --- ### 1.2.2) AI_CAS_ZONE Events --- --- * **Start** ( Group ): Start the process. --- * **Route** ( Group ): Route the AI to a new random 3D point within the Patrol Zone. --- * **Engage** ( Group ): Engage the AI to provide CAS in the Engage Zone, destroying any target it finds. --- * **RTB** ( Group ): Route the AI to the home base. --- * **Detect** ( Group ): The AI is detecting targets. --- * **Detected** ( Group ): The AI has detected new targets. --- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. --- --- ==== --- --- # **API CHANGE HISTORY** --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- Hereby the change log: --- --- 2017-01-15: Initial class and API. --- --- === --- --- # **AUTHORS and CONTRIBUTIONS** --- --- ### Contributions: --- --- * **[Quax](https://forums.eagle.ru/member.php?u=90530)**: Concept, Advice & Testing. --- * **[Pikey](https://forums.eagle.ru/member.php?u=62835)**: Concept, Advice & Testing. --- * **[Gunterlund](http://forums.eagle.ru:8080/member.php?u=75036)**: Test case revision. --- --- ### Authors: --- --- * **FlightControl**: Concept, Design & Programming. --- --- @module AI_Cas - - ---- AI_CAS_ZONE class --- @type AI_CAS_ZONE --- @field Wrapper.Controllable#CONTROLLABLE AIControllable The @{Controllable} patrolling. --- @field Core.Zone#ZONE_BASE TargetZone The @{Zone} where the patrol needs to be executed. --- @extends AI.AI_Patrol#AI_PATROL_ZONE -AI_CAS_ZONE = { - ClassName = "AI_CAS_ZONE", -} - - - ---- Creates a new AI_CAS_ZONE object --- @param #AI_CAS_ZONE self --- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. --- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. --- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h. --- @param Core.Zone#ZONE_BASE EngageZone The zone where the engage will happen. --- @param Dcs.DCSTypes#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO --- @return #AI_CAS_ZONE self -function AI_CAS_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageZone, PatrolAltType ) - - -- Inherits from BASE - local self = BASE:Inherit( self, AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_CAS_ZONE - - self.EngageZone = EngageZone - self.Accomplished = false - - self:SetDetectionZone( self.EngageZone ) - - self:AddTransition( { "Patrolling", "Engaging" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. - - --- OnBefore Transition Handler for Event Engage. - -- @function [parent=#AI_CAS_ZONE] OnBeforeEngage - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone. - -- @param Dcs.DCSTypes#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. - -- @param Dcs.DCSTypes#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. - -- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. - -- @param Dcs.DCSTypes#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. - - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Engage. - -- @function [parent=#AI_CAS_ZONE] OnAfterEngage - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone. - -- @param Dcs.DCSTypes#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. - -- @param Dcs.DCSTypes#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. - -- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. - -- @param Dcs.DCSTypes#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. - - --- Synchronous Event Trigger for Event Engage. - -- @function [parent=#AI_CAS_ZONE] Engage - -- @param #AI_CAS_ZONE self - - --- Asynchronous Event Trigger for Event Engage. - -- @function [parent=#AI_CAS_ZONE] __Engage - -- @param #AI_CAS_ZONE self - -- @param #number Delay The delay in seconds. - ---- OnLeave Transition Handler for State Engaging. --- @function [parent=#AI_CAS_ZONE] OnLeaveEngaging --- @param #AI_CAS_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnEnter Transition Handler for State Engaging. --- @function [parent=#AI_CAS_ZONE] OnEnterEngaging --- @param #AI_CAS_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - - self:AddTransition( "Engaging", "Target", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. - - self:AddTransition( "Engaging", "Fired", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. - - --- OnBefore Transition Handler for Event Fired. - -- @function [parent=#AI_CAS_ZONE] OnBeforeFired - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Fired. - -- @function [parent=#AI_CAS_ZONE] OnAfterFired - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Fired. - -- @function [parent=#AI_CAS_ZONE] Fired - -- @param #AI_CAS_ZONE self - - --- Asynchronous Event Trigger for Event Fired. - -- @function [parent=#AI_CAS_ZONE] __Fired - -- @param #AI_CAS_ZONE self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. - - --- OnBefore Transition Handler for Event Destroy. - -- @function [parent=#AI_CAS_ZONE] OnBeforeDestroy - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Destroy. - -- @function [parent=#AI_CAS_ZONE] OnAfterDestroy - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Destroy. - -- @function [parent=#AI_CAS_ZONE] Destroy - -- @param #AI_CAS_ZONE self - - --- Asynchronous Event Trigger for Event Destroy. - -- @function [parent=#AI_CAS_ZONE] __Destroy - -- @param #AI_CAS_ZONE self - -- @param #number Delay The delay in seconds. - - - self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. - - --- OnBefore Transition Handler for Event Abort. - -- @function [parent=#AI_CAS_ZONE] OnBeforeAbort - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Abort. - -- @function [parent=#AI_CAS_ZONE] OnAfterAbort - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Abort. - -- @function [parent=#AI_CAS_ZONE] Abort - -- @param #AI_CAS_ZONE self - - --- Asynchronous Event Trigger for Event Abort. - -- @function [parent=#AI_CAS_ZONE] __Abort - -- @param #AI_CAS_ZONE self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. - - --- OnBefore Transition Handler for Event Accomplish. - -- @function [parent=#AI_CAS_ZONE] OnBeforeAccomplish - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Accomplish. - -- @function [parent=#AI_CAS_ZONE] OnAfterAccomplish - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Accomplish. - -- @function [parent=#AI_CAS_ZONE] Accomplish - -- @param #AI_CAS_ZONE self - - --- Asynchronous Event Trigger for Event Accomplish. - -- @function [parent=#AI_CAS_ZONE] __Accomplish - -- @param #AI_CAS_ZONE self - -- @param #number Delay The delay in seconds. - - return self -end - - ---- Set the Engage Zone where the AI is performing CAS. Note that if the EngageZone is changed, the AI needs to re-detect targets. --- @param #AI_CAS_ZONE self --- @param Core.Zone#ZONE EngageZone The zone where the AI is performing CAS. --- @return #AI_CAS_ZONE self -function AI_CAS_ZONE:SetEngageZone( EngageZone ) - self:F2() - - if EngageZone then - self.EngageZone = EngageZone - else - self.EngageZone = nil - end -end - - - ---- onafter State Transition for Event Start. --- @param #AI_CAS_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAS_ZONE:onafterStart( Controllable, From, Event, To ) - - -- Call the parent Start event handler - self:GetParent(self).onafterStart( self, Controllable, From, Event, To ) - self:HandleEvent( EVENTS.Dead, self.OnDead ) - - self:SetDetectionDeactivated() -- When not engaging, set the detection off. -end - ---- @param Wrapper.Controllable#CONTROLLABLE AIControllable -function _NewEngageRoute( AIControllable ) - - AIControllable:T( "NewEngageRoute" ) - local EngageZone = AIControllable:GetState( AIControllable, "EngageZone" ) -- AI.AI_Cas#AI_CAS_ZONE - EngageZone:__Engage( 1 ) -end - ---- @param #AI_CAS_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAS_ZONE:onbeforeEngage( Controllable, From, Event, To ) - - if self.Accomplished == true then - return false - end -end - ---- @param #AI_CAS_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAS_ZONE:onafterTarget( Controllable, From, Event, To ) - self:E("onafterTarget") - - if Controllable:IsAlive() then - - local AttackTasks = {} - - for DetectedUnit, Detected in pairs( self.DetectedUnits ) do - local DetectedUnit = DetectedUnit -- Wrapper.Unit#UNIT - if DetectedUnit:IsAlive() then - if DetectedUnit:IsInZone( self.EngageZone ) then - if Detected == true then - self:E( {"Target: ", DetectedUnit } ) - self.DetectedUnits[DetectedUnit] = false - local AttackTask = Controllable:EnRouteTaskEngageUnit( DetectedUnit, 1, true, self.EngageWeaponExpend, self.EngageAttackQty, self.EngageDirection, self.EngageAltitude, nil ) - self.Controllable:PushTask( AttackTask, 1 ) - end - end - else - self.DetectedUnits[DetectedUnit] = nil - end - end - - self:__Target( -10 ) - - end -end - - - ---- @param #AI_CAS_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone. --- @param Dcs.DCSTypes#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. --- @param Dcs.DCSTypes#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. --- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. --- @param Dcs.DCSTypes#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. -function AI_CAS_ZONE:onafterEngage( Controllable, From, Event, To, EngageSpeed, EngageAltitude, EngageWeaponExpend, EngageAttackQty, EngageDirection ) - self:E("onafterEngage") - - self.EngageSpeed = EngageSpeed or 400 - self.EngageAltitude = EngageAltitude or 2000 - self.EngageWeaponExpend = EngageWeaponExpend - self.EngageAttackQty = EngageAttackQty - self.EngageDirection = EngageDirection - - if Controllable:IsAlive() then - - - local EngageRoute = {} - - --- Calculate the current route point. - local CurrentVec2 = self.Controllable:GetVec2() - - --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). - local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() - local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) - local ToEngageZoneSpeed = self.PatrolMaxSpeed - local CurrentRoutePoint = CurrentPointVec3:RoutePointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - self.EngageSpeed, - true - ) - - EngageRoute[#EngageRoute+1] = CurrentRoutePoint - - - if self.Controllable:IsNotInZone( self.EngageZone ) then - - -- Find a random 2D point in EngageZone. - local ToEngageZoneVec2 = self.EngageZone:GetRandomVec2() - self:T2( ToEngageZoneVec2 ) - - -- Obtain a 3D @{Point} from the 2D point + altitude. - local ToEngageZonePointVec3 = POINT_VEC3:New( ToEngageZoneVec2.x, self.EngageAltitude, ToEngageZoneVec2.y ) - - -- Create a route point of type air. - local ToEngageZoneRoutePoint = ToEngageZonePointVec3:RoutePointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - self.EngageSpeed, - true - ) - - EngageRoute[#EngageRoute+1] = ToEngageZoneRoutePoint - - end - - --- Define a random point in the @{Zone}. The AI will fly to that point within the zone. - - --- Find a random 2D point in EngageZone. - local ToTargetVec2 = self.EngageZone:GetRandomVec2() - self:T2( ToTargetVec2 ) - - --- Obtain a 3D @{Point} from the 2D point + altitude. - local ToTargetPointVec3 = POINT_VEC3:New( ToTargetVec2.x, self.EngageAltitude, ToTargetVec2.y ) - - --- Create a route point of type air. - local ToTargetRoutePoint = ToTargetPointVec3:RoutePointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - self.EngageSpeed, - true - ) - - --ToTargetPointVec3:SmokeBlue() - - EngageRoute[#EngageRoute+1] = ToTargetRoutePoint - - - Controllable:OptionROEOpenFire() - Controllable:OptionROTVertical() - --- local AttackTasks = {} --- --- for DetectedUnitID, DetectedUnit in pairs( self.DetectedUnits ) do --- local DetectedUnit = DetectedUnit -- Wrapper.Unit#UNIT --- self:T( DetectedUnit ) --- if DetectedUnit:IsAlive() then --- if DetectedUnit:IsInZone( self.EngageZone ) then --- self:E( {"Engaging ", DetectedUnit } ) --- AttackTasks[#AttackTasks+1] = Controllable:TaskAttackUnit( DetectedUnit ) --- end --- else --- self.DetectedUnits[DetectedUnit] = nil --- end --- end --- --- EngageRoute[1].task = Controllable:TaskCombo( AttackTasks ) - - --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... - self.Controllable:WayPointInitialize( EngageRoute ) - - --- Do a trick, link the NewEngageRoute function of the object to the AIControllable in a temporary variable ... - self.Controllable:SetState( self.Controllable, "EngageZone", self ) - - self.Controllable:WayPointFunction( #EngageRoute, 1, "_NewEngageRoute" ) - - --- NOW ROUTE THE GROUP! - self.Controllable:WayPointExecute( 1 ) - - self:SetDetectionInterval( 10 ) - self:SetDetectionActivated() - self:__Target( -10 ) -- Start Targetting - end -end - ---- @param #AI_CAS_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @param Core.Event#EVENTDATA EventData -function AI_CAS_ZONE:onafterDestroy( Controllable, From, Event, To, EventData ) - - if EventData.IniUnit then - self.DetectedUnits[EventData.IniUnit] = nil - end - - Controllable:MessageToAll( "Destroyed a target", 15 , "Destroyed!" ) -end - ---- @param #AI_CAS_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAS_ZONE:onafterAccomplish( Controllable, From, Event, To ) - self.Accomplished = true - self:SetDetectionDeactivated() -end - ---- @param #AI_CAS_ZONE self --- @param Core.Event#EVENTDATA EventData -function AI_CAS_ZONE:OnDead( EventData ) - self:T( { "EventDead", EventData } ) - - if EventData.IniDCSUnit then - self:__Destroy( 1, EventData ) - end -end - - ---- Single-Player:**Yes** / Multi-Player:**Yes** / AI:**Yes** / Human:**No** / Types:**Air** -- **Execute Combat Air Patrol (CAP).** --- --- ![Banner Image](..\Presentations\AI_CAP\Dia1.JPG) --- --- === --- --- # 1) @{#AI_CAP_ZONE} class, extends @{AI_CAP#AI_PATROL_ZONE} --- --- The @{#AI_CAP_ZONE} class implements the core functions to patrol a @{Zone} by an AI @{Controllable} or @{Group} --- and automatically engage any airborne enemies that are within a certain range or within a certain zone. --- --- ![Process](..\Presentations\AI_CAP\Dia3.JPG) --- --- The AI_CAP_ZONE is assigned a @{Group} and this must be done before the AI_CAP_ZONE process can be started using the **Start** event. --- --- ![Process](..\Presentations\AI_CAP\Dia4.JPG) --- --- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits. --- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. --- --- ![Process](..\Presentations\AI_CAP\Dia5.JPG) --- --- This cycle will continue. --- --- ![Process](..\Presentations\AI_CAP\Dia6.JPG) --- --- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event. --- --- ![Process](..\Presentations\AI_CAP\Dia9.JPG) --- --- When enemies are detected, the AI will automatically engage the enemy. --- --- ![Process](..\Presentations\AI_CAP\Dia10.JPG) --- --- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. --- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. --- --- ![Process](..\Presentations\AI_CAP\Dia13.JPG) --- --- ## 1.1) AI_CAP_ZONE constructor --- --- * @{#AI_CAP_ZONE.New}(): Creates a new AI_CAP_ZONE object. --- --- ## 1.2) AI_CAP_ZONE is a FSM --- --- ![Process](..\Presentations\AI_CAP\Dia2.JPG) --- --- ### 1.2.1) AI_CAP_ZONE States --- --- * **None** ( Group ): The process is not started yet. --- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. --- * **Engaging** ( Group ): The AI is engaging the bogeys. --- * **Returning** ( Group ): The AI is returning to Base.. --- --- ### 1.2.2) AI_CAP_ZONE Events --- --- * **Start** ( Group ): Start the process. --- * **Route** ( Group ): Route the AI to a new random 3D point within the Patrol Zone. --- * **Engage** ( Group ): Let the AI engage the bogeys. --- * **RTB** ( Group ): Route the AI to the home base. --- * **Detect** ( Group ): The AI is detecting targets. --- * **Detected** ( Group ): The AI has detected new targets. --- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. --- --- ## 1.3) Set the Range of Engagement --- --- ![Range](..\Presentations\AI_CAP\Dia11.JPG) --- --- An optional range can be set in meters, --- that will define when the AI will engage with the detected airborne enemy targets. --- The range can be beyond or smaller than the range of the Patrol Zone. --- The range is applied at the position of the AI. --- Use the method @{AI_CAP#AI_CAP_ZONE.SetEngageRange}() to define that range. --- --- ## 1.4) Set the Zone of Engagement --- --- ![Zone](..\Presentations\AI_CAP\Dia12.JPG) --- --- An optional @{Zone} can be set, --- that will define when the AI will engage with the detected airborne enemy targets. --- Use the method @{AI_Cap#AI_CAP_ZONE.SetEngageZone}() to define that Zone. --- --- ==== --- --- # **API CHANGE HISTORY** --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- Hereby the change log: --- --- 2017-01-15: Initial class and API. --- --- === --- --- # **AUTHORS and CONTRIBUTIONS** --- --- ### Contributions: --- --- * **[Quax](https://forums.eagle.ru/member.php?u=90530)**: Concept, Advice & Testing. --- * **[Pikey](https://forums.eagle.ru/member.php?u=62835)**: Concept, Advice & Testing. --- * **[Gunterlund](http://forums.eagle.ru:8080/member.php?u=75036)**: Test case revision. --- * **[Whisper](http://forums.eagle.ru/member.php?u=3829): Testing. --- * **[Delta99](https://forums.eagle.ru/member.php?u=125166): Testing. --- --- ### Authors: --- --- * **FlightControl**: Concept, Design & Programming. --- --- @module AI_Cap - - ---- AI_CAP_ZONE class --- @type AI_CAP_ZONE --- @field Wrapper.Controllable#CONTROLLABLE AIControllable The @{Controllable} patrolling. --- @field Core.Zone#ZONE_BASE TargetZone The @{Zone} where the patrol needs to be executed. --- @extends AI.AI_Patrol#AI_PATROL_ZONE -AI_CAP_ZONE = { - ClassName = "AI_CAP_ZONE", -} - - - ---- Creates a new AI_CAP_ZONE object --- @param #AI_CAP_ZONE self --- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. --- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. --- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h. --- @param Dcs.DCSTypes#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO --- @return #AI_CAP_ZONE self -function AI_CAP_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) - - -- Inherits from BASE - local self = BASE:Inherit( self, AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_CAP_ZONE - - self.Accomplished = false - self.Engaging = false - - self:AddTransition( { "Patrolling", "Engaging" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAP_ZONE. - - --- OnBefore Transition Handler for Event Engage. - -- @function [parent=#AI_CAP_ZONE] OnBeforeEngage - -- @param #AI_CAP_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Engage. - -- @function [parent=#AI_CAP_ZONE] OnAfterEngage - -- @param #AI_CAP_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Engage. - -- @function [parent=#AI_CAP_ZONE] Engage - -- @param #AI_CAP_ZONE self - - --- Asynchronous Event Trigger for Event Engage. - -- @function [parent=#AI_CAP_ZONE] __Engage - -- @param #AI_CAP_ZONE self - -- @param #number Delay The delay in seconds. - ---- OnLeave Transition Handler for State Engaging. --- @function [parent=#AI_CAP_ZONE] OnLeaveEngaging --- @param #AI_CAP_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnEnter Transition Handler for State Engaging. --- @function [parent=#AI_CAP_ZONE] OnEnterEngaging --- @param #AI_CAP_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - - self:AddTransition( "Engaging", "Fired", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAP_ZONE. - - --- OnBefore Transition Handler for Event Fired. - -- @function [parent=#AI_CAP_ZONE] OnBeforeFired - -- @param #AI_CAP_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Fired. - -- @function [parent=#AI_CAP_ZONE] OnAfterFired - -- @param #AI_CAP_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Fired. - -- @function [parent=#AI_CAP_ZONE] Fired - -- @param #AI_CAP_ZONE self - - --- Asynchronous Event Trigger for Event Fired. - -- @function [parent=#AI_CAP_ZONE] __Fired - -- @param #AI_CAP_ZONE self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_CAP_ZONE. - - --- OnBefore Transition Handler for Event Destroy. - -- @function [parent=#AI_CAP_ZONE] OnBeforeDestroy - -- @param #AI_CAP_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Destroy. - -- @function [parent=#AI_CAP_ZONE] OnAfterDestroy - -- @param #AI_CAP_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Destroy. - -- @function [parent=#AI_CAP_ZONE] Destroy - -- @param #AI_CAP_ZONE self - - --- Asynchronous Event Trigger for Event Destroy. - -- @function [parent=#AI_CAP_ZONE] __Destroy - -- @param #AI_CAP_ZONE self - -- @param #number Delay The delay in seconds. - - - self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_CAP_ZONE. - - --- OnBefore Transition Handler for Event Abort. - -- @function [parent=#AI_CAP_ZONE] OnBeforeAbort - -- @param #AI_CAP_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Abort. - -- @function [parent=#AI_CAP_ZONE] OnAfterAbort - -- @param #AI_CAP_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Abort. - -- @function [parent=#AI_CAP_ZONE] Abort - -- @param #AI_CAP_ZONE self - - --- Asynchronous Event Trigger for Event Abort. - -- @function [parent=#AI_CAP_ZONE] __Abort - -- @param #AI_CAP_ZONE self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_CAP_ZONE. - - --- OnBefore Transition Handler for Event Accomplish. - -- @function [parent=#AI_CAP_ZONE] OnBeforeAccomplish - -- @param #AI_CAP_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Accomplish. - -- @function [parent=#AI_CAP_ZONE] OnAfterAccomplish - -- @param #AI_CAP_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Accomplish. - -- @function [parent=#AI_CAP_ZONE] Accomplish - -- @param #AI_CAP_ZONE self - - --- Asynchronous Event Trigger for Event Accomplish. - -- @function [parent=#AI_CAP_ZONE] __Accomplish - -- @param #AI_CAP_ZONE self - -- @param #number Delay The delay in seconds. - - return self -end - - ---- Set the Engage Zone which defines where the AI will engage bogies. --- @param #AI_CAP_ZONE self --- @param Core.Zone#ZONE EngageZone The zone where the AI is performing CAP. --- @return #AI_CAP_ZONE self -function AI_CAP_ZONE:SetEngageZone( EngageZone ) - self:F2() - - if EngageZone then - self.EngageZone = EngageZone - else - self.EngageZone = nil - end -end - ---- Set the Engage Range when the AI will engage with airborne enemies. --- @param #AI_CAP_ZONE self --- @param #number EngageRange The Engage Range. --- @return #AI_CAP_ZONE self -function AI_CAP_ZONE:SetEngageRange( EngageRange ) - self:F2() - - if EngageRange then - self.EngageRange = EngageRange - else - self.EngageRange = nil - end -end - ---- onafter State Transition for Event Start. --- @param #AI_CAP_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAP_ZONE:onafterStart( Controllable, From, Event, To ) - - -- Call the parent Start event handler - self:GetParent(self).onafterStart( self, Controllable, From, Event, To ) - -end - ---- @param Wrapper.Controllable#CONTROLLABLE AIControllable -function _NewEngageCapRoute( AIControllable ) - - AIControllable:T( "NewEngageRoute" ) - local EngageZone = AIControllable:GetState( AIControllable, "EngageZone" ) -- AI.AI_Cap#AI_CAP_ZONE - EngageZone:__Engage( 1 ) -end - ---- @param #AI_CAP_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAP_ZONE:onbeforeEngage( Controllable, From, Event, To ) - - if self.Accomplished == true then - return false - end -end - ---- @param #AI_CAP_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAP_ZONE:onafterDetected( Controllable, From, Event, To ) - - if From ~= "Engaging" then - - local Engage = false - - for DetectedUnit, Detected in pairs( self.DetectedUnits ) do - - local DetectedUnit = DetectedUnit -- Wrapper.Unit#UNIT - self:T( DetectedUnit ) - if DetectedUnit:IsAlive() and DetectedUnit:IsAir() then - Engage = true - break - end - end - - if Engage == true then - self:E( 'Detected -> Engaging' ) - self:__Engage( 1 ) - end - end -end - - - ---- @param #AI_CAP_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAP_ZONE:onafterEngage( Controllable, From, Event, To ) - - if Controllable:IsAlive() then - - local EngageRoute = {} - - --- Calculate the current route point. - local CurrentVec2 = self.Controllable:GetVec2() - - --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). - local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() - local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) - local ToEngageZoneSpeed = self.PatrolMaxSpeed - local CurrentRoutePoint = CurrentPointVec3:RoutePointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToEngageZoneSpeed, - true - ) - - EngageRoute[#EngageRoute+1] = CurrentRoutePoint - - - --- Find a random 2D point in PatrolZone. - local ToTargetVec2 = self.PatrolZone:GetRandomVec2() - self:T2( ToTargetVec2 ) - - --- Define Speed and Altitude. - local ToTargetAltitude = math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) - local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) - self:T2( { self.PatrolMinSpeed, self.PatrolMaxSpeed, ToTargetSpeed } ) - - --- Obtain a 3D @{Point} from the 2D point + altitude. - local ToTargetPointVec3 = POINT_VEC3:New( ToTargetVec2.x, ToTargetAltitude, ToTargetVec2.y ) - - --- Create a route point of type air. - local ToPatrolRoutePoint = ToTargetPointVec3:RoutePointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, - true - ) - - EngageRoute[#EngageRoute+1] = ToPatrolRoutePoint - - Controllable:OptionROEOpenFire() - Controllable:OptionROTPassiveDefense() - - local AttackTasks = {} - - for DetectedUnit, Detected in pairs( self.DetectedUnits ) do - local DetectedUnit = DetectedUnit -- Wrapper.Unit#UNIT - self:T( { DetectedUnit, DetectedUnit:IsAlive(), DetectedUnit:IsAir() } ) - if DetectedUnit:IsAlive() and DetectedUnit:IsAir() then - if self.EngageZone then - if DetectedUnit:IsInZone( self.EngageZone ) then - self:E( {"Within Zone and Engaging ", DetectedUnit } ) - AttackTasks[#AttackTasks+1] = Controllable:TaskAttackUnit( DetectedUnit ) - end - else - if self.EngageRange then - if DetectedUnit:GetPointVec3():Get2DDistance(Controllable:GetPointVec3() ) <= self.EngageRange then - self:E( {"Within Range and Engaging", DetectedUnit } ) - AttackTasks[#AttackTasks+1] = Controllable:TaskAttackUnit( DetectedUnit ) - end - else - AttackTasks[#AttackTasks+1] = Controllable:TaskAttackUnit( DetectedUnit ) - end - end - else - self.DetectedUnits[DetectedUnit] = nil - end - end - - --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... - self.Controllable:WayPointInitialize( EngageRoute ) - - - if #AttackTasks == 0 then - self:E("No targets found -> Going back to Patrolling") - self:__Abort( 1 ) - self:__Route( 1 ) - self:SetDetectionActivated() - else - EngageRoute[1].task = Controllable:TaskCombo( AttackTasks ) - - --- Do a trick, link the NewEngageRoute function of the object to the AIControllable in a temporary variable ... - self.Controllable:SetState( self.Controllable, "EngageZone", self ) - - self.Controllable:WayPointFunction( #EngageRoute, 1, "_NewEngageCapRoute" ) - - self:SetDetectionDeactivated() - end - - --- NOW ROUTE THE GROUP! - self.Controllable:WayPointExecute( 1, 2 ) - - end -end - ---- @param #AI_CAP_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @param Core.Event#EVENTDATA EventData -function AI_CAP_ZONE:onafterDestroy( Controllable, From, Event, To, EventData ) - - if EventData.IniUnit then - self.DetectedUnits[EventData.IniUnit] = nil - end - - Controllable:MessageToAll( "Destroyed a target", 15 , "Destroyed!" ) -end - ---- @param #AI_CAP_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAP_ZONE:onafterAccomplish( Controllable, From, Event, To ) - self.Accomplished = true - self:SetDetectionOff() -end - - ----Single-Player:**Yes** / Multi-Player:**Yes** / AI:**Yes** / Human:**No** / Types:**Ground** -- --- **Management of logical cargo objects, that can be transported from and to transportation carriers.** --- --- ![Banner Image](..\Presentations\AI_CARGO\CARGO.JPG) --- --- === --- --- Cargo can be of various forms, always are composed out of ONE object ( one unit or one static or one slingload crate ): --- --- * AI_CARGO_UNIT, represented by a @{Unit} in a @{Group}: Cargo can be represented by a Unit in a Group. Destruction of the Unit will mean that the cargo is lost. --- * CARGO_STATIC, represented by a @{Static}: Cargo can be represented by a Static. Destruction of the Static will mean that the cargo is lost. --- * AI_CARGO_PACKAGE, contained in a @{Unit} of a @{Group}: Cargo can be contained within a Unit of a Group. The cargo can be **delivered** by the @{Unit}. If the Unit is destroyed, the cargo will be destroyed also. --- * AI_CARGO_PACKAGE, Contained in a @{Static}: Cargo can be contained within a Static. The cargo can be **collected** from the @Static. If the @{Static} is destroyed, the cargo will be destroyed. --- * CARGO_SLINGLOAD, represented by a @{Cargo} that is transportable: Cargo can be represented by a Cargo object that is transportable. Destruction of the Cargo will mean that the cargo is lost. --- --- * AI_CARGO_GROUPED, represented by a Group of CARGO_UNITs. --- --- # 1) @{#AI_CARGO} class, extends @{Fsm#FSM_PROCESS} --- --- The @{#AI_CARGO} class defines the core functions that defines a cargo object within MOOSE. --- A cargo is a logical object defined that is available for transport, and has a life status within a simulation. --- --- The AI_CARGO is a state machine: it manages the different events and states of the cargo. --- All derived classes from AI_CARGO follow the same state machine, expose the same cargo event functions, and provide the same cargo states. --- --- ## 1.2.1) AI_CARGO Events: --- --- * @{#AI_CARGO.Board}( ToCarrier ): Boards the cargo to a carrier. --- * @{#AI_CARGO.Load}( ToCarrier ): Loads the cargo into a carrier, regardless of its position. --- * @{#AI_CARGO.UnBoard}( ToPointVec2 ): UnBoard the cargo from a carrier. This will trigger a movement of the cargo to the option ToPointVec2. --- * @{#AI_CARGO.UnLoad}( ToPointVec2 ): UnLoads the cargo from a carrier. --- * @{#AI_CARGO.Dead}( Controllable ): The cargo is dead. The cargo process will be ended. --- --- ## 1.2.2) AI_CARGO States: --- --- * **UnLoaded**: The cargo is unloaded from a carrier. --- * **Boarding**: The cargo is currently boarding (= running) into a carrier. --- * **Loaded**: The cargo is loaded into a carrier. --- * **UnBoarding**: The cargo is currently unboarding (=running) from a carrier. --- * **Dead**: The cargo is dead ... --- * **End**: The process has come to an end. --- --- ## 1.2.3) AI_CARGO state transition methods: --- --- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. --- There are 2 moments when state transition methods will be called by the state machine: --- --- * **Leaving** the state. --- The state transition method needs to start with the name **OnLeave + the name of the state**. --- If the state transition method returns false, then the processing of the state transition will not be done! --- If you want to change the behaviour of the AIControllable at this event, return false, --- but then you'll need to specify your own logic using the AIControllable! --- --- * **Entering** the state. --- The state transition method needs to start with the name **OnEnter + the name of the state**. --- These state transition methods need to provide a return value, which is specified at the function description. --- --- # 2) #AI_CARGO_UNIT class --- --- The AI_CARGO_UNIT class defines a cargo that is represented by a UNIT object within the simulator, and can be transported by a carrier. --- Use the event functions as described above to Load, UnLoad, Board, UnBoard the AI_CARGO_UNIT objects to and from carriers. --- --- # 5) #AI_CARGO_GROUPED class --- --- The AI_CARGO_GROUPED class defines a cargo that is represented by a group of UNIT objects within the simulator, and can be transported by a carrier. --- Use the event functions as described above to Load, UnLoad, Board, UnBoard the AI_CARGO_UNIT objects to and from carriers. --- --- This module is still under construction, but is described above works already, and will keep working ... --- --- @module Cargo - --- Events - --- Board - ---- Boards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo to the Carrier. --- The cargo must be in the **UnLoaded** state. --- @function [parent=#AI_CARGO] Board --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. - ---- Boards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo to the Carrier. --- The cargo must be in the **UnLoaded** state. --- @function [parent=#AI_CARGO] __Board --- @param #AI_CARGO self --- @param #number DelaySeconds The amount of seconds to delay the action. --- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. - - --- UnBoard - ---- UnBoards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo from the Carrier. --- The cargo must be in the **Loaded** state. --- @function [parent=#AI_CARGO] UnBoard --- @param #AI_CARGO self --- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Point#POINT_VEC2) to where the cargo should run after onboarding. If not provided, the cargo will run to 60 meters behind the Carrier location. - ---- UnBoards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo from the Carrier. --- The cargo must be in the **Loaded** state. --- @function [parent=#AI_CARGO] __UnBoard --- @param #AI_CARGO self --- @param #number DelaySeconds The amount of seconds to delay the action. --- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Point#POINT_VEC2) to where the cargo should run after onboarding. If not provided, the cargo will run to 60 meters behind the Carrier location. - - --- Load - ---- Loads the cargo to a Carrier. The event will load the cargo into the Carrier regardless of its position. There will be no movement simulated of the cargo loading. --- The cargo must be in the **UnLoaded** state. --- @function [parent=#AI_CARGO] Load --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. - ---- Loads the cargo to a Carrier. The event will load the cargo into the Carrier regardless of its position. There will be no movement simulated of the cargo loading. --- The cargo must be in the **UnLoaded** state. --- @function [parent=#AI_CARGO] __Load --- @param #AI_CARGO self --- @param #number DelaySeconds The amount of seconds to delay the action. --- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. - - --- UnLoad - ---- UnLoads the cargo to a Carrier. The event will unload the cargo from the Carrier. There will be no movement simulated of the cargo loading. --- The cargo must be in the **Loaded** state. --- @function [parent=#AI_CARGO] UnLoad --- @param #AI_CARGO self --- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Point#POINT_VEC2) to where the cargo will be placed after unloading. If not provided, the cargo will be placed 60 meters behind the Carrier location. - ---- UnLoads the cargo to a Carrier. The event will unload the cargo from the Carrier. There will be no movement simulated of the cargo loading. --- The cargo must be in the **Loaded** state. --- @function [parent=#AI_CARGO] __UnLoad --- @param #AI_CARGO self --- @param #number DelaySeconds The amount of seconds to delay the action. --- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Point#POINT_VEC2) to where the cargo will be placed after unloading. If not provided, the cargo will be placed 60 meters behind the Carrier location. - --- State Transition Functions - --- UnLoaded - ---- @function [parent=#AI_CARGO] OnLeaveUnLoaded --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable --- @return #boolean - ---- @function [parent=#AI_CARGO] OnEnterUnLoaded --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable - --- Loaded - ---- @function [parent=#AI_CARGO] OnLeaveLoaded --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable --- @return #boolean - ---- @function [parent=#AI_CARGO] OnEnterLoaded --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable - --- Boarding - ---- @function [parent=#AI_CARGO] OnLeaveBoarding --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable --- @return #boolean - ---- @function [parent=#AI_CARGO] OnEnterBoarding --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable - --- UnBoarding - ---- @function [parent=#AI_CARGO] OnLeaveUnBoarding --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable --- @return #boolean - ---- @function [parent=#AI_CARGO] OnEnterUnBoarding --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable - - --- TODO: Find all Carrier objects and make the type of the Carriers Wrapper.Unit#UNIT in the documentation. - -CARGOS = {} - -do -- AI_CARGO - - --- @type AI_CARGO - -- @extends Core.Fsm#FSM_PROCESS - -- @field #string Type A string defining the type of the cargo. eg. Engineers, Equipment, Screwdrivers. - -- @field #string Name A string defining the name of the cargo. The name is the unique identifier of the cargo. - -- @field #number Weight A number defining the weight of the cargo. The weight is expressed in kg. - -- @field #number ReportRadius (optional) A number defining the radius in meters when the cargo is signalling or reporting to a Carrier. - -- @field #number NearRadius (optional) A number defining the radius in meters when the cargo is near to a Carrier, so that it can be loaded. - -- @field Wrapper.Controllable#CONTROLLABLE CargoObject The alive DCS object representing the cargo. This value can be nil, meaning, that the cargo is not represented anywhere... - -- @field Wrapper.Controllable#CONTROLLABLE CargoCarrier The alive DCS object carrying the cargo. This value can be nil, meaning, that the cargo is not contained anywhere... - -- @field #boolean Slingloadable This flag defines if the cargo can be slingloaded. - -- @field #boolean Moveable This flag defines if the cargo is moveable. - -- @field #boolean Representable This flag defines if the cargo can be represented by a DCS Unit. - -- @field #boolean Containable This flag defines if the cargo can be contained within a DCS Unit. - AI_CARGO = { - ClassName = "AI_CARGO", - Type = nil, - Name = nil, - Weight = nil, - CargoObject = nil, - CargoCarrier = nil, - Representable = false, - Slingloadable = false, - Moveable = false, - Containable = false, - } - ---- @type AI_CARGO.CargoObjects --- @map < #string, Wrapper.Positionable#POSITIONABLE > The alive POSITIONABLE objects representing the the cargo. - - ---- AI_CARGO Constructor. This class is an abstract class and should not be instantiated. --- @param #AI_CARGO self --- @param #string Type --- @param #string Name --- @param #number Weight --- @param #number ReportRadius (optional) --- @param #number NearRadius (optional) --- @return #AI_CARGO -function AI_CARGO:New( Type, Name, Weight, ReportRadius, NearRadius ) - - local self = BASE:Inherit( self, FSM:New() ) -- Core.Fsm#FSM_CONTROLLABLE - self:F( { Type, Name, Weight, ReportRadius, NearRadius } ) - - self:SetStartState( "UnLoaded" ) - self:AddTransition( "UnLoaded", "Board", "Boarding" ) - self:AddTransition( "Boarding", "Boarding", "Boarding" ) - self:AddTransition( "Boarding", "Load", "Loaded" ) - self:AddTransition( "UnLoaded", "Load", "Loaded" ) - self:AddTransition( "Loaded", "UnBoard", "UnBoarding" ) - self:AddTransition( "UnBoarding", "UnBoarding", "UnBoarding" ) - self:AddTransition( "UnBoarding", "UnLoad", "UnLoaded" ) - self:AddTransition( "Loaded", "UnLoad", "UnLoaded" ) - - - self.Type = Type - self.Name = Name - self.Weight = Weight - self.ReportRadius = ReportRadius - self.NearRadius = NearRadius - self.CargoObject = nil - self.CargoCarrier = nil - self.Representable = false - self.Slingloadable = false - self.Moveable = false - self.Containable = false - - - self.CargoScheduler = SCHEDULER:New() - - CARGOS[self.Name] = self - - return self -end - - ---- Template method to spawn a new representation of the AI_CARGO in the simulator. --- @param #AI_CARGO self --- @return #AI_CARGO -function AI_CARGO:Spawn( PointVec2 ) - self:F() - -end - - ---- Check if CargoCarrier is near the Cargo to be Loaded. --- @param #AI_CARGO self --- @param Core.Point#POINT_VEC2 PointVec2 --- @return #boolean -function AI_CARGO:IsNear( PointVec2 ) - self:F( { PointVec2 } ) - - local Distance = PointVec2:DistanceFromPointVec2( self.CargoObject:GetPointVec2() ) - self:T( Distance ) - - if Distance <= self.NearRadius then - return true - else - return false - end -end - -end - -do -- AI_CARGO_REPRESENTABLE - - --- @type AI_CARGO_REPRESENTABLE - -- @extends #AI_CARGO - AI_CARGO_REPRESENTABLE = { - ClassName = "AI_CARGO_REPRESENTABLE" - } - ---- AI_CARGO_REPRESENTABLE Constructor. --- @param #AI_CARGO_REPRESENTABLE self --- @param Wrapper.Controllable#Controllable CargoObject --- @param #string Type --- @param #string Name --- @param #number Weight --- @param #number ReportRadius (optional) --- @param #number NearRadius (optional) --- @return #AI_CARGO_REPRESENTABLE -function AI_CARGO_REPRESENTABLE:New( CargoObject, Type, Name, Weight, ReportRadius, NearRadius ) - local self = BASE:Inherit( self, AI_CARGO:New( Type, Name, Weight, ReportRadius, NearRadius ) ) -- #AI_CARGO - self:F( { Type, Name, Weight, ReportRadius, NearRadius } ) - - return self -end - ---- Route a cargo unit to a PointVec2. --- @param #AI_CARGO_REPRESENTABLE self --- @param Core.Point#POINT_VEC2 ToPointVec2 --- @param #number Speed --- @return #AI_CARGO_REPRESENTABLE -function AI_CARGO_REPRESENTABLE:RouteTo( ToPointVec2, Speed ) - self:F2( ToPointVec2 ) - - local Points = {} - - local PointStartVec2 = self.CargoObject:GetPointVec2() - - Points[#Points+1] = PointStartVec2:RoutePointGround( Speed ) - Points[#Points+1] = ToPointVec2:RoutePointGround( Speed ) - - local TaskRoute = self.CargoObject:TaskRoute( Points ) - self.CargoObject:SetTask( TaskRoute, 2 ) - return self -end - -end -- AI_CARGO - -do -- AI_CARGO_UNIT - - --- @type AI_CARGO_UNIT - -- @extends #AI_CARGO_REPRESENTABLE - AI_CARGO_UNIT = { - ClassName = "AI_CARGO_UNIT" - } - ---- AI_CARGO_UNIT Constructor. --- @param #AI_CARGO_UNIT self --- @param Wrapper.Unit#UNIT CargoUnit --- @param #string Type --- @param #string Name --- @param #number Weight --- @param #number ReportRadius (optional) --- @param #number NearRadius (optional) --- @return #AI_CARGO_UNIT -function AI_CARGO_UNIT:New( CargoUnit, Type, Name, Weight, ReportRadius, NearRadius ) - local self = BASE:Inherit( self, AI_CARGO_REPRESENTABLE:New( CargoUnit, Type, Name, Weight, ReportRadius, NearRadius ) ) -- #AI_CARGO_UNIT - self:F( { Type, Name, Weight, ReportRadius, NearRadius } ) - - self:T( CargoUnit ) - self.CargoObject = CargoUnit - - self:T( self.ClassName ) - - return self -end - ---- Enter UnBoarding State. --- @param #AI_CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To --- @param Core.Point#POINT_VEC2 ToPointVec2 -function AI_CARGO_UNIT:onenterUnBoarding( From, Event, To, ToPointVec2 ) - self:F() - - local Angle = 180 - local Speed = 10 - local DeployDistance = 5 - local RouteDistance = 60 - - if From == "Loaded" then - - local CargoCarrierPointVec2 = self.CargoCarrier:GetPointVec2() - local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - local CargoDeployPointVec2 = CargoCarrierPointVec2:Translate( DeployDistance, CargoDeployHeading ) - local CargoRoutePointVec2 = CargoCarrierPointVec2:Translate( RouteDistance, CargoDeployHeading ) - - -- if there is no ToPointVec2 given, then use the CargoRoutePointVec2 - ToPointVec2 = ToPointVec2 or CargoRoutePointVec2 - - local FromPointVec2 = CargoCarrierPointVec2 - - -- Respawn the group... - if self.CargoObject then - self.CargoObject:ReSpawn( CargoDeployPointVec2:GetVec3(), CargoDeployHeading ) - self.CargoCarrier = nil - - local Points = {} - Points[#Points+1] = FromPointVec2:RoutePointGround( Speed ) - Points[#Points+1] = ToPointVec2:RoutePointGround( Speed ) - - local TaskRoute = self.CargoObject:TaskRoute( Points ) - self.CargoObject:SetTask( TaskRoute, 1 ) - - self:__UnBoarding( 1, ToPointVec2 ) - end - end - -end - ---- Leave UnBoarding State. --- @param #AI_CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To --- @param Core.Point#POINT_VEC2 ToPointVec2 -function AI_CARGO_UNIT:onleaveUnBoarding( From, Event, To, ToPointVec2 ) - self:F( { ToPointVec2, From, Event, To } ) - - local Angle = 180 - local Speed = 10 - local Distance = 5 - - if From == "UnBoarding" then - if self:IsNear( ToPointVec2 ) then - return true - else - self:__UnBoarding( 1, ToPointVec2 ) - end - return false - end - -end - ---- UnBoard Event. --- @param #AI_CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To --- @param Core.Point#POINT_VEC2 ToPointVec2 -function AI_CARGO_UNIT:onafterUnBoarding( From, Event, To, ToPointVec2 ) - self:F( { ToPointVec2, From, Event, To } ) - - self.CargoInAir = self.CargoObject:InAir() - - self:T( self.CargoInAir ) - - -- Only unboard the cargo when the carrier is not in the air. - -- (eg. cargo can be on a oil derrick, moving the cargo on the oil derrick will drop the cargo on the sea). - if not self.CargoInAir then - - end - - self:__UnLoad( 1, ToPointVec2 ) - -end - - - ---- Enter UnLoaded State. --- @param #AI_CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To --- @param Core.Point#POINT_VEC2 -function AI_CARGO_UNIT:onenterUnLoaded( From, Event, To, ToPointVec2 ) - self:F( { ToPointVec2, From, Event, To } ) - - local Angle = 180 - local Speed = 10 - local Distance = 5 - - if From == "Loaded" then - local StartPointVec2 = self.CargoCarrier:GetPointVec2() - local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - local CargoDeployPointVec2 = StartPointVec2:Translate( Distance, CargoDeployHeading ) - - ToPointVec2 = ToPointVec2 or POINT_VEC2:New( CargoDeployPointVec2:GetX(), CargoDeployPointVec2:GetY() ) - - -- Respawn the group... - if self.CargoObject then - self.CargoObject:ReSpawn( ToPointVec2:GetVec3(), 0 ) - self.CargoCarrier = nil - end - - end - - if self.OnUnLoadedCallBack then - self.OnUnLoadedCallBack( self, unpack( self.OnUnLoadedParameters ) ) - self.OnUnLoadedCallBack = nil - end - -end - - - ---- Enter Boarding State. --- @param #AI_CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT CargoCarrier -function AI_CARGO_UNIT:onenterBoarding( From, Event, To, CargoCarrier ) - self:F( { CargoCarrier.UnitName, From, Event, To } ) - - local Speed = 10 - local Angle = 180 - local Distance = 5 - - if From == "UnLoaded" then - local CargoCarrierPointVec2 = CargoCarrier:GetPointVec2() - local CargoCarrierHeading = CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - local CargoDeployPointVec2 = CargoCarrierPointVec2:Translate( Distance, CargoDeployHeading ) - - local Points = {} - - local PointStartVec2 = self.CargoObject:GetPointVec2() - - Points[#Points+1] = PointStartVec2:RoutePointGround( Speed ) - Points[#Points+1] = CargoDeployPointVec2:RoutePointGround( Speed ) - - local TaskRoute = self.CargoObject:TaskRoute( Points ) - self.CargoObject:SetTask( TaskRoute, 2 ) - end - -end - ---- Leave Boarding State. --- @param #AI_CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT CargoCarrier -function AI_CARGO_UNIT:onleaveBoarding( From, Event, To, CargoCarrier ) - self:F( { CargoCarrier.UnitName, From, Event, To } ) - - if self:IsNear( CargoCarrier:GetPointVec2() ) then - self:__Load( 1, CargoCarrier ) - return true - else - self:__Boarding( 1, CargoCarrier ) - end - return false -end - ---- Loaded State. --- @param #AI_CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT CargoCarrier -function AI_CARGO_UNIT:onenterLoaded( From, Event, To, CargoCarrier ) - self:F() - - self.CargoCarrier = CargoCarrier - - -- Only destroy the CargoObject is if there is a CargoObject (packages don't have CargoObjects). - if self.CargoObject then - self:T("Destroying") - self.CargoObject:Destroy() - end -end - - ---- Board Event. --- @param #AI_CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To -function AI_CARGO_UNIT:onafterBoard( From, Event, To, CargoCarrier ) - self:F() - - self.CargoInAir = self.CargoObject:InAir() - - self:T( self.CargoInAir ) - - -- Only move the group to the carrier when the cargo is not in the air - -- (eg. cargo can be on a oil derrick, moving the cargo on the oil derrick will drop the cargo on the sea). - if not self.CargoInAir then - self:Load( CargoCarrier ) - end - -end - -end - -do -- AI_CARGO_PACKAGE - - --- @type AI_CARGO_PACKAGE - -- @extends #AI_CARGO_REPRESENTABLE - AI_CARGO_PACKAGE = { - ClassName = "AI_CARGO_PACKAGE" - } - ---- AI_CARGO_PACKAGE Constructor. --- @param #AI_CARGO_PACKAGE self --- @param Wrapper.Unit#UNIT CargoCarrier The UNIT carrying the package. --- @param #string Type --- @param #string Name --- @param #number Weight --- @param #number ReportRadius (optional) --- @param #number NearRadius (optional) --- @return #AI_CARGO_PACKAGE -function AI_CARGO_PACKAGE:New( CargoCarrier, Type, Name, Weight, ReportRadius, NearRadius ) - local self = BASE:Inherit( self, AI_CARGO_REPRESENTABLE:New( CargoCarrier, Type, Name, Weight, ReportRadius, NearRadius ) ) -- #AI_CARGO_PACKAGE - self:F( { Type, Name, Weight, ReportRadius, NearRadius } ) - - self:T( CargoCarrier ) - self.CargoCarrier = CargoCarrier - - return self -end - ---- Board Event. --- @param #AI_CARGO_PACKAGE self --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT CargoCarrier --- @param #number Speed --- @param #number BoardDistance --- @param #number Angle -function AI_CARGO_PACKAGE:onafterOnBoard( From, Event, To, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) - self:F() - - self.CargoInAir = self.CargoCarrier:InAir() - - self:T( self.CargoInAir ) - - -- Only move the CargoCarrier to the New CargoCarrier when the New CargoCarrier is not in the air. - if not self.CargoInAir then - - local Points = {} - - local StartPointVec2 = self.CargoCarrier:GetPointVec2() - local CargoCarrierHeading = CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - self:T( { CargoCarrierHeading, CargoDeployHeading } ) - local CargoDeployPointVec2 = CargoCarrier:GetPointVec2():Translate( BoardDistance, CargoDeployHeading ) - - Points[#Points+1] = StartPointVec2:RoutePointGround( Speed ) - Points[#Points+1] = CargoDeployPointVec2:RoutePointGround( Speed ) - - local TaskRoute = self.CargoCarrier:TaskRoute( Points ) - self.CargoCarrier:SetTask( TaskRoute, 1 ) - end - - self:Boarded( CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) - -end - ---- Check if CargoCarrier is near the Cargo to be Loaded. --- @param #AI_CARGO_PACKAGE self --- @param Wrapper.Unit#UNIT CargoCarrier --- @return #boolean -function AI_CARGO_PACKAGE:IsNear( CargoCarrier ) - self:F() - - local CargoCarrierPoint = CargoCarrier:GetPointVec2() - - local Distance = CargoCarrierPoint:DistanceFromPointVec2( self.CargoCarrier:GetPointVec2() ) - self:T( Distance ) - - if Distance <= self.NearRadius then - return true - else - return false - end -end - ---- Boarded Event. --- @param #AI_CARGO_PACKAGE self --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT CargoCarrier -function AI_CARGO_PACKAGE:onafterOnBoarded( From, Event, To, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) - self:F() - - if self:IsNear( CargoCarrier ) then - self:__Load( 1, CargoCarrier, Speed, LoadDistance, Angle ) - else - self:__Boarded( 1, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) - end -end - ---- UnBoard Event. --- @param #AI_CARGO_PACKAGE self --- @param #string Event --- @param #string From --- @param #string To --- @param #number Speed --- @param #number UnLoadDistance --- @param #number UnBoardDistance --- @param #number Radius --- @param #number Angle -function AI_CARGO_PACKAGE:onafterUnBoard( From, Event, To, CargoCarrier, Speed, UnLoadDistance, UnBoardDistance, Radius, Angle ) - self:F() - - self.CargoInAir = self.CargoCarrier:InAir() - - self:T( self.CargoInAir ) - - -- Only unboard the cargo when the carrier is not in the air. - -- (eg. cargo can be on a oil derrick, moving the cargo on the oil derrick will drop the cargo on the sea). - if not self.CargoInAir then - - self:_Next( self.FsmP.UnLoad, UnLoadDistance, Angle ) - - local Points = {} - - local StartPointVec2 = CargoCarrier:GetPointVec2() - local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - self:T( { CargoCarrierHeading, CargoDeployHeading } ) - local CargoDeployPointVec2 = StartPointVec2:Translate( UnBoardDistance, CargoDeployHeading ) - - Points[#Points+1] = StartPointVec2:RoutePointGround( Speed ) - Points[#Points+1] = CargoDeployPointVec2:RoutePointGround( Speed ) - - local TaskRoute = CargoCarrier:TaskRoute( Points ) - CargoCarrier:SetTask( TaskRoute, 1 ) - end - - self:__UnBoarded( 1 , CargoCarrier, Speed ) - -end - ---- UnBoarded Event. --- @param #AI_CARGO_PACKAGE self --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT CargoCarrier -function AI_CARGO_PACKAGE:onafterUnBoarded( From, Event, To, CargoCarrier, Speed ) - self:F() - - if self:IsNear( CargoCarrier ) then - self:__UnLoad( 1, CargoCarrier, Speed ) - else - self:__UnBoarded( 1, CargoCarrier, Speed ) - end -end - ---- Load Event. --- @param #AI_CARGO_PACKAGE self --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT CargoCarrier --- @param #number Speed --- @param #number LoadDistance --- @param #number Angle -function AI_CARGO_PACKAGE:onafterLoad( From, Event, To, CargoCarrier, Speed, LoadDistance, Angle ) - self:F() - - self.CargoCarrier = CargoCarrier - - local StartPointVec2 = self.CargoCarrier:GetPointVec2() - local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - local CargoDeployPointVec2 = StartPointVec2:Translate( LoadDistance, CargoDeployHeading ) - - local Points = {} - Points[#Points+1] = StartPointVec2:RoutePointGround( Speed ) - Points[#Points+1] = CargoDeployPointVec2:RoutePointGround( Speed ) - - local TaskRoute = self.CargoCarrier:TaskRoute( Points ) - self.CargoCarrier:SetTask( TaskRoute, 1 ) - -end - ---- UnLoad Event. --- @param #AI_CARGO_PACKAGE self --- @param #string Event --- @param #string From --- @param #string To --- @param #number Distance --- @param #number Angle -function AI_CARGO_PACKAGE:onafterUnLoad( From, Event, To, CargoCarrier, Speed, Distance, Angle ) - self:F() - - local StartPointVec2 = self.CargoCarrier:GetPointVec2() - local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - local CargoDeployPointVec2 = StartPointVec2:Translate( Distance, CargoDeployHeading ) - - self.CargoCarrier = CargoCarrier - - local Points = {} - Points[#Points+1] = StartPointVec2:RoutePointGround( Speed ) - Points[#Points+1] = CargoDeployPointVec2:RoutePointGround( Speed ) - - local TaskRoute = self.CargoCarrier:TaskRoute( Points ) - self.CargoCarrier:SetTask( TaskRoute, 1 ) - -end - - -end - -do -- AI_CARGO_GROUP - - --- @type AI_CARGO_GROUP - -- @extends AI.AI_Cargo#AI_CARGO - -- @field Set#SET_BASE CargoSet A set of cargo objects. - -- @field #string Name A string defining the name of the cargo group. The name is the unique identifier of the cargo. - AI_CARGO_GROUP = { - ClassName = "AI_CARGO_GROUP", - } - ---- AI_CARGO_GROUP constructor. --- @param #AI_CARGO_GROUP self --- @param Core.Set#Set_BASE CargoSet --- @param #string Type --- @param #string Name --- @param #number Weight --- @param #number ReportRadius (optional) --- @param #number NearRadius (optional) --- @return #AI_CARGO_GROUP -function AI_CARGO_GROUP:New( CargoSet, Type, Name, ReportRadius, NearRadius ) - local self = BASE:Inherit( self, AI_CARGO:New( Type, Name, 0, ReportRadius, NearRadius ) ) -- #AI_CARGO_GROUP - self:F( { Type, Name, ReportRadius, NearRadius } ) - - self.CargoSet = CargoSet - - - return self -end - -end -- AI_CARGO_GROUP - -do -- AI_CARGO_GROUPED - - --- @type AI_CARGO_GROUPED - -- @extends AI.AI_Cargo#AI_CARGO_GROUP - AI_CARGO_GROUPED = { - ClassName = "AI_CARGO_GROUPED", - } - ---- AI_CARGO_GROUPED constructor. --- @param #AI_CARGO_GROUPED self --- @param Core.Set#Set_BASE CargoSet --- @param #string Type --- @param #string Name --- @param #number Weight --- @param #number ReportRadius (optional) --- @param #number NearRadius (optional) --- @return #AI_CARGO_GROUPED -function AI_CARGO_GROUPED:New( CargoSet, Type, Name, ReportRadius, NearRadius ) - local self = BASE:Inherit( self, AI_CARGO_GROUP:New( CargoSet, Type, Name, ReportRadius, NearRadius ) ) -- #AI_CARGO_GROUPED - self:F( { Type, Name, ReportRadius, NearRadius } ) - - return self -end - ---- Enter Boarding State. --- @param #AI_CARGO_GROUPED self --- @param Wrapper.Unit#UNIT CargoCarrier --- @param #string Event --- @param #string From --- @param #string To -function AI_CARGO_GROUPED:onenterBoarding( From, Event, To, CargoCarrier ) - self:F( { CargoCarrier.UnitName, From, Event, To } ) - - if From == "UnLoaded" then - - -- For each Cargo object within the AI_CARGO_GROUPED, route each object to the CargoLoadPointVec2 - self.CargoSet:ForEach( - function( Cargo ) - Cargo:__Board( 1, CargoCarrier ) - end - ) - - self:__Boarding( 1, CargoCarrier ) - end - -end - ---- Enter Loaded State. --- @param #AI_CARGO_GROUPED self --- @param Wrapper.Unit#UNIT CargoCarrier --- @param #string Event --- @param #string From --- @param #string To -function AI_CARGO_GROUPED:onenterLoaded( From, Event, To, CargoCarrier ) - self:F( { CargoCarrier.UnitName, From, Event, To } ) - - if From == "UnLoaded" then - -- For each Cargo object within the AI_CARGO_GROUPED, load each cargo to the CargoCarrier. - for CargoID, Cargo in pairs( self.CargoSet:GetSet() ) do - Cargo:Load( CargoCarrier ) - end - end -end - ---- Leave Boarding State. --- @param #AI_CARGO_GROUPED self --- @param Wrapper.Unit#UNIT CargoCarrier --- @param #string Event --- @param #string From --- @param #string To -function AI_CARGO_GROUPED:onleaveBoarding( From, Event, To, CargoCarrier ) - self:F( { CargoCarrier.UnitName, From, Event, To } ) - - local Boarded = true - - -- For each Cargo object within the AI_CARGO_GROUPED, route each object to the CargoLoadPointVec2 - for CargoID, Cargo in pairs( self.CargoSet:GetSet() ) do - self:T( Cargo.current ) - if not Cargo:is( "Loaded" ) then - Boarded = false - end - end - - if not Boarded then - self:__Boarding( 1, CargoCarrier ) - else - self:__Load( 1, CargoCarrier ) - end - return Boarded -end - ---- Enter UnBoarding State. --- @param #AI_CARGO_GROUPED self --- @param Core.Point#POINT_VEC2 ToPointVec2 --- @param #string Event --- @param #string From --- @param #string To -function AI_CARGO_GROUPED:onenterUnBoarding( From, Event, To, ToPointVec2 ) - self:F() - - local Timer = 1 - - if From == "Loaded" then - - -- For each Cargo object within the AI_CARGO_GROUPED, route each object to the CargoLoadPointVec2 - self.CargoSet:ForEach( - function( Cargo ) - Cargo:__UnBoard( Timer, ToPointVec2 ) - Timer = Timer + 10 - end - ) - - self:__UnBoarding( 1, ToPointVec2 ) - end - -end - ---- Leave UnBoarding State. --- @param #AI_CARGO_GROUPED self --- @param Core.Point#POINT_VEC2 ToPointVec2 --- @param #string Event --- @param #string From --- @param #string To -function AI_CARGO_GROUPED:onleaveUnBoarding( From, Event, To, ToPointVec2 ) - self:F( { ToPointVec2, From, Event, To } ) - - local Angle = 180 - local Speed = 10 - local Distance = 5 - - if From == "UnBoarding" then - local UnBoarded = true - - -- For each Cargo object within the AI_CARGO_GROUPED, route each object to the CargoLoadPointVec2 - for CargoID, Cargo in pairs( self.CargoSet:GetSet() ) do - self:T( Cargo.current ) - if not Cargo:is( "UnLoaded" ) then - UnBoarded = false - end - end - - if UnBoarded then - return true - else - self:__UnBoarding( 1, ToPointVec2 ) - end - - return false - end - -end - ---- UnBoard Event. --- @param #AI_CARGO_GROUPED self --- @param Core.Point#POINT_VEC2 ToPointVec2 --- @param #string Event --- @param #string From --- @param #string To -function AI_CARGO_GROUPED:onafterUnBoarding( From, Event, To, ToPointVec2 ) - self:F( { ToPointVec2, From, Event, To } ) - - self:__UnLoad( 1, ToPointVec2 ) -end - - - ---- Enter UnLoaded State. --- @param #AI_CARGO_GROUPED self --- @param Core.Point#POINT_VEC2 --- @param #string Event --- @param #string From --- @param #string To -function AI_CARGO_GROUPED:onenterUnLoaded( From, Event, To, ToPointVec2 ) - self:F( { ToPointVec2, From, Event, To } ) - - if From == "Loaded" then - - -- For each Cargo object within the AI_CARGO_GROUPED, route each object to the CargoLoadPointVec2 - self.CargoSet:ForEach( - function( Cargo ) - Cargo:UnLoad( ToPointVec2 ) - end - ) - - end - -end - -end -- AI_CARGO_GROUPED - - - ---- (SP) (MP) (FSM) Accept or reject process for player (task) assignments. --- --- === --- --- # @{#ACT_ASSIGN} FSM template class, extends @{Fsm#FSM_PROCESS} --- --- ## ACT_ASSIGN state machine: --- --- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur. --- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below. --- Each derived class follows exactly the same process, using the same events and following the same state transitions, --- but will have **different implementation behaviour** upon each event or state transition. --- --- ### ACT_ASSIGN **Events**: --- --- These are the events defined in this class: --- --- * **Start**: Start the tasking acceptance process. --- * **Assign**: Assign the task. --- * **Reject**: Reject the task.. --- --- ### ACT_ASSIGN **Event methods**: --- --- Event methods are available (dynamically allocated by the state machine), that accomodate for state transitions occurring in the process. --- There are two types of event methods, which you can use to influence the normal mechanisms in the state machine: --- --- * **Immediate**: The event method has exactly the name of the event. --- * **Delayed**: The event method starts with a __ + the name of the event. The first parameter of the event method is a number value, expressing the delay in seconds when the event will be executed. --- --- ### ACT_ASSIGN **States**: --- --- * **UnAssigned**: The player has not accepted the task. --- * **Assigned (*)**: The player has accepted the task. --- * **Rejected (*)**: The player has not accepted the task. --- * **Waiting**: The process is awaiting player feedback. --- * **Failed (*)**: The process has failed. --- --- (*) End states of the process. --- --- ### ACT_ASSIGN state transition methods: --- --- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. --- There are 2 moments when state transition methods will be called by the state machine: --- --- * **Before** the state transition. --- The state transition method needs to start with the name **OnBefore + the name of the state**. --- If the state transition method returns false, then the processing of the state transition will not be done! --- If you want to change the behaviour of the AIControllable at this event, return false, --- but then you'll need to specify your own logic using the AIControllable! --- --- * **After** the state transition. --- The state transition method needs to start with the name **OnAfter + the name of the state**. --- These state transition methods need to provide a return value, which is specified at the function description. --- --- === --- --- # 1) @{#ACT_ASSIGN_ACCEPT} class, extends @{Fsm.Assign#ACT_ASSIGN} --- --- The ACT_ASSIGN_ACCEPT class accepts by default a task for a player. No player intervention is allowed to reject the task. --- --- ## 1.1) ACT_ASSIGN_ACCEPT constructor: --- --- * @{#ACT_ASSIGN_ACCEPT.New}(): Creates a new ACT_ASSIGN_ACCEPT object. --- --- === --- --- # 2) @{#ACT_ASSIGN_MENU_ACCEPT} class, extends @{Fsm.Assign#ACT_ASSIGN} --- --- The ACT_ASSIGN_MENU_ACCEPT class accepts a task when the player accepts the task through an added menu option. --- This assignment type is useful to conditionally allow the player to choose whether or not he would accept the task. --- The assignment type also allows to reject the task. --- --- ## 2.1) ACT_ASSIGN_MENU_ACCEPT constructor: --- ----------------------------------------- --- --- * @{#ACT_ASSIGN_MENU_ACCEPT.New}(): Creates a new ACT_ASSIGN_MENU_ACCEPT object. --- --- === --- --- @module Assign - - -do -- ACT_ASSIGN - - --- ACT_ASSIGN class - -- @type ACT_ASSIGN - -- @field Tasking.Task#TASK Task - -- @field Wrapper.Unit#UNIT ProcessUnit - -- @field Core.Zone#ZONE_BASE TargetZone - -- @extends Core.Fsm#FSM_PROCESS - ACT_ASSIGN = { - ClassName = "ACT_ASSIGN", - } - - - --- Creates a new task assignment state machine. The process will accept the task by default, no player intervention accepted. - -- @param #ACT_ASSIGN self - -- @return #ACT_ASSIGN The task acceptance process. - function ACT_ASSIGN:New() - - -- Inherits from BASE - local self = BASE:Inherit( self, FSM_PROCESS:New( "ACT_ASSIGN" ) ) -- Core.Fsm#FSM_PROCESS - - self:AddTransition( "UnAssigned", "Start", "Waiting" ) - self:AddTransition( "Waiting", "Assign", "Assigned" ) - self:AddTransition( "Waiting", "Reject", "Rejected" ) - self:AddTransition( "*", "Fail", "Failed" ) - - self:AddEndState( "Assigned" ) - self:AddEndState( "Rejected" ) - self:AddEndState( "Failed" ) - - self:SetStartState( "UnAssigned" ) - - return self - end - -end -- ACT_ASSIGN - - - -do -- ACT_ASSIGN_ACCEPT - - --- ACT_ASSIGN_ACCEPT class - -- @type ACT_ASSIGN_ACCEPT - -- @field Tasking.Task#TASK Task - -- @field Wrapper.Unit#UNIT ProcessUnit - -- @field Core.Zone#ZONE_BASE TargetZone - -- @extends #ACT_ASSIGN - ACT_ASSIGN_ACCEPT = { - ClassName = "ACT_ASSIGN_ACCEPT", - } - - - --- Creates a new task assignment state machine. The process will accept the task by default, no player intervention accepted. - -- @param #ACT_ASSIGN_ACCEPT self - -- @param #string TaskBriefing - function ACT_ASSIGN_ACCEPT:New( TaskBriefing ) - - local self = BASE:Inherit( self, ACT_ASSIGN:New() ) -- #ACT_ASSIGN_ACCEPT - - self.TaskBriefing = TaskBriefing - - return self - end - - function ACT_ASSIGN_ACCEPT:Init( FsmAssign ) - - self.TaskBriefing = FsmAssign.TaskBriefing - end - - --- StateMachine callback function - -- @param #ACT_ASSIGN_ACCEPT self - -- @param Wrapper.Unit#UNIT ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ASSIGN_ACCEPT:onafterStart( ProcessUnit, From, Event, To ) - self:E( { ProcessUnit, From, Event, To } ) - - self:__Assign( 1 ) - end - - --- StateMachine callback function - -- @param #ACT_ASSIGN_ACCEPT self - -- @param Wrapper.Unit#UNIT ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ASSIGN_ACCEPT:onenterAssigned( ProcessUnit, From, Event, To ) - env.info( "in here" ) - self:E( { ProcessUnit, From, Event, To } ) - - local ProcessGroup = ProcessUnit:GetGroup() - - self:Message( "You are assigned to the task " .. self.Task:GetName() ) - - self.Task:Assign() - end - -end -- ACT_ASSIGN_ACCEPT - - -do -- ACT_ASSIGN_MENU_ACCEPT - - --- ACT_ASSIGN_MENU_ACCEPT class - -- @type ACT_ASSIGN_MENU_ACCEPT - -- @field Tasking.Task#TASK Task - -- @field Wrapper.Unit#UNIT ProcessUnit - -- @field Core.Zone#ZONE_BASE TargetZone - -- @extends #ACT_ASSIGN - ACT_ASSIGN_MENU_ACCEPT = { - ClassName = "ACT_ASSIGN_MENU_ACCEPT", - } - - --- Init. - -- @param #ACT_ASSIGN_MENU_ACCEPT self - -- @param #string TaskName - -- @param #string TaskBriefing - -- @return #ACT_ASSIGN_MENU_ACCEPT self - function ACT_ASSIGN_MENU_ACCEPT:New( TaskName, TaskBriefing ) - - -- Inherits from BASE - local self = BASE:Inherit( self, ACT_ASSIGN:New() ) -- #ACT_ASSIGN_MENU_ACCEPT - - self.TaskName = TaskName - self.TaskBriefing = TaskBriefing - - return self - end - - function ACT_ASSIGN_MENU_ACCEPT:Init( FsmAssign ) - - self.TaskName = FsmAssign.TaskName - self.TaskBriefing = FsmAssign.TaskBriefing - end - - - --- Creates a new task assignment state machine. The process will request from the menu if it accepts the task, if not, the unit is removed from the simulator. - -- @param #ACT_ASSIGN_MENU_ACCEPT self - -- @param #string TaskName - -- @param #string TaskBriefing - -- @return #ACT_ASSIGN_MENU_ACCEPT self - function ACT_ASSIGN_MENU_ACCEPT:Init( TaskName, TaskBriefing ) - - self.TaskBriefing = TaskBriefing - self.TaskName = TaskName - - return self - end - - --- StateMachine callback function - -- @param #ACT_ASSIGN_MENU_ACCEPT self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ASSIGN_MENU_ACCEPT:onafterStart( ProcessUnit, From, Event, To ) - self:E( { ProcessUnit, From, Event, To } ) - - self:Message( "Access the radio menu to accept the task. You have 30 seconds or the assignment will be cancelled." ) - - local ProcessGroup = ProcessUnit:GetGroup() - - self.Menu = MENU_GROUP:New( ProcessGroup, "Task " .. self.TaskName .. " acceptance" ) - self.MenuAcceptTask = MENU_GROUP_COMMAND:New( ProcessGroup, "Accept task " .. self.TaskName, self.Menu, self.MenuAssign, self ) - self.MenuRejectTask = MENU_GROUP_COMMAND:New( ProcessGroup, "Reject task " .. self.TaskName, self.Menu, self.MenuReject, self ) - end - - --- Menu function. - -- @param #ACT_ASSIGN_MENU_ACCEPT self - function ACT_ASSIGN_MENU_ACCEPT:MenuAssign() - self:E( ) - - self:__Assign( 1 ) - end - - --- Menu function. - -- @param #ACT_ASSIGN_MENU_ACCEPT self - function ACT_ASSIGN_MENU_ACCEPT:MenuReject() - self:E( ) - - self:__Reject( 1 ) - end - - --- StateMachine callback function - -- @param #ACT_ASSIGN_MENU_ACCEPT self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ASSIGN_MENU_ACCEPT:onafterAssign( ProcessUnit, From, Event, To ) - self:E( { ProcessUnit.UnitNameFrom, Event, To } ) - - self.Menu:Remove() - end - - --- StateMachine callback function - -- @param #ACT_ASSIGN_MENU_ACCEPT self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ASSIGN_MENU_ACCEPT:onafterReject( ProcessUnit, From, Event, To ) - self:E( { ProcessUnit.UnitName, From, Event, To } ) - - self.Menu:Remove() - --TODO: need to resolve this problem ... it has to do with the events ... - --self.Task:UnAssignFromUnit( ProcessUnit )needs to become a callback funtion call upon the event - ProcessUnit:Destroy() - end - -end -- ACT_ASSIGN_MENU_ACCEPT ---- (SP) (MP) (FSM) Route AI or players through waypoints or to zones. --- --- === --- --- # @{#ACT_ROUTE} FSM class, extends @{Fsm#FSM_PROCESS} --- --- ## ACT_ROUTE state machine: --- --- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur. --- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below. --- Each derived class follows exactly the same process, using the same events and following the same state transitions, --- but will have **different implementation behaviour** upon each event or state transition. --- --- ### ACT_ROUTE **Events**: --- --- These are the events defined in this class: --- --- * **Start**: The process is started. The process will go into the Report state. --- * **Report**: The process is reporting to the player the route to be followed. --- * **Route**: The process is routing the controllable. --- * **Pause**: The process is pausing the route of the controllable. --- * **Arrive**: The controllable has arrived at a route point. --- * **More**: There are more route points that need to be followed. The process will go back into the Report state. --- * **NoMore**: There are no more route points that need to be followed. The process will go into the Success state. --- --- ### ACT_ROUTE **Event methods**: --- --- Event methods are available (dynamically allocated by the state machine), that accomodate for state transitions occurring in the process. --- There are two types of event methods, which you can use to influence the normal mechanisms in the state machine: --- --- * **Immediate**: The event method has exactly the name of the event. --- * **Delayed**: The event method starts with a __ + the name of the event. The first parameter of the event method is a number value, expressing the delay in seconds when the event will be executed. --- --- ### ACT_ROUTE **States**: --- --- * **None**: The controllable did not receive route commands. --- * **Arrived (*)**: The controllable has arrived at a route point. --- * **Aborted (*)**: The controllable has aborted the route path. --- * **Routing**: The controllable is understay to the route point. --- * **Pausing**: The process is pausing the routing. AI air will go into hover, AI ground will stop moving. Players can fly around. --- * **Success (*)**: All route points were reached. --- * **Failed (*)**: The process has failed. --- --- (*) End states of the process. --- --- ### ACT_ROUTE state transition methods: --- --- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. --- There are 2 moments when state transition methods will be called by the state machine: --- --- * **Before** the state transition. --- The state transition method needs to start with the name **OnBefore + the name of the state**. --- If the state transition method returns false, then the processing of the state transition will not be done! --- If you want to change the behaviour of the AIControllable at this event, return false, --- but then you'll need to specify your own logic using the AIControllable! --- --- * **After** the state transition. --- The state transition method needs to start with the name **OnAfter + the name of the state**. --- These state transition methods need to provide a return value, which is specified at the function description. --- --- === --- --- # 1) @{#ACT_ROUTE_ZONE} class, extends @{Fsm.Route#ACT_ROUTE} --- --- The ACT_ROUTE_ZONE class implements the core functions to route an AIR @{Controllable} player @{Unit} to a @{Zone}. --- The player receives on perioding times messages with the coordinates of the route to follow. --- Upon arrival at the zone, a confirmation of arrival is sent, and the process will be ended. --- --- # 1.1) ACT_ROUTE_ZONE constructor: --- --- * @{#ACT_ROUTE_ZONE.New}(): Creates a new ACT_ROUTE_ZONE object. --- --- === --- --- @module Route - - -do -- ACT_ROUTE - - --- ACT_ROUTE class - -- @type ACT_ROUTE - -- @field Tasking.Task#TASK TASK - -- @field Wrapper.Unit#UNIT ProcessUnit - -- @field Core.Zone#ZONE_BASE TargetZone - -- @extends Core.Fsm#FSM_PROCESS - ACT_ROUTE = { - ClassName = "ACT_ROUTE", - } - - - --- Creates a new routing state machine. The process will route a CLIENT to a ZONE until the CLIENT is within that ZONE. - -- @param #ACT_ROUTE self - -- @return #ACT_ROUTE self - function ACT_ROUTE:New() - - -- Inherits from BASE - local self = BASE:Inherit( self, FSM_PROCESS:New( "ACT_ROUTE" ) ) -- Core.Fsm#FSM_PROCESS - - self:AddTransition( "None", "Start", "Routing" ) - self:AddTransition( "*", "Report", "Reporting" ) - self:AddTransition( "*", "Route", "Routing" ) - self:AddTransition( "Routing", "Pause", "Pausing" ) - self:AddTransition( "*", "Abort", "Aborted" ) - self:AddTransition( "Routing", "Arrive", "Arrived" ) - self:AddTransition( "Arrived", "Success", "Success" ) - self:AddTransition( "*", "Fail", "Failed" ) - self:AddTransition( "", "", "" ) - self:AddTransition( "", "", "" ) - - self:AddEndState( "Arrived" ) - self:AddEndState( "Failed" ) - - self:SetStartState( "None" ) - - return self - end - - --- Task Events - - --- StateMachine callback function - -- @param #ACT_ROUTE self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ROUTE:onafterStart( ProcessUnit, From, Event, To ) - - - self:__Route( 1 ) - end - - --- Check if the controllable has arrived. - -- @param #ACT_ROUTE self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @return #boolean - function ACT_ROUTE:onfuncHasArrived( ProcessUnit ) - return false - end - - --- StateMachine callback function - -- @param #ACT_ROUTE self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ROUTE:onbeforeRoute( ProcessUnit, From, Event, To ) - self:F( { "BeforeRoute 1", self.DisplayCount, self.DisplayInterval } ) - - if ProcessUnit:IsAlive() then - self:F( "BeforeRoute 2" ) - local HasArrived = self:onfuncHasArrived( ProcessUnit ) -- Polymorphic - if self.DisplayCount >= self.DisplayInterval then - self:T( { HasArrived = HasArrived } ) - if not HasArrived then - self:Report() - end - self.DisplayCount = 1 - else - self.DisplayCount = self.DisplayCount + 1 - end - - self:T( { DisplayCount = self.DisplayCount } ) - - if HasArrived then - self:__Arrive( 1 ) - else - self:__Route( 1 ) - end - - return HasArrived -- if false, then the event will not be executed... - end - - return false - - end - -end -- ACT_ROUTE - - - -do -- ACT_ROUTE_ZONE - - --- ACT_ROUTE_ZONE class - -- @type ACT_ROUTE_ZONE - -- @field Tasking.Task#TASK TASK - -- @field Wrapper.Unit#UNIT ProcessUnit - -- @field Core.Zone#ZONE_BASE TargetZone - -- @extends #ACT_ROUTE - ACT_ROUTE_ZONE = { - ClassName = "ACT_ROUTE_ZONE", - } - - - --- Creates a new routing state machine. The task will route a controllable to a ZONE until the controllable is within that ZONE. - -- @param #ACT_ROUTE_ZONE self - -- @param Core.Zone#ZONE_BASE TargetZone - function ACT_ROUTE_ZONE:New( TargetZone ) - local self = BASE:Inherit( self, ACT_ROUTE:New() ) -- #ACT_ROUTE_ZONE - - self.TargetZone = TargetZone - - self.DisplayInterval = 30 - self.DisplayCount = 30 - self.DisplayMessage = true - self.DisplayTime = 10 -- 10 seconds is the default - - return self - end - - function ACT_ROUTE_ZONE:Init( FsmRoute ) - - self.TargetZone = FsmRoute.TargetZone - - self.DisplayInterval = 30 - self.DisplayCount = 30 - self.DisplayMessage = true - self.DisplayTime = 10 -- 10 seconds is the default - end - - --- Method override to check if the controllable has arrived. - -- @param #ACT_ROUTE self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @return #boolean - function ACT_ROUTE_ZONE:onfuncHasArrived( ProcessUnit ) - - if ProcessUnit:IsInZone( self.TargetZone ) then - local RouteText = "You have arrived within the zone." - self:Message( RouteText ) - end - - return ProcessUnit:IsInZone( self.TargetZone ) - end - - --- Task Events - - --- StateMachine callback function - -- @param #ACT_ROUTE_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ROUTE_ZONE:onenterReporting( ProcessUnit, From, Event, To ) - - local ZoneVec2 = self.TargetZone:GetVec2() - local ZonePointVec2 = POINT_VEC2:New( ZoneVec2.x, ZoneVec2.y ) - local TaskUnitVec2 = ProcessUnit:GetVec2() - local TaskUnitPointVec2 = POINT_VEC2:New( TaskUnitVec2.x, TaskUnitVec2.y ) - local RouteText = "Route to " .. TaskUnitPointVec2:GetBRText( ZonePointVec2 ) .. " km to target." - self:Message( RouteText ) - end - -end -- ACT_ROUTE_ZONE ---- (SP) (MP) (FSM) Account for (Detect, count and report) DCS events occuring on DCS objects (units). --- --- === --- --- # @{#ACT_ACCOUNT} FSM class, extends @{Fsm#FSM_PROCESS} --- --- ## ACT_ACCOUNT state machine: --- --- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur. --- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below. --- Each derived class follows exactly the same process, using the same events and following the same state transitions, --- but will have **different implementation behaviour** upon each event or state transition. --- --- ### ACT_ACCOUNT **Events**: --- --- These are the events defined in this class: --- --- * **Start**: The process is started. The process will go into the Report state. --- * **Event**: A relevant event has occured that needs to be accounted for. The process will go into the Account state. --- * **Report**: The process is reporting to the player the accounting status of the DCS events. --- * **More**: There are more DCS events that need to be accounted for. The process will go back into the Report state. --- * **NoMore**: There are no more DCS events that need to be accounted for. The process will go into the Success state. --- --- ### ACT_ACCOUNT **Event methods**: --- --- Event methods are available (dynamically allocated by the state machine), that accomodate for state transitions occurring in the process. --- There are two types of event methods, which you can use to influence the normal mechanisms in the state machine: --- --- * **Immediate**: The event method has exactly the name of the event. --- * **Delayed**: The event method starts with a __ + the name of the event. The first parameter of the event method is a number value, expressing the delay in seconds when the event will be executed. --- --- ### ACT_ACCOUNT **States**: --- --- * **Assigned**: The player is assigned to the task. This is the initialization state for the process. --- * **Waiting**: the process is waiting for a DCS event to occur within the simulator. This state is set automatically. --- * **Report**: The process is Reporting to the players in the group of the unit. This state is set automatically every 30 seconds. --- * **Account**: The relevant DCS event has occurred, and is accounted for. --- * **Success (*)**: All DCS events were accounted for. --- * **Failed (*)**: The process has failed. --- --- (*) End states of the process. --- --- ### ACT_ACCOUNT state transition methods: --- --- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. --- There are 2 moments when state transition methods will be called by the state machine: --- --- * **Before** the state transition. --- The state transition method needs to start with the name **OnBefore + the name of the state**. --- If the state transition method returns false, then the processing of the state transition will not be done! --- If you want to change the behaviour of the AIControllable at this event, return false, --- but then you'll need to specify your own logic using the AIControllable! --- --- * **After** the state transition. --- The state transition method needs to start with the name **OnAfter + the name of the state**. --- These state transition methods need to provide a return value, which is specified at the function description. --- --- # 1) @{#ACT_ACCOUNT_DEADS} FSM class, extends @{Fsm.Account#ACT_ACCOUNT} --- --- The ACT_ACCOUNT_DEADS class accounts (detects, counts and reports) successful kills of DCS units. --- The process is given a @{Set} of units that will be tracked upon successful destruction. --- The process will end after each target has been successfully destroyed. --- Each successful dead will trigger an Account state transition that can be scored, modified or administered. --- --- --- ## ACT_ACCOUNT_DEADS constructor: --- --- * @{#ACT_ACCOUNT_DEADS.New}(): Creates a new ACT_ACCOUNT_DEADS object. --- --- === --- --- @module Account - - -do -- ACT_ACCOUNT - - --- ACT_ACCOUNT class - -- @type ACT_ACCOUNT - -- @field Set#SET_UNIT TargetSetUnit - -- @extends Core.Fsm#FSM_PROCESS - ACT_ACCOUNT = { - ClassName = "ACT_ACCOUNT", - TargetSetUnit = nil, - } - - --- Creates a new DESTROY process. - -- @param #ACT_ACCOUNT self - -- @return #ACT_ACCOUNT - function ACT_ACCOUNT:New() - - -- Inherits from BASE - local self = BASE:Inherit( self, FSM_PROCESS:New() ) -- Core.Fsm#FSM_PROCESS - - self:AddTransition( "Assigned", "Start", "Waiting") - self:AddTransition( "*", "Wait", "Waiting") - self:AddTransition( "*", "Report", "Report") - self:AddTransition( "*", "Event", "Account") - self:AddTransition( "Account", "More", "Wait") - self:AddTransition( "Account", "NoMore", "Accounted") - self:AddTransition( "*", "Fail", "Failed") - - self:AddEndState( "Accounted" ) - self:AddEndState( "Failed" ) - - self:SetStartState( "Assigned" ) - - return self - end - - --- Process Events - - --- StateMachine callback function - -- @param #ACT_ACCOUNT self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ACCOUNT:onafterStart( ProcessUnit, From, Event, To ) - - self:HandleEvent( EVENTS.Dead, self.onfuncEventDead ) - - self:__Wait( 1 ) - end - - - --- StateMachine callback function - -- @param #ACT_ACCOUNT self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ACCOUNT:onenterWaiting( ProcessUnit, From, Event, To ) - - if self.DisplayCount >= self.DisplayInterval then - self:Report() - self.DisplayCount = 1 - else - self.DisplayCount = self.DisplayCount + 1 - end - - return true -- Process always the event. - end - - --- StateMachine callback function - -- @param #ACT_ACCOUNT self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ACCOUNT:onafterEvent( ProcessUnit, From, Event, To, Event ) - - self:__NoMore( 1 ) - end - -end -- ACT_ACCOUNT - -do -- ACT_ACCOUNT_DEADS - - --- ACT_ACCOUNT_DEADS class - -- @type ACT_ACCOUNT_DEADS - -- @field Set#SET_UNIT TargetSetUnit - -- @extends #ACT_ACCOUNT - ACT_ACCOUNT_DEADS = { - ClassName = "ACT_ACCOUNT_DEADS", - TargetSetUnit = nil, - } - - - --- Creates a new DESTROY process. - -- @param #ACT_ACCOUNT_DEADS self - -- @param Set#SET_UNIT TargetSetUnit - -- @param #string TaskName - function ACT_ACCOUNT_DEADS:New( TargetSetUnit, TaskName ) - -- Inherits from BASE - local self = BASE:Inherit( self, ACT_ACCOUNT:New() ) -- #ACT_ACCOUNT_DEADS - - self.TargetSetUnit = TargetSetUnit - self.TaskName = TaskName - - self.DisplayInterval = 30 - self.DisplayCount = 30 - self.DisplayMessage = true - self.DisplayTime = 10 -- 10 seconds is the default - self.DisplayCategory = "HQ" -- Targets is the default display category - - return self - end - - function ACT_ACCOUNT_DEADS:Init( FsmAccount ) - - self.TargetSetUnit = FsmAccount.TargetSetUnit - self.TaskName = FsmAccount.TaskName - end - - - - function ACT_ACCOUNT_DEADS:_Destructor() - self:E("_Destructor") - - self:EventRemoveAll() - - end - - --- Process Events - - --- StateMachine callback function - -- @param #ACT_ACCOUNT_DEADS self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ACCOUNT_DEADS:onenterReport( ProcessUnit, From, Event, To ) - self:E( { ProcessUnit, From, Event, To } ) - - self:Message( "Your group with assigned " .. self.TaskName .. " task has " .. self.TargetSetUnit:GetUnitTypesText() .. " targets left to be destroyed." ) - end - - - --- StateMachine callback function - -- @param #ACT_ACCOUNT_DEADS self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ACCOUNT_DEADS:onenterAccount( ProcessUnit, From, Event, To, EventData ) - self:T( { ProcessUnit, EventData, From, Event, To } ) - - self:T({self.Controllable}) - - self.TargetSetUnit:Flush() - - if self.TargetSetUnit:FindUnit( EventData.IniUnitName ) then - local TaskGroup = ProcessUnit:GetGroup() - self.TargetSetUnit:RemoveUnitsByName( EventData.IniUnitName ) - self:Message( "You hit a target. Your group with assigned " .. self.TaskName .. " task has " .. self.TargetSetUnit:Count() .. " targets ( " .. self.TargetSetUnit:GetUnitTypesText() .. " ) left to be destroyed." ) - end - end - - --- StateMachine callback function - -- @param #ACT_ACCOUNT_DEADS self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ACCOUNT_DEADS:onafterEvent( ProcessUnit, From, Event, To, EventData ) - - if self.TargetSetUnit:Count() > 0 then - self:__More( 1 ) - else - self:__NoMore( 1 ) - end - end - - --- DCS Events - - --- @param #ACT_ACCOUNT_DEADS self - -- @param Event#EVENTDATA EventData - function ACT_ACCOUNT_DEADS:onfuncEventDead( EventData ) - self:T( { "EventDead", EventData } ) - - if EventData.IniDCSUnit then - self:__Event( 1, EventData ) - end - end - -end -- ACT_ACCOUNT DEADS ---- (SP) (MP) (FSM) Route AI or players through waypoints or to zones. --- --- === --- --- # @{#ACT_ASSIST} FSM class, extends @{Fsm#FSM_PROCESS} --- --- ## ACT_ASSIST state machine: --- --- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur. --- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below. --- Each derived class follows exactly the same process, using the same events and following the same state transitions, --- but will have **different implementation behaviour** upon each event or state transition. --- --- ### ACT_ASSIST **Events**: --- --- These are the events defined in this class: --- --- * **Start**: The process is started. --- * **Next**: The process is smoking the targets in the given zone. --- --- ### ACT_ASSIST **Event methods**: --- --- Event methods are available (dynamically allocated by the state machine), that accomodate for state transitions occurring in the process. --- There are two types of event methods, which you can use to influence the normal mechanisms in the state machine: --- --- * **Immediate**: The event method has exactly the name of the event. --- * **Delayed**: The event method starts with a __ + the name of the event. The first parameter of the event method is a number value, expressing the delay in seconds when the event will be executed. --- --- ### ACT_ASSIST **States**: --- --- * **None**: The controllable did not receive route commands. --- * **AwaitSmoke (*)**: The process is awaiting to smoke the targets in the zone. --- * **Smoking (*)**: The process is smoking the targets in the zone. --- * **Failed (*)**: The process has failed. --- --- (*) End states of the process. --- --- ### ACT_ASSIST state transition methods: --- --- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. --- There are 2 moments when state transition methods will be called by the state machine: --- --- * **Before** the state transition. --- The state transition method needs to start with the name **OnBefore + the name of the state**. --- If the state transition method returns false, then the processing of the state transition will not be done! --- If you want to change the behaviour of the AIControllable at this event, return false, --- but then you'll need to specify your own logic using the AIControllable! --- --- * **After** the state transition. --- The state transition method needs to start with the name **OnAfter + the name of the state**. --- These state transition methods need to provide a return value, which is specified at the function description. --- --- === --- --- # 1) @{#ACT_ASSIST_SMOKE_TARGETS_ZONE} class, extends @{Fsm.Route#ACT_ASSIST} --- --- The ACT_ASSIST_SMOKE_TARGETS_ZONE class implements the core functions to smoke targets in a @{Zone}. --- The targets are smoked within a certain range around each target, simulating a realistic smoking behaviour. --- At random intervals, a new target is smoked. --- --- # 1.1) ACT_ASSIST_SMOKE_TARGETS_ZONE constructor: --- --- * @{#ACT_ASSIST_SMOKE_TARGETS_ZONE.New}(): Creates a new ACT_ASSIST_SMOKE_TARGETS_ZONE object. --- --- === --- --- @module Smoke - -do -- ACT_ASSIST - - --- ACT_ASSIST class - -- @type ACT_ASSIST - -- @extends Core.Fsm#FSM_PROCESS - ACT_ASSIST = { - ClassName = "ACT_ASSIST", - } - - --- Creates a new target smoking state machine. The process will request from the menu if it accepts the task, if not, the unit is removed from the simulator. - -- @param #ACT_ASSIST self - -- @return #ACT_ASSIST - function ACT_ASSIST:New() - - -- Inherits from BASE - local self = BASE:Inherit( self, FSM_PROCESS:New( "ACT_ASSIST" ) ) -- Core.Fsm#FSM_PROCESS - - self:AddTransition( "None", "Start", "AwaitSmoke" ) - self:AddTransition( "AwaitSmoke", "Next", "Smoking" ) - self:AddTransition( "Smoking", "Next", "AwaitSmoke" ) - self:AddTransition( "*", "Stop", "Success" ) - self:AddTransition( "*", "Fail", "Failed" ) - - self:AddEndState( "Failed" ) - self:AddEndState( "Success" ) - - self:SetStartState( "None" ) - - return self - end - - --- Task Events - - --- StateMachine callback function - -- @param #ACT_ASSIST self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ASSIST:onafterStart( ProcessUnit, From, Event, To ) - - local ProcessGroup = ProcessUnit:GetGroup() - local MissionMenu = self:GetMission():GetMissionMenu( ProcessGroup ) - - local function MenuSmoke( MenuParam ) - self:E( MenuParam ) - local self = MenuParam.self - local SmokeColor = MenuParam.SmokeColor - self.SmokeColor = SmokeColor - self:__Next( 1 ) - end - - self.Menu = MENU_GROUP:New( ProcessGroup, "Target acquisition", MissionMenu ) - self.MenuSmokeBlue = MENU_GROUP_COMMAND:New( ProcessGroup, "Drop blue smoke on targets", self.Menu, MenuSmoke, { self = self, SmokeColor = SMOKECOLOR.Blue } ) - self.MenuSmokeGreen = MENU_GROUP_COMMAND:New( ProcessGroup, "Drop green smoke on targets", self.Menu, MenuSmoke, { self = self, SmokeColor = SMOKECOLOR.Green } ) - self.MenuSmokeOrange = MENU_GROUP_COMMAND:New( ProcessGroup, "Drop Orange smoke on targets", self.Menu, MenuSmoke, { self = self, SmokeColor = SMOKECOLOR.Orange } ) - self.MenuSmokeRed = MENU_GROUP_COMMAND:New( ProcessGroup, "Drop Red smoke on targets", self.Menu, MenuSmoke, { self = self, SmokeColor = SMOKECOLOR.Red } ) - self.MenuSmokeWhite = MENU_GROUP_COMMAND:New( ProcessGroup, "Drop White smoke on targets", self.Menu, MenuSmoke, { self = self, SmokeColor = SMOKECOLOR.White } ) - end - -end - -do -- ACT_ASSIST_SMOKE_TARGETS_ZONE - - --- ACT_ASSIST_SMOKE_TARGETS_ZONE class - -- @type ACT_ASSIST_SMOKE_TARGETS_ZONE - -- @field Set#SET_UNIT TargetSetUnit - -- @field Core.Zone#ZONE_BASE TargetZone - -- @extends #ACT_ASSIST - ACT_ASSIST_SMOKE_TARGETS_ZONE = { - ClassName = "ACT_ASSIST_SMOKE_TARGETS_ZONE", - } - --- function ACT_ASSIST_SMOKE_TARGETS_ZONE:_Destructor() --- self:E("_Destructor") --- --- self.Menu:Remove() --- self:EventRemoveAll() --- end - - --- Creates a new target smoking state machine. The process will request from the menu if it accepts the task, if not, the unit is removed from the simulator. - -- @param #ACT_ASSIST_SMOKE_TARGETS_ZONE self - -- @param Set#SET_UNIT TargetSetUnit - -- @param Core.Zone#ZONE_BASE TargetZone - function ACT_ASSIST_SMOKE_TARGETS_ZONE:New( TargetSetUnit, TargetZone ) - local self = BASE:Inherit( self, ACT_ASSIST:New() ) -- #ACT_ASSIST - - self.TargetSetUnit = TargetSetUnit - self.TargetZone = TargetZone - - return self - end - - function ACT_ASSIST_SMOKE_TARGETS_ZONE:Init( FsmSmoke ) - - self.TargetSetUnit = FsmSmoke.TargetSetUnit - self.TargetZone = FsmSmoke.TargetZone - end - - --- Creates a new target smoking state machine. The process will request from the menu if it accepts the task, if not, the unit is removed from the simulator. - -- @param #ACT_ASSIST_SMOKE_TARGETS_ZONE self - -- @param Set#SET_UNIT TargetSetUnit - -- @param Core.Zone#ZONE_BASE TargetZone - -- @return #ACT_ASSIST_SMOKE_TARGETS_ZONE self - function ACT_ASSIST_SMOKE_TARGETS_ZONE:Init( TargetSetUnit, TargetZone ) - - self.TargetSetUnit = TargetSetUnit - self.TargetZone = TargetZone - - return self - end - - --- StateMachine callback function - -- @param #ACT_ASSIST_SMOKE_TARGETS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ASSIST_SMOKE_TARGETS_ZONE:onenterSmoking( ProcessUnit, From, Event, To ) - - self.TargetSetUnit:ForEachUnit( - --- @param Wrapper.Unit#UNIT SmokeUnit - function( SmokeUnit ) - if math.random( 1, ( 100 * self.TargetSetUnit:Count() ) / 4 ) <= 100 then - SCHEDULER:New( self, - function() - if SmokeUnit:IsAlive() then - SmokeUnit:Smoke( self.SmokeColor, 150 ) - end - end, {}, math.random( 10, 60 ) - ) - end - end - ) - - end - -end--- A COMMANDCENTER is the owner of multiple missions within MOOSE. --- A COMMANDCENTER governs multiple missions, the tasking and the reporting. --- @module CommandCenter - - - ---- The REPORT class --- @type REPORT --- @extends Core.Base#BASE -REPORT = { - ClassName = "REPORT", -} - ---- Create a new REPORT. --- @param #REPORT self --- @param #string Title --- @return #REPORT -function REPORT:New( Title ) - - local self = BASE:Inherit( self, BASE:New() ) - - self.Report = {} - self.Report[#self.Report+1] = Title - - return self -end - ---- Add a new line to a REPORT. --- @param #REPORT self --- @param #string Text --- @return #REPORT -function REPORT:Add( Text ) - self.Report[#self.Report+1] = Text - return self.Report[#self.Report+1] -end - -function REPORT:Text() - return table.concat( self.Report, "\n" ) -end - ---- The COMMANDCENTER class --- @type COMMANDCENTER --- @field Wrapper.Group#GROUP HQ --- @field Dcs.DCSCoalitionWrapper.Object#coalition CommandCenterCoalition --- @list Missions --- @extends Core.Base#BASE -COMMANDCENTER = { - ClassName = "COMMANDCENTER", - CommandCenterName = "", - CommandCenterCoalition = nil, - CommandCenterPositionable = nil, - Name = "", -} ---- The constructor takes an IDENTIFIABLE as the HQ command center. --- @param #COMMANDCENTER self --- @param Wrapper.Positionable#POSITIONABLE CommandCenterPositionable --- @param #string CommandCenterName --- @return #COMMANDCENTER -function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName ) - - local self = BASE:Inherit( self, BASE:New() ) - - self.CommandCenterPositionable = CommandCenterPositionable - self.CommandCenterName = CommandCenterName or CommandCenterPositionable:GetName() - self.CommandCenterCoalition = CommandCenterPositionable:GetCoalition() - - self.Missions = {} - - self:HandleEvent( EVENTS.Birth, - --- @param #COMMANDCENTER self - --- @param Core.Event#EVENTDATA EventData - function( self, EventData ) - self:E( { EventData } ) - local EventGroup = GROUP:Find( EventData.IniDCSGroup ) - if EventGroup and self:HasGroup( EventGroup ) then - local MenuReporting = MENU_GROUP:New( EventGroup, "Reporting", self.CommandCenterMenu ) - local MenuMissionsSummary = MENU_GROUP_COMMAND:New( EventGroup, "Missions Summary Report", MenuReporting, self.ReportSummary, self, EventGroup ) - local MenuMissionsDetails = MENU_GROUP_COMMAND:New( EventGroup, "Missions Details Report", MenuReporting, self.ReportDetails, self, EventGroup ) - self:ReportSummary( EventGroup ) - end - local PlayerUnit = EventData.IniUnit - for MissionID, Mission in pairs( self:GetMissions() ) do - local Mission = Mission -- Tasking.Mission#MISSION - local PlayerGroup = EventData.IniGroup -- The GROUP object should be filled! - Mission:JoinUnit( PlayerUnit, PlayerGroup ) - Mission:ReportDetails() - end - - end - ) - - -- When a player enters a client or a unit, the CommandCenter will check for each Mission and each Task in the Mission if the player has things to do. - -- For these elements, it will= - -- - Set the correct menu. - -- - Assign the PlayerUnit to the Task if required. - -- - Send a message to the other players in the group that this player has joined. - self:HandleEvent( EVENTS.PlayerEnterUnit, - --- @param #COMMANDCENTER self - -- @param Core.Event#EVENTDATA EventData - function( self, EventData ) - local PlayerUnit = EventData.IniUnit - for MissionID, Mission in pairs( self:GetMissions() ) do - local Mission = Mission -- Tasking.Mission#MISSION - local PlayerGroup = EventData.IniGroup -- The GROUP object should be filled! - Mission:JoinUnit( PlayerUnit, PlayerGroup ) - Mission:ReportDetails() - end - end - ) - - -- Handle when a player leaves a slot and goes back to spectators ... - -- The PlayerUnit will be UnAssigned from the Task. - -- When there is no Unit left running the Task, the Task goes into Abort... - self:HandleEvent( EVENTS.PlayerLeaveUnit, - --- @param #TASK self - -- @param Core.Event#EVENTDATA EventData - function( self, EventData ) - local PlayerUnit = EventData.IniUnit - for MissionID, Mission in pairs( self:GetMissions() ) do - local Mission = Mission -- Tasking.Mission#MISSION - Mission:AbortUnit( PlayerUnit ) - end - end - ) - - -- Handle when a player leaves a slot and goes back to spectators ... - -- The PlayerUnit will be UnAssigned from the Task. - -- When there is no Unit left running the Task, the Task goes into Abort... - self:HandleEvent( EVENTS.Crash, - --- @param #TASK self - -- @param Core.Event#EVENTDATA EventData - function( self, EventData ) - local PlayerUnit = EventData.IniUnit - for MissionID, Mission in pairs( self:GetMissions() ) do - Mission:CrashUnit( PlayerUnit ) - end - end - ) - - return self -end - ---- Gets the name of the HQ command center. --- @param #COMMANDCENTER self --- @return #string -function COMMANDCENTER:GetName() - - return self.CommandCenterName -end - ---- Gets the POSITIONABLE of the HQ command center. --- @param #COMMANDCENTER self --- @return Wrapper.Positionable#POSITIONABLE -function COMMANDCENTER:GetPositionable() - return self.CommandCenterPositionable -end - ---- Get the Missions governed by the HQ command center. --- @param #COMMANDCENTER self --- @return #list -function COMMANDCENTER:GetMissions() - - return self.Missions -end - ---- Add a MISSION to be governed by the HQ command center. --- @param #COMMANDCENTER self --- @param Tasking.Mission#MISSION Mission --- @return Tasking.Mission#MISSION -function COMMANDCENTER:AddMission( Mission ) - - self.Missions[Mission] = Mission - - return Mission -end - ---- Removes a MISSION to be governed by the HQ command center. --- The given Mission is not nilified. --- @param #COMMANDCENTER self --- @param Tasking.Mission#MISSION Mission --- @return Tasking.Mission#MISSION -function COMMANDCENTER:RemoveMission( Mission ) - - self.Missions[Mission] = nil - - return Mission -end - ---- Sets the menu structure of the Missions governed by the HQ command center. --- @param #COMMANDCENTER self -function COMMANDCENTER:SetMenu() - self:F() - - self.CommandCenterMenu = self.CommandCenterMenu or MENU_COALITION:New( self.CommandCenterCoalition, "Command Center (" .. self:GetName() .. ")" ) - - for MissionID, Mission in pairs( self:GetMissions() ) do - local Mission = Mission -- Tasking.Mission#MISSION - Mission:RemoveMenu() - end - - for MissionID, Mission in pairs( self:GetMissions() ) do - local Mission = Mission -- Tasking.Mission#MISSION - Mission:SetMenu() - end -end - - ---- Checks of the COMMANDCENTER has a GROUP. --- @param #COMMANDCENTER self --- @param Wrapper.Group#GROUP --- @return #boolean -function COMMANDCENTER:HasGroup( MissionGroup ) - - local Has = false - - for MissionID, Mission in pairs( self.Missions ) do - local Mission = Mission -- Tasking.Mission#MISSION - if Mission:HasGroup( MissionGroup ) then - Has = true - break - end - end - - return Has -end - ---- Send a CC message to a GROUP. --- @param #COMMANDCENTER self --- @param #string Message --- @param Wrapper.Group#GROUP TaskGroup --- @param #sring Name (optional) The name of the Group used as a prefix for the message to the Group. If not provided, there will be nothing shown. -function COMMANDCENTER:MessageToGroup( Message, TaskGroup, Name ) - - local Prefix = Name and "@ Group (" .. Name .. "): " or '' - Message = Prefix .. Message - self:GetPositionable():MessageToGroup( Message , 20, TaskGroup, self:GetName() ) - -end - ---- Send a CC message to the coalition of the CC. --- @param #COMMANDCENTER self -function COMMANDCENTER:MessageToCoalition( Message ) - - local CCCoalition = self:GetPositionable():GetCoalition() - --TODO: Fix coalition bug! - self:GetPositionable():MessageToCoalition( Message, 20, CCCoalition, self:GetName() ) - -end - ---- Report the status of all MISSIONs to a GROUP. --- Each Mission is listed, with an indication how many Tasks are still to be completed. --- @param #COMMANDCENTER self -function COMMANDCENTER:ReportSummary( ReportGroup ) - self:E( ReportGroup ) - - local Report = REPORT:New() - - for MissionID, Mission in pairs( self.Missions ) do - local Mission = Mission -- Tasking.Mission#MISSION - Report:Add( " - " .. Mission:ReportOverview() ) - end - - self:GetPositionable():MessageToGroup( Report:Text(), 30, ReportGroup ) - -end - ---- Report the status of a Task to a Group. --- Report the details of a Mission, listing the Mission, and all the Task details. --- @param #COMMANDCENTER self -function COMMANDCENTER:ReportDetails( ReportGroup, Task ) - self:E( ReportGroup ) - - local Report = REPORT:New() - - for MissionID, Mission in pairs( self.Missions ) do - local Mission = Mission -- Tasking.Mission#MISSION - Report:Add( " - " .. Mission:ReportDetails() ) - end - - self:GetPositionable():MessageToGroup( Report:Text(), 30, ReportGroup ) -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 --- @field #MISSION.Clients _Clients --- @field Core.Menu#MENU_COALITION MissionMenu --- @field #string MissionBriefing --- @extends Core.Fsm#FSM -MISSION = { - ClassName = "MISSION", - Name = "", - MissionStatus = "PENDING", - _Clients = {}, - TaskMenus = {}, - TaskCategoryMenus = {}, - TaskTypeMenus = {}, - _ActiveTasks = {}, - GoalFunction = nil, - MissionReportTrigger = 0, - MissionProgressTrigger = 0, - MissionReportShow = false, - MissionReportFlash = false, - MissionTimeInterval = 0, - MissionCoalition = "", - SUCCESS = 1, - FAILED = 2, - REPEAT = 3, - _GoalTasks = {} -} - ---- This is the main MISSION declaration method. Each Mission is like the master or a Mission orchestration between, Clients, Tasks, Stages etc. --- @param #MISSION self --- @param Tasking.CommandCenter#COMMANDCENTER CommandCenter --- @param #string MissionName is the name of the mission. This name will be used to reference the status of each mission by the players. --- @param #string MissionPriority is a string indicating the "priority" of the Mission. f.e. "Primary", "Secondary" or "First", "Second". It is free format and up to the Mission designer to choose. There are no rules behind this field. --- @param #string MissionBriefing is a string indicating the mission briefing to be shown when a player joins a @{CLIENT}. --- @param Dcs.DCSCoalitionWrapper.Object#coalition MissionCoalition is a string indicating the coalition or party to which this mission belongs to. It is free format and can be chosen freely by the mission designer. Note that this field is not to be confused with the coalition concept of the ME. Examples of a Mission Coalition could be "NATO", "CCCP", "Intruders", "Terrorists"... --- @return #MISSION self -function MISSION:New( CommandCenter, MissionName, MissionPriority, MissionBriefing, MissionCoalition ) - - local self = BASE:Inherit( self, FSM:New() ) -- Core.Fsm#FSM - - self:SetStartState( "Idle" ) - - self:AddTransition( "Idle", "Start", "Ongoing" ) - self:AddTransition( "Ongoing", "Stop", "Idle" ) - self:AddTransition( "Ongoing", "Complete", "Completed" ) - self:AddTransition( "*", "Fail", "Failed" ) - - self:T( { MissionName, MissionPriority, MissionBriefing, MissionCoalition } ) - - self.CommandCenter = CommandCenter - CommandCenter:AddMission( self ) - - self.Name = MissionName - self.MissionPriority = MissionPriority - self.MissionBriefing = MissionBriefing - self.MissionCoalition = MissionCoalition - - self.Tasks = {} - - return self -end - ---- FSM function for a MISSION --- @param #MISSION self --- @param #string Event --- @param #string From --- @param #string To -function MISSION:onbeforeComplete( From, Event, To ) - - for TaskID, Task in pairs( self:GetTasks() ) do - local Task = Task -- Tasking.Task#TASK - if not Task:IsStateSuccess() and not Task:IsStateFailed() and not Task:IsStateAborted() and not Task:IsStateCancelled() then - return false -- Mission cannot be completed. Other Tasks are still active. - end - end - return true -- Allow Mission completion. -end - ---- FSM function for a MISSION --- @param #MISSION self --- @param #string Event --- @param #string From --- @param #string To -function MISSION:onenterCompleted( From, Event, To ) - - self:GetCommandCenter():MessageToCoalition( "Mission " .. self:GetName() .. " has been completed! Good job guys!" ) -end - ---- Gets the mission name. --- @param #MISSION self --- @return #MISSION self -function MISSION:GetName() - return self.Name -end - ---- Add a Unit to join the Mission. --- For each Task within the Mission, the Unit is joined with the Task. --- If the Unit was not part of a Task in the Mission, false is returned. --- If the Unit is part of a Task in the Mission, true is returned. --- @param #MISSION self --- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player joining the Mission. --- @param Wrapper.Group#GROUP PlayerGroup The GROUP of the player joining the Mission. --- @return #boolean true if Unit is part of a Task in the Mission. -function MISSION:JoinUnit( PlayerUnit, PlayerGroup ) - self:F( { PlayerUnit = PlayerUnit, PlayerGroup = PlayerGroup } ) - - local PlayerUnitAdded = false - - for TaskID, Task in pairs( self:GetTasks() ) do - local Task = Task -- Tasking.Task#TASK - if Task:JoinUnit( PlayerUnit, PlayerGroup ) then - PlayerUnitAdded = true - end - end - - return PlayerUnitAdded -end - ---- Aborts a PlayerUnit from the Mission. --- For each Task within the Mission, the PlayerUnit is removed from Task where it is assigned. --- If the Unit was not part of a Task in the Mission, false is returned. --- If the Unit is part of a Task in the Mission, true is returned. --- @param #MISSION self --- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player joining the Mission. --- @return #boolean true if Unit is part of a Task in the Mission. -function MISSION:AbortUnit( PlayerUnit ) - self:F( { PlayerUnit = PlayerUnit } ) - - local PlayerUnitRemoved = false - - for TaskID, Task in pairs( self:GetTasks() ) do - if Task:AbortUnit( PlayerUnit ) then - PlayerUnitRemoved = true - end - end - - return PlayerUnitRemoved -end - ---- Handles a crash of a PlayerUnit from the Mission. --- For each Task within the Mission, the PlayerUnit is removed from Task where it is assigned. --- If the Unit was not part of a Task in the Mission, false is returned. --- If the Unit is part of a Task in the Mission, true is returned. --- @param #MISSION self --- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player crashing. --- @return #boolean true if Unit is part of a Task in the Mission. -function MISSION:CrashUnit( PlayerUnit ) - self:F( { PlayerUnit = PlayerUnit } ) - - local PlayerUnitRemoved = false - - for TaskID, Task in pairs( self:GetTasks() ) do - if Task:CrashUnit( PlayerUnit ) then - PlayerUnitRemoved = true - end - end - - return PlayerUnitRemoved -end - ---- Add a scoring to the mission. --- @param #MISSION self --- @return #MISSION self -function MISSION:AddScoring( Scoring ) - self.Scoring = Scoring - return self -end - ---- Get the scoring object of a mission. --- @param #MISSION self --- @return #SCORING Scoring -function MISSION:GetScoring() - return self.Scoring -end - ---- Get the groups for which TASKS are given in the mission --- @param #MISSION self --- @return Core.Set#SET_GROUP -function MISSION:GetGroups() - - local SetGroup = SET_GROUP:New() - - for TaskID, Task in pairs( self:GetTasks() ) do - local Task = Task -- Tasking.Task#TASK - local GroupSet = Task:GetGroups() - GroupSet:ForEachGroup( - function( TaskGroup ) - SetGroup:Add( TaskGroup, TaskGroup ) - end - ) - end - - return SetGroup - -end - - ---- Sets the Planned Task menu. --- @param #MISSION self -function MISSION:SetMenu() - self:F() - - for _, Task in pairs( self:GetTasks() ) do - local Task = Task -- Tasking.Task#TASK - Task:SetMenu() - end -end - ---- Removes the Planned Task menu. --- @param #MISSION self -function MISSION:RemoveMenu() - self:F() - - for _, Task in pairs( self:GetTasks() ) do - local Task = Task -- Tasking.Task#TASK - Task:RemoveMenu() - end -end - - ---- Gets the COMMANDCENTER. --- @param #MISSION self --- @return Tasking.CommandCenter#COMMANDCENTER -function MISSION:GetCommandCenter() - return self.CommandCenter -end - ---- Sets the Assigned Task menu. --- @param #MISSION self --- @param Tasking.Task#TASK Task --- @param #string MenuText The menu text. --- @return #MISSION self -function MISSION:SetAssignedMenu( Task ) - - for _, Task in pairs( self.Tasks ) do - local Task = Task -- Tasking.Task#TASK - Task:RemoveMenu() - Task:SetAssignedMenu() - end - -end - ---- Removes a Task menu. --- @param #MISSION self --- @param Tasking.Task#TASK Task --- @return #MISSION self -function MISSION:RemoveTaskMenu( Task ) - - Task:RemoveMenu() -end - - ---- Gets the mission menu for the coalition. --- @param #MISSION self --- @param Wrapper.Group#GROUP TaskGroup --- @return Core.Menu#MENU_COALITION self -function MISSION:GetMissionMenu( TaskGroup ) - - local CommandCenter = self:GetCommandCenter() - local CommandCenterMenu = CommandCenter.CommandCenterMenu - - local MissionName = self:GetName() - - local TaskGroupName = TaskGroup:GetName() - local MissionMenu = MENU_GROUP:New( TaskGroup, MissionName, CommandCenterMenu ) - - return MissionMenu -end - - ---- Clears the mission menu for the coalition. --- @param #MISSION self --- @return #MISSION self -function MISSION:ClearMissionMenu() - self.MissionMenu:Remove() - self.MissionMenu = nil -end - ---- Get the TASK identified by the TaskNumber from the Mission. This function is useful in GoalFunctions. --- @param #string TaskName The Name of the @{Task} within the @{Mission}. --- @return Tasking.Task#TASK The Task --- @return #nil Returns nil if no task was found. -function MISSION:GetTask( TaskName ) - self:F( { TaskName } ) - - return self.Tasks[TaskName] -end - - ---- Register a @{Task} to be completed within the @{Mission}. --- Note that there can be multiple @{Task}s registered to be completed. --- Each Task can be set a certain Goals. The Mission will not be completed until all Goals are reached. --- @param #MISSION self --- @param Tasking.Task#TASK Task is the @{Task} object. --- @return Tasking.Task#TASK The task added. -function MISSION:AddTask( Task ) - - local TaskName = Task:GetTaskName() - self:F( TaskName ) - - self.Tasks[TaskName] = self.Tasks[TaskName] or { n = 0 } - - self.Tasks[TaskName] = Task - - self:GetCommandCenter():SetMenu() - - return Task -end - ---- Removes a @{Task} to be completed within the @{Mission}. --- Note that there can be multiple @{Task}s registered to be completed. --- Each Task can be set a certain Goals. The Mission will not be completed until all Goals are reached. --- @param #MISSION self --- @param Tasking.Task#TASK Task is the @{Task} object. --- @return #nil The cleaned Task reference. -function MISSION:RemoveTask( Task ) - - local TaskName = Task:GetTaskName() - - self:F( TaskName ) - self.Tasks[TaskName] = self.Tasks[TaskName] or { n = 0 } - - -- Ensure everything gets garbarge collected. - self.Tasks[TaskName] = nil - Task = nil - - collectgarbage() - - self:GetCommandCenter():SetMenu() - - return nil -end - ---- Return the next @{Task} ID to be completed within the @{Mission}. --- @param #MISSION self --- @param Tasking.Task#TASK Task is the @{Task} object. --- @return Tasking.Task#TASK The task added. -function MISSION:GetNextTaskID( Task ) - - local TaskName = Task:GetTaskName() - self:F( TaskName ) - self.Tasks[TaskName] = self.Tasks[TaskName] or { n = 0 } - - self.Tasks[TaskName].n = self.Tasks[TaskName].n + 1 - - return self.Tasks[TaskName].n -end - - - ---- old stuff - ---- 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 - -function MISSION:HasGroup( TaskGroup ) - local Has = false - - for TaskID, Task in pairs( self:GetTasks() ) do - local Task = Task -- Tasking.Task#TASK - if Task:HasGroup( TaskGroup ) then - Has = true - break - end - end - - return Has -end - ---- Create a summary report of the Mission (one line). --- @param #MISSION self --- @return #string -function MISSION:ReportSummary() - - local Report = REPORT:New() - - -- List the name of the mission. - local Name = self:GetName() - - -- Determine the status of the mission. - local Status = self:GetState() - - -- Determine how many tasks are remaining. - local TasksRemaining = 0 - for TaskID, Task in pairs( self:GetTasks() ) do - local Task = Task -- Tasking.Task#TASK - if Task:IsStateSuccess() or Task:IsStateFailed() then - else - TasksRemaining = TasksRemaining + 1 - end - end - - Report:Add( "Mission " .. Name .. " - " .. Status .. " - " .. TasksRemaining .. " tasks remaining." ) - - return Report:Text() -end - ---- Create a overview report of the Mission (multiple lines). --- @param #MISSION self --- @return #string -function MISSION:ReportOverview() - - local Report = REPORT:New() - - -- List the name of the mission. - local Name = self:GetName() - - -- Determine the status of the mission. - local Status = self:GetState() - - Report:Add( "Mission " .. Name .. " - State '" .. Status .. "'" ) - - -- Determine how many tasks are remaining. - local TasksRemaining = 0 - for TaskID, Task in pairs( self:GetTasks() ) do - local Task = Task -- Tasking.Task#TASK - Report:Add( "- " .. Task:ReportSummary() ) - end - - return Report:Text() -end - ---- Create a detailed report of the Mission, listing all the details of the Task. --- @param #MISSION self --- @return #string -function MISSION:ReportDetails() - - local Report = REPORT:New() - - -- List the name of the mission. - local Name = self:GetName() - - -- Determine the status of the mission. - local Status = self:GetState() - - Report:Add( "Mission " .. Name .. " - State '" .. Status .. "'" ) - - -- Determine how many tasks are remaining. - local TasksRemaining = 0 - for TaskID, Task in pairs( self:GetTasks() ) do - local Task = Task -- Tasking.Task#TASK - Report:Add( Task:ReportDetails() ) - end - - return Report:Text() -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 - - ---- 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 -- Wrapper.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 - ---- This module contains the TASK class. --- --- 1) @{#TASK} class, extends @{Base#BASE} --- ============================================ --- 1.1) The @{#TASK} class implements the methods for task orchestration within MOOSE. --- ---------------------------------------------------------------------------------------- --- The class provides a couple of methods to: --- --- * @{#TASK.AssignToGroup}():Assign a task to a group (of players). --- * @{#TASK.AddProcess}():Add a @{Process} to a task. --- * @{#TASK.RemoveProcesses}():Remove a running @{Process} from a running task. --- * @{#TASK.SetStateMachine}():Set a @{Fsm} to a task. --- * @{#TASK.RemoveStateMachine}():Remove @{Fsm} from a task. --- * @{#TASK.HasStateMachine}():Enquire if the task has a @{Fsm} --- * @{#TASK.AssignToUnit}(): Assign a task to a unit. (Needs to be implemented in the derived classes from @{#TASK}. --- * @{#TASK.UnAssignFromUnit}(): Unassign the task from a unit. --- * @{#TASK.SetTimeOut}(): Set timer in seconds before task gets cancelled if not assigned. --- --- 1.2) Set and enquire task status (beyond the task state machine processing). --- ---------------------------------------------------------------------------- --- A task needs to implement as a minimum the following task states: --- --- * **Success**: Expresses the successful execution and finalization of the task. --- * **Failed**: Expresses the failure of a task. --- * **Planned**: Expresses that the task is created, but not yet in execution and is not assigned yet. --- * **Assigned**: Expresses that the task is assigned to a Group of players, and that the task is in execution mode. --- --- A task may also implement the following task states: --- --- * **Rejected**: Expresses that the task is rejected by a player, who was requested to accept the task. --- * **Cancelled**: Expresses that the task is cancelled by HQ or through a logical situation where a cancellation of the task is required. --- --- A task can implement more statusses than the ones outlined above. Please consult the documentation of the specific tasks to understand the different status modelled. --- --- The status of tasks can be set by the methods **State** followed by the task status. An example is `StateAssigned()`. --- The status of tasks can be enquired by the methods **IsState** followed by the task status name. An example is `if IsStateAssigned() then`. --- --- 1.3) Add scoring when reaching a certain task status: --- ----------------------------------------------------- --- Upon reaching a certain task status in a task, additional scoring can be given. If the Mission has a scoring system attached, the scores will be added to the mission scoring. --- Use the method @{#TASK.AddScore}() to add scores when a status is reached. --- --- 1.4) Task briefing: --- ------------------- --- A task briefing can be given that is shown to the player when he is assigned to the task. --- --- === --- --- ### Authors: FlightControl - Design and Programming --- --- @module Task - ---- The TASK class --- @type TASK --- @field Core.Scheduler#SCHEDULER TaskScheduler --- @field Tasking.Mission#MISSION Mission --- @field Core.Set#SET_GROUP SetGroup The Set of Groups assigned to the Task --- @field Core.Fsm#FSM_PROCESS FsmTemplate --- @field Tasking.Mission#MISSION Mission --- @field Tasking.CommandCenter#COMMANDCENTER CommandCenter --- @extends Core.Fsm#FSM_TASK -TASK = { - ClassName = "TASK", - TaskScheduler = nil, - ProcessClasses = {}, -- The container of the Process classes that will be used to create and assign new processes for the task to ProcessUnits. - Processes = {}, -- The container of actual process objects instantiated and assigned to ProcessUnits. - Players = nil, - Scores = {}, - Menu = {}, - SetGroup = nil, - FsmTemplate = nil, - Mission = nil, - CommandCenter = nil, - TimeOut = 0, -} - ---- FSM PlayerAborted event handler prototype for TASK. --- @function [parent=#TASK] OnAfterPlayerAborted --- @param #TASK self --- @param Wrapper.Unit#UNIT PlayerUnit The Unit of the Player when he went back to spectators or left the mission. --- @param #string PlayerName The name of the Player. - ---- FSM PlayerCrashed event handler prototype for TASK. --- @function [parent=#TASK] OnAfterPlayerCrashed --- @param #TASK self --- @param Wrapper.Unit#UNIT PlayerUnit The Unit of the Player when he crashed in the mission. --- @param #string PlayerName The name of the Player. - ---- FSM PlayerDead event handler prototype for TASK. --- @function [parent=#TASK] OnAfterPlayerDead --- @param #TASK self --- @param Wrapper.Unit#UNIT PlayerUnit The Unit of the Player when he died in the mission. --- @param #string PlayerName The name of the Player. - ---- FSM Fail synchronous event function for TASK. --- Use this event to Fail the Task. --- @function [parent=#TASK] Fail --- @param #TASK self - ---- FSM Fail asynchronous event function for TASK. --- Use this event to Fail the Task. --- @function [parent=#TASK] __Fail --- @param #TASK self - ---- FSM Abort synchronous event function for TASK. --- Use this event to Abort the Task. --- @function [parent=#TASK] Abort --- @param #TASK self - ---- FSM Abort asynchronous event function for TASK. --- Use this event to Abort the Task. --- @function [parent=#TASK] __Abort --- @param #TASK self - ---- FSM Success synchronous event function for TASK. --- Use this event to make the Task a Success. --- @function [parent=#TASK] Success --- @param #TASK self - ---- FSM Success asynchronous event function for TASK. --- Use this event to make the Task a Success. --- @function [parent=#TASK] __Success --- @param #TASK self - ---- FSM Cancel synchronous event function for TASK. --- Use this event to Cancel the Task. --- @function [parent=#TASK] Cancel --- @param #TASK self - ---- FSM Cancel asynchronous event function for TASK. --- Use this event to Cancel the Task. --- @function [parent=#TASK] __Cancel --- @param #TASK self - ---- FSM Replan synchronous event function for TASK. --- Use this event to Replan the Task. --- @function [parent=#TASK] Replan --- @param #TASK self - ---- FSM Replan asynchronous event function for TASK. --- Use this event to Replan the Task. --- @function [parent=#TASK] __Replan --- @param #TASK self - - ---- Instantiates a new TASK. Should never be used. Interface Class. --- @param #TASK self --- @param Tasking.Mission#MISSION Mission The mission wherein the Task is registered. --- @param Core.Set#SET_GROUP SetGroupAssign The set of groups for which the Task can be assigned. --- @param #string TaskName The name of the Task --- @param #string TaskType The type of the Task --- @return #TASK self -function TASK:New( Mission, SetGroupAssign, TaskName, TaskType ) - - local self = BASE:Inherit( self, FSM_TASK:New() ) -- Core.Fsm#FSM_TASK - - self:SetStartState( "Planned" ) - self:AddTransition( "Planned", "Assign", "Assigned" ) - self:AddTransition( "Assigned", "AssignUnit", "Assigned" ) - self:AddTransition( "Assigned", "Success", "Success" ) - self:AddTransition( "Assigned", "Fail", "Failed" ) - self:AddTransition( "Assigned", "Abort", "Aborted" ) - self:AddTransition( "Assigned", "Cancel", "Cancelled" ) - self:AddTransition( "*", "PlayerCrashed", "*" ) - self:AddTransition( "*", "PlayerAborted", "*" ) - self:AddTransition( "*", "PlayerDead", "*" ) - self:AddTransition( { "Failed", "Aborted", "Cancelled" }, "Replan", "Planned" ) - self:AddTransition( "*", "TimeOut", "Cancelled" ) - - self:E( "New TASK " .. TaskName ) - - self.Processes = {} - self.Fsm = {} - - self.Mission = Mission - self.CommandCenter = Mission:GetCommandCenter() - - self.SetGroup = SetGroupAssign - - self:SetType( TaskType ) - self:SetName( TaskName ) - self:SetID( Mission:GetNextTaskID( self ) ) -- The Mission orchestrates the task sequences .. - - self.TaskBriefing = "You are invited for the task: " .. self.TaskName .. "." - - self.FsmTemplate = self.FsmTemplate or FSM_PROCESS:New() - - Mission:AddTask( self ) - - return self -end - ---- Get the Task FSM Process Template --- @param #TASK self --- @return Core.Fsm#FSM_PROCESS -function TASK:GetUnitProcess() - - return self.FsmTemplate -end - ---- Sets the Task FSM Process Template --- @param #TASK self --- @param Core.Fsm#FSM_PROCESS -function TASK:SetUnitProcess( FsmTemplate ) - - self.FsmTemplate = FsmTemplate -end - ---- Add a PlayerUnit to join the Task. --- For each Group within the Task, the Unit is check if it can join the Task. --- If the Unit was not part of the Task, false is returned. --- If the Unit is part of the Task, true is returned. --- @param #TASK self --- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player joining the Mission. --- @param Wrapper.Group#GROUP PlayerGroup The GROUP of the player joining the Mission. --- @return #boolean true if Unit is part of the Task. -function TASK:JoinUnit( PlayerUnit, PlayerGroup ) - self:F( { PlayerUnit = PlayerUnit, PlayerGroup = PlayerGroup } ) - - local PlayerUnitAdded = false - - local PlayerGroups = self:GetGroups() - - -- Is the PlayerGroup part of the PlayerGroups? - if PlayerGroups:IsIncludeObject( PlayerGroup ) then - - -- Check if the PlayerGroup is already assigned to the Task. If yes, the PlayerGroup is added to the Task. - -- If the PlayerGroup is not assigned to the Task, the menu needs to be set. In that case, the PlayerUnit will become the GroupPlayer leader. - if self:IsStatePlanned() or self:IsStateReplanned() then - self:SetMenuForGroup( PlayerGroup ) - self:MessageToGroups( PlayerUnit:GetPlayerName() .. " is planning to join Task " .. self:GetName() ) - end - if self:IsStateAssigned() then - local IsAssignedToGroup = self:IsAssignedToGroup( PlayerGroup ) - self:E( { IsAssignedToGroup = IsAssignedToGroup } ) - if IsAssignedToGroup then - self:AssignToUnit( PlayerUnit ) - self:MessageToGroups( PlayerUnit:GetPlayerName() .. " joined Task " .. self:GetName() ) - end - end - end - - return PlayerUnitAdded -end - ---- Abort a PlayerUnit from a Task. --- If the Unit was not part of the Task, false is returned. --- If the Unit is part of the Task, true is returned. --- @param #TASK self --- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player aborting the Task. --- @return #boolean true if Unit is part of the Task. -function TASK:AbortUnit( PlayerUnit ) - self:F( { PlayerUnit = PlayerUnit } ) - - local PlayerUnitAborted = false - - local PlayerGroups = self:GetGroups() - local PlayerGroup = PlayerUnit:GetGroup() - - -- Is the PlayerGroup part of the PlayerGroups? - if PlayerGroups:IsIncludeObject( PlayerGroup ) then - - -- Check if the PlayerGroup is already assigned to the Task. If yes, the PlayerGroup is aborted from the Task. - -- If the PlayerUnit was the last unit of the PlayerGroup, the menu needs to be removed from the Group. - if self:IsStateAssigned() then - local IsAssignedToGroup = self:IsAssignedToGroup( PlayerGroup ) - self:E( { IsAssignedToGroup = IsAssignedToGroup } ) - if IsAssignedToGroup then - self:UnAssignFromUnit( PlayerUnit ) - self:MessageToGroups( PlayerUnit:GetPlayerName() .. " aborted Task " .. self:GetName() ) - self:E( { TaskGroup = PlayerGroup:GetName(), GetUnits = PlayerGroup:GetUnits() } ) - if #PlayerGroup:GetUnits() == 1 then - PlayerGroup:SetState( PlayerGroup, "Assigned", nil ) - self:RemoveMenuForGroup( PlayerGroup ) - end - self:PlayerAborted( PlayerUnit ) - end - end - end - - return PlayerUnitAborted -end - ---- A PlayerUnit crashed in a Task. Abort the Player. --- If the Unit was not part of the Task, false is returned. --- If the Unit is part of the Task, true is returned. --- @param #TASK self --- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player aborting the Task. --- @return #boolean true if Unit is part of the Task. -function TASK:CrashUnit( PlayerUnit ) - self:F( { PlayerUnit = PlayerUnit } ) - - local PlayerUnitCrashed = false - - local PlayerGroups = self:GetGroups() - local PlayerGroup = PlayerUnit:GetGroup() - - -- Is the PlayerGroup part of the PlayerGroups? - if PlayerGroups:IsIncludeObject( PlayerGroup ) then - - -- Check if the PlayerGroup is already assigned to the Task. If yes, the PlayerGroup is aborted from the Task. - -- If the PlayerUnit was the last unit of the PlayerGroup, the menu needs to be removed from the Group. - if self:IsStateAssigned() then - local IsAssignedToGroup = self:IsAssignedToGroup( PlayerGroup ) - self:E( { IsAssignedToGroup = IsAssignedToGroup } ) - if IsAssignedToGroup then - self:UnAssignFromUnit( PlayerUnit ) - self:MessageToGroups( PlayerUnit:GetPlayerName() .. " crashed in Task " .. self:GetName() ) - self:E( { TaskGroup = PlayerGroup:GetName(), GetUnits = PlayerGroup:GetUnits() } ) - if #PlayerGroup:GetUnits() == 1 then - PlayerGroup:SetState( PlayerGroup, "Assigned", nil ) - self:RemoveMenuForGroup( PlayerGroup ) - end - self:PlayerCrashed( PlayerUnit ) - end - end - end - - return PlayerUnitCrashed -end - - - ---- Gets the Mission to where the TASK belongs. --- @param #TASK self --- @return Tasking.Mission#MISSION -function TASK:GetMission() - - return self.Mission -end - - ---- Gets the SET_GROUP assigned to the TASK. --- @param #TASK self --- @return Core.Set#SET_GROUP -function TASK:GetGroups() - return self.SetGroup -end - - - ---- Assign the @{Task}to a @{Group}. --- @param #TASK self --- @param Wrapper.Group#GROUP TaskGroup --- @return #TASK -function TASK:AssignToGroup( TaskGroup ) - self:F2( TaskGroup:GetName() ) - - local TaskGroupName = TaskGroup:GetName() - - TaskGroup:SetState( TaskGroup, "Assigned", self ) - - self:RemoveMenuForGroup( TaskGroup ) - self:SetAssignedMenuForGroup( TaskGroup ) - - local TaskUnits = TaskGroup:GetUnits() - for UnitID, UnitData in pairs( TaskUnits ) do - local TaskUnit = UnitData -- Wrapper.Unit#UNIT - local PlayerName = TaskUnit:GetPlayerName() - self:E(PlayerName) - if PlayerName ~= nil or PlayerName ~= "" then - self:AssignToUnit( TaskUnit ) - end - end - - return self -end - ---- --- @param #TASK self --- @param Wrapper.Group#GROUP FindGroup --- @return #boolean -function TASK:HasGroup( FindGroup ) - - return self:GetGroups():IsIncludeObject( FindGroup ) - -end - ---- Assign the @{Task} to an alive @{Unit}. --- @param #TASK self --- @param Wrapper.Unit#UNIT TaskUnit --- @return #TASK self -function TASK:AssignToUnit( TaskUnit ) - self:F( TaskUnit:GetName() ) - - local FsmTemplate = self:GetUnitProcess() - - -- Assign a new FsmUnit to TaskUnit. - local FsmUnit = self:SetStateMachine( TaskUnit, FsmTemplate:Copy( TaskUnit, self ) ) -- Core.Fsm#FSM_PROCESS - self:E({"Address FsmUnit", tostring( FsmUnit ) } ) - - FsmUnit:SetStartState( "Planned" ) - FsmUnit:Accept() -- Each Task needs to start with an Accept event to start the flow. - - return self -end - ---- UnAssign the @{Task} from an alive @{Unit}. --- @param #TASK self --- @param Wrapper.Unit#UNIT TaskUnit --- @return #TASK self -function TASK:UnAssignFromUnit( TaskUnit ) - self:F( TaskUnit ) - - self:RemoveStateMachine( TaskUnit ) - - return self -end - ---- Sets the TimeOut for the @{Task}. If @{Task} stayed planned for longer than TimeOut, it gets into Cancelled status. --- @param #TASK self --- @param #integer Timer in seconds --- @return #TASK self -function TASK:SetTimeOut ( Timer ) - self:F( Timer ) - self.TimeOut = Timer - self:__TimeOut( self.TimeOut ) - return self -end - ---- Send a message of the @{Task} to the assigned @{Group}s. --- @param #TASK self -function TASK:MessageToGroups( Message ) - self:F( { Message = Message } ) - - local Mission = self:GetMission() - local CC = Mission:GetCommandCenter() - - for TaskGroupName, TaskGroup in pairs( self.SetGroup:GetSet() ) do - local TaskGroup = TaskGroup -- Wrapper.Group#GROUP - CC:MessageToGroup( Message, TaskGroup, TaskGroup:GetName() ) - end -end - - ---- Send the briefng message of the @{Task} to the assigned @{Group}s. --- @param #TASK self -function TASK:SendBriefingToAssignedGroups() - self:F2() - - for TaskGroupName, TaskGroup in pairs( self.SetGroup:GetSet() ) do - - if self:IsAssignedToGroup( TaskGroup ) then - TaskGroup:Message( self.TaskBriefing, 60 ) - end - end -end - - ---- Assign the @{Task} from the @{Group}s. --- @param #TASK self -function TASK:UnAssignFromGroups() - self:F2() - - for TaskGroupName, TaskGroup in pairs( self.SetGroup:GetSet() ) do - - TaskGroup:SetState( TaskGroup, "Assigned", nil ) - - self:RemoveMenuForGroup( TaskGroup ) - - local TaskUnits = TaskGroup:GetUnits() - for UnitID, UnitData in pairs( TaskUnits ) do - local TaskUnit = UnitData -- Wrapper.Unit#UNIT - local PlayerName = TaskUnit:GetPlayerName() - if PlayerName ~= nil or PlayerName ~= "" then - self:UnAssignFromUnit( TaskUnit ) - end - end - end -end - ---- Returns if the @{Task} is assigned to the Group. --- @param #TASK self --- @param Wrapper.Group#GROUP TaskGroup --- @return #boolean -function TASK:IsAssignedToGroup( TaskGroup ) - - local TaskGroupName = TaskGroup:GetName() - - if self:IsStateAssigned() then - if TaskGroup:GetState( TaskGroup, "Assigned" ) == self then - return true - end - end - - return false -end - ---- Returns if the @{Task} has still alive and assigned Units. --- @param #TASK self --- @return #boolean -function TASK:HasAliveUnits() - self:F() - - for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do - if self:IsStateAssigned() then - if self:IsAssignedToGroup( TaskGroup ) then - for TaskUnitID, TaskUnit in pairs( TaskGroup:GetUnits() ) do - if TaskUnit:IsAlive() then - self:T( { HasAliveUnits = true } ) - return true - end - end - end - end - end - - self:T( { HasAliveUnits = false } ) - return false -end - ---- Set the menu options of the @{Task} to all the groups in the SetGroup. --- @param #TASK self -function TASK:SetMenu() - self:F() - - self.SetGroup:Flush() - for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do - if self:IsStatePlanned() or self:IsStateReplanned() then - self:SetMenuForGroup( TaskGroup ) - end - end -end - - ---- Remove the menu options of the @{Task} to all the groups in the SetGroup. --- @param #TASK self --- @return #TASK self -function TASK:RemoveMenu() - - for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do - self:RemoveMenuForGroup( TaskGroup ) - end -end - - ---- Set the Menu for a Group --- @param #TASK self -function TASK:SetMenuForGroup( TaskGroup ) - - if not self:IsAssignedToGroup( TaskGroup ) then - self:SetPlannedMenuForGroup( TaskGroup, self:GetTaskName() ) - else - self:SetAssignedMenuForGroup( TaskGroup ) - end -end - - ---- Set the planned menu option of the @{Task}. --- @param #TASK self --- @param Wrapper.Group#GROUP TaskGroup --- @param #string MenuText The menu text. --- @return #TASK self -function TASK:SetPlannedMenuForGroup( TaskGroup, MenuText ) - self:E( TaskGroup:GetName() ) - - local Mission = self:GetMission() - local MissionMenu = Mission:GetMissionMenu( TaskGroup ) - - local TaskType = self:GetType() - local TaskTypeMenu = MENU_GROUP:New( TaskGroup, TaskType, MissionMenu ) - local TaskMenu = MENU_GROUP_COMMAND:New( TaskGroup, MenuText, TaskTypeMenu, self.MenuAssignToGroup, { self = self, TaskGroup = TaskGroup } ) - - return self -end - ---- Set the assigned menu options of the @{Task}. --- @param #TASK self --- @param Wrapper.Group#GROUP TaskGroup --- @return #TASK self -function TASK:SetAssignedMenuForGroup( TaskGroup ) - self:E( TaskGroup:GetName() ) - - local Mission = self:GetMission() - local MissionMenu = Mission:GetMissionMenu( TaskGroup ) - - self:E( { MissionMenu = MissionMenu } ) - - local TaskTypeMenu = MENU_GROUP_COMMAND:New( TaskGroup, "Task Status", MissionMenu, self.MenuTaskStatus, { self = self, TaskGroup = TaskGroup } ) - local TaskMenu = MENU_GROUP_COMMAND:New( TaskGroup, "Abort Task", MissionMenu, self.MenuTaskAbort, { self = self, TaskGroup = TaskGroup } ) - - return self -end - ---- Remove the menu option of the @{Task} for a @{Group}. --- @param #TASK self --- @param Wrapper.Group#GROUP TaskGroup --- @return #TASK self -function TASK:RemoveMenuForGroup( TaskGroup ) - - local Mission = self:GetMission() - local MissionName = Mission:GetName() - - local MissionMenu = Mission:GetMissionMenu( TaskGroup ) - MissionMenu:Remove() -end - -function TASK.MenuAssignToGroup( MenuParam ) - - local self = MenuParam.self - local TaskGroup = MenuParam.TaskGroup - - self:E( "Assigned menu selected") - - self:AssignToGroup( TaskGroup ) -end - -function TASK.MenuTaskStatus( MenuParam ) - - local self = MenuParam.self - local TaskGroup = MenuParam.TaskGroup - - --self:AssignToGroup( TaskGroup ) -end - -function TASK.MenuTaskAbort( MenuParam ) - - local self = MenuParam.self - local TaskGroup = MenuParam.TaskGroup - - self:Abort() -end - - - ---- Returns the @{Task} name. --- @param #TASK self --- @return #string TaskName -function TASK:GetTaskName() - return self.TaskName -end - - - - ---- Get the default or currently assigned @{Process} template with key ProcessName. --- @param #TASK self --- @param #string ProcessName --- @return Core.Fsm#FSM_PROCESS -function TASK:GetProcessTemplate( ProcessName ) - - local ProcessTemplate = self.ProcessClasses[ProcessName] - - return ProcessTemplate -end - - - --- TODO: Obscolete? ---- Fail processes from @{Task} with key @{Unit} --- @param #TASK self --- @param #string TaskUnitName --- @return #TASK self -function TASK:FailProcesses( TaskUnitName ) - - for ProcessID, ProcessData in pairs( self.Processes[TaskUnitName] ) do - local Process = ProcessData - Process.Fsm:Fail() - end -end - ---- Add a FiniteStateMachine to @{Task} with key Task@{Unit} --- @param #TASK self --- @param Wrapper.Unit#UNIT TaskUnit --- @return #TASK self -function TASK:SetStateMachine( TaskUnit, Fsm ) - self:F( { TaskUnit, self.Fsm[TaskUnit] ~= nil } ) - - self.Fsm[TaskUnit] = Fsm - - return Fsm -end - ---- Remove FiniteStateMachines from @{Task} with key Task@{Unit} --- @param #TASK self --- @param Wrapper.Unit#UNIT TaskUnit --- @return #TASK self -function TASK:RemoveStateMachine( TaskUnit ) - self:F( { TaskUnit, self.Fsm[TaskUnit] ~= nil } ) - - self.Fsm[TaskUnit] = nil - collectgarbage() - self:T( "Garbage Collected, Processes should be finalized now ...") -end - ---- Checks if there is a FiniteStateMachine assigned to Task@{Unit} for @{Task} --- @param #TASK self --- @param Wrapper.Unit#UNIT TaskUnit --- @return #TASK self -function TASK:HasStateMachine( TaskUnit ) - self:F( { TaskUnit, self.Fsm[TaskUnit] ~= nil } ) - - return ( self.Fsm[TaskUnit] ~= nil ) -end - - ---- Gets the Scoring of the task --- @param #TASK self --- @return Functional.Scoring#SCORING Scoring -function TASK:GetScoring() - return self.Mission:GetScoring() -end - - ---- Gets the Task Index, which is a combination of the Task type, the Task name. --- @param #TASK self --- @return #string The Task ID -function TASK:GetTaskIndex() - - local TaskType = self:GetType() - local TaskName = self:GetName() - - return TaskType .. "." .. TaskName -end - ---- Sets the Name of the Task --- @param #TASK self --- @param #string TaskName -function TASK:SetName( TaskName ) - self.TaskName = TaskName -end - ---- Gets the Name of the Task --- @param #TASK self --- @return #string The Task Name -function TASK:GetName() - return self.TaskName -end - ---- Sets the Type of the Task --- @param #TASK self --- @param #string TaskType -function TASK:SetType( TaskType ) - self.TaskType = TaskType -end - ---- Gets the Type of the Task --- @param #TASK self --- @return #string TaskType -function TASK:GetType() - return self.TaskType -end - ---- Sets the ID of the Task --- @param #TASK self --- @param #string TaskID -function TASK:SetID( TaskID ) - self.TaskID = TaskID -end - ---- Gets the ID of the Task --- @param #TASK self --- @return #string TaskID -function TASK:GetID() - return self.TaskID -end - - ---- Sets a @{Task} to status **Success**. --- @param #TASK self -function TASK:StateSuccess() - self:SetState( self, "State", "Success" ) - return self -end - ---- Is the @{Task} status **Success**. --- @param #TASK self -function TASK:IsStateSuccess() - return self:Is( "Success" ) -end - ---- Sets a @{Task} to status **Failed**. --- @param #TASK self -function TASK:StateFailed() - self:SetState( self, "State", "Failed" ) - return self -end - ---- Is the @{Task} status **Failed**. --- @param #TASK self -function TASK:IsStateFailed() - return self:Is( "Failed" ) -end - ---- Sets a @{Task} to status **Planned**. --- @param #TASK self -function TASK:StatePlanned() - self:SetState( self, "State", "Planned" ) - return self -end - ---- Is the @{Task} status **Planned**. --- @param #TASK self -function TASK:IsStatePlanned() - return self:Is( "Planned" ) -end - ---- Sets a @{Task} to status **Assigned**. --- @param #TASK self -function TASK:StateAssigned() - self:SetState( self, "State", "Assigned" ) - return self -end - ---- Is the @{Task} status **Assigned**. --- @param #TASK self -function TASK:IsStateAssigned() - return self:Is( "Assigned" ) -end - ---- Sets a @{Task} to status **Hold**. --- @param #TASK self -function TASK:StateHold() - self:SetState( self, "State", "Hold" ) - return self -end - ---- Is the @{Task} status **Hold**. --- @param #TASK self -function TASK:IsStateHold() - return self:Is( "Hold" ) -end - ---- Sets a @{Task} to status **Replanned**. --- @param #TASK self -function TASK:StateReplanned() - self:SetState( self, "State", "Replanned" ) - return self -end - ---- Is the @{Task} status **Replanned**. --- @param #TASK self -function TASK:IsStateReplanned() - return self:Is( "Replanned" ) -end - ---- Gets the @{Task} status. --- @param #TASK self -function TASK:GetStateString() - return self:GetState( self, "State" ) -end - ---- Sets a @{Task} briefing. --- @param #TASK self --- @param #string TaskBriefing --- @return #TASK self -function TASK:SetBriefing( TaskBriefing ) - self.TaskBriefing = TaskBriefing - return self -end - - - - ---- FSM function for a TASK --- @param #TASK self --- @param #string Event --- @param #string From --- @param #string To -function TASK:onenterAssigned( From, Event, To ) - - self:E("Task Assigned") - - self:MessageToGroups( "Task " .. self:GetName() .. " has been assigned to your group." ) - self:GetMission():__Start( 1 ) -end - - ---- FSM function for a TASK --- @param #TASK self --- @param #string Event --- @param #string From --- @param #string To -function TASK:onenterSuccess( From, Event, To ) - - self:E( "Task Success" ) - - self:MessageToGroups( "Task " .. self:GetName() .. " is successful! Good job!" ) - self:UnAssignFromGroups() - - self:GetMission():__Complete( 1 ) - -end - - ---- FSM function for a TASK --- @param #TASK self --- @param #string From --- @param #string Event --- @param #string To -function TASK:onenterAborted( From, Event, To ) - - self:E( "Task Aborted" ) - - self:GetMission():GetCommandCenter():MessageToCoalition( "Task " .. self:GetName() .. " has been aborted! Task may be replanned." ) - - self:UnAssignFromGroups() - - self:__Replan( 5 ) -end - ---- FSM function for a TASK --- @param #TASK self --- @param #string From --- @param #string Event --- @param #string To -function TASK:onafterReplan( From, Event, To ) - - self:E( "Task Replanned" ) - - self:GetMission():GetCommandCenter():MessageToCoalition( "Replanning Task " .. self:GetName() .. "." ) - - self:SetMenu() - -end - ---- FSM function for a TASK --- @param #TASK self --- @param #string From --- @param #string Event --- @param #string To -function TASK:onenterFailed( From, Event, To ) - - self:E( "Task Failed" ) - - self:GetMission():GetCommandCenter():MessageToCoalition( "Task " .. self:GetName() .. " has failed!" ) - - self:UnAssignFromGroups() -end - ---- FSM function for a TASK --- @param #TASK self --- @param #string Event --- @param #string From --- @param #string To -function TASK:onstatechange( From, Event, To ) - - if self:IsTrace() then - MESSAGE:New( "@ Task " .. self.TaskName .. " : " .. Event .. " changed to state " .. To, 2 ):ToAll() - end - - if self.Scores[To] then - local Scoring = self:GetScoring() - if Scoring then - self:E( { self.Scores[To].ScoreText, self.Scores[To].Score } ) - Scoring:_AddMissionScore( self.Mission, self.Scores[To].ScoreText, self.Scores[To].Score ) - end - end - -end - ---- FSM function for a TASK --- @param #TASK self --- @param #string Event --- @param #string From --- @param #string To -function TASK:onenterPlanned( From, Event, To) - if not self.TimeOut == 0 then - self.__TimeOut( self.TimeOut ) - end -end - ---- FSM function for a TASK --- @param #TASK self --- @param #string Event --- @param #string From --- @param #string To -function TASK:onbeforeTimeOut( From, Event, To ) - if From == "Planned" then - self:RemoveMenu() - return true - end - return false -end - -do -- Reporting - ---- Create a summary report of the Task. --- List the Task Name and Status --- @param #TASK self --- @return #string -function TASK:ReportSummary() - - local Report = REPORT:New() - - -- List the name of the Task. - local Name = self:GetName() - - -- Determine the status of the Task. - local State = self:GetState() - - Report:Add( "Task " .. Name .. " - State '" .. State ) - - return Report:Text() -end - - ---- Create a detailed report of the Task. --- List the Task Status, and the Players assigned to the Task. --- @param #TASK self --- @return #string -function TASK:ReportDetails() - - local Report = REPORT:New() - - -- List the name of the Task. - local Name = self:GetName() - - -- Determine the status of the Task. - local State = self:GetState() - - - -- Loop each Unit active in the Task, and find Player Names. - local PlayerNames = {} - for PlayerGroupID, PlayerGroup in pairs( self:GetGroups():GetSet() ) do - local Player = PlayerGroup -- Wrapper.Group#GROUP - for PlayerUnitID, PlayerUnit in pairs( PlayerGroup:GetUnits() ) do - local PlayerUnit = PlayerUnit -- Wrapper.Unit#UNIT - if PlayerUnit and PlayerUnit:IsAlive() then - local PlayerName = PlayerUnit:GetPlayerName() - PlayerNames[#PlayerNames+1] = PlayerName - end - end - local PlayerNameText = table.concat( PlayerNames, ", " ) - Report:Add( "Task " .. Name .. " - State '" .. State .. "' - Players " .. PlayerNameText ) - end - - -- Loop each Process in the Task, and find Reporting Details. - - return Report:Text() -end - - -end -- Reporting ---- This module contains the DETECTION_MANAGER class and derived classes. --- --- === --- --- 1) @{DetectionManager#DETECTION_MANAGER} class, extends @{Base#BASE} --- ==================================================================== --- The @{DetectionManager#DETECTION_MANAGER} class defines the core functions to report detected objects to groups. --- Reportings can be done in several manners, and it is up to the derived classes if DETECTION_MANAGER to model the reporting behaviour. --- --- 1.1) DETECTION_MANAGER constructor: --- ----------------------------------- --- * @{DetectionManager#DETECTION_MANAGER.New}(): Create a new DETECTION_MANAGER instance. --- --- 1.2) DETECTION_MANAGER reporting: --- --------------------------------- --- Derived DETECTION_MANAGER classes will reports detected units using the method @{DetectionManager#DETECTION_MANAGER.ReportDetected}(). This method implements polymorphic behaviour. --- --- The time interval in seconds of the reporting can be changed using the methods @{DetectionManager#DETECTION_MANAGER.SetReportInterval}(). --- To control how long a reporting message is displayed, use @{DetectionManager#DETECTION_MANAGER.SetReportDisplayTime}(). --- Derived classes need to implement the method @{DetectionManager#DETECTION_MANAGER.GetReportDisplayTime}() to use the correct display time for displayed messages during a report. --- --- Reporting can be started and stopped using the methods @{DetectionManager#DETECTION_MANAGER.StartReporting}() and @{DetectionManager#DETECTION_MANAGER.StopReporting}() respectively. --- If an ad-hoc report is requested, use the method @{DetectionManager#DETECTION_MANAGER#ReportNow}(). --- --- The default reporting interval is every 60 seconds. The reporting messages are displayed 15 seconds. --- --- === --- --- 2) @{DetectionManager#DETECTION_REPORTING} class, extends @{DetectionManager#DETECTION_MANAGER} --- ========================================================================================= --- The @{DetectionManager#DETECTION_REPORTING} class implements detected units reporting. Reporting can be controlled using the reporting methods available in the @{DetectionManager#DETECTION_MANAGER} class. --- --- 2.1) DETECTION_REPORTING constructor: --- ------------------------------- --- The @{DetectionManager#DETECTION_REPORTING.New}() method creates a new DETECTION_REPORTING instance. --- --- === --- --- 3) @{#DETECTION_DISPATCHER} class, extends @{#DETECTION_MANAGER} --- ================================================================ --- The @{#DETECTION_DISPATCHER} class implements the dynamic dispatching of tasks upon groups of detected units determined a @{Set} of FAC (groups). --- The FAC will detect units, will group them, and will dispatch @{Task}s to groups. Depending on the type of target detected, different tasks will be dispatched. --- Find a summary below describing for which situation a task type is created: --- --- * **CAS Task**: Is created when there are enemy ground units within range of the FAC, while there are friendly units in the FAC perimeter. --- * **BAI Task**: Is created when there are enemy ground units within range of the FAC, while there are NO other friendly units within the FAC perimeter. --- * **SEAD Task**: Is created when there are enemy ground units wihtin range of the FAC, with air search radars. --- --- Other task types will follow... --- --- 3.1) DETECTION_DISPATCHER constructor: --- -------------------------------------- --- The @{#DETECTION_DISPATCHER.New}() method creates a new DETECTION_DISPATCHER instance. --- --- === --- --- ### Contributions: Mechanist, Prof_Hilactic, FlightControl - Concept & Testing --- ### Author: FlightControl - Framework Design & Programming --- --- @module DetectionManager - -do -- DETECTION MANAGER - - --- DETECTION_MANAGER class. - -- @type DETECTION_MANAGER - -- @field Set#SET_GROUP SetGroup The groups to which the FAC will report to. - -- @field Functional.Detection#DETECTION_BASE Detection The DETECTION_BASE object that is used to report the detected objects. - -- @extends Base#BASE - DETECTION_MANAGER = { - ClassName = "DETECTION_MANAGER", - SetGroup = nil, - Detection = nil, - } - - --- FAC constructor. - -- @param #DETECTION_MANAGER self - -- @param Set#SET_GROUP SetGroup - -- @param Functional.Detection#DETECTION_BASE Detection - -- @return #DETECTION_MANAGER self - function DETECTION_MANAGER:New( SetGroup, Detection ) - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) -- Functional.Detection#DETECTION_MANAGER - - self.SetGroup = SetGroup - self.Detection = Detection - - self:SetReportInterval( 30 ) - self:SetReportDisplayTime( 25 ) - - return self - end - - --- Set the reporting time interval. - -- @param #DETECTION_MANAGER self - -- @param #number ReportInterval The interval in seconds when a report needs to be done. - -- @return #DETECTION_MANAGER self - function DETECTION_MANAGER:SetReportInterval( ReportInterval ) - self:F2() - - self._ReportInterval = ReportInterval - end - - - --- Set the reporting message display time. - -- @param #DETECTION_MANAGER self - -- @param #number ReportDisplayTime The display time in seconds when a report needs to be done. - -- @return #DETECTION_MANAGER self - function DETECTION_MANAGER:SetReportDisplayTime( ReportDisplayTime ) - self:F2() - - self._ReportDisplayTime = ReportDisplayTime - end - - --- Get the reporting message display time. - -- @param #DETECTION_MANAGER self - -- @return #number ReportDisplayTime The display time in seconds when a report needs to be done. - function DETECTION_MANAGER:GetReportDisplayTime() - self:F2() - - return self._ReportDisplayTime - end - - - - --- Reports the detected items to the @{Set#SET_GROUP}. - -- @param #DETECTION_MANAGER self - -- @param Functional.Detection#DETECTION_BASE Detection - -- @return #DETECTION_MANAGER self - function DETECTION_MANAGER:ReportDetected( Detection ) - self:F2() - - end - - --- Schedule the FAC reporting. - -- @param #DETECTION_MANAGER self - -- @param #number DelayTime The delay in seconds to wait the reporting. - -- @param #number ReportInterval The repeat interval in seconds for the reporting to happen repeatedly. - -- @return #DETECTION_MANAGER self - function DETECTION_MANAGER:Schedule( DelayTime, ReportInterval ) - self:F2() - - self._ScheduleDelayTime = DelayTime - - self:SetReportInterval( ReportInterval ) - - self.FacScheduler = SCHEDULER:New(self, self._FacScheduler, { self, "DetectionManager" }, self._ScheduleDelayTime, self._ReportInterval ) - return self - end - - --- Report the detected @{Unit#UNIT}s detected within the @{Detection#DETECTION_BASE} object to the @{Set#SET_GROUP}s. - -- @param #DETECTION_MANAGER self - function DETECTION_MANAGER:_FacScheduler( SchedulerName ) - self:F2( { SchedulerName } ) - - return self:ProcessDetected( self.Detection ) - --- self.SetGroup:ForEachGroup( --- --- @param Wrapper.Group#GROUP Group --- function( Group ) --- if Group:IsAlive() then --- return self:ProcessDetected( self.Detection ) --- end --- end --- ) - --- return true - end - -end - - -do -- DETECTION_REPORTING - - --- DETECTION_REPORTING class. - -- @type DETECTION_REPORTING - -- @field Set#SET_GROUP SetGroup The groups to which the FAC will report to. - -- @field Functional.Detection#DETECTION_BASE Detection The DETECTION_BASE object that is used to report the detected objects. - -- @extends #DETECTION_MANAGER - DETECTION_REPORTING = { - ClassName = "DETECTION_REPORTING", - } - - - --- DETECTION_REPORTING constructor. - -- @param #DETECTION_REPORTING self - -- @param Set#SET_GROUP SetGroup - -- @param Functional.Detection#DETECTION_AREAS Detection - -- @return #DETECTION_REPORTING self - function DETECTION_REPORTING:New( SetGroup, Detection ) - - -- Inherits from DETECTION_MANAGER - local self = BASE:Inherit( self, DETECTION_MANAGER:New( SetGroup, Detection ) ) -- #DETECTION_REPORTING - - self:Schedule( 1, 30 ) - return self - end - - --- Creates a string of the detected items in a @{Detection}. - -- @param #DETECTION_MANAGER self - -- @param Set#SET_UNIT DetectedSet The detected Set created by the @{Detection#DETECTION_BASE} object. - -- @return #DETECTION_MANAGER self - function DETECTION_REPORTING:GetDetectedItemsText( DetectedSet ) - self:F2() - - local MT = {} -- Message Text - local UnitTypes = {} - - for DetectedUnitID, DetectedUnitData in pairs( DetectedSet:GetSet() ) do - local DetectedUnit = DetectedUnitData -- Wrapper.Unit#UNIT - if DetectedUnit:IsAlive() then - local UnitType = DetectedUnit:GetTypeName() - - if not UnitTypes[UnitType] then - UnitTypes[UnitType] = 1 - else - UnitTypes[UnitType] = UnitTypes[UnitType] + 1 - end - end - end - - for UnitTypeID, UnitType in pairs( UnitTypes ) do - MT[#MT+1] = UnitType .. " of " .. UnitTypeID - end - - return table.concat( MT, ", " ) - end - - - - --- Reports the detected items to the @{Set#SET_GROUP}. - -- @param #DETECTION_REPORTING self - -- @param Wrapper.Group#GROUP Group The @{Group} object to where the report needs to go. - -- @param Functional.Detection#DETECTION_AREAS Detection The detection created by the @{Detection#DETECTION_BASE} object. - -- @return #boolean Return true if you want the reporting to continue... false will cancel the reporting loop. - function DETECTION_REPORTING:ProcessDetected( Group, Detection ) - self:F2( Group ) - - self:E( Group ) - local DetectedMsg = {} - for DetectedAreaID, DetectedAreaData in pairs( Detection:GetDetectedAreas() ) do - local DetectedArea = DetectedAreaData -- Functional.Detection#DETECTION_AREAS.DetectedArea - DetectedMsg[#DetectedMsg+1] = " - Group #" .. DetectedAreaID .. ": " .. self:GetDetectedItemsText( DetectedArea.Set ) - end - local FACGroup = Detection:GetDetectionGroups() - FACGroup:MessageToGroup( "Reporting detected target groups:\n" .. table.concat( DetectedMsg, "\n" ), self:GetReportDisplayTime(), Group ) - - return true - end - -end - -do -- DETECTION_DISPATCHER - - --- DETECTION_DISPATCHER class. - -- @type DETECTION_DISPATCHER - -- @field Set#SET_GROUP SetGroup The groups to which the FAC will report to. - -- @field Functional.Detection#DETECTION_BASE Detection The DETECTION_BASE object that is used to report the detected objects. - -- @field Tasking.Mission#MISSION Mission - -- @field Wrapper.Group#GROUP CommandCenter - -- @extends Tasking.DetectionManager#DETECTION_MANAGER - DETECTION_DISPATCHER = { - ClassName = "DETECTION_DISPATCHER", - Mission = nil, - CommandCenter = nil, - Detection = nil, - } - - - --- DETECTION_DISPATCHER constructor. - -- @param #DETECTION_DISPATCHER self - -- @param Set#SET_GROUP SetGroup - -- @param Functional.Detection#DETECTION_BASE Detection - -- @return #DETECTION_DISPATCHER self - function DETECTION_DISPATCHER:New( Mission, CommandCenter, SetGroup, Detection ) - - -- Inherits from DETECTION_MANAGER - local self = BASE:Inherit( self, DETECTION_MANAGER:New( SetGroup, Detection ) ) -- #DETECTION_DISPATCHER - - self.Detection = Detection - self.CommandCenter = CommandCenter - self.Mission = Mission - - self:Schedule( 30 ) - return self - end - - - --- Creates a SEAD task when there are targets for it. - -- @param #DETECTION_DISPATCHER self - -- @param Functional.Detection#DETECTION_AREAS.DetectedArea DetectedArea - -- @return Set#SET_UNIT TargetSetUnit: The target set of units. - -- @return #nil If there are no targets to be set. - function DETECTION_DISPATCHER:EvaluateSEAD( DetectedArea ) - self:F( { DetectedArea.AreaID } ) - - local DetectedSet = DetectedArea.Set - local DetectedZone = DetectedArea.Zone - - -- Determine if the set has radar targets. If it does, construct a SEAD task. - local RadarCount = DetectedSet:HasSEAD() - - if RadarCount > 0 then - - -- Here we're doing something advanced... We're copying the DetectedSet, but making a new Set only with SEADable Radar units in it. - local TargetSetUnit = SET_UNIT:New() - TargetSetUnit:SetDatabase( DetectedSet ) - TargetSetUnit:FilterHasSEAD() - TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection. - - return TargetSetUnit - end - - return nil - end - - --- Creates a CAS task when there are targets for it. - -- @param #DETECTION_DISPATCHER self - -- @param Functional.Detection#DETECTION_AREAS.DetectedArea DetectedArea - -- @return Tasking.Task#TASK - function DETECTION_DISPATCHER:EvaluateCAS( DetectedArea ) - self:F( { DetectedArea.AreaID } ) - - local DetectedSet = DetectedArea.Set - local DetectedZone = DetectedArea.Zone - - - -- Determine if the set has radar targets. If it does, construct a SEAD task. - local GroundUnitCount = DetectedSet:HasGroundUnits() - local FriendliesNearBy = self.Detection:IsFriendliesNearBy( DetectedArea ) - - if GroundUnitCount > 0 and FriendliesNearBy == true then - - -- Copy the Set - local TargetSetUnit = SET_UNIT:New() - TargetSetUnit:SetDatabase( DetectedSet ) - TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection. - - return TargetSetUnit - end - - return nil - end - - --- Creates a BAI task when there are targets for it. - -- @param #DETECTION_DISPATCHER self - -- @param Functional.Detection#DETECTION_AREAS.DetectedArea DetectedArea - -- @return Tasking.Task#TASK - function DETECTION_DISPATCHER:EvaluateBAI( DetectedArea, FriendlyCoalition ) - self:F( { DetectedArea.AreaID } ) - - local DetectedSet = DetectedArea.Set - local DetectedZone = DetectedArea.Zone - - - -- Determine if the set has radar targets. If it does, construct a SEAD task. - local GroundUnitCount = DetectedSet:HasGroundUnits() - local FriendliesNearBy = self.Detection:IsFriendliesNearBy( DetectedArea ) - - if GroundUnitCount > 0 and FriendliesNearBy == false then - - -- Copy the Set - local TargetSetUnit = SET_UNIT:New() - TargetSetUnit:SetDatabase( DetectedSet ) - TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection. - - return TargetSetUnit - end - - return nil - end - - --- Evaluates the removal of the Task from the Mission. - -- Can only occur when the DetectedArea is Changed AND the state of the Task is "Planned". - -- @param #DETECTION_DISPATCHER self - -- @param Tasking.Mission#MISSION Mission - -- @param Tasking.Task#TASK Task - -- @param Functional.Detection#DETECTION_AREAS.DetectedArea DetectedArea - -- @return Tasking.Task#TASK - function DETECTION_DISPATCHER:EvaluateRemoveTask( Mission, Task, DetectedArea ) - - if Task then - if Task:IsStatePlanned() and DetectedArea.Changed == true then - self:E( "Removing Tasking: " .. Task:GetTaskName() ) - Task = Mission:RemoveTask( Task ) - end - end - - return Task - end - - - --- Assigns tasks in relation to the detected items to the @{Set#SET_GROUP}. - -- @param #DETECTION_DISPATCHER self - -- @param Functional.Detection#DETECTION_AREAS Detection The detection created by the @{Detection#DETECTION_AREAS} object. - -- @return #boolean Return true if you want the task assigning to continue... false will cancel the loop. - function DETECTION_DISPATCHER:ProcessDetected( Detection ) - self:F2() - - local AreaMsg = {} - local TaskMsg = {} - local ChangeMsg = {} - - local Mission = self.Mission - - --- First we need to the detected targets. - for DetectedAreaID, DetectedAreaData in ipairs( Detection:GetDetectedAreas() ) do - - local DetectedArea = DetectedAreaData -- Functional.Detection#DETECTION_AREAS.DetectedArea - local DetectedSet = DetectedArea.Set - local DetectedZone = DetectedArea.Zone - self:E( { "Targets in DetectedArea", DetectedArea.AreaID, DetectedSet:Count(), tostring( DetectedArea ) } ) - DetectedSet:Flush() - - local AreaID = DetectedArea.AreaID - - -- Evaluate SEAD Tasking - local SEADTask = Mission:GetTask( "SEAD." .. AreaID ) - SEADTask = self:EvaluateRemoveTask( Mission, SEADTask, DetectedArea ) - if not SEADTask then - local TargetSetUnit = self:EvaluateSEAD( DetectedArea ) -- Returns a SetUnit if there are targets to be SEADed... - if TargetSetUnit then - SEADTask = Mission:AddTask( TASK_SEAD:New( Mission, self.SetGroup, "SEAD." .. AreaID, TargetSetUnit , DetectedZone ) ) - end - end - if SEADTask and SEADTask:IsStatePlanned() then - self:E( "Planned" ) - --SEADTask:SetPlannedMenu() - TaskMsg[#TaskMsg+1] = " - " .. SEADTask:GetStateString() .. " SEAD " .. AreaID .. " - " .. SEADTask.TargetSetUnit:GetUnitTypesText() - end - - -- Evaluate CAS Tasking - local CASTask = Mission:GetTask( "CAS." .. AreaID ) - CASTask = self:EvaluateRemoveTask( Mission, CASTask, DetectedArea ) - if not CASTask then - local TargetSetUnit = self:EvaluateCAS( DetectedArea ) -- Returns a SetUnit if there are targets to be SEADed... - if TargetSetUnit then - CASTask = Mission:AddTask( TASK_A2G:New( Mission, self.SetGroup, "CAS." .. AreaID, "CAS", TargetSetUnit , DetectedZone, DetectedArea.NearestFAC ) ) - end - end - if CASTask and CASTask:IsStatePlanned() then - --CASTask:SetPlannedMenu() - TaskMsg[#TaskMsg+1] = " - " .. CASTask:GetStateString() .. " CAS " .. AreaID .. " - " .. CASTask.TargetSetUnit:GetUnitTypesText() - end - - -- Evaluate BAI Tasking - local BAITask = Mission:GetTask( "BAI." .. AreaID ) - BAITask = self:EvaluateRemoveTask( Mission, BAITask, DetectedArea ) - if not BAITask then - local TargetSetUnit = self:EvaluateBAI( DetectedArea, self.CommandCenter:GetCoalition() ) -- Returns a SetUnit if there are targets to be SEADed... - if TargetSetUnit then - BAITask = Mission:AddTask( TASK_A2G:New( Mission, self.SetGroup, "BAI." .. AreaID, "BAI", TargetSetUnit , DetectedZone, DetectedArea.NearestFAC ) ) - end - end - if BAITask and BAITask:IsStatePlanned() then - --BAITask:SetPlannedMenu() - TaskMsg[#TaskMsg+1] = " - " .. BAITask:GetStateString() .. " BAI " .. AreaID .. " - " .. BAITask.TargetSetUnit:GetUnitTypesText() - end - - if #TaskMsg > 0 then - - local ThreatLevel = Detection:GetTreatLevelA2G( DetectedArea ) - - local DetectedAreaVec3 = DetectedZone:GetVec3() - local DetectedAreaPointVec3 = POINT_VEC3:New( DetectedAreaVec3.x, DetectedAreaVec3.y, DetectedAreaVec3.z ) - local DetectedAreaPointLL = DetectedAreaPointVec3:ToStringLL( 3, true ) - AreaMsg[#AreaMsg+1] = string.format( " - Area #%d - %s - Threat Level [%s] (%2d)", - DetectedAreaID, - DetectedAreaPointLL, - string.rep( "â– ", ThreatLevel ), - ThreatLevel - ) - - -- Loop through the changes ... - local ChangeText = Detection:GetChangeText( DetectedArea ) - - if ChangeText ~= "" then - ChangeMsg[#ChangeMsg+1] = string.gsub( string.gsub( ChangeText, "\n", "%1 - " ), "^.", " - %1" ) - end - end - - -- OK, so the tasking has been done, now delete the changes reported for the area. - Detection:AcceptChanges( DetectedArea ) - - end - - -- TODO set menus using the HQ coordinator - Mission:GetCommandCenter():SetMenu() - - if #AreaMsg > 0 then - for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do - if not TaskGroup:GetState( TaskGroup, "Assigned" ) then - self.CommandCenter:MessageToGroup( - string.format( "HQ Reporting - Target areas for mission '%s':\nAreas:\n%s\n\nTasks:\n%s\n\nChanges:\n%s ", - self.Mission:GetName(), - table.concat( AreaMsg, "\n" ), - table.concat( TaskMsg, "\n" ), - table.concat( ChangeMsg, "\n" ) - ), self:GetReportDisplayTime(), TaskGroup - ) - end - end - end - - return true - end - -end--- This module contains the TASK_SEAD classes. --- --- 1) @{#TASK_SEAD} class, extends @{Task#TASK} --- ================================================= --- The @{#TASK_SEAD} class defines a SEAD task for a @{Set} of Target Units, located at a Target Zone, --- based on the tasking capabilities defined in @{Task#TASK}. --- The TASK_SEAD is implemented using a @{Statemachine#FSM_TASK}, and has the following statuses: --- --- * **None**: Start of the process --- * **Planned**: The SEAD task is planned. Upon Planned, the sub-process @{Process_Fsm.Assign#ACT_ASSIGN_ACCEPT} is started to accept the task. --- * **Assigned**: The SEAD task is assigned to a @{Group#GROUP}. Upon Assigned, the sub-process @{Process_Fsm.Route#ACT_ROUTE} is started to route the active Units in the Group to the attack zone. --- * **Success**: The SEAD task is successfully completed. Upon Success, the sub-process @{Process_SEAD#PROCESS_SEAD} is started to follow-up successful SEADing of the targets assigned in the task. --- * **Failed**: The SEAD task has failed. This will happen if the player exists the task early, without communicating a possible cancellation to HQ. --- --- === --- --- ### Authors: FlightControl - Design and Programming --- --- @module Task_SEAD - - - -do -- TASK_SEAD - - --- The TASK_SEAD class - -- @type TASK_SEAD - -- @field Set#SET_UNIT TargetSetUnit - -- @extends Tasking.Task#TASK - TASK_SEAD = { - ClassName = "TASK_SEAD", - } - - --- Instantiates a new TASK_SEAD. - -- @param #TASK_SEAD self - -- @param Tasking.Mission#MISSION Mission - -- @param Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. - -- @param #string TaskName The name of the Task. - -- @param Set#SET_UNIT UnitSetTargets - -- @param Core.Zone#ZONE_BASE TargetZone - -- @return #TASK_SEAD self - function TASK_SEAD:New( Mission, SetGroup, TaskName, TargetSetUnit, TargetZone ) - local self = BASE:Inherit( self, TASK:New( Mission, SetGroup, TaskName, "SEAD" ) ) -- Tasking.Task_SEAD#TASK_SEAD - self:F() - - self.TargetSetUnit = TargetSetUnit - self.TargetZone = TargetZone - - local Fsm = self:GetUnitProcess() - - Fsm:AddProcess ( "Planned", "Accept", ACT_ASSIGN_ACCEPT:New( self.TaskBriefing ), { Assigned = "Route", Rejected = "Eject" } ) - Fsm:AddProcess ( "Assigned", "Route", ACT_ROUTE_ZONE:New( self.TargetZone ), { Arrived = "Update" } ) - Fsm:AddTransition( "Rejected", "Eject", "Planned" ) - Fsm:AddTransition( "Arrived", "Update", "Updated" ) - Fsm:AddProcess ( "Updated", "Account", ACT_ACCOUNT_DEADS:New( self.TargetSetUnit, "SEAD" ), { Accounted = "Success" } ) - Fsm:AddProcess ( "Updated", "Smoke", ACT_ASSIST_SMOKE_TARGETS_ZONE:New( self.TargetSetUnit, self.TargetZone ) ) - Fsm:AddTransition( "Accounted", "Success", "Success" ) - Fsm:AddTransition( "Failed", "Fail", "Failed" ) - - function Fsm:onenterUpdated( TaskUnit ) - self:E( { self } ) - self:Account() - self:Smoke() - end - --- _EVENTDISPATCHER:OnPlayerLeaveUnit( self._EventPlayerLeaveUnit, self ) --- _EVENTDISPATCHER:OnDead( self._EventDead, self ) --- _EVENTDISPATCHER:OnCrash( self._EventDead, self ) --- _EVENTDISPATCHER:OnPilotDead( self._EventDead, self ) - - return self - end - - --- @param #TASK_SEAD self - function TASK_SEAD:GetPlannedMenuText() - return self:GetStateString() .. " - " .. self:GetTaskName() .. " ( " .. self.TargetSetUnit:GetUnitTypesText() .. " )" - end - -end ---- (AI) (SP) (MP) Tasking for Air to Ground Processes. --- --- 1) @{#TASK_A2G} class, extends @{Task#TASK} --- ================================================= --- The @{#TASK_A2G} class defines a CAS or BAI task of a @{Set} of Target Units, --- located at a Target Zone, based on the tasking capabilities defined in @{Task#TASK}. --- The TASK_A2G is implemented using a @{Statemachine#FSM_TASK}, and has the following statuses: --- --- * **None**: Start of the process --- * **Planned**: The SEAD task is planned. Upon Planned, the sub-process @{Process_Fsm.Assign#ACT_ASSIGN_ACCEPT} is started to accept the task. --- * **Assigned**: The SEAD task is assigned to a @{Group#GROUP}. Upon Assigned, the sub-process @{Process_Fsm.Route#ACT_ROUTE} is started to route the active Units in the Group to the attack zone. --- * **Success**: The SEAD task is successfully completed. Upon Success, the sub-process @{Process_SEAD#PROCESS_SEAD} is started to follow-up successful SEADing of the targets assigned in the task. --- * **Failed**: The SEAD task has failed. This will happen if the player exists the task early, without communicating a possible cancellation to HQ. --- --- === --- --- ### Authors: FlightControl - Design and Programming --- --- @module Task_A2G - - -do -- TASK_A2G - - --- The TASK_A2G class - -- @type TASK_A2G - -- @extends Tasking.Task#TASK - TASK_A2G = { - ClassName = "TASK_A2G", - } - - --- Instantiates a new TASK_A2G. - -- @param #TASK_A2G self - -- @param Tasking.Mission#MISSION Mission - -- @param Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. - -- @param #string TaskName The name of the Task. - -- @param #string TaskType BAI or CAS - -- @param Set#SET_UNIT UnitSetTargets - -- @param Core.Zone#ZONE_BASE TargetZone - -- @return #TASK_A2G self - function TASK_A2G:New( Mission, SetGroup, TaskName, TaskType, TargetSetUnit, TargetZone, FACUnit ) - local self = BASE:Inherit( self, TASK:New( Mission, SetGroup, TaskName, TaskType ) ) - self:F() - - self.TargetSetUnit = TargetSetUnit - self.TargetZone = TargetZone - self.FACUnit = FACUnit - - local A2GUnitProcess = self:GetUnitProcess() - - A2GUnitProcess:AddProcess ( "Planned", "Accept", ACT_ASSIGN_ACCEPT:New( "Attack the Area" ), { Assigned = "Route", Rejected = "Eject" } ) - A2GUnitProcess:AddProcess ( "Assigned", "Route", ACT_ROUTE_ZONE:New( self.TargetZone ), { Arrived = "Update" } ) - A2GUnitProcess:AddTransition( "Rejected", "Eject", "Planned" ) - A2GUnitProcess:AddTransition( "Arrived", "Update", "Updated" ) - A2GUnitProcess:AddProcess ( "Updated", "Account", ACT_ACCOUNT_DEADS:New( self.TargetSetUnit, "Attack" ), { Accounted = "Success" } ) - A2GUnitProcess:AddProcess ( "Updated", "Smoke", ACT_ASSIST_SMOKE_TARGETS_ZONE:New( self.TargetSetUnit, self.TargetZone ) ) - --Fsm:AddProcess ( "Updated", "JTAC", PROCESS_JTAC:New( self, TaskUnit, self.TargetSetUnit, self.FACUnit ) ) - A2GUnitProcess:AddTransition( "Accounted", "Success", "Success" ) - A2GUnitProcess:AddTransition( "Failed", "Fail", "Failed" ) - - function A2GUnitProcess:onenterUpdated( TaskUnit ) - self:E( { self } ) - self:Account() - self:Smoke() - end - - - - --_EVENTDISPATCHER:OnPlayerLeaveUnit( self._EventPlayerLeaveUnit, self ) - --_EVENTDISPATCHER:OnDead( self._EventDead, self ) - --_EVENTDISPATCHER:OnCrash( self._EventDead, self ) - --_EVENTDISPATCHER:OnPilotDead( self._EventDead, self ) - - return self - end - - --- @param #TASK_A2G self - function TASK_A2G:GetPlannedMenuText() - return self:GetStateString() .. " - " .. self:GetTaskName() .. " ( " .. self.TargetSetUnit:GetUnitTypesText() .. " )" - end - - end - - - ---- The main include file for the MOOSE system. - ---- Core Routines -Include.File( "Utilities/Routines" ) -Include.File( "Utilities/Utils" ) - ---- Core Classes -Include.File( "Core/Base" ) -Include.File( "Core/Scheduler" ) -Include.File( "Core/ScheduleDispatcher") -Include.File( "Core/Event" ) -Include.File( "Core/Menu" ) -Include.File( "Core/Zone" ) -Include.File( "Core/Database" ) -Include.File( "Core/Set" ) -Include.File( "Core/Point" ) -Include.File( "Core/Message" ) -Include.File( "Core/Fsm" ) - ---- Wrapper Classes -Include.File( "Wrapper/Object" ) -Include.File( "Wrapper/Identifiable" ) -Include.File( "Wrapper/Positionable" ) -Include.File( "Wrapper/Controllable" ) -Include.File( "Wrapper/Group" ) -Include.File( "Wrapper/Unit" ) -Include.File( "Wrapper/Client" ) -Include.File( "Wrapper/Static" ) -Include.File( "Wrapper/Airbase" ) -Include.File( "Wrapper/Scenery" ) - ---- Functional Classes -Include.File( "Functional/Scoring" ) -Include.File( "Functional/CleanUp" ) -Include.File( "Functional/Spawn" ) -Include.File( "Functional/Movement" ) -Include.File( "Functional/Sead" ) -Include.File( "Functional/Escort" ) -Include.File( "Functional/MissileTrainer" ) -Include.File( "Functional/AirbasePolice" ) -Include.File( "Functional/Detection" ) - ---- AI Classes -Include.File( "AI/AI_Balancer" ) -Include.File( "AI/AI_Patrol" ) -Include.File( "AI/AI_Cap" ) -Include.File( "AI/AI_Cas" ) -Include.File( "AI/AI_Cargo" ) - ---- Actions -Include.File( "Actions/Act_Assign" ) -Include.File( "Actions/Act_Route" ) -Include.File( "Actions/Act_Account" ) -Include.File( "Actions/Act_Assist" ) - ---- Task Handling Classes -Include.File( "Tasking/CommandCenter" ) -Include.File( "Tasking/Mission" ) -Include.File( "Tasking/Task" ) -Include.File( "Tasking/DetectionManager" ) -Include.File( "Tasking/Task_SEAD" ) -Include.File( "Tasking/Task_A2G" ) - - --- The order of the declarations is important here. Don't touch it. - ---- Declare the event dispatcher based on the EVENT class -_EVENTDISPATCHER = EVENT:New() -- Core.Event#EVENT - ---- Declare the timer dispatcher based on the SCHEDULEDISPATCHER class -_SCHEDULEDISPATCHER = SCHEDULEDISPATCHER:New() -- Core.Timer#SCHEDULEDISPATCHER - ---- Declare the main database object, which is used internally by the MOOSE classes. -_DATABASE = DATABASE:New() -- Database#DATABASE +Include.ProgramPath = "Scripts/Moose/" +env.info( "Include.ProgramPath = " .. Include.ProgramPath) +Include.Files = {} +Include.File( "Moose" ) -BASE:TraceOnOff( false ) +BASE:TraceOnOff( true ) env.info( '*** MOOSE INCLUDE END *** ' ) diff --git a/Moose Test Missions/EVT - Event Handling/EVT-200 - GROUP OnEventShot Example/EVT-200 - GROUP OnEventShot Example.lua b/Moose Test Missions/EVT - Event Handling/EVT-200 - GROUP OnEventShot Example/EVT-200 - GROUP OnEventShot Example.lua new file mode 100644 index 000000000..b569b45c0 --- /dev/null +++ b/Moose Test Missions/EVT - Event Handling/EVT-200 - GROUP OnEventShot Example/EVT-200 - GROUP OnEventShot Example.lua @@ -0,0 +1,28 @@ +--- +-- Name: EVT-200 - GROUP OnEventShot Example +-- Author: FlightControl +-- Date Created: 07 Mar 2017 +-- +-- # Situation: +-- +-- Two groups of planes are flying in the air and shoot an missile to a multitude of ground targets. +-- +-- # Test cases: +-- +-- 1. Observe the planes shooting the missile. +-- 2. Observe when the planes shoots the missile, a dcs.log entry is written in the logging. +-- 3. Check the contents of the fields of the S_EVENT_SHOT entry. +-- 4. The planes of GROUP "Group Plane A", should only send a message when they shoot a missile. +-- 5. The planes of GROUP "Group Plane B", should NOT send a message when they shoot a missile. + +local PlaneGroup = GROUP:FindByName( "Group Plane A" ) + +PlaneGroup:HandleEvent( EVENTS.Shot ) + +function PlaneGroup:OnEventShot( EventData ) + + self:E( "I just fired a missile and I am part of " .. EventData.IniGroupName ) + EventData.IniUnit:MessageToAll( "I just fired a missile and I am part of " .. EventData.IniGroupName, 15, "Alert!" ) +end + + diff --git a/Moose Test Missions/EVT - Event Handling/EVT-200 - GROUP OnEventShot Example/EVT-200 - GROUP OnEventShot Example.miz b/Moose Test Missions/EVT - Event Handling/EVT-200 - GROUP OnEventShot Example/EVT-200 - GROUP OnEventShot Example.miz new file mode 100644 index 0000000000000000000000000000000000000000..0f10d126eb9d78611153972b240c31b912c7ea83 GIT binary patch literal 26669 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 Uz}o@@1Rl5t0);?{CjYelA2f{qmjD0& literal 0 HcmV?d00001 From 8dc13f7a0cd16115534e0c09e035ed8d04e45cdd Mon Sep 17 00:00:00 2001 From: FlightControl Date: Tue, 7 Mar 2017 09:24:10 +0100 Subject: [PATCH 3/5] Finalization of patch. --- Moose Development/Moose/Core/Event.lua | 7 + Moose Development/Moose/Wrapper/Group.lua | 3 + .../l10n/DEFAULT/Moose.lua | 33646 +++++++++++++++- Moose Mission Setup/Moose.lua | 33646 +++++++++++++++- .../EVT-200 - GROUP OnEventShot Example.miz | Bin 26669 -> 235773 bytes docs/Documentation/AI_Patrol.html | 246 +- docs/Documentation/Cargo.html | 1 - docs/Documentation/Event.html | 130 +- docs/Documentation/Group.html | 73 + docs/Documentation/Spawn.html | 7 +- 10 files changed, 67708 insertions(+), 51 deletions(-) diff --git a/Moose Development/Moose/Core/Event.lua b/Moose Development/Moose/Core/Event.lua index 9a2d8f8aa..10aa2804e 100644 --- a/Moose Development/Moose/Core/Event.lua +++ b/Moose Development/Moose/Core/Event.lua @@ -64,6 +64,13 @@ -- -- * @{Base#BASE.HandleEvent}(): Subscribe to a DCS Event. -- * @{Base#BASE.UnHandleEvent}(): Unsubscribe from a DCS Event. +-- +-- Note that for a UNIT, the event will be handled **for that UNIT only**! +-- Note that for a GROUP, the event will be handled **for all the UNITs in that GROUP only**! +-- +-- For all objects of other classes, the subscribed events will be handled for **all UNITs within the Mission**! +-- So if a UNIT within the mission has the subscribed event for that object, +-- then the object event handler will receive the event for that UNIT! -- -- ### 1.3.2 Event Handling of DCS Events -- diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index dccfe2ed6..4669911e3 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -76,6 +76,9 @@ -- -- Hereby the change log: -- +-- 2017-03-07: GROUP:**HandleEvent( Event, EventFunction )** added. +-- 2017-03-07: GROUP:**UnHandleEvent( Event )** added. +-- -- 2017-01-24: GROUP:**SetAIOnOff( AIOnOff )** added. -- -- 2017-01-24: GROUP:**SetAIOn()** added. 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 f73ae2d1c..e32e1c346 100644 --- a/Moose Mission Setup/Moose Mission Update/l10n/DEFAULT/Moose.lua +++ b/Moose Mission Setup/Moose Mission Update/l10n/DEFAULT/Moose.lua @@ -1,31 +1,33643 @@ -env.info( '*** MOOSE DYNAMIC INCLUDE START *** ' ) -env.info( 'Moose Generation Timestamp: 20170307_0856' ) - +env.info( '*** MOOSE STATIC INCLUDE START *** ' ) +env.info( 'Moose Generation Timestamp: 20170307_0923' ) local base = _G Include = {} - +Include.Files = {} Include.File = function( IncludeFile ) - 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 - error ("Could not load MOOSE file " .. IncludeFile .. ".lua" ) +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 - env.info( "Include:" .. IncludeFile .. " loaded from " .. Include.ProgramPath ) - return f() + 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 -Include.ProgramPath = "Scripts/Moose/" -env.info( "Include.ProgramPath = " .. Include.ProgramPath) +routines.utils.toDegree = function(angle) + return angle*180/math.pi +end -Include.Files = {} +routines.utils.toRadian = function(angle) + return angle*math.pi/180 +end -Include.File( "Moose" ) +routines.utils.metersToNM = function(meters) + return meters/1852 +end -BASE:TraceOnOff( true ) +routines.utils.metersToFeet = function(meters) + return meters/0.3048 +end + +routines.utils.NMToMeters = function(NM) + return NM*1852 +end + +routines.utils.feetToMeters = function(feet) + return feet*0.3048 +end + +routines.utils.mpsToKnots = function(mps) + return mps*3600/1852 +end + +routines.utils.mpsToKmph = function(mps) + return mps*3.6 +end + +routines.utils.knotsToMps = function(knots) + return knots*1852/3600 +end + +routines.utils.kmphToMps = function(kmph) + return kmph/3.6 +end + +function routines.utils.makeVec2(Vec3) + if Vec3.z then + return {x = Vec3.x, y = Vec3.z} + else + return {x = Vec3.x, y = Vec3.y} -- it was actually already vec2. + end +end + +function routines.utils.makeVec3(Vec2, y) + if not Vec2.z then + if not y then + y = 0 + end + return {x = Vec2.x, y = y, z = Vec2.y} + else + return {x = Vec2.x, y = Vec2.y, z = Vec2.z} -- it was already Vec3, actually. + end +end + +function routines.utils.makeVec3GL(Vec2, offset) + local adj = offset or 0 + + if not Vec2.z then + return {x = Vec2.x, y = (land.getHeight(Vec2) + adj), z = Vec2.y} + else + return {x = Vec2.x, y = (land.getHeight({x = Vec2.x, y = Vec2.z}) + adj), z = Vec2.z} + end +end + +routines.utils.zoneToVec3 = function(zone) + local new = {} + if type(zone) == 'table' and zone.point then + new.x = zone.point.x + new.y = zone.point.y + new.z = zone.point.z + return new + elseif type(zone) == 'string' then + zone = trigger.misc.getZone(zone) + if zone then + new.x = zone.point.x + new.y = zone.point.y + new.z = zone.point.z + return new + end + end +end + +-- gets heading-error corrected direction from point along vector vec. +function routines.utils.getDir(vec, point) + local dir = math.atan2(vec.z, vec.x) + dir = dir + routines.getNorthCorrection(point) + if dir < 0 then + dir = dir + 2*math.pi -- put dir in range of 0 to 2*pi + end + return dir +end + +-- gets distance in meters between two points (2 dimensional) +function routines.utils.get2DDist(point1, point2) + point1 = routines.utils.makeVec3(point1) + point2 = routines.utils.makeVec3(point2) + return routines.vec.mag({x = point1.x - point2.x, y = 0, z = point1.z - point2.z}) +end + +-- gets distance in meters between two points (3 dimensional) +function routines.utils.get3DDist(point1, point2) + return routines.vec.mag({x = point1.x - point2.x, y = point1.y - point2.y, z = point1.z - point2.z}) +end + + + + + +--3D Vector manipulation +routines.vec = {} + +routines.vec.add = function(vec1, vec2) + return {x = vec1.x + vec2.x, y = vec1.y + vec2.y, z = vec1.z + vec2.z} +end + +routines.vec.sub = function(vec1, vec2) + return {x = vec1.x - vec2.x, y = vec1.y - vec2.y, z = vec1.z - vec2.z} +end + +routines.vec.scalarMult = function(vec, mult) + return {x = vec.x*mult, y = vec.y*mult, z = vec.z*mult} +end + +routines.vec.scalar_mult = routines.vec.scalarMult + +routines.vec.dp = function(vec1, vec2) + return vec1.x*vec2.x + vec1.y*vec2.y + vec1.z*vec2.z +end + +routines.vec.cp = function(vec1, vec2) + return { x = vec1.y*vec2.z - vec1.z*vec2.y, y = vec1.z*vec2.x - vec1.x*vec2.z, z = vec1.x*vec2.y - vec1.y*vec2.x} +end + +routines.vec.mag = function(vec) + return (vec.x^2 + vec.y^2 + vec.z^2)^0.5 +end + +routines.vec.getUnitVec = function(vec) + local mag = routines.vec.mag(vec) + return { x = vec.x/mag, y = vec.y/mag, z = vec.z/mag } +end + +routines.vec.rotateVec2 = function(vec2, theta) + return { x = vec2.x*math.cos(theta) - vec2.y*math.sin(theta), y = vec2.x*math.sin(theta) + vec2.y*math.cos(theta)} +end +--------------------------------------------------------------------------------------------------------------------------- + + + + +-- acc- the accuracy of each easting/northing. 0, 1, 2, 3, 4, or 5. +routines.tostringMGRS = function(MGRS, acc) + if acc == 0 then + return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph + else + return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph .. ' ' .. string.format('%0' .. acc .. 'd', routines.utils.round(MGRS.Easting/(10^(5-acc)), 0)) + .. ' ' .. string.format('%0' .. acc .. 'd', routines.utils.round(MGRS.Northing/(10^(5-acc)), 0)) + end +end + +--[[acc: +in DM: decimal point of minutes. +In DMS: decimal point of seconds. +position after the decimal of the least significant digit: +So: +42.32 - acc of 2. +]] +routines.tostringLL = function(lat, lon, acc, DMS) + + local latHemi, lonHemi + if lat > 0 then + latHemi = 'N' + else + latHemi = 'S' + end + + if lon > 0 then + lonHemi = 'E' + else + lonHemi = 'W' + end + + lat = math.abs(lat) + lon = math.abs(lon) + + local latDeg = math.floor(lat) + local latMin = (lat - latDeg)*60 + + local lonDeg = math.floor(lon) + local lonMin = (lon - lonDeg)*60 + + if DMS then -- degrees, minutes, and seconds. + local oldLatMin = latMin + latMin = math.floor(latMin) + local latSec = routines.utils.round((oldLatMin - latMin)*60, acc) + + local oldLonMin = lonMin + lonMin = math.floor(lonMin) + local lonSec = routines.utils.round((oldLonMin - lonMin)*60, acc) + + if latSec == 60 then + latSec = 0 + latMin = latMin + 1 + end + + if lonSec == 60 then + lonSec = 0 + lonMin = lonMin + 1 + end + + local secFrmtStr -- create the formatting string for the seconds place + if acc <= 0 then -- no decimal place. + secFrmtStr = '%02d' + else + local width = 3 + acc -- 01.310 - that's a width of 6, for example. + secFrmtStr = '%0' .. width .. '.' .. acc .. 'f' + end + + return string.format('%02d', latDeg) .. ' ' .. string.format('%02d', latMin) .. '\' ' .. string.format(secFrmtStr, latSec) .. '"' .. latHemi .. ' ' + .. string.format('%02d', lonDeg) .. ' ' .. string.format('%02d', lonMin) .. '\' ' .. string.format(secFrmtStr, lonSec) .. '"' .. lonHemi + + else -- degrees, decimal minutes. + latMin = routines.utils.round(latMin, acc) + lonMin = routines.utils.round(lonMin, acc) + + if latMin == 60 then + latMin = 0 + latDeg = latDeg + 1 + end + + if lonMin == 60 then + lonMin = 0 + lonDeg = lonDeg + 1 + end + + local minFrmtStr -- create the formatting string for the minutes place + if acc <= 0 then -- no decimal place. + minFrmtStr = '%02d' + else + local width = 3 + acc -- 01.310 - that's a width of 6, for example. + minFrmtStr = '%0' .. width .. '.' .. acc .. 'f' + end + + return string.format('%02d', latDeg) .. ' ' .. string.format(minFrmtStr, latMin) .. '\'' .. latHemi .. ' ' + .. string.format('%02d', lonDeg) .. ' ' .. string.format(minFrmtStr, lonMin) .. '\'' .. lonHemi + + end +end + +--[[ required: az - radian + required: dist - meters + optional: alt - meters (set to false or nil if you don't want to use it). + optional: metric - set true to get dist and alt in km and m. + precision will always be nearest degree and NM or km.]] +routines.tostringBR = function(az, dist, alt, metric) + az = routines.utils.round(routines.utils.toDegree(az), 0) + + if metric then + dist = routines.utils.round(dist/1000, 2) + else + dist = routines.utils.round(routines.utils.metersToNM(dist), 2) + end + + local s = string.format('%03d', az) .. ' for ' .. dist + + if alt then + if metric then + s = s .. ' at ' .. routines.utils.round(alt, 0) + else + s = s .. ' at ' .. routines.utils.round(routines.utils.metersToFeet(alt), 0) + end + end + return s +end + +routines.getNorthCorrection = function(point) --gets the correction needed for true north + if not point.z then --Vec2; convert to Vec3 + point.z = point.y + point.y = 0 + end + local lat, lon = coord.LOtoLL(point) + local north_posit = coord.LLtoLO(lat + 1, lon) + return math.atan2(north_posit.z - point.z, north_posit.x - point.x) +end + + +do + local idNum = 0 + + --Simplified event handler + routines.addEventHandler = function(f) --id is optional! + local handler = {} + idNum = idNum + 1 + handler.id = idNum + handler.f = f + handler.onEvent = function(self, event) + self.f(event) + end + world.addEventHandler(handler) + end + + routines.removeEventHandler = function(id) + for key, handler in pairs(world.eventHandlers) do + if handler.id and handler.id == id then + world.eventHandlers[key] = nil + return true + end + end + return false + end +end + +-- need to return a Vec3 or Vec2? +function routines.getRandPointInCircle(point, radius, innerRadius) + local theta = 2*math.pi*math.random() + local rad = math.random() + math.random() + if rad > 1 then + rad = 2 - rad + end + + local radMult + if innerRadius and innerRadius <= radius then + radMult = (radius - innerRadius)*rad + innerRadius + else + radMult = radius*rad + end + + if not point.z then --might as well work with vec2/3 + point.z = point.y + end + + local rndCoord + if radius > 0 then + rndCoord = {x = math.cos(theta)*radMult + point.x, y = math.sin(theta)*radMult + point.z} + else + rndCoord = {x = point.x, y = point.z} + end + return rndCoord +end + +routines.goRoute = function(group, path) + local misTask = { + id = 'Mission', + params = { + route = { + points = routines.utils.deepCopy(path), + }, + }, + } + if type(group) == 'string' then + group = Group.getByName(group) + end + local groupCon = group:getController() + if groupCon then + groupCon:setTask(misTask) + return true + end + + Controller.setTask(groupCon, misTask) + return false +end + + +-- Useful atomic functions from mist, ported. + +routines.ground = {} +routines.fixedWing = {} +routines.heli = {} + +routines.ground.buildWP = function(point, overRideForm, overRideSpeed) + + local wp = {} + wp.x = point.x + + if point.z then + wp.y = point.z + else + wp.y = point.y + end + local form, speed + + if point.speed and not overRideSpeed then + wp.speed = point.speed + elseif type(overRideSpeed) == 'number' then + wp.speed = overRideSpeed + else + wp.speed = routines.utils.kmphToMps(20) + end + + if point.form and not overRideForm then + form = point.form + else + form = overRideForm + end + + if not form then + wp.action = 'Cone' + else + form = string.lower(form) + if form == 'off_road' or form == 'off road' then + wp.action = 'Off Road' + elseif form == 'on_road' or form == 'on road' then + wp.action = 'On Road' + elseif form == 'rank' or form == 'line_abrest' or form == 'line abrest' or form == 'lineabrest'then + wp.action = 'Rank' + elseif form == 'cone' then + wp.action = 'Cone' + elseif form == 'diamond' then + wp.action = 'Diamond' + elseif form == 'vee' then + wp.action = 'Vee' + elseif form == 'echelon_left' or form == 'echelon left' or form == 'echelonl' then + wp.action = 'EchelonL' + elseif form == 'echelon_right' or form == 'echelon right' or form == 'echelonr' then + wp.action = 'EchelonR' + else + wp.action = 'Cone' -- if nothing matched + end + end + + wp.type = 'Turning Point' + + return wp + +end + +routines.fixedWing.buildWP = function(point, WPtype, speed, alt, altType) + + local wp = {} + wp.x = point.x + + if point.z then + wp.y = point.z + else + wp.y = point.y + end + + if alt and type(alt) == 'number' then + wp.alt = alt + else + wp.alt = 2000 + end + + if altType then + altType = string.lower(altType) + if altType == 'radio' or 'agl' then + wp.alt_type = 'RADIO' + elseif altType == 'baro' or 'asl' then + wp.alt_type = 'BARO' + end + else + wp.alt_type = 'RADIO' + end + + if point.speed then + speed = point.speed + end + + if point.type then + WPtype = point.type + end + + if not speed then + wp.speed = routines.utils.kmphToMps(500) + else + wp.speed = speed + end + + if not WPtype then + wp.action = 'Turning Point' + else + WPtype = string.lower(WPtype) + if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then + wp.action = 'Fly Over Point' + elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then + wp.action = 'Turning Point' + else + wp.action = 'Turning Point' + end + end + + wp.type = 'Turning Point' + return wp +end + +routines.heli.buildWP = function(point, WPtype, speed, alt, altType) + + local wp = {} + wp.x = point.x + + if point.z then + wp.y = point.z + else + wp.y = point.y + end + + if alt and type(alt) == 'number' then + wp.alt = alt + else + wp.alt = 500 + end + + if altType then + altType = string.lower(altType) + if altType == 'radio' or 'agl' then + wp.alt_type = 'RADIO' + elseif altType == 'baro' or 'asl' then + wp.alt_type = 'BARO' + end + else + wp.alt_type = 'RADIO' + end + + if point.speed then + speed = point.speed + end + + if point.type then + WPtype = point.type + end + + if not speed then + wp.speed = routines.utils.kmphToMps(200) + else + wp.speed = speed + end + + if not WPtype then + wp.action = 'Turning Point' + else + WPtype = string.lower(WPtype) + if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then + wp.action = 'Fly Over Point' + elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then + wp.action = 'Turning Point' + else + wp.action = 'Turning Point' + end + end + + wp.type = 'Turning Point' + return wp +end + +routines.groupToRandomPoint = function(vars) + local group = vars.group --Required + local point = vars.point --required + local radius = vars.radius or 0 + local innerRadius = vars.innerRadius + local form = vars.form or 'Cone' + local heading = vars.heading or math.random()*2*math.pi + local headingDegrees = vars.headingDegrees + local speed = vars.speed or routines.utils.kmphToMps(20) + + + local useRoads + if not vars.disableRoads then + useRoads = true + else + useRoads = false + end + + local path = {} + + if headingDegrees then + heading = headingDegrees*math.pi/180 + end + + if heading >= 2*math.pi then + heading = heading - 2*math.pi + end + + local rndCoord = routines.getRandPointInCircle(point, radius, innerRadius) + + local offset = {} + local posStart = routines.getLeadPos(group) + + offset.x = routines.utils.round(math.sin(heading - (math.pi/2)) * 50 + rndCoord.x, 3) + offset.z = routines.utils.round(math.cos(heading + (math.pi/2)) * 50 + rndCoord.y, 3) + path[#path + 1] = routines.ground.buildWP(posStart, form, speed) + + + if useRoads == true and ((point.x - posStart.x)^2 + (point.z - posStart.z)^2)^0.5 > radius * 1.3 then + path[#path + 1] = routines.ground.buildWP({['x'] = posStart.x + 11, ['z'] = posStart.z + 11}, 'off_road', speed) + path[#path + 1] = routines.ground.buildWP(posStart, 'on_road', speed) + path[#path + 1] = routines.ground.buildWP(offset, 'on_road', speed) + else + path[#path + 1] = routines.ground.buildWP({['x'] = posStart.x + 25, ['z'] = posStart.z + 25}, form, speed) + end + + path[#path + 1] = routines.ground.buildWP(offset, form, speed) + path[#path + 1] = routines.ground.buildWP(rndCoord, form, speed) + + routines.goRoute(group, path) + + return +end + +routines.groupRandomDistSelf = function(gpData, dist, form, heading, speed) + local pos = routines.getLeadPos(gpData) + local fakeZone = {} + fakeZone.radius = dist or math.random(300, 1000) + fakeZone.point = {x = pos.x, y, pos.y, z = pos.z} + routines.groupToRandomZone(gpData, fakeZone, form, heading, speed) + + return +end + +routines.groupToRandomZone = function(gpData, zone, form, heading, speed) + if type(gpData) == 'string' then + gpData = Group.getByName(gpData) + end + + if type(zone) == 'string' then + zone = trigger.misc.getZone(zone) + elseif type(zone) == 'table' and not zone.radius then + zone = trigger.misc.getZone(zone[math.random(1, #zone)]) + end + + if speed then + speed = routines.utils.kmphToMps(speed) + end + + local vars = {} + vars.group = gpData + vars.radius = zone.radius + vars.form = form + vars.headingDegrees = heading + vars.speed = speed + vars.point = routines.utils.zoneToVec3(zone) + + routines.groupToRandomPoint(vars) + + return +end + +routines.isTerrainValid = function(coord, terrainTypes) -- vec2/3 and enum or table of acceptable terrain types + if coord.z then + coord.y = coord.z + end + local typeConverted = {} + + if type(terrainTypes) == 'string' then -- if its a string it does this check + for constId, constData in pairs(land.SurfaceType) do + if string.lower(constId) == string.lower(terrainTypes) or string.lower(constData) == string.lower(terrainTypes) then + table.insert(typeConverted, constId) + end + end + elseif type(terrainTypes) == 'table' then -- if its a table it does this check + for typeId, typeData in pairs(terrainTypes) do + for constId, constData in pairs(land.SurfaceType) do + if string.lower(constId) == string.lower(typeData) or string.lower(constData) == string.lower(typeId) then + table.insert(typeConverted, constId) + end + end + end + end + for validIndex, validData in pairs(typeConverted) do + if land.getSurfaceType(coord) == land.SurfaceType[validData] then + return true + end + end + return false +end + +routines.groupToPoint = function(gpData, point, form, heading, speed, useRoads) + if type(point) == 'string' then + point = trigger.misc.getZone(point) + end + if speed then + speed = routines.utils.kmphToMps(speed) + end + + local vars = {} + vars.group = gpData + vars.form = form + vars.headingDegrees = heading + vars.speed = speed + vars.disableRoads = useRoads + vars.point = routines.utils.zoneToVec3(point) + routines.groupToRandomPoint(vars) + + return +end + + +routines.getLeadPos = function(group) + if type(group) == 'string' then -- group name + group = Group.getByName(group) + end + + local units = group:getUnits() + + local leader = units[1] + if not leader then -- SHOULD be good, but if there is a bug, this code future-proofs it then. + local lowestInd = math.huge + for ind, unit in pairs(units) do + if ind < lowestInd then + lowestInd = ind + leader = unit + end + end + end + if leader and Unit.isExist(leader) then -- maybe a little too paranoid now... + return leader:getPosition().p + end +end + +--[[ vars for routines.getMGRSString: +vars.units - table of unit names (NOT unitNameTable- maybe this should change). +vars.acc - integer between 0 and 5, inclusive +]] +routines.getMGRSString = function(vars) + local units = vars.units + local acc = vars.acc or 5 + local avgPos = routines.getAvgPos(units) + if avgPos then + return routines.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(avgPos)), acc) + end +end + +--[[ vars for routines.getLLString +vars.units - table of unit names (NOT unitNameTable- maybe this should change). +vars.acc - integer, number of numbers after decimal place +vars.DMS - if true, output in degrees, minutes, seconds. Otherwise, output in degrees, minutes. + + +]] +routines.getLLString = function(vars) + local units = vars.units + local acc = vars.acc or 3 + local DMS = vars.DMS + local avgPos = routines.getAvgPos(units) + if avgPos then + local lat, lon = coord.LOtoLL(avgPos) + return routines.tostringLL(lat, lon, acc, DMS) + end +end + +--[[ +vars.zone - table of a zone name. +vars.ref - vec3 ref point, maybe overload for vec2 as well? +vars.alt - boolean, if used, includes altitude in string +vars.metric - boolean, gives distance in km instead of NM. +]] +routines.getBRStringZone = function(vars) + local zone = trigger.misc.getZone( vars.zone ) + local ref = routines.utils.makeVec3(vars.ref, 0) -- turn it into Vec3 if it is not already. + local alt = vars.alt + local metric = vars.metric + if zone then + local vec = {x = zone.point.x - ref.x, y = zone.point.y - ref.y, z = zone.point.z - ref.z} + local dir = routines.utils.getDir(vec, ref) + local dist = routines.utils.get2DDist(zone.point, ref) + if alt then + alt = zone.y + end + return routines.tostringBR(dir, dist, alt, metric) + else + env.info( 'routines.getBRStringZone: error: zone is nil' ) + end +end + +--[[ +vars.units- table of unit names (NOT unitNameTable- maybe this should change). +vars.ref - vec3 ref point, maybe overload for vec2 as well? +vars.alt - boolean, if used, includes altitude in string +vars.metric - boolean, gives distance in km instead of NM. +]] +routines.getBRString = function(vars) + local units = vars.units + local ref = routines.utils.makeVec3(vars.ref, 0) -- turn it into Vec3 if it is not already. + local alt = vars.alt + local metric = vars.metric + local avgPos = routines.getAvgPos(units) + if avgPos then + local vec = {x = avgPos.x - ref.x, y = avgPos.y - ref.y, z = avgPos.z - ref.z} + local dir = routines.utils.getDir(vec, ref) + local dist = routines.utils.get2DDist(avgPos, ref) + if alt then + alt = avgPos.y + end + return routines.tostringBR(dir, dist, alt, metric) + end +end + + +-- Returns the Vec3 coordinates of the average position of the concentration of units most in the heading direction. +--[[ vars for routines.getLeadingPos: +vars.units - table of unit names +vars.heading - direction +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees +]] +routines.getLeadingPos = function(vars) + local units = vars.units + local heading = vars.heading + local radius = vars.radius + if vars.headingDegrees then + heading = routines.utils.toRadian(vars.headingDegrees) + end + + local unitPosTbl = {} + for i = 1, #units do + local unit = Unit.getByName(units[i]) + if unit and unit:isExist() then + unitPosTbl[#unitPosTbl + 1] = unit:getPosition().p + end + end + if #unitPosTbl > 0 then -- one more more units found. + -- first, find the unit most in the heading direction + local maxPos = -math.huge + + local maxPosInd -- maxPos - the furthest in direction defined by heading; maxPosInd = + for i = 1, #unitPosTbl do + local rotatedVec2 = routines.vec.rotateVec2(routines.utils.makeVec2(unitPosTbl[i]), heading) + if (not maxPos) or maxPos < rotatedVec2.x then + maxPos = rotatedVec2.x + maxPosInd = i + end + end + + --now, get all the units around this unit... + local avgPos + if radius then + local maxUnitPos = unitPosTbl[maxPosInd] + local avgx, avgy, avgz, totNum = 0, 0, 0, 0 + for i = 1, #unitPosTbl do + if routines.utils.get2DDist(maxUnitPos, unitPosTbl[i]) <= radius then + avgx = avgx + unitPosTbl[i].x + avgy = avgy + unitPosTbl[i].y + avgz = avgz + unitPosTbl[i].z + totNum = totNum + 1 + end + end + avgPos = { x = avgx/totNum, y = avgy/totNum, z = avgz/totNum} + else + avgPos = unitPosTbl[maxPosInd] + end + + return avgPos + end +end + + +--[[ vars for routines.getLeadingMGRSString: +vars.units - table of unit names +vars.heading - direction +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees +vars.acc - number, 0 to 5. +]] +routines.getLeadingMGRSString = function(vars) + local pos = routines.getLeadingPos(vars) + if pos then + local acc = vars.acc or 5 + return routines.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(pos)), acc) + end +end + +--[[ vars for routines.getLeadingLLString: +vars.units - table of unit names +vars.heading - direction, number +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees +vars.acc - number of digits after decimal point (can be negative) +vars.DMS - boolean, true if you want DMS. +]] +routines.getLeadingLLString = function(vars) + local pos = routines.getLeadingPos(vars) + if pos then + local acc = vars.acc or 3 + local DMS = vars.DMS + local lat, lon = coord.LOtoLL(pos) + return routines.tostringLL(lat, lon, acc, DMS) + end +end + + + +--[[ vars for routines.getLeadingBRString: +vars.units - table of unit names +vars.heading - direction, number +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees +vars.metric - boolean, if true, use km instead of NM. +vars.alt - boolean, if true, include altitude. +vars.ref - vec3/vec2 reference point. +]] +routines.getLeadingBRString = function(vars) + local pos = routines.getLeadingPos(vars) + if pos then + local ref = vars.ref + local alt = vars.alt + local metric = vars.metric + + local vec = {x = pos.x - ref.x, y = pos.y - ref.y, z = pos.z - ref.z} + local dir = routines.utils.getDir(vec, ref) + local dist = routines.utils.get2DDist(pos, ref) + if alt then + alt = pos.y + end + return routines.tostringBR(dir, dist, alt, metric) + end +end + +--[[ vars for routines.message.add + vars.text = 'Hello World' + vars.displayTime = 20 + vars.msgFor = {coa = {'red'}, countries = {'Ukraine', 'Georgia'}, unitTypes = {'A-10C'}} + +]] + +--[[ vars for routines.msgMGRS +vars.units - table of unit names (NOT unitNameTable- maybe this should change). +vars.acc - integer between 0 and 5, inclusive +vars.text - text in the message +vars.displayTime - self explanatory +vars.msgFor - scope +]] +routines.msgMGRS = function(vars) + local units = vars.units + local acc = vars.acc + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = routines.getMGRSString{units = units, acc = acc} + local newText + if string.find(text, '%%s') then -- look for %s + newText = string.format(text, s) -- insert the coordinates into the message + else -- else, just append to the end. + newText = text .. s + end + + routines.message.add{ + text = newText, + displayTime = displayTime, + msgFor = msgFor + } +end + +--[[ vars for routines.msgLL +vars.units - table of unit names (NOT unitNameTable- maybe this should change) (Yes). +vars.acc - integer, number of numbers after decimal place +vars.DMS - if true, output in degrees, minutes, seconds. Otherwise, output in degrees, minutes. +vars.text - text in the message +vars.displayTime - self explanatory +vars.msgFor - scope +]] +routines.msgLL = function(vars) + local units = vars.units -- technically, I don't really need to do this, but it helps readability. + local acc = vars.acc + local DMS = vars.DMS + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = routines.getLLString{units = units, acc = acc, DMS = DMS} + local newText + if string.find(text, '%%s') then -- look for %s + newText = string.format(text, s) -- insert the coordinates into the message + else -- else, just append to the end. + newText = text .. s + end + + routines.message.add{ + text = newText, + displayTime = displayTime, + msgFor = msgFor + } + +end + + +--[[ +vars.units- table of unit names (NOT unitNameTable- maybe this should change). +vars.ref - vec3 ref point, maybe overload for vec2 as well? +vars.alt - boolean, if used, includes altitude in string +vars.metric - boolean, gives distance in km instead of NM. +vars.text - text of the message +vars.displayTime +vars.msgFor - scope +]] +routines.msgBR = function(vars) + local units = vars.units -- technically, I don't really need to do this, but it helps readability. + local ref = vars.ref -- vec2/vec3 will be handled in routines.getBRString + local alt = vars.alt + local metric = vars.metric + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = routines.getBRString{units = units, ref = ref, alt = alt, metric = metric} + local newText + if string.find(text, '%%s') then -- look for %s + newText = string.format(text, s) -- insert the coordinates into the message + else -- else, just append to the end. + newText = text .. s + end + + routines.message.add{ + text = newText, + displayTime = displayTime, + msgFor = msgFor + } + +end + + +-------------------------------------------------------------------------------------------- +-- basically, just sub-types of routines.msgBR... saves folks the work of getting the ref point. +--[[ +vars.units- table of unit names (NOT unitNameTable- maybe this should change). +vars.ref - string red, blue +vars.alt - boolean, if used, includes altitude in string +vars.metric - boolean, gives distance in km instead of NM. +vars.text - text of the message +vars.displayTime +vars.msgFor - scope +]] +routines.msgBullseye = function(vars) + if string.lower(vars.ref) == 'red' then + vars.ref = routines.DBs.missionData.bullseye.red + routines.msgBR(vars) + elseif string.lower(vars.ref) == 'blue' then + vars.ref = routines.DBs.missionData.bullseye.blue + routines.msgBR(vars) + end +end + +--[[ +vars.units- table of unit names (NOT unitNameTable- maybe this should change). +vars.ref - unit name of reference point +vars.alt - boolean, if used, includes altitude in string +vars.metric - boolean, gives distance in km instead of NM. +vars.text - text of the message +vars.displayTime +vars.msgFor - scope +]] + +routines.msgBRA = function(vars) + if Unit.getByName(vars.ref) then + vars.ref = Unit.getByName(vars.ref):getPosition().p + if not vars.alt then + vars.alt = true + end + routines.msgBR(vars) + end +end +-------------------------------------------------------------------------------------------- + +--[[ vars for routines.msgLeadingMGRS: +vars.units - table of unit names +vars.heading - direction +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees (optional) +vars.acc - number, 0 to 5. +vars.text - text of the message +vars.displayTime +vars.msgFor - scope +]] +routines.msgLeadingMGRS = function(vars) + local units = vars.units -- technically, I don't really need to do this, but it helps readability. + local heading = vars.heading + local radius = vars.radius + local headingDegrees = vars.headingDegrees + local acc = vars.acc + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = routines.getLeadingMGRSString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc} + local newText + if string.find(text, '%%s') then -- look for %s + newText = string.format(text, s) -- insert the coordinates into the message + else -- else, just append to the end. + newText = text .. s + end + + routines.message.add{ + text = newText, + displayTime = displayTime, + msgFor = msgFor + } + + +end +--[[ vars for routines.msgLeadingLL: +vars.units - table of unit names +vars.heading - direction, number +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees (optional) +vars.acc - number of digits after decimal point (can be negative) +vars.DMS - boolean, true if you want DMS. (optional) +vars.text - text of the message +vars.displayTime +vars.msgFor - scope +]] +routines.msgLeadingLL = function(vars) + local units = vars.units -- technically, I don't really need to do this, but it helps readability. + local heading = vars.heading + local radius = vars.radius + local headingDegrees = vars.headingDegrees + local acc = vars.acc + local DMS = vars.DMS + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = routines.getLeadingLLString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc, DMS = DMS} + local newText + if string.find(text, '%%s') then -- look for %s + newText = string.format(text, s) -- insert the coordinates into the message + else -- else, just append to the end. + newText = text .. s + end + + routines.message.add{ + text = newText, + displayTime = displayTime, + msgFor = msgFor + } + +end + +--[[ +vars.units - table of unit names +vars.heading - direction, number +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees (optional) +vars.metric - boolean, if true, use km instead of NM. (optional) +vars.alt - boolean, if true, include altitude. (optional) +vars.ref - vec3/vec2 reference point. +vars.text - text of the message +vars.displayTime +vars.msgFor - scope +]] +routines.msgLeadingBR = function(vars) + local units = vars.units -- technically, I don't really need to do this, but it helps readability. + local heading = vars.heading + local radius = vars.radius + local headingDegrees = vars.headingDegrees + local metric = vars.metric + local alt = vars.alt + local ref = vars.ref -- vec2/vec3 will be handled in routines.getBRString + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = routines.getLeadingBRString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, metric = metric, alt = alt, ref = ref} + local newText + if string.find(text, '%%s') then -- look for %s + newText = string.format(text, s) -- insert the coordinates into the message + else -- else, just append to the end. + newText = text .. s + end + + routines.message.add{ + text = newText, + displayTime = displayTime, + msgFor = msgFor + } +end + + +function spairs(t, order) + -- collect the keys + local keys = {} + for k in pairs(t) do keys[#keys+1] = k end + + -- if order function given, sort by it by passing the table and keys a, b, + -- otherwise just sort the keys + if order then + table.sort(keys, function(a,b) return order(t, a, b) end) + else + table.sort(keys) + end + + -- return the iterator function + local i = 0 + return function() + i = i + 1 + if keys[i] then + return keys[i], t[keys[i]] + end + end +end + + +function routines.IsPartOfGroupInZones( CargoGroup, LandingZones ) +--trace.f() + + local CurrentZoneID = nil + + if CargoGroup then + local CargoUnits = CargoGroup:getUnits() + for CargoUnitID, CargoUnit in pairs( CargoUnits ) do + if CargoUnit and CargoUnit:getLife() >= 1.0 then + CurrentZoneID = routines.IsUnitInZones( CargoUnit, LandingZones ) + if CurrentZoneID then + break + end + end + end + end + +--trace.r( "", "", { CurrentZoneID } ) + return CurrentZoneID +end + + + +function routines.IsUnitInZones( TransportUnit, LandingZones ) +--trace.f("", "routines.IsUnitInZones" ) + + local TransportZoneResult = nil + local TransportZonePos = nil + local TransportZone = nil + + -- fill-up some local variables to support further calculations to determine location of units within the zone. + if TransportUnit then + local TransportUnitPos = TransportUnit:getPosition().p + if type( LandingZones ) == "table" then + for LandingZoneID, LandingZoneName in pairs( LandingZones ) do + TransportZone = trigger.misc.getZone( LandingZoneName ) + if TransportZone then + TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} + if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then + TransportZoneResult = LandingZoneID + break + end + end + end + else + TransportZone = trigger.misc.getZone( LandingZones ) + TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} + if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then + TransportZoneResult = 1 + end + end + if TransportZoneResult then + --trace.i( "routines", "TransportZone:" .. TransportZoneResult ) + else + --trace.i( "routines", "TransportZone:nil logic" ) + end + return TransportZoneResult + else + --trace.i( "routines", "TransportZone:nil hard" ) + return nil + end +end + +function routines.IsUnitNearZonesRadius( TransportUnit, LandingZones, ZoneRadius ) +--trace.f("", "routines.IsUnitInZones" ) + + local TransportZoneResult = nil + local TransportZonePos = nil + local TransportZone = nil + + -- fill-up some local variables to support further calculations to determine location of units within the zone. + if TransportUnit then + local TransportUnitPos = TransportUnit:getPosition().p + if type( LandingZones ) == "table" then + for LandingZoneID, LandingZoneName in pairs( LandingZones ) do + TransportZone = trigger.misc.getZone( LandingZoneName ) + if TransportZone then + TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} + if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= ZoneRadius ) then + TransportZoneResult = LandingZoneID + break + end + end + end + else + TransportZone = trigger.misc.getZone( LandingZones ) + TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} + if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= ZoneRadius ) then + TransportZoneResult = 1 + end + end + if TransportZoneResult then + --trace.i( "routines", "TransportZone:" .. TransportZoneResult ) + else + --trace.i( "routines", "TransportZone:nil logic" ) + end + return TransportZoneResult + else + --trace.i( "routines", "TransportZone:nil hard" ) + return nil + end +end + + +function routines.IsStaticInZones( TransportStatic, LandingZones ) +--trace.f() + + local TransportZoneResult = nil + local TransportZonePos = nil + local TransportZone = nil + + -- fill-up some local variables to support further calculations to determine location of units within the zone. + local TransportStaticPos = TransportStatic:getPosition().p + if type( LandingZones ) == "table" then + for LandingZoneID, LandingZoneName in pairs( LandingZones ) do + TransportZone = trigger.misc.getZone( LandingZoneName ) + if TransportZone then + TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} + if ((( TransportStaticPos.x - TransportZonePos.x)^2 + (TransportStaticPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then + TransportZoneResult = LandingZoneID + break + end + end + end + else + TransportZone = trigger.misc.getZone( LandingZones ) + TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} + if ((( TransportStaticPos.x - TransportZonePos.x)^2 + (TransportStaticPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then + TransportZoneResult = 1 + end + end + +--trace.r( "", "", { TransportZoneResult } ) + return TransportZoneResult +end + + +function routines.IsUnitInRadius( CargoUnit, ReferencePosition, Radius ) +--trace.f() + + local Valid = true + + -- fill-up some local variables to support further calculations to determine location of units within the zone. + local CargoPos = CargoUnit:getPosition().p + local ReferenceP = ReferencePosition.p + + if (((CargoPos.x - ReferenceP.x)^2 + (CargoPos.z - ReferenceP.z)^2)^0.5 <= Radius) then + else + Valid = false + end + + return Valid +end + +function routines.IsPartOfGroupInRadius( CargoGroup, ReferencePosition, Radius ) +--trace.f() + + local Valid = true + + Valid = routines.ValidateGroup( CargoGroup, "CargoGroup", Valid ) + + -- fill-up some local variables to support further calculations to determine location of units within the zone + local CargoUnits = CargoGroup:getUnits() + for CargoUnitId, CargoUnit in pairs( CargoUnits ) do + local CargoUnitPos = CargoUnit:getPosition().p +-- env.info( 'routines.IsPartOfGroupInRadius: CargoUnitPos.x = ' .. CargoUnitPos.x .. ' CargoUnitPos.z = ' .. CargoUnitPos.z ) + local ReferenceP = ReferencePosition.p +-- env.info( 'routines.IsPartOfGroupInRadius: ReferenceGroupPos.x = ' .. ReferenceGroupPos.x .. ' ReferenceGroupPos.z = ' .. ReferenceGroupPos.z ) + + if ((( CargoUnitPos.x - ReferenceP.x)^2 + (CargoUnitPos.z - ReferenceP.z)^2)^0.5 <= Radius) then + else + Valid = false + break + end + end + + return Valid +end + + +function routines.ValidateString( Variable, VariableName, Valid ) +--trace.f() + + if type( Variable ) == "string" then + if Variable == "" then + error( "routines.ValidateString: error: " .. VariableName .. " must be filled out!" ) + Valid = false + end + else + error( "routines.ValidateString: error: " .. VariableName .. " is not a string." ) + Valid = false + end + +--trace.r( "", "", { Valid } ) + return Valid +end + +function routines.ValidateNumber( Variable, VariableName, Valid ) +--trace.f() + + if type( Variable ) == "number" then + else + error( "routines.ValidateNumber: error: " .. VariableName .. " is not a number." ) + Valid = false + end + +--trace.r( "", "", { Valid } ) + return Valid + +end + +function routines.ValidateGroup( Variable, VariableName, Valid ) +--trace.f() + + if Variable == nil then + error( "routines.ValidateGroup: error: " .. VariableName .. " is a nil value!" ) + Valid = false + end + +--trace.r( "", "", { Valid } ) + return Valid +end + +function routines.ValidateZone( LandingZones, VariableName, Valid ) +--trace.f() + + if LandingZones == nil then + error( "routines.ValidateGroup: error: " .. VariableName .. " is a nil value!" ) + Valid = false + end + + if type( LandingZones ) == "table" then + for LandingZoneID, LandingZoneName in pairs( LandingZones ) do + if trigger.misc.getZone( LandingZoneName ) == nil then + error( "routines.ValidateGroup: error: Zone " .. LandingZoneName .. " does not exist!" ) + Valid = false + break + end + end + else + if trigger.misc.getZone( LandingZones ) == nil then + error( "routines.ValidateGroup: error: Zone " .. LandingZones .. " does not exist!" ) + Valid = false + end + end + +--trace.r( "", "", { Valid } ) + return Valid +end + +function routines.ValidateEnumeration( Variable, VariableName, Enum, Valid ) +--trace.f() + + local ValidVariable = false + + for EnumId, EnumData in pairs( Enum ) do + if Variable == EnumData then + ValidVariable = true + break + end + end + + if ValidVariable then + else + error( 'TransportValidateEnum: " .. VariableName .. " is not a valid type.' .. Variable ) + Valid = false + end + +--trace.r( "", "", { Valid } ) + return Valid +end + +function routines.getGroupRoute(groupIdent, task) -- same as getGroupPoints but returns speed and formation type along with vec2 of point} + -- refactor to search by groupId and allow groupId and groupName as inputs + local gpId = groupIdent + if type(groupIdent) == 'string' and not tonumber(groupIdent) then + gpId = _DATABASE.Templates.Groups[groupIdent].groupId + end + + for coa_name, coa_data in pairs(env.mission.coalition) do + if (coa_name == 'red' or coa_name == 'blue') and type(coa_data) == 'table' then + if coa_data.country then --there is a country table + for cntry_id, cntry_data in pairs(coa_data.country) do + for obj_type_name, obj_type_data in pairs(cntry_data) do + if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" then -- only these types have points + if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! + for group_num, group_data in pairs(obj_type_data.group) do + if group_data and group_data.groupId == gpId then -- this is the group we are looking for + if group_data.route and group_data.route.points and #group_data.route.points > 0 then + local points = {} + + for point_num, point in pairs(group_data.route.points) do + local routeData = {} + if not point.point then + routeData.x = point.x + routeData.y = point.y + else + routeData.point = point.point --it's possible that the ME could move to the point = Vec2 notation. + end + routeData.form = point.action + routeData.speed = point.speed + routeData.alt = point.alt + routeData.alt_type = point.alt_type + routeData.airdromeId = point.airdromeId + routeData.helipadId = point.helipadId + routeData.type = point.type + routeData.action = point.action + if task then + routeData.task = point.task + end + points[point_num] = routeData + end + + return points + end + return + end --if group_data and group_data.name and group_data.name == 'groupname' + end --for group_num, group_data in pairs(obj_type_data.group) do + end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then + end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then + end --for obj_type_name, obj_type_data in pairs(cntry_data) do + end --for cntry_id, cntry_data in pairs(coa_data.country) do + end --if coa_data.country then --there is a country table + end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then + end --for coa_name, coa_data in pairs(mission.coalition) do +end + +routines.ground.patrolRoute = function(vars) + + + local tempRoute = {} + local useRoute = {} + local gpData = vars.gpData + if type(gpData) == 'string' then + gpData = Group.getByName(gpData) + end + + local useGroupRoute + if not vars.useGroupRoute then + useGroupRoute = vars.gpData + else + useGroupRoute = vars.useGroupRoute + end + local routeProvided = false + if not vars.route then + if useGroupRoute then + tempRoute = routines.getGroupRoute(useGroupRoute) + end + else + useRoute = vars.route + local posStart = routines.getLeadPos(gpData) + useRoute[1] = routines.ground.buildWP(posStart, useRoute[1].action, useRoute[1].speed) + routeProvided = true + end + + + local overRideSpeed = vars.speed or 'default' + local pType = vars.pType + local offRoadForm = vars.offRoadForm or 'default' + local onRoadForm = vars.onRoadForm or 'default' + + if routeProvided == false and #tempRoute > 0 then + local posStart = routines.getLeadPos(gpData) + + + useRoute[#useRoute + 1] = routines.ground.buildWP(posStart, offRoadForm, overRideSpeed) + for i = 1, #tempRoute do + local tempForm = tempRoute[i].action + local tempSpeed = tempRoute[i].speed + + if offRoadForm == 'default' then + tempForm = tempRoute[i].action + end + if onRoadForm == 'default' then + onRoadForm = 'On Road' + end + if (string.lower(tempRoute[i].action) == 'on road' or string.lower(tempRoute[i].action) == 'onroad' or string.lower(tempRoute[i].action) == 'on_road') then + tempForm = onRoadForm + else + tempForm = offRoadForm + end + + if type(overRideSpeed) == 'number' then + tempSpeed = overRideSpeed + end + + + useRoute[#useRoute + 1] = routines.ground.buildWP(tempRoute[i], tempForm, tempSpeed) + end + + if pType and string.lower(pType) == 'doubleback' then + local curRoute = routines.utils.deepCopy(useRoute) + for i = #curRoute, 2, -1 do + useRoute[#useRoute + 1] = routines.ground.buildWP(curRoute[i], curRoute[i].action, curRoute[i].speed) + end + end + + useRoute[1].action = useRoute[#useRoute].action -- make it so the first WP matches the last WP + end + + local cTask3 = {} + local newPatrol = {} + newPatrol.route = useRoute + newPatrol.gpData = gpData:getName() + cTask3[#cTask3 + 1] = 'routines.ground.patrolRoute(' + cTask3[#cTask3 + 1] = routines.utils.oneLineSerialize(newPatrol) + cTask3[#cTask3 + 1] = ')' + cTask3 = table.concat(cTask3) + local tempTask = { + id = 'WrappedAction', + params = { + action = { + id = 'Script', + params = { + command = cTask3, + + }, + }, + }, + } + + + useRoute[#useRoute].task = tempTask + routines.goRoute(gpData, useRoute) + + return +end + +routines.ground.patrol = function(gpData, pType, form, speed) + local vars = {} + + if type(gpData) == 'table' and gpData:getName() then + gpData = gpData:getName() + end + + vars.useGroupRoute = gpData + vars.gpData = gpData + vars.pType = pType + vars.offRoadForm = form + vars.speed = speed + + routines.ground.patrolRoute(vars) + + return +end + +function routines.GetUnitHeight( CheckUnit ) +--trace.f( "routines" ) + + local UnitPoint = CheckUnit:getPoint() + local UnitPosition = { x = UnitPoint.x, y = UnitPoint.z } + local UnitHeight = UnitPoint.y + + local LandHeight = land.getHeight( UnitPosition ) + + --env.info(( 'CarrierHeight: LandHeight = ' .. LandHeight .. ' CarrierHeight = ' .. CarrierHeight )) + + --trace.f( "routines", "Unit Height = " .. UnitHeight - LandHeight ) + + return UnitHeight - LandHeight + +end + + + +Su34Status = { status = {} } +boardMsgRed = { statusMsg = "" } +boardMsgAll = { timeMsg = "" } +SpawnSettings = {} +Su34MenuPath = {} +Su34Menus = 0 + + +function Su34AttackCarlVinson(groupName) +--trace.menu("", "Su34AttackCarlVinson") + local groupSu34 = Group.getByName( groupName ) + local controllerSu34 = groupSu34.getController(groupSu34) + local groupCarlVinson = Group.getByName("US Carl Vinson #001") + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE ) + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) + if groupCarlVinson ~= nil then + controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupCarlVinson:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = true}}) + end + Su34Status.status[groupName] = 1 + MessageToRed( string.format('%s: ',groupName) .. 'Attacking carrier Carl Vinson. ', 10, 'RedStatus' .. groupName ) +end + +function Su34AttackWest(groupName) +--trace.f("","Su34AttackWest") + local groupSu34 = Group.getByName( groupName ) + local controllerSu34 = groupSu34.getController(groupSu34) + local groupShipWest1 = Group.getByName("US Ship West #001") + local groupShipWest2 = Group.getByName("US Ship West #002") + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE ) + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) + if groupShipWest1 ~= nil then + controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipWest1:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = true}}) + end + if groupShipWest2 ~= nil then + controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipWest2:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = true}}) + end + Su34Status.status[groupName] = 2 + MessageToRed( string.format('%s: ',groupName) .. 'Attacking invading ships in the west. ', 10, 'RedStatus' .. groupName ) +end + +function Su34AttackNorth(groupName) +--trace.menu("","Su34AttackNorth") + local groupSu34 = Group.getByName( groupName ) + local controllerSu34 = groupSu34.getController(groupSu34) + local groupShipNorth1 = Group.getByName("US Ship North #001") + local groupShipNorth2 = Group.getByName("US Ship North #002") + local groupShipNorth3 = Group.getByName("US Ship North #003") + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE ) + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) + if groupShipNorth1 ~= nil then + controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipNorth1:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = false}}) + end + if groupShipNorth2 ~= nil then + controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipNorth2:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = false}}) + end + if groupShipNorth3 ~= nil then + controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipNorth3:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = false}}) + end + Su34Status.status[groupName] = 3 + MessageToRed( string.format('%s: ',groupName) .. 'Attacking invading ships in the north. ', 10, 'RedStatus' .. groupName ) +end + +function Su34Orbit(groupName) +--trace.menu("","Su34Orbit") + local groupSu34 = Group.getByName( groupName ) + local controllerSu34 = groupSu34:getController() + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD ) + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) + controllerSu34:pushTask( {id = 'ControlledTask', params = { task = { id = 'Orbit', params = { pattern = AI.Task.OrbitPattern.RACE_TRACK } }, stopCondition = { duration = 600 } } } ) + Su34Status.status[groupName] = 4 + MessageToRed( string.format('%s: ',groupName) .. 'In orbit and awaiting further instructions. ', 10, 'RedStatus' .. groupName ) +end + +function Su34TakeOff(groupName) +--trace.menu("","Su34TakeOff") + local groupSu34 = Group.getByName( groupName ) + local controllerSu34 = groupSu34:getController() + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD ) + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.BYPASS_AND_ESCAPE ) + Su34Status.status[groupName] = 8 + MessageToRed( string.format('%s: ',groupName) .. 'Take-Off. ', 10, 'RedStatus' .. groupName ) +end + +function Su34Hold(groupName) +--trace.menu("","Su34Hold") + local groupSu34 = Group.getByName( groupName ) + local controllerSu34 = groupSu34:getController() + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD ) + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.BYPASS_AND_ESCAPE ) + Su34Status.status[groupName] = 5 + MessageToRed( string.format('%s: ',groupName) .. 'Holding Weapons. ', 10, 'RedStatus' .. groupName ) +end + +function Su34RTB(groupName) +--trace.menu("","Su34RTB") + Su34Status.status[groupName] = 6 + MessageToRed( string.format('%s: ',groupName) .. 'Return to Krasnodar. ', 10, 'RedStatus' .. groupName ) +end + +function Su34Destroyed(groupName) +--trace.menu("","Su34Destroyed") + Su34Status.status[groupName] = 7 + MessageToRed( string.format('%s: ',groupName) .. 'Destroyed. ', 30, 'RedStatus' .. groupName ) +end + +function GroupAlive( groupName ) +--trace.menu("","GroupAlive") + local groupTest = Group.getByName( groupName ) + + local groupExists = false + + if groupTest then + groupExists = groupTest:isExist() + end + + --trace.r( "", "", { groupExists } ) + return groupExists +end + +function Su34IsDead() +--trace.f() + +end + +function Su34OverviewStatus() +--trace.menu("","Su34OverviewStatus") + local msg = "" + local currentStatus = 0 + local Exists = false + + for groupName, currentStatus in pairs(Su34Status.status) do + + env.info(('Su34 Overview Status: GroupName = ' .. groupName )) + Alive = GroupAlive( groupName ) + + if Alive then + if currentStatus == 1 then + msg = msg .. string.format("%s: ",groupName) + msg = msg .. "Attacking carrier Carl Vinson. " + elseif currentStatus == 2 then + msg = msg .. string.format("%s: ",groupName) + msg = msg .. "Attacking supporting ships in the west. " + elseif currentStatus == 3 then + msg = msg .. string.format("%s: ",groupName) + msg = msg .. "Attacking invading ships in the north. " + elseif currentStatus == 4 then + msg = msg .. string.format("%s: ",groupName) + msg = msg .. "In orbit and awaiting further instructions. " + elseif currentStatus == 5 then + msg = msg .. string.format("%s: ",groupName) + msg = msg .. "Holding Weapons. " + elseif currentStatus == 6 then + msg = msg .. string.format("%s: ",groupName) + msg = msg .. "Return to Krasnodar. " + elseif currentStatus == 7 then + msg = msg .. string.format("%s: ",groupName) + msg = msg .. "Destroyed. " + elseif currentStatus == 8 then + msg = msg .. string.format("%s: ",groupName) + msg = msg .. "Take-Off. " + end + else + if currentStatus == 7 then + msg = msg .. string.format("%s: ",groupName) + msg = msg .. "Destroyed. " + else + Su34Destroyed(groupName) + end + end + end + + boardMsgRed.statusMsg = msg +end + + +function UpdateBoardMsg() +--trace.f() + Su34OverviewStatus() + MessageToRed( boardMsgRed.statusMsg, 15, 'RedStatus' ) +end + +function MusicReset( flg ) +--trace.f() + trigger.action.setUserFlag(95,flg) +end + +function PlaneActivate(groupNameFormat, flg) +--trace.f() + local groupName = groupNameFormat .. string.format("#%03d", trigger.misc.getUserFlag(flg)) + --trigger.action.outText(groupName,10) + trigger.action.activateGroup(Group.getByName(groupName)) +end + +function Su34Menu(groupName) +--trace.f() + + --env.info(( 'Su34Menu(' .. groupName .. ')' )) + local groupSu34 = Group.getByName( groupName ) + + if Su34Status.status[groupName] == 1 or + Su34Status.status[groupName] == 2 or + Su34Status.status[groupName] == 3 or + Su34Status.status[groupName] == 4 or + Su34Status.status[groupName] == 5 then + if Su34MenuPath[groupName] == nil then + if planeMenuPath == nil then + planeMenuPath = missionCommands.addSubMenuForCoalition( + coalition.side.RED, + "SU-34 anti-ship flights", + nil + ) + end + Su34MenuPath[groupName] = missionCommands.addSubMenuForCoalition( + coalition.side.RED, + "Flight " .. groupName, + planeMenuPath + ) + + missionCommands.addCommandForCoalition( + coalition.side.RED, + "Attack carrier Carl Vinson", + Su34MenuPath[groupName], + Su34AttackCarlVinson, + groupName + ) + + missionCommands.addCommandForCoalition( + coalition.side.RED, + "Attack ships in the west", + Su34MenuPath[groupName], + Su34AttackWest, + groupName + ) + + missionCommands.addCommandForCoalition( + coalition.side.RED, + "Attack ships in the north", + Su34MenuPath[groupName], + Su34AttackNorth, + groupName + ) + + missionCommands.addCommandForCoalition( + coalition.side.RED, + "Hold position and await instructions", + Su34MenuPath[groupName], + Su34Orbit, + groupName + ) + + missionCommands.addCommandForCoalition( + coalition.side.RED, + "Report status", + Su34MenuPath[groupName], + Su34OverviewStatus + ) + end + else + if Su34MenuPath[groupName] then + missionCommands.removeItemForCoalition(coalition.side.RED, Su34MenuPath[groupName]) + end + end +end + +--- Obsolete function, but kept to rework in framework. + +function ChooseInfantry ( TeleportPrefixTable, TeleportMax ) +--trace.f("Spawn") + --env.info(( 'ChooseInfantry: ' )) + + TeleportPrefixTableCount = #TeleportPrefixTable + TeleportPrefixTableIndex = math.random( 1, TeleportPrefixTableCount ) + + --env.info(( 'ChooseInfantry: TeleportPrefixTableIndex = ' .. TeleportPrefixTableIndex .. ' TeleportPrefixTableCount = ' .. TeleportPrefixTableCount .. ' TeleportMax = ' .. TeleportMax )) + + local TeleportFound = false + local TeleportLoop = true + local Index = TeleportPrefixTableIndex + local TeleportPrefix = '' + + while TeleportLoop do + TeleportPrefix = TeleportPrefixTable[Index] + if SpawnSettings[TeleportPrefix] then + if SpawnSettings[TeleportPrefix]['SpawnCount'] - 1 < TeleportMax then + SpawnSettings[TeleportPrefix]['SpawnCount'] = SpawnSettings[TeleportPrefix]['SpawnCount'] + 1 + TeleportFound = true + else + TeleportFound = false + end + else + SpawnSettings[TeleportPrefix] = {} + SpawnSettings[TeleportPrefix]['SpawnCount'] = 0 + TeleportFound = true + end + if TeleportFound then + TeleportLoop = false + else + if Index < TeleportPrefixTableCount then + Index = Index + 1 + else + TeleportLoop = false + end + end + --env.info(( 'ChooseInfantry: Loop 1 - TeleportPrefix = ' .. TeleportPrefix .. ' Index = ' .. Index )) + end + + if TeleportFound == false then + TeleportLoop = true + Index = 1 + while TeleportLoop do + TeleportPrefix = TeleportPrefixTable[Index] + if SpawnSettings[TeleportPrefix] then + if SpawnSettings[TeleportPrefix]['SpawnCount'] - 1 < TeleportMax then + SpawnSettings[TeleportPrefix]['SpawnCount'] = SpawnSettings[TeleportPrefix]['SpawnCount'] + 1 + TeleportFound = true + else + TeleportFound = false + end + else + SpawnSettings[TeleportPrefix] = {} + SpawnSettings[TeleportPrefix]['SpawnCount'] = 0 + TeleportFound = true + end + if TeleportFound then + TeleportLoop = false + else + if Index < TeleportPrefixTableIndex then + Index = Index + 1 + else + TeleportLoop = false + end + end + --env.info(( 'ChooseInfantry: Loop 2 - TeleportPrefix = ' .. TeleportPrefix .. ' Index = ' .. Index )) + end + end + + local TeleportGroupName = '' + if TeleportFound == true then + TeleportGroupName = TeleportPrefix .. string.format("#%03d", SpawnSettings[TeleportPrefix]['SpawnCount'] ) + else + TeleportGroupName = '' + end + + --env.info(('ChooseInfantry: TeleportGroupName = ' .. TeleportGroupName )) + --env.info(('ChooseInfantry: return')) + + return TeleportGroupName +end + +SpawnedInfantry = 0 + +function LandCarrier ( CarrierGroup, LandingZonePrefix ) +--trace.f() + --env.info(( 'LandCarrier: ' )) + --env.info(( 'LandCarrier: CarrierGroup = ' .. CarrierGroup:getName() )) + --env.info(( 'LandCarrier: LandingZone = ' .. LandingZonePrefix )) + + local controllerGroup = CarrierGroup:getController() + + local LandingZone = trigger.misc.getZone(LandingZonePrefix) + local LandingZonePos = {} + LandingZonePos.x = LandingZone.point.x + math.random(LandingZone.radius * -1, LandingZone.radius) + LandingZonePos.y = LandingZone.point.z + math.random(LandingZone.radius * -1, LandingZone.radius) + + controllerGroup:pushTask( { id = 'Land', params = { point = LandingZonePos, durationFlag = true, duration = 10 } } ) + + --env.info(( 'LandCarrier: end' )) +end + +EscortCount = 0 +function EscortCarrier ( CarrierGroup, EscortPrefix, EscortLastWayPoint, EscortEngagementDistanceMax, EscortTargetTypes ) +--trace.f() + --env.info(( 'EscortCarrier: ' )) + --env.info(( 'EscortCarrier: CarrierGroup = ' .. CarrierGroup:getName() )) + --env.info(( 'EscortCarrier: EscortPrefix = ' .. EscortPrefix )) + + local CarrierName = CarrierGroup:getName() + + local EscortMission = {} + local CarrierMission = {} + + local EscortMission = SpawnMissionGroup( EscortPrefix ) + local CarrierMission = SpawnMissionGroup( CarrierGroup:getName() ) + + if EscortMission ~= nil and CarrierMission ~= nil then + + EscortCount = EscortCount + 1 + EscortMissionName = string.format( EscortPrefix .. '#Escort %s', CarrierName ) + EscortMission.name = EscortMissionName + EscortMission.groupId = nil + EscortMission.lateActivation = false + EscortMission.taskSelected = false + + local EscortUnits = #EscortMission.units + for u = 1, EscortUnits do + EscortMission.units[u].name = string.format( EscortPrefix .. '#Escort %s %02d', CarrierName, u ) + EscortMission.units[u].unitId = nil + end + + + EscortMission.route.points[1].task = { id = "ComboTask", + params = + { + tasks = + { + [1] = + { + enabled = true, + auto = false, + id = "Escort", + number = 1, + params = + { + lastWptIndexFlagChangedManually = false, + groupId = CarrierGroup:getID(), + lastWptIndex = nil, + lastWptIndexFlag = false, + engagementDistMax = EscortEngagementDistanceMax, + targetTypes = EscortTargetTypes, + pos = + { + y = 20, + x = 20, + z = 0, + } -- end of ["pos"] + } -- end of ["params"] + } -- end of [1] + } -- end of ["tasks"] + } -- end of ["params"] + } -- end of ["task"] + + SpawnGroupAdd( EscortPrefix, EscortMission ) + + end +end + +function SendMessageToCarrier( CarrierGroup, CarrierMessage ) +--trace.f() + + if CarrierGroup ~= nil then + MessageToGroup( CarrierGroup, CarrierMessage, 30, 'Carrier/' .. CarrierGroup:getName() ) + end + +end + +function MessageToGroup( MsgGroup, MsgText, MsgTime, MsgName ) +--trace.f() + + if type(MsgGroup) == 'string' then + --env.info( 'MessageToGroup: Converted MsgGroup string "' .. MsgGroup .. '" into a Group structure.' ) + MsgGroup = Group.getByName( MsgGroup ) + end + + if MsgGroup ~= nil then + local MsgTable = {} + MsgTable.text = MsgText + MsgTable.displayTime = MsgTime + MsgTable.msgFor = { units = { MsgGroup:getUnits()[1]:getName() } } + MsgTable.name = MsgName + --routines.message.add( MsgTable ) + --env.info(('MessageToGroup: Message sent to ' .. MsgGroup:getUnits()[1]:getName() .. ' -> ' .. MsgText )) + end +end + +function MessageToUnit( UnitName, MsgText, MsgTime, MsgName ) +--trace.f() + + if UnitName ~= nil then + local MsgTable = {} + MsgTable.text = MsgText + MsgTable.displayTime = MsgTime + MsgTable.msgFor = { units = { UnitName } } + MsgTable.name = MsgName + --routines.message.add( MsgTable ) + end +end + +function MessageToAll( MsgText, MsgTime, MsgName ) +--trace.f() + + MESSAGE:New( MsgText, MsgTime, "Message" ):ToCoalition( coalition.side.RED ):ToCoalition( coalition.side.BLUE ) +end + +function MessageToRed( MsgText, MsgTime, MsgName ) +--trace.f() + + MESSAGE:New( MsgText, MsgTime, "To Red Coalition" ):ToCoalition( coalition.side.RED ) +end + +function MessageToBlue( MsgText, MsgTime, MsgName ) +--trace.f() + + MESSAGE:New( MsgText, MsgTime, "To Blue Coalition" ):ToCoalition( coalition.side.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 derived utilities taken from the MIST framework, +-- which are excellent tools to be reused in an OO environment!. +-- +-- ### Authors: +-- +-- * Grimes : Design & Programming of the MIST framework. +-- +-- ### Contributions: +-- +-- * FlightControl : Rework to OO framework +-- +-- @module Utils + + +--- @type SMOKECOLOR +-- @field Green +-- @field Red +-- @field White +-- @field Orange +-- @field Blue + +SMOKECOLOR = trigger.smokeColor -- #SMOKECOLOR + +--- @type FLARECOLOR +-- @field Green +-- @field Red +-- @field White +-- @field Yellow + +FLARECOLOR = trigger.flareColor -- #FLARECOLOR + +--- Utilities static class. +-- @type UTILS +UTILS = {} + + +--from http://lua-users.org/wiki/CopyTable +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 +UTILS.OneLineSerialize = function( tbl ) -- serialization of a table all on a single line, no comments, made to replace old get_table_string function + + lookup_table = {} + + local function _Serialize( tbl ) + + if type(tbl) == 'table' then --function only works for tables! + + if lookup_table[tbl] then + return lookup_table[object] + end + + local tbl_str = {} + + lookup_table[tbl] = tbl_str + + tbl_str[#tbl_str + 1] = '{' + + for ind,val in pairs(tbl) do -- serialize its fields + local ind_str = {} + if type(ind) == "number" then + ind_str[#ind_str + 1] = '[' + ind_str[#ind_str + 1] = tostring(ind) + ind_str[#ind_str + 1] = ']=' + else --must be a string + ind_str[#ind_str + 1] = '[' + ind_str[#ind_str + 1] = routines.utils.basicSerialize(ind) + ind_str[#ind_str + 1] = ']=' + end + + local val_str = {} + if ((type(val) == 'number') or (type(val) == 'boolean')) then + val_str[#val_str + 1] = tostring(val) + val_str[#val_str + 1] = ',' + tbl_str[#tbl_str + 1] = table.concat(ind_str) + tbl_str[#tbl_str + 1] = table.concat(val_str) + elseif type(val) == 'string' then + val_str[#val_str + 1] = routines.utils.basicSerialize(val) + val_str[#val_str + 1] = ',' + tbl_str[#tbl_str + 1] = table.concat(ind_str) + tbl_str[#tbl_str + 1] = table.concat(val_str) + elseif type(val) == 'nil' then -- won't ever happen, right? + val_str[#val_str + 1] = 'nil,' + tbl_str[#tbl_str + 1] = table.concat(ind_str) + tbl_str[#tbl_str + 1] = table.concat(val_str) + elseif type(val) == 'table' then + if ind == "__index" then + -- tbl_str[#tbl_str + 1] = "__index" + -- tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it + else + + val_str[#val_str + 1] = _Serialize(val) + val_str[#val_str + 1] = ',' --I think this is right, I just added it + tbl_str[#tbl_str + 1] = table.concat(ind_str) + tbl_str[#tbl_str + 1] = table.concat(val_str) + end + elseif type(val) == 'function' then + tbl_str[#tbl_str + 1] = "f() " .. tostring(ind) + tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it + else + env.info('unable to serialize value type ' .. routines.utils.basicSerialize(type(val)) .. ' at index ' .. tostring(ind)) + env.info( debug.traceback() ) + end + + end + tbl_str[#tbl_str + 1] = '}' + return table.concat(tbl_str) + else + return tostring(tbl) + end + end + + local objectreturn = _Serialize(tbl) + return objectreturn +end + +--porting in Slmod's "safestring" basic serialize +UTILS.BasicSerialize = function(s) + if s == nil then + return "\"\"" + else + if ((type(s) == 'number') or (type(s) == 'boolean') or (type(s) == 'function') or (type(s) == 'table') or (type(s) == 'userdata') ) then + return tostring(s) + elseif type(s) == 'string' then + s = string.format('%q', s) + return s + end + end +end + + +UTILS.ToDegree = function(angle) + return angle*180/math.pi +end + +UTILS.ToRadian = function(angle) + return angle*math.pi/180 +end + +UTILS.MetersToNM = function(meters) + return meters/1852 +end + +UTILS.MetersToFeet = function(meters) + return meters/0.3048 +end + +UTILS.NMToMeters = function(NM) + return NM*1852 +end + +UTILS.FeetToMeters = function(feet) + return feet*0.3048 +end + +UTILS.MpsToKnots = function(mps) + return mps*3600/1852 +end + +UTILS.MpsToKmph = function(mps) + return mps*3.6 +end + +UTILS.KnotsToMps = function(knots) + return knots*1852/3600 +end + +UTILS.KmphToMps = function(kmph) + return kmph/3.6 +end + +--[[acc: +in DM: decimal point of minutes. +In DMS: decimal point of seconds. +position after the decimal of the least significant digit: +So: +42.32 - acc of 2. +]] +UTILS.tostringLL = function( lat, lon, acc, DMS) + + local latHemi, lonHemi + if lat > 0 then + latHemi = 'N' + else + latHemi = 'S' + end + + if lon > 0 then + lonHemi = 'E' + else + lonHemi = 'W' + end + + lat = math.abs(lat) + lon = math.abs(lon) + + local latDeg = math.floor(lat) + local latMin = (lat - latDeg)*60 + + local lonDeg = math.floor(lon) + local lonMin = (lon - lonDeg)*60 + + if DMS then -- degrees, minutes, and seconds. + local oldLatMin = latMin + latMin = math.floor(latMin) + local latSec = UTILS.Round((oldLatMin - latMin)*60, acc) + + local oldLonMin = lonMin + lonMin = math.floor(lonMin) + local lonSec = UTILS.Round((oldLonMin - lonMin)*60, acc) + + if latSec == 60 then + latSec = 0 + latMin = latMin + 1 + end + + if lonSec == 60 then + lonSec = 0 + lonMin = lonMin + 1 + end + + local secFrmtStr -- create the formatting string for the seconds place + 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 = UTILS.Round(latMin, acc) + lonMin = UTILS.Round(lonMin, acc) + + if latMin == 60 then + latMin = 0 + latDeg = latDeg + 1 + end + + if lonMin == 60 then + lonMin = 0 + lonDeg = lonDeg + 1 + end + + local minFrmtStr -- create the formatting string for the minutes place + if acc <= 0 then -- no decimal place. + minFrmtStr = '%02d' + else + local width = 3 + acc -- 01.310 - that's a width of 6, for example. + minFrmtStr = '%0' .. width .. '.' .. acc .. 'f' + end + + return string.format('%02d', latDeg) .. ' ' .. string.format(minFrmtStr, latMin) .. '\'' .. latHemi .. ' ' + .. string.format('%02d', lonDeg) .. ' ' .. string.format(minFrmtStr, lonMin) .. '\'' .. lonHemi + + end +end + + +--- From http://lua-users.org/wiki/SimpleRound +-- use negative idp for rounding ahead of decimal place, positive for rounding after decimal place +function UTILS.Round( num, idp ) + local mult = 10 ^ ( idp or 0 ) + return math.floor( num * mult + 0.5 ) / mult +end + +-- porting in Slmod's dostring +function UTILS.DoString( s ) + local f, err = loadstring( s ) + if f then + return true, f() + else + return false, err + end +end +--- 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.2.3) Trace activation. +-- +-- Tracing can be activated in several ways: +-- +-- * Switch tracing on or off through the @{#BASE.TraceOnOff}() method. +-- * Activate all tracing through the @{#BASE.TraceAll}() method. +-- * Activate only the tracing of a certain class (name) through the @{#BASE.TraceClass}() method. +-- * Activate only the tracing of a certain method of a certain class through the @{#BASE.TraceClassMethod}() method. +-- * Activate only the tracing of a certain level through the @{#BASE.TraceLevel}() method. +-- ### 1.2.4) Check if tracing is on. +-- +-- The method @{#BASE.IsTrace}() will validate if tracing is activated or not. +-- +-- ## 1.3 DCS simulator Event Handling +-- +-- The BASE class provides methods to catch DCS Events. These are events that are triggered from within the DCS simulator, +-- and handled through lua scripting. MOOSE provides an encapsulation to handle these events more efficiently. +-- +-- ### 1.3.1 Subscribe / Unsubscribe to DCS Events +-- +-- At first, the mission designer will need to **Subscribe** to a specific DCS event for the class. +-- So, when the DCS event occurs, the class will be notified of that event. +-- There are two functions which you use to subscribe to or unsubscribe from an event. +-- +-- * @{#BASE.HandleEvent}(): Subscribe to a DCS Event. +-- * @{#BASE.UnHandleEvent}(): Unsubscribe from a DCS Event. +-- +-- ### 1.3.2 Event Handling of DCS Events +-- +-- Once the class is subscribed to the event, an **Event Handling** method on the object or class needs to be written that will be called +-- when the DCS event occurs. The Event Handling method receives an @{Event#EVENTDATA} structure, which contains a lot of information +-- about the event that occurred. +-- +-- Find below an example of the prototype how to write an event handling function for two units: +-- +-- local Tank1 = UNIT:FindByName( "Tank A" ) +-- local Tank2 = UNIT:FindByName( "Tank B" ) +-- +-- -- Here we subscribe to the Dead events. So, if one of these tanks dies, the Tank1 or Tank2 objects will be notified. +-- Tank1:HandleEvent( EVENTS.Dead ) +-- Tank2:HandleEvent( EVENTS.Dead ) +-- +-- --- This function is an Event Handling function that will be called when Tank1 is Dead. +-- -- @param Wrapper.Unit#UNIT self +-- -- @param Core.Event#EVENTDATA EventData +-- function Tank1:OnEventDead( EventData ) +-- +-- self:SmokeGreen() +-- end +-- +-- --- This function is an Event Handling function that will be called when Tank2 is Dead. +-- -- @param Wrapper.Unit#UNIT self +-- -- @param Core.Event#EVENTDATA EventData +-- function Tank2:OnEventDead( EventData ) +-- +-- self:SmokeBlue() +-- end +-- +-- +-- +-- See the @{Event} module for more information about event handling. +-- +-- ## 1.4) Class identification methods +-- +-- BASE provides methods to get more information of each object: +-- +-- * @{#BASE.GetClassID}(): Gets the ID (number) of the object. Each object created is assigned a number, that is incremented by one. +-- * @{#BASE.GetClassName}(): Gets the name of the object, which is the name of the class the object was instantiated from. +-- * @{#BASE.GetClassNameAndID}(): Gets the name and ID of the object. +-- +-- ## 1.5) All objects derived from BASE can have "States" +-- +-- A mechanism is in place in MOOSE, that allows to let the objects administer **states**. +-- States are essentially properties of objects, which are identified by a **Key** and a **Value**. +-- The method @{#BASE.SetState}() can be used to set a Value with a reference Key to the object. +-- To **read or retrieve** a state Value based on a Key, use the @{#BASE.GetState} method. +-- These two methods provide a very handy way to keep state at long lasting processes. +-- Values can be stored within the objects, and later retrieved or changed when needed. +-- There is one other important thing to note, the @{#BASE.SetState}() and @{#BASE.GetState} methods +-- receive as the **first parameter the object for which the state needs to be set**. +-- Thus, if the state is to be set for the same object as the object for which the method is used, then provide the same +-- object name to the method. +-- +-- ## 1.10) BASE Inheritance (tree) support +-- +-- The following methods are available to support inheritance: +-- +-- * @{#BASE.Inherit}: Inherits from a class. +-- * @{#BASE.GetParent}: Returns the parent object from the object it is handling, or nil if there is no parent object. +-- +-- ==== +-- +-- # **API CHANGE HISTORY** +-- +-- The underlying change log documents the API changes. Please read this carefully. The following notation is used: +-- +-- * **Added** parts are expressed in bold type face. +-- * _Removed_ parts are expressed in italic type face. +-- +-- YYYY-MM-DD: CLASS:**NewFunction**( Params ) replaces CLASS:_OldFunction_( Params ) +-- YYYY-MM-DD: CLASS:**NewFunction( Params )** added +-- +-- Hereby the change log: +-- +-- === +-- +-- # **AUTHORS and CONTRIBUTIONS** +-- +-- ### Contributions: +-- +-- * None. +-- +-- ### Authors: +-- +-- * **FlightControl**: Design & Programming +-- +-- @module Base + + + +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, + _Private = {}, + Events = {}, + States = {} +} + +--- The Formation Class +-- @type FORMATION +-- @field Cone A cone formation. +FORMATION = { + Cone = "Cone" +} + + + +-- @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 + + + return self +end + +function BASE:_Destructor() + --self:E("_Destructor") + + --self:EventRemoveAll() +end + +function BASE:_SetDestructor() + + -- TODO: Okay, this is really technical... + -- When you set a proxy to a table to catch __gc, weak tables don't behave like weak... + -- Therefore, I am parking this logic until I've properly discussed all this with the community. + --[[ + local proxy = newproxy(true) + local proxyMeta = getmetatable(proxy) + + proxyMeta.__gc = function () + env.info("In __gc for " .. self:GetClassNameAndID() ) + if self._Destructor then + self:_Destructor() + end + end + + -- keep the userdata from newproxy reachable until the object + -- table is about to be garbage-collected - then the __gc hook + -- will be invoked and the destructor called + rawset( self, '__proxy', proxy ) + --]] +end + +--- 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 + + Child:_SetDestructor() + end + --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:GetParent( 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 string.format( '%s#%09d', self.ClassName, self.ClassID ) +end + +--- Get the ClassName of the class instance. +-- @param #BASE self +-- @return #string The ClassName of the class instance. +function BASE:GetClassName() + return self.ClassName +end + +--- Get the ClassID of the class instance. +-- @param #BASE self +-- @return #string The ClassID of the class instance. +function BASE:GetClassID() + return self.ClassID +end + +do -- Event Handling + + --- Returns the event dispatcher + -- @param #BASE self + -- @return Core.Event#EVENT + function BASE:EventDispatcher() + + return _EVENTDISPATCHER + end + + + --- Get the Class @{Event} processing Priority. + -- The Event processing Priority is a number from 1 to 10, + -- reflecting the order of the classes subscribed to the Event to be processed. + -- @param #BASE self + -- @return #number The @{Event} processing Priority. + function BASE:GetEventPriority() + return self._Private.EventPriority or 5 + end + + --- Set the Class @{Event} processing Priority. + -- The Event processing Priority is a number from 1 to 10, + -- reflecting the order of the classes subscribed to the Event to be processed. + -- @param #BASE self + -- @param #number EventPriority The @{Event} processing Priority. + -- @return self + function BASE:SetEventPriority( EventPriority ) + self._Private.EventPriority = EventPriority + end + + --- Remove all subscribed events + -- @param #BASE self + -- @return #BASE + function BASE:EventRemoveAll() + + self:EventDispatcher():RemoveAll( self ) + + return self + end + + --- Subscribe to a DCS Event. + -- @param #BASE self + -- @param Core.Event#EVENTS Event + -- @param #function EventFunction (optional) The function to be called when the event occurs for the unit. + -- @return #BASE + function BASE:HandleEvent( Event, EventFunction ) + + self:EventDispatcher():OnEventGeneric( EventFunction, self, Event ) + + return self + end + + --- UnSubscribe to a DCS event. + -- @param #BASE self + -- @param Core.Event#EVENTS Event + -- @return #BASE + function BASE:UnHandleEvent( Event ) + + self:EventDispatcher():Remove( self, Event ) + + return self + end + + -- Event handling function prototypes + + --- Occurs whenever any unit in a mission fires a weapon. But not any machine gun or autocannon based weapon, those are handled by EVENT.ShootingStart. + -- @function [parent=#BASE] OnEventShot + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs whenever an object is hit by a weapon. + -- initiator : The unit object the fired the weapon + -- weapon: Weapon object that hit the target + -- target: The Object that was hit. + -- @function [parent=#BASE] OnEventHit + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when an aircraft takes off from an airbase, farp, or ship. + -- initiator : The unit that tookoff + -- place: Object from where the AI took-off from. Can be an Airbase Object, FARP, or Ships + -- @function [parent=#BASE] OnEventTakeoff + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when an aircraft lands at an airbase, farp or ship + -- initiator : The unit that has landed + -- place: Object that the unit landed on. Can be an Airbase Object, FARP, or Ships + -- @function [parent=#BASE] OnEventLand + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when any aircraft crashes into the ground and is completely destroyed. + -- initiator : The unit that has crashed + -- @function [parent=#BASE] OnEventCrash + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when a pilot ejects from an aircraft + -- initiator : The unit that has ejected + -- @function [parent=#BASE] OnEventEjection + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when an aircraft connects with a tanker and begins taking on fuel. + -- initiator : The unit that is receiving fuel. + -- @function [parent=#BASE] OnEventRefueling + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when an object is completely destroyed. + -- initiator : The unit that is was destroyed. + -- @function [parent=#BASE] OnEvent + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when the pilot of an aircraft is killed. Can occur either if the player is alive and crashes or if a weapon kills the pilot without completely destroying the plane. + -- initiator : The unit that the pilot has died in. + -- @function [parent=#BASE] OnEventPilotDead + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when a ground unit captures either an airbase or a farp. + -- initiator : The unit that captured the base + -- place: The airbase that was captured, can be a FARP or Airbase. When calling place:getCoalition() the faction will already be the new owning faction. + -- @function [parent=#BASE] OnEventBaseCaptured + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when a mission starts + -- @function [parent=#BASE] OnEventMissionStart + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when a mission ends + -- @function [parent=#BASE] OnEventMissionEnd + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when an aircraft is finished taking fuel. + -- initiator : The unit that was receiving fuel. + -- @function [parent=#BASE] OnEventRefuelingStop + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when any object is spawned into the mission. + -- initiator : The unit that was spawned + -- @function [parent=#BASE] OnEventBirth + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when any system fails on a human controlled aircraft. + -- initiator : The unit that had the failure + -- @function [parent=#BASE] OnEventHumanFailure + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when any aircraft starts its engines. + -- initiator : The unit that is starting its engines. + -- @function [parent=#BASE] OnEventEngineStartup + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when any aircraft shuts down its engines. + -- initiator : The unit that is stopping its engines. + -- @function [parent=#BASE] OnEventEngineShutdown + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when any player assumes direct control of a unit. + -- initiator : The unit that is being taken control of. + -- @function [parent=#BASE] OnEventPlayerEnterUnit + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when any player relieves control of a unit to the AI. + -- initiator : The unit that the player left. + -- @function [parent=#BASE] OnEventPlayerLeaveUnit + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when any unit begins firing a weapon that has a high rate of fire. Most common with aircraft cannons (GAU-8), autocannons, and machine guns. + -- initiator : The unit that is doing the shooing. + -- target: The unit that is being targeted. + -- @function [parent=#BASE] OnEventShootingStart + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when any unit stops firing its weapon. Event will always correspond with a shooting start event. + -- initiator : The unit that was doing the shooing. + -- @function [parent=#BASE] OnEventShootingEnd + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + +end + + +--- Creation of a Birth Event. +-- @param #BASE self +-- @param Dcs.DCSTypes#Time EventTime The time stamp of the event. +-- @param Dcs.DCSWrapper.Object#Object Initiator The initiating object of the event. +-- @param #string IniUnitName The initiating unit name. +-- @param place +-- @param subplace +function BASE:CreateEventBirth( EventTime, Initiator, IniUnitName, place, subplace ) + self:F( { EventTime, Initiator, IniUnitName, place, subplace } ) + + local Event = { + id = world.event.S_EVENT_BIRTH, + time = EventTime, + initiator = Initiator, + IniUnitName = IniUnitName, + place = place, + subplace = subplace + } + + world.onEvent( Event ) +end + +--- Creation of a Crash Event. +-- @param #BASE self +-- @param Dcs.DCSTypes#Time EventTime The time stamp of the event. +-- @param Dcs.DCSWrapper.Object#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 Dcs.DCSTypes#Event structure. +--- The main event handling function... This function captures all events generated for the class. +-- @param #BASE self +-- @param Dcs.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 + +--- Set a state or property of the Object given a Key and a Value. +-- Note that if the Object is destroyed, nillified or garbage collected, then the Values and Keys will also be gone. +-- @param #BASE self +-- @param Object The object that will hold the Value set by the Key. +-- @param Key The key that is used as a reference of the value. Note that the key can be a #string, but it can also be any other type! +-- @param Value The value to is stored in the object. +-- @return The Value set. +-- @return #nil The Key was not found and thus the Value could not be retrieved. +function BASE:SetState( Object, Key, Value ) + + local ClassNameAndID = Object:GetClassNameAndID() + + self.States[ClassNameAndID] = self.States[ClassNameAndID] or {} + self.States[ClassNameAndID][Key] = Value + self:T2( { ClassNameAndID, Key, Value } ) + + return self.States[ClassNameAndID][Key] +end + + +--- Get a Value given a Key from the Object. +-- Note that if the Object is destroyed, nillified or garbage collected, then the Values and Keys will also be gone. +-- @param #BASE self +-- @param Object The object that holds the Value set by the Key. +-- @param Key The key that is used to retrieve the value. Note that the key can be a #string, but it can also be any other type! +-- @param Value The value to is stored in the Object. +-- @return The Value retrieved. +function BASE:GetState( Object, Key ) + + local ClassNameAndID = Object:GetClassNameAndID() + + if self.States[ClassNameAndID] then + local Value = self.States[ClassNameAndID][Key] or false + self:T2( { ClassNameAndID, Key, Value } ) + return Value + 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:TraceOnOff( true ) +-- +-- -- Switch the tracing Off +-- BASE:TraceOnOff( false ) +function BASE:TraceOnOff( TraceOnOff ) + _TraceOnOff = TraceOnOff +end + + +--- Enquires if tracing is on (for the class). +-- @param #BASE self +-- @return #boolean +function BASE:IsTrace() + + if debug and ( _TraceAll == true ) or ( _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName] ) then + return true + else + return false + end +end + +--- Set trace level +-- @param #BASE self +-- @param #number Level +function BASE:TraceLevel( Level ) + _TraceLevel = Level + 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 debug and ( _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 debug and _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 debug and _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 debug and _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 debug and ( _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 debug and _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 debug and _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 debug and _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 ) + + if debug then + 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 + +end + + + +--- This module contains the SCHEDULER class. +-- +-- # 1) @{Scheduler#SCHEDULER} class, extends @{Base#BASE} +-- +-- The @{Scheduler#SCHEDULER} class creates schedule. +-- +-- ## 1.1) SCHEDULER constructor +-- +-- The SCHEDULER class is quite easy to use, but note that the New constructor has variable parameters: +-- +-- * @{Scheduler#SCHEDULER.New}( nil ): Setup a new SCHEDULER object, which is persistently executed after garbage collection. +-- * @{Scheduler#SCHEDULER.New}( Object ): Setup a new SCHEDULER object, which is linked to the Object. When the Object is nillified or destroyed, the SCHEDULER object will also be destroyed and stopped after garbage collection. +-- * @{Scheduler#SCHEDULER.New}( nil, Function, FunctionArguments, Start, ... ): Setup a new persistent SCHEDULER object, and start a new schedule for the Function with the defined FunctionArguments according the Start and sequent parameters. +-- * @{Scheduler#SCHEDULER.New}( Object, Function, FunctionArguments, Start, ... ): Setup a new SCHEDULER object, linked to Object, and start a new schedule for the Function with the defined FunctionArguments according the Start and sequent parameters. +-- +-- ## 1.2) SCHEDULER timer stopping and (re-)starting. +-- +-- The SCHEDULER can be stopped and restarted with the following methods: +-- +-- * @{Scheduler#SCHEDULER.Start}(): (Re-)Start the schedules within the SCHEDULER object. If a CallID is provided to :Start(), only the schedule referenced by CallID will be (re-)started. +-- * @{Scheduler#SCHEDULER.Stop}(): Stop the schedules within the SCHEDULER object. If a CallID is provided to :Stop(), then only the schedule referenced by CallID will be stopped. +-- +-- ## 1.3) Create a new schedule +-- +-- With @{Scheduler#SCHEDULER.Schedule}() a new time event can be scheduled. This function is used by the :New() constructor when a new schedule is planned. +-- +-- === +-- +-- ### Contributions: +-- +-- * FlightControl : Concept & Testing +-- +-- ### Authors: +-- +-- * FlightControl : Design & Programming +-- +-- ### Test Missions: +-- +-- * SCH - Scheduler +-- +-- === +-- +-- @module Scheduler + + +--- The SCHEDULER class +-- @type SCHEDULER +-- @field #number ScheduleID the ID of the scheduler. +-- @extends Core.Base#BASE +SCHEDULER = { + ClassName = "SCHEDULER", + Schedules = {}, +} + +--- SCHEDULER constructor. +-- @param #SCHEDULER self +-- @param #table SchedulerObject Specified for which Moose object the timer is setup. If a value of nil is provided, a scheduler will be setup without an object reference. +-- @param #function SchedulerFunction The event function to be called when a timer event occurs. The event function needs to accept the parameters specified in SchedulerArguments. +-- @param #table SchedulerArguments Optional arguments that can be given as part of scheduler. The arguments need to be given as a table { param1, param 2, ... }. +-- @param #number Start Specifies the amount of seconds that will be waited before the scheduling is started, and the event function is called. +-- @param #number Repeat Specifies the interval in seconds when the scheduler will call the event function. +-- @param #number RandomizeFactor Specifies a randomization factor between 0 and 1 to randomize the Repeat. +-- @param #number Stop Specifies the amount of seconds when the scheduler will be stopped. +-- @return #SCHEDULER self. +-- @return #number The ScheduleID of the planned schedule. +function SCHEDULER:New( SchedulerObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop ) + local self = BASE:Inherit( self, BASE:New() ) + self:F2( { Start, Repeat, RandomizeFactor, Stop } ) + + local ScheduleID = nil + + self.MasterObject = SchedulerObject + + if SchedulerFunction then + ScheduleID = self:Schedule( SchedulerObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop ) + end + + return self, ScheduleID +end + +--function SCHEDULER:_Destructor() +-- --self:E("_Destructor") +-- +-- _SCHEDULEDISPATCHER:RemoveSchedule( self.CallID ) +--end + +--- Schedule a new time event. Note that the schedule will only take place if the scheduler is *started*. Even for a single schedule event, the scheduler needs to be started also. +-- @param #SCHEDULER self +-- @param #table SchedulerObject Specified for which Moose object the timer is setup. If a value of nil is provided, a scheduler will be setup without an object reference. +-- @param #function SchedulerFunction The event function to be called when a timer event occurs. The event function needs to accept the parameters specified in SchedulerArguments. +-- @param #table SchedulerArguments Optional arguments that can be given as part of scheduler. The arguments need to be given as a table { param1, param 2, ... }. +-- @param #number Start Specifies the amount of seconds that will be waited before the scheduling is started, and the event function is called. +-- @param #number Repeat Specifies the interval in seconds when the scheduler will call the event function. +-- @param #number RandomizeFactor Specifies a randomization factor between 0 and 1 to randomize the Repeat. +-- @param #number Stop Specifies the amount of seconds when the scheduler will be stopped. +-- @return #number The ScheduleID of the planned schedule. +function SCHEDULER:Schedule( SchedulerObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop ) + self:F2( { Start, Repeat, RandomizeFactor, Stop } ) + self:T3( { SchedulerArguments } ) + + local ObjectName = "-" + if SchedulerObject and SchedulerObject.ClassName and SchedulerObject.ClassID then + ObjectName = SchedulerObject.ClassName .. SchedulerObject.ClassID + end + self:F3( { "Schedule :", ObjectName, tostring( SchedulerObject ), Start, Repeat, RandomizeFactor, Stop } ) + self.SchedulerObject = SchedulerObject + + local ScheduleID = _SCHEDULEDISPATCHER:AddSchedule( + self, + SchedulerFunction, + SchedulerArguments, + Start, + Repeat, + RandomizeFactor, + Stop + ) + + self.Schedules[#self.Schedules+1] = ScheduleID + + return ScheduleID +end + +--- (Re-)Starts the schedules or a specific schedule if a valid ScheduleID is provided. +-- @param #SCHEDULER self +-- @param #number ScheduleID (optional) The ScheduleID of the planned (repeating) schedule. +function SCHEDULER:Start( ScheduleID ) + self:F3( { ScheduleID } ) + + _SCHEDULEDISPATCHER:Start( self, ScheduleID ) +end + +--- Stops the schedules or a specific schedule if a valid ScheduleID is provided. +-- @param #SCHEDULER self +-- @param #number ScheduleID (optional) The ScheduleID of the planned (repeating) schedule. +function SCHEDULER:Stop( ScheduleID ) + self:F3( { ScheduleID } ) + + _SCHEDULEDISPATCHER:Stop( self, ScheduleID ) +end + +--- Removes a specific schedule if a valid ScheduleID is provided. +-- @param #SCHEDULER self +-- @param #number ScheduleID (optional) The ScheduleID of the planned (repeating) schedule. +function SCHEDULER:Remove( ScheduleID ) + self:F3( { ScheduleID } ) + + _SCHEDULEDISPATCHER:Remove( self, ScheduleID ) +end + + + + + + + + + + + + + + + +--- This module defines the SCHEDULEDISPATCHER class, which is used by a central object called _SCHEDULEDISPATCHER. +-- +-- === +-- +-- Takes care of the creation and dispatching of scheduled functions for SCHEDULER objects. +-- +-- This class is tricky and needs some thorought explanation. +-- SCHEDULE classes are used to schedule functions for objects, or as persistent objects. +-- The SCHEDULEDISPATCHER class ensures that: +-- +-- - Scheduled functions are planned according the SCHEDULER object parameters. +-- - Scheduled functions are repeated when requested, according the SCHEDULER object parameters. +-- - Scheduled functions are automatically removed when the schedule is finished, according the SCHEDULER object parameters. +-- +-- The SCHEDULEDISPATCHER class will manage SCHEDULER object in memory during garbage collection: +-- - When a SCHEDULER object is not attached to another object (that is, it's first :Schedule() parameter is nil), then the SCHEDULER +-- object is _persistent_ within memory. +-- - When a SCHEDULER object *is* attached to another object, then the SCHEDULER object is _not persistent_ within memory after a garbage collection! +-- The none persistency of SCHEDULERS attached to objects is required to allow SCHEDULER objects to be garbage collectged, when the parent object is also desroyed or nillified and garbage collected. +-- Even when there are pending timer scheduled functions to be executed for the SCHEDULER object, +-- these will not be executed anymore when the SCHEDULER object has been destroyed. +-- +-- The SCHEDULEDISPATCHER allows multiple scheduled functions to be planned and executed for one SCHEDULER object. +-- The SCHEDULER object therefore keeps a table of "CallID's", which are returned after each planning of a new scheduled function by the SCHEDULEDISPATCHER. +-- The SCHEDULER object plans new scheduled functions through the @{Scheduler#SCHEDULER.Schedule}() method. +-- The Schedule() method returns the CallID that is the reference ID for each planned schedule. +-- +-- === +-- +-- === +-- +-- ### Contributions: - +-- ### Authors: FlightControl : Design & Programming +-- +-- @module ScheduleDispatcher + +--- The SCHEDULEDISPATCHER structure +-- @type SCHEDULEDISPATCHER +SCHEDULEDISPATCHER = { + ClassName = "SCHEDULEDISPATCHER", + CallID = 0, +} + +function SCHEDULEDISPATCHER:New() + local self = BASE:Inherit( self, BASE:New() ) + self:F3() + return self +end + +--- Add a Schedule to the ScheduleDispatcher. +-- The development of this method was really tidy. +-- It is constructed as such that a garbage collection is executed on the weak tables, when the Scheduler is nillified. +-- Nothing of this code should be modified without testing it thoroughly. +-- @param #SCHEDULEDISPATCHER self +-- @param Core.Scheduler#SCHEDULER Scheduler +function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleArguments, Start, Repeat, Randomize, Stop ) + self:F2( { Scheduler, ScheduleFunction, ScheduleArguments, Start, Repeat, Randomize, Stop } ) + + self.CallID = self.CallID + 1 + + -- Initialize the ObjectSchedulers array, which is a weakly coupled table. + -- If the object used as the key is nil, then the garbage collector will remove the item from the Functions array. + self.PersistentSchedulers = self.PersistentSchedulers or {} + + -- Initialize the ObjectSchedulers array, which is a weakly coupled table. + -- If the object used as the key is nil, then the garbage collector will remove the item from the Functions array. + self.ObjectSchedulers = self.ObjectSchedulers or {} -- setmetatable( {}, { __mode = "v" } ) + + if Scheduler.MasterObject then + self.ObjectSchedulers[self.CallID] = Scheduler + self:F3( { CallID = self.CallID, ObjectScheduler = tostring(self.ObjectSchedulers[self.CallID]), MasterObject = tostring(Scheduler.MasterObject) } ) + else + self.PersistentSchedulers[self.CallID] = Scheduler + self:F3( { CallID = self.CallID, PersistentScheduler = self.PersistentSchedulers[self.CallID] } ) + end + + self.Schedule = self.Schedule or setmetatable( {}, { __mode = "k" } ) + self.Schedule[Scheduler] = self.Schedule[Scheduler] or {} + self.Schedule[Scheduler][self.CallID] = {} + self.Schedule[Scheduler][self.CallID].Function = ScheduleFunction + self.Schedule[Scheduler][self.CallID].Arguments = ScheduleArguments + self.Schedule[Scheduler][self.CallID].StartTime = timer.getTime() + ( Start or 0 ) + self.Schedule[Scheduler][self.CallID].Start = Start + .1 + self.Schedule[Scheduler][self.CallID].Repeat = Repeat + self.Schedule[Scheduler][self.CallID].Randomize = Randomize + self.Schedule[Scheduler][self.CallID].Stop = Stop + + self:T3( self.Schedule[Scheduler][self.CallID] ) + + self.Schedule[Scheduler][self.CallID].CallHandler = function( CallID ) + self:F2( CallID ) + + local ErrorHandler = function( errmsg ) + env.info( "Error in timer function: " .. errmsg ) + if debug ~= nil then + env.info( debug.traceback() ) + end + return errmsg + end + + local Scheduler = self.ObjectSchedulers[CallID] + if not Scheduler then + Scheduler = self.PersistentSchedulers[CallID] + end + + self:T3( { Scheduler = Scheduler } ) + + if Scheduler then + + local Schedule = self.Schedule[Scheduler][CallID] + + self:T3( { Schedule = Schedule } ) + + local ScheduleObject = Scheduler.SchedulerObject + --local ScheduleObjectName = Scheduler.SchedulerObject:GetNameAndClassID() + local ScheduleFunction = Schedule.Function + local ScheduleArguments = Schedule.Arguments + local Start = Schedule.Start + local Repeat = Schedule.Repeat or 0 + local Randomize = Schedule.Randomize or 0 + local Stop = Schedule.Stop or 0 + local ScheduleID = Schedule.ScheduleID + + local Status, Result + if ScheduleObject then + local function Timer() + return ScheduleFunction( ScheduleObject, unpack( ScheduleArguments ) ) + end + Status, Result = xpcall( Timer, ErrorHandler ) + else + local function Timer() + return ScheduleFunction( unpack( ScheduleArguments ) ) + end + Status, Result = xpcall( Timer, ErrorHandler ) + end + + local CurrentTime = timer.getTime() + local StartTime = CurrentTime + Start + + if Status and (( Result == nil ) or ( Result and Result ~= false ) ) then + if Repeat ~= 0 and ( Stop == 0 ) or ( Stop ~= 0 and CurrentTime <= StartTime + Stop ) then + local ScheduleTime = + CurrentTime + + Repeat + + math.random( + - ( Randomize * Repeat / 2 ), + ( Randomize * Repeat / 2 ) + ) + + 0.01 + self:T3( { Repeat = CallID, CurrentTime, ScheduleTime, ScheduleArguments } ) + return ScheduleTime -- returns the next time the function needs to be called. + else + self:Stop( Scheduler, CallID ) + end + else + self:Stop( Scheduler, CallID ) + end + else + self:E( "Scheduled obscolete call for CallID: " .. CallID ) + end + + return nil + end + + self:Start( Scheduler, self.CallID ) + + return self.CallID +end + +function SCHEDULEDISPATCHER:RemoveSchedule( Scheduler, CallID ) + self:F2( { Remove = CallID, Scheduler = Scheduler } ) + + if CallID then + self:Stop( Scheduler, CallID ) + self.Schedule[Scheduler][CallID] = nil + end +end + +function SCHEDULEDISPATCHER:Start( Scheduler, CallID ) + self:F2( { Start = CallID, Scheduler = Scheduler } ) + + if CallID then + local Schedule = self.Schedule[Scheduler] + Schedule[CallID].ScheduleID = timer.scheduleFunction( + Schedule[CallID].CallHandler, + CallID, + timer.getTime() + Schedule[CallID].Start + ) + else + for CallID, Schedule in pairs( self.Schedule[Scheduler] ) do + self:Start( Scheduler, CallID ) -- Recursive + end + end +end + +function SCHEDULEDISPATCHER:Stop( Scheduler, CallID ) + self:F2( { Stop = CallID, Scheduler = Scheduler } ) + + if CallID then + local Schedule = self.Schedule[Scheduler] + timer.removeFunction( Schedule[CallID].ScheduleID ) + else + for CallID, Schedule in pairs( self.Schedule[Scheduler] ) do + self:Stop( Scheduler, CallID ) -- Recursive + end + end +end + + + +--- This core module models the dispatching of DCS Events to subscribed MOOSE classes, +-- following a given priority. +-- +-- ![Banner Image](..\Presentations\EVENT\Dia1.JPG) +-- +-- === +-- +-- # 1) Event Handling Overview +-- +-- ![Objects](..\Presentations\EVENT\Dia2.JPG) +-- +-- Within a running mission, various DCS events occur. Units are dynamically created, crash, die, shoot stuff, get hit etc. +-- This module provides a mechanism to dispatch those events occuring within your running mission, to the different objects orchestrating your mission. +-- +-- ![Objects](..\Presentations\EVENT\Dia3.JPG) +-- +-- Objects can subscribe to different events. The Event dispatcher will publish the received DCS events to the subscribed MOOSE objects, in a specified order. +-- In this way, the subscribed MOOSE objects are kept in sync with your evolving running mission. +-- +-- ## 1.1) Event Dispatching +-- +-- ![Objects](..\Presentations\EVENT\Dia4.JPG) +-- +-- The _EVENTDISPATCHER object is automatically created within MOOSE, +-- and handles the dispatching of DCS Events occurring +-- in the simulator to the subscribed objects +-- in the correct processing order. +-- +-- ![Objects](..\Presentations\EVENT\Dia5.JPG) +-- +-- There are 5 levels of kind of objects that the _EVENTDISPATCHER services: +-- +-- * _DATABASE object: The core of the MOOSE objects. Any object that is created, deleted or updated, is done in this database. +-- * SET_ derived classes: Subsets of the _DATABASE object. These subsets are updated by the _EVENTDISPATCHER as the second priority. +-- * UNIT objects: UNIT objects can subscribe to DCS events. Each DCS event will be directly published to teh subscribed UNIT object. +-- * GROUP objects: GROUP objects can subscribe to DCS events. Each DCS event will be directly published to the subscribed GROUP object. +-- * Any other object: Various other objects can subscribe to DCS events. Each DCS event triggered will be published to each subscribed object. +-- +-- ![Objects](..\Presentations\EVENT\Dia6.JPG) +-- +-- For most DCS events, the above order of updating will be followed. +-- +-- ![Objects](..\Presentations\EVENT\Dia7.JPG) +-- +-- But for some DCS events, the publishing order is reversed. This is due to the fact that objects need to be **erased** instead of added. +-- +-- ## 1.2) Event Handling +-- +-- ![Objects](..\Presentations\EVENT\Dia8.JPG) +-- +-- The actual event subscribing and handling is not facilitated through the _EVENTDISPATCHER, but it is done through the @{BASE} class, @{UNIT} class and @{GROUP} class. +-- The _EVENTDISPATCHER is a component that is quietly working in the background of MOOSE. +-- +-- ![Objects](..\Presentations\EVENT\Dia9.JPG) +-- +-- The BASE class provides methods to catch DCS Events. These are events that are triggered from within the DCS simulator, +-- and handled through lua scripting. MOOSE provides an encapsulation to handle these events more efficiently. +-- +-- ### 1.2.1 Subscribe / Unsubscribe to DCS Events +-- +-- At first, the mission designer will need to **Subscribe** to a specific DCS event for the class. +-- So, when the DCS event occurs, the class will be notified of that event. +-- There are two functions which you use to subscribe to or unsubscribe from an event. +-- +-- * @{Base#BASE.HandleEvent}(): Subscribe to a DCS Event. +-- * @{Base#BASE.UnHandleEvent}(): Unsubscribe from a DCS Event. +-- +-- Note that for a UNIT, the event will be handled **for that UNIT only**! +-- Note that for a GROUP, the event will be handled **for all the UNITs in that GROUP only**! +-- +-- For all objects of other classes, the subscribed events will be handled for **all UNITs within the Mission**! +-- So if a UNIT within the mission has the subscribed event for that object, +-- then the object event handler will receive the event for that UNIT! +-- +-- ### 1.3.2 Event Handling of DCS Events +-- +-- Once the class is subscribed to the event, an **Event Handling** method on the object or class needs to be written that will be called +-- when the DCS event occurs. The Event Handling method receives an @{Event#EVENTDATA} structure, which contains a lot of information +-- about the event that occurred. +-- +-- Find below an example of the prototype how to write an event handling function for two units: +-- +-- local Tank1 = UNIT:FindByName( "Tank A" ) +-- local Tank2 = UNIT:FindByName( "Tank B" ) +-- +-- -- Here we subscribe to the Dead events. So, if one of these tanks dies, the Tank1 or Tank2 objects will be notified. +-- Tank1:HandleEvent( EVENTS.Dead ) +-- Tank2:HandleEvent( EVENTS.Dead ) +-- +-- --- This function is an Event Handling function that will be called when Tank1 is Dead. +-- -- @param Wrapper.Unit#UNIT self +-- -- @param Core.Event#EVENTDATA EventData +-- function Tank1:OnEventDead( EventData ) +-- +-- self:SmokeGreen() +-- end +-- +-- --- This function is an Event Handling function that will be called when Tank2 is Dead. +-- -- @param Wrapper.Unit#UNIT self +-- -- @param Core.Event#EVENTDATA EventData +-- function Tank2:OnEventDead( EventData ) +-- +-- self:SmokeBlue() +-- end +-- +-- ### 1.3.3 Event Handling methods that are automatically called upon subscribed DCS events +-- +-- ![Objects](..\Presentations\EVENT\Dia10.JPG) +-- +-- The following list outlines which EVENTS item in the structure corresponds to which Event Handling method. +-- Always ensure that your event handling methods align with the events being subscribed to, or nothing will be executed. +-- +-- # 2) EVENTS type +-- +-- The EVENTS structure contains names for all the different DCS events that objects can subscribe to using the +-- @{Base#BASE.HandleEvent}() method. +-- +-- # 3) EVENTDATA type +-- +-- The @{Event#EVENTDATA} structure contains all the fields that are populated with event information before +-- an Event Handler method is being called by the event dispatcher. +-- The Event Handler received the EVENTDATA object as a parameter, and can be used to investigate further the different events. +-- There are basically 4 main categories of information stored in the EVENTDATA structure: +-- +-- * Initiator Unit data: Several fields documenting the initiator unit related to the event. +-- * Target Unit data: Several fields documenting the target unit related to the event. +-- * Weapon data: Certain events populate weapon information. +-- * Place data: Certain events populate place information. +-- +-- --- This function is an Event Handling function that will be called when Tank1 is Dead. +-- -- EventData is an EVENTDATA structure. +-- -- We use the EventData.IniUnit to smoke the tank Green. +-- -- @param Wrapper.Unit#UNIT self +-- -- @param Core.Event#EVENTDATA EventData +-- function Tank1:OnEventDead( EventData ) +-- +-- EventData.IniUnit:SmokeGreen() +-- end +-- +-- +-- Find below an overview which events populate which information categories: +-- +-- ![Objects](..\Presentations\EVENT\Dia14.JPG) +-- +-- **IMPORTANT NOTE:** Some events can involve not just UNIT objects, but also STATIC objects!!! +-- In that case the initiator or target unit fields will refer to a STATIC object! +-- In case a STATIC object is involved, the documentation indicates which fields will and won't not be populated. +-- The fields **IniObjectCategory** and **TgtObjectCategory** contain the indicator which **kind of object is involved** in the event. +-- You can use the enumerator **Object.Category.UNIT** and **Object.Category.STATIC** to check on IniObjectCategory and TgtObjectCategory. +-- Example code snippet: +-- +-- if Event.IniObjectCategory == Object.Category.UNIT then +-- ... +-- end +-- if Event.IniObjectCategory == Object.Category.STATIC then +-- ... +-- end +-- +-- When a static object is involved in the event, the Group and Player fields won't be populated. +-- +-- ==== +-- +-- # **API CHANGE HISTORY** +-- +-- The underlying change log documents the API changes. Please read this carefully. The following notation is used: +-- +-- * **Added** parts are expressed in bold type face. +-- * _Removed_ parts are expressed in italic type face. +-- +-- YYYY-MM-DD: CLASS:**NewFunction**( Params ) replaces CLASS:_OldFunction_( Params ) +-- YYYY-MM-DD: CLASS:**NewFunction( Params )** added +-- +-- Hereby the change log: +-- +-- * 2017-03-07: Added the correct event dispatching in case the event is subscribed by a GROUP. +-- +-- * 2017-02-07: Did a complete revision of the Event Handing API and underlying mechanisms. +-- +-- === +-- +-- # **AUTHORS and CONTRIBUTIONS** +-- +-- ### Contributions: +-- +-- ### Authors: +-- +-- * [**FlightControl**](https://forums.eagle.ru/member.php?u=89536): Design & Programming & documentation. +-- +-- @module Event + + +--- The EVENT structure +-- @type EVENT +-- @field #EVENT.Events Events +-- @extends Core.Base#BASE +EVENT = { + ClassName = "EVENT", + ClassID = 0, +} + +--- The different types of events supported by MOOSE. +-- Use this structure to subscribe to events using the @{Base#BASE.HandleEvent}() method. +-- @type EVENTS +EVENTS = { + Shot = world.event.S_EVENT_SHOT, + Hit = world.event.S_EVENT_HIT, + Takeoff = world.event.S_EVENT_TAKEOFF, + Land = world.event.S_EVENT_LAND, + Crash = world.event.S_EVENT_CRASH, + Ejection = world.event.S_EVENT_EJECTION, + Refueling = world.event.S_EVENT_REFUELING, + Dead = world.event.S_EVENT_DEAD, + PilotDead = world.event.S_EVENT_PILOT_DEAD, + BaseCaptured = world.event.S_EVENT_BASE_CAPTURED, + MissionStart = world.event.S_EVENT_MISSION_START, + MissionEnd = world.event.S_EVENT_MISSION_END, + TookControl = world.event.S_EVENT_TOOK_CONTROL, + RefuelingStop = world.event.S_EVENT_REFUELING_STOP, + Birth = world.event.S_EVENT_BIRTH, + HumanFailure = world.event.S_EVENT_HUMAN_FAILURE, + EngineStartup = world.event.S_EVENT_ENGINE_STARTUP, + EngineShutdown = world.event.S_EVENT_ENGINE_SHUTDOWN, + PlayerEnterUnit = world.event.S_EVENT_PLAYER_ENTER_UNIT, + PlayerLeaveUnit = world.event.S_EVENT_PLAYER_LEAVE_UNIT, + PlayerComment = world.event.S_EVENT_PLAYER_COMMENT, + ShootingStart = world.event.S_EVENT_SHOOTING_START, + ShootingEnd = world.event.S_EVENT_SHOOTING_END, +} + +--- The Event structure +-- Note that at the beginning of each field description, there is an indication which field will be populated depending on the object type involved in the Event: +-- +-- * A (Object.Category.)UNIT : A UNIT object type is involved in the Event. +-- * A (Object.Category.)STATIC : A STATIC object type is involved in the Event.µ +-- +-- @type EVENTDATA +-- @field #number id The identifier of the event. +-- +-- @field Dcs.DCSUnit#Unit initiator (UNIT/STATIC/SCENERY) The initiating @{Dcs.DCSUnit#Unit} or @{Dcs.DCSStaticObject#StaticObject}. +-- @field Dcs.DCSObject#Object.Category IniObjectCategory (UNIT/STATIC/SCENERY) The initiator object category ( Object.Category.UNIT or Object.Category.STATIC ). +-- @field Dcs.DCSUnit#Unit IniDCSUnit (UNIT/STATIC) The initiating @{DCSUnit#Unit} or @{DCSStaticObject#StaticObject}. +-- @field #string IniDCSUnitName (UNIT/STATIC) The initiating Unit name. +-- @field Wrapper.Unit#UNIT IniUnit (UNIT/STATIC) The initiating MOOSE wrapper @{Unit#UNIT} of the initiator Unit object. +-- @field #string IniUnitName (UNIT/STATIC) The initiating UNIT name (same as IniDCSUnitName). +-- @field Dcs.DCSGroup#Group IniDCSGroup (UNIT) The initiating {DCSGroup#Group}. +-- @field #string IniDCSGroupName (UNIT) The initiating Group name. +-- @field Wrapper.Group#GROUP IniGroup (UNIT) The initiating MOOSE wrapper @{Group#GROUP} of the initiator Group object. +-- @field #string IniGroupName UNIT) The initiating GROUP name (same as IniDCSGroupName). +-- @field #string IniPlayerName (UNIT) The name of the initiating player in case the Unit is a client or player slot. +-- @field Dcs.DCScoalition#coalition.side IniCoalition (UNIT) The coalition of the initiator. +-- @field Dcs.DCSUnit#Unit.Category IniCategory (UNIT) The category of the initiator. +-- @field #string IniTypeName (UNIT) The type name of the initiator. +-- +-- @field Dcs.DCSUnit#Unit target (UNIT/STATIC) The target @{Dcs.DCSUnit#Unit} or @{DCSStaticObject#StaticObject}. +-- @field Dcs.DCSObject#Object.Category TgtObjectCategory (UNIT/STATIC) The target object category ( Object.Category.UNIT or Object.Category.STATIC ). +-- @field Dcs.DCSUnit#Unit TgtDCSUnit (UNIT/STATIC) The target @{DCSUnit#Unit} or @{DCSStaticObject#StaticObject}. +-- @field #string TgtDCSUnitName (UNIT/STATIC) The target Unit name. +-- @field Wrapper.Unit#UNIT TgtUnit (UNIT/STATIC) The target MOOSE wrapper @{Unit#UNIT} of the target Unit object. +-- @field #string TgtUnitName (UNIT/STATIC) The target UNIT name (same as TgtDCSUnitName). +-- @field Dcs.DCSGroup#Group TgtDCSGroup (UNIT) The target {DCSGroup#Group}. +-- @field #string TgtDCSGroupName (UNIT) The target Group name. +-- @field Wrapper.Group#GROUP TgtGroup (UNIT) The target MOOSE wrapper @{Group#GROUP} of the target Group object. +-- @field #string TgtGroupName (UNIT) The target GROUP name (same as TgtDCSGroupName). +-- @field #string TgtPlayerName (UNIT) The name of the target player in case the Unit is a client or player slot. +-- @field Dcs.DCScoalition#coalition.side TgtCoalition (UNIT) The coalition of the target. +-- @field Dcs.DCSUnit#Unit.Category TgtCategory (UNIT) The category of the target. +-- @field #string TgtTypeName (UNIT) The type name of the target. +-- +-- @field weapon The weapon used during the event. +-- @field Weapon +-- @field WeaponName +-- @field WeaponTgtDCSUnit + + +local _EVENTMETA = { + [world.event.S_EVENT_SHOT] = { + Order = 1, + Event = "OnEventShot", + Text = "S_EVENT_SHOT" + }, + [world.event.S_EVENT_HIT] = { + Order = 1, + Event = "OnEventHit", + Text = "S_EVENT_HIT" + }, + [world.event.S_EVENT_TAKEOFF] = { + Order = 1, + Event = "OnEventTakeOff", + Text = "S_EVENT_TAKEOFF" + }, + [world.event.S_EVENT_LAND] = { + Order = 1, + Event = "OnEventLand", + Text = "S_EVENT_LAND" + }, + [world.event.S_EVENT_CRASH] = { + Order = -1, + Event = "OnEventCrash", + Text = "S_EVENT_CRASH" + }, + [world.event.S_EVENT_EJECTION] = { + Order = 1, + Event = "OnEventEjection", + Text = "S_EVENT_EJECTION" + }, + [world.event.S_EVENT_REFUELING] = { + Order = 1, + Event = "OnEventRefueling", + Text = "S_EVENT_REFUELING" + }, + [world.event.S_EVENT_DEAD] = { + Order = -1, + Event = "OnEventDead", + Text = "S_EVENT_DEAD" + }, + [world.event.S_EVENT_PILOT_DEAD] = { + Order = 1, + Event = "OnEventPilotDead", + Text = "S_EVENT_PILOT_DEAD" + }, + [world.event.S_EVENT_BASE_CAPTURED] = { + Order = 1, + Event = "OnEventBaseCaptured", + Text = "S_EVENT_BASE_CAPTURED" + }, + [world.event.S_EVENT_MISSION_START] = { + Order = 1, + Event = "OnEventMissionStart", + Text = "S_EVENT_MISSION_START" + }, + [world.event.S_EVENT_MISSION_END] = { + Order = 1, + Event = "OnEventMissionEnd", + Text = "S_EVENT_MISSION_END" + }, + [world.event.S_EVENT_TOOK_CONTROL] = { + Order = 1, + Event = "OnEventTookControl", + Text = "S_EVENT_TOOK_CONTROL" + }, + [world.event.S_EVENT_REFUELING_STOP] = { + Order = 1, + Event = "OnEventRefuelingStop", + Text = "S_EVENT_REFUELING_STOP" + }, + [world.event.S_EVENT_BIRTH] = { + Order = 1, + Event = "OnEventBirth", + Text = "S_EVENT_BIRTH" + }, + [world.event.S_EVENT_HUMAN_FAILURE] = { + Order = 1, + Event = "OnEventHumanFailure", + Text = "S_EVENT_HUMAN_FAILURE" + }, + [world.event.S_EVENT_ENGINE_STARTUP] = { + Order = 1, + Event = "OnEventEngineStartup", + Text = "S_EVENT_ENGINE_STARTUP" + }, + [world.event.S_EVENT_ENGINE_SHUTDOWN] = { + Order = 1, + Event = "OnEventEngineShutdown", + Text = "S_EVENT_ENGINE_SHUTDOWN" + }, + [world.event.S_EVENT_PLAYER_ENTER_UNIT] = { + Order = 1, + Event = "OnEventPlayerEnterUnit", + Text = "S_EVENT_PLAYER_ENTER_UNIT" + }, + [world.event.S_EVENT_PLAYER_LEAVE_UNIT] = { + Order = -1, + Event = "OnEventPlayerLeaveUnit", + Text = "S_EVENT_PLAYER_LEAVE_UNIT" + }, + [world.event.S_EVENT_PLAYER_COMMENT] = { + Order = 1, + Event = "OnEventPlayerComment", + Text = "S_EVENT_PLAYER_COMMENT" + }, + [world.event.S_EVENT_SHOOTING_START] = { + Order = 1, + Event = "OnEventShootingStart", + Text = "S_EVENT_SHOOTING_START" + }, + [world.event.S_EVENT_SHOOTING_END] = { + Order = 1, + Event = "OnEventShootingEnd", + Text = "S_EVENT_SHOOTING_END" + }, +} + + +--- 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 = _EVENTMETA[EventID].Text + + return EventText +end + + +--- Initializes the Events structure for the event +-- @param #EVENT self +-- @param Dcs.DCSWorld#world.event EventID +-- @param Core.Base#BASE EventClass +-- @return #EVENT.Events +function EVENT:Init( EventID, EventClass ) + self:F3( { _EVENTMETA[EventID].Text, EventClass } ) + + if not self.Events[EventID] then + -- Create a WEAK table to ensure that the garbage collector is cleaning the event links when the object usage is cleaned. + self.Events[EventID] = setmetatable( {}, { __mode = "k" } ) + end + + -- Each event has a subtable of EventClasses, ordered by EventPriority. + local EventPriority = EventClass:GetEventPriority() + if not self.Events[EventID][EventPriority] then + self.Events[EventID][EventPriority] = {} + end + + if not self.Events[EventID][EventPriority][EventClass] then + self.Events[EventID][EventPriority][EventClass] = setmetatable( {}, { __mode = "k" } ) + end + return self.Events[EventID][EventPriority][EventClass] +end + +--- Removes an Events entry +-- @param #EVENT self +-- @param Core.Base#BASE EventClass The self instance of the class for which the event is. +-- @param Dcs.DCSWorld#world.event EventID +-- @return #EVENT.Events +function EVENT:Remove( EventClass, EventID ) + self:F3( { EventClass, _EVENTMETA[EventID].Text } ) + + local EventClass = EventClass + local EventPriority = EventClass:GetEventPriority() + self.Events[EventID][EventPriority][EventClass] = nil +end + +--- Removes an Events entry for a UNIT. +-- @param #EVENT self +-- @param #string UnitName The name of the UNIT. +-- @param Core.Base#BASE EventClass The self instance of the class for which the event is. +-- @param Dcs.DCSWorld#world.event EventID +-- @return #EVENT.Events +function EVENT:RemoveForUnit( UnitName, EventClass, EventID ) + self:F3( { EventClass, _EVENTMETA[EventID].Text } ) + + local EventClass = EventClass + local EventPriority = EventClass:GetEventPriority() + local Event = self.Events[EventID][EventPriority][EventClass] + Event.IniUnit[UnitName] = nil +end + +--- Removes an Events entry for a GROUP. +-- @param #EVENT self +-- @param #string GroupName The name of the GROUP. +-- @param Core.Base#BASE EventClass The self instance of the class for which the event is. +-- @param Dcs.DCSWorld#world.event EventID +-- @return #EVENT.Events +function EVENT:RemoveForGroup( GroupName, EventClass, EventID ) + self:F3( { EventClass, _EVENTMETA[EventID].Text } ) + + local EventClass = EventClass + local EventPriority = EventClass:GetEventPriority() + local Event = self.Events[EventID][EventPriority][EventClass] + Event.IniGroup[GroupName] = nil +end + +--- Clears all event subscriptions for a @{Base#BASE} derived object. +-- @param #EVENT self +-- @param Core.Base#BASE EventObject +function EVENT:RemoveAll( EventObject ) + self:F3( { EventObject:GetClassNameAndID() } ) + + local EventClass = EventObject:GetClassNameAndID() + local EventPriority = EventClass:GetEventPriority() + for EventID, EventData in pairs( self.Events ) do + self.Events[EventID][EventPriority][EventClass] = nil + end +end + + + +--- Create an OnDead event handler for a group +-- @param #EVENT self +-- @param #table EventTemplate +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param EventClass The instance of the class for which the event is. +-- @param #function OnEventFunction +-- @return #EVENT +function EVENT:OnEventForTemplate( EventTemplate, EventFunction, EventClass, OnEventFunction ) + self:F2( EventTemplate.name ) + + for EventUnitID, EventUnit in pairs( EventTemplate.units ) do + OnEventFunction( self, EventUnit.name, EventFunction, EventClass ) + end + return self +end + +--- Set a new listener for an S_EVENT_X event independent from a unit or a weapon. +-- @param #EVENT self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Core.Base#BASE EventClass The self instance of the class for which the event is captured. When the event happens, the event process will be called in this class provided. +-- @param EventID +-- @return #EVENT +function EVENT:OnEventGeneric( EventFunction, EventClass, EventID ) + self:F2( { EventID } ) + + local Event = self:Init( EventID, EventClass ) + Event.EventFunction = EventFunction + Event.EventClass = EventClass + + return self +end + + +--- Set a new listener for an S_EVENT_X event for a UNIT. +-- @param #EVENT self +-- @param #string UnitName The name of the UNIT. +-- @param #function EventFunction The function to be called when the event occurs for the GROUP. +-- @param Core.Base#BASE EventClass The self instance of the class for which the event is. +-- @param EventID +-- @return #EVENT +function EVENT:OnEventForUnit( UnitName, EventFunction, EventClass, EventID ) + self:F2( UnitName ) + + local Event = self:Init( EventID, EventClass ) + if not Event.IniUnit then + Event.IniUnit = {} + end + Event.IniUnit[UnitName] = {} + Event.IniUnit[UnitName].EventFunction = EventFunction + Event.IniUnit[UnitName].EventClass = EventClass + return self +end + +--- Set a new listener for an S_EVENT_X event for a GROUP. +-- @param #EVENT self +-- @param #string GroupName The name of the GROUP. +-- @param #function EventFunction The function to be called when the event occurs for the GROUP. +-- @param Core.Base#BASE EventClass The self instance of the class for which the event is. +-- @param EventID +-- @return #EVENT +function EVENT:OnEventForGroup( GroupName, EventFunction, EventClass, EventID ) + self:F2( GroupName ) + + local Event = self:Init( EventID, EventClass ) + if not Event.IniGroup then + Event.IniGroup = {} + end + Event.IniGroup[GroupName] = {} + Event.IniGroup[GroupName].EventFunction = EventFunction + Event.IniGroup[GroupName].EventClass = EventClass + return self +end + +do -- OnBirth + + --- Create an OnBirth event handler for a group + -- @param #EVENT self + -- @param Wrapper.Group#GROUP EventGroup + -- @param #function EventFunction The function to be called when the event occurs for the unit. + -- @param EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnBirthForTemplate( EventTemplate, EventFunction, EventClass ) + self:F2( EventTemplate.name ) + + self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, 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 EventClass + -- @return #EVENT + function EVENT:OnBirth( EventFunction, EventClass ) + self:F2() + + self:OnEventGeneric( EventFunction, EventClass, 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 EventClass + -- @return #EVENT + function EVENT:OnBirthForUnit( EventDCSUnitName, EventFunction, EventClass ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_BIRTH ) + + return self + end + + --- Stop listening to S_EVENT_BIRTH event. + -- @param #EVENT self + -- @param Base#BASE EventClass + -- @return #EVENT + function EVENT:OnBirthRemove( EventClass ) + self:F2() + + self:Remove( EventClass, world.event.S_EVENT_BIRTH ) + + return self + end + + +end + +do -- OnCrash + + --- Create an OnCrash event handler for a group + -- @param #EVENT self + -- @param Wrapper.Group#GROUP EventGroup + -- @param #function EventFunction The function to be called when the event occurs for the unit. + -- @param EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnCrashForTemplate( EventTemplate, EventFunction, EventClass ) + self:F2( EventTemplate.name ) + + self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, 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 EventClass + -- @return #EVENT + function EVENT:OnCrash( EventFunction, EventClass ) + self:F2() + + self:OnEventGeneric( EventFunction, EventClass, 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 EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnCrashForUnit( EventDCSUnitName, EventFunction, EventClass ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_CRASH ) + + return self + end + + --- Stop listening to S_EVENT_CRASH event. + -- @param #EVENT self + -- @param Base#BASE EventClass + -- @return #EVENT + function EVENT:OnCrashRemove( EventClass ) + self:F2() + + self:Remove( EventClass, world.event.S_EVENT_CRASH ) + + return self + end + +end + +do -- OnDead + + --- Create an OnDead event handler for a group + -- @param #EVENT self + -- @param Wrapper.Group#GROUP EventGroup + -- @param #function EventFunction The function to be called when the event occurs for the unit. + -- @param EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnDeadForTemplate( EventTemplate, EventFunction, EventClass ) + self:F2( EventTemplate.name ) + + self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, 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 EventClass + -- @return #EVENT + function EVENT:OnDead( EventFunction, EventClass ) + self:F2() + + self:OnEventGeneric( EventFunction, EventClass, 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 EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnDeadForUnit( EventDCSUnitName, EventFunction, EventClass ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_DEAD ) + + return self + end + + --- Stop listening to S_EVENT_DEAD event. + -- @param #EVENT self + -- @param Base#BASE EventClass + -- @return #EVENT + function EVENT:OnDeadRemove( EventClass ) + self:F2() + + self:Remove( EventClass, world.event.S_EVENT_DEAD ) + + return self + end + + +end + +do -- OnPilotDead + + --- Set a new listener for an S_EVENT_PILOT_DEAD event. + -- @param #EVENT self + -- @param #function EventFunction The function to be called when the event occurs for the unit. + -- @param Base#BASE EventClass + -- @return #EVENT + function EVENT:OnPilotDead( EventFunction, EventClass ) + self:F2() + + self:OnEventGeneric( EventFunction, EventClass, world.event.S_EVENT_PILOT_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 EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnPilotDeadForUnit( EventDCSUnitName, EventFunction, EventClass ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_PILOT_DEAD ) + + return self + end + + --- Stop listening to S_EVENT_PILOT_DEAD event. + -- @param #EVENT self + -- @param Base#BASE EventClass + -- @return #EVENT + function EVENT:OnPilotDeadRemove( EventClass ) + self:F2() + + self:Remove( EventClass, world.event.S_EVENT_PILOT_DEAD ) + + return self + end + +end + +do -- OnLand + --- Create an OnLand event handler for a group + -- @param #EVENT self + -- @param #table EventTemplate + -- @param #function EventFunction The function to be called when the event occurs for the unit. + -- @param EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnLandForTemplate( EventTemplate, EventFunction, EventClass ) + self:F2( EventTemplate.name ) + + self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, 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 EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnLandForUnit( EventDCSUnitName, EventFunction, EventClass ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_LAND ) + + return self + end + + --- Stop listening to S_EVENT_LAND event. + -- @param #EVENT self + -- @param Base#BASE EventClass + -- @return #EVENT + function EVENT:OnLandRemove( EventClass ) + self:F2() + + self:Remove( EventClass, world.event.S_EVENT_LAND ) + + return self + end + + +end + +do -- OnTakeOff + --- Create an OnTakeOff event handler for a group + -- @param #EVENT self + -- @param #table EventTemplate + -- @param #function EventFunction The function to be called when the event occurs for the unit. + -- @param EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnTakeOffForTemplate( EventTemplate, EventFunction, EventClass ) + self:F2( EventTemplate.name ) + + self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, 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 EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnTakeOffForUnit( EventDCSUnitName, EventFunction, EventClass ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_TAKEOFF ) + + return self + end + + --- Stop listening to S_EVENT_TAKEOFF event. + -- @param #EVENT self + -- @param Base#BASE EventClass + -- @return #EVENT + function EVENT:OnTakeOffRemove( EventClass ) + self:F2() + + self:Remove( EventClass, world.event.S_EVENT_TAKEOFF ) + + return self + end + + +end + +do -- OnEngineShutDown + + --- Create an OnDead event handler for a group + -- @param #EVENT self + -- @param #table EventTemplate + -- @param #function EventFunction The function to be called when the event occurs for the unit. + -- @param EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnEngineShutDownForTemplate( EventTemplate, EventFunction, EventClass ) + self:F2( EventTemplate.name ) + + self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, 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 EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnEngineShutDownForUnit( EventDCSUnitName, EventFunction, EventClass ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_ENGINE_SHUTDOWN ) + + return self + end + + --- Stop listening to S_EVENT_ENGINE_SHUTDOWN event. + -- @param #EVENT self + -- @param Base#BASE EventClass + -- @return #EVENT + function EVENT:OnEngineShutDownRemove( EventClass ) + self:F2() + + self:Remove( EventClass, world.event.S_EVENT_ENGINE_SHUTDOWN ) + + return self + end + +end + +do -- OnEngineStartUp + + --- 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 EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnEngineStartUpForUnit( EventDCSUnitName, EventFunction, EventClass ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_ENGINE_STARTUP ) + + return self + end + + --- Stop listening to S_EVENT_ENGINE_STARTUP event. + -- @param #EVENT self + -- @param Base#BASE EventClass + -- @return #EVENT + function EVENT:OnEngineStartUpRemove( EventClass ) + self:F2() + + self:Remove( EventClass, world.event.S_EVENT_ENGINE_STARTUP ) + + return self + end + +end + +do -- OnShot + --- 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 EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnShot( EventFunction, EventClass ) + self:F2() + + self:OnEventGeneric( EventFunction, EventClass, 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 EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnShotForUnit( EventDCSUnitName, EventFunction, EventClass ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_SHOT ) + + return self + end + + --- Stop listening to S_EVENT_SHOT event. + -- @param #EVENT self + -- @param Base#BASE EventClass + -- @return #EVENT + function EVENT:OnShotRemove( EventClass ) + self:F2() + + self:Remove( EventClass, world.event.S_EVENT_SHOT ) + + return self + end + + +end + +do -- OnHit + + --- 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 EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnHit( EventFunction, EventClass ) + self:F2() + + self:OnEventGeneric( EventFunction, EventClass, 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 EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnHitForUnit( EventDCSUnitName, EventFunction, EventClass ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_HIT ) + + return self + end + + --- Stop listening to S_EVENT_HIT event. + -- @param #EVENT self + -- @param Base#BASE EventClass + -- @return #EVENT + function EVENT:OnHitRemove( EventClass ) + self:F2() + + self:Remove( EventClass, world.event.S_EVENT_HIT ) + + return self + end + +end + +do -- OnPlayerEnterUnit + + --- 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 EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnPlayerEnterUnit( EventFunction, EventClass ) + self:F2() + + self:OnEventGeneric( EventFunction, EventClass, world.event.S_EVENT_PLAYER_ENTER_UNIT ) + + return self + end + + --- Stop listening to S_EVENT_PLAYER_ENTER_UNIT event. + -- @param #EVENT self + -- @param Base#BASE EventClass + -- @return #EVENT + function EVENT:OnPlayerEnterRemove( EventClass ) + self:F2() + + self:Remove( EventClass, world.event.S_EVENT_PLAYER_ENTER_UNIT ) + + return self + end + +end + +do -- OnPlayerLeaveUnit + --- 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 EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnPlayerLeaveUnit( EventFunction, EventClass ) + self:F2() + + self:OnEventGeneric( EventFunction, EventClass, world.event.S_EVENT_PLAYER_LEAVE_UNIT ) + + return self + end + + --- Stop listening to S_EVENT_PLAYER_LEAVE_UNIT event. + -- @param #EVENT self + -- @param Base#BASE EventClass + -- @return #EVENT + function EVENT:OnPlayerLeaveRemove( EventClass ) + self:F2() + + self:Remove( EventClass, world.event.S_EVENT_PLAYER_LEAVE_UNIT ) + + return self + end + +end + + + +--- @param #EVENT self +-- @param #EVENTDATA Event +function EVENT:onEvent( Event ) + + local ErrorHandler = function( errmsg ) + + env.info( "Error in SCHEDULER function:" .. errmsg ) + if debug ~= nil then + env.info( debug.traceback() ) + end + + return errmsg + end + + if self and self.Events and self.Events[Event.id] then + + + if Event.initiator then + + Event.IniObjectCategory = Event.initiator:getCategory() + + if Event.IniObjectCategory == Object.Category.UNIT then + Event.IniDCSUnit = Event.initiator + Event.IniDCSUnitName = Event.IniDCSUnit:getName() + Event.IniUnitName = Event.IniDCSUnitName + Event.IniDCSGroup = Event.IniDCSUnit:getGroup() + Event.IniUnit = UNIT:FindByName( Event.IniDCSUnitName ) + if not Event.IniUnit then + -- Unit can be a CLIENT. Most likely this will be the case ... + Event.IniUnit = CLIENT:FindByName( Event.IniDCSUnitName, '', true ) + end + Event.IniDCSGroupName = "" + if Event.IniDCSGroup and Event.IniDCSGroup:isExist() then + Event.IniDCSGroupName = Event.IniDCSGroup:getName() + Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) + if Event.IniGroup then + Event.IniGroupName = Event.IniDCSGroupName + end + end + Event.IniPlayerName = Event.IniDCSUnit:getPlayerName() + Event.IniCoalition = Event.IniDCSUnit:getCoalition() + Event.IniTypeName = Event.IniDCSUnit:getTypeName() + Event.IniCategory = Event.IniDCSUnit:getDesc().category + end + + if Event.IniObjectCategory == Object.Category.STATIC then + Event.IniDCSUnit = Event.initiator + Event.IniDCSUnitName = Event.IniDCSUnit:getName() + Event.IniUnitName = Event.IniDCSUnitName + Event.IniUnit = STATIC:FindByName( Event.IniDCSUnitName, false ) + Event.IniCoalition = Event.IniDCSUnit:getCoalition() + Event.IniCategory = Event.IniDCSUnit:getDesc().category + Event.IniTypeName = Event.IniDCSUnit:getTypeName() + end + + if Event.IniObjectCategory == Object.Category.SCENERY then + Event.IniDCSUnit = Event.initiator + Event.IniDCSUnitName = Event.IniDCSUnit:getName() + Event.IniUnitName = Event.IniDCSUnitName + Event.IniUnit = SCENERY:Register( Event.IniDCSUnitName, Event.initiator ) + Event.IniCategory = Event.IniDCSUnit:getDesc().category + Event.IniTypeName = Event.IniDCSUnit:getTypeName() + end + end + + if Event.target then + + Event.TgtObjectCategory = Event.target:getCategory() + + if Event.TgtObjectCategory == Object.Category.UNIT then + Event.TgtDCSUnit = Event.target + Event.TgtDCSGroup = Event.TgtDCSUnit:getGroup() + Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() + Event.TgtUnitName = Event.TgtDCSUnitName + Event.TgtUnit = UNIT:FindByName( Event.TgtDCSUnitName ) + Event.TgtDCSGroupName = "" + if Event.TgtDCSGroup and Event.TgtDCSGroup:isExist() then + Event.TgtDCSGroupName = Event.TgtDCSGroup:getName() + Event.TgtGroup = GROUP:FindByName( Event.TgtDCSGroupName ) + if Event.TgtGroup then + Event.TgtGroupName = Event.TgtDCSGroupName + end + end + Event.TgtPlayerName = Event.TgtDCSUnit:getPlayerName() + Event.TgtCoalition = Event.TgtDCSUnit:getCoalition() + Event.TgtCategory = Event.TgtDCSUnit:getDesc().category + Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() + end + + if Event.TgtObjectCategory == Object.Category.STATIC then + Event.TgtDCSUnit = Event.target + Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() + Event.TgtUnitName = Event.TgtDCSUnitName + Event.TgtUnit = STATIC:FindByName( Event.TgtDCSUnitName ) + Event.TgtCoalition = Event.TgtDCSUnit:getCoalition() + Event.TgtCategory = Event.TgtDCSUnit:getDesc().category + Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() + end + + if Event.TgtObjectCategory == Object.Category.SCENERY then + Event.TgtDCSUnit = Event.target + Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() + Event.TgtUnitName = Event.TgtDCSUnitName + Event.TgtUnit = SCENERY:Register( Event.TgtDCSUnitName, Event.target ) + Event.TgtCategory = Event.TgtDCSUnit:getDesc().category + Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() + end + end + + if Event.weapon then + Event.Weapon = Event.weapon + Event.WeaponName = Event.Weapon:getTypeName() + --Event.WeaponTgtDCSUnit = Event.Weapon:getTarget() + end + + local PriorityOrder = _EVENTMETA[Event.id].Order + local PriorityBegin = PriorityOrder == -1 and 5 or 1 + local PriorityEnd = PriorityOrder == -1 and 1 or 5 + + self:E( { _EVENTMETA[Event.id].Text, Event, Event.IniDCSUnitName, Event.TgtDCSUnitName, PriorityOrder } ) + + for EventPriority = PriorityBegin, PriorityEnd, PriorityOrder do + + if self.Events[Event.id][EventPriority] then + + -- Okay, we got the event from DCS. Now loop the SORTED self.EventSorted[] table for the received Event.id, and for each EventData registered, check if a function needs to be called. + for EventClass, EventData in pairs( self.Events[Event.id][EventPriority] ) do + + Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) + + -- If the EventData is for a UNIT, the call directly the EventClass EventFunction for that UNIT. + if Event.IniDCSUnitName and EventData.IniUnit and EventData.IniUnit[Event.IniDCSUnitName] then + + -- First test if a EventFunction is Set, otherwise search for the default function + if EventData.IniUnit[Event.IniDCSUnitName].EventFunction then + + self:E( { "Calling EventFunction for UNIT ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName, EventPriority } ) + + local Result, Value = xpcall( + function() + return EventData.IniUnit[Event.IniDCSUnitName].EventFunction( EventClass, Event ) + end, ErrorHandler ) + + else + + -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. + local EventFunction = EventClass[ _EVENTMETA[Event.id].Event ] + if EventFunction and type( EventFunction ) == "function" then + + -- Now call the default event function. + self:E( { "Calling " .. _EVENTMETA[Event.id].Event .. " for Class ", EventClass:GetClassNameAndID(), EventPriority } ) + + local Result, Value = xpcall( + function() + return EventFunction( EventClass, Event ) + end, ErrorHandler ) + end + + end + + else + + -- If the EventData is for a GROUP, the call directly the EventClass EventFunction for the UNIT in that GROUP. + if Event.IniDCSUnitName and Event.IniDCSGroupName and Event.IniGroupName and EventData.IniGroup and EventData.IniGroup[Event.IniGroupName] then + + -- First test if a EventFunction is Set, otherwise search for the default function + if EventData.IniGroup[Event.IniGroupName].EventFunction then + + self:E( { "Calling EventFunction for GROUP ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName, EventPriority } ) + + local Result, Value = xpcall( + function() + return EventData.IniGroup[Event.IniGroupName].EventFunction( EventClass, Event ) + end, ErrorHandler ) + + else + + -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. + local EventFunction = EventClass[ _EVENTMETA[Event.id].Event ] + if EventFunction and type( EventFunction ) == "function" then + + -- Now call the default event function. + self:E( { "Calling " .. _EVENTMETA[Event.id].Event .. " for GROUP ", EventClass:GetClassNameAndID(), EventPriority } ) + + local Result, Value = xpcall( + function() + return EventFunction( EventClass, Event ) + end, ErrorHandler ) + end + + end + + else + + -- If the EventData is not bound to a specific unit, then call the EventClass EventFunction. + -- Note that here the EventFunction will need to implement and determine the logic for the relevant source- or target unit, or weapon. + if Event.IniDCSUnit and not EventData.IniUnit then + + if EventClass == EventData.EventClass then + + -- First test if a EventFunction is Set, otherwise search for the default function + if EventData.EventFunction then + + -- There is an EventFunction defined, so call the EventFunction. + self:E( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID(), EventPriority } ) + + local Result, Value = xpcall( + function() + return EventData.EventFunction( EventClass, Event ) + end, ErrorHandler ) + else + + -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. + local EventFunction = EventClass[ _EVENTMETA[Event.id].Event ] + if EventFunction and type( EventFunction ) == "function" then + + -- Now call the default event function. + self:E( { "Calling " .. _EVENTMETA[Event.id].Event .. " for Class ", EventClass:GetClassNameAndID(), EventPriority } ) + + local Result, Value = xpcall( + function() + return EventFunction( EventClass, Event ) + end, ErrorHandler ) + end + end + end + end + end + end + end + end + end + else + self:E( { _EVENTMETA[Event.id].Text, Event } ) + end +end + +--- The EVENTHANDLER structure +-- @type EVENTHANDLER +-- @extends Core.Base#BASE +EVENTHANDLER = { + ClassName = "EVENTHANDLER", + ClassID = 0, +} + +--- The EVENTHANDLER constructor +-- @param #EVENTHANDLER self +-- @return #EVENTHANDLER +function EVENTHANDLER:New() + self = BASE:Inherit( self, BASE:New() ) -- #EVENTHANDLER + return self +end +--- This module contains the MENU classes. +-- +-- === +-- +-- DCS Menus can be managed using the MENU classes. +-- The advantage of using MENU classes is that it hides the complexity of dealing with menu management in more advanced scanerios where you need to +-- set menus and later remove them, and later set them again. You'll find while using use normal DCS scripting functions, that setting and removing +-- menus is not a easy feat if you have complex menu hierarchies defined. +-- Using the MOOSE menu classes, the removal and refreshing of menus are nicely being handled within these classes, and becomes much more easy. +-- On top, MOOSE implements **variable parameter** passing for command menus. +-- +-- There are basically two different MENU class types that you need to use: +-- +-- ### To manage **main menus**, the classes begin with **MENU_**: +-- +-- * @{Menu#MENU_MISSION}: Manages main menus for whole mission file. +-- * @{Menu#MENU_COALITION}: Manages main menus for whole coalition. +-- * @{Menu#MENU_GROUP}: Manages main menus for GROUPs. +-- * @{Menu#MENU_CLIENT}: Manages main menus for CLIENTs. This manages menus for units with the skill level "Client". +-- +-- ### To manage **command menus**, which are menus that allow the player to issue **functions**, the classes begin with **MENU_COMMAND_**: +-- +-- * @{Menu#MENU_MISSION_COMMAND}: Manages command menus for whole mission file. +-- * @{Menu#MENU_COALITION_COMMAND}: Manages command menus for whole coalition. +-- * @{Menu#MENU_GROUP_COMMAND}: Manages command menus for GROUPs. +-- * @{Menu#MENU_CLIENT_COMMAND}: Manages command menus for CLIENTs. This manages menus for units with the skill level "Client". +-- +-- === +-- +-- The above menus classes **are derived** from 2 main **abstract** classes defined within the MOOSE framework (so don't use these): +-- +-- 1) MENU_ BASE abstract base classes (don't use them) +-- ==================================================== +-- The underlying base menu classes are **NOT** to be used within your missions. +-- These are simply abstract base classes defining a couple of fields that are used by the +-- derived MENU_ classes to manage menus. +-- +-- 1.1) @{#MENU_BASE} class, extends @{Base#BASE} +-- -------------------------------------------------- +-- The @{#MENU_BASE} class defines the main MENU class where other MENU classes are derived from. +-- +-- 1.2) @{#MENU_COMMAND_BASE} class, extends @{Base#BASE} +-- ---------------------------------------------------------- +-- The @{#MENU_COMMAND_BASE} class defines the main MENU class where other MENU COMMAND_ classes are derived from, in order to set commands. +-- +-- === +-- +-- **The next menus define the MENU classes that you can use within your missions.** +-- +-- 2) MENU MISSION classes +-- ====================== +-- The underlying classes manage the menus for a complete mission file. +-- +-- 2.1) @{#MENU_MISSION} class, extends @{Menu#MENU_BASE} +-- --------------------------------------------------------- +-- The @{Menu#MENU_MISSION} class manages the main menus for a complete mission. +-- You can add menus with the @{#MENU_MISSION.New} method, which constructs a MENU_MISSION object and returns you the object reference. +-- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_MISSION.Remove}. +-- +-- 2.2) @{#MENU_MISSION_COMMAND} class, extends @{Menu#MENU_COMMAND_BASE} +-- ------------------------------------------------------------------------- +-- The @{Menu#MENU_MISSION_COMMAND} class manages the command menus for a complete mission, which allow players to execute functions during mission execution. +-- You can add menus with the @{#MENU_MISSION_COMMAND.New} method, which constructs a MENU_MISSION_COMMAND object and returns you the object reference. +-- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_MISSION_COMMAND.Remove}. +-- +-- === +-- +-- 3) MENU COALITION classes +-- ========================= +-- The underlying classes manage the menus for whole coalitions. +-- +-- 3.1) @{#MENU_COALITION} class, extends @{Menu#MENU_BASE} +-- ------------------------------------------------------------ +-- The @{Menu#MENU_COALITION} class manages the main menus for coalitions. +-- You can add menus with the @{#MENU_COALITION.New} method, which constructs a MENU_COALITION object and returns you the object reference. +-- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_COALITION.Remove}. +-- +-- 3.2) @{Menu#MENU_COALITION_COMMAND} class, extends @{Menu#MENU_COMMAND_BASE} +-- ---------------------------------------------------------------------------- +-- The @{Menu#MENU_COALITION_COMMAND} class manages the command menus for coalitions, which allow players to execute functions during mission execution. +-- You can add menus with the @{#MENU_COALITION_COMMAND.New} method, which constructs a MENU_COALITION_COMMAND object and returns you the object reference. +-- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_COALITION_COMMAND.Remove}. +-- +-- === +-- +-- 4) MENU GROUP classes +-- ===================== +-- The underlying classes manage the menus for groups. Note that groups can be inactive, alive or can be destroyed. +-- +-- 4.1) @{Menu#MENU_GROUP} class, extends @{Menu#MENU_BASE} +-- -------------------------------------------------------- +-- The @{Menu#MENU_GROUP} class manages the main menus for coalitions. +-- You can add menus with the @{#MENU_GROUP.New} method, which constructs a MENU_GROUP object and returns you the object reference. +-- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_GROUP.Remove}. +-- +-- 4.2) @{Menu#MENU_GROUP_COMMAND} class, extends @{Menu#MENU_COMMAND_BASE} +-- ------------------------------------------------------------------------ +-- The @{Menu#MENU_GROUP_COMMAND} class manages the command menus for coalitions, which allow players to execute functions during mission execution. +-- You can add menus with the @{#MENU_GROUP_COMMAND.New} method, which constructs a MENU_GROUP_COMMAND object and returns you the object reference. +-- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_GROUP_COMMAND.Remove}. +-- +-- === +-- +-- 5) MENU CLIENT classes +-- ====================== +-- The underlying classes manage the menus for units with skill level client or player. +-- +-- 5.1) @{Menu#MENU_CLIENT} class, extends @{Menu#MENU_BASE} +-- --------------------------------------------------------- +-- The @{Menu#MENU_CLIENT} class manages the main menus for coalitions. +-- You can add menus with the @{#MENU_CLIENT.New} method, which constructs a MENU_CLIENT object and returns you the object reference. +-- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_CLIENT.Remove}. +-- +-- 5.2) @{Menu#MENU_CLIENT_COMMAND} class, extends @{Menu#MENU_COMMAND_BASE} +-- ------------------------------------------------------------------------- +-- The @{Menu#MENU_CLIENT_COMMAND} class manages the command menus for coalitions, which allow players to execute functions during mission execution. +-- You can add menus with the @{#MENU_CLIENT_COMMAND.New} method, which constructs a MENU_CLIENT_COMMAND object and returns you the object reference. +-- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_CLIENT_COMMAND.Remove}. +-- +-- === +-- +-- ### Contributions: - +-- ### Authors: FlightControl : Design & Programming +-- +-- @module Menu + + +do -- MENU_BASE + + --- The MENU_BASE class + -- @type MENU_BASE + -- @extends Base#BASE + MENU_BASE = { + ClassName = "MENU_BASE", + MenuPath = nil, + MenuText = "", + MenuParentPath = nil + } + + --- Consructor + function MENU_BASE:New( MenuText, ParentMenu ) + + local MenuParentPath = {} + if ParentMenu ~= nil then + MenuParentPath = ParentMenu.MenuPath + end + + local self = BASE:Inherit( self, BASE:New() ) + + self.MenuPath = nil + self.MenuText = MenuText + self.MenuParentPath = MenuParentPath + + return self + end + +end + +do -- MENU_COMMAND_BASE + + --- The MENU_COMMAND_BASE class + -- @type MENU_COMMAND_BASE + -- @field #function MenuCallHandler + -- @extends Menu#MENU_BASE + MENU_COMMAND_BASE = { + ClassName = "MENU_COMMAND_BASE", + CommandMenuFunction = nil, + CommandMenuArgument = nil, + MenuCallHandler = nil, + } + + --- Constructor + function MENU_COMMAND_BASE:New( MenuText, ParentMenu, CommandMenuFunction, CommandMenuArguments ) + + local self = BASE:Inherit( self, MENU_BASE:New( MenuText, ParentMenu ) ) + + self.CommandMenuFunction = CommandMenuFunction + self.MenuCallHandler = function( CommandMenuArguments ) + self.CommandMenuFunction( unpack( CommandMenuArguments ) ) + end + + return self + end + +end + + +do -- MENU_MISSION + + --- The MENU_MISSION class + -- @type MENU_MISSION + -- @extends Menu#MENU_BASE + MENU_MISSION = { + ClassName = "MENU_MISSION" + } + + --- MENU_MISSION constructor. Creates a new MENU_MISSION object and creates the menu for a complete mission file. + -- @param #MENU_MISSION self + -- @param #string MenuText The text for the menu. + -- @param #table ParentMenu The parent menu. This parameter can be ignored if you want the menu to be located at the perent menu of DCS world (under F10 other). + -- @return #MENU_MISSION self + function MENU_MISSION:New( MenuText, ParentMenu ) + + local self = BASE:Inherit( self, MENU_BASE:New( MenuText, ParentMenu ) ) + + self:F( { MenuText, ParentMenu } ) + + self.MenuText = MenuText + self.ParentMenu = ParentMenu + + self.Menus = {} + + self:T( { MenuText } ) + + self.MenuPath = missionCommands.addSubMenu( MenuText, self.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_MISSION. Note that the main menu is kept! + -- @param #MENU_MISSION self + -- @return #MENU_MISSION self + function MENU_MISSION:RemoveSubMenus() + self:F( self.MenuPath ) + + for MenuID, Menu in pairs( self.Menus ) do + Menu:Remove() + end + + end + + --- Removes the main menu and the sub menus recursively of this MENU_MISSION. + -- @param #MENU_MISSION self + -- @return #nil + function MENU_MISSION:Remove() + self:F( self.MenuPath ) + + self:RemoveSubMenus() + missionCommands.removeItem( self.MenuPath ) + if self.ParentMenu then + self.ParentMenu.Menus[self.MenuPath] = nil + end + + return nil + end + +end + +do -- MENU_MISSION_COMMAND + + --- The MENU_MISSION_COMMAND class + -- @type MENU_MISSION_COMMAND + -- @extends Menu#MENU_COMMAND_BASE + MENU_MISSION_COMMAND = { + ClassName = "MENU_MISSION_COMMAND" + } + + --- MENU_MISSION constructor. Creates a new radio command item for a complete mission file, which can invoke a function with parameters. + -- @param #MENU_MISSION_COMMAND self + -- @param #string MenuText The text for the menu. + -- @param Menu#MENU_MISSION ParentMenu The parent menu. + -- @param CommandMenuFunction A function that is called when the menu key is pressed. + -- @param CommandMenuArgument An argument for the function. There can only be ONE argument given. So multiple arguments must be wrapped into a table. See the below example how to do this. + -- @return #MENU_MISSION_COMMAND self + function MENU_MISSION_COMMAND:New( MenuText, ParentMenu, CommandMenuFunction, ... ) + + local self = BASE:Inherit( self, MENU_COMMAND_BASE:New( MenuText, ParentMenu, CommandMenuFunction, arg ) ) + + self.MenuText = MenuText + self.ParentMenu = ParentMenu + + self:T( { MenuText, CommandMenuFunction, arg } ) + + + self.MenuPath = missionCommands.addCommand( MenuText, self.MenuParentPath, self.MenuCallHandler, arg ) + + ParentMenu.Menus[self.MenuPath] = self + + return self + end + + --- Removes a radio command item for a coalition + -- @param #MENU_MISSION_COMMAND self + -- @return #nil + function MENU_MISSION_COMMAND:Remove() + self:F( self.MenuPath ) + + missionCommands.removeItem( self.MenuPath ) + if self.ParentMenu then + self.ParentMenu.Menus[self.MenuPath] = nil + end + return nil + end + +end + + + +do -- MENU_COALITION + + --- The MENU_COALITION class + -- @type MENU_COALITION + -- @extends Menu#MENU_BASE + -- @usage + -- -- This demo creates a menu structure for the planes within the red coalition. + -- -- To test, join the planes, then look at the other radio menus (Option F10). + -- -- Then switch planes and check if the menu is still there. + -- + -- local Plane1 = CLIENT:FindByName( "Plane 1" ) + -- local Plane2 = CLIENT:FindByName( "Plane 2" ) + -- + -- + -- -- This would create a menu for the red coalition under the main DCS "Others" menu. + -- local MenuCoalitionRed = MENU_COALITION:New( coalition.side.RED, "Manage Menus" ) + -- + -- + -- local function ShowStatus( StatusText, Coalition ) + -- + -- MESSAGE:New( Coalition, 15 ):ToRed() + -- Plane1:Message( StatusText, 15 ) + -- Plane2:Message( StatusText, 15 ) + -- end + -- + -- local MenuStatus -- Menu#MENU_COALITION + -- local MenuStatusShow -- Menu#MENU_COALITION_COMMAND + -- + -- local function RemoveStatusMenu() + -- MenuStatus:Remove() + -- end + -- + -- local function AddStatusMenu() + -- + -- -- This would create a menu for the red coalition under the MenuCoalitionRed menu object. + -- MenuStatus = MENU_COALITION:New( coalition.side.RED, "Status for Planes" ) + -- MenuStatusShow = MENU_COALITION_COMMAND:New( coalition.side.RED, "Show Status", MenuStatus, ShowStatus, "Status of planes is ok!", "Message to Red Coalition" ) + -- end + -- + -- local MenuAdd = MENU_COALITION_COMMAND:New( coalition.side.RED, "Add Status Menu", MenuCoalitionRed, AddStatusMenu ) + -- local MenuRemove = MENU_COALITION_COMMAND:New( coalition.side.RED, "Remove Status Menu", MenuCoalitionRed, RemoveStatusMenu ) + MENU_COALITION = { + ClassName = "MENU_COALITION" + } + + --- MENU_COALITION constructor. Creates a new MENU_COALITION object and creates the menu for a complete coalition. + -- @param #MENU_COALITION self + -- @param Dcs.DCSCoalition#coalition.side Coalition The coalition owning the menu. + -- @param #string MenuText The text for the menu. + -- @param #table ParentMenu The parent menu. This parameter can be ignored if you want the menu to be located at the perent menu of DCS world (under F10 other). + -- @return #MENU_COALITION self + function MENU_COALITION:New( Coalition, MenuText, ParentMenu ) + + local self = BASE:Inherit( self, MENU_BASE:New( MenuText, ParentMenu ) ) + + self:F( { Coalition, MenuText, ParentMenu } ) + + self.Coalition = Coalition + self.MenuText = MenuText + self.ParentMenu = ParentMenu + + self.Menus = {} + + self:T( { MenuText } ) + + self.MenuPath = missionCommands.addSubMenuForCoalition( Coalition, MenuText, self.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. Note that the main menu is kept! + -- @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 main menu and the sub menus recursively of this MENU_COALITION. + -- @param #MENU_COALITION self + -- @return #nil + function MENU_COALITION:Remove() + self:F( self.MenuPath ) + + self:RemoveSubMenus() + missionCommands.removeItemForCoalition( self.Coalition, self.MenuPath ) + if self.ParentMenu then + self.ParentMenu.Menus[self.MenuPath] = nil + end + + return nil + end + +end + +do -- MENU_COALITION_COMMAND + + --- The MENU_COALITION_COMMAND class + -- @type MENU_COALITION_COMMAND + -- @extends Menu#MENU_COMMAND_BASE + MENU_COALITION_COMMAND = { + ClassName = "MENU_COALITION_COMMAND" + } + + --- MENU_COALITION constructor. Creates a new radio command item for a coalition, which can invoke a function with parameters. + -- @param #MENU_COALITION_COMMAND self + -- @param Dcs.DCSCoalition#coalition.side Coalition The coalition owning the menu. + -- @param #string MenuText The text for the menu. + -- @param Menu#MENU_COALITION ParentMenu The parent menu. + -- @param CommandMenuFunction A function that is called when the menu key is pressed. + -- @param CommandMenuArgument An argument for the function. There can only be ONE argument given. So multiple arguments must be wrapped into a table. See the below example how to do this. + -- @return #MENU_COALITION_COMMAND self + function MENU_COALITION_COMMAND:New( Coalition, MenuText, ParentMenu, CommandMenuFunction, ... ) + + local self = BASE:Inherit( self, MENU_COMMAND_BASE:New( MenuText, ParentMenu, CommandMenuFunction, arg ) ) + + self.MenuCoalition = Coalition + self.MenuText = MenuText + self.ParentMenu = ParentMenu + + self:T( { MenuText, CommandMenuFunction, arg } ) + + + self.MenuPath = missionCommands.addCommandForCoalition( self.MenuCoalition, MenuText, self.MenuParentPath, self.MenuCallHandler, arg ) + + ParentMenu.Menus[self.MenuPath] = self + + return self + end + + --- Removes a radio command item for a coalition + -- @param #MENU_COALITION_COMMAND self + -- @return #nil + function MENU_COALITION_COMMAND:Remove() + self:F( self.MenuPath ) + + missionCommands.removeItemForCoalition( self.MenuCoalition, self.MenuPath ) + if self.ParentMenu then + self.ParentMenu.Menus[self.MenuPath] = nil + end + return nil + end + +end + +do -- MENU_CLIENT + + -- 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 = {} + + --- MENU_COALITION constructor. Creates a new radio command item for a coalition, which can invoke a function with parameters. + -- @type MENU_CLIENT + -- @extends Menu#MENU_BASE + -- @usage + -- -- This demo creates a menu structure for the two clients of planes. + -- -- Each client will receive a different menu structure. + -- -- To test, join the planes, then look at the other radio menus (Option F10). + -- -- Then switch planes and check if the menu is still there. + -- -- And play with the Add and Remove menu options. + -- + -- -- Note that in multi player, this will only work after the DCS clients bug is solved. + -- + -- local function ShowStatus( PlaneClient, StatusText, Coalition ) + -- + -- MESSAGE:New( Coalition, 15 ):ToRed() + -- PlaneClient:Message( StatusText, 15 ) + -- end + -- + -- local MenuStatus = {} + -- + -- local function RemoveStatusMenu( MenuClient ) + -- local MenuClientName = MenuClient:GetName() + -- MenuStatus[MenuClientName]:Remove() + -- end + -- + -- --- @param Wrapper.Client#CLIENT MenuClient + -- local function AddStatusMenu( MenuClient ) + -- local MenuClientName = MenuClient:GetName() + -- -- This would create a menu for the red coalition under the MenuCoalitionRed menu object. + -- MenuStatus[MenuClientName] = MENU_CLIENT:New( MenuClient, "Status for Planes" ) + -- MENU_CLIENT_COMMAND:New( MenuClient, "Show Status", MenuStatus[MenuClientName], ShowStatus, MenuClient, "Status of planes is ok!", "Message to Red Coalition" ) + -- end + -- + -- SCHEDULER:New( nil, + -- function() + -- local PlaneClient = CLIENT:FindByName( "Plane 1" ) + -- if PlaneClient and PlaneClient:IsAlive() then + -- local MenuManage = MENU_CLIENT:New( PlaneClient, "Manage Menus" ) + -- MENU_CLIENT_COMMAND:New( PlaneClient, "Add Status Menu Plane 1", MenuManage, AddStatusMenu, PlaneClient ) + -- MENU_CLIENT_COMMAND:New( PlaneClient, "Remove Status Menu Plane 1", MenuManage, RemoveStatusMenu, PlaneClient ) + -- end + -- end, {}, 10, 10 ) + -- + -- SCHEDULER:New( nil, + -- function() + -- local PlaneClient = CLIENT:FindByName( "Plane 2" ) + -- if PlaneClient and PlaneClient:IsAlive() then + -- local MenuManage = MENU_CLIENT:New( PlaneClient, "Manage Menus" ) + -- MENU_CLIENT_COMMAND:New( PlaneClient, "Add Status Menu Plane 2", MenuManage, AddStatusMenu, PlaneClient ) + -- MENU_CLIENT_COMMAND:New( PlaneClient, "Remove Status Menu Plane 2", MenuManage, RemoveStatusMenu, PlaneClient ) + -- end + -- end, {}, 10, 10 ) + MENU_CLIENT = { + ClassName = "MENU_CLIENT" + } + + --- MENU_CLIENT constructor. Creates a new radio menu item for a client. + -- @param #MENU_CLIENT self + -- @param Wrapper.Client#CLIENT Client 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( Client, MenuText, ParentMenu ) + + -- Arrange meta tables + local MenuParentPath = {} + if ParentMenu ~= nil then + MenuParentPath = ParentMenu.MenuPath + end + + local self = BASE:Inherit( self, MENU_BASE:New( MenuText, MenuParentPath ) ) + self:F( { Client, MenuText, ParentMenu } ) + + self.MenuClient = Client + self.MenuClientGroupID = Client: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( { Client: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( { Client: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 #nil + 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_COMMAND + MENU_CLIENT_COMMAND = { + ClassName = "MENU_CLIENT_COMMAND" + } + + --- MENU_CLIENT_COMMAND constructor. Creates a new radio command item for a client, which can invoke a function with parameters. + -- @param #MENU_CLIENT_COMMAND self + -- @param Wrapper.Client#CLIENT Client The Client owning the menu. + -- @param #string MenuText The text for the menu. + -- @param #MENU_BASE ParentMenu The parent menu. + -- @param CommandMenuFunction A function that is called when the menu key is pressed. + -- @return Menu#MENU_CLIENT_COMMAND self + function MENU_CLIENT_COMMAND:New( Client, MenuText, ParentMenu, CommandMenuFunction, ... ) + + -- Arrange meta tables + + local MenuParentPath = {} + if ParentMenu ~= nil then + MenuParentPath = ParentMenu.MenuPath + end + + local self = BASE:Inherit( self, MENU_COMMAND_BASE:New( MenuText, MenuParentPath, CommandMenuFunction, arg ) ) -- Menu#MENU_CLIENT_COMMAND + + self.MenuClient = Client + self.MenuClientGroupID = Client: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( { Client:GetClientGroupName(), MenuPath[table.concat(MenuParentPath)], MenuParentPath, MenuText, CommandMenuFunction, arg } ) + + 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, self.MenuCallHandler, arg ) + MenuPath[MenuPathID] = self.MenuPath + + if ParentMenu and ParentMenu.Menus then + ParentMenu.Menus[self.MenuPath] = self + end + + return self + end + + --- Removes a menu structure for a client. + -- @param #MENU_CLIENT_COMMAND self + -- @return #nil + 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 +end + +--- MENU_GROUP + +do + -- This local variable is used to cache the menus registered under groups. + -- Menus don't dissapear when groups for players are destroyed and restarted. + -- So every menu for a client created must be tracked so that program logic accidentally does not create. + -- the same menus twice during initialization logic. + -- These menu classes are handling this logic with this variable. + local _MENUGROUPS = {} + + --- The MENU_GROUP class + -- @type MENU_GROUP + -- @extends Menu#MENU_BASE + -- @usage + -- -- This demo creates a menu structure for the two groups of planes. + -- -- Each group will receive a different menu structure. + -- -- To test, join the planes, then look at the other radio menus (Option F10). + -- -- Then switch planes and check if the menu is still there. + -- -- And play with the Add and Remove menu options. + -- + -- -- Note that in multi player, this will only work after the DCS groups bug is solved. + -- + -- local function ShowStatus( PlaneGroup, StatusText, Coalition ) + -- + -- MESSAGE:New( Coalition, 15 ):ToRed() + -- PlaneGroup:Message( StatusText, 15 ) + -- end + -- + -- local MenuStatus = {} + -- + -- local function RemoveStatusMenu( MenuGroup ) + -- local MenuGroupName = MenuGroup:GetName() + -- MenuStatus[MenuGroupName]:Remove() + -- end + -- + -- --- @param Wrapper.Group#GROUP MenuGroup + -- local function AddStatusMenu( MenuGroup ) + -- local MenuGroupName = MenuGroup:GetName() + -- -- This would create a menu for the red coalition under the MenuCoalitionRed menu object. + -- MenuStatus[MenuGroupName] = MENU_GROUP:New( MenuGroup, "Status for Planes" ) + -- MENU_GROUP_COMMAND:New( MenuGroup, "Show Status", MenuStatus[MenuGroupName], ShowStatus, MenuGroup, "Status of planes is ok!", "Message to Red Coalition" ) + -- end + -- + -- SCHEDULER:New( nil, + -- function() + -- local PlaneGroup = GROUP:FindByName( "Plane 1" ) + -- if PlaneGroup and PlaneGroup:IsAlive() then + -- local MenuManage = MENU_GROUP:New( PlaneGroup, "Manage Menus" ) + -- MENU_GROUP_COMMAND:New( PlaneGroup, "Add Status Menu Plane 1", MenuManage, AddStatusMenu, PlaneGroup ) + -- MENU_GROUP_COMMAND:New( PlaneGroup, "Remove Status Menu Plane 1", MenuManage, RemoveStatusMenu, PlaneGroup ) + -- end + -- end, {}, 10, 10 ) + -- + -- SCHEDULER:New( nil, + -- function() + -- local PlaneGroup = GROUP:FindByName( "Plane 2" ) + -- if PlaneGroup and PlaneGroup:IsAlive() then + -- local MenuManage = MENU_GROUP:New( PlaneGroup, "Manage Menus" ) + -- MENU_GROUP_COMMAND:New( PlaneGroup, "Add Status Menu Plane 2", MenuManage, AddStatusMenu, PlaneGroup ) + -- MENU_GROUP_COMMAND:New( PlaneGroup, "Remove Status Menu Plane 2", MenuManage, RemoveStatusMenu, PlaneGroup ) + -- end + -- end, {}, 10, 10 ) + -- + MENU_GROUP = { + ClassName = "MENU_GROUP" + } + + --- MENU_GROUP constructor. Creates a new radio menu item for a group. + -- @param #MENU_GROUP self + -- @param Wrapper.Group#GROUP MenuGroup The Group owning the menu. + -- @param #string MenuText The text for the menu. + -- @param #table ParentMenu The parent menu. + -- @return #MENU_GROUP self + function MENU_GROUP:New( MenuGroup, MenuText, ParentMenu ) + + -- Determine if the menu was not already created and already visible at the group. + -- If it is visible, then return the cached self, otherwise, create self and cache it. + + MenuGroup._Menus = MenuGroup._Menus or {} + local Path = ( ParentMenu and ( table.concat( ParentMenu.MenuPath or {}, "@" ) .. "@" .. MenuText ) ) or MenuText + if MenuGroup._Menus[Path] then + self = MenuGroup._Menus[Path] + else + self = BASE:Inherit( self, MENU_BASE:New( MenuText, ParentMenu ) ) + MenuGroup._Menus[Path] = self + + self.Menus = {} + + self.MenuGroup = MenuGroup + self.Path = Path + self.MenuGroupID = MenuGroup:GetID() + self.MenuText = MenuText + self.ParentMenu = ParentMenu + + self:T( { "Adding Menu ", MenuText, self.MenuParentPath } ) + self.MenuPath = missionCommands.addSubMenuForGroup( self.MenuGroupID, MenuText, self.MenuParentPath ) + + if ParentMenu and ParentMenu.Menus then + ParentMenu.Menus[self.MenuPath] = self + end + end + + --self:F( { MenuGroup:GetName(), MenuText, ParentMenu.MenuPath } ) + + return self + end + + --- Removes the sub menus recursively of this MENU_GROUP. + -- @param #MENU_GROUP self + -- @return #MENU_GROUP self + function MENU_GROUP:RemoveSubMenus() + self:F( self.MenuPath ) + + for MenuID, Menu in pairs( self.Menus ) do + Menu:Remove() + end + + end + + --- Removes the main menu and sub menus recursively of this MENU_GROUP. + -- @param #MENU_GROUP self + -- @return #nil + function MENU_GROUP:Remove() + self:F( { self.MenuGroupID, self.MenuPath } ) + + self:RemoveSubMenus() + + if self.MenuGroup._Menus[self.Path] then + self = self.MenuGroup._Menus[self.Path] + + missionCommands.removeItemForGroup( self.MenuGroupID, self.MenuPath ) + if self.ParentMenu then + self.ParentMenu.Menus[self.MenuPath] = nil + end + self:E( self.MenuGroup._Menus[self.Path] ) + self.MenuGroup._Menus[self.Path] = nil + self = nil + end + return nil + end + + + --- The MENU_GROUP_COMMAND class + -- @type MENU_GROUP_COMMAND + -- @extends Menu#MENU_BASE + MENU_GROUP_COMMAND = { + ClassName = "MENU_GROUP_COMMAND" + } + + --- Creates a new radio command item for a group + -- @param #MENU_GROUP_COMMAND self + -- @param Wrapper.Group#GROUP MenuGroup The Group owning the menu. + -- @param MenuText The text for the menu. + -- @param ParentMenu The parent menu. + -- @param CommandMenuFunction A function that is called when the menu key is pressed. + -- @param CommandMenuArgument An argument for the function. + -- @return Menu#MENU_GROUP_COMMAND self + function MENU_GROUP_COMMAND:New( MenuGroup, MenuText, ParentMenu, CommandMenuFunction, ... ) + + MenuGroup._Menus = MenuGroup._Menus or {} + local Path = ( ParentMenu and ( table.concat( ParentMenu.MenuPath or {}, "@" ) .. "@" .. MenuText ) ) or MenuText + if MenuGroup._Menus[Path] then + self = MenuGroup._Menus[Path] + else + self = BASE:Inherit( self, MENU_COMMAND_BASE:New( MenuText, ParentMenu, CommandMenuFunction, arg ) ) + MenuGroup._Menus[Path] = self + + self.Path = Path + self.MenuGroup = MenuGroup + self.MenuGroupID = MenuGroup:GetID() + self.MenuText = MenuText + self.ParentMenu = ParentMenu + + self:T( { "Adding Command Menu ", MenuText, self.MenuParentPath } ) + self.MenuPath = missionCommands.addCommandForGroup( self.MenuGroupID, MenuText, self.MenuParentPath, self.MenuCallHandler, arg ) + + if ParentMenu and ParentMenu.Menus then + ParentMenu.Menus[self.MenuPath] = self + end + end + + --self:F( { MenuGroup:GetName(), MenuText, ParentMenu.MenuPath } ) + + return self + end + + --- Removes a menu structure for a group. + -- @param #MENU_GROUP_COMMAND self + -- @return #nil + function MENU_GROUP_COMMAND:Remove() + self:F( { self.MenuGroupID, self.MenuPath } ) + + if self.MenuGroup._Menus[self.Path] then + self = self.MenuGroup._Menus[self.Path] + + missionCommands.removeItemForGroup( self.MenuGroupID, self.MenuPath ) + self.ParentMenu.Menus[self.MenuPath] = nil + self:E( self.MenuGroup._Menus[self.Path] ) + self.MenuGroup._Menus[self.Path] = nil + self = nil + end + + return nil + end + +end + +--- This core 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 defines by a zone around a @{Unit#UNIT} with a radius. +-- * @{Zone#ZONE_GROUP}: The ZONE_GROUP class defines by a zone around a @{Group#GROUP} with a radius. +-- * @{Zone#ZONE_POLYGON}: The ZONE_POLYGON class defines by a sequence of @{Group#GROUP} waypoints within the Mission Editor, forming a polygon. +-- +-- === +-- +-- # 1) @{Zone#ZONE_BASE} class, extends @{Base#BASE} +-- +-- This class is an abstract BASE class for derived classes, and is not meant to be instantiated. +-- +-- ## 1.1) Each zone has a name: +-- +-- * @{#ZONE_BASE.GetName}(): Returns the name of the zone. +-- +-- ## 1.2) Each zone implements two polymorphic functions defined in @{Zone#ZONE_BASE}: +-- +-- * @{#ZONE_BASE.IsVec2InZone}(): Returns if a Vec2 is within the zone. +-- * @{#ZONE_BASE.IsVec3InZone}(): Returns if a Vec3 is within the zone. +-- +-- ## 1.3) A zone has a probability factor that can be set to randomize a selection between zones: +-- +-- * @{#ZONE_BASE.SetRandomizeProbability}(): Set the randomization probability of a zone to be selected, taking a value between 0 and 1 ( 0 = 0%, 1 = 100% ) +-- * @{#ZONE_BASE.GetRandomizeProbability}(): Get the randomization probability of a zone to be selected, passing a value between 0 and 1 ( 0 = 0%, 1 = 100% ) +-- * @{#ZONE_BASE.GetZoneMaybe}(): Get the zone taking into account the randomization probability. nil is returned if this zone is not a candidate. +-- +-- ## 1.4) A zone manages Vectors: +-- +-- * @{#ZONE_BASE.GetVec2}(): Returns the @{DCSTypes#Vec2} coordinate of the zone. +-- * @{#ZONE_BASE.GetRandomVec2}(): Define a random @{DCSTypes#Vec2} within the zone. +-- +-- ## 1.5) A zone has a bounding square: +-- +-- * @{#ZONE_BASE.GetBoundingSquare}(): Get the outer most bounding square of the zone. +-- +-- ## 1.6) A zone can be marked: +-- +-- * @{#ZONE_BASE.SmokeZone}(): Smokes the zone boundaries in a color. +-- * @{#ZONE_BASE.FlareZone}(): Flares the zone boundaries in a color. +-- +-- === +-- +-- # 2) @{Zone#ZONE_RADIUS} class, extends @{Zone#ZONE_BASE} +-- +-- The ZONE_RADIUS class defined by a zone name, a location and a radius. +-- This class implements the inherited functions from Core.Zone#ZONE_BASE taking into account the own zone format and properties. +-- +-- ## 2.1) @{Zone#ZONE_RADIUS} constructor +-- +-- * @{#ZONE_RADIUS.New}(): Constructor. +-- +-- ## 2.2) Manage the radius of the zone +-- +-- * @{#ZONE_RADIUS.SetRadius}(): Sets the radius of the zone. +-- * @{#ZONE_RADIUS.GetRadius}(): Returns the radius of the zone. +-- +-- ## 2.3) Manage the location of the zone +-- +-- * @{#ZONE_RADIUS.SetVec2}(): Sets the @{DCSTypes#Vec2} of the zone. +-- * @{#ZONE_RADIUS.GetVec2}(): Returns the @{DCSTypes#Vec2} of the zone. +-- * @{#ZONE_RADIUS.GetVec3}(): Returns the @{DCSTypes#Vec3} of the zone, taking an additional height parameter. +-- +-- ## 2.4) Zone point randomization +-- +-- Various functions exist to find random points within the zone. +-- +-- * @{#ZONE_RADIUS.GetRandomVec2}(): Gets a random 2D point in the zone. +-- * @{#ZONE_RADIUS.GetRandomPointVec2}(): Gets a @{Point#POINT_VEC2} object representing a random 2D point in the zone. +-- * @{#ZONE_RADIUS.GetRandomPointVec3}(): Gets a @{Point#POINT_VEC3} object representing a random 3D point in the zone. Note that the height of the point is at landheight. +-- +-- === +-- +-- # 3) @{Zone#ZONE} class, extends @{Zone#ZONE_RADIUS} +-- +-- The ZONE class, defined by the zone name as defined within the Mission Editor. +-- This class implements the inherited functions from {Core.Zone#ZONE_RADIUS} taking into account the own zone format and properties. +-- +-- === +-- +-- # 4) @{Zone#ZONE_UNIT} class, extends @{Zone#ZONE_RADIUS} +-- +-- The ZONE_UNIT class defined by a zone around a @{Unit#UNIT} with a radius. +-- This class implements the inherited functions from @{Zone#ZONE_RADIUS} taking into account the own zone format and properties. +-- +-- === +-- +-- # 5) @{Zone#ZONE_GROUP} class, extends @{Zone#ZONE_RADIUS} +-- +-- The ZONE_GROUP class defines by a zone around a @{Group#GROUP} with a radius. The current leader of the group defines the center of the zone. +-- This class implements the inherited functions from @{Zone#ZONE_RADIUS} taking into account the own zone format and properties. +-- +-- === +-- +-- # 6) @{Zone#ZONE_POLYGON_BASE} class, extends @{Zone#ZONE_BASE} +-- +-- The ZONE_POLYGON_BASE class defined by a sequence of @{Group#GROUP} waypoints within the Mission Editor, forming a polygon. +-- This class implements the inherited functions from @{Zone#ZONE_RADIUS} taking into account the own zone format and properties. +-- This class is an abstract BASE class for derived classes, and is not meant to be instantiated. +-- +-- ## 6.1) Zone point randomization +-- +-- Various functions exist to find random points within the zone. +-- +-- * @{#ZONE_POLYGON_BASE.GetRandomVec2}(): Gets a random 2D point in the zone. +-- * @{#ZONE_POLYGON_BASE.GetRandomPointVec2}(): Return a @{Point#POINT_VEC2} object representing a random 2D point within the zone. +-- * @{#ZONE_POLYGON_BASE.GetRandomPointVec3}(): Return a @{Point#POINT_VEC3} object representing a random 3D point at landheight within the zone. +-- +-- +-- === +-- +-- # 7) @{Zone#ZONE_POLYGON} class, extends @{Zone#ZONE_POLYGON_BASE} +-- +-- The ZONE_POLYGON class defined by a sequence of @{Group#GROUP} waypoints within the Mission Editor, forming a polygon. +-- This class implements the inherited functions from @{Zone#ZONE_RADIUS} taking into account the own zone format and properties. +-- +-- ==== +-- +-- **API CHANGE HISTORY** +-- ====================== +-- +-- The underlying change log documents the API changes. Please read this carefully. The following notation is used: +-- +-- * **Added** parts are expressed in bold type face. +-- * _Removed_ parts are expressed in italic type face. +-- +-- Hereby the change log: +-- +-- 2017-02-28: ZONE\_BASE:**IsVec2InZone()** replaces ZONE\_BASE:_IsPointVec2InZone()_. +-- 2017-02-28: ZONE\_BASE:**IsVec3InZone()** replaces ZONE\_BASE:_IsPointVec3InZone()_. +-- 2017-02-28: ZONE\_RADIUS:**IsVec2InZone()** replaces ZONE\_RADIUS:_IsPointVec2InZone()_. +-- 2017-02-28: ZONE\_RADIUS:**IsVec3InZone()** replaces ZONE\_RADIUS:_IsPointVec3InZone()_. +-- 2017-02-28: ZONE\_POLYGON:**IsVec2InZone()** replaces ZONE\_POLYGON:_IsPointVec2InZone()_. +-- 2017-02-28: ZONE\_POLYGON:**IsVec3InZone()** replaces ZONE\_POLYGON:_IsPointVec3InZone()_. +-- +-- 2017-02-18: ZONE\_POLYGON_BASE:**GetRandomPointVec2()** added. +-- +-- 2017-02-18: ZONE\_POLYGON_BASE:**GetRandomPointVec3()** added. +-- +-- 2017-02-18: ZONE\_RADIUS:**GetRandomPointVec3( inner, outer )** added. +-- +-- 2017-02-18: ZONE\_RADIUS:**GetRandomPointVec2( inner, outer )** added. +-- +-- 2016-08-15: ZONE\_BASE:**GetName()** added. +-- +-- 2016-08-15: ZONE\_BASE:**SetZoneProbability( ZoneProbability )** added. +-- +-- 2016-08-15: ZONE\_BASE:**GetZoneProbability()** added. +-- +-- 2016-08-15: ZONE\_BASE:**GetZoneMaybe()** added. +-- +-- === +-- +-- @module Zone + + +--- The ZONE_BASE class +-- @type ZONE_BASE +-- @field #string ZoneName Name of the zone. +-- @field #number ZoneProbability A value between 0 and 1. 0 = 0% and 1 = 100% probability. +-- @extends Core.Base#BASE +ZONE_BASE = { + ClassName = "ZONE_BASE", + ZoneName = "", + ZoneProbability = 1, + } + + +--- The ZONE_BASE.BoundingSquare +-- @type ZONE_BASE.BoundingSquare +-- @field Dcs.DCSTypes#Distance x1 The lower x coordinate (left down) +-- @field Dcs.DCSTypes#Distance y1 The lower y coordinate (left down) +-- @field Dcs.DCSTypes#Distance x2 The higher x coordinate (right up) +-- @field Dcs.DCSTypes#Distance y2 The higher y coordinate (right up) + + +--- 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 the name of the zone. +-- @param #ZONE_BASE self +-- @return #string The name of the zone. +function ZONE_BASE:GetName() + self:F2() + + return self.ZoneName +end +--- Returns if a location is within the zone. +-- @param #ZONE_BASE self +-- @param Dcs.DCSTypes#Vec2 Vec2 The location to test. +-- @return #boolean true if the location is within the zone. +function ZONE_BASE:IsVec2InZone( Vec2 ) + self:F2( Vec2 ) + + return false +end + +--- Returns if a point is within the zone. +-- @param #ZONE_BASE self +-- @param Dcs.DCSTypes#Vec3 Vec3 The point to test. +-- @return #boolean true if the point is within the zone. +function ZONE_BASE:IsVec3InZone( Vec3 ) + self:F2( Vec3 ) + + local InZone = self:IsVec2InZone( { x = Vec3.x, y = Vec3.z } ) + + return InZone +end + +--- Returns the @{DCSTypes#Vec2} coordinate of the zone. +-- @param #ZONE_BASE self +-- @return #nil. +function ZONE_BASE:GetVec2() + self:F2( self.ZoneName ) + + return nil +end + +--- Define a random @{DCSTypes#Vec2} within the zone. +-- @param #ZONE_BASE self +-- @return Dcs.DCSTypes#Vec2 The Vec2 coordinates. +function ZONE_BASE:GetRandomVec2() + return nil +end + +--- Define a random @{Point#POINT_VEC2} within the zone. +-- @param #ZONE_BASE self +-- @return Core.Point#POINT_VEC2 The PointVec2 coordinates. +function ZONE_BASE:GetRandomPointVec2() + return nil +end + +--- Get the bounding square the zone. +-- @param #ZONE_BASE self +-- @return #nil The bounding square. +function ZONE_BASE:GetBoundingSquare() + --return { x1 = 0, y1 = 0, x2 = 0, y2 = 0 } + return nil +end + +--- Bound the zone boundaries with a tires. +-- @param #ZONE_BASE self +function ZONE_BASE:BoundZone() + self:F2() + +end + +--- Smokes the zone boundaries in a color. +-- @param #ZONE_BASE self +-- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color. +function ZONE_BASE:SmokeZone( SmokeColor ) + self:F2( SmokeColor ) + +end + +--- Set the randomization probability of a zone to be selected. +-- @param #ZONE_BASE self +-- @param ZoneProbability A value between 0 and 1. 0 = 0% and 1 = 100% probability. +function ZONE_BASE:SetZoneProbability( ZoneProbability ) + self:F2( ZoneProbability ) + + self.ZoneProbability = ZoneProbability or 1 + return self +end + +--- Get the randomization probability of a zone to be selected. +-- @param #ZONE_BASE self +-- @return #number A value between 0 and 1. 0 = 0% and 1 = 100% probability. +function ZONE_BASE:GetZoneProbability() + self:F2() + + return self.ZoneProbability +end + +--- Get the zone taking into account the randomization probability of a zone to be selected. +-- @param #ZONE_BASE self +-- @return #ZONE_BASE The zone is selected taking into account the randomization probability factor. +-- @return #nil The zone is not selected taking into account the randomization probability factor. +function ZONE_BASE:GetZoneMaybe() + self:F2() + + local Randomization = math.random() + if Randomization <= self.ZoneProbability then + return self + else + return nil + end +end + + +--- The ZONE_RADIUS class, defined by a zone name, a location and a radius. +-- @type ZONE_RADIUS +-- @field Dcs.DCSTypes#Vec2 Vec2 The current location of the zone. +-- @field Dcs.DCSTypes#Distance Radius The radius of the zone. +-- @extends Core.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 Dcs.DCSTypes#Vec2 Vec2 The location of the zone. +-- @param Dcs.DCSTypes#Distance Radius The radius of the zone. +-- @return #ZONE_RADIUS self +function ZONE_RADIUS:New( ZoneName, Vec2, Radius ) + local self = BASE:Inherit( self, ZONE_BASE:New( ZoneName ) ) -- #ZONE_RADIUS + self:F( { ZoneName, Vec2, Radius } ) + + self.Radius = Radius + self.Vec2 = Vec2 + + return self +end + +--- Bounds the zone with tires. +-- @param #ZONE_RADIUS self +-- @param #number Points (optional) The amount of points in the circle. +-- @return #ZONE_RADIUS self +function ZONE_RADIUS:BoundZone( Points ) + + local Point = {} + local Vec2 = self:GetVec2() + + Points = Points and Points or 360 + + local Angle + local RadialBase = math.pi*2 + + -- + for Angle = 0, 360, (360 / Points ) do + local Radial = Angle * RadialBase / 360 + Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() + Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() + + local Tire = { + ["country"] = "USA", + ["category"] = "Fortifications", + ["canCargo"] = false, + ["shape_name"] = "H-tyre_B_WF", + ["type"] = "Black_Tyre_WF", + --["unitId"] = Angle + 10000, + ["y"] = Point.y, + ["x"] = Point.x, + ["name"] = string.format( "%s-Tire #%0d", self:GetName(), Angle ), + ["heading"] = 0, + } -- end of ["group"] + + coalition.addStaticObject( country.id.USA, Tire ) + end + + return self +end + + +--- Smokes the zone boundaries in a color. +-- @param #ZONE_RADIUS self +-- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color. +-- @param #number Points (optional) The amount of points in the circle. +-- @return #ZONE_RADIUS self +function ZONE_RADIUS:SmokeZone( SmokeColor, Points ) + self:F2( SmokeColor ) + + local Point = {} + local Vec2 = self:GetVec2() + + Points = Points and Points or 360 + + local Angle + local RadialBase = math.pi*2 + + for Angle = 0, 360, 360 / Points do + local Radial = Angle * RadialBase / 360 + Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() + Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() + 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 Utilities.Utils#FLARECOLOR FlareColor The flare color. +-- @param #number Points (optional) The amount of points in the circle. +-- @param Dcs.DCSTypes#Azimuth Azimuth (optional) Azimuth The azimuth of the flare. +-- @return #ZONE_RADIUS self +function ZONE_RADIUS:FlareZone( FlareColor, Points, Azimuth ) + self:F2( { FlareColor, Azimuth } ) + + local Point = {} + local Vec2 = self:GetVec2() + + Points = Points and Points or 360 + + local Angle + local RadialBase = math.pi*2 + + for Angle = 0, 360, 360 / Points do + local Radial = Angle * RadialBase / 360 + Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() + Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() + 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 Dcs.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 Dcs.DCSTypes#Distance Radius The radius of the zone. +-- @return Dcs.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 @{DCSTypes#Vec2} of the zone. +-- @param #ZONE_RADIUS self +-- @return Dcs.DCSTypes#Vec2 The location of the zone. +function ZONE_RADIUS:GetVec2() + self:F2( self.ZoneName ) + + self:T2( { self.Vec2 } ) + + return self.Vec2 +end + +--- Sets the @{DCSTypes#Vec2} of the zone. +-- @param #ZONE_RADIUS self +-- @param Dcs.DCSTypes#Vec2 Vec2 The new location of the zone. +-- @return Dcs.DCSTypes#Vec2 The new location of the zone. +function ZONE_RADIUS:SetVec2( Vec2 ) + self:F2( self.ZoneName ) + + self.Vec2 = Vec2 + + self:T2( { self.Vec2 } ) + + return self.Vec2 +end + +--- Returns the @{DCSTypes#Vec3} of the ZONE_RADIUS. +-- @param #ZONE_RADIUS self +-- @param Dcs.DCSTypes#Distance Height The height to add to the land height where the center of the zone is located. +-- @return Dcs.DCSTypes#Vec3 The point of the zone. +function ZONE_RADIUS:GetVec3( Height ) + self:F2( { self.ZoneName, Height } ) + + Height = Height or 0 + local Vec2 = self:GetVec2() + + local Vec3 = { x = Vec2.x, y = land.getHeight( self:GetVec2() ) + Height, z = Vec2.y } + + self:T2( { Vec3 } ) + + return Vec3 +end + + +--- Returns if a location is within the zone. +-- @param #ZONE_RADIUS self +-- @param Dcs.DCSTypes#Vec2 Vec2 The location to test. +-- @return #boolean true if the location is within the zone. +function ZONE_RADIUS:IsVec2InZone( Vec2 ) + self:F2( Vec2 ) + + local ZoneVec2 = self:GetVec2() + + if ZoneVec2 then + if (( Vec2.x - ZoneVec2.x )^2 + ( Vec2.y - ZoneVec2.y ) ^2 ) ^ 0.5 <= self:GetRadius() then + return true + end + end + + return false +end + +--- Returns if a point is within the zone. +-- @param #ZONE_RADIUS self +-- @param Dcs.DCSTypes#Vec3 Vec3 The point to test. +-- @return #boolean true if the point is within the zone. +function ZONE_RADIUS:IsVec3InZone( Vec3 ) + self:F2( Vec3 ) + + local InZone = self:IsVec2InZone( { x = Vec3.x, y = Vec3.z } ) + + return InZone +end + +--- Returns a random Vec2 location within the zone. +-- @param #ZONE_RADIUS self +-- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. +-- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. +-- @return Dcs.DCSTypes#Vec2 The random location within the zone. +function ZONE_RADIUS:GetRandomVec2( inner, outer ) + self:F( self.ZoneName, inner, outer ) + + local Point = {} + local Vec2 = self:GetVec2() + local _inner = inner or 0 + local _outer = outer or self:GetRadius() + + local angle = math.random() * math.pi * 2; + Point.x = Vec2.x + math.cos( angle ) * math.random(_inner, _outer); + Point.y = Vec2.y + math.sin( angle ) * math.random(_inner, _outer); + + self:T( { Point } ) + + return Point +end + +--- Returns a @{Point#POINT_VEC2} object reflecting a random 2D location within the zone. +-- @param #ZONE_RADIUS self +-- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. +-- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. +-- @return Core.Point#POINT_VEC2 The @{Point#POINT_VEC2} object reflecting the random 3D location within the zone. +function ZONE_RADIUS:GetRandomPointVec2( inner, outer ) + self:F( self.ZoneName, inner, outer ) + + local PointVec2 = POINT_VEC2:NewFromVec2( self:GetRandomVec2() ) + + self:T3( { PointVec2 } ) + + return PointVec2 +end + +--- Returns a @{Point#POINT_VEC3} object reflecting a random 3D location within the zone. +-- @param #ZONE_RADIUS self +-- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. +-- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. +-- @return Core.Point#POINT_VEC3 The @{Point#POINT_VEC3} object reflecting the random 3D location within the zone. +function ZONE_RADIUS:GetRandomPointVec3( inner, outer ) + self:F( self.ZoneName, inner, outer ) + + local PointVec3 = POINT_VEC3:NewFromVec2( self:GetRandomVec2() ) + + self:T3( { PointVec3 } ) + + return PointVec3 +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 Core.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 Wrapper.Unit#UNIT ZoneUNIT +-- @extends Core.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 Wrapper.Unit#UNIT ZoneUNIT The unit as the center of the zone. +-- @param Dcs.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:GetVec2(), Radius ) ) + self:F( { ZoneName, ZoneUNIT:GetVec2(), Radius } ) + + self.ZoneUNIT = ZoneUNIT + self.LastVec2 = ZoneUNIT:GetVec2() + + return self +end + + +--- Returns the current location of the @{Unit#UNIT}. +-- @param #ZONE_UNIT self +-- @return Dcs.DCSTypes#Vec2 The location of the zone based on the @{Unit#UNIT}location. +function ZONE_UNIT:GetVec2() + self:F( self.ZoneName ) + + local ZoneVec2 = self.ZoneUNIT:GetVec2() + if ZoneVec2 then + self.LastVec2 = ZoneVec2 + return ZoneVec2 + else + return self.LastVec2 + end + + self:T( { ZoneVec2 } ) + + return nil +end + +--- Returns a random location within the zone. +-- @param #ZONE_UNIT self +-- @return Dcs.DCSTypes#Vec2 The random location within the zone. +function ZONE_UNIT:GetRandomVec2() + self:F( self.ZoneName ) + + local RandomVec2 = {} + local Vec2 = self.ZoneUNIT:GetVec2() + + if not Vec2 then + Vec2 = self.LastVec2 + end + + local angle = math.random() * math.pi*2; + RandomVec2.x = Vec2.x + math.cos( angle ) * math.random() * self:GetRadius(); + RandomVec2.y = Vec2.y + math.sin( angle ) * math.random() * self:GetRadius(); + + self:T( { RandomVec2 } ) + + return RandomVec2 +end + +--- Returns the @{DCSTypes#Vec3} of the ZONE_UNIT. +-- @param #ZONE_UNIT self +-- @param Dcs.DCSTypes#Distance Height The height to add to the land height where the center of the zone is located. +-- @return Dcs.DCSTypes#Vec3 The point of the zone. +function ZONE_UNIT:GetVec3( Height ) + self:F2( self.ZoneName ) + + Height = Height or 0 + + local Vec2 = self:GetVec2() + + local Vec3 = { x = Vec2.x, y = land.getHeight( self:GetVec2() ) + Height, z = Vec2.y } + + self:T2( { Vec3 } ) + + return Vec3 +end + +--- The ZONE_GROUP class defined by a zone around a @{Group}, taking the average center point of all the units within the Group, with a radius. +-- @type ZONE_GROUP +-- @field Wrapper.Group#GROUP ZoneGROUP +-- @extends Core.Zone#ZONE_RADIUS +ZONE_GROUP = { + ClassName="ZONE_GROUP", + } + +--- Constructor to create a ZONE_GROUP instance, taking the zone name, a zone @{Group#GROUP} and a radius. +-- @param #ZONE_GROUP self +-- @param #string ZoneName Name of the zone. +-- @param Wrapper.Group#GROUP ZoneGROUP The @{Group} as the center of the zone. +-- @param Dcs.DCSTypes#Distance Radius The radius of the zone. +-- @return #ZONE_GROUP self +function ZONE_GROUP:New( ZoneName, ZoneGROUP, Radius ) + local self = BASE:Inherit( self, ZONE_RADIUS:New( ZoneName, ZoneGROUP:GetVec2(), Radius ) ) + self:F( { ZoneName, ZoneGROUP:GetVec2(), Radius } ) + + self.ZoneGROUP = ZoneGROUP + + return self +end + + +--- Returns the current location of the @{Group}. +-- @param #ZONE_GROUP self +-- @return Dcs.DCSTypes#Vec2 The location of the zone based on the @{Group} location. +function ZONE_GROUP:GetVec2() + self:F( self.ZoneName ) + + local ZoneVec2 = self.ZoneGROUP:GetVec2() + + self:T( { ZoneVec2 } ) + + return ZoneVec2 +end + +--- Returns a random location within the zone of the @{Group}. +-- @param #ZONE_GROUP self +-- @return Dcs.DCSTypes#Vec2 The random location of the zone based on the @{Group} location. +function ZONE_GROUP:GetRandomVec2() + self:F( self.ZoneName ) + + local Point = {} + local Vec2 = self.ZoneGROUP:GetVec2() + + local angle = math.random() * math.pi*2; + Point.x = Vec2.x + math.cos( angle ) * math.random() * self:GetRadius(); + Point.y = Vec2.y + math.sin( angle ) * math.random() * self:GetRadius(); + + self:T( { Point } ) + + return Point +end + + + +-- 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 Core.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 +-- @return #ZONE_POLYGON_BASE self +function ZONE_POLYGON_BASE:BoundZone( ) + + local i + local j + local Segments = 10 + + i = 1 + j = #self.Polygon + + while i <= #self.Polygon do + self:T( { i, j, self.Polygon[i], self.Polygon[j] } ) + + local DeltaX = self.Polygon[j].x - self.Polygon[i].x + local DeltaY = self.Polygon[j].y - self.Polygon[i].y + + for Segment = 0, Segments do -- We divide each line in 5 segments and smoke a point on the line. + local PointX = self.Polygon[i].x + ( Segment * DeltaX / Segments ) + local PointY = self.Polygon[i].y + ( Segment * DeltaY / Segments ) + local Tire = { + ["country"] = "USA", + ["category"] = "Fortifications", + ["canCargo"] = false, + ["shape_name"] = "H-tyre_B_WF", + ["type"] = "Black_Tyre_WF", + ["y"] = PointY, + ["x"] = PointX, + ["name"] = string.format( "%s-Tire #%0d", self:GetName(), ((i - 1) * Segments) + Segment ), + ["heading"] = 0, + } -- end of ["group"] + + coalition.addStaticObject( country.id.USA, Tire ) + + end + j = i + i = i + 1 + end + + return self +end + + + +--- Smokes the zone boundaries in a color. +-- @param #ZONE_POLYGON_BASE self +-- @param Utilities.Utils#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. +-- Source learned and taken from: https://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html +-- @param #ZONE_POLYGON_BASE self +-- @param Dcs.DCSTypes#Vec2 Vec2 The location to test. +-- @return #boolean true if the location is within the zone. +function ZONE_POLYGON_BASE:IsVec2InZone( Vec2 ) + self:F2( Vec2 ) + + local Next + local Prev + local InPolygon = false + + Next = 1 + Prev = #self.Polygon + + while Next <= #self.Polygon do + self:T( { Next, Prev, self.Polygon[Next], self.Polygon[Prev] } ) + if ( ( ( self.Polygon[Next].y > Vec2.y ) ~= ( self.Polygon[Prev].y > Vec2.y ) ) and + ( Vec2.x < ( self.Polygon[Prev].x - self.Polygon[Next].x ) * ( Vec2.y - self.Polygon[Next].y ) / ( self.Polygon[Prev].y - self.Polygon[Next].y ) + self.Polygon[Next].x ) + ) then + InPolygon = not InPolygon + end + self:T2( { InPolygon = InPolygon } ) + Prev = Next + Next = Next + 1 + end + + self:T( { InPolygon = InPolygon } ) + return InPolygon +end + +--- Define a random @{DCSTypes#Vec2} within the zone. +-- @param #ZONE_POLYGON_BASE self +-- @return Dcs.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:IsVec2InZone( Vec2 ) then + Vec2Found = true + end + end + + self:T2( Vec2 ) + + return Vec2 +end + +--- Return a @{Point#POINT_VEC2} object representing a random 2D point at landheight within the zone. +-- @param #ZONE_POLYGON_BASE self +-- @return @{Point#POINT_VEC2} +function ZONE_POLYGON_BASE:GetRandomPointVec2() + self:F2() + + local PointVec2 = POINT_VEC2:NewFromVec2( self:GetRandomVec2() ) + + self:T2( PointVec2 ) + + return PointVec2 +end + +--- Return a @{Point#POINT_VEC3} object representing a random 3D point at landheight within the zone. +-- @param #ZONE_POLYGON_BASE self +-- @return @{Point#POINT_VEC3} +function ZONE_POLYGON_BASE:GetRandomPointVec3() + self:F2() + + local PointVec3 = POINT_VEC3:NewFromVec2( self:GetRandomVec2() ) + + self:T2( PointVec3 ) + + return PointVec3 +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 Core.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 Wrapper.Group#GROUP ZoneGroup The GROUP waypoints as defined within the Mission Editor define the polygon shape. +-- @return #ZONE_POLYGON self +function ZONE_POLYGON:New( ZoneName, ZoneGroup ) + + local GroupPoints = ZoneGroup:GetTaskRoute() + + local self = BASE:Inherit( self, ZONE_POLYGON_BASE:New( ZoneName, GroupPoints ) ) + self:F( { ZoneName, ZoneGroup, self.Polygon } ) + + return self +end + +--- This module contains the DATABASE class, managing the database of mission objects. +-- +-- ==== +-- +-- 1) @{#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 Core.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() ) + + self:SetEventPriority( 1 ) + + self:HandleEvent( EVENTS.Birth, self._EventOnBirth ) + self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) + self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) + + -- Follow alive players and clients + self:HandleEvent( EVENTS.PlayerEnterUnit, self._EventOnPlayerEnterUnit ) + self:HandleEvent( EVENTS.PlayerLeaveUnit, self._EventOnPlayerLeaveUnit ) + + 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 Wrapper.Unit#UNIT The found Unit. +function DATABASE:FindUnit( UnitName ) + + local UnitFound = self.UNITS[UnitName] + return UnitFound +end + + +--- Adds a Unit based on the Unit Name in the DATABASE. +-- @param #DATABASE self +function DATABASE:AddUnit( DCSUnitName ) + + if not self.UNITS[DCSUnitName] then + local UnitRegister = UNIT:Register( DCSUnitName ) + 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 Wrapper.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 Wrapper.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 Wrapper.Client#CLIENT The found CLIENT. +function DATABASE:FindClient( ClientName ) + + local ClientFound = self.CLIENTS[ClientName] + return ClientFound +end + + +--- Adds a CLIENT based on the ClientName in the DATABASE. +-- @param #DATABASE self +function DATABASE:AddClient( ClientName ) + + if not self.CLIENTS[ClientName] then + self.CLIENTS[ClientName] = CLIENT:Register( ClientName ) + end + + return self.CLIENTS[ClientName] +end + + +--- Finds a GROUP based on the GroupName. +-- @param #DATABASE self +-- @param #string GroupName +-- @return Wrapper.Group#GROUP The found GROUP. +function DATABASE:FindGroup( GroupName ) + + local GroupFound = self.GROUPS[GroupName] + return GroupFound +end + + +--- Adds a GROUP based on the GroupName in the DATABASE. +-- @param #DATABASE self +function DATABASE:AddGroup( GroupName ) + + if not self.GROUPS[GroupName] then + self:E( { "Add GROUP:", GroupName } ) + self.GROUPS[GroupName] = GROUP:Register( GroupName ) + end + + return self.GROUPS[GroupName] +end + +--- Adds a player based on the Player Name in the DATABASE. +-- @param #DATABASE self +function DATABASE:AddPlayer( UnitName, PlayerName ) + + if PlayerName then + self: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:F( SpawnTemplate.name ) + + self:T( { SpawnTemplate.SpawnCountryID, SpawnTemplate.SpawnCategoryID } ) + + -- Copy the spawn variables of the template in temporary storage, nullify, and restore the spawn variables. + local SpawnCoalitionID = SpawnTemplate.CoalitionID + local SpawnCountryID = SpawnTemplate.CountryID + local SpawnCategoryID = SpawnTemplate.CategoryID + + -- Nullify + SpawnTemplate.CoalitionID = nil + SpawnTemplate.CountryID = nil + SpawnTemplate.CategoryID = nil + + self:_RegisterTemplate( SpawnTemplate, SpawnCoalitionID, SpawnCategoryID, SpawnCountryID ) + + self:T3( SpawnTemplate ) + coalition.addGroup( SpawnCountryID, SpawnCategoryID, SpawnTemplate ) + + -- Restore + SpawnTemplate.CoalitionID = SpawnCoalitionID + SpawnTemplate.CountryID = SpawnCountryID + SpawnTemplate.CategoryID = 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 + + GroupTemplate.CategoryID = CategoryID + GroupTemplate.CoalitionID = CoalitionID + GroupTemplate.CountryID = CountryID + + self.Templates.Groups[GroupTemplateName].GroupName = GroupTemplateName + self.Templates.Groups[GroupTemplateName].Template = GroupTemplate + self.Templates.Groups[GroupTemplateName].groupId = GroupTemplate.groupId + self.Templates.Groups[GroupTemplateName].UnitCount = #GroupTemplate.units + self.Templates.Groups[GroupTemplateName].Units = GroupTemplate.units + self.Templates.Groups[GroupTemplateName].CategoryID = CategoryID + self.Templates.Groups[GroupTemplateName].CoalitionID = 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 + + UnitTemplate.name = env.getValueDictByKey(UnitTemplate.name) + + self.Templates.Units[UnitTemplate.name] = {} + self.Templates.Units[UnitTemplate.name].UnitName = UnitTemplate.name + self.Templates.Units[UnitTemplate.name].Template = UnitTemplate + self.Templates.Units[UnitTemplate.name].GroupName = GroupTemplateName + self.Templates.Units[UnitTemplate.name].GroupTemplate = GroupTemplate + self.Templates.Units[UnitTemplate.name].GroupId = GroupTemplate.groupId + self.Templates.Units[UnitTemplate.name].CategoryID = CategoryID + self.Templates.Units[UnitTemplate.name].CoalitionID = CoalitionID + self.Templates.Units[UnitTemplate.name].CountryID = CountryID + + if UnitTemplate.skill and (UnitTemplate.skill == "Client" or UnitTemplate.skill == "Player") then + self.Templates.ClientsByName[UnitTemplate.name] = UnitTemplate + self.Templates.ClientsByName[UnitTemplate.name].CategoryID = CategoryID + self.Templates.ClientsByName[UnitTemplate.name].CoalitionID = CoalitionID + self.Templates.ClientsByName[UnitTemplate.name].CountryID = CountryID + self.Templates.ClientsByID[UnitTemplate.unitId] = UnitTemplate + end + + TraceTable[#TraceTable+1] = self.Templates.Units[UnitTemplate.name].UnitName + end + + self:E( TraceTable ) +end + +function DATABASE:GetGroupTemplate( GroupName ) + local GroupTemplate = self.Templates.Groups[GroupName].Template + GroupTemplate.SpawnCoalitionID = self.Templates.Groups[GroupName].CoalitionID + GroupTemplate.SpawnCategoryID = self.Templates.Groups[GroupName].CategoryID + GroupTemplate.SpawnCountryID = self.Templates.Groups[GroupName].CountryID + return GroupTemplate +end + +function DATABASE:GetGroupNameFromUnitName( UnitName ) + return self.Templates.Units[UnitName].GroupName +end + +function DATABASE:GetGroupTemplateFromUnitName( UnitName ) + return self.Templates.Units[UnitName].GroupTemplate +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 Core.Event#EVENTDATA Event +function DATABASE:_EventOnBirth( Event ) + self:F2( { Event } ) + + if Event.IniDCSUnit then + if Event.IniObjectCategory == 3 then + self:AddStatic( Event.IniDCSUnitName ) + else + if Event.IniObjectCategory == 1 then + self:AddUnit( Event.IniDCSUnitName ) + self:AddGroup( Event.IniDCSGroupName ) + end + end + self:_EventOnPlayerEnterUnit( Event ) + end +end + + +--- Handles the OnDead or OnCrash event for alive units set. +-- @param #DATABASE self +-- @param Core.Event#EVENTDATA Event +function DATABASE:_EventOnDeadOrCrash( Event ) + self:F2( { Event } ) + + if Event.IniDCSUnit then + if Event.IniObjectCategory == 3 then + if self.STATICS[Event.IniDCSUnitName] then + self:DeleteStatic( Event.IniDCSUnitName ) + end + else + if Event.IniObjectCategory == 1 then + if self.UNITS[Event.IniDCSUnitName] then + self:DeleteUnit( Event.IniDCSUnitName ) + end + end + end + end +end + + +--- Handles the OnPlayerEnterUnit event to fill the active players table (with the unit filter applied). +-- @param #DATABASE self +-- @param Core.Event#EVENTDATA Event +function DATABASE:_EventOnPlayerEnterUnit( Event ) + self:F2( { Event } ) + + if Event.IniUnit then + if Event.IniObjectCategory == 1 then + self:AddUnit( Event.IniDCSUnitName ) + self:AddGroup( Event.IniDCSGroupName ) + local PlayerName = Event.IniUnit:GetPlayerName() + if not self.PLAYERS[PlayerName] then + self:AddPlayer( Event.IniUnitName, PlayerName ) + end + end + end +end + + +--- Handles the OnPlayerLeaveUnit event to clean the active players table. +-- @param #DATABASE self +-- @param Core.Event#EVENTDATA Event +function DATABASE:_EventOnPlayerLeaveUnit( Event ) + self:F2( { Event } ) + + if Event.IniUnit then + if Event.IniObjectCategory == 1 then + local PlayerName = Event.IniUnit:GetPlayerName() + if self.PLAYERS[PlayerName] then + self:DeletePlayer( PlayerName ) + end + end + end +end + +--- Iterators + +--- Iterate the DATABASE and call an iterator function for the given set, providing the Object for each element within the set and optional parameters. +-- @param #DATABASE self +-- @param #function IteratorFunction The function that will be called when there is an alive player in the database. +-- @return #DATABASE self +function DATABASE:ForEach( IteratorFunction, FinalizeFunction, arg, Set ) + self:F2( arg ) + + local function CoRoutine() + local Count = 0 + for ObjectID, Object in pairs( Set ) do + self:T2( Object ) + IteratorFunction( Object, unpack( arg ) ) + Count = Count + 1 +-- if Count % 100 == 0 then +-- coroutine.yield( false ) +-- end + end + return true + end + +-- local co = coroutine.create( CoRoutine ) + local co = CoRoutine + + local function Schedule() + +-- local status, res = coroutine.resume( co ) + local status, res = co() + self:T3( { status, res } ) + + if status == false then + error( res ) + end + if res == false then + return true -- resume next time the loop + end + if FinalizeFunction then + FinalizeFunction( unpack( arg ) ) + end + return false + end + + local Scheduler = SCHEDULER:New( self, Schedule, {}, 0.001, 0.001, 0 ) + + 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] = {} + + local CoalitionSide = coalition.side[string.upper(CoalitionName)] + + ---------------------------------------------- + -- build nav points DB + self.Navpoints[CoalitionName] = {} + if coa_data.nav_points then --navpoints + for nav_ind, nav_data in pairs(coa_data.nav_points) do + + if type(nav_data) == 'table' then + self.Navpoints[CoalitionName][nav_ind] = routines.utils.deepCopy(nav_data) + + self.Navpoints[CoalitionName][nav_ind]['name'] = nav_data.callsignStr -- name is a little bit more self-explanatory. + self.Navpoints[CoalitionName][nav_ind]['point'] = {} -- point is used by SSE, support it. + self.Navpoints[CoalitionName][nav_ind]['point']['x'] = nav_data.x + self.Navpoints[CoalitionName][nav_ind]['point']['y'] = 0 + self.Navpoints[CoalitionName][nav_ind]['point']['z'] = nav_data.y + end + end + end + ------------------------------------------------- + if coa_data.country then --there is a country table + for cntry_id, cntry_data in pairs(coa_data.country) do + + local CountryName = string.upper(cntry_data.name) + local CountryID = cntry_data.id + + --self.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, + CoalitionSide, + _DATABASECategory[string.lower(CategoryName)], + CountryID + ) + end --if GroupTemplate and GroupTemplate.units then + end --for group_num, GroupTemplate in pairs(obj_type_data.group) do + end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then + end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then + end --for obj_type_name, obj_type_data in pairs(cntry_data) do + end --if type(cntry_data) == 'table' then + end --for cntry_id, cntry_data in pairs(coa_data.country) do + end --if coa_data.country then --there is a country table + end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then + end --for coa_name, coa_data in pairs(mission.coalition) do + + return self +end + + + + +--- 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. +-- +-- ==== +-- +-- ### Authors: +-- +-- * FlightControl : Design & Programming +-- +-- ### Contributions: +-- +-- +-- @module Set + + +--- SET_BASE class +-- @type SET_BASE +-- @field #table Filter +-- @field #table Set +-- @field #table List +-- @field Core.Scheduler#SCHEDULER CallScheduler +-- @extends Core.Base#BASE +SET_BASE = { + ClassName = "SET_BASE", + Filter = {}, + Set = {}, + List = {}, +} + +--- Creates a new SET_BASE object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. +-- @param #SET_BASE self +-- @return #SET_BASE +-- @usage +-- -- Define a new SET_BASE Object. This DBObject will contain a reference to all Group and Unit Templates defined within the ME and the DCSRTE. +-- DBObject = SET_BASE:New() +function SET_BASE:New( Database ) + + -- Inherits from BASE + local self = BASE:Inherit( self, BASE:New() ) -- Core.Set#SET_BASE + + self.Database = Database + + self.YieldInterval = 10 + self.TimeInterval = 0.001 + + self.List = {} + self.List.__index = self.List + self.List = setmetatable( { Count = 0 }, self.List ) + + self.CallScheduler = SCHEDULER:New( self ) + + self:SetEventPriority( 2 ) + + return self +end + +--- Finds an @{Base#BASE} object based on the object Name. +-- @param #SET_BASE self +-- @param #string ObjectName +-- @return Core.Base#BASE The Object found. +function SET_BASE:_Find( ObjectName ) + + local ObjectFound = self.Set[ObjectName] + return ObjectFound +end + + +--- Gets the Set. +-- @param #SET_BASE self +-- @return #SET_BASE self +function SET_BASE:GetSet() + self:F2() + + return self.Set +end + +--- Adds a @{Base#BASE} object in the @{Set#SET_BASE}, using a given ObjectName as the index. +-- @param #SET_BASE self +-- @param #string ObjectName +-- @param Core.Base#BASE Object +-- @return Core.Base#BASE The added BASE Object. +function SET_BASE:Add( ObjectName, Object ) + self:F2( ObjectName ) + + local t = { _ = Object } + + if self.List.last then + self.List.last._next = t + t._prev = self.List.last + self.List.last = t + else + -- this is the first node + self.List.first = t + self.List.last = t + end + + self.List.Count = self.List.Count + 1 + + self.Set[ObjectName] = t._ + +end + +--- Adds a @{Base#BASE} object in the @{Set#SET_BASE}, using the Object Name as the index. +-- @param #SET_BASE self +-- @param Wrapper.Object#OBJECT Object +-- @return Core.Base#BASE The added BASE Object. +function SET_BASE:AddObject( Object ) + self:F2( Object.ObjectName ) + + self:T( Object.UnitName ) + self:T( Object.ObjectName ) + self:Add( Object.ObjectName, Object ) + +end + + + +--- 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:F( ObjectName ) + + local t = self.Set[ObjectName] + + self:E( { ObjectName, t } ) + + if t then + if t._next then + if t._prev then + t._next._prev = t._prev + t._prev._next = t._next + else + -- this was the first node + t._next._prev = nil + self.List._first = t._next + end + elseif t._prev then + -- this was the last node + t._prev._next = nil + self.List._last = t._prev + else + -- this was the only node + self.List._first = nil + self.List._last = nil + end + + t._next = nil + t._prev = nil + self.List.Count = self.List.Count - 1 + + self.Set[ObjectName] = nil + end + +end + +--- Gets a @{Base#BASE} object from the @{Set#SET_BASE} and derived classes, based on the Object Name. +-- @param #SET_BASE self +-- @param #string ObjectName +-- @return Core.Base#BASE +function SET_BASE:Get( ObjectName ) + self:F( ObjectName ) + + local t = self.Set[ObjectName] + + self:T3( { ObjectName, t } ) + + return t + +end + +--- Retrieves the amount of objects in the @{Set#SET_BASE} and derived classes. +-- @param #SET_BASE self +-- @return #number Count +function SET_BASE:Count() + + return self.List.Count +end + + + +--- Copies the Filter criteria from a given Set (for rebuilding a new Set based on an existing Set). +-- @param #SET_BASE self +-- @param #SET_BASE BaseSet +-- @return #SET_BASE +function SET_BASE:SetDatabase( BaseSet ) + + -- Copy the filter criteria of the BaseSet + local OtherFilter = routines.utils.deepCopy( BaseSet.Filter ) + self.Filter = OtherFilter + + -- Now base the new Set on the BaseSet + self.Database = BaseSet:GetSet() + return self +end + + + +--- Define the SET iterator **"yield interval"** and the **"time interval"**. +-- @param #SET_BASE self +-- @param #number YieldInterval Sets the frequency when the iterator loop will yield after the number of objects processed. The default frequency is 10 objects processed. +-- @param #number TimeInterval Sets the time in seconds when the main logic will resume the iterator loop. The default time is 0.001 seconds. +-- @return #SET_BASE self +function SET_BASE:SetIteratorIntervals( YieldInterval, TimeInterval ) + + self.YieldInterval = YieldInterval + self.TimeInterval = TimeInterval + + return self +end + + +--- Filters for the defined collection. +-- @param #SET_BASE self +-- @return #SET_BASE self +function SET_BASE:FilterOnce() + + for ObjectName, Object in pairs( self.Database ) do + + if self:IsIncludeObject( Object ) then + self:Add( ObjectName, Object ) + end + end + + return self +end + +--- Starts the filtering for the defined collection. +-- @param #SET_BASE self +-- @return #SET_BASE self +function SET_BASE:_FilterStart() + + for ObjectName, Object in pairs( self.Database ) do + + if self:IsIncludeObject( Object ) then + self:E( { "Adding Object:", ObjectName } ) + self:Add( ObjectName, Object ) + end + end + + self:HandleEvent( EVENTS.Birth, self._EventOnBirth ) + self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) + self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) + + -- Follow alive players and clients + self:HandleEvent( EVENTS.PlayerEnterUnit, self._EventOnPlayerEnterUnit ) + self:HandleEvent( EVENTS.PlayerLeaveUnit, self._EventOnPlayerLeaveUnit ) + + + return self +end + +--- Stops the filtering for the defined collection. +-- @param #SET_BASE self +-- @return #SET_BASE self +function SET_BASE:FilterStop() + + self:UnHandleEvent( EVENTS.Birth ) + self:UnHandleEvent( EVENTS.Dead ) + self:UnHandleEvent( EVENTS.Crash ) + + return self +end + +--- Iterate the SET_BASE while identifying the nearest object from a @{Point#POINT_VEC2}. +-- @param #SET_BASE self +-- @param Core.Point#POINT_VEC2 PointVec2 A @{Point#POINT_VEC2} object from where to evaluate the closest object in the set. +-- @return Core.Base#BASE The closest object. +function SET_BASE:FindNearestObjectFromPointVec2( PointVec2 ) + self:F2( PointVec2 ) + + local NearestObject = nil + local ClosestDistance = nil + + for ObjectID, ObjectData in pairs( self.Set ) do + if NearestObject == nil then + NearestObject = ObjectData + ClosestDistance = PointVec2:DistanceFromVec2( ObjectData:GetVec2() ) + else + local Distance = PointVec2:DistanceFromVec2( ObjectData:GetVec2() ) + if Distance < ClosestDistance then + NearestObject = ObjectData + ClosestDistance = Distance + end + end + end + + return NearestObject +end + + + +----- Private method that registers all alive players in the mission. +---- @param #SET_BASE self +---- @return #SET_BASE self +--function SET_BASE:_RegisterPlayers() +-- +-- local CoalitionsData = { AlivePlayersRed = coalition.getPlayers( coalition.side.RED ), AlivePlayersBlue = coalition.getPlayers( coalition.side.BLUE ) } +-- for CoalitionId, CoalitionData in pairs( CoalitionsData ) do +-- for UnitId, UnitData in pairs( CoalitionData ) do +-- self:T3( { "UnitData:", UnitData } ) +-- if UnitData and UnitData:isExist() then +-- local UnitName = UnitData:getName() +-- if not self.PlayersAlive[UnitName] then +-- self:E( { "Add player for unit:", UnitName, UnitData:getPlayerName() } ) +-- self.PlayersAlive[UnitName] = UnitData:getPlayerName() +-- end +-- end +-- end +-- end +-- +-- return self +--end + +--- Events + +--- Handles the OnBirth event for the Set. +-- @param #SET_BASE self +-- @param Core.Event#EVENTDATA Event +function SET_BASE:_EventOnBirth( Event ) + self:F3( { Event } ) + + if Event.IniDCSUnit then + local ObjectName, Object = self:AddInDatabase( Event ) + self:T3( ObjectName, Object ) + if Object and self:IsIncludeObject( Object ) then + self:Add( ObjectName, Object ) + --self:_EventOnPlayerEnterUnit( Event ) + end + end +end + +--- Handles the OnDead or OnCrash event for alive units set. +-- @param #SET_BASE self +-- @param Core.Event#EVENTDATA Event +function SET_BASE:_EventOnDeadOrCrash( Event ) + self:F3( { Event } ) + + if Event.IniDCSUnit then + local ObjectName, Object = self:FindInDatabase( Event ) + if ObjectName and Object ~= nil then + self:Remove( ObjectName ) + end + end +end + +--- Handles the OnPlayerEnterUnit event to fill the active players table (with the unit filter applied). +-- @param #SET_BASE self +-- @param Core.Event#EVENTDATA Event +function SET_BASE:_EventOnPlayerEnterUnit( Event ) + self:F3( { Event } ) + + if Event.IniDCSUnit then + local ObjectName, Object = self:AddInDatabase( Event ) + self:T3( ObjectName, Object ) + if self:IsIncludeObject( Object ) then + self:Add( ObjectName, Object ) + --self:_EventOnPlayerEnterUnit( Event ) + end + end +end + +--- Handles the OnPlayerLeaveUnit event to clean the active players table. +-- @param #SET_BASE self +-- @param Core.Event#EVENTDATA Event +function SET_BASE:_EventOnPlayerLeaveUnit( Event ) + self:F3( { Event } ) + + local ObjectName = Event.IniDCSUnit + if Event.IniDCSUnit then + if Event.IniDCSGroup then + local GroupUnits = Event.IniDCSGroup:getUnits() + local PlayerCount = 0 + for _, DCSUnit in pairs( GroupUnits ) do + if DCSUnit ~= Event.IniDCSUnit then + if DCSUnit:getPlayer() ~= nil then + PlayerCount = PlayerCount + 1 + end + end + end + self:E(PlayerCount) + if PlayerCount == 0 then + self:Remove( Event.IniDCSGroupName ) + end + end + end +end + +-- Iterators + +--- Iterate the SET_BASE and derived classes and call an iterator function for the given SET_BASE, providing the Object for each element within the set and optional parameters. +-- @param #SET_BASE self +-- @param #function IteratorFunction The function that will be called. +-- @return #SET_BASE self +function SET_BASE:ForEach( IteratorFunction, arg, Set, Function, FunctionArguments ) + self:F3( arg ) + + Set = Set or self:GetSet() + arg = arg or {} + + local function CoRoutine() + local Count = 0 + for ObjectID, ObjectData in pairs( Set ) do + local Object = ObjectData + self:T3( Object ) + if Function then + if Function( unpack( FunctionArguments ), Object ) == true then + IteratorFunction( Object, unpack( arg ) ) + end + else + IteratorFunction( Object, unpack( arg ) ) + end + Count = Count + 1 +-- if Count % self.YieldInterval == 0 then +-- coroutine.yield( false ) +-- end + end + return true + end + +-- local co = coroutine.create( CoRoutine ) + local co = CoRoutine + + local function Schedule() + +-- local status, res = coroutine.resume( co ) + local status, res = co() + self:T3( { status, res } ) + + if status == false then + error( res ) + end + if res == false then + return true -- resume next time the loop + end + + return false + end + + self.CallScheduler:Schedule( self, Schedule, {}, self.TimeInterval, self.TimeInterval, 0 ) + + 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 debugging 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:E( { "Objects in Set:", ObjectNames } ) + + return ObjectNames +end + +-- SET_GROUP + +--- SET_GROUP class +-- @type SET_GROUP +-- @extends #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 Core.Set#SET_GROUP self +-- @param #string AddGroupNames A single name or an array of GROUP names. +-- @return self +function SET_GROUP:AddGroupsByName( AddGroupNames ) + + local AddGroupNamesArray = ( type( AddGroupNames ) == "table" ) and AddGroupNames or { AddGroupNames } + + for AddGroupID, AddGroupName in pairs( AddGroupNamesArray ) do + self:Add( AddGroupName, GROUP:FindByName( AddGroupName ) ) + end + + return self +end + +--- Remove GROUP(s) from SET_GROUP. +-- @param Core.Set#SET_GROUP self +-- @param Wrapper.Group#GROUP RemoveGroupNames A single name or an array of GROUP names. +-- @return self +function SET_GROUP:RemoveGroupsByName( RemoveGroupNames ) + + local RemoveGroupNamesArray = ( type( RemoveGroupNames ) == "table" ) and RemoveGroupNames or { RemoveGroupNames } + + for RemoveGroupID, RemoveGroupName in pairs( RemoveGroupNamesArray ) do + self:Remove( RemoveGroupName.GroupName ) + end + + return self +end + + + + +--- Finds a Group based on the Group Name. +-- @param #SET_GROUP self +-- @param #string GroupName +-- @return Wrapper.Group#GROUP The found Group. +function SET_GROUP:FindGroup( GroupName ) + + local GroupFound = self.Set[GroupName] + return GroupFound +end + + + +--- 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 Core.Event#EVENTDATA Event +-- @return #string The name of the GROUP +-- @return #table The GROUP +function SET_GROUP:AddInDatabase( Event ) + self:F3( { Event } ) + + if Event.IniObjectCategory == 1 then + if not self.Database[Event.IniDCSGroupName] then + self.Database[Event.IniDCSGroupName] = GROUP:Register( Event.IniDCSGroupName ) + self:T3( self.Database[Event.IniDCSGroupName] ) + end + end + + return Event.IniDCSGroupName, self.Database[Event.IniDCSGroupName] +end + +--- Handles the Database to check on any event that Object exists in the Database. +-- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! +-- @param #SET_GROUP self +-- @param Core.Event#EVENTDATA Event +-- @return #string The name of the GROUP +-- @return #table The GROUP +function SET_GROUP:FindInDatabase( Event ) + self:F3( { Event } ) + + return Event.IniDCSGroupName, self.Database[Event.IniDCSGroupName] +end + +--- Iterate the SET_GROUP and call an iterator function for each **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 Core.Zone#ZONE ZoneObject The Zone to be tested for. +-- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. +-- @return #SET_GROUP self +function SET_GROUP:ForEachGroupCompletelyInZone( ZoneObject, IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self.Set, + --- @param Core.Zone#ZONE_BASE ZoneObject + -- @param Wrapper.Group#GROUP GroupObject + function( ZoneObject, GroupObject ) + if GroupObject:IsCompletelyInZone( ZoneObject ) then + return true + else + return false + end + end, { ZoneObject } ) + + return self +end + +--- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence partly in a @{Zone}, providing the GROUP and optional parameters to the called function. +-- @param #SET_GROUP self +-- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. +-- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. +-- @return #SET_GROUP self +function SET_GROUP:ForEachGroupPartlyInZone( ZoneObject, IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self.Set, + --- @param Core.Zone#ZONE_BASE ZoneObject + -- @param Wrapper.Group#GROUP GroupObject + function( ZoneObject, GroupObject ) + if GroupObject:IsPartlyInZone( ZoneObject ) then + return true + else + return false + end + end, { ZoneObject } ) + + return self +end + +--- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence not in a @{Zone}, providing the GROUP and optional parameters to the called function. +-- @param #SET_GROUP self +-- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. +-- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. +-- @return #SET_GROUP self +function SET_GROUP:ForEachGroupNotInZone( ZoneObject, IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self.Set, + --- @param Core.Zone#ZONE_BASE ZoneObject + -- @param Wrapper.Group#GROUP GroupObject + function( ZoneObject, GroupObject ) + if GroupObject:IsNotInZone( ZoneObject ) then + return true + else + return false + end + end, { ZoneObject } ) + + return self +end + + +----- Iterate the SET_GROUP and call an interator function for each **alive** player, providing the Group of the player and optional parameters. +---- @param #SET_GROUP self +---- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_GROUP. The function needs to accept a GROUP parameter. +---- @return #SET_GROUP self +--function SET_GROUP:ForEachPlayer( IteratorFunction, ... ) +-- self:F2( arg ) +-- +-- self:ForEach( IteratorFunction, arg, self.PlayersAlive ) +-- +-- return self +--end +-- +-- +----- Iterate the SET_GROUP and call an interator function for each client, providing the Client to the function and optional parameters. +---- @param #SET_GROUP self +---- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_GROUP. The function needs to accept a CLIENT parameter. +---- @return #SET_GROUP self +--function SET_GROUP:ForEachClient( IteratorFunction, ... ) +-- self:F2( arg ) +-- +-- self:ForEach( IteratorFunction, arg, self.Clients ) +-- +-- return self +--end + + +--- +-- @param #SET_GROUP self +-- @param Wrapper.Group#GROUP MooseGroup +-- @return #SET_GROUP self +function SET_GROUP:IsIncludeObject( MooseGroup ) + self:F2( MooseGroup ) + local MooseGroupInclude = true + + if self.Filter.Coalitions then + local MooseGroupCoalition = false + for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do + self:T3( { "Coalition:", MooseGroup:GetCoalition(), self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) + if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == MooseGroup:GetCoalition() then + MooseGroupCoalition = true + 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 Core.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 ) ) + + 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 Core.Set#SET_UNIT self +-- @param Wrapper.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 ) + end + + return self +end + + +--- Finds a Unit based on the Unit Name. +-- @param #SET_UNIT self +-- @param #string UnitName +-- @return Wrapper.Unit#UNIT The found Unit. +function SET_UNIT:FindUnit( UnitName ) + + local UnitFound = self.Set[UnitName] + return UnitFound +end + + + +--- Builds a set of units of coalitions. +-- Possible current coalitions are red, blue and neutral. +-- @param #SET_UNIT self +-- @param #string Coalitions Can take the following values: "red", "blue", "neutral". +-- @return #SET_UNIT self +function SET_UNIT:FilterCoalitions( Coalitions ) + 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 + +--- Builds a set of units having a radar of give types. +-- All the units having a radar of a given type will be included within the set. +-- @param #SET_UNIT self +-- @param #table RadarTypes The radar types. +-- @return #SET_UNIT self +function SET_UNIT:FilterHasRadar( RadarTypes ) + + self.Filter.RadarTypes = self.Filter.RadarTypes or {} + if type( RadarTypes ) ~= "table" then + RadarTypes = { RadarTypes } + end + for RadarTypeID, RadarType in pairs( RadarTypes ) do + self.Filter.RadarTypes[RadarType] = RadarType + end + return self +end + +--- Builds a set of SEADable units. +-- @param #SET_UNIT self +-- @return #SET_UNIT self +function SET_UNIT:FilterHasSEAD() + + self.Filter.SEAD = true + return self +end + + + +--- 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 Core.Event#EVENTDATA Event +-- @return #string The name of the UNIT +-- @return #table The UNIT +function SET_UNIT:AddInDatabase( Event ) + self:F3( { Event } ) + + if Event.IniObjectCategory == 1 then + if not self.Database[Event.IniDCSUnitName] then + self.Database[Event.IniDCSUnitName] = UNIT:Register( Event.IniDCSUnitName ) + self:T3( self.Database[Event.IniDCSUnitName] ) + end + end + + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] +end + +--- Handles the Database to check on any event that Object exists in the Database. +-- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! +-- @param #SET_UNIT self +-- @param Core.Event#EVENTDATA Event +-- @return #string The name of the UNIT +-- @return #table The UNIT +function SET_UNIT:FindInDatabase( Event ) + self:E( { Event.IniDCSUnitName, self.Set[Event.IniDCSUnitName], Event } ) + + + return Event.IniDCSUnitName, self.Set[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 Core.Zone#ZONE ZoneObject The Zone to be tested for. +-- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the SET_UNIT. The function needs to accept a UNIT parameter. +-- @return #SET_UNIT self +function SET_UNIT:ForEachUnitCompletelyInZone( ZoneObject, IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self.Set, + --- @param Core.Zone#ZONE_BASE ZoneObject + -- @param Wrapper.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 Core.Zone#ZONE ZoneObject The Zone to be tested for. +-- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the SET_UNIT. The function needs to accept a UNIT parameter. +-- @return #SET_UNIT self +function SET_UNIT:ForEachUnitNotInZone( ZoneObject, IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self.Set, + --- @param Core.Zone#ZONE_BASE ZoneObject + -- @param Wrapper.Unit#UNIT UnitObject + function( ZoneObject, UnitObject ) + if UnitObject:IsNotInZone( ZoneObject ) then + return true + else + return false + end + end, { ZoneObject } ) + + return self +end + +--- Returns map of unit types. +-- @param #SET_UNIT self +-- @return #map<#string,#number> A map of the unit types found. The key is the UnitTypeName and the value is the amount of unit types found. +function SET_UNIT:GetUnitTypes() + self:F2() + + local MT = {} -- Message Text + local UnitTypes = {} + + for UnitID, UnitData in pairs( self:GetSet() ) do + local TextUnit = UnitData -- Wrapper.Unit#UNIT + if TextUnit:IsAlive() then + local UnitType = TextUnit:GetTypeName() + + if not UnitTypes[UnitType] then + UnitTypes[UnitType] = 1 + else + UnitTypes[UnitType] = UnitTypes[UnitType] + 1 + end + end + end + + for UnitTypeID, UnitType in pairs( UnitTypes ) do + MT[#MT+1] = UnitType .. " of " .. UnitTypeID + end + + return UnitTypes +end + + +--- Returns a comma separated string of the unit types with a count in the @{Set}. +-- @param #SET_UNIT self +-- @return #string The unit types string +function SET_UNIT:GetUnitTypesText() + self:F2() + + local MT = {} -- Message Text + local UnitTypes = self:GetUnitTypes() + + for UnitTypeID, UnitType in pairs( UnitTypes ) do + MT[#MT+1] = UnitType .. " of " .. UnitTypeID + end + + return table.concat( MT, ", " ) +end + +--- Returns map of unit threat levels. +-- @param #SET_UNIT self +-- @return #table. +function SET_UNIT:GetUnitThreatLevels() + self:F2() + + local UnitThreatLevels = {} + + for UnitID, UnitData in pairs( self:GetSet() ) do + local ThreatUnit = UnitData -- Wrapper.Unit#UNIT + if ThreatUnit:IsAlive() then + local UnitThreatLevel, UnitThreatLevelText = ThreatUnit:GetThreatLevel() + local ThreatUnitName = ThreatUnit:GetName() + + UnitThreatLevels[UnitThreatLevel] = UnitThreatLevels[UnitThreatLevel] or {} + UnitThreatLevels[UnitThreatLevel].UnitThreatLevelText = UnitThreatLevelText + UnitThreatLevels[UnitThreatLevel].Units = UnitThreatLevels[UnitThreatLevel].Units or {} + UnitThreatLevels[UnitThreatLevel].Units[ThreatUnitName] = ThreatUnit + end + end + + return UnitThreatLevels +end + +--- Calculate the maxium A2G threat level of the SET_UNIT. +-- @param #SET_UNIT self +function SET_UNIT:CalculateThreatLevelA2G() + + local MaxThreatLevelA2G = 0 + for UnitName, UnitData in pairs( self:GetSet() ) do + local ThreatUnit = UnitData -- Wrapper.Unit#UNIT + local ThreatLevelA2G = ThreatUnit:GetThreatLevel() + if ThreatLevelA2G > MaxThreatLevelA2G then + MaxThreatLevelA2G = ThreatLevelA2G + end + end + + self:T3( MaxThreatLevelA2G ) + return MaxThreatLevelA2G + +end + + +--- Returns if the @{Set} has targets having a radar (of a given type). +-- @param #SET_UNIT self +-- @param Dcs.DCSWrapper.Unit#Unit.RadarType RadarType +-- @return #number The amount of radars in the Set with the given type +function SET_UNIT:HasRadar( RadarType ) + self:F2( RadarType ) + + local RadarCount = 0 + for UnitID, UnitData in pairs( self:GetSet()) do + local UnitSensorTest = UnitData -- Wrapper.Unit#UNIT + local HasSensors + if RadarType then + HasSensors = UnitSensorTest:HasSensors( Unit.SensorType.RADAR, RadarType ) + else + HasSensors = UnitSensorTest:HasSensors( Unit.SensorType.RADAR ) + end + self:T3(HasSensors) + if HasSensors then + RadarCount = RadarCount + 1 + end + end + + return RadarCount +end + +--- Returns if the @{Set} has targets that can be SEADed. +-- @param #SET_UNIT self +-- @return #number The amount of SEADable units in the Set +function SET_UNIT:HasSEAD() + self:F2() + + local SEADCount = 0 + for UnitID, UnitData in pairs( self:GetSet()) do + local UnitSEAD = UnitData -- Wrapper.Unit#UNIT + if UnitSEAD:IsAlive() then + local UnitSEADAttributes = UnitSEAD:GetDesc().attributes + + local HasSEAD = UnitSEAD:HasSEAD() + + self:T3(HasSEAD) + if HasSEAD then + SEADCount = SEADCount + 1 + end + end + end + + return SEADCount +end + +--- Returns if the @{Set} has ground targets. +-- @param #SET_UNIT self +-- @return #number The amount of ground targets in the Set. +function SET_UNIT:HasGroundUnits() + self:F2() + + local GroundUnitCount = 0 + for UnitID, UnitData in pairs( self:GetSet()) do + local UnitTest = UnitData -- Wrapper.Unit#UNIT + if UnitTest:IsGround() then + GroundUnitCount = GroundUnitCount + 1 + end + end + + return GroundUnitCount +end + +--- Returns if the @{Set} has friendly ground units. +-- @param #SET_UNIT self +-- @return #number The amount of ground targets in the Set. +function SET_UNIT:HasFriendlyUnits( FriendlyCoalition ) + self:F2() + + local FriendlyUnitCount = 0 + for UnitID, UnitData in pairs( self:GetSet()) do + local UnitTest = UnitData -- Wrapper.Unit#UNIT + if UnitTest:IsFriendly( FriendlyCoalition ) then + FriendlyUnitCount = FriendlyUnitCount + 1 + end + end + + return FriendlyUnitCount +end + + + +----- Iterate the SET_UNIT and call an interator function for each **alive** player, providing the Unit of the player and optional parameters. +---- @param #SET_UNIT self +---- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_UNIT. The function needs to accept a UNIT parameter. +---- @return #SET_UNIT self +--function SET_UNIT:ForEachPlayer( IteratorFunction, ... ) +-- self:F2( arg ) +-- +-- self:ForEach( IteratorFunction, arg, self.PlayersAlive ) +-- +-- return self +--end +-- +-- +----- Iterate the SET_UNIT and call an interator function for each client, providing the Client to the function and optional parameters. +---- @param #SET_UNIT self +---- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_UNIT. The function needs to accept a CLIENT parameter. +---- @return #SET_UNIT self +--function SET_UNIT:ForEachClient( IteratorFunction, ... ) +-- self:F2( arg ) +-- +-- self:ForEach( IteratorFunction, arg, self.Clients ) +-- +-- return self +--end + + +--- +-- @param #SET_UNIT self +-- @param Wrapper.Unit#UNIT MUnit +-- @return #SET_UNIT self +function SET_UNIT:IsIncludeObject( MUnit ) + self:F2( MUnit ) + local MUnitInclude = 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 + + if self.Filter.RadarTypes then + local MUnitRadar = false + for RadarTypeID, RadarType in pairs( self.Filter.RadarTypes ) do + self:T3( { "Radar:", RadarType } ) + if MUnit:HasSensors( Unit.SensorType.RADAR, RadarType ) == true then + if MUnit:GetRadar() == true then -- This call is necessary to evaluate the SEAD capability. + self:T3( "RADAR Found" ) + end + MUnitRadar = true + end + end + MUnitInclude = MUnitInclude and MUnitRadar + end + + if self.Filter.SEAD then + local MUnitSEAD = false + if MUnit:HasSEAD() == true then + self:T3( "SEAD Found" ) + MUnitSEAD = true + end + MUnitInclude = MUnitInclude and MUnitSEAD + end + + self:T2( MUnitInclude ) + return MUnitInclude +end + + +--- SET_CLIENT + +--- SET_CLIENT class +-- @type SET_CLIENT +-- @extends Core.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 Core.Set#SET_CLIENT self +-- @param #string AddClientNames A single name or an array of CLIENT names. +-- @return self +function SET_CLIENT:AddClientsByName( AddClientNames ) + + local AddClientNamesArray = ( type( AddClientNames ) == "table" ) and AddClientNames or { AddClientNames } + + for AddClientID, AddClientName in pairs( AddClientNamesArray ) do + self:Add( AddClientName, CLIENT:FindByName( AddClientName ) ) + end + + return self +end + +--- Remove CLIENT(s) from SET_CLIENT. +-- @param Core.Set#SET_CLIENT self +-- @param Wrapper.Client#CLIENT RemoveClientNames A single name or an array of CLIENT names. +-- @return self +function SET_CLIENT:RemoveClientsByName( RemoveClientNames ) + + local RemoveClientNamesArray = ( type( RemoveClientNames ) == "table" ) and RemoveClientNames or { RemoveClientNames } + + for RemoveClientID, RemoveClientName in pairs( RemoveClientNamesArray ) do + self:Remove( RemoveClientName.ClientName ) + end + + return self +end + + +--- Finds a Client based on the Client Name. +-- @param #SET_CLIENT self +-- @param #string ClientName +-- @return Wrapper.Client#CLIENT The found Client. +function SET_CLIENT:FindClient( ClientName ) + + local ClientFound = self.Set[ClientName] + return ClientFound +end + + + +--- Builds a set of clients of coalitions. +-- Possible current coalitions are red, blue and neutral. +-- @param #SET_CLIENT self +-- @param #string Coalitions Can take the following values: "red", "blue", "neutral". +-- @return #SET_CLIENT self +function SET_CLIENT:FilterCoalitions( Coalitions ) + if not self.Filter.Coalitions then + self.Filter.Coalitions = {} + end + if type( Coalitions ) ~= "table" then + Coalitions = { Coalitions } + end + for CoalitionID, Coalition in pairs( Coalitions ) do + self.Filter.Coalitions[Coalition] = Coalition + end + return self +end + + +--- Builds a set of clients out of categories. +-- Possible current categories are plane, helicopter, ground, ship. +-- @param #SET_CLIENT self +-- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship". +-- @return #SET_CLIENT self +function SET_CLIENT:FilterCategories( Categories ) + if not self.Filter.Categories then + self.Filter.Categories = {} + end + if type( Categories ) ~= "table" then + Categories = { Categories } + end + for CategoryID, Category in pairs( Categories ) do + self.Filter.Categories[Category] = Category + end + return self +end + + +--- Builds a set of clients of defined client types. +-- Possible current types are those types known within DCS world. +-- @param #SET_CLIENT self +-- @param #string Types Can take those type strings known within DCS world. +-- @return #SET_CLIENT self +function SET_CLIENT:FilterTypes( Types ) + if not self.Filter.Types then + self.Filter.Types = {} + end + if type( Types ) ~= "table" then + Types = { Types } + end + for TypeID, Type in pairs( Types ) do + self.Filter.Types[Type] = Type + end + return self +end + + +--- Builds a set of clients of defined countries. +-- Possible current countries are those known within DCS world. +-- @param #SET_CLIENT self +-- @param #string Countries Can take those country strings known within DCS world. +-- @return #SET_CLIENT self +function SET_CLIENT:FilterCountries( Countries ) + if not self.Filter.Countries then + self.Filter.Countries = {} + end + if type( Countries ) ~= "table" then + Countries = { Countries } + end + for CountryID, Country in pairs( Countries ) do + self.Filter.Countries[Country] = Country + end + return self +end + + +--- Builds a set of clients 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 Core.Event#EVENTDATA Event +-- @return #string The name of the CLIENT +-- @return #table The CLIENT +function SET_CLIENT:AddInDatabase( Event ) + self:F3( { Event } ) + + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] +end + +--- Handles the Database to check on any event that Object exists in the Database. +-- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! +-- @param #SET_CLIENT self +-- @param Core.Event#EVENTDATA Event +-- @return #string The name of the CLIENT +-- @return #table The CLIENT +function SET_CLIENT:FindInDatabase( Event ) + self:F3( { Event } ) + + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] +end + +--- Iterate the SET_CLIENT and call an interator function for each **alive** CLIENT, providing the CLIENT and optional parameters. +-- @param #SET_CLIENT self +-- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_CLIENT. The function needs to accept a CLIENT parameter. +-- @return #SET_CLIENT self +function SET_CLIENT:ForEachClient( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self.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 Core.Zone#ZONE ZoneObject The Zone to be tested for. +-- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_CLIENT. The function needs to accept a CLIENT parameter. +-- @return #SET_CLIENT self +function SET_CLIENT:ForEachClientInZone( ZoneObject, IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self.Set, + --- @param Core.Zone#ZONE_BASE ZoneObject + -- @param Wrapper.Client#CLIENT ClientObject + function( ZoneObject, ClientObject ) + if ClientObject:IsInZone( ZoneObject ) then + return true + else + return false + end + end, { ZoneObject } ) + + return self +end + +--- Iterate the SET_CLIENT and call an iterator function for each **alive** CLIENT presence not in a @{Zone}, providing the CLIENT and optional parameters to the called function. +-- @param #SET_CLIENT self +-- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. +-- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_CLIENT. The function needs to accept a CLIENT parameter. +-- @return #SET_CLIENT self +function SET_CLIENT:ForEachClientNotInZone( ZoneObject, IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self.Set, + --- @param Core.Zone#ZONE_BASE ZoneObject + -- @param Wrapper.Client#CLIENT ClientObject + function( ZoneObject, ClientObject ) + if ClientObject:IsNotInZone( ZoneObject ) then + return true + else + return false + end + end, { ZoneObject } ) + + return self +end + +--- +-- @param #SET_CLIENT self +-- @param Wrapper.Client#CLIENT MClient +-- @return #SET_CLIENT self +function SET_CLIENT:IsIncludeObject( MClient ) + self:F2( MClient ) + + local MClientInclude = true + + if MClient then + local MClientName = MClient.UnitName + + if self.Filter.Coalitions then + local MClientCoalition = false + for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do + local ClientCoalitionID = _DATABASE:GetCoalitionFromClientTemplate( MClientName ) + self:T3( { "Coalition:", ClientCoalitionID, self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) + if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == ClientCoalitionID then + MClientCoalition = true + end + end + self:T( { "Evaluated Coalition", MClientCoalition } ) + MClientInclude = MClientInclude and MClientCoalition + end + + if self.Filter.Categories then + local MClientCategory = false + for CategoryID, CategoryName in pairs( self.Filter.Categories ) do + local ClientCategoryID = _DATABASE:GetCategoryFromClientTemplate( MClientName ) + self:T3( { "Category:", ClientCategoryID, self.FilterMeta.Categories[CategoryName], CategoryName } ) + if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == ClientCategoryID then + MClientCategory = true + end + end + self:T( { "Evaluated Category", MClientCategory } ) + MClientInclude = MClientInclude and MClientCategory + end + + if self.Filter.Types then + local MClientType = false + for TypeID, TypeName in pairs( self.Filter.Types ) do + self:T3( { "Type:", MClient:GetTypeName(), TypeName } ) + if TypeName == MClient:GetTypeName() then + MClientType = true + end + end + self:T( { "Evaluated Type", MClientType } ) + MClientInclude = MClientInclude and MClientType + end + + if self.Filter.Countries then + local MClientCountry = false + for CountryID, CountryName in pairs( self.Filter.Countries ) do + local ClientCountryID = _DATABASE:GetCountryFromClientTemplate(MClientName) + self:T3( { "Country:", ClientCountryID, country.id[CountryName], CountryName } ) + if country.id[CountryName] and country.id[CountryName] == ClientCountryID then + MClientCountry = true + end + end + self:T( { "Evaluated Country", MClientCountry } ) + MClientInclude = MClientInclude and MClientCountry + end + + if self.Filter.ClientPrefixes then + local MClientPrefix = false + for ClientPrefixId, ClientPrefix in pairs( self.Filter.ClientPrefixes ) do + self:T3( { "Prefix:", string.find( MClient.UnitName, ClientPrefix, 1 ), ClientPrefix } ) + if string.find( MClient.UnitName, ClientPrefix, 1 ) then + MClientPrefix = true + end + end + self:T( { "Evaluated Prefix", MClientPrefix } ) + MClientInclude = MClientInclude and MClientPrefix + end + end + + self:T2( MClientInclude ) + return MClientInclude +end + +--- SET_AIRBASE + +--- SET_AIRBASE class +-- @type SET_AIRBASE +-- @extends Core.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 Core.Set#SET_AIRBASE self +-- @param #string AddAirbaseNames A single name or an array of AIRBASE names. +-- @return self +function SET_AIRBASE:AddAirbasesByName( AddAirbaseNames ) + + local AddAirbaseNamesArray = ( type( AddAirbaseNames ) == "table" ) and AddAirbaseNames or { AddAirbaseNames } + + for AddAirbaseID, AddAirbaseName in pairs( AddAirbaseNamesArray ) do + self:Add( AddAirbaseName, AIRBASE:FindByName( AddAirbaseName ) ) + end + + return self +end + +--- Remove AIRBASEs from SET_AIRBASE. +-- @param Core.Set#SET_AIRBASE self +-- @param Wrapper.Airbase#AIRBASE RemoveAirbaseNames A single name or an array of AIRBASE names. +-- @return self +function SET_AIRBASE:RemoveAirbasesByName( RemoveAirbaseNames ) + + local RemoveAirbaseNamesArray = ( type( RemoveAirbaseNames ) == "table" ) and RemoveAirbaseNames or { RemoveAirbaseNames } + + for RemoveAirbaseID, RemoveAirbaseName in pairs( RemoveAirbaseNamesArray ) do + self:Remove( RemoveAirbaseName.AirbaseName ) + end + + return self +end + + +--- Finds a Airbase based on the Airbase Name. +-- @param #SET_AIRBASE self +-- @param #string AirbaseName +-- @return Wrapper.Airbase#AIRBASE The found Airbase. +function SET_AIRBASE:FindAirbase( AirbaseName ) + + local AirbaseFound = self.Set[AirbaseName] + return AirbaseFound +end + + + +--- 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 Core.Event#EVENTDATA Event +-- @return #string The name of the AIRBASE +-- @return #table The AIRBASE +function SET_AIRBASE:AddInDatabase( Event ) + self:F3( { Event } ) + + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] +end + +--- Handles the Database to check on any event that Object exists in the Database. +-- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! +-- @param #SET_AIRBASE self +-- @param Core.Event#EVENTDATA Event +-- @return #string The name of the AIRBASE +-- @return #table The AIRBASE +function SET_AIRBASE:FindInDatabase( Event ) + self:F3( { Event } ) + + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] +end + +--- Iterate the SET_AIRBASE and call an interator function for each AIRBASE, providing the AIRBASE and optional parameters. +-- @param #SET_AIRBASE self +-- @param #function IteratorFunction The function that will be called when there is an alive AIRBASE in the SET_AIRBASE. The function needs to accept a AIRBASE parameter. +-- @return #SET_AIRBASE self +function SET_AIRBASE:ForEachAirbase( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self.Set ) + + return self +end + +--- Iterate the SET_AIRBASE while identifying the nearest @{Airbase#AIRBASE} from a @{Point#POINT_VEC2}. +-- @param #SET_AIRBASE self +-- @param Core.Point#POINT_VEC2 PointVec2 A @{Point#POINT_VEC2} object from where to evaluate the closest @{Airbase#AIRBASE}. +-- @return Wrapper.Airbase#AIRBASE The closest @{Airbase#AIRBASE}. +function SET_AIRBASE:FindNearestAirbaseFromPointVec2( PointVec2 ) + self:F2( PointVec2 ) + + local NearestAirbase = self:FindNearestObjectFromPointVec2( PointVec2 ) + return NearestAirbase +end + + + +--- +-- @param #SET_AIRBASE self +-- @param Wrapper.Airbase#AIRBASE MAirbase +-- @return #SET_AIRBASE self +function SET_AIRBASE:IsIncludeObject( MAirbase ) + self:F2( MAirbase ) + + local MAirbaseInclude = true + + if MAirbase then + local MAirbaseName = MAirbase:GetName() + + if self.Filter.Coalitions then + local MAirbaseCoalition = false + for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do + local AirbaseCoalitionID = _DATABASE:GetCoalitionFromAirbase( MAirbaseName ) + self:T3( { "Coalition:", AirbaseCoalitionID, self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) + if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == AirbaseCoalitionID then + MAirbaseCoalition = true + end + end + self:T( { "Evaluated Coalition", MAirbaseCoalition } ) + MAirbaseInclude = MAirbaseInclude and MAirbaseCoalition + end + + if self.Filter.Categories then + local MAirbaseCategory = false + for CategoryID, CategoryName in pairs( self.Filter.Categories ) do + local AirbaseCategoryID = _DATABASE:GetCategoryFromAirbase( MAirbaseName ) + self:T3( { "Category:", AirbaseCategoryID, self.FilterMeta.Categories[CategoryName], CategoryName } ) + if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == AirbaseCategoryID then + MAirbaseCategory = true + end + end + self:T( { "Evaluated Category", MAirbaseCategory } ) + MAirbaseInclude = MAirbaseInclude and MAirbaseCategory + end + end + + self:T2( MAirbaseInclude ) + return MAirbaseInclude +end +--- 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. +-- +-- **Important Note:** Most of the functions in this section were taken from MIST, and reworked to OO concepts. +-- In order to keep the credibility of the the author, I want to emphasize that the of the MIST framework was created by Grimes, who you can find on the Eagle Dynamics Forums. +-- +-- ## 1.1) POINT_VEC3 constructor +-- +-- A new POINT_VEC3 instance can be created with: +-- +-- * @{Point#POINT_VEC3.New}(): a 3D point. +-- * @{Point#POINT_VEC3.NewFromVec3}(): a 3D point created from a @{DCSTypes#Vec3}. +-- +-- ## 1.2) Manupulate the X, Y, Z coordinates of the point +-- +-- A POINT_VEC3 class works in 3D space. It contains internally an X, Y, Z coordinate. +-- Methods exist to manupulate these coordinates. +-- +-- The current X, Y, Z axis can be retrieved with the methods @{#POINT_VEC3.GetX}(), @{#POINT_VEC3.GetY}(), @{#POINT_VEC3.GetZ}() respectively. +-- The methods @{#POINT_VEC3.SetX}(), @{#POINT_VEC3.SetY}(), @{#POINT_VEC3.SetZ}() change the respective axis with a new value. +-- The current axis values can be changed by using the methods @{#POINT_VEC3.AddX}(), @{#POINT_VEC3.AddY}(), @{#POINT_VEC3.AddZ}() +-- to add or substract a value from the current respective axis value. +-- Note that the Set and Add methods return the current POINT_VEC3 object, so these manipulation methods can be chained... For example: +-- +-- local Vec3 = PointVec3:AddX( 100 ):AddZ( 150 ):GetVec3() +-- +-- ## 1.3) Create waypoints for routes +-- +-- A POINT_VEC3 can prepare waypoints for Ground, Air and Naval groups to be embedded into a Route. +-- +-- +-- ## 1.5) Smoke, flare, explode, illuminate +-- +-- At the point a smoke, flare, explosion and illumination bomb can be triggered. Use the following methods: +-- +-- ### 1.5.1) Smoke +-- +-- * @{#POINT_VEC3.Smoke}(): To smoke the point in a certain color. +-- * @{#POINT_VEC3.SmokeBlue}(): To smoke the point in blue. +-- * @{#POINT_VEC3.SmokeRed}(): To smoke the point in red. +-- * @{#POINT_VEC3.SmokeOrange}(): To smoke the point in orange. +-- * @{#POINT_VEC3.SmokeWhite}(): To smoke the point in white. +-- * @{#POINT_VEC3.SmokeGreen}(): To smoke the point in green. +-- +-- ### 1.5.2) Flare +-- +-- * @{#POINT_VEC3.Flare}(): To flare the point in a certain color. +-- * @{#POINT_VEC3.FlareRed}(): To flare the point in red. +-- * @{#POINT_VEC3.FlareYellow}(): To flare the point in yellow. +-- * @{#POINT_VEC3.FlareWhite}(): To flare the point in white. +-- * @{#POINT_VEC3.FlareGreen}(): To flare the point in green. +-- +-- ### 1.5.3) Explode +-- +-- * @{#POINT_VEC3.Explosion}(): To explode the point with a certain intensity. +-- +-- ### 1.5.4) Illuminate +-- +-- * @{#POINT_VEC3.IlluminationBomb}(): To illuminate the 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_VEC2 instance can be created with: +-- +-- * @{Point#POINT_VEC2.New}(): a 2D point, taking an additional height parameter. +-- * @{Point#POINT_VEC2.NewFromVec2}(): a 2D point created from a @{DCSTypes#Vec2}. +-- +-- ## 1.2) Manupulate the X, Altitude, Y coordinates of the 2D point +-- +-- A POINT_VEC2 class works in 2D space, with an altitude setting. It contains internally an X, Altitude, Y coordinate. +-- Methods exist to manupulate these coordinates. +-- +-- The current X, Altitude, Y axis can be retrieved with the methods @{#POINT_VEC2.GetX}(), @{#POINT_VEC2.GetAlt}(), @{#POINT_VEC2.GetY}() respectively. +-- The methods @{#POINT_VEC2.SetX}(), @{#POINT_VEC2.SetAlt}(), @{#POINT_VEC2.SetY}() change the respective axis with a new value. +-- The current axis values can be changed by using the methods @{#POINT_VEC2.AddX}(), @{#POINT_VEC2.AddAlt}(), @{#POINT_VEC2.AddY}() +-- to add or substract a value from the current respective axis value. +-- Note that the Set and Add methods return the current POINT_VEC2 object, so these manipulation methods can be chained... For example: +-- +-- local Vec2 = PointVec2:AddX( 100 ):AddY( 2000 ):GetVec2() +-- +-- === +-- +-- **API CHANGE HISTORY** +-- ====================== +-- +-- The underlying change log documents the API changes. Please read this carefully. The following notation is used: +-- +-- * **Added** parts are expressed in bold type face. +-- * _Removed_ parts are expressed in italic type face. +-- +-- Hereby the change log: +-- +-- 2017-03-03: POINT\_VEC3:**Explosion( ExplosionIntensity )** added. +-- 2017-03-03: POINT\_VEC3:**IlluminationBomb()** added. +-- +-- 2017-02-18: POINT\_VEC3:**NewFromVec2( Vec2, LandHeightAdd )** added. +-- +-- 2016-08-12: POINT\_VEC3:**Translate( Distance, Angle )** added. +-- +-- 2016-08-06: Made PointVec3 and Vec3, PointVec2 and Vec2 terminology used in the code consistent. +-- +-- * Replaced method _Point_Vec3() to **Vec3**() where the code manages a Vec3. Replaced all references to the method. +-- * Replaced method _Point_Vec2() to **Vec2**() where the code manages a Vec2. Replaced all references to the method. +-- * Replaced method Random_Point_Vec3() to **RandomVec3**() where the code manages a Vec3. Replaced all references to the method. +-- . +-- === +-- +-- ### Authors: +-- +-- * FlightControl : Design & Programming +-- +-- ### Contributions: +-- +-- @module Point + +--- The POINT_VEC3 class +-- @type POINT_VEC3 +-- @field #number x The x coordinate in 3D space. +-- @field #number y The y coordinate in 3D space. +-- @field #number z The z coordiante in 3D space. +-- @field Utilities.Utils#SMOKECOLOR SmokeColor +-- @field Utilities.Utils#FLARECOLOR FlareColor +-- @field #POINT_VEC3.RoutePointAltType RoutePointAltType +-- @field #POINT_VEC3.RoutePointType RoutePointType +-- @field #POINT_VEC3.RoutePointAction RoutePointAction +-- @extends Core.Base#BASE +POINT_VEC3 = { + ClassName = "POINT_VEC3", + Metric = true, + RoutePointAltType = { + BARO = "BARO", + }, + RoutePointType = { + TakeOffParking = "TakeOffParking", + TurningPoint = "Turning Point", + }, + RoutePointAction = { + FromParkingArea = "From Parking Area", + TurningPoint = "Turning Point", + }, +} + +--- The POINT_VEC2 class +-- @type POINT_VEC2 +-- @field Dcs.DCSTypes#Distance x The x coordinate in meters. +-- @field Dcs.DCSTypes#Distance y the y coordinate in meters. +-- @extends Core.Point#POINT_VEC3 +POINT_VEC2 = { + ClassName = "POINT_VEC2", +} + + +do -- POINT_VEC3 + +--- RoutePoint AltTypes +-- @type POINT_VEC3.RoutePointAltType +-- @field BARO "BARO" + +--- RoutePoint Types +-- @type POINT_VEC3.RoutePointType +-- @field TakeOffParking "TakeOffParking" +-- @field TurningPoint "Turning Point" + +--- RoutePoint Actions +-- @type POINT_VEC3.RoutePointAction +-- @field FromParkingArea "From Parking Area" +-- @field TurningPoint "Turning Point" + +-- Constructor. + +--- Create a new POINT_VEC3 object. +-- @param #POINT_VEC3 self +-- @param Dcs.DCSTypes#Distance x The x coordinate of the Vec3 point, pointing to the North. +-- @param Dcs.DCSTypes#Distance y The y coordinate of the Vec3 point, pointing Upwards. +-- @param Dcs.DCSTypes#Distance z The z coordinate of the Vec3 point, pointing to the Right. +-- @return Core.Point#POINT_VEC3 self +function POINT_VEC3:New( x, y, z ) + + local self = BASE:Inherit( self, BASE:New() ) + self.x = x + self.y = y + self.z = z + + return self +end + +--- Create a new POINT_VEC3 object from Vec2 coordinates. +-- @param #POINT_VEC3 self +-- @param Dcs.DCSTypes#Vec2 Vec2 The Vec2 point. +-- @return Core.Point#POINT_VEC3 self +function POINT_VEC3:NewFromVec2( Vec2, LandHeightAdd ) + + local LandHeight = land.getHeight( Vec2 ) + + LandHeightAdd = LandHeightAdd or 0 + LandHeight = LandHeight + LandHeightAdd + + self = self:New( Vec2.x, LandHeight, Vec2.y ) + + self:F2( self ) + + return self +end + +--- Create a new POINT_VEC3 object from Vec3 coordinates. +-- @param #POINT_VEC3 self +-- @param Dcs.DCSTypes#Vec3 Vec3 The Vec3 point. +-- @return Core.Point#POINT_VEC3 self +function POINT_VEC3:NewFromVec3( Vec3 ) + + self = self:New( Vec3.x, Vec3.y, Vec3.z ) + self:F2( self ) + return self +end + + +--- Return the coordinates of the POINT_VEC3 in Vec3 format. +-- @param #POINT_VEC3 self +-- @return Dcs.DCSTypes#Vec3 The Vec3 coodinate. +function POINT_VEC3:GetVec3() + return { x = self.x, y = self.y, z = self.z } +end + +--- Return the coordinates of the POINT_VEC3 in Vec2 format. +-- @param #POINT_VEC3 self +-- @return Dcs.DCSTypes#Vec2 The Vec2 coodinate. +function POINT_VEC3:GetVec2() + return { x = self.x, y = self.z } +end + + +--- Return the x coordinate of the POINT_VEC3. +-- @param #POINT_VEC3 self +-- @return #number The x coodinate. +function POINT_VEC3:GetX() + return self.x +end + +--- Return the y coordinate of the POINT_VEC3. +-- @param #POINT_VEC3 self +-- @return #number The y coodinate. +function POINT_VEC3:GetY() + return self.y +end + +--- Return the z coordinate of the POINT_VEC3. +-- @param #POINT_VEC3 self +-- @return #number The z coodinate. +function POINT_VEC3:GetZ() + return self.z +end + +--- Set the x coordinate of the POINT_VEC3. +-- @param #POINT_VEC3 self +-- @param #number x The x coordinate. +-- @return #POINT_VEC3 +function POINT_VEC3:SetX( x ) + self.x = x + return self +end + +--- Set the y coordinate of the POINT_VEC3. +-- @param #POINT_VEC3 self +-- @param #number y The y coordinate. +-- @return #POINT_VEC3 +function POINT_VEC3:SetY( y ) + self.y = y + return self +end + +--- Set the z coordinate of the POINT_VEC3. +-- @param #POINT_VEC3 self +-- @param #number z The z coordinate. +-- @return #POINT_VEC3 +function POINT_VEC3:SetZ( z ) + self.z = z + return self +end + +--- Add to the x coordinate of the POINT_VEC3. +-- @param #POINT_VEC3 self +-- @param #number x The x coordinate value to add to the current x coodinate. +-- @return #POINT_VEC3 +function POINT_VEC3:AddX( x ) + self.x = self.x + x + return self +end + +--- Add to the y coordinate of the POINT_VEC3. +-- @param #POINT_VEC3 self +-- @param #number y The y coordinate value to add to the current y coodinate. +-- @return #POINT_VEC3 +function POINT_VEC3:AddY( y ) + self.y = self.y + y + return self +end + +--- Add to the z coordinate of the POINT_VEC3. +-- @param #POINT_VEC3 self +-- @param #number z The z coordinate value to add to the current z coodinate. +-- @return #POINT_VEC3 +function POINT_VEC3:AddZ( z ) + self.z = self.z +z + return self +end + +--- Return a random Vec2 within an Outer Radius and optionally NOT within an Inner Radius of the POINT_VEC3. +-- @param #POINT_VEC3 self +-- @param Dcs.DCSTypes#Distance OuterRadius +-- @param Dcs.DCSTypes#Distance InnerRadius +-- @return Dcs.DCSTypes#Vec2 Vec2 +function POINT_VEC3:GetRandomVec2InRadius( OuterRadius, InnerRadius ) + self:F2( { OuterRadius, InnerRadius } ) + + local Theta = 2 * math.pi * math.random() + local Radials = math.random() + math.random() + if Radials > 1 then + Radials = 2 - Radials + end + + local RadialMultiplier + if InnerRadius and InnerRadius <= OuterRadius then + RadialMultiplier = ( OuterRadius - InnerRadius ) * Radials + InnerRadius + else + RadialMultiplier = OuterRadius * Radials + end + + local RandomVec2 + if OuterRadius > 0 then + RandomVec2 = { x = math.cos( Theta ) * RadialMultiplier + self:GetX(), y = math.sin( Theta ) * RadialMultiplier + self:GetZ() } + else + RandomVec2 = { x = self:GetX(), y = self:GetZ() } + end + + return RandomVec2 +end + +--- Return a random POINT_VEC2 within an Outer Radius and optionally NOT within an Inner Radius of the POINT_VEC3. +-- @param #POINT_VEC3 self +-- @param Dcs.DCSTypes#Distance OuterRadius +-- @param Dcs.DCSTypes#Distance InnerRadius +-- @return #POINT_VEC2 +function POINT_VEC3:GetRandomPointVec2InRadius( OuterRadius, InnerRadius ) + self:F2( { OuterRadius, InnerRadius } ) + + return POINT_VEC2:NewFromVec2( self:GetRandomVec2InRadius( OuterRadius, InnerRadius ) ) +end + +--- Return a random Vec3 within an Outer Radius and optionally NOT within an Inner Radius of the POINT_VEC3. +-- @param #POINT_VEC3 self +-- @param Dcs.DCSTypes#Distance OuterRadius +-- @param Dcs.DCSTypes#Distance InnerRadius +-- @return Dcs.DCSTypes#Vec3 Vec3 +function POINT_VEC3:GetRandomVec3InRadius( OuterRadius, InnerRadius ) + + local RandomVec2 = self:GetRandomVec2InRadius( OuterRadius, InnerRadius ) + local y = self:GetY() + math.random( InnerRadius, OuterRadius ) + local RandomVec3 = { x = RandomVec2.x, y = y, z = RandomVec2.z } + + return RandomVec3 +end + +--- Return a random POINT_VEC3 within an Outer Radius and optionally NOT within an Inner Radius of the POINT_VEC3. +-- @param #POINT_VEC3 self +-- @param Dcs.DCSTypes#Distance OuterRadius +-- @param Dcs.DCSTypes#Distance InnerRadius +-- @return #POINT_VEC3 +function POINT_VEC3:GetRandomPointVec3InRadius( OuterRadius, InnerRadius ) + + return POINT_VEC3:NewFromVec3( self:GetRandomVec3InRadius( OuterRadius, InnerRadius ) ) +end + + +--- Return a direction vector Vec3 from POINT_VEC3 to the POINT_VEC3. +-- @param #POINT_VEC3 self +-- @param #POINT_VEC3 TargetPointVec3 The target POINT_VEC3. +-- @return Dcs.DCSTypes#Vec3 DirectionVec3 The direction vector in Vec3 format. +function POINT_VEC3:GetDirectionVec3( TargetPointVec3 ) + return { x = TargetPointVec3:GetX() - self:GetX(), y = TargetPointVec3:GetY() - self:GetY(), z = TargetPointVec3:GetZ() - self:GetZ() } +end + +--- Get a correction in radians of the real magnetic north of the POINT_VEC3. +-- @param #POINT_VEC3 self +-- @return #number CorrectionRadians The correction in radians. +function POINT_VEC3:GetNorthCorrectionRadians() + local TargetVec3 = self:GetVec3() + local lat, lon = coord.LOtoLL(TargetVec3) + local north_posit = coord.LLtoLO(lat + 1, lon) + return math.atan2( north_posit.z - TargetVec3.z, north_posit.x - TargetVec3.x ) +end + + +--- Return a direction in radians from the POINT_VEC3 using a direction vector in Vec3 format. +-- @param #POINT_VEC3 self +-- @param Dcs.DCSTypes#Vec3 DirectionVec3 The direction vector in Vec3 format. +-- @return #number DirectionRadians The direction in radians. +function POINT_VEC3:GetDirectionRadians( DirectionVec3 ) + local DirectionRadians = math.atan2( DirectionVec3.z, DirectionVec3.x ) + --DirectionRadians = DirectionRadians + self:GetNorthCorrectionRadians() + if DirectionRadians < 0 then + DirectionRadians = DirectionRadians + 2 * math.pi -- put dir in range of 0 to 2*pi ( the full circle ) + end + return DirectionRadians +end + +--- Return the 2D distance in meters between the target POINT_VEC3 and the POINT_VEC3. +-- @param #POINT_VEC3 self +-- @param #POINT_VEC3 TargetPointVec3 The target POINT_VEC3. +-- @return Dcs.DCSTypes#Distance Distance The distance in meters. +function POINT_VEC3:Get2DDistance( TargetPointVec3 ) + local TargetVec3 = TargetPointVec3:GetVec3() + local SourceVec3 = self:GetVec3() + return ( ( TargetVec3.x - SourceVec3.x ) ^ 2 + ( TargetVec3.z - SourceVec3.z ) ^ 2 ) ^ 0.5 +end + +--- Return the 3D distance in meters between the target POINT_VEC3 and the POINT_VEC3. +-- @param #POINT_VEC3 self +-- @param #POINT_VEC3 TargetPointVec3 The target POINT_VEC3. +-- @return Dcs.DCSTypes#Distance Distance The distance in meters. +function POINT_VEC3:Get3DDistance( TargetPointVec3 ) + local TargetVec3 = TargetPointVec3:GetVec3() + local SourceVec3 = self:GetVec3() + return ( ( TargetVec3.x - SourceVec3.x ) ^ 2 + ( TargetVec3.y - SourceVec3.y ) ^ 2 + ( TargetVec3.z - SourceVec3.z ) ^ 2 ) ^ 0.5 +end + +--- Provides a Bearing / Range string +-- @param #POINT_VEC3 self +-- @param #number AngleRadians The angle in randians +-- @param #number Distance The distance +-- @return #string The BR Text +function POINT_VEC3:ToStringBR( AngleRadians, Distance ) + + AngleRadians = UTILS.Round( UTILS.ToDegree( AngleRadians ), 0 ) + if self:IsMetric() then + Distance = UTILS.Round( Distance / 1000, 2 ) + else + Distance = UTILS.Round( UTILS.MetersToNM( Distance ), 2 ) + end + + local s = string.format( '%03d', AngleRadians ) .. ' for ' .. Distance + + s = s .. self:GetAltitudeText() -- When the POINT is a VEC2, there will be no altitude shown. + + return s +end + +--- Provides a Bearing / Range string +-- @param #POINT_VEC3 self +-- @param #number AngleRadians The angle in randians +-- @param #number Distance The distance +-- @return #string The BR Text +function POINT_VEC3:ToStringLL( acc, DMS ) + + acc = acc or 3 + local lat, lon = coord.LOtoLL( self:GetVec3() ) + return UTILS.tostringLL(lat, lon, acc, DMS) +end + +--- Return the altitude text of the POINT_VEC3. +-- @param #POINT_VEC3 self +-- @return #string Altitude text. +function POINT_VEC3:GetAltitudeText() + if self:IsMetric() then + return ' at ' .. UTILS.Round( self:GetY(), 0 ) + else + return ' at ' .. UTILS.Round( UTILS.MetersToFeet( self:GetY() ), 0 ) + end +end + +--- Return a BR string from a POINT_VEC3 to the POINT_VEC3. +-- @param #POINT_VEC3 self +-- @param #POINT_VEC3 TargetPointVec3 The target POINT_VEC3. +-- @return #string The BR text. +function POINT_VEC3:GetBRText( TargetPointVec3 ) + local DirectionVec3 = self:GetDirectionVec3( TargetPointVec3 ) + local AngleRadians = self:GetDirectionRadians( DirectionVec3 ) + local Distance = self:Get2DDistance( TargetPointVec3 ) + return self:ToStringBR( AngleRadians, Distance ) +end + +--- Sets the POINT_VEC3 metric or NM. +-- @param #POINT_VEC3 self +-- @param #boolean Metric true means metric, false means NM. +function POINT_VEC3:SetMetric( Metric ) + self.Metric = Metric +end + +--- Gets if the POINT_VEC3 is metric or NM. +-- @param #POINT_VEC3 self +-- @return #boolean Metric true means metric, false means NM. +function POINT_VEC3:IsMetric() + return self.Metric +end + +--- Add a Distance in meters from the POINT_VEC3 horizontal plane, with the given angle, and calculate the new POINT_VEC3. +-- @param #POINT_VEC3 self +-- @param Dcs.DCSTypes#Distance Distance The Distance to be added in meters. +-- @param Dcs.DCSTypes#Angle Angle The Angle in degrees. +-- @return #POINT_VEC3 The new calculated POINT_VEC3. +function POINT_VEC3:Translate( Distance, Angle ) + local SX = self:GetX() + local SZ = self:GetZ() + local Radians = Angle / 180 * math.pi + local TX = Distance * math.cos( Radians ) + SX + local TZ = Distance * math.sin( Radians ) + SZ + + return POINT_VEC3:New( TX, self:GetY(), TZ ) +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 Dcs.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:GetX() + RoutePoint.y = self:GetZ() + RoutePoint.alt = self:GetY() + 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 + +--- Build an ground type route point. +-- @param #POINT_VEC3 self +-- @param Dcs.DCSTypes#Speed Speed Speed in km/h. +-- @param #POINT_VEC3.RoutePointAction Formation The route point Formation. +-- @return #table The route point. +function POINT_VEC3:RoutePointGround( Speed, Formation ) + self:F2( { Formation, Speed } ) + + local RoutePoint = {} + RoutePoint.x = self:GetX() + RoutePoint.y = self:GetZ() + + RoutePoint.action = Formation or "" + + + 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 + +--- Creates an explosion at the point of a certain intensity. +-- @param #POINT_VEC3 self +-- @param #number ExplosionIntensity +function POINT_VEC3:Explosion( ExplosionIntensity ) + self:F2( { ExplosionIntensity } ) + trigger.action.explosion( self:GetVec3(), ExplosionIntensity ) +end + +--- Creates an illumination bomb at the point. +-- @param #POINT_VEC3 self +function POINT_VEC3:IlluminationBomb() + self:F2() + trigger.action.illuminationBomb( self:GetVec3() ) +end + + +--- Smokes the point in a color. +-- @param #POINT_VEC3 self +-- @param Utilities.Utils#SMOKECOLOR SmokeColor +function POINT_VEC3:Smoke( SmokeColor ) + self:F2( { SmokeColor } ) + trigger.action.smoke( self:GetVec3(), SmokeColor ) +end + +--- Smoke the POINT_VEC3 Green. +-- @param #POINT_VEC3 self +function POINT_VEC3:SmokeGreen() + self:F2() + self:Smoke( SMOKECOLOR.Green ) +end + +--- Smoke the POINT_VEC3 Red. +-- @param #POINT_VEC3 self +function POINT_VEC3:SmokeRed() + self:F2() + self:Smoke( SMOKECOLOR.Red ) +end + +--- Smoke the POINT_VEC3 White. +-- @param #POINT_VEC3 self +function POINT_VEC3:SmokeWhite() + self:F2() + self:Smoke( SMOKECOLOR.White ) +end + +--- Smoke the POINT_VEC3 Orange. +-- @param #POINT_VEC3 self +function POINT_VEC3:SmokeOrange() + self:F2() + self:Smoke( SMOKECOLOR.Orange ) +end + +--- Smoke the POINT_VEC3 Blue. +-- @param #POINT_VEC3 self +function POINT_VEC3:SmokeBlue() + self:F2() + self:Smoke( SMOKECOLOR.Blue ) +end + +--- Flares the point in a color. +-- @param #POINT_VEC3 self +-- @param Utilities.Utils#FLARECOLOR FlareColor +-- @param Dcs.DCSTypes#Azimuth (optional) Azimuth The azimuth of the flare direction. The default azimuth is 0. +function POINT_VEC3:Flare( FlareColor, Azimuth ) + self:F2( { FlareColor } ) + trigger.action.signalFlare( self:GetVec3(), FlareColor, Azimuth and Azimuth or 0 ) +end + +--- Flare the POINT_VEC3 White. +-- @param #POINT_VEC3 self +-- @param Dcs.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( FLARECOLOR.White, Azimuth ) +end + +--- Flare the POINT_VEC3 Yellow. +-- @param #POINT_VEC3 self +-- @param Dcs.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( FLARECOLOR.Yellow, Azimuth ) +end + +--- Flare the POINT_VEC3 Green. +-- @param #POINT_VEC3 self +-- @param Dcs.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( FLARECOLOR.Green, Azimuth ) +end + +--- Flare the POINT_VEC3 Red. +-- @param #POINT_VEC3 self +function POINT_VEC3:FlareRed( Azimuth ) + self:F2( Azimuth ) + self:Flare( FLARECOLOR.Red, Azimuth ) +end + +end + +do -- POINT_VEC2 + + + +--- POINT_VEC2 constructor. +-- @param #POINT_VEC2 self +-- @param Dcs.DCSTypes#Distance x The x coordinate of the Vec3 point, pointing to the North. +-- @param Dcs.DCSTypes#Distance y The y coordinate of the Vec3 point, pointing to the Right. +-- @param Dcs.DCSTypes#Distance LandHeightAdd (optional) The default height if required to be evaluated will be the land height of the x, y coordinate. You can specify an extra height to be added to the land height. +-- @return Core.Point#POINT_VEC2 +function POINT_VEC2:New( x, y, LandHeightAdd ) + + local LandHeight = land.getHeight( { ["x"] = x, ["y"] = y } ) + + LandHeightAdd = LandHeightAdd or 0 + LandHeight = LandHeight + LandHeightAdd + + self = BASE:Inherit( self, POINT_VEC3:New( x, LandHeight, y ) ) + self:F2( self ) + + return self +end + +--- Create a new POINT_VEC2 object from Vec2 coordinates. +-- @param #POINT_VEC2 self +-- @param Dcs.DCSTypes#Vec2 Vec2 The Vec2 point. +-- @return Core.Point#POINT_VEC2 self +function POINT_VEC2:NewFromVec2( Vec2, LandHeightAdd ) + + local LandHeight = land.getHeight( Vec2 ) + + LandHeightAdd = LandHeightAdd or 0 + LandHeight = LandHeight + LandHeightAdd + + self = BASE:Inherit( self, POINT_VEC3:New( Vec2.x, LandHeight, Vec2.y ) ) + self:F2( self ) + + return self +end + +--- Create a new POINT_VEC2 object from Vec3 coordinates. +-- @param #POINT_VEC2 self +-- @param Dcs.DCSTypes#Vec3 Vec3 The Vec3 point. +-- @return Core.Point#POINT_VEC2 self +function POINT_VEC2:NewFromVec3( Vec3 ) + + local self = BASE:Inherit( self, BASE:New() ) + local Vec2 = { x = Vec3.x, y = Vec3.z } + + local LandHeight = land.getHeight( Vec2 ) + + self = BASE:Inherit( self, POINT_VEC3:New( Vec2.x, LandHeight, Vec2.y ) ) + self:F2( self ) + + return self +end + +--- Return the x coordinate of the POINT_VEC2. +-- @param #POINT_VEC2 self +-- @return #number The x coodinate. +function POINT_VEC2:GetX() + return self.x +end + +--- Return the y coordinate of the POINT_VEC2. +-- @param #POINT_VEC2 self +-- @return #number The y coodinate. +function POINT_VEC2:GetY() + return self.z +end + +--- Return the altitude of the land at the POINT_VEC2. +-- @param #POINT_VEC2 self +-- @return #number The land altitude. +function POINT_VEC2:GetAlt() + return land.getHeight( { x = self.x, y = self.z } ) +end + +--- Set the x coordinate of the POINT_VEC2. +-- @param #POINT_VEC2 self +-- @param #number x The x coordinate. +-- @return #POINT_VEC2 +function POINT_VEC2:SetX( x ) + self.x = x + return self +end + +--- Set the y coordinate of the POINT_VEC2. +-- @param #POINT_VEC2 self +-- @param #number y The y coordinate. +-- @return #POINT_VEC2 +function POINT_VEC2:SetY( y ) + self.z = y + return self +end + +--- Set the altitude of the POINT_VEC2. +-- @param #POINT_VEC2 self +-- @param #number Altitude The land altitude. If nothing (nil) is given, then the current land altitude is set. +-- @return #POINT_VEC2 +function POINT_VEC2:SetAlt( Altitude ) + self.y = Altitude or land.getHeight( { x = self.x, y = self.z } ) + return self +end + +--- Add to the x coordinate of the POINT_VEC2. +-- @param #POINT_VEC2 self +-- @param #number x The x coordinate. +-- @return #POINT_VEC2 +function POINT_VEC2:AddX( x ) + self.x = self.x + x + return self +end + +--- Add to the y coordinate of the POINT_VEC2. +-- @param #POINT_VEC2 self +-- @param #number y The y coordinate. +-- @return #POINT_VEC2 +function POINT_VEC2:AddY( y ) + self.z = self.z + y + return self +end + +--- Add to the current land height an altitude. +-- @param #POINT_VEC2 self +-- @param #number Altitude The Altitude to add. If nothing (nil) is given, then the current land altitude is set. +-- @return #POINT_VEC2 +function POINT_VEC2:AddAlt( Altitude ) + self.y = land.getHeight( { x = self.x, y = self.z } ) + Altitude or 0 + return self +end + + + +--- Calculate the distance from a reference @{#POINT_VEC2}. +-- @param #POINT_VEC2 self +-- @param #POINT_VEC2 PointVec2Reference The reference @{#POINT_VEC2}. +-- @return Dcs.DCSTypes#Distance The distance from the reference @{#POINT_VEC2} in meters. +function POINT_VEC2:DistanceFromPointVec2( PointVec2Reference ) + self:F2( PointVec2Reference ) + + local Distance = ( ( PointVec2Reference:GetX() - self:GetX() ) ^ 2 + ( PointVec2Reference:GetY() - self:GetY() ) ^2 ) ^0.5 + + self:T2( Distance ) + return Distance +end + +--- Calculate the distance from a reference @{DCSTypes#Vec2}. +-- @param #POINT_VEC2 self +-- @param Dcs.DCSTypes#Vec2 Vec2Reference The reference @{DCSTypes#Vec2}. +-- @return Dcs.DCSTypes#Distance The distance from the reference @{DCSTypes#Vec2} in meters. +function POINT_VEC2:DistanceFromVec2( Vec2Reference ) + self:F2( Vec2Reference ) + + local Distance = ( ( Vec2Reference.x - self:GetX() ) ^ 2 + ( Vec2Reference.y - self:GetY() ) ^2 ) ^0.5 + + self:T2( Distance ) + return Distance +end + + +--- Return no text for the altitude of the POINT_VEC2. +-- @param #POINT_VEC2 self +-- @return #string Empty string. +function POINT_VEC2:GetAltitudeText() + return '' +end + +--- Add a Distance in meters from the POINT_VEC2 orthonormal plane, with the given angle, and calculate the new POINT_VEC2. +-- @param #POINT_VEC2 self +-- @param Dcs.DCSTypes#Distance Distance The Distance to be added in meters. +-- @param Dcs.DCSTypes#Angle Angle The Angle in degrees. +-- @return #POINT_VEC2 The new calculated POINT_VEC2. +function POINT_VEC2:Translate( Distance, Angle ) + local SX = self:GetX() + local SY = self:GetY() + local Radians = Angle / 180 * math.pi + local TX = Distance * math.cos( Radians ) + SX + local TY = Distance * math.sin( Radians ) + SY + + return POINT_VEC2:New( TX, TY ) +end + +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 Core.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 + if MessageCategory:sub(-1) ~= "\n" then + self.MessageCategory = MessageCategory .. ": " + else + self.MessageCategory = MessageCategory:sub( 1, -2 ) .. ":\n" + end + else + self.MessageCategory = "" + end + + self.MessageDuration = MessageDuration or 5 + 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 Wrapper.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 a Group. +-- @param #MESSAGE self +-- @param Wrapper.Group#GROUP Group is the Group. +-- @return #MESSAGE +function MESSAGE:ToGroup( Group ) + self:F( Group.GroupName ) + + if Group then + + self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration ) + trigger.action.outTextForGroup( Group:GetID(), self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration ) + 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 a Coalition if the given Condition is true. +-- @param #MESSAGE self +-- @param CoalitionSide needs to be filled out by the defined structure of the standard scripting engine @{coalition.side}. +-- @return #MESSAGE +function MESSAGE:ToCoalitionIf( CoalitionSide, Condition ) + self:F( CoalitionSide ) + + if Condition and Condition == true then + self:ToCoalition( CoalitionSide ) + end + + return self +end + +--- Sends a MESSAGE to all players. +-- @param #MESSAGE self +-- @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 + + +--- Sends a MESSAGE to all players if the given Condition is true. +-- @param #MESSAGE self +-- @return #MESSAGE +function MESSAGE:ToAllIf( Condition ) + + if Condition and Condition == true then + self:ToCoalition( coalition.side.RED ) + self:ToCoalition( coalition.side.BLUE ) + end + + 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 ) +-- +--- This module contains the **FSM** (**F**inite **S**tate **M**achine) class and derived **FSM\_** classes. +-- ## Finite State Machines (FSM) are design patterns allowing efficient (long-lasting) processes and workflows. +-- +-- ![Banner Image](..\Presentations\FSM\Dia1.JPG) +-- +-- === +-- +-- A FSM can only be in one of a finite number of states. +-- The machine is in only one state at a time; the state it is in at any given time is called the **current state**. +-- It can change from one state to another when initiated by an **__internal__ or __external__ triggering event**, which is called a **transition**. +-- An **FSM implementation** is defined by **a list of its states**, **its initial state**, and **the triggering events** for **each possible transition**. +-- An FSM implementation is composed out of **two parts**, a set of **state transition rules**, and an implementation set of **state transition handlers**, implementing those transitions. +-- +-- The FSM class supports a **hierarchical implementation of a Finite State Machine**, +-- that is, it allows to **embed existing FSM implementations in a master FSM**. +-- FSM hierarchies allow for efficient FSM re-use, **not having to re-invent the wheel every time again** when designing complex processes. +-- +-- ![Workflow Example](..\Presentations\FSM\Dia2.JPG) +-- +-- The above diagram shows a graphical representation of a FSM implementation for a **Task**, which guides a Human towards a Zone, +-- orders him to destroy x targets and account the results. +-- Other examples of ready made FSM could be: +-- +-- * route a plane to a zone flown by a human +-- * detect targets by an AI and report to humans +-- * account for destroyed targets by human players +-- * handle AI infantry to deploy from or embark to a helicopter or airplane or vehicle +-- * let an AI patrol a zone +-- +-- The **MOOSE framework** uses extensively the FSM class and derived FSM\_ classes, +-- because **the goal of MOOSE is to simplify mission design complexity for mission building**. +-- By efficiently utilizing the FSM class and derived classes, MOOSE allows mission designers to quickly build processes. +-- **Ready made FSM-based implementations classes** exist within the MOOSE framework that **can easily be re-used, +-- and tailored** by mission designers through **the implementation of Transition Handlers**. +-- Each of these FSM implementation classes start either with: +-- +-- * an acronym **AI\_**, which indicates an FSM implementation directing **AI controlled** @{GROUP} and/or @{UNIT}. These AI\_ classes derive the @{#FSM_CONTROLLABLE} class. +-- * an acronym **TASK\_**, which indicates an FSM implementation executing a @{TASK} executed by Groups of players. These TASK\_ classes derive the @{#FSM_TASK} class. +-- * an acronym **ACT\_**, which indicates an Sub-FSM implementation, directing **Humans actions** that need to be done in a @{TASK}, seated in a @{CLIENT} (slot) or a @{UNIT} (CA join). These ACT\_ classes derive the @{#FSM_PROCESS} class. +-- +-- Detailed explanations and API specifics are further below clarified and FSM derived class specifics are described in those class documentation sections. +-- +-- ##__Dislaimer:__ +-- The FSM class development is based on a finite state machine implementation made by Conroy Kyle. +-- The state machine can be found on [github](https://github.com/kyleconroy/lua-state-machine) +-- I've reworked this development (taken the concept), and created a **hierarchical state machine** out of it, embedded within the DCS simulator. +-- Additionally, I've added extendability and created an API that allows seamless FSM implementation. +-- +-- === +-- +-- # 1) @{#FSM} class, extends @{Base#BASE} +-- +-- ![Transition Rules and Transition Handlers and Event Triggers](..\Presentations\FSM\Dia3.JPG) +-- +-- The FSM class is the base class of all FSM\_ derived classes. It implements the main functionality to define and execute Finite State Machines. +-- The derived FSM\_ classes extend the Finite State Machine functionality to run a workflow process for a specific purpose or component. +-- +-- Finite State Machines have **Transition Rules**, **Transition Handlers** and **Event Triggers**. +-- +-- The **Transition Rules** define the "Process Flow Boundaries", that is, +-- the path that can be followed hopping from state to state upon triggered events. +-- If an event is triggered, and there is no valid path found for that event, +-- an error will be raised and the FSM will stop functioning. +-- +-- The **Transition Handlers** are special methods that can be defined by the mission designer, following a defined syntax. +-- If the FSM object finds a method of such a handler, then the method will be called by the FSM, passing specific parameters. +-- The method can then define its own custom logic to implement the FSM workflow, and to conduct other actions. +-- +-- The **Event Triggers** are methods that are defined by the FSM, which the mission designer can use to implement the workflow. +-- Most of the time, these Event Triggers are used within the Transition Handler methods, so that a workflow is created running through the state machine. +-- +-- As explained above, a FSM supports **Linear State Transitions** and **Hierarchical State Transitions**, and both can be mixed to make a comprehensive FSM implementation. +-- The below documentation has a seperate chapter explaining both transition modes, taking into account the **Transition Rules**, **Transition Handlers** and **Event Triggers**. +-- +-- ## 1.1) FSM Linear Transitions +-- +-- Linear Transitions are Transition Rules allowing an FSM to transition from one or multiple possible **From** state(s) towards a **To** state upon a Triggered **Event**. +-- The Lineair transition rule evaluation will always be done from the **current state** of the FSM. +-- If no valid Transition Rule can be found in the FSM, the FSM will log an error and stop. +-- +-- ### 1.1.1) FSM Transition Rules +-- +-- The FSM has transition rules that it follows and validates, as it walks the process. +-- These rules define when an FSM can transition from a specific state towards an other specific state upon a triggered event. +-- +-- The method @{#FSM.AddTransition}() specifies a new possible Transition Rule for the FSM. +-- +-- The initial state can be defined using the method @{#FSM.SetStartState}(). The default start state of an FSM is "None". +-- +-- Find below an example of a Linear Transition Rule definition for an FSM. +-- +-- local Fsm3Switch = FSM:New() -- #FsmDemo +-- FsmSwitch:SetStartState( "Off" ) +-- FsmSwitch:AddTransition( "Off", "SwitchOn", "On" ) +-- FsmSwitch:AddTransition( "Off", "SwitchMiddle", "Middle" ) +-- FsmSwitch:AddTransition( "On", "SwitchOff", "Off" ) +-- FsmSwitch:AddTransition( "Middle", "SwitchOff", "Off" ) +-- +-- The above code snippet models a 3-way switch Linear Transition: +-- +-- * It can be switched **On** by triggering event **SwitchOn**. +-- * It can be switched to the **Middle** position, by triggering event **SwitchMiddle**. +-- * It can be switched **Off** by triggering event **SwitchOff**. +-- * Note that once the Switch is **On** or **Middle**, it can only be switched **Off**. +-- +-- ### Some additional comments: +-- +-- Note that Linear Transition Rules **can be declared in a few variations**: +-- +-- * The From states can be **a table of strings**, indicating that the transition rule will be valid **if the current state** of the FSM will be **one of the given From states**. +-- * The From state can be a **"*"**, indicating that **the transition rule will always be valid**, regardless of the current state of the FSM. +-- +-- The below code snippet shows how the two last lines can be rewritten and consensed. +-- +-- FsmSwitch:AddTransition( { "On", "Middle" }, "SwitchOff", "Off" ) +-- +-- ### 1.1.2) Transition Handling +-- +-- ![Transition Handlers](..\Presentations\FSM\Dia4.JPG) +-- +-- An FSM transitions in **4 moments** when an Event is being triggered and processed. +-- The mission designer can define for each moment specific logic within methods implementations following a defined API syntax. +-- These methods define the flow of the FSM process; because in those methods the FSM Internal Events will be triggered. +-- +-- * To handle **State** transition moments, create methods starting with OnLeave or OnEnter concatenated with the State name. +-- * To handle **Event** transition moments, create methods starting with OnBefore or OnAfter concatenated with the Event name. +-- +-- **The OnLeave and OnBefore transition methods may return false, which will cancel the transition!** +-- +-- Transition Handler methods need to follow the above specified naming convention, but are also passed parameters from the FSM. +-- These parameters are on the correct order: From, Event, To: +-- +-- * From = A string containing the From state. +-- * Event = A string containing the Event name that was triggered. +-- * To = A string containing the To state. +-- +-- On top, each of these methods can have a variable amount of parameters passed. See the example in section [1.1.3](#1.1.3\)-event-triggers). +-- +-- ### 1.1.3) Event Triggers +-- +-- ![Event Triggers](..\Presentations\FSM\Dia5.JPG) +-- +-- The FSM creates for each Event two **Event Trigger methods**. +-- There are two modes how Events can be triggered, which is **synchronous** and **asynchronous**: +-- +-- * The method **FSM:Event()** triggers an Event that will be processed **synchronously** or **immediately**. +-- * The method **FSM:__Event( __seconds__ )** triggers an Event that will be processed **asynchronously** over time, waiting __x seconds__. +-- +-- The destinction between these 2 Event Trigger methods are important to understand. An asynchronous call will "log" the Event Trigger to be executed at a later time. +-- Processing will just continue. Synchronous Event Trigger methods are useful to change states of the FSM immediately, but may have a larger processing impact. +-- +-- The following example provides a little demonstration on the difference between synchronous and asynchronous Event Triggering. +-- +-- function FSM:OnAfterEvent( From, Event, To, Amount ) +-- self:T( { Amount = Amount } ) +-- end +-- +-- local Amount = 1 +-- FSM:__Event( 5, Amount ) +-- +-- Amount = Amount + 1 +-- FSM:Event( Text, Amount ) +-- +-- In this example, the **:OnAfterEvent**() Transition Handler implementation will get called when **Event** is being triggered. +-- Before we go into more detail, let's look at the last 4 lines of the example. +-- The last line triggers synchronously the **Event**, and passes Amount as a parameter. +-- The 3rd last line of the example triggers asynchronously **Event**. +-- Event will be processed after 5 seconds, and Amount is given as a parameter. +-- +-- The output of this little code fragment will be: +-- +-- * Amount = 2 +-- * Amount = 2 +-- +-- Because ... When Event was asynchronously processed after 5 seconds, Amount was set to 2. So be careful when processing and passing values and objects in asynchronous processing! +-- +-- ### 1.1.4) Linear Transition Example +-- +-- This example is fully implemented in the MOOSE test mission on GITHUB: [FSM-100 - Transition Explanation](https://github.com/FlightControl-Master/MOOSE/blob/master/Moose%20Test%20Missions/FSM%20-%20Finite%20State%20Machine/FSM-100%20-%20Transition%20Explanation/FSM-100%20-%20Transition%20Explanation.lua) +-- +-- It models a unit standing still near Batumi, and flaring every 5 seconds while switching between a Green flare and a Red flare. +-- The purpose of this example is not to show how exciting flaring is, but it demonstrates how a Linear Transition FSM can be build. +-- Have a look at the source code. The source code is also further explained below in this section. +-- +-- The example creates a new FsmDemo object from class FSM. +-- It will set the start state of FsmDemo to state **Green**. +-- Two Linear Transition Rules are created, where upon the event **Switch**, +-- the FsmDemo will transition from state **Green** to **Red** and from **Red** back to **Green**. +-- +-- ![Transition Example](..\Presentations\FSM\Dia6.JPG) +-- +-- local FsmDemo = FSM:New() -- #FsmDemo +-- FsmDemo:SetStartState( "Green" ) +-- FsmDemo:AddTransition( "Green", "Switch", "Red" ) +-- FsmDemo:AddTransition( "Red", "Switch", "Green" ) +-- +-- In the above example, the FsmDemo could flare every 5 seconds a Green or a Red flare into the air. +-- The next code implements this through the event handling method **OnAfterSwitch**. +-- +-- ![Transition Flow](..\Presentations\FSM\Dia7.JPG) +-- +-- function FsmDemo:OnAfterSwitch( From, Event, To, FsmUnit ) +-- self:T( { From, Event, To, FsmUnit } ) +-- +-- if From == "Green" then +-- FsmUnit:Flare(FLARECOLOR.Green) +-- else +-- if From == "Red" then +-- FsmUnit:Flare(FLARECOLOR.Red) +-- end +-- end +-- self:__Switch( 5, FsmUnit ) -- Trigger the next Switch event to happen in 5 seconds. +-- end +-- +-- FsmDemo:__Switch( 5, FsmUnit ) -- Trigger the first Switch event to happen in 5 seconds. +-- +-- The OnAfterSwitch implements a loop. The last line of the code fragment triggers the Switch Event within 5 seconds. +-- Upon the event execution (after 5 seconds), the OnAfterSwitch method is called of FsmDemo (cfr. the double point notation!!! ":"). +-- The OnAfterSwitch method receives from the FSM the 3 transition parameter details ( From, Event, To ), +-- and one additional parameter that was given when the event was triggered, which is in this case the Unit that is used within OnSwitchAfter. +-- +-- function FsmDemo:OnAfterSwitch( From, Event, To, FsmUnit ) +-- +-- For debugging reasons the received parameters are traced within the DCS.log. +-- +-- self:T( { From, Event, To, FsmUnit } ) +-- +-- The method will check if the From state received is either "Green" or "Red" and will flare the respective color from the FsmUnit. +-- +-- if From == "Green" then +-- FsmUnit:Flare(FLARECOLOR.Green) +-- else +-- if From == "Red" then +-- FsmUnit:Flare(FLARECOLOR.Red) +-- end +-- end +-- +-- It is important that the Switch event is again triggered, otherwise, the FsmDemo would stop working after having the first Event being handled. +-- +-- FsmDemo:__Switch( 5, FsmUnit ) -- Trigger the next Switch event to happen in 5 seconds. +-- +-- The below code fragment extends the FsmDemo, demonstrating multiple **From states declared as a table**, adding a **Linear Transition Rule**. +-- The new event **Stop** will cancel the Switching process. +-- The transition for event Stop can be executed if the current state of the FSM is either "Red" or "Green". +-- +-- local FsmDemo = FSM:New() -- #FsmDemo +-- FsmDemo:SetStartState( "Green" ) +-- FsmDemo:AddTransition( "Green", "Switch", "Red" ) +-- FsmDemo:AddTransition( "Red", "Switch", "Green" ) +-- FsmDemo:AddTransition( { "Red", "Green" }, "Stop", "Stopped" ) +-- +-- The transition for event Stop can also be simplified, as any current state of the FSM is valid. +-- +-- FsmDemo:AddTransition( "*", "Stop", "Stopped" ) +-- +-- So... When FsmDemo:Stop() is being triggered, the state of FsmDemo will transition from Red or Green to Stopped. +-- And there is no transition handling method defined for that transition, thus, no new event is being triggered causing the FsmDemo process flow to halt. +-- +-- ## 1.5) FSM Hierarchical Transitions +-- +-- Hierarchical Transitions allow to re-use readily available and implemented FSMs. +-- This becomes in very useful for mission building, where mission designers build complex processes and workflows, +-- combining smaller FSMs to one single FSM. +-- +-- The FSM can embed **Sub-FSMs** that will execute and return **multiple possible Return (End) States**. +-- Depending upon **which state is returned**, the main FSM can continue the flow **triggering specific events**. +-- +-- The method @{#FSM.AddProcess}() adds a new Sub-FSM to the FSM. +-- +-- ==== +-- +-- # **API CHANGE HISTORY** +-- +-- The underlying change log documents the API changes. Please read this carefully. The following notation is used: +-- +-- * **Added** parts are expressed in bold type face. +-- * _Removed_ parts are expressed in italic type face. +-- +-- YYYY-MM-DD: CLASS:**NewFunction**( Params ) replaces CLASS:_OldFunction_( Params ) +-- YYYY-MM-DD: CLASS:**NewFunction( Params )** added +-- +-- Hereby the change log: +-- +-- * 2016-12-18: Released. +-- +-- === +-- +-- # **AUTHORS and CONTRIBUTIONS** +-- +-- ### Contributions: +-- +-- * [**Pikey**](https://forums.eagle.ru/member.php?u=62835): Review of documentation & advice for improvements. +-- +-- ### Authors: +-- +-- * [**FlightControl**](https://forums.eagle.ru/member.php?u=89536): Design & Programming & documentation. +-- +-- @module Fsm + +do -- FSM + + --- FSM class + -- @type FSM + -- @extends Core.Base#BASE + FSM = { + ClassName = "FSM", + } + + --- Creates a new FSM object. + -- @param #FSM self + -- @return #FSM + function FSM:New( FsmT ) + + -- Inherits from BASE + self = BASE:Inherit( self, BASE:New() ) + + self.options = options or {} + self.options.subs = self.options.subs or {} + self.current = self.options.initial or 'none' + self.Events = {} + self.subs = {} + self.endstates = {} + + self.Scores = {} + + self._StartState = "none" + self._Transitions = {} + self._Processes = {} + self._EndStates = {} + self._Scores = {} + self._EventSchedules = {} + + self.CallScheduler = SCHEDULER:New( self ) + + + return self + end + + + --- Sets the start state of the FSM. + -- @param #FSM self + -- @param #string State A string defining the start state. + function FSM:SetStartState( State ) + + self._StartState = State + self.current = State + end + + + --- Returns the start state of the FSM. + -- @param #FSM self + -- @return #string A string containing the start state. + function FSM:GetStartState() + + return self._StartState or {} + end + + --- Add a new transition rule to the FSM. + -- A transition rule defines when and if the FSM can transition from a state towards another state upon a triggered event. + -- @param #FSM self + -- @param #table From Can contain a string indicating the From state or a table of strings containing multiple From states. + -- @param #string Event The Event name. + -- @param #string To The To state. + function FSM:AddTransition( From, Event, To ) + + local Transition = {} + Transition.From = From + Transition.Event = Event + Transition.To = To + + self:T( Transition ) + + self._Transitions[Transition] = Transition + self:_eventmap( self.Events, Transition ) + end + + + --- Returns a table of the transition rules defined within the FSM. + -- @return #table + function FSM:GetTransitions() + + return self._Transitions or {} + end + + --- Set the default @{Process} template with key ProcessName providing the ProcessClass and the process object when it is assigned to a @{Controllable} by the task. + -- @param #FSM self + -- @param #table From Can contain a string indicating the From state or a table of strings containing multiple From states. + -- @param #string Event The Event name. + -- @param Core.Fsm#FSM_PROCESS Process An sub-process FSM. + -- @param #table ReturnEvents A table indicating for which returned events of the SubFSM which Event must be triggered in the FSM. + -- @return Core.Fsm#FSM_PROCESS The SubFSM. + function FSM:AddProcess( From, Event, Process, ReturnEvents ) + self:T( { From, Event, Process, ReturnEvents } ) + + local Sub = {} + Sub.From = From + Sub.Event = Event + Sub.fsm = Process + Sub.StartEvent = "Start" + Sub.ReturnEvents = ReturnEvents + + self._Processes[Sub] = Sub + + self:_submap( self.subs, Sub, nil ) + + self:AddTransition( From, Event, From ) + + return Process + end + + + --- Returns a table of the SubFSM rules defined within the FSM. + -- @return #table + function FSM:GetProcesses() + + return self._Processes or {} + end + + function FSM:GetProcess( From, Event ) + + for ProcessID, Process in pairs( self:GetProcesses() ) do + if Process.From == From and Process.Event == Event then + self:T( Process ) + return Process.fsm + end + end + + error( "Sub-Process from state " .. From .. " with event " .. Event .. " not found!" ) + end + + --- Adds an End state. + function FSM:AddEndState( State ) + + self._EndStates[State] = State + self.endstates[State] = State + end + + --- Returns the End states. + function FSM:GetEndStates() + + return self._EndStates or {} + end + + + --- Adds a score for the FSM to be achieved. + -- @param #FSM self + -- @param #string State is the state of the process when the score needs to be given. (See the relevant state descriptions of the process). + -- @param #string ScoreText is a text describing the score that is given according the status. + -- @param #number Score is a number providing the score of the status. + -- @return #FSM self + function FSM:AddScore( State, ScoreText, Score ) + self:F2( { State, ScoreText, Score } ) + + self._Scores[State] = self._Scores[State] or {} + self._Scores[State].ScoreText = ScoreText + self._Scores[State].Score = Score + + return self + end + + --- Adds a score for the FSM_PROCESS to be achieved. + -- @param #FSM self + -- @param #string From is the From State of the main process. + -- @param #string Event is the Event of the main process. + -- @param #string State is the state of the process when the score needs to be given. (See the relevant state descriptions of the process). + -- @param #string ScoreText is a text describing the score that is given according the status. + -- @param #number Score is a number providing the score of the status. + -- @return #FSM self + function FSM:AddScoreProcess( From, Event, State, ScoreText, Score ) + self:F2( { Event, State, ScoreText, Score } ) + + local Process = self:GetProcess( From, Event ) + + self:T( { Process = Process._Name, Scores = Process._Scores, State = State, ScoreText = ScoreText, Score = Score } ) + Process._Scores[State] = Process._Scores[State] or {} + Process._Scores[State].ScoreText = ScoreText + Process._Scores[State].Score = Score + + return Process + end + + --- Returns a table with the scores defined. + function FSM:GetScores() + + return self._Scores or {} + end + + --- Returns a table with the Subs defined. + function FSM:GetSubs() + + return self.options.subs + end + + + function FSM:LoadCallBacks( CallBackTable ) + + for name, callback in pairs( CallBackTable or {} ) do + self[name] = callback + end + + end + + function FSM:_eventmap( Events, EventStructure ) + + local Event = EventStructure.Event + local __Event = "__" .. EventStructure.Event + self[Event] = self[Event] or self:_create_transition(Event) + self[__Event] = self[__Event] or self:_delayed_transition(Event) + self:T( "Added methods: " .. Event .. ", " .. __Event ) + Events[Event] = self.Events[Event] or { map = {} } + self:_add_to_map( Events[Event].map, EventStructure ) + + end + + function FSM:_submap( subs, sub, name ) + self:F( { sub = sub, name = name } ) + subs[sub.From] = subs[sub.From] or {} + subs[sub.From][sub.Event] = subs[sub.From][sub.Event] or {} + + -- Make the reference table weak. + -- setmetatable( subs[sub.From][sub.Event], { __mode = "k" } ) + + subs[sub.From][sub.Event][sub] = {} + subs[sub.From][sub.Event][sub].fsm = sub.fsm + subs[sub.From][sub.Event][sub].StartEvent = sub.StartEvent + subs[sub.From][sub.Event][sub].ReturnEvents = sub.ReturnEvents or {} -- these events need to be given to find the correct continue event ... if none given, the processing will stop. + subs[sub.From][sub.Event][sub].name = name + subs[sub.From][sub.Event][sub].fsmparent = self + end + + + function FSM:_call_handler( handler, params, EventName ) + if self[handler] then + self:T( "Calling " .. handler ) + self._EventSchedules[EventName] = nil + local Value = self[handler]( self, unpack(params) ) + return Value + end + end + + function FSM._handler( self, EventName, ... ) + + local Can, to = self:can( EventName ) + + if to == "*" then + to = self.current + end + + if Can then + local from = self.current + local params = { from, EventName, to, ... } + + if self.Controllable then + self:T( "FSM Transition for " .. self.Controllable.ControllableName .. " :" .. self.current .. " --> " .. EventName .. " --> " .. to ) + else + self:T( "FSM Transition:" .. self.current .. " --> " .. EventName .. " --> " .. to ) + end + + if ( self:_call_handler("onbefore" .. EventName, params, EventName ) == false ) + or ( self:_call_handler("OnBefore" .. EventName, params, EventName ) == false ) + or ( self:_call_handler("onleave" .. from, params, EventName ) == false ) + or ( self:_call_handler("OnLeave" .. from, params, EventName ) == false ) then + self:T( "Cancel Transition" ) + return false + end + + self.current = to + + local execute = true + + local subtable = self:_gosub( from, EventName ) + for _, sub in pairs( subtable ) do + --if sub.nextevent then + -- self:F2( "nextevent = " .. sub.nextevent ) + -- self[sub.nextevent]( self ) + --end + self:T( "calling sub start event: " .. sub.StartEvent ) + sub.fsm.fsmparent = self + sub.fsm.ReturnEvents = sub.ReturnEvents + sub.fsm[sub.StartEvent]( sub.fsm ) + execute = false + end + + local fsmparent, Event = self:_isendstate( to ) + if fsmparent and Event then + self:F2( { "end state: ", fsmparent, Event } ) + self:_call_handler("onenter" .. to, params, EventName ) + self:_call_handler("OnEnter" .. to, params, EventName ) + self:_call_handler("onafter" .. EventName, params, EventName ) + self:_call_handler("OnAfter" .. EventName, params, EventName ) + self:_call_handler("onstatechange", params, EventName ) + fsmparent[Event]( fsmparent ) + execute = false + end + + if execute then + -- only execute the call if the From state is not equal to the To state! Otherwise this function should never execute! + --if from ~= to then + self:_call_handler("onenter" .. to, params, EventName ) + self:_call_handler("OnEnter" .. to, params, EventName ) + --end + + self:_call_handler("onafter" .. EventName, params, EventName ) + self:_call_handler("OnAfter" .. EventName, params, EventName ) + + self:_call_handler("onstatechange", params, EventName ) + end + else + self:T( "Cannot execute transition." ) + self:T( { From = self.current, Event = EventName, To = to, Can = Can } ) + end + + return nil + end + + function FSM:_delayed_transition( EventName ) + return function( self, DelaySeconds, ... ) + self:T2( "Delayed Event: " .. EventName ) + local CallID = 0 + if DelaySeconds ~= nil then + if DelaySeconds < 0 then -- Only call the event ONCE! + DelaySeconds = math.abs( DelaySeconds ) + if not self._EventSchedules[EventName] then + CallID = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1 ) + self._EventSchedules[EventName] = CallID + else + -- reschedule + end + else + CallID = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1 ) + end + else + error( "FSM: An asynchronous event trigger requires a DelaySeconds parameter!!! This can be positive or negative! Sorry, but will not process this." ) + end + self:T2( { CallID = CallID } ) + end + end + + function FSM:_create_transition( EventName ) + return function( self, ... ) return self._handler( self, EventName , ... ) end + end + + function FSM:_gosub( ParentFrom, ParentEvent ) + local fsmtable = {} + if self.subs[ParentFrom] and self.subs[ParentFrom][ParentEvent] then + self:T( { ParentFrom, ParentEvent, self.subs[ParentFrom], self.subs[ParentFrom][ParentEvent] } ) + return self.subs[ParentFrom][ParentEvent] + else + return {} + end + end + + function FSM:_isendstate( Current ) + local FSMParent = self.fsmparent + if FSMParent and self.endstates[Current] then + self:T( { state = Current, endstates = self.endstates, endstate = self.endstates[Current] } ) + FSMParent.current = Current + local ParentFrom = FSMParent.current + self:T( ParentFrom ) + self:T( self.ReturnEvents ) + local Event = self.ReturnEvents[Current] + self:T( { ParentFrom, Event, self.ReturnEvents } ) + if Event then + return FSMParent, Event + else + self:T( { "Could not find parent event name for state ", ParentFrom } ) + end + end + + return nil + end + + function FSM:_add_to_map( Map, Event ) + self:F3( { Map, Event } ) + if type(Event.From) == 'string' then + Map[Event.From] = Event.To + else + for _, From in ipairs(Event.From) do + Map[From] = Event.To + end + end + self:T3( { Map, Event } ) + end + + function FSM:GetState() + return self.current + end + + + function FSM:Is( State ) + return self.current == State + end + + function FSM:is(state) + return self.current == state + end + + function FSM:can(e) + local Event = self.Events[e] + self:F3( { self.current, Event } ) + local To = Event and Event.map[self.current] or Event.map['*'] + return To ~= nil, To + end + + function FSM:cannot(e) + return not self:can(e) + end + +end + +do -- FSM_CONTROLLABLE + + --- FSM_CONTROLLABLE class + -- @type FSM_CONTROLLABLE + -- @field Wrapper.Controllable#CONTROLLABLE Controllable + -- @extends Core.Fsm#FSM + FSM_CONTROLLABLE = { + ClassName = "FSM_CONTROLLABLE", + } + + --- Creates a new FSM_CONTROLLABLE object. + -- @param #FSM_CONTROLLABLE self + -- @param #table FSMT Finite State Machine Table + -- @param Wrapper.Controllable#CONTROLLABLE Controllable (optional) The CONTROLLABLE object that the FSM_CONTROLLABLE governs. + -- @return #FSM_CONTROLLABLE + function FSM_CONTROLLABLE:New( FSMT, Controllable ) + + -- Inherits from BASE + local self = BASE:Inherit( self, FSM:New( FSMT ) ) -- Core.Fsm#FSM_CONTROLLABLE + + if Controllable then + self:SetControllable( Controllable ) + end + + return self + end + + --- Sets the CONTROLLABLE object that the FSM_CONTROLLABLE governs. + -- @param #FSM_CONTROLLABLE self + -- @param Wrapper.Controllable#CONTROLLABLE FSMControllable + -- @return #FSM_CONTROLLABLE + function FSM_CONTROLLABLE:SetControllable( FSMControllable ) + self:F( FSMControllable ) + self.Controllable = FSMControllable + end + + --- Gets the CONTROLLABLE object that the FSM_CONTROLLABLE governs. + -- @param #FSM_CONTROLLABLE self + -- @return Wrapper.Controllable#CONTROLLABLE + function FSM_CONTROLLABLE:GetControllable() + return self.Controllable + end + + function FSM_CONTROLLABLE:_call_handler( handler, params, EventName ) + + local ErrorHandler = function( errmsg ) + + env.info( "Error in SCHEDULER function:" .. errmsg ) + if debug ~= nil then + env.info( debug.traceback() ) + end + + return errmsg + end + + if self[handler] then + self:F3( "Calling " .. handler ) + self._EventSchedules[EventName] = nil + local Result, Value = xpcall( function() return self[handler]( self, self.Controllable, unpack( params ) ) end, ErrorHandler ) + return Value + --return self[handler]( self, self.Controllable, unpack( params ) ) + end + end + +end + +do -- FSM_PROCESS + + --- FSM_PROCESS class + -- @type FSM_PROCESS + -- @field Tasking.Task#TASK Task + -- @extends Core.Fsm#FSM_CONTROLLABLE + FSM_PROCESS = { + ClassName = "FSM_PROCESS", + } + + --- Creates a new FSM_PROCESS object. + -- @param #FSM_PROCESS self + -- @return #FSM_PROCESS + function FSM_PROCESS:New( Controllable, Task ) + + local self = BASE:Inherit( self, FSM_CONTROLLABLE:New() ) -- Core.Fsm#FSM_PROCESS + + self:F( Controllable, Task ) + + self:Assign( Controllable, Task ) + + return self + end + + function FSM_PROCESS:Init( FsmProcess ) + self:T( "No Initialisation" ) + end + + --- Creates a new FSM_PROCESS object based on this FSM_PROCESS. + -- @param #FSM_PROCESS self + -- @return #FSM_PROCESS + function FSM_PROCESS:Copy( Controllable, Task ) + self:T( { self:GetClassNameAndID() } ) + + local NewFsm = self:New( Controllable, Task ) -- Core.Fsm#FSM_PROCESS + + NewFsm:Assign( Controllable, Task ) + + -- Polymorphic call to initialize the new FSM_PROCESS based on self FSM_PROCESS + NewFsm:Init( self ) + + -- Set Start State + NewFsm:SetStartState( self:GetStartState() ) + + -- Copy Transitions + for TransitionID, Transition in pairs( self:GetTransitions() ) do + NewFsm:AddTransition( Transition.From, Transition.Event, Transition.To ) + end + + -- Copy Processes + for ProcessID, Process in pairs( self:GetProcesses() ) do + self:T( { Process} ) + local FsmProcess = NewFsm:AddProcess( Process.From, Process.Event, Process.fsm:Copy( Controllable, Task ), Process.ReturnEvents ) + end + + -- Copy End States + for EndStateID, EndState in pairs( self:GetEndStates() ) do + self:T( EndState ) + NewFsm:AddEndState( EndState ) + end + + -- Copy the score tables + for ScoreID, Score in pairs( self:GetScores() ) do + self:T( Score ) + NewFsm:AddScore( ScoreID, Score.ScoreText, Score.Score ) + end + + return NewFsm + end + + --- Sets the task of the process. + -- @param #FSM_PROCESS self + -- @param Tasking.Task#TASK Task + -- @return #FSM_PROCESS + function FSM_PROCESS:SetTask( Task ) + + self.Task = Task + + return self + end + + --- Gets the task of the process. + -- @param #FSM_PROCESS self + -- @return Tasking.Task#TASK + function FSM_PROCESS:GetTask() + + return self.Task + end + + --- Gets the mission of the process. + -- @param #FSM_PROCESS self + -- @return Tasking.Mission#MISSION + function FSM_PROCESS:GetMission() + + return self.Task.Mission + end + + --- Gets the mission of the process. + -- @param #FSM_PROCESS self + -- @return Tasking.CommandCenter#COMMANDCENTER + function FSM_PROCESS:GetCommandCenter() + + return self:GetTask():GetMission():GetCommandCenter() + end + +-- TODO: Need to check and fix that an FSM_PROCESS is only for a UNIT. Not for a GROUP. + + --- Send a message of the @{Task} to the Group of the Unit. +-- @param #FSM_PROCESS self +function FSM_PROCESS:Message( Message ) + self:F( { Message = Message } ) + + local CC = self:GetCommandCenter() + local TaskGroup = self.Controllable:GetGroup() + + local PlayerName = self.Controllable:GetPlayerName() -- Only for a unit + PlayerName = PlayerName and " (" .. PlayerName .. ")" or "" -- If PlayerName is nil, then keep it nil, otherwise add brackets. + local Callsign = self.Controllable:GetCallsign() + local Prefix = Callsign and " @ " .. Callsign .. PlayerName or "" + + Message = Prefix .. ": " .. Message + CC:MessageToGroup( Message, TaskGroup ) +end + + + + + --- Assign the process to a @{Unit} and activate the process. + -- @param #FSM_PROCESS self + -- @param Task.Tasking#TASK Task + -- @param Wrapper.Unit#UNIT ProcessUnit + -- @return #FSM_PROCESS self + function FSM_PROCESS:Assign( ProcessUnit, Task ) + self:T( { Task, ProcessUnit } ) + + self:SetControllable( ProcessUnit ) + self:SetTask( Task ) + + --self.ProcessGroup = ProcessUnit:GetGroup() + + return self + end + + function FSM_PROCESS:onenterAssigned( ProcessUnit ) + self:T( "Assign" ) + + self.Task:Assign() + end + + function FSM_PROCESS:onenterFailed( ProcessUnit ) + self:T( "Failed" ) + + self.Task:Fail() + end + + function FSM_PROCESS:onenterSuccess( ProcessUnit ) + self:T( "Success" ) + + self.Task:Success() + end + + --- StateMachine callback function for a FSM_PROCESS + -- @param #FSM_PROCESS self + -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function FSM_PROCESS:onstatechange( ProcessUnit, From, Event, To, Dummy ) + self:T( { ProcessUnit, From, Event, To, Dummy, self:IsTrace() } ) + + if self:IsTrace() then + MESSAGE:New( "@ Process " .. self:GetClassNameAndID() .. " : " .. Event .. " changed to state " .. To, 2 ):ToAll() + end + + self:T( self._Scores[To] ) + -- TODO: This needs to be reworked with a callback functions allocated within Task, and set within the mission script from the Task Objects... + if self._Scores[To] then + + local Task = self.Task + local Scoring = Task:GetScoring() + if Scoring then + Scoring:_AddMissionTaskScore( Task.Mission, ProcessUnit, self._Scores[To].ScoreText, self._Scores[To].Score ) + end + end + end + +end + +do -- FSM_TASK + + --- FSM_TASK class + -- @type FSM_TASK + -- @field Tasking.Task#TASK Task + -- @extends Core.Fsm#FSM + FSM_TASK = { + ClassName = "FSM_TASK", + } + + --- Creates a new FSM_TASK object. + -- @param #FSM_TASK self + -- @param #table FSMT + -- @param Tasking.Task#TASK Task + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return #FSM_TASK + function FSM_TASK:New( FSMT ) + + local self = BASE:Inherit( self, FSM_CONTROLLABLE:New( FSMT ) ) -- Core.Fsm#FSM_TASK + + self["onstatechange"] = self.OnStateChange + + return self + end + + function FSM_TASK:_call_handler( handler, params, EventName ) + if self[handler] then + self:T( "Calling " .. handler ) + self._EventSchedules[EventName] = nil + return self[handler]( self, unpack( params ) ) + end + end + +end -- FSM_TASK + +do -- FSM_SET + + --- FSM_SET class + -- @type FSM_SET + -- @field Core.Set#SET_BASE Set + -- @extends Core.Fsm#FSM + FSM_SET = { + ClassName = "FSM_SET", + } + + --- Creates a new FSM_SET object. + -- @param #FSM_SET self + -- @param #table FSMT Finite State Machine Table + -- @param Set_SET_BASE FSMSet (optional) The Set object that the FSM_SET governs. + -- @return #FSM_SET + function FSM_SET:New( FSMSet ) + + -- Inherits from BASE + self = BASE:Inherit( self, FSM:New() ) -- Core.Fsm#FSM_SET + + if FSMSet then + self:Set( FSMSet ) + end + + return self + end + + --- Sets the SET_BASE object that the FSM_SET governs. + -- @param #FSM_SET self + -- @param Core.Set#SET_BASE FSMSet + -- @return #FSM_SET + function FSM_SET:Set( FSMSet ) + self:F( FSMSet ) + self.Set = FSMSet + end + + --- Gets the SET_BASE object that the FSM_SET governs. + -- @param #FSM_SET self + -- @return Core.Set#SET_BASE + function FSM_SET:Get() + return self.Controllable + end + + function FSM_SET:_call_handler( handler, params, EventName ) + if self[handler] then + self:T( "Calling " .. handler ) + self._EventSchedules[EventName] = nil + return self[handler]( self, self.Set, unpack( params ) ) + end + end + +end -- FSM_SET + +--- 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 + +--- The OBJECT class +-- @type OBJECT +-- @extends Core.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 Dcs.DCSWrapper.Object#Object ObjectName The Object name +-- @return #OBJECT self +function OBJECT:New( ObjectName, Test ) + local self = BASE:Inherit( self, BASE:New() ) + self:F2( ObjectName ) + self.ObjectName = ObjectName + + return self +end + + +--- Returns the unit's unique identifier. +-- @param Wrapper.Object#OBJECT self +-- @return Dcs.DCSWrapper.Object#Object.ID ObjectID +-- @return #nil The DCS Object is not existing or alive. +function OBJECT:GetID() + self:F2( self.ObjectName ) + + local DCSObject = self:GetDCSObject() + + if DCSObject then + local ObjectID = DCSObject:getID() + return ObjectID + end + + return nil +end + +--- Destroys the OBJECT. +-- @param #OBJECT self +-- @return #nil The DCS Unit is not existing or alive. +function OBJECT:Destroy() + self:F2( self.ObjectName ) + + local DCSObject = self:GetDCSObject() + + if DCSObject then + + DCSObject:destroy() + end + + return nil +end + + + + +--- This module contains the IDENTIFIABLE class. +-- +-- 1) @{#IDENTIFIABLE} class, extends @{Object#OBJECT} +-- =============================================================== +-- The @{#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.New}(): Create a IDENTIFIABLE instance. +-- +-- 1.2) IDENTIFIABLE methods: +-- -------------------------- +-- The following methods can be used to identify an identifiable object: +-- +-- * @{#IDENTIFIABLE.GetName}(): Returns the name of the Identifiable. +-- * @{#IDENTIFIABLE.IsAlive}(): Returns if the Identifiable is alive. +-- * @{#IDENTIFIABLE.GetTypeName}(): Returns the type name of the Identifiable. +-- * @{#IDENTIFIABLE.GetCoalition}(): Returns the coalition of the Identifiable. +-- * @{#IDENTIFIABLE.GetCountry}(): Returns the country of the Identifiable. +-- * @{#IDENTIFIABLE.GetDesc}(): Returns the descriptor structure of the Identifiable. +-- +-- +-- === +-- +-- @module Identifiable + +--- The IDENTIFIABLE class +-- @type IDENTIFIABLE +-- @extends Wrapper.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 Dcs.DCSWrapper.Identifiable#Identifiable IdentifiableName The DCS Identifiable name +-- @return #IDENTIFIABLE self +function IDENTIFIABLE:New( IdentifiableName ) + local self = BASE:Inherit( self, OBJECT:New( IdentifiableName ) ) + self:F2( IdentifiableName ) + self.IdentifiableName = IdentifiableName + return self +end + +--- Returns if the Identifiable is alive. +-- @param #IDENTIFIABLE self +-- @return #boolean true if Identifiable is alive. +-- @return #nil The DCS Identifiable is not existing or alive. +function IDENTIFIABLE:IsAlive() + self:F3( 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 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 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 category of the DCS Identifiable. +-- @param #IDENTIFIABLE self +-- @return Dcs.DCSWrapper.Object#Object.Category The category ID +function IDENTIFIABLE:GetCategory() + self:F2( self.ObjectName ) + + local DCSObject = self:GetDCSObject() + if DCSObject then + local ObjectCategory = DCSObject:getCategory() + self:T3( ObjectCategory ) + return ObjectCategory + end + + return nil +end + + +--- Returns the DCS Identifiable category name as defined within the DCS Identifiable Descriptor. +-- @param #IDENTIFIABLE self +-- @return #string The DCS Identifiable Category Name +function IDENTIFIABLE:GetCategoryName() + local DCSIdentifiable = self:GetDCSObject() + + if DCSIdentifiable then + local IdentifiableCategoryName = _CategoryName[ self:GetDesc().category ] + return IdentifiableCategoryName + end + + self:E( self.ClassName .. " " .. self.IdentifiableName .. " not found!" ) + return nil +end + +--- Returns coalition of the Identifiable. +-- @param #IDENTIFIABLE self +-- @return Dcs.DCSCoalitionWrapper.Object#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 self +-- @return Dcs.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 self +-- @return Dcs.DCSWrapper.Identifiable#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 + +--- Gets the CallSign of the IDENTIFIABLE, which is a blank by default. +-- @param #IDENTIFIABLE self +-- @return #string The CallSign of the IDENTIFIABLE. +function IDENTIFIABLE:GetCallsign() + return '' +end + + +function IDENTIFIABLE:GetThreatLevel() + + return 0, "Scenery" +end +--- This module contains the POSITIONABLE class. +-- +-- 1) @{Positionable#POSITIONABLE} class, extends @{Identifiable#IDENTIFIABLE} +-- =========================================================== +-- The @{Positionable#POSITIONABLE} class is a wrapper class to handle the POSITIONABLE objects: +-- +-- * Support all DCS APIs. +-- * Enhance with POSITIONABLE specific APIs not in the DCS API set. +-- * Manage the "state" of the POSITIONABLE. +-- +-- 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 + +--- The POSITIONABLE class +-- @type POSITIONABLE +-- @extends Wrapper.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 Dcs.DCSWrapper.Positionable#Positionable PositionableName The POSITIONABLE name +-- @return #POSITIONABLE self +function POSITIONABLE:New( PositionableName ) + local self = BASE:Inherit( self, IDENTIFIABLE:New( PositionableName ) ) + + self.PositionableName = PositionableName + return self +end + +--- Returns the @{DCSTypes#Position3} position vectors indicating the point and direction vectors in 3D of the POSITIONABLE within the mission. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return Dcs.DCSTypes#Position The 3D position vectors of the POSITIONABLE. +-- @return #nil The POSITIONABLE is not existing or alive. +function POSITIONABLE:GetPositionVec3() + self:F2( self.PositionableName ) + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + local PositionablePosition = DCSPositionable:getPosition().p + self:T3( PositionablePosition ) + return PositionablePosition + end + + return nil +end + +--- Returns the @{DCSTypes#Vec2} vector indicating the point in 2D of the POSITIONABLE within the mission. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return Dcs.DCSTypes#Vec2 The 2D point vector of the POSITIONABLE. +-- @return #nil The POSITIONABLE is not existing or alive. +function POSITIONABLE:GetVec2() + self:F2( self.PositionableName ) + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + local PositionableVec3 = DCSPositionable:getPosition().p + + local PositionableVec2 = {} + PositionableVec2.x = PositionableVec3.x + PositionableVec2.y = PositionableVec3.z + + self:T2( PositionableVec2 ) + return PositionableVec2 + end + + return nil +end + +--- Returns a POINT_VEC2 object indicating the point in 2D of the POSITIONABLE within the mission. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return Core.Point#POINT_VEC2 The 2D point vector of the POSITIONABLE. +-- @return #nil The POSITIONABLE is not existing or alive. +function POSITIONABLE:GetPointVec2() + self:F2( self.PositionableName ) + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + local PositionableVec3 = DCSPositionable:getPosition().p + + local PositionablePointVec2 = POINT_VEC2:NewFromVec3( PositionableVec3 ) + + self:T2( PositionablePointVec2 ) + return PositionablePointVec2 + end + + return nil +end + +--- Returns a POINT_VEC3 object indicating the point in 3D of the POSITIONABLE within the mission. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return Core.Point#POINT_VEC3 The 3D point vector of the POSITIONABLE. +-- @return #nil The POSITIONABLE is not existing or alive. +function POSITIONABLE:GetPointVec3() + self:F2( self.PositionableName ) + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + local PositionableVec3 = self:GetPositionVec3() + + local PositionablePointVec3 = POINT_VEC3:NewFromVec3( PositionableVec3 ) + + self:T2( PositionablePointVec3 ) + return PositionablePointVec3 + end + + return nil +end + + +--- Returns a random @{DCSTypes#Vec3} vector within a range, indicating the point in 3D of the POSITIONABLE within the mission. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return Dcs.DCSTypes#Vec3 The 3D point vector of the POSITIONABLE. +-- @return #nil The POSITIONABLE is not existing or alive. +function POSITIONABLE:GetRandomVec3( Radius ) + self:F2( self.PositionableName ) + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + local PositionablePointVec3 = DCSPositionable:getPosition().p + local PositionableRandomVec3 = {} + local angle = math.random() * math.pi*2; + PositionableRandomVec3.x = PositionablePointVec3.x + math.cos( angle ) * math.random() * Radius; + PositionableRandomVec3.y = PositionablePointVec3.y + PositionableRandomVec3.z = PositionablePointVec3.z + math.sin( angle ) * math.random() * Radius; + + self:T3( PositionableRandomVec3 ) + return PositionableRandomVec3 + end + + return nil +end + +--- Returns the @{DCSTypes#Vec3} vector indicating the 3D vector of the POSITIONABLE within the mission. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return Dcs.DCSTypes#Vec3 The 3D point vector of the POSITIONABLE. +-- @return #nil The POSITIONABLE is not existing or alive. +function POSITIONABLE:GetVec3() + self:F2( self.PositionableName ) + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + local PositionableVec3 = DCSPositionable:getPosition().p + self:T3( PositionableVec3 ) + return PositionableVec3 + end + + return nil +end + +--- Returns the altitude of the POSITIONABLE. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return Dcs.DCSTypes#Distance The altitude of the POSITIONABLE. +-- @return #nil The POSITIONABLE is not existing or alive. +function POSITIONABLE:GetAltitude() + self:F2() + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + local PositionablePointVec3 = DCSPositionable:getPoint() --Dcs.DCSTypes#Vec3 + return PositionablePointVec3.y + end + + return nil +end + +--- Returns if the Positionable is located above a runway. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return #boolean true if Positionable is above a runway. +-- @return #nil The POSITIONABLE is not existing or alive. +function POSITIONABLE:IsAboveRunway() + self:F2( self.PositionableName ) + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + + local Vec2 = self:GetVec2() + local SurfaceType = land.getSurfaceType( Vec2 ) + local IsAboveRunway = SurfaceType == land.SurfaceType.RUNWAY + + self:T2( IsAboveRunway ) + return IsAboveRunway + end + + return nil +end + + + +--- Returns the POSITIONABLE heading in degrees. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return #number The POSTIONABLE 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 + PositionableHeading = PositionableHeading * 180 / math.pi + self:T2( PositionableHeading ) + return PositionableHeading + end + end + + return nil +end + + +--- Returns true if the POSITIONABLE is in the air. +-- Polymorphic, is overridden in GROUP and UNIT. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return #boolean true if in the air. +-- @return #nil The POSITIONABLE is not existing or alive. +function POSITIONABLE:InAir() + self:F2( self.PositionableName ) + + return nil +end + + +--- Returns the POSITIONABLE velocity vector. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return Dcs.DCSTypes#Vec3 The velocity vector +-- @return #nil The 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 + +--- Returns the POSITIONABLE velocity in km/h. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return #number The velocity in km/h +-- @return #nil The POSITIONABLE is not existing or alive. +function POSITIONABLE:GetVelocityKMH() + self:F2( self.PositionableName ) + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + local VelocityVec3 = self:GetVelocity() + local Velocity = ( VelocityVec3.x ^ 2 + VelocityVec3.y ^ 2 + VelocityVec3.z ^ 2 ) ^ 0.5 -- in meters / sec + local Velocity = Velocity * 3.6 -- now it is in km/h. + self:T3( Velocity ) + return Velocity + end + + return nil +end + +--- Returns a message with the callsign embedded (if there is one). +-- @param #POSITIONABLE self +-- @param #string Message The message text +-- @param Dcs.DCSTypes#Duration Duration The duration of the message. +-- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +-- @return Core.Message#MESSAGE +function POSITIONABLE:GetMessage( Message, Duration, Name ) + + local DCSObject = self:GetDCSObject() + if DCSObject then + Name = Name or self:GetTypeName() + return MESSAGE:New( Message, Duration, self:GetCallsign() .. " (" .. Name .. ")" ) + end + + return nil +end + +--- Send a message to all coalitions. +-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. +-- @param #POSITIONABLE self +-- @param #string Message The message text +-- @param Dcs.DCSTypes#Duration Duration The duration of the message. +-- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +function POSITIONABLE:MessageToAll( Message, Duration, Name ) + self:F2( { Message, Duration } ) + + local DCSObject = self:GetDCSObject() + if DCSObject then + self:GetMessage( Message, Duration, Name ):ToAll() + end + + return nil +end + +--- Send a message to a coalition. +-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. +-- @param #POSITIONABLE self +-- @param #string Message The message text +-- @param Dcs.DCSTYpes#Duration Duration The duration of the message. +-- @param Dcs.DCScoalition#coalition MessageCoalition The Coalition receiving the message. +-- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +function POSITIONABLE:MessageToCoalition( Message, Duration, MessageCoalition, Name ) + self:F2( { Message, Duration } ) + + local DCSObject = self:GetDCSObject() + if DCSObject then + self:GetMessage( Message, Duration, Name ):ToCoalition( MessageCoalition ) + end + + return nil +end + + +--- Send a message to the red coalition. +-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. +-- @param #POSITIONABLE self +-- @param #string Message The message text +-- @param Dcs.DCSTYpes#Duration Duration The duration of the message. +-- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +function POSITIONABLE:MessageToRed( Message, Duration, Name ) + self:F2( { Message, Duration } ) + + local DCSObject = self:GetDCSObject() + if DCSObject then + self:GetMessage( Message, Duration, Name ):ToRed() + end + + return nil +end + +--- Send a message to the blue coalition. +-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. +-- @param #POSITIONABLE self +-- @param #string Message The message text +-- @param Dcs.DCSTypes#Duration Duration The duration of the message. +-- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +function POSITIONABLE:MessageToBlue( Message, Duration, Name ) + self:F2( { Message, Duration } ) + + local DCSObject = self:GetDCSObject() + if DCSObject then + self:GetMessage( Message, Duration, Name ):ToBlue() + end + + return nil +end + +--- Send a message to a client. +-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. +-- @param #POSITIONABLE self +-- @param #string Message The message text +-- @param Dcs.DCSTypes#Duration Duration The duration of the message. +-- @param Wrapper.Client#CLIENT Client The client object receiving the message. +-- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +function POSITIONABLE:MessageToClient( Message, Duration, Client, Name ) + self:F2( { Message, Duration } ) + + local DCSObject = self:GetDCSObject() + if DCSObject then + self:GetMessage( Message, Duration, Name ):ToClient( Client ) + end + + return nil +end + +--- Send a message to a @{Group}. +-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. +-- @param #POSITIONABLE self +-- @param #string Message The message text +-- @param Dcs.DCSTypes#Duration Duration The duration of the message. +-- @param Wrapper.Group#GROUP MessageGroup The GROUP object receiving the message. +-- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +function POSITIONABLE:MessageToGroup( Message, Duration, MessageGroup, Name ) + self:F2( { Message, Duration } ) + + local DCSObject = self:GetDCSObject() + if DCSObject then + if DCSObject:isExist() then + self:GetMessage( Message, Duration, Name ):ToGroup( MessageGroup ) + end + end + + return nil +end + +--- Send a message to the players in the @{Group}. +-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. +-- @param #POSITIONABLE self +-- @param #string Message The message text +-- @param Dcs.DCSTypes#Duration Duration The duration of the message. +-- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +function POSITIONABLE:Message( Message, Duration, Name ) + self:F2( { Message, Duration } ) + + local DCSObject = self:GetDCSObject() + if DCSObject then + self:GetMessage( Message, Duration, Name ):ToGroup( self ) + end + + return nil +end + + + + + +--- This module contains the CONTROLLABLE class. +-- +-- 1) @{Controllable#CONTROLLABLE} class, extends @{Positionable#POSITIONABLE} +-- =========================================================== +-- 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 some or all ammunition at a VEC2 point. +-- * @{#CONTROLLABLE.TaskFollow}: (AIR) Following another airborne controllable. +-- * @{#CONTROLLABLE.TaskHold}: (GROUND) Hold ground controllable from moving. +-- * @{#CONTROLLABLE.TaskHoldPosition}: (AIR) Hold position at the current position of the first unit of the controllable. +-- * @{#CONTROLLABLE.TaskLand}: (AIR HELICOPTER) Landing at the ground. For helicopters only. +-- * @{#CONTROLLABLE.TaskLandAtZone}: (AIR) Land the controllable at a @{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 + +--- The CONTROLLABLE class +-- @type CONTROLLABLE +-- @extends Wrapper.Positionable#POSITIONABLE +-- @field Dcs.DCSWrapper.Controllable#Controllable DCSControllable The DCS controllable class. +-- @field #string ControllableName The name of the controllable. +CONTROLLABLE = { + ClassName = "CONTROLLABLE", + ControllableName = "", + WayPointFunctions = {}, +} + +--- Create a new CONTROLLABLE from a DCSControllable +-- @param #CONTROLLABLE self +-- @param Dcs.DCSWrapper.Controllable#Controllable ControllableName The DCS Controllable name +-- @return #CONTROLLABLE self +function CONTROLLABLE:New( ControllableName ) + local self = BASE:Inherit( self, POSITIONABLE:New( ControllableName ) ) + self:F2( ControllableName ) + self.ControllableName = ControllableName + + self.TaskScheduler = SCHEDULER:New( self ) + return self +end + +-- DCS Controllable methods support. + +--- Get the controller for the CONTROLLABLE. +-- @param #CONTROLLABLE self +-- @return Dcs.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 + +-- Get methods + +--- Returns the UNITs wrappers of the DCS Units of the Controllable (default is a GROUP). +-- @param #CONTROLLABLE self +-- @return #list The UNITs wrappers. +function CONTROLLABLE:GetUnits() + self:F2( { self.ControllableName } ) + local DCSControllable = self:GetDCSObject() + + if DCSControllable then + local DCSUnits = DCSControllable:getUnits() + local Units = {} + for Index, UnitData in pairs( DCSUnits ) do + Units[#Units+1] = UNIT:Find( UnitData ) + end + self:T3( Units ) + return Units + end + + return nil +end + + +--- Returns the health. Dead controllables have health <= 1.0. +-- @param #CONTROLLABLE self +-- @return #number The controllable health value (unit or group average). +-- @return #nil The controllable is not existing or alive. +function CONTROLLABLE:GetLife() + self:F2( self.ControllableName ) + + local DCSControllable = self:GetDCSObject() + + if DCSControllable then + local UnitLife = 0 + local Units = self:GetUnits() + if #Units == 1 then + local Unit = Units[1] -- Wrapper.Unit#UNIT + UnitLife = Unit:GetLife() + else + local UnitLifeTotal = 0 + for UnitID, Unit in pairs( Units ) do + local Unit = Unit -- Wrapper.Unit#UNIT + UnitLifeTotal = UnitLifeTotal + Unit:GetLife() + end + UnitLife = UnitLifeTotal / #Units + end + return UnitLife + end + + return nil +end + + + +-- Tasks + +--- Popping current Task from the controllable. +-- @param #CONTROLLABLE self +-- @return Wrapper.Controllable#CONTROLLABLE self +function CONTROLLABLE:PopCurrentTask() + self:F2() + + local DCSControllable = self:GetDCSObject() + + if DCSControllable then + local Controller = self:_GetController() + Controller:popTask() + return self + end + + return nil +end + +--- Pushing Task on the queue from the controllable. +-- @param #CONTROLLABLE self +-- @return Wrapper.Controllable#CONTROLLABLE self +function CONTROLLABLE:PushTask( DCSTask, WaitTime ) + self:F2() + + local DCSControllable = self:GetDCSObject() + + if DCSControllable then + local 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 + self.TaskScheduler:Schedule( 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 Wrapper.Controllable#CONTROLLABLE self +function CONTROLLABLE:SetTask( DCSTask, WaitTime ) + self:F2( { DCSTask } ) + + local DCSControllable = self:GetDCSObject() + + if DCSControllable then + + local Controller = self:_GetController() + self:T3( Controller ) + + -- 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 + Controller:setTask( DCSTask ) + else + self.TaskScheduler:Schedule( Controller, Controller.setTask, { DCSTask }, WaitTime ) + end + + return self + end + + return nil +end + + +--- Return a condition section for a controlled task. +-- @param #CONTROLLABLE self +-- @param Dcs.DCSTime#Time time +-- @param #string userFlag +-- @param #boolean userFlagValue +-- @param #string condition +-- @param Dcs.DCSTime#Time duration +-- @param #number lastWayPoint +-- return Dcs.DCSTasking.Task#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 Dcs.DCSTasking.Task#Task DCSTask +-- @param #DCSStopCondition DCSStopCondition +-- @return Dcs.DCSTasking.Task#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 Dcs.DCSTasking.Task#TaskArray DCSTasks Array of @{DCSTasking.Task#Task} +-- @return Dcs.DCSTasking.Task#Task +function CONTROLLABLE:TaskCombo( DCSTasks ) + self:F2( { DCSTasks } ) + + local DCSTaskCombo + + DCSTaskCombo = { + id = 'ComboTask', + params = { + tasks = DCSTasks + } + } + + for TaskID, Task in ipairs( DCSTasks ) do + self:E( Task ) + end + + self:T3( { DCSTaskCombo } ) + return DCSTaskCombo +end + +--- Return a WrappedAction Task taking a Command. +-- @param #CONTROLLABLE self +-- @param Dcs.DCSCommand#Command DCSCommand +-- @return Dcs.DCSTasking.Task#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 Dcs.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 Dcs.DCSTasking.Task#Task +-- @usage +-- --- This test demonstrates the use(s) of the SwitchWayPoint method of the GROUP class. +-- HeliGroup = GROUP:FindByName( "Helicopter" ) +-- +-- --- Route the helicopter back to the FARP after 60 seconds. +-- -- We use the SCHEDULER class to do this. +-- SCHEDULER:New( nil, +-- function( HeliGroup ) +-- local CommandRTB = HeliGroup:CommandSwitchWayPoint( 2, 8 ) +-- HeliGroup:SetCommand( CommandRTB ) +-- end, { HeliGroup }, 90 +-- ) +function CONTROLLABLE:CommandSwitchWayPoint( FromWayPoint, ToWayPoint ) + self:F2( { FromWayPoint, ToWayPoint } ) + + local CommandSwitchWayPoint = { + id = 'SwitchWaypoint', + params = { + fromWaypointIndex = FromWayPoint, + goToWaypointIndex = ToWayPoint, + }, + } + + self:T3( { CommandSwitchWayPoint } ) + return CommandSwitchWayPoint +end + +--- Perform stop route command +-- @param #CONTROLLABLE self +-- @param #boolean StopRoute +-- @return Dcs.DCSTasking.Task#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 Wrapper.Controllable#CONTROLLABLE AttackGroup The Controllable to be attacked. +-- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. +-- @param Dcs.DCSTypes#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. +-- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. +-- @param Dcs.DCSTypes#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. +-- @param Dcs.DCSTypes#Distance Altitude (optional) Desired attack start altitude. Controllable/aircraft will make its attacks from the altitude. If the altitude is too low or too high to use weapon aircraft/controllable will choose closest altitude to the desired attack start altitude. If the desired altitude is defined controllable/aircraft will not attack from safe altitude. +-- @param #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 Dcs.DCSTasking.Task#Task The DCS task structure. +function CONTROLLABLE:TaskAttackGroup( AttackGroup, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit ) + self:F2( { self.ControllableName, AttackGroup, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit } ) + + -- AttackControllable = { + -- id = 'AttackControllable', + -- params = { + -- groupId = Group.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 = { + groupId = AttackGroup: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 Wrapper.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 Dcs.DCSTypes#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. +-- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. +-- @param Dcs.DCSTypes#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. +-- @param #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 Dcs.DCSTasking.Task#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 = { + altitudeEnabled = true, + unitId = AttackUnit:GetID(), + attackQtyLimit = AttackQtyLimit or false, + attackQty = AttackQty or 2, + expend = WeaponExpend or "Auto", + altitude = 2000, + directionEnabled = true, + groupAttack = true, + --weaponType = WeaponType or 1073741822, + direction = Direction or 0, + } + } + + self:E( DCSTask ) + + return DCSTask +end + + +--- (AIR) Delivering weapon at the point on the ground. +-- @param #CONTROLLABLE self +-- @param Dcs.DCSTypes#Vec2 Vec2 2D-coordinates of the point to deliver weapon at. +-- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. +-- @param Dcs.DCSTypes#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. +-- @param #number AttackQty (optional) Desired quantity of passes. The parameter is not the same in AttackControllable and AttackUnit tasks. +-- @param Dcs.DCSTypes#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. +-- @param #boolean ControllableAttack (optional) Flag indicates that the target must be engaged by all aircrafts of the controllable. Has effect only if the task is assigned to a controllable, not to a single aircraft. +-- @return Dcs.DCSTasking.Task#Task The DCS task structure. +function CONTROLLABLE:TaskBombing( Vec2, WeaponType, WeaponExpend, AttackQty, Direction, ControllableAttack ) + self:F2( { self.ControllableName, Vec2, 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 = Vec2, + 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 Dcs.DCSTypes#Vec2 Point The point to hold the position. +-- @param #number Altitude The altitude to hold the position. +-- @param #number Speed The speed flying when holding the position. +-- @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:GetVec2() + 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 Dcs.DCSTypes#Vec2 Vec2 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 Dcs.DCSTypes#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. +-- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. +-- @param Dcs.DCSTypes#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. +-- @param #boolean ControllableAttack (optional) Flag indicates that the target must be engaged by all aircrafts of the controllable. Has effect only if the task is assigned to a controllable, not to a single aircraft. +-- @return Dcs.DCSTasking.Task#Task The DCS task structure. +function CONTROLLABLE:TaskAttackMapObject( Vec2, WeaponType, WeaponExpend, AttackQty, Direction, ControllableAttack ) + self:F2( { self.ControllableName, Vec2, 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 = Vec2, + 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 Wrapper.Airbase#AIRBASE Airbase Airbase to attack. +-- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. +-- @param Dcs.DCSTypes#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. +-- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. +-- @param Dcs.DCSTypes#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. +-- @param #boolean ControllableAttack (optional) Flag indicates that the target must be engaged by all aircrafts of the controllable. Has effect only if the task is assigned to a controllable, not to a single aircraft. +-- @return Dcs.DCSTasking.Task#Task The DCS task structure. +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 Dcs.DCSTasking.Task#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 Dcs.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 Core.Zone#ZONE Zone The zone where to land. +-- @param #number Duration The duration in seconds to stay on the ground. +-- @return #CONTROLLABLE self +function CONTROLLABLE:TaskLandAtZone( Zone, Duration, RandomPoint ) + self:F2( { self.ControllableName, Zone, Duration, RandomPoint } ) + + local Point + if RandomPoint then + Point = Zone:GetRandomVec2() + else + Point = Zone:GetVec2() + 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 Wrapper.Controllable#CONTROLLABLE FollowControllable The controllable to be followed. +-- @param Dcs.DCSTypes#Vec3 Vec3 Position of the unit / lead unit of the controllable relative lead unit of another controllable in frame reference oriented by course of lead unit of another controllable. If another controllable is on land the unit / controllable will orbit around. +-- @param #number LastWaypointIndex Detach waypoint of another controllable. Once reached the unit / controllable Follow task is finished. +-- @return Dcs.DCSTasking.Task#Task The DCS task structure. +function CONTROLLABLE:TaskFollow( FollowControllable, Vec3, LastWaypointIndex ) + self:F2( { self.ControllableName, FollowControllable, Vec3, LastWaypointIndex } ) + +-- Follow = { +-- id = 'Follow', +-- params = { +-- groupId = Group.ID, +-- pos = Vec3, +-- lastWptIndexFlag = boolean, +-- lastWptIndex = number +-- } +-- } + + local LastWaypointIndexFlag = false + if LastWaypointIndex then + LastWaypointIndexFlag = true + end + + local DCSTask + DCSTask = { + id = 'Follow', + params = { + groupId = FollowControllable:GetID(), + pos = Vec3, + 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 Wrapper.Controllable#CONTROLLABLE EscortControllable The controllable to be escorted. +-- @param Dcs.DCSTypes#Vec3 Vec3 Position of the unit / lead unit of the controllable relative lead unit of another controllable in frame reference oriented by course of lead unit of another controllable. If another controllable is on land the unit / controllable will orbit around. +-- @param #number LastWaypointIndex Detach waypoint of another controllable. Once reached the unit / controllable Follow task is finished. +-- @param #number EngagementDistanceMax Maximal distance from escorted controllable to threat. If the threat is already engaged by escort escort will disengage if the distance becomes greater than 1.5 * engagementDistMax. +-- @param Dcs.DCSTypes#AttributeNameArray TargetTypes Array of AttributeName that is contains threat categories allowed to engage. +-- @return Dcs.DCSTasking.Task#Task The DCS task structure. +function CONTROLLABLE:TaskEscort( FollowControllable, Vec3, LastWaypointIndex, EngagementDistance, TargetTypes ) + self:F2( { self.ControllableName, FollowControllable, Vec3, LastWaypointIndex, EngagementDistance, TargetTypes } ) + +-- Escort = { +-- id = 'Escort', +-- params = { +-- groupId = Group.ID, +-- pos = Vec3, +-- lastWptIndexFlag = boolean, +-- lastWptIndex = number, +-- engagementDistMax = Distance, +-- targetTypes = array of AttributeName, +-- } +-- } + + local LastWaypointIndexFlag = false + if LastWaypointIndex then + LastWaypointIndexFlag = true + end + + local DCSTask + DCSTask = { id = 'Escort', + params = { + groupId = FollowControllable:GetID(), + pos = Vec3, + 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 Dcs.DCSTypes#Vec2 Vec2 The point to fire at. +-- @param Dcs.DCSTypes#Distance Radius The radius of the zone to deploy the fire at. +-- @param #number AmmoCount (optional) Quantity of ammunition to expand (omit to fire until ammunition is depleted). +-- @return Dcs.DCSTasking.Task#Task The DCS task structure. +function CONTROLLABLE:TaskFireAtPoint( Vec2, Radius, AmmoCount ) + self:F2( { self.ControllableName, Vec2, Radius, AmmoCount } ) + + -- FireAtPoint = { + -- id = 'FireAtPoint', + -- params = { + -- point = Vec2, + -- radius = Distance, + -- expendQty = number, + -- expendQtyEnabled = boolean, + -- } + -- } + + local DCSTask + DCSTask = { id = 'FireAtPoint', + params = { + point = Vec2, + radius = Radius, + expendQty = 100, -- dummy value + expendQtyEnabled = false, + } + } + + if AmmoCount then + DCSTask.params.expendQty = AmmoCount + DCSTask.params.expendQtyEnabled = true + end + + self:T3( { DCSTask } ) + return DCSTask +end + +--- (GROUND) Hold ground controllable from moving. +-- @param #CONTROLLABLE self +-- @return Dcs.DCSTasking.Task#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 Wrapper.Controllable#CONTROLLABLE AttackGroup Target CONTROLLABLE. +-- @param #number WeaponType Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. +-- @param Dcs.DCSTypes#AI.Task.Designation Designation (optional) Designation type. +-- @param #boolean Datalink (optional) Allows to use datalink to send the target information to attack aircraft. Enabled by default. +-- @return Dcs.DCSTasking.Task#Task The DCS task structure. +function CONTROLLABLE:TaskFAC_AttackGroup( AttackGroup, WeaponType, Designation, Datalink ) + self:F2( { self.ControllableName, AttackGroup, WeaponType, Designation, Datalink } ) + +-- FAC_AttackControllable = { +-- id = 'FAC_AttackControllable', +-- params = { +-- groupId = Group.ID, +-- weaponType = number, +-- designation = enum AI.Task.Designation, +-- datalink = boolean +-- } +-- } + + local DCSTask + DCSTask = { id = 'FAC_AttackControllable', + params = { + groupId = AttackGroup:GetID(), + weaponType = WeaponType, + designation = Designation, + datalink = Datalink, + } + } + + self:T3( { DCSTask } ) + return DCSTask +end + +-- EN-ACT_ROUTE TASKS FOR AIRBORNE CONTROLLABLES + +--- (AIR) Engaging targets of defined types. +-- @param #CONTROLLABLE self +-- @param Dcs.DCSTypes#Distance Distance Maximal distance from the target to a route leg. If the target is on a greater distance it will be ignored. +-- @param Dcs.DCSTypes#AttributeNameArray TargetTypes Array of target categories allowed to engage. +-- @param #number Priority All enroute tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. +-- @return Dcs.DCSTasking.Task#Task The DCS task structure. +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 Dcs.DCSTypes#Vec2 Vec2 2D-coordinates of the zone. +-- @param Dcs.DCSTypes#Distance Radius Radius of the zone. +-- @param Dcs.DCSTypes#AttributeNameArray TargetTypes Array of target categories allowed to engage. +-- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. +-- @return Dcs.DCSTasking.Task#Task The DCS task structure. +function CONTROLLABLE:EnRouteTaskEngageTargets( Vec2, Radius, TargetTypes, Priority ) + self:F2( { self.ControllableName, Vec2, Radius, TargetTypes, Priority } ) + +-- EngageTargetsInZone = { +-- id = 'EngageTargetsInZone', +-- params = { +-- point = Vec2, +-- zoneRadius = Distance, +-- targetTypes = array of AttributeName, +-- priority = number +-- } +-- } + + local DCSTask + DCSTask = { id = 'EngageTargetsInZone', + params = { + point = Vec2, + 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 Wrapper.Controllable#CONTROLLABLE AttackGroup The Controllable to be attacked. +-- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. +-- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. +-- @param Dcs.DCSTypes#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. +-- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. +-- @param Dcs.DCSTypes#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. +-- @param Dcs.DCSTypes#Distance Altitude (optional) Desired attack start altitude. Controllable/aircraft will make its attacks from the altitude. If the altitude is too low or too high to use weapon aircraft/controllable will choose closest altitude to the desired attack start altitude. If the desired altitude is defined controllable/aircraft will not attack from safe altitude. +-- @param #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 Dcs.DCSTasking.Task#Task The DCS task structure. +function CONTROLLABLE:EnRouteTaskEngageGroup( AttackGroup, Priority, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit ) + self:F2( { self.ControllableName, AttackGroup, Priority, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit } ) + + -- EngageControllable = { + -- id = 'EngageControllable ', + -- params = { + -- groupId = Group.ID, + -- weaponType = number, + -- expend = enum AI.Task.WeaponExpend, + -- attackQty = number, + -- directionEnabled = boolean, + -- direction = Azimuth, + -- altitudeEnabled = boolean, + -- altitude = Distance, + -- attackQtyLimit = boolean, + -- priority = number, + -- } + -- } + + local DirectionEnabled = nil + if Direction then + DirectionEnabled = true + end + + local AltitudeEnabled = nil + if Altitude then + AltitudeEnabled = true + end + + local DCSTask + DCSTask = { id = 'EngageControllable', + params = { + groupId = AttackGroup:GetID(), + weaponType = WeaponType, + expend = WeaponExpend, + attackQty = AttackQty, + directionEnabled = DirectionEnabled, + direction = Direction, + altitudeEnabled = AltitudeEnabled, + altitude = Altitude, + attackQtyLimit = AttackQtyLimit, + priority = Priority, + }, + }, + + self:T3( { DCSTask } ) + return DCSTask +end + + +--- (AIR) Attack the Unit. +-- @param #CONTROLLABLE self +-- @param Wrapper.Unit#UNIT EngageUnit The UNIT. +-- @param #number Priority (optional) All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. +-- @param #boolean GroupAttack (optional) If true, all units in the group will attack the Unit when found. +-- @param Dcs.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 Dcs.DCSTypes#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. +-- @param Dcs.DCSTypes#Distance Altitude (optional) Desired altitude to perform the unit engagement. +-- @param #boolean Visible (optional) Unit must be visible. +-- @param #boolean ControllableAttack (optional) Flag indicates that the target must be engaged by all aircrafts of the controllable. Has effect only if the task is assigned to a controllable, not to a single aircraft. +-- @return Dcs.DCSTasking.Task#Task The DCS task structure. +function CONTROLLABLE:EnRouteTaskEngageUnit( EngageUnit, Priority, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, Visible, ControllableAttack ) + self:F2( { self.ControllableName, EngageUnit, Priority, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, Visible, ControllableAttack } ) + + -- EngageUnit = { + -- id = 'EngageUnit', + -- params = { + -- unitId = Unit.ID, + -- weaponType = number, + -- expend = enum AI.Task.WeaponExpend + -- attackQty = number, + -- direction = Azimuth, + -- attackQtyLimit = boolean, + -- controllableAttack = boolean, + -- priority = number, + -- } + -- } + + local DCSTask + DCSTask = { id = 'EngageUnit', + params = { + unitId = EngageUnit:GetID(), + priority = Priority or 1, + groupAttack = GroupAttack or false, + visible = Visible or false, + expend = WeaponExpend or "Auto", + directionEnabled = Direction and true or false, + direction = Direction, + altitudeEnabled = Altitude and true or false, + altitude = Altitude, + attackQtyLimit = AttackQty and true or false, + attackQty = AttackQty, + controllableAttack = ControllableAttack, + }, + }, + + 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 Dcs.DCSTasking.Task#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 Dcs.DCSTasking.Task#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 Dcs.DCSTasking.Task#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 Wrapper.Controllable#CONTROLLABLE AttackGroup Target CONTROLLABLE. +-- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. +-- @param #number WeaponType Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. +-- @param Dcs.DCSTypes#AI.Task.Designation Designation (optional) Designation type. +-- @param #boolean Datalink (optional) Allows to use datalink to send the target information to attack aircraft. Enabled by default. +-- @return Dcs.DCSTasking.Task#Task The DCS task structure. +function CONTROLLABLE:EnRouteTaskFAC_EngageGroup( AttackGroup, Priority, WeaponType, Designation, Datalink ) + self:F2( { self.ControllableName, AttackGroup, WeaponType, Priority, Designation, Datalink } ) + +-- FAC_EngageControllable = { +-- id = 'FAC_EngageControllable', +-- params = { +-- groupId = Group.ID, +-- weaponType = number, +-- designation = enum AI.Task.Designation, +-- datalink = boolean, +-- priority = number, +-- } +-- } + + local DCSTask + DCSTask = { id = 'FAC_EngageControllable', + params = { + groupId = AttackGroup: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 Dcs.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 Dcs.DCSTasking.Task#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 Dcs.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 Dcs.DCSTasking.Task#Task The DCS task structure +function CONTROLLABLE:TaskEmbarking( Point, Duration, EmbarkingControllable ) + self:F2( { self.ControllableName, Point, Duration, EmbarkingControllable.DCSControllable } ) + + local DCSTask + DCSTask = { id = 'Embarking', + params = { x = Point.x, + y = Point.y, + duration = Duration, + controllablesForEmbarking = { EmbarkingControllable.ControllableID }, + durationFlag = true, + distributionFlag = false, + distribution = {}, + } + } + + self:T3( { DCSTask } ) + return DCSTask +end + +--- (GROUND) Embark to a Transport landed at a location. + +--- Move to a defined Vec2 Point, and embark to a controllable when arrived within a defined Radius. +-- @param #CONTROLLABLE self +-- @param Dcs.DCSTypes#Vec2 Point The point where to wait. +-- @param #number Radius The radius of the embarking zone around the Point. +-- @return Dcs.DCSTasking.Task#Task The DCS task structure. +function CONTROLLABLE:TaskEmbarkToTransport( Point, Radius ) + self:F2( { self.ControllableName, Point, Radius } ) + + local DCSTask --Dcs.DCSTasking.Task#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 Dcs.DCSTasking.Task#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 Dcs.DCSTasking.Task#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 Dcs.DCSTypes#Vec3 Point The destination point in Vec3 format. +-- @param #number Speed The speed to travel. +-- @return #CONTROLLABLE self +function CONTROLLABLE:RouteToVec2( Point, Speed ) + self:F2( { Point, Speed } ) + + local ControllablePoint = self:GetUnit( 1 ):GetVec2() + + local PointFrom = {} + PointFrom.x = ControllablePoint.x + PointFrom.y = ControllablePoint.y + PointFrom.type = "Turning Point" + PointFrom.action = "Turning Point" + PointFrom.speed = Speed + PointFrom.speed_locked = true + PointFrom.properties = { + ["vnav"] = 1, + ["scale"] = 0, + ["angle"] = 0, + ["vangle"] = 0, + ["steer"] = 2, + } + + + local PointTo = {} + PointTo.x = Point.x + PointTo.y = Point.y + PointTo.type = "Turning Point" + PointTo.action = "Fly Over Point" + PointTo.speed = Speed + PointTo.speed_locked = true + PointTo.properties = { + ["vnav"] = 1, + ["scale"] = 0, + ["angle"] = 0, + ["vangle"] = 0, + ["steer"] = 2, + } + + + local Points = { PointFrom, PointTo } + + self:T3( Points ) + + self:Route( Points ) + + return self +end + +--- (AIR + GROUND) Make the Controllable move to a given point. +-- @param #CONTROLLABLE self +-- @param Dcs.DCSTypes#Vec3 Point The destination point in Vec3 format. +-- @param #number Speed The speed to travel. +-- @return #CONTROLLABLE self +function CONTROLLABLE:RouteToVec3( Point, Speed ) + self:F2( { Point, Speed } ) + + local ControllableVec3 = self:GetUnit( 1 ):GetVec3() + + local PointFrom = {} + PointFrom.x = ControllableVec3.x + PointFrom.y = ControllableVec3.z + PointFrom.alt = ControllableVec3.y + PointFrom.alt_type = "BARO" + PointFrom.type = "Turning Point" + PointFrom.action = "Turning Point" + PointFrom.speed = Speed + PointFrom.speed_locked = true + PointFrom.properties = { + ["vnav"] = 1, + ["scale"] = 0, + ["angle"] = 0, + ["vangle"] = 0, + ["steer"] = 2, + } + + + local PointTo = {} + PointTo.x = Point.x + PointTo.y = Point.z + PointTo.alt = Point.y + PointTo.alt_type = "BARO" + PointTo.type = "Turning Point" + PointTo.action = "Fly Over Point" + PointTo.speed = Speed + PointTo.speed_locked = true + PointTo.properties = { + ["vnav"] = 1, + ["scale"] = 0, + ["angle"] = 0, + ["vangle"] = 0, + ["steer"] = 2, + } + + + local Points = { PointFrom, PointTo } + + self:T3( Points ) + + self:Route( Points ) + + return self +end + + + +--- Make the controllable to follow a given route. +-- @param #CONTROLLABLE self +-- @param #table 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 ) + self.TaskScheduler:Schedule( 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 Core.Zone#ZONE Zone The zone where to route to. +-- @param #boolean Randomize Defines whether to target point gets randomized within the Zone. +-- @param #number Speed The speed. +-- @param Base#FORMATION Formation The formation string. +function CONTROLLABLE:TaskRouteToZone( Zone, Randomize, Speed, Formation ) + self:F2( Zone ) + + local DCSControllable = self:GetDCSObject() + + if DCSControllable then + + local ControllablePoint = self:GetVec2() + + local PointFrom = {} + PointFrom.x = ControllablePoint.x + PointFrom.y = ControllablePoint.y + PointFrom.type = "Turning Point" + PointFrom.action = "Cone" + PointFrom.speed = 20 / 1.6 + + + local PointTo = {} + local ZonePoint + + if Randomize then + ZonePoint = Zone:GetRandomVec2() + else + ZonePoint = Zone:GetVec2() + end + + PointTo.x = ZonePoint.x + PointTo.y = ZonePoint.y + PointTo.type = "Turning Point" + + if Formation then + PointTo.action = Formation + else + PointTo.action = "Cone" + end + + if Speed then + PointTo.speed = Speed + else + PointTo.speed = 20 / 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 Wrapper.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:GetVec2() + 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:GetVec2() + + 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 Wrapper.Controllable#CONTROLLABLE self +-- @param #boolean DetectVisual (optional) +-- @param #boolean DetectOptical (optional) +-- @param #boolean DetectRadar (optional) +-- @param #boolean DetectIRST (optional) +-- @param #boolean DetectRWR (optional) +-- @param #boolean DetectDLINK (optional) +-- @return #table DetectedTargets +function CONTROLLABLE:GetDetectedTargets( DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK ) + self:F2( self.ControllableName ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local DetectionVisual = ( DetectVisual and DetectVisual == true ) and Controller.Detection.VISUAL or nil + local DetectionOptical = ( DetectOptical and DetectOptical == true ) and Controller.Detection.OPTICAL or nil + local DetectionRadar = ( DetectRadar and DetectRadar == true ) and Controller.Detection.RADAR or nil + local DetectionIRST = ( DetectIRST and DetectIRST == true ) and Controller.Detection.IRST or nil + local DetectionRWR = ( DetectRWR and DetectRWR == true ) and Controller.Detection.RWR or nil + local DetectionDLINK = ( DetectDLINK and DetectDLINK == true ) and Controller.Detection.DLINK or nil + + + 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 Wrapper.Controllable#CONTROLLABLE self +-- @return Wrapper.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 ) + self:F( { WayPoints } ) + + if WayPoints then + self.WayPoints = WayPoints + else + self.WayPoints = self:GetTaskRoute() + end + + return self +end + +--- Get the current WayPoints set with the WayPoint functions( Note that the WayPoints can be nil, although there ARE waypoints). +-- @param #CONTROLLABLE self +-- @return #table WayPoints If WayPoints is given, then return the WayPoints structure. +function CONTROLLABLE:GetWayPoints() + self:F( ) + + if self.WayPoints then + return self.WayPoints + end + + return nil +end + +--- Registers a waypoint function that will be executed when the controllable moves over the WayPoint. +-- @param #CONTROLLABLE self +-- @param #number WayPoint The waypoint number. Note that the start waypoint on the route is WayPoint 1! +-- @param #number WayPointIndex When defining multiple WayPoint functions for one WayPoint, use WayPointIndex to set the sequence of actions. +-- @param #function WayPointFunction The waypoint function to be called when the controllable moves over the waypoint. The waypoint function takes variable parameters. +-- @return #CONTROLLABLE +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 = GROUP: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 ) + self:F( { WayPoint, WaitTime } ) + + if not WayPoint then + WayPoint = 1 + end + + -- When starting the mission from a certain point, the TaskPoints need to be deleted before the given WayPoint. + for TaskPointID = 1, WayPoint - 1 do + table.remove( self.WayPoints, 1 ) + end + + self:T3( self.WayPoints ) + + self:SetTask( self:TaskRoute( self.WayPoints ), WaitTime ) + + return self +end + +-- Message APIs--- 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 +-- +-- A GROUP is a @{Controllable}. See the @{Controllable} task methods section for a description of the task methods. +-- +-- ### 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: +-- +-- * @{Controllable#CONTROLLABLE.TaskMission}: (AIR + GROUND) Return a mission task from a mission template. +-- +-- ## 1.3) GROUP Command methods +-- +-- A GROUP is a @{Controllable}. See the @{Controllable} command methods section for a description of the command methods. +-- +-- ## 1.4) GROUP option methods +-- +-- A GROUP is a @{Controllable}. See the @{Controllable} option methods section for a description of the option methods. +-- +-- ## 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. +-- +-- ## 1.6) GROUP AI methods +-- +-- A GROUP has AI methods to control the AI activation. +-- +-- * @{#GROUP.SetAIOnOff}(): Turns the GROUP AI On or Off. +-- * @{#GROUP.SetAIOn}(): Turns the GROUP AI On. +-- * @{#GROUP.SetAIOff}(): Turns the GROUP AI Off. +-- +-- ==== +-- +-- # **API CHANGE HISTORY** +-- +-- The underlying change log documents the API changes. Please read this carefully. The following notation is used: +-- +-- * **Added** parts are expressed in bold type face. +-- * _Removed_ parts are expressed in italic type face. +-- +-- Hereby the change log: +-- +-- 2017-03-07: GROUP:**HandleEvent( Event, EventFunction )** added. +-- 2017-03-07: GROUP:**UnHandleEvent( Event )** added. +-- +-- 2017-01-24: GROUP:**SetAIOnOff( AIOnOff )** added. +-- +-- 2017-01-24: GROUP:**SetAIOn()** added. +-- +-- 2017-01-24: GROUP:**SetAIOff()** added. +-- +-- === +-- +-- # **AUTHORS and CONTRIBUTIONS** +-- +-- ### Contributions: +-- +-- * [**Entropy**](https://forums.eagle.ru/member.php?u=111471), **Afinegan**: Came up with the requirement for AIOnOff(). +-- +-- ### Authors: +-- +-- * **FlightControl**: Design & Programming +-- +-- @module Group +-- @author FlightControl + +--- The GROUP class +-- @type GROUP +-- @extends Wrapper.Controllable#CONTROLLABLE +-- @field #string GroupName The name of the group. +GROUP = { + ClassName = "GROUP", +} + +--- Create a new GROUP from a DCSGroup +-- @param #GROUP self +-- @param Dcs.DCSWrapper.Group#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 + + self:SetEventPriority( 4 ) + return self +end + +-- Reference methods. + +--- Find the GROUP wrapper class instance using the DCS Group. +-- @param #GROUP self +-- @param Dcs.DCSWrapper.Group#Group DCSGroup The DCS Group. +-- @return #GROUP The GROUP. +function GROUP:Find( DCSGroup ) + + local GroupName = DCSGroup:getName() -- Wrapper.Group#GROUP + local GroupFound = _DATABASE:FindGroup( GroupName ) + return GroupFound +end + +--- Find the created GROUP using the DCS Group Name. +-- @param #GROUP self +-- @param #string GroupName The DCS Group Name. +-- @return #GROUP The GROUP. +function GROUP:FindByName( GroupName ) + + local GroupFound = _DATABASE:FindGroup( GroupName ) + return GroupFound +end + +-- DCS Group methods support. + +--- Returns the DCS Group. +-- @param #GROUP self +-- @return Dcs.DCSWrapper.Group#Group The DCS Group. +function GROUP:GetDCSObject() + local DCSGroup = Group.getByName( self.GroupName ) + + if DCSGroup then + return DCSGroup + end + + return nil +end + +--- Returns the @{DCSTypes#Position3} position vectors indicating the point and direction vectors in 3D of the POSITIONABLE within the mission. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return Dcs.DCSTypes#Position The 3D position vectors of the POSITIONABLE. +-- @return #nil The POSITIONABLE is not existing or alive. +function GROUP:GetPositionVec3() -- Overridden from POSITIONABLE:GetPositionVec3() + self:F2( self.PositionableName ) + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + local PositionablePosition = DCSPositionable:getUnits()[1]:getPosition().p + self:T3( PositionablePosition ) + return PositionablePosition + 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() and DCSGroup:getUnit(1) ~= nil + 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 Dcs.DCSWrapper.Group#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 Dcs.DCSCoalitionWrapper.Object#coalition.side The coalition side of the DCS Group. +function GROUP:GetCoalition() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSObject() + if DCSGroup then + local GroupCoalition = DCSGroup:getCoalition() + self:T3( GroupCoalition ) + return GroupCoalition + end + + return nil +end + +--- Returns the country of the DCS Group. +-- @param #GROUP self +-- @return Dcs.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 UNIT wrapper class with number UnitNumber. +-- If the underlying DCS Unit does not exist, the method will return nil. . +-- @param #GROUP self +-- @param #number UnitNumber The number of the UNIT wrapper class to be returned. +-- @return Wrapper.Unit#UNIT The UNIT wrapper class. +function GROUP:GetUnit( UnitNumber ) + self:F2( { self.GroupName, UnitNumber } ) + + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + local UnitFound = UNIT:Find( DCSGroup:getUnit( UnitNumber ) ) + 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 Dcs.DCSWrapper.Unit#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 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. +-- @param #GROUP self +-- @return Dcs.DCSTypes#Vec2 Current Vec2 point of the first DCS Unit of the DCS Group. +function GROUP:GetVec2() + self:F2( self.GroupName ) + + local UnitPoint = self:GetUnit(1) + UnitPoint:GetVec2() + local GroupPointVec2 = UnitPoint:GetVec2() + self:T3( GroupPointVec2 ) + return GroupPointVec2 +end + +--- Returns the current Vec3 vector of the first DCS Unit in the GROUP. +-- @return Dcs.DCSTypes#Vec3 Current Vec3 of the first DCS Unit of the GROUP. +function GROUP:GetVec3() + self:F2( self.GroupName ) + + local GroupVec3 = self:GetUnit(1):GetVec3() + self:T3( GroupVec3 ) + return GroupVec3 +end + + + +do -- Is Zone methods + +--- Returns true if all units of the group are within a @{Zone}. +-- @param #GROUP self +-- @param Core.Zone#ZONE_BASE Zone The zone to test. +-- @return #boolean Returns true if the Group is completely within the @{Zone#ZONE_BASE} +function GROUP:IsCompletelyInZone( Zone ) + self:F2( { self.GroupName, Zone } ) + + for UnitID, UnitData in pairs( self:GetUnits() ) do + local Unit = UnitData -- Wrapper.Unit#UNIT + if Zone:IsVec3InZone( Unit:GetVec3() ) then + else + return false + end + end + + return true +end + +--- Returns true if some units of the group are within a @{Zone}. +-- @param #GROUP self +-- @param Core.Zone#ZONE_BASE Zone The zone to test. +-- @return #boolean Returns true if the Group is completely within the @{Zone#ZONE_BASE} +function GROUP:IsPartlyInZone( Zone ) + self:F2( { self.GroupName, Zone } ) + + for UnitID, UnitData in pairs( self:GetUnits() ) do + local Unit = UnitData -- Wrapper.Unit#UNIT + if Zone:IsVec3InZone( Unit:GetVec3() ) then + return true + end + end + + return false +end + +--- Returns true if none of the group units of the group are within a @{Zone}. +-- @param #GROUP self +-- @param Core.Zone#ZONE_BASE Zone The zone to test. +-- @return #boolean Returns true if the Group is 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 -- Wrapper.Unit#UNIT + if Zone:IsVec3InZone( Unit:GetVec3() ) 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 + +end + +do -- AI methods + + --- Turns the AI On or Off for the GROUP. + -- @param #GROUP self + -- @param #boolean AIOnOff The value true turns the AI On, the value false turns the AI Off. + -- @return #GROUP The GROUP. + function GROUP:SetAIOnOff( AIOnOff ) + + local DCSGroup = self:GetDCSObject() -- Dcs.DCSGroup#Group + + if DCSGroup then + local DCSController = DCSGroup:getController() -- Dcs.DCSController#Controller + if DCSController then + DCSController:setOnOff( AIOnOff ) + return self + end + end + + return nil + end + + --- Turns the AI On for the GROUP. + -- @param #GROUP self + -- @return #GROUP The GROUP. + function GROUP:SetAIOn() + + return self:SetAIOnOff( true ) + end + + --- Turns the AI Off for the GROUP. + -- @param #GROUP self + -- @return #GROUP The GROUP. + function GROUP:SetAIOff() + + return self:SetAIOnOff( false ) + end + +end + + + +--- Returns the current maximum velocity of the group. +-- Each unit within the group gets evaluated, and the maximum velocity (= the unit which is going the fastest) is returned. +-- @param #GROUP self +-- @return #number Maximum velocity found. +function GROUP:GetMaxVelocity() + self:F2() + + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + local GroupVelocityMax = 0 + + for Index, UnitData in pairs( DCSGroup:getUnits() ) do + + local UnitVelocityVec3 = UnitData:getVelocity() + local UnitVelocity = math.abs( UnitVelocityVec3.x ) + math.abs( UnitVelocityVec3.y ) + math.abs( UnitVelocityVec3.z ) + + if UnitVelocity > GroupVelocityMax then + GroupVelocityMax = UnitVelocity + end + end + + return GroupVelocityMax + end + + return nil +end + +--- Returns the current minimum height of the group. +-- Each unit within the group gets evaluated, and the minimum height (= the unit which is the lowest elevated) is returned. +-- @param #GROUP self +-- @return #number Minimum height found. +function GROUP:GetMinHeight() + self:F2() + +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 + +-- SPAWNING + +--- Respawn the @{GROUP} using a (tweaked) template of the Group. +-- The template must be retrieved with the @{Group#GROUP.GetTemplate}() function. +-- The template contains all the definitions as declared within the mission file. +-- To understand templates, do the following: +-- +-- * unpack your .miz file into a directory using 7-zip. +-- * browse in the directory created to the file **mission**. +-- * open the file and search for the country group definitions. +-- +-- Your group template will contain the fields as described within the mission file. +-- +-- This function will: +-- +-- * Get the current position and heading of the group. +-- * When the group is alive, it will tweak the template x, y and heading coordinates of the group and the embedded units to the current units positions. +-- * Then it will destroy the current alive group. +-- * And it will respawn the group using your new template definition. +-- @param Wrapper.Group#GROUP self +-- @param #table Template The template of the Group retrieved with GROUP:GetTemplate() +function GROUP:Respawn( Template ) + + local Vec3 = self:GetVec3() + 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 -- Wrapper.Unit#UNIT + self:E( GroupUnit:GetName() ) + if GroupUnit:IsAlive() then + local GroupUnitVec3 = GroupUnit:GetVec3() + 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 + + self:Destroy() + _DATABASE:Spawn( Template ) +end + +--- Returns the group template from the @{DATABASE} (_DATABASE object). +-- @param #GROUP self +-- @return #table +function GROUP:GetTemplate() + local GroupName = self:GetName() + self:E( GroupName ) + return _DATABASE:GetGroupTemplate( GroupName ) +end + +--- Sets the controlled status in a Template. +-- @param #GROUP self +-- @param #boolean Controlled true is controlled, false is uncontrolled. +-- @return #table +function GROUP:SetTemplateControlled( Template, Controlled ) + Template.uncontrolled = not Controlled + return Template +end + +--- Sets the CountryID of the group in a Template. +-- @param #GROUP self +-- @param Dcs.DCScountry#country.id CountryID The country ID. +-- @return #table +function GROUP:SetTemplateCountry( Template, CountryID ) + Template.CountryID = CountryID + return Template +end + +--- Sets the CoalitionID of the group in a Template. +-- @param #GROUP self +-- @param Dcs.DCSCoalitionWrapper.Object#coalition.side CoalitionID The coalition ID. +-- @return #table +function GROUP:SetTemplateCoalition( Template, CoalitionID ) + Template.CoalitionID = CoalitionID + return 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 + +--- Calculate the maxium A2G threat level of the Group. +-- @param #GROUP self +function GROUP:CalculateThreatLevelA2G() + + local MaxThreatLevelA2G = 0 + for UnitName, UnitData in pairs( self:GetUnits() ) do + local ThreatUnit = UnitData -- Wrapper.Unit#UNIT + local ThreatLevelA2G = ThreatUnit:GetThreatLevel() + if ThreatLevelA2G > MaxThreatLevelA2G then + MaxThreatLevelA2G = ThreatLevelA2G + end + end + + self:T3( MaxThreatLevelA2G ) + return MaxThreatLevelA2G +end + +--- Returns true if the first unit of the GROUP is in the air. +-- @param Wrapper.Group#GROUP self +-- @return #boolean true if in the first unit of the group is in the air. +-- @return #nil The GROUP is not existing or not alive. +function GROUP:InAir() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + local DCSUnit = DCSGroup:getUnit(1) + if DCSUnit then + local GroupInAir = DCSGroup:getUnit(1):inAir() + self:T3( GroupInAir ) + return GroupInAir + end + end + + return nil +end + +function GROUP:OnReSpawn( ReSpawnFunction ) + + self.ReSpawnFunction = ReSpawnFunction +end + +do -- Event Handling + + --- Subscribe to a DCS Event. + -- @param #GROUP self + -- @param Core.Event#EVENTS Event + -- @param #function EventFunction (optional) The function to be called when the event occurs for the GROUP. + -- @return #GROUP + function GROUP:HandleEvent( Event, EventFunction ) + + self:EventDispatcher():OnEventForGroup( self:GetName(), EventFunction, self, Event ) + + return self + end + + --- UnSubscribe to a DCS event. + -- @param #GROUP self + -- @param Core.Event#EVENTS Event + -- @return #GROUP + function GROUP:UnHandleEvent( Event ) + + self:EventDispatcher():RemoveForGroup( self:GetName(), self, Event ) + + return self + end + +end +--- This module contains the UNIT class. +-- +-- 1) @{#UNIT} class, extends @{Controllable#CONTROLLABLE} +-- =========================================================== +-- The @{#UNIT} class is a wrapper class to handle the DCS Unit objects: +-- +-- * Support all DCS Unit APIs. +-- * Enhance with Unit specific APIs not in the DCS Unit API set. +-- * Handle local Unit Controller. +-- * Manage the "state" of the DCS Unit. +-- +-- +-- 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 @{DCSWrapper.Unit#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.GetVec3}() will obtain the current **location** of the DCS Unit in a Vec2 (2D) or a **point** in a Vec3 (3D) vector respectively. +-- If you want to obtain the complete **3D position** including 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 Wrapper.Controllable#CONTROLLABLE +UNIT = { + ClassName="UNIT", +} + + +--- Unit.SensorType +-- @type Unit.SensorType +-- @field OPTIC +-- @field RADAR +-- @field IRST +-- @field RWR + + +-- Registration. + +--- Create a new UNIT from DCSUnit. +-- @param #UNIT self +-- @param #string UnitName The name of the DCS unit. +-- @return #UNIT +function UNIT:Register( UnitName ) + local self = BASE:Inherit( self, CONTROLLABLE:New( UnitName ) ) + self.UnitName = UnitName + + self:SetEventPriority( 3 ) + return self +end + +-- Reference methods. + +--- Finds a UNIT from the _DATABASE using a DCSUnit object. +-- @param #UNIT self +-- @param Dcs.DCSWrapper.Unit#Unit DCSUnit An existing DCS Unit object reference. +-- @return #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 self +function UNIT:FindByName( UnitName ) + + local UnitFound = _DATABASE:FindUnit( UnitName ) + return UnitFound +end + +--- Return the name of the UNIT. +-- @param #UNIT self +-- @return #string The UNIT name. +function UNIT:Name() + + return self.UnitName +end + + +--- @param #UNIT self +-- @return Dcs.DCSWrapper.Unit#Unit +function UNIT:GetDCSObject() + + local DCSUnit = Unit.getByName( self.UnitName ) + + if DCSUnit then + return DCSUnit + end + + return nil +end + +--- Respawn the @{Unit} using a (tweaked) template of the parent Group. +-- +-- This function will: +-- +-- * Get the current position and heading of the group. +-- * When the unit is alive, it will tweak the template x, y and heading coordinates of the group and the embedded units to the current units positions. +-- * Then it will respawn the re-modelled group. +-- +-- @param #UNIT self +-- @param Dcs.DCSTypes#Vec3 SpawnVec3 The position where to Spawn the new Unit at. +-- @param #number Heading The heading of the unit respawn. +function UNIT:ReSpawn( SpawnVec3, Heading ) + + local SpawnGroupTemplate = UTILS.DeepCopy( _DATABASE:GetGroupTemplateFromUnitName( self:Name() ) ) + self:T( SpawnGroupTemplate ) + + local SpawnGroup = self:GetGroup() + + if SpawnGroup then + + local Vec3 = SpawnGroup:GetVec3() + SpawnGroupTemplate.x = SpawnVec3.x + SpawnGroupTemplate.y = SpawnVec3.z + + self:E( #SpawnGroupTemplate.units ) + for UnitID, UnitData in pairs( SpawnGroup:GetUnits() ) do + local GroupUnit = UnitData -- #UNIT + self:E( GroupUnit:GetName() ) + if GroupUnit:IsAlive() then + local GroupUnitVec3 = GroupUnit:GetVec3() + local GroupUnitHeading = GroupUnit:GetHeading() + SpawnGroupTemplate.units[UnitID].alt = GroupUnitVec3.y + SpawnGroupTemplate.units[UnitID].x = GroupUnitVec3.x + SpawnGroupTemplate.units[UnitID].y = GroupUnitVec3.z + SpawnGroupTemplate.units[UnitID].heading = GroupUnitHeading + self:E( { UnitID, SpawnGroupTemplate.units[UnitID], SpawnGroupTemplate.units[UnitID] } ) + end + end + end + + for UnitTemplateID, UnitTemplateData in pairs( SpawnGroupTemplate.units ) do + self:T( UnitTemplateData.name ) + if UnitTemplateData.name == self:Name() then + self:T("Adjusting") + SpawnGroupTemplate.units[UnitTemplateID].alt = SpawnVec3.y + SpawnGroupTemplate.units[UnitTemplateID].x = SpawnVec3.x + SpawnGroupTemplate.units[UnitTemplateID].y = SpawnVec3.z + SpawnGroupTemplate.units[UnitTemplateID].heading = Heading + self:E( { UnitTemplateID, SpawnGroupTemplate.units[UnitTemplateID], SpawnGroupTemplate.units[UnitTemplateID] } ) + else + self:E( SpawnGroupTemplate.units[UnitTemplateID].name ) + local GroupUnit = UNIT:FindByName( SpawnGroupTemplate.units[UnitTemplateID].name ) -- #UNIT + if GroupUnit and GroupUnit:IsAlive() then + local GroupUnitVec3 = GroupUnit:GetVec3() + local GroupUnitHeading = GroupUnit:GetHeading() + UnitTemplateData.alt = GroupUnitVec3.y + UnitTemplateData.x = GroupUnitVec3.x + UnitTemplateData.y = GroupUnitVec3.z + UnitTemplateData.heading = GroupUnitHeading + else + if SpawnGroupTemplate.units[UnitTemplateID].name ~= self:Name() then + self:T("nilling") + SpawnGroupTemplate.units[UnitTemplateID].delete = true + end + end + end + end + + -- Remove obscolete units from the group structure + i = 1 + while i <= #SpawnGroupTemplate.units do + + local UnitTemplateData = SpawnGroupTemplate.units[i] + self:T( UnitTemplateData.name ) + + if UnitTemplateData.delete then + table.remove( SpawnGroupTemplate.units, i ) + else + i = i + 1 + end + end + + _DATABASE:Spawn( SpawnGroupTemplate ) +end + + + +--- Returns if the unit is activated. +-- @param #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 the Unit's callsign - the localized string. +-- @param #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 + + 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 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 number in the group. +-- The number is the same number the unit has in ME. +-- It may not be changed during the mission. +-- If any unit in the group is destroyed, the numbers of another units will not be changed. +-- @param #UNIT self +-- @return #number The Unit number. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetNumber() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitNumber = DCSUnit:getNumber() + return UnitNumber + end + + return nil +end + +--- Returns the unit's group if it exist and nil otherwise. +-- @param Wrapper.Unit#UNIT self +-- @return Wrapper.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 + + +-- Need to add here functions to check if radar is on and which object etc. + +--- Returns the prefix name of the DCS Unit. A prefix name is a part of the name before a '#'-sign. +-- DCS Units spawned with the @{SPAWN} class contain a '#'-sign to indicate the end of the (base) DCS Unit name. +-- The spawn sequence number and unit number are contained within the name after the '#' sign. +-- @param #UNIT self +-- @return #string The name of the DCS Unit. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetPrefix() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitPrefix = string.match( self.UnitName, ".*#" ):sub( 1, -2 ) + self:T3( UnitPrefix ) + return UnitPrefix + end + + return nil +end + +--- Returns the Unit's ammunition. +-- @param #UNIT self +-- @return Dcs.DCSWrapper.Unit#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 self +-- @return Dcs.DCSWrapper.Unit#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 if the unit has sensors of a certain type. +-- @param #UNIT self +-- @return #boolean returns true if the unit has specified types of sensors. This function is more preferable than Unit.getSensors() if you don't want to get information about all the unit's sensors, and just want to check if the unit has specified types of sensors. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:HasSensors( ... ) + self:F2( arg ) + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local HasSensors = DCSUnit:hasSensors( unpack( arg ) ) + return HasSensors + end + + return nil +end + +--- Returns if the unit is SEADable. +-- @param #UNIT self +-- @return #boolean returns true if the unit is SEADable. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:HasSEAD() + self:F2() + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitSEADAttributes = DCSUnit:getDesc().attributes + + local HasSEAD = false + if UnitSEADAttributes["RADAR_BAND1_FOR_ARM"] and UnitSEADAttributes["RADAR_BAND1_FOR_ARM"] == true or + UnitSEADAttributes["RADAR_BAND2_FOR_ARM"] and UnitSEADAttributes["RADAR_BAND2_FOR_ARM"] == true then + HasSEAD = true + end + return HasSEAD + end + + return nil +end + +--- Returns two values: +-- +-- * First value indicates if at least one of the unit's radar(s) is on. +-- * Second value is the object of the radar's interest. Not nil only if at least one radar of the unit is tracking a target. +-- @param #UNIT self +-- @return #boolean Indicates if at least one of the unit's radar(s) is on. +-- @return Dcs.DCSWrapper.Object#Object The object of the radar's interest. Not nil only if at least one radar of the unit is tracking a target. +-- @return #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 + +--- Returns relative amount of fuel (from 0.0 to 1.0) the unit has in its internal tanks. If there are additional fuel tanks the value may be greater than 1.0. +-- @param #UNIT self +-- @return #number The relative amount of fuel (from 0.0 to 1.0). +-- @return #nil The DCS Unit is not existing or alive. +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 in a UNIT list of one element. +-- @param #UNIT self +-- @return #list The UNITs wrappers. +function UNIT:GetUnits() + self:F2( { self.UnitName } ) + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local DCSUnits = DCSUnit:getUnits() + local Units = {} + Units[1] = UNIT:Find( DCSUnit ) + self:T3( Units ) + return Units + end + + return nil +end + + +--- Returns the unit's health. Dead units has health <= 1.0. +-- @param #UNIT self +-- @return #number The Unit's health value. +-- @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 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 the Unit's A2G threat level on a scale from 1 to 10 ... +-- The following threat levels are foreseen: +-- +-- * Threat level 0: Unit is unarmed. +-- * Threat level 1: Unit is infantry. +-- * Threat level 2: Unit is an infantry vehicle. +-- * Threat level 3: Unit is ground artillery. +-- * Threat level 4: Unit is a tank. +-- * Threat level 5: Unit is a modern tank or ifv with ATGM. +-- * Threat level 6: Unit is a AAA. +-- * Threat level 7: Unit is a SAM or manpad, IR guided. +-- * Threat level 8: Unit is a Short Range SAM, radar guided. +-- * Threat level 9: Unit is a Medium Range SAM, radar guided. +-- * Threat level 10: Unit is a Long Range SAM, radar guided. +-- @param #UNIT self +function UNIT:GetThreatLevel() + + local Attributes = self:GetDesc().attributes + self:E( Attributes ) + + local ThreatLevel = 0 + local ThreatText = "" + + if self:IsGround() then + + self:E( "Ground" ) + + local ThreatLevels = { + "Unarmed", + "Infantry", + "Old Tanks & APCs", + "Tanks & IFVs without ATGM", + "Tanks & IFV with ATGM", + "Modern Tanks", + "AAA", + "IR Guided SAMs", + "SR SAMs", + "MR SAMs", + "LR SAMs" + } + + + if Attributes["LR SAM"] then ThreatLevel = 10 + elseif Attributes["MR SAM"] then ThreatLevel = 9 + elseif Attributes["SR SAM"] and + not Attributes["IR Guided SAM"] then ThreatLevel = 8 + elseif ( Attributes["SR SAM"] or Attributes["MANPADS"] ) and + Attributes["IR Guided SAM"] then ThreatLevel = 7 + elseif Attributes["AAA"] then ThreatLevel = 6 + elseif Attributes["Modern Tanks"] then ThreatLevel = 5 + elseif ( Attributes["Tanks"] or Attributes["IFV"] ) and + Attributes["ATGM"] then ThreatLevel = 4 + elseif ( Attributes["Tanks"] or Attributes["IFV"] ) and + not Attributes["ATGM"] then ThreatLevel = 3 + elseif Attributes["Old Tanks"] or Attributes["APC"] or Attributes["Artillery"] then ThreatLevel = 2 + elseif Attributes["Infantry"] then ThreatLevel = 1 + end + + ThreatText = ThreatLevels[ThreatLevel+1] + end + + if self:IsAir() then + + self:E( "Air" ) + + local ThreatLevels = { + "Unarmed", + "Tanker", + "AWACS", + "Transport Helicpter", + "UAV", + "Bomber", + "Strategic Bomber", + "Attack Helicopter", + "Interceptor", + "Multirole Fighter", + "Fighter" + } + + + if Attributes["Fighters"] then ThreatLevel = 10 + elseif Attributes["Multirole fighters"] then ThreatLevel = 9 + elseif Attributes["Battleplanes"] then ThreatLevel = 8 + elseif Attributes["Attack helicopters"] then ThreatLevel = 7 + elseif Attributes["Strategic bombers"] then ThreatLevel = 6 + elseif Attributes["Bombers"] then ThreatLevel = 5 + elseif Attributes["UAVs"] then ThreatLevel = 4 + elseif Attributes["Transport helicopters"] then ThreatLevel = 3 + elseif Attributes["AWACS"] then ThreatLevel = 2 + elseif Attributes["Tankers"] then ThreatLevel = 1 + end + + ThreatText = ThreatLevels[ThreatLevel+1] + end + + if self:IsShip() then + + self:E( "Ship" ) + +--["Aircraft Carriers"] = {"Heavy armed ships",}, +--["Cruisers"] = {"Heavy armed ships",}, +--["Destroyers"] = {"Heavy armed ships",}, +--["Frigates"] = {"Heavy armed ships",}, +--["Corvettes"] = {"Heavy armed ships",}, +--["Heavy armed ships"] = {"Armed ships", "Armed Air Defence", "HeavyArmoredUnits",}, +--["Light armed ships"] = {"Armed ships","NonArmoredUnits"}, +--["Armed ships"] = {"Ships"}, +--["Unarmed ships"] = {"Ships","HeavyArmoredUnits",}, + + local ThreatLevels = { + "Unarmed ship", + "Light armed ships", + "Corvettes", + "", + "Frigates", + "", + "Cruiser", + "", + "Destroyer", + "", + "Aircraft Carrier" + } + + + if Attributes["Aircraft Carriers"] then ThreatLevel = 10 + elseif Attributes["Destroyers"] then ThreatLevel = 8 + elseif Attributes["Cruisers"] then ThreatLevel = 6 + elseif Attributes["Frigates"] then ThreatLevel = 4 + elseif Attributes["Corvettes"] then ThreatLevel = 2 + elseif Attributes["Light armed ships"] then ThreatLevel = 1 + end + + ThreatText = ThreatLevels[ThreatLevel+1] + end + + self:T2( ThreatLevel ) + return ThreatLevel, ThreatText + +end + + +-- Is functions + +--- Returns true if the unit is within a @{Zone}. +-- @param #UNIT self +-- @param Core.Zone#ZONE_BASE Zone The zone to test. +-- @return #boolean Returns true if the unit is within the @{Zone#ZONE_BASE} +function UNIT:IsInZone( Zone ) + self:F2( { self.UnitName, Zone } ) + + if self:IsAlive() then + local IsInZone = Zone:IsVec3InZone( self:GetVec3() ) + + self:T( { IsInZone } ) + return IsInZone + end + + return false +end + +--- Returns true if the unit is not within a @{Zone}. +-- @param #UNIT self +-- @param Core.Zone#ZONE_BASE Zone The zone to test. +-- @return #boolean Returns true if the unit is not within the @{Zone#ZONE_BASE} +function UNIT:IsNotInZone( Zone ) + self:F2( { self.UnitName, Zone } ) + + if self:IsAlive() then + local IsInZone = not Zone:IsVec3InZone( self:GetVec3() ) + + self:T( { IsInZone } ) + return IsInZone + else + return false + end +end + + +--- Returns true if there is an **other** DCS Unit within a radius of the current 2D point of the DCS Unit. +-- @param #UNIT self +-- @param #UNIT AwaitUnit The other UNIT wrapper object. +-- @param Radius The radius in meters with the DCS Unit in the centre. +-- @return true If the other DCS Unit is within the radius of the 2D point of the DCS Unit. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:OtherUnitInRadius( AwaitUnit, Radius ) + self:F2( { self.UnitName, AwaitUnit.UnitName, Radius } ) + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitVec3 = self:GetVec3() + local AwaitUnitVec3 = AwaitUnit:GetVec3() + + if (((UnitVec3.x - AwaitUnitVec3.x)^2 + (UnitVec3.z - AwaitUnitVec3.z)^2)^0.5 <= Radius) then + self:T3( "true" ) + return true + else + self:T3( "false" ) + return false + end + end + + return nil +end + + + +--- Signal a flare at the position of the UNIT. +-- @param #UNIT self +-- @param Utilities.Utils#FLARECOLOR FlareColor +function UNIT:Flare( FlareColor ) + self:F2() + trigger.action.signalFlare( self:GetVec3(), 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:GetVec3(), 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:GetVec3(), 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:GetVec3(), trigger.flareColor.Green , 0 ) +end + +--- Signal a red flare at the position of the UNIT. +-- @param #UNIT self +function UNIT:FlareRed() + self:F2() + local Vec3 = self:GetVec3() + if Vec3 then + trigger.action.signalFlare( Vec3, trigger.flareColor.Red, 0 ) + end +end + +--- Smoke the UNIT. +-- @param #UNIT self +function UNIT:Smoke( SmokeColor, Range ) + self:F2() + if Range then + trigger.action.smoke( self:GetRandomVec3( Range ), SmokeColor ) + else + trigger.action.smoke( self:GetVec3(), SmokeColor ) + end + +end + +--- Smoke the UNIT Green. +-- @param #UNIT self +function UNIT:SmokeGreen() + self:F2() + trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Green ) +end + +--- Smoke the UNIT Red. +-- @param #UNIT self +function UNIT:SmokeRed() + self:F2() + trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Red ) +end + +--- Smoke the UNIT White. +-- @param #UNIT self +function UNIT:SmokeWhite() + self:F2() + trigger.action.smoke( self:GetVec3(), trigger.smokeColor.White ) +end + +--- Smoke the UNIT Orange. +-- @param #UNIT self +function UNIT:SmokeOrange() + self:F2() + trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Orange ) +end + +--- Smoke the UNIT Blue. +-- @param #UNIT self +function UNIT:SmokeBlue() + self:F2() + trigger.action.smoke( self:GetVec3(), 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 DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitDescriptor = DCSUnit:getDesc() + self:T3( { UnitDescriptor.category, Unit.Category.AIRPLANE, Unit.Category.HELICOPTER } ) + + local IsAirResult = ( UnitDescriptor.category == Unit.Category.AIRPLANE ) or ( UnitDescriptor.category == Unit.Category.HELICOPTER ) + + self:T3( IsAirResult ) + return IsAirResult + end + + return nil +end + +--- Returns if the unit is of an ground category. +-- If the unit is a ground vehicle or infantry, this method will return true, otherwise false. +-- @param #UNIT self +-- @return #boolean Ground category evaluation result. +function UNIT:IsGround() + self:F2() + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitDescriptor = DCSUnit:getDesc() + self:T3( { UnitDescriptor.category, Unit.Category.GROUND_UNIT } ) + + local IsGroundResult = ( UnitDescriptor.category == Unit.Category.GROUND_UNIT ) + + self:T3( IsGroundResult ) + return IsGroundResult + end + + return nil +end + +--- Returns if the unit is a friendly unit. +-- @param #UNIT self +-- @return #boolean IsFriendly evaluation result. +function UNIT:IsFriendly( FriendlyCoalition ) + self:F2() + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitCoalition = DCSUnit:getCoalition() + self:T3( { UnitCoalition, FriendlyCoalition } ) + + local IsFriendlyResult = ( UnitCoalition == FriendlyCoalition ) + + self:E( IsFriendlyResult ) + return IsFriendlyResult + end + + return nil +end + +--- Returns if the unit is of a ship category. +-- If the unit is a ship, this method will return true, otherwise false. +-- @param #UNIT self +-- @return #boolean Ship category evaluation result. +function UNIT:IsShip() + self:F2() + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitDescriptor = DCSUnit:getDesc() + self:T3( { UnitDescriptor.category, Unit.Category.SHIP } ) + + local IsShipResult = ( UnitDescriptor.category == Unit.Category.SHIP ) + + self:T3( IsShipResult ) + return IsShipResult + end + + return nil +end + +--- Returns true if the UNIT is in the air. +-- @param Wrapper.Positionable#UNIT self +-- @return #boolean true if in the air. +-- @return #nil The UNIT is not existing or alive. +function UNIT:InAir() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitInAir = DCSUnit:inAir() + self:T3( UnitInAir ) + return UnitInAir + end + + return nil +end + +do -- Event Handling + + --- Subscribe to a DCS Event. + -- @param #UNIT self + -- @param Core.Event#EVENTS Event + -- @param #function EventFunction (optional) The function to be called when the event occurs for the unit. + -- @return #UNIT + function UNIT:HandleEvent( Event, EventFunction ) + + self:EventDispatcher():OnEventForUnit( self:GetName(), EventFunction, self, Event ) + + return self + end + + --- UnSubscribe to a DCS event. + -- @param #UNIT self + -- @param Core.Event#EVENTS Event + -- @return #UNIT + function UNIT:UnHandleEvent( Event ) + + self:EventDispatcher():RemoveForUnit( self:GetName(), self, Event ) + + return self + end + +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 + +--- The CLIENT class +-- @type CLIENT +-- @extends Wrapper.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. +-- @param #boolean Error A flag that indicates whether an error should be raised if the CLIENT cannot be found. By default an error will be raised. +-- @return #CLIENT +-- @usage +-- -- Create new Clients. +-- local Mission = MISSIONSCHEDULER.AddMission( 'Russia Transport Troops SA-6', 'Operational', 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', 'Russia' ) +-- Mission:AddGoal( DeploySA6TroopsGoal ) +-- +-- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 1' ):Transport() ) +-- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 3' ):Transport() ) +-- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 2' ):Transport() ) +-- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 4' ):Transport() ) +function CLIENT:FindByName( ClientName, ClientBriefing, Error ) + local ClientFound = _DATABASE:FindClient( ClientName ) + + if ClientFound then + ClientFound:F( { ClientName, ClientBriefing } ) + ClientFound:AddBriefing( ClientBriefing ) + ClientFound.MessageSwitch = true + + return ClientFound + end + + if not Error then + error( "CLIENT not found for: " .. ClientName ) + end +end + +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 CallBackFunction Create a function that will be called when a player joins the slot. +-- @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:F3( { 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 Dcs.DCSWrapper.Group#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 Dcs.DCSTypes#Group.ID +--- Get the group ID of the client. +-- @param #CLIENT self +-- @return Dcs.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 Wrapper.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 Dcs.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 @{AI_Cargo#CARGO} contained within the CLIENT to the player as a message. +-- The @{AI_Cargo#CARGO} is shown using the @{Message#MESSAGE} distribution system. +-- @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 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 @{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: +-- +-- * 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 Wrapper.Positionable#POSITIONABLE +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. +-- @param #boolean RaiseError Raise an error if not found. +-- @return #STATIC +function STATIC:FindByName( StaticName, RaiseError ) + local StaticFound = _DATABASE:FindStatic( StaticName ) + + self.StaticName = StaticName + + if StaticFound then + StaticFound:F( { StaticName } ) + + return StaticFound + end + + if RaiseError == nil or RaiseError == true then + error( "STATIC not found for: " .. StaticName ) + end + + return nil +end + +function STATIC:Register( StaticName ) + local self = BASE:Inherit( self, POSITIONABLE:New( StaticName ) ) + self.StaticName = StaticName + return self +end + + +function STATIC:GetDCSObject() + local DCSStatic = StaticObject.getByName( self.StaticName ) + + if DCSStatic then + return DCSStatic + end + + return nil +end + +function STATIC:GetThreatLevel() + + return 1, "Static" +end--- This module contains the AIRBASE classes. +-- +-- === +-- +-- 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. +-- * 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 @{DCSWrapper.Airbase#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 Wrapper.Positionable#POSITIONABLE +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 #string AirbaseName The name of the airbase. +-- @return Wrapper.Airbase#AIRBASE +function AIRBASE:Register( AirbaseName ) + + local self = BASE:Inherit( self, POSITIONABLE:New( AirbaseName ) ) + self.AirbaseName = AirbaseName + return self +end + +-- Reference methods. + +--- Finds a AIRBASE from the _DATABASE using a DCSAirbase object. +-- @param #AIRBASE self +-- @param Dcs.DCSWrapper.Airbase#Airbase DCSAirbase An existing DCS Airbase object reference. +-- @return Wrapper.Airbase#AIRBASE self +function AIRBASE:Find( DCSAirbase ) + + local AirbaseName = DCSAirbase:getName() + local AirbaseFound = _DATABASE:FindAirbase( AirbaseName ) + return AirbaseFound +end + +--- Find a AIRBASE in the _DATABASE using the name of an existing DCS Airbase. +-- @param #AIRBASE self +-- @param #string AirbaseName The Airbase Name. +-- @return Wrapper.Airbase#AIRBASE self +function AIRBASE:FindByName( AirbaseName ) + + local AirbaseFound = _DATABASE:FindAirbase( AirbaseName ) + return AirbaseFound +end + +function AIRBASE:GetDCSObject() + local DCSAirbase = Airbase.getByName( self.AirbaseName ) + + if DCSAirbase then + return DCSAirbase + end + + return nil +end + + + +--- This module contains the SCENERY class. +-- +-- 1) @{Scenery#SCENERY} class, extends @{Positionable#POSITIONABLE} +-- =============================================================== +-- Scenery objects are defined on the map. +-- The @{Scenery#SCENERY} class is a wrapper class to handle the DCS Scenery objects: +-- +-- * Wraps the DCS Scenery objects. +-- * Support all DCS Scenery APIs. +-- * Enhance with Scenery specific APIs not in the DCS API set. +-- +-- @module Scenery +-- @author FlightControl + + + +--- The SCENERY class +-- @type SCENERY +-- @extends Wrapper.Positionable#POSITIONABLE +SCENERY = { + ClassName = "SCENERY", +} + + +function SCENERY:Register( SceneryName, SceneryObject ) + local self = BASE:Inherit( self, POSITIONABLE:New( SceneryName ) ) + self.SceneryName = SceneryName + self.SceneryObject = SceneryObject + return self +end + +function SCENERY:GetDCSObject() + return self.SceneryObject +end + +function SCENERY:GetThreatLevel() + + return 0, "Scenery" +end +--- Single-Player:**Yes** / Multi-Player:**Yes** / Core:**Yes** -- **Administer the scoring of player achievements, +-- and create a CSV file logging the scoring events for use at team or squadron websites.** +-- +-- ![Banner Image](..\Presentations\SCORING\Dia1.JPG) +-- +-- === +-- +-- # 1) @{Scoring#SCORING} class, extends @{Base#BASE} +-- +-- The @{#SCORING} class administers the scoring of player achievements, +-- and creates a CSV file logging the scoring events and results for use at team or squadron websites. +-- +-- SCORING automatically calculates the threat level of the objects hit and destroyed by players, +-- which can be @{Unit}, @{Static) and @{Scenery} objects. +-- +-- Positive score points are granted when enemy or neutral targets are destroyed. +-- Negative score points or penalties are given when a friendly target is hit or destroyed. +-- This brings a lot of dynamism in the scoring, where players need to take care to inflict damage on the right target. +-- By default, penalties weight heavier in the scoring, to ensure that players don't commit fratricide. +-- The total score of the player is calculated by **adding the scores minus the penalties**. +-- +-- ![Banner Image](..\Presentations\SCORING\Dia4.JPG) +-- +-- The score value is calculated based on the **threat level of the player** and the **threat level of the target**. +-- A calculated score takes the threat level of the target divided by a balanced threat level of the player unit. +-- As such, if the threat level of the target is high, and the player threat level is low, a higher score will be given than +-- if the threat level of the player would be high too. +-- +-- ![Banner Image](..\Presentations\SCORING\Dia5.JPG) +-- +-- When multiple players hit the same target, and finally succeed in destroying the target, then each player who contributed to the target +-- destruction, will receive a score. This is important for targets that require significant damage before it can be destroyed, like +-- ships or heavy planes. +-- +-- ![Banner Image](..\Presentations\SCORING\Dia13.JPG) +-- +-- Optionally, the score values can be **scaled** by a **scale**. Specific scales can be set for positive cores or negative penalties. +-- The default range of the scores granted is a value between 0 and 10. The default range of penalties given is a value between 0 and 30. +-- +-- ![Banner Image](..\Presentations\SCORING\Dia7.JPG) +-- +-- **Additional scores** can be granted to **specific objects**, when the player(s) destroy these objects. +-- +-- ![Banner Image](..\Presentations\SCORING\Dia9.JPG) +-- +-- Various @{Zone}s can be defined for which scores are also granted when objects in that @{Zone} are destroyed. +-- This is **specifically useful** to designate **scenery targets on the map** that will generate points when destroyed. +-- +-- With a small change in MissionScripting.lua, the scoring results can also be logged in a **CSV file**. +-- These CSV files can be used to: +-- +-- * Upload scoring to a database or a BI tool to publish the scoring results to the player community. +-- * Upload scoring in an (online) Excel like tool, using pivot tables and pivot charts to show mission results. +-- * Share scoring amoung players after the mission to discuss mission results. +-- +-- Scores can be **reported**. **Menu options** are automatically added to **each player group** when a player joins a client slot or a CA unit. +-- Use the radio menu F10 to consult the scores while running the mission. +-- Scores can be reported for your user, or an overall score can be reported of all players currently active in the mission. +-- +-- ## 1.1) Set the destroy score or penalty scale +-- +-- Score scales can be set for scores granted when enemies or friendlies are destroyed. +-- Use the method @{#SCORING.SetScaleDestroyScore}() to set the scale of enemy destroys (positive destroys). +-- Use the method @{#SCORING.SetScaleDestroyPenalty}() to set the scale of friendly destroys (negative destroys). +-- +-- local Scoring = SCORING:New( "Scoring File" ) +-- Scoring:SetScaleDestroyScore( 10 ) +-- Scoring:SetScaleDestroyPenalty( 40 ) +-- +-- The above sets the scale for valid scores to 10. So scores will be given in a scale from 0 to 10. +-- The penalties will be given in a scale from 0 to 40. +-- +-- ## 1.2) Define special targets that will give extra scores. +-- +-- Special targets can be set that will give extra scores to the players when these are destroyed. +-- Use the methods @{#SCORING.AddUnitScore}() and @{#SCORING.RemoveUnitScore}() to specify a special additional score for a specific @{Unit}s. +-- Use the methods @{#SCORING.AddStaticScore}() and @{#SCORING.RemoveStaticScore}() to specify a special additional score for a specific @{Static}s. +-- Use the method @{#SCORING.SetGroupGroup}() to specify a special additional score for a specific @{Group}s. +-- +-- local Scoring = SCORING:New( "Scoring File" ) +-- Scoring:AddUnitScore( UNIT:FindByName( "Unit #001" ), 200 ) +-- Scoring:AddStaticScore( STATIC:FindByName( "Static #1" ), 100 ) +-- +-- The above grants an additional score of 200 points for Unit #001 and an additional 100 points of Static #1 if these are destroyed. +-- Note that later in the mission, one can remove these scores set, for example, when the a goal achievement time limit is over. +-- For example, this can be done as follows: +-- +-- Scoring:RemoveUnitScore( UNIT:FindByName( "Unit #001" ) ) +-- +-- +-- +-- ## 1.3) Define destruction zones that will give extra scores. +-- +-- Define zones of destruction. Any object destroyed within the zone of the given category will give extra points. +-- Use the method @{#SCORING.AddZoneScore}() to add a @{Zone} for additional scoring. +-- Use the method @{#SCORING.RemoveZoneScore}() to remove a @{Zone} for additional scoring. +-- There are interesting variations that can be achieved with this functionality. For example, if the @{Zone} is a @{Zone#ZONE_UNIT}, +-- then the zone is a moving zone, and anything destroyed within that @{Zone} will generate points. +-- The other implementation could be to designate a scenery target (a building) in the mission editor surrounded by a @{Zone}, +-- just large enough around that building. +-- +-- ## 1.4) Add extra Goal scores upon an event or a condition. +-- +-- A mission has goals and achievements. The scoring system provides an API to set additional scores when a goal or achievement event happens. +-- Use the method @{#SCORING.AddGoalScore}() to add a score for a Player at any time in your mission. +-- +-- ## 1.5) Configure fratricide level. +-- +-- When a player commits too much damage to friendlies, his penalty score will reach a certain level. +-- Use the method @{#SCORING.SetFratricide}() to define the level when a player gets kicked. +-- By default, the fratricide level is the default penalty mutiplier * 2 for the penalty score. +-- +-- ## 1.6) Penalty score when a player changes the coalition. +-- +-- When a player changes the coalition, he can receive a penalty score. +-- Use the method @{#SCORING.SetCoalitionChangePenalty}() to define the penalty when a player changes coalition. +-- By default, the penalty for changing coalition is the default penalty scale. +-- +-- ## 1.8) Define output CSV files. +-- +-- The CSV file is given the name of the string given in the @{#SCORING.New}{} constructor, followed by the .csv extension. +-- The file is incrementally saved in the **\\Saved Games\\DCS\\Logs** folder, and has a time stamp indicating each mission run. +-- See the following example: +-- +-- local ScoringFirstMission = SCORING:New( "FirstMission" ) +-- local ScoringSecondMission = SCORING:New( "SecondMission" ) +-- +-- The above documents that 2 Scoring objects are created, ScoringFirstMission and ScoringSecondMission. +-- +-- ## 1.9) Configure messages. +-- +-- When players hit or destroy targets, messages are sent. +-- Various methods exist to configure: +-- +-- * Which messages are sent upon the event. +-- * Which audience receives the message. +-- +-- ### 1.9.1) Configure the messages sent upon the event. +-- +-- Use the following methods to configure when to send messages. By default, all messages are sent. +-- +-- * @{#SCORING.SetMessagesHit}(): Configure to send messages after a target has been hit. +-- * @{#SCORING.SetMessagesDestroy}(): Configure to send messages after a target has been destroyed. +-- * @{#SCORING.SetMessagesAddon}(): Configure to send messages for additional score, after a target has been destroyed. +-- * @{#SCORING.SetMessagesZone}(): Configure to send messages for additional score, after a target has been destroyed within a given zone. +-- +-- ### 1.9.2) Configure the audience of the messages. +-- +-- Use the following methods to configure the audience of the messages. By default, the messages are sent to all players in the mission. +-- +-- * @{#SCORING.SetMessagesToAll}(): Configure to send messages to all players. +-- * @{#SCORING.SetMessagesToCoalition}(): Configure to send messages to only those players within the same coalition as the player. +-- +-- +-- ==== +-- +-- # **API CHANGE HISTORY** +-- +-- The underlying change log documents the API changes. Please read this carefully. The following notation is used: +-- +-- * **Added** parts are expressed in bold type face. +-- * _Removed_ parts are expressed in italic type face. +-- +-- Hereby the change log: +-- +-- 2017-02-26: Initial class and API. +-- +-- === +-- +-- # **AUTHORS and CONTRIBUTIONS** +-- +-- ### Contributions: +-- +-- * **Wingthor (TAW)**: Testing & Advice. +-- * **Dutch-Baron (TAW)**: Testing & Advice. +-- * **[Whisper](http://forums.eagle.ru/member.php?u=3829): Testing and Advice. +-- +-- ### Authors: +-- +-- * **FlightControl**: Concept, Design & Programming. +-- +-- @module Scoring + + +--- The Scoring class +-- @type SCORING +-- @field Players A collection of the current players that have joined the game. +-- @extends Core.Base#BASE +SCORING = { + ClassName = "SCORING", + ClassID = 0, + Players = {}, +} + +local _SCORINGCoalition = + { + [1] = "Red", + [2] = "Blue", + } + +local _SCORINGCategory = + { + [Unit.Category.AIRPLANE] = "Plane", + [Unit.Category.HELICOPTER] = "Helicopter", + [Unit.Category.GROUND_UNIT] = "Vehicle", + [Unit.Category.SHIP] = "Ship", + [Unit.Category.STRUCTURE] = "Structure", + } + +--- Creates a new SCORING object to administer the scoring achieved by players. +-- @param #SCORING self +-- @param #string GameName The name of the game. This name is also logged in the CSV score file. +-- @return #SCORING self +-- @usage +-- -- Define a new scoring object for the mission Gori Valley. +-- ScoringObject = SCORING:New( "Gori Valley" ) +function SCORING:New( GameName ) + + -- Inherits from BASE + local self = BASE:Inherit( self, BASE:New() ) -- #SCORING + + if GameName then + self.GameName = GameName + else + error( "A game name must be given to register the scoring results" ) + end + + + -- Additional Object scores + self.ScoringObjects = {} + + -- Additional Zone scores. + self.ScoringZones = {} + + -- Configure Messages + self:SetMessagesToAll() + self:SetMessagesHit( true ) + self:SetMessagesDestroy( true ) + self:SetMessagesScore( true ) + self:SetMessagesZone( true ) + + -- Scales + self:SetScaleDestroyScore( 10 ) + self:SetScaleDestroyPenalty( 30 ) + + -- Default fratricide penalty level (maximum penalty that can be assigned to a player before he gets kicked). + self:SetFratricide( self.ScaleDestroyPenalty * 3 ) + + -- Default penalty when a player changes coalition. + self:SetCoalitionChangePenalty( self.ScaleDestroyPenalty ) + + -- Event handlers + self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) + self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) + self:HandleEvent( EVENTS.Hit, self._EventOnHit ) + self:HandleEvent( EVENTS.PlayerEnterUnit ) + self:HandleEvent( EVENTS.PlayerLeaveUnit ) + + -- Create the CSV file. + self:OpenCSV( GameName ) + + return self + +end + +--- Set the scale for scoring valid destroys (enemy destroys). +-- A default calculated score is a value between 1 and 10. +-- The scale magnifies the scores given to the players. +-- @param #SCORING self +-- @param #number Scale The scale of the score given. +function SCORING:SetScaleDestroyScore( Scale ) + + self.ScaleDestroyScore = Scale + + return self +end + +--- Set the scale for scoring penalty destroys (friendly destroys). +-- A default calculated penalty is a value between 1 and 10. +-- The scale magnifies the scores given to the players. +-- @param #SCORING self +-- @param #number Scale The scale of the score given. +-- @return #SCORING +function SCORING:SetScaleDestroyPenalty( Scale ) + + self.ScaleDestroyPenalty = Scale + + return self +end + +--- Add a @{Unit} for additional scoring when the @{Unit} is destroyed. +-- Note that if there was already a @{Unit} declared within the scoring with the same name, +-- then the old @{Unit} will be replaced with the new @{Unit}. +-- @param #SCORING self +-- @param Wrapper.Unit#UNIT ScoreUnit The @{Unit} for which the Score needs to be given. +-- @param #number Score The Score value. +-- @return #SCORING +function SCORING:AddUnitScore( ScoreUnit, Score ) + + local UnitName = ScoreUnit:GetName() + + self.ScoringObjects[UnitName] = Score + + return self +end + +--- Removes a @{Unit} for additional scoring when the @{Unit} is destroyed. +-- @param #SCORING self +-- @param Wrapper.Unit#UNIT ScoreUnit The @{Unit} for which the Score needs to be given. +-- @return #SCORING +function SCORING:RemoveUnitScore( ScoreUnit ) + + local UnitName = ScoreUnit:GetName() + + self.ScoringObjects[UnitName] = nil + + return self +end + +--- Add a @{Static} for additional scoring when the @{Static} is destroyed. +-- Note that if there was already a @{Static} declared within the scoring with the same name, +-- then the old @{Static} will be replaced with the new @{Static}. +-- @param #SCORING self +-- @param Wrapper.Static#UNIT ScoreStatic The @{Static} for which the Score needs to be given. +-- @param #number Score The Score value. +-- @return #SCORING +function SCORING:AddStaticScore( ScoreStatic, Score ) + + local StaticName = ScoreStatic:GetName() + + self.ScoringObjects[StaticName] = Score + + return self +end + +--- Removes a @{Static} for additional scoring when the @{Static} is destroyed. +-- @param #SCORING self +-- @param Wrapper.Static#UNIT ScoreStatic The @{Static} for which the Score needs to be given. +-- @return #SCORING +function SCORING:RemoveStaticScore( ScoreStatic ) + + local StaticName = ScoreStatic:GetName() + + self.ScoringObjects[StaticName] = nil + + return self +end + + +--- Specify a special additional score for a @{Group}. +-- @param #SCORING self +-- @param Wrapper.Group#GROUP ScoreGroup The @{Group} for which each @{Unit} a Score is given. +-- @param #number Score The Score value. +-- @return #SCORING +function SCORING:AddScoreGroup( ScoreGroup, Score ) + + local ScoreUnits = ScoreGroup:GetUnits() + + for ScoreUnitID, ScoreUnit in pairs( ScoreUnits ) do + local UnitName = ScoreUnit:GetName() + self.ScoringObjects[UnitName] = Score + end + + return self +end + +--- Add a @{Zone} to define additional scoring when any object is destroyed in that zone. +-- Note that if a @{Zone} with the same name is already within the scoring added, the @{Zone} (type) and Score will be replaced! +-- This allows for a dynamic destruction zone evolution within your mission. +-- @param #SCORING self +-- @param Core.Zone#ZONE_BASE ScoreZone The @{Zone} which defines the destruction score perimeters. +-- Note that a zone can be a polygon or a moving zone. +-- @param #number Score The Score value. +-- @return #SCORING +function SCORING:AddZoneScore( ScoreZone, Score ) + + local ZoneName = ScoreZone:GetName() + + self.ScoringZones[ZoneName] = {} + self.ScoringZones[ZoneName].ScoreZone = ScoreZone + self.ScoringZones[ZoneName].Score = Score + + return self +end + +--- Remove a @{Zone} for additional scoring. +-- The scoring will search if any @{Zone} is added with the given name, and will remove that zone from the scoring. +-- This allows for a dynamic destruction zone evolution within your mission. +-- @param #SCORING self +-- @param Core.Zone#ZONE_BASE ScoreZone The @{Zone} which defines the destruction score perimeters. +-- Note that a zone can be a polygon or a moving zone. +-- @return #SCORING +function SCORING:RemoveZoneScore( ScoreZone ) + + local ZoneName = ScoreZone:GetName() + + self.ScoringZones[ZoneName] = nil + + return self +end + + +--- Configure to send messages after a target has been hit. +-- @param #SCORING self +-- @param #boolean OnOff If true is given, the messages are sent. +-- @return #SCORING +function SCORING:SetMessagesHit( OnOff ) + + self.MessagesHit = OnOff + return self +end + +--- If to send messages after a target has been hit. +-- @param #SCORING self +-- @return #boolean +function SCORING:IfMessagesHit() + + return self.MessagesHit +end + +--- Configure to send messages after a target has been destroyed. +-- @param #SCORING self +-- @param #boolean OnOff If true is given, the messages are sent. +-- @return #SCORING +function SCORING:SetMessagesDestroy( OnOff ) + + self.MessagesDestroy = OnOff + return self +end + +--- If to send messages after a target has been destroyed. +-- @param #SCORING self +-- @return #boolean +function SCORING:IfMessagesDestroy() + + return self.MessagesDestroy +end + +--- Configure to send messages after a target has been destroyed and receives additional scores. +-- @param #SCORING self +-- @param #boolean OnOff If true is given, the messages are sent. +-- @return #SCORING +function SCORING:SetMessagesScore( OnOff ) + + self.MessagesScore = OnOff + return self +end + +--- If to send messages after a target has been destroyed and receives additional scores. +-- @param #SCORING self +-- @return #boolean +function SCORING:IfMessagesScore() + + return self.MessagesScore +end + +--- Configure to send messages after a target has been hit in a zone, and additional score is received. +-- @param #SCORING self +-- @param #boolean OnOff If true is given, the messages are sent. +-- @return #SCORING +function SCORING:SetMessagesZone( OnOff ) + + self.MessagesZone = OnOff + return self +end + +--- If to send messages after a target has been hit in a zone, and additional score is received. +-- @param #SCORING self +-- @return #boolean +function SCORING:IfMessagesZone() + + return self.MessagesZone +end + +--- Configure to send messages to all players. +-- @param #SCORING self +-- @return #SCORING +function SCORING:SetMessagesToAll() + + self.MessagesAudience = 1 + return self +end + +--- If to send messages to all players. +-- @param #SCORING self +-- @return #boolean +function SCORING:IfMessagesToAll() + + return self.MessagesAudience == 1 +end + +--- Configure to send messages to only those players within the same coalition as the player. +-- @param #SCORING self +-- @return #SCORING +function SCORING:SetMessagesToCoalition() + + self.MessagesAudience = 2 + return self +end + +--- If to send messages to only those players within the same coalition as the player. +-- @param #SCORING self +-- @return #boolean +function SCORING:IfMessagesToCoalition() + + return self.MessagesAudience == 2 +end + + +--- When a player commits too much damage to friendlies, his penalty score will reach a certain level. +-- Use this method to define the level when a player gets kicked. +-- By default, the fratricide level is the default penalty mutiplier * 2 for the penalty score. +-- @param #SCORING self +-- @param #number Fratricide The amount of maximum penalty that may be inflicted by a friendly player before he gets kicked. +-- @return #SCORING +function SCORING:SetFratricide( Fratricide ) + + self.Fratricide = Fratricide + return self +end + + +--- When a player changes the coalition, he can receive a penalty score. +-- Use the method @{#SCORING.SetCoalitionChangePenalty}() to define the penalty when a player changes coalition. +-- By default, the penalty for changing coalition is the default penalty scale. +-- @param #SCORING self +-- @param #number CoalitionChangePenalty The amount of penalty that is given. +-- @return #SCORING +function SCORING:SetCoalitionChangePenalty( CoalitionChangePenalty ) + + self.CoalitionChangePenalty = CoalitionChangePenalty + return self +end + + +--- Add a new player entering a Unit. +-- @param #SCORING self +-- @param Wrapper.Unit#UNIT UnitData +function SCORING:_AddPlayerFromUnit( UnitData ) + self:F( UnitData ) + + if UnitData:IsAlive() then + local UnitName = UnitData:GetName() + local PlayerName = UnitData:GetPlayerName() + local UnitDesc = UnitData:GetDesc() + local UnitCategory = UnitDesc.category + local UnitCoalition = UnitData:GetCoalition() + local UnitTypeName = UnitData:GetTypeName() + + self:T( { PlayerName, UnitName, UnitCategory, UnitCoalition, UnitTypeName } ) + + if self.Players[PlayerName] == nil then -- I believe this is the place where a Player gets a life in a mission when he enters a unit ... + self.Players[PlayerName] = {} + self.Players[PlayerName].Hit = {} + self.Players[PlayerName].Destroy = {} + self.Players[PlayerName].Goals = {} + self.Players[PlayerName].Mission = {} + + -- for CategoryID, CategoryName in pairs( SCORINGCategory ) do + -- self.Players[PlayerName].Hit[CategoryID] = {} + -- self.Players[PlayerName].Destroy[CategoryID] = {} + -- end + self.Players[PlayerName].HitPlayers = {} + self.Players[PlayerName].Score = 0 + self.Players[PlayerName].Penalty = 0 + self.Players[PlayerName].PenaltyCoalition = 0 + self.Players[PlayerName].PenaltyWarning = 0 + end + + if not self.Players[PlayerName].UnitCoalition then + self.Players[PlayerName].UnitCoalition = UnitCoalition + else + if self.Players[PlayerName].UnitCoalition ~= UnitCoalition then + self.Players[PlayerName].Penalty = self.Players[PlayerName].Penalty + 50 + self.Players[PlayerName].PenaltyCoalition = self.Players[PlayerName].PenaltyCoalition + 1 + MESSAGE: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 + self.Players[PlayerName].UNIT = UnitData + + if self.Players[PlayerName].Penalty > self.Fratricide * 0.50 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 " .. self.Fratricide .. ", 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 > self.Fratricide then + UnitData:Destroy() + MESSAGE:New( "Player '" .. PlayerName .. "' committed FRATRICIDE, he will be COURT MARTIALED and is DISMISSED from this mission!", + 10 + ):ToAll() + end + + end +end + + +--- Add a goal score for a player. +-- The method takes the PlayerUnit for which the Goal score needs to be set. +-- The GoalTag is a string or identifier that is taken into the CSV file scoring log to identify the goal. +-- A free text can be given that is shown to the players. +-- The Score can be both positive and negative. +-- @param #SCORING self +-- @param Wrapper.Unit#UNIT PlayerUnit The @{Unit} of the Player. Other Properties for the scoring are taken from this PlayerUnit, like coalition, type etc. +-- @param #string GoalTag The string or identifier that is used in the CSV file to identify the goal (sort or group later in Excel). +-- @param #string Text A free text that is shown to the players. +-- @param #number Score The score can be both positive or negative ( Penalty ). +function SCORING:AddGoalScore( PlayerUnit, GoalTag, Text, Score ) + + local PlayerName = PlayerUnit:GetPlayerName() + + self:E( { PlayerUnit.UnitName, PlayerName, GoalTag, Text, Score } ) + + -- PlayerName can be nil, if the Unit with the player crashed or due to another reason. + if PlayerName then + local PlayerData = self.Players[PlayerName] + + PlayerData.Goals[GoalTag] = PlayerData.Goals[GoalTag] or { Score = 0 } + PlayerData.Goals[GoalTag].Score = PlayerData.Goals[GoalTag].Score + Score + PlayerData.Score = PlayerData.Score + Score + + MESSAGE:New( Text, 30 ):ToAll() + + self:ScoreCSV( PlayerName, "GOAL_" .. string.upper( GoalTag ), 1, Score, PlayerUnit:GetName() ) + end +end + + +--- Registers Scores the players completing a Mission Task. +-- @param #SCORING self +-- @param Tasking.Mission#MISSION Mission +-- @param Wrapper.Unit#UNIT PlayerUnit +-- @param #string Text +-- @param #number Score +function SCORING:_AddMissionTaskScore( Mission, PlayerUnit, Text, Score ) + + local PlayerName = PlayerUnit:GetPlayerName() + local MissionName = Mission:GetName() + + self:E( { Mission:GetName(), PlayerUnit.UnitName, PlayerName, Text, Score } ) + + -- PlayerName can be nil, if the Unit with the player crashed or due to another reason. + if PlayerName then + local PlayerData = self.Players[PlayerName] + + if not PlayerData.Mission[MissionName] then + PlayerData.Mission[MissionName] = {} + PlayerData.Mission[MissionName].ScoreTask = 0 + PlayerData.Mission[MissionName].ScoreMission = 0 + end + + self:T( PlayerName ) + self:T( PlayerData.Mission[MissionName] ) + + PlayerData.Score = self.Players[PlayerName].Score + Score + PlayerData.Mission[MissionName].ScoreTask = self.Players[PlayerName].Mission[MissionName].ScoreTask + Score + + MESSAGE:New( "Player '" .. PlayerName .. "' has " .. Text .. " in Mission '" .. MissionName .. "'. " .. + Score .. " task score!", + 30 ):ToAll() + + self:ScoreCSV( PlayerName, "TASK_" .. MissionName:gsub( ' ', '_' ), 1, Score, PlayerUnit:GetName() ) + end +end + + +--- Registers Mission Scores for possible multiple players that contributed in the Mission. +-- @param #SCORING self +-- @param Tasking.Mission#MISSION Mission +-- @param Wrapper.Unit#UNIT PlayerUnit +-- @param #string Text +-- @param #number Score +function SCORING:_AddMissionScore( Mission, Text, Score ) + + local MissionName = Mission:GetName() + + self:E( { Mission, Text, Score } ) + self:E( self.Players ) + + for PlayerName, PlayerData in pairs( self.Players ) do + + self:E( PlayerData ) + if PlayerData.Mission[MissionName] then + + PlayerData.Score = PlayerData.Score + Score + PlayerData.Mission[MissionName].ScoreMission = PlayerData.Mission[MissionName].ScoreMission + Score + + MESSAGE:New( "Player '" .. PlayerName .. "' has " .. Text .. " in Mission '" .. MissionName .. "'. " .. + Score .. " mission score!", + 60 ):ToAll() + + self:ScoreCSV( PlayerName, "MISSION_" .. MissionName:gsub( ' ', '_' ), 1, Score ) + end + end +end + + +--- Handles the OnPlayerEnterUnit event for the scoring. +-- @param #SCORING self +-- @param Core.Event#EVENTDATA Event +function SCORING:OnEventPlayerEnterUnit( Event ) + if Event.IniUnit then + self:_AddPlayerFromUnit( Event.IniUnit ) + local Menu = MENU_GROUP:New( Event.IniGroup, 'Scoring' ) + local ReportGroupSummary = MENU_GROUP_COMMAND:New( Event.IniGroup, 'Summary report players in group', Menu, SCORING.ReportScoreGroupSummary, self, Event.IniGroup ) + local ReportGroupDetailed = MENU_GROUP_COMMAND:New( Event.IniGroup, 'Detailed report players in group', Menu, SCORING.ReportScoreGroupDetailed, self, Event.IniGroup ) + local ReportToAllSummary = MENU_GROUP_COMMAND:New( Event.IniGroup, 'Summary report all players', Menu, SCORING.ReportScoreAllSummary, self, Event.IniGroup ) + self:SetState( Event.IniUnit, "ScoringMenu", Menu ) + end +end + +--- Handles the OnPlayerLeaveUnit event for the scoring. +-- @param #SCORING self +-- @param Core.Event#EVENTDATA Event +function SCORING:OnEventPlayerLeaveUnit( Event ) + if Event.IniUnit then + local Menu = self:GetState( Event.IniUnit, "ScoringMenu" ) -- Core.Menu#MENU_GROUP + if Menu then + Menu:Remove() + end + end +end + + +--- Handles the OnHit event for the scoring. +-- @param #SCORING self +-- @param Core.Event#EVENTDATA Event +function SCORING:_EventOnHit( Event ) + self:F( { Event } ) + + local InitUnit = nil + local InitUNIT = nil + local InitUnitName = "" + local InitGroup = nil + local InitGroupName = "" + local InitPlayerName = nil + + local InitCoalition = nil + local InitCategory = nil + local InitType = nil + local InitUnitCoalition = nil + local InitUnitCategory = nil + local InitUnitType = nil + + local TargetUnit = nil + local TargetUNIT = nil + local TargetUnitName = "" + local TargetGroup = nil + local TargetGroupName = "" + local TargetPlayerName = nil + + local TargetCoalition = nil + local TargetCategory = nil + local TargetType = nil + local TargetUnitCoalition = nil + local TargetUnitCategory = nil + local TargetUnitType = nil + + if Event.IniDCSUnit then + + InitUnit = Event.IniDCSUnit + InitUNIT = Event.IniUnit + InitUnitName = Event.IniDCSUnitName + InitGroup = Event.IniDCSGroup + InitGroupName = Event.IniDCSGroupName + InitPlayerName = Event.IniPlayerName + + InitCoalition = Event.IniCoalition + --TODO: Workaround Client DCS Bug + --InitCategory = InitUnit:getCategory() + --InitCategory = InitUnit:getDesc().category + InitCategory = Event.IniCategory + InitType = Event.IniTypeName + + InitUnitCoalition = _SCORINGCoalition[InitCoalition] + InitUnitCategory = _SCORINGCategory[InitCategory] + InitUnitType = InitType + + self:T( { InitUnitName, InitGroupName, InitPlayerName, InitCoalition, InitCategory, InitType , InitUnitCoalition, InitUnitCategory, InitUnitType } ) + end + + + if Event.TgtDCSUnit then + + TargetUnit = Event.TgtDCSUnit + TargetUNIT = Event.TgtUnit + TargetUnitName = Event.TgtDCSUnitName + TargetGroup = Event.TgtDCSGroup + TargetGroupName = Event.TgtDCSGroupName + TargetPlayerName = Event.TgtPlayerName + + TargetCoalition = Event.TgtCoalition + --TODO: Workaround Client DCS Bug + --TargetCategory = TargetUnit:getCategory() + --TargetCategory = TargetUnit:getDesc().category + TargetCategory = Event.TgtCategory + TargetType = Event.TgtTypeName + + TargetUnitCoalition = _SCORINGCoalition[TargetCoalition] + TargetUnitCategory = _SCORINGCategory[TargetCategory] + TargetUnitType = TargetType + + self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType, TargetUnitCoalition, TargetUnitCategory, TargetUnitType } ) + end + + if InitPlayerName ~= nil then -- It is a player that is hitting something + self:_AddPlayerFromUnit( InitUNIT ) + if self.Players[InitPlayerName] then -- This should normally not happen, but i'll test it anyway. + if TargetPlayerName ~= nil then -- It is a player hitting another player ... + self:_AddPlayerFromUnit( TargetUNIT ) + end + + self:T( "Hitting Something" ) + + -- What is he hitting? + if TargetCategory then + + -- A target got hit, score it. + -- Player contains the score data from self.Players[InitPlayerName] + local Player = self.Players[InitPlayerName] + + -- Ensure there is a hit table per TargetCategory and TargetUnitName. + Player.Hit[TargetCategory] = Player.Hit[TargetCategory] or {} + Player.Hit[TargetCategory][TargetUnitName] = Player.Hit[TargetCategory][TargetUnitName] or {} + + -- PlayerHit contains the score counters and data per unit that was hit. + local PlayerHit = Player.Hit[TargetCategory][TargetUnitName] + + PlayerHit.Score = PlayerHit.Score or 0 + PlayerHit.Penalty = PlayerHit.Penalty or 0 + PlayerHit.ScoreHit = PlayerHit.ScoreHit or 0 + PlayerHit.PenaltyHit = PlayerHit.PenaltyHit or 0 + PlayerHit.TimeStamp = PlayerHit.TimeStamp or 0 + PlayerHit.UNIT = PlayerHit.UNIT or TargetUNIT + + -- Only grant hit scores if there was more than one second between the last hit. + if timer.getTime() - PlayerHit.TimeStamp > 1 then + PlayerHit.TimeStamp = timer.getTime() + + if TargetPlayerName ~= nil then -- It is a player hitting another player ... + + -- Ensure there is a Player to Player hit reference table. + Player.HitPlayers[TargetPlayerName] = true + end + + local Score = 0 + + if InitCoalition then -- A coalition object was hit. + if InitCoalition == TargetCoalition then + Player.Penalty = Player.Penalty + 10 + PlayerHit.Penalty = PlayerHit.Penalty + 10 + PlayerHit.PenaltyHit = PlayerHit.PenaltyHit + 1 + + if TargetPlayerName ~= nil then -- It is a player hitting another player ... + MESSAGE + :New( "Player '" .. InitPlayerName .. "' hit friendly player '" .. TargetPlayerName .. "' " .. + TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.PenaltyHit .. " times. " .. + "Penalty: -" .. PlayerHit.Penalty .. ". Score Total:" .. Player.Score - Player.Penalty, + 2 + ) + :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) + else + MESSAGE + :New( "Player '" .. InitPlayerName .. "' hit a friendly target " .. + TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.PenaltyHit .. " times. " .. + "Penalty: -" .. PlayerHit.Penalty .. ". Score Total:" .. Player.Score - Player.Penalty, + 2 + ) + :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) + end + self:ScoreCSV( InitPlayerName, "HIT_PENALTY", 1, -25, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + else + Player.Score = Player.Score + 1 + PlayerHit.Score = PlayerHit.Score + 1 + PlayerHit.ScoreHit = PlayerHit.ScoreHit + 1 + if TargetPlayerName ~= nil then -- It is a player hitting another player ... + MESSAGE + :New( "Player '" .. InitPlayerName .. "' hit enemy player '" .. TargetPlayerName .. "' " .. + TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.ScoreHit .. " times. " .. + "Score: " .. PlayerHit.Score .. ". Score Total:" .. Player.Score - Player.Penalty, + 2 + ) + :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) + else + MESSAGE + :New( "Player '" .. InitPlayerName .. "' hit an enemy target " .. + TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.ScoreHit .. " times. " .. + "Score: " .. PlayerHit.Score .. ". Score Total:" .. Player.Score - Player.Penalty, + 2 + ) + :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) + end + self:ScoreCSV( InitPlayerName, "HIT_SCORE", 1, 1, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + end + else -- A scenery object was hit. + MESSAGE + :New( "Player '" .. InitPlayerName .. "' hit a scenery object.", + 2 + ) + :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) + self:ScoreCSV( InitPlayerName, "HIT_SCORE", 1, 1, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, "", "Scenery", TargetUnitType ) + end + end + end + end + elseif InitPlayerName == nil then -- It is an AI hitting a player??? + + end +end + +--- Track DEAD or CRASH events for the scoring. +-- @param #SCORING self +-- @param Core.Event#EVENTDATA Event +function SCORING:_EventOnDeadOrCrash( Event ) + self:F( { Event } ) + + local TargetUnit = nil + local TargetGroup = nil + local TargetUnitName = "" + local TargetGroupName = "" + local TargetPlayerName = "" + local TargetCoalition = nil + local TargetCategory = nil + local TargetType = nil + local TargetUnitCoalition = nil + local TargetUnitCategory = nil + local TargetUnitType = nil + + if Event.IniDCSUnit then + + TargetUnit = Event.IniUnit + TargetUnitName = Event.IniDCSUnitName + TargetGroup = Event.IniDCSGroup + TargetGroupName = Event.IniDCSGroupName + TargetPlayerName = Event.IniPlayerName + + TargetCoalition = Event.IniCoalition + --TargetCategory = TargetUnit:getCategory() + --TargetCategory = TargetUnit:getDesc().category -- Workaround + TargetCategory = Event.IniCategory + TargetType = Event.IniTypeName + + TargetUnitCoalition = _SCORINGCoalition[TargetCoalition] + TargetUnitCategory = _SCORINGCategory[TargetCategory] + TargetUnitType = TargetType + + self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType } ) + end + + -- Player contains the score and reference data for the player. + for PlayerName, Player in pairs( self.Players ) do + if Player then -- This should normally not happen, but i'll test it anyway. + self:T( "Something got destroyed" ) + + -- Some variables + local InitUnitName = Player.UnitName + local InitUnitType = Player.UnitType + local InitCoalition = Player.UnitCoalition + local InitCategory = Player.UnitCategory + local InitUnitCoalition = _SCORINGCoalition[InitCoalition] + local InitUnitCategory = _SCORINGCategory[InitCategory] + + self:T( { InitUnitName, InitUnitType, InitUnitCoalition, InitCoalition, InitUnitCategory, InitCategory } ) + + -- What is the player destroying? + if Player and Player.Hit and Player.Hit[TargetCategory] and Player.Hit[TargetCategory][TargetUnitName] then -- Was there a hit for this unit for this player before registered??? + + Player.Destroy[TargetCategory] = Player.Destroy[TargetCategory] or {} + Player.Destroy[TargetCategory][TargetType] = Player.Destroy[TargetCategory][TargetType] or {} + + -- PlayerDestroy contains the destroy score data per category and target type of the player. + local TargetDestroy = Player.Destroy[TargetCategory][TargetType] + TargetDestroy.Score = TargetDestroy.Score or 0 + TargetDestroy.ScoreDestroy = TargetDestroy.ScoreDestroy or 0 + TargetDestroy.Penalty = TargetDestroy.Penalty or 0 + TargetDestroy.PenaltyDestroy = TargetDestroy.PenaltyDestroy or 0 + + if TargetCoalition then + if InitCoalition == TargetCoalition then + local ThreatLevelTarget, ThreatTypeTarget = TargetUnit:GetThreatLevel() + local ThreatLevelPlayer = Player.UNIT:GetThreatLevel() / 10 + 1 + local ThreatPenalty = math.ceil( ( ThreatLevelTarget / ThreatLevelPlayer ) * self.ScaleDestroyPenalty / 10 ) + self:E( { ThreatLevel = ThreatPenalty, ThreatLevelTarget = ThreatLevelTarget, ThreatTypeTarget = ThreatTypeTarget, ThreatLevelPlayer = ThreatLevelPlayer } ) + + Player.Penalty = Player.Penalty + ThreatPenalty + TargetDestroy.Penalty = TargetDestroy.Penalty + ThreatPenalty + TargetDestroy.PenaltyDestroy = TargetDestroy.PenaltyDestroy + 1 + + if Player.HitPlayers[TargetPlayerName] then -- A player destroyed another player + MESSAGE + :New( "Player '" .. PlayerName .. "' destroyed friendly player '" .. TargetPlayerName .. "' " .. + TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. TargetDestroy.PenaltyDestroy .. " times. " .. + "Penalty: -" .. TargetDestroy.Penalty .. ". Score Total:" .. Player.Score - Player.Penalty, + 15 + ) + :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) + else + MESSAGE + :New( "Player '" .. PlayerName .. "' destroyed a friendly target " .. + TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. TargetDestroy.PenaltyDestroy .. " times. " .. + "Penalty: -" .. TargetDestroy.Penalty .. ". Score Total:" .. Player.Score - Player.Penalty, + 15 + ) + :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) + end + self:ScoreCSV( PlayerName, "DESTROY_PENALTY", 1, ThreatPenalty, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + else + + local ThreatLevelTarget, ThreatTypeTarget = TargetUnit:GetThreatLevel() + local ThreatLevelPlayer = Player.UNIT:GetThreatLevel() / 10 + 1 + local ThreatScore = math.ceil( ( ThreatLevelTarget / ThreatLevelPlayer ) * self.ScaleDestroyScore / 10 ) + + self:E( { ThreatLevel = ThreatScore, ThreatLevelTarget = ThreatLevelTarget, ThreatTypeTarget = ThreatTypeTarget, ThreatLevelPlayer = ThreatLevelPlayer } ) + + Player.Score = Player.Score + ThreatScore + TargetDestroy.Score = TargetDestroy.Score + ThreatScore + TargetDestroy.ScoreDestroy = TargetDestroy.ScoreDestroy + 1 + if Player.HitPlayers[TargetPlayerName] then -- A player destroyed another player + MESSAGE + :New( "Player '" .. PlayerName .. "' destroyed enemy player '" .. TargetPlayerName .. "' " .. + TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. TargetDestroy.ScoreDestroy .. " times. " .. + "Score: " .. TargetDestroy.Score .. ". Score Total:" .. Player.Score - Player.Penalty, + 15 + ) + :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) + else + MESSAGE + :New( "Player '" .. PlayerName .. "' destroyed an enemy " .. + TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. TargetDestroy.ScoreDestroy .. " times. " .. + "Score: " .. TargetDestroy.Score .. ". Total:" .. Player.Score - Player.Penalty, + 15 + ) + :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) + end + self:ScoreCSV( PlayerName, "DESTROY_SCORE", 1, ThreatScore, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + + local UnitName = TargetUnit:GetName() + local Score = self.ScoringObjects[UnitName] + if Score then + Player.Score = Player.Score + Score + TargetDestroy.Score = TargetDestroy.Score + Score + MESSAGE + :New( "Special target '" .. TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. " destroyed! " .. + "Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! Total: " .. Player.Score - Player.Penalty, + 15 + ) + :ToAllIf( self:IfMessagesScore() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesScore() and self:IfMessagesToCoalition() ) + self:ScoreCSV( PlayerName, "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + end + + -- Check if there are Zones where the destruction happened. + for ZoneName, ScoreZoneData in pairs( self.ScoringZones ) do + self:E( { ScoringZone = ScoreZoneData } ) + local ScoreZone = ScoreZoneData.ScoreZone -- Core.Zone#ZONE_BASE + local Score = ScoreZoneData.Score + if ScoreZone:IsVec2InZone( TargetUnit:GetVec2() ) then + Player.Score = Player.Score + Score + TargetDestroy.Score = TargetDestroy.Score + Score + MESSAGE + :New( "Target destroyed in zone '" .. ScoreZone:GetName() .. "'." .. + "Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! " .. + "Total: " .. Player.Score - Player.Penalty, + 15 ) + :ToAllIf( self:IfMessagesZone() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesZone() and self:IfMessagesToCoalition() ) + self:ScoreCSV( PlayerName, "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + end + end + + end + else + -- Check if there are Zones where the destruction happened. + for ZoneName, ScoreZoneData in pairs( self.ScoringZones ) do + self:E( { ScoringZone = ScoreZoneData } ) + local ScoreZone = ScoreZoneData.ScoreZone -- Core.Zone#ZONE_BASE + local Score = ScoreZoneData.Score + if ScoreZone:IsVec2InZone( TargetUnit:GetVec2() ) then + Player.Score = Player.Score + Score + TargetDestroy.Score = TargetDestroy.Score + Score + MESSAGE + :New( "Scenery destroyed in zone '" .. ScoreZone:GetName() .. "'." .. + "Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! " .. + "Total: " .. Player.Score - Player.Penalty, + 15 + ) + :ToAllIf( self:IfMessagesZone() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesZone() and self:IfMessagesToCoalition() ) + self:ScoreCSV( PlayerName, "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, "", "Scenery", TargetUnitType ) + end + end + end + end + end + end +end + + +--- Produce detailed report of player hit scores. +-- @param #SCORING self +-- @param #string PlayerName The name of the player. +-- @return #string The report. +function SCORING:ReportDetailedPlayerHits( PlayerName ) + + local ScoreMessage = "" + local PlayerScore = 0 + local PlayerPenalty = 0 + + local PlayerData = self.Players[PlayerName] + if PlayerData then -- This should normally not happen, but i'll test it anyway. + self:T( "Score Player: " .. PlayerName ) + + -- Some variables + local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] + local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] + local InitUnitType = PlayerData.UnitType + local InitUnitName = PlayerData.UnitName + + local ScoreMessageHits = "" + for CategoryID, CategoryName in pairs( _SCORINGCategory ) do + self:T( CategoryName ) + if PlayerData.Hit[CategoryID] then + self:T( "Hit scores exist for player " .. PlayerName ) + local Score = 0 + local ScoreHit = 0 + local Penalty = 0 + local PenaltyHit = 0 + for UnitName, UnitData in pairs( PlayerData.Hit[CategoryID] ) do + Score = Score + UnitData.Score + ScoreHit = ScoreHit + UnitData.ScoreHit + Penalty = Penalty + UnitData.Penalty + PenaltyHit = UnitData.PenaltyHit + end + local ScoreMessageHit = string.format( "%s:%d ", CategoryName, Score - Penalty ) + self:T( ScoreMessageHit ) + ScoreMessageHits = ScoreMessageHits .. ScoreMessageHit + PlayerScore = PlayerScore + Score + PlayerPenalty = PlayerPenalty + Penalty + else + --ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) + end + end + if ScoreMessageHits ~= "" then + ScoreMessage = "Hits: " .. ScoreMessageHits + end + end + + return ScoreMessage, PlayerScore, PlayerPenalty +end + + +--- Produce detailed report of player destroy scores. +-- @param #SCORING self +-- @param #string PlayerName The name of the player. +-- @return #string The report. +function SCORING:ReportDetailedPlayerDestroys( PlayerName ) + + local ScoreMessage = "" + local PlayerScore = 0 + local PlayerPenalty = 0 + + local PlayerData = self.Players[PlayerName] + if PlayerData then -- This should normally not happen, but i'll test it anyway. + self:T( "Score Player: " .. PlayerName ) + + -- Some variables + local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] + local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] + local InitUnitType = PlayerData.UnitType + local InitUnitName = PlayerData.UnitName + + local ScoreMessageDestroys = "" + for CategoryID, CategoryName in pairs( _SCORINGCategory ) do + if PlayerData.Destroy[CategoryID] then + self:T( "Destroy scores exist for player " .. PlayerName ) + local Score = 0 + local ScoreDestroy = 0 + local Penalty = 0 + local PenaltyDestroy = 0 + + for UnitName, UnitData in pairs( PlayerData.Destroy[CategoryID] ) do + self:E( { UnitData = UnitData } ) + if UnitData ~= {} then + Score = Score + UnitData.Score + ScoreDestroy = ScoreDestroy + UnitData.ScoreDestroy + Penalty = Penalty + UnitData.Penalty + PenaltyDestroy = PenaltyDestroy + UnitData.PenaltyDestroy + end + end + + local ScoreMessageDestroy = string.format( " %s:%d ", CategoryName, Score - Penalty ) + self:T( ScoreMessageDestroy ) + ScoreMessageDestroys = ScoreMessageDestroys .. ScoreMessageDestroy + + PlayerScore = PlayerScore + Score + PlayerPenalty = PlayerPenalty + Penalty + else + --ScoreMessageDestroys = ScoreMessageDestroys .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) + end + end + if ScoreMessageDestroys ~= "" then + ScoreMessage = "Destroys: " .. ScoreMessageDestroys + end + end + + return ScoreMessage, PlayerScore, PlayerPenalty +end + +--- Produce detailed report of player penalty scores because of changing the coalition. +-- @param #SCORING self +-- @param #string PlayerName The name of the player. +-- @return #string The report. +function SCORING:ReportDetailedPlayerCoalitionChanges( PlayerName ) + + local ScoreMessage = "" + local PlayerScore = 0 + local PlayerPenalty = 0 + + local PlayerData = self.Players[PlayerName] + if PlayerData then -- This should normally not happen, but i'll test it anyway. + self:T( "Score Player: " .. PlayerName ) + + -- Some variables + local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] + local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] + local InitUnitType = PlayerData.UnitType + local InitUnitName = PlayerData.UnitName + + local ScoreMessageCoalitionChangePenalties = "" + if PlayerData.PenaltyCoalition ~= 0 then + ScoreMessageCoalitionChangePenalties = ScoreMessageCoalitionChangePenalties .. string.format( " -%d (%d changed)", PlayerData.Penalty, PlayerData.PenaltyCoalition ) + PlayerPenalty = PlayerPenalty + PlayerData.Penalty + end + if ScoreMessageCoalitionChangePenalties ~= "" then + ScoreMessage = ScoreMessage .. "Coalition Penalties: " .. ScoreMessageCoalitionChangePenalties + end + end + + return ScoreMessage, PlayerScore, PlayerPenalty +end + +--- Produce detailed report of player goal scores. +-- @param #SCORING self +-- @param #string PlayerName The name of the player. +-- @return #string The report. +function SCORING:ReportDetailedPlayerGoals( PlayerName ) + + local ScoreMessage = "" + local PlayerScore = 0 + local PlayerPenalty = 0 + + local PlayerData = self.Players[PlayerName] + if PlayerData then -- This should normally not happen, but i'll test it anyway. + self:T( "Score Player: " .. PlayerName ) + + -- Some variables + local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] + local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] + local InitUnitType = PlayerData.UnitType + local InitUnitName = PlayerData.UnitName + + local ScoreMessageGoal = "" + local ScoreGoal = 0 + local ScoreTask = 0 + for GoalName, GoalData in pairs( PlayerData.Goals ) do + ScoreGoal = ScoreGoal + GoalData.Score + ScoreMessageGoal = ScoreMessageGoal .. "'" .. GoalName .. "':" .. GoalData.Score .. "; " + end + PlayerScore = PlayerScore + ScoreGoal + + if ScoreMessageGoal ~= "" then + ScoreMessage = "Goals: " .. ScoreMessageGoal + end + end + + return ScoreMessage, PlayerScore, PlayerPenalty +end + +--- Produce detailed report of player penalty scores because of changing the coalition. +-- @param #SCORING self +-- @param #string PlayerName The name of the player. +-- @return #string The report. +function SCORING:ReportDetailedPlayerMissions( PlayerName ) + + local ScoreMessage = "" + local PlayerScore = 0 + local PlayerPenalty = 0 + + local PlayerData = self.Players[PlayerName] + if PlayerData then -- This should normally not happen, but i'll test it anyway. + self:T( "Score Player: " .. PlayerName ) + + -- Some variables + local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] + local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] + local InitUnitType = PlayerData.UnitType + local InitUnitName = PlayerData.UnitName + + local ScoreMessageMission = "" + local ScoreMission = 0 + local ScoreTask = 0 + for MissionName, MissionData in pairs( PlayerData.Mission ) do + ScoreMission = ScoreMission + MissionData.ScoreMission + ScoreTask = ScoreTask + MissionData.ScoreTask + ScoreMessageMission = ScoreMessageMission .. "'" .. MissionName .. "'; " + end + PlayerScore = PlayerScore + ScoreMission + ScoreTask + + if ScoreMessageMission ~= "" then + ScoreMessage = "Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ")" + end + end + + return ScoreMessage, PlayerScore, PlayerPenalty +end + + +--- Report Group Score Summary +-- @param #SCORING self +-- @param Wrapper.Group#GROUP PlayerGroup The player group. +function SCORING:ReportScoreGroupSummary( PlayerGroup ) + + local PlayerMessage = "" + + self:T( "Report Score Group Summary" ) + + local PlayerUnits = PlayerGroup:GetUnits() + for UnitID, PlayerUnit in pairs( PlayerUnits ) do + local PlayerUnit = PlayerUnit -- Wrapper.Unit#UNIT + local PlayerName = PlayerUnit:GetPlayerName() + + if PlayerName then + + local ReportHits, ScoreHits, PenaltyHits = self:ReportDetailedPlayerHits( PlayerName ) + ReportHits = ReportHits ~= "" and "\n- " .. ReportHits or ReportHits + self:E( { ReportHits, ScoreHits, PenaltyHits } ) + + local ReportDestroys, ScoreDestroys, PenaltyDestroys = self:ReportDetailedPlayerDestroys( PlayerName ) + ReportDestroys = ReportDestroys ~= "" and "\n- " .. ReportDestroys or ReportDestroys + self:E( { ReportDestroys, ScoreDestroys, PenaltyDestroys } ) + + local ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges = self:ReportDetailedPlayerCoalitionChanges( PlayerName ) + ReportCoalitionChanges = ReportCoalitionChanges ~= "" and "\n- " .. ReportCoalitionChanges or ReportCoalitionChanges + self:E( { ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges } ) + + local ReportGoals, ScoreGoals, PenaltyGoals = self:ReportDetailedPlayerGoals( PlayerName ) + ReportGoals = ReportGoals ~= "" and "\n- " .. ReportGoals or ReportGoals + self:E( { ReportGoals, ScoreGoals, PenaltyGoals } ) + + local ReportMissions, ScoreMissions, PenaltyMissions = self:ReportDetailedPlayerMissions( PlayerName ) + ReportMissions = ReportMissions ~= "" and "\n- " .. ReportMissions or ReportMissions + self:E( { ReportMissions, ScoreMissions, PenaltyMissions } ) + + local PlayerScore = ScoreHits + ScoreDestroys + ScoreCoalitionChanges + ScoreGoals + ScoreMissions + local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + ScoreGoals + PenaltyMissions + + PlayerMessage = + string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )", + PlayerName, + PlayerScore - PlayerPenalty, + PlayerScore, + PlayerPenalty + ) + MESSAGE:New( PlayerMessage, 30, "Player '" .. PlayerName .. "'" ):ToGroup( PlayerGroup ) + end + end + +end + +--- Report Group Score Detailed +-- @param #SCORING self +-- @param Wrapper.Group#GROUP PlayerGroup The player group. +function SCORING:ReportScoreGroupDetailed( PlayerGroup ) + + local PlayerMessage = "" + + self:T( "Report Score Group Detailed" ) + + local PlayerUnits = PlayerGroup:GetUnits() + for UnitID, PlayerUnit in pairs( PlayerUnits ) do + local PlayerUnit = PlayerUnit -- Wrapper.Unit#UNIT + local PlayerName = PlayerUnit:GetPlayerName() + + if PlayerName then + + local ReportHits, ScoreHits, PenaltyHits = self:ReportDetailedPlayerHits( PlayerName ) + ReportHits = ReportHits ~= "" and "\n- " .. ReportHits or ReportHits + self:E( { ReportHits, ScoreHits, PenaltyHits } ) + + local ReportDestroys, ScoreDestroys, PenaltyDestroys = self:ReportDetailedPlayerDestroys( PlayerName ) + ReportDestroys = ReportDestroys ~= "" and "\n- " .. ReportDestroys or ReportDestroys + self:E( { ReportDestroys, ScoreDestroys, PenaltyDestroys } ) + + local ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges = self:ReportDetailedPlayerCoalitionChanges( PlayerName ) + ReportCoalitionChanges = ReportCoalitionChanges ~= "" and "\n- " .. ReportCoalitionChanges or ReportCoalitionChanges + self:E( { ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges } ) + + local ReportGoals, ScoreGoals, PenaltyGoals = self:ReportDetailedPlayerGoals( PlayerName ) + ReportGoals = ReportGoals ~= "" and "\n- " .. ReportGoals or ReportGoals + self:E( { ReportGoals, ScoreGoals, PenaltyGoals } ) + + local ReportMissions, ScoreMissions, PenaltyMissions = self:ReportDetailedPlayerMissions( PlayerName ) + ReportMissions = ReportMissions ~= "" and "\n- " .. ReportMissions or ReportMissions + self:E( { ReportMissions, ScoreMissions, PenaltyMissions } ) + + local PlayerScore = ScoreHits + ScoreDestroys + ScoreCoalitionChanges + ScoreGoals + ScoreMissions + local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + ScoreGoals + PenaltyMissions + + PlayerMessage = + string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )%s%s%s%s%s", + PlayerName, + PlayerScore - PlayerPenalty, + PlayerScore, + PlayerPenalty, + ReportHits, + ReportDestroys, + ReportCoalitionChanges, + ReportGoals, + ReportMissions + ) + MESSAGE:New( PlayerMessage, 30, "Player '" .. PlayerName .. "'" ):ToGroup( PlayerGroup ) + end + end + +end + +--- Report all players score +-- @param #SCORING self +-- @param Wrapper.Group#GROUP PlayerGroup The player group. +function SCORING:ReportScoreAllSummary( PlayerGroup ) + + local PlayerMessage = "" + + self:T( "Report Score All Players" ) + + for PlayerName, PlayerData in pairs( self.Players ) do + + if PlayerName then + + local ReportHits, ScoreHits, PenaltyHits = self:ReportDetailedPlayerHits( PlayerName ) + ReportHits = ReportHits ~= "" and "\n- " .. ReportHits or ReportHits + self:E( { ReportHits, ScoreHits, PenaltyHits } ) + + local ReportDestroys, ScoreDestroys, PenaltyDestroys = self:ReportDetailedPlayerDestroys( PlayerName ) + ReportDestroys = ReportDestroys ~= "" and "\n- " .. ReportDestroys or ReportDestroys + self:E( { ReportDestroys, ScoreDestroys, PenaltyDestroys } ) + + local ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges = self:ReportDetailedPlayerCoalitionChanges( PlayerName ) + ReportCoalitionChanges = ReportCoalitionChanges ~= "" and "\n- " .. ReportCoalitionChanges or ReportCoalitionChanges + self:E( { ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges } ) + + local ReportGoals, ScoreGoals, PenaltyGoals = self:ReportDetailedPlayerGoals( PlayerName ) + ReportGoals = ReportGoals ~= "" and "\n- " .. ReportGoals or ReportGoals + self:E( { ReportGoals, ScoreGoals, PenaltyGoals } ) + + local ReportMissions, ScoreMissions, PenaltyMissions = self:ReportDetailedPlayerMissions( PlayerName ) + ReportMissions = ReportMissions ~= "" and "\n- " .. ReportMissions or ReportMissions + self:E( { ReportMissions, ScoreMissions, PenaltyMissions } ) + + local PlayerScore = ScoreHits + ScoreDestroys + ScoreCoalitionChanges + ScoreGoals + ScoreMissions + local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + ScoreGoals + PenaltyMissions + + PlayerMessage = + string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )", + PlayerName, + PlayerScore - PlayerPenalty, + PlayerScore, + PlayerPenalty + ) + MESSAGE:New( PlayerMessage, 30, "Player '" .. PlayerName .. "'" ):ToGroup( PlayerGroup ) + end + end + +end + + +function SCORING:SecondsToClock(sSeconds) + local nSeconds = sSeconds + if nSeconds == 0 then + --return nil; + return "00:00:00"; + else + nHours = string.format("%02.f", math.floor(nSeconds/3600)); + nMins = string.format("%02.f", math.floor(nSeconds/60 - (nHours*60))); + nSecs = string.format("%02.f", math.floor(nSeconds - nHours*3600 - nMins *60)); + return nHours..":"..nMins..":"..nSecs + end +end + +--- Opens a score CSV file to log the scores. +-- @param #SCORING self +-- @param #string ScoringCSV +-- @return #SCORING self +-- @usage +-- -- Open a new CSV file to log the scores of the game Gori Valley. Let the name of the CSV file begin with "Player Scores". +-- ScoringObject = SCORING:New( "Gori Valley" ) +-- ScoringObject:OpenCSV( "Player Scores" ) +function SCORING:OpenCSV( ScoringCSV ) + self:F( ScoringCSV ) + + if lfs and io and os then + if ScoringCSV then + self.ScoringCSV = ScoringCSV + local fdir = lfs.writedir() .. [[Logs\]] .. self.ScoringCSV .. " " .. os.date( "%Y-%m-%d %H-%M-%S" ) .. ".csv" + + self.CSVFile, self.err = io.open( fdir, "w+" ) + if not self.CSVFile then + error( "Error: Cannot open CSV file in " .. lfs.writedir() ) + end + + self.CSVFile:write( '"GameName","RunTime","Time","PlayerName","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 + + TargetUnitCoalition = TargetUnitCoalition or "" + TargetUnitCategory = TargetUnitCategory or "" + TargetUnitType = TargetUnitType or "" + TargetUnitName = TargetUnitName or "" + + if lfs and io and os then + self.CSVFile:write( + '"' .. self.GameName .. '"' .. ',' .. + '"' .. self.RunTime .. '"' .. ',' .. + '' .. ScoreTime .. '' .. ',' .. + '"' .. PlayerName .. '"' .. ',' .. + '"' .. 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 + +--- 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 Core.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 = 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 Dcs.DCSWrapper.Group#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 @{DCSWrapper.Unit#Unit} from the simulator, but checks first if it is still existing! +-- @param #CLEANUP self +-- @param Dcs.DCSWrapper.Unit#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 Dcs.DCSTypes#Weapon +--- Destroys a missile from the simulator, but checks first if it is still existing! +-- @param #CLEANUP self +-- @param Dcs.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 Dcs.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 Dcs.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() + 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 Dcs.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 ) + 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 ) + SCHEDULER:New( self, CLEANUP._DestroyUnit, { Event.TgtDCSUnit }, 0.1 ) + end + end + end +end + +--- Add the @{DCSWrapper.Unit#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 Dcs.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 + +--- Single-Player:**Yes** / Multi-Player:**Yes** / AI:**Yes** / Human:**No** / Types:**All** -- +-- **Spawn groups of units dynamically in your missions.** +-- +-- ![Banner Image](..\Presentations\SPAWN\SPAWN.JPG) +-- +-- === +-- +-- # 1) @{#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 methods (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 represents the GROUP Template (definition). +-- * @{#SPAWN.NewWithAlias}(): Creates a new SPAWN object taking the name of the group that represents the GROUP Template (definition), and gives each spawned @{Group} an different name. +-- +-- It is important to understand how the SPAWN class works internally. The SPAWN object created will contain internally a list of groups that will be spawned and that are already spawned. +-- The initialization methods will modify this list of groups so that when a group gets spawned, ALL information is already prepared when spawning. This is done for performance reasons. +-- So in principle, the group list will contain all parameters and configurations after initialization, and when groups get actually spawned, this spawning can be done quickly and efficient. +-- +-- ## 1.2) SPAWN initialization methods +-- +-- A spawn object will behave differently based on the usage of **initialization** methods, which all start with the **Init** prefix: +-- +-- * @{#SPAWN.InitLimit}(): Limits the amount of groups that can be alive at the same time and that can be dynamically spawned. +-- * @{#SPAWN.InitRandomizeRoute}(): Randomize the routes of spawned groups, and for air groups also optionally the height. +-- * @{#SPAWN.InitRandomizeTemplate}(): Randomize the group templates so that when a new group is spawned, a random group template is selected from one of the templates defined. +-- * @{#SPAWN.InitUnControlled}(): Spawn plane groups uncontrolled. +-- * @{#SPAWN.InitArray}(): 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 methods are @{#SPAWN.InitRepeatOnLanding} and @{#SPAWN.InitRepeatOnEngineShutDown}. +-- * @{#SPAWN.InitRandomizeUnits}(): Randomizes the @{Unit}s in the @{Group} that is spawned within a **radius band**, given an Outer and Inner radius. +-- * @{#SPAWN.InitRandomizeZones}(): Randomizes the spawning between a predefined list of @{Zone}s that are declared using this function. Each zone can be given a probability factor. +-- * @{#SPAWN.InitAIOn}(): Turns the AI On when spawning the new @{Group} object. +-- * @{#SPAWN.InitAIOff}(): Turns the AI Off when spawning the new @{Group} object. +-- * @{#SPAWN.InitAIOnOff}(): Turns the AI On or Off when spawning the new @{Group} object. +-- +-- ## 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.SpawnFromVec3}(): Spawn a new group from a Vec3 coordinate. (The group will can be spawned at a point in the air). +-- * @{#SPAWN.SpawnFromVec2}(): Spawn a new group from a Vec2 coordinate. (The group will be spawned at land height ). +-- * @{#SPAWN.SpawnFromStatic}(): Spawn a new group from a structure, taking the position of a @{Static}. +-- * @{#SPAWN.SpawnFromUnit}(): Spawn a new group taking the position of a @{Unit}. +-- * @{#SPAWN.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) Retrieve alive GROUPs spawned by the SPAWN object +-- +-- The SPAWN class administers which GROUPS it has reserved (in stock) or has created during mission execution. +-- Every time a SPAWN object spawns a new GROUP object, a reference to the GROUP object is added to an internal table of GROUPS. +-- SPAWN provides methods to iterate through that internal GROUP object reference table: +-- +-- * @{#SPAWN.GetFirstAliveGroup}(): Will find the first alive GROUP it has spawned, and return the alive GROUP object and the first Index where the first alive GROUP object has been found. +-- * @{#SPAWN.GetNextAliveGroup}(): Will find the next alive GROUP object from a given Index, and return a reference to the alive GROUP object and the next Index where the alive GROUP has been found. +-- * @{#SPAWN.GetLastAliveGroup}(): Will find the last alive GROUP object, and will return a reference to the last live GROUP object and the last Index where the last alive GROUP object has been found. +-- +-- You can use the methods @{#SPAWN.GetFirstAliveGroup}() and sequently @{#SPAWN.GetNextAliveGroup}() to iterate through the alive GROUPS within the SPAWN object, and to actions... See the respective methods for an example. +-- The method @{#SPAWN.GetGroupFromIndex}() will return the GROUP object reference from the given Index, dead or alive... +-- +-- ## 1.5) 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.InitCleanUp}() initialization method has been defined that will silently monitor the status of each spawned group. +-- Once a group has a velocity = 0, and has been waiting for a defined interval, that group will be cleaned or removed from run-time. +-- There is a catch however :-) If a damaged group has returned to an airbase within the coalition, that group will not be considered as "lost"... +-- In such a case, when the inactive group is cleaned, a new group will Re-spawned automatically. +-- This models AI that has succesfully returned to their airbase, to restart their combat activities. +-- Check the @{#SPAWN.InitCleanUp}() for further info. +-- +-- ## 1.6) Catch the @{Group} spawn event in a callback function! +-- +-- When using the SpawnScheduled method, new @{Group}s are created following the schedule timing parameters. +-- When a new @{Group} is spawned, you maybe want to execute actions with that group spawned at the spawn event. +-- To SPAWN class supports this functionality through the @{#SPAWN.OnSpawnGroup}( **function( SpawnedGroup ) end ** ) method, which takes a function as a parameter that you can define locally. +-- Whenever a new @{Group} is spawned, the given function is called, and the @{Group} that was just spawned, is given as a parameter. +-- As a result, your spawn event handling function requires one parameter to be declared, which will contain the spawned @{Group} object. +-- A coding example is provided at the description of the @{#SPAWN.OnSpawnGroup}( **function( SpawnedGroup ) end ** ) method. +-- +-- ==== +-- +-- # **API CHANGE HISTORY** +-- +-- The underlying change log documents the API changes. Please read this carefully. The following notation is used: +-- +-- * **Added** parts are expressed in bold type face. +-- * _Removed_ parts are expressed in italic type face. +-- +-- Hereby the change log: +-- +-- 2017-02-04: SPAWN:InitUnControlled( **UnControlled** ) replaces SPAWN:InitUnControlled(). +-- +-- 2017-01-24: SPAWN:**InitAIOnOff( AIOnOff )** added. +-- +-- 2017-01-24: SPAWN:**InitAIOn()** added. +-- +-- 2017-01-24: SPAWN:**InitAIOff()** added. +-- +-- 2016-08-15: SPAWN:**InitCleanUp**( SpawnCleanUpInterval ) replaces SPAWN:_CleanUp_( SpawnCleanUpInterval ). +-- +-- 2016-08-15: SPAWN:**InitRandomizeZones( SpawnZones )** added. +-- +-- 2016-08-14: SPAWN:**OnSpawnGroup**( SpawnCallBackFunction, ... ) replaces SPAWN:_SpawnFunction_( SpawnCallBackFunction, ... ). +-- +-- 2016-08-14: SPAWN.SpawnInZone( Zone, __RandomizeGroup__, SpawnIndex ) replaces SpawnInZone( Zone, _RandomizeUnits, OuterRadius, InnerRadius,_ SpawnIndex ). +-- +-- 2016-08-14: SPAWN.SpawnFromVec3( Vec3, SpawnIndex ) replaces SpawnFromVec3( Vec3, _RandomizeUnits, OuterRadius, InnerRadius,_ SpawnIndex ): +-- +-- 2016-08-14: SPAWN.SpawnFromVec2( Vec2, SpawnIndex ) replaces SpawnFromVec2( Vec2, _RandomizeUnits, OuterRadius, InnerRadius,_ SpawnIndex ): +-- +-- 2016-08-14: SPAWN.SpawnFromUnit( SpawnUnit, SpawnIndex ) replaces SpawnFromUnit( SpawnUnit, _RandomizeUnits, OuterRadius, InnerRadius,_ SpawnIndex ): +-- +-- 2016-08-14: SPAWN.SpawnFromUnit( SpawnUnit, SpawnIndex ) replaces SpawnFromStatic( SpawnStatic, _RandomizeUnits, OuterRadius, InnerRadius,_ SpawnIndex ): +-- +-- 2016-08-14: SPAWN.**InitRandomizeUnits( RandomizeUnits, OuterRadius, InnerRadius )** added: +-- +-- 2016-08-14: SPAWN.**Init**Limit( SpawnMaxUnitsAlive, SpawnMaxGroups ) replaces SPAWN._Limit_( SpawnMaxUnitsAlive, SpawnMaxGroups ): +-- +-- 2016-08-14: SPAWN.**Init**Array( SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY ) replaces SPAWN._Array_( SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY ). +-- +-- 2016-08-14: SPAWN.**Init**RandomizeRoute( SpawnStartPoint, SpawnEndPoint, SpawnRadius, SpawnHeight ) replaces SPAWN._RandomizeRoute_( SpawnStartPoint, SpawnEndPoint, SpawnRadius, SpawnHeight ). +-- +-- 2016-08-14: SPAWN.**Init**RandomizeTemplate( SpawnTemplatePrefixTable ) replaces SPAWN._RandomizeTemplate_( SpawnTemplatePrefixTable ). +-- +-- 2016-08-14: SPAWN.**Init**UnControlled() replaces SPAWN._UnControlled_(). +-- +-- === +-- +-- # **AUTHORS and CONTRIBUTIONS** +-- +-- ### Contributions: +-- +-- * **Aaron**: Posed the idea for Group position randomization at SpawnInZone and make the Unit randomization separate from the Group randomization. +-- * [**Entropy**](https://forums.eagle.ru/member.php?u=111471), **Afinegan**: Came up with the requirement for AIOnOff(). +-- +-- ### Authors: +-- +-- * **FlightControl**: Design & Programming +-- +-- @module Spawn + + + +--- SPAWN Class +-- @type SPAWN +-- @extends Core.Base#BASE +-- @field ClassName +-- @field #string SpawnTemplatePrefix +-- @field #string SpawnAliasPrefix +-- @field #number AliveUnits +-- @field #number MaxAliveUnits +-- @field #number SpawnIndex +-- @field #number MaxAliveGroups +-- @field #SPAWN.SpawnZoneTable SpawnZoneTable +SPAWN = { + ClassName = "SPAWN", + SpawnTemplatePrefix = nil, + SpawnAliasPrefix = nil, +} + + +--- @type SPAWN.SpawnZoneTable +-- @list SpawnZone + + +--- 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() ) -- #SPAWN + 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.AIOnOff = true -- The AI is on by default when spawning a group. + self.SpawnUnControlled = false + + 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.AIOnOff = true -- The AI is on by default when spawning a group. + self.SpawnUnControlled = false + + 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 method should be used... +-- When a @{#SPAWN.New} is executed and the limit of the amount of units alive is reached, then no new spawn will happen of the group, until some of these units of the spawn object will be destroyed. +-- @param #SPAWN self +-- @param #number SpawnMaxUnitsAlive The maximum amount of units that can be alive at runtime. +-- @param #number SpawnMaxGroups The maximum amount of groups that can be spawned. When the limit is reached, then no more actual spawns will happen of the group. +-- This parameter is useful to define a maximum amount of airplanes, ground troops, helicopters, ships etc within a supply area. +-- This parameter accepts the value 0, which defines that there are no maximum group limits, but there are limits on the maximum of units that can be alive at the same time. +-- @return #SPAWN self +-- @usage +-- -- NATO helicopters engaging in the battle field. +-- -- This helicopter group consists of one Unit. So, this group will SPAWN maximum 2 groups simultaneously within the DCSRTE. +-- -- There will be maximum 24 groups spawned during the whole mission lifetime. +-- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):InitLimit( 2, 24 ) +function SPAWN:InitLimit( SpawnMaxUnitsAlive, SpawnMaxGroups ) + self:F( { self.SpawnTemplatePrefix, SpawnMaxUnitsAlive, SpawnMaxGroups } ) + + self.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 ... +-- @param #number SpawnHeight (optional) Specifies the **additional** height in meters that can be added to the base height specified at each waypoint in the ME. +-- @return #SPAWN +-- @usage +-- -- NATO helicopters engaging in the battle field. +-- -- The KA-50 has waypoints Start point ( =0 or SP ), 1, 2, 3, 4, End point (= 5 or DP). +-- -- Waypoints 2 and 3 will only be randomized. The others will remain on their original position with each new spawn of the helicopter. +-- -- The randomization of waypoint 2 and 3 will take place within a radius of 2000 meters. +-- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):InitRandomizeRoute( 2, 2, 2000 ) +function SPAWN:InitRandomizeRoute( SpawnStartPoint, SpawnEndPoint, SpawnRadius, SpawnHeight ) + self:F( { self.SpawnTemplatePrefix, SpawnStartPoint, SpawnEndPoint, SpawnRadius, SpawnHeight } ) + + self.SpawnRandomizeRoute = true + self.SpawnRandomizeRouteStartPoint = SpawnStartPoint + self.SpawnRandomizeRouteEndPoint = SpawnEndPoint + self.SpawnRandomizeRouteRadius = SpawnRadius + self.SpawnRandomizeRouteHeight = SpawnHeight + + for GroupID = 1, self.SpawnMaxGroups do + self:_RandomizeRoute( GroupID ) + end + + return self +end + +--- Randomizes the UNITs that are spawned within a radius band given an Outer and Inner radius. +-- @param #SPAWN self +-- @param #boolean RandomizeUnits If true, SPAWN will perform the randomization of the @{UNIT}s position within the group between a given outer and inner radius. +-- @param Dcs.DCSTypes#Distance OuterRadius (optional) The outer radius in meters where the new group will be spawned. +-- @param Dcs.DCSTypes#Distance InnerRadius (optional) The inner radius in meters where the new group will NOT be spawned. +-- @return #SPAWN +-- @usage +-- -- NATO helicopters engaging in the battle field. +-- -- The KA-50 has waypoints Start point ( =0 or SP ), 1, 2, 3, 4, End point (= 5 or DP). +-- -- Waypoints 2 and 3 will only be randomized. The others will remain on their original position with each new spawn of the helicopter. +-- -- The randomization of waypoint 2 and 3 will take place within a radius of 2000 meters. +-- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):InitRandomizeRoute( 2, 2, 2000 ) +function SPAWN:InitRandomizeUnits( RandomizeUnits, OuterRadius, InnerRadius ) + self:F( { self.SpawnTemplatePrefix, RandomizeUnits, OuterRadius, InnerRadius } ) + + self.SpawnRandomizeUnits = RandomizeUnits or false + self.SpawnOuterRadius = OuterRadius or 0 + self.SpawnInnerRadius = InnerRadius or 0 + + for GroupID = 1, self.SpawnMaxGroups do + self:_RandomizeRoute( GroupID ) + end + + return self +end + +--- This method is rather complicated to understand. But I'll try to explain. +-- This method becomes useful when you need to spawn groups with random templates of groups defined within the mission editor, +-- but they will all follow the same Template route and have the same prefix name. +-- In other words, this method randomizes between a defined set of groups the template to be used for each new spawn of a group. +-- @param #SPAWN self +-- @param #string SpawnTemplatePrefixTable A table with the names of the groups defined within the mission editor, from which one will be choosen when a new group will be spawned. +-- @return #SPAWN +-- @usage +-- -- NATO Tank Platoons invading Gori. +-- -- Choose between 13 different 'US Tank Platoon' configurations for each new SPAWN the Group to be spawned for the +-- -- 'US Tank Platoon Left', 'US Tank Platoon Middle' and 'US Tank Platoon Right' SpawnTemplatePrefixes. +-- -- Each new SPAWN will randomize the route, with a defined time interval of 200 seconds with 40% time variation (randomization) and +-- -- with a limit set of maximum 12 Units alive simulteneously and 150 Groups to be spawned during the whole mission. +-- Spawn_US_Platoon = { 'US Tank Platoon 1', 'US Tank Platoon 2', 'US Tank Platoon 3', 'US Tank Platoon 4', 'US Tank Platoon 5', +-- 'US Tank Platoon 6', 'US Tank Platoon 7', 'US Tank Platoon 8', 'US Tank Platoon 9', 'US Tank Platoon 10', +-- 'US Tank Platoon 11', 'US Tank Platoon 12', 'US Tank Platoon 13' } +-- Spawn_US_Platoon_Left = SPAWN:New( 'US Tank Platoon Left' ):InitLimit( 12, 150 ):Schedule( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 ) +-- Spawn_US_Platoon_Middle = SPAWN:New( 'US Tank Platoon Middle' ):InitLimit( 12, 150 ):Schedule( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 ) +-- Spawn_US_Platoon_Right = SPAWN:New( 'US Tank Platoon Right' ):InitLimit( 12, 150 ):Schedule( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 ) +function SPAWN:InitRandomizeTemplate( SpawnTemplatePrefixTable ) + self:F( { self.SpawnTemplatePrefix, SpawnTemplatePrefixTable } ) + + self.SpawnTemplatePrefixTable = SpawnTemplatePrefixTable + self.SpawnRandomizeTemplate = true + + for SpawnGroupID = 1, self.SpawnMaxGroups do + self:_RandomizeTemplate( SpawnGroupID ) + end + + return self +end + +--TODO: Add example. +--- This method provides the functionality to randomize the spawning of the Groups at a given list of zones of different types. +-- @param #SPAWN self +-- @param #table SpawnZoneTable A table with @{Zone} objects. If this table is given, then each spawn will be executed within the given list of @{Zone}s objects. +-- @return #SPAWN +-- @usage +-- -- NATO Tank Platoons invading Gori. +-- -- Choose between 3 different zones for each new SPAWN the Group to be executed, regardless of the zone type. +function SPAWN:InitRandomizeZones( SpawnZoneTable ) + self:F( { self.SpawnTemplatePrefix, SpawnZoneTable } ) + + self.SpawnZoneTable = SpawnZoneTable + self.SpawnRandomizeZones = true + + for SpawnGroupID = 1, self.SpawnMaxGroups do + self:_RandomizeZones( SpawnGroupID ) + end + + return self +end + + + + + +--- For planes and helicopters, when these groups go home and land on their home airbases and farps, they normally would taxi to the parking spot, shut-down their engines and wait forever until the Group is removed by the runtime environment. +-- This method is used to re-spawn automatically (so no extra call is needed anymore) the same group after it has landed. +-- This will enable a spawned group to be re-spawned after it lands, until it is destroyed... +-- Note: When the group is respawned, it will re-spawn from the original airbase where it took off. +-- So ensure that the routes for groups that respawn, always return to the original airbase, or players may get confused ... +-- @param #SPAWN self +-- @return #SPAWN self +-- @usage +-- -- RU Su-34 - AI Ship Attack +-- -- Re-SPAWN the Group(s) after each landing and Engine Shut-Down automatically. +-- SpawnRU_SU34 = SPAWN:New( 'TF1 RU Su-34 Krymsk@AI - Attack Ships' ):Schedule( 2, 3, 1800, 0.4 ):SpawnUncontrolled():InitRandomizeRoute( 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:InitCleanUp( SpawnCleanUpInterval ) + self:F( { self.SpawnTemplatePrefix, SpawnCleanUpInterval } ) + + self.SpawnCleanUpInterval = SpawnCleanUpInterval + self.SpawnCleanUpTimeStamps = {} + + local SpawnGroup, SpawnCursor = self:GetFirstAliveGroup() + self:T( { "CleanUp Scheduler:", SpawnGroup } ) + + --self.CleanUpFunction = routines.scheduleFunction( self._SpawnCleanUpScheduler, { self }, timer.getTime() + 1, SpawnCleanUpInterval ) + self.CleanUpScheduler = SCHEDULER:New( self, self._SpawnCleanUpScheduler, {}, 1, SpawnCleanUpInterval, 0.2 ) + return self +end + + + +--- Makes the groups visible before start (like a batallion). +-- The method will take the position of the group as the first position in the array. +-- @param #SPAWN self +-- @param #number SpawnAngle The angle in degrees how the groups and each unit of the group will be positioned. +-- @param #number SpawnWidth The amount of Groups that will be positioned on the X axis. +-- @param #number SpawnDeltaX The space between each Group on the X-axis. +-- @param #number SpawnDeltaY The space between each Group on the Y-axis. +-- @return #SPAWN self +-- @usage +-- -- Define an array of Groups. +-- Spawn_BE_Ground = SPAWN:New( 'BE Ground' ):InitLimit( 2, 24 ):InitArray( 90, "Diamond", 10, 100, 50 ) +function SPAWN:InitArray( SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY ) + self:F( { self.SpawnTemplatePrefix, SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY } ) + + self.SpawnVisible = true -- When the first Spawn executes, all the Groups need to be made visible before start. + + local SpawnX = 0 + local SpawnY = 0 + local SpawnXIndex = 0 + local SpawnYIndex = 0 + + for SpawnGroupID = 1, self.SpawnMaxGroups do + self:T( { SpawnX, SpawnY, SpawnXIndex, SpawnYIndex } ) + + self.SpawnGroups[SpawnGroupID].Visible = true + self.SpawnGroups[SpawnGroupID].Spawned = false + + SpawnXIndex = SpawnXIndex + 1 + if SpawnWidth and SpawnWidth ~= 0 then + if SpawnXIndex >= SpawnWidth then + SpawnXIndex = 0 + SpawnYIndex = SpawnYIndex + 1 + end + end + + local SpawnRootX = self.SpawnGroups[SpawnGroupID].SpawnTemplate.x + local SpawnRootY = self.SpawnGroups[SpawnGroupID].SpawnTemplate.y + + self:_TranslateRotate( SpawnGroupID, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle ) + + self.SpawnGroups[SpawnGroupID].SpawnTemplate.lateActivation = true + self.SpawnGroups[SpawnGroupID].SpawnTemplate.visible = true + + self.SpawnGroups[SpawnGroupID].Visible = true + + _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 + +do -- AI methods + --- Turns the AI On or Off for the @{Group} when spawning. + -- @param #SPAWN self + -- @param #boolean AIOnOff A value of true sets the AI On, a value of false sets the AI Off. + -- @return #SPAWN The SPAWN object + function SPAWN:InitAIOnOff( AIOnOff ) + + self.AIOnOff = AIOnOff + return self + end + + --- Turns the AI On for the @{Group} when spawning. + -- @param #SPAWN self + -- @return #SPAWN The SPAWN object + function SPAWN:InitAIOn() + + return self:InitAIOnOff( true ) + end + + --- Turns the AI Off for the @{Group} when spawning. + -- @param #SPAWN self + -- @return #SPAWN The SPAWN object + function SPAWN:InitAIOff() + + return self:InitAIOnOff( false ) + end + +end -- AI methods + +--- Will spawn a group based on the internal index. +-- Note: Uses @{DATABASE} module defined in MOOSE. +-- @param #SPAWN self +-- @return Wrapper.Group#GROUP The group that was spawned. You can use this group for further actions. +function SPAWN:Spawn() + self:F( { self.SpawnTemplatePrefix, self.SpawnIndex, self.AliveUnits } ) + + 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 Wrapper.Group#GROUP The group that was spawned. You can use this group for further actions. +function SPAWN:ReSpawn( SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, SpawnIndex } ) + + if not SpawnIndex then + SpawnIndex = 1 + end + +-- TODO: This logic makes DCS crash and i don't know why (yet). + local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) + local WayPoints = SpawnGroup and SpawnGroup.WayPoints or nil + if SpawnGroup then + local SpawnDCSGroup = SpawnGroup:GetDCSObject() + if SpawnDCSGroup then + SpawnGroup:Destroy() + end + end + + local SpawnGroup = self:SpawnWithIndex( SpawnIndex ) + if SpawnGroup and WayPoints then + -- If there were WayPoints set, then Re-Execute those WayPoints! + SpawnGroup:WayPointInitialize( WayPoints ) + SpawnGroup:WayPointExecute( 1, 5 ) + end + + if SpawnGroup.ReSpawnFunction then + SpawnGroup:ReSpawnFunction() + end + + return SpawnGroup +end + +--- Will spawn a group with a specified index number. +-- Uses @{DATABASE} global object defined in MOOSE. +-- @param #SPAWN self +-- @param #string SpawnIndex The index of the group to be spawned. +-- @return Wrapper.Group#GROUP The group that was spawned. You can use this group for further actions. +function SPAWN:SpawnWithIndex( SpawnIndex ) + self:F2( { SpawnTemplatePrefix = self.SpawnTemplatePrefix, SpawnIndex = SpawnIndex, AliveUnits = self.AliveUnits, SpawnMaxGroups = self.SpawnMaxGroups } ) + + if self:_GetSpawnIndex( SpawnIndex ) then + + if self.SpawnGroups[self.SpawnIndex].Visible then + self.SpawnGroups[self.SpawnIndex].Group:Activate() + else + + local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate + self:T( SpawnTemplate.name ) + + if SpawnTemplate then + + local PointVec3 = POINT_VEC3:New( SpawnTemplate.route.points[1].x, SpawnTemplate.route.points[1].alt, SpawnTemplate.route.points[1].y ) + self:T( { "Current point of ", self.SpawnTemplatePrefix, PointVec3 } ) + + -- If RandomizeUnits, then Randomize the formation at the start point. + if self.SpawnRandomizeUnits then + for UnitID = 1, #SpawnTemplate.units do + local RandomVec2 = PointVec3:GetRandomVec2InRadius( self.SpawnOuterRadius, self.SpawnInnerRadius ) + SpawnTemplate.units[UnitID].x = RandomVec2.x + SpawnTemplate.units[UnitID].y = RandomVec2.y + self:T( 'SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) + end + end + + if SpawnTemplate.CategoryID == Group.Category.HELICOPTER or SpawnTemplate.CategoryID == Group.Category.AIRPLANE then + if SpawnTemplate.route.points[1].type == "TakeOffParking" then + SpawnTemplate.uncontrolled = self.SpawnUnControlled + end + end + end + + _EVENTDISPATCHER:OnBirthForTemplate( SpawnTemplate, self._OnBirth, self ) + _EVENTDISPATCHER:OnCrashForTemplate( SpawnTemplate, self._OnDeadOrCrash, self ) + _EVENTDISPATCHER:OnDeadForTemplate( SpawnTemplate, self._OnDeadOrCrash, self ) + + if self.Repeat then + _EVENTDISPATCHER:OnTakeOffForTemplate( SpawnTemplate, self._OnTakeOff, self ) + _EVENTDISPATCHER:OnLandForTemplate( SpawnTemplate, self._OnLand, self ) + end + if self.RepeatOnEngineShutDown then + _EVENTDISPATCHER:OnEngineShutDownForTemplate( SpawnTemplate, self._OnEngineShutDown, self ) + end + self:T3( SpawnTemplate.name ) + + self.SpawnGroups[self.SpawnIndex].Group = _DATABASE:Spawn( SpawnTemplate ) + + local SpawnGroup = self.SpawnGroups[self.SpawnIndex].Group -- Wrapper.Group#GROUP + + --TODO: Need to check if this function doesn't need to be scheduled, as the group may not be immediately there! + if SpawnGroup then + + SpawnGroup:SetAIOnOff( self.AIOnOff ) + end + + -- 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 method 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 method will be called when a new group is spawned, including its given parameters. +-- The first parameter of the SpawnFunction is the @{Group#GROUP} that was spawned. +-- @param #SPAWN self +-- @param #function SpawnCallBackFunction The function to be called when a group spawns. +-- @param SpawnFunctionArguments A random amount of arguments to be provided to the function when the group spawns. +-- @return #SPAWN +-- @usage +-- -- Declare SpawnObject and call a function when a new Group is spawned. +-- local SpawnObject = SPAWN +-- :New( "SpawnObject" ) +-- :InitLimit( 2, 10 ) +-- :OnSpawnGroup( +-- function( SpawnGroup ) +-- SpawnGroup:E( "I am spawned" ) +-- end +-- ) +-- :SpawnScheduled( 300, 0.3 ) +function SPAWN:OnSpawnGroup( SpawnCallBackFunction, ... ) + self:F( "OnSpawnGroup" ) + + self.SpawnFunctionHook = SpawnCallBackFunction + self.SpawnFunctionArguments = {} + if arg then + self.SpawnFunctionArguments = arg + end + + return self +end + + +--- Will spawn a group from a Vec3 in 3D space. +-- This method is mostly advisable to be used if you want to simulate spawning units in the air, like helicopters or airplanes. +-- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. +-- You can use the returned group to further define the route to be followed. +-- @param #SPAWN self +-- @param Dcs.DCSTypes#Vec3 Vec3 The Vec3 coordinates where to spawn the group. +-- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. +-- @return Wrapper.Group#GROUP that was spawned. +-- @return #nil Nothing was spawned. +function SPAWN:SpawnFromVec3( Vec3, SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, Vec3, SpawnIndex } ) + + local PointVec3 = POINT_VEC3:NewFromVec3( Vec3 ) + self:T2(PointVec3) + + if SpawnIndex then + else + SpawnIndex = self.SpawnIndex + 1 + end + + if self:_GetSpawnIndex( SpawnIndex ) then + + local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate + + if SpawnTemplate then + + self:T( { "Current point of ", self.SpawnTemplatePrefix, Vec3 } ) + + -- Translate the position of the Group Template to the Vec3. + for UnitID = 1, #SpawnTemplate.units do + self:T( 'Before Translation SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) + local UnitTemplate = SpawnTemplate.units[UnitID] + local SX = UnitTemplate.x + local SY = UnitTemplate.y + local BX = SpawnTemplate.route.points[1].x + local BY = SpawnTemplate.route.points[1].y + local TX = Vec3.x + ( SX - BX ) + local TY = Vec3.z + ( SY - BY ) + SpawnTemplate.units[UnitID].x = TX + SpawnTemplate.units[UnitID].y = TY + SpawnTemplate.units[UnitID].alt = Vec3.y + self:T( 'After Translation SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) + end + + SpawnTemplate.route.points[1].x = Vec3.x + SpawnTemplate.route.points[1].y = Vec3.z + SpawnTemplate.route.points[1].alt = Vec3.y + + SpawnTemplate.x = Vec3.x + SpawnTemplate.y = Vec3.z + + return self:SpawnWithIndex( self.SpawnIndex ) + end + end + + return nil +end + +--- Will spawn a group from a Vec2 in 3D space. +-- This method is mostly advisable to be used if you want to simulate spawning groups on the ground from air units, like vehicles. +-- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. +-- You can use the returned group to further define the route to be followed. +-- @param #SPAWN self +-- @param Dcs.DCSTypes#Vec2 Vec2 The Vec2 coordinates where to spawn the group. +-- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. +-- @return Wrapper.Group#GROUP that was spawned. +-- @return #nil Nothing was spawned. +function SPAWN:SpawnFromVec2( Vec2, SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, Vec2, SpawnIndex } ) + + local PointVec2 = POINT_VEC2:NewFromVec2( Vec2 ) + return self:SpawnFromVec3( PointVec2:GetVec3(), SpawnIndex ) +end + + +--- Will spawn a group from a hosting unit. This method is mostly advisable to be used if you want to simulate spawning from air units, like helicopters, which are dropping infantry into a defined Landing Zone. +-- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. +-- You can use the returned group to further define the route to be followed. +-- @param #SPAWN self +-- @param Wrapper.Unit#UNIT HostUnit The air or ground unit dropping or unloading the group. +-- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. +-- @return Wrapper.Group#GROUP that was spawned. +-- @return #nil Nothing was spawned. +function SPAWN:SpawnFromUnit( HostUnit, SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, HostUnit, SpawnIndex } ) + + if HostUnit and HostUnit:IsAlive() then -- and HostUnit:getUnit(1):inAir() == false then + return self:SpawnFromVec3( HostUnit:GetVec3(), SpawnIndex ) + end + + return nil +end + +--- Will spawn a group from a hosting static. This method is mostly advisable to be used if you want to simulate spawning from buldings and structures (static buildings). +-- You can use the returned group to further define the route to be followed. +-- @param #SPAWN self +-- @param Wrapper.Static#STATIC HostStatic The static dropping or unloading the group. +-- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. +-- @return Wrapper.Group#GROUP that was spawned. +-- @return #nil Nothing was spawned. +function SPAWN:SpawnFromStatic( HostStatic, SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, HostStatic, SpawnIndex } ) + + if HostStatic and HostStatic:IsAlive() then + return self:SpawnFromVec3( HostStatic:GetVec3(), SpawnIndex ) + end + + return nil +end + +--- Will spawn a Group within a given @{Zone}. +-- The @{Zone} can be of any type derived from @{Zone#ZONE_BASE}. +-- Once the @{Group} is spawned within the zone, the @{Group} will continue on its route. +-- The **first waypoint** (where the group is spawned) is replaced with the zone location coordinates. +-- @param #SPAWN self +-- @param Core.Zone#ZONE Zone The zone where the group is to be spawned. +-- @param #boolean RandomizeGroup (optional) Randomization of the @{Group} position in the zone. +-- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. +-- @return Wrapper.Group#GROUP that was spawned. +-- @return #nil when nothing was spawned. +function SPAWN:SpawnInZone( Zone, RandomizeGroup, SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, Zone, RandomizeGroup, SpawnIndex } ) + + if Zone then + if RandomizeGroup then + return self:SpawnFromVec2( Zone:GetRandomVec2(), SpawnIndex ) + else + return self:SpawnFromVec2( Zone:GetVec2(), SpawnIndex ) + end + end + + return nil +end + +--- (**AIR**) Will spawn a plane group in UnControlled or Controlled mode... +-- This will be similar to the uncontrolled flag setting in the ME. +-- You can use UnControlled mode to simulate planes startup and ready for take-off but aren't moving (yet). +-- ReSpawn the plane in Controlled mode, and the plane will move... +-- @param #SPAWN self +-- @param #boolean UnControlled true if UnControlled, false if Controlled. +-- @return #SPAWN self +function SPAWN:InitUnControlled( UnControlled ) + self:F2( { self.SpawnTemplatePrefix, UnControlled } ) + + self.SpawnUnControlled = UnControlled + + for SpawnGroupID = 1, self.SpawnMaxGroups do + self.SpawnGroups[SpawnGroupID].UnControlled = UnControlled + 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 + +--- Will find the first alive @{Group} it has spawned, and return the alive @{Group} object and the first Index where the first alive @{Group} object has been found. +-- @param #SPAWN self +-- @return Wrapper.Group#GROUP, #number The @{Group} object found, the new Index where the group was found. +-- @return #nil, #nil When no group is found, #nil is returned. +-- @usage +-- -- Find the first alive @{Group} object of the SpawnPlanes SPAWN object @{Group} collection that it has spawned during the mission. +-- local GroupPlane, Index = SpawnPlanes:GetFirstAliveGroup() +-- while GroupPlane ~= nil do +-- -- Do actions with the GroupPlane object. +-- GroupPlane, Index = SpawnPlanes:GetNextAliveGroup( Index ) +-- end +function SPAWN:GetFirstAliveGroup() + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) + + for SpawnIndex = 1, self.SpawnCount do + local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) + if SpawnGroup and SpawnGroup:IsAlive() then + return SpawnGroup, SpawnIndex + end + end + + return nil, nil +end + + +--- Will find the next alive @{Group} object from a given Index, and return a reference to the alive @{Group} object and the next Index where the alive @{Group} has been found. +-- @param #SPAWN self +-- @param #number SpawnIndexStart A Index holding the start position to search from. This method can also be used to find the first alive @{Group} object from the given Index. +-- @return Wrapper.Group#GROUP, #number The next alive @{Group} object found, the next Index where the next alive @{Group} object was found. +-- @return #nil, #nil When no alive @{Group} object is found from the start Index position, #nil is returned. +-- @usage +-- -- Find the first alive @{Group} object of the SpawnPlanes SPAWN object @{Group} collection that it has spawned during the mission. +-- local GroupPlane, Index = SpawnPlanes:GetFirstAliveGroup() +-- while GroupPlane ~= nil do +-- -- Do actions with the GroupPlane object. +-- GroupPlane, Index = SpawnPlanes:GetNextAliveGroup( Index ) +-- end +function SPAWN:GetNextAliveGroup( SpawnIndexStart ) + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnIndexStart } ) + + SpawnIndexStart = SpawnIndexStart + 1 + for SpawnIndex = SpawnIndexStart, self.SpawnCount do + local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) + if SpawnGroup and SpawnGroup:IsAlive() then + return SpawnGroup, SpawnIndex + end + end + + return nil, nil +end + +--- Will find the last alive @{Group} object, and will return a reference to the last live @{Group} object and the last Index where the last alive @{Group} object has been found. +-- @param #SPAWN self +-- @return Wrapper.Group#GROUP, #number The last alive @{Group} object found, the last Index where the last alive @{Group} object was found. +-- @return #nil, #nil When no alive @{Group} object is found, #nil is returned. +-- @usage +-- -- Find the last alive @{Group} object of the SpawnPlanes SPAWN object @{Group} collection that it has spawned during the mission. +-- local GroupPlane, Index = SpawnPlanes:GetLastAliveGroup() +-- if GroupPlane then -- GroupPlane can be nil!!! +-- -- Do actions with the GroupPlane object. +-- end +function SPAWN:GetLastAliveGroup() + self:F( { self.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 Wrapper.Group#GROUP self +function SPAWN:GetGroupFromIndex( SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnIndex } ) + + if not SpawnIndex then + SpawnIndex = 1 + end + + if self.SpawnGroups and self.SpawnGroups[SpawnIndex] then + local SpawnGroup = self.SpawnGroups[SpawnIndex].Group + return SpawnGroup + else + return nil + end +end + +--- 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 Dcs.DCSWrapper.Unit#Unit DCSUnit The @{DCSUnit} to be searched. +-- @return #string The prefix +-- @return #nil Nothing found +function SPAWN:_GetGroupIndexFromDCSUnit( DCSUnit ) + self:F3( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, DCSUnit } ) + + local SpawnUnitName = ( DCSUnit and DCSUnit:getName() ) or nil + if SpawnUnitName then + local IndexString = string.match( SpawnUnitName, "#.*-" ):sub( 2, -2 ) + if IndexString then + local Index = tonumber( IndexString ) + return Index + end + end + + return nil +end + +--- Return the prefix of a SpawnUnit. +-- The method will search for a #-mark, and will return the text before the #-mark. +-- It will return nil of no prefix was found. +-- @param #SPAWN self +-- @param Dcs.DCSWrapper.Unit#UNIT DCSUnit The @{DCSUnit} to be searched. +-- @return #string The prefix +-- @return #nil Nothing found +function SPAWN:_GetPrefixFromDCSUnit( DCSUnit ) + self:F3( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, DCSUnit } ) + + local DCSUnitName = ( DCSUnit and DCSUnit:getName() ) or nil + if DCSUnitName then + local SpawnPrefix = string.match( DCSUnitName, ".*#" ) + if SpawnPrefix then + SpawnPrefix = SpawnPrefix:sub( 1, -2 ) + end + return SpawnPrefix + end + + return nil +end + +--- Return the group within the SpawnGroups collection with input a DCSUnit. +-- @param #SPAWN self +-- @param Dcs.DCSWrapper.Unit#Unit DCSUnit The @{DCSUnit} to be searched. +-- @return Wrapper.Group#GROUP The Group +-- @return #nil Nothing found +function SPAWN:_GetGroupFromDCSUnit( DCSUnit ) + self:F3( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, DCSUnit } ) + + 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 + + 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:F3( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnGroup } ) + + local IndexString = string.match( SpawnGroup:GetName(), "#.*$" ):sub( 2 ) + local Index = tonumber( IndexString ) + + self:T3( IndexString, Index ) + return Index + +end + +--- Return the last maximum index that can be used. +function SPAWN:_GetLastIndex() + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) + + return self.SpawnMaxGroups +end + +--- Initalize the SpawnGroups collection. +function SPAWN:_InitializeSpawnGroups( SpawnIndex ) + self:F3( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnIndex } ) + + if not self.SpawnGroups[SpawnIndex] then + self.SpawnGroups[SpawnIndex] = {} + self.SpawnGroups[SpawnIndex].Visible = false + self.SpawnGroups[SpawnIndex].Spawned = false + self.SpawnGroups[SpawnIndex].UnControlled = false + self.SpawnGroups[SpawnIndex].SpawnTime = 0 + + self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix = self.SpawnTemplatePrefix + self.SpawnGroups[SpawnIndex].SpawnTemplate = self:_Prepare( self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix, SpawnIndex ) + end + + self:_RandomizeTemplate( SpawnIndex ) + self:_RandomizeRoute( SpawnIndex ) + --self:_TranslateRotate( SpawnIndex ) + + return self.SpawnGroups[SpawnIndex] +end + + + +--- Gets the CategoryID of the Group with the given SpawnPrefix +function SPAWN:_GetGroupCategoryID( SpawnPrefix ) + local TemplateGroup = Group.getByName( SpawnPrefix ) + + if TemplateGroup then + return TemplateGroup:getCategory() + else + return nil + end +end + +--- Gets the CoalitionID of the Group with the given SpawnPrefix +function SPAWN:_GetGroupCoalitionID( SpawnPrefix ) + local TemplateGroup = Group.getByName( SpawnPrefix ) + + if TemplateGroup then + return TemplateGroup:getCoalition() + else + return nil + end +end + +--- Gets the CountryID of the Group with the given SpawnPrefix +function SPAWN:_GetGroupCountryID( SpawnPrefix ) + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnPrefix } ) + + local TemplateGroup = Group.getByName( SpawnPrefix ) + + if TemplateGroup then + local TemplateUnits = TemplateGroup:getUnits() + return TemplateUnits[1]:getCountry() + else + return nil + end +end + +--- Gets the Group Template from the ME environment definition. +-- This method used the @{DATABASE} object, which contains ALL initial and new spawned object in MOOSE. +-- @param #SPAWN self +-- @param #string SpawnTemplatePrefix +-- @return @SPAWN self +function SPAWN:_GetTemplate( SpawnTemplatePrefix ) + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnTemplatePrefix } ) + + local SpawnTemplate = nil + + 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:T3( { SpawnTemplate } ) + return SpawnTemplate +end + +--- Prepares the new Group Template. +-- @param #SPAWN self +-- @param #string SpawnTemplatePrefix +-- @param #number SpawnIndex +-- @return #SPAWN self +function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) + 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 + + if SpawnTemplate.CategoryID == Group.Category.GROUND then + self:T3( "For ground units, visible needs to be false..." ) + SpawnTemplate.visible = false + end + + + for UnitID = 1, #SpawnTemplate.units do + SpawnTemplate.units[UnitID].name = string.format( SpawnTemplate.name .. '-%02d', UnitID ) + SpawnTemplate.units[UnitID].unitId = nil + end + + self:T3( { "Template:", SpawnTemplate } ) + return SpawnTemplate + +end + +--- Private method randomizing the routes. +-- @param #SPAWN self +-- @param #number SpawnIndex The index of the group to be spawned. +-- @return #SPAWN +function SPAWN:_RandomizeRoute( SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeRoute, self.SpawnRandomizeRouteStartPoint, self.SpawnRandomizeRouteEndPoint, self.SpawnRandomizeRouteRadius } ) + + if self.SpawnRandomizeRoute then + local SpawnTemplate = self.SpawnGroups[SpawnIndex].SpawnTemplate + local RouteCount = #SpawnTemplate.route.points + + for t = self.SpawnRandomizeRouteStartPoint + 1, ( RouteCount - self.SpawnRandomizeRouteEndPoint ) do + + SpawnTemplate.route.points[t].x = SpawnTemplate.route.points[t].x + math.random( self.SpawnRandomizeRouteRadius * -1, self.SpawnRandomizeRouteRadius ) + SpawnTemplate.route.points[t].y = SpawnTemplate.route.points[t].y + math.random( self.SpawnRandomizeRouteRadius * -1, self.SpawnRandomizeRouteRadius ) + + -- Manage randomization of altitude for airborne units ... + if SpawnTemplate.CategoryID == Group.Category.AIRPLANE or SpawnTemplate.CategoryID == Group.Category.HELICOPTER then + if SpawnTemplate.route.points[t].alt and self.SpawnRandomizeRouteHeight then + SpawnTemplate.route.points[t].alt = SpawnTemplate.route.points[t].alt + math.random( 1, self.SpawnRandomizeRouteHeight ) + end + else + SpawnTemplate.route.points[t].alt = nil + end + + self:T( 'SpawnTemplate.route.points[' .. t .. '].x = ' .. SpawnTemplate.route.points[t].x .. ', SpawnTemplate.route.points[' .. t .. '].y = ' .. SpawnTemplate.route.points[t].y ) + end + end + + self:_RandomizeZones( SpawnIndex ) + + return self +end + +--- Private method that randomizes the template of the group. +-- @param #SPAWN self +-- @param #number SpawnIndex +-- @return #SPAWN self +function SPAWN:_RandomizeTemplate( SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeTemplate } ) + + if self.SpawnRandomizeTemplate then + self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix = self.SpawnTemplatePrefixTable[ math.random( 1, #self.SpawnTemplatePrefixTable ) ] + self.SpawnGroups[SpawnIndex].SpawnTemplate = self:_Prepare( self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix, SpawnIndex ) + self.SpawnGroups[SpawnIndex].SpawnTemplate.route = 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 + local OldX = self.SpawnGroups[SpawnIndex].SpawnTemplate.units[1].x + local OldY = self.SpawnGroups[SpawnIndex].SpawnTemplate.units[1].y + for UnitID = 1, #self.SpawnGroups[SpawnIndex].SpawnTemplate.units do + self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].heading = self.SpawnTemplate.units[1].heading + self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].x = self.SpawnTemplate.units[1].x + ( self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].x - OldX ) + self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].y = self.SpawnTemplate.units[1].y + ( self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].y - OldY ) + self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].alt = self.SpawnTemplate.units[1].alt + end + end + + self:_RandomizeRoute( SpawnIndex ) + + return self +end + +--- Private method that randomizes the @{Zone}s where the Group will be spawned. +-- @param #SPAWN self +-- @param #number SpawnIndex +-- @return #SPAWN self +function SPAWN:_RandomizeZones( SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeZones } ) + + if self.SpawnRandomizeZones then + local SpawnZone = nil -- Core.Zone#ZONE_BASE + while not SpawnZone do + self:T( { SpawnZoneTableCount = #self.SpawnZoneTable, self.SpawnZoneTable } ) + local ZoneID = math.random( #self.SpawnZoneTable ) + self:T( ZoneID ) + SpawnZone = self.SpawnZoneTable[ ZoneID ]:GetZoneMaybe() + end + + self:T( "Preparing Spawn in Zone", SpawnZone:GetName() ) + + local SpawnVec2 = SpawnZone:GetRandomVec2() + + self:T( { SpawnVec2 = SpawnVec2 } ) + + local SpawnTemplate = self.SpawnGroups[SpawnIndex].SpawnTemplate + + self:T( { Route = SpawnTemplate.route } ) + + for UnitID = 1, #SpawnTemplate.units do + local UnitTemplate = SpawnTemplate.units[UnitID] + self:T( 'Before Translation SpawnTemplate.units['..UnitID..'].x = ' .. UnitTemplate.x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. UnitTemplate.y ) + local SX = UnitTemplate.x + local SY = UnitTemplate.y + local BX = SpawnTemplate.route.points[1].x + local BY = SpawnTemplate.route.points[1].y + local TX = SpawnVec2.x + ( SX - BX ) + local TY = SpawnVec2.y + ( SY - BY ) + UnitTemplate.x = TX + UnitTemplate.y = TY + -- TODO: Manage altitude based on landheight... + --SpawnTemplate.units[UnitID].alt = SpawnVec2: + self:T( 'After Translation SpawnTemplate.units['..UnitID..'].x = ' .. UnitTemplate.x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. UnitTemplate.y ) + end + SpawnTemplate.x = SpawnVec2.x + SpawnTemplate.y = SpawnVec2.y + SpawnTemplate.route.points[1].x = SpawnVec2.x + SpawnTemplate.route.points[1].y = SpawnVec2.y + end + + return self + +end + +function SPAWN:_TranslateRotate( SpawnIndex, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle ) + self:F( { self.SpawnTemplatePrefix, SpawnIndex, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle } ) + + -- Translate + local TranslatedX = SpawnX + local TranslatedY = SpawnY + + -- Rotate + -- From Wikipedia: https://en.wikipedia.org/wiki/Rotation_matrix#Common_rotations + -- x' = x \cos \theta - y \sin \theta\ + -- y' = x \sin \theta + y \cos \theta\ + local RotatedX = - TranslatedX * math.cos( math.rad( SpawnAngle ) ) + + TranslatedY * math.sin( math.rad( SpawnAngle ) ) + local RotatedY = TranslatedX * math.sin( math.rad( SpawnAngle ) ) + + TranslatedY * math.cos( math.rad( SpawnAngle ) ) + + -- Assign + self.SpawnGroups[SpawnIndex].SpawnTemplate.x = SpawnRootX - RotatedX + self.SpawnGroups[SpawnIndex].SpawnTemplate.y = SpawnRootY + RotatedY + + + local SpawnUnitCount = table.getn( self.SpawnGroups[SpawnIndex].SpawnTemplate.units ) + for u = 1, SpawnUnitCount do + + -- Translate + local TranslatedX = SpawnX + local TranslatedY = SpawnY - 10 * ( u - 1 ) + + -- Rotate + local RotatedX = - TranslatedX * math.cos( math.rad( SpawnAngle ) ) + + TranslatedY * math.sin( math.rad( SpawnAngle ) ) + local RotatedY = TranslatedX * math.sin( math.rad( SpawnAngle ) ) + + TranslatedY * math.cos( math.rad( SpawnAngle ) ) + + -- Assign + self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].x = SpawnRootX - RotatedX + self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].y = SpawnRootY + RotatedY + self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].heading = self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].heading + math.rad( SpawnAngle ) + end + + return self +end + +--- Get the next index of the groups to be spawned. This method is complicated, as it is used at several spaces. +function SPAWN:_GetSpawnIndex( SpawnIndex ) + self:F2( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnMaxGroups, self.SpawnMaxUnitsAlive, self.AliveUnits, #self.SpawnTemplate.units } ) + + if ( self.SpawnMaxGroups == 0 ) or ( SpawnIndex <= self.SpawnMaxGroups ) then + if ( self.SpawnMaxUnitsAlive == 0 ) or ( self.AliveUnits + #self.SpawnTemplate.units <= self.SpawnMaxUnitsAlive ) or self.UnControlled == true then + if SpawnIndex and SpawnIndex >= self.SpawnCount + 1 then + self.SpawnCount = self.SpawnCount + 1 + SpawnIndex = self.SpawnCount + end + self.SpawnIndex = SpawnIndex + if not self.SpawnGroups[self.SpawnIndex] then + self:_InitializeSpawnGroups( self.SpawnIndex ) + end + else + return nil + end + else + return nil + end + + return self.SpawnIndex +end + + +-- TODO Need to delete this... _DATABASE does this now ... + +--- @param #SPAWN self +-- @param Core.Event#EVENTDATA Event +function SPAWN:_OnBirth( Event ) + + if timer.getTime0() < timer.getAbsTime() then + if Event.IniDCSUnit then + local EventPrefix = self:_GetPrefixFromDCSUnit( Event.IniDCSUnit ) + self:T( { "Birth Event:", EventPrefix, self.SpawnTemplatePrefix } ) + if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then + self.AliveUnits = self.AliveUnits + 1 + self:T( "Alive Units: " .. self.AliveUnits ) + end + end + end + +end + +--- Obscolete +-- @todo Need to delete this... _DATABASE does this now ... + +--- @param #SPAWN self +-- @param Core.Event#EVENTDATA Event +function SPAWN:_OnDeadOrCrash( Event ) + self:F( self.SpawnTemplatePrefix, Event ) + + if Event.IniDCSUnit then + local EventPrefix = self:_GetPrefixFromDCSUnit( Event.IniDCSUnit ) + self:T( { "Dead event: " .. EventPrefix, self.SpawnTemplatePrefix } ) + if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then + self.AliveUnits = self.AliveUnits - 1 + self:T( "Alive Units: " .. self.AliveUnits ) + end + end +end + +--- 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:F2( { "_Scheduler", self.SpawnTemplatePrefix, self.SpawnAliasPrefix, self.SpawnIndex, self.SpawnMaxGroups, self.SpawnMaxUnitsAlive } ) + + -- Validate if there are still groups left in the batch... + self:Spawn() + + return true +end + +--- Schedules the CleanUp of Groups +-- @param #SPAWN self +-- @return #boolean True = Continue Scheduler +function SPAWN:_SpawnCleanUpScheduler() + self:F( { "CleanUp Scheduler:", self.SpawnTemplatePrefix } ) + + local SpawnGroup, SpawnCursor = self:GetFirstAliveGroup() + self:T( { "CleanUp Scheduler:", SpawnGroup, SpawnCursor } ) + + while SpawnGroup do + + local SpawnUnits = SpawnGroup:GetUnits() + + for UnitID, UnitData in pairs( SpawnUnits ) do + + local SpawnUnit = UnitData -- Wrapper.Unit#UNIT + local SpawnUnitName = SpawnUnit:GetName() + + + self.SpawnCleanUpTimeStamps[SpawnUnitName] = self.SpawnCleanUpTimeStamps[SpawnUnitName] or {} + local Stamp = self.SpawnCleanUpTimeStamps[SpawnUnitName] + self:T( { SpawnUnitName, Stamp } ) + + if Stamp.Vec2 then + if SpawnUnit:InAir() == false and SpawnUnit:GetVelocityKMH() < 1 then + local NewVec2 = SpawnUnit:GetVec2() + if Stamp.Vec2.x == NewVec2.x and Stamp.Vec2.y == NewVec2.y then + -- If the plane is not moving, and is on the ground, assign it with a timestamp... + if Stamp.Time + self.SpawnCleanUpInterval < timer.getTime() then + self:T( { "CleanUp Scheduler:", "ReSpawning:", SpawnGroup:GetName() } ) + self:ReSpawn( SpawnCursor ) + Stamp.Vec2 = nil + Stamp.Time = nil + end + else + Stamp.Time = timer.getTime() + Stamp.Vec2 = SpawnUnit:GetVec2() + end + else + Stamp.Vec2 = nil + Stamp.Time = nil + end + else + if SpawnUnit:InAir() == false then + Stamp.Vec2 = SpawnUnit:GetVec2() + if SpawnUnit:GetVelocityKMH() < 1 then + Stamp.Time = timer.getTime() + end + else + Stamp.Time = nil + Stamp.Vec2 = nil + end + end + end + + SpawnGroup, SpawnCursor = self:GetNextAliveGroup( SpawnCursor ) + + self:T( { "CleanUp Scheduler:", SpawnGroup, SpawnCursor } ) + + end + + return true -- Repeat + +end +--- 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 Core.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 + -- 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 ) + if self.TargetSkill[_targetskill] then + if (_evade > self.TargetSkill[_targetskill].Evade) then + self:T( string.format("Evading, target skill " ..string.format(_targetskill)) ) + local _targetMim = Weapon.getTarget(SEADWeapon) + local _targetMimname = Unit.getName(_targetMim) + local _targetMimgroup = Unit.getGroup(Weapon.getTarget(SEADWeapon)) + local _targetMimcont= _targetMimgroup:getController() + routines.groupRandomDistSelf(_targetMimgroup,300,'Diamond',250,20) -- move randomly + local SuppressedGroups1 = {} -- unit suppressed radar off for a random time + local function SuppressionEnd1(id) + id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN) + SuppressedGroups1[id.groupName] = nil + end + local id = { + groupName = _targetMimgroup, + ctrl = _targetMimcont + } + local delay1 = math.random(self.TargetSkill[_targetskill].DelayOff[1], self.TargetSkill[_targetskill].DelayOff[2]) + if SuppressedGroups1[id.groupName] == nil then + SuppressedGroups1[id.groupName] = { + SuppressionEndTime1 = timer.getTime() + delay1, + SuppressionEndN1 = SuppressionEndCounter1 --Store instance of SuppressionEnd() scheduled function + } + Controller.setOption(_targetMimcont, AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN) + timer.scheduleFunction(SuppressionEnd1, id, SuppressedGroups1[id.groupName].SuppressionEndTime1) --Schedule the SuppressionEnd() function + --trigger.action.outText( string.format("Radar Off " ..string.format(delay1)), 20) + end + + local SuppressedGroups = {} + local function SuppressionEnd(id) + id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.RED) + SuppressedGroups[id.groupName] = nil + end + local id = { + groupName = _targetMimgroup, + ctrl = _targetMimcont + } + local delay = math.random(self.TargetSkill[_targetskill].DelayOn[1], self.TargetSkill[_targetskill].DelayOn[2]) + if SuppressedGroups[id.groupName] == nil then + SuppressedGroups[id.groupName] = { + SuppressionEndTime = timer.getTime() + delay, + SuppressionEndN = SuppressionEndCounter --Store instance of SuppressionEnd() scheduled function + } + timer.scheduleFunction(SuppressionEnd, id, SuppressedGroups[id.groupName].SuppressionEndTime) --Schedule the SuppressionEnd() function + --trigger.action.outText( string.format("Radar On " ..string.format(delay)), 20) + end + 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 Core.Base#BASE +-- @field Wrapper.Client#CLIENT EscortClient +-- @field Wrapper.Group#GROUP EscortGroup +-- @field #string EscortName +-- @field #ESCORT.MODE EscortMode The mode the escort is in. +-- @field Core.Scheduler#SCHEDULER FollowScheduler The instance of the SCHEDULER class. +-- @field #number FollowDistance The current follow distance. +-- @field #boolean ReportTargets If true, nearby targets are reported. +-- @Field Dcs.DCSTypes#AI.Option.Air.val.ROE OptionROE Which ROE is set to the EscortGroup. +-- @field Dcs.DCSTypes#AI.Option.Air.val.REACTION_ON_THREAT OptionReactionOnThreat Which REACTION_ON_THREAT is set to the EscortGroup. +-- @field Core.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 Wrapper.Client#CLIENT EscortClient The client escorted by the EscortGroup. +-- @param Wrapper.Group#GROUP EscortGroup The group AI escorting the EscortClient. +-- @param #string EscortName Name of the escort. +-- @param #string EscortBriefing A text showing the ESCORT briefing to the player. Note that if no EscortBriefing is provided, the default briefing will be shown. +-- @return #ESCORT self +-- @usage +-- -- Declare a new EscortPlanes object as follows: +-- +-- -- First find the GROUP object and the CLIENT object. +-- local EscortClient = CLIENT:FindByName( "Unit Name" ) -- The Unit Name is the name of the unit flagged with the skill Client in the mission editor. +-- local EscortGroup = GROUP:FindByName( "Group Name" ) -- The Group Name is the name of the group that will escort the Escort Client. +-- +-- -- Now use these 2 objects to construct the new EscortPlanes object. +-- EscortPlanes = ESCORT:New( EscortClient, EscortGroup, "Desert", "Welcome to the mission. You are escorted by a plane with code name 'Desert', which can be instructed through the F10 radio menu." ) +function ESCORT:New( EscortClient, EscortGroup, EscortName, EscortBriefing ) + local self = BASE:Inherit( self, BASE:New() ) + self:F( { EscortClient, EscortGroup, EscortName } ) + + self.EscortClient = EscortClient -- Wrapper.Client#CLIENT + self.EscortGroup = EscortGroup -- Wrapper.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() + + if not EscortBriefing then + EscortGroup:MessageToClient( EscortGroup:GetCategoryName() .. " '" .. EscortName .. "' (" .. EscortGroup:GetCallsign() .. ") reporting! " .. + "We're escorting your flight. " .. + "Use the Radio Menu and F10 and use the options under + " .. EscortName .. "\n", + 60, EscortClient + ) + else + EscortGroup:MessageToClient( EscortGroup:GetCategoryName() .. " '" .. EscortName .. "' (" .. EscortGroup:GetCallsign() .. ") " .. EscortBriefing, + 60, EscortClient + ) + end + + self.FollowDistance = 100 + self.CT1 = 0 + self.GT1 = 0 + self.FollowScheduler = 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 Dcs.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 Dcs.DCSTypes#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. +-- @param Dcs.DCSTypes#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. +-- @param #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 Dcs.DCSTypes#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. +-- @param Dcs.DCSTypes#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. +-- @param #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 Dcs.DCSTypes#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. +-- @param Dcs.DCSTypes#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. +-- @param #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 = 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 = 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 = 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 = 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 Dcs.DCSTypes#Time Seconds Optional parameter that lets the escort report their current detected targets after specified time interval in seconds. The default time is 30 seconds. +-- @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 = 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 -- Wrapper.Group#GROUP + local OrbitUnit = OrbitGroup:GetUnit(1) -- Wrapper.Unit#UNIT + local OrbitHeight = MenuParam.ParamHeight + local OrbitSeconds = MenuParam.ParamSeconds -- Not implemented yet + + self.FollowScheduler:Stop() + + local PointFrom = {} + local GroupVec3 = EscortGroup:GetUnit(1):GetVec3() + PointFrom = {} + PointFrom.x = GroupVec3.x + PointFrom.y = GroupVec3.z + PointFrom.speed = 250 + PointFrom.type = AI.Task.WaypointType.TURNING_POINT + PointFrom.alt = GroupVec3.y + PointFrom.alt_type = AI.Task.AltitudeType.BARO + + local OrbitPoint = OrbitUnit:GetVec2() + local PointTo = {} + PointTo.x = OrbitPoint.x + PointTo.y = OrbitPoint.y + PointTo.speed = 250 + PointTo.type = AI.Task.WaypointType.TURNING_POINT + PointTo.alt = OrbitHeight + PointTo.alt_type = AI.Task.AltitudeType.BARO + PointTo.task = EscortGroup:TaskOrbitCircleAtVec2( OrbitPoint, OrbitHeight, 0 ) + + local Points = { PointFrom, PointTo } + + EscortGroup:OptionROEHoldFire() + EscortGroup:OptionROTPassiveDefense() + + EscortGroup:SetTask( EscortGroup:TaskRoute( Points ) ) + EscortGroup:MessageToClient( "Orbiting at location.", 10, EscortClient ) + +end + +--- @param #MENUPARAM MenuParam +function ESCORT._JoinUpAndFollow( 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 Functional.Escort#ESCORT self +-- @param Wrapper.Group#GROUP EscortGroup +-- @param Wrapper.Client#CLIENT EscortClient +-- @param Dcs.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 = 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 Wrapper.Group#GROUP EscortGroup +function _Resume( EscortGroup ) + env.info( '_Resume' ) + + local Escort = EscortGroup:GetState( EscortGroup, "Escort" ) + env.info( "EscortMode = " .. Escort.EscortMode ) + if Escort.EscortMode == ESCORT.MODE.FOLLOW then + Escort:JoinUpAndFollow( EscortGroup, Escort.EscortClient, Escort.Distance ) + end + +end + +--- @param #MENUPARAM MenuParam +function ESCORT._AttackTarget( MenuParam ) + + local self = MenuParam.ParamSelf + local EscortGroup = self.EscortGroup + + local EscortClient = self.EscortClient + local AttackUnit = MenuParam.ParamUnit -- Wrapper.Unit#UNIT + + self.FollowScheduler:Stop() + + self:T( AttackUnit ) + + if EscortGroup:IsAir() then + EscortGroup:OptionROEOpenFire() + EscortGroup:OptionROTPassiveDefense() + EscortGroup:SetState( EscortGroup, "Escort", self ) + SCHEDULER:New( EscortGroup, + EscortGroup.PushTask, + { EscortGroup:TaskCombo( + { EscortGroup:TaskAttackUnit( AttackUnit ), + EscortGroup:TaskFunction( 1, 2, "_Resume", { "''" } ) + } + ) + }, 10 + ) + else + SCHEDULER:New( EscortGroup, + EscortGroup.PushTask, + { EscortGroup:TaskCombo( + { EscortGroup:TaskFireAtPoint( AttackUnit:GetVec2(), 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 -- Wrapper.Unit#UNIT + + self.FollowScheduler:Stop() + + self:T( AttackUnit ) + + if EscortGroupAttack:IsAir() then + EscortGroupAttack:OptionROEOpenFire() + EscortGroupAttack:OptionROTVertical() + SCHDULER:New( EscortGroupAttack, + EscortGroupAttack.PushTask, + { EscortGroupAttack:TaskCombo( + { EscortGroupAttack:TaskAttackUnit( AttackUnit ), + EscortGroupAttack:TaskOrbitCircle( 500, 350 ) + } + ) + }, 10 + ) + else + SCHEDULER:New( EscortGroupAttack, + EscortGroupAttack.PushTask, + { EscortGroupAttack:TaskCombo( + { EscortGroupAttack:TaskFireAtPoint( AttackUnit:GetVec2(), 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 + + SCHEDULER:New( EscortGroup, EscortGroup.SetTask, { EscortGroup:TaskRoute( WayPoints ) }, 1 ) + + EscortGroup:MessageToClient( "Resuming mission from waypoint " .. WayPoint .. ".", 10, EscortClient ) +end + +--- Registers the waypoints +-- @param #ESCORT self +-- @return #table +function ESCORT:RegisterRoute() + self:F() + + local EscortGroup = self.EscortGroup -- Wrapper.Group#GROUP + + local TaskPoints = EscortGroup:GetTaskRoute() + + self:T( TaskPoints ) + + return TaskPoints +end + +--- @param Functional.Escort#ESCORT self +function ESCORT:_FollowScheduler() + self:F( { self.FollowDistance } ) + + self:T( {self.EscortClient.UnitName, self.EscortGroup.GroupName } ) + if self.EscortGroup:IsAlive() and self.EscortClient:IsAlive() then + + local ClientUnit = self.EscortClient:GetClientGroupUnit() + local GroupUnit = self.EscortGroup:GetUnit( 1 ) + local FollowDistance = self.FollowDistance + + self:T( {ClientUnit.UnitName, GroupUnit.UnitName } ) + + if self.CT1 == 0 and self.GT1 == 0 then + self.CV1 = ClientUnit:GetVec3() + self:T( { "self.CV1", self.CV1 } ) + self.CT1 = timer.getTime() + self.GV1 = GroupUnit:GetVec3() + self.GT1 = timer.getTime() + else + local CT1 = self.CT1 + local CT2 = timer.getTime() + local CV1 = self.CV1 + local CV2 = ClientUnit:GetVec3() + self.CT1 = CT2 + self.CV1 = CV2 + + local CD = ( ( CV2.x - CV1.x )^2 + ( CV2.y - CV1.y )^2 + ( CV2.z - CV1.z )^2 ) ^ 0.5 + local CT = CT2 - CT1 + + local CS = ( 3600 / CT ) * ( CD / 1000 ) + + self:T2( { "Client:", CS, CD, CT, CV2, CV1, CT2, CT1 } ) + + local GT1 = self.GT1 + local GT2 = timer.getTime() + local GV1 = self.GV1 + local GV2 = GroupUnit:GetVec3() + self.GT1 = GT2 + self.GV1 = GV2 + + local GD = ( ( GV2.x - GV1.x )^2 + ( GV2.y - GV1.y )^2 + ( GV2.z - GV1.z )^2 ) ^ 0.5 + local GT = GT2 - GT1 + + local GS = ( 3600 / GT ) * ( GD / 1000 ) + + self:T2( { "Group:", GS, GD, GT, GV2, GV1, GT2, GT1 } ) + + -- Calculate the group direction vector + local GV = { x = GV2.x - CV2.x, y = GV2.y - CV2.y, z = GV2.z - CV2.z } + + -- Calculate GH2, GH2 with the same height as CV2. + local GH2 = { x = GV2.x, y = CV2.y, z = GV2.z } + + -- Calculate the angle of GV to the orthonormal plane + local alpha = math.atan2( GV.z, GV.x ) + + -- Now we calculate the intersecting vector between the circle around CV2 with radius FollowDistance and GH2. + -- From the GeoGebra model: CVI = (x(CV2) + FollowDistance cos(alpha), y(GH2) + FollowDistance sin(alpha), z(CV2)) + local CVI = { x = CV2.x + FollowDistance * math.cos(alpha), + y = GH2.y, + z = CV2.z + FollowDistance * math.sin(alpha), + } + + -- Calculate the direction vector DV of the escort group. We use CVI as the base and CV2 as the direction. + local DV = { x = CV2.x - CVI.x, y = CV2.y - CVI.y, z = CV2.z - CVI.z } + + -- We now calculate the unary direction vector DVu, so that we can multiply DVu with the speed, which is expressed in meters / s. + -- We need to calculate this vector to predict the point the escort group needs to fly to according its speed. + -- The distance of the destination point should be far enough not to have the aircraft starting to swipe left to right... + local DVu = { x = DV.x / FollowDistance, y = DV.y / FollowDistance, z = DV.z / FollowDistance } + + -- Now we can calculate the group destination vector GDV. + local GDV = { x = DVu.x * CS * 8 + CVI.x, y = CVI.y, z = DVu.z * CS * 8 + CVI.z } + + if self.SmokeDirectionVector == true then + trigger.action.smoke( GDV, trigger.smokeColor.Red ) + end + + self:T2( { "CV2:", CV2 } ) + self:T2( { "CVI:", CVI } ) + self:T2( { "GDV:", GDV } ) + + -- Measure distance between client and group + local CatchUpDistance = ( ( GDV.x - GV2.x )^2 + ( GDV.y - GV2.y )^2 + ( GDV.z - GV2.z )^2 ) ^ 0.5 + + -- The calculation of the Speed would simulate that the group would take 30 seconds to overcome + -- the requested Distance). + local Time = 10 + local CatchUpSpeed = ( CatchUpDistance - ( CS * 8.4 ) ) / Time + + local Speed = CS + CatchUpSpeed + if Speed < 0 then + Speed = 0 + end + + self:T( { "Client Speed, Escort Speed, Speed, FollowDistance, Time:", CS, GS, Speed, FollowDistance, Time } ) + + -- Now route the escort to the desired point with the desired speed. + self.EscortGroup:RouteToVec3( GDV, Speed / 3.6 ) -- DCS models speed in Mps (Miles per second) + end + + return true + end + + return false +end + + +--- Report Targets Scheduler. +-- @param #ESCORT self +function ESCORT:_ReportTargetsScheduler() + self:F( self.EscortGroup:GetName() ) + + if self.EscortGroup:IsAlive() and self.EscortClient:IsAlive() then + local EscortGroupName = self.EscortGroup:GetName() + local EscortTargets = self.EscortGroup:GetDetectedTargets() + + local ClientEscortTargets = self.EscortClient._EscortGroups[EscortGroupName].Targets + + local EscortTargetMessages = "" + for EscortTargetID, EscortTarget in pairs( EscortTargets ) do + local EscortObject = EscortTarget.object + self:T( EscortObject ) + if EscortObject and EscortObject:isExist() and EscortObject.id_ < 50000000 then + + local EscortTargetUnit = UNIT:Find( EscortObject ) + local EscortTargetUnitName = EscortTargetUnit:GetName() + + + + -- local EscortTargetIsDetected, + -- EscortTargetIsVisible, + -- EscortTargetLastTime, + -- EscortTargetKnowType, + -- EscortTargetKnowDistance, + -- EscortTargetLastPos, + -- EscortTargetLastVelocity + -- = self.EscortGroup:IsTargetDetected( EscortObject ) + -- + -- self:T( { EscortTargetIsDetected, + -- EscortTargetIsVisible, + -- EscortTargetLastTime, + -- EscortTargetKnowType, + -- EscortTargetKnowDistance, + -- EscortTargetLastPos, + -- EscortTargetLastVelocity } ) + + + local EscortTargetUnitVec3 = EscortTargetUnit:GetVec3() + local EscortVec3 = self.EscortGroup:GetVec3() + local Distance = ( ( EscortTargetUnitVec3.x - EscortVec3.x )^2 + + ( EscortTargetUnitVec3.y - EscortVec3.y )^2 + + ( EscortTargetUnitVec3.z - EscortVec3.z )^2 + ) ^ 0.5 / 1000 + + self:T( { self.EscortGroup:GetName(), EscortTargetUnit:GetName(), Distance, EscortTarget } ) + + if Distance <= 15 then + + if not ClientEscortTargets[EscortTargetUnitName] then + ClientEscortTargets[EscortTargetUnitName] = {} + end + ClientEscortTargets[EscortTargetUnitName].AttackUnit = EscortTargetUnit + ClientEscortTargets[EscortTargetUnitName].visible = EscortTarget.visible + ClientEscortTargets[EscortTargetUnitName].type = EscortTarget.type + ClientEscortTargets[EscortTargetUnitName].distance = EscortTarget.distance + else + if ClientEscortTargets[EscortTargetUnitName] then + ClientEscortTargets[EscortTargetUnitName] = nil + end + end + end + end + + self:T( { "Sorting Targets Table:", ClientEscortTargets } ) + table.sort( ClientEscortTargets, function( a, b ) return a.Distance < b.Distance end ) + self:T( { "Sorted Targets Table:", ClientEscortTargets } ) + + -- Remove the sub menus of the Attack menu of the Escort for the EscortGroup. + self.EscortMenuAttackNearbyTargets:RemoveSubMenus() + + if self.EscortMenuTargetAssistance then + self.EscortMenuTargetAssistance:RemoveSubMenus() + end + + --for MenuIndex = 1, #self.EscortMenuAttackTargets do + -- self:T( { "Remove Menu:", self.EscortMenuAttackTargets[MenuIndex] } ) + -- self.EscortMenuAttackTargets[MenuIndex] = self.EscortMenuAttackTargets[MenuIndex]:Remove() + --end + + + if ClientEscortTargets then + for ClientEscortTargetUnitName, ClientEscortTargetData in pairs( ClientEscortTargets ) do + + for ClientEscortGroupName, EscortGroupData in pairs( self.EscortClient._EscortGroups ) do + + if ClientEscortTargetData and ClientEscortTargetData.AttackUnit:IsAlive() then + + local EscortTargetMessage = "" + local EscortTargetCategoryName = ClientEscortTargetData.AttackUnit:GetCategoryName() + local EscortTargetCategoryType = ClientEscortTargetData.AttackUnit:GetTypeName() + if ClientEscortTargetData.type then + EscortTargetMessage = EscortTargetMessage .. EscortTargetCategoryName .. " (" .. EscortTargetCategoryType .. ") at " + else + EscortTargetMessage = EscortTargetMessage .. "Unknown target at " + end + + local EscortTargetUnitVec3 = ClientEscortTargetData.AttackUnit:GetVec3() + local EscortVec3 = self.EscortGroup:GetVec3() + local Distance = ( ( EscortTargetUnitVec3.x - EscortVec3.x )^2 + + ( EscortTargetUnitVec3.y - EscortVec3.y )^2 + + ( EscortTargetUnitVec3.z - EscortVec3.z )^2 + ) ^ 0.5 / 1000 + + self:T( { self.EscortGroup:GetName(), ClientEscortTargetData.AttackUnit:GetName(), Distance, ClientEscortTargetData.AttackUnit } ) + if ClientEscortTargetData.visible == false then + EscortTargetMessage = EscortTargetMessage .. string.format( "%.2f", Distance ) .. " estimated km" + else + EscortTargetMessage = EscortTargetMessage .. string.format( "%.2f", Distance ) .. " km" + end + + if ClientEscortTargetData.visible then + EscortTargetMessage = EscortTargetMessage .. ", visual" + end + + if ClientEscortGroupName == EscortGroupName then + + MENU_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 EscortVec3 = self.EscortGroup:GetVec3() + local Distance = ( ( WayPoint.x - EscortVec3.x )^2 + + ( WayPoint.y - EscortVec3.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 Core.Set#SET_CLIENT DBClients +-- @extends Core.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 Wrapper.Client#CLIENT Client +-- function( Client ) +-- +-- ... actions ... +-- +-- end +-- ) + + self.MessagesOnOff = true + + self.TrackingToAll = false + self.TrackingOnOff = true + self.TrackingFrequency = 3 + + self.AlertsToAll = true + self.AlertsHitsOnOff = true + self.AlertsLaunchesOnOff = true + + self.DetailsRangeOnOff = true + self.DetailsBearingOnOff = true + + self.MenusOnOff = true + + self.TrackingMissiles = {} + + self.TrackingScheduler = SCHEDULER:New( self, self._TrackMissiles, {}, 0.5, 0.05, 0 ) + + return self +end + +-- Initialization methods. + + + +--- Sets by default the display of any message to be ON or OFF. +-- @param #MISSILETRAINER self +-- @param #boolean MessagesOnOff true or false +-- @return #MISSILETRAINER self +function MISSILETRAINER:InitMessagesOnOff( MessagesOnOff ) + self:F( MessagesOnOff ) + + self.MessagesOnOff = MessagesOnOff + if self.MessagesOnOff == true then + MESSAGE:New( "Messages ON", 15, "Menu" ):ToAll() + else + MESSAGE:New( "Messages OFF", 15, "Menu" ):ToAll() + end + + return self +end + +--- Sets by default the missile tracking report for all players or only for those missiles targetted to you. +-- @param #MISSILETRAINER self +-- @param #boolean TrackingToAll true or false +-- @return #MISSILETRAINER self +function MISSILETRAINER:InitTrackingToAll( TrackingToAll ) + self:F( TrackingToAll ) + + self.TrackingToAll = TrackingToAll + if self.TrackingToAll == true then + MESSAGE:New( "Missile tracking to all players ON", 15, "Menu" ):ToAll() + else + MESSAGE:New( "Missile tracking to all players OFF", 15, "Menu" ):ToAll() + end + + return self +end + +--- Sets by default the display of missile tracking report to be ON or OFF. +-- @param #MISSILETRAINER self +-- @param #boolean TrackingOnOff true or false +-- @return #MISSILETRAINER self +function MISSILETRAINER:InitTrackingOnOff( TrackingOnOff ) + self:F( TrackingOnOff ) + + self.TrackingOnOff = TrackingOnOff + if self.TrackingOnOff == true then + MESSAGE:New( "Missile tracking ON", 15, "Menu" ):ToAll() + else + MESSAGE:New( "Missile tracking OFF", 15, "Menu" ):ToAll() + end + + return self +end + +--- Increases, decreases the missile tracking message display frequency with the provided time interval in seconds. +-- The default frequency is a 3 second interval, so the Tracking Frequency parameter specifies the increase or decrease from the default 3 seconds or the last frequency update. +-- @param #MISSILETRAINER self +-- @param #number TrackingFrequency Provide a negative or positive value in seconds to incraese or decrease the display frequency. +-- @return #MISSILETRAINER self +function MISSILETRAINER:InitTrackingFrequency( TrackingFrequency ) + self:F( TrackingFrequency ) + + self.TrackingFrequency = self.TrackingFrequency + TrackingFrequency + if self.TrackingFrequency < 0.5 then + self.TrackingFrequency = 0.5 + end + if self.TrackingFrequency then + MESSAGE:New( "Missile tracking frequency is " .. self.TrackingFrequency .. " seconds.", 15, "Menu" ):ToAll() + end + + return self +end + +--- Sets by default the display of alerts to be shown to all players or only to you. +-- @param #MISSILETRAINER self +-- @param #boolean AlertsToAll true or false +-- @return #MISSILETRAINER self +function MISSILETRAINER:InitAlertsToAll( AlertsToAll ) + self:F( AlertsToAll ) + + self.AlertsToAll = AlertsToAll + if self.AlertsToAll == true then + MESSAGE:New( "Alerts to all players ON", 15, "Menu" ):ToAll() + else + MESSAGE:New( "Alerts to all players OFF", 15, "Menu" ):ToAll() + end + + return self +end + +--- Sets by default the display of hit alerts ON or OFF. +-- @param #MISSILETRAINER self +-- @param #boolean AlertsHitsOnOff true or false +-- @return #MISSILETRAINER self +function MISSILETRAINER:InitAlertsHitsOnOff( AlertsHitsOnOff ) + self:F( AlertsHitsOnOff ) + + self.AlertsHitsOnOff = AlertsHitsOnOff + if self.AlertsHitsOnOff == true then + MESSAGE:New( "Alerts Hits ON", 15, "Menu" ):ToAll() + else + MESSAGE:New( "Alerts Hits OFF", 15, "Menu" ):ToAll() + end + + return self +end + +--- Sets by default the display of launch alerts ON or OFF. +-- @param #MISSILETRAINER self +-- @param #boolean AlertsLaunchesOnOff true or false +-- @return #MISSILETRAINER self +function MISSILETRAINER:InitAlertsLaunchesOnOff( AlertsLaunchesOnOff ) + self:F( AlertsLaunchesOnOff ) + + self.AlertsLaunchesOnOff = AlertsLaunchesOnOff + if self.AlertsLaunchesOnOff == true then + MESSAGE:New( "Alerts Launches ON", 15, "Menu" ):ToAll() + else + MESSAGE:New( "Alerts Launches OFF", 15, "Menu" ):ToAll() + end + + return self +end + +--- Sets by default the display of range information of missiles ON of OFF. +-- @param #MISSILETRAINER self +-- @param #boolean DetailsRangeOnOff true or false +-- @return #MISSILETRAINER self +function MISSILETRAINER:InitRangeOnOff( DetailsRangeOnOff ) + self:F( DetailsRangeOnOff ) + + self.DetailsRangeOnOff = DetailsRangeOnOff + if self.DetailsRangeOnOff == true then + MESSAGE:New( "Range display ON", 15, "Menu" ):ToAll() + else + MESSAGE:New( "Range display OFF", 15, "Menu" ):ToAll() + end + + return self +end + +--- Sets by default the display of bearing information of missiles ON of OFF. +-- @param #MISSILETRAINER self +-- @param #boolean DetailsBearingOnOff true or false +-- @return #MISSILETRAINER self +function MISSILETRAINER:InitBearingOnOff( DetailsBearingOnOff ) + self:F( DetailsBearingOnOff ) + + self.DetailsBearingOnOff = DetailsBearingOnOff + if self.DetailsBearingOnOff == true then + MESSAGE:New( "Bearing display OFF", 15, "Menu" ):ToAll() + else + MESSAGE:New( "Bearing display OFF", 15, "Menu" ):ToAll() + end + + return self +end + +--- Enables / Disables the menus. +-- @param #MISSILETRAINER self +-- @param #boolean MenusOnOff true or false +-- @return #MISSILETRAINER self +function MISSILETRAINER:InitMenusOnOff( MenusOnOff ) + self:F( MenusOnOff ) + + self.MenusOnOff = MenusOnOff + if self.MenusOnOff == true then + MESSAGE:New( "Menus are ENABLED (only when a player rejoins a slot)", 15, "Menu" ):ToAll() + else + MESSAGE:New( "Menus are DISABLED", 15, "Menu" ):ToAll() + end + + return self +end + + +-- Menu functions + +function MISSILETRAINER._MenuMessages( MenuParameters ) + + local self = MenuParameters.MenuSelf + + if MenuParameters.MessagesOnOff ~= nil then + self:InitMessagesOnOff( MenuParameters.MessagesOnOff ) + end + + if MenuParameters.TrackingToAll ~= nil then + self:InitTrackingToAll( MenuParameters.TrackingToAll ) + end + + if MenuParameters.TrackingOnOff ~= nil then + self:InitTrackingOnOff( MenuParameters.TrackingOnOff ) + end + + if MenuParameters.TrackingFrequency ~= nil then + self:InitTrackingFrequency( MenuParameters.TrackingFrequency ) + end + + if MenuParameters.AlertsToAll ~= nil then + self:InitAlertsToAll( MenuParameters.AlertsToAll ) + end + + if MenuParameters.AlertsHitsOnOff ~= nil then + self:InitAlertsHitsOnOff( MenuParameters.AlertsHitsOnOff ) + end + + if MenuParameters.AlertsLaunchesOnOff ~= nil then + self:InitAlertsLaunchesOnOff( MenuParameters.AlertsLaunchesOnOff ) + end + + if MenuParameters.DetailsRangeOnOff ~= nil then + self:InitRangeOnOff( MenuParameters.DetailsRangeOnOff ) + end + + if MenuParameters.DetailsBearingOnOff ~= nil then + self:InitBearingOnOff( MenuParameters.DetailsBearingOnOff ) + end + + if MenuParameters.Distance ~= nil then + self.Distance = MenuParameters.Distance + MESSAGE:New( "Hit detection distance set to " .. self.Distance .. " meters", 15, "Menu" ):ToAll() + end + +end + +--- Detects if an SA site was shot with an anti radiation missile. In this case, take evasive actions based on the skill level set within the ME. +-- @param #MISSILETRAINER self +-- @param Core.Event#EVENTDATA 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 + if TrainerTargetDCSUnit then + local TrainerTargetDCSUnitName = Unit.getName( TrainerTargetDCSUnit ) + local TrainerTargetSkill = _DATABASE.Templates.Units[TrainerTargetDCSUnitName].Template.skill + + self:T(TrainerTargetDCSUnitName ) + + local Client = self.DBClients:FindClient( TrainerTargetDCSUnitName ) + if Client then + + local TrainerSourceUnit = UNIT:Find( TrainerSourceDCSUnit ) + local TrainerTargetUnit = UNIT:Find( TrainerTargetDCSUnit ) + + if self.MessagesOnOff == true and self.AlertsLaunchesOnOff == true then + + local Message = MESSAGE:New( + string.format( "%s launched a %s", + TrainerSourceUnit:GetTypeName(), + TrainerWeaponName + ) .. self:_AddRange( Client, TrainerWeapon ) .. self:_AddBearing( Client, TrainerWeapon ), 5, "Launch Alert" ) + + if self.AlertsToAll then + Message:ToAll() + else + Message:ToClient( Client ) + end + end + + local ClientID = Client:GetID() + self:T( ClientID ) + local MissileData = {} + MissileData.TrainerSourceUnit = TrainerSourceUnit + MissileData.TrainerWeapon = TrainerWeapon + MissileData.TrainerTargetUnit = TrainerTargetUnit + MissileData.TrainerWeaponTypeName = TrainerWeapon:getTypeName() + MissileData.TrainerWeaponLaunched = true + table.insert( self.TrackingMissiles[ClientID].MissileData, MissileData ) + --self:T( self.TrackingMissiles ) + end + else + -- TODO: some weapons don't know the target unit... Need to develop a workaround for this. + if ( TrainerWeapon:getTypeName() == "9M311" ) then + SCHEDULER:New( TrainerWeapon, TrainerWeapon.destroy, {}, 1 ) + else + end + end +end + +function MISSILETRAINER:_AddRange( Client, TrainerWeapon ) + + local RangeText = "" + + if self.DetailsRangeOnOff then + + local PositionMissile = TrainerWeapon:getPoint() + local TargetVec3 = Client:GetVec3() + + local Range = ( ( PositionMissile.x - TargetVec3.x )^2 + + ( PositionMissile.y - TargetVec3.y )^2 + + ( PositionMissile.z - TargetVec3.z )^2 + ) ^ 0.5 / 1000 + + RangeText = string.format( ", at %4.2fkm", Range ) + end + + return RangeText +end + +function MISSILETRAINER:_AddBearing( Client, TrainerWeapon ) + + local BearingText = "" + + if self.DetailsBearingOnOff then + + local PositionMissile = TrainerWeapon:getPoint() + local TargetVec3 = Client:GetVec3() + + self:T2( { TargetVec3, PositionMissile }) + + local DirectionVector = { x = PositionMissile.x - TargetVec3.x, y = PositionMissile.y - TargetVec3.y, z = PositionMissile.z - TargetVec3.z } + local DirectionRadians = math.atan2( DirectionVector.z, DirectionVector.x ) + --DirectionRadians = DirectionRadians + routines.getNorthCorrection( PositionTarget ) + if DirectionRadians < 0 then + DirectionRadians = DirectionRadians + 2 * math.pi + end + local DirectionDegrees = DirectionRadians * 180 / math.pi + + BearingText = string.format( ", %d degrees", DirectionDegrees ) + end + + return BearingText +end + + +function MISSILETRAINER:_TrackMissiles() + self:F2() + + + local ShowMessages = false + if self.MessagesOnOff and self.MessageLastTime + self.TrackingFrequency <= timer.getTime() then + self.MessageLastTime = timer.getTime() + ShowMessages = true + end + + -- ALERTS PART + + -- Loop for all Player Clients to check the alerts and deletion of missiles. + for ClientDataID, ClientData in pairs( self.TrackingMissiles ) do + + local Client = ClientData.Client + 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 TargetVec3 = Client:GetVec3() + + local Distance = ( ( PositionMissile.x - TargetVec3.x )^2 + + ( PositionMissile.y - TargetVec3.y )^2 + + ( PositionMissile.z - TargetVec3.z )^2 + ) ^ 0.5 / 1000 + + if Distance <= self.Distance then + -- Hit alert + TrainerWeapon:destroy() + if self.MessagesOnOff == true and self.AlertsHitsOnOff == true then + + self:T( "killed" ) + + local Message = MESSAGE:New( + string.format( "%s launched by %s killed %s", + TrainerWeapon:getTypeName(), + TrainerSourceUnit:GetTypeName(), + TrainerTargetUnit:GetPlayerName() + ), 15, "Hit Alert" ) + + if self.AlertsToAll == true then + Message:ToAll() + else + Message:ToClient( Client ) + end + + MissileData = nil + table.remove( ClientData.MissileData, MissileDataID ) + self:T(ClientData.MissileData) + end + end + else + if not ( TrainerWeapon and TrainerWeapon:isExist() ) then + if self.MessagesOnOff == true and self.AlertsLaunchesOnOff == true then + -- Weapon does not exist anymore. Delete from Table + local Message = MESSAGE:New( + string.format( "%s launched by %s self destructed!", + TrainerWeaponTypeName, + TrainerSourceUnit:GetTypeName() + ), 5, "Tracking" ) + + if self.AlertsToAll == true then + Message:ToAll() + else + Message:ToClient( Client ) + end + end + MissileData = nil + table.remove( ClientData.MissileData, MissileDataID ) + self:T( ClientData.MissileData ) + end + end + end + 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 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 +-- +-- 3) @{AirbasePolice#AIRBASEPOLICE_NEVADA} class, extends @{AirbasePolice#AIRBASEPOLICE_BASE} +-- ============================================================================================= +-- All the airbases on the NEVADA 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: +-- * Nellis +-- * McCarran +-- * Creech +-- * Groom Lake +-- +-- ### Contributions: Dutch Baron - Concept & Testing +-- ### Author: FlightControl - Framework Design & Programming +-- +-- @module AirbasePolice + + + + + +--- @type AIRBASEPOLICE_BASE +-- @field Core.Set#SET_CLIENT SetClient +-- @extends Core.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(SMOKECOLOR.White):Flush() + for PointsRunwayID, PointsRunway in pairs( Airbase.PointsRunways ) do + Airbase.ZoneRunways[PointsRunwayID] = ZONE_POLYGON_BASE:New( "Runway " .. PointsRunwayID, PointsRunway ):SmokeZone(SMOKECOLOR.Red):Flush() + end + end + +-- -- Template +-- local TemplateBoundary = GROUP:FindByName( "Template Boundary" ) +-- self.Airbases.Template.ZoneBoundary = ZONE_POLYGON:New( "Template Boundary", TemplateBoundary ):SmokeZone(SMOKECOLOR.White):Flush() +-- +-- local TemplateRunway1 = GROUP:FindByName( "Template Runway 1" ) +-- self.Airbases.Template.ZoneRunways[1] = ZONE_POLYGON:New( "Template Runway 1", TemplateRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + + self.SetClient:ForEachClient( + --- @param Wrapper.Client#CLIENT Client + function( Client ) + Client:SetState( self, "Speeding", false ) + Client:SetState( self, "Warnings", 0) + Client:SetState( self, "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 Wrapper.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 + + -- TODO: GetVelocityKMH function usage + local VelocityVec3 = Client:GetVelocity() + local Velocity = ( VelocityVec3.x ^ 2 + VelocityVec3.y ^ 2 + VelocityVec3.z ^ 2 ) ^ 0.5 -- in meters / sec + local Velocity = Velocity * 3.6 -- now it is in km/h. + -- MESSAGE:New( "Velocity = " .. Velocity, 1 ):ToAll() + 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 <= 3 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 .. " / 3" ) + 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:Destroy() + trigger.action.setUserFlag( "AIRCRAFT_"..Client:GetID(), 100) + Client:SetState( self, "Speeding", false ) + Client:SetState( self, "Warnings", 0 ) + end + + else + Client:Message( "You are speeding on the taxiway, slow down now! 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 Core.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] = { + [1]={["y"]=242140.57142858,["x"]=-6478.8571428583,}, + [2]={["y"]=242188.57142858,["x"]=-6522.0000000011,}, + [3]={["y"]=244124.2857143,["x"]=-4344.0000000011,}, + [4]={["y"]=244068.2857143,["x"]=-4296.5714285726,}, + [5]={["y"]=242140.57142858,["x"]=-6480.0000000011,} + }, + }, + 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,}, + }, + [2] = { + [1]={["y"]=895605.71428572,["x"]=-314724.57142857,}, + [2]={["y"]=897639.71428572,["x"]=-316148,}, + [3]={["y"]=897683.42857143,["x"]=-316087.14285714,}, + [4]={["y"]=895650,["x"]=-314660,}, + [5]={["y"]=895606,["x"]=-314724.85714286,} + }, + }, + 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(SMOKECOLOR.White):Flush() + -- + -- local AnapaVityazevoRunway1 = GROUP:FindByName( "AnapaVityazevo Runway 1" ) + -- self.Airbases.AnapaVityazevo.ZoneRunways[1] = ZONE_POLYGON:New( "AnapaVityazevo Runway 1", AnapaVityazevoRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- Batumi + -- local BatumiBoundary = GROUP:FindByName( "Batumi Boundary" ) + -- self.Airbases.Batumi.ZoneBoundary = ZONE_POLYGON:New( "Batumi Boundary", BatumiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local BatumiRunway1 = GROUP:FindByName( "Batumi Runway 1" ) + -- self.Airbases.Batumi.ZoneRunways[1] = ZONE_POLYGON:New( "Batumi Runway 1", BatumiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- Beslan + -- local BeslanBoundary = GROUP:FindByName( "Beslan Boundary" ) + -- self.Airbases.Beslan.ZoneBoundary = ZONE_POLYGON:New( "Beslan Boundary", BeslanBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local BeslanRunway1 = GROUP:FindByName( "Beslan Runway 1" ) + -- self.Airbases.Beslan.ZoneRunways[1] = ZONE_POLYGON:New( "Beslan Runway 1", BeslanRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- Gelendzhik + -- local GelendzhikBoundary = GROUP:FindByName( "Gelendzhik Boundary" ) + -- self.Airbases.Gelendzhik.ZoneBoundary = ZONE_POLYGON:New( "Gelendzhik Boundary", GelendzhikBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local GelendzhikRunway1 = GROUP:FindByName( "Gelendzhik Runway 1" ) + -- self.Airbases.Gelendzhik.ZoneRunways[1] = ZONE_POLYGON:New( "Gelendzhik Runway 1", GelendzhikRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- Gudauta + -- local GudautaBoundary = GROUP:FindByName( "Gudauta Boundary" ) + -- self.Airbases.Gudauta.ZoneBoundary = ZONE_POLYGON:New( "Gudauta Boundary", GudautaBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local GudautaRunway1 = GROUP:FindByName( "Gudauta Runway 1" ) + -- self.Airbases.Gudauta.ZoneRunways[1] = ZONE_POLYGON:New( "Gudauta Runway 1", GudautaRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- Kobuleti + -- local KobuletiBoundary = GROUP:FindByName( "Kobuleti Boundary" ) + -- self.Airbases.Kobuleti.ZoneBoundary = ZONE_POLYGON:New( "Kobuleti Boundary", KobuletiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local KobuletiRunway1 = GROUP:FindByName( "Kobuleti Runway 1" ) + -- self.Airbases.Kobuleti.ZoneRunways[1] = ZONE_POLYGON:New( "Kobuleti Runway 1", KobuletiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- KrasnodarCenter + -- local KrasnodarCenterBoundary = GROUP:FindByName( "KrasnodarCenter Boundary" ) + -- self.Airbases.KrasnodarCenter.ZoneBoundary = ZONE_POLYGON:New( "KrasnodarCenter Boundary", KrasnodarCenterBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local KrasnodarCenterRunway1 = GROUP:FindByName( "KrasnodarCenter Runway 1" ) + -- self.Airbases.KrasnodarCenter.ZoneRunways[1] = ZONE_POLYGON:New( "KrasnodarCenter Runway 1", KrasnodarCenterRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- KrasnodarPashkovsky + -- local KrasnodarPashkovskyBoundary = GROUP:FindByName( "KrasnodarPashkovsky Boundary" ) + -- self.Airbases.KrasnodarPashkovsky.ZoneBoundary = ZONE_POLYGON:New( "KrasnodarPashkovsky Boundary", KrasnodarPashkovskyBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local KrasnodarPashkovskyRunway1 = GROUP:FindByName( "KrasnodarPashkovsky Runway 1" ) + -- self.Airbases.KrasnodarPashkovsky.ZoneRunways[1] = ZONE_POLYGON:New( "KrasnodarPashkovsky Runway 1", KrasnodarPashkovskyRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- local KrasnodarPashkovskyRunway2 = GROUP:FindByName( "KrasnodarPashkovsky Runway 2" ) + -- self.Airbases.KrasnodarPashkovsky.ZoneRunways[2] = ZONE_POLYGON:New( "KrasnodarPashkovsky Runway 2", KrasnodarPashkovskyRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- Krymsk + -- local KrymskBoundary = GROUP:FindByName( "Krymsk Boundary" ) + -- self.Airbases.Krymsk.ZoneBoundary = ZONE_POLYGON:New( "Krymsk Boundary", KrymskBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local KrymskRunway1 = GROUP:FindByName( "Krymsk Runway 1" ) + -- self.Airbases.Krymsk.ZoneRunways[1] = ZONE_POLYGON:New( "Krymsk Runway 1", KrymskRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- Kutaisi + -- local KutaisiBoundary = GROUP:FindByName( "Kutaisi Boundary" ) + -- self.Airbases.Kutaisi.ZoneBoundary = ZONE_POLYGON:New( "Kutaisi Boundary", KutaisiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local KutaisiRunway1 = GROUP:FindByName( "Kutaisi Runway 1" ) + -- self.Airbases.Kutaisi.ZoneRunways[1] = ZONE_POLYGON:New( "Kutaisi Runway 1", KutaisiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- MaykopKhanskaya + -- local MaykopKhanskayaBoundary = GROUP:FindByName( "MaykopKhanskaya Boundary" ) + -- self.Airbases.MaykopKhanskaya.ZoneBoundary = ZONE_POLYGON:New( "MaykopKhanskaya Boundary", MaykopKhanskayaBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local MaykopKhanskayaRunway1 = GROUP:FindByName( "MaykopKhanskaya Runway 1" ) + -- self.Airbases.MaykopKhanskaya.ZoneRunways[1] = ZONE_POLYGON:New( "MaykopKhanskaya Runway 1", MaykopKhanskayaRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- MineralnyeVody + -- local MineralnyeVodyBoundary = GROUP:FindByName( "MineralnyeVody Boundary" ) + -- self.Airbases.MineralnyeVody.ZoneBoundary = ZONE_POLYGON:New( "MineralnyeVody Boundary", MineralnyeVodyBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local MineralnyeVodyRunway1 = GROUP:FindByName( "MineralnyeVody Runway 1" ) + -- self.Airbases.MineralnyeVody.ZoneRunways[1] = ZONE_POLYGON:New( "MineralnyeVody Runway 1", MineralnyeVodyRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- Mozdok + -- local MozdokBoundary = GROUP:FindByName( "Mozdok Boundary" ) + -- self.Airbases.Mozdok.ZoneBoundary = ZONE_POLYGON:New( "Mozdok Boundary", MozdokBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local MozdokRunway1 = GROUP:FindByName( "Mozdok Runway 1" ) + -- self.Airbases.Mozdok.ZoneRunways[1] = ZONE_POLYGON:New( "Mozdok Runway 1", MozdokRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- Nalchik + -- local NalchikBoundary = GROUP:FindByName( "Nalchik Boundary" ) + -- self.Airbases.Nalchik.ZoneBoundary = ZONE_POLYGON:New( "Nalchik Boundary", NalchikBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local NalchikRunway1 = GROUP:FindByName( "Nalchik Runway 1" ) + -- self.Airbases.Nalchik.ZoneRunways[1] = ZONE_POLYGON:New( "Nalchik Runway 1", NalchikRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- Novorossiysk + -- local NovorossiyskBoundary = GROUP:FindByName( "Novorossiysk Boundary" ) + -- self.Airbases.Novorossiysk.ZoneBoundary = ZONE_POLYGON:New( "Novorossiysk Boundary", NovorossiyskBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local NovorossiyskRunway1 = GROUP:FindByName( "Novorossiysk Runway 1" ) + -- self.Airbases.Novorossiysk.ZoneRunways[1] = ZONE_POLYGON:New( "Novorossiysk Runway 1", NovorossiyskRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- SenakiKolkhi + -- local SenakiKolkhiBoundary = GROUP:FindByName( "SenakiKolkhi Boundary" ) + -- self.Airbases.SenakiKolkhi.ZoneBoundary = ZONE_POLYGON:New( "SenakiKolkhi Boundary", SenakiKolkhiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local SenakiKolkhiRunway1 = GROUP:FindByName( "SenakiKolkhi Runway 1" ) + -- self.Airbases.SenakiKolkhi.ZoneRunways[1] = ZONE_POLYGON:New( "SenakiKolkhi Runway 1", SenakiKolkhiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- SochiAdler + -- local SochiAdlerBoundary = GROUP:FindByName( "SochiAdler Boundary" ) + -- self.Airbases.SochiAdler.ZoneBoundary = ZONE_POLYGON:New( "SochiAdler Boundary", SochiAdlerBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local SochiAdlerRunway1 = GROUP:FindByName( "SochiAdler Runway 1" ) + -- self.Airbases.SochiAdler.ZoneRunways[1] = ZONE_POLYGON:New( "SochiAdler Runway 1", SochiAdlerRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- local SochiAdlerRunway2 = GROUP:FindByName( "SochiAdler Runway 2" ) + -- self.Airbases.SochiAdler.ZoneRunways[2] = ZONE_POLYGON:New( "SochiAdler Runway 2", SochiAdlerRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- Soganlug + -- local SoganlugBoundary = GROUP:FindByName( "Soganlug Boundary" ) + -- self.Airbases.Soganlug.ZoneBoundary = ZONE_POLYGON:New( "Soganlug Boundary", SoganlugBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local SoganlugRunway1 = GROUP:FindByName( "Soganlug Runway 1" ) + -- self.Airbases.Soganlug.ZoneRunways[1] = ZONE_POLYGON:New( "Soganlug Runway 1", SoganlugRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- SukhumiBabushara + -- local SukhumiBabusharaBoundary = GROUP:FindByName( "SukhumiBabushara Boundary" ) + -- self.Airbases.SukhumiBabushara.ZoneBoundary = ZONE_POLYGON:New( "SukhumiBabushara Boundary", SukhumiBabusharaBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local SukhumiBabusharaRunway1 = GROUP:FindByName( "SukhumiBabushara Runway 1" ) + -- self.Airbases.SukhumiBabushara.ZoneRunways[1] = ZONE_POLYGON:New( "SukhumiBabushara Runway 1", SukhumiBabusharaRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- TbilisiLochini + -- local TbilisiLochiniBoundary = GROUP:FindByName( "TbilisiLochini Boundary" ) + -- self.Airbases.TbilisiLochini.ZoneBoundary = ZONE_POLYGON:New( "TbilisiLochini Boundary", TbilisiLochiniBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local TbilisiLochiniRunway1 = GROUP:FindByName( "TbilisiLochini Runway 1" ) + -- self.Airbases.TbilisiLochini.ZoneRunways[1] = ZONE_POLYGON:New( "TbilisiLochini Runway 1", TbilisiLochiniRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- local TbilisiLochiniRunway2 = GROUP:FindByName( "TbilisiLochini Runway 2" ) + -- self.Airbases.TbilisiLochini.ZoneRunways[2] = ZONE_POLYGON:New( "TbilisiLochini Runway 2", TbilisiLochiniRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- Vaziani + -- local VazianiBoundary = GROUP:FindByName( "Vaziani Boundary" ) + -- self.Airbases.Vaziani.ZoneBoundary = ZONE_POLYGON:New( "Vaziani Boundary", VazianiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local VazianiRunway1 = GROUP:FindByName( "Vaziani Runway 1" ) + -- self.Airbases.Vaziani.ZoneRunways[1] = ZONE_POLYGON:New( "Vaziani Runway 1", VazianiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + + + -- Template + -- local TemplateBoundary = GROUP:FindByName( "Template Boundary" ) + -- self.Airbases.Template.ZoneBoundary = ZONE_POLYGON:New( "Template Boundary", TemplateBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local TemplateRunway1 = GROUP:FindByName( "Template Runway 1" ) + -- self.Airbases.Template.ZoneRunways[1] = ZONE_POLYGON:New( "Template Runway 1", TemplateRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + + return self + +end + + + + +--- @type AIRBASEPOLICE_NEVADA +-- @extends Functional.AirbasePolice#AIRBASEPOLICE_BASE +AIRBASEPOLICE_NEVADA = { + ClassName = "AIRBASEPOLICE_NEVADA", + Airbases = { + Nellis = { + PointsBoundary = { + [1]={["y"]=-17814.714285714,["x"]=-399823.14285714,}, + [2]={["y"]=-16875.857142857,["x"]=-398763.14285714,}, + [3]={["y"]=-16251.571428571,["x"]=-398988.85714286,}, + [4]={["y"]=-16163,["x"]=-398693.14285714,}, + [5]={["y"]=-16328.714285714,["x"]=-398034.57142857,}, + [6]={["y"]=-15943,["x"]=-397571.71428571,}, + [7]={["y"]=-15711.571428571,["x"]=-397551.71428571,}, + [8]={["y"]=-15748.714285714,["x"]=-396806,}, + [9]={["y"]=-16288.714285714,["x"]=-396517.42857143,}, + [10]={["y"]=-16751.571428571,["x"]=-396308.85714286,}, + [11]={["y"]=-17263,["x"]=-396234.57142857,}, + [12]={["y"]=-17577.285714286,["x"]=-396640.28571429,}, + [13]={["y"]=-17614.428571429,["x"]=-397400.28571429,}, + [14]={["y"]=-19405.857142857,["x"]=-399428.85714286,}, + [15]={["y"]=-19234.428571429,["x"]=-399683.14285714,}, + [16]={["y"]=-18708.714285714,["x"]=-399408.85714286,}, + [17]={["y"]=-18397.285714286,["x"]=-399657.42857143,}, + [18]={["y"]=-17814.428571429,["x"]=-399823.42857143,}, + }, + PointsRunways = { + [1] = { + [1]={["y"]=-18687,["x"]=-399380.28571429,}, + [2]={["y"]=-18620.714285714,["x"]=-399436.85714286,}, + [3]={["y"]=-16217.857142857,["x"]=-396596.85714286,}, + [4]={["y"]=-16300.142857143,["x"]=-396530,}, + [5]={["y"]=-18687,["x"]=-399380.85714286,}, + }, + [2] = { + [1]={["y"]=-18451.571428572,["x"]=-399580.57142857,}, + [2]={["y"]=-18392.142857143,["x"]=-399628.57142857,}, + [3]={["y"]=-16011,["x"]=-396806.85714286,}, + [4]={["y"]=-16074.714285714,["x"]=-396751.71428572,}, + [5]={["y"]=-18451.571428572,["x"]=-399580.85714285,}, + }, + }, + ZoneBoundary = {}, + ZoneRunways = {}, + MaximumSpeed = 50, + }, + McCarran = { + PointsBoundary = { + [1]={["y"]=-29455.285714286,["x"]=-416277.42857142,}, + [2]={["y"]=-28860.142857143,["x"]=-416492,}, + [3]={["y"]=-25044.428571429,["x"]=-416344.85714285,}, + [4]={["y"]=-24580.142857143,["x"]=-415959.14285714,}, + [5]={["y"]=-25073,["x"]=-415630.57142857,}, + [6]={["y"]=-25087.285714286,["x"]=-415130.57142857,}, + [7]={["y"]=-25830.142857143,["x"]=-414866.28571428,}, + [8]={["y"]=-26658.714285715,["x"]=-414880.57142857,}, + [9]={["y"]=-26973,["x"]=-415273.42857142,}, + [10]={["y"]=-27380.142857143,["x"]=-415187.71428571,}, + [11]={["y"]=-27715.857142857,["x"]=-414144.85714285,}, + [12]={["y"]=-27551.571428572,["x"]=-413473.42857142,}, + [13]={["y"]=-28630.142857143,["x"]=-413201.99999999,}, + [14]={["y"]=-29494.428571429,["x"]=-415437.71428571,}, + [15]={["y"]=-29455.571428572,["x"]=-416277.71428571,}, + }, + PointsRunways = { + [1] = { + [1]={["y"]=-29408.428571429,["x"]=-416016.28571428,}, + [2]={["y"]=-29408.142857144,["x"]=-416105.42857142,}, + [3]={["y"]=-24680.714285715,["x"]=-416003.14285713,}, + [4]={["y"]=-24681.857142858,["x"]=-415926.57142856,}, + [5]={["y"]=-29408.42857143,["x"]=-416016.57142856,}, + }, + [2] = { + [1]={["y"]=-28575.571428572,["x"]=-416303.14285713,}, + [2]={["y"]=-28575.571428572,["x"]=-416382.57142856,}, + [3]={["y"]=-25111.000000001,["x"]=-416309.7142857,}, + [4]={["y"]=-25111.000000001,["x"]=-416249.14285713,}, + [5]={["y"]=-28575.571428572,["x"]=-416303.7142857,}, + }, + [3] = { + [1]={["y"]=-29331.000000001,["x"]=-416275.42857141,}, + [2]={["y"]=-29259.000000001,["x"]=-416306.85714284,}, + [3]={["y"]=-28005.571428572,["x"]=-413449.7142857,}, + [4]={["y"]=-28068.714285715,["x"]=-413422.85714284,}, + [5]={["y"]=-29331.000000001,["x"]=-416275.7142857,}, + }, + [4] = { + [1]={["y"]=-29073.285714286,["x"]=-416386.57142856,}, + [2]={["y"]=-28997.285714286,["x"]=-416417.42857141,}, + [3]={["y"]=-27697.571428572,["x"]=-413464.57142856,}, + [4]={["y"]=-27767.857142858,["x"]=-413434.28571427,}, + [5]={["y"]=-29073.000000001,["x"]=-416386.85714284,}, + }, + }, + ZoneBoundary = {}, + ZoneRunways = {}, + MaximumSpeed = 50, + }, + Creech = { + PointsBoundary = { + [1]={["y"]=-74522.714285715,["x"]=-360887.99999998,}, + [2]={["y"]=-74197,["x"]=-360556.57142855,}, + [3]={["y"]=-74402.714285715,["x"]=-359639.42857141,}, + [4]={["y"]=-74637,["x"]=-359279.42857141,}, + [5]={["y"]=-75759.857142857,["x"]=-359005.14285712,}, + [6]={["y"]=-75834.142857143,["x"]=-359045.14285712,}, + [7]={["y"]=-75902.714285714,["x"]=-359782.28571427,}, + [8]={["y"]=-76099.857142857,["x"]=-360399.42857141,}, + [9]={["y"]=-77314.142857143,["x"]=-360219.42857141,}, + [10]={["y"]=-77728.428571429,["x"]=-360445.14285713,}, + [11]={["y"]=-77585.571428571,["x"]=-360585.14285713,}, + [12]={["y"]=-76471.285714286,["x"]=-360819.42857141,}, + [13]={["y"]=-76325.571428571,["x"]=-360942.28571427,}, + [14]={["y"]=-74671.857142857,["x"]=-360927.7142857,}, + [15]={["y"]=-74522.714285714,["x"]=-360888.85714284,}, + }, + PointsRunways = { + [1] = { + [1]={["y"]=-74237.571428571,["x"]=-360591.7142857,}, + [2]={["y"]=-74234.428571429,["x"]=-360493.71428571,}, + [3]={["y"]=-77605.285714286,["x"]=-360399.14285713,}, + [4]={["y"]=-77608.714285715,["x"]=-360498.85714285,}, + [5]={["y"]=-74237.857142857,["x"]=-360591.7142857,}, + }, + [2] = { + [1]={["y"]=-75807.571428572,["x"]=-359073.42857142,}, + [2]={["y"]=-74770.142857144,["x"]=-360581.71428571,}, + [3]={["y"]=-74641.285714287,["x"]=-360585.42857142,}, + [4]={["y"]=-75734.142857144,["x"]=-359023.14285714,}, + [5]={["y"]=-75807.285714287,["x"]=-359073.42857142,}, + }, + }, + ZoneBoundary = {}, + ZoneRunways = {}, + MaximumSpeed = 50, + }, + GroomLake = { + PointsBoundary = { + [1]={["y"]=-88916.714285714,["x"]=-289102.28571425,}, + [2]={["y"]=-87023.571428572,["x"]=-290388.57142857,}, + [3]={["y"]=-85916.428571429,["x"]=-290674.28571428,}, + [4]={["y"]=-87645.000000001,["x"]=-286567.14285714,}, + [5]={["y"]=-88380.714285715,["x"]=-286388.57142857,}, + [6]={["y"]=-89670.714285715,["x"]=-283524.28571428,}, + [7]={["y"]=-89797.857142858,["x"]=-283567.14285714,}, + [8]={["y"]=-88635.000000001,["x"]=-286749.99999999,}, + [9]={["y"]=-89177.857142858,["x"]=-287207.14285714,}, + [10]={["y"]=-89092.142857144,["x"]=-288892.85714285,}, + [11]={["y"]=-88917.000000001,["x"]=-289102.85714285,}, + }, + PointsRunways = { + [1] = { + [1]={["y"]=-86039.000000001,["x"]=-290606.28571428,}, + [2]={["y"]=-85965.285714287,["x"]=-290573.99999999,}, + [3]={["y"]=-87692.714285715,["x"]=-286634.85714285,}, + [4]={["y"]=-87756.714285715,["x"]=-286663.99999999,}, + [5]={["y"]=-86038.714285715,["x"]=-290606.85714285,}, + }, + [2] = { + [1]={["y"]=-86808.428571429,["x"]=-290375.7142857,}, + [2]={["y"]=-86732.714285715,["x"]=-290344.28571427,}, + [3]={["y"]=-89672.714285714,["x"]=-283546.57142855,}, + [4]={["y"]=-89772.142857143,["x"]=-283587.71428569,}, + [5]={["y"]=-86808.142857143,["x"]=-290375.7142857,}, + }, + }, + ZoneBoundary = {}, + ZoneRunways = {}, + MaximumSpeed = 50, + }, + }, +} + +--- Creates a new AIRBASEPOLICE_NEVADA object. +-- @param #AIRBASEPOLICE_NEVADA 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_NEVADA self +function AIRBASEPOLICE_NEVADA:New( SetClient ) + + -- Inherits from BASE + local self = BASE:Inherit( self, AIRBASEPOLICE_BASE:New( SetClient, self.Airbases ) ) + +-- -- Nellis +-- local NellisBoundary = GROUP:FindByName( "Nellis Boundary" ) +-- self.Airbases.Nellis.ZoneBoundary = ZONE_POLYGON:New( "Nellis Boundary", NellisBoundary ):SmokeZone(SMOKECOLOR.White):Flush() +-- +-- local NellisRunway1 = GROUP:FindByName( "Nellis Runway 1" ) +-- self.Airbases.Nellis.ZoneRunways[1] = ZONE_POLYGON:New( "Nellis Runway 1", NellisRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() +-- +-- local NellisRunway2 = GROUP:FindByName( "Nellis Runway 2" ) +-- self.Airbases.Nellis.ZoneRunways[2] = ZONE_POLYGON:New( "Nellis Runway 2", NellisRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() +-- +-- -- McCarran +-- local McCarranBoundary = GROUP:FindByName( "McCarran Boundary" ) +-- self.Airbases.McCarran.ZoneBoundary = ZONE_POLYGON:New( "McCarran Boundary", McCarranBoundary ):SmokeZone(SMOKECOLOR.White):Flush() +-- +-- local McCarranRunway1 = GROUP:FindByName( "McCarran Runway 1" ) +-- self.Airbases.McCarran.ZoneRunways[1] = ZONE_POLYGON:New( "McCarran Runway 1", McCarranRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() +-- +-- local McCarranRunway2 = GROUP:FindByName( "McCarran Runway 2" ) +-- self.Airbases.McCarran.ZoneRunways[2] = ZONE_POLYGON:New( "McCarran Runway 2", McCarranRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() +-- +-- local McCarranRunway3 = GROUP:FindByName( "McCarran Runway 3" ) +-- self.Airbases.McCarran.ZoneRunways[3] = ZONE_POLYGON:New( "McCarran Runway 3", McCarranRunway3 ):SmokeZone(SMOKECOLOR.Red):Flush() +-- +-- local McCarranRunway4 = GROUP:FindByName( "McCarran Runway 4" ) +-- self.Airbases.McCarran.ZoneRunways[4] = ZONE_POLYGON:New( "McCarran Runway 4", McCarranRunway4 ):SmokeZone(SMOKECOLOR.Red):Flush() +-- +-- -- Creech +-- local CreechBoundary = GROUP:FindByName( "Creech Boundary" ) +-- self.Airbases.Creech.ZoneBoundary = ZONE_POLYGON:New( "Creech Boundary", CreechBoundary ):SmokeZone(SMOKECOLOR.White):Flush() +-- +-- local CreechRunway1 = GROUP:FindByName( "Creech Runway 1" ) +-- self.Airbases.Creech.ZoneRunways[1] = ZONE_POLYGON:New( "Creech Runway 1", CreechRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() +-- +-- local CreechRunway2 = GROUP:FindByName( "Creech Runway 2" ) +-- self.Airbases.Creech.ZoneRunways[2] = ZONE_POLYGON:New( "Creech Runway 2", CreechRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() +-- +-- -- Groom Lake +-- local GroomLakeBoundary = GROUP:FindByName( "GroomLake Boundary" ) +-- self.Airbases.GroomLake.ZoneBoundary = ZONE_POLYGON:New( "GroomLake Boundary", GroomLakeBoundary ):SmokeZone(SMOKECOLOR.White):Flush() +-- +-- local GroomLakeRunway1 = GROUP:FindByName( "GroomLake Runway 1" ) +-- self.Airbases.GroomLake.ZoneRunways[1] = ZONE_POLYGON:New( "GroomLake Runway 1", GroomLakeRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() +-- +-- local GroomLakeRunway2 = GROUP:FindByName( "GroomLake Runway 2" ) +-- self.Airbases.GroomLake.ZoneRunways[2] = ZONE_POLYGON:New( "GroomLake Runway 2", GroomLakeRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + +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. +-- The @{Detection#DETECTION_BASE} class will detect objects within the battle zone for a list of @{Group}s detecting targets following (a) detection method(s). +-- +-- 1.1) DETECTION_BASE constructor +-- ------------------------------- +-- Construct a new DETECTION_BASE instance using the @{Detection#DETECTION_BASE.New}() method. +-- +-- 1.2) DETECTION_BASE initialization +-- ---------------------------------- +-- By default, detection will return detected objects with all the detection sensors available. +-- However, you can ask how the objects were found with specific detection methods. +-- If you use one of the below methods, the detection will work with the detection method specified. +-- You can specify to apply multiple detection methods. +-- +-- Use the following functions to report the objects it detected using the methods Visual, Optical, Radar, IRST, RWR, DLINK: +-- +-- * @{Detection#DETECTION_BASE.InitDetectVisual}(): Detected using Visual. +-- * @{Detection#DETECTION_BASE.InitDetectOptical}(): Detected using Optical. +-- * @{Detection#DETECTION_BASE.InitDetectRadar}(): Detected using Radar. +-- * @{Detection#DETECTION_BASE.InitDetectIRST}(): Detected using IRST. +-- * @{Detection#DETECTION_BASE.InitDetectRWR}(): Detected using RWR. +-- * @{Detection#DETECTION_BASE.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_AREAS} class, extends @{Detection#DETECTION_BASE} +-- =============================================================================== +-- The @{Detection#DETECTION_AREAS} class will detect units within the battle zone for a list of @{Group}s detecting targets following (a) detection method(s), +-- and will build a list (table) of @{Set#SET_UNIT}s containing the @{Unit#UNIT}s detected. +-- The class is group the detected units within zones given a DetectedZoneRange parameter. +-- A set with multiple detected zones will be created as there are groups of units detected. +-- +-- 2.1) Retrieve the Detected Unit sets and Detected Zones +-- ------------------------------------------------------- +-- The DetectedUnitSets methods are implemented in @{Detection#DECTECTION_BASE} and the DetectedZones methods is implemented in @{Detection#DETECTION_AREAS}. +-- +-- Retrieve the DetectedUnitSets with the method @{Detection#DETECTION_BASE.GetDetectedSets}(). A table will be return of @{Set#SET_UNIT}s. +-- To understand the amount of sets created, use the method @{Detection#DETECTION_BASE.GetDetectedSetCount}(). +-- If you want to obtain a specific set from the DetectedSets, use the method @{Detection#DETECTION_BASE.GetDetectedSet}() with a given index. +-- +-- Retrieve the formed @{Zone@ZONE_UNIT}s as a result of the grouping the detected units within the DetectionZoneRange, use the method @{Detection#DETECTION_BASE.GetDetectionZones}(). +-- To understand the amount of zones created, use the method @{Detection#DETECTION_BASE.GetDetectionZoneCount}(). +-- If you want to obtain a specific zone from the DetectedZones, use the method @{Detection#DETECTION_BASE.GetDetectionZone}() with a given index. +-- +-- 1.4) Flare or Smoke detected units +-- ---------------------------------- +-- Use the methods @{Detection#DETECTION_AREAS.FlareDetectedUnits}() or @{Detection#DETECTION_AREAS.SmokeDetectedUnits}() to flare or smoke the detected units when a new detection has taken place. +-- +-- 1.5) Flare or Smoke detected zones +-- ---------------------------------- +-- Use the methods @{Detection#DETECTION_AREAS.FlareDetectedZones}() or @{Detection#DETECTION_AREAS.SmokeDetectedZones}() to flare or smoke the detected zones when a new detection has taken place. +-- +-- === +-- +-- ### Contributions: +-- +-- * Mechanist : Concept & Testing +-- +-- ### Authors: +-- +-- * FlightControl : Design & Programming +-- +-- @module Detection + + + +--- DETECTION_BASE class +-- @type DETECTION_BASE +-- @field Core.Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Forward Air Controller role. +-- @field Dcs.DCSTypes#Distance DetectionRange The range till which targets are accepted to be detected. +-- @field #DETECTION_BASE.DetectedObjects DetectedObjects The list of detected objects. +-- @field #table DetectedObjectsIdentified Map of the DetectedObjects identified. +-- @field #number DetectionRun +-- @extends Core.Base#BASE +DETECTION_BASE = { + ClassName = "DETECTION_BASE", + DetectionSetGroup = nil, + DetectionRange = nil, + DetectedObjects = {}, + DetectionRun = 0, + DetectedObjectsIdentified = {}, +} + +--- @type DETECTION_BASE.DetectedObjects +-- @list <#DETECTION_BASE.DetectedObject> + +--- @type DETECTION_BASE.DetectedObject +-- @field #string Name +-- @field #boolean Visible +-- @field #string Type +-- @field #number Distance +-- @field #boolean Identified + +--- DETECTION constructor. +-- @param #DETECTION_BASE self +-- @param Core.Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Forward Air Controller role. +-- @param Dcs.DCSTypes#Distance DetectionRange The range till which targets are accepted to be detected. +-- @return #DETECTION_BASE self +function DETECTION_BASE:New( DetectionSetGroup, DetectionRange ) + + -- Inherits from BASE + local self = BASE:Inherit( self, BASE:New() ) + + self.DetectionSetGroup = DetectionSetGroup + self.DetectionRange = DetectionRange + + self:InitDetectVisual( false ) + self:InitDetectOptical( false ) + self:InitDetectRadar( false ) + self:InitDetectRWR( false ) + self:InitDetectIRST( false ) + self:InitDetectDLINK( false ) + + 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 + +--- Determines if a detected object has already been identified during detection processing. +-- @param #DETECTION_BASE self +-- @param #DETECTION_BASE.DetectedObject DetectedObject +-- @return #boolean true if already identified. +function DETECTION_BASE:IsDetectedObjectIdentified( DetectedObject ) + self:F3( DetectedObject.Name ) + + local DetectedObjectName = DetectedObject.Name + local DetectedObjectIdentified = self.DetectedObjectsIdentified[DetectedObjectName] == true + self:T3( DetectedObjectIdentified ) + return DetectedObjectIdentified +end + +--- Identifies a detected object during detection processing. +-- @param #DETECTION_BASE self +-- @param #DETECTION_BASE.DetectedObject DetectedObject +function DETECTION_BASE:IdentifyDetectedObject( DetectedObject ) + self:F( DetectedObject.Name ) + + local DetectedObjectName = DetectedObject.Name + self.DetectedObjectsIdentified[DetectedObjectName] = true +end + +--- UnIdentify a detected object during detection processing. +-- @param #DETECTION_BASE self +-- @param #DETECTION_BASE.DetectedObject DetectedObject +function DETECTION_BASE:UnIdentifyDetectedObject( DetectedObject ) + + local DetectedObjectName = DetectedObject.Name + self.DetectedObjectsIdentified[DetectedObjectName] = false +end + +--- UnIdentify all detected objects during detection processing. +-- @param #DETECTION_BASE self +function DETECTION_BASE:UnIdentifyAllDetectedObjects() + + self.DetectedObjectsIdentified = {} -- Table will be garbage collected. +end + +--- Gets a detected object with a given name. +-- @param #DETECTION_BASE self +-- @param #string ObjectName +-- @return #DETECTION_BASE.DetectedObject +function DETECTION_BASE:GetDetectedObject( ObjectName ) + self:F3( ObjectName ) + + if ObjectName then + local DetectedObject = self.DetectedObjects[ObjectName] + + -- Only return detected objects that are alive! + local DetectedUnit = UNIT:FindByName( ObjectName ) + if DetectedUnit and DetectedUnit:IsAlive() then + if self:IsDetectedObjectIdentified( DetectedObject ) == false then + return DetectedObject + end + end + end + + return nil +end + +--- Get the detected @{Set#SET_BASE}s. +-- @param #DETECTION_BASE self +-- @return #DETECTION_BASE.DetectedSets DetectedSets +function DETECTION_BASE:GetDetectedSets() + + local DetectionSets = self.DetectedSets + return DetectionSets +end + +--- Get the amount of SETs with detected objects. +-- @param #DETECTION_BASE self +-- @return #number Count +function DETECTION_BASE:GetDetectedSetCount() + + local DetectionSetCount = #self.DetectedSets + return DetectionSetCount +end + +--- Get a SET of detected objects using a given numeric index. +-- @param #DETECTION_BASE self +-- @param #number Index +-- @return Core.Set#SET_BASE +function DETECTION_BASE:GetDetectedSet( Index ) + + local DetectionSet = self.DetectedSets[Index] + if DetectionSet then + return DetectionSet + end + + return nil +end + +--- Get the detection Groups. +-- @param #DETECTION_BASE self +-- @return Wrapper.Group#GROUP +function DETECTION_BASE:GetDetectionSetGroup() + + local DetectionSetGroup = self.DetectionSetGroup + return DetectionSetGroup +end + +--- 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 + + +--- Schedule the DETECTION construction. +-- @param #DETECTION_BASE self +-- @param #number DelayTime The delay in seconds to wait the reporting. +-- @param #number RepeatInterval The repeat interval in seconds for the reporting to happen repeatedly. +-- @return #DETECTION_BASE self +function DETECTION_BASE:Schedule( DelayTime, RepeatInterval ) + self:F2() + + self.ScheduleDelayTime = DelayTime + self.ScheduleRepeatInterval = RepeatInterval + + self.DetectionScheduler = SCHEDULER:New( self, self._DetectionScheduler, { self, "Detection" }, DelayTime, RepeatInterval ) + return self +end + + +--- 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.DetectionRun = self.DetectionRun + 1 + + self:UnIdentifyAllDetectedObjects() -- Resets the DetectedObjectsIdentified table + + for DetectionGroupID, DetectionGroupData in pairs( self.DetectionSetGroup:GetSet() ) do + local DetectionGroup = DetectionGroupData -- Wrapper.Group#GROUP + + if DetectionGroup:IsAlive() then + + local DetectionGroupName = DetectionGroup:GetName() + + local DetectionDetectedTargets = DetectionGroup:GetDetectedTargets( + self.DetectVisual, + self.DetectOptical, + self.DetectRadar, + self.DetectIRST, + self.DetectRWR, + self.DetectDLINK + ) + + for DetectionDetectedTargetID, DetectionDetectedTarget in pairs( DetectionDetectedTargets ) do + local DetectionObject = DetectionDetectedTarget.object -- Dcs.DCSWrapper.Object#Object + self:T2( DetectionObject ) + + if DetectionObject and DetectionObject:isExist() and DetectionObject.id_ < 50000000 then + + local DetectionDetectedObjectName = DetectionObject:getName() + + local DetectionDetectedObjectPositionVec3 = DetectionObject:getPoint() + local DetectionGroupVec3 = DetectionGroup:GetVec3() + + local Distance = ( ( DetectionDetectedObjectPositionVec3.x - DetectionGroupVec3.x )^2 + + ( DetectionDetectedObjectPositionVec3.y - DetectionGroupVec3.y )^2 + + ( DetectionDetectedObjectPositionVec3.z - DetectionGroupVec3.z )^2 + ) ^ 0.5 / 1000 + + self:T2( { DetectionGroupName, DetectionDetectedObjectName, Distance } ) + + if Distance <= self.DetectionRange then + + if not self.DetectedObjects[DetectionDetectedObjectName] then + self.DetectedObjects[DetectionDetectedObjectName] = {} + end + self.DetectedObjects[DetectionDetectedObjectName].Name = DetectionDetectedObjectName + self.DetectedObjects[DetectionDetectedObjectName].Visible = DetectionDetectedTarget.visible + self.DetectedObjects[DetectionDetectedObjectName].Type = DetectionDetectedTarget.type + self.DetectedObjects[DetectionDetectedObjectName].Distance = DetectionDetectedTarget.distance + else + -- if beyond the DetectionRange then nullify... + if self.DetectedObjects[DetectionDetectedObjectName] then + self.DetectedObjects[DetectionDetectedObjectName] = nil + end + end + end + end + + self:T2( self.DetectedObjects ) + + -- okay, now we have a list of detected object names ... + -- Sort the table based on distance ... + table.sort( self.DetectedObjects, function( a, b ) return a.Distance < b.Distance end ) + end + end + + if self.DetectedObjects then + self:CreateDetectionSets() + end + + return true +end + + + +--- DETECTION_AREAS class +-- @type DETECTION_AREAS +-- @field Dcs.DCSTypes#Distance DetectionZoneRange The range till which targets are grouped upon the first detected target. +-- @field #DETECTION_AREAS.DetectedAreas DetectedAreas A list of areas containing the set of @{Unit}s, @{Zone}s, the center @{Unit} within the zone, and ID of each area that was detected within a DetectionZoneRange. +-- @extends Functional.Detection#DETECTION_BASE +DETECTION_AREAS = { + ClassName = "DETECTION_AREAS", + DetectedAreas = { n = 0 }, + DetectionZoneRange = nil, +} + +--- @type DETECTION_AREAS.DetectedAreas +-- @list <#DETECTION_AREAS.DetectedArea> + +--- @type DETECTION_AREAS.DetectedArea +-- @field Core.Set#SET_UNIT Set -- The Set of Units in the detected area. +-- @field Core.Zone#ZONE_UNIT Zone -- The Zone of the detected area. +-- @field #boolean Changed Documents if the detected area has changes. +-- @field #table Changes A list of the changes reported on the detected area. (It is up to the user of the detected area to consume those changes). +-- @field #number AreaID -- The identifier of the detected area. +-- @field #boolean FriendliesNearBy Indicates if there are friendlies within the detected area. +-- @field Wrapper.Unit#UNIT NearestFAC The nearest FAC near the Area. + + +--- DETECTION_AREAS constructor. +-- @param Functional.Detection#DETECTION_AREAS self +-- @param Core.Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Forward Air Controller role. +-- @param Dcs.DCSTypes#Distance DetectionRange The range till which targets are accepted to be detected. +-- @param Dcs.DCSTypes#Distance DetectionZoneRange The range till which targets are grouped upon the first detected target. +-- @return Functional.Detection#DETECTION_AREAS self +function DETECTION_AREAS:New( DetectionSetGroup, DetectionRange, DetectionZoneRange ) + + -- Inherits from DETECTION_BASE + local self = BASE:Inherit( self, DETECTION_BASE:New( DetectionSetGroup, DetectionRange ) ) + + self.DetectionZoneRange = DetectionZoneRange + + self._SmokeDetectedUnits = false + self._FlareDetectedUnits = false + self._SmokeDetectedZones = false + self._FlareDetectedZones = false + + self:Schedule( 10, 10 ) + + return self +end + +--- Add a detected @{#DETECTION_AREAS.DetectedArea}. +-- @param Core.Set#SET_UNIT Set -- The Set of Units in the detected area. +-- @param Core.Zone#ZONE_UNIT Zone -- The Zone of the detected area. +-- @return #DETECTION_AREAS.DetectedArea DetectedArea +function DETECTION_AREAS:AddDetectedArea( Set, Zone ) + local DetectedAreas = self:GetDetectedAreas() + DetectedAreas.n = self:GetDetectedAreaCount() + 1 + DetectedAreas[DetectedAreas.n] = {} + local DetectedArea = DetectedAreas[DetectedAreas.n] + DetectedArea.Set = Set + DetectedArea.Zone = Zone + DetectedArea.Removed = false + DetectedArea.AreaID = DetectedAreas.n + + return DetectedArea +end + +--- Remove a detected @{#DETECTION_AREAS.DetectedArea} with a given Index. +-- @param #DETECTION_AREAS self +-- @param #number Index The Index of the detection are to be removed. +-- @return #nil +function DETECTION_AREAS:RemoveDetectedArea( Index ) + local DetectedAreas = self:GetDetectedAreas() + local DetectedAreaCount = self:GetDetectedAreaCount() + local DetectedArea = DetectedAreas[Index] + local DetectedAreaSet = DetectedArea.Set + DetectedArea[Index] = nil + return nil +end + + +--- Get the detected @{#DETECTION_AREAS.DetectedAreas}. +-- @param #DETECTION_AREAS self +-- @return #DETECTION_AREAS.DetectedAreas DetectedAreas +function DETECTION_AREAS:GetDetectedAreas() + + local DetectedAreas = self.DetectedAreas + return DetectedAreas +end + +--- Get the amount of @{#DETECTION_AREAS.DetectedAreas}. +-- @param #DETECTION_AREAS self +-- @return #number DetectedAreaCount +function DETECTION_AREAS:GetDetectedAreaCount() + + local DetectedAreaCount = self.DetectedAreas.n + return DetectedAreaCount +end + +--- Get the @{Set#SET_UNIT} of a detecttion area using a given numeric index. +-- @param #DETECTION_AREAS self +-- @param #number Index +-- @return Core.Set#SET_UNIT DetectedSet +function DETECTION_AREAS:GetDetectedSet( Index ) + + local DetectedSetUnit = self.DetectedAreas[Index].Set + if DetectedSetUnit then + return DetectedSetUnit + end + + return nil +end + +--- Get the @{Zone#ZONE_UNIT} of a detection area using a given numeric index. +-- @param #DETECTION_AREAS self +-- @param #number Index +-- @return Core.Zone#ZONE_UNIT DetectedZone +function DETECTION_AREAS:GetDetectedZone( Index ) + + local DetectedZone = self.DetectedAreas[Index].Zone + if DetectedZone then + return DetectedZone + end + + return nil +end + +--- Background worker function to determine if there are friendlies nearby ... +-- @param #DETECTION_AREAS self +-- @param Wrapper.Unit#UNIT ReportUnit +function DETECTION_AREAS:ReportFriendliesNearBy( ReportGroupData ) + self:F2() + + local DetectedArea = ReportGroupData.DetectedArea -- Functional.Detection#DETECTION_AREAS.DetectedArea + local DetectedSet = ReportGroupData.DetectedArea.Set + local DetectedZone = ReportGroupData.DetectedArea.Zone + local DetectedZoneUnit = DetectedZone.ZoneUNIT + + DetectedArea.FriendliesNearBy = false + + local SphereSearch = { + id = world.VolumeType.SPHERE, + params = { + point = DetectedZoneUnit:GetVec3(), + radius = 6000, + } + + } + + --- @param Dcs.DCSWrapper.Unit#Unit FoundDCSUnit + -- @param Wrapper.Group#GROUP ReportGroup + -- @param Set#SET_GROUP ReportSetGroup + local FindNearByFriendlies = function( FoundDCSUnit, ReportGroupData ) + + local DetectedArea = ReportGroupData.DetectedArea -- Functional.Detection#DETECTION_AREAS.DetectedArea + local DetectedSet = ReportGroupData.DetectedArea.Set + local DetectedZone = ReportGroupData.DetectedArea.Zone + local DetectedZoneUnit = DetectedZone.ZoneUNIT -- Wrapper.Unit#UNIT + local ReportSetGroup = ReportGroupData.ReportSetGroup + + local EnemyCoalition = DetectedZoneUnit:GetCoalition() + + local FoundUnitCoalition = FoundDCSUnit:getCoalition() + local FoundUnitName = FoundDCSUnit:getName() + local FoundUnitGroupName = FoundDCSUnit:getGroup():getName() + local EnemyUnitName = DetectedZoneUnit:GetName() + local FoundUnitInReportSetGroup = ReportSetGroup:FindGroup( FoundUnitGroupName ) ~= nil + + self:T3( { "Friendlies search:", FoundUnitName, FoundUnitCoalition, EnemyUnitName, EnemyCoalition, FoundUnitInReportSetGroup } ) + + if FoundUnitCoalition ~= EnemyCoalition and FoundUnitInReportSetGroup == false then + DetectedArea.FriendliesNearBy = true + return false + end + + return true + end + + world.searchObjects( Object.Category.UNIT, SphereSearch, FindNearByFriendlies, ReportGroupData ) + +end + + + +--- Returns if there are friendlies nearby the FAC units ... +-- @param #DETECTION_AREAS self +-- @return #boolean trhe if there are friendlies nearby +function DETECTION_AREAS:IsFriendliesNearBy( DetectedArea ) + + self:T3( DetectedArea.FriendliesNearBy ) + return DetectedArea.FriendliesNearBy or false +end + +--- Calculate the maxium A2G threat level of the DetectedArea. +-- @param #DETECTION_AREAS self +-- @param #DETECTION_AREAS.DetectedArea DetectedArea +function DETECTION_AREAS:CalculateThreatLevelA2G( DetectedArea ) + + local MaxThreatLevelA2G = 0 + for UnitName, UnitData in pairs( DetectedArea.Set:GetSet() ) do + local ThreatUnit = UnitData -- Wrapper.Unit#UNIT + local ThreatLevelA2G = ThreatUnit:GetThreatLevel() + if ThreatLevelA2G > MaxThreatLevelA2G then + MaxThreatLevelA2G = ThreatLevelA2G + end + end + + self:T3( MaxThreatLevelA2G ) + DetectedArea.MaxThreatLevelA2G = MaxThreatLevelA2G + +end + +--- Find the nearest FAC of the DetectedArea. +-- @param #DETECTION_AREAS self +-- @param #DETECTION_AREAS.DetectedArea DetectedArea +-- @return Wrapper.Unit#UNIT The nearest FAC unit +function DETECTION_AREAS:NearestFAC( DetectedArea ) + + local NearestFAC = nil + local MinDistance = 1000000000 -- Units are not further than 1000000 km away from an area :-) + + for FACGroupName, FACGroupData in pairs( self.DetectionSetGroup:GetSet() ) do + for FACUnit, FACUnitData in pairs( FACGroupData:GetUnits() ) do + local FACUnit = FACUnitData -- Wrapper.Unit#UNIT + if FACUnit:IsActive() then + local Vec3 = FACUnit:GetVec3() + local PointVec3 = POINT_VEC3:NewFromVec3( Vec3 ) + local Distance = PointVec3:Get2DDistance(POINT_VEC3:NewFromVec3( FACUnit:GetVec3() ) ) + if Distance < MinDistance then + MinDistance = Distance + NearestFAC = FACUnit + end + end + end + end + + DetectedArea.NearestFAC = NearestFAC + +end + +--- Returns the A2G threat level of the units in the DetectedArea +-- @param #DETECTION_AREAS self +-- @param #DETECTION_AREAS.DetectedArea DetectedArea +-- @return #number a scale from 0 to 10. +function DETECTION_AREAS:GetTreatLevelA2G( DetectedArea ) + + self:T3( DetectedArea.MaxThreatLevelA2G ) + return DetectedArea.MaxThreatLevelA2G +end + + + +--- Smoke the detected units +-- @param #DETECTION_AREAS self +-- @return #DETECTION_AREAS self +function DETECTION_AREAS:SmokeDetectedUnits() + self:F2() + + self._SmokeDetectedUnits = true + return self +end + +--- Flare the detected units +-- @param #DETECTION_AREAS self +-- @return #DETECTION_AREAS self +function DETECTION_AREAS:FlareDetectedUnits() + self:F2() + + self._FlareDetectedUnits = true + return self +end + +--- Smoke the detected zones +-- @param #DETECTION_AREAS self +-- @return #DETECTION_AREAS self +function DETECTION_AREAS:SmokeDetectedZones() + self:F2() + + self._SmokeDetectedZones = true + return self +end + +--- Flare the detected zones +-- @param #DETECTION_AREAS self +-- @return #DETECTION_AREAS self +function DETECTION_AREAS:FlareDetectedZones() + self:F2() + + self._FlareDetectedZones = true + return self +end + +--- Add a change to the detected zone. +-- @param #DETECTION_AREAS self +-- @param #DETECTION_AREAS.DetectedArea DetectedArea +-- @param #string ChangeCode +-- @return #DETECTION_AREAS self +function DETECTION_AREAS:AddChangeArea( DetectedArea, ChangeCode, AreaUnitType ) + + DetectedArea.Changed = true + local AreaID = DetectedArea.AreaID + + DetectedArea.Changes = DetectedArea.Changes or {} + DetectedArea.Changes[ChangeCode] = DetectedArea.Changes[ChangeCode] or {} + DetectedArea.Changes[ChangeCode].AreaID = AreaID + DetectedArea.Changes[ChangeCode].AreaUnitType = AreaUnitType + + self:T( { "Change on Detection Area:", DetectedArea.AreaID, ChangeCode, AreaUnitType } ) + + return self +end + + +--- Add a change to the detected zone. +-- @param #DETECTION_AREAS self +-- @param #DETECTION_AREAS.DetectedArea DetectedArea +-- @param #string ChangeCode +-- @param #string ChangeUnitType +-- @return #DETECTION_AREAS self +function DETECTION_AREAS:AddChangeUnit( DetectedArea, ChangeCode, ChangeUnitType ) + + DetectedArea.Changed = true + local AreaID = DetectedArea.AreaID + + DetectedArea.Changes = DetectedArea.Changes or {} + DetectedArea.Changes[ChangeCode] = DetectedArea.Changes[ChangeCode] or {} + DetectedArea.Changes[ChangeCode][ChangeUnitType] = DetectedArea.Changes[ChangeCode][ChangeUnitType] or 0 + DetectedArea.Changes[ChangeCode][ChangeUnitType] = DetectedArea.Changes[ChangeCode][ChangeUnitType] + 1 + DetectedArea.Changes[ChangeCode].AreaID = AreaID + + self:T( { "Change on Detection Area:", DetectedArea.AreaID, ChangeCode, ChangeUnitType } ) + + return self +end + +--- Make text documenting the changes of the detected zone. +-- @param #DETECTION_AREAS self +-- @param #DETECTION_AREAS.DetectedArea DetectedArea +-- @return #string The Changes text +function DETECTION_AREAS:GetChangeText( DetectedArea ) + self:F( DetectedArea ) + + local MT = {} + + for ChangeCode, ChangeData in pairs( DetectedArea.Changes ) do + + if ChangeCode == "AA" then + MT[#MT+1] = "Detected new area " .. ChangeData.AreaID .. ". The center target is a " .. ChangeData.AreaUnitType .. "." + end + + if ChangeCode == "RAU" then + MT[#MT+1] = "Changed area " .. ChangeData.AreaID .. ". Removed the center target." + end + + if ChangeCode == "AAU" then + MT[#MT+1] = "Changed area " .. ChangeData.AreaID .. ". The new center target is a " .. ChangeData.AreaUnitType "." + end + + if ChangeCode == "RA" then + MT[#MT+1] = "Removed old area " .. ChangeData.AreaID .. ". No more targets in this area." + end + + if ChangeCode == "AU" then + local MTUT = {} + for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do + if ChangeUnitType ~= "AreaID" then + MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType + end + end + MT[#MT+1] = "Detected for area " .. ChangeData.AreaID .. " new target(s) " .. table.concat( MTUT, ", " ) .. "." + end + + if ChangeCode == "RU" then + local MTUT = {} + for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do + if ChangeUnitType ~= "AreaID" then + MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType + end + end + MT[#MT+1] = "Removed for area " .. ChangeData.AreaID .. " invisible or destroyed target(s) " .. table.concat( MTUT, ", " ) .. "." + end + + end + + return table.concat( MT, "\n" ) + +end + + +--- Accepts changes from the detected zone. +-- @param #DETECTION_AREAS self +-- @param #DETECTION_AREAS.DetectedArea DetectedArea +-- @return #DETECTION_AREAS self +function DETECTION_AREAS:AcceptChanges( DetectedArea ) + + DetectedArea.Changed = false + DetectedArea.Changes = {} + + return self +end + + +--- Make a DetectionSet table. This function will be overridden in the derived clsses. +-- @param #DETECTION_AREAS self +-- @return #DETECTION_AREAS self +function DETECTION_AREAS:CreateDetectionSets() + self:F2() + + -- First go through all detected sets, and check if there are new detected units, match all existing detected units and identify undetected units. + -- Regroup when needed, split groups when needed. + for DetectedAreaID, DetectedAreaData in ipairs( self.DetectedAreas ) do + + local DetectedArea = DetectedAreaData -- #DETECTION_AREAS.DetectedArea + if DetectedArea then + + local DetectedSet = DetectedArea.Set + + local AreaExists = false -- This flag will determine of the detected area is still existing. + + -- First test if the center unit is detected in the detection area. + self:T3( DetectedArea.Zone.ZoneUNIT.UnitName ) + local DetectedZoneObject = self:GetDetectedObject( DetectedArea.Zone.ZoneUNIT.UnitName ) + self:T3( { "Detecting Zone Object", DetectedArea.AreaID, DetectedArea.Zone, DetectedZoneObject } ) + + if DetectedZoneObject then + + --self:IdentifyDetectedObject( DetectedZoneObject ) + AreaExists = true + + + + else + -- The center object of the detected area has not been detected. Find an other unit of the set to become the center of the area. + -- First remove the center unit from the set. + DetectedSet:RemoveUnitsByName( DetectedArea.Zone.ZoneUNIT.UnitName ) + + self:AddChangeArea( DetectedArea, 'RAU', "Dummy" ) + + -- Then search for a new center area unit within the set. Note that the new area unit candidate must be within the area range. + for DetectedUnitName, DetectedUnitData in pairs( DetectedSet:GetSet() ) do + + local DetectedUnit = DetectedUnitData -- Wrapper.Unit#UNIT + local DetectedObject = self:GetDetectedObject( DetectedUnit.UnitName ) + + -- The DetectedObject can be nil when the DetectedUnit is not alive anymore or it is not in the DetectedObjects map. + -- If the DetectedUnit was already identified, DetectedObject will be nil. + if DetectedObject then + self:IdentifyDetectedObject( DetectedObject ) + AreaExists = true + + -- Assign the Unit as the new center unit of the detected area. + DetectedArea.Zone = ZONE_UNIT:New( DetectedUnit:GetName(), DetectedUnit, self.DetectionZoneRange ) + + self:AddChangeArea( DetectedArea, "AAU", DetectedArea.Zone.ZoneUNIT:GetTypeName() ) + + -- We don't need to add the DetectedObject to the area set, because it is already there ... + break + end + end + end + + -- Now we've determined the center unit of the area, now we can iterate the units in the detected area. + -- Note that the position of the area may have moved due to the center unit repositioning. + -- If no center unit was identified, then the detected area does not exist anymore and should be deleted, as there are no valid units that can be the center unit. + if AreaExists then + + -- ok, we found the center unit of the area, now iterate through the detected area set and see which units are still within the center unit zone ... + -- Those units within the zone are flagged as Identified. + -- If a unit was not found in the set, remove it from the set. This may be added later to other existing or new sets. + for DetectedUnitName, DetectedUnitData in pairs( DetectedSet:GetSet() ) do + + local DetectedUnit = DetectedUnitData -- Wrapper.Unit#UNIT + local DetectedObject = nil + if DetectedUnit:IsAlive() then + --self:E(DetectedUnit:GetName()) + DetectedObject = self:GetDetectedObject( DetectedUnit:GetName() ) + end + if DetectedObject then + + -- Check if the DetectedUnit is within the DetectedArea.Zone + if DetectedUnit:IsInZone( DetectedArea.Zone ) then + + -- Yes, the DetectedUnit is within the DetectedArea.Zone, no changes, DetectedUnit can be kept within the Set. + self:IdentifyDetectedObject( DetectedObject ) + + else + -- No, the DetectedUnit is not within the DetectedArea.Zone, remove DetectedUnit from the Set. + DetectedSet:Remove( DetectedUnitName ) + self:AddChangeUnit( DetectedArea, "RU", DetectedUnit:GetTypeName() ) + end + + else + -- There was no DetectedObject, remove DetectedUnit from the Set. + self:AddChangeUnit( DetectedArea, "RU", "destroyed target" ) + DetectedSet:Remove( DetectedUnitName ) + + -- The DetectedObject has been identified, because it does not exist ... + -- self:IdentifyDetectedObject( DetectedObject ) + end + end + else + self:RemoveDetectedArea( DetectedAreaID ) + self:AddChangeArea( DetectedArea, "RA" ) + end + end + end + + -- We iterated through the existing detection areas and: + -- - We checked which units are still detected in each detection area. Those units were flagged as Identified. + -- - We recentered the detection area to new center units where it was needed. + -- + -- Now we need to loop through the unidentified detected units and see where they belong: + -- - They can be added to a new detection area and become the new center unit. + -- - They can be added to a new detection area. + for DetectedUnitName, DetectedObjectData in pairs( self.DetectedObjects ) do + + local DetectedObject = self:GetDetectedObject( DetectedUnitName ) + + if DetectedObject then + + -- We found an unidentified unit outside of any existing detection area. + local DetectedUnit = UNIT:FindByName( DetectedUnitName ) -- Wrapper.Unit#UNIT + + local AddedToDetectionArea = false + + for DetectedAreaID, DetectedAreaData in ipairs( self.DetectedAreas ) do + + local DetectedArea = DetectedAreaData -- #DETECTION_AREAS.DetectedArea + if DetectedArea then + self:T( "Detection Area #" .. DetectedArea.AreaID ) + local DetectedSet = DetectedArea.Set + if not self:IsDetectedObjectIdentified( DetectedObject ) and DetectedUnit:IsInZone( DetectedArea.Zone ) then + self:IdentifyDetectedObject( DetectedObject ) + DetectedSet:AddUnit( DetectedUnit ) + AddedToDetectionArea = true + self:AddChangeUnit( DetectedArea, "AU", DetectedUnit:GetTypeName() ) + end + end + end + + if AddedToDetectionArea == false then + + -- New detection area + local DetectedArea = self:AddDetectedArea( + SET_UNIT:New(), + ZONE_UNIT:New( DetectedUnitName, DetectedUnit, self.DetectionZoneRange ) + ) + --self:E( DetectedArea.Zone.ZoneUNIT.UnitName ) + DetectedArea.Set:AddUnit( DetectedUnit ) + self:AddChangeArea( DetectedArea, "AA", DetectedUnit:GetTypeName() ) + end + end + end + + -- Now all the tests should have been build, now make some smoke and flares... + -- We also report here the friendlies within the detected areas. + + for DetectedAreaID, DetectedAreaData in ipairs( self.DetectedAreas ) do + + local DetectedArea = DetectedAreaData -- #DETECTION_AREAS.DetectedArea + local DetectedSet = DetectedArea.Set + local DetectedZone = DetectedArea.Zone + + self:ReportFriendliesNearBy( { DetectedArea = DetectedArea, ReportSetGroup = self.DetectionSetGroup } ) -- Fill the Friendlies table + self:CalculateThreatLevelA2G( DetectedArea ) -- Calculate A2G threat level + self:NearestFAC( DetectedArea ) + + if DETECTION_AREAS._SmokeDetectedUnits or self._SmokeDetectedUnits then + DetectedZone.ZoneUNIT:SmokeRed() + end + DetectedSet:ForEachUnit( + --- @param Wrapper.Unit#UNIT DetectedUnit + function( DetectedUnit ) + if DetectedUnit:IsAlive() then + self:T( "Detected Set #" .. DetectedArea.AreaID .. ":" .. DetectedUnit:GetName() ) + if DETECTION_AREAS._FlareDetectedUnits or self._FlareDetectedUnits then + DetectedUnit:FlareGreen() + end + if DETECTION_AREAS._SmokeDetectedUnits or self._SmokeDetectedUnits then + DetectedUnit:SmokeGreen() + end + end + end + ) + if DETECTION_AREAS._FlareDetectedZones or self._FlareDetectedZones then + DetectedZone:FlareZone( SMOKECOLOR.White, 30, math.random( 0,90 ) ) + end + if DETECTION_AREAS._SmokeDetectedZones or self._SmokeDetectedZones then + DetectedZone:SmokeZone( SMOKECOLOR.White, 30 ) + end + end + +end + + +--- Single-Player:**No** / Multi-Player:**Yes** / AI:**Yes** / Human:**No** / Types:**All** -- **AI Balancing will replace in multi player missions +-- non-occupied human slots with AI groups, in order to provide an engaging simulation environment, +-- even when there are hardly any players in the mission.** +-- +-- ![Banner Image](..\Presentations\AI_Balancer\Dia1.JPG) +-- +-- === +-- +-- # 1) @{AI_Balancer#AI_BALANCER} class, extends @{Fsm#FSM_SET} +-- +-- The @{AI_Balancer#AI_BALANCER} class monitors and manages as many replacement AI groups as there are +-- CLIENTS in a SET_CLIENT collection, which are not occupied by human players. +-- In other words, use AI_BALANCER to simulate human behaviour by spawning in replacement AI in multi player missions. +-- +-- The parent class @{Fsm#FSM_SET} manages the functionality to control the Finite State Machine (FSM). +-- The mission designer can tailor the behaviour of the AI_BALANCER, by defining event and state transition methods. +-- An explanation about state and event transition methods can be found in the @{FSM} module documentation. +-- +-- The mission designer can tailor the AI_BALANCER behaviour, by implementing a state or event handling method for the following: +-- +-- * **@{#AI_BALANCER.OnAfterSpawned}**( AISet, From, Event, To, AIGroup ): Define to add extra logic when an AI is spawned. +-- +-- ## 1.1) AI_BALANCER construction +-- +-- Create a new AI_BALANCER object with the @{#AI_BALANCER.New}() method: +-- +-- ## 1.2) AI_BALANCER is a FSM +-- +-- ![Process](..\Presentations\AI_Balancer\Dia13.JPG) +-- +-- ### 1.2.1) AI_BALANCER States +-- +-- * **Monitoring** ( Set ): Monitoring the Set if all AI is spawned for the Clients. +-- * **Spawning** ( Set, ClientName ): There is a new AI group spawned with ClientName as the name of reference. +-- * **Spawned** ( Set, AIGroup ): A new AI has been spawned. You can handle this event to customize the AI behaviour with other AI FSMs or own processes. +-- * **Destroying** ( Set, AIGroup ): The AI is being destroyed. +-- * **Returning** ( Set, AIGroup ): The AI is returning to the airbase specified by the ReturnToAirbase methods. Handle this state to customize the return behaviour of the AI, if any. +-- +-- ### 1.2.2) AI_BALANCER Events +-- +-- * **Monitor** ( Set ): Every 10 seconds, the Monitor event is triggered to monitor the Set. +-- * **Spawn** ( Set, ClientName ): Triggers when there is a new AI group to be spawned with ClientName as the name of reference. +-- * **Spawned** ( Set, AIGroup ): Triggers when a new AI has been spawned. You can handle this event to customize the AI behaviour with other AI FSMs or own processes. +-- * **Destroy** ( Set, AIGroup ): The AI is being destroyed. +-- * **Return** ( Set, AIGroup ): The AI is returning to the airbase specified by the ReturnToAirbase methods. +-- +-- ## 1.3) AI_BALANCER spawn interval for replacement AI +-- +-- Use the method @{#AI_BALANCER.InitSpawnInterval}() to set the earliest and latest interval in seconds that is waited until a new replacement AI is spawned. +-- +-- ## 1.4) AI_BALANCER returns AI to Airbases +-- +-- By default, When a human player joins a slot that is AI_BALANCED, the AI group will be destroyed by default. +-- However, there are 2 additional options that you can use to customize the destroy behaviour. +-- When a human player joins a slot, you can configure to let the AI return to: +-- +-- * @{#AI_BALANCER.ReturnToHomeAirbase}: Returns the AI to the **home** @{Airbase#AIRBASE}. +-- * @{#AI_BALANCER.ReturnToNearestAirbases}: Returns the AI to the **nearest friendly** @{Airbase#AIRBASE}. +-- +-- Note that when AI returns to an airbase, the AI_BALANCER will trigger the **Return** event and the AI will return, +-- otherwise the AI_BALANCER will trigger a **Destroy** event, and the AI will be destroyed. +-- +-- === +-- +-- # **API CHANGE HISTORY** +-- +-- The underlying change log documents the API changes. Please read this carefully. The following notation is used: +-- +-- * **Added** parts are expressed in bold type face. +-- * _Removed_ parts are expressed in italic type face. +-- +-- Hereby the change log: +-- +-- 2017-01-17: There is still a problem with AI being destroyed, but not respawned. Need to check further upon that. +-- +-- 2017-01-08: AI_BALANCER:**InitSpawnInterval( Earliest, Latest )** added. +-- +-- === +-- +-- # **AUTHORS and CONTRIBUTIONS** +-- +-- ### Contributions: +-- +-- * **[Dutch_Baron](https://forums.eagle.ru/member.php?u=112075)**: Working together with James has resulted in the creation of the AI_BALANCER class. James has shared his ideas on balancing AI with air units, and together we made a first design which you can use now :-) +-- * **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 AI_BALANCER moose class. +-- +-- ### Authors: +-- +-- * FlightControl: Framework Design & Programming and Documentation. +-- +-- @module AI_Balancer + +--- AI_BALANCER class +-- @type AI_BALANCER +-- @field Core.Set#SET_CLIENT SetClient +-- @field Functional.Spawn#SPAWN SpawnAI +-- @field Wrapper.Group#GROUP Test +-- @extends Core.Fsm#FSM_SET +AI_BALANCER = { + ClassName = "AI_BALANCER", + PatrolZones = {}, + AIGroups = {}, + Earliest = 5, -- Earliest a new AI can be spawned is in 5 seconds. + Latest = 60, -- Latest a new AI can be spawned is in 60 seconds. +} + + + +--- Creates a new AI_BALANCER object +-- @param #AI_BALANCER self +-- @param Core.Set#SET_CLIENT SetClient A SET\_CLIENT object that will contain the CLIENT objects to be monitored if they are alive or not (joined by a player). +-- @param Functional.Spawn#SPAWN SpawnAI The default Spawn object to spawn new AI Groups when needed. +-- @return #AI_BALANCER +function AI_BALANCER:New( SetClient, SpawnAI ) + + -- Inherits from BASE + local self = BASE:Inherit( self, FSM_SET:New( SET_GROUP:New() ) ) -- AI.AI_Balancer#AI_BALANCER + + -- TODO: Define the OnAfterSpawned event + self:SetStartState( "None" ) + self:AddTransition( "*", "Monitor", "Monitoring" ) + self:AddTransition( "*", "Spawn", "Spawning" ) + self:AddTransition( "Spawning", "Spawned", "Spawned" ) + self:AddTransition( "*", "Destroy", "Destroying" ) + self:AddTransition( "*", "Return", "Returning" ) + + self.SetClient = SetClient + self.SetClient:FilterOnce() + self.SpawnAI = SpawnAI + + self.SpawnQueue = {} + + self.ToNearestAirbase = false + self.ToHomeAirbase = false + + self:__Monitor( 1 ) + + return self +end + +--- Sets the earliest to the latest interval in seconds how long AI_BALANCER will wait to spawn a new AI. +-- Provide 2 identical seconds if the interval should be a fixed amount of seconds. +-- @param #AI_BALANCER self +-- @param #number Earliest The earliest a new AI can be spawned in seconds. +-- @param #number Latest The latest a new AI can be spawned in seconds. +-- @return self +function AI_BALANCER:InitSpawnInterval( Earliest, Latest ) + + self.Earliest = Earliest + self.Latest = Latest + + return self +end + +--- Returns the AI to the nearest friendly @{Airbase#AIRBASE}. +-- @param #AI_BALANCER self +-- @param Dcs.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 Core.Set#SET_AIRBASE ReturnAirbaseSet The SET of @{Set#SET_AIRBASE}s to evaluate where to return to. +function AI_BALANCER:ReturnToNearestAirbases( ReturnTresholdRange, ReturnAirbaseSet ) + + self.ToNearestAirbase = true + self.ReturnTresholdRange = ReturnTresholdRange + self.ReturnAirbaseSet = ReturnAirbaseSet +end + +--- Returns the AI to the home @{Airbase#AIRBASE}. +-- @param #AI_BALANCER self +-- @param Dcs.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 AI_BALANCER:ReturnToHomeAirbase( ReturnTresholdRange ) + + self.ToHomeAirbase = true + self.ReturnTresholdRange = ReturnTresholdRange +end + +--- @param #AI_BALANCER self +-- @param Core.Set#SET_GROUP SetGroup +-- @param #string ClientName +-- @param Wrapper.Group#GROUP AIGroup +function AI_BALANCER:onenterSpawning( SetGroup, From, Event, To, ClientName ) + + -- OK, Spawn a new group from the default SpawnAI object provided. + local AIGroup = self.SpawnAI:Spawn() -- Wrapper.Group#GROUP + if AIGroup then + AIGroup:E( "Spawning new AIGroup" ) + --TODO: need to rework UnitName thing ... + + SetGroup:Add( ClientName, AIGroup ) + self.SpawnQueue[ClientName] = nil + + -- Fire the Spawned event. The first parameter is the AIGroup just Spawned. + -- Mission designers can catch this event to bind further actions to the AIGroup. + self:Spawned( AIGroup ) + end +end + +--- @param #AI_BALANCER self +-- @param Core.Set#SET_GROUP SetGroup +-- @param Wrapper.Group#GROUP AIGroup +function AI_BALANCER:onenterDestroying( SetGroup, From, Event, To, ClientName, AIGroup ) + + AIGroup:Destroy() + SetGroup:Flush() + SetGroup:Remove( ClientName ) + SetGroup:Flush() +end + +--- @param #AI_BALANCER self +-- @param Core.Set#SET_GROUP SetGroup +-- @param Wrapper.Group#GROUP AIGroup +function AI_BALANCER:onenterReturning( SetGroup, From, Event, To, AIGroup ) + + local AIGroupTemplate = AIGroup:GetTemplate() + if self.ToHomeAirbase == true then + local WayPointCount = #AIGroupTemplate.route.points + local SwitchWayPointCommand = AIGroup:CommandSwitchWayPoint( 1, WayPointCount, 1 ) + AIGroup:SetCommand( SwitchWayPointCommand ) + AIGroup:MessageToRed( "Returning to home base ...", 30 ) + else + -- Okay, we need to send this Group back to the nearest base of the Coalition of the AI. + --TODO: i need to rework the POINT_VEC2 thing. + local PointVec2 = POINT_VEC2:New( AIGroup:GetVec2().x, AIGroup:GetVec2().y ) + local ClosestAirbase = self.ReturnAirbaseSet:FindNearestAirbaseFromPointVec2( PointVec2 ) + self:T( ClosestAirbase.AirbaseName ) + AIGroup:MessageToRed( "Returning to " .. ClosestAirbase:GetName().. " ...", 30 ) + local RTBRoute = AIGroup:RouteReturnToAirbase( ClosestAirbase ) + AIGroupTemplate.route = RTBRoute + AIGroup:Respawn( AIGroupTemplate ) + end + +end + + +--- @param #AI_BALANCER self +function AI_BALANCER:onenterMonitoring( SetGroup ) + + self:T2( { self.SetClient:Count() } ) + --self.SetClient:Flush() + + self.SetClient:ForEachClient( + --- @param Wrapper.Client#CLIENT Client + function( Client ) + self:T3(Client.ClientName) + + local AIGroup = self.Set:Get( Client.UnitName ) -- Wrapper.Group#GROUP + if Client:IsAlive() then + + if AIGroup and AIGroup:IsAlive() == true then + + if self.ToNearestAirbase == false and self.ToHomeAirbase == false then + self:Destroy( Client.UnitName, AIGroup ) + else + -- We test if there is no other CLIENT within the self.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:GetVec2(), self.ReturnTresholdRange ) + + self:T2( RangeZone ) + + _DATABASE:ForEachPlayer( + --- @param Wrapper.Unit#UNIT RangeTestUnit + function( RangeTestUnit, RangeZone, AIGroup, PlayerInRange ) + self:T2( { PlayerInRange, RangeTestUnit.UnitName, RangeZone.ZoneName } ) + if RangeTestUnit:IsInZone( RangeZone ) == true then + self:T2( "in zone" ) + if RangeTestUnit:GetCoalition() ~= AIGroup:GetCoalition() then + self:T2( "in range" ) + PlayerInRange.Value = true + end + end + end, + + --- @param Core.Zone#ZONE_RADIUS RangeZone + -- @param Wrapper.Group#GROUP AIGroup + function( RangeZone, AIGroup, PlayerInRange ) + if PlayerInRange.Value == false then + self:Return( AIGroup ) + end + end + , RangeZone, AIGroup, PlayerInRange + ) + + end + self.Set:Remove( Client.UnitName ) + end + else + if not AIGroup or not AIGroup:IsAlive() == true then + self:T( "Client " .. Client.UnitName .. " not alive." ) + if not self.SpawnQueue[Client.UnitName] then + -- Spawn a new AI taking into account the spawn interval Earliest, Latest + self:__Spawn( math.random( self.Earliest, self.Latest ), Client.UnitName ) + self.SpawnQueue[Client.UnitName] = true + self:E( "New AI Spawned for Client " .. Client.UnitName ) + end + end + end + return true + end + ) + + self:__Monitor( 10 ) +end + + + +--- Single-Player:**Yes** / Multi-Player:**Yes** / AI:**Yes** / Human:**No** / Types:**Air** -- +-- **Air Patrolling or Staging.** +-- +-- ![Banner Image](..\Presentations\AI_PATROL\Dia1.JPG) +-- +-- === +-- +-- # 1) @{#AI_PATROL_ZONE} class, extends @{Fsm#FSM_CONTROLLABLE} +-- +-- The @{#AI_PATROL_ZONE} class implements the core functions to patrol a @{Zone} by an AI @{Controllable} or @{Group}. +-- +-- ![Process](..\Presentations\AI_PATROL\Dia3.JPG) +-- +-- The AI_PATROL_ZONE is assigned a @{Group} and this must be done before the AI_PATROL_ZONE process can be started using the **Start** event. +-- +-- ![Process](..\Presentations\AI_PATROL\Dia4.JPG) +-- +-- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits. +-- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. +-- +-- ![Process](..\Presentations\AI_PATROL\Dia5.JPG) +-- +-- This cycle will continue. +-- +-- ![Process](..\Presentations\AI_PATROL\Dia6.JPG) +-- +-- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event. +-- +-- ![Process](..\Presentations\AI_PATROL\Dia9.JPG) +-- +---- Note that the enemy is not engaged! To model enemy engagement, either tailor the **Detected** event, or +-- use derived AI_ classes to model AI offensive or defensive behaviour. +-- +-- ![Process](..\Presentations\AI_PATROL\Dia10.JPG) +-- +-- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. +-- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. +-- +-- ![Process](..\Presentations\AI_PATROL\Dia11.JPG) +-- +-- ## 1.1) AI_PATROL_ZONE constructor +-- +-- * @{#AI_PATROL_ZONE.New}(): Creates a new AI_PATROL_ZONE object. +-- +-- ## 1.2) AI_PATROL_ZONE is a FSM +-- +-- ![Process](..\Presentations\AI_PATROL\Dia2.JPG) +-- +-- ### 1.2.1) AI_PATROL_ZONE States +-- +-- * **None** ( Group ): The process is not started yet. +-- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. +-- * **Returning** ( Group ): The AI is returning to Base. +-- * **Stopped** ( Group ): The process is stopped. +-- * **Crashed** ( Group ): The AI has crashed or is dead. +-- +-- ### 1.2.2) AI_PATROL_ZONE Events +-- +-- * **Start** ( Group ): Start the process. +-- * **Stop** ( Group ): Stop the process. +-- * **Route** ( Group ): Route the AI to a new random 3D point within the Patrol Zone. +-- * **RTB** ( Group ): Route the AI to the home base. +-- * **Detect** ( Group ): The AI is detecting targets. +-- * **Detected** ( Group ): The AI has detected new targets. +-- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. +-- +-- ## 1.3) Set or Get the AI controllable +-- +-- * @{#AI_PATROL_ZONE.SetControllable}(): Set the AIControllable. +-- * @{#AI_PATROL_ZONE.GetControllable}(): Get the AIControllable. +-- +-- ## 1.4) Set the Speed and Altitude boundaries of the AI controllable +-- +-- * @{#AI_PATROL_ZONE.SetSpeed}(): Set the patrol speed boundaries of the AI, for the next patrol. +-- * @{#AI_PATROL_ZONE.SetAltitude}(): Set altitude boundaries of the AI, for the next patrol. +-- +-- ## 1.5) Manage the detection process of the AI controllable +-- +-- The detection process of the AI controllable can be manipulated. +-- Detection requires an amount of CPU power, which has an impact on your mission performance. +-- Only put detection on when absolutely necessary, and the frequency of the detection can also be set. +-- +-- * @{#AI_PATROL_ZONE.SetDetectionOn}(): Set the detection on. The AI will detect for targets. +-- * @{#AI_PATROL_ZONE.SetDetectionOff}(): Set the detection off, the AI will not detect for targets. The existing target list will NOT be erased. +-- +-- The detection frequency can be set with @{#AI_PATROL_ZONE.SetDetectionInterval}( seconds ), where the amount of seconds specify how much seconds will be waited before the next detection. +-- Use the method @{#AI_PATROL_ZONE.GetDetectedUnits}() to obtain a list of the @{Unit}s detected by the AI. +-- +-- The detection can be filtered to potential targets in a specific zone. +-- Use the method @{#AI_PATROL_ZONE.SetDetectionZone}() to set the zone where targets need to be detected. +-- Note that when the zone is too far away, or the AI is not heading towards the zone, or the AI is too high, no targets may be detected +-- according the weather conditions. +-- +-- ## 1.6) Manage the "out of fuel" in the AI_PATROL_ZONE +-- +-- When the AI is out of fuel, it is required that a new AI is started, before the old AI can return to the home base. +-- Therefore, with a parameter and a calculation of the distance to the home base, the fuel treshold is calculated. +-- When the fuel treshold is reached, the AI will continue for a given time its patrol task in orbit, +-- while a new AI is targetted to the AI_PATROL_ZONE. +-- Once the time is finished, the old AI will return to the base. +-- Use the method @{#AI_PATROL_ZONE.ManageFuel}() to have this proces in place. +-- +-- ## 1.7) Manage "damage" behaviour of the AI in the AI_PATROL_ZONE +-- +-- When the AI is damaged, it is required that a new AIControllable is started. However, damage cannon be foreseen early on. +-- Therefore, when the damage treshold is reached, the AI will return immediately to the home base (RTB). +-- Use the method @{#AI_PATROL_ZONE.ManageDamage}() to have this proces in place. +-- +-- ==== +-- +-- # **OPEN ISSUES** +-- +-- 2017-01-17: When Spawned AI is located at an airbase, it will be routed first back to the airbase after take-off. +-- +-- 2016-01-17: +-- -- Fixed problem with AI returning to base too early and unexpected. +-- -- ReSpawning of AI will reset the AI_PATROL and derived classes. +-- -- Checked the correct workings of SCHEDULER, and it DOES work correctly. +-- +-- ==== +-- +-- # **API CHANGE HISTORY** +-- +-- The underlying change log documents the API changes. Please read this carefully. The following notation is used: +-- +-- * **Added** parts are expressed in bold type face. +-- * _Removed_ parts are expressed in italic type face. +-- +-- Hereby the change log: +-- +-- 2017-01-17: Rename of class: **AI\_PATROL\_ZONE** is the new name for the old _AI\_PATROLZONE_. +-- +-- 2017-01-15: Complete revision. AI_PATROL_ZONE is the base class for other AI_PATROL like classes. +-- +-- 2016-09-01: Initial class and API. +-- +-- === +-- +-- # **AUTHORS and CONTRIBUTIONS** +-- +-- ### Contributions: +-- +-- * **[Dutch_Baron](https://forums.eagle.ru/member.php?u=112075)**: Working together with James has resulted in the creation of the AI_BALANCER class. James has shared his ideas on balancing AI with air units, and together we made a first design which you can use now :-) +-- * **[Pikey](https://forums.eagle.ru/member.php?u=62835)**: Testing and API concept review. +-- +-- ### Authors: +-- +-- * **FlightControl**: Design & Programming. +-- +-- @module AI_Patrol + +--- AI_PATROL_ZONE class +-- @type AI_PATROL_ZONE +-- @field Wrapper.Controllable#CONTROLLABLE AIControllable The @{Controllable} patrolling. +-- @field Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @field Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @field Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @field Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. +-- @field Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h. +-- @field Functional.Spawn#SPAWN CoordTest +-- @extends Core.Fsm#FSM_CONTROLLABLE +AI_PATROL_ZONE = { + ClassName = "AI_PATROL_ZONE", +} + +--- Creates a new AI_PATROL_ZONE object +-- @param #AI_PATROL_ZONE self +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. +-- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h. +-- @param Dcs.DCSTypes#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO +-- @return #AI_PATROL_ZONE self +-- @usage +-- -- Define a new AI_PATROL_ZONE Object. This PatrolArea will patrol an AIControllable within PatrolZone between 3000 and 6000 meters, with a variying speed between 600 and 900 km/h. +-- PatrolZone = ZONE:New( 'PatrolZone' ) +-- PatrolSpawn = SPAWN:New( 'Patrol Group' ) +-- PatrolArea = AI_PATROL_ZONE:New( PatrolZone, 3000, 6000, 600, 900 ) +function AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) + + -- Inherits from BASE + local self = BASE:Inherit( self, FSM_CONTROLLABLE:New() ) -- #AI_PATROL_ZONE + + + self.PatrolZone = PatrolZone + self.PatrolFloorAltitude = PatrolFloorAltitude + self.PatrolCeilingAltitude = PatrolCeilingAltitude + self.PatrolMinSpeed = PatrolMinSpeed + self.PatrolMaxSpeed = PatrolMaxSpeed + + -- defafult PatrolAltType to "RADIO" if not specified + self.PatrolAltType = PatrolAltType or "RADIO" + + self:SetDetectionInterval( 30 ) + + self.CheckStatus = true + + self:ManageFuel( .2, 60 ) + self:ManageDamage( 1 ) + + + self.DetectedUnits = {} -- This table contains the targets detected during patrol. + + self:SetStartState( "None" ) + + self:AddTransition( "*", "Stop", "Stopped" ) + +--- OnLeave Transition Handler for State Stopped. +-- @function [parent=#AI_PATROL_ZONE] OnLeaveStopped +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnEnter Transition Handler for State Stopped. +-- @function [parent=#AI_PATROL_ZONE] OnEnterStopped +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- OnBefore Transition Handler for Event Stop. +-- @function [parent=#AI_PATROL_ZONE] OnBeforeStop +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnAfter Transition Handler for Event Stop. +-- @function [parent=#AI_PATROL_ZONE] OnAfterStop +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- Synchronous Event Trigger for Event Stop. +-- @function [parent=#AI_PATROL_ZONE] Stop +-- @param #AI_PATROL_ZONE self + +--- Asynchronous Event Trigger for Event Stop. +-- @function [parent=#AI_PATROL_ZONE] __Stop +-- @param #AI_PATROL_ZONE self +-- @param #number Delay The delay in seconds. + + self:AddTransition( "None", "Start", "Patrolling" ) + +--- OnBefore Transition Handler for Event Start. +-- @function [parent=#AI_PATROL_ZONE] OnBeforeStart +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnAfter Transition Handler for Event Start. +-- @function [parent=#AI_PATROL_ZONE] OnAfterStart +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- Synchronous Event Trigger for Event Start. +-- @function [parent=#AI_PATROL_ZONE] Start +-- @param #AI_PATROL_ZONE self + +--- Asynchronous Event Trigger for Event Start. +-- @function [parent=#AI_PATROL_ZONE] __Start +-- @param #AI_PATROL_ZONE self +-- @param #number Delay The delay in seconds. + +--- OnLeave Transition Handler for State Patrolling. +-- @function [parent=#AI_PATROL_ZONE] OnLeavePatrolling +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnEnter Transition Handler for State Patrolling. +-- @function [parent=#AI_PATROL_ZONE] OnEnterPatrolling +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + + self:AddTransition( "Patrolling", "Route", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. + +--- OnBefore Transition Handler for Event Route. +-- @function [parent=#AI_PATROL_ZONE] OnBeforeRoute +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnAfter Transition Handler for Event Route. +-- @function [parent=#AI_PATROL_ZONE] OnAfterRoute +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- Synchronous Event Trigger for Event Route. +-- @function [parent=#AI_PATROL_ZONE] Route +-- @param #AI_PATROL_ZONE self + +--- Asynchronous Event Trigger for Event Route. +-- @function [parent=#AI_PATROL_ZONE] __Route +-- @param #AI_PATROL_ZONE self +-- @param #number Delay The delay in seconds. + + self:AddTransition( "*", "Status", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. + +--- OnBefore Transition Handler for Event Status. +-- @function [parent=#AI_PATROL_ZONE] OnBeforeStatus +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnAfter Transition Handler for Event Status. +-- @function [parent=#AI_PATROL_ZONE] OnAfterStatus +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- Synchronous Event Trigger for Event Status. +-- @function [parent=#AI_PATROL_ZONE] Status +-- @param #AI_PATROL_ZONE self + +--- Asynchronous Event Trigger for Event Status. +-- @function [parent=#AI_PATROL_ZONE] __Status +-- @param #AI_PATROL_ZONE self +-- @param #number Delay The delay in seconds. + + self:AddTransition( "*", "Detect", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. + +--- OnBefore Transition Handler for Event Detect. +-- @function [parent=#AI_PATROL_ZONE] OnBeforeDetect +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnAfter Transition Handler for Event Detect. +-- @function [parent=#AI_PATROL_ZONE] OnAfterDetect +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- Synchronous Event Trigger for Event Detect. +-- @function [parent=#AI_PATROL_ZONE] Detect +-- @param #AI_PATROL_ZONE self + +--- Asynchronous Event Trigger for Event Detect. +-- @function [parent=#AI_PATROL_ZONE] __Detect +-- @param #AI_PATROL_ZONE self +-- @param #number Delay The delay in seconds. + + self:AddTransition( "*", "Detected", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. + +--- OnBefore Transition Handler for Event Detected. +-- @function [parent=#AI_PATROL_ZONE] OnBeforeDetected +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnAfter Transition Handler for Event Detected. +-- @function [parent=#AI_PATROL_ZONE] OnAfterDetected +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- Synchronous Event Trigger for Event Detected. +-- @function [parent=#AI_PATROL_ZONE] Detected +-- @param #AI_PATROL_ZONE self + +--- Asynchronous Event Trigger for Event Detected. +-- @function [parent=#AI_PATROL_ZONE] __Detected +-- @param #AI_PATROL_ZONE self +-- @param #number Delay The delay in seconds. + + self:AddTransition( "*", "RTB", "Returning" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. + +--- OnBefore Transition Handler for Event RTB. +-- @function [parent=#AI_PATROL_ZONE] OnBeforeRTB +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnAfter Transition Handler for Event RTB. +-- @function [parent=#AI_PATROL_ZONE] OnAfterRTB +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- Synchronous Event Trigger for Event RTB. +-- @function [parent=#AI_PATROL_ZONE] RTB +-- @param #AI_PATROL_ZONE self + +--- Asynchronous Event Trigger for Event RTB. +-- @function [parent=#AI_PATROL_ZONE] __RTB +-- @param #AI_PATROL_ZONE self +-- @param #number Delay The delay in seconds. + +--- OnLeave Transition Handler for State Returning. +-- @function [parent=#AI_PATROL_ZONE] OnLeaveReturning +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnEnter Transition Handler for State Returning. +-- @function [parent=#AI_PATROL_ZONE] OnEnterReturning +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + + self:AddTransition( "*", "Reset", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. + + self:AddTransition( "*", "Eject", "*" ) + self:AddTransition( "*", "Crash", "Crashed" ) + self:AddTransition( "*", "PilotDead", "*" ) + + return self +end + + + + +--- Sets (modifies) the minimum and maximum speed of the patrol. +-- @param #AI_PATROL_ZONE self +-- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. +-- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h. +-- @return #AI_PATROL_ZONE self +function AI_PATROL_ZONE:SetSpeed( PatrolMinSpeed, PatrolMaxSpeed ) + self:F2( { PatrolMinSpeed, PatrolMaxSpeed } ) + + self.PatrolMinSpeed = PatrolMinSpeed + self.PatrolMaxSpeed = PatrolMaxSpeed +end + + + +--- Sets the floor and ceiling altitude of the patrol. +-- @param #AI_PATROL_ZONE self +-- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @return #AI_PATROL_ZONE self +function AI_PATROL_ZONE:SetAltitude( PatrolFloorAltitude, PatrolCeilingAltitude ) + self:F2( { PatrolFloorAltitude, PatrolCeilingAltitude } ) + + self.PatrolFloorAltitude = PatrolFloorAltitude + self.PatrolCeilingAltitude = PatrolCeilingAltitude +end + +-- * @{#AI_PATROL_ZONE.SetDetectionOn}(): Set the detection on. The AI will detect for targets. +-- * @{#AI_PATROL_ZONE.SetDetectionOff}(): Set the detection off, the AI will not detect for targets. The existing target list will NOT be erased. + +--- Set the detection on. The AI will detect for targets. +-- @param #AI_PATROL_ZONE self +-- @return #AI_PATROL_ZONE self +function AI_PATROL_ZONE:SetDetectionOn() + self:F2() + + self.DetectOn = true +end + +--- Set the detection off. The AI will NOT detect for targets. +-- However, the list of already detected targets will be kept and can be enquired! +-- @param #AI_PATROL_ZONE self +-- @return #AI_PATROL_ZONE self +function AI_PATROL_ZONE:SetDetectionOff() + self:F2() + + self.DetectOn = false +end + +--- Set the status checking off. +-- @param #AI_PATROL_ZONE self +-- @return #AI_PATROL_ZONE self +function AI_PATROL_ZONE:SetStatusOff() + self:F2() + + self.CheckStatus = false +end + +--- Activate the detection. The AI will detect for targets if the Detection is switched On. +-- @param #AI_PATROL_ZONE self +-- @return #AI_PATROL_ZONE self +function AI_PATROL_ZONE:SetDetectionActivated() + self:F2() + + self:ClearDetectedUnits() + self.DetectActivated = true + self:__Detect( -self.DetectInterval ) +end + +--- Deactivate the detection. The AI will NOT detect for targets. +-- @param #AI_PATROL_ZONE self +-- @return #AI_PATROL_ZONE self +function AI_PATROL_ZONE:SetDetectionDeactivated() + self:F2() + + self:ClearDetectedUnits() + self.DetectActivated = false +end + +--- Set the interval in seconds between each detection executed by the AI. +-- The list of already detected targets will be kept and updated. +-- Newly detected targets will be added, but already detected targets that were +-- not detected in this cycle, will NOT be removed! +-- The default interval is 30 seconds. +-- @param #AI_PATROL_ZONE self +-- @param #number Seconds The interval in seconds. +-- @return #AI_PATROL_ZONE self +function AI_PATROL_ZONE:SetDetectionInterval( Seconds ) + self:F2() + + if Seconds then + self.DetectInterval = Seconds + else + self.DetectInterval = 30 + end +end + +--- Set the detection zone where the AI is detecting targets. +-- @param #AI_PATROL_ZONE self +-- @param Core.Zone#ZONE DetectionZone The zone where to detect targets. +-- @return #AI_PATROL_ZONE self +function AI_PATROL_ZONE:SetDetectionZone( DetectionZone ) + self:F2() + + if DetectionZone then + self.DetectZone = DetectionZone + else + self.DetectZone = nil + end +end + +--- Gets a list of @{Unit#UNIT}s that were detected by the AI. +-- No filtering is applied, so, ANY detected UNIT can be in this list. +-- It is up to the mission designer to use the @{Unit} class and methods to filter the targets. +-- @param #AI_PATROL_ZONE self +-- @return #table The list of @{Unit#UNIT}s +function AI_PATROL_ZONE:GetDetectedUnits() + self:F2() + + return self.DetectedUnits +end + +--- Clears the list of @{Unit#UNIT}s that were detected by the AI. +-- @param #AI_PATROL_ZONE self +function AI_PATROL_ZONE:ClearDetectedUnits() + self:F2() + self.DetectedUnits = {} +end + +--- When the AI is out of fuel, it is required that a new AI is started, before the old AI can return to the home base. +-- Therefore, with a parameter and a calculation of the distance to the home base, the fuel treshold is calculated. +-- When the fuel treshold is reached, the AI will continue for a given time its patrol task in orbit, while a new AIControllable is targetted to the AI_PATROL_ZONE. +-- Once the time is finished, the old AI will return to the base. +-- @param #AI_PATROL_ZONE self +-- @param #number PatrolFuelTresholdPercentage The treshold in percentage (between 0 and 1) when the AIControllable is considered to get out of fuel. +-- @param #number PatrolOutOfFuelOrbitTime The amount of seconds the out of fuel AIControllable will orbit before returning to the base. +-- @return #AI_PATROL_ZONE self +function AI_PATROL_ZONE:ManageFuel( PatrolFuelTresholdPercentage, PatrolOutOfFuelOrbitTime ) + + self.PatrolManageFuel = true + self.PatrolFuelTresholdPercentage = PatrolFuelTresholdPercentage + self.PatrolOutOfFuelOrbitTime = PatrolOutOfFuelOrbitTime + + return self +end + +--- When the AI is damaged beyond a certain treshold, it is required that the AI returns to the home base. +-- However, damage cannot be foreseen early on. +-- Therefore, when the damage treshold is reached, +-- the AI will return immediately to the home base (RTB). +-- Note that for groups, the average damage of the complete group will be calculated. +-- So, in a group of 4 airplanes, 2 lost and 2 with damage 0.2, the damage treshold will be 0.25. +-- @param #AI_PATROL_ZONE self +-- @param #number PatrolDamageTreshold The treshold in percentage (between 0 and 1) when the AI is considered to be damaged. +-- @return #AI_PATROL_ZONE self +function AI_PATROL_ZONE:ManageDamage( PatrolDamageTreshold ) + + self.PatrolManageDamage = true + self.PatrolDamageTreshold = PatrolDamageTreshold + + return self +end + +--- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. +-- @param #AI_PATROL_ZONE self +-- @return #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_PATROL_ZONE:onafterStart( Controllable, From, Event, To ) + self:F2() + + self:__Route( 1 ) -- Route to the patrol point. The asynchronous trigger is important, because a spawned group and units takes at least one second to come live. + self:__Status( 60 ) -- Check status status every 30 seconds. + self:SetDetectionActivated() + + self:HandleEvent( EVENTS.PilotDead, self.OnPilotDead ) + self:HandleEvent( EVENTS.Crash, self.OnCrash ) + self:HandleEvent( EVENTS.Ejection, self.OnEjection ) + + Controllable:OptionROEHoldFire() + Controllable:OptionROTVertical() + + self.Controllable:OnReSpawn( + function( PatrolGroup ) + self:E( "ReSpawn" ) + self:__Reset( 1 ) + self:__Route( 5 ) + end + ) + + self:SetDetectionOn() + +end + + +--- @param #AI_PATROL_ZONE self +--- @param Wrapper.Controllable#CONTROLLABLE Controllable +function AI_PATROL_ZONE:onbeforeDetect( Controllable, From, Event, To ) + + return self.DetectOn and self.DetectActivated +end + +--- @param #AI_PATROL_ZONE self +--- @param Wrapper.Controllable#CONTROLLABLE Controllable +function AI_PATROL_ZONE:onafterDetect( Controllable, From, Event, To ) + + local Detected = false + + local DetectedTargets = Controllable:GetDetectedTargets() + for TargetID, Target in pairs( DetectedTargets or {} ) do + local TargetObject = Target.object + + if TargetObject and TargetObject:isExist() and TargetObject.id_ < 50000000 then + + local TargetUnit = UNIT:Find( TargetObject ) + local TargetUnitName = TargetUnit:GetName() + + if self.DetectionZone then + if TargetUnit:IsInZone( self.DetectionZone ) then + self:T( {"Detected ", TargetUnit } ) + if self.DetectedUnits[TargetUnit] == nil then + self.DetectedUnits[TargetUnit] = true + end + Detected = true + end + else + if self.DetectedUnits[TargetUnit] == nil then + self.DetectedUnits[TargetUnit] = true + end + Detected = true + end + end + end + + self:__Detect( -self.DetectInterval ) + + if Detected == true then + self:__Detected( 1.5 ) + end + +end + +--- @param Wrapper.Controllable#CONTROLLABLE AIControllable +-- This statis method is called from the route path within the last task at the last waaypoint of the Controllable. +-- Note that this method is required, as triggers the next route when patrolling for the Controllable. +function AI_PATROL_ZONE:_NewPatrolRoute( AIControllable ) + + local PatrolZone = AIControllable:GetState( AIControllable, "PatrolZone" ) -- PatrolCore.Zone#AI_PATROL_ZONE + PatrolZone:__Route( 1 ) +end + + +--- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_PATROL_ZONE:onafterRoute( Controllable, From, Event, To ) + + self:F2() + + -- When RTB, don't allow anymore the routing. + if From == "RTB" then + return + end + + + if self.Controllable:IsAlive() then + -- Determine if the AIControllable is within the PatrolZone. + -- If not, make a waypoint within the to that the AIControllable will fly at maximum speed to that point. + + local PatrolRoute = {} + + -- Calculate the current route point of the controllable as the start point of the route. + -- However, when the controllable is not in the air, + -- the controllable current waypoint is probably the airbase... + -- Thus, if we would take the current waypoint as the startpoint, upon take-off, the controllable flies + -- immediately back to the airbase, and this is not correct. + -- Therefore, when on a runway, get as the current route point a random point within the PatrolZone. + -- This will make the plane fly immediately to the patrol zone. + + if self.Controllable:InAir() == false then + self:E( "Not in the air, finding route path within PatrolZone" ) + local CurrentVec2 = self.Controllable:GetVec2() + --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). + local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() + local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) + local ToPatrolZoneSpeed = self.PatrolMaxSpeed + local CurrentRoutePoint = CurrentPointVec3:RoutePointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TakeOffParking, + POINT_VEC3.RoutePointAction.FromParkingArea, + ToPatrolZoneSpeed, + true + ) + PatrolRoute[#PatrolRoute+1] = CurrentRoutePoint + else + self:E( "In the air, finding route path within PatrolZone" ) + local CurrentVec2 = self.Controllable:GetVec2() + --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). + local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() + local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) + local ToPatrolZoneSpeed = self.PatrolMaxSpeed + local CurrentRoutePoint = CurrentPointVec3:RoutePointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToPatrolZoneSpeed, + true + ) + PatrolRoute[#PatrolRoute+1] = CurrentRoutePoint + end + + + --- Define a random point in the @{Zone}. The AI will fly to that point within the zone. + + --- Find a random 2D point in PatrolZone. + local ToTargetVec2 = self.PatrolZone:GetRandomVec2() + self:T2( ToTargetVec2 ) + + --- Define Speed and Altitude. + local ToTargetAltitude = math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ) + local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) + self:T2( { self.PatrolMinSpeed, self.PatrolMaxSpeed, ToTargetSpeed } ) + + --- Obtain a 3D @{Point} from the 2D point + altitude. + local ToTargetPointVec3 = POINT_VEC3:New( ToTargetVec2.x, ToTargetAltitude, ToTargetVec2.y ) + + --- Create a route point of type air. + local ToTargetRoutePoint = ToTargetPointVec3:RoutePointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true + ) + + --self.CoordTest:SpawnFromVec3( ToTargetPointVec3:GetVec3() ) + + --ToTargetPointVec3:SmokeRed() + + PatrolRoute[#PatrolRoute+1] = ToTargetRoutePoint + + --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... + self.Controllable:WayPointInitialize( PatrolRoute ) + + --- Do a trick, link the NewPatrolRoute function of the PATROLGROUP object to the AIControllable in a temporary variable ... + self.Controllable:SetState( self.Controllable, "PatrolZone", self ) + self.Controllable:WayPointFunction( #PatrolRoute, 1, "AI_PATROL_ZONE:_NewPatrolRoute" ) + + --- NOW ROUTE THE GROUP! + self.Controllable:WayPointExecute( 1, 2 ) + end + +end + +--- @param #AI_PATROL_ZONE self +function AI_PATROL_ZONE:onbeforeStatus() + + return self.CheckStatus +end + +--- @param #AI_PATROL_ZONE self +function AI_PATROL_ZONE:onafterStatus() + self:F2() + + if self.Controllable and self.Controllable:IsAlive() then + + local RTB = false + + local Fuel = self.Controllable:GetUnit(1):GetFuel() + if Fuel < self.PatrolFuelTresholdPercentage then + self:E( self.Controllable:GetName() .. " is out of fuel:" .. Fuel .. ", RTB!" ) + local OldAIControllable = self.Controllable + local AIControllableTemplate = self.Controllable:GetTemplate() + + local OrbitTask = OldAIControllable:TaskOrbitCircle( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ), self.PatrolMinSpeed ) + local TimedOrbitTask = OldAIControllable:TaskControlled( OrbitTask, OldAIControllable:TaskCondition(nil,nil,nil,nil,self.PatrolOutOfFuelOrbitTime,nil ) ) + OldAIControllable:SetTask( TimedOrbitTask, 10 ) + + RTB = true + else + end + + -- TODO: Check GROUP damage function. + local Damage = self.Controllable:GetLife() + if Damage <= self.PatrolDamageTreshold then + self:E( self.Controllable:GetName() .. " is damaged:" .. Damage .. ", RTB!" ) + RTB = true + end + + if RTB == true then + self:RTB() + else + self:__Status( 60 ) -- Execute the Patrol event after 30 seconds. + end + end +end + +--- @param #AI_PATROL_ZONE self +function AI_PATROL_ZONE:onafterRTB() + self:F2() + + if self.Controllable and self.Controllable:IsAlive() then + + self:SetDetectionOff() + self.CheckStatus = false + + local PatrolRoute = {} + + --- Calculate the current route point. + local CurrentVec2 = self.Controllable:GetVec2() + + --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). + local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() + local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) + local ToPatrolZoneSpeed = self.PatrolMaxSpeed + local CurrentRoutePoint = CurrentPointVec3:RoutePointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToPatrolZoneSpeed, + true + ) + + PatrolRoute[#PatrolRoute+1] = CurrentRoutePoint + + --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... + self.Controllable:WayPointInitialize( PatrolRoute ) + + --- NOW ROUTE THE GROUP! + self.Controllable:WayPointExecute( 1, 1 ) + + end + +end + +--- @param #AI_PATROL_ZONE self +function AI_PATROL_ZONE:onafterDead() + self:SetDetectionOff() + self:SetStatusOff() +end + +--- @param #AI_PATROL_ZONE self +-- @param Core.Event#EVENTDATA EventData +function AI_PATROL_ZONE:OnCrash( EventData ) + + if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then + self:E( self.Controllable:GetUnits() ) + if #self.Controllable:GetUnits() == 1 then + self:__Crash( 1, EventData ) + end + end +end + +--- @param #AI_PATROL_ZONE self +-- @param Core.Event#EVENTDATA EventData +function AI_PATROL_ZONE:OnEjection( EventData ) + + if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then + self:__Eject( 1, EventData ) + end +end + +--- @param #AI_PATROL_ZONE self +-- @param Core.Event#EVENTDATA EventData +function AI_PATROL_ZONE:OnPilotDead( EventData ) + + if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then + self:__PilotDead( 1, EventData ) + end +end +--- Single-Player:**Yes** / Multi-Player:**Yes** / AI:**Yes** / Human:**No** / Types:**Air** -- +-- **Provide Close Air Support to friendly ground troops.** +-- +-- ![Banner Image](..\Presentations\AI_CAS\Dia1.JPG) +-- +-- === +-- +-- # 1) @{#AI_CAS_ZONE} class, extends @{AI_Patrol#AI_PATROL_ZONE} +-- +-- @{#AI_CAS_ZONE} derives from the @{AI_Patrol#AI_PATROL_ZONE}, inheriting its methods and behaviour. +-- +-- The @{#AI_CAS_ZONE} class implements the core functions to provide Close Air Support in an Engage @{Zone} by an AIR @{Controllable} or @{Group}. +-- The AI_CAS_ZONE runs a process. It holds an AI in a Patrol Zone and when the AI is commanded to engage, it will fly to an Engage Zone. +-- +-- ![HoldAndEngage](..\Presentations\AI_CAS\Dia3.JPG) +-- +-- The AI_CAS_ZONE is assigned a @{Group} and this must be done before the AI_CAS_ZONE process can be started through the **Start** event. +-- +-- ![Start Event](..\Presentations\AI_CAS\Dia4.JPG) +-- +-- Upon started, The AI will **Route** itself towards the random 3D point within a patrol zone, +-- using a random speed within the given altitude and speed limits. +-- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. +-- This cycle will continue until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. +-- +-- ![Route Event](..\Presentations\AI_CAS\Dia5.JPG) +-- +-- When the AI is commanded to provide Close Air Support (through the event **Engage**), the AI will fly towards the Engage Zone. +-- Any target that is detected in the Engage Zone will be reported and will be destroyed by the AI. +-- +-- ![Engage Event](..\Presentations\AI_CAS\Dia6.JPG) +-- +-- The AI will detect the targets and will only destroy the targets within the Engage Zone. +-- +-- ![Engage Event](..\Presentations\AI_CAS\Dia7.JPG) +-- +-- Every target that is destroyed, is reported< by the AI. +-- +-- ![Engage Event](..\Presentations\AI_CAS\Dia8.JPG) +-- +-- Note that the AI does not know when the Engage Zone is cleared, and therefore will keep circling in the zone. +-- +-- ![Engage Event](..\Presentations\AI_CAS\Dia9.JPG) +-- +-- Until it is notified through the event **Accomplish**, which is to be triggered by an observing party: +-- +-- * a FAC +-- * a timed event +-- * a menu option selected by a human +-- * a condition +-- * others ... +-- +-- ![Engage Event](..\Presentations\AI_CAS\Dia10.JPG) +-- +-- When the AI has accomplished the CAS, it will fly back to the Patrol Zone. +-- +-- ![Engage Event](..\Presentations\AI_CAS\Dia11.JPG) +-- +-- It will keep patrolling there, until it is notified to RTB or move to another CAS Zone. +-- It can be notified to go RTB through the **RTB** event. +-- +-- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. +-- +-- ![Engage Event](..\Presentations\AI_CAS\Dia12.JPG) +-- +-- # 1.1) AI_CAS_ZONE constructor +-- +-- * @{#AI_CAS_ZONE.New}(): Creates a new AI_CAS_ZONE object. +-- +-- ## 1.2) AI_CAS_ZONE is a FSM +-- +-- ![Process](..\Presentations\AI_CAS\Dia2.JPG) +-- +-- ### 1.2.1) AI_CAS_ZONE States +-- +-- * **None** ( Group ): The process is not started yet. +-- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. +-- * **Engaging** ( Group ): The AI is engaging the targets in the Engage Zone, executing CAS. +-- * **Returning** ( Group ): The AI is returning to Base.. +-- +-- ### 1.2.2) AI_CAS_ZONE Events +-- +-- * **Start** ( Group ): Start the process. +-- * **Route** ( Group ): Route the AI to a new random 3D point within the Patrol Zone. +-- * **Engage** ( Group ): Engage the AI to provide CAS in the Engage Zone, destroying any target it finds. +-- * **RTB** ( Group ): Route the AI to the home base. +-- * **Detect** ( Group ): The AI is detecting targets. +-- * **Detected** ( Group ): The AI has detected new targets. +-- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. +-- +-- ==== +-- +-- # **API CHANGE HISTORY** +-- +-- The underlying change log documents the API changes. Please read this carefully. The following notation is used: +-- +-- * **Added** parts are expressed in bold type face. +-- * _Removed_ parts are expressed in italic type face. +-- +-- Hereby the change log: +-- +-- 2017-01-15: Initial class and API. +-- +-- === +-- +-- # **AUTHORS and CONTRIBUTIONS** +-- +-- ### Contributions: +-- +-- * **[Quax](https://forums.eagle.ru/member.php?u=90530)**: Concept, Advice & Testing. +-- * **[Pikey](https://forums.eagle.ru/member.php?u=62835)**: Concept, Advice & Testing. +-- * **[Gunterlund](http://forums.eagle.ru:8080/member.php?u=75036)**: Test case revision. +-- +-- ### Authors: +-- +-- * **FlightControl**: Concept, Design & Programming. +-- +-- @module AI_Cas + + +--- AI_CAS_ZONE class +-- @type AI_CAS_ZONE +-- @field Wrapper.Controllable#CONTROLLABLE AIControllable The @{Controllable} patrolling. +-- @field Core.Zone#ZONE_BASE TargetZone The @{Zone} where the patrol needs to be executed. +-- @extends AI.AI_Patrol#AI_PATROL_ZONE +AI_CAS_ZONE = { + ClassName = "AI_CAS_ZONE", +} + + + +--- Creates a new AI_CAS_ZONE object +-- @param #AI_CAS_ZONE self +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. +-- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h. +-- @param Core.Zone#ZONE_BASE EngageZone The zone where the engage will happen. +-- @param Dcs.DCSTypes#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO +-- @return #AI_CAS_ZONE self +function AI_CAS_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageZone, PatrolAltType ) + + -- Inherits from BASE + local self = BASE:Inherit( self, AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_CAS_ZONE + + self.EngageZone = EngageZone + self.Accomplished = false + + self:SetDetectionZone( self.EngageZone ) + + self:AddTransition( { "Patrolling", "Engaging" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. + + --- OnBefore Transition Handler for Event Engage. + -- @function [parent=#AI_CAS_ZONE] OnBeforeEngage + -- @param #AI_CAS_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone. + -- @param Dcs.DCSTypes#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. + -- @param Dcs.DCSTypes#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. + -- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. + -- @param Dcs.DCSTypes#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. + + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Engage. + -- @function [parent=#AI_CAS_ZONE] OnAfterEngage + -- @param #AI_CAS_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone. + -- @param Dcs.DCSTypes#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. + -- @param Dcs.DCSTypes#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. + -- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. + -- @param Dcs.DCSTypes#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. + + --- Synchronous Event Trigger for Event Engage. + -- @function [parent=#AI_CAS_ZONE] Engage + -- @param #AI_CAS_ZONE self + + --- Asynchronous Event Trigger for Event Engage. + -- @function [parent=#AI_CAS_ZONE] __Engage + -- @param #AI_CAS_ZONE self + -- @param #number Delay The delay in seconds. + +--- OnLeave Transition Handler for State Engaging. +-- @function [parent=#AI_CAS_ZONE] OnLeaveEngaging +-- @param #AI_CAS_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnEnter Transition Handler for State Engaging. +-- @function [parent=#AI_CAS_ZONE] OnEnterEngaging +-- @param #AI_CAS_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + + self:AddTransition( "Engaging", "Target", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. + + self:AddTransition( "Engaging", "Fired", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. + + --- OnBefore Transition Handler for Event Fired. + -- @function [parent=#AI_CAS_ZONE] OnBeforeFired + -- @param #AI_CAS_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Fired. + -- @function [parent=#AI_CAS_ZONE] OnAfterFired + -- @param #AI_CAS_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Fired. + -- @function [parent=#AI_CAS_ZONE] Fired + -- @param #AI_CAS_ZONE self + + --- Asynchronous Event Trigger for Event Fired. + -- @function [parent=#AI_CAS_ZONE] __Fired + -- @param #AI_CAS_ZONE self + -- @param #number Delay The delay in seconds. + + self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. + + --- OnBefore Transition Handler for Event Destroy. + -- @function [parent=#AI_CAS_ZONE] OnBeforeDestroy + -- @param #AI_CAS_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Destroy. + -- @function [parent=#AI_CAS_ZONE] OnAfterDestroy + -- @param #AI_CAS_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Destroy. + -- @function [parent=#AI_CAS_ZONE] Destroy + -- @param #AI_CAS_ZONE self + + --- Asynchronous Event Trigger for Event Destroy. + -- @function [parent=#AI_CAS_ZONE] __Destroy + -- @param #AI_CAS_ZONE self + -- @param #number Delay The delay in seconds. + + + self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. + + --- OnBefore Transition Handler for Event Abort. + -- @function [parent=#AI_CAS_ZONE] OnBeforeAbort + -- @param #AI_CAS_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Abort. + -- @function [parent=#AI_CAS_ZONE] OnAfterAbort + -- @param #AI_CAS_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Abort. + -- @function [parent=#AI_CAS_ZONE] Abort + -- @param #AI_CAS_ZONE self + + --- Asynchronous Event Trigger for Event Abort. + -- @function [parent=#AI_CAS_ZONE] __Abort + -- @param #AI_CAS_ZONE self + -- @param #number Delay The delay in seconds. + + self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. + + --- OnBefore Transition Handler for Event Accomplish. + -- @function [parent=#AI_CAS_ZONE] OnBeforeAccomplish + -- @param #AI_CAS_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Accomplish. + -- @function [parent=#AI_CAS_ZONE] OnAfterAccomplish + -- @param #AI_CAS_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Accomplish. + -- @function [parent=#AI_CAS_ZONE] Accomplish + -- @param #AI_CAS_ZONE self + + --- Asynchronous Event Trigger for Event Accomplish. + -- @function [parent=#AI_CAS_ZONE] __Accomplish + -- @param #AI_CAS_ZONE self + -- @param #number Delay The delay in seconds. + + return self +end + + +--- Set the Engage Zone where the AI is performing CAS. Note that if the EngageZone is changed, the AI needs to re-detect targets. +-- @param #AI_CAS_ZONE self +-- @param Core.Zone#ZONE EngageZone The zone where the AI is performing CAS. +-- @return #AI_CAS_ZONE self +function AI_CAS_ZONE:SetEngageZone( EngageZone ) + self:F2() + + if EngageZone then + self.EngageZone = EngageZone + else + self.EngageZone = nil + end +end + + + +--- onafter State Transition for Event Start. +-- @param #AI_CAS_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_CAS_ZONE:onafterStart( Controllable, From, Event, To ) + + -- Call the parent Start event handler + self:GetParent(self).onafterStart( self, Controllable, From, Event, To ) + self:HandleEvent( EVENTS.Dead, self.OnDead ) + + self:SetDetectionDeactivated() -- When not engaging, set the detection off. +end + +--- @param Wrapper.Controllable#CONTROLLABLE AIControllable +function _NewEngageRoute( AIControllable ) + + AIControllable:T( "NewEngageRoute" ) + local EngageZone = AIControllable:GetState( AIControllable, "EngageZone" ) -- AI.AI_Cas#AI_CAS_ZONE + EngageZone:__Engage( 1 ) +end + +--- @param #AI_CAS_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_CAS_ZONE:onbeforeEngage( Controllable, From, Event, To ) + + if self.Accomplished == true then + return false + end +end + +--- @param #AI_CAS_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_CAS_ZONE:onafterTarget( Controllable, From, Event, To ) + self:E("onafterTarget") + + if Controllable:IsAlive() then + + local AttackTasks = {} + + for DetectedUnit, Detected in pairs( self.DetectedUnits ) do + local DetectedUnit = DetectedUnit -- Wrapper.Unit#UNIT + if DetectedUnit:IsAlive() then + if DetectedUnit:IsInZone( self.EngageZone ) then + if Detected == true then + self:E( {"Target: ", DetectedUnit } ) + self.DetectedUnits[DetectedUnit] = false + local AttackTask = Controllable:EnRouteTaskEngageUnit( DetectedUnit, 1, true, self.EngageWeaponExpend, self.EngageAttackQty, self.EngageDirection, self.EngageAltitude, nil ) + self.Controllable:PushTask( AttackTask, 1 ) + end + end + else + self.DetectedUnits[DetectedUnit] = nil + end + end + + self:__Target( -10 ) + + end +end + + + +--- @param #AI_CAS_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone. +-- @param Dcs.DCSTypes#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. +-- @param Dcs.DCSTypes#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. +-- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. +-- @param Dcs.DCSTypes#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. +function AI_CAS_ZONE:onafterEngage( Controllable, From, Event, To, EngageSpeed, EngageAltitude, EngageWeaponExpend, EngageAttackQty, EngageDirection ) + self:E("onafterEngage") + + self.EngageSpeed = EngageSpeed or 400 + self.EngageAltitude = EngageAltitude or 2000 + self.EngageWeaponExpend = EngageWeaponExpend + self.EngageAttackQty = EngageAttackQty + self.EngageDirection = EngageDirection + + if Controllable:IsAlive() then + + + local EngageRoute = {} + + --- Calculate the current route point. + local CurrentVec2 = self.Controllable:GetVec2() + + --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). + local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() + local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) + local ToEngageZoneSpeed = self.PatrolMaxSpeed + local CurrentRoutePoint = CurrentPointVec3:RoutePointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + self.EngageSpeed, + true + ) + + EngageRoute[#EngageRoute+1] = CurrentRoutePoint + + + if self.Controllable:IsNotInZone( self.EngageZone ) then + + -- Find a random 2D point in EngageZone. + local ToEngageZoneVec2 = self.EngageZone:GetRandomVec2() + self:T2( ToEngageZoneVec2 ) + + -- Obtain a 3D @{Point} from the 2D point + altitude. + local ToEngageZonePointVec3 = POINT_VEC3:New( ToEngageZoneVec2.x, self.EngageAltitude, ToEngageZoneVec2.y ) + + -- Create a route point of type air. + local ToEngageZoneRoutePoint = ToEngageZonePointVec3:RoutePointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + self.EngageSpeed, + true + ) + + EngageRoute[#EngageRoute+1] = ToEngageZoneRoutePoint + + end + + --- Define a random point in the @{Zone}. The AI will fly to that point within the zone. + + --- Find a random 2D point in EngageZone. + local ToTargetVec2 = self.EngageZone:GetRandomVec2() + self:T2( ToTargetVec2 ) + + --- Obtain a 3D @{Point} from the 2D point + altitude. + local ToTargetPointVec3 = POINT_VEC3:New( ToTargetVec2.x, self.EngageAltitude, ToTargetVec2.y ) + + --- Create a route point of type air. + local ToTargetRoutePoint = ToTargetPointVec3:RoutePointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + self.EngageSpeed, + true + ) + + --ToTargetPointVec3:SmokeBlue() + + EngageRoute[#EngageRoute+1] = ToTargetRoutePoint + + + Controllable:OptionROEOpenFire() + Controllable:OptionROTVertical() + +-- local AttackTasks = {} +-- +-- for DetectedUnitID, DetectedUnit in pairs( self.DetectedUnits ) do +-- local DetectedUnit = DetectedUnit -- Wrapper.Unit#UNIT +-- self:T( DetectedUnit ) +-- if DetectedUnit:IsAlive() then +-- if DetectedUnit:IsInZone( self.EngageZone ) then +-- self:E( {"Engaging ", DetectedUnit } ) +-- AttackTasks[#AttackTasks+1] = Controllable:TaskAttackUnit( DetectedUnit ) +-- end +-- else +-- self.DetectedUnits[DetectedUnit] = nil +-- end +-- end +-- +-- EngageRoute[1].task = Controllable:TaskCombo( AttackTasks ) + + --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... + self.Controllable:WayPointInitialize( EngageRoute ) + + --- Do a trick, link the NewEngageRoute function of the object to the AIControllable in a temporary variable ... + self.Controllable:SetState( self.Controllable, "EngageZone", self ) + + self.Controllable:WayPointFunction( #EngageRoute, 1, "_NewEngageRoute" ) + + --- NOW ROUTE THE GROUP! + self.Controllable:WayPointExecute( 1 ) + + self:SetDetectionInterval( 10 ) + self:SetDetectionActivated() + self:__Target( -10 ) -- Start Targetting + end +end + +--- @param #AI_CAS_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @param Core.Event#EVENTDATA EventData +function AI_CAS_ZONE:onafterDestroy( Controllable, From, Event, To, EventData ) + + if EventData.IniUnit then + self.DetectedUnits[EventData.IniUnit] = nil + end + + Controllable:MessageToAll( "Destroyed a target", 15 , "Destroyed!" ) +end + +--- @param #AI_CAS_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_CAS_ZONE:onafterAccomplish( Controllable, From, Event, To ) + self.Accomplished = true + self:SetDetectionDeactivated() +end + +--- @param #AI_CAS_ZONE self +-- @param Core.Event#EVENTDATA EventData +function AI_CAS_ZONE:OnDead( EventData ) + self:T( { "EventDead", EventData } ) + + if EventData.IniDCSUnit then + self:__Destroy( 1, EventData ) + end +end + + +--- Single-Player:**Yes** / Multi-Player:**Yes** / AI:**Yes** / Human:**No** / Types:**Air** -- **Execute Combat Air Patrol (CAP).** +-- +-- ![Banner Image](..\Presentations\AI_CAP\Dia1.JPG) +-- +-- === +-- +-- # 1) @{#AI_CAP_ZONE} class, extends @{AI_CAP#AI_PATROL_ZONE} +-- +-- The @{#AI_CAP_ZONE} class implements the core functions to patrol a @{Zone} by an AI @{Controllable} or @{Group} +-- and automatically engage any airborne enemies that are within a certain range or within a certain zone. +-- +-- ![Process](..\Presentations\AI_CAP\Dia3.JPG) +-- +-- The AI_CAP_ZONE is assigned a @{Group} and this must be done before the AI_CAP_ZONE process can be started using the **Start** event. +-- +-- ![Process](..\Presentations\AI_CAP\Dia4.JPG) +-- +-- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits. +-- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. +-- +-- ![Process](..\Presentations\AI_CAP\Dia5.JPG) +-- +-- This cycle will continue. +-- +-- ![Process](..\Presentations\AI_CAP\Dia6.JPG) +-- +-- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event. +-- +-- ![Process](..\Presentations\AI_CAP\Dia9.JPG) +-- +-- When enemies are detected, the AI will automatically engage the enemy. +-- +-- ![Process](..\Presentations\AI_CAP\Dia10.JPG) +-- +-- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. +-- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. +-- +-- ![Process](..\Presentations\AI_CAP\Dia13.JPG) +-- +-- ## 1.1) AI_CAP_ZONE constructor +-- +-- * @{#AI_CAP_ZONE.New}(): Creates a new AI_CAP_ZONE object. +-- +-- ## 1.2) AI_CAP_ZONE is a FSM +-- +-- ![Process](..\Presentations\AI_CAP\Dia2.JPG) +-- +-- ### 1.2.1) AI_CAP_ZONE States +-- +-- * **None** ( Group ): The process is not started yet. +-- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. +-- * **Engaging** ( Group ): The AI is engaging the bogeys. +-- * **Returning** ( Group ): The AI is returning to Base.. +-- +-- ### 1.2.2) AI_CAP_ZONE Events +-- +-- * **Start** ( Group ): Start the process. +-- * **Route** ( Group ): Route the AI to a new random 3D point within the Patrol Zone. +-- * **Engage** ( Group ): Let the AI engage the bogeys. +-- * **RTB** ( Group ): Route the AI to the home base. +-- * **Detect** ( Group ): The AI is detecting targets. +-- * **Detected** ( Group ): The AI has detected new targets. +-- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. +-- +-- ## 1.3) Set the Range of Engagement +-- +-- ![Range](..\Presentations\AI_CAP\Dia11.JPG) +-- +-- An optional range can be set in meters, +-- that will define when the AI will engage with the detected airborne enemy targets. +-- The range can be beyond or smaller than the range of the Patrol Zone. +-- The range is applied at the position of the AI. +-- Use the method @{AI_CAP#AI_CAP_ZONE.SetEngageRange}() to define that range. +-- +-- ## 1.4) Set the Zone of Engagement +-- +-- ![Zone](..\Presentations\AI_CAP\Dia12.JPG) +-- +-- An optional @{Zone} can be set, +-- that will define when the AI will engage with the detected airborne enemy targets. +-- Use the method @{AI_Cap#AI_CAP_ZONE.SetEngageZone}() to define that Zone. +-- +-- ==== +-- +-- # **API CHANGE HISTORY** +-- +-- The underlying change log documents the API changes. Please read this carefully. The following notation is used: +-- +-- * **Added** parts are expressed in bold type face. +-- * _Removed_ parts are expressed in italic type face. +-- +-- Hereby the change log: +-- +-- 2017-01-15: Initial class and API. +-- +-- === +-- +-- # **AUTHORS and CONTRIBUTIONS** +-- +-- ### Contributions: +-- +-- * **[Quax](https://forums.eagle.ru/member.php?u=90530)**: Concept, Advice & Testing. +-- * **[Pikey](https://forums.eagle.ru/member.php?u=62835)**: Concept, Advice & Testing. +-- * **[Gunterlund](http://forums.eagle.ru:8080/member.php?u=75036)**: Test case revision. +-- * **[Whisper](http://forums.eagle.ru/member.php?u=3829): Testing. +-- * **[Delta99](https://forums.eagle.ru/member.php?u=125166): Testing. +-- +-- ### Authors: +-- +-- * **FlightControl**: Concept, Design & Programming. +-- +-- @module AI_Cap + + +--- AI_CAP_ZONE class +-- @type AI_CAP_ZONE +-- @field Wrapper.Controllable#CONTROLLABLE AIControllable The @{Controllable} patrolling. +-- @field Core.Zone#ZONE_BASE TargetZone The @{Zone} where the patrol needs to be executed. +-- @extends AI.AI_Patrol#AI_PATROL_ZONE +AI_CAP_ZONE = { + ClassName = "AI_CAP_ZONE", +} + + + +--- Creates a new AI_CAP_ZONE object +-- @param #AI_CAP_ZONE self +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. +-- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h. +-- @param Dcs.DCSTypes#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO +-- @return #AI_CAP_ZONE self +function AI_CAP_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) + + -- Inherits from BASE + local self = BASE:Inherit( self, AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_CAP_ZONE + + self.Accomplished = false + self.Engaging = false + + self:AddTransition( { "Patrolling", "Engaging" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAP_ZONE. + + --- OnBefore Transition Handler for Event Engage. + -- @function [parent=#AI_CAP_ZONE] OnBeforeEngage + -- @param #AI_CAP_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Engage. + -- @function [parent=#AI_CAP_ZONE] OnAfterEngage + -- @param #AI_CAP_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Engage. + -- @function [parent=#AI_CAP_ZONE] Engage + -- @param #AI_CAP_ZONE self + + --- Asynchronous Event Trigger for Event Engage. + -- @function [parent=#AI_CAP_ZONE] __Engage + -- @param #AI_CAP_ZONE self + -- @param #number Delay The delay in seconds. + +--- OnLeave Transition Handler for State Engaging. +-- @function [parent=#AI_CAP_ZONE] OnLeaveEngaging +-- @param #AI_CAP_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnEnter Transition Handler for State Engaging. +-- @function [parent=#AI_CAP_ZONE] OnEnterEngaging +-- @param #AI_CAP_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + + self:AddTransition( "Engaging", "Fired", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAP_ZONE. + + --- OnBefore Transition Handler for Event Fired. + -- @function [parent=#AI_CAP_ZONE] OnBeforeFired + -- @param #AI_CAP_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Fired. + -- @function [parent=#AI_CAP_ZONE] OnAfterFired + -- @param #AI_CAP_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Fired. + -- @function [parent=#AI_CAP_ZONE] Fired + -- @param #AI_CAP_ZONE self + + --- Asynchronous Event Trigger for Event Fired. + -- @function [parent=#AI_CAP_ZONE] __Fired + -- @param #AI_CAP_ZONE self + -- @param #number Delay The delay in seconds. + + self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_CAP_ZONE. + + --- OnBefore Transition Handler for Event Destroy. + -- @function [parent=#AI_CAP_ZONE] OnBeforeDestroy + -- @param #AI_CAP_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Destroy. + -- @function [parent=#AI_CAP_ZONE] OnAfterDestroy + -- @param #AI_CAP_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Destroy. + -- @function [parent=#AI_CAP_ZONE] Destroy + -- @param #AI_CAP_ZONE self + + --- Asynchronous Event Trigger for Event Destroy. + -- @function [parent=#AI_CAP_ZONE] __Destroy + -- @param #AI_CAP_ZONE self + -- @param #number Delay The delay in seconds. + + + self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_CAP_ZONE. + + --- OnBefore Transition Handler for Event Abort. + -- @function [parent=#AI_CAP_ZONE] OnBeforeAbort + -- @param #AI_CAP_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Abort. + -- @function [parent=#AI_CAP_ZONE] OnAfterAbort + -- @param #AI_CAP_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Abort. + -- @function [parent=#AI_CAP_ZONE] Abort + -- @param #AI_CAP_ZONE self + + --- Asynchronous Event Trigger for Event Abort. + -- @function [parent=#AI_CAP_ZONE] __Abort + -- @param #AI_CAP_ZONE self + -- @param #number Delay The delay in seconds. + + self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_CAP_ZONE. + + --- OnBefore Transition Handler for Event Accomplish. + -- @function [parent=#AI_CAP_ZONE] OnBeforeAccomplish + -- @param #AI_CAP_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Accomplish. + -- @function [parent=#AI_CAP_ZONE] OnAfterAccomplish + -- @param #AI_CAP_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Accomplish. + -- @function [parent=#AI_CAP_ZONE] Accomplish + -- @param #AI_CAP_ZONE self + + --- Asynchronous Event Trigger for Event Accomplish. + -- @function [parent=#AI_CAP_ZONE] __Accomplish + -- @param #AI_CAP_ZONE self + -- @param #number Delay The delay in seconds. + + return self +end + + +--- Set the Engage Zone which defines where the AI will engage bogies. +-- @param #AI_CAP_ZONE self +-- @param Core.Zone#ZONE EngageZone The zone where the AI is performing CAP. +-- @return #AI_CAP_ZONE self +function AI_CAP_ZONE:SetEngageZone( EngageZone ) + self:F2() + + if EngageZone then + self.EngageZone = EngageZone + else + self.EngageZone = nil + end +end + +--- Set the Engage Range when the AI will engage with airborne enemies. +-- @param #AI_CAP_ZONE self +-- @param #number EngageRange The Engage Range. +-- @return #AI_CAP_ZONE self +function AI_CAP_ZONE:SetEngageRange( EngageRange ) + self:F2() + + if EngageRange then + self.EngageRange = EngageRange + else + self.EngageRange = nil + end +end + +--- onafter State Transition for Event Start. +-- @param #AI_CAP_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_CAP_ZONE:onafterStart( Controllable, From, Event, To ) + + -- Call the parent Start event handler + self:GetParent(self).onafterStart( self, Controllable, From, Event, To ) + +end + +--- @param Wrapper.Controllable#CONTROLLABLE AIControllable +function _NewEngageCapRoute( AIControllable ) + + AIControllable:T( "NewEngageRoute" ) + local EngageZone = AIControllable:GetState( AIControllable, "EngageZone" ) -- AI.AI_Cap#AI_CAP_ZONE + EngageZone:__Engage( 1 ) +end + +--- @param #AI_CAP_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_CAP_ZONE:onbeforeEngage( Controllable, From, Event, To ) + + if self.Accomplished == true then + return false + end +end + +--- @param #AI_CAP_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_CAP_ZONE:onafterDetected( Controllable, From, Event, To ) + + if From ~= "Engaging" then + + local Engage = false + + for DetectedUnit, Detected in pairs( self.DetectedUnits ) do + + local DetectedUnit = DetectedUnit -- Wrapper.Unit#UNIT + self:T( DetectedUnit ) + if DetectedUnit:IsAlive() and DetectedUnit:IsAir() then + Engage = true + break + end + end + + if Engage == true then + self:E( 'Detected -> Engaging' ) + self:__Engage( 1 ) + end + end +end + + + +--- @param #AI_CAP_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_CAP_ZONE:onafterEngage( Controllable, From, Event, To ) + + if Controllable:IsAlive() then + + local EngageRoute = {} + + --- Calculate the current route point. + local CurrentVec2 = self.Controllable:GetVec2() + + --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). + local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() + local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) + local ToEngageZoneSpeed = self.PatrolMaxSpeed + local CurrentRoutePoint = CurrentPointVec3:RoutePointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToEngageZoneSpeed, + true + ) + + EngageRoute[#EngageRoute+1] = CurrentRoutePoint + + + --- Find a random 2D point in PatrolZone. + local ToTargetVec2 = self.PatrolZone:GetRandomVec2() + self:T2( ToTargetVec2 ) + + --- Define Speed and Altitude. + local ToTargetAltitude = math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) + local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) + self:T2( { self.PatrolMinSpeed, self.PatrolMaxSpeed, ToTargetSpeed } ) + + --- Obtain a 3D @{Point} from the 2D point + altitude. + local ToTargetPointVec3 = POINT_VEC3:New( ToTargetVec2.x, ToTargetAltitude, ToTargetVec2.y ) + + --- Create a route point of type air. + local ToPatrolRoutePoint = ToTargetPointVec3:RoutePointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true + ) + + EngageRoute[#EngageRoute+1] = ToPatrolRoutePoint + + Controllable:OptionROEOpenFire() + Controllable:OptionROTPassiveDefense() + + local AttackTasks = {} + + for DetectedUnit, Detected in pairs( self.DetectedUnits ) do + local DetectedUnit = DetectedUnit -- Wrapper.Unit#UNIT + self:T( { DetectedUnit, DetectedUnit:IsAlive(), DetectedUnit:IsAir() } ) + if DetectedUnit:IsAlive() and DetectedUnit:IsAir() then + if self.EngageZone then + if DetectedUnit:IsInZone( self.EngageZone ) then + self:E( {"Within Zone and Engaging ", DetectedUnit } ) + AttackTasks[#AttackTasks+1] = Controllable:TaskAttackUnit( DetectedUnit ) + end + else + if self.EngageRange then + if DetectedUnit:GetPointVec3():Get2DDistance(Controllable:GetPointVec3() ) <= self.EngageRange then + self:E( {"Within Range and Engaging", DetectedUnit } ) + AttackTasks[#AttackTasks+1] = Controllable:TaskAttackUnit( DetectedUnit ) + end + else + AttackTasks[#AttackTasks+1] = Controllable:TaskAttackUnit( DetectedUnit ) + end + end + else + self.DetectedUnits[DetectedUnit] = nil + end + end + + --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... + self.Controllable:WayPointInitialize( EngageRoute ) + + + if #AttackTasks == 0 then + self:E("No targets found -> Going back to Patrolling") + self:__Abort( 1 ) + self:__Route( 1 ) + self:SetDetectionActivated() + else + EngageRoute[1].task = Controllable:TaskCombo( AttackTasks ) + + --- Do a trick, link the NewEngageRoute function of the object to the AIControllable in a temporary variable ... + self.Controllable:SetState( self.Controllable, "EngageZone", self ) + + self.Controllable:WayPointFunction( #EngageRoute, 1, "_NewEngageCapRoute" ) + + self:SetDetectionDeactivated() + end + + --- NOW ROUTE THE GROUP! + self.Controllable:WayPointExecute( 1, 2 ) + + end +end + +--- @param #AI_CAP_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @param Core.Event#EVENTDATA EventData +function AI_CAP_ZONE:onafterDestroy( Controllable, From, Event, To, EventData ) + + if EventData.IniUnit then + self.DetectedUnits[EventData.IniUnit] = nil + end + + Controllable:MessageToAll( "Destroyed a target", 15 , "Destroyed!" ) +end + +--- @param #AI_CAP_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_CAP_ZONE:onafterAccomplish( Controllable, From, Event, To ) + self.Accomplished = true + self:SetDetectionOff() +end + + +---Single-Player:**Yes** / Multi-Player:**Yes** / AI:**Yes** / Human:**No** / Types:**Ground** -- +-- **Management of logical cargo objects, that can be transported from and to transportation carriers.** +-- +-- ![Banner Image](..\Presentations\AI_CARGO\CARGO.JPG) +-- +-- === +-- +-- Cargo can be of various forms, always are composed out of ONE object ( one unit or one static or one slingload crate ): +-- +-- * AI_CARGO_UNIT, represented by a @{Unit} in a @{Group}: Cargo can be represented by a Unit in a Group. Destruction of the Unit will mean that the cargo is lost. +-- * CARGO_STATIC, represented by a @{Static}: Cargo can be represented by a Static. Destruction of the Static will mean that the cargo is lost. +-- * AI_CARGO_PACKAGE, contained in a @{Unit} of a @{Group}: Cargo can be contained within a Unit of a Group. The cargo can be **delivered** by the @{Unit}. If the Unit is destroyed, the cargo will be destroyed also. +-- * AI_CARGO_PACKAGE, Contained in a @{Static}: Cargo can be contained within a Static. The cargo can be **collected** from the @Static. If the @{Static} is destroyed, the cargo will be destroyed. +-- * CARGO_SLINGLOAD, represented by a @{Cargo} that is transportable: Cargo can be represented by a Cargo object that is transportable. Destruction of the Cargo will mean that the cargo is lost. +-- +-- * AI_CARGO_GROUPED, represented by a Group of CARGO_UNITs. +-- +-- # 1) @{#AI_CARGO} class, extends @{Fsm#FSM_PROCESS} +-- +-- The @{#AI_CARGO} class defines the core functions that defines a cargo object within MOOSE. +-- A cargo is a logical object defined that is available for transport, and has a life status within a simulation. +-- +-- The AI_CARGO is a state machine: it manages the different events and states of the cargo. +-- All derived classes from AI_CARGO follow the same state machine, expose the same cargo event functions, and provide the same cargo states. +-- +-- ## 1.2.1) AI_CARGO Events: +-- +-- * @{#AI_CARGO.Board}( ToCarrier ): Boards the cargo to a carrier. +-- * @{#AI_CARGO.Load}( ToCarrier ): Loads the cargo into a carrier, regardless of its position. +-- * @{#AI_CARGO.UnBoard}( ToPointVec2 ): UnBoard the cargo from a carrier. This will trigger a movement of the cargo to the option ToPointVec2. +-- * @{#AI_CARGO.UnLoad}( ToPointVec2 ): UnLoads the cargo from a carrier. +-- * @{#AI_CARGO.Dead}( Controllable ): The cargo is dead. The cargo process will be ended. +-- +-- ## 1.2.2) AI_CARGO States: +-- +-- * **UnLoaded**: The cargo is unloaded from a carrier. +-- * **Boarding**: The cargo is currently boarding (= running) into a carrier. +-- * **Loaded**: The cargo is loaded into a carrier. +-- * **UnBoarding**: The cargo is currently unboarding (=running) from a carrier. +-- * **Dead**: The cargo is dead ... +-- * **End**: The process has come to an end. +-- +-- ## 1.2.3) AI_CARGO state transition methods: +-- +-- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. +-- There are 2 moments when state transition methods will be called by the state machine: +-- +-- * **Leaving** the state. +-- The state transition method needs to start with the name **OnLeave + the name of the state**. +-- If the state transition method returns false, then the processing of the state transition will not be done! +-- If you want to change the behaviour of the AIControllable at this event, return false, +-- but then you'll need to specify your own logic using the AIControllable! +-- +-- * **Entering** the state. +-- The state transition method needs to start with the name **OnEnter + the name of the state**. +-- These state transition methods need to provide a return value, which is specified at the function description. +-- +-- # 2) #AI_CARGO_UNIT class +-- +-- The AI_CARGO_UNIT class defines a cargo that is represented by a UNIT object within the simulator, and can be transported by a carrier. +-- Use the event functions as described above to Load, UnLoad, Board, UnBoard the AI_CARGO_UNIT objects to and from carriers. +-- +-- # 5) #AI_CARGO_GROUPED class +-- +-- The AI_CARGO_GROUPED class defines a cargo that is represented by a group of UNIT objects within the simulator, and can be transported by a carrier. +-- Use the event functions as described above to Load, UnLoad, Board, UnBoard the AI_CARGO_UNIT objects to and from carriers. +-- +-- This module is still under construction, but is described above works already, and will keep working ... +-- +-- @module Cargo + +-- Events + +-- Board + +--- Boards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo to the Carrier. +-- The cargo must be in the **UnLoaded** state. +-- @function [parent=#AI_CARGO] Board +-- @param #AI_CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. + +--- Boards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo to the Carrier. +-- The cargo must be in the **UnLoaded** state. +-- @function [parent=#AI_CARGO] __Board +-- @param #AI_CARGO self +-- @param #number DelaySeconds The amount of seconds to delay the action. +-- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. + + +-- UnBoard + +--- UnBoards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo from the Carrier. +-- The cargo must be in the **Loaded** state. +-- @function [parent=#AI_CARGO] UnBoard +-- @param #AI_CARGO self +-- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Point#POINT_VEC2) to where the cargo should run after onboarding. If not provided, the cargo will run to 60 meters behind the Carrier location. + +--- UnBoards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo from the Carrier. +-- The cargo must be in the **Loaded** state. +-- @function [parent=#AI_CARGO] __UnBoard +-- @param #AI_CARGO self +-- @param #number DelaySeconds The amount of seconds to delay the action. +-- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Point#POINT_VEC2) to where the cargo should run after onboarding. If not provided, the cargo will run to 60 meters behind the Carrier location. + + +-- Load + +--- Loads the cargo to a Carrier. The event will load the cargo into the Carrier regardless of its position. There will be no movement simulated of the cargo loading. +-- The cargo must be in the **UnLoaded** state. +-- @function [parent=#AI_CARGO] Load +-- @param #AI_CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. + +--- Loads the cargo to a Carrier. The event will load the cargo into the Carrier regardless of its position. There will be no movement simulated of the cargo loading. +-- The cargo must be in the **UnLoaded** state. +-- @function [parent=#AI_CARGO] __Load +-- @param #AI_CARGO self +-- @param #number DelaySeconds The amount of seconds to delay the action. +-- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. + + +-- UnLoad + +--- UnLoads the cargo to a Carrier. The event will unload the cargo from the Carrier. There will be no movement simulated of the cargo loading. +-- The cargo must be in the **Loaded** state. +-- @function [parent=#AI_CARGO] UnLoad +-- @param #AI_CARGO self +-- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Point#POINT_VEC2) to where the cargo will be placed after unloading. If not provided, the cargo will be placed 60 meters behind the Carrier location. + +--- UnLoads the cargo to a Carrier. The event will unload the cargo from the Carrier. There will be no movement simulated of the cargo loading. +-- The cargo must be in the **Loaded** state. +-- @function [parent=#AI_CARGO] __UnLoad +-- @param #AI_CARGO self +-- @param #number DelaySeconds The amount of seconds to delay the action. +-- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Point#POINT_VEC2) to where the cargo will be placed after unloading. If not provided, the cargo will be placed 60 meters behind the Carrier location. + +-- State Transition Functions + +-- UnLoaded + +--- @function [parent=#AI_CARGO] OnLeaveUnLoaded +-- @param #AI_CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable +-- @return #boolean + +--- @function [parent=#AI_CARGO] OnEnterUnLoaded +-- @param #AI_CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable + +-- Loaded + +--- @function [parent=#AI_CARGO] OnLeaveLoaded +-- @param #AI_CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable +-- @return #boolean + +--- @function [parent=#AI_CARGO] OnEnterLoaded +-- @param #AI_CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable + +-- Boarding + +--- @function [parent=#AI_CARGO] OnLeaveBoarding +-- @param #AI_CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable +-- @return #boolean + +--- @function [parent=#AI_CARGO] OnEnterBoarding +-- @param #AI_CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable + +-- UnBoarding + +--- @function [parent=#AI_CARGO] OnLeaveUnBoarding +-- @param #AI_CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable +-- @return #boolean + +--- @function [parent=#AI_CARGO] OnEnterUnBoarding +-- @param #AI_CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable + + +-- TODO: Find all Carrier objects and make the type of the Carriers Wrapper.Unit#UNIT in the documentation. + +CARGOS = {} + +do -- AI_CARGO + + --- @type AI_CARGO + -- @extends Core.Fsm#FSM_PROCESS + -- @field #string Type A string defining the type of the cargo. eg. Engineers, Equipment, Screwdrivers. + -- @field #string Name A string defining the name of the cargo. The name is the unique identifier of the cargo. + -- @field #number Weight A number defining the weight of the cargo. The weight is expressed in kg. + -- @field #number ReportRadius (optional) A number defining the radius in meters when the cargo is signalling or reporting to a Carrier. + -- @field #number NearRadius (optional) A number defining the radius in meters when the cargo is near to a Carrier, so that it can be loaded. + -- @field Wrapper.Controllable#CONTROLLABLE CargoObject The alive DCS object representing the cargo. This value can be nil, meaning, that the cargo is not represented anywhere... + -- @field Wrapper.Controllable#CONTROLLABLE CargoCarrier The alive DCS object carrying the cargo. This value can be nil, meaning, that the cargo is not contained anywhere... + -- @field #boolean Slingloadable This flag defines if the cargo can be slingloaded. + -- @field #boolean Moveable This flag defines if the cargo is moveable. + -- @field #boolean Representable This flag defines if the cargo can be represented by a DCS Unit. + -- @field #boolean Containable This flag defines if the cargo can be contained within a DCS Unit. + AI_CARGO = { + ClassName = "AI_CARGO", + Type = nil, + Name = nil, + Weight = nil, + CargoObject = nil, + CargoCarrier = nil, + Representable = false, + Slingloadable = false, + Moveable = false, + Containable = false, + } + +--- @type AI_CARGO.CargoObjects +-- @map < #string, Wrapper.Positionable#POSITIONABLE > The alive POSITIONABLE objects representing the the cargo. + + +--- AI_CARGO Constructor. This class is an abstract class and should not be instantiated. +-- @param #AI_CARGO self +-- @param #string Type +-- @param #string Name +-- @param #number Weight +-- @param #number ReportRadius (optional) +-- @param #number NearRadius (optional) +-- @return #AI_CARGO +function AI_CARGO:New( Type, Name, Weight, ReportRadius, NearRadius ) + + local self = BASE:Inherit( self, FSM:New() ) -- Core.Fsm#FSM_CONTROLLABLE + self:F( { Type, Name, Weight, ReportRadius, NearRadius } ) + + self:SetStartState( "UnLoaded" ) + self:AddTransition( "UnLoaded", "Board", "Boarding" ) + self:AddTransition( "Boarding", "Boarding", "Boarding" ) + self:AddTransition( "Boarding", "Load", "Loaded" ) + self:AddTransition( "UnLoaded", "Load", "Loaded" ) + self:AddTransition( "Loaded", "UnBoard", "UnBoarding" ) + self:AddTransition( "UnBoarding", "UnBoarding", "UnBoarding" ) + self:AddTransition( "UnBoarding", "UnLoad", "UnLoaded" ) + self:AddTransition( "Loaded", "UnLoad", "UnLoaded" ) + + + self.Type = Type + self.Name = Name + self.Weight = Weight + self.ReportRadius = ReportRadius + self.NearRadius = NearRadius + self.CargoObject = nil + self.CargoCarrier = nil + self.Representable = false + self.Slingloadable = false + self.Moveable = false + self.Containable = false + + + self.CargoScheduler = SCHEDULER:New() + + CARGOS[self.Name] = self + + return self +end + + +--- Template method to spawn a new representation of the AI_CARGO in the simulator. +-- @param #AI_CARGO self +-- @return #AI_CARGO +function AI_CARGO:Spawn( PointVec2 ) + self:F() + +end + + +--- Check if CargoCarrier is near the Cargo to be Loaded. +-- @param #AI_CARGO self +-- @param Core.Point#POINT_VEC2 PointVec2 +-- @return #boolean +function AI_CARGO:IsNear( PointVec2 ) + self:F( { PointVec2 } ) + + local Distance = PointVec2:DistanceFromPointVec2( self.CargoObject:GetPointVec2() ) + self:T( Distance ) + + if Distance <= self.NearRadius then + return true + else + return false + end +end + +end + +do -- AI_CARGO_REPRESENTABLE + + --- @type AI_CARGO_REPRESENTABLE + -- @extends #AI_CARGO + AI_CARGO_REPRESENTABLE = { + ClassName = "AI_CARGO_REPRESENTABLE" + } + +--- AI_CARGO_REPRESENTABLE Constructor. +-- @param #AI_CARGO_REPRESENTABLE self +-- @param Wrapper.Controllable#Controllable CargoObject +-- @param #string Type +-- @param #string Name +-- @param #number Weight +-- @param #number ReportRadius (optional) +-- @param #number NearRadius (optional) +-- @return #AI_CARGO_REPRESENTABLE +function AI_CARGO_REPRESENTABLE:New( CargoObject, Type, Name, Weight, ReportRadius, NearRadius ) + local self = BASE:Inherit( self, AI_CARGO:New( Type, Name, Weight, ReportRadius, NearRadius ) ) -- #AI_CARGO + self:F( { Type, Name, Weight, ReportRadius, NearRadius } ) + + return self +end + +--- Route a cargo unit to a PointVec2. +-- @param #AI_CARGO_REPRESENTABLE self +-- @param Core.Point#POINT_VEC2 ToPointVec2 +-- @param #number Speed +-- @return #AI_CARGO_REPRESENTABLE +function AI_CARGO_REPRESENTABLE:RouteTo( ToPointVec2, Speed ) + self:F2( ToPointVec2 ) + + local Points = {} + + local PointStartVec2 = self.CargoObject:GetPointVec2() + + Points[#Points+1] = PointStartVec2:RoutePointGround( Speed ) + Points[#Points+1] = ToPointVec2:RoutePointGround( Speed ) + + local TaskRoute = self.CargoObject:TaskRoute( Points ) + self.CargoObject:SetTask( TaskRoute, 2 ) + return self +end + +end -- AI_CARGO + +do -- AI_CARGO_UNIT + + --- @type AI_CARGO_UNIT + -- @extends #AI_CARGO_REPRESENTABLE + AI_CARGO_UNIT = { + ClassName = "AI_CARGO_UNIT" + } + +--- AI_CARGO_UNIT Constructor. +-- @param #AI_CARGO_UNIT self +-- @param Wrapper.Unit#UNIT CargoUnit +-- @param #string Type +-- @param #string Name +-- @param #number Weight +-- @param #number ReportRadius (optional) +-- @param #number NearRadius (optional) +-- @return #AI_CARGO_UNIT +function AI_CARGO_UNIT:New( CargoUnit, Type, Name, Weight, ReportRadius, NearRadius ) + local self = BASE:Inherit( self, AI_CARGO_REPRESENTABLE:New( CargoUnit, Type, Name, Weight, ReportRadius, NearRadius ) ) -- #AI_CARGO_UNIT + self:F( { Type, Name, Weight, ReportRadius, NearRadius } ) + + self:T( CargoUnit ) + self.CargoObject = CargoUnit + + self:T( self.ClassName ) + + return self +end + +--- Enter UnBoarding State. +-- @param #AI_CARGO_UNIT self +-- @param #string Event +-- @param #string From +-- @param #string To +-- @param Core.Point#POINT_VEC2 ToPointVec2 +function AI_CARGO_UNIT:onenterUnBoarding( From, Event, To, ToPointVec2 ) + self:F() + + local Angle = 180 + local Speed = 10 + local DeployDistance = 5 + local RouteDistance = 60 + + if From == "Loaded" then + + local CargoCarrierPointVec2 = self.CargoCarrier:GetPointVec2() + local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. + local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) + local CargoDeployPointVec2 = CargoCarrierPointVec2:Translate( DeployDistance, CargoDeployHeading ) + local CargoRoutePointVec2 = CargoCarrierPointVec2:Translate( RouteDistance, CargoDeployHeading ) + + -- if there is no ToPointVec2 given, then use the CargoRoutePointVec2 + ToPointVec2 = ToPointVec2 or CargoRoutePointVec2 + + local FromPointVec2 = CargoCarrierPointVec2 + + -- Respawn the group... + if self.CargoObject then + self.CargoObject:ReSpawn( CargoDeployPointVec2:GetVec3(), CargoDeployHeading ) + self.CargoCarrier = nil + + local Points = {} + Points[#Points+1] = FromPointVec2:RoutePointGround( Speed ) + Points[#Points+1] = ToPointVec2:RoutePointGround( Speed ) + + local TaskRoute = self.CargoObject:TaskRoute( Points ) + self.CargoObject:SetTask( TaskRoute, 1 ) + + self:__UnBoarding( 1, ToPointVec2 ) + end + end + +end + +--- Leave UnBoarding State. +-- @param #AI_CARGO_UNIT self +-- @param #string Event +-- @param #string From +-- @param #string To +-- @param Core.Point#POINT_VEC2 ToPointVec2 +function AI_CARGO_UNIT:onleaveUnBoarding( From, Event, To, ToPointVec2 ) + self:F( { ToPointVec2, From, Event, To } ) + + local Angle = 180 + local Speed = 10 + local Distance = 5 + + if From == "UnBoarding" then + if self:IsNear( ToPointVec2 ) then + return true + else + self:__UnBoarding( 1, ToPointVec2 ) + end + return false + end + +end + +--- UnBoard Event. +-- @param #AI_CARGO_UNIT self +-- @param #string Event +-- @param #string From +-- @param #string To +-- @param Core.Point#POINT_VEC2 ToPointVec2 +function AI_CARGO_UNIT:onafterUnBoarding( From, Event, To, ToPointVec2 ) + self:F( { ToPointVec2, From, Event, To } ) + + self.CargoInAir = self.CargoObject:InAir() + + self:T( self.CargoInAir ) + + -- Only unboard the cargo when the carrier is not in the air. + -- (eg. cargo can be on a oil derrick, moving the cargo on the oil derrick will drop the cargo on the sea). + if not self.CargoInAir then + + end + + self:__UnLoad( 1, ToPointVec2 ) + +end + + + +--- Enter UnLoaded State. +-- @param #AI_CARGO_UNIT self +-- @param #string Event +-- @param #string From +-- @param #string To +-- @param Core.Point#POINT_VEC2 +function AI_CARGO_UNIT:onenterUnLoaded( From, Event, To, ToPointVec2 ) + self:F( { ToPointVec2, From, Event, To } ) + + local Angle = 180 + local Speed = 10 + local Distance = 5 + + if From == "Loaded" then + local StartPointVec2 = self.CargoCarrier:GetPointVec2() + local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. + local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) + local CargoDeployPointVec2 = StartPointVec2:Translate( Distance, CargoDeployHeading ) + + ToPointVec2 = ToPointVec2 or POINT_VEC2:New( CargoDeployPointVec2:GetX(), CargoDeployPointVec2:GetY() ) + + -- Respawn the group... + if self.CargoObject then + self.CargoObject:ReSpawn( ToPointVec2:GetVec3(), 0 ) + self.CargoCarrier = nil + end + + end + + if self.OnUnLoadedCallBack then + self.OnUnLoadedCallBack( self, unpack( self.OnUnLoadedParameters ) ) + self.OnUnLoadedCallBack = nil + end + +end + + + +--- Enter Boarding State. +-- @param #AI_CARGO_UNIT self +-- @param #string Event +-- @param #string From +-- @param #string To +-- @param Wrapper.Unit#UNIT CargoCarrier +function AI_CARGO_UNIT:onenterBoarding( From, Event, To, CargoCarrier ) + self:F( { CargoCarrier.UnitName, From, Event, To } ) + + local Speed = 10 + local Angle = 180 + local Distance = 5 + + if From == "UnLoaded" then + local CargoCarrierPointVec2 = CargoCarrier:GetPointVec2() + local CargoCarrierHeading = CargoCarrier:GetHeading() -- Get Heading of object in degrees. + local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) + local CargoDeployPointVec2 = CargoCarrierPointVec2:Translate( Distance, CargoDeployHeading ) + + local Points = {} + + local PointStartVec2 = self.CargoObject:GetPointVec2() + + Points[#Points+1] = PointStartVec2:RoutePointGround( Speed ) + Points[#Points+1] = CargoDeployPointVec2:RoutePointGround( Speed ) + + local TaskRoute = self.CargoObject:TaskRoute( Points ) + self.CargoObject:SetTask( TaskRoute, 2 ) + end + +end + +--- Leave Boarding State. +-- @param #AI_CARGO_UNIT self +-- @param #string Event +-- @param #string From +-- @param #string To +-- @param Wrapper.Unit#UNIT CargoCarrier +function AI_CARGO_UNIT:onleaveBoarding( From, Event, To, CargoCarrier ) + self:F( { CargoCarrier.UnitName, From, Event, To } ) + + if self:IsNear( CargoCarrier:GetPointVec2() ) then + self:__Load( 1, CargoCarrier ) + return true + else + self:__Boarding( 1, CargoCarrier ) + end + return false +end + +--- Loaded State. +-- @param #AI_CARGO_UNIT self +-- @param #string Event +-- @param #string From +-- @param #string To +-- @param Wrapper.Unit#UNIT CargoCarrier +function AI_CARGO_UNIT:onenterLoaded( From, Event, To, CargoCarrier ) + self:F() + + self.CargoCarrier = CargoCarrier + + -- Only destroy the CargoObject is if there is a CargoObject (packages don't have CargoObjects). + if self.CargoObject then + self:T("Destroying") + self.CargoObject:Destroy() + end +end + + +--- Board Event. +-- @param #AI_CARGO_UNIT self +-- @param #string Event +-- @param #string From +-- @param #string To +function AI_CARGO_UNIT:onafterBoard( From, Event, To, CargoCarrier ) + self:F() + + self.CargoInAir = self.CargoObject:InAir() + + self:T( self.CargoInAir ) + + -- Only move the group to the carrier when the cargo is not in the air + -- (eg. cargo can be on a oil derrick, moving the cargo on the oil derrick will drop the cargo on the sea). + if not self.CargoInAir then + self:Load( CargoCarrier ) + end + +end + +end + +do -- AI_CARGO_PACKAGE + + --- @type AI_CARGO_PACKAGE + -- @extends #AI_CARGO_REPRESENTABLE + AI_CARGO_PACKAGE = { + ClassName = "AI_CARGO_PACKAGE" + } + +--- AI_CARGO_PACKAGE Constructor. +-- @param #AI_CARGO_PACKAGE self +-- @param Wrapper.Unit#UNIT CargoCarrier The UNIT carrying the package. +-- @param #string Type +-- @param #string Name +-- @param #number Weight +-- @param #number ReportRadius (optional) +-- @param #number NearRadius (optional) +-- @return #AI_CARGO_PACKAGE +function AI_CARGO_PACKAGE:New( CargoCarrier, Type, Name, Weight, ReportRadius, NearRadius ) + local self = BASE:Inherit( self, AI_CARGO_REPRESENTABLE:New( CargoCarrier, Type, Name, Weight, ReportRadius, NearRadius ) ) -- #AI_CARGO_PACKAGE + self:F( { Type, Name, Weight, ReportRadius, NearRadius } ) + + self:T( CargoCarrier ) + self.CargoCarrier = CargoCarrier + + return self +end + +--- Board Event. +-- @param #AI_CARGO_PACKAGE self +-- @param #string Event +-- @param #string From +-- @param #string To +-- @param Wrapper.Unit#UNIT CargoCarrier +-- @param #number Speed +-- @param #number BoardDistance +-- @param #number Angle +function AI_CARGO_PACKAGE:onafterOnBoard( From, Event, To, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) + self:F() + + self.CargoInAir = self.CargoCarrier:InAir() + + self:T( self.CargoInAir ) + + -- Only move the CargoCarrier to the New CargoCarrier when the New CargoCarrier is not in the air. + if not self.CargoInAir then + + local Points = {} + + local StartPointVec2 = self.CargoCarrier:GetPointVec2() + local CargoCarrierHeading = CargoCarrier:GetHeading() -- Get Heading of object in degrees. + local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) + self:T( { CargoCarrierHeading, CargoDeployHeading } ) + local CargoDeployPointVec2 = CargoCarrier:GetPointVec2():Translate( BoardDistance, CargoDeployHeading ) + + Points[#Points+1] = StartPointVec2:RoutePointGround( Speed ) + Points[#Points+1] = CargoDeployPointVec2:RoutePointGround( Speed ) + + local TaskRoute = self.CargoCarrier:TaskRoute( Points ) + self.CargoCarrier:SetTask( TaskRoute, 1 ) + end + + self:Boarded( CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) + +end + +--- Check if CargoCarrier is near the Cargo to be Loaded. +-- @param #AI_CARGO_PACKAGE self +-- @param Wrapper.Unit#UNIT CargoCarrier +-- @return #boolean +function AI_CARGO_PACKAGE:IsNear( CargoCarrier ) + self:F() + + local CargoCarrierPoint = CargoCarrier:GetPointVec2() + + local Distance = CargoCarrierPoint:DistanceFromPointVec2( self.CargoCarrier:GetPointVec2() ) + self:T( Distance ) + + if Distance <= self.NearRadius then + return true + else + return false + end +end + +--- Boarded Event. +-- @param #AI_CARGO_PACKAGE self +-- @param #string Event +-- @param #string From +-- @param #string To +-- @param Wrapper.Unit#UNIT CargoCarrier +function AI_CARGO_PACKAGE:onafterOnBoarded( From, Event, To, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) + self:F() + + if self:IsNear( CargoCarrier ) then + self:__Load( 1, CargoCarrier, Speed, LoadDistance, Angle ) + else + self:__Boarded( 1, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) + end +end + +--- UnBoard Event. +-- @param #AI_CARGO_PACKAGE self +-- @param #string Event +-- @param #string From +-- @param #string To +-- @param #number Speed +-- @param #number UnLoadDistance +-- @param #number UnBoardDistance +-- @param #number Radius +-- @param #number Angle +function AI_CARGO_PACKAGE:onafterUnBoard( From, Event, To, CargoCarrier, Speed, UnLoadDistance, UnBoardDistance, Radius, Angle ) + self:F() + + self.CargoInAir = self.CargoCarrier:InAir() + + self:T( self.CargoInAir ) + + -- Only unboard the cargo when the carrier is not in the air. + -- (eg. cargo can be on a oil derrick, moving the cargo on the oil derrick will drop the cargo on the sea). + if not self.CargoInAir then + + self:_Next( self.FsmP.UnLoad, UnLoadDistance, Angle ) + + local Points = {} + + local StartPointVec2 = CargoCarrier:GetPointVec2() + local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. + local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) + self:T( { CargoCarrierHeading, CargoDeployHeading } ) + local CargoDeployPointVec2 = StartPointVec2:Translate( UnBoardDistance, CargoDeployHeading ) + + Points[#Points+1] = StartPointVec2:RoutePointGround( Speed ) + Points[#Points+1] = CargoDeployPointVec2:RoutePointGround( Speed ) + + local TaskRoute = CargoCarrier:TaskRoute( Points ) + CargoCarrier:SetTask( TaskRoute, 1 ) + end + + self:__UnBoarded( 1 , CargoCarrier, Speed ) + +end + +--- UnBoarded Event. +-- @param #AI_CARGO_PACKAGE self +-- @param #string Event +-- @param #string From +-- @param #string To +-- @param Wrapper.Unit#UNIT CargoCarrier +function AI_CARGO_PACKAGE:onafterUnBoarded( From, Event, To, CargoCarrier, Speed ) + self:F() + + if self:IsNear( CargoCarrier ) then + self:__UnLoad( 1, CargoCarrier, Speed ) + else + self:__UnBoarded( 1, CargoCarrier, Speed ) + end +end + +--- Load Event. +-- @param #AI_CARGO_PACKAGE self +-- @param #string Event +-- @param #string From +-- @param #string To +-- @param Wrapper.Unit#UNIT CargoCarrier +-- @param #number Speed +-- @param #number LoadDistance +-- @param #number Angle +function AI_CARGO_PACKAGE:onafterLoad( From, Event, To, CargoCarrier, Speed, LoadDistance, Angle ) + self:F() + + self.CargoCarrier = CargoCarrier + + local StartPointVec2 = self.CargoCarrier:GetPointVec2() + local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. + local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) + local CargoDeployPointVec2 = StartPointVec2:Translate( LoadDistance, CargoDeployHeading ) + + local Points = {} + Points[#Points+1] = StartPointVec2:RoutePointGround( Speed ) + Points[#Points+1] = CargoDeployPointVec2:RoutePointGround( Speed ) + + local TaskRoute = self.CargoCarrier:TaskRoute( Points ) + self.CargoCarrier:SetTask( TaskRoute, 1 ) + +end + +--- UnLoad Event. +-- @param #AI_CARGO_PACKAGE self +-- @param #string Event +-- @param #string From +-- @param #string To +-- @param #number Distance +-- @param #number Angle +function AI_CARGO_PACKAGE:onafterUnLoad( From, Event, To, CargoCarrier, Speed, Distance, Angle ) + self:F() + + local StartPointVec2 = self.CargoCarrier:GetPointVec2() + local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. + local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) + local CargoDeployPointVec2 = StartPointVec2:Translate( Distance, CargoDeployHeading ) + + self.CargoCarrier = CargoCarrier + + local Points = {} + Points[#Points+1] = StartPointVec2:RoutePointGround( Speed ) + Points[#Points+1] = CargoDeployPointVec2:RoutePointGround( Speed ) + + local TaskRoute = self.CargoCarrier:TaskRoute( Points ) + self.CargoCarrier:SetTask( TaskRoute, 1 ) + +end + + +end + +do -- AI_CARGO_GROUP + + --- @type AI_CARGO_GROUP + -- @extends AI.AI_Cargo#AI_CARGO + -- @field Set#SET_BASE CargoSet A set of cargo objects. + -- @field #string Name A string defining the name of the cargo group. The name is the unique identifier of the cargo. + AI_CARGO_GROUP = { + ClassName = "AI_CARGO_GROUP", + } + +--- AI_CARGO_GROUP constructor. +-- @param #AI_CARGO_GROUP self +-- @param Core.Set#Set_BASE CargoSet +-- @param #string Type +-- @param #string Name +-- @param #number Weight +-- @param #number ReportRadius (optional) +-- @param #number NearRadius (optional) +-- @return #AI_CARGO_GROUP +function AI_CARGO_GROUP:New( CargoSet, Type, Name, ReportRadius, NearRadius ) + local self = BASE:Inherit( self, AI_CARGO:New( Type, Name, 0, ReportRadius, NearRadius ) ) -- #AI_CARGO_GROUP + self:F( { Type, Name, ReportRadius, NearRadius } ) + + self.CargoSet = CargoSet + + + return self +end + +end -- AI_CARGO_GROUP + +do -- AI_CARGO_GROUPED + + --- @type AI_CARGO_GROUPED + -- @extends AI.AI_Cargo#AI_CARGO_GROUP + AI_CARGO_GROUPED = { + ClassName = "AI_CARGO_GROUPED", + } + +--- AI_CARGO_GROUPED constructor. +-- @param #AI_CARGO_GROUPED self +-- @param Core.Set#Set_BASE CargoSet +-- @param #string Type +-- @param #string Name +-- @param #number Weight +-- @param #number ReportRadius (optional) +-- @param #number NearRadius (optional) +-- @return #AI_CARGO_GROUPED +function AI_CARGO_GROUPED:New( CargoSet, Type, Name, ReportRadius, NearRadius ) + local self = BASE:Inherit( self, AI_CARGO_GROUP:New( CargoSet, Type, Name, ReportRadius, NearRadius ) ) -- #AI_CARGO_GROUPED + self:F( { Type, Name, ReportRadius, NearRadius } ) + + return self +end + +--- Enter Boarding State. +-- @param #AI_CARGO_GROUPED self +-- @param Wrapper.Unit#UNIT CargoCarrier +-- @param #string Event +-- @param #string From +-- @param #string To +function AI_CARGO_GROUPED:onenterBoarding( From, Event, To, CargoCarrier ) + self:F( { CargoCarrier.UnitName, From, Event, To } ) + + if From == "UnLoaded" then + + -- For each Cargo object within the AI_CARGO_GROUPED, route each object to the CargoLoadPointVec2 + self.CargoSet:ForEach( + function( Cargo ) + Cargo:__Board( 1, CargoCarrier ) + end + ) + + self:__Boarding( 1, CargoCarrier ) + end + +end + +--- Enter Loaded State. +-- @param #AI_CARGO_GROUPED self +-- @param Wrapper.Unit#UNIT CargoCarrier +-- @param #string Event +-- @param #string From +-- @param #string To +function AI_CARGO_GROUPED:onenterLoaded( From, Event, To, CargoCarrier ) + self:F( { CargoCarrier.UnitName, From, Event, To } ) + + if From == "UnLoaded" then + -- For each Cargo object within the AI_CARGO_GROUPED, load each cargo to the CargoCarrier. + for CargoID, Cargo in pairs( self.CargoSet:GetSet() ) do + Cargo:Load( CargoCarrier ) + end + end +end + +--- Leave Boarding State. +-- @param #AI_CARGO_GROUPED self +-- @param Wrapper.Unit#UNIT CargoCarrier +-- @param #string Event +-- @param #string From +-- @param #string To +function AI_CARGO_GROUPED:onleaveBoarding( From, Event, To, CargoCarrier ) + self:F( { CargoCarrier.UnitName, From, Event, To } ) + + local Boarded = true + + -- For each Cargo object within the AI_CARGO_GROUPED, route each object to the CargoLoadPointVec2 + for CargoID, Cargo in pairs( self.CargoSet:GetSet() ) do + self:T( Cargo.current ) + if not Cargo:is( "Loaded" ) then + Boarded = false + end + end + + if not Boarded then + self:__Boarding( 1, CargoCarrier ) + else + self:__Load( 1, CargoCarrier ) + end + return Boarded +end + +--- Enter UnBoarding State. +-- @param #AI_CARGO_GROUPED self +-- @param Core.Point#POINT_VEC2 ToPointVec2 +-- @param #string Event +-- @param #string From +-- @param #string To +function AI_CARGO_GROUPED:onenterUnBoarding( From, Event, To, ToPointVec2 ) + self:F() + + local Timer = 1 + + if From == "Loaded" then + + -- For each Cargo object within the AI_CARGO_GROUPED, route each object to the CargoLoadPointVec2 + self.CargoSet:ForEach( + function( Cargo ) + Cargo:__UnBoard( Timer, ToPointVec2 ) + Timer = Timer + 10 + end + ) + + self:__UnBoarding( 1, ToPointVec2 ) + end + +end + +--- Leave UnBoarding State. +-- @param #AI_CARGO_GROUPED self +-- @param Core.Point#POINT_VEC2 ToPointVec2 +-- @param #string Event +-- @param #string From +-- @param #string To +function AI_CARGO_GROUPED:onleaveUnBoarding( From, Event, To, ToPointVec2 ) + self:F( { ToPointVec2, From, Event, To } ) + + local Angle = 180 + local Speed = 10 + local Distance = 5 + + if From == "UnBoarding" then + local UnBoarded = true + + -- For each Cargo object within the AI_CARGO_GROUPED, route each object to the CargoLoadPointVec2 + for CargoID, Cargo in pairs( self.CargoSet:GetSet() ) do + self:T( Cargo.current ) + if not Cargo:is( "UnLoaded" ) then + UnBoarded = false + end + end + + if UnBoarded then + return true + else + self:__UnBoarding( 1, ToPointVec2 ) + end + + return false + end + +end + +--- UnBoard Event. +-- @param #AI_CARGO_GROUPED self +-- @param Core.Point#POINT_VEC2 ToPointVec2 +-- @param #string Event +-- @param #string From +-- @param #string To +function AI_CARGO_GROUPED:onafterUnBoarding( From, Event, To, ToPointVec2 ) + self:F( { ToPointVec2, From, Event, To } ) + + self:__UnLoad( 1, ToPointVec2 ) +end + + + +--- Enter UnLoaded State. +-- @param #AI_CARGO_GROUPED self +-- @param Core.Point#POINT_VEC2 +-- @param #string Event +-- @param #string From +-- @param #string To +function AI_CARGO_GROUPED:onenterUnLoaded( From, Event, To, ToPointVec2 ) + self:F( { ToPointVec2, From, Event, To } ) + + if From == "Loaded" then + + -- For each Cargo object within the AI_CARGO_GROUPED, route each object to the CargoLoadPointVec2 + self.CargoSet:ForEach( + function( Cargo ) + Cargo:UnLoad( ToPointVec2 ) + end + ) + + end + +end + +end -- AI_CARGO_GROUPED + + + +--- (SP) (MP) (FSM) Accept or reject process for player (task) assignments. +-- +-- === +-- +-- # @{#ACT_ASSIGN} FSM template class, extends @{Fsm#FSM_PROCESS} +-- +-- ## ACT_ASSIGN state machine: +-- +-- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur. +-- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below. +-- Each derived class follows exactly the same process, using the same events and following the same state transitions, +-- but will have **different implementation behaviour** upon each event or state transition. +-- +-- ### ACT_ASSIGN **Events**: +-- +-- These are the events defined in this class: +-- +-- * **Start**: Start the tasking acceptance process. +-- * **Assign**: Assign the task. +-- * **Reject**: Reject the task.. +-- +-- ### ACT_ASSIGN **Event methods**: +-- +-- Event methods are available (dynamically allocated by the state machine), that accomodate for state transitions occurring in the process. +-- There are two types of event methods, which you can use to influence the normal mechanisms in the state machine: +-- +-- * **Immediate**: The event method has exactly the name of the event. +-- * **Delayed**: The event method starts with a __ + the name of the event. The first parameter of the event method is a number value, expressing the delay in seconds when the event will be executed. +-- +-- ### ACT_ASSIGN **States**: +-- +-- * **UnAssigned**: The player has not accepted the task. +-- * **Assigned (*)**: The player has accepted the task. +-- * **Rejected (*)**: The player has not accepted the task. +-- * **Waiting**: The process is awaiting player feedback. +-- * **Failed (*)**: The process has failed. +-- +-- (*) End states of the process. +-- +-- ### ACT_ASSIGN state transition methods: +-- +-- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. +-- There are 2 moments when state transition methods will be called by the state machine: +-- +-- * **Before** the state transition. +-- The state transition method needs to start with the name **OnBefore + the name of the state**. +-- If the state transition method returns false, then the processing of the state transition will not be done! +-- If you want to change the behaviour of the AIControllable at this event, return false, +-- but then you'll need to specify your own logic using the AIControllable! +-- +-- * **After** the state transition. +-- The state transition method needs to start with the name **OnAfter + the name of the state**. +-- These state transition methods need to provide a return value, which is specified at the function description. +-- +-- === +-- +-- # 1) @{#ACT_ASSIGN_ACCEPT} class, extends @{Fsm.Assign#ACT_ASSIGN} +-- +-- The ACT_ASSIGN_ACCEPT class accepts by default a task for a player. No player intervention is allowed to reject the task. +-- +-- ## 1.1) ACT_ASSIGN_ACCEPT constructor: +-- +-- * @{#ACT_ASSIGN_ACCEPT.New}(): Creates a new ACT_ASSIGN_ACCEPT object. +-- +-- === +-- +-- # 2) @{#ACT_ASSIGN_MENU_ACCEPT} class, extends @{Fsm.Assign#ACT_ASSIGN} +-- +-- The ACT_ASSIGN_MENU_ACCEPT class accepts a task when the player accepts the task through an added menu option. +-- This assignment type is useful to conditionally allow the player to choose whether or not he would accept the task. +-- The assignment type also allows to reject the task. +-- +-- ## 2.1) ACT_ASSIGN_MENU_ACCEPT constructor: +-- ----------------------------------------- +-- +-- * @{#ACT_ASSIGN_MENU_ACCEPT.New}(): Creates a new ACT_ASSIGN_MENU_ACCEPT object. +-- +-- === +-- +-- @module Assign + + +do -- ACT_ASSIGN + + --- ACT_ASSIGN class + -- @type ACT_ASSIGN + -- @field Tasking.Task#TASK Task + -- @field Wrapper.Unit#UNIT ProcessUnit + -- @field Core.Zone#ZONE_BASE TargetZone + -- @extends Core.Fsm#FSM_PROCESS + ACT_ASSIGN = { + ClassName = "ACT_ASSIGN", + } + + + --- Creates a new task assignment state machine. The process will accept the task by default, no player intervention accepted. + -- @param #ACT_ASSIGN self + -- @return #ACT_ASSIGN The task acceptance process. + function ACT_ASSIGN:New() + + -- Inherits from BASE + local self = BASE:Inherit( self, FSM_PROCESS:New( "ACT_ASSIGN" ) ) -- Core.Fsm#FSM_PROCESS + + self:AddTransition( "UnAssigned", "Start", "Waiting" ) + self:AddTransition( "Waiting", "Assign", "Assigned" ) + self:AddTransition( "Waiting", "Reject", "Rejected" ) + self:AddTransition( "*", "Fail", "Failed" ) + + self:AddEndState( "Assigned" ) + self:AddEndState( "Rejected" ) + self:AddEndState( "Failed" ) + + self:SetStartState( "UnAssigned" ) + + return self + end + +end -- ACT_ASSIGN + + + +do -- ACT_ASSIGN_ACCEPT + + --- ACT_ASSIGN_ACCEPT class + -- @type ACT_ASSIGN_ACCEPT + -- @field Tasking.Task#TASK Task + -- @field Wrapper.Unit#UNIT ProcessUnit + -- @field Core.Zone#ZONE_BASE TargetZone + -- @extends #ACT_ASSIGN + ACT_ASSIGN_ACCEPT = { + ClassName = "ACT_ASSIGN_ACCEPT", + } + + + --- Creates a new task assignment state machine. The process will accept the task by default, no player intervention accepted. + -- @param #ACT_ASSIGN_ACCEPT self + -- @param #string TaskBriefing + function ACT_ASSIGN_ACCEPT:New( TaskBriefing ) + + local self = BASE:Inherit( self, ACT_ASSIGN:New() ) -- #ACT_ASSIGN_ACCEPT + + self.TaskBriefing = TaskBriefing + + return self + end + + function ACT_ASSIGN_ACCEPT:Init( FsmAssign ) + + self.TaskBriefing = FsmAssign.TaskBriefing + end + + --- StateMachine callback function + -- @param #ACT_ASSIGN_ACCEPT self + -- @param Wrapper.Unit#UNIT ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ASSIGN_ACCEPT:onafterStart( ProcessUnit, From, Event, To ) + self:E( { ProcessUnit, From, Event, To } ) + + self:__Assign( 1 ) + end + + --- StateMachine callback function + -- @param #ACT_ASSIGN_ACCEPT self + -- @param Wrapper.Unit#UNIT ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ASSIGN_ACCEPT:onenterAssigned( ProcessUnit, From, Event, To ) + env.info( "in here" ) + self:E( { ProcessUnit, From, Event, To } ) + + local ProcessGroup = ProcessUnit:GetGroup() + + self:Message( "You are assigned to the task " .. self.Task:GetName() ) + + self.Task:Assign() + end + +end -- ACT_ASSIGN_ACCEPT + + +do -- ACT_ASSIGN_MENU_ACCEPT + + --- ACT_ASSIGN_MENU_ACCEPT class + -- @type ACT_ASSIGN_MENU_ACCEPT + -- @field Tasking.Task#TASK Task + -- @field Wrapper.Unit#UNIT ProcessUnit + -- @field Core.Zone#ZONE_BASE TargetZone + -- @extends #ACT_ASSIGN + ACT_ASSIGN_MENU_ACCEPT = { + ClassName = "ACT_ASSIGN_MENU_ACCEPT", + } + + --- Init. + -- @param #ACT_ASSIGN_MENU_ACCEPT self + -- @param #string TaskName + -- @param #string TaskBriefing + -- @return #ACT_ASSIGN_MENU_ACCEPT self + function ACT_ASSIGN_MENU_ACCEPT:New( TaskName, TaskBriefing ) + + -- Inherits from BASE + local self = BASE:Inherit( self, ACT_ASSIGN:New() ) -- #ACT_ASSIGN_MENU_ACCEPT + + self.TaskName = TaskName + self.TaskBriefing = TaskBriefing + + return self + end + + function ACT_ASSIGN_MENU_ACCEPT:Init( FsmAssign ) + + self.TaskName = FsmAssign.TaskName + self.TaskBriefing = FsmAssign.TaskBriefing + end + + + --- Creates a new task assignment state machine. The process will request from the menu if it accepts the task, if not, the unit is removed from the simulator. + -- @param #ACT_ASSIGN_MENU_ACCEPT self + -- @param #string TaskName + -- @param #string TaskBriefing + -- @return #ACT_ASSIGN_MENU_ACCEPT self + function ACT_ASSIGN_MENU_ACCEPT:Init( TaskName, TaskBriefing ) + + self.TaskBriefing = TaskBriefing + self.TaskName = TaskName + + return self + end + + --- StateMachine callback function + -- @param #ACT_ASSIGN_MENU_ACCEPT self + -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ASSIGN_MENU_ACCEPT:onafterStart( ProcessUnit, From, Event, To ) + self:E( { ProcessUnit, From, Event, To } ) + + self:Message( "Access the radio menu to accept the task. You have 30 seconds or the assignment will be cancelled." ) + + local ProcessGroup = ProcessUnit:GetGroup() + + self.Menu = MENU_GROUP:New( ProcessGroup, "Task " .. self.TaskName .. " acceptance" ) + self.MenuAcceptTask = MENU_GROUP_COMMAND:New( ProcessGroup, "Accept task " .. self.TaskName, self.Menu, self.MenuAssign, self ) + self.MenuRejectTask = MENU_GROUP_COMMAND:New( ProcessGroup, "Reject task " .. self.TaskName, self.Menu, self.MenuReject, self ) + end + + --- Menu function. + -- @param #ACT_ASSIGN_MENU_ACCEPT self + function ACT_ASSIGN_MENU_ACCEPT:MenuAssign() + self:E( ) + + self:__Assign( 1 ) + end + + --- Menu function. + -- @param #ACT_ASSIGN_MENU_ACCEPT self + function ACT_ASSIGN_MENU_ACCEPT:MenuReject() + self:E( ) + + self:__Reject( 1 ) + end + + --- StateMachine callback function + -- @param #ACT_ASSIGN_MENU_ACCEPT self + -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ASSIGN_MENU_ACCEPT:onafterAssign( ProcessUnit, From, Event, To ) + self:E( { ProcessUnit.UnitNameFrom, Event, To } ) + + self.Menu:Remove() + end + + --- StateMachine callback function + -- @param #ACT_ASSIGN_MENU_ACCEPT self + -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ASSIGN_MENU_ACCEPT:onafterReject( ProcessUnit, From, Event, To ) + self:E( { ProcessUnit.UnitName, From, Event, To } ) + + self.Menu:Remove() + --TODO: need to resolve this problem ... it has to do with the events ... + --self.Task:UnAssignFromUnit( ProcessUnit )needs to become a callback funtion call upon the event + ProcessUnit:Destroy() + end + +end -- ACT_ASSIGN_MENU_ACCEPT +--- (SP) (MP) (FSM) Route AI or players through waypoints or to zones. +-- +-- === +-- +-- # @{#ACT_ROUTE} FSM class, extends @{Fsm#FSM_PROCESS} +-- +-- ## ACT_ROUTE state machine: +-- +-- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur. +-- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below. +-- Each derived class follows exactly the same process, using the same events and following the same state transitions, +-- but will have **different implementation behaviour** upon each event or state transition. +-- +-- ### ACT_ROUTE **Events**: +-- +-- These are the events defined in this class: +-- +-- * **Start**: The process is started. The process will go into the Report state. +-- * **Report**: The process is reporting to the player the route to be followed. +-- * **Route**: The process is routing the controllable. +-- * **Pause**: The process is pausing the route of the controllable. +-- * **Arrive**: The controllable has arrived at a route point. +-- * **More**: There are more route points that need to be followed. The process will go back into the Report state. +-- * **NoMore**: There are no more route points that need to be followed. The process will go into the Success state. +-- +-- ### ACT_ROUTE **Event methods**: +-- +-- Event methods are available (dynamically allocated by the state machine), that accomodate for state transitions occurring in the process. +-- There are two types of event methods, which you can use to influence the normal mechanisms in the state machine: +-- +-- * **Immediate**: The event method has exactly the name of the event. +-- * **Delayed**: The event method starts with a __ + the name of the event. The first parameter of the event method is a number value, expressing the delay in seconds when the event will be executed. +-- +-- ### ACT_ROUTE **States**: +-- +-- * **None**: The controllable did not receive route commands. +-- * **Arrived (*)**: The controllable has arrived at a route point. +-- * **Aborted (*)**: The controllable has aborted the route path. +-- * **Routing**: The controllable is understay to the route point. +-- * **Pausing**: The process is pausing the routing. AI air will go into hover, AI ground will stop moving. Players can fly around. +-- * **Success (*)**: All route points were reached. +-- * **Failed (*)**: The process has failed. +-- +-- (*) End states of the process. +-- +-- ### ACT_ROUTE state transition methods: +-- +-- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. +-- There are 2 moments when state transition methods will be called by the state machine: +-- +-- * **Before** the state transition. +-- The state transition method needs to start with the name **OnBefore + the name of the state**. +-- If the state transition method returns false, then the processing of the state transition will not be done! +-- If you want to change the behaviour of the AIControllable at this event, return false, +-- but then you'll need to specify your own logic using the AIControllable! +-- +-- * **After** the state transition. +-- The state transition method needs to start with the name **OnAfter + the name of the state**. +-- These state transition methods need to provide a return value, which is specified at the function description. +-- +-- === +-- +-- # 1) @{#ACT_ROUTE_ZONE} class, extends @{Fsm.Route#ACT_ROUTE} +-- +-- The ACT_ROUTE_ZONE class implements the core functions to route an AIR @{Controllable} player @{Unit} to a @{Zone}. +-- The player receives on perioding times messages with the coordinates of the route to follow. +-- Upon arrival at the zone, a confirmation of arrival is sent, and the process will be ended. +-- +-- # 1.1) ACT_ROUTE_ZONE constructor: +-- +-- * @{#ACT_ROUTE_ZONE.New}(): Creates a new ACT_ROUTE_ZONE object. +-- +-- === +-- +-- @module Route + + +do -- ACT_ROUTE + + --- ACT_ROUTE class + -- @type ACT_ROUTE + -- @field Tasking.Task#TASK TASK + -- @field Wrapper.Unit#UNIT ProcessUnit + -- @field Core.Zone#ZONE_BASE TargetZone + -- @extends Core.Fsm#FSM_PROCESS + ACT_ROUTE = { + ClassName = "ACT_ROUTE", + } + + + --- Creates a new routing state machine. The process will route a CLIENT to a ZONE until the CLIENT is within that ZONE. + -- @param #ACT_ROUTE self + -- @return #ACT_ROUTE self + function ACT_ROUTE:New() + + -- Inherits from BASE + local self = BASE:Inherit( self, FSM_PROCESS:New( "ACT_ROUTE" ) ) -- Core.Fsm#FSM_PROCESS + + self:AddTransition( "None", "Start", "Routing" ) + self:AddTransition( "*", "Report", "Reporting" ) + self:AddTransition( "*", "Route", "Routing" ) + self:AddTransition( "Routing", "Pause", "Pausing" ) + self:AddTransition( "*", "Abort", "Aborted" ) + self:AddTransition( "Routing", "Arrive", "Arrived" ) + self:AddTransition( "Arrived", "Success", "Success" ) + self:AddTransition( "*", "Fail", "Failed" ) + self:AddTransition( "", "", "" ) + self:AddTransition( "", "", "" ) + + self:AddEndState( "Arrived" ) + self:AddEndState( "Failed" ) + + self:SetStartState( "None" ) + + return self + end + + --- Task Events + + --- StateMachine callback function + -- @param #ACT_ROUTE self + -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ROUTE:onafterStart( ProcessUnit, From, Event, To ) + + + self:__Route( 1 ) + end + + --- Check if the controllable has arrived. + -- @param #ACT_ROUTE self + -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit + -- @return #boolean + function ACT_ROUTE:onfuncHasArrived( ProcessUnit ) + return false + end + + --- StateMachine callback function + -- @param #ACT_ROUTE self + -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ROUTE:onbeforeRoute( ProcessUnit, From, Event, To ) + self:F( { "BeforeRoute 1", self.DisplayCount, self.DisplayInterval } ) + + if ProcessUnit:IsAlive() then + self:F( "BeforeRoute 2" ) + local HasArrived = self:onfuncHasArrived( ProcessUnit ) -- Polymorphic + if self.DisplayCount >= self.DisplayInterval then + self:T( { HasArrived = HasArrived } ) + if not HasArrived then + self:Report() + end + self.DisplayCount = 1 + else + self.DisplayCount = self.DisplayCount + 1 + end + + self:T( { DisplayCount = self.DisplayCount } ) + + if HasArrived then + self:__Arrive( 1 ) + else + self:__Route( 1 ) + end + + return HasArrived -- if false, then the event will not be executed... + end + + return false + + end + +end -- ACT_ROUTE + + + +do -- ACT_ROUTE_ZONE + + --- ACT_ROUTE_ZONE class + -- @type ACT_ROUTE_ZONE + -- @field Tasking.Task#TASK TASK + -- @field Wrapper.Unit#UNIT ProcessUnit + -- @field Core.Zone#ZONE_BASE TargetZone + -- @extends #ACT_ROUTE + ACT_ROUTE_ZONE = { + ClassName = "ACT_ROUTE_ZONE", + } + + + --- Creates a new routing state machine. The task will route a controllable to a ZONE until the controllable is within that ZONE. + -- @param #ACT_ROUTE_ZONE self + -- @param Core.Zone#ZONE_BASE TargetZone + function ACT_ROUTE_ZONE:New( TargetZone ) + local self = BASE:Inherit( self, ACT_ROUTE:New() ) -- #ACT_ROUTE_ZONE + + self.TargetZone = TargetZone + + self.DisplayInterval = 30 + self.DisplayCount = 30 + self.DisplayMessage = true + self.DisplayTime = 10 -- 10 seconds is the default + + return self + end + + function ACT_ROUTE_ZONE:Init( FsmRoute ) + + self.TargetZone = FsmRoute.TargetZone + + self.DisplayInterval = 30 + self.DisplayCount = 30 + self.DisplayMessage = true + self.DisplayTime = 10 -- 10 seconds is the default + end + + --- Method override to check if the controllable has arrived. + -- @param #ACT_ROUTE self + -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit + -- @return #boolean + function ACT_ROUTE_ZONE:onfuncHasArrived( ProcessUnit ) + + if ProcessUnit:IsInZone( self.TargetZone ) then + local RouteText = "You have arrived within the zone." + self:Message( RouteText ) + end + + return ProcessUnit:IsInZone( self.TargetZone ) + end + + --- Task Events + + --- StateMachine callback function + -- @param #ACT_ROUTE_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ROUTE_ZONE:onenterReporting( ProcessUnit, From, Event, To ) + + local ZoneVec2 = self.TargetZone:GetVec2() + local ZonePointVec2 = POINT_VEC2:New( ZoneVec2.x, ZoneVec2.y ) + local TaskUnitVec2 = ProcessUnit:GetVec2() + local TaskUnitPointVec2 = POINT_VEC2:New( TaskUnitVec2.x, TaskUnitVec2.y ) + local RouteText = "Route to " .. TaskUnitPointVec2:GetBRText( ZonePointVec2 ) .. " km to target." + self:Message( RouteText ) + end + +end -- ACT_ROUTE_ZONE +--- (SP) (MP) (FSM) Account for (Detect, count and report) DCS events occuring on DCS objects (units). +-- +-- === +-- +-- # @{#ACT_ACCOUNT} FSM class, extends @{Fsm#FSM_PROCESS} +-- +-- ## ACT_ACCOUNT state machine: +-- +-- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur. +-- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below. +-- Each derived class follows exactly the same process, using the same events and following the same state transitions, +-- but will have **different implementation behaviour** upon each event or state transition. +-- +-- ### ACT_ACCOUNT **Events**: +-- +-- These are the events defined in this class: +-- +-- * **Start**: The process is started. The process will go into the Report state. +-- * **Event**: A relevant event has occured that needs to be accounted for. The process will go into the Account state. +-- * **Report**: The process is reporting to the player the accounting status of the DCS events. +-- * **More**: There are more DCS events that need to be accounted for. The process will go back into the Report state. +-- * **NoMore**: There are no more DCS events that need to be accounted for. The process will go into the Success state. +-- +-- ### ACT_ACCOUNT **Event methods**: +-- +-- Event methods are available (dynamically allocated by the state machine), that accomodate for state transitions occurring in the process. +-- There are two types of event methods, which you can use to influence the normal mechanisms in the state machine: +-- +-- * **Immediate**: The event method has exactly the name of the event. +-- * **Delayed**: The event method starts with a __ + the name of the event. The first parameter of the event method is a number value, expressing the delay in seconds when the event will be executed. +-- +-- ### ACT_ACCOUNT **States**: +-- +-- * **Assigned**: The player is assigned to the task. This is the initialization state for the process. +-- * **Waiting**: the process is waiting for a DCS event to occur within the simulator. This state is set automatically. +-- * **Report**: The process is Reporting to the players in the group of the unit. This state is set automatically every 30 seconds. +-- * **Account**: The relevant DCS event has occurred, and is accounted for. +-- * **Success (*)**: All DCS events were accounted for. +-- * **Failed (*)**: The process has failed. +-- +-- (*) End states of the process. +-- +-- ### ACT_ACCOUNT state transition methods: +-- +-- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. +-- There are 2 moments when state transition methods will be called by the state machine: +-- +-- * **Before** the state transition. +-- The state transition method needs to start with the name **OnBefore + the name of the state**. +-- If the state transition method returns false, then the processing of the state transition will not be done! +-- If you want to change the behaviour of the AIControllable at this event, return false, +-- but then you'll need to specify your own logic using the AIControllable! +-- +-- * **After** the state transition. +-- The state transition method needs to start with the name **OnAfter + the name of the state**. +-- These state transition methods need to provide a return value, which is specified at the function description. +-- +-- # 1) @{#ACT_ACCOUNT_DEADS} FSM class, extends @{Fsm.Account#ACT_ACCOUNT} +-- +-- The ACT_ACCOUNT_DEADS class accounts (detects, counts and reports) successful kills of DCS units. +-- The process is given a @{Set} of units that will be tracked upon successful destruction. +-- The process will end after each target has been successfully destroyed. +-- Each successful dead will trigger an Account state transition that can be scored, modified or administered. +-- +-- +-- ## ACT_ACCOUNT_DEADS constructor: +-- +-- * @{#ACT_ACCOUNT_DEADS.New}(): Creates a new ACT_ACCOUNT_DEADS object. +-- +-- === +-- +-- @module Account + + +do -- ACT_ACCOUNT + + --- ACT_ACCOUNT class + -- @type ACT_ACCOUNT + -- @field Set#SET_UNIT TargetSetUnit + -- @extends Core.Fsm#FSM_PROCESS + ACT_ACCOUNT = { + ClassName = "ACT_ACCOUNT", + TargetSetUnit = nil, + } + + --- Creates a new DESTROY process. + -- @param #ACT_ACCOUNT self + -- @return #ACT_ACCOUNT + function ACT_ACCOUNT:New() + + -- Inherits from BASE + local self = BASE:Inherit( self, FSM_PROCESS:New() ) -- Core.Fsm#FSM_PROCESS + + self:AddTransition( "Assigned", "Start", "Waiting") + self:AddTransition( "*", "Wait", "Waiting") + self:AddTransition( "*", "Report", "Report") + self:AddTransition( "*", "Event", "Account") + self:AddTransition( "Account", "More", "Wait") + self:AddTransition( "Account", "NoMore", "Accounted") + self:AddTransition( "*", "Fail", "Failed") + + self:AddEndState( "Accounted" ) + self:AddEndState( "Failed" ) + + self:SetStartState( "Assigned" ) + + return self + end + + --- Process Events + + --- StateMachine callback function + -- @param #ACT_ACCOUNT self + -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ACCOUNT:onafterStart( ProcessUnit, From, Event, To ) + + self:HandleEvent( EVENTS.Dead, self.onfuncEventDead ) + + self:__Wait( 1 ) + end + + + --- StateMachine callback function + -- @param #ACT_ACCOUNT self + -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ACCOUNT:onenterWaiting( ProcessUnit, From, Event, To ) + + if self.DisplayCount >= self.DisplayInterval then + self:Report() + self.DisplayCount = 1 + else + self.DisplayCount = self.DisplayCount + 1 + end + + return true -- Process always the event. + end + + --- StateMachine callback function + -- @param #ACT_ACCOUNT self + -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ACCOUNT:onafterEvent( ProcessUnit, From, Event, To, Event ) + + self:__NoMore( 1 ) + end + +end -- ACT_ACCOUNT + +do -- ACT_ACCOUNT_DEADS + + --- ACT_ACCOUNT_DEADS class + -- @type ACT_ACCOUNT_DEADS + -- @field Set#SET_UNIT TargetSetUnit + -- @extends #ACT_ACCOUNT + ACT_ACCOUNT_DEADS = { + ClassName = "ACT_ACCOUNT_DEADS", + TargetSetUnit = nil, + } + + + --- Creates a new DESTROY process. + -- @param #ACT_ACCOUNT_DEADS self + -- @param Set#SET_UNIT TargetSetUnit + -- @param #string TaskName + function ACT_ACCOUNT_DEADS:New( TargetSetUnit, TaskName ) + -- Inherits from BASE + local self = BASE:Inherit( self, ACT_ACCOUNT:New() ) -- #ACT_ACCOUNT_DEADS + + self.TargetSetUnit = TargetSetUnit + self.TaskName = TaskName + + self.DisplayInterval = 30 + self.DisplayCount = 30 + self.DisplayMessage = true + self.DisplayTime = 10 -- 10 seconds is the default + self.DisplayCategory = "HQ" -- Targets is the default display category + + return self + end + + function ACT_ACCOUNT_DEADS:Init( FsmAccount ) + + self.TargetSetUnit = FsmAccount.TargetSetUnit + self.TaskName = FsmAccount.TaskName + end + + + + function ACT_ACCOUNT_DEADS:_Destructor() + self:E("_Destructor") + + self:EventRemoveAll() + + end + + --- Process Events + + --- StateMachine callback function + -- @param #ACT_ACCOUNT_DEADS self + -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ACCOUNT_DEADS:onenterReport( ProcessUnit, From, Event, To ) + self:E( { ProcessUnit, From, Event, To } ) + + self:Message( "Your group with assigned " .. self.TaskName .. " task has " .. self.TargetSetUnit:GetUnitTypesText() .. " targets left to be destroyed." ) + end + + + --- StateMachine callback function + -- @param #ACT_ACCOUNT_DEADS self + -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ACCOUNT_DEADS:onenterAccount( ProcessUnit, From, Event, To, EventData ) + self:T( { ProcessUnit, EventData, From, Event, To } ) + + self:T({self.Controllable}) + + self.TargetSetUnit:Flush() + + if self.TargetSetUnit:FindUnit( EventData.IniUnitName ) then + local TaskGroup = ProcessUnit:GetGroup() + self.TargetSetUnit:RemoveUnitsByName( EventData.IniUnitName ) + self:Message( "You hit a target. Your group with assigned " .. self.TaskName .. " task has " .. self.TargetSetUnit:Count() .. " targets ( " .. self.TargetSetUnit:GetUnitTypesText() .. " ) left to be destroyed." ) + end + end + + --- StateMachine callback function + -- @param #ACT_ACCOUNT_DEADS self + -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ACCOUNT_DEADS:onafterEvent( ProcessUnit, From, Event, To, EventData ) + + if self.TargetSetUnit:Count() > 0 then + self:__More( 1 ) + else + self:__NoMore( 1 ) + end + end + + --- DCS Events + + --- @param #ACT_ACCOUNT_DEADS self + -- @param Event#EVENTDATA EventData + function ACT_ACCOUNT_DEADS:onfuncEventDead( EventData ) + self:T( { "EventDead", EventData } ) + + if EventData.IniDCSUnit then + self:__Event( 1, EventData ) + end + end + +end -- ACT_ACCOUNT DEADS +--- (SP) (MP) (FSM) Route AI or players through waypoints or to zones. +-- +-- === +-- +-- # @{#ACT_ASSIST} FSM class, extends @{Fsm#FSM_PROCESS} +-- +-- ## ACT_ASSIST state machine: +-- +-- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur. +-- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below. +-- Each derived class follows exactly the same process, using the same events and following the same state transitions, +-- but will have **different implementation behaviour** upon each event or state transition. +-- +-- ### ACT_ASSIST **Events**: +-- +-- These are the events defined in this class: +-- +-- * **Start**: The process is started. +-- * **Next**: The process is smoking the targets in the given zone. +-- +-- ### ACT_ASSIST **Event methods**: +-- +-- Event methods are available (dynamically allocated by the state machine), that accomodate for state transitions occurring in the process. +-- There are two types of event methods, which you can use to influence the normal mechanisms in the state machine: +-- +-- * **Immediate**: The event method has exactly the name of the event. +-- * **Delayed**: The event method starts with a __ + the name of the event. The first parameter of the event method is a number value, expressing the delay in seconds when the event will be executed. +-- +-- ### ACT_ASSIST **States**: +-- +-- * **None**: The controllable did not receive route commands. +-- * **AwaitSmoke (*)**: The process is awaiting to smoke the targets in the zone. +-- * **Smoking (*)**: The process is smoking the targets in the zone. +-- * **Failed (*)**: The process has failed. +-- +-- (*) End states of the process. +-- +-- ### ACT_ASSIST state transition methods: +-- +-- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. +-- There are 2 moments when state transition methods will be called by the state machine: +-- +-- * **Before** the state transition. +-- The state transition method needs to start with the name **OnBefore + the name of the state**. +-- If the state transition method returns false, then the processing of the state transition will not be done! +-- If you want to change the behaviour of the AIControllable at this event, return false, +-- but then you'll need to specify your own logic using the AIControllable! +-- +-- * **After** the state transition. +-- The state transition method needs to start with the name **OnAfter + the name of the state**. +-- These state transition methods need to provide a return value, which is specified at the function description. +-- +-- === +-- +-- # 1) @{#ACT_ASSIST_SMOKE_TARGETS_ZONE} class, extends @{Fsm.Route#ACT_ASSIST} +-- +-- The ACT_ASSIST_SMOKE_TARGETS_ZONE class implements the core functions to smoke targets in a @{Zone}. +-- The targets are smoked within a certain range around each target, simulating a realistic smoking behaviour. +-- At random intervals, a new target is smoked. +-- +-- # 1.1) ACT_ASSIST_SMOKE_TARGETS_ZONE constructor: +-- +-- * @{#ACT_ASSIST_SMOKE_TARGETS_ZONE.New}(): Creates a new ACT_ASSIST_SMOKE_TARGETS_ZONE object. +-- +-- === +-- +-- @module Smoke + +do -- ACT_ASSIST + + --- ACT_ASSIST class + -- @type ACT_ASSIST + -- @extends Core.Fsm#FSM_PROCESS + ACT_ASSIST = { + ClassName = "ACT_ASSIST", + } + + --- Creates a new target smoking state machine. The process will request from the menu if it accepts the task, if not, the unit is removed from the simulator. + -- @param #ACT_ASSIST self + -- @return #ACT_ASSIST + function ACT_ASSIST:New() + + -- Inherits from BASE + local self = BASE:Inherit( self, FSM_PROCESS:New( "ACT_ASSIST" ) ) -- Core.Fsm#FSM_PROCESS + + self:AddTransition( "None", "Start", "AwaitSmoke" ) + self:AddTransition( "AwaitSmoke", "Next", "Smoking" ) + self:AddTransition( "Smoking", "Next", "AwaitSmoke" ) + self:AddTransition( "*", "Stop", "Success" ) + self:AddTransition( "*", "Fail", "Failed" ) + + self:AddEndState( "Failed" ) + self:AddEndState( "Success" ) + + self:SetStartState( "None" ) + + return self + end + + --- Task Events + + --- StateMachine callback function + -- @param #ACT_ASSIST self + -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ASSIST:onafterStart( ProcessUnit, From, Event, To ) + + local ProcessGroup = ProcessUnit:GetGroup() + local MissionMenu = self:GetMission():GetMissionMenu( ProcessGroup ) + + local function MenuSmoke( MenuParam ) + self:E( MenuParam ) + local self = MenuParam.self + local SmokeColor = MenuParam.SmokeColor + self.SmokeColor = SmokeColor + self:__Next( 1 ) + end + + self.Menu = MENU_GROUP:New( ProcessGroup, "Target acquisition", MissionMenu ) + self.MenuSmokeBlue = MENU_GROUP_COMMAND:New( ProcessGroup, "Drop blue smoke on targets", self.Menu, MenuSmoke, { self = self, SmokeColor = SMOKECOLOR.Blue } ) + self.MenuSmokeGreen = MENU_GROUP_COMMAND:New( ProcessGroup, "Drop green smoke on targets", self.Menu, MenuSmoke, { self = self, SmokeColor = SMOKECOLOR.Green } ) + self.MenuSmokeOrange = MENU_GROUP_COMMAND:New( ProcessGroup, "Drop Orange smoke on targets", self.Menu, MenuSmoke, { self = self, SmokeColor = SMOKECOLOR.Orange } ) + self.MenuSmokeRed = MENU_GROUP_COMMAND:New( ProcessGroup, "Drop Red smoke on targets", self.Menu, MenuSmoke, { self = self, SmokeColor = SMOKECOLOR.Red } ) + self.MenuSmokeWhite = MENU_GROUP_COMMAND:New( ProcessGroup, "Drop White smoke on targets", self.Menu, MenuSmoke, { self = self, SmokeColor = SMOKECOLOR.White } ) + end + +end + +do -- ACT_ASSIST_SMOKE_TARGETS_ZONE + + --- ACT_ASSIST_SMOKE_TARGETS_ZONE class + -- @type ACT_ASSIST_SMOKE_TARGETS_ZONE + -- @field Set#SET_UNIT TargetSetUnit + -- @field Core.Zone#ZONE_BASE TargetZone + -- @extends #ACT_ASSIST + ACT_ASSIST_SMOKE_TARGETS_ZONE = { + ClassName = "ACT_ASSIST_SMOKE_TARGETS_ZONE", + } + +-- function ACT_ASSIST_SMOKE_TARGETS_ZONE:_Destructor() +-- self:E("_Destructor") +-- +-- self.Menu:Remove() +-- self:EventRemoveAll() +-- end + + --- Creates a new target smoking state machine. The process will request from the menu if it accepts the task, if not, the unit is removed from the simulator. + -- @param #ACT_ASSIST_SMOKE_TARGETS_ZONE self + -- @param Set#SET_UNIT TargetSetUnit + -- @param Core.Zone#ZONE_BASE TargetZone + function ACT_ASSIST_SMOKE_TARGETS_ZONE:New( TargetSetUnit, TargetZone ) + local self = BASE:Inherit( self, ACT_ASSIST:New() ) -- #ACT_ASSIST + + self.TargetSetUnit = TargetSetUnit + self.TargetZone = TargetZone + + return self + end + + function ACT_ASSIST_SMOKE_TARGETS_ZONE:Init( FsmSmoke ) + + self.TargetSetUnit = FsmSmoke.TargetSetUnit + self.TargetZone = FsmSmoke.TargetZone + end + + --- Creates a new target smoking state machine. The process will request from the menu if it accepts the task, if not, the unit is removed from the simulator. + -- @param #ACT_ASSIST_SMOKE_TARGETS_ZONE self + -- @param Set#SET_UNIT TargetSetUnit + -- @param Core.Zone#ZONE_BASE TargetZone + -- @return #ACT_ASSIST_SMOKE_TARGETS_ZONE self + function ACT_ASSIST_SMOKE_TARGETS_ZONE:Init( TargetSetUnit, TargetZone ) + + self.TargetSetUnit = TargetSetUnit + self.TargetZone = TargetZone + + return self + end + + --- StateMachine callback function + -- @param #ACT_ASSIST_SMOKE_TARGETS_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ASSIST_SMOKE_TARGETS_ZONE:onenterSmoking( ProcessUnit, From, Event, To ) + + self.TargetSetUnit:ForEachUnit( + --- @param Wrapper.Unit#UNIT SmokeUnit + function( SmokeUnit ) + if math.random( 1, ( 100 * self.TargetSetUnit:Count() ) / 4 ) <= 100 then + SCHEDULER:New( self, + function() + if SmokeUnit:IsAlive() then + SmokeUnit:Smoke( self.SmokeColor, 150 ) + end + end, {}, math.random( 10, 60 ) + ) + end + end + ) + + end + +end--- A COMMANDCENTER is the owner of multiple missions within MOOSE. +-- A COMMANDCENTER governs multiple missions, the tasking and the reporting. +-- @module CommandCenter + + + +--- The REPORT class +-- @type REPORT +-- @extends Core.Base#BASE +REPORT = { + ClassName = "REPORT", +} + +--- Create a new REPORT. +-- @param #REPORT self +-- @param #string Title +-- @return #REPORT +function REPORT:New( Title ) + + local self = BASE:Inherit( self, BASE:New() ) + + self.Report = {} + self.Report[#self.Report+1] = Title + + return self +end + +--- Add a new line to a REPORT. +-- @param #REPORT self +-- @param #string Text +-- @return #REPORT +function REPORT:Add( Text ) + self.Report[#self.Report+1] = Text + return self.Report[#self.Report+1] +end + +function REPORT:Text() + return table.concat( self.Report, "\n" ) +end + +--- The COMMANDCENTER class +-- @type COMMANDCENTER +-- @field Wrapper.Group#GROUP HQ +-- @field Dcs.DCSCoalitionWrapper.Object#coalition CommandCenterCoalition +-- @list Missions +-- @extends Core.Base#BASE +COMMANDCENTER = { + ClassName = "COMMANDCENTER", + CommandCenterName = "", + CommandCenterCoalition = nil, + CommandCenterPositionable = nil, + Name = "", +} +--- The constructor takes an IDENTIFIABLE as the HQ command center. +-- @param #COMMANDCENTER self +-- @param Wrapper.Positionable#POSITIONABLE CommandCenterPositionable +-- @param #string CommandCenterName +-- @return #COMMANDCENTER +function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName ) + + local self = BASE:Inherit( self, BASE:New() ) + + self.CommandCenterPositionable = CommandCenterPositionable + self.CommandCenterName = CommandCenterName or CommandCenterPositionable:GetName() + self.CommandCenterCoalition = CommandCenterPositionable:GetCoalition() + + self.Missions = {} + + self:HandleEvent( EVENTS.Birth, + --- @param #COMMANDCENTER self + --- @param Core.Event#EVENTDATA EventData + function( self, EventData ) + self:E( { EventData } ) + local EventGroup = GROUP:Find( EventData.IniDCSGroup ) + if EventGroup and self:HasGroup( EventGroup ) then + local MenuReporting = MENU_GROUP:New( EventGroup, "Reporting", self.CommandCenterMenu ) + local MenuMissionsSummary = MENU_GROUP_COMMAND:New( EventGroup, "Missions Summary Report", MenuReporting, self.ReportSummary, self, EventGroup ) + local MenuMissionsDetails = MENU_GROUP_COMMAND:New( EventGroup, "Missions Details Report", MenuReporting, self.ReportDetails, self, EventGroup ) + self:ReportSummary( EventGroup ) + end + local PlayerUnit = EventData.IniUnit + for MissionID, Mission in pairs( self:GetMissions() ) do + local Mission = Mission -- Tasking.Mission#MISSION + local PlayerGroup = EventData.IniGroup -- The GROUP object should be filled! + Mission:JoinUnit( PlayerUnit, PlayerGroup ) + Mission:ReportDetails() + end + + end + ) + + -- When a player enters a client or a unit, the CommandCenter will check for each Mission and each Task in the Mission if the player has things to do. + -- For these elements, it will= + -- - Set the correct menu. + -- - Assign the PlayerUnit to the Task if required. + -- - Send a message to the other players in the group that this player has joined. + self:HandleEvent( EVENTS.PlayerEnterUnit, + --- @param #COMMANDCENTER self + -- @param Core.Event#EVENTDATA EventData + function( self, EventData ) + local PlayerUnit = EventData.IniUnit + for MissionID, Mission in pairs( self:GetMissions() ) do + local Mission = Mission -- Tasking.Mission#MISSION + local PlayerGroup = EventData.IniGroup -- The GROUP object should be filled! + Mission:JoinUnit( PlayerUnit, PlayerGroup ) + Mission:ReportDetails() + end + end + ) + + -- Handle when a player leaves a slot and goes back to spectators ... + -- The PlayerUnit will be UnAssigned from the Task. + -- When there is no Unit left running the Task, the Task goes into Abort... + self:HandleEvent( EVENTS.PlayerLeaveUnit, + --- @param #TASK self + -- @param Core.Event#EVENTDATA EventData + function( self, EventData ) + local PlayerUnit = EventData.IniUnit + for MissionID, Mission in pairs( self:GetMissions() ) do + local Mission = Mission -- Tasking.Mission#MISSION + Mission:AbortUnit( PlayerUnit ) + end + end + ) + + -- Handle when a player leaves a slot and goes back to spectators ... + -- The PlayerUnit will be UnAssigned from the Task. + -- When there is no Unit left running the Task, the Task goes into Abort... + self:HandleEvent( EVENTS.Crash, + --- @param #TASK self + -- @param Core.Event#EVENTDATA EventData + function( self, EventData ) + local PlayerUnit = EventData.IniUnit + for MissionID, Mission in pairs( self:GetMissions() ) do + Mission:CrashUnit( PlayerUnit ) + end + end + ) + + return self +end + +--- Gets the name of the HQ command center. +-- @param #COMMANDCENTER self +-- @return #string +function COMMANDCENTER:GetName() + + return self.CommandCenterName +end + +--- Gets the POSITIONABLE of the HQ command center. +-- @param #COMMANDCENTER self +-- @return Wrapper.Positionable#POSITIONABLE +function COMMANDCENTER:GetPositionable() + return self.CommandCenterPositionable +end + +--- Get the Missions governed by the HQ command center. +-- @param #COMMANDCENTER self +-- @return #list +function COMMANDCENTER:GetMissions() + + return self.Missions +end + +--- Add a MISSION to be governed by the HQ command center. +-- @param #COMMANDCENTER self +-- @param Tasking.Mission#MISSION Mission +-- @return Tasking.Mission#MISSION +function COMMANDCENTER:AddMission( Mission ) + + self.Missions[Mission] = Mission + + return Mission +end + +--- Removes a MISSION to be governed by the HQ command center. +-- The given Mission is not nilified. +-- @param #COMMANDCENTER self +-- @param Tasking.Mission#MISSION Mission +-- @return Tasking.Mission#MISSION +function COMMANDCENTER:RemoveMission( Mission ) + + self.Missions[Mission] = nil + + return Mission +end + +--- Sets the menu structure of the Missions governed by the HQ command center. +-- @param #COMMANDCENTER self +function COMMANDCENTER:SetMenu() + self:F() + + self.CommandCenterMenu = self.CommandCenterMenu or MENU_COALITION:New( self.CommandCenterCoalition, "Command Center (" .. self:GetName() .. ")" ) + + for MissionID, Mission in pairs( self:GetMissions() ) do + local Mission = Mission -- Tasking.Mission#MISSION + Mission:RemoveMenu() + end + + for MissionID, Mission in pairs( self:GetMissions() ) do + local Mission = Mission -- Tasking.Mission#MISSION + Mission:SetMenu() + end +end + + +--- Checks of the COMMANDCENTER has a GROUP. +-- @param #COMMANDCENTER self +-- @param Wrapper.Group#GROUP +-- @return #boolean +function COMMANDCENTER:HasGroup( MissionGroup ) + + local Has = false + + for MissionID, Mission in pairs( self.Missions ) do + local Mission = Mission -- Tasking.Mission#MISSION + if Mission:HasGroup( MissionGroup ) then + Has = true + break + end + end + + return Has +end + +--- Send a CC message to a GROUP. +-- @param #COMMANDCENTER self +-- @param #string Message +-- @param Wrapper.Group#GROUP TaskGroup +-- @param #sring Name (optional) The name of the Group used as a prefix for the message to the Group. If not provided, there will be nothing shown. +function COMMANDCENTER:MessageToGroup( Message, TaskGroup, Name ) + + local Prefix = Name and "@ Group (" .. Name .. "): " or '' + Message = Prefix .. Message + self:GetPositionable():MessageToGroup( Message , 20, TaskGroup, self:GetName() ) + +end + +--- Send a CC message to the coalition of the CC. +-- @param #COMMANDCENTER self +function COMMANDCENTER:MessageToCoalition( Message ) + + local CCCoalition = self:GetPositionable():GetCoalition() + --TODO: Fix coalition bug! + self:GetPositionable():MessageToCoalition( Message, 20, CCCoalition, self:GetName() ) + +end + +--- Report the status of all MISSIONs to a GROUP. +-- Each Mission is listed, with an indication how many Tasks are still to be completed. +-- @param #COMMANDCENTER self +function COMMANDCENTER:ReportSummary( ReportGroup ) + self:E( ReportGroup ) + + local Report = REPORT:New() + + for MissionID, Mission in pairs( self.Missions ) do + local Mission = Mission -- Tasking.Mission#MISSION + Report:Add( " - " .. Mission:ReportOverview() ) + end + + self:GetPositionable():MessageToGroup( Report:Text(), 30, ReportGroup ) + +end + +--- Report the status of a Task to a Group. +-- Report the details of a Mission, listing the Mission, and all the Task details. +-- @param #COMMANDCENTER self +function COMMANDCENTER:ReportDetails( ReportGroup, Task ) + self:E( ReportGroup ) + + local Report = REPORT:New() + + for MissionID, Mission in pairs( self.Missions ) do + local Mission = Mission -- Tasking.Mission#MISSION + Report:Add( " - " .. Mission:ReportDetails() ) + end + + self:GetPositionable():MessageToGroup( Report:Text(), 30, ReportGroup ) +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 +-- @field #MISSION.Clients _Clients +-- @field Core.Menu#MENU_COALITION MissionMenu +-- @field #string MissionBriefing +-- @extends Core.Fsm#FSM +MISSION = { + ClassName = "MISSION", + Name = "", + MissionStatus = "PENDING", + _Clients = {}, + TaskMenus = {}, + TaskCategoryMenus = {}, + TaskTypeMenus = {}, + _ActiveTasks = {}, + GoalFunction = nil, + MissionReportTrigger = 0, + MissionProgressTrigger = 0, + MissionReportShow = false, + MissionReportFlash = false, + MissionTimeInterval = 0, + MissionCoalition = "", + SUCCESS = 1, + FAILED = 2, + REPEAT = 3, + _GoalTasks = {} +} + +--- This is the main MISSION declaration method. Each Mission is like the master or a Mission orchestration between, Clients, Tasks, Stages etc. +-- @param #MISSION self +-- @param Tasking.CommandCenter#COMMANDCENTER CommandCenter +-- @param #string MissionName is the name of the mission. This name will be used to reference the status of each mission by the players. +-- @param #string MissionPriority is a string indicating the "priority" of the Mission. f.e. "Primary", "Secondary" or "First", "Second". It is free format and up to the Mission designer to choose. There are no rules behind this field. +-- @param #string MissionBriefing is a string indicating the mission briefing to be shown when a player joins a @{CLIENT}. +-- @param Dcs.DCSCoalitionWrapper.Object#coalition MissionCoalition is a string indicating the coalition or party to which this mission belongs to. It is free format and can be chosen freely by the mission designer. Note that this field is not to be confused with the coalition concept of the ME. Examples of a Mission Coalition could be "NATO", "CCCP", "Intruders", "Terrorists"... +-- @return #MISSION self +function MISSION:New( CommandCenter, MissionName, MissionPriority, MissionBriefing, MissionCoalition ) + + local self = BASE:Inherit( self, FSM:New() ) -- Core.Fsm#FSM + + self:SetStartState( "Idle" ) + + self:AddTransition( "Idle", "Start", "Ongoing" ) + self:AddTransition( "Ongoing", "Stop", "Idle" ) + self:AddTransition( "Ongoing", "Complete", "Completed" ) + self:AddTransition( "*", "Fail", "Failed" ) + + self:T( { MissionName, MissionPriority, MissionBriefing, MissionCoalition } ) + + self.CommandCenter = CommandCenter + CommandCenter:AddMission( self ) + + self.Name = MissionName + self.MissionPriority = MissionPriority + self.MissionBriefing = MissionBriefing + self.MissionCoalition = MissionCoalition + + self.Tasks = {} + + return self +end + +--- FSM function for a MISSION +-- @param #MISSION self +-- @param #string Event +-- @param #string From +-- @param #string To +function MISSION:onbeforeComplete( From, Event, To ) + + for TaskID, Task in pairs( self:GetTasks() ) do + local Task = Task -- Tasking.Task#TASK + if not Task:IsStateSuccess() and not Task:IsStateFailed() and not Task:IsStateAborted() and not Task:IsStateCancelled() then + return false -- Mission cannot be completed. Other Tasks are still active. + end + end + return true -- Allow Mission completion. +end + +--- FSM function for a MISSION +-- @param #MISSION self +-- @param #string Event +-- @param #string From +-- @param #string To +function MISSION:onenterCompleted( From, Event, To ) + + self:GetCommandCenter():MessageToCoalition( "Mission " .. self:GetName() .. " has been completed! Good job guys!" ) +end + +--- Gets the mission name. +-- @param #MISSION self +-- @return #MISSION self +function MISSION:GetName() + return self.Name +end + +--- Add a Unit to join the Mission. +-- For each Task within the Mission, the Unit is joined with the Task. +-- If the Unit was not part of a Task in the Mission, false is returned. +-- If the Unit is part of a Task in the Mission, true is returned. +-- @param #MISSION self +-- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player joining the Mission. +-- @param Wrapper.Group#GROUP PlayerGroup The GROUP of the player joining the Mission. +-- @return #boolean true if Unit is part of a Task in the Mission. +function MISSION:JoinUnit( PlayerUnit, PlayerGroup ) + self:F( { PlayerUnit = PlayerUnit, PlayerGroup = PlayerGroup } ) + + local PlayerUnitAdded = false + + for TaskID, Task in pairs( self:GetTasks() ) do + local Task = Task -- Tasking.Task#TASK + if Task:JoinUnit( PlayerUnit, PlayerGroup ) then + PlayerUnitAdded = true + end + end + + return PlayerUnitAdded +end + +--- Aborts a PlayerUnit from the Mission. +-- For each Task within the Mission, the PlayerUnit is removed from Task where it is assigned. +-- If the Unit was not part of a Task in the Mission, false is returned. +-- If the Unit is part of a Task in the Mission, true is returned. +-- @param #MISSION self +-- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player joining the Mission. +-- @return #boolean true if Unit is part of a Task in the Mission. +function MISSION:AbortUnit( PlayerUnit ) + self:F( { PlayerUnit = PlayerUnit } ) + + local PlayerUnitRemoved = false + + for TaskID, Task in pairs( self:GetTasks() ) do + if Task:AbortUnit( PlayerUnit ) then + PlayerUnitRemoved = true + end + end + + return PlayerUnitRemoved +end + +--- Handles a crash of a PlayerUnit from the Mission. +-- For each Task within the Mission, the PlayerUnit is removed from Task where it is assigned. +-- If the Unit was not part of a Task in the Mission, false is returned. +-- If the Unit is part of a Task in the Mission, true is returned. +-- @param #MISSION self +-- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player crashing. +-- @return #boolean true if Unit is part of a Task in the Mission. +function MISSION:CrashUnit( PlayerUnit ) + self:F( { PlayerUnit = PlayerUnit } ) + + local PlayerUnitRemoved = false + + for TaskID, Task in pairs( self:GetTasks() ) do + if Task:CrashUnit( PlayerUnit ) then + PlayerUnitRemoved = true + end + end + + return PlayerUnitRemoved +end + +--- Add a scoring to the mission. +-- @param #MISSION self +-- @return #MISSION self +function MISSION:AddScoring( Scoring ) + self.Scoring = Scoring + return self +end + +--- Get the scoring object of a mission. +-- @param #MISSION self +-- @return #SCORING Scoring +function MISSION:GetScoring() + return self.Scoring +end + +--- Get the groups for which TASKS are given in the mission +-- @param #MISSION self +-- @return Core.Set#SET_GROUP +function MISSION:GetGroups() + + local SetGroup = SET_GROUP:New() + + for TaskID, Task in pairs( self:GetTasks() ) do + local Task = Task -- Tasking.Task#TASK + local GroupSet = Task:GetGroups() + GroupSet:ForEachGroup( + function( TaskGroup ) + SetGroup:Add( TaskGroup, TaskGroup ) + end + ) + end + + return SetGroup + +end + + +--- Sets the Planned Task menu. +-- @param #MISSION self +function MISSION:SetMenu() + self:F() + + for _, Task in pairs( self:GetTasks() ) do + local Task = Task -- Tasking.Task#TASK + Task:SetMenu() + end +end + +--- Removes the Planned Task menu. +-- @param #MISSION self +function MISSION:RemoveMenu() + self:F() + + for _, Task in pairs( self:GetTasks() ) do + local Task = Task -- Tasking.Task#TASK + Task:RemoveMenu() + end +end + + +--- Gets the COMMANDCENTER. +-- @param #MISSION self +-- @return Tasking.CommandCenter#COMMANDCENTER +function MISSION:GetCommandCenter() + return self.CommandCenter +end + +--- Sets the Assigned Task menu. +-- @param #MISSION self +-- @param Tasking.Task#TASK Task +-- @param #string MenuText The menu text. +-- @return #MISSION self +function MISSION:SetAssignedMenu( Task ) + + for _, Task in pairs( self.Tasks ) do + local Task = Task -- Tasking.Task#TASK + Task:RemoveMenu() + Task:SetAssignedMenu() + end + +end + +--- Removes a Task menu. +-- @param #MISSION self +-- @param Tasking.Task#TASK Task +-- @return #MISSION self +function MISSION:RemoveTaskMenu( Task ) + + Task:RemoveMenu() +end + + +--- Gets the mission menu for the coalition. +-- @param #MISSION self +-- @param Wrapper.Group#GROUP TaskGroup +-- @return Core.Menu#MENU_COALITION self +function MISSION:GetMissionMenu( TaskGroup ) + + local CommandCenter = self:GetCommandCenter() + local CommandCenterMenu = CommandCenter.CommandCenterMenu + + local MissionName = self:GetName() + + local TaskGroupName = TaskGroup:GetName() + local MissionMenu = MENU_GROUP:New( TaskGroup, MissionName, CommandCenterMenu ) + + return MissionMenu +end + + +--- Clears the mission menu for the coalition. +-- @param #MISSION self +-- @return #MISSION self +function MISSION:ClearMissionMenu() + self.MissionMenu:Remove() + self.MissionMenu = nil +end + +--- Get the TASK identified by the TaskNumber from the Mission. This function is useful in GoalFunctions. +-- @param #string TaskName The Name of the @{Task} within the @{Mission}. +-- @return Tasking.Task#TASK The Task +-- @return #nil Returns nil if no task was found. +function MISSION:GetTask( TaskName ) + self:F( { TaskName } ) + + return self.Tasks[TaskName] +end + + +--- Register a @{Task} to be completed within the @{Mission}. +-- Note that there can be multiple @{Task}s registered to be completed. +-- Each Task can be set a certain Goals. The Mission will not be completed until all Goals are reached. +-- @param #MISSION self +-- @param Tasking.Task#TASK Task is the @{Task} object. +-- @return Tasking.Task#TASK The task added. +function MISSION:AddTask( Task ) + + local TaskName = Task:GetTaskName() + self:F( TaskName ) + + self.Tasks[TaskName] = self.Tasks[TaskName] or { n = 0 } + + self.Tasks[TaskName] = Task + + self:GetCommandCenter():SetMenu() + + return Task +end + +--- Removes a @{Task} to be completed within the @{Mission}. +-- Note that there can be multiple @{Task}s registered to be completed. +-- Each Task can be set a certain Goals. The Mission will not be completed until all Goals are reached. +-- @param #MISSION self +-- @param Tasking.Task#TASK Task is the @{Task} object. +-- @return #nil The cleaned Task reference. +function MISSION:RemoveTask( Task ) + + local TaskName = Task:GetTaskName() + + self:F( TaskName ) + self.Tasks[TaskName] = self.Tasks[TaskName] or { n = 0 } + + -- Ensure everything gets garbarge collected. + self.Tasks[TaskName] = nil + Task = nil + + collectgarbage() + + self:GetCommandCenter():SetMenu() + + return nil +end + +--- Return the next @{Task} ID to be completed within the @{Mission}. +-- @param #MISSION self +-- @param Tasking.Task#TASK Task is the @{Task} object. +-- @return Tasking.Task#TASK The task added. +function MISSION:GetNextTaskID( Task ) + + local TaskName = Task:GetTaskName() + self:F( TaskName ) + self.Tasks[TaskName] = self.Tasks[TaskName] or { n = 0 } + + self.Tasks[TaskName].n = self.Tasks[TaskName].n + 1 + + return self.Tasks[TaskName].n +end + + + +--- old stuff + +--- 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 + +function MISSION:HasGroup( TaskGroup ) + local Has = false + + for TaskID, Task in pairs( self:GetTasks() ) do + local Task = Task -- Tasking.Task#TASK + if Task:HasGroup( TaskGroup ) then + Has = true + break + end + end + + return Has +end + +--- Create a summary report of the Mission (one line). +-- @param #MISSION self +-- @return #string +function MISSION:ReportSummary() + + local Report = REPORT:New() + + -- List the name of the mission. + local Name = self:GetName() + + -- Determine the status of the mission. + local Status = self:GetState() + + -- Determine how many tasks are remaining. + local TasksRemaining = 0 + for TaskID, Task in pairs( self:GetTasks() ) do + local Task = Task -- Tasking.Task#TASK + if Task:IsStateSuccess() or Task:IsStateFailed() then + else + TasksRemaining = TasksRemaining + 1 + end + end + + Report:Add( "Mission " .. Name .. " - " .. Status .. " - " .. TasksRemaining .. " tasks remaining." ) + + return Report:Text() +end + +--- Create a overview report of the Mission (multiple lines). +-- @param #MISSION self +-- @return #string +function MISSION:ReportOverview() + + local Report = REPORT:New() + + -- List the name of the mission. + local Name = self:GetName() + + -- Determine the status of the mission. + local Status = self:GetState() + + Report:Add( "Mission " .. Name .. " - State '" .. Status .. "'" ) + + -- Determine how many tasks are remaining. + local TasksRemaining = 0 + for TaskID, Task in pairs( self:GetTasks() ) do + local Task = Task -- Tasking.Task#TASK + Report:Add( "- " .. Task:ReportSummary() ) + end + + return Report:Text() +end + +--- Create a detailed report of the Mission, listing all the details of the Task. +-- @param #MISSION self +-- @return #string +function MISSION:ReportDetails() + + local Report = REPORT:New() + + -- List the name of the mission. + local Name = self:GetName() + + -- Determine the status of the mission. + local Status = self:GetState() + + Report:Add( "Mission " .. Name .. " - State '" .. Status .. "'" ) + + -- Determine how many tasks are remaining. + local TasksRemaining = 0 + for TaskID, Task in pairs( self:GetTasks() ) do + local Task = Task -- Tasking.Task#TASK + Report:Add( Task:ReportDetails() ) + end + + return Report:Text() +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 + + +--- 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 -- Wrapper.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 + +--- This module contains the TASK class. +-- +-- 1) @{#TASK} class, extends @{Base#BASE} +-- ============================================ +-- 1.1) The @{#TASK} class implements the methods for task orchestration within MOOSE. +-- ---------------------------------------------------------------------------------------- +-- The class provides a couple of methods to: +-- +-- * @{#TASK.AssignToGroup}():Assign a task to a group (of players). +-- * @{#TASK.AddProcess}():Add a @{Process} to a task. +-- * @{#TASK.RemoveProcesses}():Remove a running @{Process} from a running task. +-- * @{#TASK.SetStateMachine}():Set a @{Fsm} to a task. +-- * @{#TASK.RemoveStateMachine}():Remove @{Fsm} from a task. +-- * @{#TASK.HasStateMachine}():Enquire if the task has a @{Fsm} +-- * @{#TASK.AssignToUnit}(): Assign a task to a unit. (Needs to be implemented in the derived classes from @{#TASK}. +-- * @{#TASK.UnAssignFromUnit}(): Unassign the task from a unit. +-- * @{#TASK.SetTimeOut}(): Set timer in seconds before task gets cancelled if not assigned. +-- +-- 1.2) Set and enquire task status (beyond the task state machine processing). +-- ---------------------------------------------------------------------------- +-- A task needs to implement as a minimum the following task states: +-- +-- * **Success**: Expresses the successful execution and finalization of the task. +-- * **Failed**: Expresses the failure of a task. +-- * **Planned**: Expresses that the task is created, but not yet in execution and is not assigned yet. +-- * **Assigned**: Expresses that the task is assigned to a Group of players, and that the task is in execution mode. +-- +-- A task may also implement the following task states: +-- +-- * **Rejected**: Expresses that the task is rejected by a player, who was requested to accept the task. +-- * **Cancelled**: Expresses that the task is cancelled by HQ or through a logical situation where a cancellation of the task is required. +-- +-- A task can implement more statusses than the ones outlined above. Please consult the documentation of the specific tasks to understand the different status modelled. +-- +-- The status of tasks can be set by the methods **State** followed by the task status. An example is `StateAssigned()`. +-- The status of tasks can be enquired by the methods **IsState** followed by the task status name. An example is `if IsStateAssigned() then`. +-- +-- 1.3) Add scoring when reaching a certain task status: +-- ----------------------------------------------------- +-- Upon reaching a certain task status in a task, additional scoring can be given. If the Mission has a scoring system attached, the scores will be added to the mission scoring. +-- Use the method @{#TASK.AddScore}() to add scores when a status is reached. +-- +-- 1.4) Task briefing: +-- ------------------- +-- A task briefing can be given that is shown to the player when he is assigned to the task. +-- +-- === +-- +-- ### Authors: FlightControl - Design and Programming +-- +-- @module Task + +--- The TASK class +-- @type TASK +-- @field Core.Scheduler#SCHEDULER TaskScheduler +-- @field Tasking.Mission#MISSION Mission +-- @field Core.Set#SET_GROUP SetGroup The Set of Groups assigned to the Task +-- @field Core.Fsm#FSM_PROCESS FsmTemplate +-- @field Tasking.Mission#MISSION Mission +-- @field Tasking.CommandCenter#COMMANDCENTER CommandCenter +-- @extends Core.Fsm#FSM_TASK +TASK = { + ClassName = "TASK", + TaskScheduler = nil, + ProcessClasses = {}, -- The container of the Process classes that will be used to create and assign new processes for the task to ProcessUnits. + Processes = {}, -- The container of actual process objects instantiated and assigned to ProcessUnits. + Players = nil, + Scores = {}, + Menu = {}, + SetGroup = nil, + FsmTemplate = nil, + Mission = nil, + CommandCenter = nil, + TimeOut = 0, +} + +--- FSM PlayerAborted event handler prototype for TASK. +-- @function [parent=#TASK] OnAfterPlayerAborted +-- @param #TASK self +-- @param Wrapper.Unit#UNIT PlayerUnit The Unit of the Player when he went back to spectators or left the mission. +-- @param #string PlayerName The name of the Player. + +--- FSM PlayerCrashed event handler prototype for TASK. +-- @function [parent=#TASK] OnAfterPlayerCrashed +-- @param #TASK self +-- @param Wrapper.Unit#UNIT PlayerUnit The Unit of the Player when he crashed in the mission. +-- @param #string PlayerName The name of the Player. + +--- FSM PlayerDead event handler prototype for TASK. +-- @function [parent=#TASK] OnAfterPlayerDead +-- @param #TASK self +-- @param Wrapper.Unit#UNIT PlayerUnit The Unit of the Player when he died in the mission. +-- @param #string PlayerName The name of the Player. + +--- FSM Fail synchronous event function for TASK. +-- Use this event to Fail the Task. +-- @function [parent=#TASK] Fail +-- @param #TASK self + +--- FSM Fail asynchronous event function for TASK. +-- Use this event to Fail the Task. +-- @function [parent=#TASK] __Fail +-- @param #TASK self + +--- FSM Abort synchronous event function for TASK. +-- Use this event to Abort the Task. +-- @function [parent=#TASK] Abort +-- @param #TASK self + +--- FSM Abort asynchronous event function for TASK. +-- Use this event to Abort the Task. +-- @function [parent=#TASK] __Abort +-- @param #TASK self + +--- FSM Success synchronous event function for TASK. +-- Use this event to make the Task a Success. +-- @function [parent=#TASK] Success +-- @param #TASK self + +--- FSM Success asynchronous event function for TASK. +-- Use this event to make the Task a Success. +-- @function [parent=#TASK] __Success +-- @param #TASK self + +--- FSM Cancel synchronous event function for TASK. +-- Use this event to Cancel the Task. +-- @function [parent=#TASK] Cancel +-- @param #TASK self + +--- FSM Cancel asynchronous event function for TASK. +-- Use this event to Cancel the Task. +-- @function [parent=#TASK] __Cancel +-- @param #TASK self + +--- FSM Replan synchronous event function for TASK. +-- Use this event to Replan the Task. +-- @function [parent=#TASK] Replan +-- @param #TASK self + +--- FSM Replan asynchronous event function for TASK. +-- Use this event to Replan the Task. +-- @function [parent=#TASK] __Replan +-- @param #TASK self + + +--- Instantiates a new TASK. Should never be used. Interface Class. +-- @param #TASK self +-- @param Tasking.Mission#MISSION Mission The mission wherein the Task is registered. +-- @param Core.Set#SET_GROUP SetGroupAssign The set of groups for which the Task can be assigned. +-- @param #string TaskName The name of the Task +-- @param #string TaskType The type of the Task +-- @return #TASK self +function TASK:New( Mission, SetGroupAssign, TaskName, TaskType ) + + local self = BASE:Inherit( self, FSM_TASK:New() ) -- Core.Fsm#FSM_TASK + + self:SetStartState( "Planned" ) + self:AddTransition( "Planned", "Assign", "Assigned" ) + self:AddTransition( "Assigned", "AssignUnit", "Assigned" ) + self:AddTransition( "Assigned", "Success", "Success" ) + self:AddTransition( "Assigned", "Fail", "Failed" ) + self:AddTransition( "Assigned", "Abort", "Aborted" ) + self:AddTransition( "Assigned", "Cancel", "Cancelled" ) + self:AddTransition( "*", "PlayerCrashed", "*" ) + self:AddTransition( "*", "PlayerAborted", "*" ) + self:AddTransition( "*", "PlayerDead", "*" ) + self:AddTransition( { "Failed", "Aborted", "Cancelled" }, "Replan", "Planned" ) + self:AddTransition( "*", "TimeOut", "Cancelled" ) + + self:E( "New TASK " .. TaskName ) + + self.Processes = {} + self.Fsm = {} + + self.Mission = Mission + self.CommandCenter = Mission:GetCommandCenter() + + self.SetGroup = SetGroupAssign + + self:SetType( TaskType ) + self:SetName( TaskName ) + self:SetID( Mission:GetNextTaskID( self ) ) -- The Mission orchestrates the task sequences .. + + self.TaskBriefing = "You are invited for the task: " .. self.TaskName .. "." + + self.FsmTemplate = self.FsmTemplate or FSM_PROCESS:New() + + Mission:AddTask( self ) + + return self +end + +--- Get the Task FSM Process Template +-- @param #TASK self +-- @return Core.Fsm#FSM_PROCESS +function TASK:GetUnitProcess() + + return self.FsmTemplate +end + +--- Sets the Task FSM Process Template +-- @param #TASK self +-- @param Core.Fsm#FSM_PROCESS +function TASK:SetUnitProcess( FsmTemplate ) + + self.FsmTemplate = FsmTemplate +end + +--- Add a PlayerUnit to join the Task. +-- For each Group within the Task, the Unit is check if it can join the Task. +-- If the Unit was not part of the Task, false is returned. +-- If the Unit is part of the Task, true is returned. +-- @param #TASK self +-- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player joining the Mission. +-- @param Wrapper.Group#GROUP PlayerGroup The GROUP of the player joining the Mission. +-- @return #boolean true if Unit is part of the Task. +function TASK:JoinUnit( PlayerUnit, PlayerGroup ) + self:F( { PlayerUnit = PlayerUnit, PlayerGroup = PlayerGroup } ) + + local PlayerUnitAdded = false + + local PlayerGroups = self:GetGroups() + + -- Is the PlayerGroup part of the PlayerGroups? + if PlayerGroups:IsIncludeObject( PlayerGroup ) then + + -- Check if the PlayerGroup is already assigned to the Task. If yes, the PlayerGroup is added to the Task. + -- If the PlayerGroup is not assigned to the Task, the menu needs to be set. In that case, the PlayerUnit will become the GroupPlayer leader. + if self:IsStatePlanned() or self:IsStateReplanned() then + self:SetMenuForGroup( PlayerGroup ) + self:MessageToGroups( PlayerUnit:GetPlayerName() .. " is planning to join Task " .. self:GetName() ) + end + if self:IsStateAssigned() then + local IsAssignedToGroup = self:IsAssignedToGroup( PlayerGroup ) + self:E( { IsAssignedToGroup = IsAssignedToGroup } ) + if IsAssignedToGroup then + self:AssignToUnit( PlayerUnit ) + self:MessageToGroups( PlayerUnit:GetPlayerName() .. " joined Task " .. self:GetName() ) + end + end + end + + return PlayerUnitAdded +end + +--- Abort a PlayerUnit from a Task. +-- If the Unit was not part of the Task, false is returned. +-- If the Unit is part of the Task, true is returned. +-- @param #TASK self +-- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player aborting the Task. +-- @return #boolean true if Unit is part of the Task. +function TASK:AbortUnit( PlayerUnit ) + self:F( { PlayerUnit = PlayerUnit } ) + + local PlayerUnitAborted = false + + local PlayerGroups = self:GetGroups() + local PlayerGroup = PlayerUnit:GetGroup() + + -- Is the PlayerGroup part of the PlayerGroups? + if PlayerGroups:IsIncludeObject( PlayerGroup ) then + + -- Check if the PlayerGroup is already assigned to the Task. If yes, the PlayerGroup is aborted from the Task. + -- If the PlayerUnit was the last unit of the PlayerGroup, the menu needs to be removed from the Group. + if self:IsStateAssigned() then + local IsAssignedToGroup = self:IsAssignedToGroup( PlayerGroup ) + self:E( { IsAssignedToGroup = IsAssignedToGroup } ) + if IsAssignedToGroup then + self:UnAssignFromUnit( PlayerUnit ) + self:MessageToGroups( PlayerUnit:GetPlayerName() .. " aborted Task " .. self:GetName() ) + self:E( { TaskGroup = PlayerGroup:GetName(), GetUnits = PlayerGroup:GetUnits() } ) + if #PlayerGroup:GetUnits() == 1 then + PlayerGroup:SetState( PlayerGroup, "Assigned", nil ) + self:RemoveMenuForGroup( PlayerGroup ) + end + self:PlayerAborted( PlayerUnit ) + end + end + end + + return PlayerUnitAborted +end + +--- A PlayerUnit crashed in a Task. Abort the Player. +-- If the Unit was not part of the Task, false is returned. +-- If the Unit is part of the Task, true is returned. +-- @param #TASK self +-- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player aborting the Task. +-- @return #boolean true if Unit is part of the Task. +function TASK:CrashUnit( PlayerUnit ) + self:F( { PlayerUnit = PlayerUnit } ) + + local PlayerUnitCrashed = false + + local PlayerGroups = self:GetGroups() + local PlayerGroup = PlayerUnit:GetGroup() + + -- Is the PlayerGroup part of the PlayerGroups? + if PlayerGroups:IsIncludeObject( PlayerGroup ) then + + -- Check if the PlayerGroup is already assigned to the Task. If yes, the PlayerGroup is aborted from the Task. + -- If the PlayerUnit was the last unit of the PlayerGroup, the menu needs to be removed from the Group. + if self:IsStateAssigned() then + local IsAssignedToGroup = self:IsAssignedToGroup( PlayerGroup ) + self:E( { IsAssignedToGroup = IsAssignedToGroup } ) + if IsAssignedToGroup then + self:UnAssignFromUnit( PlayerUnit ) + self:MessageToGroups( PlayerUnit:GetPlayerName() .. " crashed in Task " .. self:GetName() ) + self:E( { TaskGroup = PlayerGroup:GetName(), GetUnits = PlayerGroup:GetUnits() } ) + if #PlayerGroup:GetUnits() == 1 then + PlayerGroup:SetState( PlayerGroup, "Assigned", nil ) + self:RemoveMenuForGroup( PlayerGroup ) + end + self:PlayerCrashed( PlayerUnit ) + end + end + end + + return PlayerUnitCrashed +end + + + +--- Gets the Mission to where the TASK belongs. +-- @param #TASK self +-- @return Tasking.Mission#MISSION +function TASK:GetMission() + + return self.Mission +end + + +--- Gets the SET_GROUP assigned to the TASK. +-- @param #TASK self +-- @return Core.Set#SET_GROUP +function TASK:GetGroups() + return self.SetGroup +end + + + +--- Assign the @{Task}to a @{Group}. +-- @param #TASK self +-- @param Wrapper.Group#GROUP TaskGroup +-- @return #TASK +function TASK:AssignToGroup( TaskGroup ) + self:F2( TaskGroup:GetName() ) + + local TaskGroupName = TaskGroup:GetName() + + TaskGroup:SetState( TaskGroup, "Assigned", self ) + + self:RemoveMenuForGroup( TaskGroup ) + self:SetAssignedMenuForGroup( TaskGroup ) + + local TaskUnits = TaskGroup:GetUnits() + for UnitID, UnitData in pairs( TaskUnits ) do + local TaskUnit = UnitData -- Wrapper.Unit#UNIT + local PlayerName = TaskUnit:GetPlayerName() + self:E(PlayerName) + if PlayerName ~= nil or PlayerName ~= "" then + self:AssignToUnit( TaskUnit ) + end + end + + return self +end + +--- +-- @param #TASK self +-- @param Wrapper.Group#GROUP FindGroup +-- @return #boolean +function TASK:HasGroup( FindGroup ) + + return self:GetGroups():IsIncludeObject( FindGroup ) + +end + +--- Assign the @{Task} to an alive @{Unit}. +-- @param #TASK self +-- @param Wrapper.Unit#UNIT TaskUnit +-- @return #TASK self +function TASK:AssignToUnit( TaskUnit ) + self:F( TaskUnit:GetName() ) + + local FsmTemplate = self:GetUnitProcess() + + -- Assign a new FsmUnit to TaskUnit. + local FsmUnit = self:SetStateMachine( TaskUnit, FsmTemplate:Copy( TaskUnit, self ) ) -- Core.Fsm#FSM_PROCESS + self:E({"Address FsmUnit", tostring( FsmUnit ) } ) + + FsmUnit:SetStartState( "Planned" ) + FsmUnit:Accept() -- Each Task needs to start with an Accept event to start the flow. + + return self +end + +--- UnAssign the @{Task} from an alive @{Unit}. +-- @param #TASK self +-- @param Wrapper.Unit#UNIT TaskUnit +-- @return #TASK self +function TASK:UnAssignFromUnit( TaskUnit ) + self:F( TaskUnit ) + + self:RemoveStateMachine( TaskUnit ) + + return self +end + +--- Sets the TimeOut for the @{Task}. If @{Task} stayed planned for longer than TimeOut, it gets into Cancelled status. +-- @param #TASK self +-- @param #integer Timer in seconds +-- @return #TASK self +function TASK:SetTimeOut ( Timer ) + self:F( Timer ) + self.TimeOut = Timer + self:__TimeOut( self.TimeOut ) + return self +end + +--- Send a message of the @{Task} to the assigned @{Group}s. +-- @param #TASK self +function TASK:MessageToGroups( Message ) + self:F( { Message = Message } ) + + local Mission = self:GetMission() + local CC = Mission:GetCommandCenter() + + for TaskGroupName, TaskGroup in pairs( self.SetGroup:GetSet() ) do + local TaskGroup = TaskGroup -- Wrapper.Group#GROUP + CC:MessageToGroup( Message, TaskGroup, TaskGroup:GetName() ) + end +end + + +--- Send the briefng message of the @{Task} to the assigned @{Group}s. +-- @param #TASK self +function TASK:SendBriefingToAssignedGroups() + self:F2() + + for TaskGroupName, TaskGroup in pairs( self.SetGroup:GetSet() ) do + + if self:IsAssignedToGroup( TaskGroup ) then + TaskGroup:Message( self.TaskBriefing, 60 ) + end + end +end + + +--- Assign the @{Task} from the @{Group}s. +-- @param #TASK self +function TASK:UnAssignFromGroups() + self:F2() + + for TaskGroupName, TaskGroup in pairs( self.SetGroup:GetSet() ) do + + TaskGroup:SetState( TaskGroup, "Assigned", nil ) + + self:RemoveMenuForGroup( TaskGroup ) + + local TaskUnits = TaskGroup:GetUnits() + for UnitID, UnitData in pairs( TaskUnits ) do + local TaskUnit = UnitData -- Wrapper.Unit#UNIT + local PlayerName = TaskUnit:GetPlayerName() + if PlayerName ~= nil or PlayerName ~= "" then + self:UnAssignFromUnit( TaskUnit ) + end + end + end +end + +--- Returns if the @{Task} is assigned to the Group. +-- @param #TASK self +-- @param Wrapper.Group#GROUP TaskGroup +-- @return #boolean +function TASK:IsAssignedToGroup( TaskGroup ) + + local TaskGroupName = TaskGroup:GetName() + + if self:IsStateAssigned() then + if TaskGroup:GetState( TaskGroup, "Assigned" ) == self then + return true + end + end + + return false +end + +--- Returns if the @{Task} has still alive and assigned Units. +-- @param #TASK self +-- @return #boolean +function TASK:HasAliveUnits() + self:F() + + for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do + if self:IsStateAssigned() then + if self:IsAssignedToGroup( TaskGroup ) then + for TaskUnitID, TaskUnit in pairs( TaskGroup:GetUnits() ) do + if TaskUnit:IsAlive() then + self:T( { HasAliveUnits = true } ) + return true + end + end + end + end + end + + self:T( { HasAliveUnits = false } ) + return false +end + +--- Set the menu options of the @{Task} to all the groups in the SetGroup. +-- @param #TASK self +function TASK:SetMenu() + self:F() + + self.SetGroup:Flush() + for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do + if self:IsStatePlanned() or self:IsStateReplanned() then + self:SetMenuForGroup( TaskGroup ) + end + end +end + + +--- Remove the menu options of the @{Task} to all the groups in the SetGroup. +-- @param #TASK self +-- @return #TASK self +function TASK:RemoveMenu() + + for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do + self:RemoveMenuForGroup( TaskGroup ) + end +end + + +--- Set the Menu for a Group +-- @param #TASK self +function TASK:SetMenuForGroup( TaskGroup ) + + if not self:IsAssignedToGroup( TaskGroup ) then + self:SetPlannedMenuForGroup( TaskGroup, self:GetTaskName() ) + else + self:SetAssignedMenuForGroup( TaskGroup ) + end +end + + +--- Set the planned menu option of the @{Task}. +-- @param #TASK self +-- @param Wrapper.Group#GROUP TaskGroup +-- @param #string MenuText The menu text. +-- @return #TASK self +function TASK:SetPlannedMenuForGroup( TaskGroup, MenuText ) + self:E( TaskGroup:GetName() ) + + local Mission = self:GetMission() + local MissionMenu = Mission:GetMissionMenu( TaskGroup ) + + local TaskType = self:GetType() + local TaskTypeMenu = MENU_GROUP:New( TaskGroup, TaskType, MissionMenu ) + local TaskMenu = MENU_GROUP_COMMAND:New( TaskGroup, MenuText, TaskTypeMenu, self.MenuAssignToGroup, { self = self, TaskGroup = TaskGroup } ) + + return self +end + +--- Set the assigned menu options of the @{Task}. +-- @param #TASK self +-- @param Wrapper.Group#GROUP TaskGroup +-- @return #TASK self +function TASK:SetAssignedMenuForGroup( TaskGroup ) + self:E( TaskGroup:GetName() ) + + local Mission = self:GetMission() + local MissionMenu = Mission:GetMissionMenu( TaskGroup ) + + self:E( { MissionMenu = MissionMenu } ) + + local TaskTypeMenu = MENU_GROUP_COMMAND:New( TaskGroup, "Task Status", MissionMenu, self.MenuTaskStatus, { self = self, TaskGroup = TaskGroup } ) + local TaskMenu = MENU_GROUP_COMMAND:New( TaskGroup, "Abort Task", MissionMenu, self.MenuTaskAbort, { self = self, TaskGroup = TaskGroup } ) + + return self +end + +--- Remove the menu option of the @{Task} for a @{Group}. +-- @param #TASK self +-- @param Wrapper.Group#GROUP TaskGroup +-- @return #TASK self +function TASK:RemoveMenuForGroup( TaskGroup ) + + local Mission = self:GetMission() + local MissionName = Mission:GetName() + + local MissionMenu = Mission:GetMissionMenu( TaskGroup ) + MissionMenu:Remove() +end + +function TASK.MenuAssignToGroup( MenuParam ) + + local self = MenuParam.self + local TaskGroup = MenuParam.TaskGroup + + self:E( "Assigned menu selected") + + self:AssignToGroup( TaskGroup ) +end + +function TASK.MenuTaskStatus( MenuParam ) + + local self = MenuParam.self + local TaskGroup = MenuParam.TaskGroup + + --self:AssignToGroup( TaskGroup ) +end + +function TASK.MenuTaskAbort( MenuParam ) + + local self = MenuParam.self + local TaskGroup = MenuParam.TaskGroup + + self:Abort() +end + + + +--- Returns the @{Task} name. +-- @param #TASK self +-- @return #string TaskName +function TASK:GetTaskName() + return self.TaskName +end + + + + +--- Get the default or currently assigned @{Process} template with key ProcessName. +-- @param #TASK self +-- @param #string ProcessName +-- @return Core.Fsm#FSM_PROCESS +function TASK:GetProcessTemplate( ProcessName ) + + local ProcessTemplate = self.ProcessClasses[ProcessName] + + return ProcessTemplate +end + + + +-- TODO: Obscolete? +--- Fail processes from @{Task} with key @{Unit} +-- @param #TASK self +-- @param #string TaskUnitName +-- @return #TASK self +function TASK:FailProcesses( TaskUnitName ) + + for ProcessID, ProcessData in pairs( self.Processes[TaskUnitName] ) do + local Process = ProcessData + Process.Fsm:Fail() + end +end + +--- Add a FiniteStateMachine to @{Task} with key Task@{Unit} +-- @param #TASK self +-- @param Wrapper.Unit#UNIT TaskUnit +-- @return #TASK self +function TASK:SetStateMachine( TaskUnit, Fsm ) + self:F( { TaskUnit, self.Fsm[TaskUnit] ~= nil } ) + + self.Fsm[TaskUnit] = Fsm + + return Fsm +end + +--- Remove FiniteStateMachines from @{Task} with key Task@{Unit} +-- @param #TASK self +-- @param Wrapper.Unit#UNIT TaskUnit +-- @return #TASK self +function TASK:RemoveStateMachine( TaskUnit ) + self:F( { TaskUnit, self.Fsm[TaskUnit] ~= nil } ) + + self.Fsm[TaskUnit] = nil + collectgarbage() + self:T( "Garbage Collected, Processes should be finalized now ...") +end + +--- Checks if there is a FiniteStateMachine assigned to Task@{Unit} for @{Task} +-- @param #TASK self +-- @param Wrapper.Unit#UNIT TaskUnit +-- @return #TASK self +function TASK:HasStateMachine( TaskUnit ) + self:F( { TaskUnit, self.Fsm[TaskUnit] ~= nil } ) + + return ( self.Fsm[TaskUnit] ~= nil ) +end + + +--- Gets the Scoring of the task +-- @param #TASK self +-- @return Functional.Scoring#SCORING Scoring +function TASK:GetScoring() + return self.Mission:GetScoring() +end + + +--- Gets the Task Index, which is a combination of the Task type, the Task name. +-- @param #TASK self +-- @return #string The Task ID +function TASK:GetTaskIndex() + + local TaskType = self:GetType() + local TaskName = self:GetName() + + return TaskType .. "." .. TaskName +end + +--- Sets the Name of the Task +-- @param #TASK self +-- @param #string TaskName +function TASK:SetName( TaskName ) + self.TaskName = TaskName +end + +--- Gets the Name of the Task +-- @param #TASK self +-- @return #string The Task Name +function TASK:GetName() + return self.TaskName +end + +--- Sets the Type of the Task +-- @param #TASK self +-- @param #string TaskType +function TASK:SetType( TaskType ) + self.TaskType = TaskType +end + +--- Gets the Type of the Task +-- @param #TASK self +-- @return #string TaskType +function TASK:GetType() + return self.TaskType +end + +--- Sets the ID of the Task +-- @param #TASK self +-- @param #string TaskID +function TASK:SetID( TaskID ) + self.TaskID = TaskID +end + +--- Gets the ID of the Task +-- @param #TASK self +-- @return #string TaskID +function TASK:GetID() + return self.TaskID +end + + +--- Sets a @{Task} to status **Success**. +-- @param #TASK self +function TASK:StateSuccess() + self:SetState( self, "State", "Success" ) + return self +end + +--- Is the @{Task} status **Success**. +-- @param #TASK self +function TASK:IsStateSuccess() + return self:Is( "Success" ) +end + +--- Sets a @{Task} to status **Failed**. +-- @param #TASK self +function TASK:StateFailed() + self:SetState( self, "State", "Failed" ) + return self +end + +--- Is the @{Task} status **Failed**. +-- @param #TASK self +function TASK:IsStateFailed() + return self:Is( "Failed" ) +end + +--- Sets a @{Task} to status **Planned**. +-- @param #TASK self +function TASK:StatePlanned() + self:SetState( self, "State", "Planned" ) + return self +end + +--- Is the @{Task} status **Planned**. +-- @param #TASK self +function TASK:IsStatePlanned() + return self:Is( "Planned" ) +end + +--- Sets a @{Task} to status **Assigned**. +-- @param #TASK self +function TASK:StateAssigned() + self:SetState( self, "State", "Assigned" ) + return self +end + +--- Is the @{Task} status **Assigned**. +-- @param #TASK self +function TASK:IsStateAssigned() + return self:Is( "Assigned" ) +end + +--- Sets a @{Task} to status **Hold**. +-- @param #TASK self +function TASK:StateHold() + self:SetState( self, "State", "Hold" ) + return self +end + +--- Is the @{Task} status **Hold**. +-- @param #TASK self +function TASK:IsStateHold() + return self:Is( "Hold" ) +end + +--- Sets a @{Task} to status **Replanned**. +-- @param #TASK self +function TASK:StateReplanned() + self:SetState( self, "State", "Replanned" ) + return self +end + +--- Is the @{Task} status **Replanned**. +-- @param #TASK self +function TASK:IsStateReplanned() + return self:Is( "Replanned" ) +end + +--- Gets the @{Task} status. +-- @param #TASK self +function TASK:GetStateString() + return self:GetState( self, "State" ) +end + +--- Sets a @{Task} briefing. +-- @param #TASK self +-- @param #string TaskBriefing +-- @return #TASK self +function TASK:SetBriefing( TaskBriefing ) + self.TaskBriefing = TaskBriefing + return self +end + + + + +--- FSM function for a TASK +-- @param #TASK self +-- @param #string Event +-- @param #string From +-- @param #string To +function TASK:onenterAssigned( From, Event, To ) + + self:E("Task Assigned") + + self:MessageToGroups( "Task " .. self:GetName() .. " has been assigned to your group." ) + self:GetMission():__Start( 1 ) +end + + +--- FSM function for a TASK +-- @param #TASK self +-- @param #string Event +-- @param #string From +-- @param #string To +function TASK:onenterSuccess( From, Event, To ) + + self:E( "Task Success" ) + + self:MessageToGroups( "Task " .. self:GetName() .. " is successful! Good job!" ) + self:UnAssignFromGroups() + + self:GetMission():__Complete( 1 ) + +end + + +--- FSM function for a TASK +-- @param #TASK self +-- @param #string From +-- @param #string Event +-- @param #string To +function TASK:onenterAborted( From, Event, To ) + + self:E( "Task Aborted" ) + + self:GetMission():GetCommandCenter():MessageToCoalition( "Task " .. self:GetName() .. " has been aborted! Task may be replanned." ) + + self:UnAssignFromGroups() + + self:__Replan( 5 ) +end + +--- FSM function for a TASK +-- @param #TASK self +-- @param #string From +-- @param #string Event +-- @param #string To +function TASK:onafterReplan( From, Event, To ) + + self:E( "Task Replanned" ) + + self:GetMission():GetCommandCenter():MessageToCoalition( "Replanning Task " .. self:GetName() .. "." ) + + self:SetMenu() + +end + +--- FSM function for a TASK +-- @param #TASK self +-- @param #string From +-- @param #string Event +-- @param #string To +function TASK:onenterFailed( From, Event, To ) + + self:E( "Task Failed" ) + + self:GetMission():GetCommandCenter():MessageToCoalition( "Task " .. self:GetName() .. " has failed!" ) + + self:UnAssignFromGroups() +end + +--- FSM function for a TASK +-- @param #TASK self +-- @param #string Event +-- @param #string From +-- @param #string To +function TASK:onstatechange( From, Event, To ) + + if self:IsTrace() then + MESSAGE:New( "@ Task " .. self.TaskName .. " : " .. Event .. " changed to state " .. To, 2 ):ToAll() + end + + if self.Scores[To] then + local Scoring = self:GetScoring() + if Scoring then + self:E( { self.Scores[To].ScoreText, self.Scores[To].Score } ) + Scoring:_AddMissionScore( self.Mission, self.Scores[To].ScoreText, self.Scores[To].Score ) + end + end + +end + +--- FSM function for a TASK +-- @param #TASK self +-- @param #string Event +-- @param #string From +-- @param #string To +function TASK:onenterPlanned( From, Event, To) + if not self.TimeOut == 0 then + self.__TimeOut( self.TimeOut ) + end +end + +--- FSM function for a TASK +-- @param #TASK self +-- @param #string Event +-- @param #string From +-- @param #string To +function TASK:onbeforeTimeOut( From, Event, To ) + if From == "Planned" then + self:RemoveMenu() + return true + end + return false +end + +do -- Reporting + +--- Create a summary report of the Task. +-- List the Task Name and Status +-- @param #TASK self +-- @return #string +function TASK:ReportSummary() + + local Report = REPORT:New() + + -- List the name of the Task. + local Name = self:GetName() + + -- Determine the status of the Task. + local State = self:GetState() + + Report:Add( "Task " .. Name .. " - State '" .. State ) + + return Report:Text() +end + + +--- Create a detailed report of the Task. +-- List the Task Status, and the Players assigned to the Task. +-- @param #TASK self +-- @return #string +function TASK:ReportDetails() + + local Report = REPORT:New() + + -- List the name of the Task. + local Name = self:GetName() + + -- Determine the status of the Task. + local State = self:GetState() + + + -- Loop each Unit active in the Task, and find Player Names. + local PlayerNames = {} + for PlayerGroupID, PlayerGroup in pairs( self:GetGroups():GetSet() ) do + local Player = PlayerGroup -- Wrapper.Group#GROUP + for PlayerUnitID, PlayerUnit in pairs( PlayerGroup:GetUnits() ) do + local PlayerUnit = PlayerUnit -- Wrapper.Unit#UNIT + if PlayerUnit and PlayerUnit:IsAlive() then + local PlayerName = PlayerUnit:GetPlayerName() + PlayerNames[#PlayerNames+1] = PlayerName + end + end + local PlayerNameText = table.concat( PlayerNames, ", " ) + Report:Add( "Task " .. Name .. " - State '" .. State .. "' - Players " .. PlayerNameText ) + end + + -- Loop each Process in the Task, and find Reporting Details. + + return Report:Text() +end + + +end -- Reporting +--- This module contains the DETECTION_MANAGER class and derived classes. +-- +-- === +-- +-- 1) @{DetectionManager#DETECTION_MANAGER} class, extends @{Base#BASE} +-- ==================================================================== +-- The @{DetectionManager#DETECTION_MANAGER} class defines the core functions to report detected objects to groups. +-- Reportings can be done in several manners, and it is up to the derived classes if DETECTION_MANAGER to model the reporting behaviour. +-- +-- 1.1) DETECTION_MANAGER constructor: +-- ----------------------------------- +-- * @{DetectionManager#DETECTION_MANAGER.New}(): Create a new DETECTION_MANAGER instance. +-- +-- 1.2) DETECTION_MANAGER reporting: +-- --------------------------------- +-- Derived DETECTION_MANAGER classes will reports detected units using the method @{DetectionManager#DETECTION_MANAGER.ReportDetected}(). This method implements polymorphic behaviour. +-- +-- The time interval in seconds of the reporting can be changed using the methods @{DetectionManager#DETECTION_MANAGER.SetReportInterval}(). +-- To control how long a reporting message is displayed, use @{DetectionManager#DETECTION_MANAGER.SetReportDisplayTime}(). +-- Derived classes need to implement the method @{DetectionManager#DETECTION_MANAGER.GetReportDisplayTime}() to use the correct display time for displayed messages during a report. +-- +-- Reporting can be started and stopped using the methods @{DetectionManager#DETECTION_MANAGER.StartReporting}() and @{DetectionManager#DETECTION_MANAGER.StopReporting}() respectively. +-- If an ad-hoc report is requested, use the method @{DetectionManager#DETECTION_MANAGER#ReportNow}(). +-- +-- The default reporting interval is every 60 seconds. The reporting messages are displayed 15 seconds. +-- +-- === +-- +-- 2) @{DetectionManager#DETECTION_REPORTING} class, extends @{DetectionManager#DETECTION_MANAGER} +-- ========================================================================================= +-- The @{DetectionManager#DETECTION_REPORTING} class implements detected units reporting. Reporting can be controlled using the reporting methods available in the @{DetectionManager#DETECTION_MANAGER} class. +-- +-- 2.1) DETECTION_REPORTING constructor: +-- ------------------------------- +-- The @{DetectionManager#DETECTION_REPORTING.New}() method creates a new DETECTION_REPORTING instance. +-- +-- === +-- +-- 3) @{#DETECTION_DISPATCHER} class, extends @{#DETECTION_MANAGER} +-- ================================================================ +-- The @{#DETECTION_DISPATCHER} class implements the dynamic dispatching of tasks upon groups of detected units determined a @{Set} of FAC (groups). +-- The FAC will detect units, will group them, and will dispatch @{Task}s to groups. Depending on the type of target detected, different tasks will be dispatched. +-- Find a summary below describing for which situation a task type is created: +-- +-- * **CAS Task**: Is created when there are enemy ground units within range of the FAC, while there are friendly units in the FAC perimeter. +-- * **BAI Task**: Is created when there are enemy ground units within range of the FAC, while there are NO other friendly units within the FAC perimeter. +-- * **SEAD Task**: Is created when there are enemy ground units wihtin range of the FAC, with air search radars. +-- +-- Other task types will follow... +-- +-- 3.1) DETECTION_DISPATCHER constructor: +-- -------------------------------------- +-- The @{#DETECTION_DISPATCHER.New}() method creates a new DETECTION_DISPATCHER instance. +-- +-- === +-- +-- ### Contributions: Mechanist, Prof_Hilactic, FlightControl - Concept & Testing +-- ### Author: FlightControl - Framework Design & Programming +-- +-- @module DetectionManager + +do -- DETECTION MANAGER + + --- DETECTION_MANAGER class. + -- @type DETECTION_MANAGER + -- @field Set#SET_GROUP SetGroup The groups to which the FAC will report to. + -- @field Functional.Detection#DETECTION_BASE Detection The DETECTION_BASE object that is used to report the detected objects. + -- @extends Base#BASE + DETECTION_MANAGER = { + ClassName = "DETECTION_MANAGER", + SetGroup = nil, + Detection = nil, + } + + --- FAC constructor. + -- @param #DETECTION_MANAGER self + -- @param Set#SET_GROUP SetGroup + -- @param Functional.Detection#DETECTION_BASE Detection + -- @return #DETECTION_MANAGER self + function DETECTION_MANAGER:New( SetGroup, Detection ) + + -- Inherits from BASE + local self = BASE:Inherit( self, BASE:New() ) -- Functional.Detection#DETECTION_MANAGER + + self.SetGroup = SetGroup + self.Detection = Detection + + self:SetReportInterval( 30 ) + self:SetReportDisplayTime( 25 ) + + return self + end + + --- Set the reporting time interval. + -- @param #DETECTION_MANAGER self + -- @param #number ReportInterval The interval in seconds when a report needs to be done. + -- @return #DETECTION_MANAGER self + function DETECTION_MANAGER:SetReportInterval( ReportInterval ) + self:F2() + + self._ReportInterval = ReportInterval + end + + + --- Set the reporting message display time. + -- @param #DETECTION_MANAGER self + -- @param #number ReportDisplayTime The display time in seconds when a report needs to be done. + -- @return #DETECTION_MANAGER self + function DETECTION_MANAGER:SetReportDisplayTime( ReportDisplayTime ) + self:F2() + + self._ReportDisplayTime = ReportDisplayTime + end + + --- Get the reporting message display time. + -- @param #DETECTION_MANAGER self + -- @return #number ReportDisplayTime The display time in seconds when a report needs to be done. + function DETECTION_MANAGER:GetReportDisplayTime() + self:F2() + + return self._ReportDisplayTime + end + + + + --- Reports the detected items to the @{Set#SET_GROUP}. + -- @param #DETECTION_MANAGER self + -- @param Functional.Detection#DETECTION_BASE Detection + -- @return #DETECTION_MANAGER self + function DETECTION_MANAGER:ReportDetected( Detection ) + self:F2() + + end + + --- Schedule the FAC reporting. + -- @param #DETECTION_MANAGER self + -- @param #number DelayTime The delay in seconds to wait the reporting. + -- @param #number ReportInterval The repeat interval in seconds for the reporting to happen repeatedly. + -- @return #DETECTION_MANAGER self + function DETECTION_MANAGER:Schedule( DelayTime, ReportInterval ) + self:F2() + + self._ScheduleDelayTime = DelayTime + + self:SetReportInterval( ReportInterval ) + + self.FacScheduler = SCHEDULER:New(self, self._FacScheduler, { self, "DetectionManager" }, self._ScheduleDelayTime, self._ReportInterval ) + return self + end + + --- Report the detected @{Unit#UNIT}s detected within the @{Detection#DETECTION_BASE} object to the @{Set#SET_GROUP}s. + -- @param #DETECTION_MANAGER self + function DETECTION_MANAGER:_FacScheduler( SchedulerName ) + self:F2( { SchedulerName } ) + + return self:ProcessDetected( self.Detection ) + +-- self.SetGroup:ForEachGroup( +-- --- @param Wrapper.Group#GROUP Group +-- function( Group ) +-- if Group:IsAlive() then +-- return self:ProcessDetected( self.Detection ) +-- end +-- end +-- ) + +-- return true + end + +end + + +do -- DETECTION_REPORTING + + --- DETECTION_REPORTING class. + -- @type DETECTION_REPORTING + -- @field Set#SET_GROUP SetGroup The groups to which the FAC will report to. + -- @field Functional.Detection#DETECTION_BASE Detection The DETECTION_BASE object that is used to report the detected objects. + -- @extends #DETECTION_MANAGER + DETECTION_REPORTING = { + ClassName = "DETECTION_REPORTING", + } + + + --- DETECTION_REPORTING constructor. + -- @param #DETECTION_REPORTING self + -- @param Set#SET_GROUP SetGroup + -- @param Functional.Detection#DETECTION_AREAS Detection + -- @return #DETECTION_REPORTING self + function DETECTION_REPORTING:New( SetGroup, Detection ) + + -- Inherits from DETECTION_MANAGER + local self = BASE:Inherit( self, DETECTION_MANAGER:New( SetGroup, Detection ) ) -- #DETECTION_REPORTING + + self:Schedule( 1, 30 ) + return self + end + + --- Creates a string of the detected items in a @{Detection}. + -- @param #DETECTION_MANAGER self + -- @param Set#SET_UNIT DetectedSet The detected Set created by the @{Detection#DETECTION_BASE} object. + -- @return #DETECTION_MANAGER self + function DETECTION_REPORTING:GetDetectedItemsText( DetectedSet ) + self:F2() + + local MT = {} -- Message Text + local UnitTypes = {} + + for DetectedUnitID, DetectedUnitData in pairs( DetectedSet:GetSet() ) do + local DetectedUnit = DetectedUnitData -- Wrapper.Unit#UNIT + if DetectedUnit:IsAlive() then + local UnitType = DetectedUnit:GetTypeName() + + if not UnitTypes[UnitType] then + UnitTypes[UnitType] = 1 + else + UnitTypes[UnitType] = UnitTypes[UnitType] + 1 + end + end + end + + for UnitTypeID, UnitType in pairs( UnitTypes ) do + MT[#MT+1] = UnitType .. " of " .. UnitTypeID + end + + return table.concat( MT, ", " ) + end + + + + --- Reports the detected items to the @{Set#SET_GROUP}. + -- @param #DETECTION_REPORTING self + -- @param Wrapper.Group#GROUP Group The @{Group} object to where the report needs to go. + -- @param Functional.Detection#DETECTION_AREAS Detection The detection created by the @{Detection#DETECTION_BASE} object. + -- @return #boolean Return true if you want the reporting to continue... false will cancel the reporting loop. + function DETECTION_REPORTING:ProcessDetected( Group, Detection ) + self:F2( Group ) + + self:E( Group ) + local DetectedMsg = {} + for DetectedAreaID, DetectedAreaData in pairs( Detection:GetDetectedAreas() ) do + local DetectedArea = DetectedAreaData -- Functional.Detection#DETECTION_AREAS.DetectedArea + DetectedMsg[#DetectedMsg+1] = " - Group #" .. DetectedAreaID .. ": " .. self:GetDetectedItemsText( DetectedArea.Set ) + end + local FACGroup = Detection:GetDetectionGroups() + FACGroup:MessageToGroup( "Reporting detected target groups:\n" .. table.concat( DetectedMsg, "\n" ), self:GetReportDisplayTime(), Group ) + + return true + end + +end + +do -- DETECTION_DISPATCHER + + --- DETECTION_DISPATCHER class. + -- @type DETECTION_DISPATCHER + -- @field Set#SET_GROUP SetGroup The groups to which the FAC will report to. + -- @field Functional.Detection#DETECTION_BASE Detection The DETECTION_BASE object that is used to report the detected objects. + -- @field Tasking.Mission#MISSION Mission + -- @field Wrapper.Group#GROUP CommandCenter + -- @extends Tasking.DetectionManager#DETECTION_MANAGER + DETECTION_DISPATCHER = { + ClassName = "DETECTION_DISPATCHER", + Mission = nil, + CommandCenter = nil, + Detection = nil, + } + + + --- DETECTION_DISPATCHER constructor. + -- @param #DETECTION_DISPATCHER self + -- @param Set#SET_GROUP SetGroup + -- @param Functional.Detection#DETECTION_BASE Detection + -- @return #DETECTION_DISPATCHER self + function DETECTION_DISPATCHER:New( Mission, CommandCenter, SetGroup, Detection ) + + -- Inherits from DETECTION_MANAGER + local self = BASE:Inherit( self, DETECTION_MANAGER:New( SetGroup, Detection ) ) -- #DETECTION_DISPATCHER + + self.Detection = Detection + self.CommandCenter = CommandCenter + self.Mission = Mission + + self:Schedule( 30 ) + return self + end + + + --- Creates a SEAD task when there are targets for it. + -- @param #DETECTION_DISPATCHER self + -- @param Functional.Detection#DETECTION_AREAS.DetectedArea DetectedArea + -- @return Set#SET_UNIT TargetSetUnit: The target set of units. + -- @return #nil If there are no targets to be set. + function DETECTION_DISPATCHER:EvaluateSEAD( DetectedArea ) + self:F( { DetectedArea.AreaID } ) + + local DetectedSet = DetectedArea.Set + local DetectedZone = DetectedArea.Zone + + -- Determine if the set has radar targets. If it does, construct a SEAD task. + local RadarCount = DetectedSet:HasSEAD() + + if RadarCount > 0 then + + -- Here we're doing something advanced... We're copying the DetectedSet, but making a new Set only with SEADable Radar units in it. + local TargetSetUnit = SET_UNIT:New() + TargetSetUnit:SetDatabase( DetectedSet ) + TargetSetUnit:FilterHasSEAD() + TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection. + + return TargetSetUnit + end + + return nil + end + + --- Creates a CAS task when there are targets for it. + -- @param #DETECTION_DISPATCHER self + -- @param Functional.Detection#DETECTION_AREAS.DetectedArea DetectedArea + -- @return Tasking.Task#TASK + function DETECTION_DISPATCHER:EvaluateCAS( DetectedArea ) + self:F( { DetectedArea.AreaID } ) + + local DetectedSet = DetectedArea.Set + local DetectedZone = DetectedArea.Zone + + + -- Determine if the set has radar targets. If it does, construct a SEAD task. + local GroundUnitCount = DetectedSet:HasGroundUnits() + local FriendliesNearBy = self.Detection:IsFriendliesNearBy( DetectedArea ) + + if GroundUnitCount > 0 and FriendliesNearBy == true then + + -- Copy the Set + local TargetSetUnit = SET_UNIT:New() + TargetSetUnit:SetDatabase( DetectedSet ) + TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection. + + return TargetSetUnit + end + + return nil + end + + --- Creates a BAI task when there are targets for it. + -- @param #DETECTION_DISPATCHER self + -- @param Functional.Detection#DETECTION_AREAS.DetectedArea DetectedArea + -- @return Tasking.Task#TASK + function DETECTION_DISPATCHER:EvaluateBAI( DetectedArea, FriendlyCoalition ) + self:F( { DetectedArea.AreaID } ) + + local DetectedSet = DetectedArea.Set + local DetectedZone = DetectedArea.Zone + + + -- Determine if the set has radar targets. If it does, construct a SEAD task. + local GroundUnitCount = DetectedSet:HasGroundUnits() + local FriendliesNearBy = self.Detection:IsFriendliesNearBy( DetectedArea ) + + if GroundUnitCount > 0 and FriendliesNearBy == false then + + -- Copy the Set + local TargetSetUnit = SET_UNIT:New() + TargetSetUnit:SetDatabase( DetectedSet ) + TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection. + + return TargetSetUnit + end + + return nil + end + + --- Evaluates the removal of the Task from the Mission. + -- Can only occur when the DetectedArea is Changed AND the state of the Task is "Planned". + -- @param #DETECTION_DISPATCHER self + -- @param Tasking.Mission#MISSION Mission + -- @param Tasking.Task#TASK Task + -- @param Functional.Detection#DETECTION_AREAS.DetectedArea DetectedArea + -- @return Tasking.Task#TASK + function DETECTION_DISPATCHER:EvaluateRemoveTask( Mission, Task, DetectedArea ) + + if Task then + if Task:IsStatePlanned() and DetectedArea.Changed == true then + self:E( "Removing Tasking: " .. Task:GetTaskName() ) + Task = Mission:RemoveTask( Task ) + end + end + + return Task + end + + + --- Assigns tasks in relation to the detected items to the @{Set#SET_GROUP}. + -- @param #DETECTION_DISPATCHER self + -- @param Functional.Detection#DETECTION_AREAS Detection The detection created by the @{Detection#DETECTION_AREAS} object. + -- @return #boolean Return true if you want the task assigning to continue... false will cancel the loop. + function DETECTION_DISPATCHER:ProcessDetected( Detection ) + self:F2() + + local AreaMsg = {} + local TaskMsg = {} + local ChangeMsg = {} + + local Mission = self.Mission + + --- First we need to the detected targets. + for DetectedAreaID, DetectedAreaData in ipairs( Detection:GetDetectedAreas() ) do + + local DetectedArea = DetectedAreaData -- Functional.Detection#DETECTION_AREAS.DetectedArea + local DetectedSet = DetectedArea.Set + local DetectedZone = DetectedArea.Zone + self:E( { "Targets in DetectedArea", DetectedArea.AreaID, DetectedSet:Count(), tostring( DetectedArea ) } ) + DetectedSet:Flush() + + local AreaID = DetectedArea.AreaID + + -- Evaluate SEAD Tasking + local SEADTask = Mission:GetTask( "SEAD." .. AreaID ) + SEADTask = self:EvaluateRemoveTask( Mission, SEADTask, DetectedArea ) + if not SEADTask then + local TargetSetUnit = self:EvaluateSEAD( DetectedArea ) -- Returns a SetUnit if there are targets to be SEADed... + if TargetSetUnit then + SEADTask = Mission:AddTask( TASK_SEAD:New( Mission, self.SetGroup, "SEAD." .. AreaID, TargetSetUnit , DetectedZone ) ) + end + end + if SEADTask and SEADTask:IsStatePlanned() then + self:E( "Planned" ) + --SEADTask:SetPlannedMenu() + TaskMsg[#TaskMsg+1] = " - " .. SEADTask:GetStateString() .. " SEAD " .. AreaID .. " - " .. SEADTask.TargetSetUnit:GetUnitTypesText() + end + + -- Evaluate CAS Tasking + local CASTask = Mission:GetTask( "CAS." .. AreaID ) + CASTask = self:EvaluateRemoveTask( Mission, CASTask, DetectedArea ) + if not CASTask then + local TargetSetUnit = self:EvaluateCAS( DetectedArea ) -- Returns a SetUnit if there are targets to be SEADed... + if TargetSetUnit then + CASTask = Mission:AddTask( TASK_A2G:New( Mission, self.SetGroup, "CAS." .. AreaID, "CAS", TargetSetUnit , DetectedZone, DetectedArea.NearestFAC ) ) + end + end + if CASTask and CASTask:IsStatePlanned() then + --CASTask:SetPlannedMenu() + TaskMsg[#TaskMsg+1] = " - " .. CASTask:GetStateString() .. " CAS " .. AreaID .. " - " .. CASTask.TargetSetUnit:GetUnitTypesText() + end + + -- Evaluate BAI Tasking + local BAITask = Mission:GetTask( "BAI." .. AreaID ) + BAITask = self:EvaluateRemoveTask( Mission, BAITask, DetectedArea ) + if not BAITask then + local TargetSetUnit = self:EvaluateBAI( DetectedArea, self.CommandCenter:GetCoalition() ) -- Returns a SetUnit if there are targets to be SEADed... + if TargetSetUnit then + BAITask = Mission:AddTask( TASK_A2G:New( Mission, self.SetGroup, "BAI." .. AreaID, "BAI", TargetSetUnit , DetectedZone, DetectedArea.NearestFAC ) ) + end + end + if BAITask and BAITask:IsStatePlanned() then + --BAITask:SetPlannedMenu() + TaskMsg[#TaskMsg+1] = " - " .. BAITask:GetStateString() .. " BAI " .. AreaID .. " - " .. BAITask.TargetSetUnit:GetUnitTypesText() + end + + if #TaskMsg > 0 then + + local ThreatLevel = Detection:GetTreatLevelA2G( DetectedArea ) + + local DetectedAreaVec3 = DetectedZone:GetVec3() + local DetectedAreaPointVec3 = POINT_VEC3:New( DetectedAreaVec3.x, DetectedAreaVec3.y, DetectedAreaVec3.z ) + local DetectedAreaPointLL = DetectedAreaPointVec3:ToStringLL( 3, true ) + AreaMsg[#AreaMsg+1] = string.format( " - Area #%d - %s - Threat Level [%s] (%2d)", + DetectedAreaID, + DetectedAreaPointLL, + string.rep( "â– ", ThreatLevel ), + ThreatLevel + ) + + -- Loop through the changes ... + local ChangeText = Detection:GetChangeText( DetectedArea ) + + if ChangeText ~= "" then + ChangeMsg[#ChangeMsg+1] = string.gsub( string.gsub( ChangeText, "\n", "%1 - " ), "^.", " - %1" ) + end + end + + -- OK, so the tasking has been done, now delete the changes reported for the area. + Detection:AcceptChanges( DetectedArea ) + + end + + -- TODO set menus using the HQ coordinator + Mission:GetCommandCenter():SetMenu() + + if #AreaMsg > 0 then + for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do + if not TaskGroup:GetState( TaskGroup, "Assigned" ) then + self.CommandCenter:MessageToGroup( + string.format( "HQ Reporting - Target areas for mission '%s':\nAreas:\n%s\n\nTasks:\n%s\n\nChanges:\n%s ", + self.Mission:GetName(), + table.concat( AreaMsg, "\n" ), + table.concat( TaskMsg, "\n" ), + table.concat( ChangeMsg, "\n" ) + ), self:GetReportDisplayTime(), TaskGroup + ) + end + end + end + + return true + end + +end--- This module contains the TASK_SEAD classes. +-- +-- 1) @{#TASK_SEAD} class, extends @{Task#TASK} +-- ================================================= +-- The @{#TASK_SEAD} class defines a SEAD task for a @{Set} of Target Units, located at a Target Zone, +-- based on the tasking capabilities defined in @{Task#TASK}. +-- The TASK_SEAD is implemented using a @{Statemachine#FSM_TASK}, and has the following statuses: +-- +-- * **None**: Start of the process +-- * **Planned**: The SEAD task is planned. Upon Planned, the sub-process @{Process_Fsm.Assign#ACT_ASSIGN_ACCEPT} is started to accept the task. +-- * **Assigned**: The SEAD task is assigned to a @{Group#GROUP}. Upon Assigned, the sub-process @{Process_Fsm.Route#ACT_ROUTE} is started to route the active Units in the Group to the attack zone. +-- * **Success**: The SEAD task is successfully completed. Upon Success, the sub-process @{Process_SEAD#PROCESS_SEAD} is started to follow-up successful SEADing of the targets assigned in the task. +-- * **Failed**: The SEAD task has failed. This will happen if the player exists the task early, without communicating a possible cancellation to HQ. +-- +-- === +-- +-- ### Authors: FlightControl - Design and Programming +-- +-- @module Task_SEAD + + + +do -- TASK_SEAD + + --- The TASK_SEAD class + -- @type TASK_SEAD + -- @field Set#SET_UNIT TargetSetUnit + -- @extends Tasking.Task#TASK + TASK_SEAD = { + ClassName = "TASK_SEAD", + } + + --- Instantiates a new TASK_SEAD. + -- @param #TASK_SEAD self + -- @param Tasking.Mission#MISSION Mission + -- @param Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. + -- @param #string TaskName The name of the Task. + -- @param Set#SET_UNIT UnitSetTargets + -- @param Core.Zone#ZONE_BASE TargetZone + -- @return #TASK_SEAD self + function TASK_SEAD:New( Mission, SetGroup, TaskName, TargetSetUnit, TargetZone ) + local self = BASE:Inherit( self, TASK:New( Mission, SetGroup, TaskName, "SEAD" ) ) -- Tasking.Task_SEAD#TASK_SEAD + self:F() + + self.TargetSetUnit = TargetSetUnit + self.TargetZone = TargetZone + + local Fsm = self:GetUnitProcess() + + Fsm:AddProcess ( "Planned", "Accept", ACT_ASSIGN_ACCEPT:New( self.TaskBriefing ), { Assigned = "Route", Rejected = "Eject" } ) + Fsm:AddProcess ( "Assigned", "Route", ACT_ROUTE_ZONE:New( self.TargetZone ), { Arrived = "Update" } ) + Fsm:AddTransition( "Rejected", "Eject", "Planned" ) + Fsm:AddTransition( "Arrived", "Update", "Updated" ) + Fsm:AddProcess ( "Updated", "Account", ACT_ACCOUNT_DEADS:New( self.TargetSetUnit, "SEAD" ), { Accounted = "Success" } ) + Fsm:AddProcess ( "Updated", "Smoke", ACT_ASSIST_SMOKE_TARGETS_ZONE:New( self.TargetSetUnit, self.TargetZone ) ) + Fsm:AddTransition( "Accounted", "Success", "Success" ) + Fsm:AddTransition( "Failed", "Fail", "Failed" ) + + function Fsm:onenterUpdated( TaskUnit ) + self:E( { self } ) + self:Account() + self:Smoke() + end + +-- _EVENTDISPATCHER:OnPlayerLeaveUnit( self._EventPlayerLeaveUnit, self ) +-- _EVENTDISPATCHER:OnDead( self._EventDead, self ) +-- _EVENTDISPATCHER:OnCrash( self._EventDead, self ) +-- _EVENTDISPATCHER:OnPilotDead( self._EventDead, self ) + + return self + end + + --- @param #TASK_SEAD self + function TASK_SEAD:GetPlannedMenuText() + return self:GetStateString() .. " - " .. self:GetTaskName() .. " ( " .. self.TargetSetUnit:GetUnitTypesText() .. " )" + end + +end +--- (AI) (SP) (MP) Tasking for Air to Ground Processes. +-- +-- 1) @{#TASK_A2G} class, extends @{Task#TASK} +-- ================================================= +-- The @{#TASK_A2G} class defines a CAS or BAI task of a @{Set} of Target Units, +-- located at a Target Zone, based on the tasking capabilities defined in @{Task#TASK}. +-- The TASK_A2G is implemented using a @{Statemachine#FSM_TASK}, and has the following statuses: +-- +-- * **None**: Start of the process +-- * **Planned**: The SEAD task is planned. Upon Planned, the sub-process @{Process_Fsm.Assign#ACT_ASSIGN_ACCEPT} is started to accept the task. +-- * **Assigned**: The SEAD task is assigned to a @{Group#GROUP}. Upon Assigned, the sub-process @{Process_Fsm.Route#ACT_ROUTE} is started to route the active Units in the Group to the attack zone. +-- * **Success**: The SEAD task is successfully completed. Upon Success, the sub-process @{Process_SEAD#PROCESS_SEAD} is started to follow-up successful SEADing of the targets assigned in the task. +-- * **Failed**: The SEAD task has failed. This will happen if the player exists the task early, without communicating a possible cancellation to HQ. +-- +-- === +-- +-- ### Authors: FlightControl - Design and Programming +-- +-- @module Task_A2G + + +do -- TASK_A2G + + --- The TASK_A2G class + -- @type TASK_A2G + -- @extends Tasking.Task#TASK + TASK_A2G = { + ClassName = "TASK_A2G", + } + + --- Instantiates a new TASK_A2G. + -- @param #TASK_A2G self + -- @param Tasking.Mission#MISSION Mission + -- @param Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. + -- @param #string TaskName The name of the Task. + -- @param #string TaskType BAI or CAS + -- @param Set#SET_UNIT UnitSetTargets + -- @param Core.Zone#ZONE_BASE TargetZone + -- @return #TASK_A2G self + function TASK_A2G:New( Mission, SetGroup, TaskName, TaskType, TargetSetUnit, TargetZone, FACUnit ) + local self = BASE:Inherit( self, TASK:New( Mission, SetGroup, TaskName, TaskType ) ) + self:F() + + self.TargetSetUnit = TargetSetUnit + self.TargetZone = TargetZone + self.FACUnit = FACUnit + + local A2GUnitProcess = self:GetUnitProcess() + + A2GUnitProcess:AddProcess ( "Planned", "Accept", ACT_ASSIGN_ACCEPT:New( "Attack the Area" ), { Assigned = "Route", Rejected = "Eject" } ) + A2GUnitProcess:AddProcess ( "Assigned", "Route", ACT_ROUTE_ZONE:New( self.TargetZone ), { Arrived = "Update" } ) + A2GUnitProcess:AddTransition( "Rejected", "Eject", "Planned" ) + A2GUnitProcess:AddTransition( "Arrived", "Update", "Updated" ) + A2GUnitProcess:AddProcess ( "Updated", "Account", ACT_ACCOUNT_DEADS:New( self.TargetSetUnit, "Attack" ), { Accounted = "Success" } ) + A2GUnitProcess:AddProcess ( "Updated", "Smoke", ACT_ASSIST_SMOKE_TARGETS_ZONE:New( self.TargetSetUnit, self.TargetZone ) ) + --Fsm:AddProcess ( "Updated", "JTAC", PROCESS_JTAC:New( self, TaskUnit, self.TargetSetUnit, self.FACUnit ) ) + A2GUnitProcess:AddTransition( "Accounted", "Success", "Success" ) + A2GUnitProcess:AddTransition( "Failed", "Fail", "Failed" ) + + function A2GUnitProcess:onenterUpdated( TaskUnit ) + self:E( { self } ) + self:Account() + self:Smoke() + end + + + + --_EVENTDISPATCHER:OnPlayerLeaveUnit( self._EventPlayerLeaveUnit, self ) + --_EVENTDISPATCHER:OnDead( self._EventDead, self ) + --_EVENTDISPATCHER:OnCrash( self._EventDead, self ) + --_EVENTDISPATCHER:OnPilotDead( self._EventDead, self ) + + return self + end + + --- @param #TASK_A2G self + function TASK_A2G:GetPlannedMenuText() + return self:GetStateString() .. " - " .. self:GetTaskName() .. " ( " .. self.TargetSetUnit:GetUnitTypesText() .. " )" + end + + end + + + +--- The main include file for the MOOSE system. + +--- Core Routines +Include.File( "Utilities/Routines" ) +Include.File( "Utilities/Utils" ) + +--- Core Classes +Include.File( "Core/Base" ) +Include.File( "Core/Scheduler" ) +Include.File( "Core/ScheduleDispatcher") +Include.File( "Core/Event" ) +Include.File( "Core/Menu" ) +Include.File( "Core/Zone" ) +Include.File( "Core/Database" ) +Include.File( "Core/Set" ) +Include.File( "Core/Point" ) +Include.File( "Core/Message" ) +Include.File( "Core/Fsm" ) + +--- Wrapper Classes +Include.File( "Wrapper/Object" ) +Include.File( "Wrapper/Identifiable" ) +Include.File( "Wrapper/Positionable" ) +Include.File( "Wrapper/Controllable" ) +Include.File( "Wrapper/Group" ) +Include.File( "Wrapper/Unit" ) +Include.File( "Wrapper/Client" ) +Include.File( "Wrapper/Static" ) +Include.File( "Wrapper/Airbase" ) +Include.File( "Wrapper/Scenery" ) + +--- Functional Classes +Include.File( "Functional/Scoring" ) +Include.File( "Functional/CleanUp" ) +Include.File( "Functional/Spawn" ) +Include.File( "Functional/Movement" ) +Include.File( "Functional/Sead" ) +Include.File( "Functional/Escort" ) +Include.File( "Functional/MissileTrainer" ) +Include.File( "Functional/AirbasePolice" ) +Include.File( "Functional/Detection" ) + +--- AI Classes +Include.File( "AI/AI_Balancer" ) +Include.File( "AI/AI_Patrol" ) +Include.File( "AI/AI_Cap" ) +Include.File( "AI/AI_Cas" ) +Include.File( "AI/AI_Cargo" ) + +--- Actions +Include.File( "Actions/Act_Assign" ) +Include.File( "Actions/Act_Route" ) +Include.File( "Actions/Act_Account" ) +Include.File( "Actions/Act_Assist" ) + +--- Task Handling Classes +Include.File( "Tasking/CommandCenter" ) +Include.File( "Tasking/Mission" ) +Include.File( "Tasking/Task" ) +Include.File( "Tasking/DetectionManager" ) +Include.File( "Tasking/Task_SEAD" ) +Include.File( "Tasking/Task_A2G" ) + + +-- The order of the declarations is important here. Don't touch it. + +--- Declare the event dispatcher based on the EVENT class +_EVENTDISPATCHER = EVENT:New() -- Core.Event#EVENT + +--- Declare the timer dispatcher based on the SCHEDULEDISPATCHER class +_SCHEDULEDISPATCHER = SCHEDULEDISPATCHER:New() -- Core.Timer#SCHEDULEDISPATCHER + +--- Declare the main database object, which is used internally by the MOOSE classes. +_DATABASE = DATABASE:New() -- Database#DATABASE + + + + +BASE:TraceOnOff( false ) env.info( '*** MOOSE INCLUDE END *** ' ) diff --git a/Moose Mission Setup/Moose.lua b/Moose Mission Setup/Moose.lua index f73ae2d1c..e32e1c346 100644 --- a/Moose Mission Setup/Moose.lua +++ b/Moose Mission Setup/Moose.lua @@ -1,31 +1,33643 @@ -env.info( '*** MOOSE DYNAMIC INCLUDE START *** ' ) -env.info( 'Moose Generation Timestamp: 20170307_0856' ) - +env.info( '*** MOOSE STATIC INCLUDE START *** ' ) +env.info( 'Moose Generation Timestamp: 20170307_0923' ) local base = _G Include = {} - +Include.Files = {} Include.File = function( IncludeFile ) - 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 - error ("Could not load MOOSE file " .. IncludeFile .. ".lua" ) +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 - env.info( "Include:" .. IncludeFile .. " loaded from " .. Include.ProgramPath ) - return f() + 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 -Include.ProgramPath = "Scripts/Moose/" -env.info( "Include.ProgramPath = " .. Include.ProgramPath) +routines.utils.toDegree = function(angle) + return angle*180/math.pi +end -Include.Files = {} +routines.utils.toRadian = function(angle) + return angle*math.pi/180 +end -Include.File( "Moose" ) +routines.utils.metersToNM = function(meters) + return meters/1852 +end -BASE:TraceOnOff( true ) +routines.utils.metersToFeet = function(meters) + return meters/0.3048 +end + +routines.utils.NMToMeters = function(NM) + return NM*1852 +end + +routines.utils.feetToMeters = function(feet) + return feet*0.3048 +end + +routines.utils.mpsToKnots = function(mps) + return mps*3600/1852 +end + +routines.utils.mpsToKmph = function(mps) + return mps*3.6 +end + +routines.utils.knotsToMps = function(knots) + return knots*1852/3600 +end + +routines.utils.kmphToMps = function(kmph) + return kmph/3.6 +end + +function routines.utils.makeVec2(Vec3) + if Vec3.z then + return {x = Vec3.x, y = Vec3.z} + else + return {x = Vec3.x, y = Vec3.y} -- it was actually already vec2. + end +end + +function routines.utils.makeVec3(Vec2, y) + if not Vec2.z then + if not y then + y = 0 + end + return {x = Vec2.x, y = y, z = Vec2.y} + else + return {x = Vec2.x, y = Vec2.y, z = Vec2.z} -- it was already Vec3, actually. + end +end + +function routines.utils.makeVec3GL(Vec2, offset) + local adj = offset or 0 + + if not Vec2.z then + return {x = Vec2.x, y = (land.getHeight(Vec2) + adj), z = Vec2.y} + else + return {x = Vec2.x, y = (land.getHeight({x = Vec2.x, y = Vec2.z}) + adj), z = Vec2.z} + end +end + +routines.utils.zoneToVec3 = function(zone) + local new = {} + if type(zone) == 'table' and zone.point then + new.x = zone.point.x + new.y = zone.point.y + new.z = zone.point.z + return new + elseif type(zone) == 'string' then + zone = trigger.misc.getZone(zone) + if zone then + new.x = zone.point.x + new.y = zone.point.y + new.z = zone.point.z + return new + end + end +end + +-- gets heading-error corrected direction from point along vector vec. +function routines.utils.getDir(vec, point) + local dir = math.atan2(vec.z, vec.x) + dir = dir + routines.getNorthCorrection(point) + if dir < 0 then + dir = dir + 2*math.pi -- put dir in range of 0 to 2*pi + end + return dir +end + +-- gets distance in meters between two points (2 dimensional) +function routines.utils.get2DDist(point1, point2) + point1 = routines.utils.makeVec3(point1) + point2 = routines.utils.makeVec3(point2) + return routines.vec.mag({x = point1.x - point2.x, y = 0, z = point1.z - point2.z}) +end + +-- gets distance in meters between two points (3 dimensional) +function routines.utils.get3DDist(point1, point2) + return routines.vec.mag({x = point1.x - point2.x, y = point1.y - point2.y, z = point1.z - point2.z}) +end + + + + + +--3D Vector manipulation +routines.vec = {} + +routines.vec.add = function(vec1, vec2) + return {x = vec1.x + vec2.x, y = vec1.y + vec2.y, z = vec1.z + vec2.z} +end + +routines.vec.sub = function(vec1, vec2) + return {x = vec1.x - vec2.x, y = vec1.y - vec2.y, z = vec1.z - vec2.z} +end + +routines.vec.scalarMult = function(vec, mult) + return {x = vec.x*mult, y = vec.y*mult, z = vec.z*mult} +end + +routines.vec.scalar_mult = routines.vec.scalarMult + +routines.vec.dp = function(vec1, vec2) + return vec1.x*vec2.x + vec1.y*vec2.y + vec1.z*vec2.z +end + +routines.vec.cp = function(vec1, vec2) + return { x = vec1.y*vec2.z - vec1.z*vec2.y, y = vec1.z*vec2.x - vec1.x*vec2.z, z = vec1.x*vec2.y - vec1.y*vec2.x} +end + +routines.vec.mag = function(vec) + return (vec.x^2 + vec.y^2 + vec.z^2)^0.5 +end + +routines.vec.getUnitVec = function(vec) + local mag = routines.vec.mag(vec) + return { x = vec.x/mag, y = vec.y/mag, z = vec.z/mag } +end + +routines.vec.rotateVec2 = function(vec2, theta) + return { x = vec2.x*math.cos(theta) - vec2.y*math.sin(theta), y = vec2.x*math.sin(theta) + vec2.y*math.cos(theta)} +end +--------------------------------------------------------------------------------------------------------------------------- + + + + +-- acc- the accuracy of each easting/northing. 0, 1, 2, 3, 4, or 5. +routines.tostringMGRS = function(MGRS, acc) + if acc == 0 then + return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph + else + return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph .. ' ' .. string.format('%0' .. acc .. 'd', routines.utils.round(MGRS.Easting/(10^(5-acc)), 0)) + .. ' ' .. string.format('%0' .. acc .. 'd', routines.utils.round(MGRS.Northing/(10^(5-acc)), 0)) + end +end + +--[[acc: +in DM: decimal point of minutes. +In DMS: decimal point of seconds. +position after the decimal of the least significant digit: +So: +42.32 - acc of 2. +]] +routines.tostringLL = function(lat, lon, acc, DMS) + + local latHemi, lonHemi + if lat > 0 then + latHemi = 'N' + else + latHemi = 'S' + end + + if lon > 0 then + lonHemi = 'E' + else + lonHemi = 'W' + end + + lat = math.abs(lat) + lon = math.abs(lon) + + local latDeg = math.floor(lat) + local latMin = (lat - latDeg)*60 + + local lonDeg = math.floor(lon) + local lonMin = (lon - lonDeg)*60 + + if DMS then -- degrees, minutes, and seconds. + local oldLatMin = latMin + latMin = math.floor(latMin) + local latSec = routines.utils.round((oldLatMin - latMin)*60, acc) + + local oldLonMin = lonMin + lonMin = math.floor(lonMin) + local lonSec = routines.utils.round((oldLonMin - lonMin)*60, acc) + + if latSec == 60 then + latSec = 0 + latMin = latMin + 1 + end + + if lonSec == 60 then + lonSec = 0 + lonMin = lonMin + 1 + end + + local secFrmtStr -- create the formatting string for the seconds place + if acc <= 0 then -- no decimal place. + secFrmtStr = '%02d' + else + local width = 3 + acc -- 01.310 - that's a width of 6, for example. + secFrmtStr = '%0' .. width .. '.' .. acc .. 'f' + end + + return string.format('%02d', latDeg) .. ' ' .. string.format('%02d', latMin) .. '\' ' .. string.format(secFrmtStr, latSec) .. '"' .. latHemi .. ' ' + .. string.format('%02d', lonDeg) .. ' ' .. string.format('%02d', lonMin) .. '\' ' .. string.format(secFrmtStr, lonSec) .. '"' .. lonHemi + + else -- degrees, decimal minutes. + latMin = routines.utils.round(latMin, acc) + lonMin = routines.utils.round(lonMin, acc) + + if latMin == 60 then + latMin = 0 + latDeg = latDeg + 1 + end + + if lonMin == 60 then + lonMin = 0 + lonDeg = lonDeg + 1 + end + + local minFrmtStr -- create the formatting string for the minutes place + if acc <= 0 then -- no decimal place. + minFrmtStr = '%02d' + else + local width = 3 + acc -- 01.310 - that's a width of 6, for example. + minFrmtStr = '%0' .. width .. '.' .. acc .. 'f' + end + + return string.format('%02d', latDeg) .. ' ' .. string.format(minFrmtStr, latMin) .. '\'' .. latHemi .. ' ' + .. string.format('%02d', lonDeg) .. ' ' .. string.format(minFrmtStr, lonMin) .. '\'' .. lonHemi + + end +end + +--[[ required: az - radian + required: dist - meters + optional: alt - meters (set to false or nil if you don't want to use it). + optional: metric - set true to get dist and alt in km and m. + precision will always be nearest degree and NM or km.]] +routines.tostringBR = function(az, dist, alt, metric) + az = routines.utils.round(routines.utils.toDegree(az), 0) + + if metric then + dist = routines.utils.round(dist/1000, 2) + else + dist = routines.utils.round(routines.utils.metersToNM(dist), 2) + end + + local s = string.format('%03d', az) .. ' for ' .. dist + + if alt then + if metric then + s = s .. ' at ' .. routines.utils.round(alt, 0) + else + s = s .. ' at ' .. routines.utils.round(routines.utils.metersToFeet(alt), 0) + end + end + return s +end + +routines.getNorthCorrection = function(point) --gets the correction needed for true north + if not point.z then --Vec2; convert to Vec3 + point.z = point.y + point.y = 0 + end + local lat, lon = coord.LOtoLL(point) + local north_posit = coord.LLtoLO(lat + 1, lon) + return math.atan2(north_posit.z - point.z, north_posit.x - point.x) +end + + +do + local idNum = 0 + + --Simplified event handler + routines.addEventHandler = function(f) --id is optional! + local handler = {} + idNum = idNum + 1 + handler.id = idNum + handler.f = f + handler.onEvent = function(self, event) + self.f(event) + end + world.addEventHandler(handler) + end + + routines.removeEventHandler = function(id) + for key, handler in pairs(world.eventHandlers) do + if handler.id and handler.id == id then + world.eventHandlers[key] = nil + return true + end + end + return false + end +end + +-- need to return a Vec3 or Vec2? +function routines.getRandPointInCircle(point, radius, innerRadius) + local theta = 2*math.pi*math.random() + local rad = math.random() + math.random() + if rad > 1 then + rad = 2 - rad + end + + local radMult + if innerRadius and innerRadius <= radius then + radMult = (radius - innerRadius)*rad + innerRadius + else + radMult = radius*rad + end + + if not point.z then --might as well work with vec2/3 + point.z = point.y + end + + local rndCoord + if radius > 0 then + rndCoord = {x = math.cos(theta)*radMult + point.x, y = math.sin(theta)*radMult + point.z} + else + rndCoord = {x = point.x, y = point.z} + end + return rndCoord +end + +routines.goRoute = function(group, path) + local misTask = { + id = 'Mission', + params = { + route = { + points = routines.utils.deepCopy(path), + }, + }, + } + if type(group) == 'string' then + group = Group.getByName(group) + end + local groupCon = group:getController() + if groupCon then + groupCon:setTask(misTask) + return true + end + + Controller.setTask(groupCon, misTask) + return false +end + + +-- Useful atomic functions from mist, ported. + +routines.ground = {} +routines.fixedWing = {} +routines.heli = {} + +routines.ground.buildWP = function(point, overRideForm, overRideSpeed) + + local wp = {} + wp.x = point.x + + if point.z then + wp.y = point.z + else + wp.y = point.y + end + local form, speed + + if point.speed and not overRideSpeed then + wp.speed = point.speed + elseif type(overRideSpeed) == 'number' then + wp.speed = overRideSpeed + else + wp.speed = routines.utils.kmphToMps(20) + end + + if point.form and not overRideForm then + form = point.form + else + form = overRideForm + end + + if not form then + wp.action = 'Cone' + else + form = string.lower(form) + if form == 'off_road' or form == 'off road' then + wp.action = 'Off Road' + elseif form == 'on_road' or form == 'on road' then + wp.action = 'On Road' + elseif form == 'rank' or form == 'line_abrest' or form == 'line abrest' or form == 'lineabrest'then + wp.action = 'Rank' + elseif form == 'cone' then + wp.action = 'Cone' + elseif form == 'diamond' then + wp.action = 'Diamond' + elseif form == 'vee' then + wp.action = 'Vee' + elseif form == 'echelon_left' or form == 'echelon left' or form == 'echelonl' then + wp.action = 'EchelonL' + elseif form == 'echelon_right' or form == 'echelon right' or form == 'echelonr' then + wp.action = 'EchelonR' + else + wp.action = 'Cone' -- if nothing matched + end + end + + wp.type = 'Turning Point' + + return wp + +end + +routines.fixedWing.buildWP = function(point, WPtype, speed, alt, altType) + + local wp = {} + wp.x = point.x + + if point.z then + wp.y = point.z + else + wp.y = point.y + end + + if alt and type(alt) == 'number' then + wp.alt = alt + else + wp.alt = 2000 + end + + if altType then + altType = string.lower(altType) + if altType == 'radio' or 'agl' then + wp.alt_type = 'RADIO' + elseif altType == 'baro' or 'asl' then + wp.alt_type = 'BARO' + end + else + wp.alt_type = 'RADIO' + end + + if point.speed then + speed = point.speed + end + + if point.type then + WPtype = point.type + end + + if not speed then + wp.speed = routines.utils.kmphToMps(500) + else + wp.speed = speed + end + + if not WPtype then + wp.action = 'Turning Point' + else + WPtype = string.lower(WPtype) + if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then + wp.action = 'Fly Over Point' + elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then + wp.action = 'Turning Point' + else + wp.action = 'Turning Point' + end + end + + wp.type = 'Turning Point' + return wp +end + +routines.heli.buildWP = function(point, WPtype, speed, alt, altType) + + local wp = {} + wp.x = point.x + + if point.z then + wp.y = point.z + else + wp.y = point.y + end + + if alt and type(alt) == 'number' then + wp.alt = alt + else + wp.alt = 500 + end + + if altType then + altType = string.lower(altType) + if altType == 'radio' or 'agl' then + wp.alt_type = 'RADIO' + elseif altType == 'baro' or 'asl' then + wp.alt_type = 'BARO' + end + else + wp.alt_type = 'RADIO' + end + + if point.speed then + speed = point.speed + end + + if point.type then + WPtype = point.type + end + + if not speed then + wp.speed = routines.utils.kmphToMps(200) + else + wp.speed = speed + end + + if not WPtype then + wp.action = 'Turning Point' + else + WPtype = string.lower(WPtype) + if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then + wp.action = 'Fly Over Point' + elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then + wp.action = 'Turning Point' + else + wp.action = 'Turning Point' + end + end + + wp.type = 'Turning Point' + return wp +end + +routines.groupToRandomPoint = function(vars) + local group = vars.group --Required + local point = vars.point --required + local radius = vars.radius or 0 + local innerRadius = vars.innerRadius + local form = vars.form or 'Cone' + local heading = vars.heading or math.random()*2*math.pi + local headingDegrees = vars.headingDegrees + local speed = vars.speed or routines.utils.kmphToMps(20) + + + local useRoads + if not vars.disableRoads then + useRoads = true + else + useRoads = false + end + + local path = {} + + if headingDegrees then + heading = headingDegrees*math.pi/180 + end + + if heading >= 2*math.pi then + heading = heading - 2*math.pi + end + + local rndCoord = routines.getRandPointInCircle(point, radius, innerRadius) + + local offset = {} + local posStart = routines.getLeadPos(group) + + offset.x = routines.utils.round(math.sin(heading - (math.pi/2)) * 50 + rndCoord.x, 3) + offset.z = routines.utils.round(math.cos(heading + (math.pi/2)) * 50 + rndCoord.y, 3) + path[#path + 1] = routines.ground.buildWP(posStart, form, speed) + + + if useRoads == true and ((point.x - posStart.x)^2 + (point.z - posStart.z)^2)^0.5 > radius * 1.3 then + path[#path + 1] = routines.ground.buildWP({['x'] = posStart.x + 11, ['z'] = posStart.z + 11}, 'off_road', speed) + path[#path + 1] = routines.ground.buildWP(posStart, 'on_road', speed) + path[#path + 1] = routines.ground.buildWP(offset, 'on_road', speed) + else + path[#path + 1] = routines.ground.buildWP({['x'] = posStart.x + 25, ['z'] = posStart.z + 25}, form, speed) + end + + path[#path + 1] = routines.ground.buildWP(offset, form, speed) + path[#path + 1] = routines.ground.buildWP(rndCoord, form, speed) + + routines.goRoute(group, path) + + return +end + +routines.groupRandomDistSelf = function(gpData, dist, form, heading, speed) + local pos = routines.getLeadPos(gpData) + local fakeZone = {} + fakeZone.radius = dist or math.random(300, 1000) + fakeZone.point = {x = pos.x, y, pos.y, z = pos.z} + routines.groupToRandomZone(gpData, fakeZone, form, heading, speed) + + return +end + +routines.groupToRandomZone = function(gpData, zone, form, heading, speed) + if type(gpData) == 'string' then + gpData = Group.getByName(gpData) + end + + if type(zone) == 'string' then + zone = trigger.misc.getZone(zone) + elseif type(zone) == 'table' and not zone.radius then + zone = trigger.misc.getZone(zone[math.random(1, #zone)]) + end + + if speed then + speed = routines.utils.kmphToMps(speed) + end + + local vars = {} + vars.group = gpData + vars.radius = zone.radius + vars.form = form + vars.headingDegrees = heading + vars.speed = speed + vars.point = routines.utils.zoneToVec3(zone) + + routines.groupToRandomPoint(vars) + + return +end + +routines.isTerrainValid = function(coord, terrainTypes) -- vec2/3 and enum or table of acceptable terrain types + if coord.z then + coord.y = coord.z + end + local typeConverted = {} + + if type(terrainTypes) == 'string' then -- if its a string it does this check + for constId, constData in pairs(land.SurfaceType) do + if string.lower(constId) == string.lower(terrainTypes) or string.lower(constData) == string.lower(terrainTypes) then + table.insert(typeConverted, constId) + end + end + elseif type(terrainTypes) == 'table' then -- if its a table it does this check + for typeId, typeData in pairs(terrainTypes) do + for constId, constData in pairs(land.SurfaceType) do + if string.lower(constId) == string.lower(typeData) or string.lower(constData) == string.lower(typeId) then + table.insert(typeConverted, constId) + end + end + end + end + for validIndex, validData in pairs(typeConverted) do + if land.getSurfaceType(coord) == land.SurfaceType[validData] then + return true + end + end + return false +end + +routines.groupToPoint = function(gpData, point, form, heading, speed, useRoads) + if type(point) == 'string' then + point = trigger.misc.getZone(point) + end + if speed then + speed = routines.utils.kmphToMps(speed) + end + + local vars = {} + vars.group = gpData + vars.form = form + vars.headingDegrees = heading + vars.speed = speed + vars.disableRoads = useRoads + vars.point = routines.utils.zoneToVec3(point) + routines.groupToRandomPoint(vars) + + return +end + + +routines.getLeadPos = function(group) + if type(group) == 'string' then -- group name + group = Group.getByName(group) + end + + local units = group:getUnits() + + local leader = units[1] + if not leader then -- SHOULD be good, but if there is a bug, this code future-proofs it then. + local lowestInd = math.huge + for ind, unit in pairs(units) do + if ind < lowestInd then + lowestInd = ind + leader = unit + end + end + end + if leader and Unit.isExist(leader) then -- maybe a little too paranoid now... + return leader:getPosition().p + end +end + +--[[ vars for routines.getMGRSString: +vars.units - table of unit names (NOT unitNameTable- maybe this should change). +vars.acc - integer between 0 and 5, inclusive +]] +routines.getMGRSString = function(vars) + local units = vars.units + local acc = vars.acc or 5 + local avgPos = routines.getAvgPos(units) + if avgPos then + return routines.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(avgPos)), acc) + end +end + +--[[ vars for routines.getLLString +vars.units - table of unit names (NOT unitNameTable- maybe this should change). +vars.acc - integer, number of numbers after decimal place +vars.DMS - if true, output in degrees, minutes, seconds. Otherwise, output in degrees, minutes. + + +]] +routines.getLLString = function(vars) + local units = vars.units + local acc = vars.acc or 3 + local DMS = vars.DMS + local avgPos = routines.getAvgPos(units) + if avgPos then + local lat, lon = coord.LOtoLL(avgPos) + return routines.tostringLL(lat, lon, acc, DMS) + end +end + +--[[ +vars.zone - table of a zone name. +vars.ref - vec3 ref point, maybe overload for vec2 as well? +vars.alt - boolean, if used, includes altitude in string +vars.metric - boolean, gives distance in km instead of NM. +]] +routines.getBRStringZone = function(vars) + local zone = trigger.misc.getZone( vars.zone ) + local ref = routines.utils.makeVec3(vars.ref, 0) -- turn it into Vec3 if it is not already. + local alt = vars.alt + local metric = vars.metric + if zone then + local vec = {x = zone.point.x - ref.x, y = zone.point.y - ref.y, z = zone.point.z - ref.z} + local dir = routines.utils.getDir(vec, ref) + local dist = routines.utils.get2DDist(zone.point, ref) + if alt then + alt = zone.y + end + return routines.tostringBR(dir, dist, alt, metric) + else + env.info( 'routines.getBRStringZone: error: zone is nil' ) + end +end + +--[[ +vars.units- table of unit names (NOT unitNameTable- maybe this should change). +vars.ref - vec3 ref point, maybe overload for vec2 as well? +vars.alt - boolean, if used, includes altitude in string +vars.metric - boolean, gives distance in km instead of NM. +]] +routines.getBRString = function(vars) + local units = vars.units + local ref = routines.utils.makeVec3(vars.ref, 0) -- turn it into Vec3 if it is not already. + local alt = vars.alt + local metric = vars.metric + local avgPos = routines.getAvgPos(units) + if avgPos then + local vec = {x = avgPos.x - ref.x, y = avgPos.y - ref.y, z = avgPos.z - ref.z} + local dir = routines.utils.getDir(vec, ref) + local dist = routines.utils.get2DDist(avgPos, ref) + if alt then + alt = avgPos.y + end + return routines.tostringBR(dir, dist, alt, metric) + end +end + + +-- Returns the Vec3 coordinates of the average position of the concentration of units most in the heading direction. +--[[ vars for routines.getLeadingPos: +vars.units - table of unit names +vars.heading - direction +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees +]] +routines.getLeadingPos = function(vars) + local units = vars.units + local heading = vars.heading + local radius = vars.radius + if vars.headingDegrees then + heading = routines.utils.toRadian(vars.headingDegrees) + end + + local unitPosTbl = {} + for i = 1, #units do + local unit = Unit.getByName(units[i]) + if unit and unit:isExist() then + unitPosTbl[#unitPosTbl + 1] = unit:getPosition().p + end + end + if #unitPosTbl > 0 then -- one more more units found. + -- first, find the unit most in the heading direction + local maxPos = -math.huge + + local maxPosInd -- maxPos - the furthest in direction defined by heading; maxPosInd = + for i = 1, #unitPosTbl do + local rotatedVec2 = routines.vec.rotateVec2(routines.utils.makeVec2(unitPosTbl[i]), heading) + if (not maxPos) or maxPos < rotatedVec2.x then + maxPos = rotatedVec2.x + maxPosInd = i + end + end + + --now, get all the units around this unit... + local avgPos + if radius then + local maxUnitPos = unitPosTbl[maxPosInd] + local avgx, avgy, avgz, totNum = 0, 0, 0, 0 + for i = 1, #unitPosTbl do + if routines.utils.get2DDist(maxUnitPos, unitPosTbl[i]) <= radius then + avgx = avgx + unitPosTbl[i].x + avgy = avgy + unitPosTbl[i].y + avgz = avgz + unitPosTbl[i].z + totNum = totNum + 1 + end + end + avgPos = { x = avgx/totNum, y = avgy/totNum, z = avgz/totNum} + else + avgPos = unitPosTbl[maxPosInd] + end + + return avgPos + end +end + + +--[[ vars for routines.getLeadingMGRSString: +vars.units - table of unit names +vars.heading - direction +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees +vars.acc - number, 0 to 5. +]] +routines.getLeadingMGRSString = function(vars) + local pos = routines.getLeadingPos(vars) + if pos then + local acc = vars.acc or 5 + return routines.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(pos)), acc) + end +end + +--[[ vars for routines.getLeadingLLString: +vars.units - table of unit names +vars.heading - direction, number +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees +vars.acc - number of digits after decimal point (can be negative) +vars.DMS - boolean, true if you want DMS. +]] +routines.getLeadingLLString = function(vars) + local pos = routines.getLeadingPos(vars) + if pos then + local acc = vars.acc or 3 + local DMS = vars.DMS + local lat, lon = coord.LOtoLL(pos) + return routines.tostringLL(lat, lon, acc, DMS) + end +end + + + +--[[ vars for routines.getLeadingBRString: +vars.units - table of unit names +vars.heading - direction, number +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees +vars.metric - boolean, if true, use km instead of NM. +vars.alt - boolean, if true, include altitude. +vars.ref - vec3/vec2 reference point. +]] +routines.getLeadingBRString = function(vars) + local pos = routines.getLeadingPos(vars) + if pos then + local ref = vars.ref + local alt = vars.alt + local metric = vars.metric + + local vec = {x = pos.x - ref.x, y = pos.y - ref.y, z = pos.z - ref.z} + local dir = routines.utils.getDir(vec, ref) + local dist = routines.utils.get2DDist(pos, ref) + if alt then + alt = pos.y + end + return routines.tostringBR(dir, dist, alt, metric) + end +end + +--[[ vars for routines.message.add + vars.text = 'Hello World' + vars.displayTime = 20 + vars.msgFor = {coa = {'red'}, countries = {'Ukraine', 'Georgia'}, unitTypes = {'A-10C'}} + +]] + +--[[ vars for routines.msgMGRS +vars.units - table of unit names (NOT unitNameTable- maybe this should change). +vars.acc - integer between 0 and 5, inclusive +vars.text - text in the message +vars.displayTime - self explanatory +vars.msgFor - scope +]] +routines.msgMGRS = function(vars) + local units = vars.units + local acc = vars.acc + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = routines.getMGRSString{units = units, acc = acc} + local newText + if string.find(text, '%%s') then -- look for %s + newText = string.format(text, s) -- insert the coordinates into the message + else -- else, just append to the end. + newText = text .. s + end + + routines.message.add{ + text = newText, + displayTime = displayTime, + msgFor = msgFor + } +end + +--[[ vars for routines.msgLL +vars.units - table of unit names (NOT unitNameTable- maybe this should change) (Yes). +vars.acc - integer, number of numbers after decimal place +vars.DMS - if true, output in degrees, minutes, seconds. Otherwise, output in degrees, minutes. +vars.text - text in the message +vars.displayTime - self explanatory +vars.msgFor - scope +]] +routines.msgLL = function(vars) + local units = vars.units -- technically, I don't really need to do this, but it helps readability. + local acc = vars.acc + local DMS = vars.DMS + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = routines.getLLString{units = units, acc = acc, DMS = DMS} + local newText + if string.find(text, '%%s') then -- look for %s + newText = string.format(text, s) -- insert the coordinates into the message + else -- else, just append to the end. + newText = text .. s + end + + routines.message.add{ + text = newText, + displayTime = displayTime, + msgFor = msgFor + } + +end + + +--[[ +vars.units- table of unit names (NOT unitNameTable- maybe this should change). +vars.ref - vec3 ref point, maybe overload for vec2 as well? +vars.alt - boolean, if used, includes altitude in string +vars.metric - boolean, gives distance in km instead of NM. +vars.text - text of the message +vars.displayTime +vars.msgFor - scope +]] +routines.msgBR = function(vars) + local units = vars.units -- technically, I don't really need to do this, but it helps readability. + local ref = vars.ref -- vec2/vec3 will be handled in routines.getBRString + local alt = vars.alt + local metric = vars.metric + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = routines.getBRString{units = units, ref = ref, alt = alt, metric = metric} + local newText + if string.find(text, '%%s') then -- look for %s + newText = string.format(text, s) -- insert the coordinates into the message + else -- else, just append to the end. + newText = text .. s + end + + routines.message.add{ + text = newText, + displayTime = displayTime, + msgFor = msgFor + } + +end + + +-------------------------------------------------------------------------------------------- +-- basically, just sub-types of routines.msgBR... saves folks the work of getting the ref point. +--[[ +vars.units- table of unit names (NOT unitNameTable- maybe this should change). +vars.ref - string red, blue +vars.alt - boolean, if used, includes altitude in string +vars.metric - boolean, gives distance in km instead of NM. +vars.text - text of the message +vars.displayTime +vars.msgFor - scope +]] +routines.msgBullseye = function(vars) + if string.lower(vars.ref) == 'red' then + vars.ref = routines.DBs.missionData.bullseye.red + routines.msgBR(vars) + elseif string.lower(vars.ref) == 'blue' then + vars.ref = routines.DBs.missionData.bullseye.blue + routines.msgBR(vars) + end +end + +--[[ +vars.units- table of unit names (NOT unitNameTable- maybe this should change). +vars.ref - unit name of reference point +vars.alt - boolean, if used, includes altitude in string +vars.metric - boolean, gives distance in km instead of NM. +vars.text - text of the message +vars.displayTime +vars.msgFor - scope +]] + +routines.msgBRA = function(vars) + if Unit.getByName(vars.ref) then + vars.ref = Unit.getByName(vars.ref):getPosition().p + if not vars.alt then + vars.alt = true + end + routines.msgBR(vars) + end +end +-------------------------------------------------------------------------------------------- + +--[[ vars for routines.msgLeadingMGRS: +vars.units - table of unit names +vars.heading - direction +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees (optional) +vars.acc - number, 0 to 5. +vars.text - text of the message +vars.displayTime +vars.msgFor - scope +]] +routines.msgLeadingMGRS = function(vars) + local units = vars.units -- technically, I don't really need to do this, but it helps readability. + local heading = vars.heading + local radius = vars.radius + local headingDegrees = vars.headingDegrees + local acc = vars.acc + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = routines.getLeadingMGRSString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc} + local newText + if string.find(text, '%%s') then -- look for %s + newText = string.format(text, s) -- insert the coordinates into the message + else -- else, just append to the end. + newText = text .. s + end + + routines.message.add{ + text = newText, + displayTime = displayTime, + msgFor = msgFor + } + + +end +--[[ vars for routines.msgLeadingLL: +vars.units - table of unit names +vars.heading - direction, number +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees (optional) +vars.acc - number of digits after decimal point (can be negative) +vars.DMS - boolean, true if you want DMS. (optional) +vars.text - text of the message +vars.displayTime +vars.msgFor - scope +]] +routines.msgLeadingLL = function(vars) + local units = vars.units -- technically, I don't really need to do this, but it helps readability. + local heading = vars.heading + local radius = vars.radius + local headingDegrees = vars.headingDegrees + local acc = vars.acc + local DMS = vars.DMS + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = routines.getLeadingLLString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc, DMS = DMS} + local newText + if string.find(text, '%%s') then -- look for %s + newText = string.format(text, s) -- insert the coordinates into the message + else -- else, just append to the end. + newText = text .. s + end + + routines.message.add{ + text = newText, + displayTime = displayTime, + msgFor = msgFor + } + +end + +--[[ +vars.units - table of unit names +vars.heading - direction, number +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees (optional) +vars.metric - boolean, if true, use km instead of NM. (optional) +vars.alt - boolean, if true, include altitude. (optional) +vars.ref - vec3/vec2 reference point. +vars.text - text of the message +vars.displayTime +vars.msgFor - scope +]] +routines.msgLeadingBR = function(vars) + local units = vars.units -- technically, I don't really need to do this, but it helps readability. + local heading = vars.heading + local radius = vars.radius + local headingDegrees = vars.headingDegrees + local metric = vars.metric + local alt = vars.alt + local ref = vars.ref -- vec2/vec3 will be handled in routines.getBRString + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = routines.getLeadingBRString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, metric = metric, alt = alt, ref = ref} + local newText + if string.find(text, '%%s') then -- look for %s + newText = string.format(text, s) -- insert the coordinates into the message + else -- else, just append to the end. + newText = text .. s + end + + routines.message.add{ + text = newText, + displayTime = displayTime, + msgFor = msgFor + } +end + + +function spairs(t, order) + -- collect the keys + local keys = {} + for k in pairs(t) do keys[#keys+1] = k end + + -- if order function given, sort by it by passing the table and keys a, b, + -- otherwise just sort the keys + if order then + table.sort(keys, function(a,b) return order(t, a, b) end) + else + table.sort(keys) + end + + -- return the iterator function + local i = 0 + return function() + i = i + 1 + if keys[i] then + return keys[i], t[keys[i]] + end + end +end + + +function routines.IsPartOfGroupInZones( CargoGroup, LandingZones ) +--trace.f() + + local CurrentZoneID = nil + + if CargoGroup then + local CargoUnits = CargoGroup:getUnits() + for CargoUnitID, CargoUnit in pairs( CargoUnits ) do + if CargoUnit and CargoUnit:getLife() >= 1.0 then + CurrentZoneID = routines.IsUnitInZones( CargoUnit, LandingZones ) + if CurrentZoneID then + break + end + end + end + end + +--trace.r( "", "", { CurrentZoneID } ) + return CurrentZoneID +end + + + +function routines.IsUnitInZones( TransportUnit, LandingZones ) +--trace.f("", "routines.IsUnitInZones" ) + + local TransportZoneResult = nil + local TransportZonePos = nil + local TransportZone = nil + + -- fill-up some local variables to support further calculations to determine location of units within the zone. + if TransportUnit then + local TransportUnitPos = TransportUnit:getPosition().p + if type( LandingZones ) == "table" then + for LandingZoneID, LandingZoneName in pairs( LandingZones ) do + TransportZone = trigger.misc.getZone( LandingZoneName ) + if TransportZone then + TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} + if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then + TransportZoneResult = LandingZoneID + break + end + end + end + else + TransportZone = trigger.misc.getZone( LandingZones ) + TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} + if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then + TransportZoneResult = 1 + end + end + if TransportZoneResult then + --trace.i( "routines", "TransportZone:" .. TransportZoneResult ) + else + --trace.i( "routines", "TransportZone:nil logic" ) + end + return TransportZoneResult + else + --trace.i( "routines", "TransportZone:nil hard" ) + return nil + end +end + +function routines.IsUnitNearZonesRadius( TransportUnit, LandingZones, ZoneRadius ) +--trace.f("", "routines.IsUnitInZones" ) + + local TransportZoneResult = nil + local TransportZonePos = nil + local TransportZone = nil + + -- fill-up some local variables to support further calculations to determine location of units within the zone. + if TransportUnit then + local TransportUnitPos = TransportUnit:getPosition().p + if type( LandingZones ) == "table" then + for LandingZoneID, LandingZoneName in pairs( LandingZones ) do + TransportZone = trigger.misc.getZone( LandingZoneName ) + if TransportZone then + TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} + if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= ZoneRadius ) then + TransportZoneResult = LandingZoneID + break + end + end + end + else + TransportZone = trigger.misc.getZone( LandingZones ) + TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} + if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= ZoneRadius ) then + TransportZoneResult = 1 + end + end + if TransportZoneResult then + --trace.i( "routines", "TransportZone:" .. TransportZoneResult ) + else + --trace.i( "routines", "TransportZone:nil logic" ) + end + return TransportZoneResult + else + --trace.i( "routines", "TransportZone:nil hard" ) + return nil + end +end + + +function routines.IsStaticInZones( TransportStatic, LandingZones ) +--trace.f() + + local TransportZoneResult = nil + local TransportZonePos = nil + local TransportZone = nil + + -- fill-up some local variables to support further calculations to determine location of units within the zone. + local TransportStaticPos = TransportStatic:getPosition().p + if type( LandingZones ) == "table" then + for LandingZoneID, LandingZoneName in pairs( LandingZones ) do + TransportZone = trigger.misc.getZone( LandingZoneName ) + if TransportZone then + TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} + if ((( TransportStaticPos.x - TransportZonePos.x)^2 + (TransportStaticPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then + TransportZoneResult = LandingZoneID + break + end + end + end + else + TransportZone = trigger.misc.getZone( LandingZones ) + TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} + if ((( TransportStaticPos.x - TransportZonePos.x)^2 + (TransportStaticPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then + TransportZoneResult = 1 + end + end + +--trace.r( "", "", { TransportZoneResult } ) + return TransportZoneResult +end + + +function routines.IsUnitInRadius( CargoUnit, ReferencePosition, Radius ) +--trace.f() + + local Valid = true + + -- fill-up some local variables to support further calculations to determine location of units within the zone. + local CargoPos = CargoUnit:getPosition().p + local ReferenceP = ReferencePosition.p + + if (((CargoPos.x - ReferenceP.x)^2 + (CargoPos.z - ReferenceP.z)^2)^0.5 <= Radius) then + else + Valid = false + end + + return Valid +end + +function routines.IsPartOfGroupInRadius( CargoGroup, ReferencePosition, Radius ) +--trace.f() + + local Valid = true + + Valid = routines.ValidateGroup( CargoGroup, "CargoGroup", Valid ) + + -- fill-up some local variables to support further calculations to determine location of units within the zone + local CargoUnits = CargoGroup:getUnits() + for CargoUnitId, CargoUnit in pairs( CargoUnits ) do + local CargoUnitPos = CargoUnit:getPosition().p +-- env.info( 'routines.IsPartOfGroupInRadius: CargoUnitPos.x = ' .. CargoUnitPos.x .. ' CargoUnitPos.z = ' .. CargoUnitPos.z ) + local ReferenceP = ReferencePosition.p +-- env.info( 'routines.IsPartOfGroupInRadius: ReferenceGroupPos.x = ' .. ReferenceGroupPos.x .. ' ReferenceGroupPos.z = ' .. ReferenceGroupPos.z ) + + if ((( CargoUnitPos.x - ReferenceP.x)^2 + (CargoUnitPos.z - ReferenceP.z)^2)^0.5 <= Radius) then + else + Valid = false + break + end + end + + return Valid +end + + +function routines.ValidateString( Variable, VariableName, Valid ) +--trace.f() + + if type( Variable ) == "string" then + if Variable == "" then + error( "routines.ValidateString: error: " .. VariableName .. " must be filled out!" ) + Valid = false + end + else + error( "routines.ValidateString: error: " .. VariableName .. " is not a string." ) + Valid = false + end + +--trace.r( "", "", { Valid } ) + return Valid +end + +function routines.ValidateNumber( Variable, VariableName, Valid ) +--trace.f() + + if type( Variable ) == "number" then + else + error( "routines.ValidateNumber: error: " .. VariableName .. " is not a number." ) + Valid = false + end + +--trace.r( "", "", { Valid } ) + return Valid + +end + +function routines.ValidateGroup( Variable, VariableName, Valid ) +--trace.f() + + if Variable == nil then + error( "routines.ValidateGroup: error: " .. VariableName .. " is a nil value!" ) + Valid = false + end + +--trace.r( "", "", { Valid } ) + return Valid +end + +function routines.ValidateZone( LandingZones, VariableName, Valid ) +--trace.f() + + if LandingZones == nil then + error( "routines.ValidateGroup: error: " .. VariableName .. " is a nil value!" ) + Valid = false + end + + if type( LandingZones ) == "table" then + for LandingZoneID, LandingZoneName in pairs( LandingZones ) do + if trigger.misc.getZone( LandingZoneName ) == nil then + error( "routines.ValidateGroup: error: Zone " .. LandingZoneName .. " does not exist!" ) + Valid = false + break + end + end + else + if trigger.misc.getZone( LandingZones ) == nil then + error( "routines.ValidateGroup: error: Zone " .. LandingZones .. " does not exist!" ) + Valid = false + end + end + +--trace.r( "", "", { Valid } ) + return Valid +end + +function routines.ValidateEnumeration( Variable, VariableName, Enum, Valid ) +--trace.f() + + local ValidVariable = false + + for EnumId, EnumData in pairs( Enum ) do + if Variable == EnumData then + ValidVariable = true + break + end + end + + if ValidVariable then + else + error( 'TransportValidateEnum: " .. VariableName .. " is not a valid type.' .. Variable ) + Valid = false + end + +--trace.r( "", "", { Valid } ) + return Valid +end + +function routines.getGroupRoute(groupIdent, task) -- same as getGroupPoints but returns speed and formation type along with vec2 of point} + -- refactor to search by groupId and allow groupId and groupName as inputs + local gpId = groupIdent + if type(groupIdent) == 'string' and not tonumber(groupIdent) then + gpId = _DATABASE.Templates.Groups[groupIdent].groupId + end + + for coa_name, coa_data in pairs(env.mission.coalition) do + if (coa_name == 'red' or coa_name == 'blue') and type(coa_data) == 'table' then + if coa_data.country then --there is a country table + for cntry_id, cntry_data in pairs(coa_data.country) do + for obj_type_name, obj_type_data in pairs(cntry_data) do + if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" then -- only these types have points + if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! + for group_num, group_data in pairs(obj_type_data.group) do + if group_data and group_data.groupId == gpId then -- this is the group we are looking for + if group_data.route and group_data.route.points and #group_data.route.points > 0 then + local points = {} + + for point_num, point in pairs(group_data.route.points) do + local routeData = {} + if not point.point then + routeData.x = point.x + routeData.y = point.y + else + routeData.point = point.point --it's possible that the ME could move to the point = Vec2 notation. + end + routeData.form = point.action + routeData.speed = point.speed + routeData.alt = point.alt + routeData.alt_type = point.alt_type + routeData.airdromeId = point.airdromeId + routeData.helipadId = point.helipadId + routeData.type = point.type + routeData.action = point.action + if task then + routeData.task = point.task + end + points[point_num] = routeData + end + + return points + end + return + end --if group_data and group_data.name and group_data.name == 'groupname' + end --for group_num, group_data in pairs(obj_type_data.group) do + end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then + end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then + end --for obj_type_name, obj_type_data in pairs(cntry_data) do + end --for cntry_id, cntry_data in pairs(coa_data.country) do + end --if coa_data.country then --there is a country table + end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then + end --for coa_name, coa_data in pairs(mission.coalition) do +end + +routines.ground.patrolRoute = function(vars) + + + local tempRoute = {} + local useRoute = {} + local gpData = vars.gpData + if type(gpData) == 'string' then + gpData = Group.getByName(gpData) + end + + local useGroupRoute + if not vars.useGroupRoute then + useGroupRoute = vars.gpData + else + useGroupRoute = vars.useGroupRoute + end + local routeProvided = false + if not vars.route then + if useGroupRoute then + tempRoute = routines.getGroupRoute(useGroupRoute) + end + else + useRoute = vars.route + local posStart = routines.getLeadPos(gpData) + useRoute[1] = routines.ground.buildWP(posStart, useRoute[1].action, useRoute[1].speed) + routeProvided = true + end + + + local overRideSpeed = vars.speed or 'default' + local pType = vars.pType + local offRoadForm = vars.offRoadForm or 'default' + local onRoadForm = vars.onRoadForm or 'default' + + if routeProvided == false and #tempRoute > 0 then + local posStart = routines.getLeadPos(gpData) + + + useRoute[#useRoute + 1] = routines.ground.buildWP(posStart, offRoadForm, overRideSpeed) + for i = 1, #tempRoute do + local tempForm = tempRoute[i].action + local tempSpeed = tempRoute[i].speed + + if offRoadForm == 'default' then + tempForm = tempRoute[i].action + end + if onRoadForm == 'default' then + onRoadForm = 'On Road' + end + if (string.lower(tempRoute[i].action) == 'on road' or string.lower(tempRoute[i].action) == 'onroad' or string.lower(tempRoute[i].action) == 'on_road') then + tempForm = onRoadForm + else + tempForm = offRoadForm + end + + if type(overRideSpeed) == 'number' then + tempSpeed = overRideSpeed + end + + + useRoute[#useRoute + 1] = routines.ground.buildWP(tempRoute[i], tempForm, tempSpeed) + end + + if pType and string.lower(pType) == 'doubleback' then + local curRoute = routines.utils.deepCopy(useRoute) + for i = #curRoute, 2, -1 do + useRoute[#useRoute + 1] = routines.ground.buildWP(curRoute[i], curRoute[i].action, curRoute[i].speed) + end + end + + useRoute[1].action = useRoute[#useRoute].action -- make it so the first WP matches the last WP + end + + local cTask3 = {} + local newPatrol = {} + newPatrol.route = useRoute + newPatrol.gpData = gpData:getName() + cTask3[#cTask3 + 1] = 'routines.ground.patrolRoute(' + cTask3[#cTask3 + 1] = routines.utils.oneLineSerialize(newPatrol) + cTask3[#cTask3 + 1] = ')' + cTask3 = table.concat(cTask3) + local tempTask = { + id = 'WrappedAction', + params = { + action = { + id = 'Script', + params = { + command = cTask3, + + }, + }, + }, + } + + + useRoute[#useRoute].task = tempTask + routines.goRoute(gpData, useRoute) + + return +end + +routines.ground.patrol = function(gpData, pType, form, speed) + local vars = {} + + if type(gpData) == 'table' and gpData:getName() then + gpData = gpData:getName() + end + + vars.useGroupRoute = gpData + vars.gpData = gpData + vars.pType = pType + vars.offRoadForm = form + vars.speed = speed + + routines.ground.patrolRoute(vars) + + return +end + +function routines.GetUnitHeight( CheckUnit ) +--trace.f( "routines" ) + + local UnitPoint = CheckUnit:getPoint() + local UnitPosition = { x = UnitPoint.x, y = UnitPoint.z } + local UnitHeight = UnitPoint.y + + local LandHeight = land.getHeight( UnitPosition ) + + --env.info(( 'CarrierHeight: LandHeight = ' .. LandHeight .. ' CarrierHeight = ' .. CarrierHeight )) + + --trace.f( "routines", "Unit Height = " .. UnitHeight - LandHeight ) + + return UnitHeight - LandHeight + +end + + + +Su34Status = { status = {} } +boardMsgRed = { statusMsg = "" } +boardMsgAll = { timeMsg = "" } +SpawnSettings = {} +Su34MenuPath = {} +Su34Menus = 0 + + +function Su34AttackCarlVinson(groupName) +--trace.menu("", "Su34AttackCarlVinson") + local groupSu34 = Group.getByName( groupName ) + local controllerSu34 = groupSu34.getController(groupSu34) + local groupCarlVinson = Group.getByName("US Carl Vinson #001") + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE ) + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) + if groupCarlVinson ~= nil then + controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupCarlVinson:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = true}}) + end + Su34Status.status[groupName] = 1 + MessageToRed( string.format('%s: ',groupName) .. 'Attacking carrier Carl Vinson. ', 10, 'RedStatus' .. groupName ) +end + +function Su34AttackWest(groupName) +--trace.f("","Su34AttackWest") + local groupSu34 = Group.getByName( groupName ) + local controllerSu34 = groupSu34.getController(groupSu34) + local groupShipWest1 = Group.getByName("US Ship West #001") + local groupShipWest2 = Group.getByName("US Ship West #002") + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE ) + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) + if groupShipWest1 ~= nil then + controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipWest1:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = true}}) + end + if groupShipWest2 ~= nil then + controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipWest2:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = true}}) + end + Su34Status.status[groupName] = 2 + MessageToRed( string.format('%s: ',groupName) .. 'Attacking invading ships in the west. ', 10, 'RedStatus' .. groupName ) +end + +function Su34AttackNorth(groupName) +--trace.menu("","Su34AttackNorth") + local groupSu34 = Group.getByName( groupName ) + local controllerSu34 = groupSu34.getController(groupSu34) + local groupShipNorth1 = Group.getByName("US Ship North #001") + local groupShipNorth2 = Group.getByName("US Ship North #002") + local groupShipNorth3 = Group.getByName("US Ship North #003") + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE ) + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) + if groupShipNorth1 ~= nil then + controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipNorth1:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = false}}) + end + if groupShipNorth2 ~= nil then + controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipNorth2:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = false}}) + end + if groupShipNorth3 ~= nil then + controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipNorth3:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = false}}) + end + Su34Status.status[groupName] = 3 + MessageToRed( string.format('%s: ',groupName) .. 'Attacking invading ships in the north. ', 10, 'RedStatus' .. groupName ) +end + +function Su34Orbit(groupName) +--trace.menu("","Su34Orbit") + local groupSu34 = Group.getByName( groupName ) + local controllerSu34 = groupSu34:getController() + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD ) + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) + controllerSu34:pushTask( {id = 'ControlledTask', params = { task = { id = 'Orbit', params = { pattern = AI.Task.OrbitPattern.RACE_TRACK } }, stopCondition = { duration = 600 } } } ) + Su34Status.status[groupName] = 4 + MessageToRed( string.format('%s: ',groupName) .. 'In orbit and awaiting further instructions. ', 10, 'RedStatus' .. groupName ) +end + +function Su34TakeOff(groupName) +--trace.menu("","Su34TakeOff") + local groupSu34 = Group.getByName( groupName ) + local controllerSu34 = groupSu34:getController() + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD ) + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.BYPASS_AND_ESCAPE ) + Su34Status.status[groupName] = 8 + MessageToRed( string.format('%s: ',groupName) .. 'Take-Off. ', 10, 'RedStatus' .. groupName ) +end + +function Su34Hold(groupName) +--trace.menu("","Su34Hold") + local groupSu34 = Group.getByName( groupName ) + local controllerSu34 = groupSu34:getController() + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD ) + controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.BYPASS_AND_ESCAPE ) + Su34Status.status[groupName] = 5 + MessageToRed( string.format('%s: ',groupName) .. 'Holding Weapons. ', 10, 'RedStatus' .. groupName ) +end + +function Su34RTB(groupName) +--trace.menu("","Su34RTB") + Su34Status.status[groupName] = 6 + MessageToRed( string.format('%s: ',groupName) .. 'Return to Krasnodar. ', 10, 'RedStatus' .. groupName ) +end + +function Su34Destroyed(groupName) +--trace.menu("","Su34Destroyed") + Su34Status.status[groupName] = 7 + MessageToRed( string.format('%s: ',groupName) .. 'Destroyed. ', 30, 'RedStatus' .. groupName ) +end + +function GroupAlive( groupName ) +--trace.menu("","GroupAlive") + local groupTest = Group.getByName( groupName ) + + local groupExists = false + + if groupTest then + groupExists = groupTest:isExist() + end + + --trace.r( "", "", { groupExists } ) + return groupExists +end + +function Su34IsDead() +--trace.f() + +end + +function Su34OverviewStatus() +--trace.menu("","Su34OverviewStatus") + local msg = "" + local currentStatus = 0 + local Exists = false + + for groupName, currentStatus in pairs(Su34Status.status) do + + env.info(('Su34 Overview Status: GroupName = ' .. groupName )) + Alive = GroupAlive( groupName ) + + if Alive then + if currentStatus == 1 then + msg = msg .. string.format("%s: ",groupName) + msg = msg .. "Attacking carrier Carl Vinson. " + elseif currentStatus == 2 then + msg = msg .. string.format("%s: ",groupName) + msg = msg .. "Attacking supporting ships in the west. " + elseif currentStatus == 3 then + msg = msg .. string.format("%s: ",groupName) + msg = msg .. "Attacking invading ships in the north. " + elseif currentStatus == 4 then + msg = msg .. string.format("%s: ",groupName) + msg = msg .. "In orbit and awaiting further instructions. " + elseif currentStatus == 5 then + msg = msg .. string.format("%s: ",groupName) + msg = msg .. "Holding Weapons. " + elseif currentStatus == 6 then + msg = msg .. string.format("%s: ",groupName) + msg = msg .. "Return to Krasnodar. " + elseif currentStatus == 7 then + msg = msg .. string.format("%s: ",groupName) + msg = msg .. "Destroyed. " + elseif currentStatus == 8 then + msg = msg .. string.format("%s: ",groupName) + msg = msg .. "Take-Off. " + end + else + if currentStatus == 7 then + msg = msg .. string.format("%s: ",groupName) + msg = msg .. "Destroyed. " + else + Su34Destroyed(groupName) + end + end + end + + boardMsgRed.statusMsg = msg +end + + +function UpdateBoardMsg() +--trace.f() + Su34OverviewStatus() + MessageToRed( boardMsgRed.statusMsg, 15, 'RedStatus' ) +end + +function MusicReset( flg ) +--trace.f() + trigger.action.setUserFlag(95,flg) +end + +function PlaneActivate(groupNameFormat, flg) +--trace.f() + local groupName = groupNameFormat .. string.format("#%03d", trigger.misc.getUserFlag(flg)) + --trigger.action.outText(groupName,10) + trigger.action.activateGroup(Group.getByName(groupName)) +end + +function Su34Menu(groupName) +--trace.f() + + --env.info(( 'Su34Menu(' .. groupName .. ')' )) + local groupSu34 = Group.getByName( groupName ) + + if Su34Status.status[groupName] == 1 or + Su34Status.status[groupName] == 2 or + Su34Status.status[groupName] == 3 or + Su34Status.status[groupName] == 4 or + Su34Status.status[groupName] == 5 then + if Su34MenuPath[groupName] == nil then + if planeMenuPath == nil then + planeMenuPath = missionCommands.addSubMenuForCoalition( + coalition.side.RED, + "SU-34 anti-ship flights", + nil + ) + end + Su34MenuPath[groupName] = missionCommands.addSubMenuForCoalition( + coalition.side.RED, + "Flight " .. groupName, + planeMenuPath + ) + + missionCommands.addCommandForCoalition( + coalition.side.RED, + "Attack carrier Carl Vinson", + Su34MenuPath[groupName], + Su34AttackCarlVinson, + groupName + ) + + missionCommands.addCommandForCoalition( + coalition.side.RED, + "Attack ships in the west", + Su34MenuPath[groupName], + Su34AttackWest, + groupName + ) + + missionCommands.addCommandForCoalition( + coalition.side.RED, + "Attack ships in the north", + Su34MenuPath[groupName], + Su34AttackNorth, + groupName + ) + + missionCommands.addCommandForCoalition( + coalition.side.RED, + "Hold position and await instructions", + Su34MenuPath[groupName], + Su34Orbit, + groupName + ) + + missionCommands.addCommandForCoalition( + coalition.side.RED, + "Report status", + Su34MenuPath[groupName], + Su34OverviewStatus + ) + end + else + if Su34MenuPath[groupName] then + missionCommands.removeItemForCoalition(coalition.side.RED, Su34MenuPath[groupName]) + end + end +end + +--- Obsolete function, but kept to rework in framework. + +function ChooseInfantry ( TeleportPrefixTable, TeleportMax ) +--trace.f("Spawn") + --env.info(( 'ChooseInfantry: ' )) + + TeleportPrefixTableCount = #TeleportPrefixTable + TeleportPrefixTableIndex = math.random( 1, TeleportPrefixTableCount ) + + --env.info(( 'ChooseInfantry: TeleportPrefixTableIndex = ' .. TeleportPrefixTableIndex .. ' TeleportPrefixTableCount = ' .. TeleportPrefixTableCount .. ' TeleportMax = ' .. TeleportMax )) + + local TeleportFound = false + local TeleportLoop = true + local Index = TeleportPrefixTableIndex + local TeleportPrefix = '' + + while TeleportLoop do + TeleportPrefix = TeleportPrefixTable[Index] + if SpawnSettings[TeleportPrefix] then + if SpawnSettings[TeleportPrefix]['SpawnCount'] - 1 < TeleportMax then + SpawnSettings[TeleportPrefix]['SpawnCount'] = SpawnSettings[TeleportPrefix]['SpawnCount'] + 1 + TeleportFound = true + else + TeleportFound = false + end + else + SpawnSettings[TeleportPrefix] = {} + SpawnSettings[TeleportPrefix]['SpawnCount'] = 0 + TeleportFound = true + end + if TeleportFound then + TeleportLoop = false + else + if Index < TeleportPrefixTableCount then + Index = Index + 1 + else + TeleportLoop = false + end + end + --env.info(( 'ChooseInfantry: Loop 1 - TeleportPrefix = ' .. TeleportPrefix .. ' Index = ' .. Index )) + end + + if TeleportFound == false then + TeleportLoop = true + Index = 1 + while TeleportLoop do + TeleportPrefix = TeleportPrefixTable[Index] + if SpawnSettings[TeleportPrefix] then + if SpawnSettings[TeleportPrefix]['SpawnCount'] - 1 < TeleportMax then + SpawnSettings[TeleportPrefix]['SpawnCount'] = SpawnSettings[TeleportPrefix]['SpawnCount'] + 1 + TeleportFound = true + else + TeleportFound = false + end + else + SpawnSettings[TeleportPrefix] = {} + SpawnSettings[TeleportPrefix]['SpawnCount'] = 0 + TeleportFound = true + end + if TeleportFound then + TeleportLoop = false + else + if Index < TeleportPrefixTableIndex then + Index = Index + 1 + else + TeleportLoop = false + end + end + --env.info(( 'ChooseInfantry: Loop 2 - TeleportPrefix = ' .. TeleportPrefix .. ' Index = ' .. Index )) + end + end + + local TeleportGroupName = '' + if TeleportFound == true then + TeleportGroupName = TeleportPrefix .. string.format("#%03d", SpawnSettings[TeleportPrefix]['SpawnCount'] ) + else + TeleportGroupName = '' + end + + --env.info(('ChooseInfantry: TeleportGroupName = ' .. TeleportGroupName )) + --env.info(('ChooseInfantry: return')) + + return TeleportGroupName +end + +SpawnedInfantry = 0 + +function LandCarrier ( CarrierGroup, LandingZonePrefix ) +--trace.f() + --env.info(( 'LandCarrier: ' )) + --env.info(( 'LandCarrier: CarrierGroup = ' .. CarrierGroup:getName() )) + --env.info(( 'LandCarrier: LandingZone = ' .. LandingZonePrefix )) + + local controllerGroup = CarrierGroup:getController() + + local LandingZone = trigger.misc.getZone(LandingZonePrefix) + local LandingZonePos = {} + LandingZonePos.x = LandingZone.point.x + math.random(LandingZone.radius * -1, LandingZone.radius) + LandingZonePos.y = LandingZone.point.z + math.random(LandingZone.radius * -1, LandingZone.radius) + + controllerGroup:pushTask( { id = 'Land', params = { point = LandingZonePos, durationFlag = true, duration = 10 } } ) + + --env.info(( 'LandCarrier: end' )) +end + +EscortCount = 0 +function EscortCarrier ( CarrierGroup, EscortPrefix, EscortLastWayPoint, EscortEngagementDistanceMax, EscortTargetTypes ) +--trace.f() + --env.info(( 'EscortCarrier: ' )) + --env.info(( 'EscortCarrier: CarrierGroup = ' .. CarrierGroup:getName() )) + --env.info(( 'EscortCarrier: EscortPrefix = ' .. EscortPrefix )) + + local CarrierName = CarrierGroup:getName() + + local EscortMission = {} + local CarrierMission = {} + + local EscortMission = SpawnMissionGroup( EscortPrefix ) + local CarrierMission = SpawnMissionGroup( CarrierGroup:getName() ) + + if EscortMission ~= nil and CarrierMission ~= nil then + + EscortCount = EscortCount + 1 + EscortMissionName = string.format( EscortPrefix .. '#Escort %s', CarrierName ) + EscortMission.name = EscortMissionName + EscortMission.groupId = nil + EscortMission.lateActivation = false + EscortMission.taskSelected = false + + local EscortUnits = #EscortMission.units + for u = 1, EscortUnits do + EscortMission.units[u].name = string.format( EscortPrefix .. '#Escort %s %02d', CarrierName, u ) + EscortMission.units[u].unitId = nil + end + + + EscortMission.route.points[1].task = { id = "ComboTask", + params = + { + tasks = + { + [1] = + { + enabled = true, + auto = false, + id = "Escort", + number = 1, + params = + { + lastWptIndexFlagChangedManually = false, + groupId = CarrierGroup:getID(), + lastWptIndex = nil, + lastWptIndexFlag = false, + engagementDistMax = EscortEngagementDistanceMax, + targetTypes = EscortTargetTypes, + pos = + { + y = 20, + x = 20, + z = 0, + } -- end of ["pos"] + } -- end of ["params"] + } -- end of [1] + } -- end of ["tasks"] + } -- end of ["params"] + } -- end of ["task"] + + SpawnGroupAdd( EscortPrefix, EscortMission ) + + end +end + +function SendMessageToCarrier( CarrierGroup, CarrierMessage ) +--trace.f() + + if CarrierGroup ~= nil then + MessageToGroup( CarrierGroup, CarrierMessage, 30, 'Carrier/' .. CarrierGroup:getName() ) + end + +end + +function MessageToGroup( MsgGroup, MsgText, MsgTime, MsgName ) +--trace.f() + + if type(MsgGroup) == 'string' then + --env.info( 'MessageToGroup: Converted MsgGroup string "' .. MsgGroup .. '" into a Group structure.' ) + MsgGroup = Group.getByName( MsgGroup ) + end + + if MsgGroup ~= nil then + local MsgTable = {} + MsgTable.text = MsgText + MsgTable.displayTime = MsgTime + MsgTable.msgFor = { units = { MsgGroup:getUnits()[1]:getName() } } + MsgTable.name = MsgName + --routines.message.add( MsgTable ) + --env.info(('MessageToGroup: Message sent to ' .. MsgGroup:getUnits()[1]:getName() .. ' -> ' .. MsgText )) + end +end + +function MessageToUnit( UnitName, MsgText, MsgTime, MsgName ) +--trace.f() + + if UnitName ~= nil then + local MsgTable = {} + MsgTable.text = MsgText + MsgTable.displayTime = MsgTime + MsgTable.msgFor = { units = { UnitName } } + MsgTable.name = MsgName + --routines.message.add( MsgTable ) + end +end + +function MessageToAll( MsgText, MsgTime, MsgName ) +--trace.f() + + MESSAGE:New( MsgText, MsgTime, "Message" ):ToCoalition( coalition.side.RED ):ToCoalition( coalition.side.BLUE ) +end + +function MessageToRed( MsgText, MsgTime, MsgName ) +--trace.f() + + MESSAGE:New( MsgText, MsgTime, "To Red Coalition" ):ToCoalition( coalition.side.RED ) +end + +function MessageToBlue( MsgText, MsgTime, MsgName ) +--trace.f() + + MESSAGE:New( MsgText, MsgTime, "To Blue Coalition" ):ToCoalition( coalition.side.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 derived utilities taken from the MIST framework, +-- which are excellent tools to be reused in an OO environment!. +-- +-- ### Authors: +-- +-- * Grimes : Design & Programming of the MIST framework. +-- +-- ### Contributions: +-- +-- * FlightControl : Rework to OO framework +-- +-- @module Utils + + +--- @type SMOKECOLOR +-- @field Green +-- @field Red +-- @field White +-- @field Orange +-- @field Blue + +SMOKECOLOR = trigger.smokeColor -- #SMOKECOLOR + +--- @type FLARECOLOR +-- @field Green +-- @field Red +-- @field White +-- @field Yellow + +FLARECOLOR = trigger.flareColor -- #FLARECOLOR + +--- Utilities static class. +-- @type UTILS +UTILS = {} + + +--from http://lua-users.org/wiki/CopyTable +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 +UTILS.OneLineSerialize = function( tbl ) -- serialization of a table all on a single line, no comments, made to replace old get_table_string function + + lookup_table = {} + + local function _Serialize( tbl ) + + if type(tbl) == 'table' then --function only works for tables! + + if lookup_table[tbl] then + return lookup_table[object] + end + + local tbl_str = {} + + lookup_table[tbl] = tbl_str + + tbl_str[#tbl_str + 1] = '{' + + for ind,val in pairs(tbl) do -- serialize its fields + local ind_str = {} + if type(ind) == "number" then + ind_str[#ind_str + 1] = '[' + ind_str[#ind_str + 1] = tostring(ind) + ind_str[#ind_str + 1] = ']=' + else --must be a string + ind_str[#ind_str + 1] = '[' + ind_str[#ind_str + 1] = routines.utils.basicSerialize(ind) + ind_str[#ind_str + 1] = ']=' + end + + local val_str = {} + if ((type(val) == 'number') or (type(val) == 'boolean')) then + val_str[#val_str + 1] = tostring(val) + val_str[#val_str + 1] = ',' + tbl_str[#tbl_str + 1] = table.concat(ind_str) + tbl_str[#tbl_str + 1] = table.concat(val_str) + elseif type(val) == 'string' then + val_str[#val_str + 1] = routines.utils.basicSerialize(val) + val_str[#val_str + 1] = ',' + tbl_str[#tbl_str + 1] = table.concat(ind_str) + tbl_str[#tbl_str + 1] = table.concat(val_str) + elseif type(val) == 'nil' then -- won't ever happen, right? + val_str[#val_str + 1] = 'nil,' + tbl_str[#tbl_str + 1] = table.concat(ind_str) + tbl_str[#tbl_str + 1] = table.concat(val_str) + elseif type(val) == 'table' then + if ind == "__index" then + -- tbl_str[#tbl_str + 1] = "__index" + -- tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it + else + + val_str[#val_str + 1] = _Serialize(val) + val_str[#val_str + 1] = ',' --I think this is right, I just added it + tbl_str[#tbl_str + 1] = table.concat(ind_str) + tbl_str[#tbl_str + 1] = table.concat(val_str) + end + elseif type(val) == 'function' then + tbl_str[#tbl_str + 1] = "f() " .. tostring(ind) + tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it + else + env.info('unable to serialize value type ' .. routines.utils.basicSerialize(type(val)) .. ' at index ' .. tostring(ind)) + env.info( debug.traceback() ) + end + + end + tbl_str[#tbl_str + 1] = '}' + return table.concat(tbl_str) + else + return tostring(tbl) + end + end + + local objectreturn = _Serialize(tbl) + return objectreturn +end + +--porting in Slmod's "safestring" basic serialize +UTILS.BasicSerialize = function(s) + if s == nil then + return "\"\"" + else + if ((type(s) == 'number') or (type(s) == 'boolean') or (type(s) == 'function') or (type(s) == 'table') or (type(s) == 'userdata') ) then + return tostring(s) + elseif type(s) == 'string' then + s = string.format('%q', s) + return s + end + end +end + + +UTILS.ToDegree = function(angle) + return angle*180/math.pi +end + +UTILS.ToRadian = function(angle) + return angle*math.pi/180 +end + +UTILS.MetersToNM = function(meters) + return meters/1852 +end + +UTILS.MetersToFeet = function(meters) + return meters/0.3048 +end + +UTILS.NMToMeters = function(NM) + return NM*1852 +end + +UTILS.FeetToMeters = function(feet) + return feet*0.3048 +end + +UTILS.MpsToKnots = function(mps) + return mps*3600/1852 +end + +UTILS.MpsToKmph = function(mps) + return mps*3.6 +end + +UTILS.KnotsToMps = function(knots) + return knots*1852/3600 +end + +UTILS.KmphToMps = function(kmph) + return kmph/3.6 +end + +--[[acc: +in DM: decimal point of minutes. +In DMS: decimal point of seconds. +position after the decimal of the least significant digit: +So: +42.32 - acc of 2. +]] +UTILS.tostringLL = function( lat, lon, acc, DMS) + + local latHemi, lonHemi + if lat > 0 then + latHemi = 'N' + else + latHemi = 'S' + end + + if lon > 0 then + lonHemi = 'E' + else + lonHemi = 'W' + end + + lat = math.abs(lat) + lon = math.abs(lon) + + local latDeg = math.floor(lat) + local latMin = (lat - latDeg)*60 + + local lonDeg = math.floor(lon) + local lonMin = (lon - lonDeg)*60 + + if DMS then -- degrees, minutes, and seconds. + local oldLatMin = latMin + latMin = math.floor(latMin) + local latSec = UTILS.Round((oldLatMin - latMin)*60, acc) + + local oldLonMin = lonMin + lonMin = math.floor(lonMin) + local lonSec = UTILS.Round((oldLonMin - lonMin)*60, acc) + + if latSec == 60 then + latSec = 0 + latMin = latMin + 1 + end + + if lonSec == 60 then + lonSec = 0 + lonMin = lonMin + 1 + end + + local secFrmtStr -- create the formatting string for the seconds place + 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 = UTILS.Round(latMin, acc) + lonMin = UTILS.Round(lonMin, acc) + + if latMin == 60 then + latMin = 0 + latDeg = latDeg + 1 + end + + if lonMin == 60 then + lonMin = 0 + lonDeg = lonDeg + 1 + end + + local minFrmtStr -- create the formatting string for the minutes place + if acc <= 0 then -- no decimal place. + minFrmtStr = '%02d' + else + local width = 3 + acc -- 01.310 - that's a width of 6, for example. + minFrmtStr = '%0' .. width .. '.' .. acc .. 'f' + end + + return string.format('%02d', latDeg) .. ' ' .. string.format(minFrmtStr, latMin) .. '\'' .. latHemi .. ' ' + .. string.format('%02d', lonDeg) .. ' ' .. string.format(minFrmtStr, lonMin) .. '\'' .. lonHemi + + end +end + + +--- From http://lua-users.org/wiki/SimpleRound +-- use negative idp for rounding ahead of decimal place, positive for rounding after decimal place +function UTILS.Round( num, idp ) + local mult = 10 ^ ( idp or 0 ) + return math.floor( num * mult + 0.5 ) / mult +end + +-- porting in Slmod's dostring +function UTILS.DoString( s ) + local f, err = loadstring( s ) + if f then + return true, f() + else + return false, err + end +end +--- 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.2.3) Trace activation. +-- +-- Tracing can be activated in several ways: +-- +-- * Switch tracing on or off through the @{#BASE.TraceOnOff}() method. +-- * Activate all tracing through the @{#BASE.TraceAll}() method. +-- * Activate only the tracing of a certain class (name) through the @{#BASE.TraceClass}() method. +-- * Activate only the tracing of a certain method of a certain class through the @{#BASE.TraceClassMethod}() method. +-- * Activate only the tracing of a certain level through the @{#BASE.TraceLevel}() method. +-- ### 1.2.4) Check if tracing is on. +-- +-- The method @{#BASE.IsTrace}() will validate if tracing is activated or not. +-- +-- ## 1.3 DCS simulator Event Handling +-- +-- The BASE class provides methods to catch DCS Events. These are events that are triggered from within the DCS simulator, +-- and handled through lua scripting. MOOSE provides an encapsulation to handle these events more efficiently. +-- +-- ### 1.3.1 Subscribe / Unsubscribe to DCS Events +-- +-- At first, the mission designer will need to **Subscribe** to a specific DCS event for the class. +-- So, when the DCS event occurs, the class will be notified of that event. +-- There are two functions which you use to subscribe to or unsubscribe from an event. +-- +-- * @{#BASE.HandleEvent}(): Subscribe to a DCS Event. +-- * @{#BASE.UnHandleEvent}(): Unsubscribe from a DCS Event. +-- +-- ### 1.3.2 Event Handling of DCS Events +-- +-- Once the class is subscribed to the event, an **Event Handling** method on the object or class needs to be written that will be called +-- when the DCS event occurs. The Event Handling method receives an @{Event#EVENTDATA} structure, which contains a lot of information +-- about the event that occurred. +-- +-- Find below an example of the prototype how to write an event handling function for two units: +-- +-- local Tank1 = UNIT:FindByName( "Tank A" ) +-- local Tank2 = UNIT:FindByName( "Tank B" ) +-- +-- -- Here we subscribe to the Dead events. So, if one of these tanks dies, the Tank1 or Tank2 objects will be notified. +-- Tank1:HandleEvent( EVENTS.Dead ) +-- Tank2:HandleEvent( EVENTS.Dead ) +-- +-- --- This function is an Event Handling function that will be called when Tank1 is Dead. +-- -- @param Wrapper.Unit#UNIT self +-- -- @param Core.Event#EVENTDATA EventData +-- function Tank1:OnEventDead( EventData ) +-- +-- self:SmokeGreen() +-- end +-- +-- --- This function is an Event Handling function that will be called when Tank2 is Dead. +-- -- @param Wrapper.Unit#UNIT self +-- -- @param Core.Event#EVENTDATA EventData +-- function Tank2:OnEventDead( EventData ) +-- +-- self:SmokeBlue() +-- end +-- +-- +-- +-- See the @{Event} module for more information about event handling. +-- +-- ## 1.4) Class identification methods +-- +-- BASE provides methods to get more information of each object: +-- +-- * @{#BASE.GetClassID}(): Gets the ID (number) of the object. Each object created is assigned a number, that is incremented by one. +-- * @{#BASE.GetClassName}(): Gets the name of the object, which is the name of the class the object was instantiated from. +-- * @{#BASE.GetClassNameAndID}(): Gets the name and ID of the object. +-- +-- ## 1.5) All objects derived from BASE can have "States" +-- +-- A mechanism is in place in MOOSE, that allows to let the objects administer **states**. +-- States are essentially properties of objects, which are identified by a **Key** and a **Value**. +-- The method @{#BASE.SetState}() can be used to set a Value with a reference Key to the object. +-- To **read or retrieve** a state Value based on a Key, use the @{#BASE.GetState} method. +-- These two methods provide a very handy way to keep state at long lasting processes. +-- Values can be stored within the objects, and later retrieved or changed when needed. +-- There is one other important thing to note, the @{#BASE.SetState}() and @{#BASE.GetState} methods +-- receive as the **first parameter the object for which the state needs to be set**. +-- Thus, if the state is to be set for the same object as the object for which the method is used, then provide the same +-- object name to the method. +-- +-- ## 1.10) BASE Inheritance (tree) support +-- +-- The following methods are available to support inheritance: +-- +-- * @{#BASE.Inherit}: Inherits from a class. +-- * @{#BASE.GetParent}: Returns the parent object from the object it is handling, or nil if there is no parent object. +-- +-- ==== +-- +-- # **API CHANGE HISTORY** +-- +-- The underlying change log documents the API changes. Please read this carefully. The following notation is used: +-- +-- * **Added** parts are expressed in bold type face. +-- * _Removed_ parts are expressed in italic type face. +-- +-- YYYY-MM-DD: CLASS:**NewFunction**( Params ) replaces CLASS:_OldFunction_( Params ) +-- YYYY-MM-DD: CLASS:**NewFunction( Params )** added +-- +-- Hereby the change log: +-- +-- === +-- +-- # **AUTHORS and CONTRIBUTIONS** +-- +-- ### Contributions: +-- +-- * None. +-- +-- ### Authors: +-- +-- * **FlightControl**: Design & Programming +-- +-- @module Base + + + +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, + _Private = {}, + Events = {}, + States = {} +} + +--- The Formation Class +-- @type FORMATION +-- @field Cone A cone formation. +FORMATION = { + Cone = "Cone" +} + + + +-- @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 + + + return self +end + +function BASE:_Destructor() + --self:E("_Destructor") + + --self:EventRemoveAll() +end + +function BASE:_SetDestructor() + + -- TODO: Okay, this is really technical... + -- When you set a proxy to a table to catch __gc, weak tables don't behave like weak... + -- Therefore, I am parking this logic until I've properly discussed all this with the community. + --[[ + local proxy = newproxy(true) + local proxyMeta = getmetatable(proxy) + + proxyMeta.__gc = function () + env.info("In __gc for " .. self:GetClassNameAndID() ) + if self._Destructor then + self:_Destructor() + end + end + + -- keep the userdata from newproxy reachable until the object + -- table is about to be garbage-collected - then the __gc hook + -- will be invoked and the destructor called + rawset( self, '__proxy', proxy ) + --]] +end + +--- 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 + + Child:_SetDestructor() + end + --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:GetParent( 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 string.format( '%s#%09d', self.ClassName, self.ClassID ) +end + +--- Get the ClassName of the class instance. +-- @param #BASE self +-- @return #string The ClassName of the class instance. +function BASE:GetClassName() + return self.ClassName +end + +--- Get the ClassID of the class instance. +-- @param #BASE self +-- @return #string The ClassID of the class instance. +function BASE:GetClassID() + return self.ClassID +end + +do -- Event Handling + + --- Returns the event dispatcher + -- @param #BASE self + -- @return Core.Event#EVENT + function BASE:EventDispatcher() + + return _EVENTDISPATCHER + end + + + --- Get the Class @{Event} processing Priority. + -- The Event processing Priority is a number from 1 to 10, + -- reflecting the order of the classes subscribed to the Event to be processed. + -- @param #BASE self + -- @return #number The @{Event} processing Priority. + function BASE:GetEventPriority() + return self._Private.EventPriority or 5 + end + + --- Set the Class @{Event} processing Priority. + -- The Event processing Priority is a number from 1 to 10, + -- reflecting the order of the classes subscribed to the Event to be processed. + -- @param #BASE self + -- @param #number EventPriority The @{Event} processing Priority. + -- @return self + function BASE:SetEventPriority( EventPriority ) + self._Private.EventPriority = EventPriority + end + + --- Remove all subscribed events + -- @param #BASE self + -- @return #BASE + function BASE:EventRemoveAll() + + self:EventDispatcher():RemoveAll( self ) + + return self + end + + --- Subscribe to a DCS Event. + -- @param #BASE self + -- @param Core.Event#EVENTS Event + -- @param #function EventFunction (optional) The function to be called when the event occurs for the unit. + -- @return #BASE + function BASE:HandleEvent( Event, EventFunction ) + + self:EventDispatcher():OnEventGeneric( EventFunction, self, Event ) + + return self + end + + --- UnSubscribe to a DCS event. + -- @param #BASE self + -- @param Core.Event#EVENTS Event + -- @return #BASE + function BASE:UnHandleEvent( Event ) + + self:EventDispatcher():Remove( self, Event ) + + return self + end + + -- Event handling function prototypes + + --- Occurs whenever any unit in a mission fires a weapon. But not any machine gun or autocannon based weapon, those are handled by EVENT.ShootingStart. + -- @function [parent=#BASE] OnEventShot + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs whenever an object is hit by a weapon. + -- initiator : The unit object the fired the weapon + -- weapon: Weapon object that hit the target + -- target: The Object that was hit. + -- @function [parent=#BASE] OnEventHit + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when an aircraft takes off from an airbase, farp, or ship. + -- initiator : The unit that tookoff + -- place: Object from where the AI took-off from. Can be an Airbase Object, FARP, or Ships + -- @function [parent=#BASE] OnEventTakeoff + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when an aircraft lands at an airbase, farp or ship + -- initiator : The unit that has landed + -- place: Object that the unit landed on. Can be an Airbase Object, FARP, or Ships + -- @function [parent=#BASE] OnEventLand + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when any aircraft crashes into the ground and is completely destroyed. + -- initiator : The unit that has crashed + -- @function [parent=#BASE] OnEventCrash + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when a pilot ejects from an aircraft + -- initiator : The unit that has ejected + -- @function [parent=#BASE] OnEventEjection + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when an aircraft connects with a tanker and begins taking on fuel. + -- initiator : The unit that is receiving fuel. + -- @function [parent=#BASE] OnEventRefueling + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when an object is completely destroyed. + -- initiator : The unit that is was destroyed. + -- @function [parent=#BASE] OnEvent + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when the pilot of an aircraft is killed. Can occur either if the player is alive and crashes or if a weapon kills the pilot without completely destroying the plane. + -- initiator : The unit that the pilot has died in. + -- @function [parent=#BASE] OnEventPilotDead + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when a ground unit captures either an airbase or a farp. + -- initiator : The unit that captured the base + -- place: The airbase that was captured, can be a FARP or Airbase. When calling place:getCoalition() the faction will already be the new owning faction. + -- @function [parent=#BASE] OnEventBaseCaptured + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when a mission starts + -- @function [parent=#BASE] OnEventMissionStart + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when a mission ends + -- @function [parent=#BASE] OnEventMissionEnd + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when an aircraft is finished taking fuel. + -- initiator : The unit that was receiving fuel. + -- @function [parent=#BASE] OnEventRefuelingStop + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when any object is spawned into the mission. + -- initiator : The unit that was spawned + -- @function [parent=#BASE] OnEventBirth + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when any system fails on a human controlled aircraft. + -- initiator : The unit that had the failure + -- @function [parent=#BASE] OnEventHumanFailure + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when any aircraft starts its engines. + -- initiator : The unit that is starting its engines. + -- @function [parent=#BASE] OnEventEngineStartup + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when any aircraft shuts down its engines. + -- initiator : The unit that is stopping its engines. + -- @function [parent=#BASE] OnEventEngineShutdown + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when any player assumes direct control of a unit. + -- initiator : The unit that is being taken control of. + -- @function [parent=#BASE] OnEventPlayerEnterUnit + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when any player relieves control of a unit to the AI. + -- initiator : The unit that the player left. + -- @function [parent=#BASE] OnEventPlayerLeaveUnit + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when any unit begins firing a weapon that has a high rate of fire. Most common with aircraft cannons (GAU-8), autocannons, and machine guns. + -- initiator : The unit that is doing the shooing. + -- target: The unit that is being targeted. + -- @function [parent=#BASE] OnEventShootingStart + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when any unit stops firing its weapon. Event will always correspond with a shooting start event. + -- initiator : The unit that was doing the shooing. + -- @function [parent=#BASE] OnEventShootingEnd + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + +end + + +--- Creation of a Birth Event. +-- @param #BASE self +-- @param Dcs.DCSTypes#Time EventTime The time stamp of the event. +-- @param Dcs.DCSWrapper.Object#Object Initiator The initiating object of the event. +-- @param #string IniUnitName The initiating unit name. +-- @param place +-- @param subplace +function BASE:CreateEventBirth( EventTime, Initiator, IniUnitName, place, subplace ) + self:F( { EventTime, Initiator, IniUnitName, place, subplace } ) + + local Event = { + id = world.event.S_EVENT_BIRTH, + time = EventTime, + initiator = Initiator, + IniUnitName = IniUnitName, + place = place, + subplace = subplace + } + + world.onEvent( Event ) +end + +--- Creation of a Crash Event. +-- @param #BASE self +-- @param Dcs.DCSTypes#Time EventTime The time stamp of the event. +-- @param Dcs.DCSWrapper.Object#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 Dcs.DCSTypes#Event structure. +--- The main event handling function... This function captures all events generated for the class. +-- @param #BASE self +-- @param Dcs.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 + +--- Set a state or property of the Object given a Key and a Value. +-- Note that if the Object is destroyed, nillified or garbage collected, then the Values and Keys will also be gone. +-- @param #BASE self +-- @param Object The object that will hold the Value set by the Key. +-- @param Key The key that is used as a reference of the value. Note that the key can be a #string, but it can also be any other type! +-- @param Value The value to is stored in the object. +-- @return The Value set. +-- @return #nil The Key was not found and thus the Value could not be retrieved. +function BASE:SetState( Object, Key, Value ) + + local ClassNameAndID = Object:GetClassNameAndID() + + self.States[ClassNameAndID] = self.States[ClassNameAndID] or {} + self.States[ClassNameAndID][Key] = Value + self:T2( { ClassNameAndID, Key, Value } ) + + return self.States[ClassNameAndID][Key] +end + + +--- Get a Value given a Key from the Object. +-- Note that if the Object is destroyed, nillified or garbage collected, then the Values and Keys will also be gone. +-- @param #BASE self +-- @param Object The object that holds the Value set by the Key. +-- @param Key The key that is used to retrieve the value. Note that the key can be a #string, but it can also be any other type! +-- @param Value The value to is stored in the Object. +-- @return The Value retrieved. +function BASE:GetState( Object, Key ) + + local ClassNameAndID = Object:GetClassNameAndID() + + if self.States[ClassNameAndID] then + local Value = self.States[ClassNameAndID][Key] or false + self:T2( { ClassNameAndID, Key, Value } ) + return Value + 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:TraceOnOff( true ) +-- +-- -- Switch the tracing Off +-- BASE:TraceOnOff( false ) +function BASE:TraceOnOff( TraceOnOff ) + _TraceOnOff = TraceOnOff +end + + +--- Enquires if tracing is on (for the class). +-- @param #BASE self +-- @return #boolean +function BASE:IsTrace() + + if debug and ( _TraceAll == true ) or ( _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName] ) then + return true + else + return false + end +end + +--- Set trace level +-- @param #BASE self +-- @param #number Level +function BASE:TraceLevel( Level ) + _TraceLevel = Level + 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 debug and ( _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 debug and _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 debug and _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 debug and _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 debug and ( _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 debug and _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 debug and _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 debug and _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 ) + + if debug then + 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 + +end + + + +--- This module contains the SCHEDULER class. +-- +-- # 1) @{Scheduler#SCHEDULER} class, extends @{Base#BASE} +-- +-- The @{Scheduler#SCHEDULER} class creates schedule. +-- +-- ## 1.1) SCHEDULER constructor +-- +-- The SCHEDULER class is quite easy to use, but note that the New constructor has variable parameters: +-- +-- * @{Scheduler#SCHEDULER.New}( nil ): Setup a new SCHEDULER object, which is persistently executed after garbage collection. +-- * @{Scheduler#SCHEDULER.New}( Object ): Setup a new SCHEDULER object, which is linked to the Object. When the Object is nillified or destroyed, the SCHEDULER object will also be destroyed and stopped after garbage collection. +-- * @{Scheduler#SCHEDULER.New}( nil, Function, FunctionArguments, Start, ... ): Setup a new persistent SCHEDULER object, and start a new schedule for the Function with the defined FunctionArguments according the Start and sequent parameters. +-- * @{Scheduler#SCHEDULER.New}( Object, Function, FunctionArguments, Start, ... ): Setup a new SCHEDULER object, linked to Object, and start a new schedule for the Function with the defined FunctionArguments according the Start and sequent parameters. +-- +-- ## 1.2) SCHEDULER timer stopping and (re-)starting. +-- +-- The SCHEDULER can be stopped and restarted with the following methods: +-- +-- * @{Scheduler#SCHEDULER.Start}(): (Re-)Start the schedules within the SCHEDULER object. If a CallID is provided to :Start(), only the schedule referenced by CallID will be (re-)started. +-- * @{Scheduler#SCHEDULER.Stop}(): Stop the schedules within the SCHEDULER object. If a CallID is provided to :Stop(), then only the schedule referenced by CallID will be stopped. +-- +-- ## 1.3) Create a new schedule +-- +-- With @{Scheduler#SCHEDULER.Schedule}() a new time event can be scheduled. This function is used by the :New() constructor when a new schedule is planned. +-- +-- === +-- +-- ### Contributions: +-- +-- * FlightControl : Concept & Testing +-- +-- ### Authors: +-- +-- * FlightControl : Design & Programming +-- +-- ### Test Missions: +-- +-- * SCH - Scheduler +-- +-- === +-- +-- @module Scheduler + + +--- The SCHEDULER class +-- @type SCHEDULER +-- @field #number ScheduleID the ID of the scheduler. +-- @extends Core.Base#BASE +SCHEDULER = { + ClassName = "SCHEDULER", + Schedules = {}, +} + +--- SCHEDULER constructor. +-- @param #SCHEDULER self +-- @param #table SchedulerObject Specified for which Moose object the timer is setup. If a value of nil is provided, a scheduler will be setup without an object reference. +-- @param #function SchedulerFunction The event function to be called when a timer event occurs. The event function needs to accept the parameters specified in SchedulerArguments. +-- @param #table SchedulerArguments Optional arguments that can be given as part of scheduler. The arguments need to be given as a table { param1, param 2, ... }. +-- @param #number Start Specifies the amount of seconds that will be waited before the scheduling is started, and the event function is called. +-- @param #number Repeat Specifies the interval in seconds when the scheduler will call the event function. +-- @param #number RandomizeFactor Specifies a randomization factor between 0 and 1 to randomize the Repeat. +-- @param #number Stop Specifies the amount of seconds when the scheduler will be stopped. +-- @return #SCHEDULER self. +-- @return #number The ScheduleID of the planned schedule. +function SCHEDULER:New( SchedulerObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop ) + local self = BASE:Inherit( self, BASE:New() ) + self:F2( { Start, Repeat, RandomizeFactor, Stop } ) + + local ScheduleID = nil + + self.MasterObject = SchedulerObject + + if SchedulerFunction then + ScheduleID = self:Schedule( SchedulerObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop ) + end + + return self, ScheduleID +end + +--function SCHEDULER:_Destructor() +-- --self:E("_Destructor") +-- +-- _SCHEDULEDISPATCHER:RemoveSchedule( self.CallID ) +--end + +--- Schedule a new time event. Note that the schedule will only take place if the scheduler is *started*. Even for a single schedule event, the scheduler needs to be started also. +-- @param #SCHEDULER self +-- @param #table SchedulerObject Specified for which Moose object the timer is setup. If a value of nil is provided, a scheduler will be setup without an object reference. +-- @param #function SchedulerFunction The event function to be called when a timer event occurs. The event function needs to accept the parameters specified in SchedulerArguments. +-- @param #table SchedulerArguments Optional arguments that can be given as part of scheduler. The arguments need to be given as a table { param1, param 2, ... }. +-- @param #number Start Specifies the amount of seconds that will be waited before the scheduling is started, and the event function is called. +-- @param #number Repeat Specifies the interval in seconds when the scheduler will call the event function. +-- @param #number RandomizeFactor Specifies a randomization factor between 0 and 1 to randomize the Repeat. +-- @param #number Stop Specifies the amount of seconds when the scheduler will be stopped. +-- @return #number The ScheduleID of the planned schedule. +function SCHEDULER:Schedule( SchedulerObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop ) + self:F2( { Start, Repeat, RandomizeFactor, Stop } ) + self:T3( { SchedulerArguments } ) + + local ObjectName = "-" + if SchedulerObject and SchedulerObject.ClassName and SchedulerObject.ClassID then + ObjectName = SchedulerObject.ClassName .. SchedulerObject.ClassID + end + self:F3( { "Schedule :", ObjectName, tostring( SchedulerObject ), Start, Repeat, RandomizeFactor, Stop } ) + self.SchedulerObject = SchedulerObject + + local ScheduleID = _SCHEDULEDISPATCHER:AddSchedule( + self, + SchedulerFunction, + SchedulerArguments, + Start, + Repeat, + RandomizeFactor, + Stop + ) + + self.Schedules[#self.Schedules+1] = ScheduleID + + return ScheduleID +end + +--- (Re-)Starts the schedules or a specific schedule if a valid ScheduleID is provided. +-- @param #SCHEDULER self +-- @param #number ScheduleID (optional) The ScheduleID of the planned (repeating) schedule. +function SCHEDULER:Start( ScheduleID ) + self:F3( { ScheduleID } ) + + _SCHEDULEDISPATCHER:Start( self, ScheduleID ) +end + +--- Stops the schedules or a specific schedule if a valid ScheduleID is provided. +-- @param #SCHEDULER self +-- @param #number ScheduleID (optional) The ScheduleID of the planned (repeating) schedule. +function SCHEDULER:Stop( ScheduleID ) + self:F3( { ScheduleID } ) + + _SCHEDULEDISPATCHER:Stop( self, ScheduleID ) +end + +--- Removes a specific schedule if a valid ScheduleID is provided. +-- @param #SCHEDULER self +-- @param #number ScheduleID (optional) The ScheduleID of the planned (repeating) schedule. +function SCHEDULER:Remove( ScheduleID ) + self:F3( { ScheduleID } ) + + _SCHEDULEDISPATCHER:Remove( self, ScheduleID ) +end + + + + + + + + + + + + + + + +--- This module defines the SCHEDULEDISPATCHER class, which is used by a central object called _SCHEDULEDISPATCHER. +-- +-- === +-- +-- Takes care of the creation and dispatching of scheduled functions for SCHEDULER objects. +-- +-- This class is tricky and needs some thorought explanation. +-- SCHEDULE classes are used to schedule functions for objects, or as persistent objects. +-- The SCHEDULEDISPATCHER class ensures that: +-- +-- - Scheduled functions are planned according the SCHEDULER object parameters. +-- - Scheduled functions are repeated when requested, according the SCHEDULER object parameters. +-- - Scheduled functions are automatically removed when the schedule is finished, according the SCHEDULER object parameters. +-- +-- The SCHEDULEDISPATCHER class will manage SCHEDULER object in memory during garbage collection: +-- - When a SCHEDULER object is not attached to another object (that is, it's first :Schedule() parameter is nil), then the SCHEDULER +-- object is _persistent_ within memory. +-- - When a SCHEDULER object *is* attached to another object, then the SCHEDULER object is _not persistent_ within memory after a garbage collection! +-- The none persistency of SCHEDULERS attached to objects is required to allow SCHEDULER objects to be garbage collectged, when the parent object is also desroyed or nillified and garbage collected. +-- Even when there are pending timer scheduled functions to be executed for the SCHEDULER object, +-- these will not be executed anymore when the SCHEDULER object has been destroyed. +-- +-- The SCHEDULEDISPATCHER allows multiple scheduled functions to be planned and executed for one SCHEDULER object. +-- The SCHEDULER object therefore keeps a table of "CallID's", which are returned after each planning of a new scheduled function by the SCHEDULEDISPATCHER. +-- The SCHEDULER object plans new scheduled functions through the @{Scheduler#SCHEDULER.Schedule}() method. +-- The Schedule() method returns the CallID that is the reference ID for each planned schedule. +-- +-- === +-- +-- === +-- +-- ### Contributions: - +-- ### Authors: FlightControl : Design & Programming +-- +-- @module ScheduleDispatcher + +--- The SCHEDULEDISPATCHER structure +-- @type SCHEDULEDISPATCHER +SCHEDULEDISPATCHER = { + ClassName = "SCHEDULEDISPATCHER", + CallID = 0, +} + +function SCHEDULEDISPATCHER:New() + local self = BASE:Inherit( self, BASE:New() ) + self:F3() + return self +end + +--- Add a Schedule to the ScheduleDispatcher. +-- The development of this method was really tidy. +-- It is constructed as such that a garbage collection is executed on the weak tables, when the Scheduler is nillified. +-- Nothing of this code should be modified without testing it thoroughly. +-- @param #SCHEDULEDISPATCHER self +-- @param Core.Scheduler#SCHEDULER Scheduler +function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleArguments, Start, Repeat, Randomize, Stop ) + self:F2( { Scheduler, ScheduleFunction, ScheduleArguments, Start, Repeat, Randomize, Stop } ) + + self.CallID = self.CallID + 1 + + -- Initialize the ObjectSchedulers array, which is a weakly coupled table. + -- If the object used as the key is nil, then the garbage collector will remove the item from the Functions array. + self.PersistentSchedulers = self.PersistentSchedulers or {} + + -- Initialize the ObjectSchedulers array, which is a weakly coupled table. + -- If the object used as the key is nil, then the garbage collector will remove the item from the Functions array. + self.ObjectSchedulers = self.ObjectSchedulers or {} -- setmetatable( {}, { __mode = "v" } ) + + if Scheduler.MasterObject then + self.ObjectSchedulers[self.CallID] = Scheduler + self:F3( { CallID = self.CallID, ObjectScheduler = tostring(self.ObjectSchedulers[self.CallID]), MasterObject = tostring(Scheduler.MasterObject) } ) + else + self.PersistentSchedulers[self.CallID] = Scheduler + self:F3( { CallID = self.CallID, PersistentScheduler = self.PersistentSchedulers[self.CallID] } ) + end + + self.Schedule = self.Schedule or setmetatable( {}, { __mode = "k" } ) + self.Schedule[Scheduler] = self.Schedule[Scheduler] or {} + self.Schedule[Scheduler][self.CallID] = {} + self.Schedule[Scheduler][self.CallID].Function = ScheduleFunction + self.Schedule[Scheduler][self.CallID].Arguments = ScheduleArguments + self.Schedule[Scheduler][self.CallID].StartTime = timer.getTime() + ( Start or 0 ) + self.Schedule[Scheduler][self.CallID].Start = Start + .1 + self.Schedule[Scheduler][self.CallID].Repeat = Repeat + self.Schedule[Scheduler][self.CallID].Randomize = Randomize + self.Schedule[Scheduler][self.CallID].Stop = Stop + + self:T3( self.Schedule[Scheduler][self.CallID] ) + + self.Schedule[Scheduler][self.CallID].CallHandler = function( CallID ) + self:F2( CallID ) + + local ErrorHandler = function( errmsg ) + env.info( "Error in timer function: " .. errmsg ) + if debug ~= nil then + env.info( debug.traceback() ) + end + return errmsg + end + + local Scheduler = self.ObjectSchedulers[CallID] + if not Scheduler then + Scheduler = self.PersistentSchedulers[CallID] + end + + self:T3( { Scheduler = Scheduler } ) + + if Scheduler then + + local Schedule = self.Schedule[Scheduler][CallID] + + self:T3( { Schedule = Schedule } ) + + local ScheduleObject = Scheduler.SchedulerObject + --local ScheduleObjectName = Scheduler.SchedulerObject:GetNameAndClassID() + local ScheduleFunction = Schedule.Function + local ScheduleArguments = Schedule.Arguments + local Start = Schedule.Start + local Repeat = Schedule.Repeat or 0 + local Randomize = Schedule.Randomize or 0 + local Stop = Schedule.Stop or 0 + local ScheduleID = Schedule.ScheduleID + + local Status, Result + if ScheduleObject then + local function Timer() + return ScheduleFunction( ScheduleObject, unpack( ScheduleArguments ) ) + end + Status, Result = xpcall( Timer, ErrorHandler ) + else + local function Timer() + return ScheduleFunction( unpack( ScheduleArguments ) ) + end + Status, Result = xpcall( Timer, ErrorHandler ) + end + + local CurrentTime = timer.getTime() + local StartTime = CurrentTime + Start + + if Status and (( Result == nil ) or ( Result and Result ~= false ) ) then + if Repeat ~= 0 and ( Stop == 0 ) or ( Stop ~= 0 and CurrentTime <= StartTime + Stop ) then + local ScheduleTime = + CurrentTime + + Repeat + + math.random( + - ( Randomize * Repeat / 2 ), + ( Randomize * Repeat / 2 ) + ) + + 0.01 + self:T3( { Repeat = CallID, CurrentTime, ScheduleTime, ScheduleArguments } ) + return ScheduleTime -- returns the next time the function needs to be called. + else + self:Stop( Scheduler, CallID ) + end + else + self:Stop( Scheduler, CallID ) + end + else + self:E( "Scheduled obscolete call for CallID: " .. CallID ) + end + + return nil + end + + self:Start( Scheduler, self.CallID ) + + return self.CallID +end + +function SCHEDULEDISPATCHER:RemoveSchedule( Scheduler, CallID ) + self:F2( { Remove = CallID, Scheduler = Scheduler } ) + + if CallID then + self:Stop( Scheduler, CallID ) + self.Schedule[Scheduler][CallID] = nil + end +end + +function SCHEDULEDISPATCHER:Start( Scheduler, CallID ) + self:F2( { Start = CallID, Scheduler = Scheduler } ) + + if CallID then + local Schedule = self.Schedule[Scheduler] + Schedule[CallID].ScheduleID = timer.scheduleFunction( + Schedule[CallID].CallHandler, + CallID, + timer.getTime() + Schedule[CallID].Start + ) + else + for CallID, Schedule in pairs( self.Schedule[Scheduler] ) do + self:Start( Scheduler, CallID ) -- Recursive + end + end +end + +function SCHEDULEDISPATCHER:Stop( Scheduler, CallID ) + self:F2( { Stop = CallID, Scheduler = Scheduler } ) + + if CallID then + local Schedule = self.Schedule[Scheduler] + timer.removeFunction( Schedule[CallID].ScheduleID ) + else + for CallID, Schedule in pairs( self.Schedule[Scheduler] ) do + self:Stop( Scheduler, CallID ) -- Recursive + end + end +end + + + +--- This core module models the dispatching of DCS Events to subscribed MOOSE classes, +-- following a given priority. +-- +-- ![Banner Image](..\Presentations\EVENT\Dia1.JPG) +-- +-- === +-- +-- # 1) Event Handling Overview +-- +-- ![Objects](..\Presentations\EVENT\Dia2.JPG) +-- +-- Within a running mission, various DCS events occur. Units are dynamically created, crash, die, shoot stuff, get hit etc. +-- This module provides a mechanism to dispatch those events occuring within your running mission, to the different objects orchestrating your mission. +-- +-- ![Objects](..\Presentations\EVENT\Dia3.JPG) +-- +-- Objects can subscribe to different events. The Event dispatcher will publish the received DCS events to the subscribed MOOSE objects, in a specified order. +-- In this way, the subscribed MOOSE objects are kept in sync with your evolving running mission. +-- +-- ## 1.1) Event Dispatching +-- +-- ![Objects](..\Presentations\EVENT\Dia4.JPG) +-- +-- The _EVENTDISPATCHER object is automatically created within MOOSE, +-- and handles the dispatching of DCS Events occurring +-- in the simulator to the subscribed objects +-- in the correct processing order. +-- +-- ![Objects](..\Presentations\EVENT\Dia5.JPG) +-- +-- There are 5 levels of kind of objects that the _EVENTDISPATCHER services: +-- +-- * _DATABASE object: The core of the MOOSE objects. Any object that is created, deleted or updated, is done in this database. +-- * SET_ derived classes: Subsets of the _DATABASE object. These subsets are updated by the _EVENTDISPATCHER as the second priority. +-- * UNIT objects: UNIT objects can subscribe to DCS events. Each DCS event will be directly published to teh subscribed UNIT object. +-- * GROUP objects: GROUP objects can subscribe to DCS events. Each DCS event will be directly published to the subscribed GROUP object. +-- * Any other object: Various other objects can subscribe to DCS events. Each DCS event triggered will be published to each subscribed object. +-- +-- ![Objects](..\Presentations\EVENT\Dia6.JPG) +-- +-- For most DCS events, the above order of updating will be followed. +-- +-- ![Objects](..\Presentations\EVENT\Dia7.JPG) +-- +-- But for some DCS events, the publishing order is reversed. This is due to the fact that objects need to be **erased** instead of added. +-- +-- ## 1.2) Event Handling +-- +-- ![Objects](..\Presentations\EVENT\Dia8.JPG) +-- +-- The actual event subscribing and handling is not facilitated through the _EVENTDISPATCHER, but it is done through the @{BASE} class, @{UNIT} class and @{GROUP} class. +-- The _EVENTDISPATCHER is a component that is quietly working in the background of MOOSE. +-- +-- ![Objects](..\Presentations\EVENT\Dia9.JPG) +-- +-- The BASE class provides methods to catch DCS Events. These are events that are triggered from within the DCS simulator, +-- and handled through lua scripting. MOOSE provides an encapsulation to handle these events more efficiently. +-- +-- ### 1.2.1 Subscribe / Unsubscribe to DCS Events +-- +-- At first, the mission designer will need to **Subscribe** to a specific DCS event for the class. +-- So, when the DCS event occurs, the class will be notified of that event. +-- There are two functions which you use to subscribe to or unsubscribe from an event. +-- +-- * @{Base#BASE.HandleEvent}(): Subscribe to a DCS Event. +-- * @{Base#BASE.UnHandleEvent}(): Unsubscribe from a DCS Event. +-- +-- Note that for a UNIT, the event will be handled **for that UNIT only**! +-- Note that for a GROUP, the event will be handled **for all the UNITs in that GROUP only**! +-- +-- For all objects of other classes, the subscribed events will be handled for **all UNITs within the Mission**! +-- So if a UNIT within the mission has the subscribed event for that object, +-- then the object event handler will receive the event for that UNIT! +-- +-- ### 1.3.2 Event Handling of DCS Events +-- +-- Once the class is subscribed to the event, an **Event Handling** method on the object or class needs to be written that will be called +-- when the DCS event occurs. The Event Handling method receives an @{Event#EVENTDATA} structure, which contains a lot of information +-- about the event that occurred. +-- +-- Find below an example of the prototype how to write an event handling function for two units: +-- +-- local Tank1 = UNIT:FindByName( "Tank A" ) +-- local Tank2 = UNIT:FindByName( "Tank B" ) +-- +-- -- Here we subscribe to the Dead events. So, if one of these tanks dies, the Tank1 or Tank2 objects will be notified. +-- Tank1:HandleEvent( EVENTS.Dead ) +-- Tank2:HandleEvent( EVENTS.Dead ) +-- +-- --- This function is an Event Handling function that will be called when Tank1 is Dead. +-- -- @param Wrapper.Unit#UNIT self +-- -- @param Core.Event#EVENTDATA EventData +-- function Tank1:OnEventDead( EventData ) +-- +-- self:SmokeGreen() +-- end +-- +-- --- This function is an Event Handling function that will be called when Tank2 is Dead. +-- -- @param Wrapper.Unit#UNIT self +-- -- @param Core.Event#EVENTDATA EventData +-- function Tank2:OnEventDead( EventData ) +-- +-- self:SmokeBlue() +-- end +-- +-- ### 1.3.3 Event Handling methods that are automatically called upon subscribed DCS events +-- +-- ![Objects](..\Presentations\EVENT\Dia10.JPG) +-- +-- The following list outlines which EVENTS item in the structure corresponds to which Event Handling method. +-- Always ensure that your event handling methods align with the events being subscribed to, or nothing will be executed. +-- +-- # 2) EVENTS type +-- +-- The EVENTS structure contains names for all the different DCS events that objects can subscribe to using the +-- @{Base#BASE.HandleEvent}() method. +-- +-- # 3) EVENTDATA type +-- +-- The @{Event#EVENTDATA} structure contains all the fields that are populated with event information before +-- an Event Handler method is being called by the event dispatcher. +-- The Event Handler received the EVENTDATA object as a parameter, and can be used to investigate further the different events. +-- There are basically 4 main categories of information stored in the EVENTDATA structure: +-- +-- * Initiator Unit data: Several fields documenting the initiator unit related to the event. +-- * Target Unit data: Several fields documenting the target unit related to the event. +-- * Weapon data: Certain events populate weapon information. +-- * Place data: Certain events populate place information. +-- +-- --- This function is an Event Handling function that will be called when Tank1 is Dead. +-- -- EventData is an EVENTDATA structure. +-- -- We use the EventData.IniUnit to smoke the tank Green. +-- -- @param Wrapper.Unit#UNIT self +-- -- @param Core.Event#EVENTDATA EventData +-- function Tank1:OnEventDead( EventData ) +-- +-- EventData.IniUnit:SmokeGreen() +-- end +-- +-- +-- Find below an overview which events populate which information categories: +-- +-- ![Objects](..\Presentations\EVENT\Dia14.JPG) +-- +-- **IMPORTANT NOTE:** Some events can involve not just UNIT objects, but also STATIC objects!!! +-- In that case the initiator or target unit fields will refer to a STATIC object! +-- In case a STATIC object is involved, the documentation indicates which fields will and won't not be populated. +-- The fields **IniObjectCategory** and **TgtObjectCategory** contain the indicator which **kind of object is involved** in the event. +-- You can use the enumerator **Object.Category.UNIT** and **Object.Category.STATIC** to check on IniObjectCategory and TgtObjectCategory. +-- Example code snippet: +-- +-- if Event.IniObjectCategory == Object.Category.UNIT then +-- ... +-- end +-- if Event.IniObjectCategory == Object.Category.STATIC then +-- ... +-- end +-- +-- When a static object is involved in the event, the Group and Player fields won't be populated. +-- +-- ==== +-- +-- # **API CHANGE HISTORY** +-- +-- The underlying change log documents the API changes. Please read this carefully. The following notation is used: +-- +-- * **Added** parts are expressed in bold type face. +-- * _Removed_ parts are expressed in italic type face. +-- +-- YYYY-MM-DD: CLASS:**NewFunction**( Params ) replaces CLASS:_OldFunction_( Params ) +-- YYYY-MM-DD: CLASS:**NewFunction( Params )** added +-- +-- Hereby the change log: +-- +-- * 2017-03-07: Added the correct event dispatching in case the event is subscribed by a GROUP. +-- +-- * 2017-02-07: Did a complete revision of the Event Handing API and underlying mechanisms. +-- +-- === +-- +-- # **AUTHORS and CONTRIBUTIONS** +-- +-- ### Contributions: +-- +-- ### Authors: +-- +-- * [**FlightControl**](https://forums.eagle.ru/member.php?u=89536): Design & Programming & documentation. +-- +-- @module Event + + +--- The EVENT structure +-- @type EVENT +-- @field #EVENT.Events Events +-- @extends Core.Base#BASE +EVENT = { + ClassName = "EVENT", + ClassID = 0, +} + +--- The different types of events supported by MOOSE. +-- Use this structure to subscribe to events using the @{Base#BASE.HandleEvent}() method. +-- @type EVENTS +EVENTS = { + Shot = world.event.S_EVENT_SHOT, + Hit = world.event.S_EVENT_HIT, + Takeoff = world.event.S_EVENT_TAKEOFF, + Land = world.event.S_EVENT_LAND, + Crash = world.event.S_EVENT_CRASH, + Ejection = world.event.S_EVENT_EJECTION, + Refueling = world.event.S_EVENT_REFUELING, + Dead = world.event.S_EVENT_DEAD, + PilotDead = world.event.S_EVENT_PILOT_DEAD, + BaseCaptured = world.event.S_EVENT_BASE_CAPTURED, + MissionStart = world.event.S_EVENT_MISSION_START, + MissionEnd = world.event.S_EVENT_MISSION_END, + TookControl = world.event.S_EVENT_TOOK_CONTROL, + RefuelingStop = world.event.S_EVENT_REFUELING_STOP, + Birth = world.event.S_EVENT_BIRTH, + HumanFailure = world.event.S_EVENT_HUMAN_FAILURE, + EngineStartup = world.event.S_EVENT_ENGINE_STARTUP, + EngineShutdown = world.event.S_EVENT_ENGINE_SHUTDOWN, + PlayerEnterUnit = world.event.S_EVENT_PLAYER_ENTER_UNIT, + PlayerLeaveUnit = world.event.S_EVENT_PLAYER_LEAVE_UNIT, + PlayerComment = world.event.S_EVENT_PLAYER_COMMENT, + ShootingStart = world.event.S_EVENT_SHOOTING_START, + ShootingEnd = world.event.S_EVENT_SHOOTING_END, +} + +--- The Event structure +-- Note that at the beginning of each field description, there is an indication which field will be populated depending on the object type involved in the Event: +-- +-- * A (Object.Category.)UNIT : A UNIT object type is involved in the Event. +-- * A (Object.Category.)STATIC : A STATIC object type is involved in the Event.µ +-- +-- @type EVENTDATA +-- @field #number id The identifier of the event. +-- +-- @field Dcs.DCSUnit#Unit initiator (UNIT/STATIC/SCENERY) The initiating @{Dcs.DCSUnit#Unit} or @{Dcs.DCSStaticObject#StaticObject}. +-- @field Dcs.DCSObject#Object.Category IniObjectCategory (UNIT/STATIC/SCENERY) The initiator object category ( Object.Category.UNIT or Object.Category.STATIC ). +-- @field Dcs.DCSUnit#Unit IniDCSUnit (UNIT/STATIC) The initiating @{DCSUnit#Unit} or @{DCSStaticObject#StaticObject}. +-- @field #string IniDCSUnitName (UNIT/STATIC) The initiating Unit name. +-- @field Wrapper.Unit#UNIT IniUnit (UNIT/STATIC) The initiating MOOSE wrapper @{Unit#UNIT} of the initiator Unit object. +-- @field #string IniUnitName (UNIT/STATIC) The initiating UNIT name (same as IniDCSUnitName). +-- @field Dcs.DCSGroup#Group IniDCSGroup (UNIT) The initiating {DCSGroup#Group}. +-- @field #string IniDCSGroupName (UNIT) The initiating Group name. +-- @field Wrapper.Group#GROUP IniGroup (UNIT) The initiating MOOSE wrapper @{Group#GROUP} of the initiator Group object. +-- @field #string IniGroupName UNIT) The initiating GROUP name (same as IniDCSGroupName). +-- @field #string IniPlayerName (UNIT) The name of the initiating player in case the Unit is a client or player slot. +-- @field Dcs.DCScoalition#coalition.side IniCoalition (UNIT) The coalition of the initiator. +-- @field Dcs.DCSUnit#Unit.Category IniCategory (UNIT) The category of the initiator. +-- @field #string IniTypeName (UNIT) The type name of the initiator. +-- +-- @field Dcs.DCSUnit#Unit target (UNIT/STATIC) The target @{Dcs.DCSUnit#Unit} or @{DCSStaticObject#StaticObject}. +-- @field Dcs.DCSObject#Object.Category TgtObjectCategory (UNIT/STATIC) The target object category ( Object.Category.UNIT or Object.Category.STATIC ). +-- @field Dcs.DCSUnit#Unit TgtDCSUnit (UNIT/STATIC) The target @{DCSUnit#Unit} or @{DCSStaticObject#StaticObject}. +-- @field #string TgtDCSUnitName (UNIT/STATIC) The target Unit name. +-- @field Wrapper.Unit#UNIT TgtUnit (UNIT/STATIC) The target MOOSE wrapper @{Unit#UNIT} of the target Unit object. +-- @field #string TgtUnitName (UNIT/STATIC) The target UNIT name (same as TgtDCSUnitName). +-- @field Dcs.DCSGroup#Group TgtDCSGroup (UNIT) The target {DCSGroup#Group}. +-- @field #string TgtDCSGroupName (UNIT) The target Group name. +-- @field Wrapper.Group#GROUP TgtGroup (UNIT) The target MOOSE wrapper @{Group#GROUP} of the target Group object. +-- @field #string TgtGroupName (UNIT) The target GROUP name (same as TgtDCSGroupName). +-- @field #string TgtPlayerName (UNIT) The name of the target player in case the Unit is a client or player slot. +-- @field Dcs.DCScoalition#coalition.side TgtCoalition (UNIT) The coalition of the target. +-- @field Dcs.DCSUnit#Unit.Category TgtCategory (UNIT) The category of the target. +-- @field #string TgtTypeName (UNIT) The type name of the target. +-- +-- @field weapon The weapon used during the event. +-- @field Weapon +-- @field WeaponName +-- @field WeaponTgtDCSUnit + + +local _EVENTMETA = { + [world.event.S_EVENT_SHOT] = { + Order = 1, + Event = "OnEventShot", + Text = "S_EVENT_SHOT" + }, + [world.event.S_EVENT_HIT] = { + Order = 1, + Event = "OnEventHit", + Text = "S_EVENT_HIT" + }, + [world.event.S_EVENT_TAKEOFF] = { + Order = 1, + Event = "OnEventTakeOff", + Text = "S_EVENT_TAKEOFF" + }, + [world.event.S_EVENT_LAND] = { + Order = 1, + Event = "OnEventLand", + Text = "S_EVENT_LAND" + }, + [world.event.S_EVENT_CRASH] = { + Order = -1, + Event = "OnEventCrash", + Text = "S_EVENT_CRASH" + }, + [world.event.S_EVENT_EJECTION] = { + Order = 1, + Event = "OnEventEjection", + Text = "S_EVENT_EJECTION" + }, + [world.event.S_EVENT_REFUELING] = { + Order = 1, + Event = "OnEventRefueling", + Text = "S_EVENT_REFUELING" + }, + [world.event.S_EVENT_DEAD] = { + Order = -1, + Event = "OnEventDead", + Text = "S_EVENT_DEAD" + }, + [world.event.S_EVENT_PILOT_DEAD] = { + Order = 1, + Event = "OnEventPilotDead", + Text = "S_EVENT_PILOT_DEAD" + }, + [world.event.S_EVENT_BASE_CAPTURED] = { + Order = 1, + Event = "OnEventBaseCaptured", + Text = "S_EVENT_BASE_CAPTURED" + }, + [world.event.S_EVENT_MISSION_START] = { + Order = 1, + Event = "OnEventMissionStart", + Text = "S_EVENT_MISSION_START" + }, + [world.event.S_EVENT_MISSION_END] = { + Order = 1, + Event = "OnEventMissionEnd", + Text = "S_EVENT_MISSION_END" + }, + [world.event.S_EVENT_TOOK_CONTROL] = { + Order = 1, + Event = "OnEventTookControl", + Text = "S_EVENT_TOOK_CONTROL" + }, + [world.event.S_EVENT_REFUELING_STOP] = { + Order = 1, + Event = "OnEventRefuelingStop", + Text = "S_EVENT_REFUELING_STOP" + }, + [world.event.S_EVENT_BIRTH] = { + Order = 1, + Event = "OnEventBirth", + Text = "S_EVENT_BIRTH" + }, + [world.event.S_EVENT_HUMAN_FAILURE] = { + Order = 1, + Event = "OnEventHumanFailure", + Text = "S_EVENT_HUMAN_FAILURE" + }, + [world.event.S_EVENT_ENGINE_STARTUP] = { + Order = 1, + Event = "OnEventEngineStartup", + Text = "S_EVENT_ENGINE_STARTUP" + }, + [world.event.S_EVENT_ENGINE_SHUTDOWN] = { + Order = 1, + Event = "OnEventEngineShutdown", + Text = "S_EVENT_ENGINE_SHUTDOWN" + }, + [world.event.S_EVENT_PLAYER_ENTER_UNIT] = { + Order = 1, + Event = "OnEventPlayerEnterUnit", + Text = "S_EVENT_PLAYER_ENTER_UNIT" + }, + [world.event.S_EVENT_PLAYER_LEAVE_UNIT] = { + Order = -1, + Event = "OnEventPlayerLeaveUnit", + Text = "S_EVENT_PLAYER_LEAVE_UNIT" + }, + [world.event.S_EVENT_PLAYER_COMMENT] = { + Order = 1, + Event = "OnEventPlayerComment", + Text = "S_EVENT_PLAYER_COMMENT" + }, + [world.event.S_EVENT_SHOOTING_START] = { + Order = 1, + Event = "OnEventShootingStart", + Text = "S_EVENT_SHOOTING_START" + }, + [world.event.S_EVENT_SHOOTING_END] = { + Order = 1, + Event = "OnEventShootingEnd", + Text = "S_EVENT_SHOOTING_END" + }, +} + + +--- 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 = _EVENTMETA[EventID].Text + + return EventText +end + + +--- Initializes the Events structure for the event +-- @param #EVENT self +-- @param Dcs.DCSWorld#world.event EventID +-- @param Core.Base#BASE EventClass +-- @return #EVENT.Events +function EVENT:Init( EventID, EventClass ) + self:F3( { _EVENTMETA[EventID].Text, EventClass } ) + + if not self.Events[EventID] then + -- Create a WEAK table to ensure that the garbage collector is cleaning the event links when the object usage is cleaned. + self.Events[EventID] = setmetatable( {}, { __mode = "k" } ) + end + + -- Each event has a subtable of EventClasses, ordered by EventPriority. + local EventPriority = EventClass:GetEventPriority() + if not self.Events[EventID][EventPriority] then + self.Events[EventID][EventPriority] = {} + end + + if not self.Events[EventID][EventPriority][EventClass] then + self.Events[EventID][EventPriority][EventClass] = setmetatable( {}, { __mode = "k" } ) + end + return self.Events[EventID][EventPriority][EventClass] +end + +--- Removes an Events entry +-- @param #EVENT self +-- @param Core.Base#BASE EventClass The self instance of the class for which the event is. +-- @param Dcs.DCSWorld#world.event EventID +-- @return #EVENT.Events +function EVENT:Remove( EventClass, EventID ) + self:F3( { EventClass, _EVENTMETA[EventID].Text } ) + + local EventClass = EventClass + local EventPriority = EventClass:GetEventPriority() + self.Events[EventID][EventPriority][EventClass] = nil +end + +--- Removes an Events entry for a UNIT. +-- @param #EVENT self +-- @param #string UnitName The name of the UNIT. +-- @param Core.Base#BASE EventClass The self instance of the class for which the event is. +-- @param Dcs.DCSWorld#world.event EventID +-- @return #EVENT.Events +function EVENT:RemoveForUnit( UnitName, EventClass, EventID ) + self:F3( { EventClass, _EVENTMETA[EventID].Text } ) + + local EventClass = EventClass + local EventPriority = EventClass:GetEventPriority() + local Event = self.Events[EventID][EventPriority][EventClass] + Event.IniUnit[UnitName] = nil +end + +--- Removes an Events entry for a GROUP. +-- @param #EVENT self +-- @param #string GroupName The name of the GROUP. +-- @param Core.Base#BASE EventClass The self instance of the class for which the event is. +-- @param Dcs.DCSWorld#world.event EventID +-- @return #EVENT.Events +function EVENT:RemoveForGroup( GroupName, EventClass, EventID ) + self:F3( { EventClass, _EVENTMETA[EventID].Text } ) + + local EventClass = EventClass + local EventPriority = EventClass:GetEventPriority() + local Event = self.Events[EventID][EventPriority][EventClass] + Event.IniGroup[GroupName] = nil +end + +--- Clears all event subscriptions for a @{Base#BASE} derived object. +-- @param #EVENT self +-- @param Core.Base#BASE EventObject +function EVENT:RemoveAll( EventObject ) + self:F3( { EventObject:GetClassNameAndID() } ) + + local EventClass = EventObject:GetClassNameAndID() + local EventPriority = EventClass:GetEventPriority() + for EventID, EventData in pairs( self.Events ) do + self.Events[EventID][EventPriority][EventClass] = nil + end +end + + + +--- Create an OnDead event handler for a group +-- @param #EVENT self +-- @param #table EventTemplate +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param EventClass The instance of the class for which the event is. +-- @param #function OnEventFunction +-- @return #EVENT +function EVENT:OnEventForTemplate( EventTemplate, EventFunction, EventClass, OnEventFunction ) + self:F2( EventTemplate.name ) + + for EventUnitID, EventUnit in pairs( EventTemplate.units ) do + OnEventFunction( self, EventUnit.name, EventFunction, EventClass ) + end + return self +end + +--- Set a new listener for an S_EVENT_X event independent from a unit or a weapon. +-- @param #EVENT self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Core.Base#BASE EventClass The self instance of the class for which the event is captured. When the event happens, the event process will be called in this class provided. +-- @param EventID +-- @return #EVENT +function EVENT:OnEventGeneric( EventFunction, EventClass, EventID ) + self:F2( { EventID } ) + + local Event = self:Init( EventID, EventClass ) + Event.EventFunction = EventFunction + Event.EventClass = EventClass + + return self +end + + +--- Set a new listener for an S_EVENT_X event for a UNIT. +-- @param #EVENT self +-- @param #string UnitName The name of the UNIT. +-- @param #function EventFunction The function to be called when the event occurs for the GROUP. +-- @param Core.Base#BASE EventClass The self instance of the class for which the event is. +-- @param EventID +-- @return #EVENT +function EVENT:OnEventForUnit( UnitName, EventFunction, EventClass, EventID ) + self:F2( UnitName ) + + local Event = self:Init( EventID, EventClass ) + if not Event.IniUnit then + Event.IniUnit = {} + end + Event.IniUnit[UnitName] = {} + Event.IniUnit[UnitName].EventFunction = EventFunction + Event.IniUnit[UnitName].EventClass = EventClass + return self +end + +--- Set a new listener for an S_EVENT_X event for a GROUP. +-- @param #EVENT self +-- @param #string GroupName The name of the GROUP. +-- @param #function EventFunction The function to be called when the event occurs for the GROUP. +-- @param Core.Base#BASE EventClass The self instance of the class for which the event is. +-- @param EventID +-- @return #EVENT +function EVENT:OnEventForGroup( GroupName, EventFunction, EventClass, EventID ) + self:F2( GroupName ) + + local Event = self:Init( EventID, EventClass ) + if not Event.IniGroup then + Event.IniGroup = {} + end + Event.IniGroup[GroupName] = {} + Event.IniGroup[GroupName].EventFunction = EventFunction + Event.IniGroup[GroupName].EventClass = EventClass + return self +end + +do -- OnBirth + + --- Create an OnBirth event handler for a group + -- @param #EVENT self + -- @param Wrapper.Group#GROUP EventGroup + -- @param #function EventFunction The function to be called when the event occurs for the unit. + -- @param EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnBirthForTemplate( EventTemplate, EventFunction, EventClass ) + self:F2( EventTemplate.name ) + + self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, 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 EventClass + -- @return #EVENT + function EVENT:OnBirth( EventFunction, EventClass ) + self:F2() + + self:OnEventGeneric( EventFunction, EventClass, 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 EventClass + -- @return #EVENT + function EVENT:OnBirthForUnit( EventDCSUnitName, EventFunction, EventClass ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_BIRTH ) + + return self + end + + --- Stop listening to S_EVENT_BIRTH event. + -- @param #EVENT self + -- @param Base#BASE EventClass + -- @return #EVENT + function EVENT:OnBirthRemove( EventClass ) + self:F2() + + self:Remove( EventClass, world.event.S_EVENT_BIRTH ) + + return self + end + + +end + +do -- OnCrash + + --- Create an OnCrash event handler for a group + -- @param #EVENT self + -- @param Wrapper.Group#GROUP EventGroup + -- @param #function EventFunction The function to be called when the event occurs for the unit. + -- @param EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnCrashForTemplate( EventTemplate, EventFunction, EventClass ) + self:F2( EventTemplate.name ) + + self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, 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 EventClass + -- @return #EVENT + function EVENT:OnCrash( EventFunction, EventClass ) + self:F2() + + self:OnEventGeneric( EventFunction, EventClass, 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 EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnCrashForUnit( EventDCSUnitName, EventFunction, EventClass ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_CRASH ) + + return self + end + + --- Stop listening to S_EVENT_CRASH event. + -- @param #EVENT self + -- @param Base#BASE EventClass + -- @return #EVENT + function EVENT:OnCrashRemove( EventClass ) + self:F2() + + self:Remove( EventClass, world.event.S_EVENT_CRASH ) + + return self + end + +end + +do -- OnDead + + --- Create an OnDead event handler for a group + -- @param #EVENT self + -- @param Wrapper.Group#GROUP EventGroup + -- @param #function EventFunction The function to be called when the event occurs for the unit. + -- @param EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnDeadForTemplate( EventTemplate, EventFunction, EventClass ) + self:F2( EventTemplate.name ) + + self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, 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 EventClass + -- @return #EVENT + function EVENT:OnDead( EventFunction, EventClass ) + self:F2() + + self:OnEventGeneric( EventFunction, EventClass, 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 EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnDeadForUnit( EventDCSUnitName, EventFunction, EventClass ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_DEAD ) + + return self + end + + --- Stop listening to S_EVENT_DEAD event. + -- @param #EVENT self + -- @param Base#BASE EventClass + -- @return #EVENT + function EVENT:OnDeadRemove( EventClass ) + self:F2() + + self:Remove( EventClass, world.event.S_EVENT_DEAD ) + + return self + end + + +end + +do -- OnPilotDead + + --- Set a new listener for an S_EVENT_PILOT_DEAD event. + -- @param #EVENT self + -- @param #function EventFunction The function to be called when the event occurs for the unit. + -- @param Base#BASE EventClass + -- @return #EVENT + function EVENT:OnPilotDead( EventFunction, EventClass ) + self:F2() + + self:OnEventGeneric( EventFunction, EventClass, world.event.S_EVENT_PILOT_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 EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnPilotDeadForUnit( EventDCSUnitName, EventFunction, EventClass ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_PILOT_DEAD ) + + return self + end + + --- Stop listening to S_EVENT_PILOT_DEAD event. + -- @param #EVENT self + -- @param Base#BASE EventClass + -- @return #EVENT + function EVENT:OnPilotDeadRemove( EventClass ) + self:F2() + + self:Remove( EventClass, world.event.S_EVENT_PILOT_DEAD ) + + return self + end + +end + +do -- OnLand + --- Create an OnLand event handler for a group + -- @param #EVENT self + -- @param #table EventTemplate + -- @param #function EventFunction The function to be called when the event occurs for the unit. + -- @param EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnLandForTemplate( EventTemplate, EventFunction, EventClass ) + self:F2( EventTemplate.name ) + + self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, 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 EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnLandForUnit( EventDCSUnitName, EventFunction, EventClass ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_LAND ) + + return self + end + + --- Stop listening to S_EVENT_LAND event. + -- @param #EVENT self + -- @param Base#BASE EventClass + -- @return #EVENT + function EVENT:OnLandRemove( EventClass ) + self:F2() + + self:Remove( EventClass, world.event.S_EVENT_LAND ) + + return self + end + + +end + +do -- OnTakeOff + --- Create an OnTakeOff event handler for a group + -- @param #EVENT self + -- @param #table EventTemplate + -- @param #function EventFunction The function to be called when the event occurs for the unit. + -- @param EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnTakeOffForTemplate( EventTemplate, EventFunction, EventClass ) + self:F2( EventTemplate.name ) + + self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, 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 EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnTakeOffForUnit( EventDCSUnitName, EventFunction, EventClass ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_TAKEOFF ) + + return self + end + + --- Stop listening to S_EVENT_TAKEOFF event. + -- @param #EVENT self + -- @param Base#BASE EventClass + -- @return #EVENT + function EVENT:OnTakeOffRemove( EventClass ) + self:F2() + + self:Remove( EventClass, world.event.S_EVENT_TAKEOFF ) + + return self + end + + +end + +do -- OnEngineShutDown + + --- Create an OnDead event handler for a group + -- @param #EVENT self + -- @param #table EventTemplate + -- @param #function EventFunction The function to be called when the event occurs for the unit. + -- @param EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnEngineShutDownForTemplate( EventTemplate, EventFunction, EventClass ) + self:F2( EventTemplate.name ) + + self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, 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 EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnEngineShutDownForUnit( EventDCSUnitName, EventFunction, EventClass ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_ENGINE_SHUTDOWN ) + + return self + end + + --- Stop listening to S_EVENT_ENGINE_SHUTDOWN event. + -- @param #EVENT self + -- @param Base#BASE EventClass + -- @return #EVENT + function EVENT:OnEngineShutDownRemove( EventClass ) + self:F2() + + self:Remove( EventClass, world.event.S_EVENT_ENGINE_SHUTDOWN ) + + return self + end + +end + +do -- OnEngineStartUp + + --- 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 EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnEngineStartUpForUnit( EventDCSUnitName, EventFunction, EventClass ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_ENGINE_STARTUP ) + + return self + end + + --- Stop listening to S_EVENT_ENGINE_STARTUP event. + -- @param #EVENT self + -- @param Base#BASE EventClass + -- @return #EVENT + function EVENT:OnEngineStartUpRemove( EventClass ) + self:F2() + + self:Remove( EventClass, world.event.S_EVENT_ENGINE_STARTUP ) + + return self + end + +end + +do -- OnShot + --- 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 EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnShot( EventFunction, EventClass ) + self:F2() + + self:OnEventGeneric( EventFunction, EventClass, 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 EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnShotForUnit( EventDCSUnitName, EventFunction, EventClass ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_SHOT ) + + return self + end + + --- Stop listening to S_EVENT_SHOT event. + -- @param #EVENT self + -- @param Base#BASE EventClass + -- @return #EVENT + function EVENT:OnShotRemove( EventClass ) + self:F2() + + self:Remove( EventClass, world.event.S_EVENT_SHOT ) + + return self + end + + +end + +do -- OnHit + + --- 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 EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnHit( EventFunction, EventClass ) + self:F2() + + self:OnEventGeneric( EventFunction, EventClass, 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 EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnHitForUnit( EventDCSUnitName, EventFunction, EventClass ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventClass, world.event.S_EVENT_HIT ) + + return self + end + + --- Stop listening to S_EVENT_HIT event. + -- @param #EVENT self + -- @param Base#BASE EventClass + -- @return #EVENT + function EVENT:OnHitRemove( EventClass ) + self:F2() + + self:Remove( EventClass, world.event.S_EVENT_HIT ) + + return self + end + +end + +do -- OnPlayerEnterUnit + + --- 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 EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnPlayerEnterUnit( EventFunction, EventClass ) + self:F2() + + self:OnEventGeneric( EventFunction, EventClass, world.event.S_EVENT_PLAYER_ENTER_UNIT ) + + return self + end + + --- Stop listening to S_EVENT_PLAYER_ENTER_UNIT event. + -- @param #EVENT self + -- @param Base#BASE EventClass + -- @return #EVENT + function EVENT:OnPlayerEnterRemove( EventClass ) + self:F2() + + self:Remove( EventClass, world.event.S_EVENT_PLAYER_ENTER_UNIT ) + + return self + end + +end + +do -- OnPlayerLeaveUnit + --- 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 EventClass The self instance of the class for which the event is. + -- @return #EVENT + function EVENT:OnPlayerLeaveUnit( EventFunction, EventClass ) + self:F2() + + self:OnEventGeneric( EventFunction, EventClass, world.event.S_EVENT_PLAYER_LEAVE_UNIT ) + + return self + end + + --- Stop listening to S_EVENT_PLAYER_LEAVE_UNIT event. + -- @param #EVENT self + -- @param Base#BASE EventClass + -- @return #EVENT + function EVENT:OnPlayerLeaveRemove( EventClass ) + self:F2() + + self:Remove( EventClass, world.event.S_EVENT_PLAYER_LEAVE_UNIT ) + + return self + end + +end + + + +--- @param #EVENT self +-- @param #EVENTDATA Event +function EVENT:onEvent( Event ) + + local ErrorHandler = function( errmsg ) + + env.info( "Error in SCHEDULER function:" .. errmsg ) + if debug ~= nil then + env.info( debug.traceback() ) + end + + return errmsg + end + + if self and self.Events and self.Events[Event.id] then + + + if Event.initiator then + + Event.IniObjectCategory = Event.initiator:getCategory() + + if Event.IniObjectCategory == Object.Category.UNIT then + Event.IniDCSUnit = Event.initiator + Event.IniDCSUnitName = Event.IniDCSUnit:getName() + Event.IniUnitName = Event.IniDCSUnitName + Event.IniDCSGroup = Event.IniDCSUnit:getGroup() + Event.IniUnit = UNIT:FindByName( Event.IniDCSUnitName ) + if not Event.IniUnit then + -- Unit can be a CLIENT. Most likely this will be the case ... + Event.IniUnit = CLIENT:FindByName( Event.IniDCSUnitName, '', true ) + end + Event.IniDCSGroupName = "" + if Event.IniDCSGroup and Event.IniDCSGroup:isExist() then + Event.IniDCSGroupName = Event.IniDCSGroup:getName() + Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) + if Event.IniGroup then + Event.IniGroupName = Event.IniDCSGroupName + end + end + Event.IniPlayerName = Event.IniDCSUnit:getPlayerName() + Event.IniCoalition = Event.IniDCSUnit:getCoalition() + Event.IniTypeName = Event.IniDCSUnit:getTypeName() + Event.IniCategory = Event.IniDCSUnit:getDesc().category + end + + if Event.IniObjectCategory == Object.Category.STATIC then + Event.IniDCSUnit = Event.initiator + Event.IniDCSUnitName = Event.IniDCSUnit:getName() + Event.IniUnitName = Event.IniDCSUnitName + Event.IniUnit = STATIC:FindByName( Event.IniDCSUnitName, false ) + Event.IniCoalition = Event.IniDCSUnit:getCoalition() + Event.IniCategory = Event.IniDCSUnit:getDesc().category + Event.IniTypeName = Event.IniDCSUnit:getTypeName() + end + + if Event.IniObjectCategory == Object.Category.SCENERY then + Event.IniDCSUnit = Event.initiator + Event.IniDCSUnitName = Event.IniDCSUnit:getName() + Event.IniUnitName = Event.IniDCSUnitName + Event.IniUnit = SCENERY:Register( Event.IniDCSUnitName, Event.initiator ) + Event.IniCategory = Event.IniDCSUnit:getDesc().category + Event.IniTypeName = Event.IniDCSUnit:getTypeName() + end + end + + if Event.target then + + Event.TgtObjectCategory = Event.target:getCategory() + + if Event.TgtObjectCategory == Object.Category.UNIT then + Event.TgtDCSUnit = Event.target + Event.TgtDCSGroup = Event.TgtDCSUnit:getGroup() + Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() + Event.TgtUnitName = Event.TgtDCSUnitName + Event.TgtUnit = UNIT:FindByName( Event.TgtDCSUnitName ) + Event.TgtDCSGroupName = "" + if Event.TgtDCSGroup and Event.TgtDCSGroup:isExist() then + Event.TgtDCSGroupName = Event.TgtDCSGroup:getName() + Event.TgtGroup = GROUP:FindByName( Event.TgtDCSGroupName ) + if Event.TgtGroup then + Event.TgtGroupName = Event.TgtDCSGroupName + end + end + Event.TgtPlayerName = Event.TgtDCSUnit:getPlayerName() + Event.TgtCoalition = Event.TgtDCSUnit:getCoalition() + Event.TgtCategory = Event.TgtDCSUnit:getDesc().category + Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() + end + + if Event.TgtObjectCategory == Object.Category.STATIC then + Event.TgtDCSUnit = Event.target + Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() + Event.TgtUnitName = Event.TgtDCSUnitName + Event.TgtUnit = STATIC:FindByName( Event.TgtDCSUnitName ) + Event.TgtCoalition = Event.TgtDCSUnit:getCoalition() + Event.TgtCategory = Event.TgtDCSUnit:getDesc().category + Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() + end + + if Event.TgtObjectCategory == Object.Category.SCENERY then + Event.TgtDCSUnit = Event.target + Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() + Event.TgtUnitName = Event.TgtDCSUnitName + Event.TgtUnit = SCENERY:Register( Event.TgtDCSUnitName, Event.target ) + Event.TgtCategory = Event.TgtDCSUnit:getDesc().category + Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() + end + end + + if Event.weapon then + Event.Weapon = Event.weapon + Event.WeaponName = Event.Weapon:getTypeName() + --Event.WeaponTgtDCSUnit = Event.Weapon:getTarget() + end + + local PriorityOrder = _EVENTMETA[Event.id].Order + local PriorityBegin = PriorityOrder == -1 and 5 or 1 + local PriorityEnd = PriorityOrder == -1 and 1 or 5 + + self:E( { _EVENTMETA[Event.id].Text, Event, Event.IniDCSUnitName, Event.TgtDCSUnitName, PriorityOrder } ) + + for EventPriority = PriorityBegin, PriorityEnd, PriorityOrder do + + if self.Events[Event.id][EventPriority] then + + -- Okay, we got the event from DCS. Now loop the SORTED self.EventSorted[] table for the received Event.id, and for each EventData registered, check if a function needs to be called. + for EventClass, EventData in pairs( self.Events[Event.id][EventPriority] ) do + + Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) + + -- If the EventData is for a UNIT, the call directly the EventClass EventFunction for that UNIT. + if Event.IniDCSUnitName and EventData.IniUnit and EventData.IniUnit[Event.IniDCSUnitName] then + + -- First test if a EventFunction is Set, otherwise search for the default function + if EventData.IniUnit[Event.IniDCSUnitName].EventFunction then + + self:E( { "Calling EventFunction for UNIT ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName, EventPriority } ) + + local Result, Value = xpcall( + function() + return EventData.IniUnit[Event.IniDCSUnitName].EventFunction( EventClass, Event ) + end, ErrorHandler ) + + else + + -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. + local EventFunction = EventClass[ _EVENTMETA[Event.id].Event ] + if EventFunction and type( EventFunction ) == "function" then + + -- Now call the default event function. + self:E( { "Calling " .. _EVENTMETA[Event.id].Event .. " for Class ", EventClass:GetClassNameAndID(), EventPriority } ) + + local Result, Value = xpcall( + function() + return EventFunction( EventClass, Event ) + end, ErrorHandler ) + end + + end + + else + + -- If the EventData is for a GROUP, the call directly the EventClass EventFunction for the UNIT in that GROUP. + if Event.IniDCSUnitName and Event.IniDCSGroupName and Event.IniGroupName and EventData.IniGroup and EventData.IniGroup[Event.IniGroupName] then + + -- First test if a EventFunction is Set, otherwise search for the default function + if EventData.IniGroup[Event.IniGroupName].EventFunction then + + self:E( { "Calling EventFunction for GROUP ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName, EventPriority } ) + + local Result, Value = xpcall( + function() + return EventData.IniGroup[Event.IniGroupName].EventFunction( EventClass, Event ) + end, ErrorHandler ) + + else + + -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. + local EventFunction = EventClass[ _EVENTMETA[Event.id].Event ] + if EventFunction and type( EventFunction ) == "function" then + + -- Now call the default event function. + self:E( { "Calling " .. _EVENTMETA[Event.id].Event .. " for GROUP ", EventClass:GetClassNameAndID(), EventPriority } ) + + local Result, Value = xpcall( + function() + return EventFunction( EventClass, Event ) + end, ErrorHandler ) + end + + end + + else + + -- If the EventData is not bound to a specific unit, then call the EventClass EventFunction. + -- Note that here the EventFunction will need to implement and determine the logic for the relevant source- or target unit, or weapon. + if Event.IniDCSUnit and not EventData.IniUnit then + + if EventClass == EventData.EventClass then + + -- First test if a EventFunction is Set, otherwise search for the default function + if EventData.EventFunction then + + -- There is an EventFunction defined, so call the EventFunction. + self:E( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID(), EventPriority } ) + + local Result, Value = xpcall( + function() + return EventData.EventFunction( EventClass, Event ) + end, ErrorHandler ) + else + + -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. + local EventFunction = EventClass[ _EVENTMETA[Event.id].Event ] + if EventFunction and type( EventFunction ) == "function" then + + -- Now call the default event function. + self:E( { "Calling " .. _EVENTMETA[Event.id].Event .. " for Class ", EventClass:GetClassNameAndID(), EventPriority } ) + + local Result, Value = xpcall( + function() + return EventFunction( EventClass, Event ) + end, ErrorHandler ) + end + end + end + end + end + end + end + end + end + else + self:E( { _EVENTMETA[Event.id].Text, Event } ) + end +end + +--- The EVENTHANDLER structure +-- @type EVENTHANDLER +-- @extends Core.Base#BASE +EVENTHANDLER = { + ClassName = "EVENTHANDLER", + ClassID = 0, +} + +--- The EVENTHANDLER constructor +-- @param #EVENTHANDLER self +-- @return #EVENTHANDLER +function EVENTHANDLER:New() + self = BASE:Inherit( self, BASE:New() ) -- #EVENTHANDLER + return self +end +--- This module contains the MENU classes. +-- +-- === +-- +-- DCS Menus can be managed using the MENU classes. +-- The advantage of using MENU classes is that it hides the complexity of dealing with menu management in more advanced scanerios where you need to +-- set menus and later remove them, and later set them again. You'll find while using use normal DCS scripting functions, that setting and removing +-- menus is not a easy feat if you have complex menu hierarchies defined. +-- Using the MOOSE menu classes, the removal and refreshing of menus are nicely being handled within these classes, and becomes much more easy. +-- On top, MOOSE implements **variable parameter** passing for command menus. +-- +-- There are basically two different MENU class types that you need to use: +-- +-- ### To manage **main menus**, the classes begin with **MENU_**: +-- +-- * @{Menu#MENU_MISSION}: Manages main menus for whole mission file. +-- * @{Menu#MENU_COALITION}: Manages main menus for whole coalition. +-- * @{Menu#MENU_GROUP}: Manages main menus for GROUPs. +-- * @{Menu#MENU_CLIENT}: Manages main menus for CLIENTs. This manages menus for units with the skill level "Client". +-- +-- ### To manage **command menus**, which are menus that allow the player to issue **functions**, the classes begin with **MENU_COMMAND_**: +-- +-- * @{Menu#MENU_MISSION_COMMAND}: Manages command menus for whole mission file. +-- * @{Menu#MENU_COALITION_COMMAND}: Manages command menus for whole coalition. +-- * @{Menu#MENU_GROUP_COMMAND}: Manages command menus for GROUPs. +-- * @{Menu#MENU_CLIENT_COMMAND}: Manages command menus for CLIENTs. This manages menus for units with the skill level "Client". +-- +-- === +-- +-- The above menus classes **are derived** from 2 main **abstract** classes defined within the MOOSE framework (so don't use these): +-- +-- 1) MENU_ BASE abstract base classes (don't use them) +-- ==================================================== +-- The underlying base menu classes are **NOT** to be used within your missions. +-- These are simply abstract base classes defining a couple of fields that are used by the +-- derived MENU_ classes to manage menus. +-- +-- 1.1) @{#MENU_BASE} class, extends @{Base#BASE} +-- -------------------------------------------------- +-- The @{#MENU_BASE} class defines the main MENU class where other MENU classes are derived from. +-- +-- 1.2) @{#MENU_COMMAND_BASE} class, extends @{Base#BASE} +-- ---------------------------------------------------------- +-- The @{#MENU_COMMAND_BASE} class defines the main MENU class where other MENU COMMAND_ classes are derived from, in order to set commands. +-- +-- === +-- +-- **The next menus define the MENU classes that you can use within your missions.** +-- +-- 2) MENU MISSION classes +-- ====================== +-- The underlying classes manage the menus for a complete mission file. +-- +-- 2.1) @{#MENU_MISSION} class, extends @{Menu#MENU_BASE} +-- --------------------------------------------------------- +-- The @{Menu#MENU_MISSION} class manages the main menus for a complete mission. +-- You can add menus with the @{#MENU_MISSION.New} method, which constructs a MENU_MISSION object and returns you the object reference. +-- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_MISSION.Remove}. +-- +-- 2.2) @{#MENU_MISSION_COMMAND} class, extends @{Menu#MENU_COMMAND_BASE} +-- ------------------------------------------------------------------------- +-- The @{Menu#MENU_MISSION_COMMAND} class manages the command menus for a complete mission, which allow players to execute functions during mission execution. +-- You can add menus with the @{#MENU_MISSION_COMMAND.New} method, which constructs a MENU_MISSION_COMMAND object and returns you the object reference. +-- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_MISSION_COMMAND.Remove}. +-- +-- === +-- +-- 3) MENU COALITION classes +-- ========================= +-- The underlying classes manage the menus for whole coalitions. +-- +-- 3.1) @{#MENU_COALITION} class, extends @{Menu#MENU_BASE} +-- ------------------------------------------------------------ +-- The @{Menu#MENU_COALITION} class manages the main menus for coalitions. +-- You can add menus with the @{#MENU_COALITION.New} method, which constructs a MENU_COALITION object and returns you the object reference. +-- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_COALITION.Remove}. +-- +-- 3.2) @{Menu#MENU_COALITION_COMMAND} class, extends @{Menu#MENU_COMMAND_BASE} +-- ---------------------------------------------------------------------------- +-- The @{Menu#MENU_COALITION_COMMAND} class manages the command menus for coalitions, which allow players to execute functions during mission execution. +-- You can add menus with the @{#MENU_COALITION_COMMAND.New} method, which constructs a MENU_COALITION_COMMAND object and returns you the object reference. +-- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_COALITION_COMMAND.Remove}. +-- +-- === +-- +-- 4) MENU GROUP classes +-- ===================== +-- The underlying classes manage the menus for groups. Note that groups can be inactive, alive or can be destroyed. +-- +-- 4.1) @{Menu#MENU_GROUP} class, extends @{Menu#MENU_BASE} +-- -------------------------------------------------------- +-- The @{Menu#MENU_GROUP} class manages the main menus for coalitions. +-- You can add menus with the @{#MENU_GROUP.New} method, which constructs a MENU_GROUP object and returns you the object reference. +-- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_GROUP.Remove}. +-- +-- 4.2) @{Menu#MENU_GROUP_COMMAND} class, extends @{Menu#MENU_COMMAND_BASE} +-- ------------------------------------------------------------------------ +-- The @{Menu#MENU_GROUP_COMMAND} class manages the command menus for coalitions, which allow players to execute functions during mission execution. +-- You can add menus with the @{#MENU_GROUP_COMMAND.New} method, which constructs a MENU_GROUP_COMMAND object and returns you the object reference. +-- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_GROUP_COMMAND.Remove}. +-- +-- === +-- +-- 5) MENU CLIENT classes +-- ====================== +-- The underlying classes manage the menus for units with skill level client or player. +-- +-- 5.1) @{Menu#MENU_CLIENT} class, extends @{Menu#MENU_BASE} +-- --------------------------------------------------------- +-- The @{Menu#MENU_CLIENT} class manages the main menus for coalitions. +-- You can add menus with the @{#MENU_CLIENT.New} method, which constructs a MENU_CLIENT object and returns you the object reference. +-- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_CLIENT.Remove}. +-- +-- 5.2) @{Menu#MENU_CLIENT_COMMAND} class, extends @{Menu#MENU_COMMAND_BASE} +-- ------------------------------------------------------------------------- +-- The @{Menu#MENU_CLIENT_COMMAND} class manages the command menus for coalitions, which allow players to execute functions during mission execution. +-- You can add menus with the @{#MENU_CLIENT_COMMAND.New} method, which constructs a MENU_CLIENT_COMMAND object and returns you the object reference. +-- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_CLIENT_COMMAND.Remove}. +-- +-- === +-- +-- ### Contributions: - +-- ### Authors: FlightControl : Design & Programming +-- +-- @module Menu + + +do -- MENU_BASE + + --- The MENU_BASE class + -- @type MENU_BASE + -- @extends Base#BASE + MENU_BASE = { + ClassName = "MENU_BASE", + MenuPath = nil, + MenuText = "", + MenuParentPath = nil + } + + --- Consructor + function MENU_BASE:New( MenuText, ParentMenu ) + + local MenuParentPath = {} + if ParentMenu ~= nil then + MenuParentPath = ParentMenu.MenuPath + end + + local self = BASE:Inherit( self, BASE:New() ) + + self.MenuPath = nil + self.MenuText = MenuText + self.MenuParentPath = MenuParentPath + + return self + end + +end + +do -- MENU_COMMAND_BASE + + --- The MENU_COMMAND_BASE class + -- @type MENU_COMMAND_BASE + -- @field #function MenuCallHandler + -- @extends Menu#MENU_BASE + MENU_COMMAND_BASE = { + ClassName = "MENU_COMMAND_BASE", + CommandMenuFunction = nil, + CommandMenuArgument = nil, + MenuCallHandler = nil, + } + + --- Constructor + function MENU_COMMAND_BASE:New( MenuText, ParentMenu, CommandMenuFunction, CommandMenuArguments ) + + local self = BASE:Inherit( self, MENU_BASE:New( MenuText, ParentMenu ) ) + + self.CommandMenuFunction = CommandMenuFunction + self.MenuCallHandler = function( CommandMenuArguments ) + self.CommandMenuFunction( unpack( CommandMenuArguments ) ) + end + + return self + end + +end + + +do -- MENU_MISSION + + --- The MENU_MISSION class + -- @type MENU_MISSION + -- @extends Menu#MENU_BASE + MENU_MISSION = { + ClassName = "MENU_MISSION" + } + + --- MENU_MISSION constructor. Creates a new MENU_MISSION object and creates the menu for a complete mission file. + -- @param #MENU_MISSION self + -- @param #string MenuText The text for the menu. + -- @param #table ParentMenu The parent menu. This parameter can be ignored if you want the menu to be located at the perent menu of DCS world (under F10 other). + -- @return #MENU_MISSION self + function MENU_MISSION:New( MenuText, ParentMenu ) + + local self = BASE:Inherit( self, MENU_BASE:New( MenuText, ParentMenu ) ) + + self:F( { MenuText, ParentMenu } ) + + self.MenuText = MenuText + self.ParentMenu = ParentMenu + + self.Menus = {} + + self:T( { MenuText } ) + + self.MenuPath = missionCommands.addSubMenu( MenuText, self.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_MISSION. Note that the main menu is kept! + -- @param #MENU_MISSION self + -- @return #MENU_MISSION self + function MENU_MISSION:RemoveSubMenus() + self:F( self.MenuPath ) + + for MenuID, Menu in pairs( self.Menus ) do + Menu:Remove() + end + + end + + --- Removes the main menu and the sub menus recursively of this MENU_MISSION. + -- @param #MENU_MISSION self + -- @return #nil + function MENU_MISSION:Remove() + self:F( self.MenuPath ) + + self:RemoveSubMenus() + missionCommands.removeItem( self.MenuPath ) + if self.ParentMenu then + self.ParentMenu.Menus[self.MenuPath] = nil + end + + return nil + end + +end + +do -- MENU_MISSION_COMMAND + + --- The MENU_MISSION_COMMAND class + -- @type MENU_MISSION_COMMAND + -- @extends Menu#MENU_COMMAND_BASE + MENU_MISSION_COMMAND = { + ClassName = "MENU_MISSION_COMMAND" + } + + --- MENU_MISSION constructor. Creates a new radio command item for a complete mission file, which can invoke a function with parameters. + -- @param #MENU_MISSION_COMMAND self + -- @param #string MenuText The text for the menu. + -- @param Menu#MENU_MISSION ParentMenu The parent menu. + -- @param CommandMenuFunction A function that is called when the menu key is pressed. + -- @param CommandMenuArgument An argument for the function. There can only be ONE argument given. So multiple arguments must be wrapped into a table. See the below example how to do this. + -- @return #MENU_MISSION_COMMAND self + function MENU_MISSION_COMMAND:New( MenuText, ParentMenu, CommandMenuFunction, ... ) + + local self = BASE:Inherit( self, MENU_COMMAND_BASE:New( MenuText, ParentMenu, CommandMenuFunction, arg ) ) + + self.MenuText = MenuText + self.ParentMenu = ParentMenu + + self:T( { MenuText, CommandMenuFunction, arg } ) + + + self.MenuPath = missionCommands.addCommand( MenuText, self.MenuParentPath, self.MenuCallHandler, arg ) + + ParentMenu.Menus[self.MenuPath] = self + + return self + end + + --- Removes a radio command item for a coalition + -- @param #MENU_MISSION_COMMAND self + -- @return #nil + function MENU_MISSION_COMMAND:Remove() + self:F( self.MenuPath ) + + missionCommands.removeItem( self.MenuPath ) + if self.ParentMenu then + self.ParentMenu.Menus[self.MenuPath] = nil + end + return nil + end + +end + + + +do -- MENU_COALITION + + --- The MENU_COALITION class + -- @type MENU_COALITION + -- @extends Menu#MENU_BASE + -- @usage + -- -- This demo creates a menu structure for the planes within the red coalition. + -- -- To test, join the planes, then look at the other radio menus (Option F10). + -- -- Then switch planes and check if the menu is still there. + -- + -- local Plane1 = CLIENT:FindByName( "Plane 1" ) + -- local Plane2 = CLIENT:FindByName( "Plane 2" ) + -- + -- + -- -- This would create a menu for the red coalition under the main DCS "Others" menu. + -- local MenuCoalitionRed = MENU_COALITION:New( coalition.side.RED, "Manage Menus" ) + -- + -- + -- local function ShowStatus( StatusText, Coalition ) + -- + -- MESSAGE:New( Coalition, 15 ):ToRed() + -- Plane1:Message( StatusText, 15 ) + -- Plane2:Message( StatusText, 15 ) + -- end + -- + -- local MenuStatus -- Menu#MENU_COALITION + -- local MenuStatusShow -- Menu#MENU_COALITION_COMMAND + -- + -- local function RemoveStatusMenu() + -- MenuStatus:Remove() + -- end + -- + -- local function AddStatusMenu() + -- + -- -- This would create a menu for the red coalition under the MenuCoalitionRed menu object. + -- MenuStatus = MENU_COALITION:New( coalition.side.RED, "Status for Planes" ) + -- MenuStatusShow = MENU_COALITION_COMMAND:New( coalition.side.RED, "Show Status", MenuStatus, ShowStatus, "Status of planes is ok!", "Message to Red Coalition" ) + -- end + -- + -- local MenuAdd = MENU_COALITION_COMMAND:New( coalition.side.RED, "Add Status Menu", MenuCoalitionRed, AddStatusMenu ) + -- local MenuRemove = MENU_COALITION_COMMAND:New( coalition.side.RED, "Remove Status Menu", MenuCoalitionRed, RemoveStatusMenu ) + MENU_COALITION = { + ClassName = "MENU_COALITION" + } + + --- MENU_COALITION constructor. Creates a new MENU_COALITION object and creates the menu for a complete coalition. + -- @param #MENU_COALITION self + -- @param Dcs.DCSCoalition#coalition.side Coalition The coalition owning the menu. + -- @param #string MenuText The text for the menu. + -- @param #table ParentMenu The parent menu. This parameter can be ignored if you want the menu to be located at the perent menu of DCS world (under F10 other). + -- @return #MENU_COALITION self + function MENU_COALITION:New( Coalition, MenuText, ParentMenu ) + + local self = BASE:Inherit( self, MENU_BASE:New( MenuText, ParentMenu ) ) + + self:F( { Coalition, MenuText, ParentMenu } ) + + self.Coalition = Coalition + self.MenuText = MenuText + self.ParentMenu = ParentMenu + + self.Menus = {} + + self:T( { MenuText } ) + + self.MenuPath = missionCommands.addSubMenuForCoalition( Coalition, MenuText, self.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. Note that the main menu is kept! + -- @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 main menu and the sub menus recursively of this MENU_COALITION. + -- @param #MENU_COALITION self + -- @return #nil + function MENU_COALITION:Remove() + self:F( self.MenuPath ) + + self:RemoveSubMenus() + missionCommands.removeItemForCoalition( self.Coalition, self.MenuPath ) + if self.ParentMenu then + self.ParentMenu.Menus[self.MenuPath] = nil + end + + return nil + end + +end + +do -- MENU_COALITION_COMMAND + + --- The MENU_COALITION_COMMAND class + -- @type MENU_COALITION_COMMAND + -- @extends Menu#MENU_COMMAND_BASE + MENU_COALITION_COMMAND = { + ClassName = "MENU_COALITION_COMMAND" + } + + --- MENU_COALITION constructor. Creates a new radio command item for a coalition, which can invoke a function with parameters. + -- @param #MENU_COALITION_COMMAND self + -- @param Dcs.DCSCoalition#coalition.side Coalition The coalition owning the menu. + -- @param #string MenuText The text for the menu. + -- @param Menu#MENU_COALITION ParentMenu The parent menu. + -- @param CommandMenuFunction A function that is called when the menu key is pressed. + -- @param CommandMenuArgument An argument for the function. There can only be ONE argument given. So multiple arguments must be wrapped into a table. See the below example how to do this. + -- @return #MENU_COALITION_COMMAND self + function MENU_COALITION_COMMAND:New( Coalition, MenuText, ParentMenu, CommandMenuFunction, ... ) + + local self = BASE:Inherit( self, MENU_COMMAND_BASE:New( MenuText, ParentMenu, CommandMenuFunction, arg ) ) + + self.MenuCoalition = Coalition + self.MenuText = MenuText + self.ParentMenu = ParentMenu + + self:T( { MenuText, CommandMenuFunction, arg } ) + + + self.MenuPath = missionCommands.addCommandForCoalition( self.MenuCoalition, MenuText, self.MenuParentPath, self.MenuCallHandler, arg ) + + ParentMenu.Menus[self.MenuPath] = self + + return self + end + + --- Removes a radio command item for a coalition + -- @param #MENU_COALITION_COMMAND self + -- @return #nil + function MENU_COALITION_COMMAND:Remove() + self:F( self.MenuPath ) + + missionCommands.removeItemForCoalition( self.MenuCoalition, self.MenuPath ) + if self.ParentMenu then + self.ParentMenu.Menus[self.MenuPath] = nil + end + return nil + end + +end + +do -- MENU_CLIENT + + -- 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 = {} + + --- MENU_COALITION constructor. Creates a new radio command item for a coalition, which can invoke a function with parameters. + -- @type MENU_CLIENT + -- @extends Menu#MENU_BASE + -- @usage + -- -- This demo creates a menu structure for the two clients of planes. + -- -- Each client will receive a different menu structure. + -- -- To test, join the planes, then look at the other radio menus (Option F10). + -- -- Then switch planes and check if the menu is still there. + -- -- And play with the Add and Remove menu options. + -- + -- -- Note that in multi player, this will only work after the DCS clients bug is solved. + -- + -- local function ShowStatus( PlaneClient, StatusText, Coalition ) + -- + -- MESSAGE:New( Coalition, 15 ):ToRed() + -- PlaneClient:Message( StatusText, 15 ) + -- end + -- + -- local MenuStatus = {} + -- + -- local function RemoveStatusMenu( MenuClient ) + -- local MenuClientName = MenuClient:GetName() + -- MenuStatus[MenuClientName]:Remove() + -- end + -- + -- --- @param Wrapper.Client#CLIENT MenuClient + -- local function AddStatusMenu( MenuClient ) + -- local MenuClientName = MenuClient:GetName() + -- -- This would create a menu for the red coalition under the MenuCoalitionRed menu object. + -- MenuStatus[MenuClientName] = MENU_CLIENT:New( MenuClient, "Status for Planes" ) + -- MENU_CLIENT_COMMAND:New( MenuClient, "Show Status", MenuStatus[MenuClientName], ShowStatus, MenuClient, "Status of planes is ok!", "Message to Red Coalition" ) + -- end + -- + -- SCHEDULER:New( nil, + -- function() + -- local PlaneClient = CLIENT:FindByName( "Plane 1" ) + -- if PlaneClient and PlaneClient:IsAlive() then + -- local MenuManage = MENU_CLIENT:New( PlaneClient, "Manage Menus" ) + -- MENU_CLIENT_COMMAND:New( PlaneClient, "Add Status Menu Plane 1", MenuManage, AddStatusMenu, PlaneClient ) + -- MENU_CLIENT_COMMAND:New( PlaneClient, "Remove Status Menu Plane 1", MenuManage, RemoveStatusMenu, PlaneClient ) + -- end + -- end, {}, 10, 10 ) + -- + -- SCHEDULER:New( nil, + -- function() + -- local PlaneClient = CLIENT:FindByName( "Plane 2" ) + -- if PlaneClient and PlaneClient:IsAlive() then + -- local MenuManage = MENU_CLIENT:New( PlaneClient, "Manage Menus" ) + -- MENU_CLIENT_COMMAND:New( PlaneClient, "Add Status Menu Plane 2", MenuManage, AddStatusMenu, PlaneClient ) + -- MENU_CLIENT_COMMAND:New( PlaneClient, "Remove Status Menu Plane 2", MenuManage, RemoveStatusMenu, PlaneClient ) + -- end + -- end, {}, 10, 10 ) + MENU_CLIENT = { + ClassName = "MENU_CLIENT" + } + + --- MENU_CLIENT constructor. Creates a new radio menu item for a client. + -- @param #MENU_CLIENT self + -- @param Wrapper.Client#CLIENT Client 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( Client, MenuText, ParentMenu ) + + -- Arrange meta tables + local MenuParentPath = {} + if ParentMenu ~= nil then + MenuParentPath = ParentMenu.MenuPath + end + + local self = BASE:Inherit( self, MENU_BASE:New( MenuText, MenuParentPath ) ) + self:F( { Client, MenuText, ParentMenu } ) + + self.MenuClient = Client + self.MenuClientGroupID = Client: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( { Client: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( { Client: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 #nil + 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_COMMAND + MENU_CLIENT_COMMAND = { + ClassName = "MENU_CLIENT_COMMAND" + } + + --- MENU_CLIENT_COMMAND constructor. Creates a new radio command item for a client, which can invoke a function with parameters. + -- @param #MENU_CLIENT_COMMAND self + -- @param Wrapper.Client#CLIENT Client The Client owning the menu. + -- @param #string MenuText The text for the menu. + -- @param #MENU_BASE ParentMenu The parent menu. + -- @param CommandMenuFunction A function that is called when the menu key is pressed. + -- @return Menu#MENU_CLIENT_COMMAND self + function MENU_CLIENT_COMMAND:New( Client, MenuText, ParentMenu, CommandMenuFunction, ... ) + + -- Arrange meta tables + + local MenuParentPath = {} + if ParentMenu ~= nil then + MenuParentPath = ParentMenu.MenuPath + end + + local self = BASE:Inherit( self, MENU_COMMAND_BASE:New( MenuText, MenuParentPath, CommandMenuFunction, arg ) ) -- Menu#MENU_CLIENT_COMMAND + + self.MenuClient = Client + self.MenuClientGroupID = Client: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( { Client:GetClientGroupName(), MenuPath[table.concat(MenuParentPath)], MenuParentPath, MenuText, CommandMenuFunction, arg } ) + + 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, self.MenuCallHandler, arg ) + MenuPath[MenuPathID] = self.MenuPath + + if ParentMenu and ParentMenu.Menus then + ParentMenu.Menus[self.MenuPath] = self + end + + return self + end + + --- Removes a menu structure for a client. + -- @param #MENU_CLIENT_COMMAND self + -- @return #nil + 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 +end + +--- MENU_GROUP + +do + -- This local variable is used to cache the menus registered under groups. + -- Menus don't dissapear when groups for players are destroyed and restarted. + -- So every menu for a client created must be tracked so that program logic accidentally does not create. + -- the same menus twice during initialization logic. + -- These menu classes are handling this logic with this variable. + local _MENUGROUPS = {} + + --- The MENU_GROUP class + -- @type MENU_GROUP + -- @extends Menu#MENU_BASE + -- @usage + -- -- This demo creates a menu structure for the two groups of planes. + -- -- Each group will receive a different menu structure. + -- -- To test, join the planes, then look at the other radio menus (Option F10). + -- -- Then switch planes and check if the menu is still there. + -- -- And play with the Add and Remove menu options. + -- + -- -- Note that in multi player, this will only work after the DCS groups bug is solved. + -- + -- local function ShowStatus( PlaneGroup, StatusText, Coalition ) + -- + -- MESSAGE:New( Coalition, 15 ):ToRed() + -- PlaneGroup:Message( StatusText, 15 ) + -- end + -- + -- local MenuStatus = {} + -- + -- local function RemoveStatusMenu( MenuGroup ) + -- local MenuGroupName = MenuGroup:GetName() + -- MenuStatus[MenuGroupName]:Remove() + -- end + -- + -- --- @param Wrapper.Group#GROUP MenuGroup + -- local function AddStatusMenu( MenuGroup ) + -- local MenuGroupName = MenuGroup:GetName() + -- -- This would create a menu for the red coalition under the MenuCoalitionRed menu object. + -- MenuStatus[MenuGroupName] = MENU_GROUP:New( MenuGroup, "Status for Planes" ) + -- MENU_GROUP_COMMAND:New( MenuGroup, "Show Status", MenuStatus[MenuGroupName], ShowStatus, MenuGroup, "Status of planes is ok!", "Message to Red Coalition" ) + -- end + -- + -- SCHEDULER:New( nil, + -- function() + -- local PlaneGroup = GROUP:FindByName( "Plane 1" ) + -- if PlaneGroup and PlaneGroup:IsAlive() then + -- local MenuManage = MENU_GROUP:New( PlaneGroup, "Manage Menus" ) + -- MENU_GROUP_COMMAND:New( PlaneGroup, "Add Status Menu Plane 1", MenuManage, AddStatusMenu, PlaneGroup ) + -- MENU_GROUP_COMMAND:New( PlaneGroup, "Remove Status Menu Plane 1", MenuManage, RemoveStatusMenu, PlaneGroup ) + -- end + -- end, {}, 10, 10 ) + -- + -- SCHEDULER:New( nil, + -- function() + -- local PlaneGroup = GROUP:FindByName( "Plane 2" ) + -- if PlaneGroup and PlaneGroup:IsAlive() then + -- local MenuManage = MENU_GROUP:New( PlaneGroup, "Manage Menus" ) + -- MENU_GROUP_COMMAND:New( PlaneGroup, "Add Status Menu Plane 2", MenuManage, AddStatusMenu, PlaneGroup ) + -- MENU_GROUP_COMMAND:New( PlaneGroup, "Remove Status Menu Plane 2", MenuManage, RemoveStatusMenu, PlaneGroup ) + -- end + -- end, {}, 10, 10 ) + -- + MENU_GROUP = { + ClassName = "MENU_GROUP" + } + + --- MENU_GROUP constructor. Creates a new radio menu item for a group. + -- @param #MENU_GROUP self + -- @param Wrapper.Group#GROUP MenuGroup The Group owning the menu. + -- @param #string MenuText The text for the menu. + -- @param #table ParentMenu The parent menu. + -- @return #MENU_GROUP self + function MENU_GROUP:New( MenuGroup, MenuText, ParentMenu ) + + -- Determine if the menu was not already created and already visible at the group. + -- If it is visible, then return the cached self, otherwise, create self and cache it. + + MenuGroup._Menus = MenuGroup._Menus or {} + local Path = ( ParentMenu and ( table.concat( ParentMenu.MenuPath or {}, "@" ) .. "@" .. MenuText ) ) or MenuText + if MenuGroup._Menus[Path] then + self = MenuGroup._Menus[Path] + else + self = BASE:Inherit( self, MENU_BASE:New( MenuText, ParentMenu ) ) + MenuGroup._Menus[Path] = self + + self.Menus = {} + + self.MenuGroup = MenuGroup + self.Path = Path + self.MenuGroupID = MenuGroup:GetID() + self.MenuText = MenuText + self.ParentMenu = ParentMenu + + self:T( { "Adding Menu ", MenuText, self.MenuParentPath } ) + self.MenuPath = missionCommands.addSubMenuForGroup( self.MenuGroupID, MenuText, self.MenuParentPath ) + + if ParentMenu and ParentMenu.Menus then + ParentMenu.Menus[self.MenuPath] = self + end + end + + --self:F( { MenuGroup:GetName(), MenuText, ParentMenu.MenuPath } ) + + return self + end + + --- Removes the sub menus recursively of this MENU_GROUP. + -- @param #MENU_GROUP self + -- @return #MENU_GROUP self + function MENU_GROUP:RemoveSubMenus() + self:F( self.MenuPath ) + + for MenuID, Menu in pairs( self.Menus ) do + Menu:Remove() + end + + end + + --- Removes the main menu and sub menus recursively of this MENU_GROUP. + -- @param #MENU_GROUP self + -- @return #nil + function MENU_GROUP:Remove() + self:F( { self.MenuGroupID, self.MenuPath } ) + + self:RemoveSubMenus() + + if self.MenuGroup._Menus[self.Path] then + self = self.MenuGroup._Menus[self.Path] + + missionCommands.removeItemForGroup( self.MenuGroupID, self.MenuPath ) + if self.ParentMenu then + self.ParentMenu.Menus[self.MenuPath] = nil + end + self:E( self.MenuGroup._Menus[self.Path] ) + self.MenuGroup._Menus[self.Path] = nil + self = nil + end + return nil + end + + + --- The MENU_GROUP_COMMAND class + -- @type MENU_GROUP_COMMAND + -- @extends Menu#MENU_BASE + MENU_GROUP_COMMAND = { + ClassName = "MENU_GROUP_COMMAND" + } + + --- Creates a new radio command item for a group + -- @param #MENU_GROUP_COMMAND self + -- @param Wrapper.Group#GROUP MenuGroup The Group owning the menu. + -- @param MenuText The text for the menu. + -- @param ParentMenu The parent menu. + -- @param CommandMenuFunction A function that is called when the menu key is pressed. + -- @param CommandMenuArgument An argument for the function. + -- @return Menu#MENU_GROUP_COMMAND self + function MENU_GROUP_COMMAND:New( MenuGroup, MenuText, ParentMenu, CommandMenuFunction, ... ) + + MenuGroup._Menus = MenuGroup._Menus or {} + local Path = ( ParentMenu and ( table.concat( ParentMenu.MenuPath or {}, "@" ) .. "@" .. MenuText ) ) or MenuText + if MenuGroup._Menus[Path] then + self = MenuGroup._Menus[Path] + else + self = BASE:Inherit( self, MENU_COMMAND_BASE:New( MenuText, ParentMenu, CommandMenuFunction, arg ) ) + MenuGroup._Menus[Path] = self + + self.Path = Path + self.MenuGroup = MenuGroup + self.MenuGroupID = MenuGroup:GetID() + self.MenuText = MenuText + self.ParentMenu = ParentMenu + + self:T( { "Adding Command Menu ", MenuText, self.MenuParentPath } ) + self.MenuPath = missionCommands.addCommandForGroup( self.MenuGroupID, MenuText, self.MenuParentPath, self.MenuCallHandler, arg ) + + if ParentMenu and ParentMenu.Menus then + ParentMenu.Menus[self.MenuPath] = self + end + end + + --self:F( { MenuGroup:GetName(), MenuText, ParentMenu.MenuPath } ) + + return self + end + + --- Removes a menu structure for a group. + -- @param #MENU_GROUP_COMMAND self + -- @return #nil + function MENU_GROUP_COMMAND:Remove() + self:F( { self.MenuGroupID, self.MenuPath } ) + + if self.MenuGroup._Menus[self.Path] then + self = self.MenuGroup._Menus[self.Path] + + missionCommands.removeItemForGroup( self.MenuGroupID, self.MenuPath ) + self.ParentMenu.Menus[self.MenuPath] = nil + self:E( self.MenuGroup._Menus[self.Path] ) + self.MenuGroup._Menus[self.Path] = nil + self = nil + end + + return nil + end + +end + +--- This core 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 defines by a zone around a @{Unit#UNIT} with a radius. +-- * @{Zone#ZONE_GROUP}: The ZONE_GROUP class defines by a zone around a @{Group#GROUP} with a radius. +-- * @{Zone#ZONE_POLYGON}: The ZONE_POLYGON class defines by a sequence of @{Group#GROUP} waypoints within the Mission Editor, forming a polygon. +-- +-- === +-- +-- # 1) @{Zone#ZONE_BASE} class, extends @{Base#BASE} +-- +-- This class is an abstract BASE class for derived classes, and is not meant to be instantiated. +-- +-- ## 1.1) Each zone has a name: +-- +-- * @{#ZONE_BASE.GetName}(): Returns the name of the zone. +-- +-- ## 1.2) Each zone implements two polymorphic functions defined in @{Zone#ZONE_BASE}: +-- +-- * @{#ZONE_BASE.IsVec2InZone}(): Returns if a Vec2 is within the zone. +-- * @{#ZONE_BASE.IsVec3InZone}(): Returns if a Vec3 is within the zone. +-- +-- ## 1.3) A zone has a probability factor that can be set to randomize a selection between zones: +-- +-- * @{#ZONE_BASE.SetRandomizeProbability}(): Set the randomization probability of a zone to be selected, taking a value between 0 and 1 ( 0 = 0%, 1 = 100% ) +-- * @{#ZONE_BASE.GetRandomizeProbability}(): Get the randomization probability of a zone to be selected, passing a value between 0 and 1 ( 0 = 0%, 1 = 100% ) +-- * @{#ZONE_BASE.GetZoneMaybe}(): Get the zone taking into account the randomization probability. nil is returned if this zone is not a candidate. +-- +-- ## 1.4) A zone manages Vectors: +-- +-- * @{#ZONE_BASE.GetVec2}(): Returns the @{DCSTypes#Vec2} coordinate of the zone. +-- * @{#ZONE_BASE.GetRandomVec2}(): Define a random @{DCSTypes#Vec2} within the zone. +-- +-- ## 1.5) A zone has a bounding square: +-- +-- * @{#ZONE_BASE.GetBoundingSquare}(): Get the outer most bounding square of the zone. +-- +-- ## 1.6) A zone can be marked: +-- +-- * @{#ZONE_BASE.SmokeZone}(): Smokes the zone boundaries in a color. +-- * @{#ZONE_BASE.FlareZone}(): Flares the zone boundaries in a color. +-- +-- === +-- +-- # 2) @{Zone#ZONE_RADIUS} class, extends @{Zone#ZONE_BASE} +-- +-- The ZONE_RADIUS class defined by a zone name, a location and a radius. +-- This class implements the inherited functions from Core.Zone#ZONE_BASE taking into account the own zone format and properties. +-- +-- ## 2.1) @{Zone#ZONE_RADIUS} constructor +-- +-- * @{#ZONE_RADIUS.New}(): Constructor. +-- +-- ## 2.2) Manage the radius of the zone +-- +-- * @{#ZONE_RADIUS.SetRadius}(): Sets the radius of the zone. +-- * @{#ZONE_RADIUS.GetRadius}(): Returns the radius of the zone. +-- +-- ## 2.3) Manage the location of the zone +-- +-- * @{#ZONE_RADIUS.SetVec2}(): Sets the @{DCSTypes#Vec2} of the zone. +-- * @{#ZONE_RADIUS.GetVec2}(): Returns the @{DCSTypes#Vec2} of the zone. +-- * @{#ZONE_RADIUS.GetVec3}(): Returns the @{DCSTypes#Vec3} of the zone, taking an additional height parameter. +-- +-- ## 2.4) Zone point randomization +-- +-- Various functions exist to find random points within the zone. +-- +-- * @{#ZONE_RADIUS.GetRandomVec2}(): Gets a random 2D point in the zone. +-- * @{#ZONE_RADIUS.GetRandomPointVec2}(): Gets a @{Point#POINT_VEC2} object representing a random 2D point in the zone. +-- * @{#ZONE_RADIUS.GetRandomPointVec3}(): Gets a @{Point#POINT_VEC3} object representing a random 3D point in the zone. Note that the height of the point is at landheight. +-- +-- === +-- +-- # 3) @{Zone#ZONE} class, extends @{Zone#ZONE_RADIUS} +-- +-- The ZONE class, defined by the zone name as defined within the Mission Editor. +-- This class implements the inherited functions from {Core.Zone#ZONE_RADIUS} taking into account the own zone format and properties. +-- +-- === +-- +-- # 4) @{Zone#ZONE_UNIT} class, extends @{Zone#ZONE_RADIUS} +-- +-- The ZONE_UNIT class defined by a zone around a @{Unit#UNIT} with a radius. +-- This class implements the inherited functions from @{Zone#ZONE_RADIUS} taking into account the own zone format and properties. +-- +-- === +-- +-- # 5) @{Zone#ZONE_GROUP} class, extends @{Zone#ZONE_RADIUS} +-- +-- The ZONE_GROUP class defines by a zone around a @{Group#GROUP} with a radius. The current leader of the group defines the center of the zone. +-- This class implements the inherited functions from @{Zone#ZONE_RADIUS} taking into account the own zone format and properties. +-- +-- === +-- +-- # 6) @{Zone#ZONE_POLYGON_BASE} class, extends @{Zone#ZONE_BASE} +-- +-- The ZONE_POLYGON_BASE class defined by a sequence of @{Group#GROUP} waypoints within the Mission Editor, forming a polygon. +-- This class implements the inherited functions from @{Zone#ZONE_RADIUS} taking into account the own zone format and properties. +-- This class is an abstract BASE class for derived classes, and is not meant to be instantiated. +-- +-- ## 6.1) Zone point randomization +-- +-- Various functions exist to find random points within the zone. +-- +-- * @{#ZONE_POLYGON_BASE.GetRandomVec2}(): Gets a random 2D point in the zone. +-- * @{#ZONE_POLYGON_BASE.GetRandomPointVec2}(): Return a @{Point#POINT_VEC2} object representing a random 2D point within the zone. +-- * @{#ZONE_POLYGON_BASE.GetRandomPointVec3}(): Return a @{Point#POINT_VEC3} object representing a random 3D point at landheight within the zone. +-- +-- +-- === +-- +-- # 7) @{Zone#ZONE_POLYGON} class, extends @{Zone#ZONE_POLYGON_BASE} +-- +-- The ZONE_POLYGON class defined by a sequence of @{Group#GROUP} waypoints within the Mission Editor, forming a polygon. +-- This class implements the inherited functions from @{Zone#ZONE_RADIUS} taking into account the own zone format and properties. +-- +-- ==== +-- +-- **API CHANGE HISTORY** +-- ====================== +-- +-- The underlying change log documents the API changes. Please read this carefully. The following notation is used: +-- +-- * **Added** parts are expressed in bold type face. +-- * _Removed_ parts are expressed in italic type face. +-- +-- Hereby the change log: +-- +-- 2017-02-28: ZONE\_BASE:**IsVec2InZone()** replaces ZONE\_BASE:_IsPointVec2InZone()_. +-- 2017-02-28: ZONE\_BASE:**IsVec3InZone()** replaces ZONE\_BASE:_IsPointVec3InZone()_. +-- 2017-02-28: ZONE\_RADIUS:**IsVec2InZone()** replaces ZONE\_RADIUS:_IsPointVec2InZone()_. +-- 2017-02-28: ZONE\_RADIUS:**IsVec3InZone()** replaces ZONE\_RADIUS:_IsPointVec3InZone()_. +-- 2017-02-28: ZONE\_POLYGON:**IsVec2InZone()** replaces ZONE\_POLYGON:_IsPointVec2InZone()_. +-- 2017-02-28: ZONE\_POLYGON:**IsVec3InZone()** replaces ZONE\_POLYGON:_IsPointVec3InZone()_. +-- +-- 2017-02-18: ZONE\_POLYGON_BASE:**GetRandomPointVec2()** added. +-- +-- 2017-02-18: ZONE\_POLYGON_BASE:**GetRandomPointVec3()** added. +-- +-- 2017-02-18: ZONE\_RADIUS:**GetRandomPointVec3( inner, outer )** added. +-- +-- 2017-02-18: ZONE\_RADIUS:**GetRandomPointVec2( inner, outer )** added. +-- +-- 2016-08-15: ZONE\_BASE:**GetName()** added. +-- +-- 2016-08-15: ZONE\_BASE:**SetZoneProbability( ZoneProbability )** added. +-- +-- 2016-08-15: ZONE\_BASE:**GetZoneProbability()** added. +-- +-- 2016-08-15: ZONE\_BASE:**GetZoneMaybe()** added. +-- +-- === +-- +-- @module Zone + + +--- The ZONE_BASE class +-- @type ZONE_BASE +-- @field #string ZoneName Name of the zone. +-- @field #number ZoneProbability A value between 0 and 1. 0 = 0% and 1 = 100% probability. +-- @extends Core.Base#BASE +ZONE_BASE = { + ClassName = "ZONE_BASE", + ZoneName = "", + ZoneProbability = 1, + } + + +--- The ZONE_BASE.BoundingSquare +-- @type ZONE_BASE.BoundingSquare +-- @field Dcs.DCSTypes#Distance x1 The lower x coordinate (left down) +-- @field Dcs.DCSTypes#Distance y1 The lower y coordinate (left down) +-- @field Dcs.DCSTypes#Distance x2 The higher x coordinate (right up) +-- @field Dcs.DCSTypes#Distance y2 The higher y coordinate (right up) + + +--- 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 the name of the zone. +-- @param #ZONE_BASE self +-- @return #string The name of the zone. +function ZONE_BASE:GetName() + self:F2() + + return self.ZoneName +end +--- Returns if a location is within the zone. +-- @param #ZONE_BASE self +-- @param Dcs.DCSTypes#Vec2 Vec2 The location to test. +-- @return #boolean true if the location is within the zone. +function ZONE_BASE:IsVec2InZone( Vec2 ) + self:F2( Vec2 ) + + return false +end + +--- Returns if a point is within the zone. +-- @param #ZONE_BASE self +-- @param Dcs.DCSTypes#Vec3 Vec3 The point to test. +-- @return #boolean true if the point is within the zone. +function ZONE_BASE:IsVec3InZone( Vec3 ) + self:F2( Vec3 ) + + local InZone = self:IsVec2InZone( { x = Vec3.x, y = Vec3.z } ) + + return InZone +end + +--- Returns the @{DCSTypes#Vec2} coordinate of the zone. +-- @param #ZONE_BASE self +-- @return #nil. +function ZONE_BASE:GetVec2() + self:F2( self.ZoneName ) + + return nil +end + +--- Define a random @{DCSTypes#Vec2} within the zone. +-- @param #ZONE_BASE self +-- @return Dcs.DCSTypes#Vec2 The Vec2 coordinates. +function ZONE_BASE:GetRandomVec2() + return nil +end + +--- Define a random @{Point#POINT_VEC2} within the zone. +-- @param #ZONE_BASE self +-- @return Core.Point#POINT_VEC2 The PointVec2 coordinates. +function ZONE_BASE:GetRandomPointVec2() + return nil +end + +--- Get the bounding square the zone. +-- @param #ZONE_BASE self +-- @return #nil The bounding square. +function ZONE_BASE:GetBoundingSquare() + --return { x1 = 0, y1 = 0, x2 = 0, y2 = 0 } + return nil +end + +--- Bound the zone boundaries with a tires. +-- @param #ZONE_BASE self +function ZONE_BASE:BoundZone() + self:F2() + +end + +--- Smokes the zone boundaries in a color. +-- @param #ZONE_BASE self +-- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color. +function ZONE_BASE:SmokeZone( SmokeColor ) + self:F2( SmokeColor ) + +end + +--- Set the randomization probability of a zone to be selected. +-- @param #ZONE_BASE self +-- @param ZoneProbability A value between 0 and 1. 0 = 0% and 1 = 100% probability. +function ZONE_BASE:SetZoneProbability( ZoneProbability ) + self:F2( ZoneProbability ) + + self.ZoneProbability = ZoneProbability or 1 + return self +end + +--- Get the randomization probability of a zone to be selected. +-- @param #ZONE_BASE self +-- @return #number A value between 0 and 1. 0 = 0% and 1 = 100% probability. +function ZONE_BASE:GetZoneProbability() + self:F2() + + return self.ZoneProbability +end + +--- Get the zone taking into account the randomization probability of a zone to be selected. +-- @param #ZONE_BASE self +-- @return #ZONE_BASE The zone is selected taking into account the randomization probability factor. +-- @return #nil The zone is not selected taking into account the randomization probability factor. +function ZONE_BASE:GetZoneMaybe() + self:F2() + + local Randomization = math.random() + if Randomization <= self.ZoneProbability then + return self + else + return nil + end +end + + +--- The ZONE_RADIUS class, defined by a zone name, a location and a radius. +-- @type ZONE_RADIUS +-- @field Dcs.DCSTypes#Vec2 Vec2 The current location of the zone. +-- @field Dcs.DCSTypes#Distance Radius The radius of the zone. +-- @extends Core.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 Dcs.DCSTypes#Vec2 Vec2 The location of the zone. +-- @param Dcs.DCSTypes#Distance Radius The radius of the zone. +-- @return #ZONE_RADIUS self +function ZONE_RADIUS:New( ZoneName, Vec2, Radius ) + local self = BASE:Inherit( self, ZONE_BASE:New( ZoneName ) ) -- #ZONE_RADIUS + self:F( { ZoneName, Vec2, Radius } ) + + self.Radius = Radius + self.Vec2 = Vec2 + + return self +end + +--- Bounds the zone with tires. +-- @param #ZONE_RADIUS self +-- @param #number Points (optional) The amount of points in the circle. +-- @return #ZONE_RADIUS self +function ZONE_RADIUS:BoundZone( Points ) + + local Point = {} + local Vec2 = self:GetVec2() + + Points = Points and Points or 360 + + local Angle + local RadialBase = math.pi*2 + + -- + for Angle = 0, 360, (360 / Points ) do + local Radial = Angle * RadialBase / 360 + Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() + Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() + + local Tire = { + ["country"] = "USA", + ["category"] = "Fortifications", + ["canCargo"] = false, + ["shape_name"] = "H-tyre_B_WF", + ["type"] = "Black_Tyre_WF", + --["unitId"] = Angle + 10000, + ["y"] = Point.y, + ["x"] = Point.x, + ["name"] = string.format( "%s-Tire #%0d", self:GetName(), Angle ), + ["heading"] = 0, + } -- end of ["group"] + + coalition.addStaticObject( country.id.USA, Tire ) + end + + return self +end + + +--- Smokes the zone boundaries in a color. +-- @param #ZONE_RADIUS self +-- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color. +-- @param #number Points (optional) The amount of points in the circle. +-- @return #ZONE_RADIUS self +function ZONE_RADIUS:SmokeZone( SmokeColor, Points ) + self:F2( SmokeColor ) + + local Point = {} + local Vec2 = self:GetVec2() + + Points = Points and Points or 360 + + local Angle + local RadialBase = math.pi*2 + + for Angle = 0, 360, 360 / Points do + local Radial = Angle * RadialBase / 360 + Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() + Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() + 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 Utilities.Utils#FLARECOLOR FlareColor The flare color. +-- @param #number Points (optional) The amount of points in the circle. +-- @param Dcs.DCSTypes#Azimuth Azimuth (optional) Azimuth The azimuth of the flare. +-- @return #ZONE_RADIUS self +function ZONE_RADIUS:FlareZone( FlareColor, Points, Azimuth ) + self:F2( { FlareColor, Azimuth } ) + + local Point = {} + local Vec2 = self:GetVec2() + + Points = Points and Points or 360 + + local Angle + local RadialBase = math.pi*2 + + for Angle = 0, 360, 360 / Points do + local Radial = Angle * RadialBase / 360 + Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() + Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() + 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 Dcs.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 Dcs.DCSTypes#Distance Radius The radius of the zone. +-- @return Dcs.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 @{DCSTypes#Vec2} of the zone. +-- @param #ZONE_RADIUS self +-- @return Dcs.DCSTypes#Vec2 The location of the zone. +function ZONE_RADIUS:GetVec2() + self:F2( self.ZoneName ) + + self:T2( { self.Vec2 } ) + + return self.Vec2 +end + +--- Sets the @{DCSTypes#Vec2} of the zone. +-- @param #ZONE_RADIUS self +-- @param Dcs.DCSTypes#Vec2 Vec2 The new location of the zone. +-- @return Dcs.DCSTypes#Vec2 The new location of the zone. +function ZONE_RADIUS:SetVec2( Vec2 ) + self:F2( self.ZoneName ) + + self.Vec2 = Vec2 + + self:T2( { self.Vec2 } ) + + return self.Vec2 +end + +--- Returns the @{DCSTypes#Vec3} of the ZONE_RADIUS. +-- @param #ZONE_RADIUS self +-- @param Dcs.DCSTypes#Distance Height The height to add to the land height where the center of the zone is located. +-- @return Dcs.DCSTypes#Vec3 The point of the zone. +function ZONE_RADIUS:GetVec3( Height ) + self:F2( { self.ZoneName, Height } ) + + Height = Height or 0 + local Vec2 = self:GetVec2() + + local Vec3 = { x = Vec2.x, y = land.getHeight( self:GetVec2() ) + Height, z = Vec2.y } + + self:T2( { Vec3 } ) + + return Vec3 +end + + +--- Returns if a location is within the zone. +-- @param #ZONE_RADIUS self +-- @param Dcs.DCSTypes#Vec2 Vec2 The location to test. +-- @return #boolean true if the location is within the zone. +function ZONE_RADIUS:IsVec2InZone( Vec2 ) + self:F2( Vec2 ) + + local ZoneVec2 = self:GetVec2() + + if ZoneVec2 then + if (( Vec2.x - ZoneVec2.x )^2 + ( Vec2.y - ZoneVec2.y ) ^2 ) ^ 0.5 <= self:GetRadius() then + return true + end + end + + return false +end + +--- Returns if a point is within the zone. +-- @param #ZONE_RADIUS self +-- @param Dcs.DCSTypes#Vec3 Vec3 The point to test. +-- @return #boolean true if the point is within the zone. +function ZONE_RADIUS:IsVec3InZone( Vec3 ) + self:F2( Vec3 ) + + local InZone = self:IsVec2InZone( { x = Vec3.x, y = Vec3.z } ) + + return InZone +end + +--- Returns a random Vec2 location within the zone. +-- @param #ZONE_RADIUS self +-- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. +-- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. +-- @return Dcs.DCSTypes#Vec2 The random location within the zone. +function ZONE_RADIUS:GetRandomVec2( inner, outer ) + self:F( self.ZoneName, inner, outer ) + + local Point = {} + local Vec2 = self:GetVec2() + local _inner = inner or 0 + local _outer = outer or self:GetRadius() + + local angle = math.random() * math.pi * 2; + Point.x = Vec2.x + math.cos( angle ) * math.random(_inner, _outer); + Point.y = Vec2.y + math.sin( angle ) * math.random(_inner, _outer); + + self:T( { Point } ) + + return Point +end + +--- Returns a @{Point#POINT_VEC2} object reflecting a random 2D location within the zone. +-- @param #ZONE_RADIUS self +-- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. +-- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. +-- @return Core.Point#POINT_VEC2 The @{Point#POINT_VEC2} object reflecting the random 3D location within the zone. +function ZONE_RADIUS:GetRandomPointVec2( inner, outer ) + self:F( self.ZoneName, inner, outer ) + + local PointVec2 = POINT_VEC2:NewFromVec2( self:GetRandomVec2() ) + + self:T3( { PointVec2 } ) + + return PointVec2 +end + +--- Returns a @{Point#POINT_VEC3} object reflecting a random 3D location within the zone. +-- @param #ZONE_RADIUS self +-- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. +-- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. +-- @return Core.Point#POINT_VEC3 The @{Point#POINT_VEC3} object reflecting the random 3D location within the zone. +function ZONE_RADIUS:GetRandomPointVec3( inner, outer ) + self:F( self.ZoneName, inner, outer ) + + local PointVec3 = POINT_VEC3:NewFromVec2( self:GetRandomVec2() ) + + self:T3( { PointVec3 } ) + + return PointVec3 +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 Core.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 Wrapper.Unit#UNIT ZoneUNIT +-- @extends Core.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 Wrapper.Unit#UNIT ZoneUNIT The unit as the center of the zone. +-- @param Dcs.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:GetVec2(), Radius ) ) + self:F( { ZoneName, ZoneUNIT:GetVec2(), Radius } ) + + self.ZoneUNIT = ZoneUNIT + self.LastVec2 = ZoneUNIT:GetVec2() + + return self +end + + +--- Returns the current location of the @{Unit#UNIT}. +-- @param #ZONE_UNIT self +-- @return Dcs.DCSTypes#Vec2 The location of the zone based on the @{Unit#UNIT}location. +function ZONE_UNIT:GetVec2() + self:F( self.ZoneName ) + + local ZoneVec2 = self.ZoneUNIT:GetVec2() + if ZoneVec2 then + self.LastVec2 = ZoneVec2 + return ZoneVec2 + else + return self.LastVec2 + end + + self:T( { ZoneVec2 } ) + + return nil +end + +--- Returns a random location within the zone. +-- @param #ZONE_UNIT self +-- @return Dcs.DCSTypes#Vec2 The random location within the zone. +function ZONE_UNIT:GetRandomVec2() + self:F( self.ZoneName ) + + local RandomVec2 = {} + local Vec2 = self.ZoneUNIT:GetVec2() + + if not Vec2 then + Vec2 = self.LastVec2 + end + + local angle = math.random() * math.pi*2; + RandomVec2.x = Vec2.x + math.cos( angle ) * math.random() * self:GetRadius(); + RandomVec2.y = Vec2.y + math.sin( angle ) * math.random() * self:GetRadius(); + + self:T( { RandomVec2 } ) + + return RandomVec2 +end + +--- Returns the @{DCSTypes#Vec3} of the ZONE_UNIT. +-- @param #ZONE_UNIT self +-- @param Dcs.DCSTypes#Distance Height The height to add to the land height where the center of the zone is located. +-- @return Dcs.DCSTypes#Vec3 The point of the zone. +function ZONE_UNIT:GetVec3( Height ) + self:F2( self.ZoneName ) + + Height = Height or 0 + + local Vec2 = self:GetVec2() + + local Vec3 = { x = Vec2.x, y = land.getHeight( self:GetVec2() ) + Height, z = Vec2.y } + + self:T2( { Vec3 } ) + + return Vec3 +end + +--- The ZONE_GROUP class defined by a zone around a @{Group}, taking the average center point of all the units within the Group, with a radius. +-- @type ZONE_GROUP +-- @field Wrapper.Group#GROUP ZoneGROUP +-- @extends Core.Zone#ZONE_RADIUS +ZONE_GROUP = { + ClassName="ZONE_GROUP", + } + +--- Constructor to create a ZONE_GROUP instance, taking the zone name, a zone @{Group#GROUP} and a radius. +-- @param #ZONE_GROUP self +-- @param #string ZoneName Name of the zone. +-- @param Wrapper.Group#GROUP ZoneGROUP The @{Group} as the center of the zone. +-- @param Dcs.DCSTypes#Distance Radius The radius of the zone. +-- @return #ZONE_GROUP self +function ZONE_GROUP:New( ZoneName, ZoneGROUP, Radius ) + local self = BASE:Inherit( self, ZONE_RADIUS:New( ZoneName, ZoneGROUP:GetVec2(), Radius ) ) + self:F( { ZoneName, ZoneGROUP:GetVec2(), Radius } ) + + self.ZoneGROUP = ZoneGROUP + + return self +end + + +--- Returns the current location of the @{Group}. +-- @param #ZONE_GROUP self +-- @return Dcs.DCSTypes#Vec2 The location of the zone based on the @{Group} location. +function ZONE_GROUP:GetVec2() + self:F( self.ZoneName ) + + local ZoneVec2 = self.ZoneGROUP:GetVec2() + + self:T( { ZoneVec2 } ) + + return ZoneVec2 +end + +--- Returns a random location within the zone of the @{Group}. +-- @param #ZONE_GROUP self +-- @return Dcs.DCSTypes#Vec2 The random location of the zone based on the @{Group} location. +function ZONE_GROUP:GetRandomVec2() + self:F( self.ZoneName ) + + local Point = {} + local Vec2 = self.ZoneGROUP:GetVec2() + + local angle = math.random() * math.pi*2; + Point.x = Vec2.x + math.cos( angle ) * math.random() * self:GetRadius(); + Point.y = Vec2.y + math.sin( angle ) * math.random() * self:GetRadius(); + + self:T( { Point } ) + + return Point +end + + + +-- 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 Core.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 +-- @return #ZONE_POLYGON_BASE self +function ZONE_POLYGON_BASE:BoundZone( ) + + local i + local j + local Segments = 10 + + i = 1 + j = #self.Polygon + + while i <= #self.Polygon do + self:T( { i, j, self.Polygon[i], self.Polygon[j] } ) + + local DeltaX = self.Polygon[j].x - self.Polygon[i].x + local DeltaY = self.Polygon[j].y - self.Polygon[i].y + + for Segment = 0, Segments do -- We divide each line in 5 segments and smoke a point on the line. + local PointX = self.Polygon[i].x + ( Segment * DeltaX / Segments ) + local PointY = self.Polygon[i].y + ( Segment * DeltaY / Segments ) + local Tire = { + ["country"] = "USA", + ["category"] = "Fortifications", + ["canCargo"] = false, + ["shape_name"] = "H-tyre_B_WF", + ["type"] = "Black_Tyre_WF", + ["y"] = PointY, + ["x"] = PointX, + ["name"] = string.format( "%s-Tire #%0d", self:GetName(), ((i - 1) * Segments) + Segment ), + ["heading"] = 0, + } -- end of ["group"] + + coalition.addStaticObject( country.id.USA, Tire ) + + end + j = i + i = i + 1 + end + + return self +end + + + +--- Smokes the zone boundaries in a color. +-- @param #ZONE_POLYGON_BASE self +-- @param Utilities.Utils#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. +-- Source learned and taken from: https://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html +-- @param #ZONE_POLYGON_BASE self +-- @param Dcs.DCSTypes#Vec2 Vec2 The location to test. +-- @return #boolean true if the location is within the zone. +function ZONE_POLYGON_BASE:IsVec2InZone( Vec2 ) + self:F2( Vec2 ) + + local Next + local Prev + local InPolygon = false + + Next = 1 + Prev = #self.Polygon + + while Next <= #self.Polygon do + self:T( { Next, Prev, self.Polygon[Next], self.Polygon[Prev] } ) + if ( ( ( self.Polygon[Next].y > Vec2.y ) ~= ( self.Polygon[Prev].y > Vec2.y ) ) and + ( Vec2.x < ( self.Polygon[Prev].x - self.Polygon[Next].x ) * ( Vec2.y - self.Polygon[Next].y ) / ( self.Polygon[Prev].y - self.Polygon[Next].y ) + self.Polygon[Next].x ) + ) then + InPolygon = not InPolygon + end + self:T2( { InPolygon = InPolygon } ) + Prev = Next + Next = Next + 1 + end + + self:T( { InPolygon = InPolygon } ) + return InPolygon +end + +--- Define a random @{DCSTypes#Vec2} within the zone. +-- @param #ZONE_POLYGON_BASE self +-- @return Dcs.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:IsVec2InZone( Vec2 ) then + Vec2Found = true + end + end + + self:T2( Vec2 ) + + return Vec2 +end + +--- Return a @{Point#POINT_VEC2} object representing a random 2D point at landheight within the zone. +-- @param #ZONE_POLYGON_BASE self +-- @return @{Point#POINT_VEC2} +function ZONE_POLYGON_BASE:GetRandomPointVec2() + self:F2() + + local PointVec2 = POINT_VEC2:NewFromVec2( self:GetRandomVec2() ) + + self:T2( PointVec2 ) + + return PointVec2 +end + +--- Return a @{Point#POINT_VEC3} object representing a random 3D point at landheight within the zone. +-- @param #ZONE_POLYGON_BASE self +-- @return @{Point#POINT_VEC3} +function ZONE_POLYGON_BASE:GetRandomPointVec3() + self:F2() + + local PointVec3 = POINT_VEC3:NewFromVec2( self:GetRandomVec2() ) + + self:T2( PointVec3 ) + + return PointVec3 +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 Core.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 Wrapper.Group#GROUP ZoneGroup The GROUP waypoints as defined within the Mission Editor define the polygon shape. +-- @return #ZONE_POLYGON self +function ZONE_POLYGON:New( ZoneName, ZoneGroup ) + + local GroupPoints = ZoneGroup:GetTaskRoute() + + local self = BASE:Inherit( self, ZONE_POLYGON_BASE:New( ZoneName, GroupPoints ) ) + self:F( { ZoneName, ZoneGroup, self.Polygon } ) + + return self +end + +--- This module contains the DATABASE class, managing the database of mission objects. +-- +-- ==== +-- +-- 1) @{#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 Core.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() ) + + self:SetEventPriority( 1 ) + + self:HandleEvent( EVENTS.Birth, self._EventOnBirth ) + self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) + self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) + + -- Follow alive players and clients + self:HandleEvent( EVENTS.PlayerEnterUnit, self._EventOnPlayerEnterUnit ) + self:HandleEvent( EVENTS.PlayerLeaveUnit, self._EventOnPlayerLeaveUnit ) + + 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 Wrapper.Unit#UNIT The found Unit. +function DATABASE:FindUnit( UnitName ) + + local UnitFound = self.UNITS[UnitName] + return UnitFound +end + + +--- Adds a Unit based on the Unit Name in the DATABASE. +-- @param #DATABASE self +function DATABASE:AddUnit( DCSUnitName ) + + if not self.UNITS[DCSUnitName] then + local UnitRegister = UNIT:Register( DCSUnitName ) + 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 Wrapper.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 Wrapper.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 Wrapper.Client#CLIENT The found CLIENT. +function DATABASE:FindClient( ClientName ) + + local ClientFound = self.CLIENTS[ClientName] + return ClientFound +end + + +--- Adds a CLIENT based on the ClientName in the DATABASE. +-- @param #DATABASE self +function DATABASE:AddClient( ClientName ) + + if not self.CLIENTS[ClientName] then + self.CLIENTS[ClientName] = CLIENT:Register( ClientName ) + end + + return self.CLIENTS[ClientName] +end + + +--- Finds a GROUP based on the GroupName. +-- @param #DATABASE self +-- @param #string GroupName +-- @return Wrapper.Group#GROUP The found GROUP. +function DATABASE:FindGroup( GroupName ) + + local GroupFound = self.GROUPS[GroupName] + return GroupFound +end + + +--- Adds a GROUP based on the GroupName in the DATABASE. +-- @param #DATABASE self +function DATABASE:AddGroup( GroupName ) + + if not self.GROUPS[GroupName] then + self:E( { "Add GROUP:", GroupName } ) + self.GROUPS[GroupName] = GROUP:Register( GroupName ) + end + + return self.GROUPS[GroupName] +end + +--- Adds a player based on the Player Name in the DATABASE. +-- @param #DATABASE self +function DATABASE:AddPlayer( UnitName, PlayerName ) + + if PlayerName then + self: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:F( SpawnTemplate.name ) + + self:T( { SpawnTemplate.SpawnCountryID, SpawnTemplate.SpawnCategoryID } ) + + -- Copy the spawn variables of the template in temporary storage, nullify, and restore the spawn variables. + local SpawnCoalitionID = SpawnTemplate.CoalitionID + local SpawnCountryID = SpawnTemplate.CountryID + local SpawnCategoryID = SpawnTemplate.CategoryID + + -- Nullify + SpawnTemplate.CoalitionID = nil + SpawnTemplate.CountryID = nil + SpawnTemplate.CategoryID = nil + + self:_RegisterTemplate( SpawnTemplate, SpawnCoalitionID, SpawnCategoryID, SpawnCountryID ) + + self:T3( SpawnTemplate ) + coalition.addGroup( SpawnCountryID, SpawnCategoryID, SpawnTemplate ) + + -- Restore + SpawnTemplate.CoalitionID = SpawnCoalitionID + SpawnTemplate.CountryID = SpawnCountryID + SpawnTemplate.CategoryID = 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 + + GroupTemplate.CategoryID = CategoryID + GroupTemplate.CoalitionID = CoalitionID + GroupTemplate.CountryID = CountryID + + self.Templates.Groups[GroupTemplateName].GroupName = GroupTemplateName + self.Templates.Groups[GroupTemplateName].Template = GroupTemplate + self.Templates.Groups[GroupTemplateName].groupId = GroupTemplate.groupId + self.Templates.Groups[GroupTemplateName].UnitCount = #GroupTemplate.units + self.Templates.Groups[GroupTemplateName].Units = GroupTemplate.units + self.Templates.Groups[GroupTemplateName].CategoryID = CategoryID + self.Templates.Groups[GroupTemplateName].CoalitionID = 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 + + UnitTemplate.name = env.getValueDictByKey(UnitTemplate.name) + + self.Templates.Units[UnitTemplate.name] = {} + self.Templates.Units[UnitTemplate.name].UnitName = UnitTemplate.name + self.Templates.Units[UnitTemplate.name].Template = UnitTemplate + self.Templates.Units[UnitTemplate.name].GroupName = GroupTemplateName + self.Templates.Units[UnitTemplate.name].GroupTemplate = GroupTemplate + self.Templates.Units[UnitTemplate.name].GroupId = GroupTemplate.groupId + self.Templates.Units[UnitTemplate.name].CategoryID = CategoryID + self.Templates.Units[UnitTemplate.name].CoalitionID = CoalitionID + self.Templates.Units[UnitTemplate.name].CountryID = CountryID + + if UnitTemplate.skill and (UnitTemplate.skill == "Client" or UnitTemplate.skill == "Player") then + self.Templates.ClientsByName[UnitTemplate.name] = UnitTemplate + self.Templates.ClientsByName[UnitTemplate.name].CategoryID = CategoryID + self.Templates.ClientsByName[UnitTemplate.name].CoalitionID = CoalitionID + self.Templates.ClientsByName[UnitTemplate.name].CountryID = CountryID + self.Templates.ClientsByID[UnitTemplate.unitId] = UnitTemplate + end + + TraceTable[#TraceTable+1] = self.Templates.Units[UnitTemplate.name].UnitName + end + + self:E( TraceTable ) +end + +function DATABASE:GetGroupTemplate( GroupName ) + local GroupTemplate = self.Templates.Groups[GroupName].Template + GroupTemplate.SpawnCoalitionID = self.Templates.Groups[GroupName].CoalitionID + GroupTemplate.SpawnCategoryID = self.Templates.Groups[GroupName].CategoryID + GroupTemplate.SpawnCountryID = self.Templates.Groups[GroupName].CountryID + return GroupTemplate +end + +function DATABASE:GetGroupNameFromUnitName( UnitName ) + return self.Templates.Units[UnitName].GroupName +end + +function DATABASE:GetGroupTemplateFromUnitName( UnitName ) + return self.Templates.Units[UnitName].GroupTemplate +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 Core.Event#EVENTDATA Event +function DATABASE:_EventOnBirth( Event ) + self:F2( { Event } ) + + if Event.IniDCSUnit then + if Event.IniObjectCategory == 3 then + self:AddStatic( Event.IniDCSUnitName ) + else + if Event.IniObjectCategory == 1 then + self:AddUnit( Event.IniDCSUnitName ) + self:AddGroup( Event.IniDCSGroupName ) + end + end + self:_EventOnPlayerEnterUnit( Event ) + end +end + + +--- Handles the OnDead or OnCrash event for alive units set. +-- @param #DATABASE self +-- @param Core.Event#EVENTDATA Event +function DATABASE:_EventOnDeadOrCrash( Event ) + self:F2( { Event } ) + + if Event.IniDCSUnit then + if Event.IniObjectCategory == 3 then + if self.STATICS[Event.IniDCSUnitName] then + self:DeleteStatic( Event.IniDCSUnitName ) + end + else + if Event.IniObjectCategory == 1 then + if self.UNITS[Event.IniDCSUnitName] then + self:DeleteUnit( Event.IniDCSUnitName ) + end + end + end + end +end + + +--- Handles the OnPlayerEnterUnit event to fill the active players table (with the unit filter applied). +-- @param #DATABASE self +-- @param Core.Event#EVENTDATA Event +function DATABASE:_EventOnPlayerEnterUnit( Event ) + self:F2( { Event } ) + + if Event.IniUnit then + if Event.IniObjectCategory == 1 then + self:AddUnit( Event.IniDCSUnitName ) + self:AddGroup( Event.IniDCSGroupName ) + local PlayerName = Event.IniUnit:GetPlayerName() + if not self.PLAYERS[PlayerName] then + self:AddPlayer( Event.IniUnitName, PlayerName ) + end + end + end +end + + +--- Handles the OnPlayerLeaveUnit event to clean the active players table. +-- @param #DATABASE self +-- @param Core.Event#EVENTDATA Event +function DATABASE:_EventOnPlayerLeaveUnit( Event ) + self:F2( { Event } ) + + if Event.IniUnit then + if Event.IniObjectCategory == 1 then + local PlayerName = Event.IniUnit:GetPlayerName() + if self.PLAYERS[PlayerName] then + self:DeletePlayer( PlayerName ) + end + end + end +end + +--- Iterators + +--- Iterate the DATABASE and call an iterator function for the given set, providing the Object for each element within the set and optional parameters. +-- @param #DATABASE self +-- @param #function IteratorFunction The function that will be called when there is an alive player in the database. +-- @return #DATABASE self +function DATABASE:ForEach( IteratorFunction, FinalizeFunction, arg, Set ) + self:F2( arg ) + + local function CoRoutine() + local Count = 0 + for ObjectID, Object in pairs( Set ) do + self:T2( Object ) + IteratorFunction( Object, unpack( arg ) ) + Count = Count + 1 +-- if Count % 100 == 0 then +-- coroutine.yield( false ) +-- end + end + return true + end + +-- local co = coroutine.create( CoRoutine ) + local co = CoRoutine + + local function Schedule() + +-- local status, res = coroutine.resume( co ) + local status, res = co() + self:T3( { status, res } ) + + if status == false then + error( res ) + end + if res == false then + return true -- resume next time the loop + end + if FinalizeFunction then + FinalizeFunction( unpack( arg ) ) + end + return false + end + + local Scheduler = SCHEDULER:New( self, Schedule, {}, 0.001, 0.001, 0 ) + + 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] = {} + + local CoalitionSide = coalition.side[string.upper(CoalitionName)] + + ---------------------------------------------- + -- build nav points DB + self.Navpoints[CoalitionName] = {} + if coa_data.nav_points then --navpoints + for nav_ind, nav_data in pairs(coa_data.nav_points) do + + if type(nav_data) == 'table' then + self.Navpoints[CoalitionName][nav_ind] = routines.utils.deepCopy(nav_data) + + self.Navpoints[CoalitionName][nav_ind]['name'] = nav_data.callsignStr -- name is a little bit more self-explanatory. + self.Navpoints[CoalitionName][nav_ind]['point'] = {} -- point is used by SSE, support it. + self.Navpoints[CoalitionName][nav_ind]['point']['x'] = nav_data.x + self.Navpoints[CoalitionName][nav_ind]['point']['y'] = 0 + self.Navpoints[CoalitionName][nav_ind]['point']['z'] = nav_data.y + end + end + end + ------------------------------------------------- + if coa_data.country then --there is a country table + for cntry_id, cntry_data in pairs(coa_data.country) do + + local CountryName = string.upper(cntry_data.name) + local CountryID = cntry_data.id + + --self.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, + CoalitionSide, + _DATABASECategory[string.lower(CategoryName)], + CountryID + ) + end --if GroupTemplate and GroupTemplate.units then + end --for group_num, GroupTemplate in pairs(obj_type_data.group) do + end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then + end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then + end --for obj_type_name, obj_type_data in pairs(cntry_data) do + end --if type(cntry_data) == 'table' then + end --for cntry_id, cntry_data in pairs(coa_data.country) do + end --if coa_data.country then --there is a country table + end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then + end --for coa_name, coa_data in pairs(mission.coalition) do + + return self +end + + + + +--- 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. +-- +-- ==== +-- +-- ### Authors: +-- +-- * FlightControl : Design & Programming +-- +-- ### Contributions: +-- +-- +-- @module Set + + +--- SET_BASE class +-- @type SET_BASE +-- @field #table Filter +-- @field #table Set +-- @field #table List +-- @field Core.Scheduler#SCHEDULER CallScheduler +-- @extends Core.Base#BASE +SET_BASE = { + ClassName = "SET_BASE", + Filter = {}, + Set = {}, + List = {}, +} + +--- Creates a new SET_BASE object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. +-- @param #SET_BASE self +-- @return #SET_BASE +-- @usage +-- -- Define a new SET_BASE Object. This DBObject will contain a reference to all Group and Unit Templates defined within the ME and the DCSRTE. +-- DBObject = SET_BASE:New() +function SET_BASE:New( Database ) + + -- Inherits from BASE + local self = BASE:Inherit( self, BASE:New() ) -- Core.Set#SET_BASE + + self.Database = Database + + self.YieldInterval = 10 + self.TimeInterval = 0.001 + + self.List = {} + self.List.__index = self.List + self.List = setmetatable( { Count = 0 }, self.List ) + + self.CallScheduler = SCHEDULER:New( self ) + + self:SetEventPriority( 2 ) + + return self +end + +--- Finds an @{Base#BASE} object based on the object Name. +-- @param #SET_BASE self +-- @param #string ObjectName +-- @return Core.Base#BASE The Object found. +function SET_BASE:_Find( ObjectName ) + + local ObjectFound = self.Set[ObjectName] + return ObjectFound +end + + +--- Gets the Set. +-- @param #SET_BASE self +-- @return #SET_BASE self +function SET_BASE:GetSet() + self:F2() + + return self.Set +end + +--- Adds a @{Base#BASE} object in the @{Set#SET_BASE}, using a given ObjectName as the index. +-- @param #SET_BASE self +-- @param #string ObjectName +-- @param Core.Base#BASE Object +-- @return Core.Base#BASE The added BASE Object. +function SET_BASE:Add( ObjectName, Object ) + self:F2( ObjectName ) + + local t = { _ = Object } + + if self.List.last then + self.List.last._next = t + t._prev = self.List.last + self.List.last = t + else + -- this is the first node + self.List.first = t + self.List.last = t + end + + self.List.Count = self.List.Count + 1 + + self.Set[ObjectName] = t._ + +end + +--- Adds a @{Base#BASE} object in the @{Set#SET_BASE}, using the Object Name as the index. +-- @param #SET_BASE self +-- @param Wrapper.Object#OBJECT Object +-- @return Core.Base#BASE The added BASE Object. +function SET_BASE:AddObject( Object ) + self:F2( Object.ObjectName ) + + self:T( Object.UnitName ) + self:T( Object.ObjectName ) + self:Add( Object.ObjectName, Object ) + +end + + + +--- 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:F( ObjectName ) + + local t = self.Set[ObjectName] + + self:E( { ObjectName, t } ) + + if t then + if t._next then + if t._prev then + t._next._prev = t._prev + t._prev._next = t._next + else + -- this was the first node + t._next._prev = nil + self.List._first = t._next + end + elseif t._prev then + -- this was the last node + t._prev._next = nil + self.List._last = t._prev + else + -- this was the only node + self.List._first = nil + self.List._last = nil + end + + t._next = nil + t._prev = nil + self.List.Count = self.List.Count - 1 + + self.Set[ObjectName] = nil + end + +end + +--- Gets a @{Base#BASE} object from the @{Set#SET_BASE} and derived classes, based on the Object Name. +-- @param #SET_BASE self +-- @param #string ObjectName +-- @return Core.Base#BASE +function SET_BASE:Get( ObjectName ) + self:F( ObjectName ) + + local t = self.Set[ObjectName] + + self:T3( { ObjectName, t } ) + + return t + +end + +--- Retrieves the amount of objects in the @{Set#SET_BASE} and derived classes. +-- @param #SET_BASE self +-- @return #number Count +function SET_BASE:Count() + + return self.List.Count +end + + + +--- Copies the Filter criteria from a given Set (for rebuilding a new Set based on an existing Set). +-- @param #SET_BASE self +-- @param #SET_BASE BaseSet +-- @return #SET_BASE +function SET_BASE:SetDatabase( BaseSet ) + + -- Copy the filter criteria of the BaseSet + local OtherFilter = routines.utils.deepCopy( BaseSet.Filter ) + self.Filter = OtherFilter + + -- Now base the new Set on the BaseSet + self.Database = BaseSet:GetSet() + return self +end + + + +--- Define the SET iterator **"yield interval"** and the **"time interval"**. +-- @param #SET_BASE self +-- @param #number YieldInterval Sets the frequency when the iterator loop will yield after the number of objects processed. The default frequency is 10 objects processed. +-- @param #number TimeInterval Sets the time in seconds when the main logic will resume the iterator loop. The default time is 0.001 seconds. +-- @return #SET_BASE self +function SET_BASE:SetIteratorIntervals( YieldInterval, TimeInterval ) + + self.YieldInterval = YieldInterval + self.TimeInterval = TimeInterval + + return self +end + + +--- Filters for the defined collection. +-- @param #SET_BASE self +-- @return #SET_BASE self +function SET_BASE:FilterOnce() + + for ObjectName, Object in pairs( self.Database ) do + + if self:IsIncludeObject( Object ) then + self:Add( ObjectName, Object ) + end + end + + return self +end + +--- Starts the filtering for the defined collection. +-- @param #SET_BASE self +-- @return #SET_BASE self +function SET_BASE:_FilterStart() + + for ObjectName, Object in pairs( self.Database ) do + + if self:IsIncludeObject( Object ) then + self:E( { "Adding Object:", ObjectName } ) + self:Add( ObjectName, Object ) + end + end + + self:HandleEvent( EVENTS.Birth, self._EventOnBirth ) + self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) + self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) + + -- Follow alive players and clients + self:HandleEvent( EVENTS.PlayerEnterUnit, self._EventOnPlayerEnterUnit ) + self:HandleEvent( EVENTS.PlayerLeaveUnit, self._EventOnPlayerLeaveUnit ) + + + return self +end + +--- Stops the filtering for the defined collection. +-- @param #SET_BASE self +-- @return #SET_BASE self +function SET_BASE:FilterStop() + + self:UnHandleEvent( EVENTS.Birth ) + self:UnHandleEvent( EVENTS.Dead ) + self:UnHandleEvent( EVENTS.Crash ) + + return self +end + +--- Iterate the SET_BASE while identifying the nearest object from a @{Point#POINT_VEC2}. +-- @param #SET_BASE self +-- @param Core.Point#POINT_VEC2 PointVec2 A @{Point#POINT_VEC2} object from where to evaluate the closest object in the set. +-- @return Core.Base#BASE The closest object. +function SET_BASE:FindNearestObjectFromPointVec2( PointVec2 ) + self:F2( PointVec2 ) + + local NearestObject = nil + local ClosestDistance = nil + + for ObjectID, ObjectData in pairs( self.Set ) do + if NearestObject == nil then + NearestObject = ObjectData + ClosestDistance = PointVec2:DistanceFromVec2( ObjectData:GetVec2() ) + else + local Distance = PointVec2:DistanceFromVec2( ObjectData:GetVec2() ) + if Distance < ClosestDistance then + NearestObject = ObjectData + ClosestDistance = Distance + end + end + end + + return NearestObject +end + + + +----- Private method that registers all alive players in the mission. +---- @param #SET_BASE self +---- @return #SET_BASE self +--function SET_BASE:_RegisterPlayers() +-- +-- local CoalitionsData = { AlivePlayersRed = coalition.getPlayers( coalition.side.RED ), AlivePlayersBlue = coalition.getPlayers( coalition.side.BLUE ) } +-- for CoalitionId, CoalitionData in pairs( CoalitionsData ) do +-- for UnitId, UnitData in pairs( CoalitionData ) do +-- self:T3( { "UnitData:", UnitData } ) +-- if UnitData and UnitData:isExist() then +-- local UnitName = UnitData:getName() +-- if not self.PlayersAlive[UnitName] then +-- self:E( { "Add player for unit:", UnitName, UnitData:getPlayerName() } ) +-- self.PlayersAlive[UnitName] = UnitData:getPlayerName() +-- end +-- end +-- end +-- end +-- +-- return self +--end + +--- Events + +--- Handles the OnBirth event for the Set. +-- @param #SET_BASE self +-- @param Core.Event#EVENTDATA Event +function SET_BASE:_EventOnBirth( Event ) + self:F3( { Event } ) + + if Event.IniDCSUnit then + local ObjectName, Object = self:AddInDatabase( Event ) + self:T3( ObjectName, Object ) + if Object and self:IsIncludeObject( Object ) then + self:Add( ObjectName, Object ) + --self:_EventOnPlayerEnterUnit( Event ) + end + end +end + +--- Handles the OnDead or OnCrash event for alive units set. +-- @param #SET_BASE self +-- @param Core.Event#EVENTDATA Event +function SET_BASE:_EventOnDeadOrCrash( Event ) + self:F3( { Event } ) + + if Event.IniDCSUnit then + local ObjectName, Object = self:FindInDatabase( Event ) + if ObjectName and Object ~= nil then + self:Remove( ObjectName ) + end + end +end + +--- Handles the OnPlayerEnterUnit event to fill the active players table (with the unit filter applied). +-- @param #SET_BASE self +-- @param Core.Event#EVENTDATA Event +function SET_BASE:_EventOnPlayerEnterUnit( Event ) + self:F3( { Event } ) + + if Event.IniDCSUnit then + local ObjectName, Object = self:AddInDatabase( Event ) + self:T3( ObjectName, Object ) + if self:IsIncludeObject( Object ) then + self:Add( ObjectName, Object ) + --self:_EventOnPlayerEnterUnit( Event ) + end + end +end + +--- Handles the OnPlayerLeaveUnit event to clean the active players table. +-- @param #SET_BASE self +-- @param Core.Event#EVENTDATA Event +function SET_BASE:_EventOnPlayerLeaveUnit( Event ) + self:F3( { Event } ) + + local ObjectName = Event.IniDCSUnit + if Event.IniDCSUnit then + if Event.IniDCSGroup then + local GroupUnits = Event.IniDCSGroup:getUnits() + local PlayerCount = 0 + for _, DCSUnit in pairs( GroupUnits ) do + if DCSUnit ~= Event.IniDCSUnit then + if DCSUnit:getPlayer() ~= nil then + PlayerCount = PlayerCount + 1 + end + end + end + self:E(PlayerCount) + if PlayerCount == 0 then + self:Remove( Event.IniDCSGroupName ) + end + end + end +end + +-- Iterators + +--- Iterate the SET_BASE and derived classes and call an iterator function for the given SET_BASE, providing the Object for each element within the set and optional parameters. +-- @param #SET_BASE self +-- @param #function IteratorFunction The function that will be called. +-- @return #SET_BASE self +function SET_BASE:ForEach( IteratorFunction, arg, Set, Function, FunctionArguments ) + self:F3( arg ) + + Set = Set or self:GetSet() + arg = arg or {} + + local function CoRoutine() + local Count = 0 + for ObjectID, ObjectData in pairs( Set ) do + local Object = ObjectData + self:T3( Object ) + if Function then + if Function( unpack( FunctionArguments ), Object ) == true then + IteratorFunction( Object, unpack( arg ) ) + end + else + IteratorFunction( Object, unpack( arg ) ) + end + Count = Count + 1 +-- if Count % self.YieldInterval == 0 then +-- coroutine.yield( false ) +-- end + end + return true + end + +-- local co = coroutine.create( CoRoutine ) + local co = CoRoutine + + local function Schedule() + +-- local status, res = coroutine.resume( co ) + local status, res = co() + self:T3( { status, res } ) + + if status == false then + error( res ) + end + if res == false then + return true -- resume next time the loop + end + + return false + end + + self.CallScheduler:Schedule( self, Schedule, {}, self.TimeInterval, self.TimeInterval, 0 ) + + 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 debugging 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:E( { "Objects in Set:", ObjectNames } ) + + return ObjectNames +end + +-- SET_GROUP + +--- SET_GROUP class +-- @type SET_GROUP +-- @extends #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 Core.Set#SET_GROUP self +-- @param #string AddGroupNames A single name or an array of GROUP names. +-- @return self +function SET_GROUP:AddGroupsByName( AddGroupNames ) + + local AddGroupNamesArray = ( type( AddGroupNames ) == "table" ) and AddGroupNames or { AddGroupNames } + + for AddGroupID, AddGroupName in pairs( AddGroupNamesArray ) do + self:Add( AddGroupName, GROUP:FindByName( AddGroupName ) ) + end + + return self +end + +--- Remove GROUP(s) from SET_GROUP. +-- @param Core.Set#SET_GROUP self +-- @param Wrapper.Group#GROUP RemoveGroupNames A single name or an array of GROUP names. +-- @return self +function SET_GROUP:RemoveGroupsByName( RemoveGroupNames ) + + local RemoveGroupNamesArray = ( type( RemoveGroupNames ) == "table" ) and RemoveGroupNames or { RemoveGroupNames } + + for RemoveGroupID, RemoveGroupName in pairs( RemoveGroupNamesArray ) do + self:Remove( RemoveGroupName.GroupName ) + end + + return self +end + + + + +--- Finds a Group based on the Group Name. +-- @param #SET_GROUP self +-- @param #string GroupName +-- @return Wrapper.Group#GROUP The found Group. +function SET_GROUP:FindGroup( GroupName ) + + local GroupFound = self.Set[GroupName] + return GroupFound +end + + + +--- 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 Core.Event#EVENTDATA Event +-- @return #string The name of the GROUP +-- @return #table The GROUP +function SET_GROUP:AddInDatabase( Event ) + self:F3( { Event } ) + + if Event.IniObjectCategory == 1 then + if not self.Database[Event.IniDCSGroupName] then + self.Database[Event.IniDCSGroupName] = GROUP:Register( Event.IniDCSGroupName ) + self:T3( self.Database[Event.IniDCSGroupName] ) + end + end + + return Event.IniDCSGroupName, self.Database[Event.IniDCSGroupName] +end + +--- Handles the Database to check on any event that Object exists in the Database. +-- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! +-- @param #SET_GROUP self +-- @param Core.Event#EVENTDATA Event +-- @return #string The name of the GROUP +-- @return #table The GROUP +function SET_GROUP:FindInDatabase( Event ) + self:F3( { Event } ) + + return Event.IniDCSGroupName, self.Database[Event.IniDCSGroupName] +end + +--- Iterate the SET_GROUP and call an iterator function for each **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 Core.Zone#ZONE ZoneObject The Zone to be tested for. +-- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. +-- @return #SET_GROUP self +function SET_GROUP:ForEachGroupCompletelyInZone( ZoneObject, IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self.Set, + --- @param Core.Zone#ZONE_BASE ZoneObject + -- @param Wrapper.Group#GROUP GroupObject + function( ZoneObject, GroupObject ) + if GroupObject:IsCompletelyInZone( ZoneObject ) then + return true + else + return false + end + end, { ZoneObject } ) + + return self +end + +--- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence partly in a @{Zone}, providing the GROUP and optional parameters to the called function. +-- @param #SET_GROUP self +-- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. +-- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. +-- @return #SET_GROUP self +function SET_GROUP:ForEachGroupPartlyInZone( ZoneObject, IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self.Set, + --- @param Core.Zone#ZONE_BASE ZoneObject + -- @param Wrapper.Group#GROUP GroupObject + function( ZoneObject, GroupObject ) + if GroupObject:IsPartlyInZone( ZoneObject ) then + return true + else + return false + end + end, { ZoneObject } ) + + return self +end + +--- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence not in a @{Zone}, providing the GROUP and optional parameters to the called function. +-- @param #SET_GROUP self +-- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. +-- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. +-- @return #SET_GROUP self +function SET_GROUP:ForEachGroupNotInZone( ZoneObject, IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self.Set, + --- @param Core.Zone#ZONE_BASE ZoneObject + -- @param Wrapper.Group#GROUP GroupObject + function( ZoneObject, GroupObject ) + if GroupObject:IsNotInZone( ZoneObject ) then + return true + else + return false + end + end, { ZoneObject } ) + + return self +end + + +----- Iterate the SET_GROUP and call an interator function for each **alive** player, providing the Group of the player and optional parameters. +---- @param #SET_GROUP self +---- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_GROUP. The function needs to accept a GROUP parameter. +---- @return #SET_GROUP self +--function SET_GROUP:ForEachPlayer( IteratorFunction, ... ) +-- self:F2( arg ) +-- +-- self:ForEach( IteratorFunction, arg, self.PlayersAlive ) +-- +-- return self +--end +-- +-- +----- Iterate the SET_GROUP and call an interator function for each client, providing the Client to the function and optional parameters. +---- @param #SET_GROUP self +---- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_GROUP. The function needs to accept a CLIENT parameter. +---- @return #SET_GROUP self +--function SET_GROUP:ForEachClient( IteratorFunction, ... ) +-- self:F2( arg ) +-- +-- self:ForEach( IteratorFunction, arg, self.Clients ) +-- +-- return self +--end + + +--- +-- @param #SET_GROUP self +-- @param Wrapper.Group#GROUP MooseGroup +-- @return #SET_GROUP self +function SET_GROUP:IsIncludeObject( MooseGroup ) + self:F2( MooseGroup ) + local MooseGroupInclude = true + + if self.Filter.Coalitions then + local MooseGroupCoalition = false + for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do + self:T3( { "Coalition:", MooseGroup:GetCoalition(), self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) + if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == MooseGroup:GetCoalition() then + MooseGroupCoalition = true + 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 Core.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 ) ) + + 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 Core.Set#SET_UNIT self +-- @param Wrapper.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 ) + end + + return self +end + + +--- Finds a Unit based on the Unit Name. +-- @param #SET_UNIT self +-- @param #string UnitName +-- @return Wrapper.Unit#UNIT The found Unit. +function SET_UNIT:FindUnit( UnitName ) + + local UnitFound = self.Set[UnitName] + return UnitFound +end + + + +--- Builds a set of units of coalitions. +-- Possible current coalitions are red, blue and neutral. +-- @param #SET_UNIT self +-- @param #string Coalitions Can take the following values: "red", "blue", "neutral". +-- @return #SET_UNIT self +function SET_UNIT:FilterCoalitions( Coalitions ) + 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 + +--- Builds a set of units having a radar of give types. +-- All the units having a radar of a given type will be included within the set. +-- @param #SET_UNIT self +-- @param #table RadarTypes The radar types. +-- @return #SET_UNIT self +function SET_UNIT:FilterHasRadar( RadarTypes ) + + self.Filter.RadarTypes = self.Filter.RadarTypes or {} + if type( RadarTypes ) ~= "table" then + RadarTypes = { RadarTypes } + end + for RadarTypeID, RadarType in pairs( RadarTypes ) do + self.Filter.RadarTypes[RadarType] = RadarType + end + return self +end + +--- Builds a set of SEADable units. +-- @param #SET_UNIT self +-- @return #SET_UNIT self +function SET_UNIT:FilterHasSEAD() + + self.Filter.SEAD = true + return self +end + + + +--- 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 Core.Event#EVENTDATA Event +-- @return #string The name of the UNIT +-- @return #table The UNIT +function SET_UNIT:AddInDatabase( Event ) + self:F3( { Event } ) + + if Event.IniObjectCategory == 1 then + if not self.Database[Event.IniDCSUnitName] then + self.Database[Event.IniDCSUnitName] = UNIT:Register( Event.IniDCSUnitName ) + self:T3( self.Database[Event.IniDCSUnitName] ) + end + end + + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] +end + +--- Handles the Database to check on any event that Object exists in the Database. +-- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! +-- @param #SET_UNIT self +-- @param Core.Event#EVENTDATA Event +-- @return #string The name of the UNIT +-- @return #table The UNIT +function SET_UNIT:FindInDatabase( Event ) + self:E( { Event.IniDCSUnitName, self.Set[Event.IniDCSUnitName], Event } ) + + + return Event.IniDCSUnitName, self.Set[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 Core.Zone#ZONE ZoneObject The Zone to be tested for. +-- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the SET_UNIT. The function needs to accept a UNIT parameter. +-- @return #SET_UNIT self +function SET_UNIT:ForEachUnitCompletelyInZone( ZoneObject, IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self.Set, + --- @param Core.Zone#ZONE_BASE ZoneObject + -- @param Wrapper.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 Core.Zone#ZONE ZoneObject The Zone to be tested for. +-- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the SET_UNIT. The function needs to accept a UNIT parameter. +-- @return #SET_UNIT self +function SET_UNIT:ForEachUnitNotInZone( ZoneObject, IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self.Set, + --- @param Core.Zone#ZONE_BASE ZoneObject + -- @param Wrapper.Unit#UNIT UnitObject + function( ZoneObject, UnitObject ) + if UnitObject:IsNotInZone( ZoneObject ) then + return true + else + return false + end + end, { ZoneObject } ) + + return self +end + +--- Returns map of unit types. +-- @param #SET_UNIT self +-- @return #map<#string,#number> A map of the unit types found. The key is the UnitTypeName and the value is the amount of unit types found. +function SET_UNIT:GetUnitTypes() + self:F2() + + local MT = {} -- Message Text + local UnitTypes = {} + + for UnitID, UnitData in pairs( self:GetSet() ) do + local TextUnit = UnitData -- Wrapper.Unit#UNIT + if TextUnit:IsAlive() then + local UnitType = TextUnit:GetTypeName() + + if not UnitTypes[UnitType] then + UnitTypes[UnitType] = 1 + else + UnitTypes[UnitType] = UnitTypes[UnitType] + 1 + end + end + end + + for UnitTypeID, UnitType in pairs( UnitTypes ) do + MT[#MT+1] = UnitType .. " of " .. UnitTypeID + end + + return UnitTypes +end + + +--- Returns a comma separated string of the unit types with a count in the @{Set}. +-- @param #SET_UNIT self +-- @return #string The unit types string +function SET_UNIT:GetUnitTypesText() + self:F2() + + local MT = {} -- Message Text + local UnitTypes = self:GetUnitTypes() + + for UnitTypeID, UnitType in pairs( UnitTypes ) do + MT[#MT+1] = UnitType .. " of " .. UnitTypeID + end + + return table.concat( MT, ", " ) +end + +--- Returns map of unit threat levels. +-- @param #SET_UNIT self +-- @return #table. +function SET_UNIT:GetUnitThreatLevels() + self:F2() + + local UnitThreatLevels = {} + + for UnitID, UnitData in pairs( self:GetSet() ) do + local ThreatUnit = UnitData -- Wrapper.Unit#UNIT + if ThreatUnit:IsAlive() then + local UnitThreatLevel, UnitThreatLevelText = ThreatUnit:GetThreatLevel() + local ThreatUnitName = ThreatUnit:GetName() + + UnitThreatLevels[UnitThreatLevel] = UnitThreatLevels[UnitThreatLevel] or {} + UnitThreatLevels[UnitThreatLevel].UnitThreatLevelText = UnitThreatLevelText + UnitThreatLevels[UnitThreatLevel].Units = UnitThreatLevels[UnitThreatLevel].Units or {} + UnitThreatLevels[UnitThreatLevel].Units[ThreatUnitName] = ThreatUnit + end + end + + return UnitThreatLevels +end + +--- Calculate the maxium A2G threat level of the SET_UNIT. +-- @param #SET_UNIT self +function SET_UNIT:CalculateThreatLevelA2G() + + local MaxThreatLevelA2G = 0 + for UnitName, UnitData in pairs( self:GetSet() ) do + local ThreatUnit = UnitData -- Wrapper.Unit#UNIT + local ThreatLevelA2G = ThreatUnit:GetThreatLevel() + if ThreatLevelA2G > MaxThreatLevelA2G then + MaxThreatLevelA2G = ThreatLevelA2G + end + end + + self:T3( MaxThreatLevelA2G ) + return MaxThreatLevelA2G + +end + + +--- Returns if the @{Set} has targets having a radar (of a given type). +-- @param #SET_UNIT self +-- @param Dcs.DCSWrapper.Unit#Unit.RadarType RadarType +-- @return #number The amount of radars in the Set with the given type +function SET_UNIT:HasRadar( RadarType ) + self:F2( RadarType ) + + local RadarCount = 0 + for UnitID, UnitData in pairs( self:GetSet()) do + local UnitSensorTest = UnitData -- Wrapper.Unit#UNIT + local HasSensors + if RadarType then + HasSensors = UnitSensorTest:HasSensors( Unit.SensorType.RADAR, RadarType ) + else + HasSensors = UnitSensorTest:HasSensors( Unit.SensorType.RADAR ) + end + self:T3(HasSensors) + if HasSensors then + RadarCount = RadarCount + 1 + end + end + + return RadarCount +end + +--- Returns if the @{Set} has targets that can be SEADed. +-- @param #SET_UNIT self +-- @return #number The amount of SEADable units in the Set +function SET_UNIT:HasSEAD() + self:F2() + + local SEADCount = 0 + for UnitID, UnitData in pairs( self:GetSet()) do + local UnitSEAD = UnitData -- Wrapper.Unit#UNIT + if UnitSEAD:IsAlive() then + local UnitSEADAttributes = UnitSEAD:GetDesc().attributes + + local HasSEAD = UnitSEAD:HasSEAD() + + self:T3(HasSEAD) + if HasSEAD then + SEADCount = SEADCount + 1 + end + end + end + + return SEADCount +end + +--- Returns if the @{Set} has ground targets. +-- @param #SET_UNIT self +-- @return #number The amount of ground targets in the Set. +function SET_UNIT:HasGroundUnits() + self:F2() + + local GroundUnitCount = 0 + for UnitID, UnitData in pairs( self:GetSet()) do + local UnitTest = UnitData -- Wrapper.Unit#UNIT + if UnitTest:IsGround() then + GroundUnitCount = GroundUnitCount + 1 + end + end + + return GroundUnitCount +end + +--- Returns if the @{Set} has friendly ground units. +-- @param #SET_UNIT self +-- @return #number The amount of ground targets in the Set. +function SET_UNIT:HasFriendlyUnits( FriendlyCoalition ) + self:F2() + + local FriendlyUnitCount = 0 + for UnitID, UnitData in pairs( self:GetSet()) do + local UnitTest = UnitData -- Wrapper.Unit#UNIT + if UnitTest:IsFriendly( FriendlyCoalition ) then + FriendlyUnitCount = FriendlyUnitCount + 1 + end + end + + return FriendlyUnitCount +end + + + +----- Iterate the SET_UNIT and call an interator function for each **alive** player, providing the Unit of the player and optional parameters. +---- @param #SET_UNIT self +---- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_UNIT. The function needs to accept a UNIT parameter. +---- @return #SET_UNIT self +--function SET_UNIT:ForEachPlayer( IteratorFunction, ... ) +-- self:F2( arg ) +-- +-- self:ForEach( IteratorFunction, arg, self.PlayersAlive ) +-- +-- return self +--end +-- +-- +----- Iterate the SET_UNIT and call an interator function for each client, providing the Client to the function and optional parameters. +---- @param #SET_UNIT self +---- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_UNIT. The function needs to accept a CLIENT parameter. +---- @return #SET_UNIT self +--function SET_UNIT:ForEachClient( IteratorFunction, ... ) +-- self:F2( arg ) +-- +-- self:ForEach( IteratorFunction, arg, self.Clients ) +-- +-- return self +--end + + +--- +-- @param #SET_UNIT self +-- @param Wrapper.Unit#UNIT MUnit +-- @return #SET_UNIT self +function SET_UNIT:IsIncludeObject( MUnit ) + self:F2( MUnit ) + local MUnitInclude = 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 + + if self.Filter.RadarTypes then + local MUnitRadar = false + for RadarTypeID, RadarType in pairs( self.Filter.RadarTypes ) do + self:T3( { "Radar:", RadarType } ) + if MUnit:HasSensors( Unit.SensorType.RADAR, RadarType ) == true then + if MUnit:GetRadar() == true then -- This call is necessary to evaluate the SEAD capability. + self:T3( "RADAR Found" ) + end + MUnitRadar = true + end + end + MUnitInclude = MUnitInclude and MUnitRadar + end + + if self.Filter.SEAD then + local MUnitSEAD = false + if MUnit:HasSEAD() == true then + self:T3( "SEAD Found" ) + MUnitSEAD = true + end + MUnitInclude = MUnitInclude and MUnitSEAD + end + + self:T2( MUnitInclude ) + return MUnitInclude +end + + +--- SET_CLIENT + +--- SET_CLIENT class +-- @type SET_CLIENT +-- @extends Core.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 Core.Set#SET_CLIENT self +-- @param #string AddClientNames A single name or an array of CLIENT names. +-- @return self +function SET_CLIENT:AddClientsByName( AddClientNames ) + + local AddClientNamesArray = ( type( AddClientNames ) == "table" ) and AddClientNames or { AddClientNames } + + for AddClientID, AddClientName in pairs( AddClientNamesArray ) do + self:Add( AddClientName, CLIENT:FindByName( AddClientName ) ) + end + + return self +end + +--- Remove CLIENT(s) from SET_CLIENT. +-- @param Core.Set#SET_CLIENT self +-- @param Wrapper.Client#CLIENT RemoveClientNames A single name or an array of CLIENT names. +-- @return self +function SET_CLIENT:RemoveClientsByName( RemoveClientNames ) + + local RemoveClientNamesArray = ( type( RemoveClientNames ) == "table" ) and RemoveClientNames or { RemoveClientNames } + + for RemoveClientID, RemoveClientName in pairs( RemoveClientNamesArray ) do + self:Remove( RemoveClientName.ClientName ) + end + + return self +end + + +--- Finds a Client based on the Client Name. +-- @param #SET_CLIENT self +-- @param #string ClientName +-- @return Wrapper.Client#CLIENT The found Client. +function SET_CLIENT:FindClient( ClientName ) + + local ClientFound = self.Set[ClientName] + return ClientFound +end + + + +--- Builds a set of clients of coalitions. +-- Possible current coalitions are red, blue and neutral. +-- @param #SET_CLIENT self +-- @param #string Coalitions Can take the following values: "red", "blue", "neutral". +-- @return #SET_CLIENT self +function SET_CLIENT:FilterCoalitions( Coalitions ) + if not self.Filter.Coalitions then + self.Filter.Coalitions = {} + end + if type( Coalitions ) ~= "table" then + Coalitions = { Coalitions } + end + for CoalitionID, Coalition in pairs( Coalitions ) do + self.Filter.Coalitions[Coalition] = Coalition + end + return self +end + + +--- Builds a set of clients out of categories. +-- Possible current categories are plane, helicopter, ground, ship. +-- @param #SET_CLIENT self +-- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship". +-- @return #SET_CLIENT self +function SET_CLIENT:FilterCategories( Categories ) + if not self.Filter.Categories then + self.Filter.Categories = {} + end + if type( Categories ) ~= "table" then + Categories = { Categories } + end + for CategoryID, Category in pairs( Categories ) do + self.Filter.Categories[Category] = Category + end + return self +end + + +--- Builds a set of clients of defined client types. +-- Possible current types are those types known within DCS world. +-- @param #SET_CLIENT self +-- @param #string Types Can take those type strings known within DCS world. +-- @return #SET_CLIENT self +function SET_CLIENT:FilterTypes( Types ) + if not self.Filter.Types then + self.Filter.Types = {} + end + if type( Types ) ~= "table" then + Types = { Types } + end + for TypeID, Type in pairs( Types ) do + self.Filter.Types[Type] = Type + end + return self +end + + +--- Builds a set of clients of defined countries. +-- Possible current countries are those known within DCS world. +-- @param #SET_CLIENT self +-- @param #string Countries Can take those country strings known within DCS world. +-- @return #SET_CLIENT self +function SET_CLIENT:FilterCountries( Countries ) + if not self.Filter.Countries then + self.Filter.Countries = {} + end + if type( Countries ) ~= "table" then + Countries = { Countries } + end + for CountryID, Country in pairs( Countries ) do + self.Filter.Countries[Country] = Country + end + return self +end + + +--- Builds a set of clients 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 Core.Event#EVENTDATA Event +-- @return #string The name of the CLIENT +-- @return #table The CLIENT +function SET_CLIENT:AddInDatabase( Event ) + self:F3( { Event } ) + + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] +end + +--- Handles the Database to check on any event that Object exists in the Database. +-- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! +-- @param #SET_CLIENT self +-- @param Core.Event#EVENTDATA Event +-- @return #string The name of the CLIENT +-- @return #table The CLIENT +function SET_CLIENT:FindInDatabase( Event ) + self:F3( { Event } ) + + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] +end + +--- Iterate the SET_CLIENT and call an interator function for each **alive** CLIENT, providing the CLIENT and optional parameters. +-- @param #SET_CLIENT self +-- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_CLIENT. The function needs to accept a CLIENT parameter. +-- @return #SET_CLIENT self +function SET_CLIENT:ForEachClient( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self.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 Core.Zone#ZONE ZoneObject The Zone to be tested for. +-- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_CLIENT. The function needs to accept a CLIENT parameter. +-- @return #SET_CLIENT self +function SET_CLIENT:ForEachClientInZone( ZoneObject, IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self.Set, + --- @param Core.Zone#ZONE_BASE ZoneObject + -- @param Wrapper.Client#CLIENT ClientObject + function( ZoneObject, ClientObject ) + if ClientObject:IsInZone( ZoneObject ) then + return true + else + return false + end + end, { ZoneObject } ) + + return self +end + +--- Iterate the SET_CLIENT and call an iterator function for each **alive** CLIENT presence not in a @{Zone}, providing the CLIENT and optional parameters to the called function. +-- @param #SET_CLIENT self +-- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. +-- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_CLIENT. The function needs to accept a CLIENT parameter. +-- @return #SET_CLIENT self +function SET_CLIENT:ForEachClientNotInZone( ZoneObject, IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self.Set, + --- @param Core.Zone#ZONE_BASE ZoneObject + -- @param Wrapper.Client#CLIENT ClientObject + function( ZoneObject, ClientObject ) + if ClientObject:IsNotInZone( ZoneObject ) then + return true + else + return false + end + end, { ZoneObject } ) + + return self +end + +--- +-- @param #SET_CLIENT self +-- @param Wrapper.Client#CLIENT MClient +-- @return #SET_CLIENT self +function SET_CLIENT:IsIncludeObject( MClient ) + self:F2( MClient ) + + local MClientInclude = true + + if MClient then + local MClientName = MClient.UnitName + + if self.Filter.Coalitions then + local MClientCoalition = false + for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do + local ClientCoalitionID = _DATABASE:GetCoalitionFromClientTemplate( MClientName ) + self:T3( { "Coalition:", ClientCoalitionID, self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) + if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == ClientCoalitionID then + MClientCoalition = true + end + end + self:T( { "Evaluated Coalition", MClientCoalition } ) + MClientInclude = MClientInclude and MClientCoalition + end + + if self.Filter.Categories then + local MClientCategory = false + for CategoryID, CategoryName in pairs( self.Filter.Categories ) do + local ClientCategoryID = _DATABASE:GetCategoryFromClientTemplate( MClientName ) + self:T3( { "Category:", ClientCategoryID, self.FilterMeta.Categories[CategoryName], CategoryName } ) + if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == ClientCategoryID then + MClientCategory = true + end + end + self:T( { "Evaluated Category", MClientCategory } ) + MClientInclude = MClientInclude and MClientCategory + end + + if self.Filter.Types then + local MClientType = false + for TypeID, TypeName in pairs( self.Filter.Types ) do + self:T3( { "Type:", MClient:GetTypeName(), TypeName } ) + if TypeName == MClient:GetTypeName() then + MClientType = true + end + end + self:T( { "Evaluated Type", MClientType } ) + MClientInclude = MClientInclude and MClientType + end + + if self.Filter.Countries then + local MClientCountry = false + for CountryID, CountryName in pairs( self.Filter.Countries ) do + local ClientCountryID = _DATABASE:GetCountryFromClientTemplate(MClientName) + self:T3( { "Country:", ClientCountryID, country.id[CountryName], CountryName } ) + if country.id[CountryName] and country.id[CountryName] == ClientCountryID then + MClientCountry = true + end + end + self:T( { "Evaluated Country", MClientCountry } ) + MClientInclude = MClientInclude and MClientCountry + end + + if self.Filter.ClientPrefixes then + local MClientPrefix = false + for ClientPrefixId, ClientPrefix in pairs( self.Filter.ClientPrefixes ) do + self:T3( { "Prefix:", string.find( MClient.UnitName, ClientPrefix, 1 ), ClientPrefix } ) + if string.find( MClient.UnitName, ClientPrefix, 1 ) then + MClientPrefix = true + end + end + self:T( { "Evaluated Prefix", MClientPrefix } ) + MClientInclude = MClientInclude and MClientPrefix + end + end + + self:T2( MClientInclude ) + return MClientInclude +end + +--- SET_AIRBASE + +--- SET_AIRBASE class +-- @type SET_AIRBASE +-- @extends Core.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 Core.Set#SET_AIRBASE self +-- @param #string AddAirbaseNames A single name or an array of AIRBASE names. +-- @return self +function SET_AIRBASE:AddAirbasesByName( AddAirbaseNames ) + + local AddAirbaseNamesArray = ( type( AddAirbaseNames ) == "table" ) and AddAirbaseNames or { AddAirbaseNames } + + for AddAirbaseID, AddAirbaseName in pairs( AddAirbaseNamesArray ) do + self:Add( AddAirbaseName, AIRBASE:FindByName( AddAirbaseName ) ) + end + + return self +end + +--- Remove AIRBASEs from SET_AIRBASE. +-- @param Core.Set#SET_AIRBASE self +-- @param Wrapper.Airbase#AIRBASE RemoveAirbaseNames A single name or an array of AIRBASE names. +-- @return self +function SET_AIRBASE:RemoveAirbasesByName( RemoveAirbaseNames ) + + local RemoveAirbaseNamesArray = ( type( RemoveAirbaseNames ) == "table" ) and RemoveAirbaseNames or { RemoveAirbaseNames } + + for RemoveAirbaseID, RemoveAirbaseName in pairs( RemoveAirbaseNamesArray ) do + self:Remove( RemoveAirbaseName.AirbaseName ) + end + + return self +end + + +--- Finds a Airbase based on the Airbase Name. +-- @param #SET_AIRBASE self +-- @param #string AirbaseName +-- @return Wrapper.Airbase#AIRBASE The found Airbase. +function SET_AIRBASE:FindAirbase( AirbaseName ) + + local AirbaseFound = self.Set[AirbaseName] + return AirbaseFound +end + + + +--- 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 Core.Event#EVENTDATA Event +-- @return #string The name of the AIRBASE +-- @return #table The AIRBASE +function SET_AIRBASE:AddInDatabase( Event ) + self:F3( { Event } ) + + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] +end + +--- Handles the Database to check on any event that Object exists in the Database. +-- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! +-- @param #SET_AIRBASE self +-- @param Core.Event#EVENTDATA Event +-- @return #string The name of the AIRBASE +-- @return #table The AIRBASE +function SET_AIRBASE:FindInDatabase( Event ) + self:F3( { Event } ) + + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] +end + +--- Iterate the SET_AIRBASE and call an interator function for each AIRBASE, providing the AIRBASE and optional parameters. +-- @param #SET_AIRBASE self +-- @param #function IteratorFunction The function that will be called when there is an alive AIRBASE in the SET_AIRBASE. The function needs to accept a AIRBASE parameter. +-- @return #SET_AIRBASE self +function SET_AIRBASE:ForEachAirbase( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self.Set ) + + return self +end + +--- Iterate the SET_AIRBASE while identifying the nearest @{Airbase#AIRBASE} from a @{Point#POINT_VEC2}. +-- @param #SET_AIRBASE self +-- @param Core.Point#POINT_VEC2 PointVec2 A @{Point#POINT_VEC2} object from where to evaluate the closest @{Airbase#AIRBASE}. +-- @return Wrapper.Airbase#AIRBASE The closest @{Airbase#AIRBASE}. +function SET_AIRBASE:FindNearestAirbaseFromPointVec2( PointVec2 ) + self:F2( PointVec2 ) + + local NearestAirbase = self:FindNearestObjectFromPointVec2( PointVec2 ) + return NearestAirbase +end + + + +--- +-- @param #SET_AIRBASE self +-- @param Wrapper.Airbase#AIRBASE MAirbase +-- @return #SET_AIRBASE self +function SET_AIRBASE:IsIncludeObject( MAirbase ) + self:F2( MAirbase ) + + local MAirbaseInclude = true + + if MAirbase then + local MAirbaseName = MAirbase:GetName() + + if self.Filter.Coalitions then + local MAirbaseCoalition = false + for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do + local AirbaseCoalitionID = _DATABASE:GetCoalitionFromAirbase( MAirbaseName ) + self:T3( { "Coalition:", AirbaseCoalitionID, self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) + if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == AirbaseCoalitionID then + MAirbaseCoalition = true + end + end + self:T( { "Evaluated Coalition", MAirbaseCoalition } ) + MAirbaseInclude = MAirbaseInclude and MAirbaseCoalition + end + + if self.Filter.Categories then + local MAirbaseCategory = false + for CategoryID, CategoryName in pairs( self.Filter.Categories ) do + local AirbaseCategoryID = _DATABASE:GetCategoryFromAirbase( MAirbaseName ) + self:T3( { "Category:", AirbaseCategoryID, self.FilterMeta.Categories[CategoryName], CategoryName } ) + if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == AirbaseCategoryID then + MAirbaseCategory = true + end + end + self:T( { "Evaluated Category", MAirbaseCategory } ) + MAirbaseInclude = MAirbaseInclude and MAirbaseCategory + end + end + + self:T2( MAirbaseInclude ) + return MAirbaseInclude +end +--- 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. +-- +-- **Important Note:** Most of the functions in this section were taken from MIST, and reworked to OO concepts. +-- In order to keep the credibility of the the author, I want to emphasize that the of the MIST framework was created by Grimes, who you can find on the Eagle Dynamics Forums. +-- +-- ## 1.1) POINT_VEC3 constructor +-- +-- A new POINT_VEC3 instance can be created with: +-- +-- * @{Point#POINT_VEC3.New}(): a 3D point. +-- * @{Point#POINT_VEC3.NewFromVec3}(): a 3D point created from a @{DCSTypes#Vec3}. +-- +-- ## 1.2) Manupulate the X, Y, Z coordinates of the point +-- +-- A POINT_VEC3 class works in 3D space. It contains internally an X, Y, Z coordinate. +-- Methods exist to manupulate these coordinates. +-- +-- The current X, Y, Z axis can be retrieved with the methods @{#POINT_VEC3.GetX}(), @{#POINT_VEC3.GetY}(), @{#POINT_VEC3.GetZ}() respectively. +-- The methods @{#POINT_VEC3.SetX}(), @{#POINT_VEC3.SetY}(), @{#POINT_VEC3.SetZ}() change the respective axis with a new value. +-- The current axis values can be changed by using the methods @{#POINT_VEC3.AddX}(), @{#POINT_VEC3.AddY}(), @{#POINT_VEC3.AddZ}() +-- to add or substract a value from the current respective axis value. +-- Note that the Set and Add methods return the current POINT_VEC3 object, so these manipulation methods can be chained... For example: +-- +-- local Vec3 = PointVec3:AddX( 100 ):AddZ( 150 ):GetVec3() +-- +-- ## 1.3) Create waypoints for routes +-- +-- A POINT_VEC3 can prepare waypoints for Ground, Air and Naval groups to be embedded into a Route. +-- +-- +-- ## 1.5) Smoke, flare, explode, illuminate +-- +-- At the point a smoke, flare, explosion and illumination bomb can be triggered. Use the following methods: +-- +-- ### 1.5.1) Smoke +-- +-- * @{#POINT_VEC3.Smoke}(): To smoke the point in a certain color. +-- * @{#POINT_VEC3.SmokeBlue}(): To smoke the point in blue. +-- * @{#POINT_VEC3.SmokeRed}(): To smoke the point in red. +-- * @{#POINT_VEC3.SmokeOrange}(): To smoke the point in orange. +-- * @{#POINT_VEC3.SmokeWhite}(): To smoke the point in white. +-- * @{#POINT_VEC3.SmokeGreen}(): To smoke the point in green. +-- +-- ### 1.5.2) Flare +-- +-- * @{#POINT_VEC3.Flare}(): To flare the point in a certain color. +-- * @{#POINT_VEC3.FlareRed}(): To flare the point in red. +-- * @{#POINT_VEC3.FlareYellow}(): To flare the point in yellow. +-- * @{#POINT_VEC3.FlareWhite}(): To flare the point in white. +-- * @{#POINT_VEC3.FlareGreen}(): To flare the point in green. +-- +-- ### 1.5.3) Explode +-- +-- * @{#POINT_VEC3.Explosion}(): To explode the point with a certain intensity. +-- +-- ### 1.5.4) Illuminate +-- +-- * @{#POINT_VEC3.IlluminationBomb}(): To illuminate the 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_VEC2 instance can be created with: +-- +-- * @{Point#POINT_VEC2.New}(): a 2D point, taking an additional height parameter. +-- * @{Point#POINT_VEC2.NewFromVec2}(): a 2D point created from a @{DCSTypes#Vec2}. +-- +-- ## 1.2) Manupulate the X, Altitude, Y coordinates of the 2D point +-- +-- A POINT_VEC2 class works in 2D space, with an altitude setting. It contains internally an X, Altitude, Y coordinate. +-- Methods exist to manupulate these coordinates. +-- +-- The current X, Altitude, Y axis can be retrieved with the methods @{#POINT_VEC2.GetX}(), @{#POINT_VEC2.GetAlt}(), @{#POINT_VEC2.GetY}() respectively. +-- The methods @{#POINT_VEC2.SetX}(), @{#POINT_VEC2.SetAlt}(), @{#POINT_VEC2.SetY}() change the respective axis with a new value. +-- The current axis values can be changed by using the methods @{#POINT_VEC2.AddX}(), @{#POINT_VEC2.AddAlt}(), @{#POINT_VEC2.AddY}() +-- to add or substract a value from the current respective axis value. +-- Note that the Set and Add methods return the current POINT_VEC2 object, so these manipulation methods can be chained... For example: +-- +-- local Vec2 = PointVec2:AddX( 100 ):AddY( 2000 ):GetVec2() +-- +-- === +-- +-- **API CHANGE HISTORY** +-- ====================== +-- +-- The underlying change log documents the API changes. Please read this carefully. The following notation is used: +-- +-- * **Added** parts are expressed in bold type face. +-- * _Removed_ parts are expressed in italic type face. +-- +-- Hereby the change log: +-- +-- 2017-03-03: POINT\_VEC3:**Explosion( ExplosionIntensity )** added. +-- 2017-03-03: POINT\_VEC3:**IlluminationBomb()** added. +-- +-- 2017-02-18: POINT\_VEC3:**NewFromVec2( Vec2, LandHeightAdd )** added. +-- +-- 2016-08-12: POINT\_VEC3:**Translate( Distance, Angle )** added. +-- +-- 2016-08-06: Made PointVec3 and Vec3, PointVec2 and Vec2 terminology used in the code consistent. +-- +-- * Replaced method _Point_Vec3() to **Vec3**() where the code manages a Vec3. Replaced all references to the method. +-- * Replaced method _Point_Vec2() to **Vec2**() where the code manages a Vec2. Replaced all references to the method. +-- * Replaced method Random_Point_Vec3() to **RandomVec3**() where the code manages a Vec3. Replaced all references to the method. +-- . +-- === +-- +-- ### Authors: +-- +-- * FlightControl : Design & Programming +-- +-- ### Contributions: +-- +-- @module Point + +--- The POINT_VEC3 class +-- @type POINT_VEC3 +-- @field #number x The x coordinate in 3D space. +-- @field #number y The y coordinate in 3D space. +-- @field #number z The z coordiante in 3D space. +-- @field Utilities.Utils#SMOKECOLOR SmokeColor +-- @field Utilities.Utils#FLARECOLOR FlareColor +-- @field #POINT_VEC3.RoutePointAltType RoutePointAltType +-- @field #POINT_VEC3.RoutePointType RoutePointType +-- @field #POINT_VEC3.RoutePointAction RoutePointAction +-- @extends Core.Base#BASE +POINT_VEC3 = { + ClassName = "POINT_VEC3", + Metric = true, + RoutePointAltType = { + BARO = "BARO", + }, + RoutePointType = { + TakeOffParking = "TakeOffParking", + TurningPoint = "Turning Point", + }, + RoutePointAction = { + FromParkingArea = "From Parking Area", + TurningPoint = "Turning Point", + }, +} + +--- The POINT_VEC2 class +-- @type POINT_VEC2 +-- @field Dcs.DCSTypes#Distance x The x coordinate in meters. +-- @field Dcs.DCSTypes#Distance y the y coordinate in meters. +-- @extends Core.Point#POINT_VEC3 +POINT_VEC2 = { + ClassName = "POINT_VEC2", +} + + +do -- POINT_VEC3 + +--- RoutePoint AltTypes +-- @type POINT_VEC3.RoutePointAltType +-- @field BARO "BARO" + +--- RoutePoint Types +-- @type POINT_VEC3.RoutePointType +-- @field TakeOffParking "TakeOffParking" +-- @field TurningPoint "Turning Point" + +--- RoutePoint Actions +-- @type POINT_VEC3.RoutePointAction +-- @field FromParkingArea "From Parking Area" +-- @field TurningPoint "Turning Point" + +-- Constructor. + +--- Create a new POINT_VEC3 object. +-- @param #POINT_VEC3 self +-- @param Dcs.DCSTypes#Distance x The x coordinate of the Vec3 point, pointing to the North. +-- @param Dcs.DCSTypes#Distance y The y coordinate of the Vec3 point, pointing Upwards. +-- @param Dcs.DCSTypes#Distance z The z coordinate of the Vec3 point, pointing to the Right. +-- @return Core.Point#POINT_VEC3 self +function POINT_VEC3:New( x, y, z ) + + local self = BASE:Inherit( self, BASE:New() ) + self.x = x + self.y = y + self.z = z + + return self +end + +--- Create a new POINT_VEC3 object from Vec2 coordinates. +-- @param #POINT_VEC3 self +-- @param Dcs.DCSTypes#Vec2 Vec2 The Vec2 point. +-- @return Core.Point#POINT_VEC3 self +function POINT_VEC3:NewFromVec2( Vec2, LandHeightAdd ) + + local LandHeight = land.getHeight( Vec2 ) + + LandHeightAdd = LandHeightAdd or 0 + LandHeight = LandHeight + LandHeightAdd + + self = self:New( Vec2.x, LandHeight, Vec2.y ) + + self:F2( self ) + + return self +end + +--- Create a new POINT_VEC3 object from Vec3 coordinates. +-- @param #POINT_VEC3 self +-- @param Dcs.DCSTypes#Vec3 Vec3 The Vec3 point. +-- @return Core.Point#POINT_VEC3 self +function POINT_VEC3:NewFromVec3( Vec3 ) + + self = self:New( Vec3.x, Vec3.y, Vec3.z ) + self:F2( self ) + return self +end + + +--- Return the coordinates of the POINT_VEC3 in Vec3 format. +-- @param #POINT_VEC3 self +-- @return Dcs.DCSTypes#Vec3 The Vec3 coodinate. +function POINT_VEC3:GetVec3() + return { x = self.x, y = self.y, z = self.z } +end + +--- Return the coordinates of the POINT_VEC3 in Vec2 format. +-- @param #POINT_VEC3 self +-- @return Dcs.DCSTypes#Vec2 The Vec2 coodinate. +function POINT_VEC3:GetVec2() + return { x = self.x, y = self.z } +end + + +--- Return the x coordinate of the POINT_VEC3. +-- @param #POINT_VEC3 self +-- @return #number The x coodinate. +function POINT_VEC3:GetX() + return self.x +end + +--- Return the y coordinate of the POINT_VEC3. +-- @param #POINT_VEC3 self +-- @return #number The y coodinate. +function POINT_VEC3:GetY() + return self.y +end + +--- Return the z coordinate of the POINT_VEC3. +-- @param #POINT_VEC3 self +-- @return #number The z coodinate. +function POINT_VEC3:GetZ() + return self.z +end + +--- Set the x coordinate of the POINT_VEC3. +-- @param #POINT_VEC3 self +-- @param #number x The x coordinate. +-- @return #POINT_VEC3 +function POINT_VEC3:SetX( x ) + self.x = x + return self +end + +--- Set the y coordinate of the POINT_VEC3. +-- @param #POINT_VEC3 self +-- @param #number y The y coordinate. +-- @return #POINT_VEC3 +function POINT_VEC3:SetY( y ) + self.y = y + return self +end + +--- Set the z coordinate of the POINT_VEC3. +-- @param #POINT_VEC3 self +-- @param #number z The z coordinate. +-- @return #POINT_VEC3 +function POINT_VEC3:SetZ( z ) + self.z = z + return self +end + +--- Add to the x coordinate of the POINT_VEC3. +-- @param #POINT_VEC3 self +-- @param #number x The x coordinate value to add to the current x coodinate. +-- @return #POINT_VEC3 +function POINT_VEC3:AddX( x ) + self.x = self.x + x + return self +end + +--- Add to the y coordinate of the POINT_VEC3. +-- @param #POINT_VEC3 self +-- @param #number y The y coordinate value to add to the current y coodinate. +-- @return #POINT_VEC3 +function POINT_VEC3:AddY( y ) + self.y = self.y + y + return self +end + +--- Add to the z coordinate of the POINT_VEC3. +-- @param #POINT_VEC3 self +-- @param #number z The z coordinate value to add to the current z coodinate. +-- @return #POINT_VEC3 +function POINT_VEC3:AddZ( z ) + self.z = self.z +z + return self +end + +--- Return a random Vec2 within an Outer Radius and optionally NOT within an Inner Radius of the POINT_VEC3. +-- @param #POINT_VEC3 self +-- @param Dcs.DCSTypes#Distance OuterRadius +-- @param Dcs.DCSTypes#Distance InnerRadius +-- @return Dcs.DCSTypes#Vec2 Vec2 +function POINT_VEC3:GetRandomVec2InRadius( OuterRadius, InnerRadius ) + self:F2( { OuterRadius, InnerRadius } ) + + local Theta = 2 * math.pi * math.random() + local Radials = math.random() + math.random() + if Radials > 1 then + Radials = 2 - Radials + end + + local RadialMultiplier + if InnerRadius and InnerRadius <= OuterRadius then + RadialMultiplier = ( OuterRadius - InnerRadius ) * Radials + InnerRadius + else + RadialMultiplier = OuterRadius * Radials + end + + local RandomVec2 + if OuterRadius > 0 then + RandomVec2 = { x = math.cos( Theta ) * RadialMultiplier + self:GetX(), y = math.sin( Theta ) * RadialMultiplier + self:GetZ() } + else + RandomVec2 = { x = self:GetX(), y = self:GetZ() } + end + + return RandomVec2 +end + +--- Return a random POINT_VEC2 within an Outer Radius and optionally NOT within an Inner Radius of the POINT_VEC3. +-- @param #POINT_VEC3 self +-- @param Dcs.DCSTypes#Distance OuterRadius +-- @param Dcs.DCSTypes#Distance InnerRadius +-- @return #POINT_VEC2 +function POINT_VEC3:GetRandomPointVec2InRadius( OuterRadius, InnerRadius ) + self:F2( { OuterRadius, InnerRadius } ) + + return POINT_VEC2:NewFromVec2( self:GetRandomVec2InRadius( OuterRadius, InnerRadius ) ) +end + +--- Return a random Vec3 within an Outer Radius and optionally NOT within an Inner Radius of the POINT_VEC3. +-- @param #POINT_VEC3 self +-- @param Dcs.DCSTypes#Distance OuterRadius +-- @param Dcs.DCSTypes#Distance InnerRadius +-- @return Dcs.DCSTypes#Vec3 Vec3 +function POINT_VEC3:GetRandomVec3InRadius( OuterRadius, InnerRadius ) + + local RandomVec2 = self:GetRandomVec2InRadius( OuterRadius, InnerRadius ) + local y = self:GetY() + math.random( InnerRadius, OuterRadius ) + local RandomVec3 = { x = RandomVec2.x, y = y, z = RandomVec2.z } + + return RandomVec3 +end + +--- Return a random POINT_VEC3 within an Outer Radius and optionally NOT within an Inner Radius of the POINT_VEC3. +-- @param #POINT_VEC3 self +-- @param Dcs.DCSTypes#Distance OuterRadius +-- @param Dcs.DCSTypes#Distance InnerRadius +-- @return #POINT_VEC3 +function POINT_VEC3:GetRandomPointVec3InRadius( OuterRadius, InnerRadius ) + + return POINT_VEC3:NewFromVec3( self:GetRandomVec3InRadius( OuterRadius, InnerRadius ) ) +end + + +--- Return a direction vector Vec3 from POINT_VEC3 to the POINT_VEC3. +-- @param #POINT_VEC3 self +-- @param #POINT_VEC3 TargetPointVec3 The target POINT_VEC3. +-- @return Dcs.DCSTypes#Vec3 DirectionVec3 The direction vector in Vec3 format. +function POINT_VEC3:GetDirectionVec3( TargetPointVec3 ) + return { x = TargetPointVec3:GetX() - self:GetX(), y = TargetPointVec3:GetY() - self:GetY(), z = TargetPointVec3:GetZ() - self:GetZ() } +end + +--- Get a correction in radians of the real magnetic north of the POINT_VEC3. +-- @param #POINT_VEC3 self +-- @return #number CorrectionRadians The correction in radians. +function POINT_VEC3:GetNorthCorrectionRadians() + local TargetVec3 = self:GetVec3() + local lat, lon = coord.LOtoLL(TargetVec3) + local north_posit = coord.LLtoLO(lat + 1, lon) + return math.atan2( north_posit.z - TargetVec3.z, north_posit.x - TargetVec3.x ) +end + + +--- Return a direction in radians from the POINT_VEC3 using a direction vector in Vec3 format. +-- @param #POINT_VEC3 self +-- @param Dcs.DCSTypes#Vec3 DirectionVec3 The direction vector in Vec3 format. +-- @return #number DirectionRadians The direction in radians. +function POINT_VEC3:GetDirectionRadians( DirectionVec3 ) + local DirectionRadians = math.atan2( DirectionVec3.z, DirectionVec3.x ) + --DirectionRadians = DirectionRadians + self:GetNorthCorrectionRadians() + if DirectionRadians < 0 then + DirectionRadians = DirectionRadians + 2 * math.pi -- put dir in range of 0 to 2*pi ( the full circle ) + end + return DirectionRadians +end + +--- Return the 2D distance in meters between the target POINT_VEC3 and the POINT_VEC3. +-- @param #POINT_VEC3 self +-- @param #POINT_VEC3 TargetPointVec3 The target POINT_VEC3. +-- @return Dcs.DCSTypes#Distance Distance The distance in meters. +function POINT_VEC3:Get2DDistance( TargetPointVec3 ) + local TargetVec3 = TargetPointVec3:GetVec3() + local SourceVec3 = self:GetVec3() + return ( ( TargetVec3.x - SourceVec3.x ) ^ 2 + ( TargetVec3.z - SourceVec3.z ) ^ 2 ) ^ 0.5 +end + +--- Return the 3D distance in meters between the target POINT_VEC3 and the POINT_VEC3. +-- @param #POINT_VEC3 self +-- @param #POINT_VEC3 TargetPointVec3 The target POINT_VEC3. +-- @return Dcs.DCSTypes#Distance Distance The distance in meters. +function POINT_VEC3:Get3DDistance( TargetPointVec3 ) + local TargetVec3 = TargetPointVec3:GetVec3() + local SourceVec3 = self:GetVec3() + return ( ( TargetVec3.x - SourceVec3.x ) ^ 2 + ( TargetVec3.y - SourceVec3.y ) ^ 2 + ( TargetVec3.z - SourceVec3.z ) ^ 2 ) ^ 0.5 +end + +--- Provides a Bearing / Range string +-- @param #POINT_VEC3 self +-- @param #number AngleRadians The angle in randians +-- @param #number Distance The distance +-- @return #string The BR Text +function POINT_VEC3:ToStringBR( AngleRadians, Distance ) + + AngleRadians = UTILS.Round( UTILS.ToDegree( AngleRadians ), 0 ) + if self:IsMetric() then + Distance = UTILS.Round( Distance / 1000, 2 ) + else + Distance = UTILS.Round( UTILS.MetersToNM( Distance ), 2 ) + end + + local s = string.format( '%03d', AngleRadians ) .. ' for ' .. Distance + + s = s .. self:GetAltitudeText() -- When the POINT is a VEC2, there will be no altitude shown. + + return s +end + +--- Provides a Bearing / Range string +-- @param #POINT_VEC3 self +-- @param #number AngleRadians The angle in randians +-- @param #number Distance The distance +-- @return #string The BR Text +function POINT_VEC3:ToStringLL( acc, DMS ) + + acc = acc or 3 + local lat, lon = coord.LOtoLL( self:GetVec3() ) + return UTILS.tostringLL(lat, lon, acc, DMS) +end + +--- Return the altitude text of the POINT_VEC3. +-- @param #POINT_VEC3 self +-- @return #string Altitude text. +function POINT_VEC3:GetAltitudeText() + if self:IsMetric() then + return ' at ' .. UTILS.Round( self:GetY(), 0 ) + else + return ' at ' .. UTILS.Round( UTILS.MetersToFeet( self:GetY() ), 0 ) + end +end + +--- Return a BR string from a POINT_VEC3 to the POINT_VEC3. +-- @param #POINT_VEC3 self +-- @param #POINT_VEC3 TargetPointVec3 The target POINT_VEC3. +-- @return #string The BR text. +function POINT_VEC3:GetBRText( TargetPointVec3 ) + local DirectionVec3 = self:GetDirectionVec3( TargetPointVec3 ) + local AngleRadians = self:GetDirectionRadians( DirectionVec3 ) + local Distance = self:Get2DDistance( TargetPointVec3 ) + return self:ToStringBR( AngleRadians, Distance ) +end + +--- Sets the POINT_VEC3 metric or NM. +-- @param #POINT_VEC3 self +-- @param #boolean Metric true means metric, false means NM. +function POINT_VEC3:SetMetric( Metric ) + self.Metric = Metric +end + +--- Gets if the POINT_VEC3 is metric or NM. +-- @param #POINT_VEC3 self +-- @return #boolean Metric true means metric, false means NM. +function POINT_VEC3:IsMetric() + return self.Metric +end + +--- Add a Distance in meters from the POINT_VEC3 horizontal plane, with the given angle, and calculate the new POINT_VEC3. +-- @param #POINT_VEC3 self +-- @param Dcs.DCSTypes#Distance Distance The Distance to be added in meters. +-- @param Dcs.DCSTypes#Angle Angle The Angle in degrees. +-- @return #POINT_VEC3 The new calculated POINT_VEC3. +function POINT_VEC3:Translate( Distance, Angle ) + local SX = self:GetX() + local SZ = self:GetZ() + local Radians = Angle / 180 * math.pi + local TX = Distance * math.cos( Radians ) + SX + local TZ = Distance * math.sin( Radians ) + SZ + + return POINT_VEC3:New( TX, self:GetY(), TZ ) +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 Dcs.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:GetX() + RoutePoint.y = self:GetZ() + RoutePoint.alt = self:GetY() + 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 + +--- Build an ground type route point. +-- @param #POINT_VEC3 self +-- @param Dcs.DCSTypes#Speed Speed Speed in km/h. +-- @param #POINT_VEC3.RoutePointAction Formation The route point Formation. +-- @return #table The route point. +function POINT_VEC3:RoutePointGround( Speed, Formation ) + self:F2( { Formation, Speed } ) + + local RoutePoint = {} + RoutePoint.x = self:GetX() + RoutePoint.y = self:GetZ() + + RoutePoint.action = Formation or "" + + + 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 + +--- Creates an explosion at the point of a certain intensity. +-- @param #POINT_VEC3 self +-- @param #number ExplosionIntensity +function POINT_VEC3:Explosion( ExplosionIntensity ) + self:F2( { ExplosionIntensity } ) + trigger.action.explosion( self:GetVec3(), ExplosionIntensity ) +end + +--- Creates an illumination bomb at the point. +-- @param #POINT_VEC3 self +function POINT_VEC3:IlluminationBomb() + self:F2() + trigger.action.illuminationBomb( self:GetVec3() ) +end + + +--- Smokes the point in a color. +-- @param #POINT_VEC3 self +-- @param Utilities.Utils#SMOKECOLOR SmokeColor +function POINT_VEC3:Smoke( SmokeColor ) + self:F2( { SmokeColor } ) + trigger.action.smoke( self:GetVec3(), SmokeColor ) +end + +--- Smoke the POINT_VEC3 Green. +-- @param #POINT_VEC3 self +function POINT_VEC3:SmokeGreen() + self:F2() + self:Smoke( SMOKECOLOR.Green ) +end + +--- Smoke the POINT_VEC3 Red. +-- @param #POINT_VEC3 self +function POINT_VEC3:SmokeRed() + self:F2() + self:Smoke( SMOKECOLOR.Red ) +end + +--- Smoke the POINT_VEC3 White. +-- @param #POINT_VEC3 self +function POINT_VEC3:SmokeWhite() + self:F2() + self:Smoke( SMOKECOLOR.White ) +end + +--- Smoke the POINT_VEC3 Orange. +-- @param #POINT_VEC3 self +function POINT_VEC3:SmokeOrange() + self:F2() + self:Smoke( SMOKECOLOR.Orange ) +end + +--- Smoke the POINT_VEC3 Blue. +-- @param #POINT_VEC3 self +function POINT_VEC3:SmokeBlue() + self:F2() + self:Smoke( SMOKECOLOR.Blue ) +end + +--- Flares the point in a color. +-- @param #POINT_VEC3 self +-- @param Utilities.Utils#FLARECOLOR FlareColor +-- @param Dcs.DCSTypes#Azimuth (optional) Azimuth The azimuth of the flare direction. The default azimuth is 0. +function POINT_VEC3:Flare( FlareColor, Azimuth ) + self:F2( { FlareColor } ) + trigger.action.signalFlare( self:GetVec3(), FlareColor, Azimuth and Azimuth or 0 ) +end + +--- Flare the POINT_VEC3 White. +-- @param #POINT_VEC3 self +-- @param Dcs.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( FLARECOLOR.White, Azimuth ) +end + +--- Flare the POINT_VEC3 Yellow. +-- @param #POINT_VEC3 self +-- @param Dcs.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( FLARECOLOR.Yellow, Azimuth ) +end + +--- Flare the POINT_VEC3 Green. +-- @param #POINT_VEC3 self +-- @param Dcs.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( FLARECOLOR.Green, Azimuth ) +end + +--- Flare the POINT_VEC3 Red. +-- @param #POINT_VEC3 self +function POINT_VEC3:FlareRed( Azimuth ) + self:F2( Azimuth ) + self:Flare( FLARECOLOR.Red, Azimuth ) +end + +end + +do -- POINT_VEC2 + + + +--- POINT_VEC2 constructor. +-- @param #POINT_VEC2 self +-- @param Dcs.DCSTypes#Distance x The x coordinate of the Vec3 point, pointing to the North. +-- @param Dcs.DCSTypes#Distance y The y coordinate of the Vec3 point, pointing to the Right. +-- @param Dcs.DCSTypes#Distance LandHeightAdd (optional) The default height if required to be evaluated will be the land height of the x, y coordinate. You can specify an extra height to be added to the land height. +-- @return Core.Point#POINT_VEC2 +function POINT_VEC2:New( x, y, LandHeightAdd ) + + local LandHeight = land.getHeight( { ["x"] = x, ["y"] = y } ) + + LandHeightAdd = LandHeightAdd or 0 + LandHeight = LandHeight + LandHeightAdd + + self = BASE:Inherit( self, POINT_VEC3:New( x, LandHeight, y ) ) + self:F2( self ) + + return self +end + +--- Create a new POINT_VEC2 object from Vec2 coordinates. +-- @param #POINT_VEC2 self +-- @param Dcs.DCSTypes#Vec2 Vec2 The Vec2 point. +-- @return Core.Point#POINT_VEC2 self +function POINT_VEC2:NewFromVec2( Vec2, LandHeightAdd ) + + local LandHeight = land.getHeight( Vec2 ) + + LandHeightAdd = LandHeightAdd or 0 + LandHeight = LandHeight + LandHeightAdd + + self = BASE:Inherit( self, POINT_VEC3:New( Vec2.x, LandHeight, Vec2.y ) ) + self:F2( self ) + + return self +end + +--- Create a new POINT_VEC2 object from Vec3 coordinates. +-- @param #POINT_VEC2 self +-- @param Dcs.DCSTypes#Vec3 Vec3 The Vec3 point. +-- @return Core.Point#POINT_VEC2 self +function POINT_VEC2:NewFromVec3( Vec3 ) + + local self = BASE:Inherit( self, BASE:New() ) + local Vec2 = { x = Vec3.x, y = Vec3.z } + + local LandHeight = land.getHeight( Vec2 ) + + self = BASE:Inherit( self, POINT_VEC3:New( Vec2.x, LandHeight, Vec2.y ) ) + self:F2( self ) + + return self +end + +--- Return the x coordinate of the POINT_VEC2. +-- @param #POINT_VEC2 self +-- @return #number The x coodinate. +function POINT_VEC2:GetX() + return self.x +end + +--- Return the y coordinate of the POINT_VEC2. +-- @param #POINT_VEC2 self +-- @return #number The y coodinate. +function POINT_VEC2:GetY() + return self.z +end + +--- Return the altitude of the land at the POINT_VEC2. +-- @param #POINT_VEC2 self +-- @return #number The land altitude. +function POINT_VEC2:GetAlt() + return land.getHeight( { x = self.x, y = self.z } ) +end + +--- Set the x coordinate of the POINT_VEC2. +-- @param #POINT_VEC2 self +-- @param #number x The x coordinate. +-- @return #POINT_VEC2 +function POINT_VEC2:SetX( x ) + self.x = x + return self +end + +--- Set the y coordinate of the POINT_VEC2. +-- @param #POINT_VEC2 self +-- @param #number y The y coordinate. +-- @return #POINT_VEC2 +function POINT_VEC2:SetY( y ) + self.z = y + return self +end + +--- Set the altitude of the POINT_VEC2. +-- @param #POINT_VEC2 self +-- @param #number Altitude The land altitude. If nothing (nil) is given, then the current land altitude is set. +-- @return #POINT_VEC2 +function POINT_VEC2:SetAlt( Altitude ) + self.y = Altitude or land.getHeight( { x = self.x, y = self.z } ) + return self +end + +--- Add to the x coordinate of the POINT_VEC2. +-- @param #POINT_VEC2 self +-- @param #number x The x coordinate. +-- @return #POINT_VEC2 +function POINT_VEC2:AddX( x ) + self.x = self.x + x + return self +end + +--- Add to the y coordinate of the POINT_VEC2. +-- @param #POINT_VEC2 self +-- @param #number y The y coordinate. +-- @return #POINT_VEC2 +function POINT_VEC2:AddY( y ) + self.z = self.z + y + return self +end + +--- Add to the current land height an altitude. +-- @param #POINT_VEC2 self +-- @param #number Altitude The Altitude to add. If nothing (nil) is given, then the current land altitude is set. +-- @return #POINT_VEC2 +function POINT_VEC2:AddAlt( Altitude ) + self.y = land.getHeight( { x = self.x, y = self.z } ) + Altitude or 0 + return self +end + + + +--- Calculate the distance from a reference @{#POINT_VEC2}. +-- @param #POINT_VEC2 self +-- @param #POINT_VEC2 PointVec2Reference The reference @{#POINT_VEC2}. +-- @return Dcs.DCSTypes#Distance The distance from the reference @{#POINT_VEC2} in meters. +function POINT_VEC2:DistanceFromPointVec2( PointVec2Reference ) + self:F2( PointVec2Reference ) + + local Distance = ( ( PointVec2Reference:GetX() - self:GetX() ) ^ 2 + ( PointVec2Reference:GetY() - self:GetY() ) ^2 ) ^0.5 + + self:T2( Distance ) + return Distance +end + +--- Calculate the distance from a reference @{DCSTypes#Vec2}. +-- @param #POINT_VEC2 self +-- @param Dcs.DCSTypes#Vec2 Vec2Reference The reference @{DCSTypes#Vec2}. +-- @return Dcs.DCSTypes#Distance The distance from the reference @{DCSTypes#Vec2} in meters. +function POINT_VEC2:DistanceFromVec2( Vec2Reference ) + self:F2( Vec2Reference ) + + local Distance = ( ( Vec2Reference.x - self:GetX() ) ^ 2 + ( Vec2Reference.y - self:GetY() ) ^2 ) ^0.5 + + self:T2( Distance ) + return Distance +end + + +--- Return no text for the altitude of the POINT_VEC2. +-- @param #POINT_VEC2 self +-- @return #string Empty string. +function POINT_VEC2:GetAltitudeText() + return '' +end + +--- Add a Distance in meters from the POINT_VEC2 orthonormal plane, with the given angle, and calculate the new POINT_VEC2. +-- @param #POINT_VEC2 self +-- @param Dcs.DCSTypes#Distance Distance The Distance to be added in meters. +-- @param Dcs.DCSTypes#Angle Angle The Angle in degrees. +-- @return #POINT_VEC2 The new calculated POINT_VEC2. +function POINT_VEC2:Translate( Distance, Angle ) + local SX = self:GetX() + local SY = self:GetY() + local Radians = Angle / 180 * math.pi + local TX = Distance * math.cos( Radians ) + SX + local TY = Distance * math.sin( Radians ) + SY + + return POINT_VEC2:New( TX, TY ) +end + +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 Core.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 + if MessageCategory:sub(-1) ~= "\n" then + self.MessageCategory = MessageCategory .. ": " + else + self.MessageCategory = MessageCategory:sub( 1, -2 ) .. ":\n" + end + else + self.MessageCategory = "" + end + + self.MessageDuration = MessageDuration or 5 + 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 Wrapper.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 a Group. +-- @param #MESSAGE self +-- @param Wrapper.Group#GROUP Group is the Group. +-- @return #MESSAGE +function MESSAGE:ToGroup( Group ) + self:F( Group.GroupName ) + + if Group then + + self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration ) + trigger.action.outTextForGroup( Group:GetID(), self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration ) + 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 a Coalition if the given Condition is true. +-- @param #MESSAGE self +-- @param CoalitionSide needs to be filled out by the defined structure of the standard scripting engine @{coalition.side}. +-- @return #MESSAGE +function MESSAGE:ToCoalitionIf( CoalitionSide, Condition ) + self:F( CoalitionSide ) + + if Condition and Condition == true then + self:ToCoalition( CoalitionSide ) + end + + return self +end + +--- Sends a MESSAGE to all players. +-- @param #MESSAGE self +-- @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 + + +--- Sends a MESSAGE to all players if the given Condition is true. +-- @param #MESSAGE self +-- @return #MESSAGE +function MESSAGE:ToAllIf( Condition ) + + if Condition and Condition == true then + self:ToCoalition( coalition.side.RED ) + self:ToCoalition( coalition.side.BLUE ) + end + + 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 ) +-- +--- This module contains the **FSM** (**F**inite **S**tate **M**achine) class and derived **FSM\_** classes. +-- ## Finite State Machines (FSM) are design patterns allowing efficient (long-lasting) processes and workflows. +-- +-- ![Banner Image](..\Presentations\FSM\Dia1.JPG) +-- +-- === +-- +-- A FSM can only be in one of a finite number of states. +-- The machine is in only one state at a time; the state it is in at any given time is called the **current state**. +-- It can change from one state to another when initiated by an **__internal__ or __external__ triggering event**, which is called a **transition**. +-- An **FSM implementation** is defined by **a list of its states**, **its initial state**, and **the triggering events** for **each possible transition**. +-- An FSM implementation is composed out of **two parts**, a set of **state transition rules**, and an implementation set of **state transition handlers**, implementing those transitions. +-- +-- The FSM class supports a **hierarchical implementation of a Finite State Machine**, +-- that is, it allows to **embed existing FSM implementations in a master FSM**. +-- FSM hierarchies allow for efficient FSM re-use, **not having to re-invent the wheel every time again** when designing complex processes. +-- +-- ![Workflow Example](..\Presentations\FSM\Dia2.JPG) +-- +-- The above diagram shows a graphical representation of a FSM implementation for a **Task**, which guides a Human towards a Zone, +-- orders him to destroy x targets and account the results. +-- Other examples of ready made FSM could be: +-- +-- * route a plane to a zone flown by a human +-- * detect targets by an AI and report to humans +-- * account for destroyed targets by human players +-- * handle AI infantry to deploy from or embark to a helicopter or airplane or vehicle +-- * let an AI patrol a zone +-- +-- The **MOOSE framework** uses extensively the FSM class and derived FSM\_ classes, +-- because **the goal of MOOSE is to simplify mission design complexity for mission building**. +-- By efficiently utilizing the FSM class and derived classes, MOOSE allows mission designers to quickly build processes. +-- **Ready made FSM-based implementations classes** exist within the MOOSE framework that **can easily be re-used, +-- and tailored** by mission designers through **the implementation of Transition Handlers**. +-- Each of these FSM implementation classes start either with: +-- +-- * an acronym **AI\_**, which indicates an FSM implementation directing **AI controlled** @{GROUP} and/or @{UNIT}. These AI\_ classes derive the @{#FSM_CONTROLLABLE} class. +-- * an acronym **TASK\_**, which indicates an FSM implementation executing a @{TASK} executed by Groups of players. These TASK\_ classes derive the @{#FSM_TASK} class. +-- * an acronym **ACT\_**, which indicates an Sub-FSM implementation, directing **Humans actions** that need to be done in a @{TASK}, seated in a @{CLIENT} (slot) or a @{UNIT} (CA join). These ACT\_ classes derive the @{#FSM_PROCESS} class. +-- +-- Detailed explanations and API specifics are further below clarified and FSM derived class specifics are described in those class documentation sections. +-- +-- ##__Dislaimer:__ +-- The FSM class development is based on a finite state machine implementation made by Conroy Kyle. +-- The state machine can be found on [github](https://github.com/kyleconroy/lua-state-machine) +-- I've reworked this development (taken the concept), and created a **hierarchical state machine** out of it, embedded within the DCS simulator. +-- Additionally, I've added extendability and created an API that allows seamless FSM implementation. +-- +-- === +-- +-- # 1) @{#FSM} class, extends @{Base#BASE} +-- +-- ![Transition Rules and Transition Handlers and Event Triggers](..\Presentations\FSM\Dia3.JPG) +-- +-- The FSM class is the base class of all FSM\_ derived classes. It implements the main functionality to define and execute Finite State Machines. +-- The derived FSM\_ classes extend the Finite State Machine functionality to run a workflow process for a specific purpose or component. +-- +-- Finite State Machines have **Transition Rules**, **Transition Handlers** and **Event Triggers**. +-- +-- The **Transition Rules** define the "Process Flow Boundaries", that is, +-- the path that can be followed hopping from state to state upon triggered events. +-- If an event is triggered, and there is no valid path found for that event, +-- an error will be raised and the FSM will stop functioning. +-- +-- The **Transition Handlers** are special methods that can be defined by the mission designer, following a defined syntax. +-- If the FSM object finds a method of such a handler, then the method will be called by the FSM, passing specific parameters. +-- The method can then define its own custom logic to implement the FSM workflow, and to conduct other actions. +-- +-- The **Event Triggers** are methods that are defined by the FSM, which the mission designer can use to implement the workflow. +-- Most of the time, these Event Triggers are used within the Transition Handler methods, so that a workflow is created running through the state machine. +-- +-- As explained above, a FSM supports **Linear State Transitions** and **Hierarchical State Transitions**, and both can be mixed to make a comprehensive FSM implementation. +-- The below documentation has a seperate chapter explaining both transition modes, taking into account the **Transition Rules**, **Transition Handlers** and **Event Triggers**. +-- +-- ## 1.1) FSM Linear Transitions +-- +-- Linear Transitions are Transition Rules allowing an FSM to transition from one or multiple possible **From** state(s) towards a **To** state upon a Triggered **Event**. +-- The Lineair transition rule evaluation will always be done from the **current state** of the FSM. +-- If no valid Transition Rule can be found in the FSM, the FSM will log an error and stop. +-- +-- ### 1.1.1) FSM Transition Rules +-- +-- The FSM has transition rules that it follows and validates, as it walks the process. +-- These rules define when an FSM can transition from a specific state towards an other specific state upon a triggered event. +-- +-- The method @{#FSM.AddTransition}() specifies a new possible Transition Rule for the FSM. +-- +-- The initial state can be defined using the method @{#FSM.SetStartState}(). The default start state of an FSM is "None". +-- +-- Find below an example of a Linear Transition Rule definition for an FSM. +-- +-- local Fsm3Switch = FSM:New() -- #FsmDemo +-- FsmSwitch:SetStartState( "Off" ) +-- FsmSwitch:AddTransition( "Off", "SwitchOn", "On" ) +-- FsmSwitch:AddTransition( "Off", "SwitchMiddle", "Middle" ) +-- FsmSwitch:AddTransition( "On", "SwitchOff", "Off" ) +-- FsmSwitch:AddTransition( "Middle", "SwitchOff", "Off" ) +-- +-- The above code snippet models a 3-way switch Linear Transition: +-- +-- * It can be switched **On** by triggering event **SwitchOn**. +-- * It can be switched to the **Middle** position, by triggering event **SwitchMiddle**. +-- * It can be switched **Off** by triggering event **SwitchOff**. +-- * Note that once the Switch is **On** or **Middle**, it can only be switched **Off**. +-- +-- ### Some additional comments: +-- +-- Note that Linear Transition Rules **can be declared in a few variations**: +-- +-- * The From states can be **a table of strings**, indicating that the transition rule will be valid **if the current state** of the FSM will be **one of the given From states**. +-- * The From state can be a **"*"**, indicating that **the transition rule will always be valid**, regardless of the current state of the FSM. +-- +-- The below code snippet shows how the two last lines can be rewritten and consensed. +-- +-- FsmSwitch:AddTransition( { "On", "Middle" }, "SwitchOff", "Off" ) +-- +-- ### 1.1.2) Transition Handling +-- +-- ![Transition Handlers](..\Presentations\FSM\Dia4.JPG) +-- +-- An FSM transitions in **4 moments** when an Event is being triggered and processed. +-- The mission designer can define for each moment specific logic within methods implementations following a defined API syntax. +-- These methods define the flow of the FSM process; because in those methods the FSM Internal Events will be triggered. +-- +-- * To handle **State** transition moments, create methods starting with OnLeave or OnEnter concatenated with the State name. +-- * To handle **Event** transition moments, create methods starting with OnBefore or OnAfter concatenated with the Event name. +-- +-- **The OnLeave and OnBefore transition methods may return false, which will cancel the transition!** +-- +-- Transition Handler methods need to follow the above specified naming convention, but are also passed parameters from the FSM. +-- These parameters are on the correct order: From, Event, To: +-- +-- * From = A string containing the From state. +-- * Event = A string containing the Event name that was triggered. +-- * To = A string containing the To state. +-- +-- On top, each of these methods can have a variable amount of parameters passed. See the example in section [1.1.3](#1.1.3\)-event-triggers). +-- +-- ### 1.1.3) Event Triggers +-- +-- ![Event Triggers](..\Presentations\FSM\Dia5.JPG) +-- +-- The FSM creates for each Event two **Event Trigger methods**. +-- There are two modes how Events can be triggered, which is **synchronous** and **asynchronous**: +-- +-- * The method **FSM:Event()** triggers an Event that will be processed **synchronously** or **immediately**. +-- * The method **FSM:__Event( __seconds__ )** triggers an Event that will be processed **asynchronously** over time, waiting __x seconds__. +-- +-- The destinction between these 2 Event Trigger methods are important to understand. An asynchronous call will "log" the Event Trigger to be executed at a later time. +-- Processing will just continue. Synchronous Event Trigger methods are useful to change states of the FSM immediately, but may have a larger processing impact. +-- +-- The following example provides a little demonstration on the difference between synchronous and asynchronous Event Triggering. +-- +-- function FSM:OnAfterEvent( From, Event, To, Amount ) +-- self:T( { Amount = Amount } ) +-- end +-- +-- local Amount = 1 +-- FSM:__Event( 5, Amount ) +-- +-- Amount = Amount + 1 +-- FSM:Event( Text, Amount ) +-- +-- In this example, the **:OnAfterEvent**() Transition Handler implementation will get called when **Event** is being triggered. +-- Before we go into more detail, let's look at the last 4 lines of the example. +-- The last line triggers synchronously the **Event**, and passes Amount as a parameter. +-- The 3rd last line of the example triggers asynchronously **Event**. +-- Event will be processed after 5 seconds, and Amount is given as a parameter. +-- +-- The output of this little code fragment will be: +-- +-- * Amount = 2 +-- * Amount = 2 +-- +-- Because ... When Event was asynchronously processed after 5 seconds, Amount was set to 2. So be careful when processing and passing values and objects in asynchronous processing! +-- +-- ### 1.1.4) Linear Transition Example +-- +-- This example is fully implemented in the MOOSE test mission on GITHUB: [FSM-100 - Transition Explanation](https://github.com/FlightControl-Master/MOOSE/blob/master/Moose%20Test%20Missions/FSM%20-%20Finite%20State%20Machine/FSM-100%20-%20Transition%20Explanation/FSM-100%20-%20Transition%20Explanation.lua) +-- +-- It models a unit standing still near Batumi, and flaring every 5 seconds while switching between a Green flare and a Red flare. +-- The purpose of this example is not to show how exciting flaring is, but it demonstrates how a Linear Transition FSM can be build. +-- Have a look at the source code. The source code is also further explained below in this section. +-- +-- The example creates a new FsmDemo object from class FSM. +-- It will set the start state of FsmDemo to state **Green**. +-- Two Linear Transition Rules are created, where upon the event **Switch**, +-- the FsmDemo will transition from state **Green** to **Red** and from **Red** back to **Green**. +-- +-- ![Transition Example](..\Presentations\FSM\Dia6.JPG) +-- +-- local FsmDemo = FSM:New() -- #FsmDemo +-- FsmDemo:SetStartState( "Green" ) +-- FsmDemo:AddTransition( "Green", "Switch", "Red" ) +-- FsmDemo:AddTransition( "Red", "Switch", "Green" ) +-- +-- In the above example, the FsmDemo could flare every 5 seconds a Green or a Red flare into the air. +-- The next code implements this through the event handling method **OnAfterSwitch**. +-- +-- ![Transition Flow](..\Presentations\FSM\Dia7.JPG) +-- +-- function FsmDemo:OnAfterSwitch( From, Event, To, FsmUnit ) +-- self:T( { From, Event, To, FsmUnit } ) +-- +-- if From == "Green" then +-- FsmUnit:Flare(FLARECOLOR.Green) +-- else +-- if From == "Red" then +-- FsmUnit:Flare(FLARECOLOR.Red) +-- end +-- end +-- self:__Switch( 5, FsmUnit ) -- Trigger the next Switch event to happen in 5 seconds. +-- end +-- +-- FsmDemo:__Switch( 5, FsmUnit ) -- Trigger the first Switch event to happen in 5 seconds. +-- +-- The OnAfterSwitch implements a loop. The last line of the code fragment triggers the Switch Event within 5 seconds. +-- Upon the event execution (after 5 seconds), the OnAfterSwitch method is called of FsmDemo (cfr. the double point notation!!! ":"). +-- The OnAfterSwitch method receives from the FSM the 3 transition parameter details ( From, Event, To ), +-- and one additional parameter that was given when the event was triggered, which is in this case the Unit that is used within OnSwitchAfter. +-- +-- function FsmDemo:OnAfterSwitch( From, Event, To, FsmUnit ) +-- +-- For debugging reasons the received parameters are traced within the DCS.log. +-- +-- self:T( { From, Event, To, FsmUnit } ) +-- +-- The method will check if the From state received is either "Green" or "Red" and will flare the respective color from the FsmUnit. +-- +-- if From == "Green" then +-- FsmUnit:Flare(FLARECOLOR.Green) +-- else +-- if From == "Red" then +-- FsmUnit:Flare(FLARECOLOR.Red) +-- end +-- end +-- +-- It is important that the Switch event is again triggered, otherwise, the FsmDemo would stop working after having the first Event being handled. +-- +-- FsmDemo:__Switch( 5, FsmUnit ) -- Trigger the next Switch event to happen in 5 seconds. +-- +-- The below code fragment extends the FsmDemo, demonstrating multiple **From states declared as a table**, adding a **Linear Transition Rule**. +-- The new event **Stop** will cancel the Switching process. +-- The transition for event Stop can be executed if the current state of the FSM is either "Red" or "Green". +-- +-- local FsmDemo = FSM:New() -- #FsmDemo +-- FsmDemo:SetStartState( "Green" ) +-- FsmDemo:AddTransition( "Green", "Switch", "Red" ) +-- FsmDemo:AddTransition( "Red", "Switch", "Green" ) +-- FsmDemo:AddTransition( { "Red", "Green" }, "Stop", "Stopped" ) +-- +-- The transition for event Stop can also be simplified, as any current state of the FSM is valid. +-- +-- FsmDemo:AddTransition( "*", "Stop", "Stopped" ) +-- +-- So... When FsmDemo:Stop() is being triggered, the state of FsmDemo will transition from Red or Green to Stopped. +-- And there is no transition handling method defined for that transition, thus, no new event is being triggered causing the FsmDemo process flow to halt. +-- +-- ## 1.5) FSM Hierarchical Transitions +-- +-- Hierarchical Transitions allow to re-use readily available and implemented FSMs. +-- This becomes in very useful for mission building, where mission designers build complex processes and workflows, +-- combining smaller FSMs to one single FSM. +-- +-- The FSM can embed **Sub-FSMs** that will execute and return **multiple possible Return (End) States**. +-- Depending upon **which state is returned**, the main FSM can continue the flow **triggering specific events**. +-- +-- The method @{#FSM.AddProcess}() adds a new Sub-FSM to the FSM. +-- +-- ==== +-- +-- # **API CHANGE HISTORY** +-- +-- The underlying change log documents the API changes. Please read this carefully. The following notation is used: +-- +-- * **Added** parts are expressed in bold type face. +-- * _Removed_ parts are expressed in italic type face. +-- +-- YYYY-MM-DD: CLASS:**NewFunction**( Params ) replaces CLASS:_OldFunction_( Params ) +-- YYYY-MM-DD: CLASS:**NewFunction( Params )** added +-- +-- Hereby the change log: +-- +-- * 2016-12-18: Released. +-- +-- === +-- +-- # **AUTHORS and CONTRIBUTIONS** +-- +-- ### Contributions: +-- +-- * [**Pikey**](https://forums.eagle.ru/member.php?u=62835): Review of documentation & advice for improvements. +-- +-- ### Authors: +-- +-- * [**FlightControl**](https://forums.eagle.ru/member.php?u=89536): Design & Programming & documentation. +-- +-- @module Fsm + +do -- FSM + + --- FSM class + -- @type FSM + -- @extends Core.Base#BASE + FSM = { + ClassName = "FSM", + } + + --- Creates a new FSM object. + -- @param #FSM self + -- @return #FSM + function FSM:New( FsmT ) + + -- Inherits from BASE + self = BASE:Inherit( self, BASE:New() ) + + self.options = options or {} + self.options.subs = self.options.subs or {} + self.current = self.options.initial or 'none' + self.Events = {} + self.subs = {} + self.endstates = {} + + self.Scores = {} + + self._StartState = "none" + self._Transitions = {} + self._Processes = {} + self._EndStates = {} + self._Scores = {} + self._EventSchedules = {} + + self.CallScheduler = SCHEDULER:New( self ) + + + return self + end + + + --- Sets the start state of the FSM. + -- @param #FSM self + -- @param #string State A string defining the start state. + function FSM:SetStartState( State ) + + self._StartState = State + self.current = State + end + + + --- Returns the start state of the FSM. + -- @param #FSM self + -- @return #string A string containing the start state. + function FSM:GetStartState() + + return self._StartState or {} + end + + --- Add a new transition rule to the FSM. + -- A transition rule defines when and if the FSM can transition from a state towards another state upon a triggered event. + -- @param #FSM self + -- @param #table From Can contain a string indicating the From state or a table of strings containing multiple From states. + -- @param #string Event The Event name. + -- @param #string To The To state. + function FSM:AddTransition( From, Event, To ) + + local Transition = {} + Transition.From = From + Transition.Event = Event + Transition.To = To + + self:T( Transition ) + + self._Transitions[Transition] = Transition + self:_eventmap( self.Events, Transition ) + end + + + --- Returns a table of the transition rules defined within the FSM. + -- @return #table + function FSM:GetTransitions() + + return self._Transitions or {} + end + + --- Set the default @{Process} template with key ProcessName providing the ProcessClass and the process object when it is assigned to a @{Controllable} by the task. + -- @param #FSM self + -- @param #table From Can contain a string indicating the From state or a table of strings containing multiple From states. + -- @param #string Event The Event name. + -- @param Core.Fsm#FSM_PROCESS Process An sub-process FSM. + -- @param #table ReturnEvents A table indicating for which returned events of the SubFSM which Event must be triggered in the FSM. + -- @return Core.Fsm#FSM_PROCESS The SubFSM. + function FSM:AddProcess( From, Event, Process, ReturnEvents ) + self:T( { From, Event, Process, ReturnEvents } ) + + local Sub = {} + Sub.From = From + Sub.Event = Event + Sub.fsm = Process + Sub.StartEvent = "Start" + Sub.ReturnEvents = ReturnEvents + + self._Processes[Sub] = Sub + + self:_submap( self.subs, Sub, nil ) + + self:AddTransition( From, Event, From ) + + return Process + end + + + --- Returns a table of the SubFSM rules defined within the FSM. + -- @return #table + function FSM:GetProcesses() + + return self._Processes or {} + end + + function FSM:GetProcess( From, Event ) + + for ProcessID, Process in pairs( self:GetProcesses() ) do + if Process.From == From and Process.Event == Event then + self:T( Process ) + return Process.fsm + end + end + + error( "Sub-Process from state " .. From .. " with event " .. Event .. " not found!" ) + end + + --- Adds an End state. + function FSM:AddEndState( State ) + + self._EndStates[State] = State + self.endstates[State] = State + end + + --- Returns the End states. + function FSM:GetEndStates() + + return self._EndStates or {} + end + + + --- Adds a score for the FSM to be achieved. + -- @param #FSM self + -- @param #string State is the state of the process when the score needs to be given. (See the relevant state descriptions of the process). + -- @param #string ScoreText is a text describing the score that is given according the status. + -- @param #number Score is a number providing the score of the status. + -- @return #FSM self + function FSM:AddScore( State, ScoreText, Score ) + self:F2( { State, ScoreText, Score } ) + + self._Scores[State] = self._Scores[State] or {} + self._Scores[State].ScoreText = ScoreText + self._Scores[State].Score = Score + + return self + end + + --- Adds a score for the FSM_PROCESS to be achieved. + -- @param #FSM self + -- @param #string From is the From State of the main process. + -- @param #string Event is the Event of the main process. + -- @param #string State is the state of the process when the score needs to be given. (See the relevant state descriptions of the process). + -- @param #string ScoreText is a text describing the score that is given according the status. + -- @param #number Score is a number providing the score of the status. + -- @return #FSM self + function FSM:AddScoreProcess( From, Event, State, ScoreText, Score ) + self:F2( { Event, State, ScoreText, Score } ) + + local Process = self:GetProcess( From, Event ) + + self:T( { Process = Process._Name, Scores = Process._Scores, State = State, ScoreText = ScoreText, Score = Score } ) + Process._Scores[State] = Process._Scores[State] or {} + Process._Scores[State].ScoreText = ScoreText + Process._Scores[State].Score = Score + + return Process + end + + --- Returns a table with the scores defined. + function FSM:GetScores() + + return self._Scores or {} + end + + --- Returns a table with the Subs defined. + function FSM:GetSubs() + + return self.options.subs + end + + + function FSM:LoadCallBacks( CallBackTable ) + + for name, callback in pairs( CallBackTable or {} ) do + self[name] = callback + end + + end + + function FSM:_eventmap( Events, EventStructure ) + + local Event = EventStructure.Event + local __Event = "__" .. EventStructure.Event + self[Event] = self[Event] or self:_create_transition(Event) + self[__Event] = self[__Event] or self:_delayed_transition(Event) + self:T( "Added methods: " .. Event .. ", " .. __Event ) + Events[Event] = self.Events[Event] or { map = {} } + self:_add_to_map( Events[Event].map, EventStructure ) + + end + + function FSM:_submap( subs, sub, name ) + self:F( { sub = sub, name = name } ) + subs[sub.From] = subs[sub.From] or {} + subs[sub.From][sub.Event] = subs[sub.From][sub.Event] or {} + + -- Make the reference table weak. + -- setmetatable( subs[sub.From][sub.Event], { __mode = "k" } ) + + subs[sub.From][sub.Event][sub] = {} + subs[sub.From][sub.Event][sub].fsm = sub.fsm + subs[sub.From][sub.Event][sub].StartEvent = sub.StartEvent + subs[sub.From][sub.Event][sub].ReturnEvents = sub.ReturnEvents or {} -- these events need to be given to find the correct continue event ... if none given, the processing will stop. + subs[sub.From][sub.Event][sub].name = name + subs[sub.From][sub.Event][sub].fsmparent = self + end + + + function FSM:_call_handler( handler, params, EventName ) + if self[handler] then + self:T( "Calling " .. handler ) + self._EventSchedules[EventName] = nil + local Value = self[handler]( self, unpack(params) ) + return Value + end + end + + function FSM._handler( self, EventName, ... ) + + local Can, to = self:can( EventName ) + + if to == "*" then + to = self.current + end + + if Can then + local from = self.current + local params = { from, EventName, to, ... } + + if self.Controllable then + self:T( "FSM Transition for " .. self.Controllable.ControllableName .. " :" .. self.current .. " --> " .. EventName .. " --> " .. to ) + else + self:T( "FSM Transition:" .. self.current .. " --> " .. EventName .. " --> " .. to ) + end + + if ( self:_call_handler("onbefore" .. EventName, params, EventName ) == false ) + or ( self:_call_handler("OnBefore" .. EventName, params, EventName ) == false ) + or ( self:_call_handler("onleave" .. from, params, EventName ) == false ) + or ( self:_call_handler("OnLeave" .. from, params, EventName ) == false ) then + self:T( "Cancel Transition" ) + return false + end + + self.current = to + + local execute = true + + local subtable = self:_gosub( from, EventName ) + for _, sub in pairs( subtable ) do + --if sub.nextevent then + -- self:F2( "nextevent = " .. sub.nextevent ) + -- self[sub.nextevent]( self ) + --end + self:T( "calling sub start event: " .. sub.StartEvent ) + sub.fsm.fsmparent = self + sub.fsm.ReturnEvents = sub.ReturnEvents + sub.fsm[sub.StartEvent]( sub.fsm ) + execute = false + end + + local fsmparent, Event = self:_isendstate( to ) + if fsmparent and Event then + self:F2( { "end state: ", fsmparent, Event } ) + self:_call_handler("onenter" .. to, params, EventName ) + self:_call_handler("OnEnter" .. to, params, EventName ) + self:_call_handler("onafter" .. EventName, params, EventName ) + self:_call_handler("OnAfter" .. EventName, params, EventName ) + self:_call_handler("onstatechange", params, EventName ) + fsmparent[Event]( fsmparent ) + execute = false + end + + if execute then + -- only execute the call if the From state is not equal to the To state! Otherwise this function should never execute! + --if from ~= to then + self:_call_handler("onenter" .. to, params, EventName ) + self:_call_handler("OnEnter" .. to, params, EventName ) + --end + + self:_call_handler("onafter" .. EventName, params, EventName ) + self:_call_handler("OnAfter" .. EventName, params, EventName ) + + self:_call_handler("onstatechange", params, EventName ) + end + else + self:T( "Cannot execute transition." ) + self:T( { From = self.current, Event = EventName, To = to, Can = Can } ) + end + + return nil + end + + function FSM:_delayed_transition( EventName ) + return function( self, DelaySeconds, ... ) + self:T2( "Delayed Event: " .. EventName ) + local CallID = 0 + if DelaySeconds ~= nil then + if DelaySeconds < 0 then -- Only call the event ONCE! + DelaySeconds = math.abs( DelaySeconds ) + if not self._EventSchedules[EventName] then + CallID = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1 ) + self._EventSchedules[EventName] = CallID + else + -- reschedule + end + else + CallID = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1 ) + end + else + error( "FSM: An asynchronous event trigger requires a DelaySeconds parameter!!! This can be positive or negative! Sorry, but will not process this." ) + end + self:T2( { CallID = CallID } ) + end + end + + function FSM:_create_transition( EventName ) + return function( self, ... ) return self._handler( self, EventName , ... ) end + end + + function FSM:_gosub( ParentFrom, ParentEvent ) + local fsmtable = {} + if self.subs[ParentFrom] and self.subs[ParentFrom][ParentEvent] then + self:T( { ParentFrom, ParentEvent, self.subs[ParentFrom], self.subs[ParentFrom][ParentEvent] } ) + return self.subs[ParentFrom][ParentEvent] + else + return {} + end + end + + function FSM:_isendstate( Current ) + local FSMParent = self.fsmparent + if FSMParent and self.endstates[Current] then + self:T( { state = Current, endstates = self.endstates, endstate = self.endstates[Current] } ) + FSMParent.current = Current + local ParentFrom = FSMParent.current + self:T( ParentFrom ) + self:T( self.ReturnEvents ) + local Event = self.ReturnEvents[Current] + self:T( { ParentFrom, Event, self.ReturnEvents } ) + if Event then + return FSMParent, Event + else + self:T( { "Could not find parent event name for state ", ParentFrom } ) + end + end + + return nil + end + + function FSM:_add_to_map( Map, Event ) + self:F3( { Map, Event } ) + if type(Event.From) == 'string' then + Map[Event.From] = Event.To + else + for _, From in ipairs(Event.From) do + Map[From] = Event.To + end + end + self:T3( { Map, Event } ) + end + + function FSM:GetState() + return self.current + end + + + function FSM:Is( State ) + return self.current == State + end + + function FSM:is(state) + return self.current == state + end + + function FSM:can(e) + local Event = self.Events[e] + self:F3( { self.current, Event } ) + local To = Event and Event.map[self.current] or Event.map['*'] + return To ~= nil, To + end + + function FSM:cannot(e) + return not self:can(e) + end + +end + +do -- FSM_CONTROLLABLE + + --- FSM_CONTROLLABLE class + -- @type FSM_CONTROLLABLE + -- @field Wrapper.Controllable#CONTROLLABLE Controllable + -- @extends Core.Fsm#FSM + FSM_CONTROLLABLE = { + ClassName = "FSM_CONTROLLABLE", + } + + --- Creates a new FSM_CONTROLLABLE object. + -- @param #FSM_CONTROLLABLE self + -- @param #table FSMT Finite State Machine Table + -- @param Wrapper.Controllable#CONTROLLABLE Controllable (optional) The CONTROLLABLE object that the FSM_CONTROLLABLE governs. + -- @return #FSM_CONTROLLABLE + function FSM_CONTROLLABLE:New( FSMT, Controllable ) + + -- Inherits from BASE + local self = BASE:Inherit( self, FSM:New( FSMT ) ) -- Core.Fsm#FSM_CONTROLLABLE + + if Controllable then + self:SetControllable( Controllable ) + end + + return self + end + + --- Sets the CONTROLLABLE object that the FSM_CONTROLLABLE governs. + -- @param #FSM_CONTROLLABLE self + -- @param Wrapper.Controllable#CONTROLLABLE FSMControllable + -- @return #FSM_CONTROLLABLE + function FSM_CONTROLLABLE:SetControllable( FSMControllable ) + self:F( FSMControllable ) + self.Controllable = FSMControllable + end + + --- Gets the CONTROLLABLE object that the FSM_CONTROLLABLE governs. + -- @param #FSM_CONTROLLABLE self + -- @return Wrapper.Controllable#CONTROLLABLE + function FSM_CONTROLLABLE:GetControllable() + return self.Controllable + end + + function FSM_CONTROLLABLE:_call_handler( handler, params, EventName ) + + local ErrorHandler = function( errmsg ) + + env.info( "Error in SCHEDULER function:" .. errmsg ) + if debug ~= nil then + env.info( debug.traceback() ) + end + + return errmsg + end + + if self[handler] then + self:F3( "Calling " .. handler ) + self._EventSchedules[EventName] = nil + local Result, Value = xpcall( function() return self[handler]( self, self.Controllable, unpack( params ) ) end, ErrorHandler ) + return Value + --return self[handler]( self, self.Controllable, unpack( params ) ) + end + end + +end + +do -- FSM_PROCESS + + --- FSM_PROCESS class + -- @type FSM_PROCESS + -- @field Tasking.Task#TASK Task + -- @extends Core.Fsm#FSM_CONTROLLABLE + FSM_PROCESS = { + ClassName = "FSM_PROCESS", + } + + --- Creates a new FSM_PROCESS object. + -- @param #FSM_PROCESS self + -- @return #FSM_PROCESS + function FSM_PROCESS:New( Controllable, Task ) + + local self = BASE:Inherit( self, FSM_CONTROLLABLE:New() ) -- Core.Fsm#FSM_PROCESS + + self:F( Controllable, Task ) + + self:Assign( Controllable, Task ) + + return self + end + + function FSM_PROCESS:Init( FsmProcess ) + self:T( "No Initialisation" ) + end + + --- Creates a new FSM_PROCESS object based on this FSM_PROCESS. + -- @param #FSM_PROCESS self + -- @return #FSM_PROCESS + function FSM_PROCESS:Copy( Controllable, Task ) + self:T( { self:GetClassNameAndID() } ) + + local NewFsm = self:New( Controllable, Task ) -- Core.Fsm#FSM_PROCESS + + NewFsm:Assign( Controllable, Task ) + + -- Polymorphic call to initialize the new FSM_PROCESS based on self FSM_PROCESS + NewFsm:Init( self ) + + -- Set Start State + NewFsm:SetStartState( self:GetStartState() ) + + -- Copy Transitions + for TransitionID, Transition in pairs( self:GetTransitions() ) do + NewFsm:AddTransition( Transition.From, Transition.Event, Transition.To ) + end + + -- Copy Processes + for ProcessID, Process in pairs( self:GetProcesses() ) do + self:T( { Process} ) + local FsmProcess = NewFsm:AddProcess( Process.From, Process.Event, Process.fsm:Copy( Controllable, Task ), Process.ReturnEvents ) + end + + -- Copy End States + for EndStateID, EndState in pairs( self:GetEndStates() ) do + self:T( EndState ) + NewFsm:AddEndState( EndState ) + end + + -- Copy the score tables + for ScoreID, Score in pairs( self:GetScores() ) do + self:T( Score ) + NewFsm:AddScore( ScoreID, Score.ScoreText, Score.Score ) + end + + return NewFsm + end + + --- Sets the task of the process. + -- @param #FSM_PROCESS self + -- @param Tasking.Task#TASK Task + -- @return #FSM_PROCESS + function FSM_PROCESS:SetTask( Task ) + + self.Task = Task + + return self + end + + --- Gets the task of the process. + -- @param #FSM_PROCESS self + -- @return Tasking.Task#TASK + function FSM_PROCESS:GetTask() + + return self.Task + end + + --- Gets the mission of the process. + -- @param #FSM_PROCESS self + -- @return Tasking.Mission#MISSION + function FSM_PROCESS:GetMission() + + return self.Task.Mission + end + + --- Gets the mission of the process. + -- @param #FSM_PROCESS self + -- @return Tasking.CommandCenter#COMMANDCENTER + function FSM_PROCESS:GetCommandCenter() + + return self:GetTask():GetMission():GetCommandCenter() + end + +-- TODO: Need to check and fix that an FSM_PROCESS is only for a UNIT. Not for a GROUP. + + --- Send a message of the @{Task} to the Group of the Unit. +-- @param #FSM_PROCESS self +function FSM_PROCESS:Message( Message ) + self:F( { Message = Message } ) + + local CC = self:GetCommandCenter() + local TaskGroup = self.Controllable:GetGroup() + + local PlayerName = self.Controllable:GetPlayerName() -- Only for a unit + PlayerName = PlayerName and " (" .. PlayerName .. ")" or "" -- If PlayerName is nil, then keep it nil, otherwise add brackets. + local Callsign = self.Controllable:GetCallsign() + local Prefix = Callsign and " @ " .. Callsign .. PlayerName or "" + + Message = Prefix .. ": " .. Message + CC:MessageToGroup( Message, TaskGroup ) +end + + + + + --- Assign the process to a @{Unit} and activate the process. + -- @param #FSM_PROCESS self + -- @param Task.Tasking#TASK Task + -- @param Wrapper.Unit#UNIT ProcessUnit + -- @return #FSM_PROCESS self + function FSM_PROCESS:Assign( ProcessUnit, Task ) + self:T( { Task, ProcessUnit } ) + + self:SetControllable( ProcessUnit ) + self:SetTask( Task ) + + --self.ProcessGroup = ProcessUnit:GetGroup() + + return self + end + + function FSM_PROCESS:onenterAssigned( ProcessUnit ) + self:T( "Assign" ) + + self.Task:Assign() + end + + function FSM_PROCESS:onenterFailed( ProcessUnit ) + self:T( "Failed" ) + + self.Task:Fail() + end + + function FSM_PROCESS:onenterSuccess( ProcessUnit ) + self:T( "Success" ) + + self.Task:Success() + end + + --- StateMachine callback function for a FSM_PROCESS + -- @param #FSM_PROCESS self + -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function FSM_PROCESS:onstatechange( ProcessUnit, From, Event, To, Dummy ) + self:T( { ProcessUnit, From, Event, To, Dummy, self:IsTrace() } ) + + if self:IsTrace() then + MESSAGE:New( "@ Process " .. self:GetClassNameAndID() .. " : " .. Event .. " changed to state " .. To, 2 ):ToAll() + end + + self:T( self._Scores[To] ) + -- TODO: This needs to be reworked with a callback functions allocated within Task, and set within the mission script from the Task Objects... + if self._Scores[To] then + + local Task = self.Task + local Scoring = Task:GetScoring() + if Scoring then + Scoring:_AddMissionTaskScore( Task.Mission, ProcessUnit, self._Scores[To].ScoreText, self._Scores[To].Score ) + end + end + end + +end + +do -- FSM_TASK + + --- FSM_TASK class + -- @type FSM_TASK + -- @field Tasking.Task#TASK Task + -- @extends Core.Fsm#FSM + FSM_TASK = { + ClassName = "FSM_TASK", + } + + --- Creates a new FSM_TASK object. + -- @param #FSM_TASK self + -- @param #table FSMT + -- @param Tasking.Task#TASK Task + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return #FSM_TASK + function FSM_TASK:New( FSMT ) + + local self = BASE:Inherit( self, FSM_CONTROLLABLE:New( FSMT ) ) -- Core.Fsm#FSM_TASK + + self["onstatechange"] = self.OnStateChange + + return self + end + + function FSM_TASK:_call_handler( handler, params, EventName ) + if self[handler] then + self:T( "Calling " .. handler ) + self._EventSchedules[EventName] = nil + return self[handler]( self, unpack( params ) ) + end + end + +end -- FSM_TASK + +do -- FSM_SET + + --- FSM_SET class + -- @type FSM_SET + -- @field Core.Set#SET_BASE Set + -- @extends Core.Fsm#FSM + FSM_SET = { + ClassName = "FSM_SET", + } + + --- Creates a new FSM_SET object. + -- @param #FSM_SET self + -- @param #table FSMT Finite State Machine Table + -- @param Set_SET_BASE FSMSet (optional) The Set object that the FSM_SET governs. + -- @return #FSM_SET + function FSM_SET:New( FSMSet ) + + -- Inherits from BASE + self = BASE:Inherit( self, FSM:New() ) -- Core.Fsm#FSM_SET + + if FSMSet then + self:Set( FSMSet ) + end + + return self + end + + --- Sets the SET_BASE object that the FSM_SET governs. + -- @param #FSM_SET self + -- @param Core.Set#SET_BASE FSMSet + -- @return #FSM_SET + function FSM_SET:Set( FSMSet ) + self:F( FSMSet ) + self.Set = FSMSet + end + + --- Gets the SET_BASE object that the FSM_SET governs. + -- @param #FSM_SET self + -- @return Core.Set#SET_BASE + function FSM_SET:Get() + return self.Controllable + end + + function FSM_SET:_call_handler( handler, params, EventName ) + if self[handler] then + self:T( "Calling " .. handler ) + self._EventSchedules[EventName] = nil + return self[handler]( self, self.Set, unpack( params ) ) + end + end + +end -- FSM_SET + +--- 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 + +--- The OBJECT class +-- @type OBJECT +-- @extends Core.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 Dcs.DCSWrapper.Object#Object ObjectName The Object name +-- @return #OBJECT self +function OBJECT:New( ObjectName, Test ) + local self = BASE:Inherit( self, BASE:New() ) + self:F2( ObjectName ) + self.ObjectName = ObjectName + + return self +end + + +--- Returns the unit's unique identifier. +-- @param Wrapper.Object#OBJECT self +-- @return Dcs.DCSWrapper.Object#Object.ID ObjectID +-- @return #nil The DCS Object is not existing or alive. +function OBJECT:GetID() + self:F2( self.ObjectName ) + + local DCSObject = self:GetDCSObject() + + if DCSObject then + local ObjectID = DCSObject:getID() + return ObjectID + end + + return nil +end + +--- Destroys the OBJECT. +-- @param #OBJECT self +-- @return #nil The DCS Unit is not existing or alive. +function OBJECT:Destroy() + self:F2( self.ObjectName ) + + local DCSObject = self:GetDCSObject() + + if DCSObject then + + DCSObject:destroy() + end + + return nil +end + + + + +--- This module contains the IDENTIFIABLE class. +-- +-- 1) @{#IDENTIFIABLE} class, extends @{Object#OBJECT} +-- =============================================================== +-- The @{#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.New}(): Create a IDENTIFIABLE instance. +-- +-- 1.2) IDENTIFIABLE methods: +-- -------------------------- +-- The following methods can be used to identify an identifiable object: +-- +-- * @{#IDENTIFIABLE.GetName}(): Returns the name of the Identifiable. +-- * @{#IDENTIFIABLE.IsAlive}(): Returns if the Identifiable is alive. +-- * @{#IDENTIFIABLE.GetTypeName}(): Returns the type name of the Identifiable. +-- * @{#IDENTIFIABLE.GetCoalition}(): Returns the coalition of the Identifiable. +-- * @{#IDENTIFIABLE.GetCountry}(): Returns the country of the Identifiable. +-- * @{#IDENTIFIABLE.GetDesc}(): Returns the descriptor structure of the Identifiable. +-- +-- +-- === +-- +-- @module Identifiable + +--- The IDENTIFIABLE class +-- @type IDENTIFIABLE +-- @extends Wrapper.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 Dcs.DCSWrapper.Identifiable#Identifiable IdentifiableName The DCS Identifiable name +-- @return #IDENTIFIABLE self +function IDENTIFIABLE:New( IdentifiableName ) + local self = BASE:Inherit( self, OBJECT:New( IdentifiableName ) ) + self:F2( IdentifiableName ) + self.IdentifiableName = IdentifiableName + return self +end + +--- Returns if the Identifiable is alive. +-- @param #IDENTIFIABLE self +-- @return #boolean true if Identifiable is alive. +-- @return #nil The DCS Identifiable is not existing or alive. +function IDENTIFIABLE:IsAlive() + self:F3( 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 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 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 category of the DCS Identifiable. +-- @param #IDENTIFIABLE self +-- @return Dcs.DCSWrapper.Object#Object.Category The category ID +function IDENTIFIABLE:GetCategory() + self:F2( self.ObjectName ) + + local DCSObject = self:GetDCSObject() + if DCSObject then + local ObjectCategory = DCSObject:getCategory() + self:T3( ObjectCategory ) + return ObjectCategory + end + + return nil +end + + +--- Returns the DCS Identifiable category name as defined within the DCS Identifiable Descriptor. +-- @param #IDENTIFIABLE self +-- @return #string The DCS Identifiable Category Name +function IDENTIFIABLE:GetCategoryName() + local DCSIdentifiable = self:GetDCSObject() + + if DCSIdentifiable then + local IdentifiableCategoryName = _CategoryName[ self:GetDesc().category ] + return IdentifiableCategoryName + end + + self:E( self.ClassName .. " " .. self.IdentifiableName .. " not found!" ) + return nil +end + +--- Returns coalition of the Identifiable. +-- @param #IDENTIFIABLE self +-- @return Dcs.DCSCoalitionWrapper.Object#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 self +-- @return Dcs.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 self +-- @return Dcs.DCSWrapper.Identifiable#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 + +--- Gets the CallSign of the IDENTIFIABLE, which is a blank by default. +-- @param #IDENTIFIABLE self +-- @return #string The CallSign of the IDENTIFIABLE. +function IDENTIFIABLE:GetCallsign() + return '' +end + + +function IDENTIFIABLE:GetThreatLevel() + + return 0, "Scenery" +end +--- This module contains the POSITIONABLE class. +-- +-- 1) @{Positionable#POSITIONABLE} class, extends @{Identifiable#IDENTIFIABLE} +-- =========================================================== +-- The @{Positionable#POSITIONABLE} class is a wrapper class to handle the POSITIONABLE objects: +-- +-- * Support all DCS APIs. +-- * Enhance with POSITIONABLE specific APIs not in the DCS API set. +-- * Manage the "state" of the POSITIONABLE. +-- +-- 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 + +--- The POSITIONABLE class +-- @type POSITIONABLE +-- @extends Wrapper.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 Dcs.DCSWrapper.Positionable#Positionable PositionableName The POSITIONABLE name +-- @return #POSITIONABLE self +function POSITIONABLE:New( PositionableName ) + local self = BASE:Inherit( self, IDENTIFIABLE:New( PositionableName ) ) + + self.PositionableName = PositionableName + return self +end + +--- Returns the @{DCSTypes#Position3} position vectors indicating the point and direction vectors in 3D of the POSITIONABLE within the mission. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return Dcs.DCSTypes#Position The 3D position vectors of the POSITIONABLE. +-- @return #nil The POSITIONABLE is not existing or alive. +function POSITIONABLE:GetPositionVec3() + self:F2( self.PositionableName ) + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + local PositionablePosition = DCSPositionable:getPosition().p + self:T3( PositionablePosition ) + return PositionablePosition + end + + return nil +end + +--- Returns the @{DCSTypes#Vec2} vector indicating the point in 2D of the POSITIONABLE within the mission. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return Dcs.DCSTypes#Vec2 The 2D point vector of the POSITIONABLE. +-- @return #nil The POSITIONABLE is not existing or alive. +function POSITIONABLE:GetVec2() + self:F2( self.PositionableName ) + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + local PositionableVec3 = DCSPositionable:getPosition().p + + local PositionableVec2 = {} + PositionableVec2.x = PositionableVec3.x + PositionableVec2.y = PositionableVec3.z + + self:T2( PositionableVec2 ) + return PositionableVec2 + end + + return nil +end + +--- Returns a POINT_VEC2 object indicating the point in 2D of the POSITIONABLE within the mission. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return Core.Point#POINT_VEC2 The 2D point vector of the POSITIONABLE. +-- @return #nil The POSITIONABLE is not existing or alive. +function POSITIONABLE:GetPointVec2() + self:F2( self.PositionableName ) + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + local PositionableVec3 = DCSPositionable:getPosition().p + + local PositionablePointVec2 = POINT_VEC2:NewFromVec3( PositionableVec3 ) + + self:T2( PositionablePointVec2 ) + return PositionablePointVec2 + end + + return nil +end + +--- Returns a POINT_VEC3 object indicating the point in 3D of the POSITIONABLE within the mission. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return Core.Point#POINT_VEC3 The 3D point vector of the POSITIONABLE. +-- @return #nil The POSITIONABLE is not existing or alive. +function POSITIONABLE:GetPointVec3() + self:F2( self.PositionableName ) + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + local PositionableVec3 = self:GetPositionVec3() + + local PositionablePointVec3 = POINT_VEC3:NewFromVec3( PositionableVec3 ) + + self:T2( PositionablePointVec3 ) + return PositionablePointVec3 + end + + return nil +end + + +--- Returns a random @{DCSTypes#Vec3} vector within a range, indicating the point in 3D of the POSITIONABLE within the mission. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return Dcs.DCSTypes#Vec3 The 3D point vector of the POSITIONABLE. +-- @return #nil The POSITIONABLE is not existing or alive. +function POSITIONABLE:GetRandomVec3( Radius ) + self:F2( self.PositionableName ) + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + local PositionablePointVec3 = DCSPositionable:getPosition().p + local PositionableRandomVec3 = {} + local angle = math.random() * math.pi*2; + PositionableRandomVec3.x = PositionablePointVec3.x + math.cos( angle ) * math.random() * Radius; + PositionableRandomVec3.y = PositionablePointVec3.y + PositionableRandomVec3.z = PositionablePointVec3.z + math.sin( angle ) * math.random() * Radius; + + self:T3( PositionableRandomVec3 ) + return PositionableRandomVec3 + end + + return nil +end + +--- Returns the @{DCSTypes#Vec3} vector indicating the 3D vector of the POSITIONABLE within the mission. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return Dcs.DCSTypes#Vec3 The 3D point vector of the POSITIONABLE. +-- @return #nil The POSITIONABLE is not existing or alive. +function POSITIONABLE:GetVec3() + self:F2( self.PositionableName ) + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + local PositionableVec3 = DCSPositionable:getPosition().p + self:T3( PositionableVec3 ) + return PositionableVec3 + end + + return nil +end + +--- Returns the altitude of the POSITIONABLE. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return Dcs.DCSTypes#Distance The altitude of the POSITIONABLE. +-- @return #nil The POSITIONABLE is not existing or alive. +function POSITIONABLE:GetAltitude() + self:F2() + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + local PositionablePointVec3 = DCSPositionable:getPoint() --Dcs.DCSTypes#Vec3 + return PositionablePointVec3.y + end + + return nil +end + +--- Returns if the Positionable is located above a runway. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return #boolean true if Positionable is above a runway. +-- @return #nil The POSITIONABLE is not existing or alive. +function POSITIONABLE:IsAboveRunway() + self:F2( self.PositionableName ) + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + + local Vec2 = self:GetVec2() + local SurfaceType = land.getSurfaceType( Vec2 ) + local IsAboveRunway = SurfaceType == land.SurfaceType.RUNWAY + + self:T2( IsAboveRunway ) + return IsAboveRunway + end + + return nil +end + + + +--- Returns the POSITIONABLE heading in degrees. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return #number The POSTIONABLE 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 + PositionableHeading = PositionableHeading * 180 / math.pi + self:T2( PositionableHeading ) + return PositionableHeading + end + end + + return nil +end + + +--- Returns true if the POSITIONABLE is in the air. +-- Polymorphic, is overridden in GROUP and UNIT. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return #boolean true if in the air. +-- @return #nil The POSITIONABLE is not existing or alive. +function POSITIONABLE:InAir() + self:F2( self.PositionableName ) + + return nil +end + + +--- Returns the POSITIONABLE velocity vector. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return Dcs.DCSTypes#Vec3 The velocity vector +-- @return #nil The 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 + +--- Returns the POSITIONABLE velocity in km/h. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return #number The velocity in km/h +-- @return #nil The POSITIONABLE is not existing or alive. +function POSITIONABLE:GetVelocityKMH() + self:F2( self.PositionableName ) + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + local VelocityVec3 = self:GetVelocity() + local Velocity = ( VelocityVec3.x ^ 2 + VelocityVec3.y ^ 2 + VelocityVec3.z ^ 2 ) ^ 0.5 -- in meters / sec + local Velocity = Velocity * 3.6 -- now it is in km/h. + self:T3( Velocity ) + return Velocity + end + + return nil +end + +--- Returns a message with the callsign embedded (if there is one). +-- @param #POSITIONABLE self +-- @param #string Message The message text +-- @param Dcs.DCSTypes#Duration Duration The duration of the message. +-- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +-- @return Core.Message#MESSAGE +function POSITIONABLE:GetMessage( Message, Duration, Name ) + + local DCSObject = self:GetDCSObject() + if DCSObject then + Name = Name or self:GetTypeName() + return MESSAGE:New( Message, Duration, self:GetCallsign() .. " (" .. Name .. ")" ) + end + + return nil +end + +--- Send a message to all coalitions. +-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. +-- @param #POSITIONABLE self +-- @param #string Message The message text +-- @param Dcs.DCSTypes#Duration Duration The duration of the message. +-- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +function POSITIONABLE:MessageToAll( Message, Duration, Name ) + self:F2( { Message, Duration } ) + + local DCSObject = self:GetDCSObject() + if DCSObject then + self:GetMessage( Message, Duration, Name ):ToAll() + end + + return nil +end + +--- Send a message to a coalition. +-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. +-- @param #POSITIONABLE self +-- @param #string Message The message text +-- @param Dcs.DCSTYpes#Duration Duration The duration of the message. +-- @param Dcs.DCScoalition#coalition MessageCoalition The Coalition receiving the message. +-- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +function POSITIONABLE:MessageToCoalition( Message, Duration, MessageCoalition, Name ) + self:F2( { Message, Duration } ) + + local DCSObject = self:GetDCSObject() + if DCSObject then + self:GetMessage( Message, Duration, Name ):ToCoalition( MessageCoalition ) + end + + return nil +end + + +--- Send a message to the red coalition. +-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. +-- @param #POSITIONABLE self +-- @param #string Message The message text +-- @param Dcs.DCSTYpes#Duration Duration The duration of the message. +-- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +function POSITIONABLE:MessageToRed( Message, Duration, Name ) + self:F2( { Message, Duration } ) + + local DCSObject = self:GetDCSObject() + if DCSObject then + self:GetMessage( Message, Duration, Name ):ToRed() + end + + return nil +end + +--- Send a message to the blue coalition. +-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. +-- @param #POSITIONABLE self +-- @param #string Message The message text +-- @param Dcs.DCSTypes#Duration Duration The duration of the message. +-- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +function POSITIONABLE:MessageToBlue( Message, Duration, Name ) + self:F2( { Message, Duration } ) + + local DCSObject = self:GetDCSObject() + if DCSObject then + self:GetMessage( Message, Duration, Name ):ToBlue() + end + + return nil +end + +--- Send a message to a client. +-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. +-- @param #POSITIONABLE self +-- @param #string Message The message text +-- @param Dcs.DCSTypes#Duration Duration The duration of the message. +-- @param Wrapper.Client#CLIENT Client The client object receiving the message. +-- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +function POSITIONABLE:MessageToClient( Message, Duration, Client, Name ) + self:F2( { Message, Duration } ) + + local DCSObject = self:GetDCSObject() + if DCSObject then + self:GetMessage( Message, Duration, Name ):ToClient( Client ) + end + + return nil +end + +--- Send a message to a @{Group}. +-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. +-- @param #POSITIONABLE self +-- @param #string Message The message text +-- @param Dcs.DCSTypes#Duration Duration The duration of the message. +-- @param Wrapper.Group#GROUP MessageGroup The GROUP object receiving the message. +-- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +function POSITIONABLE:MessageToGroup( Message, Duration, MessageGroup, Name ) + self:F2( { Message, Duration } ) + + local DCSObject = self:GetDCSObject() + if DCSObject then + if DCSObject:isExist() then + self:GetMessage( Message, Duration, Name ):ToGroup( MessageGroup ) + end + end + + return nil +end + +--- Send a message to the players in the @{Group}. +-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. +-- @param #POSITIONABLE self +-- @param #string Message The message text +-- @param Dcs.DCSTypes#Duration Duration The duration of the message. +-- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +function POSITIONABLE:Message( Message, Duration, Name ) + self:F2( { Message, Duration } ) + + local DCSObject = self:GetDCSObject() + if DCSObject then + self:GetMessage( Message, Duration, Name ):ToGroup( self ) + end + + return nil +end + + + + + +--- This module contains the CONTROLLABLE class. +-- +-- 1) @{Controllable#CONTROLLABLE} class, extends @{Positionable#POSITIONABLE} +-- =========================================================== +-- 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 some or all ammunition at a VEC2 point. +-- * @{#CONTROLLABLE.TaskFollow}: (AIR) Following another airborne controllable. +-- * @{#CONTROLLABLE.TaskHold}: (GROUND) Hold ground controllable from moving. +-- * @{#CONTROLLABLE.TaskHoldPosition}: (AIR) Hold position at the current position of the first unit of the controllable. +-- * @{#CONTROLLABLE.TaskLand}: (AIR HELICOPTER) Landing at the ground. For helicopters only. +-- * @{#CONTROLLABLE.TaskLandAtZone}: (AIR) Land the controllable at a @{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 + +--- The CONTROLLABLE class +-- @type CONTROLLABLE +-- @extends Wrapper.Positionable#POSITIONABLE +-- @field Dcs.DCSWrapper.Controllable#Controllable DCSControllable The DCS controllable class. +-- @field #string ControllableName The name of the controllable. +CONTROLLABLE = { + ClassName = "CONTROLLABLE", + ControllableName = "", + WayPointFunctions = {}, +} + +--- Create a new CONTROLLABLE from a DCSControllable +-- @param #CONTROLLABLE self +-- @param Dcs.DCSWrapper.Controllable#Controllable ControllableName The DCS Controllable name +-- @return #CONTROLLABLE self +function CONTROLLABLE:New( ControllableName ) + local self = BASE:Inherit( self, POSITIONABLE:New( ControllableName ) ) + self:F2( ControllableName ) + self.ControllableName = ControllableName + + self.TaskScheduler = SCHEDULER:New( self ) + return self +end + +-- DCS Controllable methods support. + +--- Get the controller for the CONTROLLABLE. +-- @param #CONTROLLABLE self +-- @return Dcs.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 + +-- Get methods + +--- Returns the UNITs wrappers of the DCS Units of the Controllable (default is a GROUP). +-- @param #CONTROLLABLE self +-- @return #list The UNITs wrappers. +function CONTROLLABLE:GetUnits() + self:F2( { self.ControllableName } ) + local DCSControllable = self:GetDCSObject() + + if DCSControllable then + local DCSUnits = DCSControllable:getUnits() + local Units = {} + for Index, UnitData in pairs( DCSUnits ) do + Units[#Units+1] = UNIT:Find( UnitData ) + end + self:T3( Units ) + return Units + end + + return nil +end + + +--- Returns the health. Dead controllables have health <= 1.0. +-- @param #CONTROLLABLE self +-- @return #number The controllable health value (unit or group average). +-- @return #nil The controllable is not existing or alive. +function CONTROLLABLE:GetLife() + self:F2( self.ControllableName ) + + local DCSControllable = self:GetDCSObject() + + if DCSControllable then + local UnitLife = 0 + local Units = self:GetUnits() + if #Units == 1 then + local Unit = Units[1] -- Wrapper.Unit#UNIT + UnitLife = Unit:GetLife() + else + local UnitLifeTotal = 0 + for UnitID, Unit in pairs( Units ) do + local Unit = Unit -- Wrapper.Unit#UNIT + UnitLifeTotal = UnitLifeTotal + Unit:GetLife() + end + UnitLife = UnitLifeTotal / #Units + end + return UnitLife + end + + return nil +end + + + +-- Tasks + +--- Popping current Task from the controllable. +-- @param #CONTROLLABLE self +-- @return Wrapper.Controllable#CONTROLLABLE self +function CONTROLLABLE:PopCurrentTask() + self:F2() + + local DCSControllable = self:GetDCSObject() + + if DCSControllable then + local Controller = self:_GetController() + Controller:popTask() + return self + end + + return nil +end + +--- Pushing Task on the queue from the controllable. +-- @param #CONTROLLABLE self +-- @return Wrapper.Controllable#CONTROLLABLE self +function CONTROLLABLE:PushTask( DCSTask, WaitTime ) + self:F2() + + local DCSControllable = self:GetDCSObject() + + if DCSControllable then + local 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 + self.TaskScheduler:Schedule( 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 Wrapper.Controllable#CONTROLLABLE self +function CONTROLLABLE:SetTask( DCSTask, WaitTime ) + self:F2( { DCSTask } ) + + local DCSControllable = self:GetDCSObject() + + if DCSControllable then + + local Controller = self:_GetController() + self:T3( Controller ) + + -- 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 + Controller:setTask( DCSTask ) + else + self.TaskScheduler:Schedule( Controller, Controller.setTask, { DCSTask }, WaitTime ) + end + + return self + end + + return nil +end + + +--- Return a condition section for a controlled task. +-- @param #CONTROLLABLE self +-- @param Dcs.DCSTime#Time time +-- @param #string userFlag +-- @param #boolean userFlagValue +-- @param #string condition +-- @param Dcs.DCSTime#Time duration +-- @param #number lastWayPoint +-- return Dcs.DCSTasking.Task#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 Dcs.DCSTasking.Task#Task DCSTask +-- @param #DCSStopCondition DCSStopCondition +-- @return Dcs.DCSTasking.Task#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 Dcs.DCSTasking.Task#TaskArray DCSTasks Array of @{DCSTasking.Task#Task} +-- @return Dcs.DCSTasking.Task#Task +function CONTROLLABLE:TaskCombo( DCSTasks ) + self:F2( { DCSTasks } ) + + local DCSTaskCombo + + DCSTaskCombo = { + id = 'ComboTask', + params = { + tasks = DCSTasks + } + } + + for TaskID, Task in ipairs( DCSTasks ) do + self:E( Task ) + end + + self:T3( { DCSTaskCombo } ) + return DCSTaskCombo +end + +--- Return a WrappedAction Task taking a Command. +-- @param #CONTROLLABLE self +-- @param Dcs.DCSCommand#Command DCSCommand +-- @return Dcs.DCSTasking.Task#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 Dcs.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 Dcs.DCSTasking.Task#Task +-- @usage +-- --- This test demonstrates the use(s) of the SwitchWayPoint method of the GROUP class. +-- HeliGroup = GROUP:FindByName( "Helicopter" ) +-- +-- --- Route the helicopter back to the FARP after 60 seconds. +-- -- We use the SCHEDULER class to do this. +-- SCHEDULER:New( nil, +-- function( HeliGroup ) +-- local CommandRTB = HeliGroup:CommandSwitchWayPoint( 2, 8 ) +-- HeliGroup:SetCommand( CommandRTB ) +-- end, { HeliGroup }, 90 +-- ) +function CONTROLLABLE:CommandSwitchWayPoint( FromWayPoint, ToWayPoint ) + self:F2( { FromWayPoint, ToWayPoint } ) + + local CommandSwitchWayPoint = { + id = 'SwitchWaypoint', + params = { + fromWaypointIndex = FromWayPoint, + goToWaypointIndex = ToWayPoint, + }, + } + + self:T3( { CommandSwitchWayPoint } ) + return CommandSwitchWayPoint +end + +--- Perform stop route command +-- @param #CONTROLLABLE self +-- @param #boolean StopRoute +-- @return Dcs.DCSTasking.Task#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 Wrapper.Controllable#CONTROLLABLE AttackGroup The Controllable to be attacked. +-- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. +-- @param Dcs.DCSTypes#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. +-- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. +-- @param Dcs.DCSTypes#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. +-- @param Dcs.DCSTypes#Distance Altitude (optional) Desired attack start altitude. Controllable/aircraft will make its attacks from the altitude. If the altitude is too low or too high to use weapon aircraft/controllable will choose closest altitude to the desired attack start altitude. If the desired altitude is defined controllable/aircraft will not attack from safe altitude. +-- @param #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 Dcs.DCSTasking.Task#Task The DCS task structure. +function CONTROLLABLE:TaskAttackGroup( AttackGroup, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit ) + self:F2( { self.ControllableName, AttackGroup, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit } ) + + -- AttackControllable = { + -- id = 'AttackControllable', + -- params = { + -- groupId = Group.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 = { + groupId = AttackGroup: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 Wrapper.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 Dcs.DCSTypes#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. +-- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. +-- @param Dcs.DCSTypes#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. +-- @param #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 Dcs.DCSTasking.Task#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 = { + altitudeEnabled = true, + unitId = AttackUnit:GetID(), + attackQtyLimit = AttackQtyLimit or false, + attackQty = AttackQty or 2, + expend = WeaponExpend or "Auto", + altitude = 2000, + directionEnabled = true, + groupAttack = true, + --weaponType = WeaponType or 1073741822, + direction = Direction or 0, + } + } + + self:E( DCSTask ) + + return DCSTask +end + + +--- (AIR) Delivering weapon at the point on the ground. +-- @param #CONTROLLABLE self +-- @param Dcs.DCSTypes#Vec2 Vec2 2D-coordinates of the point to deliver weapon at. +-- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. +-- @param Dcs.DCSTypes#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. +-- @param #number AttackQty (optional) Desired quantity of passes. The parameter is not the same in AttackControllable and AttackUnit tasks. +-- @param Dcs.DCSTypes#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. +-- @param #boolean ControllableAttack (optional) Flag indicates that the target must be engaged by all aircrafts of the controllable. Has effect only if the task is assigned to a controllable, not to a single aircraft. +-- @return Dcs.DCSTasking.Task#Task The DCS task structure. +function CONTROLLABLE:TaskBombing( Vec2, WeaponType, WeaponExpend, AttackQty, Direction, ControllableAttack ) + self:F2( { self.ControllableName, Vec2, 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 = Vec2, + 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 Dcs.DCSTypes#Vec2 Point The point to hold the position. +-- @param #number Altitude The altitude to hold the position. +-- @param #number Speed The speed flying when holding the position. +-- @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:GetVec2() + 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 Dcs.DCSTypes#Vec2 Vec2 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 Dcs.DCSTypes#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. +-- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. +-- @param Dcs.DCSTypes#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. +-- @param #boolean ControllableAttack (optional) Flag indicates that the target must be engaged by all aircrafts of the controllable. Has effect only if the task is assigned to a controllable, not to a single aircraft. +-- @return Dcs.DCSTasking.Task#Task The DCS task structure. +function CONTROLLABLE:TaskAttackMapObject( Vec2, WeaponType, WeaponExpend, AttackQty, Direction, ControllableAttack ) + self:F2( { self.ControllableName, Vec2, 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 = Vec2, + 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 Wrapper.Airbase#AIRBASE Airbase Airbase to attack. +-- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. +-- @param Dcs.DCSTypes#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. +-- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. +-- @param Dcs.DCSTypes#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. +-- @param #boolean ControllableAttack (optional) Flag indicates that the target must be engaged by all aircrafts of the controllable. Has effect only if the task is assigned to a controllable, not to a single aircraft. +-- @return Dcs.DCSTasking.Task#Task The DCS task structure. +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 Dcs.DCSTasking.Task#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 Dcs.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 Core.Zone#ZONE Zone The zone where to land. +-- @param #number Duration The duration in seconds to stay on the ground. +-- @return #CONTROLLABLE self +function CONTROLLABLE:TaskLandAtZone( Zone, Duration, RandomPoint ) + self:F2( { self.ControllableName, Zone, Duration, RandomPoint } ) + + local Point + if RandomPoint then + Point = Zone:GetRandomVec2() + else + Point = Zone:GetVec2() + 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 Wrapper.Controllable#CONTROLLABLE FollowControllable The controllable to be followed. +-- @param Dcs.DCSTypes#Vec3 Vec3 Position of the unit / lead unit of the controllable relative lead unit of another controllable in frame reference oriented by course of lead unit of another controllable. If another controllable is on land the unit / controllable will orbit around. +-- @param #number LastWaypointIndex Detach waypoint of another controllable. Once reached the unit / controllable Follow task is finished. +-- @return Dcs.DCSTasking.Task#Task The DCS task structure. +function CONTROLLABLE:TaskFollow( FollowControllable, Vec3, LastWaypointIndex ) + self:F2( { self.ControllableName, FollowControllable, Vec3, LastWaypointIndex } ) + +-- Follow = { +-- id = 'Follow', +-- params = { +-- groupId = Group.ID, +-- pos = Vec3, +-- lastWptIndexFlag = boolean, +-- lastWptIndex = number +-- } +-- } + + local LastWaypointIndexFlag = false + if LastWaypointIndex then + LastWaypointIndexFlag = true + end + + local DCSTask + DCSTask = { + id = 'Follow', + params = { + groupId = FollowControllable:GetID(), + pos = Vec3, + 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 Wrapper.Controllable#CONTROLLABLE EscortControllable The controllable to be escorted. +-- @param Dcs.DCSTypes#Vec3 Vec3 Position of the unit / lead unit of the controllable relative lead unit of another controllable in frame reference oriented by course of lead unit of another controllable. If another controllable is on land the unit / controllable will orbit around. +-- @param #number LastWaypointIndex Detach waypoint of another controllable. Once reached the unit / controllable Follow task is finished. +-- @param #number EngagementDistanceMax Maximal distance from escorted controllable to threat. If the threat is already engaged by escort escort will disengage if the distance becomes greater than 1.5 * engagementDistMax. +-- @param Dcs.DCSTypes#AttributeNameArray TargetTypes Array of AttributeName that is contains threat categories allowed to engage. +-- @return Dcs.DCSTasking.Task#Task The DCS task structure. +function CONTROLLABLE:TaskEscort( FollowControllable, Vec3, LastWaypointIndex, EngagementDistance, TargetTypes ) + self:F2( { self.ControllableName, FollowControllable, Vec3, LastWaypointIndex, EngagementDistance, TargetTypes } ) + +-- Escort = { +-- id = 'Escort', +-- params = { +-- groupId = Group.ID, +-- pos = Vec3, +-- lastWptIndexFlag = boolean, +-- lastWptIndex = number, +-- engagementDistMax = Distance, +-- targetTypes = array of AttributeName, +-- } +-- } + + local LastWaypointIndexFlag = false + if LastWaypointIndex then + LastWaypointIndexFlag = true + end + + local DCSTask + DCSTask = { id = 'Escort', + params = { + groupId = FollowControllable:GetID(), + pos = Vec3, + 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 Dcs.DCSTypes#Vec2 Vec2 The point to fire at. +-- @param Dcs.DCSTypes#Distance Radius The radius of the zone to deploy the fire at. +-- @param #number AmmoCount (optional) Quantity of ammunition to expand (omit to fire until ammunition is depleted). +-- @return Dcs.DCSTasking.Task#Task The DCS task structure. +function CONTROLLABLE:TaskFireAtPoint( Vec2, Radius, AmmoCount ) + self:F2( { self.ControllableName, Vec2, Radius, AmmoCount } ) + + -- FireAtPoint = { + -- id = 'FireAtPoint', + -- params = { + -- point = Vec2, + -- radius = Distance, + -- expendQty = number, + -- expendQtyEnabled = boolean, + -- } + -- } + + local DCSTask + DCSTask = { id = 'FireAtPoint', + params = { + point = Vec2, + radius = Radius, + expendQty = 100, -- dummy value + expendQtyEnabled = false, + } + } + + if AmmoCount then + DCSTask.params.expendQty = AmmoCount + DCSTask.params.expendQtyEnabled = true + end + + self:T3( { DCSTask } ) + return DCSTask +end + +--- (GROUND) Hold ground controllable from moving. +-- @param #CONTROLLABLE self +-- @return Dcs.DCSTasking.Task#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 Wrapper.Controllable#CONTROLLABLE AttackGroup Target CONTROLLABLE. +-- @param #number WeaponType Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. +-- @param Dcs.DCSTypes#AI.Task.Designation Designation (optional) Designation type. +-- @param #boolean Datalink (optional) Allows to use datalink to send the target information to attack aircraft. Enabled by default. +-- @return Dcs.DCSTasking.Task#Task The DCS task structure. +function CONTROLLABLE:TaskFAC_AttackGroup( AttackGroup, WeaponType, Designation, Datalink ) + self:F2( { self.ControllableName, AttackGroup, WeaponType, Designation, Datalink } ) + +-- FAC_AttackControllable = { +-- id = 'FAC_AttackControllable', +-- params = { +-- groupId = Group.ID, +-- weaponType = number, +-- designation = enum AI.Task.Designation, +-- datalink = boolean +-- } +-- } + + local DCSTask + DCSTask = { id = 'FAC_AttackControllable', + params = { + groupId = AttackGroup:GetID(), + weaponType = WeaponType, + designation = Designation, + datalink = Datalink, + } + } + + self:T3( { DCSTask } ) + return DCSTask +end + +-- EN-ACT_ROUTE TASKS FOR AIRBORNE CONTROLLABLES + +--- (AIR) Engaging targets of defined types. +-- @param #CONTROLLABLE self +-- @param Dcs.DCSTypes#Distance Distance Maximal distance from the target to a route leg. If the target is on a greater distance it will be ignored. +-- @param Dcs.DCSTypes#AttributeNameArray TargetTypes Array of target categories allowed to engage. +-- @param #number Priority All enroute tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. +-- @return Dcs.DCSTasking.Task#Task The DCS task structure. +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 Dcs.DCSTypes#Vec2 Vec2 2D-coordinates of the zone. +-- @param Dcs.DCSTypes#Distance Radius Radius of the zone. +-- @param Dcs.DCSTypes#AttributeNameArray TargetTypes Array of target categories allowed to engage. +-- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. +-- @return Dcs.DCSTasking.Task#Task The DCS task structure. +function CONTROLLABLE:EnRouteTaskEngageTargets( Vec2, Radius, TargetTypes, Priority ) + self:F2( { self.ControllableName, Vec2, Radius, TargetTypes, Priority } ) + +-- EngageTargetsInZone = { +-- id = 'EngageTargetsInZone', +-- params = { +-- point = Vec2, +-- zoneRadius = Distance, +-- targetTypes = array of AttributeName, +-- priority = number +-- } +-- } + + local DCSTask + DCSTask = { id = 'EngageTargetsInZone', + params = { + point = Vec2, + 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 Wrapper.Controllable#CONTROLLABLE AttackGroup The Controllable to be attacked. +-- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. +-- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. +-- @param Dcs.DCSTypes#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. +-- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. +-- @param Dcs.DCSTypes#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. +-- @param Dcs.DCSTypes#Distance Altitude (optional) Desired attack start altitude. Controllable/aircraft will make its attacks from the altitude. If the altitude is too low or too high to use weapon aircraft/controllable will choose closest altitude to the desired attack start altitude. If the desired altitude is defined controllable/aircraft will not attack from safe altitude. +-- @param #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 Dcs.DCSTasking.Task#Task The DCS task structure. +function CONTROLLABLE:EnRouteTaskEngageGroup( AttackGroup, Priority, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit ) + self:F2( { self.ControllableName, AttackGroup, Priority, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit } ) + + -- EngageControllable = { + -- id = 'EngageControllable ', + -- params = { + -- groupId = Group.ID, + -- weaponType = number, + -- expend = enum AI.Task.WeaponExpend, + -- attackQty = number, + -- directionEnabled = boolean, + -- direction = Azimuth, + -- altitudeEnabled = boolean, + -- altitude = Distance, + -- attackQtyLimit = boolean, + -- priority = number, + -- } + -- } + + local DirectionEnabled = nil + if Direction then + DirectionEnabled = true + end + + local AltitudeEnabled = nil + if Altitude then + AltitudeEnabled = true + end + + local DCSTask + DCSTask = { id = 'EngageControllable', + params = { + groupId = AttackGroup:GetID(), + weaponType = WeaponType, + expend = WeaponExpend, + attackQty = AttackQty, + directionEnabled = DirectionEnabled, + direction = Direction, + altitudeEnabled = AltitudeEnabled, + altitude = Altitude, + attackQtyLimit = AttackQtyLimit, + priority = Priority, + }, + }, + + self:T3( { DCSTask } ) + return DCSTask +end + + +--- (AIR) Attack the Unit. +-- @param #CONTROLLABLE self +-- @param Wrapper.Unit#UNIT EngageUnit The UNIT. +-- @param #number Priority (optional) All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. +-- @param #boolean GroupAttack (optional) If true, all units in the group will attack the Unit when found. +-- @param Dcs.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 Dcs.DCSTypes#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. +-- @param Dcs.DCSTypes#Distance Altitude (optional) Desired altitude to perform the unit engagement. +-- @param #boolean Visible (optional) Unit must be visible. +-- @param #boolean ControllableAttack (optional) Flag indicates that the target must be engaged by all aircrafts of the controllable. Has effect only if the task is assigned to a controllable, not to a single aircraft. +-- @return Dcs.DCSTasking.Task#Task The DCS task structure. +function CONTROLLABLE:EnRouteTaskEngageUnit( EngageUnit, Priority, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, Visible, ControllableAttack ) + self:F2( { self.ControllableName, EngageUnit, Priority, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, Visible, ControllableAttack } ) + + -- EngageUnit = { + -- id = 'EngageUnit', + -- params = { + -- unitId = Unit.ID, + -- weaponType = number, + -- expend = enum AI.Task.WeaponExpend + -- attackQty = number, + -- direction = Azimuth, + -- attackQtyLimit = boolean, + -- controllableAttack = boolean, + -- priority = number, + -- } + -- } + + local DCSTask + DCSTask = { id = 'EngageUnit', + params = { + unitId = EngageUnit:GetID(), + priority = Priority or 1, + groupAttack = GroupAttack or false, + visible = Visible or false, + expend = WeaponExpend or "Auto", + directionEnabled = Direction and true or false, + direction = Direction, + altitudeEnabled = Altitude and true or false, + altitude = Altitude, + attackQtyLimit = AttackQty and true or false, + attackQty = AttackQty, + controllableAttack = ControllableAttack, + }, + }, + + 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 Dcs.DCSTasking.Task#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 Dcs.DCSTasking.Task#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 Dcs.DCSTasking.Task#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 Wrapper.Controllable#CONTROLLABLE AttackGroup Target CONTROLLABLE. +-- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. +-- @param #number WeaponType Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. +-- @param Dcs.DCSTypes#AI.Task.Designation Designation (optional) Designation type. +-- @param #boolean Datalink (optional) Allows to use datalink to send the target information to attack aircraft. Enabled by default. +-- @return Dcs.DCSTasking.Task#Task The DCS task structure. +function CONTROLLABLE:EnRouteTaskFAC_EngageGroup( AttackGroup, Priority, WeaponType, Designation, Datalink ) + self:F2( { self.ControllableName, AttackGroup, WeaponType, Priority, Designation, Datalink } ) + +-- FAC_EngageControllable = { +-- id = 'FAC_EngageControllable', +-- params = { +-- groupId = Group.ID, +-- weaponType = number, +-- designation = enum AI.Task.Designation, +-- datalink = boolean, +-- priority = number, +-- } +-- } + + local DCSTask + DCSTask = { id = 'FAC_EngageControllable', + params = { + groupId = AttackGroup: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 Dcs.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 Dcs.DCSTasking.Task#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 Dcs.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 Dcs.DCSTasking.Task#Task The DCS task structure +function CONTROLLABLE:TaskEmbarking( Point, Duration, EmbarkingControllable ) + self:F2( { self.ControllableName, Point, Duration, EmbarkingControllable.DCSControllable } ) + + local DCSTask + DCSTask = { id = 'Embarking', + params = { x = Point.x, + y = Point.y, + duration = Duration, + controllablesForEmbarking = { EmbarkingControllable.ControllableID }, + durationFlag = true, + distributionFlag = false, + distribution = {}, + } + } + + self:T3( { DCSTask } ) + return DCSTask +end + +--- (GROUND) Embark to a Transport landed at a location. + +--- Move to a defined Vec2 Point, and embark to a controllable when arrived within a defined Radius. +-- @param #CONTROLLABLE self +-- @param Dcs.DCSTypes#Vec2 Point The point where to wait. +-- @param #number Radius The radius of the embarking zone around the Point. +-- @return Dcs.DCSTasking.Task#Task The DCS task structure. +function CONTROLLABLE:TaskEmbarkToTransport( Point, Radius ) + self:F2( { self.ControllableName, Point, Radius } ) + + local DCSTask --Dcs.DCSTasking.Task#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 Dcs.DCSTasking.Task#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 Dcs.DCSTasking.Task#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 Dcs.DCSTypes#Vec3 Point The destination point in Vec3 format. +-- @param #number Speed The speed to travel. +-- @return #CONTROLLABLE self +function CONTROLLABLE:RouteToVec2( Point, Speed ) + self:F2( { Point, Speed } ) + + local ControllablePoint = self:GetUnit( 1 ):GetVec2() + + local PointFrom = {} + PointFrom.x = ControllablePoint.x + PointFrom.y = ControllablePoint.y + PointFrom.type = "Turning Point" + PointFrom.action = "Turning Point" + PointFrom.speed = Speed + PointFrom.speed_locked = true + PointFrom.properties = { + ["vnav"] = 1, + ["scale"] = 0, + ["angle"] = 0, + ["vangle"] = 0, + ["steer"] = 2, + } + + + local PointTo = {} + PointTo.x = Point.x + PointTo.y = Point.y + PointTo.type = "Turning Point" + PointTo.action = "Fly Over Point" + PointTo.speed = Speed + PointTo.speed_locked = true + PointTo.properties = { + ["vnav"] = 1, + ["scale"] = 0, + ["angle"] = 0, + ["vangle"] = 0, + ["steer"] = 2, + } + + + local Points = { PointFrom, PointTo } + + self:T3( Points ) + + self:Route( Points ) + + return self +end + +--- (AIR + GROUND) Make the Controllable move to a given point. +-- @param #CONTROLLABLE self +-- @param Dcs.DCSTypes#Vec3 Point The destination point in Vec3 format. +-- @param #number Speed The speed to travel. +-- @return #CONTROLLABLE self +function CONTROLLABLE:RouteToVec3( Point, Speed ) + self:F2( { Point, Speed } ) + + local ControllableVec3 = self:GetUnit( 1 ):GetVec3() + + local PointFrom = {} + PointFrom.x = ControllableVec3.x + PointFrom.y = ControllableVec3.z + PointFrom.alt = ControllableVec3.y + PointFrom.alt_type = "BARO" + PointFrom.type = "Turning Point" + PointFrom.action = "Turning Point" + PointFrom.speed = Speed + PointFrom.speed_locked = true + PointFrom.properties = { + ["vnav"] = 1, + ["scale"] = 0, + ["angle"] = 0, + ["vangle"] = 0, + ["steer"] = 2, + } + + + local PointTo = {} + PointTo.x = Point.x + PointTo.y = Point.z + PointTo.alt = Point.y + PointTo.alt_type = "BARO" + PointTo.type = "Turning Point" + PointTo.action = "Fly Over Point" + PointTo.speed = Speed + PointTo.speed_locked = true + PointTo.properties = { + ["vnav"] = 1, + ["scale"] = 0, + ["angle"] = 0, + ["vangle"] = 0, + ["steer"] = 2, + } + + + local Points = { PointFrom, PointTo } + + self:T3( Points ) + + self:Route( Points ) + + return self +end + + + +--- Make the controllable to follow a given route. +-- @param #CONTROLLABLE self +-- @param #table 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 ) + self.TaskScheduler:Schedule( 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 Core.Zone#ZONE Zone The zone where to route to. +-- @param #boolean Randomize Defines whether to target point gets randomized within the Zone. +-- @param #number Speed The speed. +-- @param Base#FORMATION Formation The formation string. +function CONTROLLABLE:TaskRouteToZone( Zone, Randomize, Speed, Formation ) + self:F2( Zone ) + + local DCSControllable = self:GetDCSObject() + + if DCSControllable then + + local ControllablePoint = self:GetVec2() + + local PointFrom = {} + PointFrom.x = ControllablePoint.x + PointFrom.y = ControllablePoint.y + PointFrom.type = "Turning Point" + PointFrom.action = "Cone" + PointFrom.speed = 20 / 1.6 + + + local PointTo = {} + local ZonePoint + + if Randomize then + ZonePoint = Zone:GetRandomVec2() + else + ZonePoint = Zone:GetVec2() + end + + PointTo.x = ZonePoint.x + PointTo.y = ZonePoint.y + PointTo.type = "Turning Point" + + if Formation then + PointTo.action = Formation + else + PointTo.action = "Cone" + end + + if Speed then + PointTo.speed = Speed + else + PointTo.speed = 20 / 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 Wrapper.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:GetVec2() + 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:GetVec2() + + 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 Wrapper.Controllable#CONTROLLABLE self +-- @param #boolean DetectVisual (optional) +-- @param #boolean DetectOptical (optional) +-- @param #boolean DetectRadar (optional) +-- @param #boolean DetectIRST (optional) +-- @param #boolean DetectRWR (optional) +-- @param #boolean DetectDLINK (optional) +-- @return #table DetectedTargets +function CONTROLLABLE:GetDetectedTargets( DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK ) + self:F2( self.ControllableName ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local DetectionVisual = ( DetectVisual and DetectVisual == true ) and Controller.Detection.VISUAL or nil + local DetectionOptical = ( DetectOptical and DetectOptical == true ) and Controller.Detection.OPTICAL or nil + local DetectionRadar = ( DetectRadar and DetectRadar == true ) and Controller.Detection.RADAR or nil + local DetectionIRST = ( DetectIRST and DetectIRST == true ) and Controller.Detection.IRST or nil + local DetectionRWR = ( DetectRWR and DetectRWR == true ) and Controller.Detection.RWR or nil + local DetectionDLINK = ( DetectDLINK and DetectDLINK == true ) and Controller.Detection.DLINK or nil + + + 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 Wrapper.Controllable#CONTROLLABLE self +-- @return Wrapper.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 ) + self:F( { WayPoints } ) + + if WayPoints then + self.WayPoints = WayPoints + else + self.WayPoints = self:GetTaskRoute() + end + + return self +end + +--- Get the current WayPoints set with the WayPoint functions( Note that the WayPoints can be nil, although there ARE waypoints). +-- @param #CONTROLLABLE self +-- @return #table WayPoints If WayPoints is given, then return the WayPoints structure. +function CONTROLLABLE:GetWayPoints() + self:F( ) + + if self.WayPoints then + return self.WayPoints + end + + return nil +end + +--- Registers a waypoint function that will be executed when the controllable moves over the WayPoint. +-- @param #CONTROLLABLE self +-- @param #number WayPoint The waypoint number. Note that the start waypoint on the route is WayPoint 1! +-- @param #number WayPointIndex When defining multiple WayPoint functions for one WayPoint, use WayPointIndex to set the sequence of actions. +-- @param #function WayPointFunction The waypoint function to be called when the controllable moves over the waypoint. The waypoint function takes variable parameters. +-- @return #CONTROLLABLE +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 = GROUP: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 ) + self:F( { WayPoint, WaitTime } ) + + if not WayPoint then + WayPoint = 1 + end + + -- When starting the mission from a certain point, the TaskPoints need to be deleted before the given WayPoint. + for TaskPointID = 1, WayPoint - 1 do + table.remove( self.WayPoints, 1 ) + end + + self:T3( self.WayPoints ) + + self:SetTask( self:TaskRoute( self.WayPoints ), WaitTime ) + + return self +end + +-- Message APIs--- 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 +-- +-- A GROUP is a @{Controllable}. See the @{Controllable} task methods section for a description of the task methods. +-- +-- ### 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: +-- +-- * @{Controllable#CONTROLLABLE.TaskMission}: (AIR + GROUND) Return a mission task from a mission template. +-- +-- ## 1.3) GROUP Command methods +-- +-- A GROUP is a @{Controllable}. See the @{Controllable} command methods section for a description of the command methods. +-- +-- ## 1.4) GROUP option methods +-- +-- A GROUP is a @{Controllable}. See the @{Controllable} option methods section for a description of the option methods. +-- +-- ## 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. +-- +-- ## 1.6) GROUP AI methods +-- +-- A GROUP has AI methods to control the AI activation. +-- +-- * @{#GROUP.SetAIOnOff}(): Turns the GROUP AI On or Off. +-- * @{#GROUP.SetAIOn}(): Turns the GROUP AI On. +-- * @{#GROUP.SetAIOff}(): Turns the GROUP AI Off. +-- +-- ==== +-- +-- # **API CHANGE HISTORY** +-- +-- The underlying change log documents the API changes. Please read this carefully. The following notation is used: +-- +-- * **Added** parts are expressed in bold type face. +-- * _Removed_ parts are expressed in italic type face. +-- +-- Hereby the change log: +-- +-- 2017-03-07: GROUP:**HandleEvent( Event, EventFunction )** added. +-- 2017-03-07: GROUP:**UnHandleEvent( Event )** added. +-- +-- 2017-01-24: GROUP:**SetAIOnOff( AIOnOff )** added. +-- +-- 2017-01-24: GROUP:**SetAIOn()** added. +-- +-- 2017-01-24: GROUP:**SetAIOff()** added. +-- +-- === +-- +-- # **AUTHORS and CONTRIBUTIONS** +-- +-- ### Contributions: +-- +-- * [**Entropy**](https://forums.eagle.ru/member.php?u=111471), **Afinegan**: Came up with the requirement for AIOnOff(). +-- +-- ### Authors: +-- +-- * **FlightControl**: Design & Programming +-- +-- @module Group +-- @author FlightControl + +--- The GROUP class +-- @type GROUP +-- @extends Wrapper.Controllable#CONTROLLABLE +-- @field #string GroupName The name of the group. +GROUP = { + ClassName = "GROUP", +} + +--- Create a new GROUP from a DCSGroup +-- @param #GROUP self +-- @param Dcs.DCSWrapper.Group#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 + + self:SetEventPriority( 4 ) + return self +end + +-- Reference methods. + +--- Find the GROUP wrapper class instance using the DCS Group. +-- @param #GROUP self +-- @param Dcs.DCSWrapper.Group#Group DCSGroup The DCS Group. +-- @return #GROUP The GROUP. +function GROUP:Find( DCSGroup ) + + local GroupName = DCSGroup:getName() -- Wrapper.Group#GROUP + local GroupFound = _DATABASE:FindGroup( GroupName ) + return GroupFound +end + +--- Find the created GROUP using the DCS Group Name. +-- @param #GROUP self +-- @param #string GroupName The DCS Group Name. +-- @return #GROUP The GROUP. +function GROUP:FindByName( GroupName ) + + local GroupFound = _DATABASE:FindGroup( GroupName ) + return GroupFound +end + +-- DCS Group methods support. + +--- Returns the DCS Group. +-- @param #GROUP self +-- @return Dcs.DCSWrapper.Group#Group The DCS Group. +function GROUP:GetDCSObject() + local DCSGroup = Group.getByName( self.GroupName ) + + if DCSGroup then + return DCSGroup + end + + return nil +end + +--- Returns the @{DCSTypes#Position3} position vectors indicating the point and direction vectors in 3D of the POSITIONABLE within the mission. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return Dcs.DCSTypes#Position The 3D position vectors of the POSITIONABLE. +-- @return #nil The POSITIONABLE is not existing or alive. +function GROUP:GetPositionVec3() -- Overridden from POSITIONABLE:GetPositionVec3() + self:F2( self.PositionableName ) + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + local PositionablePosition = DCSPositionable:getUnits()[1]:getPosition().p + self:T3( PositionablePosition ) + return PositionablePosition + 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() and DCSGroup:getUnit(1) ~= nil + 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 Dcs.DCSWrapper.Group#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 Dcs.DCSCoalitionWrapper.Object#coalition.side The coalition side of the DCS Group. +function GROUP:GetCoalition() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSObject() + if DCSGroup then + local GroupCoalition = DCSGroup:getCoalition() + self:T3( GroupCoalition ) + return GroupCoalition + end + + return nil +end + +--- Returns the country of the DCS Group. +-- @param #GROUP self +-- @return Dcs.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 UNIT wrapper class with number UnitNumber. +-- If the underlying DCS Unit does not exist, the method will return nil. . +-- @param #GROUP self +-- @param #number UnitNumber The number of the UNIT wrapper class to be returned. +-- @return Wrapper.Unit#UNIT The UNIT wrapper class. +function GROUP:GetUnit( UnitNumber ) + self:F2( { self.GroupName, UnitNumber } ) + + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + local UnitFound = UNIT:Find( DCSGroup:getUnit( UnitNumber ) ) + 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 Dcs.DCSWrapper.Unit#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 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. +-- @param #GROUP self +-- @return Dcs.DCSTypes#Vec2 Current Vec2 point of the first DCS Unit of the DCS Group. +function GROUP:GetVec2() + self:F2( self.GroupName ) + + local UnitPoint = self:GetUnit(1) + UnitPoint:GetVec2() + local GroupPointVec2 = UnitPoint:GetVec2() + self:T3( GroupPointVec2 ) + return GroupPointVec2 +end + +--- Returns the current Vec3 vector of the first DCS Unit in the GROUP. +-- @return Dcs.DCSTypes#Vec3 Current Vec3 of the first DCS Unit of the GROUP. +function GROUP:GetVec3() + self:F2( self.GroupName ) + + local GroupVec3 = self:GetUnit(1):GetVec3() + self:T3( GroupVec3 ) + return GroupVec3 +end + + + +do -- Is Zone methods + +--- Returns true if all units of the group are within a @{Zone}. +-- @param #GROUP self +-- @param Core.Zone#ZONE_BASE Zone The zone to test. +-- @return #boolean Returns true if the Group is completely within the @{Zone#ZONE_BASE} +function GROUP:IsCompletelyInZone( Zone ) + self:F2( { self.GroupName, Zone } ) + + for UnitID, UnitData in pairs( self:GetUnits() ) do + local Unit = UnitData -- Wrapper.Unit#UNIT + if Zone:IsVec3InZone( Unit:GetVec3() ) then + else + return false + end + end + + return true +end + +--- Returns true if some units of the group are within a @{Zone}. +-- @param #GROUP self +-- @param Core.Zone#ZONE_BASE Zone The zone to test. +-- @return #boolean Returns true if the Group is completely within the @{Zone#ZONE_BASE} +function GROUP:IsPartlyInZone( Zone ) + self:F2( { self.GroupName, Zone } ) + + for UnitID, UnitData in pairs( self:GetUnits() ) do + local Unit = UnitData -- Wrapper.Unit#UNIT + if Zone:IsVec3InZone( Unit:GetVec3() ) then + return true + end + end + + return false +end + +--- Returns true if none of the group units of the group are within a @{Zone}. +-- @param #GROUP self +-- @param Core.Zone#ZONE_BASE Zone The zone to test. +-- @return #boolean Returns true if the Group is 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 -- Wrapper.Unit#UNIT + if Zone:IsVec3InZone( Unit:GetVec3() ) 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 + +end + +do -- AI methods + + --- Turns the AI On or Off for the GROUP. + -- @param #GROUP self + -- @param #boolean AIOnOff The value true turns the AI On, the value false turns the AI Off. + -- @return #GROUP The GROUP. + function GROUP:SetAIOnOff( AIOnOff ) + + local DCSGroup = self:GetDCSObject() -- Dcs.DCSGroup#Group + + if DCSGroup then + local DCSController = DCSGroup:getController() -- Dcs.DCSController#Controller + if DCSController then + DCSController:setOnOff( AIOnOff ) + return self + end + end + + return nil + end + + --- Turns the AI On for the GROUP. + -- @param #GROUP self + -- @return #GROUP The GROUP. + function GROUP:SetAIOn() + + return self:SetAIOnOff( true ) + end + + --- Turns the AI Off for the GROUP. + -- @param #GROUP self + -- @return #GROUP The GROUP. + function GROUP:SetAIOff() + + return self:SetAIOnOff( false ) + end + +end + + + +--- Returns the current maximum velocity of the group. +-- Each unit within the group gets evaluated, and the maximum velocity (= the unit which is going the fastest) is returned. +-- @param #GROUP self +-- @return #number Maximum velocity found. +function GROUP:GetMaxVelocity() + self:F2() + + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + local GroupVelocityMax = 0 + + for Index, UnitData in pairs( DCSGroup:getUnits() ) do + + local UnitVelocityVec3 = UnitData:getVelocity() + local UnitVelocity = math.abs( UnitVelocityVec3.x ) + math.abs( UnitVelocityVec3.y ) + math.abs( UnitVelocityVec3.z ) + + if UnitVelocity > GroupVelocityMax then + GroupVelocityMax = UnitVelocity + end + end + + return GroupVelocityMax + end + + return nil +end + +--- Returns the current minimum height of the group. +-- Each unit within the group gets evaluated, and the minimum height (= the unit which is the lowest elevated) is returned. +-- @param #GROUP self +-- @return #number Minimum height found. +function GROUP:GetMinHeight() + self:F2() + +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 + +-- SPAWNING + +--- Respawn the @{GROUP} using a (tweaked) template of the Group. +-- The template must be retrieved with the @{Group#GROUP.GetTemplate}() function. +-- The template contains all the definitions as declared within the mission file. +-- To understand templates, do the following: +-- +-- * unpack your .miz file into a directory using 7-zip. +-- * browse in the directory created to the file **mission**. +-- * open the file and search for the country group definitions. +-- +-- Your group template will contain the fields as described within the mission file. +-- +-- This function will: +-- +-- * Get the current position and heading of the group. +-- * When the group is alive, it will tweak the template x, y and heading coordinates of the group and the embedded units to the current units positions. +-- * Then it will destroy the current alive group. +-- * And it will respawn the group using your new template definition. +-- @param Wrapper.Group#GROUP self +-- @param #table Template The template of the Group retrieved with GROUP:GetTemplate() +function GROUP:Respawn( Template ) + + local Vec3 = self:GetVec3() + 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 -- Wrapper.Unit#UNIT + self:E( GroupUnit:GetName() ) + if GroupUnit:IsAlive() then + local GroupUnitVec3 = GroupUnit:GetVec3() + 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 + + self:Destroy() + _DATABASE:Spawn( Template ) +end + +--- Returns the group template from the @{DATABASE} (_DATABASE object). +-- @param #GROUP self +-- @return #table +function GROUP:GetTemplate() + local GroupName = self:GetName() + self:E( GroupName ) + return _DATABASE:GetGroupTemplate( GroupName ) +end + +--- Sets the controlled status in a Template. +-- @param #GROUP self +-- @param #boolean Controlled true is controlled, false is uncontrolled. +-- @return #table +function GROUP:SetTemplateControlled( Template, Controlled ) + Template.uncontrolled = not Controlled + return Template +end + +--- Sets the CountryID of the group in a Template. +-- @param #GROUP self +-- @param Dcs.DCScountry#country.id CountryID The country ID. +-- @return #table +function GROUP:SetTemplateCountry( Template, CountryID ) + Template.CountryID = CountryID + return Template +end + +--- Sets the CoalitionID of the group in a Template. +-- @param #GROUP self +-- @param Dcs.DCSCoalitionWrapper.Object#coalition.side CoalitionID The coalition ID. +-- @return #table +function GROUP:SetTemplateCoalition( Template, CoalitionID ) + Template.CoalitionID = CoalitionID + return 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 + +--- Calculate the maxium A2G threat level of the Group. +-- @param #GROUP self +function GROUP:CalculateThreatLevelA2G() + + local MaxThreatLevelA2G = 0 + for UnitName, UnitData in pairs( self:GetUnits() ) do + local ThreatUnit = UnitData -- Wrapper.Unit#UNIT + local ThreatLevelA2G = ThreatUnit:GetThreatLevel() + if ThreatLevelA2G > MaxThreatLevelA2G then + MaxThreatLevelA2G = ThreatLevelA2G + end + end + + self:T3( MaxThreatLevelA2G ) + return MaxThreatLevelA2G +end + +--- Returns true if the first unit of the GROUP is in the air. +-- @param Wrapper.Group#GROUP self +-- @return #boolean true if in the first unit of the group is in the air. +-- @return #nil The GROUP is not existing or not alive. +function GROUP:InAir() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + local DCSUnit = DCSGroup:getUnit(1) + if DCSUnit then + local GroupInAir = DCSGroup:getUnit(1):inAir() + self:T3( GroupInAir ) + return GroupInAir + end + end + + return nil +end + +function GROUP:OnReSpawn( ReSpawnFunction ) + + self.ReSpawnFunction = ReSpawnFunction +end + +do -- Event Handling + + --- Subscribe to a DCS Event. + -- @param #GROUP self + -- @param Core.Event#EVENTS Event + -- @param #function EventFunction (optional) The function to be called when the event occurs for the GROUP. + -- @return #GROUP + function GROUP:HandleEvent( Event, EventFunction ) + + self:EventDispatcher():OnEventForGroup( self:GetName(), EventFunction, self, Event ) + + return self + end + + --- UnSubscribe to a DCS event. + -- @param #GROUP self + -- @param Core.Event#EVENTS Event + -- @return #GROUP + function GROUP:UnHandleEvent( Event ) + + self:EventDispatcher():RemoveForGroup( self:GetName(), self, Event ) + + return self + end + +end +--- This module contains the UNIT class. +-- +-- 1) @{#UNIT} class, extends @{Controllable#CONTROLLABLE} +-- =========================================================== +-- The @{#UNIT} class is a wrapper class to handle the DCS Unit objects: +-- +-- * Support all DCS Unit APIs. +-- * Enhance with Unit specific APIs not in the DCS Unit API set. +-- * Handle local Unit Controller. +-- * Manage the "state" of the DCS Unit. +-- +-- +-- 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 @{DCSWrapper.Unit#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.GetVec3}() will obtain the current **location** of the DCS Unit in a Vec2 (2D) or a **point** in a Vec3 (3D) vector respectively. +-- If you want to obtain the complete **3D position** including 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 Wrapper.Controllable#CONTROLLABLE +UNIT = { + ClassName="UNIT", +} + + +--- Unit.SensorType +-- @type Unit.SensorType +-- @field OPTIC +-- @field RADAR +-- @field IRST +-- @field RWR + + +-- Registration. + +--- Create a new UNIT from DCSUnit. +-- @param #UNIT self +-- @param #string UnitName The name of the DCS unit. +-- @return #UNIT +function UNIT:Register( UnitName ) + local self = BASE:Inherit( self, CONTROLLABLE:New( UnitName ) ) + self.UnitName = UnitName + + self:SetEventPriority( 3 ) + return self +end + +-- Reference methods. + +--- Finds a UNIT from the _DATABASE using a DCSUnit object. +-- @param #UNIT self +-- @param Dcs.DCSWrapper.Unit#Unit DCSUnit An existing DCS Unit object reference. +-- @return #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 self +function UNIT:FindByName( UnitName ) + + local UnitFound = _DATABASE:FindUnit( UnitName ) + return UnitFound +end + +--- Return the name of the UNIT. +-- @param #UNIT self +-- @return #string The UNIT name. +function UNIT:Name() + + return self.UnitName +end + + +--- @param #UNIT self +-- @return Dcs.DCSWrapper.Unit#Unit +function UNIT:GetDCSObject() + + local DCSUnit = Unit.getByName( self.UnitName ) + + if DCSUnit then + return DCSUnit + end + + return nil +end + +--- Respawn the @{Unit} using a (tweaked) template of the parent Group. +-- +-- This function will: +-- +-- * Get the current position and heading of the group. +-- * When the unit is alive, it will tweak the template x, y and heading coordinates of the group and the embedded units to the current units positions. +-- * Then it will respawn the re-modelled group. +-- +-- @param #UNIT self +-- @param Dcs.DCSTypes#Vec3 SpawnVec3 The position where to Spawn the new Unit at. +-- @param #number Heading The heading of the unit respawn. +function UNIT:ReSpawn( SpawnVec3, Heading ) + + local SpawnGroupTemplate = UTILS.DeepCopy( _DATABASE:GetGroupTemplateFromUnitName( self:Name() ) ) + self:T( SpawnGroupTemplate ) + + local SpawnGroup = self:GetGroup() + + if SpawnGroup then + + local Vec3 = SpawnGroup:GetVec3() + SpawnGroupTemplate.x = SpawnVec3.x + SpawnGroupTemplate.y = SpawnVec3.z + + self:E( #SpawnGroupTemplate.units ) + for UnitID, UnitData in pairs( SpawnGroup:GetUnits() ) do + local GroupUnit = UnitData -- #UNIT + self:E( GroupUnit:GetName() ) + if GroupUnit:IsAlive() then + local GroupUnitVec3 = GroupUnit:GetVec3() + local GroupUnitHeading = GroupUnit:GetHeading() + SpawnGroupTemplate.units[UnitID].alt = GroupUnitVec3.y + SpawnGroupTemplate.units[UnitID].x = GroupUnitVec3.x + SpawnGroupTemplate.units[UnitID].y = GroupUnitVec3.z + SpawnGroupTemplate.units[UnitID].heading = GroupUnitHeading + self:E( { UnitID, SpawnGroupTemplate.units[UnitID], SpawnGroupTemplate.units[UnitID] } ) + end + end + end + + for UnitTemplateID, UnitTemplateData in pairs( SpawnGroupTemplate.units ) do + self:T( UnitTemplateData.name ) + if UnitTemplateData.name == self:Name() then + self:T("Adjusting") + SpawnGroupTemplate.units[UnitTemplateID].alt = SpawnVec3.y + SpawnGroupTemplate.units[UnitTemplateID].x = SpawnVec3.x + SpawnGroupTemplate.units[UnitTemplateID].y = SpawnVec3.z + SpawnGroupTemplate.units[UnitTemplateID].heading = Heading + self:E( { UnitTemplateID, SpawnGroupTemplate.units[UnitTemplateID], SpawnGroupTemplate.units[UnitTemplateID] } ) + else + self:E( SpawnGroupTemplate.units[UnitTemplateID].name ) + local GroupUnit = UNIT:FindByName( SpawnGroupTemplate.units[UnitTemplateID].name ) -- #UNIT + if GroupUnit and GroupUnit:IsAlive() then + local GroupUnitVec3 = GroupUnit:GetVec3() + local GroupUnitHeading = GroupUnit:GetHeading() + UnitTemplateData.alt = GroupUnitVec3.y + UnitTemplateData.x = GroupUnitVec3.x + UnitTemplateData.y = GroupUnitVec3.z + UnitTemplateData.heading = GroupUnitHeading + else + if SpawnGroupTemplate.units[UnitTemplateID].name ~= self:Name() then + self:T("nilling") + SpawnGroupTemplate.units[UnitTemplateID].delete = true + end + end + end + end + + -- Remove obscolete units from the group structure + i = 1 + while i <= #SpawnGroupTemplate.units do + + local UnitTemplateData = SpawnGroupTemplate.units[i] + self:T( UnitTemplateData.name ) + + if UnitTemplateData.delete then + table.remove( SpawnGroupTemplate.units, i ) + else + i = i + 1 + end + end + + _DATABASE:Spawn( SpawnGroupTemplate ) +end + + + +--- Returns if the unit is activated. +-- @param #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 the Unit's callsign - the localized string. +-- @param #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 + + 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 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 number in the group. +-- The number is the same number the unit has in ME. +-- It may not be changed during the mission. +-- If any unit in the group is destroyed, the numbers of another units will not be changed. +-- @param #UNIT self +-- @return #number The Unit number. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetNumber() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitNumber = DCSUnit:getNumber() + return UnitNumber + end + + return nil +end + +--- Returns the unit's group if it exist and nil otherwise. +-- @param Wrapper.Unit#UNIT self +-- @return Wrapper.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 + + +-- Need to add here functions to check if radar is on and which object etc. + +--- Returns the prefix name of the DCS Unit. A prefix name is a part of the name before a '#'-sign. +-- DCS Units spawned with the @{SPAWN} class contain a '#'-sign to indicate the end of the (base) DCS Unit name. +-- The spawn sequence number and unit number are contained within the name after the '#' sign. +-- @param #UNIT self +-- @return #string The name of the DCS Unit. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetPrefix() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitPrefix = string.match( self.UnitName, ".*#" ):sub( 1, -2 ) + self:T3( UnitPrefix ) + return UnitPrefix + end + + return nil +end + +--- Returns the Unit's ammunition. +-- @param #UNIT self +-- @return Dcs.DCSWrapper.Unit#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 self +-- @return Dcs.DCSWrapper.Unit#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 if the unit has sensors of a certain type. +-- @param #UNIT self +-- @return #boolean returns true if the unit has specified types of sensors. This function is more preferable than Unit.getSensors() if you don't want to get information about all the unit's sensors, and just want to check if the unit has specified types of sensors. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:HasSensors( ... ) + self:F2( arg ) + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local HasSensors = DCSUnit:hasSensors( unpack( arg ) ) + return HasSensors + end + + return nil +end + +--- Returns if the unit is SEADable. +-- @param #UNIT self +-- @return #boolean returns true if the unit is SEADable. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:HasSEAD() + self:F2() + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitSEADAttributes = DCSUnit:getDesc().attributes + + local HasSEAD = false + if UnitSEADAttributes["RADAR_BAND1_FOR_ARM"] and UnitSEADAttributes["RADAR_BAND1_FOR_ARM"] == true or + UnitSEADAttributes["RADAR_BAND2_FOR_ARM"] and UnitSEADAttributes["RADAR_BAND2_FOR_ARM"] == true then + HasSEAD = true + end + return HasSEAD + end + + return nil +end + +--- Returns two values: +-- +-- * First value indicates if at least one of the unit's radar(s) is on. +-- * Second value is the object of the radar's interest. Not nil only if at least one radar of the unit is tracking a target. +-- @param #UNIT self +-- @return #boolean Indicates if at least one of the unit's radar(s) is on. +-- @return Dcs.DCSWrapper.Object#Object The object of the radar's interest. Not nil only if at least one radar of the unit is tracking a target. +-- @return #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 + +--- Returns relative amount of fuel (from 0.0 to 1.0) the unit has in its internal tanks. If there are additional fuel tanks the value may be greater than 1.0. +-- @param #UNIT self +-- @return #number The relative amount of fuel (from 0.0 to 1.0). +-- @return #nil The DCS Unit is not existing or alive. +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 in a UNIT list of one element. +-- @param #UNIT self +-- @return #list The UNITs wrappers. +function UNIT:GetUnits() + self:F2( { self.UnitName } ) + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local DCSUnits = DCSUnit:getUnits() + local Units = {} + Units[1] = UNIT:Find( DCSUnit ) + self:T3( Units ) + return Units + end + + return nil +end + + +--- Returns the unit's health. Dead units has health <= 1.0. +-- @param #UNIT self +-- @return #number The Unit's health value. +-- @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 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 the Unit's A2G threat level on a scale from 1 to 10 ... +-- The following threat levels are foreseen: +-- +-- * Threat level 0: Unit is unarmed. +-- * Threat level 1: Unit is infantry. +-- * Threat level 2: Unit is an infantry vehicle. +-- * Threat level 3: Unit is ground artillery. +-- * Threat level 4: Unit is a tank. +-- * Threat level 5: Unit is a modern tank or ifv with ATGM. +-- * Threat level 6: Unit is a AAA. +-- * Threat level 7: Unit is a SAM or manpad, IR guided. +-- * Threat level 8: Unit is a Short Range SAM, radar guided. +-- * Threat level 9: Unit is a Medium Range SAM, radar guided. +-- * Threat level 10: Unit is a Long Range SAM, radar guided. +-- @param #UNIT self +function UNIT:GetThreatLevel() + + local Attributes = self:GetDesc().attributes + self:E( Attributes ) + + local ThreatLevel = 0 + local ThreatText = "" + + if self:IsGround() then + + self:E( "Ground" ) + + local ThreatLevels = { + "Unarmed", + "Infantry", + "Old Tanks & APCs", + "Tanks & IFVs without ATGM", + "Tanks & IFV with ATGM", + "Modern Tanks", + "AAA", + "IR Guided SAMs", + "SR SAMs", + "MR SAMs", + "LR SAMs" + } + + + if Attributes["LR SAM"] then ThreatLevel = 10 + elseif Attributes["MR SAM"] then ThreatLevel = 9 + elseif Attributes["SR SAM"] and + not Attributes["IR Guided SAM"] then ThreatLevel = 8 + elseif ( Attributes["SR SAM"] or Attributes["MANPADS"] ) and + Attributes["IR Guided SAM"] then ThreatLevel = 7 + elseif Attributes["AAA"] then ThreatLevel = 6 + elseif Attributes["Modern Tanks"] then ThreatLevel = 5 + elseif ( Attributes["Tanks"] or Attributes["IFV"] ) and + Attributes["ATGM"] then ThreatLevel = 4 + elseif ( Attributes["Tanks"] or Attributes["IFV"] ) and + not Attributes["ATGM"] then ThreatLevel = 3 + elseif Attributes["Old Tanks"] or Attributes["APC"] or Attributes["Artillery"] then ThreatLevel = 2 + elseif Attributes["Infantry"] then ThreatLevel = 1 + end + + ThreatText = ThreatLevels[ThreatLevel+1] + end + + if self:IsAir() then + + self:E( "Air" ) + + local ThreatLevels = { + "Unarmed", + "Tanker", + "AWACS", + "Transport Helicpter", + "UAV", + "Bomber", + "Strategic Bomber", + "Attack Helicopter", + "Interceptor", + "Multirole Fighter", + "Fighter" + } + + + if Attributes["Fighters"] then ThreatLevel = 10 + elseif Attributes["Multirole fighters"] then ThreatLevel = 9 + elseif Attributes["Battleplanes"] then ThreatLevel = 8 + elseif Attributes["Attack helicopters"] then ThreatLevel = 7 + elseif Attributes["Strategic bombers"] then ThreatLevel = 6 + elseif Attributes["Bombers"] then ThreatLevel = 5 + elseif Attributes["UAVs"] then ThreatLevel = 4 + elseif Attributes["Transport helicopters"] then ThreatLevel = 3 + elseif Attributes["AWACS"] then ThreatLevel = 2 + elseif Attributes["Tankers"] then ThreatLevel = 1 + end + + ThreatText = ThreatLevels[ThreatLevel+1] + end + + if self:IsShip() then + + self:E( "Ship" ) + +--["Aircraft Carriers"] = {"Heavy armed ships",}, +--["Cruisers"] = {"Heavy armed ships",}, +--["Destroyers"] = {"Heavy armed ships",}, +--["Frigates"] = {"Heavy armed ships",}, +--["Corvettes"] = {"Heavy armed ships",}, +--["Heavy armed ships"] = {"Armed ships", "Armed Air Defence", "HeavyArmoredUnits",}, +--["Light armed ships"] = {"Armed ships","NonArmoredUnits"}, +--["Armed ships"] = {"Ships"}, +--["Unarmed ships"] = {"Ships","HeavyArmoredUnits",}, + + local ThreatLevels = { + "Unarmed ship", + "Light armed ships", + "Corvettes", + "", + "Frigates", + "", + "Cruiser", + "", + "Destroyer", + "", + "Aircraft Carrier" + } + + + if Attributes["Aircraft Carriers"] then ThreatLevel = 10 + elseif Attributes["Destroyers"] then ThreatLevel = 8 + elseif Attributes["Cruisers"] then ThreatLevel = 6 + elseif Attributes["Frigates"] then ThreatLevel = 4 + elseif Attributes["Corvettes"] then ThreatLevel = 2 + elseif Attributes["Light armed ships"] then ThreatLevel = 1 + end + + ThreatText = ThreatLevels[ThreatLevel+1] + end + + self:T2( ThreatLevel ) + return ThreatLevel, ThreatText + +end + + +-- Is functions + +--- Returns true if the unit is within a @{Zone}. +-- @param #UNIT self +-- @param Core.Zone#ZONE_BASE Zone The zone to test. +-- @return #boolean Returns true if the unit is within the @{Zone#ZONE_BASE} +function UNIT:IsInZone( Zone ) + self:F2( { self.UnitName, Zone } ) + + if self:IsAlive() then + local IsInZone = Zone:IsVec3InZone( self:GetVec3() ) + + self:T( { IsInZone } ) + return IsInZone + end + + return false +end + +--- Returns true if the unit is not within a @{Zone}. +-- @param #UNIT self +-- @param Core.Zone#ZONE_BASE Zone The zone to test. +-- @return #boolean Returns true if the unit is not within the @{Zone#ZONE_BASE} +function UNIT:IsNotInZone( Zone ) + self:F2( { self.UnitName, Zone } ) + + if self:IsAlive() then + local IsInZone = not Zone:IsVec3InZone( self:GetVec3() ) + + self:T( { IsInZone } ) + return IsInZone + else + return false + end +end + + +--- Returns true if there is an **other** DCS Unit within a radius of the current 2D point of the DCS Unit. +-- @param #UNIT self +-- @param #UNIT AwaitUnit The other UNIT wrapper object. +-- @param Radius The radius in meters with the DCS Unit in the centre. +-- @return true If the other DCS Unit is within the radius of the 2D point of the DCS Unit. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:OtherUnitInRadius( AwaitUnit, Radius ) + self:F2( { self.UnitName, AwaitUnit.UnitName, Radius } ) + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitVec3 = self:GetVec3() + local AwaitUnitVec3 = AwaitUnit:GetVec3() + + if (((UnitVec3.x - AwaitUnitVec3.x)^2 + (UnitVec3.z - AwaitUnitVec3.z)^2)^0.5 <= Radius) then + self:T3( "true" ) + return true + else + self:T3( "false" ) + return false + end + end + + return nil +end + + + +--- Signal a flare at the position of the UNIT. +-- @param #UNIT self +-- @param Utilities.Utils#FLARECOLOR FlareColor +function UNIT:Flare( FlareColor ) + self:F2() + trigger.action.signalFlare( self:GetVec3(), 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:GetVec3(), 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:GetVec3(), 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:GetVec3(), trigger.flareColor.Green , 0 ) +end + +--- Signal a red flare at the position of the UNIT. +-- @param #UNIT self +function UNIT:FlareRed() + self:F2() + local Vec3 = self:GetVec3() + if Vec3 then + trigger.action.signalFlare( Vec3, trigger.flareColor.Red, 0 ) + end +end + +--- Smoke the UNIT. +-- @param #UNIT self +function UNIT:Smoke( SmokeColor, Range ) + self:F2() + if Range then + trigger.action.smoke( self:GetRandomVec3( Range ), SmokeColor ) + else + trigger.action.smoke( self:GetVec3(), SmokeColor ) + end + +end + +--- Smoke the UNIT Green. +-- @param #UNIT self +function UNIT:SmokeGreen() + self:F2() + trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Green ) +end + +--- Smoke the UNIT Red. +-- @param #UNIT self +function UNIT:SmokeRed() + self:F2() + trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Red ) +end + +--- Smoke the UNIT White. +-- @param #UNIT self +function UNIT:SmokeWhite() + self:F2() + trigger.action.smoke( self:GetVec3(), trigger.smokeColor.White ) +end + +--- Smoke the UNIT Orange. +-- @param #UNIT self +function UNIT:SmokeOrange() + self:F2() + trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Orange ) +end + +--- Smoke the UNIT Blue. +-- @param #UNIT self +function UNIT:SmokeBlue() + self:F2() + trigger.action.smoke( self:GetVec3(), 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 DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitDescriptor = DCSUnit:getDesc() + self:T3( { UnitDescriptor.category, Unit.Category.AIRPLANE, Unit.Category.HELICOPTER } ) + + local IsAirResult = ( UnitDescriptor.category == Unit.Category.AIRPLANE ) or ( UnitDescriptor.category == Unit.Category.HELICOPTER ) + + self:T3( IsAirResult ) + return IsAirResult + end + + return nil +end + +--- Returns if the unit is of an ground category. +-- If the unit is a ground vehicle or infantry, this method will return true, otherwise false. +-- @param #UNIT self +-- @return #boolean Ground category evaluation result. +function UNIT:IsGround() + self:F2() + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitDescriptor = DCSUnit:getDesc() + self:T3( { UnitDescriptor.category, Unit.Category.GROUND_UNIT } ) + + local IsGroundResult = ( UnitDescriptor.category == Unit.Category.GROUND_UNIT ) + + self:T3( IsGroundResult ) + return IsGroundResult + end + + return nil +end + +--- Returns if the unit is a friendly unit. +-- @param #UNIT self +-- @return #boolean IsFriendly evaluation result. +function UNIT:IsFriendly( FriendlyCoalition ) + self:F2() + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitCoalition = DCSUnit:getCoalition() + self:T3( { UnitCoalition, FriendlyCoalition } ) + + local IsFriendlyResult = ( UnitCoalition == FriendlyCoalition ) + + self:E( IsFriendlyResult ) + return IsFriendlyResult + end + + return nil +end + +--- Returns if the unit is of a ship category. +-- If the unit is a ship, this method will return true, otherwise false. +-- @param #UNIT self +-- @return #boolean Ship category evaluation result. +function UNIT:IsShip() + self:F2() + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitDescriptor = DCSUnit:getDesc() + self:T3( { UnitDescriptor.category, Unit.Category.SHIP } ) + + local IsShipResult = ( UnitDescriptor.category == Unit.Category.SHIP ) + + self:T3( IsShipResult ) + return IsShipResult + end + + return nil +end + +--- Returns true if the UNIT is in the air. +-- @param Wrapper.Positionable#UNIT self +-- @return #boolean true if in the air. +-- @return #nil The UNIT is not existing or alive. +function UNIT:InAir() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitInAir = DCSUnit:inAir() + self:T3( UnitInAir ) + return UnitInAir + end + + return nil +end + +do -- Event Handling + + --- Subscribe to a DCS Event. + -- @param #UNIT self + -- @param Core.Event#EVENTS Event + -- @param #function EventFunction (optional) The function to be called when the event occurs for the unit. + -- @return #UNIT + function UNIT:HandleEvent( Event, EventFunction ) + + self:EventDispatcher():OnEventForUnit( self:GetName(), EventFunction, self, Event ) + + return self + end + + --- UnSubscribe to a DCS event. + -- @param #UNIT self + -- @param Core.Event#EVENTS Event + -- @return #UNIT + function UNIT:UnHandleEvent( Event ) + + self:EventDispatcher():RemoveForUnit( self:GetName(), self, Event ) + + return self + end + +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 + +--- The CLIENT class +-- @type CLIENT +-- @extends Wrapper.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. +-- @param #boolean Error A flag that indicates whether an error should be raised if the CLIENT cannot be found. By default an error will be raised. +-- @return #CLIENT +-- @usage +-- -- Create new Clients. +-- local Mission = MISSIONSCHEDULER.AddMission( 'Russia Transport Troops SA-6', 'Operational', 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', 'Russia' ) +-- Mission:AddGoal( DeploySA6TroopsGoal ) +-- +-- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 1' ):Transport() ) +-- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 3' ):Transport() ) +-- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 2' ):Transport() ) +-- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 4' ):Transport() ) +function CLIENT:FindByName( ClientName, ClientBriefing, Error ) + local ClientFound = _DATABASE:FindClient( ClientName ) + + if ClientFound then + ClientFound:F( { ClientName, ClientBriefing } ) + ClientFound:AddBriefing( ClientBriefing ) + ClientFound.MessageSwitch = true + + return ClientFound + end + + if not Error then + error( "CLIENT not found for: " .. ClientName ) + end +end + +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 CallBackFunction Create a function that will be called when a player joins the slot. +-- @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:F3( { 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 Dcs.DCSWrapper.Group#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 Dcs.DCSTypes#Group.ID +--- Get the group ID of the client. +-- @param #CLIENT self +-- @return Dcs.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 Wrapper.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 Dcs.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 @{AI_Cargo#CARGO} contained within the CLIENT to the player as a message. +-- The @{AI_Cargo#CARGO} is shown using the @{Message#MESSAGE} distribution system. +-- @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 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 @{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: +-- +-- * 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 Wrapper.Positionable#POSITIONABLE +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. +-- @param #boolean RaiseError Raise an error if not found. +-- @return #STATIC +function STATIC:FindByName( StaticName, RaiseError ) + local StaticFound = _DATABASE:FindStatic( StaticName ) + + self.StaticName = StaticName + + if StaticFound then + StaticFound:F( { StaticName } ) + + return StaticFound + end + + if RaiseError == nil or RaiseError == true then + error( "STATIC not found for: " .. StaticName ) + end + + return nil +end + +function STATIC:Register( StaticName ) + local self = BASE:Inherit( self, POSITIONABLE:New( StaticName ) ) + self.StaticName = StaticName + return self +end + + +function STATIC:GetDCSObject() + local DCSStatic = StaticObject.getByName( self.StaticName ) + + if DCSStatic then + return DCSStatic + end + + return nil +end + +function STATIC:GetThreatLevel() + + return 1, "Static" +end--- This module contains the AIRBASE classes. +-- +-- === +-- +-- 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. +-- * 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 @{DCSWrapper.Airbase#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 Wrapper.Positionable#POSITIONABLE +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 #string AirbaseName The name of the airbase. +-- @return Wrapper.Airbase#AIRBASE +function AIRBASE:Register( AirbaseName ) + + local self = BASE:Inherit( self, POSITIONABLE:New( AirbaseName ) ) + self.AirbaseName = AirbaseName + return self +end + +-- Reference methods. + +--- Finds a AIRBASE from the _DATABASE using a DCSAirbase object. +-- @param #AIRBASE self +-- @param Dcs.DCSWrapper.Airbase#Airbase DCSAirbase An existing DCS Airbase object reference. +-- @return Wrapper.Airbase#AIRBASE self +function AIRBASE:Find( DCSAirbase ) + + local AirbaseName = DCSAirbase:getName() + local AirbaseFound = _DATABASE:FindAirbase( AirbaseName ) + return AirbaseFound +end + +--- Find a AIRBASE in the _DATABASE using the name of an existing DCS Airbase. +-- @param #AIRBASE self +-- @param #string AirbaseName The Airbase Name. +-- @return Wrapper.Airbase#AIRBASE self +function AIRBASE:FindByName( AirbaseName ) + + local AirbaseFound = _DATABASE:FindAirbase( AirbaseName ) + return AirbaseFound +end + +function AIRBASE:GetDCSObject() + local DCSAirbase = Airbase.getByName( self.AirbaseName ) + + if DCSAirbase then + return DCSAirbase + end + + return nil +end + + + +--- This module contains the SCENERY class. +-- +-- 1) @{Scenery#SCENERY} class, extends @{Positionable#POSITIONABLE} +-- =============================================================== +-- Scenery objects are defined on the map. +-- The @{Scenery#SCENERY} class is a wrapper class to handle the DCS Scenery objects: +-- +-- * Wraps the DCS Scenery objects. +-- * Support all DCS Scenery APIs. +-- * Enhance with Scenery specific APIs not in the DCS API set. +-- +-- @module Scenery +-- @author FlightControl + + + +--- The SCENERY class +-- @type SCENERY +-- @extends Wrapper.Positionable#POSITIONABLE +SCENERY = { + ClassName = "SCENERY", +} + + +function SCENERY:Register( SceneryName, SceneryObject ) + local self = BASE:Inherit( self, POSITIONABLE:New( SceneryName ) ) + self.SceneryName = SceneryName + self.SceneryObject = SceneryObject + return self +end + +function SCENERY:GetDCSObject() + return self.SceneryObject +end + +function SCENERY:GetThreatLevel() + + return 0, "Scenery" +end +--- Single-Player:**Yes** / Multi-Player:**Yes** / Core:**Yes** -- **Administer the scoring of player achievements, +-- and create a CSV file logging the scoring events for use at team or squadron websites.** +-- +-- ![Banner Image](..\Presentations\SCORING\Dia1.JPG) +-- +-- === +-- +-- # 1) @{Scoring#SCORING} class, extends @{Base#BASE} +-- +-- The @{#SCORING} class administers the scoring of player achievements, +-- and creates a CSV file logging the scoring events and results for use at team or squadron websites. +-- +-- SCORING automatically calculates the threat level of the objects hit and destroyed by players, +-- which can be @{Unit}, @{Static) and @{Scenery} objects. +-- +-- Positive score points are granted when enemy or neutral targets are destroyed. +-- Negative score points or penalties are given when a friendly target is hit or destroyed. +-- This brings a lot of dynamism in the scoring, where players need to take care to inflict damage on the right target. +-- By default, penalties weight heavier in the scoring, to ensure that players don't commit fratricide. +-- The total score of the player is calculated by **adding the scores minus the penalties**. +-- +-- ![Banner Image](..\Presentations\SCORING\Dia4.JPG) +-- +-- The score value is calculated based on the **threat level of the player** and the **threat level of the target**. +-- A calculated score takes the threat level of the target divided by a balanced threat level of the player unit. +-- As such, if the threat level of the target is high, and the player threat level is low, a higher score will be given than +-- if the threat level of the player would be high too. +-- +-- ![Banner Image](..\Presentations\SCORING\Dia5.JPG) +-- +-- When multiple players hit the same target, and finally succeed in destroying the target, then each player who contributed to the target +-- destruction, will receive a score. This is important for targets that require significant damage before it can be destroyed, like +-- ships or heavy planes. +-- +-- ![Banner Image](..\Presentations\SCORING\Dia13.JPG) +-- +-- Optionally, the score values can be **scaled** by a **scale**. Specific scales can be set for positive cores or negative penalties. +-- The default range of the scores granted is a value between 0 and 10. The default range of penalties given is a value between 0 and 30. +-- +-- ![Banner Image](..\Presentations\SCORING\Dia7.JPG) +-- +-- **Additional scores** can be granted to **specific objects**, when the player(s) destroy these objects. +-- +-- ![Banner Image](..\Presentations\SCORING\Dia9.JPG) +-- +-- Various @{Zone}s can be defined for which scores are also granted when objects in that @{Zone} are destroyed. +-- This is **specifically useful** to designate **scenery targets on the map** that will generate points when destroyed. +-- +-- With a small change in MissionScripting.lua, the scoring results can also be logged in a **CSV file**. +-- These CSV files can be used to: +-- +-- * Upload scoring to a database or a BI tool to publish the scoring results to the player community. +-- * Upload scoring in an (online) Excel like tool, using pivot tables and pivot charts to show mission results. +-- * Share scoring amoung players after the mission to discuss mission results. +-- +-- Scores can be **reported**. **Menu options** are automatically added to **each player group** when a player joins a client slot or a CA unit. +-- Use the radio menu F10 to consult the scores while running the mission. +-- Scores can be reported for your user, or an overall score can be reported of all players currently active in the mission. +-- +-- ## 1.1) Set the destroy score or penalty scale +-- +-- Score scales can be set for scores granted when enemies or friendlies are destroyed. +-- Use the method @{#SCORING.SetScaleDestroyScore}() to set the scale of enemy destroys (positive destroys). +-- Use the method @{#SCORING.SetScaleDestroyPenalty}() to set the scale of friendly destroys (negative destroys). +-- +-- local Scoring = SCORING:New( "Scoring File" ) +-- Scoring:SetScaleDestroyScore( 10 ) +-- Scoring:SetScaleDestroyPenalty( 40 ) +-- +-- The above sets the scale for valid scores to 10. So scores will be given in a scale from 0 to 10. +-- The penalties will be given in a scale from 0 to 40. +-- +-- ## 1.2) Define special targets that will give extra scores. +-- +-- Special targets can be set that will give extra scores to the players when these are destroyed. +-- Use the methods @{#SCORING.AddUnitScore}() and @{#SCORING.RemoveUnitScore}() to specify a special additional score for a specific @{Unit}s. +-- Use the methods @{#SCORING.AddStaticScore}() and @{#SCORING.RemoveStaticScore}() to specify a special additional score for a specific @{Static}s. +-- Use the method @{#SCORING.SetGroupGroup}() to specify a special additional score for a specific @{Group}s. +-- +-- local Scoring = SCORING:New( "Scoring File" ) +-- Scoring:AddUnitScore( UNIT:FindByName( "Unit #001" ), 200 ) +-- Scoring:AddStaticScore( STATIC:FindByName( "Static #1" ), 100 ) +-- +-- The above grants an additional score of 200 points for Unit #001 and an additional 100 points of Static #1 if these are destroyed. +-- Note that later in the mission, one can remove these scores set, for example, when the a goal achievement time limit is over. +-- For example, this can be done as follows: +-- +-- Scoring:RemoveUnitScore( UNIT:FindByName( "Unit #001" ) ) +-- +-- +-- +-- ## 1.3) Define destruction zones that will give extra scores. +-- +-- Define zones of destruction. Any object destroyed within the zone of the given category will give extra points. +-- Use the method @{#SCORING.AddZoneScore}() to add a @{Zone} for additional scoring. +-- Use the method @{#SCORING.RemoveZoneScore}() to remove a @{Zone} for additional scoring. +-- There are interesting variations that can be achieved with this functionality. For example, if the @{Zone} is a @{Zone#ZONE_UNIT}, +-- then the zone is a moving zone, and anything destroyed within that @{Zone} will generate points. +-- The other implementation could be to designate a scenery target (a building) in the mission editor surrounded by a @{Zone}, +-- just large enough around that building. +-- +-- ## 1.4) Add extra Goal scores upon an event or a condition. +-- +-- A mission has goals and achievements. The scoring system provides an API to set additional scores when a goal or achievement event happens. +-- Use the method @{#SCORING.AddGoalScore}() to add a score for a Player at any time in your mission. +-- +-- ## 1.5) Configure fratricide level. +-- +-- When a player commits too much damage to friendlies, his penalty score will reach a certain level. +-- Use the method @{#SCORING.SetFratricide}() to define the level when a player gets kicked. +-- By default, the fratricide level is the default penalty mutiplier * 2 for the penalty score. +-- +-- ## 1.6) Penalty score when a player changes the coalition. +-- +-- When a player changes the coalition, he can receive a penalty score. +-- Use the method @{#SCORING.SetCoalitionChangePenalty}() to define the penalty when a player changes coalition. +-- By default, the penalty for changing coalition is the default penalty scale. +-- +-- ## 1.8) Define output CSV files. +-- +-- The CSV file is given the name of the string given in the @{#SCORING.New}{} constructor, followed by the .csv extension. +-- The file is incrementally saved in the **\\Saved Games\\DCS\\Logs** folder, and has a time stamp indicating each mission run. +-- See the following example: +-- +-- local ScoringFirstMission = SCORING:New( "FirstMission" ) +-- local ScoringSecondMission = SCORING:New( "SecondMission" ) +-- +-- The above documents that 2 Scoring objects are created, ScoringFirstMission and ScoringSecondMission. +-- +-- ## 1.9) Configure messages. +-- +-- When players hit or destroy targets, messages are sent. +-- Various methods exist to configure: +-- +-- * Which messages are sent upon the event. +-- * Which audience receives the message. +-- +-- ### 1.9.1) Configure the messages sent upon the event. +-- +-- Use the following methods to configure when to send messages. By default, all messages are sent. +-- +-- * @{#SCORING.SetMessagesHit}(): Configure to send messages after a target has been hit. +-- * @{#SCORING.SetMessagesDestroy}(): Configure to send messages after a target has been destroyed. +-- * @{#SCORING.SetMessagesAddon}(): Configure to send messages for additional score, after a target has been destroyed. +-- * @{#SCORING.SetMessagesZone}(): Configure to send messages for additional score, after a target has been destroyed within a given zone. +-- +-- ### 1.9.2) Configure the audience of the messages. +-- +-- Use the following methods to configure the audience of the messages. By default, the messages are sent to all players in the mission. +-- +-- * @{#SCORING.SetMessagesToAll}(): Configure to send messages to all players. +-- * @{#SCORING.SetMessagesToCoalition}(): Configure to send messages to only those players within the same coalition as the player. +-- +-- +-- ==== +-- +-- # **API CHANGE HISTORY** +-- +-- The underlying change log documents the API changes. Please read this carefully. The following notation is used: +-- +-- * **Added** parts are expressed in bold type face. +-- * _Removed_ parts are expressed in italic type face. +-- +-- Hereby the change log: +-- +-- 2017-02-26: Initial class and API. +-- +-- === +-- +-- # **AUTHORS and CONTRIBUTIONS** +-- +-- ### Contributions: +-- +-- * **Wingthor (TAW)**: Testing & Advice. +-- * **Dutch-Baron (TAW)**: Testing & Advice. +-- * **[Whisper](http://forums.eagle.ru/member.php?u=3829): Testing and Advice. +-- +-- ### Authors: +-- +-- * **FlightControl**: Concept, Design & Programming. +-- +-- @module Scoring + + +--- The Scoring class +-- @type SCORING +-- @field Players A collection of the current players that have joined the game. +-- @extends Core.Base#BASE +SCORING = { + ClassName = "SCORING", + ClassID = 0, + Players = {}, +} + +local _SCORINGCoalition = + { + [1] = "Red", + [2] = "Blue", + } + +local _SCORINGCategory = + { + [Unit.Category.AIRPLANE] = "Plane", + [Unit.Category.HELICOPTER] = "Helicopter", + [Unit.Category.GROUND_UNIT] = "Vehicle", + [Unit.Category.SHIP] = "Ship", + [Unit.Category.STRUCTURE] = "Structure", + } + +--- Creates a new SCORING object to administer the scoring achieved by players. +-- @param #SCORING self +-- @param #string GameName The name of the game. This name is also logged in the CSV score file. +-- @return #SCORING self +-- @usage +-- -- Define a new scoring object for the mission Gori Valley. +-- ScoringObject = SCORING:New( "Gori Valley" ) +function SCORING:New( GameName ) + + -- Inherits from BASE + local self = BASE:Inherit( self, BASE:New() ) -- #SCORING + + if GameName then + self.GameName = GameName + else + error( "A game name must be given to register the scoring results" ) + end + + + -- Additional Object scores + self.ScoringObjects = {} + + -- Additional Zone scores. + self.ScoringZones = {} + + -- Configure Messages + self:SetMessagesToAll() + self:SetMessagesHit( true ) + self:SetMessagesDestroy( true ) + self:SetMessagesScore( true ) + self:SetMessagesZone( true ) + + -- Scales + self:SetScaleDestroyScore( 10 ) + self:SetScaleDestroyPenalty( 30 ) + + -- Default fratricide penalty level (maximum penalty that can be assigned to a player before he gets kicked). + self:SetFratricide( self.ScaleDestroyPenalty * 3 ) + + -- Default penalty when a player changes coalition. + self:SetCoalitionChangePenalty( self.ScaleDestroyPenalty ) + + -- Event handlers + self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) + self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) + self:HandleEvent( EVENTS.Hit, self._EventOnHit ) + self:HandleEvent( EVENTS.PlayerEnterUnit ) + self:HandleEvent( EVENTS.PlayerLeaveUnit ) + + -- Create the CSV file. + self:OpenCSV( GameName ) + + return self + +end + +--- Set the scale for scoring valid destroys (enemy destroys). +-- A default calculated score is a value between 1 and 10. +-- The scale magnifies the scores given to the players. +-- @param #SCORING self +-- @param #number Scale The scale of the score given. +function SCORING:SetScaleDestroyScore( Scale ) + + self.ScaleDestroyScore = Scale + + return self +end + +--- Set the scale for scoring penalty destroys (friendly destroys). +-- A default calculated penalty is a value between 1 and 10. +-- The scale magnifies the scores given to the players. +-- @param #SCORING self +-- @param #number Scale The scale of the score given. +-- @return #SCORING +function SCORING:SetScaleDestroyPenalty( Scale ) + + self.ScaleDestroyPenalty = Scale + + return self +end + +--- Add a @{Unit} for additional scoring when the @{Unit} is destroyed. +-- Note that if there was already a @{Unit} declared within the scoring with the same name, +-- then the old @{Unit} will be replaced with the new @{Unit}. +-- @param #SCORING self +-- @param Wrapper.Unit#UNIT ScoreUnit The @{Unit} for which the Score needs to be given. +-- @param #number Score The Score value. +-- @return #SCORING +function SCORING:AddUnitScore( ScoreUnit, Score ) + + local UnitName = ScoreUnit:GetName() + + self.ScoringObjects[UnitName] = Score + + return self +end + +--- Removes a @{Unit} for additional scoring when the @{Unit} is destroyed. +-- @param #SCORING self +-- @param Wrapper.Unit#UNIT ScoreUnit The @{Unit} for which the Score needs to be given. +-- @return #SCORING +function SCORING:RemoveUnitScore( ScoreUnit ) + + local UnitName = ScoreUnit:GetName() + + self.ScoringObjects[UnitName] = nil + + return self +end + +--- Add a @{Static} for additional scoring when the @{Static} is destroyed. +-- Note that if there was already a @{Static} declared within the scoring with the same name, +-- then the old @{Static} will be replaced with the new @{Static}. +-- @param #SCORING self +-- @param Wrapper.Static#UNIT ScoreStatic The @{Static} for which the Score needs to be given. +-- @param #number Score The Score value. +-- @return #SCORING +function SCORING:AddStaticScore( ScoreStatic, Score ) + + local StaticName = ScoreStatic:GetName() + + self.ScoringObjects[StaticName] = Score + + return self +end + +--- Removes a @{Static} for additional scoring when the @{Static} is destroyed. +-- @param #SCORING self +-- @param Wrapper.Static#UNIT ScoreStatic The @{Static} for which the Score needs to be given. +-- @return #SCORING +function SCORING:RemoveStaticScore( ScoreStatic ) + + local StaticName = ScoreStatic:GetName() + + self.ScoringObjects[StaticName] = nil + + return self +end + + +--- Specify a special additional score for a @{Group}. +-- @param #SCORING self +-- @param Wrapper.Group#GROUP ScoreGroup The @{Group} for which each @{Unit} a Score is given. +-- @param #number Score The Score value. +-- @return #SCORING +function SCORING:AddScoreGroup( ScoreGroup, Score ) + + local ScoreUnits = ScoreGroup:GetUnits() + + for ScoreUnitID, ScoreUnit in pairs( ScoreUnits ) do + local UnitName = ScoreUnit:GetName() + self.ScoringObjects[UnitName] = Score + end + + return self +end + +--- Add a @{Zone} to define additional scoring when any object is destroyed in that zone. +-- Note that if a @{Zone} with the same name is already within the scoring added, the @{Zone} (type) and Score will be replaced! +-- This allows for a dynamic destruction zone evolution within your mission. +-- @param #SCORING self +-- @param Core.Zone#ZONE_BASE ScoreZone The @{Zone} which defines the destruction score perimeters. +-- Note that a zone can be a polygon or a moving zone. +-- @param #number Score The Score value. +-- @return #SCORING +function SCORING:AddZoneScore( ScoreZone, Score ) + + local ZoneName = ScoreZone:GetName() + + self.ScoringZones[ZoneName] = {} + self.ScoringZones[ZoneName].ScoreZone = ScoreZone + self.ScoringZones[ZoneName].Score = Score + + return self +end + +--- Remove a @{Zone} for additional scoring. +-- The scoring will search if any @{Zone} is added with the given name, and will remove that zone from the scoring. +-- This allows for a dynamic destruction zone evolution within your mission. +-- @param #SCORING self +-- @param Core.Zone#ZONE_BASE ScoreZone The @{Zone} which defines the destruction score perimeters. +-- Note that a zone can be a polygon or a moving zone. +-- @return #SCORING +function SCORING:RemoveZoneScore( ScoreZone ) + + local ZoneName = ScoreZone:GetName() + + self.ScoringZones[ZoneName] = nil + + return self +end + + +--- Configure to send messages after a target has been hit. +-- @param #SCORING self +-- @param #boolean OnOff If true is given, the messages are sent. +-- @return #SCORING +function SCORING:SetMessagesHit( OnOff ) + + self.MessagesHit = OnOff + return self +end + +--- If to send messages after a target has been hit. +-- @param #SCORING self +-- @return #boolean +function SCORING:IfMessagesHit() + + return self.MessagesHit +end + +--- Configure to send messages after a target has been destroyed. +-- @param #SCORING self +-- @param #boolean OnOff If true is given, the messages are sent. +-- @return #SCORING +function SCORING:SetMessagesDestroy( OnOff ) + + self.MessagesDestroy = OnOff + return self +end + +--- If to send messages after a target has been destroyed. +-- @param #SCORING self +-- @return #boolean +function SCORING:IfMessagesDestroy() + + return self.MessagesDestroy +end + +--- Configure to send messages after a target has been destroyed and receives additional scores. +-- @param #SCORING self +-- @param #boolean OnOff If true is given, the messages are sent. +-- @return #SCORING +function SCORING:SetMessagesScore( OnOff ) + + self.MessagesScore = OnOff + return self +end + +--- If to send messages after a target has been destroyed and receives additional scores. +-- @param #SCORING self +-- @return #boolean +function SCORING:IfMessagesScore() + + return self.MessagesScore +end + +--- Configure to send messages after a target has been hit in a zone, and additional score is received. +-- @param #SCORING self +-- @param #boolean OnOff If true is given, the messages are sent. +-- @return #SCORING +function SCORING:SetMessagesZone( OnOff ) + + self.MessagesZone = OnOff + return self +end + +--- If to send messages after a target has been hit in a zone, and additional score is received. +-- @param #SCORING self +-- @return #boolean +function SCORING:IfMessagesZone() + + return self.MessagesZone +end + +--- Configure to send messages to all players. +-- @param #SCORING self +-- @return #SCORING +function SCORING:SetMessagesToAll() + + self.MessagesAudience = 1 + return self +end + +--- If to send messages to all players. +-- @param #SCORING self +-- @return #boolean +function SCORING:IfMessagesToAll() + + return self.MessagesAudience == 1 +end + +--- Configure to send messages to only those players within the same coalition as the player. +-- @param #SCORING self +-- @return #SCORING +function SCORING:SetMessagesToCoalition() + + self.MessagesAudience = 2 + return self +end + +--- If to send messages to only those players within the same coalition as the player. +-- @param #SCORING self +-- @return #boolean +function SCORING:IfMessagesToCoalition() + + return self.MessagesAudience == 2 +end + + +--- When a player commits too much damage to friendlies, his penalty score will reach a certain level. +-- Use this method to define the level when a player gets kicked. +-- By default, the fratricide level is the default penalty mutiplier * 2 for the penalty score. +-- @param #SCORING self +-- @param #number Fratricide The amount of maximum penalty that may be inflicted by a friendly player before he gets kicked. +-- @return #SCORING +function SCORING:SetFratricide( Fratricide ) + + self.Fratricide = Fratricide + return self +end + + +--- When a player changes the coalition, he can receive a penalty score. +-- Use the method @{#SCORING.SetCoalitionChangePenalty}() to define the penalty when a player changes coalition. +-- By default, the penalty for changing coalition is the default penalty scale. +-- @param #SCORING self +-- @param #number CoalitionChangePenalty The amount of penalty that is given. +-- @return #SCORING +function SCORING:SetCoalitionChangePenalty( CoalitionChangePenalty ) + + self.CoalitionChangePenalty = CoalitionChangePenalty + return self +end + + +--- Add a new player entering a Unit. +-- @param #SCORING self +-- @param Wrapper.Unit#UNIT UnitData +function SCORING:_AddPlayerFromUnit( UnitData ) + self:F( UnitData ) + + if UnitData:IsAlive() then + local UnitName = UnitData:GetName() + local PlayerName = UnitData:GetPlayerName() + local UnitDesc = UnitData:GetDesc() + local UnitCategory = UnitDesc.category + local UnitCoalition = UnitData:GetCoalition() + local UnitTypeName = UnitData:GetTypeName() + + self:T( { PlayerName, UnitName, UnitCategory, UnitCoalition, UnitTypeName } ) + + if self.Players[PlayerName] == nil then -- I believe this is the place where a Player gets a life in a mission when he enters a unit ... + self.Players[PlayerName] = {} + self.Players[PlayerName].Hit = {} + self.Players[PlayerName].Destroy = {} + self.Players[PlayerName].Goals = {} + self.Players[PlayerName].Mission = {} + + -- for CategoryID, CategoryName in pairs( SCORINGCategory ) do + -- self.Players[PlayerName].Hit[CategoryID] = {} + -- self.Players[PlayerName].Destroy[CategoryID] = {} + -- end + self.Players[PlayerName].HitPlayers = {} + self.Players[PlayerName].Score = 0 + self.Players[PlayerName].Penalty = 0 + self.Players[PlayerName].PenaltyCoalition = 0 + self.Players[PlayerName].PenaltyWarning = 0 + end + + if not self.Players[PlayerName].UnitCoalition then + self.Players[PlayerName].UnitCoalition = UnitCoalition + else + if self.Players[PlayerName].UnitCoalition ~= UnitCoalition then + self.Players[PlayerName].Penalty = self.Players[PlayerName].Penalty + 50 + self.Players[PlayerName].PenaltyCoalition = self.Players[PlayerName].PenaltyCoalition + 1 + MESSAGE: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 + self.Players[PlayerName].UNIT = UnitData + + if self.Players[PlayerName].Penalty > self.Fratricide * 0.50 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 " .. self.Fratricide .. ", 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 > self.Fratricide then + UnitData:Destroy() + MESSAGE:New( "Player '" .. PlayerName .. "' committed FRATRICIDE, he will be COURT MARTIALED and is DISMISSED from this mission!", + 10 + ):ToAll() + end + + end +end + + +--- Add a goal score for a player. +-- The method takes the PlayerUnit for which the Goal score needs to be set. +-- The GoalTag is a string or identifier that is taken into the CSV file scoring log to identify the goal. +-- A free text can be given that is shown to the players. +-- The Score can be both positive and negative. +-- @param #SCORING self +-- @param Wrapper.Unit#UNIT PlayerUnit The @{Unit} of the Player. Other Properties for the scoring are taken from this PlayerUnit, like coalition, type etc. +-- @param #string GoalTag The string or identifier that is used in the CSV file to identify the goal (sort or group later in Excel). +-- @param #string Text A free text that is shown to the players. +-- @param #number Score The score can be both positive or negative ( Penalty ). +function SCORING:AddGoalScore( PlayerUnit, GoalTag, Text, Score ) + + local PlayerName = PlayerUnit:GetPlayerName() + + self:E( { PlayerUnit.UnitName, PlayerName, GoalTag, Text, Score } ) + + -- PlayerName can be nil, if the Unit with the player crashed or due to another reason. + if PlayerName then + local PlayerData = self.Players[PlayerName] + + PlayerData.Goals[GoalTag] = PlayerData.Goals[GoalTag] or { Score = 0 } + PlayerData.Goals[GoalTag].Score = PlayerData.Goals[GoalTag].Score + Score + PlayerData.Score = PlayerData.Score + Score + + MESSAGE:New( Text, 30 ):ToAll() + + self:ScoreCSV( PlayerName, "GOAL_" .. string.upper( GoalTag ), 1, Score, PlayerUnit:GetName() ) + end +end + + +--- Registers Scores the players completing a Mission Task. +-- @param #SCORING self +-- @param Tasking.Mission#MISSION Mission +-- @param Wrapper.Unit#UNIT PlayerUnit +-- @param #string Text +-- @param #number Score +function SCORING:_AddMissionTaskScore( Mission, PlayerUnit, Text, Score ) + + local PlayerName = PlayerUnit:GetPlayerName() + local MissionName = Mission:GetName() + + self:E( { Mission:GetName(), PlayerUnit.UnitName, PlayerName, Text, Score } ) + + -- PlayerName can be nil, if the Unit with the player crashed or due to another reason. + if PlayerName then + local PlayerData = self.Players[PlayerName] + + if not PlayerData.Mission[MissionName] then + PlayerData.Mission[MissionName] = {} + PlayerData.Mission[MissionName].ScoreTask = 0 + PlayerData.Mission[MissionName].ScoreMission = 0 + end + + self:T( PlayerName ) + self:T( PlayerData.Mission[MissionName] ) + + PlayerData.Score = self.Players[PlayerName].Score + Score + PlayerData.Mission[MissionName].ScoreTask = self.Players[PlayerName].Mission[MissionName].ScoreTask + Score + + MESSAGE:New( "Player '" .. PlayerName .. "' has " .. Text .. " in Mission '" .. MissionName .. "'. " .. + Score .. " task score!", + 30 ):ToAll() + + self:ScoreCSV( PlayerName, "TASK_" .. MissionName:gsub( ' ', '_' ), 1, Score, PlayerUnit:GetName() ) + end +end + + +--- Registers Mission Scores for possible multiple players that contributed in the Mission. +-- @param #SCORING self +-- @param Tasking.Mission#MISSION Mission +-- @param Wrapper.Unit#UNIT PlayerUnit +-- @param #string Text +-- @param #number Score +function SCORING:_AddMissionScore( Mission, Text, Score ) + + local MissionName = Mission:GetName() + + self:E( { Mission, Text, Score } ) + self:E( self.Players ) + + for PlayerName, PlayerData in pairs( self.Players ) do + + self:E( PlayerData ) + if PlayerData.Mission[MissionName] then + + PlayerData.Score = PlayerData.Score + Score + PlayerData.Mission[MissionName].ScoreMission = PlayerData.Mission[MissionName].ScoreMission + Score + + MESSAGE:New( "Player '" .. PlayerName .. "' has " .. Text .. " in Mission '" .. MissionName .. "'. " .. + Score .. " mission score!", + 60 ):ToAll() + + self:ScoreCSV( PlayerName, "MISSION_" .. MissionName:gsub( ' ', '_' ), 1, Score ) + end + end +end + + +--- Handles the OnPlayerEnterUnit event for the scoring. +-- @param #SCORING self +-- @param Core.Event#EVENTDATA Event +function SCORING:OnEventPlayerEnterUnit( Event ) + if Event.IniUnit then + self:_AddPlayerFromUnit( Event.IniUnit ) + local Menu = MENU_GROUP:New( Event.IniGroup, 'Scoring' ) + local ReportGroupSummary = MENU_GROUP_COMMAND:New( Event.IniGroup, 'Summary report players in group', Menu, SCORING.ReportScoreGroupSummary, self, Event.IniGroup ) + local ReportGroupDetailed = MENU_GROUP_COMMAND:New( Event.IniGroup, 'Detailed report players in group', Menu, SCORING.ReportScoreGroupDetailed, self, Event.IniGroup ) + local ReportToAllSummary = MENU_GROUP_COMMAND:New( Event.IniGroup, 'Summary report all players', Menu, SCORING.ReportScoreAllSummary, self, Event.IniGroup ) + self:SetState( Event.IniUnit, "ScoringMenu", Menu ) + end +end + +--- Handles the OnPlayerLeaveUnit event for the scoring. +-- @param #SCORING self +-- @param Core.Event#EVENTDATA Event +function SCORING:OnEventPlayerLeaveUnit( Event ) + if Event.IniUnit then + local Menu = self:GetState( Event.IniUnit, "ScoringMenu" ) -- Core.Menu#MENU_GROUP + if Menu then + Menu:Remove() + end + end +end + + +--- Handles the OnHit event for the scoring. +-- @param #SCORING self +-- @param Core.Event#EVENTDATA Event +function SCORING:_EventOnHit( Event ) + self:F( { Event } ) + + local InitUnit = nil + local InitUNIT = nil + local InitUnitName = "" + local InitGroup = nil + local InitGroupName = "" + local InitPlayerName = nil + + local InitCoalition = nil + local InitCategory = nil + local InitType = nil + local InitUnitCoalition = nil + local InitUnitCategory = nil + local InitUnitType = nil + + local TargetUnit = nil + local TargetUNIT = nil + local TargetUnitName = "" + local TargetGroup = nil + local TargetGroupName = "" + local TargetPlayerName = nil + + local TargetCoalition = nil + local TargetCategory = nil + local TargetType = nil + local TargetUnitCoalition = nil + local TargetUnitCategory = nil + local TargetUnitType = nil + + if Event.IniDCSUnit then + + InitUnit = Event.IniDCSUnit + InitUNIT = Event.IniUnit + InitUnitName = Event.IniDCSUnitName + InitGroup = Event.IniDCSGroup + InitGroupName = Event.IniDCSGroupName + InitPlayerName = Event.IniPlayerName + + InitCoalition = Event.IniCoalition + --TODO: Workaround Client DCS Bug + --InitCategory = InitUnit:getCategory() + --InitCategory = InitUnit:getDesc().category + InitCategory = Event.IniCategory + InitType = Event.IniTypeName + + InitUnitCoalition = _SCORINGCoalition[InitCoalition] + InitUnitCategory = _SCORINGCategory[InitCategory] + InitUnitType = InitType + + self:T( { InitUnitName, InitGroupName, InitPlayerName, InitCoalition, InitCategory, InitType , InitUnitCoalition, InitUnitCategory, InitUnitType } ) + end + + + if Event.TgtDCSUnit then + + TargetUnit = Event.TgtDCSUnit + TargetUNIT = Event.TgtUnit + TargetUnitName = Event.TgtDCSUnitName + TargetGroup = Event.TgtDCSGroup + TargetGroupName = Event.TgtDCSGroupName + TargetPlayerName = Event.TgtPlayerName + + TargetCoalition = Event.TgtCoalition + --TODO: Workaround Client DCS Bug + --TargetCategory = TargetUnit:getCategory() + --TargetCategory = TargetUnit:getDesc().category + TargetCategory = Event.TgtCategory + TargetType = Event.TgtTypeName + + TargetUnitCoalition = _SCORINGCoalition[TargetCoalition] + TargetUnitCategory = _SCORINGCategory[TargetCategory] + TargetUnitType = TargetType + + self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType, TargetUnitCoalition, TargetUnitCategory, TargetUnitType } ) + end + + if InitPlayerName ~= nil then -- It is a player that is hitting something + self:_AddPlayerFromUnit( InitUNIT ) + if self.Players[InitPlayerName] then -- This should normally not happen, but i'll test it anyway. + if TargetPlayerName ~= nil then -- It is a player hitting another player ... + self:_AddPlayerFromUnit( TargetUNIT ) + end + + self:T( "Hitting Something" ) + + -- What is he hitting? + if TargetCategory then + + -- A target got hit, score it. + -- Player contains the score data from self.Players[InitPlayerName] + local Player = self.Players[InitPlayerName] + + -- Ensure there is a hit table per TargetCategory and TargetUnitName. + Player.Hit[TargetCategory] = Player.Hit[TargetCategory] or {} + Player.Hit[TargetCategory][TargetUnitName] = Player.Hit[TargetCategory][TargetUnitName] or {} + + -- PlayerHit contains the score counters and data per unit that was hit. + local PlayerHit = Player.Hit[TargetCategory][TargetUnitName] + + PlayerHit.Score = PlayerHit.Score or 0 + PlayerHit.Penalty = PlayerHit.Penalty or 0 + PlayerHit.ScoreHit = PlayerHit.ScoreHit or 0 + PlayerHit.PenaltyHit = PlayerHit.PenaltyHit or 0 + PlayerHit.TimeStamp = PlayerHit.TimeStamp or 0 + PlayerHit.UNIT = PlayerHit.UNIT or TargetUNIT + + -- Only grant hit scores if there was more than one second between the last hit. + if timer.getTime() - PlayerHit.TimeStamp > 1 then + PlayerHit.TimeStamp = timer.getTime() + + if TargetPlayerName ~= nil then -- It is a player hitting another player ... + + -- Ensure there is a Player to Player hit reference table. + Player.HitPlayers[TargetPlayerName] = true + end + + local Score = 0 + + if InitCoalition then -- A coalition object was hit. + if InitCoalition == TargetCoalition then + Player.Penalty = Player.Penalty + 10 + PlayerHit.Penalty = PlayerHit.Penalty + 10 + PlayerHit.PenaltyHit = PlayerHit.PenaltyHit + 1 + + if TargetPlayerName ~= nil then -- It is a player hitting another player ... + MESSAGE + :New( "Player '" .. InitPlayerName .. "' hit friendly player '" .. TargetPlayerName .. "' " .. + TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.PenaltyHit .. " times. " .. + "Penalty: -" .. PlayerHit.Penalty .. ". Score Total:" .. Player.Score - Player.Penalty, + 2 + ) + :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) + else + MESSAGE + :New( "Player '" .. InitPlayerName .. "' hit a friendly target " .. + TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.PenaltyHit .. " times. " .. + "Penalty: -" .. PlayerHit.Penalty .. ". Score Total:" .. Player.Score - Player.Penalty, + 2 + ) + :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) + end + self:ScoreCSV( InitPlayerName, "HIT_PENALTY", 1, -25, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + else + Player.Score = Player.Score + 1 + PlayerHit.Score = PlayerHit.Score + 1 + PlayerHit.ScoreHit = PlayerHit.ScoreHit + 1 + if TargetPlayerName ~= nil then -- It is a player hitting another player ... + MESSAGE + :New( "Player '" .. InitPlayerName .. "' hit enemy player '" .. TargetPlayerName .. "' " .. + TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.ScoreHit .. " times. " .. + "Score: " .. PlayerHit.Score .. ". Score Total:" .. Player.Score - Player.Penalty, + 2 + ) + :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) + else + MESSAGE + :New( "Player '" .. InitPlayerName .. "' hit an enemy target " .. + TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.ScoreHit .. " times. " .. + "Score: " .. PlayerHit.Score .. ". Score Total:" .. Player.Score - Player.Penalty, + 2 + ) + :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) + end + self:ScoreCSV( InitPlayerName, "HIT_SCORE", 1, 1, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + end + else -- A scenery object was hit. + MESSAGE + :New( "Player '" .. InitPlayerName .. "' hit a scenery object.", + 2 + ) + :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) + self:ScoreCSV( InitPlayerName, "HIT_SCORE", 1, 1, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, "", "Scenery", TargetUnitType ) + end + end + end + end + elseif InitPlayerName == nil then -- It is an AI hitting a player??? + + end +end + +--- Track DEAD or CRASH events for the scoring. +-- @param #SCORING self +-- @param Core.Event#EVENTDATA Event +function SCORING:_EventOnDeadOrCrash( Event ) + self:F( { Event } ) + + local TargetUnit = nil + local TargetGroup = nil + local TargetUnitName = "" + local TargetGroupName = "" + local TargetPlayerName = "" + local TargetCoalition = nil + local TargetCategory = nil + local TargetType = nil + local TargetUnitCoalition = nil + local TargetUnitCategory = nil + local TargetUnitType = nil + + if Event.IniDCSUnit then + + TargetUnit = Event.IniUnit + TargetUnitName = Event.IniDCSUnitName + TargetGroup = Event.IniDCSGroup + TargetGroupName = Event.IniDCSGroupName + TargetPlayerName = Event.IniPlayerName + + TargetCoalition = Event.IniCoalition + --TargetCategory = TargetUnit:getCategory() + --TargetCategory = TargetUnit:getDesc().category -- Workaround + TargetCategory = Event.IniCategory + TargetType = Event.IniTypeName + + TargetUnitCoalition = _SCORINGCoalition[TargetCoalition] + TargetUnitCategory = _SCORINGCategory[TargetCategory] + TargetUnitType = TargetType + + self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType } ) + end + + -- Player contains the score and reference data for the player. + for PlayerName, Player in pairs( self.Players ) do + if Player then -- This should normally not happen, but i'll test it anyway. + self:T( "Something got destroyed" ) + + -- Some variables + local InitUnitName = Player.UnitName + local InitUnitType = Player.UnitType + local InitCoalition = Player.UnitCoalition + local InitCategory = Player.UnitCategory + local InitUnitCoalition = _SCORINGCoalition[InitCoalition] + local InitUnitCategory = _SCORINGCategory[InitCategory] + + self:T( { InitUnitName, InitUnitType, InitUnitCoalition, InitCoalition, InitUnitCategory, InitCategory } ) + + -- What is the player destroying? + if Player and Player.Hit and Player.Hit[TargetCategory] and Player.Hit[TargetCategory][TargetUnitName] then -- Was there a hit for this unit for this player before registered??? + + Player.Destroy[TargetCategory] = Player.Destroy[TargetCategory] or {} + Player.Destroy[TargetCategory][TargetType] = Player.Destroy[TargetCategory][TargetType] or {} + + -- PlayerDestroy contains the destroy score data per category and target type of the player. + local TargetDestroy = Player.Destroy[TargetCategory][TargetType] + TargetDestroy.Score = TargetDestroy.Score or 0 + TargetDestroy.ScoreDestroy = TargetDestroy.ScoreDestroy or 0 + TargetDestroy.Penalty = TargetDestroy.Penalty or 0 + TargetDestroy.PenaltyDestroy = TargetDestroy.PenaltyDestroy or 0 + + if TargetCoalition then + if InitCoalition == TargetCoalition then + local ThreatLevelTarget, ThreatTypeTarget = TargetUnit:GetThreatLevel() + local ThreatLevelPlayer = Player.UNIT:GetThreatLevel() / 10 + 1 + local ThreatPenalty = math.ceil( ( ThreatLevelTarget / ThreatLevelPlayer ) * self.ScaleDestroyPenalty / 10 ) + self:E( { ThreatLevel = ThreatPenalty, ThreatLevelTarget = ThreatLevelTarget, ThreatTypeTarget = ThreatTypeTarget, ThreatLevelPlayer = ThreatLevelPlayer } ) + + Player.Penalty = Player.Penalty + ThreatPenalty + TargetDestroy.Penalty = TargetDestroy.Penalty + ThreatPenalty + TargetDestroy.PenaltyDestroy = TargetDestroy.PenaltyDestroy + 1 + + if Player.HitPlayers[TargetPlayerName] then -- A player destroyed another player + MESSAGE + :New( "Player '" .. PlayerName .. "' destroyed friendly player '" .. TargetPlayerName .. "' " .. + TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. TargetDestroy.PenaltyDestroy .. " times. " .. + "Penalty: -" .. TargetDestroy.Penalty .. ". Score Total:" .. Player.Score - Player.Penalty, + 15 + ) + :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) + else + MESSAGE + :New( "Player '" .. PlayerName .. "' destroyed a friendly target " .. + TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. TargetDestroy.PenaltyDestroy .. " times. " .. + "Penalty: -" .. TargetDestroy.Penalty .. ". Score Total:" .. Player.Score - Player.Penalty, + 15 + ) + :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) + end + self:ScoreCSV( PlayerName, "DESTROY_PENALTY", 1, ThreatPenalty, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + else + + local ThreatLevelTarget, ThreatTypeTarget = TargetUnit:GetThreatLevel() + local ThreatLevelPlayer = Player.UNIT:GetThreatLevel() / 10 + 1 + local ThreatScore = math.ceil( ( ThreatLevelTarget / ThreatLevelPlayer ) * self.ScaleDestroyScore / 10 ) + + self:E( { ThreatLevel = ThreatScore, ThreatLevelTarget = ThreatLevelTarget, ThreatTypeTarget = ThreatTypeTarget, ThreatLevelPlayer = ThreatLevelPlayer } ) + + Player.Score = Player.Score + ThreatScore + TargetDestroy.Score = TargetDestroy.Score + ThreatScore + TargetDestroy.ScoreDestroy = TargetDestroy.ScoreDestroy + 1 + if Player.HitPlayers[TargetPlayerName] then -- A player destroyed another player + MESSAGE + :New( "Player '" .. PlayerName .. "' destroyed enemy player '" .. TargetPlayerName .. "' " .. + TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. TargetDestroy.ScoreDestroy .. " times. " .. + "Score: " .. TargetDestroy.Score .. ". Score Total:" .. Player.Score - Player.Penalty, + 15 + ) + :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) + else + MESSAGE + :New( "Player '" .. PlayerName .. "' destroyed an enemy " .. + TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. TargetDestroy.ScoreDestroy .. " times. " .. + "Score: " .. TargetDestroy.Score .. ". Total:" .. Player.Score - Player.Penalty, + 15 + ) + :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) + end + self:ScoreCSV( PlayerName, "DESTROY_SCORE", 1, ThreatScore, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + + local UnitName = TargetUnit:GetName() + local Score = self.ScoringObjects[UnitName] + if Score then + Player.Score = Player.Score + Score + TargetDestroy.Score = TargetDestroy.Score + Score + MESSAGE + :New( "Special target '" .. TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. " destroyed! " .. + "Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! Total: " .. Player.Score - Player.Penalty, + 15 + ) + :ToAllIf( self:IfMessagesScore() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesScore() and self:IfMessagesToCoalition() ) + self:ScoreCSV( PlayerName, "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + end + + -- Check if there are Zones where the destruction happened. + for ZoneName, ScoreZoneData in pairs( self.ScoringZones ) do + self:E( { ScoringZone = ScoreZoneData } ) + local ScoreZone = ScoreZoneData.ScoreZone -- Core.Zone#ZONE_BASE + local Score = ScoreZoneData.Score + if ScoreZone:IsVec2InZone( TargetUnit:GetVec2() ) then + Player.Score = Player.Score + Score + TargetDestroy.Score = TargetDestroy.Score + Score + MESSAGE + :New( "Target destroyed in zone '" .. ScoreZone:GetName() .. "'." .. + "Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! " .. + "Total: " .. Player.Score - Player.Penalty, + 15 ) + :ToAllIf( self:IfMessagesZone() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesZone() and self:IfMessagesToCoalition() ) + self:ScoreCSV( PlayerName, "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + end + end + + end + else + -- Check if there are Zones where the destruction happened. + for ZoneName, ScoreZoneData in pairs( self.ScoringZones ) do + self:E( { ScoringZone = ScoreZoneData } ) + local ScoreZone = ScoreZoneData.ScoreZone -- Core.Zone#ZONE_BASE + local Score = ScoreZoneData.Score + if ScoreZone:IsVec2InZone( TargetUnit:GetVec2() ) then + Player.Score = Player.Score + Score + TargetDestroy.Score = TargetDestroy.Score + Score + MESSAGE + :New( "Scenery destroyed in zone '" .. ScoreZone:GetName() .. "'." .. + "Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! " .. + "Total: " .. Player.Score - Player.Penalty, + 15 + ) + :ToAllIf( self:IfMessagesZone() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesZone() and self:IfMessagesToCoalition() ) + self:ScoreCSV( PlayerName, "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, "", "Scenery", TargetUnitType ) + end + end + end + end + end + end +end + + +--- Produce detailed report of player hit scores. +-- @param #SCORING self +-- @param #string PlayerName The name of the player. +-- @return #string The report. +function SCORING:ReportDetailedPlayerHits( PlayerName ) + + local ScoreMessage = "" + local PlayerScore = 0 + local PlayerPenalty = 0 + + local PlayerData = self.Players[PlayerName] + if PlayerData then -- This should normally not happen, but i'll test it anyway. + self:T( "Score Player: " .. PlayerName ) + + -- Some variables + local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] + local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] + local InitUnitType = PlayerData.UnitType + local InitUnitName = PlayerData.UnitName + + local ScoreMessageHits = "" + for CategoryID, CategoryName in pairs( _SCORINGCategory ) do + self:T( CategoryName ) + if PlayerData.Hit[CategoryID] then + self:T( "Hit scores exist for player " .. PlayerName ) + local Score = 0 + local ScoreHit = 0 + local Penalty = 0 + local PenaltyHit = 0 + for UnitName, UnitData in pairs( PlayerData.Hit[CategoryID] ) do + Score = Score + UnitData.Score + ScoreHit = ScoreHit + UnitData.ScoreHit + Penalty = Penalty + UnitData.Penalty + PenaltyHit = UnitData.PenaltyHit + end + local ScoreMessageHit = string.format( "%s:%d ", CategoryName, Score - Penalty ) + self:T( ScoreMessageHit ) + ScoreMessageHits = ScoreMessageHits .. ScoreMessageHit + PlayerScore = PlayerScore + Score + PlayerPenalty = PlayerPenalty + Penalty + else + --ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) + end + end + if ScoreMessageHits ~= "" then + ScoreMessage = "Hits: " .. ScoreMessageHits + end + end + + return ScoreMessage, PlayerScore, PlayerPenalty +end + + +--- Produce detailed report of player destroy scores. +-- @param #SCORING self +-- @param #string PlayerName The name of the player. +-- @return #string The report. +function SCORING:ReportDetailedPlayerDestroys( PlayerName ) + + local ScoreMessage = "" + local PlayerScore = 0 + local PlayerPenalty = 0 + + local PlayerData = self.Players[PlayerName] + if PlayerData then -- This should normally not happen, but i'll test it anyway. + self:T( "Score Player: " .. PlayerName ) + + -- Some variables + local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] + local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] + local InitUnitType = PlayerData.UnitType + local InitUnitName = PlayerData.UnitName + + local ScoreMessageDestroys = "" + for CategoryID, CategoryName in pairs( _SCORINGCategory ) do + if PlayerData.Destroy[CategoryID] then + self:T( "Destroy scores exist for player " .. PlayerName ) + local Score = 0 + local ScoreDestroy = 0 + local Penalty = 0 + local PenaltyDestroy = 0 + + for UnitName, UnitData in pairs( PlayerData.Destroy[CategoryID] ) do + self:E( { UnitData = UnitData } ) + if UnitData ~= {} then + Score = Score + UnitData.Score + ScoreDestroy = ScoreDestroy + UnitData.ScoreDestroy + Penalty = Penalty + UnitData.Penalty + PenaltyDestroy = PenaltyDestroy + UnitData.PenaltyDestroy + end + end + + local ScoreMessageDestroy = string.format( " %s:%d ", CategoryName, Score - Penalty ) + self:T( ScoreMessageDestroy ) + ScoreMessageDestroys = ScoreMessageDestroys .. ScoreMessageDestroy + + PlayerScore = PlayerScore + Score + PlayerPenalty = PlayerPenalty + Penalty + else + --ScoreMessageDestroys = ScoreMessageDestroys .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) + end + end + if ScoreMessageDestroys ~= "" then + ScoreMessage = "Destroys: " .. ScoreMessageDestroys + end + end + + return ScoreMessage, PlayerScore, PlayerPenalty +end + +--- Produce detailed report of player penalty scores because of changing the coalition. +-- @param #SCORING self +-- @param #string PlayerName The name of the player. +-- @return #string The report. +function SCORING:ReportDetailedPlayerCoalitionChanges( PlayerName ) + + local ScoreMessage = "" + local PlayerScore = 0 + local PlayerPenalty = 0 + + local PlayerData = self.Players[PlayerName] + if PlayerData then -- This should normally not happen, but i'll test it anyway. + self:T( "Score Player: " .. PlayerName ) + + -- Some variables + local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] + local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] + local InitUnitType = PlayerData.UnitType + local InitUnitName = PlayerData.UnitName + + local ScoreMessageCoalitionChangePenalties = "" + if PlayerData.PenaltyCoalition ~= 0 then + ScoreMessageCoalitionChangePenalties = ScoreMessageCoalitionChangePenalties .. string.format( " -%d (%d changed)", PlayerData.Penalty, PlayerData.PenaltyCoalition ) + PlayerPenalty = PlayerPenalty + PlayerData.Penalty + end + if ScoreMessageCoalitionChangePenalties ~= "" then + ScoreMessage = ScoreMessage .. "Coalition Penalties: " .. ScoreMessageCoalitionChangePenalties + end + end + + return ScoreMessage, PlayerScore, PlayerPenalty +end + +--- Produce detailed report of player goal scores. +-- @param #SCORING self +-- @param #string PlayerName The name of the player. +-- @return #string The report. +function SCORING:ReportDetailedPlayerGoals( PlayerName ) + + local ScoreMessage = "" + local PlayerScore = 0 + local PlayerPenalty = 0 + + local PlayerData = self.Players[PlayerName] + if PlayerData then -- This should normally not happen, but i'll test it anyway. + self:T( "Score Player: " .. PlayerName ) + + -- Some variables + local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] + local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] + local InitUnitType = PlayerData.UnitType + local InitUnitName = PlayerData.UnitName + + local ScoreMessageGoal = "" + local ScoreGoal = 0 + local ScoreTask = 0 + for GoalName, GoalData in pairs( PlayerData.Goals ) do + ScoreGoal = ScoreGoal + GoalData.Score + ScoreMessageGoal = ScoreMessageGoal .. "'" .. GoalName .. "':" .. GoalData.Score .. "; " + end + PlayerScore = PlayerScore + ScoreGoal + + if ScoreMessageGoal ~= "" then + ScoreMessage = "Goals: " .. ScoreMessageGoal + end + end + + return ScoreMessage, PlayerScore, PlayerPenalty +end + +--- Produce detailed report of player penalty scores because of changing the coalition. +-- @param #SCORING self +-- @param #string PlayerName The name of the player. +-- @return #string The report. +function SCORING:ReportDetailedPlayerMissions( PlayerName ) + + local ScoreMessage = "" + local PlayerScore = 0 + local PlayerPenalty = 0 + + local PlayerData = self.Players[PlayerName] + if PlayerData then -- This should normally not happen, but i'll test it anyway. + self:T( "Score Player: " .. PlayerName ) + + -- Some variables + local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] + local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] + local InitUnitType = PlayerData.UnitType + local InitUnitName = PlayerData.UnitName + + local ScoreMessageMission = "" + local ScoreMission = 0 + local ScoreTask = 0 + for MissionName, MissionData in pairs( PlayerData.Mission ) do + ScoreMission = ScoreMission + MissionData.ScoreMission + ScoreTask = ScoreTask + MissionData.ScoreTask + ScoreMessageMission = ScoreMessageMission .. "'" .. MissionName .. "'; " + end + PlayerScore = PlayerScore + ScoreMission + ScoreTask + + if ScoreMessageMission ~= "" then + ScoreMessage = "Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ")" + end + end + + return ScoreMessage, PlayerScore, PlayerPenalty +end + + +--- Report Group Score Summary +-- @param #SCORING self +-- @param Wrapper.Group#GROUP PlayerGroup The player group. +function SCORING:ReportScoreGroupSummary( PlayerGroup ) + + local PlayerMessage = "" + + self:T( "Report Score Group Summary" ) + + local PlayerUnits = PlayerGroup:GetUnits() + for UnitID, PlayerUnit in pairs( PlayerUnits ) do + local PlayerUnit = PlayerUnit -- Wrapper.Unit#UNIT + local PlayerName = PlayerUnit:GetPlayerName() + + if PlayerName then + + local ReportHits, ScoreHits, PenaltyHits = self:ReportDetailedPlayerHits( PlayerName ) + ReportHits = ReportHits ~= "" and "\n- " .. ReportHits or ReportHits + self:E( { ReportHits, ScoreHits, PenaltyHits } ) + + local ReportDestroys, ScoreDestroys, PenaltyDestroys = self:ReportDetailedPlayerDestroys( PlayerName ) + ReportDestroys = ReportDestroys ~= "" and "\n- " .. ReportDestroys or ReportDestroys + self:E( { ReportDestroys, ScoreDestroys, PenaltyDestroys } ) + + local ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges = self:ReportDetailedPlayerCoalitionChanges( PlayerName ) + ReportCoalitionChanges = ReportCoalitionChanges ~= "" and "\n- " .. ReportCoalitionChanges or ReportCoalitionChanges + self:E( { ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges } ) + + local ReportGoals, ScoreGoals, PenaltyGoals = self:ReportDetailedPlayerGoals( PlayerName ) + ReportGoals = ReportGoals ~= "" and "\n- " .. ReportGoals or ReportGoals + self:E( { ReportGoals, ScoreGoals, PenaltyGoals } ) + + local ReportMissions, ScoreMissions, PenaltyMissions = self:ReportDetailedPlayerMissions( PlayerName ) + ReportMissions = ReportMissions ~= "" and "\n- " .. ReportMissions or ReportMissions + self:E( { ReportMissions, ScoreMissions, PenaltyMissions } ) + + local PlayerScore = ScoreHits + ScoreDestroys + ScoreCoalitionChanges + ScoreGoals + ScoreMissions + local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + ScoreGoals + PenaltyMissions + + PlayerMessage = + string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )", + PlayerName, + PlayerScore - PlayerPenalty, + PlayerScore, + PlayerPenalty + ) + MESSAGE:New( PlayerMessage, 30, "Player '" .. PlayerName .. "'" ):ToGroup( PlayerGroup ) + end + end + +end + +--- Report Group Score Detailed +-- @param #SCORING self +-- @param Wrapper.Group#GROUP PlayerGroup The player group. +function SCORING:ReportScoreGroupDetailed( PlayerGroup ) + + local PlayerMessage = "" + + self:T( "Report Score Group Detailed" ) + + local PlayerUnits = PlayerGroup:GetUnits() + for UnitID, PlayerUnit in pairs( PlayerUnits ) do + local PlayerUnit = PlayerUnit -- Wrapper.Unit#UNIT + local PlayerName = PlayerUnit:GetPlayerName() + + if PlayerName then + + local ReportHits, ScoreHits, PenaltyHits = self:ReportDetailedPlayerHits( PlayerName ) + ReportHits = ReportHits ~= "" and "\n- " .. ReportHits or ReportHits + self:E( { ReportHits, ScoreHits, PenaltyHits } ) + + local ReportDestroys, ScoreDestroys, PenaltyDestroys = self:ReportDetailedPlayerDestroys( PlayerName ) + ReportDestroys = ReportDestroys ~= "" and "\n- " .. ReportDestroys or ReportDestroys + self:E( { ReportDestroys, ScoreDestroys, PenaltyDestroys } ) + + local ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges = self:ReportDetailedPlayerCoalitionChanges( PlayerName ) + ReportCoalitionChanges = ReportCoalitionChanges ~= "" and "\n- " .. ReportCoalitionChanges or ReportCoalitionChanges + self:E( { ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges } ) + + local ReportGoals, ScoreGoals, PenaltyGoals = self:ReportDetailedPlayerGoals( PlayerName ) + ReportGoals = ReportGoals ~= "" and "\n- " .. ReportGoals or ReportGoals + self:E( { ReportGoals, ScoreGoals, PenaltyGoals } ) + + local ReportMissions, ScoreMissions, PenaltyMissions = self:ReportDetailedPlayerMissions( PlayerName ) + ReportMissions = ReportMissions ~= "" and "\n- " .. ReportMissions or ReportMissions + self:E( { ReportMissions, ScoreMissions, PenaltyMissions } ) + + local PlayerScore = ScoreHits + ScoreDestroys + ScoreCoalitionChanges + ScoreGoals + ScoreMissions + local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + ScoreGoals + PenaltyMissions + + PlayerMessage = + string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )%s%s%s%s%s", + PlayerName, + PlayerScore - PlayerPenalty, + PlayerScore, + PlayerPenalty, + ReportHits, + ReportDestroys, + ReportCoalitionChanges, + ReportGoals, + ReportMissions + ) + MESSAGE:New( PlayerMessage, 30, "Player '" .. PlayerName .. "'" ):ToGroup( PlayerGroup ) + end + end + +end + +--- Report all players score +-- @param #SCORING self +-- @param Wrapper.Group#GROUP PlayerGroup The player group. +function SCORING:ReportScoreAllSummary( PlayerGroup ) + + local PlayerMessage = "" + + self:T( "Report Score All Players" ) + + for PlayerName, PlayerData in pairs( self.Players ) do + + if PlayerName then + + local ReportHits, ScoreHits, PenaltyHits = self:ReportDetailedPlayerHits( PlayerName ) + ReportHits = ReportHits ~= "" and "\n- " .. ReportHits or ReportHits + self:E( { ReportHits, ScoreHits, PenaltyHits } ) + + local ReportDestroys, ScoreDestroys, PenaltyDestroys = self:ReportDetailedPlayerDestroys( PlayerName ) + ReportDestroys = ReportDestroys ~= "" and "\n- " .. ReportDestroys or ReportDestroys + self:E( { ReportDestroys, ScoreDestroys, PenaltyDestroys } ) + + local ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges = self:ReportDetailedPlayerCoalitionChanges( PlayerName ) + ReportCoalitionChanges = ReportCoalitionChanges ~= "" and "\n- " .. ReportCoalitionChanges or ReportCoalitionChanges + self:E( { ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges } ) + + local ReportGoals, ScoreGoals, PenaltyGoals = self:ReportDetailedPlayerGoals( PlayerName ) + ReportGoals = ReportGoals ~= "" and "\n- " .. ReportGoals or ReportGoals + self:E( { ReportGoals, ScoreGoals, PenaltyGoals } ) + + local ReportMissions, ScoreMissions, PenaltyMissions = self:ReportDetailedPlayerMissions( PlayerName ) + ReportMissions = ReportMissions ~= "" and "\n- " .. ReportMissions or ReportMissions + self:E( { ReportMissions, ScoreMissions, PenaltyMissions } ) + + local PlayerScore = ScoreHits + ScoreDestroys + ScoreCoalitionChanges + ScoreGoals + ScoreMissions + local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + ScoreGoals + PenaltyMissions + + PlayerMessage = + string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )", + PlayerName, + PlayerScore - PlayerPenalty, + PlayerScore, + PlayerPenalty + ) + MESSAGE:New( PlayerMessage, 30, "Player '" .. PlayerName .. "'" ):ToGroup( PlayerGroup ) + end + end + +end + + +function SCORING:SecondsToClock(sSeconds) + local nSeconds = sSeconds + if nSeconds == 0 then + --return nil; + return "00:00:00"; + else + nHours = string.format("%02.f", math.floor(nSeconds/3600)); + nMins = string.format("%02.f", math.floor(nSeconds/60 - (nHours*60))); + nSecs = string.format("%02.f", math.floor(nSeconds - nHours*3600 - nMins *60)); + return nHours..":"..nMins..":"..nSecs + end +end + +--- Opens a score CSV file to log the scores. +-- @param #SCORING self +-- @param #string ScoringCSV +-- @return #SCORING self +-- @usage +-- -- Open a new CSV file to log the scores of the game Gori Valley. Let the name of the CSV file begin with "Player Scores". +-- ScoringObject = SCORING:New( "Gori Valley" ) +-- ScoringObject:OpenCSV( "Player Scores" ) +function SCORING:OpenCSV( ScoringCSV ) + self:F( ScoringCSV ) + + if lfs and io and os then + if ScoringCSV then + self.ScoringCSV = ScoringCSV + local fdir = lfs.writedir() .. [[Logs\]] .. self.ScoringCSV .. " " .. os.date( "%Y-%m-%d %H-%M-%S" ) .. ".csv" + + self.CSVFile, self.err = io.open( fdir, "w+" ) + if not self.CSVFile then + error( "Error: Cannot open CSV file in " .. lfs.writedir() ) + end + + self.CSVFile:write( '"GameName","RunTime","Time","PlayerName","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 + + TargetUnitCoalition = TargetUnitCoalition or "" + TargetUnitCategory = TargetUnitCategory or "" + TargetUnitType = TargetUnitType or "" + TargetUnitName = TargetUnitName or "" + + if lfs and io and os then + self.CSVFile:write( + '"' .. self.GameName .. '"' .. ',' .. + '"' .. self.RunTime .. '"' .. ',' .. + '' .. ScoreTime .. '' .. ',' .. + '"' .. PlayerName .. '"' .. ',' .. + '"' .. 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 + +--- 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 Core.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 = 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 Dcs.DCSWrapper.Group#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 @{DCSWrapper.Unit#Unit} from the simulator, but checks first if it is still existing! +-- @param #CLEANUP self +-- @param Dcs.DCSWrapper.Unit#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 Dcs.DCSTypes#Weapon +--- Destroys a missile from the simulator, but checks first if it is still existing! +-- @param #CLEANUP self +-- @param Dcs.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 Dcs.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 Dcs.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() + 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 Dcs.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 ) + 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 ) + SCHEDULER:New( self, CLEANUP._DestroyUnit, { Event.TgtDCSUnit }, 0.1 ) + end + end + end +end + +--- Add the @{DCSWrapper.Unit#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 Dcs.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 + +--- Single-Player:**Yes** / Multi-Player:**Yes** / AI:**Yes** / Human:**No** / Types:**All** -- +-- **Spawn groups of units dynamically in your missions.** +-- +-- ![Banner Image](..\Presentations\SPAWN\SPAWN.JPG) +-- +-- === +-- +-- # 1) @{#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 methods (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 represents the GROUP Template (definition). +-- * @{#SPAWN.NewWithAlias}(): Creates a new SPAWN object taking the name of the group that represents the GROUP Template (definition), and gives each spawned @{Group} an different name. +-- +-- It is important to understand how the SPAWN class works internally. The SPAWN object created will contain internally a list of groups that will be spawned and that are already spawned. +-- The initialization methods will modify this list of groups so that when a group gets spawned, ALL information is already prepared when spawning. This is done for performance reasons. +-- So in principle, the group list will contain all parameters and configurations after initialization, and when groups get actually spawned, this spawning can be done quickly and efficient. +-- +-- ## 1.2) SPAWN initialization methods +-- +-- A spawn object will behave differently based on the usage of **initialization** methods, which all start with the **Init** prefix: +-- +-- * @{#SPAWN.InitLimit}(): Limits the amount of groups that can be alive at the same time and that can be dynamically spawned. +-- * @{#SPAWN.InitRandomizeRoute}(): Randomize the routes of spawned groups, and for air groups also optionally the height. +-- * @{#SPAWN.InitRandomizeTemplate}(): Randomize the group templates so that when a new group is spawned, a random group template is selected from one of the templates defined. +-- * @{#SPAWN.InitUnControlled}(): Spawn plane groups uncontrolled. +-- * @{#SPAWN.InitArray}(): 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 methods are @{#SPAWN.InitRepeatOnLanding} and @{#SPAWN.InitRepeatOnEngineShutDown}. +-- * @{#SPAWN.InitRandomizeUnits}(): Randomizes the @{Unit}s in the @{Group} that is spawned within a **radius band**, given an Outer and Inner radius. +-- * @{#SPAWN.InitRandomizeZones}(): Randomizes the spawning between a predefined list of @{Zone}s that are declared using this function. Each zone can be given a probability factor. +-- * @{#SPAWN.InitAIOn}(): Turns the AI On when spawning the new @{Group} object. +-- * @{#SPAWN.InitAIOff}(): Turns the AI Off when spawning the new @{Group} object. +-- * @{#SPAWN.InitAIOnOff}(): Turns the AI On or Off when spawning the new @{Group} object. +-- +-- ## 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.SpawnFromVec3}(): Spawn a new group from a Vec3 coordinate. (The group will can be spawned at a point in the air). +-- * @{#SPAWN.SpawnFromVec2}(): Spawn a new group from a Vec2 coordinate. (The group will be spawned at land height ). +-- * @{#SPAWN.SpawnFromStatic}(): Spawn a new group from a structure, taking the position of a @{Static}. +-- * @{#SPAWN.SpawnFromUnit}(): Spawn a new group taking the position of a @{Unit}. +-- * @{#SPAWN.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) Retrieve alive GROUPs spawned by the SPAWN object +-- +-- The SPAWN class administers which GROUPS it has reserved (in stock) or has created during mission execution. +-- Every time a SPAWN object spawns a new GROUP object, a reference to the GROUP object is added to an internal table of GROUPS. +-- SPAWN provides methods to iterate through that internal GROUP object reference table: +-- +-- * @{#SPAWN.GetFirstAliveGroup}(): Will find the first alive GROUP it has spawned, and return the alive GROUP object and the first Index where the first alive GROUP object has been found. +-- * @{#SPAWN.GetNextAliveGroup}(): Will find the next alive GROUP object from a given Index, and return a reference to the alive GROUP object and the next Index where the alive GROUP has been found. +-- * @{#SPAWN.GetLastAliveGroup}(): Will find the last alive GROUP object, and will return a reference to the last live GROUP object and the last Index where the last alive GROUP object has been found. +-- +-- You can use the methods @{#SPAWN.GetFirstAliveGroup}() and sequently @{#SPAWN.GetNextAliveGroup}() to iterate through the alive GROUPS within the SPAWN object, and to actions... See the respective methods for an example. +-- The method @{#SPAWN.GetGroupFromIndex}() will return the GROUP object reference from the given Index, dead or alive... +-- +-- ## 1.5) 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.InitCleanUp}() initialization method has been defined that will silently monitor the status of each spawned group. +-- Once a group has a velocity = 0, and has been waiting for a defined interval, that group will be cleaned or removed from run-time. +-- There is a catch however :-) If a damaged group has returned to an airbase within the coalition, that group will not be considered as "lost"... +-- In such a case, when the inactive group is cleaned, a new group will Re-spawned automatically. +-- This models AI that has succesfully returned to their airbase, to restart their combat activities. +-- Check the @{#SPAWN.InitCleanUp}() for further info. +-- +-- ## 1.6) Catch the @{Group} spawn event in a callback function! +-- +-- When using the SpawnScheduled method, new @{Group}s are created following the schedule timing parameters. +-- When a new @{Group} is spawned, you maybe want to execute actions with that group spawned at the spawn event. +-- To SPAWN class supports this functionality through the @{#SPAWN.OnSpawnGroup}( **function( SpawnedGroup ) end ** ) method, which takes a function as a parameter that you can define locally. +-- Whenever a new @{Group} is spawned, the given function is called, and the @{Group} that was just spawned, is given as a parameter. +-- As a result, your spawn event handling function requires one parameter to be declared, which will contain the spawned @{Group} object. +-- A coding example is provided at the description of the @{#SPAWN.OnSpawnGroup}( **function( SpawnedGroup ) end ** ) method. +-- +-- ==== +-- +-- # **API CHANGE HISTORY** +-- +-- The underlying change log documents the API changes. Please read this carefully. The following notation is used: +-- +-- * **Added** parts are expressed in bold type face. +-- * _Removed_ parts are expressed in italic type face. +-- +-- Hereby the change log: +-- +-- 2017-02-04: SPAWN:InitUnControlled( **UnControlled** ) replaces SPAWN:InitUnControlled(). +-- +-- 2017-01-24: SPAWN:**InitAIOnOff( AIOnOff )** added. +-- +-- 2017-01-24: SPAWN:**InitAIOn()** added. +-- +-- 2017-01-24: SPAWN:**InitAIOff()** added. +-- +-- 2016-08-15: SPAWN:**InitCleanUp**( SpawnCleanUpInterval ) replaces SPAWN:_CleanUp_( SpawnCleanUpInterval ). +-- +-- 2016-08-15: SPAWN:**InitRandomizeZones( SpawnZones )** added. +-- +-- 2016-08-14: SPAWN:**OnSpawnGroup**( SpawnCallBackFunction, ... ) replaces SPAWN:_SpawnFunction_( SpawnCallBackFunction, ... ). +-- +-- 2016-08-14: SPAWN.SpawnInZone( Zone, __RandomizeGroup__, SpawnIndex ) replaces SpawnInZone( Zone, _RandomizeUnits, OuterRadius, InnerRadius,_ SpawnIndex ). +-- +-- 2016-08-14: SPAWN.SpawnFromVec3( Vec3, SpawnIndex ) replaces SpawnFromVec3( Vec3, _RandomizeUnits, OuterRadius, InnerRadius,_ SpawnIndex ): +-- +-- 2016-08-14: SPAWN.SpawnFromVec2( Vec2, SpawnIndex ) replaces SpawnFromVec2( Vec2, _RandomizeUnits, OuterRadius, InnerRadius,_ SpawnIndex ): +-- +-- 2016-08-14: SPAWN.SpawnFromUnit( SpawnUnit, SpawnIndex ) replaces SpawnFromUnit( SpawnUnit, _RandomizeUnits, OuterRadius, InnerRadius,_ SpawnIndex ): +-- +-- 2016-08-14: SPAWN.SpawnFromUnit( SpawnUnit, SpawnIndex ) replaces SpawnFromStatic( SpawnStatic, _RandomizeUnits, OuterRadius, InnerRadius,_ SpawnIndex ): +-- +-- 2016-08-14: SPAWN.**InitRandomizeUnits( RandomizeUnits, OuterRadius, InnerRadius )** added: +-- +-- 2016-08-14: SPAWN.**Init**Limit( SpawnMaxUnitsAlive, SpawnMaxGroups ) replaces SPAWN._Limit_( SpawnMaxUnitsAlive, SpawnMaxGroups ): +-- +-- 2016-08-14: SPAWN.**Init**Array( SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY ) replaces SPAWN._Array_( SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY ). +-- +-- 2016-08-14: SPAWN.**Init**RandomizeRoute( SpawnStartPoint, SpawnEndPoint, SpawnRadius, SpawnHeight ) replaces SPAWN._RandomizeRoute_( SpawnStartPoint, SpawnEndPoint, SpawnRadius, SpawnHeight ). +-- +-- 2016-08-14: SPAWN.**Init**RandomizeTemplate( SpawnTemplatePrefixTable ) replaces SPAWN._RandomizeTemplate_( SpawnTemplatePrefixTable ). +-- +-- 2016-08-14: SPAWN.**Init**UnControlled() replaces SPAWN._UnControlled_(). +-- +-- === +-- +-- # **AUTHORS and CONTRIBUTIONS** +-- +-- ### Contributions: +-- +-- * **Aaron**: Posed the idea for Group position randomization at SpawnInZone and make the Unit randomization separate from the Group randomization. +-- * [**Entropy**](https://forums.eagle.ru/member.php?u=111471), **Afinegan**: Came up with the requirement for AIOnOff(). +-- +-- ### Authors: +-- +-- * **FlightControl**: Design & Programming +-- +-- @module Spawn + + + +--- SPAWN Class +-- @type SPAWN +-- @extends Core.Base#BASE +-- @field ClassName +-- @field #string SpawnTemplatePrefix +-- @field #string SpawnAliasPrefix +-- @field #number AliveUnits +-- @field #number MaxAliveUnits +-- @field #number SpawnIndex +-- @field #number MaxAliveGroups +-- @field #SPAWN.SpawnZoneTable SpawnZoneTable +SPAWN = { + ClassName = "SPAWN", + SpawnTemplatePrefix = nil, + SpawnAliasPrefix = nil, +} + + +--- @type SPAWN.SpawnZoneTable +-- @list SpawnZone + + +--- 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() ) -- #SPAWN + 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.AIOnOff = true -- The AI is on by default when spawning a group. + self.SpawnUnControlled = false + + 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.AIOnOff = true -- The AI is on by default when spawning a group. + self.SpawnUnControlled = false + + 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 method should be used... +-- When a @{#SPAWN.New} is executed and the limit of the amount of units alive is reached, then no new spawn will happen of the group, until some of these units of the spawn object will be destroyed. +-- @param #SPAWN self +-- @param #number SpawnMaxUnitsAlive The maximum amount of units that can be alive at runtime. +-- @param #number SpawnMaxGroups The maximum amount of groups that can be spawned. When the limit is reached, then no more actual spawns will happen of the group. +-- This parameter is useful to define a maximum amount of airplanes, ground troops, helicopters, ships etc within a supply area. +-- This parameter accepts the value 0, which defines that there are no maximum group limits, but there are limits on the maximum of units that can be alive at the same time. +-- @return #SPAWN self +-- @usage +-- -- NATO helicopters engaging in the battle field. +-- -- This helicopter group consists of one Unit. So, this group will SPAWN maximum 2 groups simultaneously within the DCSRTE. +-- -- There will be maximum 24 groups spawned during the whole mission lifetime. +-- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):InitLimit( 2, 24 ) +function SPAWN:InitLimit( SpawnMaxUnitsAlive, SpawnMaxGroups ) + self:F( { self.SpawnTemplatePrefix, SpawnMaxUnitsAlive, SpawnMaxGroups } ) + + self.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 ... +-- @param #number SpawnHeight (optional) Specifies the **additional** height in meters that can be added to the base height specified at each waypoint in the ME. +-- @return #SPAWN +-- @usage +-- -- NATO helicopters engaging in the battle field. +-- -- The KA-50 has waypoints Start point ( =0 or SP ), 1, 2, 3, 4, End point (= 5 or DP). +-- -- Waypoints 2 and 3 will only be randomized. The others will remain on their original position with each new spawn of the helicopter. +-- -- The randomization of waypoint 2 and 3 will take place within a radius of 2000 meters. +-- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):InitRandomizeRoute( 2, 2, 2000 ) +function SPAWN:InitRandomizeRoute( SpawnStartPoint, SpawnEndPoint, SpawnRadius, SpawnHeight ) + self:F( { self.SpawnTemplatePrefix, SpawnStartPoint, SpawnEndPoint, SpawnRadius, SpawnHeight } ) + + self.SpawnRandomizeRoute = true + self.SpawnRandomizeRouteStartPoint = SpawnStartPoint + self.SpawnRandomizeRouteEndPoint = SpawnEndPoint + self.SpawnRandomizeRouteRadius = SpawnRadius + self.SpawnRandomizeRouteHeight = SpawnHeight + + for GroupID = 1, self.SpawnMaxGroups do + self:_RandomizeRoute( GroupID ) + end + + return self +end + +--- Randomizes the UNITs that are spawned within a radius band given an Outer and Inner radius. +-- @param #SPAWN self +-- @param #boolean RandomizeUnits If true, SPAWN will perform the randomization of the @{UNIT}s position within the group between a given outer and inner radius. +-- @param Dcs.DCSTypes#Distance OuterRadius (optional) The outer radius in meters where the new group will be spawned. +-- @param Dcs.DCSTypes#Distance InnerRadius (optional) The inner radius in meters where the new group will NOT be spawned. +-- @return #SPAWN +-- @usage +-- -- NATO helicopters engaging in the battle field. +-- -- The KA-50 has waypoints Start point ( =0 or SP ), 1, 2, 3, 4, End point (= 5 or DP). +-- -- Waypoints 2 and 3 will only be randomized. The others will remain on their original position with each new spawn of the helicopter. +-- -- The randomization of waypoint 2 and 3 will take place within a radius of 2000 meters. +-- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):InitRandomizeRoute( 2, 2, 2000 ) +function SPAWN:InitRandomizeUnits( RandomizeUnits, OuterRadius, InnerRadius ) + self:F( { self.SpawnTemplatePrefix, RandomizeUnits, OuterRadius, InnerRadius } ) + + self.SpawnRandomizeUnits = RandomizeUnits or false + self.SpawnOuterRadius = OuterRadius or 0 + self.SpawnInnerRadius = InnerRadius or 0 + + for GroupID = 1, self.SpawnMaxGroups do + self:_RandomizeRoute( GroupID ) + end + + return self +end + +--- This method is rather complicated to understand. But I'll try to explain. +-- This method becomes useful when you need to spawn groups with random templates of groups defined within the mission editor, +-- but they will all follow the same Template route and have the same prefix name. +-- In other words, this method randomizes between a defined set of groups the template to be used for each new spawn of a group. +-- @param #SPAWN self +-- @param #string SpawnTemplatePrefixTable A table with the names of the groups defined within the mission editor, from which one will be choosen when a new group will be spawned. +-- @return #SPAWN +-- @usage +-- -- NATO Tank Platoons invading Gori. +-- -- Choose between 13 different 'US Tank Platoon' configurations for each new SPAWN the Group to be spawned for the +-- -- 'US Tank Platoon Left', 'US Tank Platoon Middle' and 'US Tank Platoon Right' SpawnTemplatePrefixes. +-- -- Each new SPAWN will randomize the route, with a defined time interval of 200 seconds with 40% time variation (randomization) and +-- -- with a limit set of maximum 12 Units alive simulteneously and 150 Groups to be spawned during the whole mission. +-- Spawn_US_Platoon = { 'US Tank Platoon 1', 'US Tank Platoon 2', 'US Tank Platoon 3', 'US Tank Platoon 4', 'US Tank Platoon 5', +-- 'US Tank Platoon 6', 'US Tank Platoon 7', 'US Tank Platoon 8', 'US Tank Platoon 9', 'US Tank Platoon 10', +-- 'US Tank Platoon 11', 'US Tank Platoon 12', 'US Tank Platoon 13' } +-- Spawn_US_Platoon_Left = SPAWN:New( 'US Tank Platoon Left' ):InitLimit( 12, 150 ):Schedule( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 ) +-- Spawn_US_Platoon_Middle = SPAWN:New( 'US Tank Platoon Middle' ):InitLimit( 12, 150 ):Schedule( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 ) +-- Spawn_US_Platoon_Right = SPAWN:New( 'US Tank Platoon Right' ):InitLimit( 12, 150 ):Schedule( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 ) +function SPAWN:InitRandomizeTemplate( SpawnTemplatePrefixTable ) + self:F( { self.SpawnTemplatePrefix, SpawnTemplatePrefixTable } ) + + self.SpawnTemplatePrefixTable = SpawnTemplatePrefixTable + self.SpawnRandomizeTemplate = true + + for SpawnGroupID = 1, self.SpawnMaxGroups do + self:_RandomizeTemplate( SpawnGroupID ) + end + + return self +end + +--TODO: Add example. +--- This method provides the functionality to randomize the spawning of the Groups at a given list of zones of different types. +-- @param #SPAWN self +-- @param #table SpawnZoneTable A table with @{Zone} objects. If this table is given, then each spawn will be executed within the given list of @{Zone}s objects. +-- @return #SPAWN +-- @usage +-- -- NATO Tank Platoons invading Gori. +-- -- Choose between 3 different zones for each new SPAWN the Group to be executed, regardless of the zone type. +function SPAWN:InitRandomizeZones( SpawnZoneTable ) + self:F( { self.SpawnTemplatePrefix, SpawnZoneTable } ) + + self.SpawnZoneTable = SpawnZoneTable + self.SpawnRandomizeZones = true + + for SpawnGroupID = 1, self.SpawnMaxGroups do + self:_RandomizeZones( SpawnGroupID ) + end + + return self +end + + + + + +--- For planes and helicopters, when these groups go home and land on their home airbases and farps, they normally would taxi to the parking spot, shut-down their engines and wait forever until the Group is removed by the runtime environment. +-- This method is used to re-spawn automatically (so no extra call is needed anymore) the same group after it has landed. +-- This will enable a spawned group to be re-spawned after it lands, until it is destroyed... +-- Note: When the group is respawned, it will re-spawn from the original airbase where it took off. +-- So ensure that the routes for groups that respawn, always return to the original airbase, or players may get confused ... +-- @param #SPAWN self +-- @return #SPAWN self +-- @usage +-- -- RU Su-34 - AI Ship Attack +-- -- Re-SPAWN the Group(s) after each landing and Engine Shut-Down automatically. +-- SpawnRU_SU34 = SPAWN:New( 'TF1 RU Su-34 Krymsk@AI - Attack Ships' ):Schedule( 2, 3, 1800, 0.4 ):SpawnUncontrolled():InitRandomizeRoute( 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:InitCleanUp( SpawnCleanUpInterval ) + self:F( { self.SpawnTemplatePrefix, SpawnCleanUpInterval } ) + + self.SpawnCleanUpInterval = SpawnCleanUpInterval + self.SpawnCleanUpTimeStamps = {} + + local SpawnGroup, SpawnCursor = self:GetFirstAliveGroup() + self:T( { "CleanUp Scheduler:", SpawnGroup } ) + + --self.CleanUpFunction = routines.scheduleFunction( self._SpawnCleanUpScheduler, { self }, timer.getTime() + 1, SpawnCleanUpInterval ) + self.CleanUpScheduler = SCHEDULER:New( self, self._SpawnCleanUpScheduler, {}, 1, SpawnCleanUpInterval, 0.2 ) + return self +end + + + +--- Makes the groups visible before start (like a batallion). +-- The method will take the position of the group as the first position in the array. +-- @param #SPAWN self +-- @param #number SpawnAngle The angle in degrees how the groups and each unit of the group will be positioned. +-- @param #number SpawnWidth The amount of Groups that will be positioned on the X axis. +-- @param #number SpawnDeltaX The space between each Group on the X-axis. +-- @param #number SpawnDeltaY The space between each Group on the Y-axis. +-- @return #SPAWN self +-- @usage +-- -- Define an array of Groups. +-- Spawn_BE_Ground = SPAWN:New( 'BE Ground' ):InitLimit( 2, 24 ):InitArray( 90, "Diamond", 10, 100, 50 ) +function SPAWN:InitArray( SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY ) + self:F( { self.SpawnTemplatePrefix, SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY } ) + + self.SpawnVisible = true -- When the first Spawn executes, all the Groups need to be made visible before start. + + local SpawnX = 0 + local SpawnY = 0 + local SpawnXIndex = 0 + local SpawnYIndex = 0 + + for SpawnGroupID = 1, self.SpawnMaxGroups do + self:T( { SpawnX, SpawnY, SpawnXIndex, SpawnYIndex } ) + + self.SpawnGroups[SpawnGroupID].Visible = true + self.SpawnGroups[SpawnGroupID].Spawned = false + + SpawnXIndex = SpawnXIndex + 1 + if SpawnWidth and SpawnWidth ~= 0 then + if SpawnXIndex >= SpawnWidth then + SpawnXIndex = 0 + SpawnYIndex = SpawnYIndex + 1 + end + end + + local SpawnRootX = self.SpawnGroups[SpawnGroupID].SpawnTemplate.x + local SpawnRootY = self.SpawnGroups[SpawnGroupID].SpawnTemplate.y + + self:_TranslateRotate( SpawnGroupID, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle ) + + self.SpawnGroups[SpawnGroupID].SpawnTemplate.lateActivation = true + self.SpawnGroups[SpawnGroupID].SpawnTemplate.visible = true + + self.SpawnGroups[SpawnGroupID].Visible = true + + _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 + +do -- AI methods + --- Turns the AI On or Off for the @{Group} when spawning. + -- @param #SPAWN self + -- @param #boolean AIOnOff A value of true sets the AI On, a value of false sets the AI Off. + -- @return #SPAWN The SPAWN object + function SPAWN:InitAIOnOff( AIOnOff ) + + self.AIOnOff = AIOnOff + return self + end + + --- Turns the AI On for the @{Group} when spawning. + -- @param #SPAWN self + -- @return #SPAWN The SPAWN object + function SPAWN:InitAIOn() + + return self:InitAIOnOff( true ) + end + + --- Turns the AI Off for the @{Group} when spawning. + -- @param #SPAWN self + -- @return #SPAWN The SPAWN object + function SPAWN:InitAIOff() + + return self:InitAIOnOff( false ) + end + +end -- AI methods + +--- Will spawn a group based on the internal index. +-- Note: Uses @{DATABASE} module defined in MOOSE. +-- @param #SPAWN self +-- @return Wrapper.Group#GROUP The group that was spawned. You can use this group for further actions. +function SPAWN:Spawn() + self:F( { self.SpawnTemplatePrefix, self.SpawnIndex, self.AliveUnits } ) + + 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 Wrapper.Group#GROUP The group that was spawned. You can use this group for further actions. +function SPAWN:ReSpawn( SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, SpawnIndex } ) + + if not SpawnIndex then + SpawnIndex = 1 + end + +-- TODO: This logic makes DCS crash and i don't know why (yet). + local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) + local WayPoints = SpawnGroup and SpawnGroup.WayPoints or nil + if SpawnGroup then + local SpawnDCSGroup = SpawnGroup:GetDCSObject() + if SpawnDCSGroup then + SpawnGroup:Destroy() + end + end + + local SpawnGroup = self:SpawnWithIndex( SpawnIndex ) + if SpawnGroup and WayPoints then + -- If there were WayPoints set, then Re-Execute those WayPoints! + SpawnGroup:WayPointInitialize( WayPoints ) + SpawnGroup:WayPointExecute( 1, 5 ) + end + + if SpawnGroup.ReSpawnFunction then + SpawnGroup:ReSpawnFunction() + end + + return SpawnGroup +end + +--- Will spawn a group with a specified index number. +-- Uses @{DATABASE} global object defined in MOOSE. +-- @param #SPAWN self +-- @param #string SpawnIndex The index of the group to be spawned. +-- @return Wrapper.Group#GROUP The group that was spawned. You can use this group for further actions. +function SPAWN:SpawnWithIndex( SpawnIndex ) + self:F2( { SpawnTemplatePrefix = self.SpawnTemplatePrefix, SpawnIndex = SpawnIndex, AliveUnits = self.AliveUnits, SpawnMaxGroups = self.SpawnMaxGroups } ) + + if self:_GetSpawnIndex( SpawnIndex ) then + + if self.SpawnGroups[self.SpawnIndex].Visible then + self.SpawnGroups[self.SpawnIndex].Group:Activate() + else + + local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate + self:T( SpawnTemplate.name ) + + if SpawnTemplate then + + local PointVec3 = POINT_VEC3:New( SpawnTemplate.route.points[1].x, SpawnTemplate.route.points[1].alt, SpawnTemplate.route.points[1].y ) + self:T( { "Current point of ", self.SpawnTemplatePrefix, PointVec3 } ) + + -- If RandomizeUnits, then Randomize the formation at the start point. + if self.SpawnRandomizeUnits then + for UnitID = 1, #SpawnTemplate.units do + local RandomVec2 = PointVec3:GetRandomVec2InRadius( self.SpawnOuterRadius, self.SpawnInnerRadius ) + SpawnTemplate.units[UnitID].x = RandomVec2.x + SpawnTemplate.units[UnitID].y = RandomVec2.y + self:T( 'SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) + end + end + + if SpawnTemplate.CategoryID == Group.Category.HELICOPTER or SpawnTemplate.CategoryID == Group.Category.AIRPLANE then + if SpawnTemplate.route.points[1].type == "TakeOffParking" then + SpawnTemplate.uncontrolled = self.SpawnUnControlled + end + end + end + + _EVENTDISPATCHER:OnBirthForTemplate( SpawnTemplate, self._OnBirth, self ) + _EVENTDISPATCHER:OnCrashForTemplate( SpawnTemplate, self._OnDeadOrCrash, self ) + _EVENTDISPATCHER:OnDeadForTemplate( SpawnTemplate, self._OnDeadOrCrash, self ) + + if self.Repeat then + _EVENTDISPATCHER:OnTakeOffForTemplate( SpawnTemplate, self._OnTakeOff, self ) + _EVENTDISPATCHER:OnLandForTemplate( SpawnTemplate, self._OnLand, self ) + end + if self.RepeatOnEngineShutDown then + _EVENTDISPATCHER:OnEngineShutDownForTemplate( SpawnTemplate, self._OnEngineShutDown, self ) + end + self:T3( SpawnTemplate.name ) + + self.SpawnGroups[self.SpawnIndex].Group = _DATABASE:Spawn( SpawnTemplate ) + + local SpawnGroup = self.SpawnGroups[self.SpawnIndex].Group -- Wrapper.Group#GROUP + + --TODO: Need to check if this function doesn't need to be scheduled, as the group may not be immediately there! + if SpawnGroup then + + SpawnGroup:SetAIOnOff( self.AIOnOff ) + end + + -- 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 method 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 method will be called when a new group is spawned, including its given parameters. +-- The first parameter of the SpawnFunction is the @{Group#GROUP} that was spawned. +-- @param #SPAWN self +-- @param #function SpawnCallBackFunction The function to be called when a group spawns. +-- @param SpawnFunctionArguments A random amount of arguments to be provided to the function when the group spawns. +-- @return #SPAWN +-- @usage +-- -- Declare SpawnObject and call a function when a new Group is spawned. +-- local SpawnObject = SPAWN +-- :New( "SpawnObject" ) +-- :InitLimit( 2, 10 ) +-- :OnSpawnGroup( +-- function( SpawnGroup ) +-- SpawnGroup:E( "I am spawned" ) +-- end +-- ) +-- :SpawnScheduled( 300, 0.3 ) +function SPAWN:OnSpawnGroup( SpawnCallBackFunction, ... ) + self:F( "OnSpawnGroup" ) + + self.SpawnFunctionHook = SpawnCallBackFunction + self.SpawnFunctionArguments = {} + if arg then + self.SpawnFunctionArguments = arg + end + + return self +end + + +--- Will spawn a group from a Vec3 in 3D space. +-- This method is mostly advisable to be used if you want to simulate spawning units in the air, like helicopters or airplanes. +-- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. +-- You can use the returned group to further define the route to be followed. +-- @param #SPAWN self +-- @param Dcs.DCSTypes#Vec3 Vec3 The Vec3 coordinates where to spawn the group. +-- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. +-- @return Wrapper.Group#GROUP that was spawned. +-- @return #nil Nothing was spawned. +function SPAWN:SpawnFromVec3( Vec3, SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, Vec3, SpawnIndex } ) + + local PointVec3 = POINT_VEC3:NewFromVec3( Vec3 ) + self:T2(PointVec3) + + if SpawnIndex then + else + SpawnIndex = self.SpawnIndex + 1 + end + + if self:_GetSpawnIndex( SpawnIndex ) then + + local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate + + if SpawnTemplate then + + self:T( { "Current point of ", self.SpawnTemplatePrefix, Vec3 } ) + + -- Translate the position of the Group Template to the Vec3. + for UnitID = 1, #SpawnTemplate.units do + self:T( 'Before Translation SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) + local UnitTemplate = SpawnTemplate.units[UnitID] + local SX = UnitTemplate.x + local SY = UnitTemplate.y + local BX = SpawnTemplate.route.points[1].x + local BY = SpawnTemplate.route.points[1].y + local TX = Vec3.x + ( SX - BX ) + local TY = Vec3.z + ( SY - BY ) + SpawnTemplate.units[UnitID].x = TX + SpawnTemplate.units[UnitID].y = TY + SpawnTemplate.units[UnitID].alt = Vec3.y + self:T( 'After Translation SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) + end + + SpawnTemplate.route.points[1].x = Vec3.x + SpawnTemplate.route.points[1].y = Vec3.z + SpawnTemplate.route.points[1].alt = Vec3.y + + SpawnTemplate.x = Vec3.x + SpawnTemplate.y = Vec3.z + + return self:SpawnWithIndex( self.SpawnIndex ) + end + end + + return nil +end + +--- Will spawn a group from a Vec2 in 3D space. +-- This method is mostly advisable to be used if you want to simulate spawning groups on the ground from air units, like vehicles. +-- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. +-- You can use the returned group to further define the route to be followed. +-- @param #SPAWN self +-- @param Dcs.DCSTypes#Vec2 Vec2 The Vec2 coordinates where to spawn the group. +-- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. +-- @return Wrapper.Group#GROUP that was spawned. +-- @return #nil Nothing was spawned. +function SPAWN:SpawnFromVec2( Vec2, SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, Vec2, SpawnIndex } ) + + local PointVec2 = POINT_VEC2:NewFromVec2( Vec2 ) + return self:SpawnFromVec3( PointVec2:GetVec3(), SpawnIndex ) +end + + +--- Will spawn a group from a hosting unit. This method is mostly advisable to be used if you want to simulate spawning from air units, like helicopters, which are dropping infantry into a defined Landing Zone. +-- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. +-- You can use the returned group to further define the route to be followed. +-- @param #SPAWN self +-- @param Wrapper.Unit#UNIT HostUnit The air or ground unit dropping or unloading the group. +-- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. +-- @return Wrapper.Group#GROUP that was spawned. +-- @return #nil Nothing was spawned. +function SPAWN:SpawnFromUnit( HostUnit, SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, HostUnit, SpawnIndex } ) + + if HostUnit and HostUnit:IsAlive() then -- and HostUnit:getUnit(1):inAir() == false then + return self:SpawnFromVec3( HostUnit:GetVec3(), SpawnIndex ) + end + + return nil +end + +--- Will spawn a group from a hosting static. This method is mostly advisable to be used if you want to simulate spawning from buldings and structures (static buildings). +-- You can use the returned group to further define the route to be followed. +-- @param #SPAWN self +-- @param Wrapper.Static#STATIC HostStatic The static dropping or unloading the group. +-- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. +-- @return Wrapper.Group#GROUP that was spawned. +-- @return #nil Nothing was spawned. +function SPAWN:SpawnFromStatic( HostStatic, SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, HostStatic, SpawnIndex } ) + + if HostStatic and HostStatic:IsAlive() then + return self:SpawnFromVec3( HostStatic:GetVec3(), SpawnIndex ) + end + + return nil +end + +--- Will spawn a Group within a given @{Zone}. +-- The @{Zone} can be of any type derived from @{Zone#ZONE_BASE}. +-- Once the @{Group} is spawned within the zone, the @{Group} will continue on its route. +-- The **first waypoint** (where the group is spawned) is replaced with the zone location coordinates. +-- @param #SPAWN self +-- @param Core.Zone#ZONE Zone The zone where the group is to be spawned. +-- @param #boolean RandomizeGroup (optional) Randomization of the @{Group} position in the zone. +-- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. +-- @return Wrapper.Group#GROUP that was spawned. +-- @return #nil when nothing was spawned. +function SPAWN:SpawnInZone( Zone, RandomizeGroup, SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, Zone, RandomizeGroup, SpawnIndex } ) + + if Zone then + if RandomizeGroup then + return self:SpawnFromVec2( Zone:GetRandomVec2(), SpawnIndex ) + else + return self:SpawnFromVec2( Zone:GetVec2(), SpawnIndex ) + end + end + + return nil +end + +--- (**AIR**) Will spawn a plane group in UnControlled or Controlled mode... +-- This will be similar to the uncontrolled flag setting in the ME. +-- You can use UnControlled mode to simulate planes startup and ready for take-off but aren't moving (yet). +-- ReSpawn the plane in Controlled mode, and the plane will move... +-- @param #SPAWN self +-- @param #boolean UnControlled true if UnControlled, false if Controlled. +-- @return #SPAWN self +function SPAWN:InitUnControlled( UnControlled ) + self:F2( { self.SpawnTemplatePrefix, UnControlled } ) + + self.SpawnUnControlled = UnControlled + + for SpawnGroupID = 1, self.SpawnMaxGroups do + self.SpawnGroups[SpawnGroupID].UnControlled = UnControlled + 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 + +--- Will find the first alive @{Group} it has spawned, and return the alive @{Group} object and the first Index where the first alive @{Group} object has been found. +-- @param #SPAWN self +-- @return Wrapper.Group#GROUP, #number The @{Group} object found, the new Index where the group was found. +-- @return #nil, #nil When no group is found, #nil is returned. +-- @usage +-- -- Find the first alive @{Group} object of the SpawnPlanes SPAWN object @{Group} collection that it has spawned during the mission. +-- local GroupPlane, Index = SpawnPlanes:GetFirstAliveGroup() +-- while GroupPlane ~= nil do +-- -- Do actions with the GroupPlane object. +-- GroupPlane, Index = SpawnPlanes:GetNextAliveGroup( Index ) +-- end +function SPAWN:GetFirstAliveGroup() + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) + + for SpawnIndex = 1, self.SpawnCount do + local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) + if SpawnGroup and SpawnGroup:IsAlive() then + return SpawnGroup, SpawnIndex + end + end + + return nil, nil +end + + +--- Will find the next alive @{Group} object from a given Index, and return a reference to the alive @{Group} object and the next Index where the alive @{Group} has been found. +-- @param #SPAWN self +-- @param #number SpawnIndexStart A Index holding the start position to search from. This method can also be used to find the first alive @{Group} object from the given Index. +-- @return Wrapper.Group#GROUP, #number The next alive @{Group} object found, the next Index where the next alive @{Group} object was found. +-- @return #nil, #nil When no alive @{Group} object is found from the start Index position, #nil is returned. +-- @usage +-- -- Find the first alive @{Group} object of the SpawnPlanes SPAWN object @{Group} collection that it has spawned during the mission. +-- local GroupPlane, Index = SpawnPlanes:GetFirstAliveGroup() +-- while GroupPlane ~= nil do +-- -- Do actions with the GroupPlane object. +-- GroupPlane, Index = SpawnPlanes:GetNextAliveGroup( Index ) +-- end +function SPAWN:GetNextAliveGroup( SpawnIndexStart ) + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnIndexStart } ) + + SpawnIndexStart = SpawnIndexStart + 1 + for SpawnIndex = SpawnIndexStart, self.SpawnCount do + local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) + if SpawnGroup and SpawnGroup:IsAlive() then + return SpawnGroup, SpawnIndex + end + end + + return nil, nil +end + +--- Will find the last alive @{Group} object, and will return a reference to the last live @{Group} object and the last Index where the last alive @{Group} object has been found. +-- @param #SPAWN self +-- @return Wrapper.Group#GROUP, #number The last alive @{Group} object found, the last Index where the last alive @{Group} object was found. +-- @return #nil, #nil When no alive @{Group} object is found, #nil is returned. +-- @usage +-- -- Find the last alive @{Group} object of the SpawnPlanes SPAWN object @{Group} collection that it has spawned during the mission. +-- local GroupPlane, Index = SpawnPlanes:GetLastAliveGroup() +-- if GroupPlane then -- GroupPlane can be nil!!! +-- -- Do actions with the GroupPlane object. +-- end +function SPAWN:GetLastAliveGroup() + self:F( { self.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 Wrapper.Group#GROUP self +function SPAWN:GetGroupFromIndex( SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnIndex } ) + + if not SpawnIndex then + SpawnIndex = 1 + end + + if self.SpawnGroups and self.SpawnGroups[SpawnIndex] then + local SpawnGroup = self.SpawnGroups[SpawnIndex].Group + return SpawnGroup + else + return nil + end +end + +--- 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 Dcs.DCSWrapper.Unit#Unit DCSUnit The @{DCSUnit} to be searched. +-- @return #string The prefix +-- @return #nil Nothing found +function SPAWN:_GetGroupIndexFromDCSUnit( DCSUnit ) + self:F3( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, DCSUnit } ) + + local SpawnUnitName = ( DCSUnit and DCSUnit:getName() ) or nil + if SpawnUnitName then + local IndexString = string.match( SpawnUnitName, "#.*-" ):sub( 2, -2 ) + if IndexString then + local Index = tonumber( IndexString ) + return Index + end + end + + return nil +end + +--- Return the prefix of a SpawnUnit. +-- The method will search for a #-mark, and will return the text before the #-mark. +-- It will return nil of no prefix was found. +-- @param #SPAWN self +-- @param Dcs.DCSWrapper.Unit#UNIT DCSUnit The @{DCSUnit} to be searched. +-- @return #string The prefix +-- @return #nil Nothing found +function SPAWN:_GetPrefixFromDCSUnit( DCSUnit ) + self:F3( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, DCSUnit } ) + + local DCSUnitName = ( DCSUnit and DCSUnit:getName() ) or nil + if DCSUnitName then + local SpawnPrefix = string.match( DCSUnitName, ".*#" ) + if SpawnPrefix then + SpawnPrefix = SpawnPrefix:sub( 1, -2 ) + end + return SpawnPrefix + end + + return nil +end + +--- Return the group within the SpawnGroups collection with input a DCSUnit. +-- @param #SPAWN self +-- @param Dcs.DCSWrapper.Unit#Unit DCSUnit The @{DCSUnit} to be searched. +-- @return Wrapper.Group#GROUP The Group +-- @return #nil Nothing found +function SPAWN:_GetGroupFromDCSUnit( DCSUnit ) + self:F3( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, DCSUnit } ) + + 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 + + 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:F3( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnGroup } ) + + local IndexString = string.match( SpawnGroup:GetName(), "#.*$" ):sub( 2 ) + local Index = tonumber( IndexString ) + + self:T3( IndexString, Index ) + return Index + +end + +--- Return the last maximum index that can be used. +function SPAWN:_GetLastIndex() + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) + + return self.SpawnMaxGroups +end + +--- Initalize the SpawnGroups collection. +function SPAWN:_InitializeSpawnGroups( SpawnIndex ) + self:F3( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnIndex } ) + + if not self.SpawnGroups[SpawnIndex] then + self.SpawnGroups[SpawnIndex] = {} + self.SpawnGroups[SpawnIndex].Visible = false + self.SpawnGroups[SpawnIndex].Spawned = false + self.SpawnGroups[SpawnIndex].UnControlled = false + self.SpawnGroups[SpawnIndex].SpawnTime = 0 + + self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix = self.SpawnTemplatePrefix + self.SpawnGroups[SpawnIndex].SpawnTemplate = self:_Prepare( self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix, SpawnIndex ) + end + + self:_RandomizeTemplate( SpawnIndex ) + self:_RandomizeRoute( SpawnIndex ) + --self:_TranslateRotate( SpawnIndex ) + + return self.SpawnGroups[SpawnIndex] +end + + + +--- Gets the CategoryID of the Group with the given SpawnPrefix +function SPAWN:_GetGroupCategoryID( SpawnPrefix ) + local TemplateGroup = Group.getByName( SpawnPrefix ) + + if TemplateGroup then + return TemplateGroup:getCategory() + else + return nil + end +end + +--- Gets the CoalitionID of the Group with the given SpawnPrefix +function SPAWN:_GetGroupCoalitionID( SpawnPrefix ) + local TemplateGroup = Group.getByName( SpawnPrefix ) + + if TemplateGroup then + return TemplateGroup:getCoalition() + else + return nil + end +end + +--- Gets the CountryID of the Group with the given SpawnPrefix +function SPAWN:_GetGroupCountryID( SpawnPrefix ) + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnPrefix } ) + + local TemplateGroup = Group.getByName( SpawnPrefix ) + + if TemplateGroup then + local TemplateUnits = TemplateGroup:getUnits() + return TemplateUnits[1]:getCountry() + else + return nil + end +end + +--- Gets the Group Template from the ME environment definition. +-- This method used the @{DATABASE} object, which contains ALL initial and new spawned object in MOOSE. +-- @param #SPAWN self +-- @param #string SpawnTemplatePrefix +-- @return @SPAWN self +function SPAWN:_GetTemplate( SpawnTemplatePrefix ) + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnTemplatePrefix } ) + + local SpawnTemplate = nil + + 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:T3( { SpawnTemplate } ) + return SpawnTemplate +end + +--- Prepares the new Group Template. +-- @param #SPAWN self +-- @param #string SpawnTemplatePrefix +-- @param #number SpawnIndex +-- @return #SPAWN self +function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) + 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 + + if SpawnTemplate.CategoryID == Group.Category.GROUND then + self:T3( "For ground units, visible needs to be false..." ) + SpawnTemplate.visible = false + end + + + for UnitID = 1, #SpawnTemplate.units do + SpawnTemplate.units[UnitID].name = string.format( SpawnTemplate.name .. '-%02d', UnitID ) + SpawnTemplate.units[UnitID].unitId = nil + end + + self:T3( { "Template:", SpawnTemplate } ) + return SpawnTemplate + +end + +--- Private method randomizing the routes. +-- @param #SPAWN self +-- @param #number SpawnIndex The index of the group to be spawned. +-- @return #SPAWN +function SPAWN:_RandomizeRoute( SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeRoute, self.SpawnRandomizeRouteStartPoint, self.SpawnRandomizeRouteEndPoint, self.SpawnRandomizeRouteRadius } ) + + if self.SpawnRandomizeRoute then + local SpawnTemplate = self.SpawnGroups[SpawnIndex].SpawnTemplate + local RouteCount = #SpawnTemplate.route.points + + for t = self.SpawnRandomizeRouteStartPoint + 1, ( RouteCount - self.SpawnRandomizeRouteEndPoint ) do + + SpawnTemplate.route.points[t].x = SpawnTemplate.route.points[t].x + math.random( self.SpawnRandomizeRouteRadius * -1, self.SpawnRandomizeRouteRadius ) + SpawnTemplate.route.points[t].y = SpawnTemplate.route.points[t].y + math.random( self.SpawnRandomizeRouteRadius * -1, self.SpawnRandomizeRouteRadius ) + + -- Manage randomization of altitude for airborne units ... + if SpawnTemplate.CategoryID == Group.Category.AIRPLANE or SpawnTemplate.CategoryID == Group.Category.HELICOPTER then + if SpawnTemplate.route.points[t].alt and self.SpawnRandomizeRouteHeight then + SpawnTemplate.route.points[t].alt = SpawnTemplate.route.points[t].alt + math.random( 1, self.SpawnRandomizeRouteHeight ) + end + else + SpawnTemplate.route.points[t].alt = nil + end + + self:T( 'SpawnTemplate.route.points[' .. t .. '].x = ' .. SpawnTemplate.route.points[t].x .. ', SpawnTemplate.route.points[' .. t .. '].y = ' .. SpawnTemplate.route.points[t].y ) + end + end + + self:_RandomizeZones( SpawnIndex ) + + return self +end + +--- Private method that randomizes the template of the group. +-- @param #SPAWN self +-- @param #number SpawnIndex +-- @return #SPAWN self +function SPAWN:_RandomizeTemplate( SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeTemplate } ) + + if self.SpawnRandomizeTemplate then + self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix = self.SpawnTemplatePrefixTable[ math.random( 1, #self.SpawnTemplatePrefixTable ) ] + self.SpawnGroups[SpawnIndex].SpawnTemplate = self:_Prepare( self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix, SpawnIndex ) + self.SpawnGroups[SpawnIndex].SpawnTemplate.route = 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 + local OldX = self.SpawnGroups[SpawnIndex].SpawnTemplate.units[1].x + local OldY = self.SpawnGroups[SpawnIndex].SpawnTemplate.units[1].y + for UnitID = 1, #self.SpawnGroups[SpawnIndex].SpawnTemplate.units do + self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].heading = self.SpawnTemplate.units[1].heading + self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].x = self.SpawnTemplate.units[1].x + ( self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].x - OldX ) + self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].y = self.SpawnTemplate.units[1].y + ( self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].y - OldY ) + self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].alt = self.SpawnTemplate.units[1].alt + end + end + + self:_RandomizeRoute( SpawnIndex ) + + return self +end + +--- Private method that randomizes the @{Zone}s where the Group will be spawned. +-- @param #SPAWN self +-- @param #number SpawnIndex +-- @return #SPAWN self +function SPAWN:_RandomizeZones( SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeZones } ) + + if self.SpawnRandomizeZones then + local SpawnZone = nil -- Core.Zone#ZONE_BASE + while not SpawnZone do + self:T( { SpawnZoneTableCount = #self.SpawnZoneTable, self.SpawnZoneTable } ) + local ZoneID = math.random( #self.SpawnZoneTable ) + self:T( ZoneID ) + SpawnZone = self.SpawnZoneTable[ ZoneID ]:GetZoneMaybe() + end + + self:T( "Preparing Spawn in Zone", SpawnZone:GetName() ) + + local SpawnVec2 = SpawnZone:GetRandomVec2() + + self:T( { SpawnVec2 = SpawnVec2 } ) + + local SpawnTemplate = self.SpawnGroups[SpawnIndex].SpawnTemplate + + self:T( { Route = SpawnTemplate.route } ) + + for UnitID = 1, #SpawnTemplate.units do + local UnitTemplate = SpawnTemplate.units[UnitID] + self:T( 'Before Translation SpawnTemplate.units['..UnitID..'].x = ' .. UnitTemplate.x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. UnitTemplate.y ) + local SX = UnitTemplate.x + local SY = UnitTemplate.y + local BX = SpawnTemplate.route.points[1].x + local BY = SpawnTemplate.route.points[1].y + local TX = SpawnVec2.x + ( SX - BX ) + local TY = SpawnVec2.y + ( SY - BY ) + UnitTemplate.x = TX + UnitTemplate.y = TY + -- TODO: Manage altitude based on landheight... + --SpawnTemplate.units[UnitID].alt = SpawnVec2: + self:T( 'After Translation SpawnTemplate.units['..UnitID..'].x = ' .. UnitTemplate.x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. UnitTemplate.y ) + end + SpawnTemplate.x = SpawnVec2.x + SpawnTemplate.y = SpawnVec2.y + SpawnTemplate.route.points[1].x = SpawnVec2.x + SpawnTemplate.route.points[1].y = SpawnVec2.y + end + + return self + +end + +function SPAWN:_TranslateRotate( SpawnIndex, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle ) + self:F( { self.SpawnTemplatePrefix, SpawnIndex, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle } ) + + -- Translate + local TranslatedX = SpawnX + local TranslatedY = SpawnY + + -- Rotate + -- From Wikipedia: https://en.wikipedia.org/wiki/Rotation_matrix#Common_rotations + -- x' = x \cos \theta - y \sin \theta\ + -- y' = x \sin \theta + y \cos \theta\ + local RotatedX = - TranslatedX * math.cos( math.rad( SpawnAngle ) ) + + TranslatedY * math.sin( math.rad( SpawnAngle ) ) + local RotatedY = TranslatedX * math.sin( math.rad( SpawnAngle ) ) + + TranslatedY * math.cos( math.rad( SpawnAngle ) ) + + -- Assign + self.SpawnGroups[SpawnIndex].SpawnTemplate.x = SpawnRootX - RotatedX + self.SpawnGroups[SpawnIndex].SpawnTemplate.y = SpawnRootY + RotatedY + + + local SpawnUnitCount = table.getn( self.SpawnGroups[SpawnIndex].SpawnTemplate.units ) + for u = 1, SpawnUnitCount do + + -- Translate + local TranslatedX = SpawnX + local TranslatedY = SpawnY - 10 * ( u - 1 ) + + -- Rotate + local RotatedX = - TranslatedX * math.cos( math.rad( SpawnAngle ) ) + + TranslatedY * math.sin( math.rad( SpawnAngle ) ) + local RotatedY = TranslatedX * math.sin( math.rad( SpawnAngle ) ) + + TranslatedY * math.cos( math.rad( SpawnAngle ) ) + + -- Assign + self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].x = SpawnRootX - RotatedX + self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].y = SpawnRootY + RotatedY + self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].heading = self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].heading + math.rad( SpawnAngle ) + end + + return self +end + +--- Get the next index of the groups to be spawned. This method is complicated, as it is used at several spaces. +function SPAWN:_GetSpawnIndex( SpawnIndex ) + self:F2( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnMaxGroups, self.SpawnMaxUnitsAlive, self.AliveUnits, #self.SpawnTemplate.units } ) + + if ( self.SpawnMaxGroups == 0 ) or ( SpawnIndex <= self.SpawnMaxGroups ) then + if ( self.SpawnMaxUnitsAlive == 0 ) or ( self.AliveUnits + #self.SpawnTemplate.units <= self.SpawnMaxUnitsAlive ) or self.UnControlled == true then + if SpawnIndex and SpawnIndex >= self.SpawnCount + 1 then + self.SpawnCount = self.SpawnCount + 1 + SpawnIndex = self.SpawnCount + end + self.SpawnIndex = SpawnIndex + if not self.SpawnGroups[self.SpawnIndex] then + self:_InitializeSpawnGroups( self.SpawnIndex ) + end + else + return nil + end + else + return nil + end + + return self.SpawnIndex +end + + +-- TODO Need to delete this... _DATABASE does this now ... + +--- @param #SPAWN self +-- @param Core.Event#EVENTDATA Event +function SPAWN:_OnBirth( Event ) + + if timer.getTime0() < timer.getAbsTime() then + if Event.IniDCSUnit then + local EventPrefix = self:_GetPrefixFromDCSUnit( Event.IniDCSUnit ) + self:T( { "Birth Event:", EventPrefix, self.SpawnTemplatePrefix } ) + if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then + self.AliveUnits = self.AliveUnits + 1 + self:T( "Alive Units: " .. self.AliveUnits ) + end + end + end + +end + +--- Obscolete +-- @todo Need to delete this... _DATABASE does this now ... + +--- @param #SPAWN self +-- @param Core.Event#EVENTDATA Event +function SPAWN:_OnDeadOrCrash( Event ) + self:F( self.SpawnTemplatePrefix, Event ) + + if Event.IniDCSUnit then + local EventPrefix = self:_GetPrefixFromDCSUnit( Event.IniDCSUnit ) + self:T( { "Dead event: " .. EventPrefix, self.SpawnTemplatePrefix } ) + if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then + self.AliveUnits = self.AliveUnits - 1 + self:T( "Alive Units: " .. self.AliveUnits ) + end + end +end + +--- 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:F2( { "_Scheduler", self.SpawnTemplatePrefix, self.SpawnAliasPrefix, self.SpawnIndex, self.SpawnMaxGroups, self.SpawnMaxUnitsAlive } ) + + -- Validate if there are still groups left in the batch... + self:Spawn() + + return true +end + +--- Schedules the CleanUp of Groups +-- @param #SPAWN self +-- @return #boolean True = Continue Scheduler +function SPAWN:_SpawnCleanUpScheduler() + self:F( { "CleanUp Scheduler:", self.SpawnTemplatePrefix } ) + + local SpawnGroup, SpawnCursor = self:GetFirstAliveGroup() + self:T( { "CleanUp Scheduler:", SpawnGroup, SpawnCursor } ) + + while SpawnGroup do + + local SpawnUnits = SpawnGroup:GetUnits() + + for UnitID, UnitData in pairs( SpawnUnits ) do + + local SpawnUnit = UnitData -- Wrapper.Unit#UNIT + local SpawnUnitName = SpawnUnit:GetName() + + + self.SpawnCleanUpTimeStamps[SpawnUnitName] = self.SpawnCleanUpTimeStamps[SpawnUnitName] or {} + local Stamp = self.SpawnCleanUpTimeStamps[SpawnUnitName] + self:T( { SpawnUnitName, Stamp } ) + + if Stamp.Vec2 then + if SpawnUnit:InAir() == false and SpawnUnit:GetVelocityKMH() < 1 then + local NewVec2 = SpawnUnit:GetVec2() + if Stamp.Vec2.x == NewVec2.x and Stamp.Vec2.y == NewVec2.y then + -- If the plane is not moving, and is on the ground, assign it with a timestamp... + if Stamp.Time + self.SpawnCleanUpInterval < timer.getTime() then + self:T( { "CleanUp Scheduler:", "ReSpawning:", SpawnGroup:GetName() } ) + self:ReSpawn( SpawnCursor ) + Stamp.Vec2 = nil + Stamp.Time = nil + end + else + Stamp.Time = timer.getTime() + Stamp.Vec2 = SpawnUnit:GetVec2() + end + else + Stamp.Vec2 = nil + Stamp.Time = nil + end + else + if SpawnUnit:InAir() == false then + Stamp.Vec2 = SpawnUnit:GetVec2() + if SpawnUnit:GetVelocityKMH() < 1 then + Stamp.Time = timer.getTime() + end + else + Stamp.Time = nil + Stamp.Vec2 = nil + end + end + end + + SpawnGroup, SpawnCursor = self:GetNextAliveGroup( SpawnCursor ) + + self:T( { "CleanUp Scheduler:", SpawnGroup, SpawnCursor } ) + + end + + return true -- Repeat + +end +--- 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 Core.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 + -- 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 ) + if self.TargetSkill[_targetskill] then + if (_evade > self.TargetSkill[_targetskill].Evade) then + self:T( string.format("Evading, target skill " ..string.format(_targetskill)) ) + local _targetMim = Weapon.getTarget(SEADWeapon) + local _targetMimname = Unit.getName(_targetMim) + local _targetMimgroup = Unit.getGroup(Weapon.getTarget(SEADWeapon)) + local _targetMimcont= _targetMimgroup:getController() + routines.groupRandomDistSelf(_targetMimgroup,300,'Diamond',250,20) -- move randomly + local SuppressedGroups1 = {} -- unit suppressed radar off for a random time + local function SuppressionEnd1(id) + id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN) + SuppressedGroups1[id.groupName] = nil + end + local id = { + groupName = _targetMimgroup, + ctrl = _targetMimcont + } + local delay1 = math.random(self.TargetSkill[_targetskill].DelayOff[1], self.TargetSkill[_targetskill].DelayOff[2]) + if SuppressedGroups1[id.groupName] == nil then + SuppressedGroups1[id.groupName] = { + SuppressionEndTime1 = timer.getTime() + delay1, + SuppressionEndN1 = SuppressionEndCounter1 --Store instance of SuppressionEnd() scheduled function + } + Controller.setOption(_targetMimcont, AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN) + timer.scheduleFunction(SuppressionEnd1, id, SuppressedGroups1[id.groupName].SuppressionEndTime1) --Schedule the SuppressionEnd() function + --trigger.action.outText( string.format("Radar Off " ..string.format(delay1)), 20) + end + + local SuppressedGroups = {} + local function SuppressionEnd(id) + id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.RED) + SuppressedGroups[id.groupName] = nil + end + local id = { + groupName = _targetMimgroup, + ctrl = _targetMimcont + } + local delay = math.random(self.TargetSkill[_targetskill].DelayOn[1], self.TargetSkill[_targetskill].DelayOn[2]) + if SuppressedGroups[id.groupName] == nil then + SuppressedGroups[id.groupName] = { + SuppressionEndTime = timer.getTime() + delay, + SuppressionEndN = SuppressionEndCounter --Store instance of SuppressionEnd() scheduled function + } + timer.scheduleFunction(SuppressionEnd, id, SuppressedGroups[id.groupName].SuppressionEndTime) --Schedule the SuppressionEnd() function + --trigger.action.outText( string.format("Radar On " ..string.format(delay)), 20) + end + 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 Core.Base#BASE +-- @field Wrapper.Client#CLIENT EscortClient +-- @field Wrapper.Group#GROUP EscortGroup +-- @field #string EscortName +-- @field #ESCORT.MODE EscortMode The mode the escort is in. +-- @field Core.Scheduler#SCHEDULER FollowScheduler The instance of the SCHEDULER class. +-- @field #number FollowDistance The current follow distance. +-- @field #boolean ReportTargets If true, nearby targets are reported. +-- @Field Dcs.DCSTypes#AI.Option.Air.val.ROE OptionROE Which ROE is set to the EscortGroup. +-- @field Dcs.DCSTypes#AI.Option.Air.val.REACTION_ON_THREAT OptionReactionOnThreat Which REACTION_ON_THREAT is set to the EscortGroup. +-- @field Core.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 Wrapper.Client#CLIENT EscortClient The client escorted by the EscortGroup. +-- @param Wrapper.Group#GROUP EscortGroup The group AI escorting the EscortClient. +-- @param #string EscortName Name of the escort. +-- @param #string EscortBriefing A text showing the ESCORT briefing to the player. Note that if no EscortBriefing is provided, the default briefing will be shown. +-- @return #ESCORT self +-- @usage +-- -- Declare a new EscortPlanes object as follows: +-- +-- -- First find the GROUP object and the CLIENT object. +-- local EscortClient = CLIENT:FindByName( "Unit Name" ) -- The Unit Name is the name of the unit flagged with the skill Client in the mission editor. +-- local EscortGroup = GROUP:FindByName( "Group Name" ) -- The Group Name is the name of the group that will escort the Escort Client. +-- +-- -- Now use these 2 objects to construct the new EscortPlanes object. +-- EscortPlanes = ESCORT:New( EscortClient, EscortGroup, "Desert", "Welcome to the mission. You are escorted by a plane with code name 'Desert', which can be instructed through the F10 radio menu." ) +function ESCORT:New( EscortClient, EscortGroup, EscortName, EscortBriefing ) + local self = BASE:Inherit( self, BASE:New() ) + self:F( { EscortClient, EscortGroup, EscortName } ) + + self.EscortClient = EscortClient -- Wrapper.Client#CLIENT + self.EscortGroup = EscortGroup -- Wrapper.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() + + if not EscortBriefing then + EscortGroup:MessageToClient( EscortGroup:GetCategoryName() .. " '" .. EscortName .. "' (" .. EscortGroup:GetCallsign() .. ") reporting! " .. + "We're escorting your flight. " .. + "Use the Radio Menu and F10 and use the options under + " .. EscortName .. "\n", + 60, EscortClient + ) + else + EscortGroup:MessageToClient( EscortGroup:GetCategoryName() .. " '" .. EscortName .. "' (" .. EscortGroup:GetCallsign() .. ") " .. EscortBriefing, + 60, EscortClient + ) + end + + self.FollowDistance = 100 + self.CT1 = 0 + self.GT1 = 0 + self.FollowScheduler = 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 Dcs.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 Dcs.DCSTypes#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. +-- @param Dcs.DCSTypes#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. +-- @param #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 Dcs.DCSTypes#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. +-- @param Dcs.DCSTypes#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. +-- @param #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 Dcs.DCSTypes#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. +-- @param Dcs.DCSTypes#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. +-- @param #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 = 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 = 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 = 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 = 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 Dcs.DCSTypes#Time Seconds Optional parameter that lets the escort report their current detected targets after specified time interval in seconds. The default time is 30 seconds. +-- @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 = 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 -- Wrapper.Group#GROUP + local OrbitUnit = OrbitGroup:GetUnit(1) -- Wrapper.Unit#UNIT + local OrbitHeight = MenuParam.ParamHeight + local OrbitSeconds = MenuParam.ParamSeconds -- Not implemented yet + + self.FollowScheduler:Stop() + + local PointFrom = {} + local GroupVec3 = EscortGroup:GetUnit(1):GetVec3() + PointFrom = {} + PointFrom.x = GroupVec3.x + PointFrom.y = GroupVec3.z + PointFrom.speed = 250 + PointFrom.type = AI.Task.WaypointType.TURNING_POINT + PointFrom.alt = GroupVec3.y + PointFrom.alt_type = AI.Task.AltitudeType.BARO + + local OrbitPoint = OrbitUnit:GetVec2() + local PointTo = {} + PointTo.x = OrbitPoint.x + PointTo.y = OrbitPoint.y + PointTo.speed = 250 + PointTo.type = AI.Task.WaypointType.TURNING_POINT + PointTo.alt = OrbitHeight + PointTo.alt_type = AI.Task.AltitudeType.BARO + PointTo.task = EscortGroup:TaskOrbitCircleAtVec2( OrbitPoint, OrbitHeight, 0 ) + + local Points = { PointFrom, PointTo } + + EscortGroup:OptionROEHoldFire() + EscortGroup:OptionROTPassiveDefense() + + EscortGroup:SetTask( EscortGroup:TaskRoute( Points ) ) + EscortGroup:MessageToClient( "Orbiting at location.", 10, EscortClient ) + +end + +--- @param #MENUPARAM MenuParam +function ESCORT._JoinUpAndFollow( 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 Functional.Escort#ESCORT self +-- @param Wrapper.Group#GROUP EscortGroup +-- @param Wrapper.Client#CLIENT EscortClient +-- @param Dcs.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 = 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 Wrapper.Group#GROUP EscortGroup +function _Resume( EscortGroup ) + env.info( '_Resume' ) + + local Escort = EscortGroup:GetState( EscortGroup, "Escort" ) + env.info( "EscortMode = " .. Escort.EscortMode ) + if Escort.EscortMode == ESCORT.MODE.FOLLOW then + Escort:JoinUpAndFollow( EscortGroup, Escort.EscortClient, Escort.Distance ) + end + +end + +--- @param #MENUPARAM MenuParam +function ESCORT._AttackTarget( MenuParam ) + + local self = MenuParam.ParamSelf + local EscortGroup = self.EscortGroup + + local EscortClient = self.EscortClient + local AttackUnit = MenuParam.ParamUnit -- Wrapper.Unit#UNIT + + self.FollowScheduler:Stop() + + self:T( AttackUnit ) + + if EscortGroup:IsAir() then + EscortGroup:OptionROEOpenFire() + EscortGroup:OptionROTPassiveDefense() + EscortGroup:SetState( EscortGroup, "Escort", self ) + SCHEDULER:New( EscortGroup, + EscortGroup.PushTask, + { EscortGroup:TaskCombo( + { EscortGroup:TaskAttackUnit( AttackUnit ), + EscortGroup:TaskFunction( 1, 2, "_Resume", { "''" } ) + } + ) + }, 10 + ) + else + SCHEDULER:New( EscortGroup, + EscortGroup.PushTask, + { EscortGroup:TaskCombo( + { EscortGroup:TaskFireAtPoint( AttackUnit:GetVec2(), 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 -- Wrapper.Unit#UNIT + + self.FollowScheduler:Stop() + + self:T( AttackUnit ) + + if EscortGroupAttack:IsAir() then + EscortGroupAttack:OptionROEOpenFire() + EscortGroupAttack:OptionROTVertical() + SCHDULER:New( EscortGroupAttack, + EscortGroupAttack.PushTask, + { EscortGroupAttack:TaskCombo( + { EscortGroupAttack:TaskAttackUnit( AttackUnit ), + EscortGroupAttack:TaskOrbitCircle( 500, 350 ) + } + ) + }, 10 + ) + else + SCHEDULER:New( EscortGroupAttack, + EscortGroupAttack.PushTask, + { EscortGroupAttack:TaskCombo( + { EscortGroupAttack:TaskFireAtPoint( AttackUnit:GetVec2(), 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 + + SCHEDULER:New( EscortGroup, EscortGroup.SetTask, { EscortGroup:TaskRoute( WayPoints ) }, 1 ) + + EscortGroup:MessageToClient( "Resuming mission from waypoint " .. WayPoint .. ".", 10, EscortClient ) +end + +--- Registers the waypoints +-- @param #ESCORT self +-- @return #table +function ESCORT:RegisterRoute() + self:F() + + local EscortGroup = self.EscortGroup -- Wrapper.Group#GROUP + + local TaskPoints = EscortGroup:GetTaskRoute() + + self:T( TaskPoints ) + + return TaskPoints +end + +--- @param Functional.Escort#ESCORT self +function ESCORT:_FollowScheduler() + self:F( { self.FollowDistance } ) + + self:T( {self.EscortClient.UnitName, self.EscortGroup.GroupName } ) + if self.EscortGroup:IsAlive() and self.EscortClient:IsAlive() then + + local ClientUnit = self.EscortClient:GetClientGroupUnit() + local GroupUnit = self.EscortGroup:GetUnit( 1 ) + local FollowDistance = self.FollowDistance + + self:T( {ClientUnit.UnitName, GroupUnit.UnitName } ) + + if self.CT1 == 0 and self.GT1 == 0 then + self.CV1 = ClientUnit:GetVec3() + self:T( { "self.CV1", self.CV1 } ) + self.CT1 = timer.getTime() + self.GV1 = GroupUnit:GetVec3() + self.GT1 = timer.getTime() + else + local CT1 = self.CT1 + local CT2 = timer.getTime() + local CV1 = self.CV1 + local CV2 = ClientUnit:GetVec3() + self.CT1 = CT2 + self.CV1 = CV2 + + local CD = ( ( CV2.x - CV1.x )^2 + ( CV2.y - CV1.y )^2 + ( CV2.z - CV1.z )^2 ) ^ 0.5 + local CT = CT2 - CT1 + + local CS = ( 3600 / CT ) * ( CD / 1000 ) + + self:T2( { "Client:", CS, CD, CT, CV2, CV1, CT2, CT1 } ) + + local GT1 = self.GT1 + local GT2 = timer.getTime() + local GV1 = self.GV1 + local GV2 = GroupUnit:GetVec3() + self.GT1 = GT2 + self.GV1 = GV2 + + local GD = ( ( GV2.x - GV1.x )^2 + ( GV2.y - GV1.y )^2 + ( GV2.z - GV1.z )^2 ) ^ 0.5 + local GT = GT2 - GT1 + + local GS = ( 3600 / GT ) * ( GD / 1000 ) + + self:T2( { "Group:", GS, GD, GT, GV2, GV1, GT2, GT1 } ) + + -- Calculate the group direction vector + local GV = { x = GV2.x - CV2.x, y = GV2.y - CV2.y, z = GV2.z - CV2.z } + + -- Calculate GH2, GH2 with the same height as CV2. + local GH2 = { x = GV2.x, y = CV2.y, z = GV2.z } + + -- Calculate the angle of GV to the orthonormal plane + local alpha = math.atan2( GV.z, GV.x ) + + -- Now we calculate the intersecting vector between the circle around CV2 with radius FollowDistance and GH2. + -- From the GeoGebra model: CVI = (x(CV2) + FollowDistance cos(alpha), y(GH2) + FollowDistance sin(alpha), z(CV2)) + local CVI = { x = CV2.x + FollowDistance * math.cos(alpha), + y = GH2.y, + z = CV2.z + FollowDistance * math.sin(alpha), + } + + -- Calculate the direction vector DV of the escort group. We use CVI as the base and CV2 as the direction. + local DV = { x = CV2.x - CVI.x, y = CV2.y - CVI.y, z = CV2.z - CVI.z } + + -- We now calculate the unary direction vector DVu, so that we can multiply DVu with the speed, which is expressed in meters / s. + -- We need to calculate this vector to predict the point the escort group needs to fly to according its speed. + -- The distance of the destination point should be far enough not to have the aircraft starting to swipe left to right... + local DVu = { x = DV.x / FollowDistance, y = DV.y / FollowDistance, z = DV.z / FollowDistance } + + -- Now we can calculate the group destination vector GDV. + local GDV = { x = DVu.x * CS * 8 + CVI.x, y = CVI.y, z = DVu.z * CS * 8 + CVI.z } + + if self.SmokeDirectionVector == true then + trigger.action.smoke( GDV, trigger.smokeColor.Red ) + end + + self:T2( { "CV2:", CV2 } ) + self:T2( { "CVI:", CVI } ) + self:T2( { "GDV:", GDV } ) + + -- Measure distance between client and group + local CatchUpDistance = ( ( GDV.x - GV2.x )^2 + ( GDV.y - GV2.y )^2 + ( GDV.z - GV2.z )^2 ) ^ 0.5 + + -- The calculation of the Speed would simulate that the group would take 30 seconds to overcome + -- the requested Distance). + local Time = 10 + local CatchUpSpeed = ( CatchUpDistance - ( CS * 8.4 ) ) / Time + + local Speed = CS + CatchUpSpeed + if Speed < 0 then + Speed = 0 + end + + self:T( { "Client Speed, Escort Speed, Speed, FollowDistance, Time:", CS, GS, Speed, FollowDistance, Time } ) + + -- Now route the escort to the desired point with the desired speed. + self.EscortGroup:RouteToVec3( GDV, Speed / 3.6 ) -- DCS models speed in Mps (Miles per second) + end + + return true + end + + return false +end + + +--- Report Targets Scheduler. +-- @param #ESCORT self +function ESCORT:_ReportTargetsScheduler() + self:F( self.EscortGroup:GetName() ) + + if self.EscortGroup:IsAlive() and self.EscortClient:IsAlive() then + local EscortGroupName = self.EscortGroup:GetName() + local EscortTargets = self.EscortGroup:GetDetectedTargets() + + local ClientEscortTargets = self.EscortClient._EscortGroups[EscortGroupName].Targets + + local EscortTargetMessages = "" + for EscortTargetID, EscortTarget in pairs( EscortTargets ) do + local EscortObject = EscortTarget.object + self:T( EscortObject ) + if EscortObject and EscortObject:isExist() and EscortObject.id_ < 50000000 then + + local EscortTargetUnit = UNIT:Find( EscortObject ) + local EscortTargetUnitName = EscortTargetUnit:GetName() + + + + -- local EscortTargetIsDetected, + -- EscortTargetIsVisible, + -- EscortTargetLastTime, + -- EscortTargetKnowType, + -- EscortTargetKnowDistance, + -- EscortTargetLastPos, + -- EscortTargetLastVelocity + -- = self.EscortGroup:IsTargetDetected( EscortObject ) + -- + -- self:T( { EscortTargetIsDetected, + -- EscortTargetIsVisible, + -- EscortTargetLastTime, + -- EscortTargetKnowType, + -- EscortTargetKnowDistance, + -- EscortTargetLastPos, + -- EscortTargetLastVelocity } ) + + + local EscortTargetUnitVec3 = EscortTargetUnit:GetVec3() + local EscortVec3 = self.EscortGroup:GetVec3() + local Distance = ( ( EscortTargetUnitVec3.x - EscortVec3.x )^2 + + ( EscortTargetUnitVec3.y - EscortVec3.y )^2 + + ( EscortTargetUnitVec3.z - EscortVec3.z )^2 + ) ^ 0.5 / 1000 + + self:T( { self.EscortGroup:GetName(), EscortTargetUnit:GetName(), Distance, EscortTarget } ) + + if Distance <= 15 then + + if not ClientEscortTargets[EscortTargetUnitName] then + ClientEscortTargets[EscortTargetUnitName] = {} + end + ClientEscortTargets[EscortTargetUnitName].AttackUnit = EscortTargetUnit + ClientEscortTargets[EscortTargetUnitName].visible = EscortTarget.visible + ClientEscortTargets[EscortTargetUnitName].type = EscortTarget.type + ClientEscortTargets[EscortTargetUnitName].distance = EscortTarget.distance + else + if ClientEscortTargets[EscortTargetUnitName] then + ClientEscortTargets[EscortTargetUnitName] = nil + end + end + end + end + + self:T( { "Sorting Targets Table:", ClientEscortTargets } ) + table.sort( ClientEscortTargets, function( a, b ) return a.Distance < b.Distance end ) + self:T( { "Sorted Targets Table:", ClientEscortTargets } ) + + -- Remove the sub menus of the Attack menu of the Escort for the EscortGroup. + self.EscortMenuAttackNearbyTargets:RemoveSubMenus() + + if self.EscortMenuTargetAssistance then + self.EscortMenuTargetAssistance:RemoveSubMenus() + end + + --for MenuIndex = 1, #self.EscortMenuAttackTargets do + -- self:T( { "Remove Menu:", self.EscortMenuAttackTargets[MenuIndex] } ) + -- self.EscortMenuAttackTargets[MenuIndex] = self.EscortMenuAttackTargets[MenuIndex]:Remove() + --end + + + if ClientEscortTargets then + for ClientEscortTargetUnitName, ClientEscortTargetData in pairs( ClientEscortTargets ) do + + for ClientEscortGroupName, EscortGroupData in pairs( self.EscortClient._EscortGroups ) do + + if ClientEscortTargetData and ClientEscortTargetData.AttackUnit:IsAlive() then + + local EscortTargetMessage = "" + local EscortTargetCategoryName = ClientEscortTargetData.AttackUnit:GetCategoryName() + local EscortTargetCategoryType = ClientEscortTargetData.AttackUnit:GetTypeName() + if ClientEscortTargetData.type then + EscortTargetMessage = EscortTargetMessage .. EscortTargetCategoryName .. " (" .. EscortTargetCategoryType .. ") at " + else + EscortTargetMessage = EscortTargetMessage .. "Unknown target at " + end + + local EscortTargetUnitVec3 = ClientEscortTargetData.AttackUnit:GetVec3() + local EscortVec3 = self.EscortGroup:GetVec3() + local Distance = ( ( EscortTargetUnitVec3.x - EscortVec3.x )^2 + + ( EscortTargetUnitVec3.y - EscortVec3.y )^2 + + ( EscortTargetUnitVec3.z - EscortVec3.z )^2 + ) ^ 0.5 / 1000 + + self:T( { self.EscortGroup:GetName(), ClientEscortTargetData.AttackUnit:GetName(), Distance, ClientEscortTargetData.AttackUnit } ) + if ClientEscortTargetData.visible == false then + EscortTargetMessage = EscortTargetMessage .. string.format( "%.2f", Distance ) .. " estimated km" + else + EscortTargetMessage = EscortTargetMessage .. string.format( "%.2f", Distance ) .. " km" + end + + if ClientEscortTargetData.visible then + EscortTargetMessage = EscortTargetMessage .. ", visual" + end + + if ClientEscortGroupName == EscortGroupName then + + MENU_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 EscortVec3 = self.EscortGroup:GetVec3() + local Distance = ( ( WayPoint.x - EscortVec3.x )^2 + + ( WayPoint.y - EscortVec3.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 Core.Set#SET_CLIENT DBClients +-- @extends Core.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 Wrapper.Client#CLIENT Client +-- function( Client ) +-- +-- ... actions ... +-- +-- end +-- ) + + self.MessagesOnOff = true + + self.TrackingToAll = false + self.TrackingOnOff = true + self.TrackingFrequency = 3 + + self.AlertsToAll = true + self.AlertsHitsOnOff = true + self.AlertsLaunchesOnOff = true + + self.DetailsRangeOnOff = true + self.DetailsBearingOnOff = true + + self.MenusOnOff = true + + self.TrackingMissiles = {} + + self.TrackingScheduler = SCHEDULER:New( self, self._TrackMissiles, {}, 0.5, 0.05, 0 ) + + return self +end + +-- Initialization methods. + + + +--- Sets by default the display of any message to be ON or OFF. +-- @param #MISSILETRAINER self +-- @param #boolean MessagesOnOff true or false +-- @return #MISSILETRAINER self +function MISSILETRAINER:InitMessagesOnOff( MessagesOnOff ) + self:F( MessagesOnOff ) + + self.MessagesOnOff = MessagesOnOff + if self.MessagesOnOff == true then + MESSAGE:New( "Messages ON", 15, "Menu" ):ToAll() + else + MESSAGE:New( "Messages OFF", 15, "Menu" ):ToAll() + end + + return self +end + +--- Sets by default the missile tracking report for all players or only for those missiles targetted to you. +-- @param #MISSILETRAINER self +-- @param #boolean TrackingToAll true or false +-- @return #MISSILETRAINER self +function MISSILETRAINER:InitTrackingToAll( TrackingToAll ) + self:F( TrackingToAll ) + + self.TrackingToAll = TrackingToAll + if self.TrackingToAll == true then + MESSAGE:New( "Missile tracking to all players ON", 15, "Menu" ):ToAll() + else + MESSAGE:New( "Missile tracking to all players OFF", 15, "Menu" ):ToAll() + end + + return self +end + +--- Sets by default the display of missile tracking report to be ON or OFF. +-- @param #MISSILETRAINER self +-- @param #boolean TrackingOnOff true or false +-- @return #MISSILETRAINER self +function MISSILETRAINER:InitTrackingOnOff( TrackingOnOff ) + self:F( TrackingOnOff ) + + self.TrackingOnOff = TrackingOnOff + if self.TrackingOnOff == true then + MESSAGE:New( "Missile tracking ON", 15, "Menu" ):ToAll() + else + MESSAGE:New( "Missile tracking OFF", 15, "Menu" ):ToAll() + end + + return self +end + +--- Increases, decreases the missile tracking message display frequency with the provided time interval in seconds. +-- The default frequency is a 3 second interval, so the Tracking Frequency parameter specifies the increase or decrease from the default 3 seconds or the last frequency update. +-- @param #MISSILETRAINER self +-- @param #number TrackingFrequency Provide a negative or positive value in seconds to incraese or decrease the display frequency. +-- @return #MISSILETRAINER self +function MISSILETRAINER:InitTrackingFrequency( TrackingFrequency ) + self:F( TrackingFrequency ) + + self.TrackingFrequency = self.TrackingFrequency + TrackingFrequency + if self.TrackingFrequency < 0.5 then + self.TrackingFrequency = 0.5 + end + if self.TrackingFrequency then + MESSAGE:New( "Missile tracking frequency is " .. self.TrackingFrequency .. " seconds.", 15, "Menu" ):ToAll() + end + + return self +end + +--- Sets by default the display of alerts to be shown to all players or only to you. +-- @param #MISSILETRAINER self +-- @param #boolean AlertsToAll true or false +-- @return #MISSILETRAINER self +function MISSILETRAINER:InitAlertsToAll( AlertsToAll ) + self:F( AlertsToAll ) + + self.AlertsToAll = AlertsToAll + if self.AlertsToAll == true then + MESSAGE:New( "Alerts to all players ON", 15, "Menu" ):ToAll() + else + MESSAGE:New( "Alerts to all players OFF", 15, "Menu" ):ToAll() + end + + return self +end + +--- Sets by default the display of hit alerts ON or OFF. +-- @param #MISSILETRAINER self +-- @param #boolean AlertsHitsOnOff true or false +-- @return #MISSILETRAINER self +function MISSILETRAINER:InitAlertsHitsOnOff( AlertsHitsOnOff ) + self:F( AlertsHitsOnOff ) + + self.AlertsHitsOnOff = AlertsHitsOnOff + if self.AlertsHitsOnOff == true then + MESSAGE:New( "Alerts Hits ON", 15, "Menu" ):ToAll() + else + MESSAGE:New( "Alerts Hits OFF", 15, "Menu" ):ToAll() + end + + return self +end + +--- Sets by default the display of launch alerts ON or OFF. +-- @param #MISSILETRAINER self +-- @param #boolean AlertsLaunchesOnOff true or false +-- @return #MISSILETRAINER self +function MISSILETRAINER:InitAlertsLaunchesOnOff( AlertsLaunchesOnOff ) + self:F( AlertsLaunchesOnOff ) + + self.AlertsLaunchesOnOff = AlertsLaunchesOnOff + if self.AlertsLaunchesOnOff == true then + MESSAGE:New( "Alerts Launches ON", 15, "Menu" ):ToAll() + else + MESSAGE:New( "Alerts Launches OFF", 15, "Menu" ):ToAll() + end + + return self +end + +--- Sets by default the display of range information of missiles ON of OFF. +-- @param #MISSILETRAINER self +-- @param #boolean DetailsRangeOnOff true or false +-- @return #MISSILETRAINER self +function MISSILETRAINER:InitRangeOnOff( DetailsRangeOnOff ) + self:F( DetailsRangeOnOff ) + + self.DetailsRangeOnOff = DetailsRangeOnOff + if self.DetailsRangeOnOff == true then + MESSAGE:New( "Range display ON", 15, "Menu" ):ToAll() + else + MESSAGE:New( "Range display OFF", 15, "Menu" ):ToAll() + end + + return self +end + +--- Sets by default the display of bearing information of missiles ON of OFF. +-- @param #MISSILETRAINER self +-- @param #boolean DetailsBearingOnOff true or false +-- @return #MISSILETRAINER self +function MISSILETRAINER:InitBearingOnOff( DetailsBearingOnOff ) + self:F( DetailsBearingOnOff ) + + self.DetailsBearingOnOff = DetailsBearingOnOff + if self.DetailsBearingOnOff == true then + MESSAGE:New( "Bearing display OFF", 15, "Menu" ):ToAll() + else + MESSAGE:New( "Bearing display OFF", 15, "Menu" ):ToAll() + end + + return self +end + +--- Enables / Disables the menus. +-- @param #MISSILETRAINER self +-- @param #boolean MenusOnOff true or false +-- @return #MISSILETRAINER self +function MISSILETRAINER:InitMenusOnOff( MenusOnOff ) + self:F( MenusOnOff ) + + self.MenusOnOff = MenusOnOff + if self.MenusOnOff == true then + MESSAGE:New( "Menus are ENABLED (only when a player rejoins a slot)", 15, "Menu" ):ToAll() + else + MESSAGE:New( "Menus are DISABLED", 15, "Menu" ):ToAll() + end + + return self +end + + +-- Menu functions + +function MISSILETRAINER._MenuMessages( MenuParameters ) + + local self = MenuParameters.MenuSelf + + if MenuParameters.MessagesOnOff ~= nil then + self:InitMessagesOnOff( MenuParameters.MessagesOnOff ) + end + + if MenuParameters.TrackingToAll ~= nil then + self:InitTrackingToAll( MenuParameters.TrackingToAll ) + end + + if MenuParameters.TrackingOnOff ~= nil then + self:InitTrackingOnOff( MenuParameters.TrackingOnOff ) + end + + if MenuParameters.TrackingFrequency ~= nil then + self:InitTrackingFrequency( MenuParameters.TrackingFrequency ) + end + + if MenuParameters.AlertsToAll ~= nil then + self:InitAlertsToAll( MenuParameters.AlertsToAll ) + end + + if MenuParameters.AlertsHitsOnOff ~= nil then + self:InitAlertsHitsOnOff( MenuParameters.AlertsHitsOnOff ) + end + + if MenuParameters.AlertsLaunchesOnOff ~= nil then + self:InitAlertsLaunchesOnOff( MenuParameters.AlertsLaunchesOnOff ) + end + + if MenuParameters.DetailsRangeOnOff ~= nil then + self:InitRangeOnOff( MenuParameters.DetailsRangeOnOff ) + end + + if MenuParameters.DetailsBearingOnOff ~= nil then + self:InitBearingOnOff( MenuParameters.DetailsBearingOnOff ) + end + + if MenuParameters.Distance ~= nil then + self.Distance = MenuParameters.Distance + MESSAGE:New( "Hit detection distance set to " .. self.Distance .. " meters", 15, "Menu" ):ToAll() + end + +end + +--- Detects if an SA site was shot with an anti radiation missile. In this case, take evasive actions based on the skill level set within the ME. +-- @param #MISSILETRAINER self +-- @param Core.Event#EVENTDATA 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 + if TrainerTargetDCSUnit then + local TrainerTargetDCSUnitName = Unit.getName( TrainerTargetDCSUnit ) + local TrainerTargetSkill = _DATABASE.Templates.Units[TrainerTargetDCSUnitName].Template.skill + + self:T(TrainerTargetDCSUnitName ) + + local Client = self.DBClients:FindClient( TrainerTargetDCSUnitName ) + if Client then + + local TrainerSourceUnit = UNIT:Find( TrainerSourceDCSUnit ) + local TrainerTargetUnit = UNIT:Find( TrainerTargetDCSUnit ) + + if self.MessagesOnOff == true and self.AlertsLaunchesOnOff == true then + + local Message = MESSAGE:New( + string.format( "%s launched a %s", + TrainerSourceUnit:GetTypeName(), + TrainerWeaponName + ) .. self:_AddRange( Client, TrainerWeapon ) .. self:_AddBearing( Client, TrainerWeapon ), 5, "Launch Alert" ) + + if self.AlertsToAll then + Message:ToAll() + else + Message:ToClient( Client ) + end + end + + local ClientID = Client:GetID() + self:T( ClientID ) + local MissileData = {} + MissileData.TrainerSourceUnit = TrainerSourceUnit + MissileData.TrainerWeapon = TrainerWeapon + MissileData.TrainerTargetUnit = TrainerTargetUnit + MissileData.TrainerWeaponTypeName = TrainerWeapon:getTypeName() + MissileData.TrainerWeaponLaunched = true + table.insert( self.TrackingMissiles[ClientID].MissileData, MissileData ) + --self:T( self.TrackingMissiles ) + end + else + -- TODO: some weapons don't know the target unit... Need to develop a workaround for this. + if ( TrainerWeapon:getTypeName() == "9M311" ) then + SCHEDULER:New( TrainerWeapon, TrainerWeapon.destroy, {}, 1 ) + else + end + end +end + +function MISSILETRAINER:_AddRange( Client, TrainerWeapon ) + + local RangeText = "" + + if self.DetailsRangeOnOff then + + local PositionMissile = TrainerWeapon:getPoint() + local TargetVec3 = Client:GetVec3() + + local Range = ( ( PositionMissile.x - TargetVec3.x )^2 + + ( PositionMissile.y - TargetVec3.y )^2 + + ( PositionMissile.z - TargetVec3.z )^2 + ) ^ 0.5 / 1000 + + RangeText = string.format( ", at %4.2fkm", Range ) + end + + return RangeText +end + +function MISSILETRAINER:_AddBearing( Client, TrainerWeapon ) + + local BearingText = "" + + if self.DetailsBearingOnOff then + + local PositionMissile = TrainerWeapon:getPoint() + local TargetVec3 = Client:GetVec3() + + self:T2( { TargetVec3, PositionMissile }) + + local DirectionVector = { x = PositionMissile.x - TargetVec3.x, y = PositionMissile.y - TargetVec3.y, z = PositionMissile.z - TargetVec3.z } + local DirectionRadians = math.atan2( DirectionVector.z, DirectionVector.x ) + --DirectionRadians = DirectionRadians + routines.getNorthCorrection( PositionTarget ) + if DirectionRadians < 0 then + DirectionRadians = DirectionRadians + 2 * math.pi + end + local DirectionDegrees = DirectionRadians * 180 / math.pi + + BearingText = string.format( ", %d degrees", DirectionDegrees ) + end + + return BearingText +end + + +function MISSILETRAINER:_TrackMissiles() + self:F2() + + + local ShowMessages = false + if self.MessagesOnOff and self.MessageLastTime + self.TrackingFrequency <= timer.getTime() then + self.MessageLastTime = timer.getTime() + ShowMessages = true + end + + -- ALERTS PART + + -- Loop for all Player Clients to check the alerts and deletion of missiles. + for ClientDataID, ClientData in pairs( self.TrackingMissiles ) do + + local Client = ClientData.Client + 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 TargetVec3 = Client:GetVec3() + + local Distance = ( ( PositionMissile.x - TargetVec3.x )^2 + + ( PositionMissile.y - TargetVec3.y )^2 + + ( PositionMissile.z - TargetVec3.z )^2 + ) ^ 0.5 / 1000 + + if Distance <= self.Distance then + -- Hit alert + TrainerWeapon:destroy() + if self.MessagesOnOff == true and self.AlertsHitsOnOff == true then + + self:T( "killed" ) + + local Message = MESSAGE:New( + string.format( "%s launched by %s killed %s", + TrainerWeapon:getTypeName(), + TrainerSourceUnit:GetTypeName(), + TrainerTargetUnit:GetPlayerName() + ), 15, "Hit Alert" ) + + if self.AlertsToAll == true then + Message:ToAll() + else + Message:ToClient( Client ) + end + + MissileData = nil + table.remove( ClientData.MissileData, MissileDataID ) + self:T(ClientData.MissileData) + end + end + else + if not ( TrainerWeapon and TrainerWeapon:isExist() ) then + if self.MessagesOnOff == true and self.AlertsLaunchesOnOff == true then + -- Weapon does not exist anymore. Delete from Table + local Message = MESSAGE:New( + string.format( "%s launched by %s self destructed!", + TrainerWeaponTypeName, + TrainerSourceUnit:GetTypeName() + ), 5, "Tracking" ) + + if self.AlertsToAll == true then + Message:ToAll() + else + Message:ToClient( Client ) + end + end + MissileData = nil + table.remove( ClientData.MissileData, MissileDataID ) + self:T( ClientData.MissileData ) + end + end + end + 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 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 +-- +-- 3) @{AirbasePolice#AIRBASEPOLICE_NEVADA} class, extends @{AirbasePolice#AIRBASEPOLICE_BASE} +-- ============================================================================================= +-- All the airbases on the NEVADA 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: +-- * Nellis +-- * McCarran +-- * Creech +-- * Groom Lake +-- +-- ### Contributions: Dutch Baron - Concept & Testing +-- ### Author: FlightControl - Framework Design & Programming +-- +-- @module AirbasePolice + + + + + +--- @type AIRBASEPOLICE_BASE +-- @field Core.Set#SET_CLIENT SetClient +-- @extends Core.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(SMOKECOLOR.White):Flush() + for PointsRunwayID, PointsRunway in pairs( Airbase.PointsRunways ) do + Airbase.ZoneRunways[PointsRunwayID] = ZONE_POLYGON_BASE:New( "Runway " .. PointsRunwayID, PointsRunway ):SmokeZone(SMOKECOLOR.Red):Flush() + end + end + +-- -- Template +-- local TemplateBoundary = GROUP:FindByName( "Template Boundary" ) +-- self.Airbases.Template.ZoneBoundary = ZONE_POLYGON:New( "Template Boundary", TemplateBoundary ):SmokeZone(SMOKECOLOR.White):Flush() +-- +-- local TemplateRunway1 = GROUP:FindByName( "Template Runway 1" ) +-- self.Airbases.Template.ZoneRunways[1] = ZONE_POLYGON:New( "Template Runway 1", TemplateRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + + self.SetClient:ForEachClient( + --- @param Wrapper.Client#CLIENT Client + function( Client ) + Client:SetState( self, "Speeding", false ) + Client:SetState( self, "Warnings", 0) + Client:SetState( self, "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 Wrapper.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 + + -- TODO: GetVelocityKMH function usage + local VelocityVec3 = Client:GetVelocity() + local Velocity = ( VelocityVec3.x ^ 2 + VelocityVec3.y ^ 2 + VelocityVec3.z ^ 2 ) ^ 0.5 -- in meters / sec + local Velocity = Velocity * 3.6 -- now it is in km/h. + -- MESSAGE:New( "Velocity = " .. Velocity, 1 ):ToAll() + 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 <= 3 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 .. " / 3" ) + 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:Destroy() + trigger.action.setUserFlag( "AIRCRAFT_"..Client:GetID(), 100) + Client:SetState( self, "Speeding", false ) + Client:SetState( self, "Warnings", 0 ) + end + + else + Client:Message( "You are speeding on the taxiway, slow down now! 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 Core.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] = { + [1]={["y"]=242140.57142858,["x"]=-6478.8571428583,}, + [2]={["y"]=242188.57142858,["x"]=-6522.0000000011,}, + [3]={["y"]=244124.2857143,["x"]=-4344.0000000011,}, + [4]={["y"]=244068.2857143,["x"]=-4296.5714285726,}, + [5]={["y"]=242140.57142858,["x"]=-6480.0000000011,} + }, + }, + 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,}, + }, + [2] = { + [1]={["y"]=895605.71428572,["x"]=-314724.57142857,}, + [2]={["y"]=897639.71428572,["x"]=-316148,}, + [3]={["y"]=897683.42857143,["x"]=-316087.14285714,}, + [4]={["y"]=895650,["x"]=-314660,}, + [5]={["y"]=895606,["x"]=-314724.85714286,} + }, + }, + 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(SMOKECOLOR.White):Flush() + -- + -- local AnapaVityazevoRunway1 = GROUP:FindByName( "AnapaVityazevo Runway 1" ) + -- self.Airbases.AnapaVityazevo.ZoneRunways[1] = ZONE_POLYGON:New( "AnapaVityazevo Runway 1", AnapaVityazevoRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- Batumi + -- local BatumiBoundary = GROUP:FindByName( "Batumi Boundary" ) + -- self.Airbases.Batumi.ZoneBoundary = ZONE_POLYGON:New( "Batumi Boundary", BatumiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local BatumiRunway1 = GROUP:FindByName( "Batumi Runway 1" ) + -- self.Airbases.Batumi.ZoneRunways[1] = ZONE_POLYGON:New( "Batumi Runway 1", BatumiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- Beslan + -- local BeslanBoundary = GROUP:FindByName( "Beslan Boundary" ) + -- self.Airbases.Beslan.ZoneBoundary = ZONE_POLYGON:New( "Beslan Boundary", BeslanBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local BeslanRunway1 = GROUP:FindByName( "Beslan Runway 1" ) + -- self.Airbases.Beslan.ZoneRunways[1] = ZONE_POLYGON:New( "Beslan Runway 1", BeslanRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- Gelendzhik + -- local GelendzhikBoundary = GROUP:FindByName( "Gelendzhik Boundary" ) + -- self.Airbases.Gelendzhik.ZoneBoundary = ZONE_POLYGON:New( "Gelendzhik Boundary", GelendzhikBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local GelendzhikRunway1 = GROUP:FindByName( "Gelendzhik Runway 1" ) + -- self.Airbases.Gelendzhik.ZoneRunways[1] = ZONE_POLYGON:New( "Gelendzhik Runway 1", GelendzhikRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- Gudauta + -- local GudautaBoundary = GROUP:FindByName( "Gudauta Boundary" ) + -- self.Airbases.Gudauta.ZoneBoundary = ZONE_POLYGON:New( "Gudauta Boundary", GudautaBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local GudautaRunway1 = GROUP:FindByName( "Gudauta Runway 1" ) + -- self.Airbases.Gudauta.ZoneRunways[1] = ZONE_POLYGON:New( "Gudauta Runway 1", GudautaRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- Kobuleti + -- local KobuletiBoundary = GROUP:FindByName( "Kobuleti Boundary" ) + -- self.Airbases.Kobuleti.ZoneBoundary = ZONE_POLYGON:New( "Kobuleti Boundary", KobuletiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local KobuletiRunway1 = GROUP:FindByName( "Kobuleti Runway 1" ) + -- self.Airbases.Kobuleti.ZoneRunways[1] = ZONE_POLYGON:New( "Kobuleti Runway 1", KobuletiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- KrasnodarCenter + -- local KrasnodarCenterBoundary = GROUP:FindByName( "KrasnodarCenter Boundary" ) + -- self.Airbases.KrasnodarCenter.ZoneBoundary = ZONE_POLYGON:New( "KrasnodarCenter Boundary", KrasnodarCenterBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local KrasnodarCenterRunway1 = GROUP:FindByName( "KrasnodarCenter Runway 1" ) + -- self.Airbases.KrasnodarCenter.ZoneRunways[1] = ZONE_POLYGON:New( "KrasnodarCenter Runway 1", KrasnodarCenterRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- KrasnodarPashkovsky + -- local KrasnodarPashkovskyBoundary = GROUP:FindByName( "KrasnodarPashkovsky Boundary" ) + -- self.Airbases.KrasnodarPashkovsky.ZoneBoundary = ZONE_POLYGON:New( "KrasnodarPashkovsky Boundary", KrasnodarPashkovskyBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local KrasnodarPashkovskyRunway1 = GROUP:FindByName( "KrasnodarPashkovsky Runway 1" ) + -- self.Airbases.KrasnodarPashkovsky.ZoneRunways[1] = ZONE_POLYGON:New( "KrasnodarPashkovsky Runway 1", KrasnodarPashkovskyRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- local KrasnodarPashkovskyRunway2 = GROUP:FindByName( "KrasnodarPashkovsky Runway 2" ) + -- self.Airbases.KrasnodarPashkovsky.ZoneRunways[2] = ZONE_POLYGON:New( "KrasnodarPashkovsky Runway 2", KrasnodarPashkovskyRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- Krymsk + -- local KrymskBoundary = GROUP:FindByName( "Krymsk Boundary" ) + -- self.Airbases.Krymsk.ZoneBoundary = ZONE_POLYGON:New( "Krymsk Boundary", KrymskBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local KrymskRunway1 = GROUP:FindByName( "Krymsk Runway 1" ) + -- self.Airbases.Krymsk.ZoneRunways[1] = ZONE_POLYGON:New( "Krymsk Runway 1", KrymskRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- Kutaisi + -- local KutaisiBoundary = GROUP:FindByName( "Kutaisi Boundary" ) + -- self.Airbases.Kutaisi.ZoneBoundary = ZONE_POLYGON:New( "Kutaisi Boundary", KutaisiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local KutaisiRunway1 = GROUP:FindByName( "Kutaisi Runway 1" ) + -- self.Airbases.Kutaisi.ZoneRunways[1] = ZONE_POLYGON:New( "Kutaisi Runway 1", KutaisiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- MaykopKhanskaya + -- local MaykopKhanskayaBoundary = GROUP:FindByName( "MaykopKhanskaya Boundary" ) + -- self.Airbases.MaykopKhanskaya.ZoneBoundary = ZONE_POLYGON:New( "MaykopKhanskaya Boundary", MaykopKhanskayaBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local MaykopKhanskayaRunway1 = GROUP:FindByName( "MaykopKhanskaya Runway 1" ) + -- self.Airbases.MaykopKhanskaya.ZoneRunways[1] = ZONE_POLYGON:New( "MaykopKhanskaya Runway 1", MaykopKhanskayaRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- MineralnyeVody + -- local MineralnyeVodyBoundary = GROUP:FindByName( "MineralnyeVody Boundary" ) + -- self.Airbases.MineralnyeVody.ZoneBoundary = ZONE_POLYGON:New( "MineralnyeVody Boundary", MineralnyeVodyBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local MineralnyeVodyRunway1 = GROUP:FindByName( "MineralnyeVody Runway 1" ) + -- self.Airbases.MineralnyeVody.ZoneRunways[1] = ZONE_POLYGON:New( "MineralnyeVody Runway 1", MineralnyeVodyRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- Mozdok + -- local MozdokBoundary = GROUP:FindByName( "Mozdok Boundary" ) + -- self.Airbases.Mozdok.ZoneBoundary = ZONE_POLYGON:New( "Mozdok Boundary", MozdokBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local MozdokRunway1 = GROUP:FindByName( "Mozdok Runway 1" ) + -- self.Airbases.Mozdok.ZoneRunways[1] = ZONE_POLYGON:New( "Mozdok Runway 1", MozdokRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- Nalchik + -- local NalchikBoundary = GROUP:FindByName( "Nalchik Boundary" ) + -- self.Airbases.Nalchik.ZoneBoundary = ZONE_POLYGON:New( "Nalchik Boundary", NalchikBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local NalchikRunway1 = GROUP:FindByName( "Nalchik Runway 1" ) + -- self.Airbases.Nalchik.ZoneRunways[1] = ZONE_POLYGON:New( "Nalchik Runway 1", NalchikRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- Novorossiysk + -- local NovorossiyskBoundary = GROUP:FindByName( "Novorossiysk Boundary" ) + -- self.Airbases.Novorossiysk.ZoneBoundary = ZONE_POLYGON:New( "Novorossiysk Boundary", NovorossiyskBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local NovorossiyskRunway1 = GROUP:FindByName( "Novorossiysk Runway 1" ) + -- self.Airbases.Novorossiysk.ZoneRunways[1] = ZONE_POLYGON:New( "Novorossiysk Runway 1", NovorossiyskRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- SenakiKolkhi + -- local SenakiKolkhiBoundary = GROUP:FindByName( "SenakiKolkhi Boundary" ) + -- self.Airbases.SenakiKolkhi.ZoneBoundary = ZONE_POLYGON:New( "SenakiKolkhi Boundary", SenakiKolkhiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local SenakiKolkhiRunway1 = GROUP:FindByName( "SenakiKolkhi Runway 1" ) + -- self.Airbases.SenakiKolkhi.ZoneRunways[1] = ZONE_POLYGON:New( "SenakiKolkhi Runway 1", SenakiKolkhiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- SochiAdler + -- local SochiAdlerBoundary = GROUP:FindByName( "SochiAdler Boundary" ) + -- self.Airbases.SochiAdler.ZoneBoundary = ZONE_POLYGON:New( "SochiAdler Boundary", SochiAdlerBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local SochiAdlerRunway1 = GROUP:FindByName( "SochiAdler Runway 1" ) + -- self.Airbases.SochiAdler.ZoneRunways[1] = ZONE_POLYGON:New( "SochiAdler Runway 1", SochiAdlerRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- local SochiAdlerRunway2 = GROUP:FindByName( "SochiAdler Runway 2" ) + -- self.Airbases.SochiAdler.ZoneRunways[2] = ZONE_POLYGON:New( "SochiAdler Runway 2", SochiAdlerRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- Soganlug + -- local SoganlugBoundary = GROUP:FindByName( "Soganlug Boundary" ) + -- self.Airbases.Soganlug.ZoneBoundary = ZONE_POLYGON:New( "Soganlug Boundary", SoganlugBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local SoganlugRunway1 = GROUP:FindByName( "Soganlug Runway 1" ) + -- self.Airbases.Soganlug.ZoneRunways[1] = ZONE_POLYGON:New( "Soganlug Runway 1", SoganlugRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- SukhumiBabushara + -- local SukhumiBabusharaBoundary = GROUP:FindByName( "SukhumiBabushara Boundary" ) + -- self.Airbases.SukhumiBabushara.ZoneBoundary = ZONE_POLYGON:New( "SukhumiBabushara Boundary", SukhumiBabusharaBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local SukhumiBabusharaRunway1 = GROUP:FindByName( "SukhumiBabushara Runway 1" ) + -- self.Airbases.SukhumiBabushara.ZoneRunways[1] = ZONE_POLYGON:New( "SukhumiBabushara Runway 1", SukhumiBabusharaRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- TbilisiLochini + -- local TbilisiLochiniBoundary = GROUP:FindByName( "TbilisiLochini Boundary" ) + -- self.Airbases.TbilisiLochini.ZoneBoundary = ZONE_POLYGON:New( "TbilisiLochini Boundary", TbilisiLochiniBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local TbilisiLochiniRunway1 = GROUP:FindByName( "TbilisiLochini Runway 1" ) + -- self.Airbases.TbilisiLochini.ZoneRunways[1] = ZONE_POLYGON:New( "TbilisiLochini Runway 1", TbilisiLochiniRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- local TbilisiLochiniRunway2 = GROUP:FindByName( "TbilisiLochini Runway 2" ) + -- self.Airbases.TbilisiLochini.ZoneRunways[2] = ZONE_POLYGON:New( "TbilisiLochini Runway 2", TbilisiLochiniRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + -- -- Vaziani + -- local VazianiBoundary = GROUP:FindByName( "Vaziani Boundary" ) + -- self.Airbases.Vaziani.ZoneBoundary = ZONE_POLYGON:New( "Vaziani Boundary", VazianiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local VazianiRunway1 = GROUP:FindByName( "Vaziani Runway 1" ) + -- self.Airbases.Vaziani.ZoneRunways[1] = ZONE_POLYGON:New( "Vaziani Runway 1", VazianiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + -- + -- + -- + + + -- Template + -- local TemplateBoundary = GROUP:FindByName( "Template Boundary" ) + -- self.Airbases.Template.ZoneBoundary = ZONE_POLYGON:New( "Template Boundary", TemplateBoundary ):SmokeZone(SMOKECOLOR.White):Flush() + -- + -- local TemplateRunway1 = GROUP:FindByName( "Template Runway 1" ) + -- self.Airbases.Template.ZoneRunways[1] = ZONE_POLYGON:New( "Template Runway 1", TemplateRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + + return self + +end + + + + +--- @type AIRBASEPOLICE_NEVADA +-- @extends Functional.AirbasePolice#AIRBASEPOLICE_BASE +AIRBASEPOLICE_NEVADA = { + ClassName = "AIRBASEPOLICE_NEVADA", + Airbases = { + Nellis = { + PointsBoundary = { + [1]={["y"]=-17814.714285714,["x"]=-399823.14285714,}, + [2]={["y"]=-16875.857142857,["x"]=-398763.14285714,}, + [3]={["y"]=-16251.571428571,["x"]=-398988.85714286,}, + [4]={["y"]=-16163,["x"]=-398693.14285714,}, + [5]={["y"]=-16328.714285714,["x"]=-398034.57142857,}, + [6]={["y"]=-15943,["x"]=-397571.71428571,}, + [7]={["y"]=-15711.571428571,["x"]=-397551.71428571,}, + [8]={["y"]=-15748.714285714,["x"]=-396806,}, + [9]={["y"]=-16288.714285714,["x"]=-396517.42857143,}, + [10]={["y"]=-16751.571428571,["x"]=-396308.85714286,}, + [11]={["y"]=-17263,["x"]=-396234.57142857,}, + [12]={["y"]=-17577.285714286,["x"]=-396640.28571429,}, + [13]={["y"]=-17614.428571429,["x"]=-397400.28571429,}, + [14]={["y"]=-19405.857142857,["x"]=-399428.85714286,}, + [15]={["y"]=-19234.428571429,["x"]=-399683.14285714,}, + [16]={["y"]=-18708.714285714,["x"]=-399408.85714286,}, + [17]={["y"]=-18397.285714286,["x"]=-399657.42857143,}, + [18]={["y"]=-17814.428571429,["x"]=-399823.42857143,}, + }, + PointsRunways = { + [1] = { + [1]={["y"]=-18687,["x"]=-399380.28571429,}, + [2]={["y"]=-18620.714285714,["x"]=-399436.85714286,}, + [3]={["y"]=-16217.857142857,["x"]=-396596.85714286,}, + [4]={["y"]=-16300.142857143,["x"]=-396530,}, + [5]={["y"]=-18687,["x"]=-399380.85714286,}, + }, + [2] = { + [1]={["y"]=-18451.571428572,["x"]=-399580.57142857,}, + [2]={["y"]=-18392.142857143,["x"]=-399628.57142857,}, + [3]={["y"]=-16011,["x"]=-396806.85714286,}, + [4]={["y"]=-16074.714285714,["x"]=-396751.71428572,}, + [5]={["y"]=-18451.571428572,["x"]=-399580.85714285,}, + }, + }, + ZoneBoundary = {}, + ZoneRunways = {}, + MaximumSpeed = 50, + }, + McCarran = { + PointsBoundary = { + [1]={["y"]=-29455.285714286,["x"]=-416277.42857142,}, + [2]={["y"]=-28860.142857143,["x"]=-416492,}, + [3]={["y"]=-25044.428571429,["x"]=-416344.85714285,}, + [4]={["y"]=-24580.142857143,["x"]=-415959.14285714,}, + [5]={["y"]=-25073,["x"]=-415630.57142857,}, + [6]={["y"]=-25087.285714286,["x"]=-415130.57142857,}, + [7]={["y"]=-25830.142857143,["x"]=-414866.28571428,}, + [8]={["y"]=-26658.714285715,["x"]=-414880.57142857,}, + [9]={["y"]=-26973,["x"]=-415273.42857142,}, + [10]={["y"]=-27380.142857143,["x"]=-415187.71428571,}, + [11]={["y"]=-27715.857142857,["x"]=-414144.85714285,}, + [12]={["y"]=-27551.571428572,["x"]=-413473.42857142,}, + [13]={["y"]=-28630.142857143,["x"]=-413201.99999999,}, + [14]={["y"]=-29494.428571429,["x"]=-415437.71428571,}, + [15]={["y"]=-29455.571428572,["x"]=-416277.71428571,}, + }, + PointsRunways = { + [1] = { + [1]={["y"]=-29408.428571429,["x"]=-416016.28571428,}, + [2]={["y"]=-29408.142857144,["x"]=-416105.42857142,}, + [3]={["y"]=-24680.714285715,["x"]=-416003.14285713,}, + [4]={["y"]=-24681.857142858,["x"]=-415926.57142856,}, + [5]={["y"]=-29408.42857143,["x"]=-416016.57142856,}, + }, + [2] = { + [1]={["y"]=-28575.571428572,["x"]=-416303.14285713,}, + [2]={["y"]=-28575.571428572,["x"]=-416382.57142856,}, + [3]={["y"]=-25111.000000001,["x"]=-416309.7142857,}, + [4]={["y"]=-25111.000000001,["x"]=-416249.14285713,}, + [5]={["y"]=-28575.571428572,["x"]=-416303.7142857,}, + }, + [3] = { + [1]={["y"]=-29331.000000001,["x"]=-416275.42857141,}, + [2]={["y"]=-29259.000000001,["x"]=-416306.85714284,}, + [3]={["y"]=-28005.571428572,["x"]=-413449.7142857,}, + [4]={["y"]=-28068.714285715,["x"]=-413422.85714284,}, + [5]={["y"]=-29331.000000001,["x"]=-416275.7142857,}, + }, + [4] = { + [1]={["y"]=-29073.285714286,["x"]=-416386.57142856,}, + [2]={["y"]=-28997.285714286,["x"]=-416417.42857141,}, + [3]={["y"]=-27697.571428572,["x"]=-413464.57142856,}, + [4]={["y"]=-27767.857142858,["x"]=-413434.28571427,}, + [5]={["y"]=-29073.000000001,["x"]=-416386.85714284,}, + }, + }, + ZoneBoundary = {}, + ZoneRunways = {}, + MaximumSpeed = 50, + }, + Creech = { + PointsBoundary = { + [1]={["y"]=-74522.714285715,["x"]=-360887.99999998,}, + [2]={["y"]=-74197,["x"]=-360556.57142855,}, + [3]={["y"]=-74402.714285715,["x"]=-359639.42857141,}, + [4]={["y"]=-74637,["x"]=-359279.42857141,}, + [5]={["y"]=-75759.857142857,["x"]=-359005.14285712,}, + [6]={["y"]=-75834.142857143,["x"]=-359045.14285712,}, + [7]={["y"]=-75902.714285714,["x"]=-359782.28571427,}, + [8]={["y"]=-76099.857142857,["x"]=-360399.42857141,}, + [9]={["y"]=-77314.142857143,["x"]=-360219.42857141,}, + [10]={["y"]=-77728.428571429,["x"]=-360445.14285713,}, + [11]={["y"]=-77585.571428571,["x"]=-360585.14285713,}, + [12]={["y"]=-76471.285714286,["x"]=-360819.42857141,}, + [13]={["y"]=-76325.571428571,["x"]=-360942.28571427,}, + [14]={["y"]=-74671.857142857,["x"]=-360927.7142857,}, + [15]={["y"]=-74522.714285714,["x"]=-360888.85714284,}, + }, + PointsRunways = { + [1] = { + [1]={["y"]=-74237.571428571,["x"]=-360591.7142857,}, + [2]={["y"]=-74234.428571429,["x"]=-360493.71428571,}, + [3]={["y"]=-77605.285714286,["x"]=-360399.14285713,}, + [4]={["y"]=-77608.714285715,["x"]=-360498.85714285,}, + [5]={["y"]=-74237.857142857,["x"]=-360591.7142857,}, + }, + [2] = { + [1]={["y"]=-75807.571428572,["x"]=-359073.42857142,}, + [2]={["y"]=-74770.142857144,["x"]=-360581.71428571,}, + [3]={["y"]=-74641.285714287,["x"]=-360585.42857142,}, + [4]={["y"]=-75734.142857144,["x"]=-359023.14285714,}, + [5]={["y"]=-75807.285714287,["x"]=-359073.42857142,}, + }, + }, + ZoneBoundary = {}, + ZoneRunways = {}, + MaximumSpeed = 50, + }, + GroomLake = { + PointsBoundary = { + [1]={["y"]=-88916.714285714,["x"]=-289102.28571425,}, + [2]={["y"]=-87023.571428572,["x"]=-290388.57142857,}, + [3]={["y"]=-85916.428571429,["x"]=-290674.28571428,}, + [4]={["y"]=-87645.000000001,["x"]=-286567.14285714,}, + [5]={["y"]=-88380.714285715,["x"]=-286388.57142857,}, + [6]={["y"]=-89670.714285715,["x"]=-283524.28571428,}, + [7]={["y"]=-89797.857142858,["x"]=-283567.14285714,}, + [8]={["y"]=-88635.000000001,["x"]=-286749.99999999,}, + [9]={["y"]=-89177.857142858,["x"]=-287207.14285714,}, + [10]={["y"]=-89092.142857144,["x"]=-288892.85714285,}, + [11]={["y"]=-88917.000000001,["x"]=-289102.85714285,}, + }, + PointsRunways = { + [1] = { + [1]={["y"]=-86039.000000001,["x"]=-290606.28571428,}, + [2]={["y"]=-85965.285714287,["x"]=-290573.99999999,}, + [3]={["y"]=-87692.714285715,["x"]=-286634.85714285,}, + [4]={["y"]=-87756.714285715,["x"]=-286663.99999999,}, + [5]={["y"]=-86038.714285715,["x"]=-290606.85714285,}, + }, + [2] = { + [1]={["y"]=-86808.428571429,["x"]=-290375.7142857,}, + [2]={["y"]=-86732.714285715,["x"]=-290344.28571427,}, + [3]={["y"]=-89672.714285714,["x"]=-283546.57142855,}, + [4]={["y"]=-89772.142857143,["x"]=-283587.71428569,}, + [5]={["y"]=-86808.142857143,["x"]=-290375.7142857,}, + }, + }, + ZoneBoundary = {}, + ZoneRunways = {}, + MaximumSpeed = 50, + }, + }, +} + +--- Creates a new AIRBASEPOLICE_NEVADA object. +-- @param #AIRBASEPOLICE_NEVADA 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_NEVADA self +function AIRBASEPOLICE_NEVADA:New( SetClient ) + + -- Inherits from BASE + local self = BASE:Inherit( self, AIRBASEPOLICE_BASE:New( SetClient, self.Airbases ) ) + +-- -- Nellis +-- local NellisBoundary = GROUP:FindByName( "Nellis Boundary" ) +-- self.Airbases.Nellis.ZoneBoundary = ZONE_POLYGON:New( "Nellis Boundary", NellisBoundary ):SmokeZone(SMOKECOLOR.White):Flush() +-- +-- local NellisRunway1 = GROUP:FindByName( "Nellis Runway 1" ) +-- self.Airbases.Nellis.ZoneRunways[1] = ZONE_POLYGON:New( "Nellis Runway 1", NellisRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() +-- +-- local NellisRunway2 = GROUP:FindByName( "Nellis Runway 2" ) +-- self.Airbases.Nellis.ZoneRunways[2] = ZONE_POLYGON:New( "Nellis Runway 2", NellisRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() +-- +-- -- McCarran +-- local McCarranBoundary = GROUP:FindByName( "McCarran Boundary" ) +-- self.Airbases.McCarran.ZoneBoundary = ZONE_POLYGON:New( "McCarran Boundary", McCarranBoundary ):SmokeZone(SMOKECOLOR.White):Flush() +-- +-- local McCarranRunway1 = GROUP:FindByName( "McCarran Runway 1" ) +-- self.Airbases.McCarran.ZoneRunways[1] = ZONE_POLYGON:New( "McCarran Runway 1", McCarranRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() +-- +-- local McCarranRunway2 = GROUP:FindByName( "McCarran Runway 2" ) +-- self.Airbases.McCarran.ZoneRunways[2] = ZONE_POLYGON:New( "McCarran Runway 2", McCarranRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() +-- +-- local McCarranRunway3 = GROUP:FindByName( "McCarran Runway 3" ) +-- self.Airbases.McCarran.ZoneRunways[3] = ZONE_POLYGON:New( "McCarran Runway 3", McCarranRunway3 ):SmokeZone(SMOKECOLOR.Red):Flush() +-- +-- local McCarranRunway4 = GROUP:FindByName( "McCarran Runway 4" ) +-- self.Airbases.McCarran.ZoneRunways[4] = ZONE_POLYGON:New( "McCarran Runway 4", McCarranRunway4 ):SmokeZone(SMOKECOLOR.Red):Flush() +-- +-- -- Creech +-- local CreechBoundary = GROUP:FindByName( "Creech Boundary" ) +-- self.Airbases.Creech.ZoneBoundary = ZONE_POLYGON:New( "Creech Boundary", CreechBoundary ):SmokeZone(SMOKECOLOR.White):Flush() +-- +-- local CreechRunway1 = GROUP:FindByName( "Creech Runway 1" ) +-- self.Airbases.Creech.ZoneRunways[1] = ZONE_POLYGON:New( "Creech Runway 1", CreechRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() +-- +-- local CreechRunway2 = GROUP:FindByName( "Creech Runway 2" ) +-- self.Airbases.Creech.ZoneRunways[2] = ZONE_POLYGON:New( "Creech Runway 2", CreechRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() +-- +-- -- Groom Lake +-- local GroomLakeBoundary = GROUP:FindByName( "GroomLake Boundary" ) +-- self.Airbases.GroomLake.ZoneBoundary = ZONE_POLYGON:New( "GroomLake Boundary", GroomLakeBoundary ):SmokeZone(SMOKECOLOR.White):Flush() +-- +-- local GroomLakeRunway1 = GROUP:FindByName( "GroomLake Runway 1" ) +-- self.Airbases.GroomLake.ZoneRunways[1] = ZONE_POLYGON:New( "GroomLake Runway 1", GroomLakeRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() +-- +-- local GroomLakeRunway2 = GROUP:FindByName( "GroomLake Runway 2" ) +-- self.Airbases.GroomLake.ZoneRunways[2] = ZONE_POLYGON:New( "GroomLake Runway 2", GroomLakeRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + +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. +-- The @{Detection#DETECTION_BASE} class will detect objects within the battle zone for a list of @{Group}s detecting targets following (a) detection method(s). +-- +-- 1.1) DETECTION_BASE constructor +-- ------------------------------- +-- Construct a new DETECTION_BASE instance using the @{Detection#DETECTION_BASE.New}() method. +-- +-- 1.2) DETECTION_BASE initialization +-- ---------------------------------- +-- By default, detection will return detected objects with all the detection sensors available. +-- However, you can ask how the objects were found with specific detection methods. +-- If you use one of the below methods, the detection will work with the detection method specified. +-- You can specify to apply multiple detection methods. +-- +-- Use the following functions to report the objects it detected using the methods Visual, Optical, Radar, IRST, RWR, DLINK: +-- +-- * @{Detection#DETECTION_BASE.InitDetectVisual}(): Detected using Visual. +-- * @{Detection#DETECTION_BASE.InitDetectOptical}(): Detected using Optical. +-- * @{Detection#DETECTION_BASE.InitDetectRadar}(): Detected using Radar. +-- * @{Detection#DETECTION_BASE.InitDetectIRST}(): Detected using IRST. +-- * @{Detection#DETECTION_BASE.InitDetectRWR}(): Detected using RWR. +-- * @{Detection#DETECTION_BASE.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_AREAS} class, extends @{Detection#DETECTION_BASE} +-- =============================================================================== +-- The @{Detection#DETECTION_AREAS} class will detect units within the battle zone for a list of @{Group}s detecting targets following (a) detection method(s), +-- and will build a list (table) of @{Set#SET_UNIT}s containing the @{Unit#UNIT}s detected. +-- The class is group the detected units within zones given a DetectedZoneRange parameter. +-- A set with multiple detected zones will be created as there are groups of units detected. +-- +-- 2.1) Retrieve the Detected Unit sets and Detected Zones +-- ------------------------------------------------------- +-- The DetectedUnitSets methods are implemented in @{Detection#DECTECTION_BASE} and the DetectedZones methods is implemented in @{Detection#DETECTION_AREAS}. +-- +-- Retrieve the DetectedUnitSets with the method @{Detection#DETECTION_BASE.GetDetectedSets}(). A table will be return of @{Set#SET_UNIT}s. +-- To understand the amount of sets created, use the method @{Detection#DETECTION_BASE.GetDetectedSetCount}(). +-- If you want to obtain a specific set from the DetectedSets, use the method @{Detection#DETECTION_BASE.GetDetectedSet}() with a given index. +-- +-- Retrieve the formed @{Zone@ZONE_UNIT}s as a result of the grouping the detected units within the DetectionZoneRange, use the method @{Detection#DETECTION_BASE.GetDetectionZones}(). +-- To understand the amount of zones created, use the method @{Detection#DETECTION_BASE.GetDetectionZoneCount}(). +-- If you want to obtain a specific zone from the DetectedZones, use the method @{Detection#DETECTION_BASE.GetDetectionZone}() with a given index. +-- +-- 1.4) Flare or Smoke detected units +-- ---------------------------------- +-- Use the methods @{Detection#DETECTION_AREAS.FlareDetectedUnits}() or @{Detection#DETECTION_AREAS.SmokeDetectedUnits}() to flare or smoke the detected units when a new detection has taken place. +-- +-- 1.5) Flare or Smoke detected zones +-- ---------------------------------- +-- Use the methods @{Detection#DETECTION_AREAS.FlareDetectedZones}() or @{Detection#DETECTION_AREAS.SmokeDetectedZones}() to flare or smoke the detected zones when a new detection has taken place. +-- +-- === +-- +-- ### Contributions: +-- +-- * Mechanist : Concept & Testing +-- +-- ### Authors: +-- +-- * FlightControl : Design & Programming +-- +-- @module Detection + + + +--- DETECTION_BASE class +-- @type DETECTION_BASE +-- @field Core.Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Forward Air Controller role. +-- @field Dcs.DCSTypes#Distance DetectionRange The range till which targets are accepted to be detected. +-- @field #DETECTION_BASE.DetectedObjects DetectedObjects The list of detected objects. +-- @field #table DetectedObjectsIdentified Map of the DetectedObjects identified. +-- @field #number DetectionRun +-- @extends Core.Base#BASE +DETECTION_BASE = { + ClassName = "DETECTION_BASE", + DetectionSetGroup = nil, + DetectionRange = nil, + DetectedObjects = {}, + DetectionRun = 0, + DetectedObjectsIdentified = {}, +} + +--- @type DETECTION_BASE.DetectedObjects +-- @list <#DETECTION_BASE.DetectedObject> + +--- @type DETECTION_BASE.DetectedObject +-- @field #string Name +-- @field #boolean Visible +-- @field #string Type +-- @field #number Distance +-- @field #boolean Identified + +--- DETECTION constructor. +-- @param #DETECTION_BASE self +-- @param Core.Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Forward Air Controller role. +-- @param Dcs.DCSTypes#Distance DetectionRange The range till which targets are accepted to be detected. +-- @return #DETECTION_BASE self +function DETECTION_BASE:New( DetectionSetGroup, DetectionRange ) + + -- Inherits from BASE + local self = BASE:Inherit( self, BASE:New() ) + + self.DetectionSetGroup = DetectionSetGroup + self.DetectionRange = DetectionRange + + self:InitDetectVisual( false ) + self:InitDetectOptical( false ) + self:InitDetectRadar( false ) + self:InitDetectRWR( false ) + self:InitDetectIRST( false ) + self:InitDetectDLINK( false ) + + 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 + +--- Determines if a detected object has already been identified during detection processing. +-- @param #DETECTION_BASE self +-- @param #DETECTION_BASE.DetectedObject DetectedObject +-- @return #boolean true if already identified. +function DETECTION_BASE:IsDetectedObjectIdentified( DetectedObject ) + self:F3( DetectedObject.Name ) + + local DetectedObjectName = DetectedObject.Name + local DetectedObjectIdentified = self.DetectedObjectsIdentified[DetectedObjectName] == true + self:T3( DetectedObjectIdentified ) + return DetectedObjectIdentified +end + +--- Identifies a detected object during detection processing. +-- @param #DETECTION_BASE self +-- @param #DETECTION_BASE.DetectedObject DetectedObject +function DETECTION_BASE:IdentifyDetectedObject( DetectedObject ) + self:F( DetectedObject.Name ) + + local DetectedObjectName = DetectedObject.Name + self.DetectedObjectsIdentified[DetectedObjectName] = true +end + +--- UnIdentify a detected object during detection processing. +-- @param #DETECTION_BASE self +-- @param #DETECTION_BASE.DetectedObject DetectedObject +function DETECTION_BASE:UnIdentifyDetectedObject( DetectedObject ) + + local DetectedObjectName = DetectedObject.Name + self.DetectedObjectsIdentified[DetectedObjectName] = false +end + +--- UnIdentify all detected objects during detection processing. +-- @param #DETECTION_BASE self +function DETECTION_BASE:UnIdentifyAllDetectedObjects() + + self.DetectedObjectsIdentified = {} -- Table will be garbage collected. +end + +--- Gets a detected object with a given name. +-- @param #DETECTION_BASE self +-- @param #string ObjectName +-- @return #DETECTION_BASE.DetectedObject +function DETECTION_BASE:GetDetectedObject( ObjectName ) + self:F3( ObjectName ) + + if ObjectName then + local DetectedObject = self.DetectedObjects[ObjectName] + + -- Only return detected objects that are alive! + local DetectedUnit = UNIT:FindByName( ObjectName ) + if DetectedUnit and DetectedUnit:IsAlive() then + if self:IsDetectedObjectIdentified( DetectedObject ) == false then + return DetectedObject + end + end + end + + return nil +end + +--- Get the detected @{Set#SET_BASE}s. +-- @param #DETECTION_BASE self +-- @return #DETECTION_BASE.DetectedSets DetectedSets +function DETECTION_BASE:GetDetectedSets() + + local DetectionSets = self.DetectedSets + return DetectionSets +end + +--- Get the amount of SETs with detected objects. +-- @param #DETECTION_BASE self +-- @return #number Count +function DETECTION_BASE:GetDetectedSetCount() + + local DetectionSetCount = #self.DetectedSets + return DetectionSetCount +end + +--- Get a SET of detected objects using a given numeric index. +-- @param #DETECTION_BASE self +-- @param #number Index +-- @return Core.Set#SET_BASE +function DETECTION_BASE:GetDetectedSet( Index ) + + local DetectionSet = self.DetectedSets[Index] + if DetectionSet then + return DetectionSet + end + + return nil +end + +--- Get the detection Groups. +-- @param #DETECTION_BASE self +-- @return Wrapper.Group#GROUP +function DETECTION_BASE:GetDetectionSetGroup() + + local DetectionSetGroup = self.DetectionSetGroup + return DetectionSetGroup +end + +--- 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 + + +--- Schedule the DETECTION construction. +-- @param #DETECTION_BASE self +-- @param #number DelayTime The delay in seconds to wait the reporting. +-- @param #number RepeatInterval The repeat interval in seconds for the reporting to happen repeatedly. +-- @return #DETECTION_BASE self +function DETECTION_BASE:Schedule( DelayTime, RepeatInterval ) + self:F2() + + self.ScheduleDelayTime = DelayTime + self.ScheduleRepeatInterval = RepeatInterval + + self.DetectionScheduler = SCHEDULER:New( self, self._DetectionScheduler, { self, "Detection" }, DelayTime, RepeatInterval ) + return self +end + + +--- 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.DetectionRun = self.DetectionRun + 1 + + self:UnIdentifyAllDetectedObjects() -- Resets the DetectedObjectsIdentified table + + for DetectionGroupID, DetectionGroupData in pairs( self.DetectionSetGroup:GetSet() ) do + local DetectionGroup = DetectionGroupData -- Wrapper.Group#GROUP + + if DetectionGroup:IsAlive() then + + local DetectionGroupName = DetectionGroup:GetName() + + local DetectionDetectedTargets = DetectionGroup:GetDetectedTargets( + self.DetectVisual, + self.DetectOptical, + self.DetectRadar, + self.DetectIRST, + self.DetectRWR, + self.DetectDLINK + ) + + for DetectionDetectedTargetID, DetectionDetectedTarget in pairs( DetectionDetectedTargets ) do + local DetectionObject = DetectionDetectedTarget.object -- Dcs.DCSWrapper.Object#Object + self:T2( DetectionObject ) + + if DetectionObject and DetectionObject:isExist() and DetectionObject.id_ < 50000000 then + + local DetectionDetectedObjectName = DetectionObject:getName() + + local DetectionDetectedObjectPositionVec3 = DetectionObject:getPoint() + local DetectionGroupVec3 = DetectionGroup:GetVec3() + + local Distance = ( ( DetectionDetectedObjectPositionVec3.x - DetectionGroupVec3.x )^2 + + ( DetectionDetectedObjectPositionVec3.y - DetectionGroupVec3.y )^2 + + ( DetectionDetectedObjectPositionVec3.z - DetectionGroupVec3.z )^2 + ) ^ 0.5 / 1000 + + self:T2( { DetectionGroupName, DetectionDetectedObjectName, Distance } ) + + if Distance <= self.DetectionRange then + + if not self.DetectedObjects[DetectionDetectedObjectName] then + self.DetectedObjects[DetectionDetectedObjectName] = {} + end + self.DetectedObjects[DetectionDetectedObjectName].Name = DetectionDetectedObjectName + self.DetectedObjects[DetectionDetectedObjectName].Visible = DetectionDetectedTarget.visible + self.DetectedObjects[DetectionDetectedObjectName].Type = DetectionDetectedTarget.type + self.DetectedObjects[DetectionDetectedObjectName].Distance = DetectionDetectedTarget.distance + else + -- if beyond the DetectionRange then nullify... + if self.DetectedObjects[DetectionDetectedObjectName] then + self.DetectedObjects[DetectionDetectedObjectName] = nil + end + end + end + end + + self:T2( self.DetectedObjects ) + + -- okay, now we have a list of detected object names ... + -- Sort the table based on distance ... + table.sort( self.DetectedObjects, function( a, b ) return a.Distance < b.Distance end ) + end + end + + if self.DetectedObjects then + self:CreateDetectionSets() + end + + return true +end + + + +--- DETECTION_AREAS class +-- @type DETECTION_AREAS +-- @field Dcs.DCSTypes#Distance DetectionZoneRange The range till which targets are grouped upon the first detected target. +-- @field #DETECTION_AREAS.DetectedAreas DetectedAreas A list of areas containing the set of @{Unit}s, @{Zone}s, the center @{Unit} within the zone, and ID of each area that was detected within a DetectionZoneRange. +-- @extends Functional.Detection#DETECTION_BASE +DETECTION_AREAS = { + ClassName = "DETECTION_AREAS", + DetectedAreas = { n = 0 }, + DetectionZoneRange = nil, +} + +--- @type DETECTION_AREAS.DetectedAreas +-- @list <#DETECTION_AREAS.DetectedArea> + +--- @type DETECTION_AREAS.DetectedArea +-- @field Core.Set#SET_UNIT Set -- The Set of Units in the detected area. +-- @field Core.Zone#ZONE_UNIT Zone -- The Zone of the detected area. +-- @field #boolean Changed Documents if the detected area has changes. +-- @field #table Changes A list of the changes reported on the detected area. (It is up to the user of the detected area to consume those changes). +-- @field #number AreaID -- The identifier of the detected area. +-- @field #boolean FriendliesNearBy Indicates if there are friendlies within the detected area. +-- @field Wrapper.Unit#UNIT NearestFAC The nearest FAC near the Area. + + +--- DETECTION_AREAS constructor. +-- @param Functional.Detection#DETECTION_AREAS self +-- @param Core.Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Forward Air Controller role. +-- @param Dcs.DCSTypes#Distance DetectionRange The range till which targets are accepted to be detected. +-- @param Dcs.DCSTypes#Distance DetectionZoneRange The range till which targets are grouped upon the first detected target. +-- @return Functional.Detection#DETECTION_AREAS self +function DETECTION_AREAS:New( DetectionSetGroup, DetectionRange, DetectionZoneRange ) + + -- Inherits from DETECTION_BASE + local self = BASE:Inherit( self, DETECTION_BASE:New( DetectionSetGroup, DetectionRange ) ) + + self.DetectionZoneRange = DetectionZoneRange + + self._SmokeDetectedUnits = false + self._FlareDetectedUnits = false + self._SmokeDetectedZones = false + self._FlareDetectedZones = false + + self:Schedule( 10, 10 ) + + return self +end + +--- Add a detected @{#DETECTION_AREAS.DetectedArea}. +-- @param Core.Set#SET_UNIT Set -- The Set of Units in the detected area. +-- @param Core.Zone#ZONE_UNIT Zone -- The Zone of the detected area. +-- @return #DETECTION_AREAS.DetectedArea DetectedArea +function DETECTION_AREAS:AddDetectedArea( Set, Zone ) + local DetectedAreas = self:GetDetectedAreas() + DetectedAreas.n = self:GetDetectedAreaCount() + 1 + DetectedAreas[DetectedAreas.n] = {} + local DetectedArea = DetectedAreas[DetectedAreas.n] + DetectedArea.Set = Set + DetectedArea.Zone = Zone + DetectedArea.Removed = false + DetectedArea.AreaID = DetectedAreas.n + + return DetectedArea +end + +--- Remove a detected @{#DETECTION_AREAS.DetectedArea} with a given Index. +-- @param #DETECTION_AREAS self +-- @param #number Index The Index of the detection are to be removed. +-- @return #nil +function DETECTION_AREAS:RemoveDetectedArea( Index ) + local DetectedAreas = self:GetDetectedAreas() + local DetectedAreaCount = self:GetDetectedAreaCount() + local DetectedArea = DetectedAreas[Index] + local DetectedAreaSet = DetectedArea.Set + DetectedArea[Index] = nil + return nil +end + + +--- Get the detected @{#DETECTION_AREAS.DetectedAreas}. +-- @param #DETECTION_AREAS self +-- @return #DETECTION_AREAS.DetectedAreas DetectedAreas +function DETECTION_AREAS:GetDetectedAreas() + + local DetectedAreas = self.DetectedAreas + return DetectedAreas +end + +--- Get the amount of @{#DETECTION_AREAS.DetectedAreas}. +-- @param #DETECTION_AREAS self +-- @return #number DetectedAreaCount +function DETECTION_AREAS:GetDetectedAreaCount() + + local DetectedAreaCount = self.DetectedAreas.n + return DetectedAreaCount +end + +--- Get the @{Set#SET_UNIT} of a detecttion area using a given numeric index. +-- @param #DETECTION_AREAS self +-- @param #number Index +-- @return Core.Set#SET_UNIT DetectedSet +function DETECTION_AREAS:GetDetectedSet( Index ) + + local DetectedSetUnit = self.DetectedAreas[Index].Set + if DetectedSetUnit then + return DetectedSetUnit + end + + return nil +end + +--- Get the @{Zone#ZONE_UNIT} of a detection area using a given numeric index. +-- @param #DETECTION_AREAS self +-- @param #number Index +-- @return Core.Zone#ZONE_UNIT DetectedZone +function DETECTION_AREAS:GetDetectedZone( Index ) + + local DetectedZone = self.DetectedAreas[Index].Zone + if DetectedZone then + return DetectedZone + end + + return nil +end + +--- Background worker function to determine if there are friendlies nearby ... +-- @param #DETECTION_AREAS self +-- @param Wrapper.Unit#UNIT ReportUnit +function DETECTION_AREAS:ReportFriendliesNearBy( ReportGroupData ) + self:F2() + + local DetectedArea = ReportGroupData.DetectedArea -- Functional.Detection#DETECTION_AREAS.DetectedArea + local DetectedSet = ReportGroupData.DetectedArea.Set + local DetectedZone = ReportGroupData.DetectedArea.Zone + local DetectedZoneUnit = DetectedZone.ZoneUNIT + + DetectedArea.FriendliesNearBy = false + + local SphereSearch = { + id = world.VolumeType.SPHERE, + params = { + point = DetectedZoneUnit:GetVec3(), + radius = 6000, + } + + } + + --- @param Dcs.DCSWrapper.Unit#Unit FoundDCSUnit + -- @param Wrapper.Group#GROUP ReportGroup + -- @param Set#SET_GROUP ReportSetGroup + local FindNearByFriendlies = function( FoundDCSUnit, ReportGroupData ) + + local DetectedArea = ReportGroupData.DetectedArea -- Functional.Detection#DETECTION_AREAS.DetectedArea + local DetectedSet = ReportGroupData.DetectedArea.Set + local DetectedZone = ReportGroupData.DetectedArea.Zone + local DetectedZoneUnit = DetectedZone.ZoneUNIT -- Wrapper.Unit#UNIT + local ReportSetGroup = ReportGroupData.ReportSetGroup + + local EnemyCoalition = DetectedZoneUnit:GetCoalition() + + local FoundUnitCoalition = FoundDCSUnit:getCoalition() + local FoundUnitName = FoundDCSUnit:getName() + local FoundUnitGroupName = FoundDCSUnit:getGroup():getName() + local EnemyUnitName = DetectedZoneUnit:GetName() + local FoundUnitInReportSetGroup = ReportSetGroup:FindGroup( FoundUnitGroupName ) ~= nil + + self:T3( { "Friendlies search:", FoundUnitName, FoundUnitCoalition, EnemyUnitName, EnemyCoalition, FoundUnitInReportSetGroup } ) + + if FoundUnitCoalition ~= EnemyCoalition and FoundUnitInReportSetGroup == false then + DetectedArea.FriendliesNearBy = true + return false + end + + return true + end + + world.searchObjects( Object.Category.UNIT, SphereSearch, FindNearByFriendlies, ReportGroupData ) + +end + + + +--- Returns if there are friendlies nearby the FAC units ... +-- @param #DETECTION_AREAS self +-- @return #boolean trhe if there are friendlies nearby +function DETECTION_AREAS:IsFriendliesNearBy( DetectedArea ) + + self:T3( DetectedArea.FriendliesNearBy ) + return DetectedArea.FriendliesNearBy or false +end + +--- Calculate the maxium A2G threat level of the DetectedArea. +-- @param #DETECTION_AREAS self +-- @param #DETECTION_AREAS.DetectedArea DetectedArea +function DETECTION_AREAS:CalculateThreatLevelA2G( DetectedArea ) + + local MaxThreatLevelA2G = 0 + for UnitName, UnitData in pairs( DetectedArea.Set:GetSet() ) do + local ThreatUnit = UnitData -- Wrapper.Unit#UNIT + local ThreatLevelA2G = ThreatUnit:GetThreatLevel() + if ThreatLevelA2G > MaxThreatLevelA2G then + MaxThreatLevelA2G = ThreatLevelA2G + end + end + + self:T3( MaxThreatLevelA2G ) + DetectedArea.MaxThreatLevelA2G = MaxThreatLevelA2G + +end + +--- Find the nearest FAC of the DetectedArea. +-- @param #DETECTION_AREAS self +-- @param #DETECTION_AREAS.DetectedArea DetectedArea +-- @return Wrapper.Unit#UNIT The nearest FAC unit +function DETECTION_AREAS:NearestFAC( DetectedArea ) + + local NearestFAC = nil + local MinDistance = 1000000000 -- Units are not further than 1000000 km away from an area :-) + + for FACGroupName, FACGroupData in pairs( self.DetectionSetGroup:GetSet() ) do + for FACUnit, FACUnitData in pairs( FACGroupData:GetUnits() ) do + local FACUnit = FACUnitData -- Wrapper.Unit#UNIT + if FACUnit:IsActive() then + local Vec3 = FACUnit:GetVec3() + local PointVec3 = POINT_VEC3:NewFromVec3( Vec3 ) + local Distance = PointVec3:Get2DDistance(POINT_VEC3:NewFromVec3( FACUnit:GetVec3() ) ) + if Distance < MinDistance then + MinDistance = Distance + NearestFAC = FACUnit + end + end + end + end + + DetectedArea.NearestFAC = NearestFAC + +end + +--- Returns the A2G threat level of the units in the DetectedArea +-- @param #DETECTION_AREAS self +-- @param #DETECTION_AREAS.DetectedArea DetectedArea +-- @return #number a scale from 0 to 10. +function DETECTION_AREAS:GetTreatLevelA2G( DetectedArea ) + + self:T3( DetectedArea.MaxThreatLevelA2G ) + return DetectedArea.MaxThreatLevelA2G +end + + + +--- Smoke the detected units +-- @param #DETECTION_AREAS self +-- @return #DETECTION_AREAS self +function DETECTION_AREAS:SmokeDetectedUnits() + self:F2() + + self._SmokeDetectedUnits = true + return self +end + +--- Flare the detected units +-- @param #DETECTION_AREAS self +-- @return #DETECTION_AREAS self +function DETECTION_AREAS:FlareDetectedUnits() + self:F2() + + self._FlareDetectedUnits = true + return self +end + +--- Smoke the detected zones +-- @param #DETECTION_AREAS self +-- @return #DETECTION_AREAS self +function DETECTION_AREAS:SmokeDetectedZones() + self:F2() + + self._SmokeDetectedZones = true + return self +end + +--- Flare the detected zones +-- @param #DETECTION_AREAS self +-- @return #DETECTION_AREAS self +function DETECTION_AREAS:FlareDetectedZones() + self:F2() + + self._FlareDetectedZones = true + return self +end + +--- Add a change to the detected zone. +-- @param #DETECTION_AREAS self +-- @param #DETECTION_AREAS.DetectedArea DetectedArea +-- @param #string ChangeCode +-- @return #DETECTION_AREAS self +function DETECTION_AREAS:AddChangeArea( DetectedArea, ChangeCode, AreaUnitType ) + + DetectedArea.Changed = true + local AreaID = DetectedArea.AreaID + + DetectedArea.Changes = DetectedArea.Changes or {} + DetectedArea.Changes[ChangeCode] = DetectedArea.Changes[ChangeCode] or {} + DetectedArea.Changes[ChangeCode].AreaID = AreaID + DetectedArea.Changes[ChangeCode].AreaUnitType = AreaUnitType + + self:T( { "Change on Detection Area:", DetectedArea.AreaID, ChangeCode, AreaUnitType } ) + + return self +end + + +--- Add a change to the detected zone. +-- @param #DETECTION_AREAS self +-- @param #DETECTION_AREAS.DetectedArea DetectedArea +-- @param #string ChangeCode +-- @param #string ChangeUnitType +-- @return #DETECTION_AREAS self +function DETECTION_AREAS:AddChangeUnit( DetectedArea, ChangeCode, ChangeUnitType ) + + DetectedArea.Changed = true + local AreaID = DetectedArea.AreaID + + DetectedArea.Changes = DetectedArea.Changes or {} + DetectedArea.Changes[ChangeCode] = DetectedArea.Changes[ChangeCode] or {} + DetectedArea.Changes[ChangeCode][ChangeUnitType] = DetectedArea.Changes[ChangeCode][ChangeUnitType] or 0 + DetectedArea.Changes[ChangeCode][ChangeUnitType] = DetectedArea.Changes[ChangeCode][ChangeUnitType] + 1 + DetectedArea.Changes[ChangeCode].AreaID = AreaID + + self:T( { "Change on Detection Area:", DetectedArea.AreaID, ChangeCode, ChangeUnitType } ) + + return self +end + +--- Make text documenting the changes of the detected zone. +-- @param #DETECTION_AREAS self +-- @param #DETECTION_AREAS.DetectedArea DetectedArea +-- @return #string The Changes text +function DETECTION_AREAS:GetChangeText( DetectedArea ) + self:F( DetectedArea ) + + local MT = {} + + for ChangeCode, ChangeData in pairs( DetectedArea.Changes ) do + + if ChangeCode == "AA" then + MT[#MT+1] = "Detected new area " .. ChangeData.AreaID .. ". The center target is a " .. ChangeData.AreaUnitType .. "." + end + + if ChangeCode == "RAU" then + MT[#MT+1] = "Changed area " .. ChangeData.AreaID .. ". Removed the center target." + end + + if ChangeCode == "AAU" then + MT[#MT+1] = "Changed area " .. ChangeData.AreaID .. ". The new center target is a " .. ChangeData.AreaUnitType "." + end + + if ChangeCode == "RA" then + MT[#MT+1] = "Removed old area " .. ChangeData.AreaID .. ". No more targets in this area." + end + + if ChangeCode == "AU" then + local MTUT = {} + for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do + if ChangeUnitType ~= "AreaID" then + MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType + end + end + MT[#MT+1] = "Detected for area " .. ChangeData.AreaID .. " new target(s) " .. table.concat( MTUT, ", " ) .. "." + end + + if ChangeCode == "RU" then + local MTUT = {} + for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do + if ChangeUnitType ~= "AreaID" then + MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType + end + end + MT[#MT+1] = "Removed for area " .. ChangeData.AreaID .. " invisible or destroyed target(s) " .. table.concat( MTUT, ", " ) .. "." + end + + end + + return table.concat( MT, "\n" ) + +end + + +--- Accepts changes from the detected zone. +-- @param #DETECTION_AREAS self +-- @param #DETECTION_AREAS.DetectedArea DetectedArea +-- @return #DETECTION_AREAS self +function DETECTION_AREAS:AcceptChanges( DetectedArea ) + + DetectedArea.Changed = false + DetectedArea.Changes = {} + + return self +end + + +--- Make a DetectionSet table. This function will be overridden in the derived clsses. +-- @param #DETECTION_AREAS self +-- @return #DETECTION_AREAS self +function DETECTION_AREAS:CreateDetectionSets() + self:F2() + + -- First go through all detected sets, and check if there are new detected units, match all existing detected units and identify undetected units. + -- Regroup when needed, split groups when needed. + for DetectedAreaID, DetectedAreaData in ipairs( self.DetectedAreas ) do + + local DetectedArea = DetectedAreaData -- #DETECTION_AREAS.DetectedArea + if DetectedArea then + + local DetectedSet = DetectedArea.Set + + local AreaExists = false -- This flag will determine of the detected area is still existing. + + -- First test if the center unit is detected in the detection area. + self:T3( DetectedArea.Zone.ZoneUNIT.UnitName ) + local DetectedZoneObject = self:GetDetectedObject( DetectedArea.Zone.ZoneUNIT.UnitName ) + self:T3( { "Detecting Zone Object", DetectedArea.AreaID, DetectedArea.Zone, DetectedZoneObject } ) + + if DetectedZoneObject then + + --self:IdentifyDetectedObject( DetectedZoneObject ) + AreaExists = true + + + + else + -- The center object of the detected area has not been detected. Find an other unit of the set to become the center of the area. + -- First remove the center unit from the set. + DetectedSet:RemoveUnitsByName( DetectedArea.Zone.ZoneUNIT.UnitName ) + + self:AddChangeArea( DetectedArea, 'RAU', "Dummy" ) + + -- Then search for a new center area unit within the set. Note that the new area unit candidate must be within the area range. + for DetectedUnitName, DetectedUnitData in pairs( DetectedSet:GetSet() ) do + + local DetectedUnit = DetectedUnitData -- Wrapper.Unit#UNIT + local DetectedObject = self:GetDetectedObject( DetectedUnit.UnitName ) + + -- The DetectedObject can be nil when the DetectedUnit is not alive anymore or it is not in the DetectedObjects map. + -- If the DetectedUnit was already identified, DetectedObject will be nil. + if DetectedObject then + self:IdentifyDetectedObject( DetectedObject ) + AreaExists = true + + -- Assign the Unit as the new center unit of the detected area. + DetectedArea.Zone = ZONE_UNIT:New( DetectedUnit:GetName(), DetectedUnit, self.DetectionZoneRange ) + + self:AddChangeArea( DetectedArea, "AAU", DetectedArea.Zone.ZoneUNIT:GetTypeName() ) + + -- We don't need to add the DetectedObject to the area set, because it is already there ... + break + end + end + end + + -- Now we've determined the center unit of the area, now we can iterate the units in the detected area. + -- Note that the position of the area may have moved due to the center unit repositioning. + -- If no center unit was identified, then the detected area does not exist anymore and should be deleted, as there are no valid units that can be the center unit. + if AreaExists then + + -- ok, we found the center unit of the area, now iterate through the detected area set and see which units are still within the center unit zone ... + -- Those units within the zone are flagged as Identified. + -- If a unit was not found in the set, remove it from the set. This may be added later to other existing or new sets. + for DetectedUnitName, DetectedUnitData in pairs( DetectedSet:GetSet() ) do + + local DetectedUnit = DetectedUnitData -- Wrapper.Unit#UNIT + local DetectedObject = nil + if DetectedUnit:IsAlive() then + --self:E(DetectedUnit:GetName()) + DetectedObject = self:GetDetectedObject( DetectedUnit:GetName() ) + end + if DetectedObject then + + -- Check if the DetectedUnit is within the DetectedArea.Zone + if DetectedUnit:IsInZone( DetectedArea.Zone ) then + + -- Yes, the DetectedUnit is within the DetectedArea.Zone, no changes, DetectedUnit can be kept within the Set. + self:IdentifyDetectedObject( DetectedObject ) + + else + -- No, the DetectedUnit is not within the DetectedArea.Zone, remove DetectedUnit from the Set. + DetectedSet:Remove( DetectedUnitName ) + self:AddChangeUnit( DetectedArea, "RU", DetectedUnit:GetTypeName() ) + end + + else + -- There was no DetectedObject, remove DetectedUnit from the Set. + self:AddChangeUnit( DetectedArea, "RU", "destroyed target" ) + DetectedSet:Remove( DetectedUnitName ) + + -- The DetectedObject has been identified, because it does not exist ... + -- self:IdentifyDetectedObject( DetectedObject ) + end + end + else + self:RemoveDetectedArea( DetectedAreaID ) + self:AddChangeArea( DetectedArea, "RA" ) + end + end + end + + -- We iterated through the existing detection areas and: + -- - We checked which units are still detected in each detection area. Those units were flagged as Identified. + -- - We recentered the detection area to new center units where it was needed. + -- + -- Now we need to loop through the unidentified detected units and see where they belong: + -- - They can be added to a new detection area and become the new center unit. + -- - They can be added to a new detection area. + for DetectedUnitName, DetectedObjectData in pairs( self.DetectedObjects ) do + + local DetectedObject = self:GetDetectedObject( DetectedUnitName ) + + if DetectedObject then + + -- We found an unidentified unit outside of any existing detection area. + local DetectedUnit = UNIT:FindByName( DetectedUnitName ) -- Wrapper.Unit#UNIT + + local AddedToDetectionArea = false + + for DetectedAreaID, DetectedAreaData in ipairs( self.DetectedAreas ) do + + local DetectedArea = DetectedAreaData -- #DETECTION_AREAS.DetectedArea + if DetectedArea then + self:T( "Detection Area #" .. DetectedArea.AreaID ) + local DetectedSet = DetectedArea.Set + if not self:IsDetectedObjectIdentified( DetectedObject ) and DetectedUnit:IsInZone( DetectedArea.Zone ) then + self:IdentifyDetectedObject( DetectedObject ) + DetectedSet:AddUnit( DetectedUnit ) + AddedToDetectionArea = true + self:AddChangeUnit( DetectedArea, "AU", DetectedUnit:GetTypeName() ) + end + end + end + + if AddedToDetectionArea == false then + + -- New detection area + local DetectedArea = self:AddDetectedArea( + SET_UNIT:New(), + ZONE_UNIT:New( DetectedUnitName, DetectedUnit, self.DetectionZoneRange ) + ) + --self:E( DetectedArea.Zone.ZoneUNIT.UnitName ) + DetectedArea.Set:AddUnit( DetectedUnit ) + self:AddChangeArea( DetectedArea, "AA", DetectedUnit:GetTypeName() ) + end + end + end + + -- Now all the tests should have been build, now make some smoke and flares... + -- We also report here the friendlies within the detected areas. + + for DetectedAreaID, DetectedAreaData in ipairs( self.DetectedAreas ) do + + local DetectedArea = DetectedAreaData -- #DETECTION_AREAS.DetectedArea + local DetectedSet = DetectedArea.Set + local DetectedZone = DetectedArea.Zone + + self:ReportFriendliesNearBy( { DetectedArea = DetectedArea, ReportSetGroup = self.DetectionSetGroup } ) -- Fill the Friendlies table + self:CalculateThreatLevelA2G( DetectedArea ) -- Calculate A2G threat level + self:NearestFAC( DetectedArea ) + + if DETECTION_AREAS._SmokeDetectedUnits or self._SmokeDetectedUnits then + DetectedZone.ZoneUNIT:SmokeRed() + end + DetectedSet:ForEachUnit( + --- @param Wrapper.Unit#UNIT DetectedUnit + function( DetectedUnit ) + if DetectedUnit:IsAlive() then + self:T( "Detected Set #" .. DetectedArea.AreaID .. ":" .. DetectedUnit:GetName() ) + if DETECTION_AREAS._FlareDetectedUnits or self._FlareDetectedUnits then + DetectedUnit:FlareGreen() + end + if DETECTION_AREAS._SmokeDetectedUnits or self._SmokeDetectedUnits then + DetectedUnit:SmokeGreen() + end + end + end + ) + if DETECTION_AREAS._FlareDetectedZones or self._FlareDetectedZones then + DetectedZone:FlareZone( SMOKECOLOR.White, 30, math.random( 0,90 ) ) + end + if DETECTION_AREAS._SmokeDetectedZones or self._SmokeDetectedZones then + DetectedZone:SmokeZone( SMOKECOLOR.White, 30 ) + end + end + +end + + +--- Single-Player:**No** / Multi-Player:**Yes** / AI:**Yes** / Human:**No** / Types:**All** -- **AI Balancing will replace in multi player missions +-- non-occupied human slots with AI groups, in order to provide an engaging simulation environment, +-- even when there are hardly any players in the mission.** +-- +-- ![Banner Image](..\Presentations\AI_Balancer\Dia1.JPG) +-- +-- === +-- +-- # 1) @{AI_Balancer#AI_BALANCER} class, extends @{Fsm#FSM_SET} +-- +-- The @{AI_Balancer#AI_BALANCER} class monitors and manages as many replacement AI groups as there are +-- CLIENTS in a SET_CLIENT collection, which are not occupied by human players. +-- In other words, use AI_BALANCER to simulate human behaviour by spawning in replacement AI in multi player missions. +-- +-- The parent class @{Fsm#FSM_SET} manages the functionality to control the Finite State Machine (FSM). +-- The mission designer can tailor the behaviour of the AI_BALANCER, by defining event and state transition methods. +-- An explanation about state and event transition methods can be found in the @{FSM} module documentation. +-- +-- The mission designer can tailor the AI_BALANCER behaviour, by implementing a state or event handling method for the following: +-- +-- * **@{#AI_BALANCER.OnAfterSpawned}**( AISet, From, Event, To, AIGroup ): Define to add extra logic when an AI is spawned. +-- +-- ## 1.1) AI_BALANCER construction +-- +-- Create a new AI_BALANCER object with the @{#AI_BALANCER.New}() method: +-- +-- ## 1.2) AI_BALANCER is a FSM +-- +-- ![Process](..\Presentations\AI_Balancer\Dia13.JPG) +-- +-- ### 1.2.1) AI_BALANCER States +-- +-- * **Monitoring** ( Set ): Monitoring the Set if all AI is spawned for the Clients. +-- * **Spawning** ( Set, ClientName ): There is a new AI group spawned with ClientName as the name of reference. +-- * **Spawned** ( Set, AIGroup ): A new AI has been spawned. You can handle this event to customize the AI behaviour with other AI FSMs or own processes. +-- * **Destroying** ( Set, AIGroup ): The AI is being destroyed. +-- * **Returning** ( Set, AIGroup ): The AI is returning to the airbase specified by the ReturnToAirbase methods. Handle this state to customize the return behaviour of the AI, if any. +-- +-- ### 1.2.2) AI_BALANCER Events +-- +-- * **Monitor** ( Set ): Every 10 seconds, the Monitor event is triggered to monitor the Set. +-- * **Spawn** ( Set, ClientName ): Triggers when there is a new AI group to be spawned with ClientName as the name of reference. +-- * **Spawned** ( Set, AIGroup ): Triggers when a new AI has been spawned. You can handle this event to customize the AI behaviour with other AI FSMs or own processes. +-- * **Destroy** ( Set, AIGroup ): The AI is being destroyed. +-- * **Return** ( Set, AIGroup ): The AI is returning to the airbase specified by the ReturnToAirbase methods. +-- +-- ## 1.3) AI_BALANCER spawn interval for replacement AI +-- +-- Use the method @{#AI_BALANCER.InitSpawnInterval}() to set the earliest and latest interval in seconds that is waited until a new replacement AI is spawned. +-- +-- ## 1.4) AI_BALANCER returns AI to Airbases +-- +-- By default, When a human player joins a slot that is AI_BALANCED, the AI group will be destroyed by default. +-- However, there are 2 additional options that you can use to customize the destroy behaviour. +-- When a human player joins a slot, you can configure to let the AI return to: +-- +-- * @{#AI_BALANCER.ReturnToHomeAirbase}: Returns the AI to the **home** @{Airbase#AIRBASE}. +-- * @{#AI_BALANCER.ReturnToNearestAirbases}: Returns the AI to the **nearest friendly** @{Airbase#AIRBASE}. +-- +-- Note that when AI returns to an airbase, the AI_BALANCER will trigger the **Return** event and the AI will return, +-- otherwise the AI_BALANCER will trigger a **Destroy** event, and the AI will be destroyed. +-- +-- === +-- +-- # **API CHANGE HISTORY** +-- +-- The underlying change log documents the API changes. Please read this carefully. The following notation is used: +-- +-- * **Added** parts are expressed in bold type face. +-- * _Removed_ parts are expressed in italic type face. +-- +-- Hereby the change log: +-- +-- 2017-01-17: There is still a problem with AI being destroyed, but not respawned. Need to check further upon that. +-- +-- 2017-01-08: AI_BALANCER:**InitSpawnInterval( Earliest, Latest )** added. +-- +-- === +-- +-- # **AUTHORS and CONTRIBUTIONS** +-- +-- ### Contributions: +-- +-- * **[Dutch_Baron](https://forums.eagle.ru/member.php?u=112075)**: Working together with James has resulted in the creation of the AI_BALANCER class. James has shared his ideas on balancing AI with air units, and together we made a first design which you can use now :-) +-- * **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 AI_BALANCER moose class. +-- +-- ### Authors: +-- +-- * FlightControl: Framework Design & Programming and Documentation. +-- +-- @module AI_Balancer + +--- AI_BALANCER class +-- @type AI_BALANCER +-- @field Core.Set#SET_CLIENT SetClient +-- @field Functional.Spawn#SPAWN SpawnAI +-- @field Wrapper.Group#GROUP Test +-- @extends Core.Fsm#FSM_SET +AI_BALANCER = { + ClassName = "AI_BALANCER", + PatrolZones = {}, + AIGroups = {}, + Earliest = 5, -- Earliest a new AI can be spawned is in 5 seconds. + Latest = 60, -- Latest a new AI can be spawned is in 60 seconds. +} + + + +--- Creates a new AI_BALANCER object +-- @param #AI_BALANCER self +-- @param Core.Set#SET_CLIENT SetClient A SET\_CLIENT object that will contain the CLIENT objects to be monitored if they are alive or not (joined by a player). +-- @param Functional.Spawn#SPAWN SpawnAI The default Spawn object to spawn new AI Groups when needed. +-- @return #AI_BALANCER +function AI_BALANCER:New( SetClient, SpawnAI ) + + -- Inherits from BASE + local self = BASE:Inherit( self, FSM_SET:New( SET_GROUP:New() ) ) -- AI.AI_Balancer#AI_BALANCER + + -- TODO: Define the OnAfterSpawned event + self:SetStartState( "None" ) + self:AddTransition( "*", "Monitor", "Monitoring" ) + self:AddTransition( "*", "Spawn", "Spawning" ) + self:AddTransition( "Spawning", "Spawned", "Spawned" ) + self:AddTransition( "*", "Destroy", "Destroying" ) + self:AddTransition( "*", "Return", "Returning" ) + + self.SetClient = SetClient + self.SetClient:FilterOnce() + self.SpawnAI = SpawnAI + + self.SpawnQueue = {} + + self.ToNearestAirbase = false + self.ToHomeAirbase = false + + self:__Monitor( 1 ) + + return self +end + +--- Sets the earliest to the latest interval in seconds how long AI_BALANCER will wait to spawn a new AI. +-- Provide 2 identical seconds if the interval should be a fixed amount of seconds. +-- @param #AI_BALANCER self +-- @param #number Earliest The earliest a new AI can be spawned in seconds. +-- @param #number Latest The latest a new AI can be spawned in seconds. +-- @return self +function AI_BALANCER:InitSpawnInterval( Earliest, Latest ) + + self.Earliest = Earliest + self.Latest = Latest + + return self +end + +--- Returns the AI to the nearest friendly @{Airbase#AIRBASE}. +-- @param #AI_BALANCER self +-- @param Dcs.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 Core.Set#SET_AIRBASE ReturnAirbaseSet The SET of @{Set#SET_AIRBASE}s to evaluate where to return to. +function AI_BALANCER:ReturnToNearestAirbases( ReturnTresholdRange, ReturnAirbaseSet ) + + self.ToNearestAirbase = true + self.ReturnTresholdRange = ReturnTresholdRange + self.ReturnAirbaseSet = ReturnAirbaseSet +end + +--- Returns the AI to the home @{Airbase#AIRBASE}. +-- @param #AI_BALANCER self +-- @param Dcs.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 AI_BALANCER:ReturnToHomeAirbase( ReturnTresholdRange ) + + self.ToHomeAirbase = true + self.ReturnTresholdRange = ReturnTresholdRange +end + +--- @param #AI_BALANCER self +-- @param Core.Set#SET_GROUP SetGroup +-- @param #string ClientName +-- @param Wrapper.Group#GROUP AIGroup +function AI_BALANCER:onenterSpawning( SetGroup, From, Event, To, ClientName ) + + -- OK, Spawn a new group from the default SpawnAI object provided. + local AIGroup = self.SpawnAI:Spawn() -- Wrapper.Group#GROUP + if AIGroup then + AIGroup:E( "Spawning new AIGroup" ) + --TODO: need to rework UnitName thing ... + + SetGroup:Add( ClientName, AIGroup ) + self.SpawnQueue[ClientName] = nil + + -- Fire the Spawned event. The first parameter is the AIGroup just Spawned. + -- Mission designers can catch this event to bind further actions to the AIGroup. + self:Spawned( AIGroup ) + end +end + +--- @param #AI_BALANCER self +-- @param Core.Set#SET_GROUP SetGroup +-- @param Wrapper.Group#GROUP AIGroup +function AI_BALANCER:onenterDestroying( SetGroup, From, Event, To, ClientName, AIGroup ) + + AIGroup:Destroy() + SetGroup:Flush() + SetGroup:Remove( ClientName ) + SetGroup:Flush() +end + +--- @param #AI_BALANCER self +-- @param Core.Set#SET_GROUP SetGroup +-- @param Wrapper.Group#GROUP AIGroup +function AI_BALANCER:onenterReturning( SetGroup, From, Event, To, AIGroup ) + + local AIGroupTemplate = AIGroup:GetTemplate() + if self.ToHomeAirbase == true then + local WayPointCount = #AIGroupTemplate.route.points + local SwitchWayPointCommand = AIGroup:CommandSwitchWayPoint( 1, WayPointCount, 1 ) + AIGroup:SetCommand( SwitchWayPointCommand ) + AIGroup:MessageToRed( "Returning to home base ...", 30 ) + else + -- Okay, we need to send this Group back to the nearest base of the Coalition of the AI. + --TODO: i need to rework the POINT_VEC2 thing. + local PointVec2 = POINT_VEC2:New( AIGroup:GetVec2().x, AIGroup:GetVec2().y ) + local ClosestAirbase = self.ReturnAirbaseSet:FindNearestAirbaseFromPointVec2( PointVec2 ) + self:T( ClosestAirbase.AirbaseName ) + AIGroup:MessageToRed( "Returning to " .. ClosestAirbase:GetName().. " ...", 30 ) + local RTBRoute = AIGroup:RouteReturnToAirbase( ClosestAirbase ) + AIGroupTemplate.route = RTBRoute + AIGroup:Respawn( AIGroupTemplate ) + end + +end + + +--- @param #AI_BALANCER self +function AI_BALANCER:onenterMonitoring( SetGroup ) + + self:T2( { self.SetClient:Count() } ) + --self.SetClient:Flush() + + self.SetClient:ForEachClient( + --- @param Wrapper.Client#CLIENT Client + function( Client ) + self:T3(Client.ClientName) + + local AIGroup = self.Set:Get( Client.UnitName ) -- Wrapper.Group#GROUP + if Client:IsAlive() then + + if AIGroup and AIGroup:IsAlive() == true then + + if self.ToNearestAirbase == false and self.ToHomeAirbase == false then + self:Destroy( Client.UnitName, AIGroup ) + else + -- We test if there is no other CLIENT within the self.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:GetVec2(), self.ReturnTresholdRange ) + + self:T2( RangeZone ) + + _DATABASE:ForEachPlayer( + --- @param Wrapper.Unit#UNIT RangeTestUnit + function( RangeTestUnit, RangeZone, AIGroup, PlayerInRange ) + self:T2( { PlayerInRange, RangeTestUnit.UnitName, RangeZone.ZoneName } ) + if RangeTestUnit:IsInZone( RangeZone ) == true then + self:T2( "in zone" ) + if RangeTestUnit:GetCoalition() ~= AIGroup:GetCoalition() then + self:T2( "in range" ) + PlayerInRange.Value = true + end + end + end, + + --- @param Core.Zone#ZONE_RADIUS RangeZone + -- @param Wrapper.Group#GROUP AIGroup + function( RangeZone, AIGroup, PlayerInRange ) + if PlayerInRange.Value == false then + self:Return( AIGroup ) + end + end + , RangeZone, AIGroup, PlayerInRange + ) + + end + self.Set:Remove( Client.UnitName ) + end + else + if not AIGroup or not AIGroup:IsAlive() == true then + self:T( "Client " .. Client.UnitName .. " not alive." ) + if not self.SpawnQueue[Client.UnitName] then + -- Spawn a new AI taking into account the spawn interval Earliest, Latest + self:__Spawn( math.random( self.Earliest, self.Latest ), Client.UnitName ) + self.SpawnQueue[Client.UnitName] = true + self:E( "New AI Spawned for Client " .. Client.UnitName ) + end + end + end + return true + end + ) + + self:__Monitor( 10 ) +end + + + +--- Single-Player:**Yes** / Multi-Player:**Yes** / AI:**Yes** / Human:**No** / Types:**Air** -- +-- **Air Patrolling or Staging.** +-- +-- ![Banner Image](..\Presentations\AI_PATROL\Dia1.JPG) +-- +-- === +-- +-- # 1) @{#AI_PATROL_ZONE} class, extends @{Fsm#FSM_CONTROLLABLE} +-- +-- The @{#AI_PATROL_ZONE} class implements the core functions to patrol a @{Zone} by an AI @{Controllable} or @{Group}. +-- +-- ![Process](..\Presentations\AI_PATROL\Dia3.JPG) +-- +-- The AI_PATROL_ZONE is assigned a @{Group} and this must be done before the AI_PATROL_ZONE process can be started using the **Start** event. +-- +-- ![Process](..\Presentations\AI_PATROL\Dia4.JPG) +-- +-- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits. +-- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. +-- +-- ![Process](..\Presentations\AI_PATROL\Dia5.JPG) +-- +-- This cycle will continue. +-- +-- ![Process](..\Presentations\AI_PATROL\Dia6.JPG) +-- +-- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event. +-- +-- ![Process](..\Presentations\AI_PATROL\Dia9.JPG) +-- +---- Note that the enemy is not engaged! To model enemy engagement, either tailor the **Detected** event, or +-- use derived AI_ classes to model AI offensive or defensive behaviour. +-- +-- ![Process](..\Presentations\AI_PATROL\Dia10.JPG) +-- +-- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. +-- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. +-- +-- ![Process](..\Presentations\AI_PATROL\Dia11.JPG) +-- +-- ## 1.1) AI_PATROL_ZONE constructor +-- +-- * @{#AI_PATROL_ZONE.New}(): Creates a new AI_PATROL_ZONE object. +-- +-- ## 1.2) AI_PATROL_ZONE is a FSM +-- +-- ![Process](..\Presentations\AI_PATROL\Dia2.JPG) +-- +-- ### 1.2.1) AI_PATROL_ZONE States +-- +-- * **None** ( Group ): The process is not started yet. +-- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. +-- * **Returning** ( Group ): The AI is returning to Base. +-- * **Stopped** ( Group ): The process is stopped. +-- * **Crashed** ( Group ): The AI has crashed or is dead. +-- +-- ### 1.2.2) AI_PATROL_ZONE Events +-- +-- * **Start** ( Group ): Start the process. +-- * **Stop** ( Group ): Stop the process. +-- * **Route** ( Group ): Route the AI to a new random 3D point within the Patrol Zone. +-- * **RTB** ( Group ): Route the AI to the home base. +-- * **Detect** ( Group ): The AI is detecting targets. +-- * **Detected** ( Group ): The AI has detected new targets. +-- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. +-- +-- ## 1.3) Set or Get the AI controllable +-- +-- * @{#AI_PATROL_ZONE.SetControllable}(): Set the AIControllable. +-- * @{#AI_PATROL_ZONE.GetControllable}(): Get the AIControllable. +-- +-- ## 1.4) Set the Speed and Altitude boundaries of the AI controllable +-- +-- * @{#AI_PATROL_ZONE.SetSpeed}(): Set the patrol speed boundaries of the AI, for the next patrol. +-- * @{#AI_PATROL_ZONE.SetAltitude}(): Set altitude boundaries of the AI, for the next patrol. +-- +-- ## 1.5) Manage the detection process of the AI controllable +-- +-- The detection process of the AI controllable can be manipulated. +-- Detection requires an amount of CPU power, which has an impact on your mission performance. +-- Only put detection on when absolutely necessary, and the frequency of the detection can also be set. +-- +-- * @{#AI_PATROL_ZONE.SetDetectionOn}(): Set the detection on. The AI will detect for targets. +-- * @{#AI_PATROL_ZONE.SetDetectionOff}(): Set the detection off, the AI will not detect for targets. The existing target list will NOT be erased. +-- +-- The detection frequency can be set with @{#AI_PATROL_ZONE.SetDetectionInterval}( seconds ), where the amount of seconds specify how much seconds will be waited before the next detection. +-- Use the method @{#AI_PATROL_ZONE.GetDetectedUnits}() to obtain a list of the @{Unit}s detected by the AI. +-- +-- The detection can be filtered to potential targets in a specific zone. +-- Use the method @{#AI_PATROL_ZONE.SetDetectionZone}() to set the zone where targets need to be detected. +-- Note that when the zone is too far away, or the AI is not heading towards the zone, or the AI is too high, no targets may be detected +-- according the weather conditions. +-- +-- ## 1.6) Manage the "out of fuel" in the AI_PATROL_ZONE +-- +-- When the AI is out of fuel, it is required that a new AI is started, before the old AI can return to the home base. +-- Therefore, with a parameter and a calculation of the distance to the home base, the fuel treshold is calculated. +-- When the fuel treshold is reached, the AI will continue for a given time its patrol task in orbit, +-- while a new AI is targetted to the AI_PATROL_ZONE. +-- Once the time is finished, the old AI will return to the base. +-- Use the method @{#AI_PATROL_ZONE.ManageFuel}() to have this proces in place. +-- +-- ## 1.7) Manage "damage" behaviour of the AI in the AI_PATROL_ZONE +-- +-- When the AI is damaged, it is required that a new AIControllable is started. However, damage cannon be foreseen early on. +-- Therefore, when the damage treshold is reached, the AI will return immediately to the home base (RTB). +-- Use the method @{#AI_PATROL_ZONE.ManageDamage}() to have this proces in place. +-- +-- ==== +-- +-- # **OPEN ISSUES** +-- +-- 2017-01-17: When Spawned AI is located at an airbase, it will be routed first back to the airbase after take-off. +-- +-- 2016-01-17: +-- -- Fixed problem with AI returning to base too early and unexpected. +-- -- ReSpawning of AI will reset the AI_PATROL and derived classes. +-- -- Checked the correct workings of SCHEDULER, and it DOES work correctly. +-- +-- ==== +-- +-- # **API CHANGE HISTORY** +-- +-- The underlying change log documents the API changes. Please read this carefully. The following notation is used: +-- +-- * **Added** parts are expressed in bold type face. +-- * _Removed_ parts are expressed in italic type face. +-- +-- Hereby the change log: +-- +-- 2017-01-17: Rename of class: **AI\_PATROL\_ZONE** is the new name for the old _AI\_PATROLZONE_. +-- +-- 2017-01-15: Complete revision. AI_PATROL_ZONE is the base class for other AI_PATROL like classes. +-- +-- 2016-09-01: Initial class and API. +-- +-- === +-- +-- # **AUTHORS and CONTRIBUTIONS** +-- +-- ### Contributions: +-- +-- * **[Dutch_Baron](https://forums.eagle.ru/member.php?u=112075)**: Working together with James has resulted in the creation of the AI_BALANCER class. James has shared his ideas on balancing AI with air units, and together we made a first design which you can use now :-) +-- * **[Pikey](https://forums.eagle.ru/member.php?u=62835)**: Testing and API concept review. +-- +-- ### Authors: +-- +-- * **FlightControl**: Design & Programming. +-- +-- @module AI_Patrol + +--- AI_PATROL_ZONE class +-- @type AI_PATROL_ZONE +-- @field Wrapper.Controllable#CONTROLLABLE AIControllable The @{Controllable} patrolling. +-- @field Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @field Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @field Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @field Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. +-- @field Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h. +-- @field Functional.Spawn#SPAWN CoordTest +-- @extends Core.Fsm#FSM_CONTROLLABLE +AI_PATROL_ZONE = { + ClassName = "AI_PATROL_ZONE", +} + +--- Creates a new AI_PATROL_ZONE object +-- @param #AI_PATROL_ZONE self +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. +-- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h. +-- @param Dcs.DCSTypes#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO +-- @return #AI_PATROL_ZONE self +-- @usage +-- -- Define a new AI_PATROL_ZONE Object. This PatrolArea will patrol an AIControllable within PatrolZone between 3000 and 6000 meters, with a variying speed between 600 and 900 km/h. +-- PatrolZone = ZONE:New( 'PatrolZone' ) +-- PatrolSpawn = SPAWN:New( 'Patrol Group' ) +-- PatrolArea = AI_PATROL_ZONE:New( PatrolZone, 3000, 6000, 600, 900 ) +function AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) + + -- Inherits from BASE + local self = BASE:Inherit( self, FSM_CONTROLLABLE:New() ) -- #AI_PATROL_ZONE + + + self.PatrolZone = PatrolZone + self.PatrolFloorAltitude = PatrolFloorAltitude + self.PatrolCeilingAltitude = PatrolCeilingAltitude + self.PatrolMinSpeed = PatrolMinSpeed + self.PatrolMaxSpeed = PatrolMaxSpeed + + -- defafult PatrolAltType to "RADIO" if not specified + self.PatrolAltType = PatrolAltType or "RADIO" + + self:SetDetectionInterval( 30 ) + + self.CheckStatus = true + + self:ManageFuel( .2, 60 ) + self:ManageDamage( 1 ) + + + self.DetectedUnits = {} -- This table contains the targets detected during patrol. + + self:SetStartState( "None" ) + + self:AddTransition( "*", "Stop", "Stopped" ) + +--- OnLeave Transition Handler for State Stopped. +-- @function [parent=#AI_PATROL_ZONE] OnLeaveStopped +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnEnter Transition Handler for State Stopped. +-- @function [parent=#AI_PATROL_ZONE] OnEnterStopped +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- OnBefore Transition Handler for Event Stop. +-- @function [parent=#AI_PATROL_ZONE] OnBeforeStop +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnAfter Transition Handler for Event Stop. +-- @function [parent=#AI_PATROL_ZONE] OnAfterStop +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- Synchronous Event Trigger for Event Stop. +-- @function [parent=#AI_PATROL_ZONE] Stop +-- @param #AI_PATROL_ZONE self + +--- Asynchronous Event Trigger for Event Stop. +-- @function [parent=#AI_PATROL_ZONE] __Stop +-- @param #AI_PATROL_ZONE self +-- @param #number Delay The delay in seconds. + + self:AddTransition( "None", "Start", "Patrolling" ) + +--- OnBefore Transition Handler for Event Start. +-- @function [parent=#AI_PATROL_ZONE] OnBeforeStart +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnAfter Transition Handler for Event Start. +-- @function [parent=#AI_PATROL_ZONE] OnAfterStart +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- Synchronous Event Trigger for Event Start. +-- @function [parent=#AI_PATROL_ZONE] Start +-- @param #AI_PATROL_ZONE self + +--- Asynchronous Event Trigger for Event Start. +-- @function [parent=#AI_PATROL_ZONE] __Start +-- @param #AI_PATROL_ZONE self +-- @param #number Delay The delay in seconds. + +--- OnLeave Transition Handler for State Patrolling. +-- @function [parent=#AI_PATROL_ZONE] OnLeavePatrolling +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnEnter Transition Handler for State Patrolling. +-- @function [parent=#AI_PATROL_ZONE] OnEnterPatrolling +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + + self:AddTransition( "Patrolling", "Route", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. + +--- OnBefore Transition Handler for Event Route. +-- @function [parent=#AI_PATROL_ZONE] OnBeforeRoute +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnAfter Transition Handler for Event Route. +-- @function [parent=#AI_PATROL_ZONE] OnAfterRoute +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- Synchronous Event Trigger for Event Route. +-- @function [parent=#AI_PATROL_ZONE] Route +-- @param #AI_PATROL_ZONE self + +--- Asynchronous Event Trigger for Event Route. +-- @function [parent=#AI_PATROL_ZONE] __Route +-- @param #AI_PATROL_ZONE self +-- @param #number Delay The delay in seconds. + + self:AddTransition( "*", "Status", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. + +--- OnBefore Transition Handler for Event Status. +-- @function [parent=#AI_PATROL_ZONE] OnBeforeStatus +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnAfter Transition Handler for Event Status. +-- @function [parent=#AI_PATROL_ZONE] OnAfterStatus +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- Synchronous Event Trigger for Event Status. +-- @function [parent=#AI_PATROL_ZONE] Status +-- @param #AI_PATROL_ZONE self + +--- Asynchronous Event Trigger for Event Status. +-- @function [parent=#AI_PATROL_ZONE] __Status +-- @param #AI_PATROL_ZONE self +-- @param #number Delay The delay in seconds. + + self:AddTransition( "*", "Detect", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. + +--- OnBefore Transition Handler for Event Detect. +-- @function [parent=#AI_PATROL_ZONE] OnBeforeDetect +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnAfter Transition Handler for Event Detect. +-- @function [parent=#AI_PATROL_ZONE] OnAfterDetect +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- Synchronous Event Trigger for Event Detect. +-- @function [parent=#AI_PATROL_ZONE] Detect +-- @param #AI_PATROL_ZONE self + +--- Asynchronous Event Trigger for Event Detect. +-- @function [parent=#AI_PATROL_ZONE] __Detect +-- @param #AI_PATROL_ZONE self +-- @param #number Delay The delay in seconds. + + self:AddTransition( "*", "Detected", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. + +--- OnBefore Transition Handler for Event Detected. +-- @function [parent=#AI_PATROL_ZONE] OnBeforeDetected +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnAfter Transition Handler for Event Detected. +-- @function [parent=#AI_PATROL_ZONE] OnAfterDetected +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- Synchronous Event Trigger for Event Detected. +-- @function [parent=#AI_PATROL_ZONE] Detected +-- @param #AI_PATROL_ZONE self + +--- Asynchronous Event Trigger for Event Detected. +-- @function [parent=#AI_PATROL_ZONE] __Detected +-- @param #AI_PATROL_ZONE self +-- @param #number Delay The delay in seconds. + + self:AddTransition( "*", "RTB", "Returning" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. + +--- OnBefore Transition Handler for Event RTB. +-- @function [parent=#AI_PATROL_ZONE] OnBeforeRTB +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnAfter Transition Handler for Event RTB. +-- @function [parent=#AI_PATROL_ZONE] OnAfterRTB +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- Synchronous Event Trigger for Event RTB. +-- @function [parent=#AI_PATROL_ZONE] RTB +-- @param #AI_PATROL_ZONE self + +--- Asynchronous Event Trigger for Event RTB. +-- @function [parent=#AI_PATROL_ZONE] __RTB +-- @param #AI_PATROL_ZONE self +-- @param #number Delay The delay in seconds. + +--- OnLeave Transition Handler for State Returning. +-- @function [parent=#AI_PATROL_ZONE] OnLeaveReturning +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnEnter Transition Handler for State Returning. +-- @function [parent=#AI_PATROL_ZONE] OnEnterReturning +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + + self:AddTransition( "*", "Reset", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. + + self:AddTransition( "*", "Eject", "*" ) + self:AddTransition( "*", "Crash", "Crashed" ) + self:AddTransition( "*", "PilotDead", "*" ) + + return self +end + + + + +--- Sets (modifies) the minimum and maximum speed of the patrol. +-- @param #AI_PATROL_ZONE self +-- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. +-- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h. +-- @return #AI_PATROL_ZONE self +function AI_PATROL_ZONE:SetSpeed( PatrolMinSpeed, PatrolMaxSpeed ) + self:F2( { PatrolMinSpeed, PatrolMaxSpeed } ) + + self.PatrolMinSpeed = PatrolMinSpeed + self.PatrolMaxSpeed = PatrolMaxSpeed +end + + + +--- Sets the floor and ceiling altitude of the patrol. +-- @param #AI_PATROL_ZONE self +-- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @return #AI_PATROL_ZONE self +function AI_PATROL_ZONE:SetAltitude( PatrolFloorAltitude, PatrolCeilingAltitude ) + self:F2( { PatrolFloorAltitude, PatrolCeilingAltitude } ) + + self.PatrolFloorAltitude = PatrolFloorAltitude + self.PatrolCeilingAltitude = PatrolCeilingAltitude +end + +-- * @{#AI_PATROL_ZONE.SetDetectionOn}(): Set the detection on. The AI will detect for targets. +-- * @{#AI_PATROL_ZONE.SetDetectionOff}(): Set the detection off, the AI will not detect for targets. The existing target list will NOT be erased. + +--- Set the detection on. The AI will detect for targets. +-- @param #AI_PATROL_ZONE self +-- @return #AI_PATROL_ZONE self +function AI_PATROL_ZONE:SetDetectionOn() + self:F2() + + self.DetectOn = true +end + +--- Set the detection off. The AI will NOT detect for targets. +-- However, the list of already detected targets will be kept and can be enquired! +-- @param #AI_PATROL_ZONE self +-- @return #AI_PATROL_ZONE self +function AI_PATROL_ZONE:SetDetectionOff() + self:F2() + + self.DetectOn = false +end + +--- Set the status checking off. +-- @param #AI_PATROL_ZONE self +-- @return #AI_PATROL_ZONE self +function AI_PATROL_ZONE:SetStatusOff() + self:F2() + + self.CheckStatus = false +end + +--- Activate the detection. The AI will detect for targets if the Detection is switched On. +-- @param #AI_PATROL_ZONE self +-- @return #AI_PATROL_ZONE self +function AI_PATROL_ZONE:SetDetectionActivated() + self:F2() + + self:ClearDetectedUnits() + self.DetectActivated = true + self:__Detect( -self.DetectInterval ) +end + +--- Deactivate the detection. The AI will NOT detect for targets. +-- @param #AI_PATROL_ZONE self +-- @return #AI_PATROL_ZONE self +function AI_PATROL_ZONE:SetDetectionDeactivated() + self:F2() + + self:ClearDetectedUnits() + self.DetectActivated = false +end + +--- Set the interval in seconds between each detection executed by the AI. +-- The list of already detected targets will be kept and updated. +-- Newly detected targets will be added, but already detected targets that were +-- not detected in this cycle, will NOT be removed! +-- The default interval is 30 seconds. +-- @param #AI_PATROL_ZONE self +-- @param #number Seconds The interval in seconds. +-- @return #AI_PATROL_ZONE self +function AI_PATROL_ZONE:SetDetectionInterval( Seconds ) + self:F2() + + if Seconds then + self.DetectInterval = Seconds + else + self.DetectInterval = 30 + end +end + +--- Set the detection zone where the AI is detecting targets. +-- @param #AI_PATROL_ZONE self +-- @param Core.Zone#ZONE DetectionZone The zone where to detect targets. +-- @return #AI_PATROL_ZONE self +function AI_PATROL_ZONE:SetDetectionZone( DetectionZone ) + self:F2() + + if DetectionZone then + self.DetectZone = DetectionZone + else + self.DetectZone = nil + end +end + +--- Gets a list of @{Unit#UNIT}s that were detected by the AI. +-- No filtering is applied, so, ANY detected UNIT can be in this list. +-- It is up to the mission designer to use the @{Unit} class and methods to filter the targets. +-- @param #AI_PATROL_ZONE self +-- @return #table The list of @{Unit#UNIT}s +function AI_PATROL_ZONE:GetDetectedUnits() + self:F2() + + return self.DetectedUnits +end + +--- Clears the list of @{Unit#UNIT}s that were detected by the AI. +-- @param #AI_PATROL_ZONE self +function AI_PATROL_ZONE:ClearDetectedUnits() + self:F2() + self.DetectedUnits = {} +end + +--- When the AI is out of fuel, it is required that a new AI is started, before the old AI can return to the home base. +-- Therefore, with a parameter and a calculation of the distance to the home base, the fuel treshold is calculated. +-- When the fuel treshold is reached, the AI will continue for a given time its patrol task in orbit, while a new AIControllable is targetted to the AI_PATROL_ZONE. +-- Once the time is finished, the old AI will return to the base. +-- @param #AI_PATROL_ZONE self +-- @param #number PatrolFuelTresholdPercentage The treshold in percentage (between 0 and 1) when the AIControllable is considered to get out of fuel. +-- @param #number PatrolOutOfFuelOrbitTime The amount of seconds the out of fuel AIControllable will orbit before returning to the base. +-- @return #AI_PATROL_ZONE self +function AI_PATROL_ZONE:ManageFuel( PatrolFuelTresholdPercentage, PatrolOutOfFuelOrbitTime ) + + self.PatrolManageFuel = true + self.PatrolFuelTresholdPercentage = PatrolFuelTresholdPercentage + self.PatrolOutOfFuelOrbitTime = PatrolOutOfFuelOrbitTime + + return self +end + +--- When the AI is damaged beyond a certain treshold, it is required that the AI returns to the home base. +-- However, damage cannot be foreseen early on. +-- Therefore, when the damage treshold is reached, +-- the AI will return immediately to the home base (RTB). +-- Note that for groups, the average damage of the complete group will be calculated. +-- So, in a group of 4 airplanes, 2 lost and 2 with damage 0.2, the damage treshold will be 0.25. +-- @param #AI_PATROL_ZONE self +-- @param #number PatrolDamageTreshold The treshold in percentage (between 0 and 1) when the AI is considered to be damaged. +-- @return #AI_PATROL_ZONE self +function AI_PATROL_ZONE:ManageDamage( PatrolDamageTreshold ) + + self.PatrolManageDamage = true + self.PatrolDamageTreshold = PatrolDamageTreshold + + return self +end + +--- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. +-- @param #AI_PATROL_ZONE self +-- @return #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_PATROL_ZONE:onafterStart( Controllable, From, Event, To ) + self:F2() + + self:__Route( 1 ) -- Route to the patrol point. The asynchronous trigger is important, because a spawned group and units takes at least one second to come live. + self:__Status( 60 ) -- Check status status every 30 seconds. + self:SetDetectionActivated() + + self:HandleEvent( EVENTS.PilotDead, self.OnPilotDead ) + self:HandleEvent( EVENTS.Crash, self.OnCrash ) + self:HandleEvent( EVENTS.Ejection, self.OnEjection ) + + Controllable:OptionROEHoldFire() + Controllable:OptionROTVertical() + + self.Controllable:OnReSpawn( + function( PatrolGroup ) + self:E( "ReSpawn" ) + self:__Reset( 1 ) + self:__Route( 5 ) + end + ) + + self:SetDetectionOn() + +end + + +--- @param #AI_PATROL_ZONE self +--- @param Wrapper.Controllable#CONTROLLABLE Controllable +function AI_PATROL_ZONE:onbeforeDetect( Controllable, From, Event, To ) + + return self.DetectOn and self.DetectActivated +end + +--- @param #AI_PATROL_ZONE self +--- @param Wrapper.Controllable#CONTROLLABLE Controllable +function AI_PATROL_ZONE:onafterDetect( Controllable, From, Event, To ) + + local Detected = false + + local DetectedTargets = Controllable:GetDetectedTargets() + for TargetID, Target in pairs( DetectedTargets or {} ) do + local TargetObject = Target.object + + if TargetObject and TargetObject:isExist() and TargetObject.id_ < 50000000 then + + local TargetUnit = UNIT:Find( TargetObject ) + local TargetUnitName = TargetUnit:GetName() + + if self.DetectionZone then + if TargetUnit:IsInZone( self.DetectionZone ) then + self:T( {"Detected ", TargetUnit } ) + if self.DetectedUnits[TargetUnit] == nil then + self.DetectedUnits[TargetUnit] = true + end + Detected = true + end + else + if self.DetectedUnits[TargetUnit] == nil then + self.DetectedUnits[TargetUnit] = true + end + Detected = true + end + end + end + + self:__Detect( -self.DetectInterval ) + + if Detected == true then + self:__Detected( 1.5 ) + end + +end + +--- @param Wrapper.Controllable#CONTROLLABLE AIControllable +-- This statis method is called from the route path within the last task at the last waaypoint of the Controllable. +-- Note that this method is required, as triggers the next route when patrolling for the Controllable. +function AI_PATROL_ZONE:_NewPatrolRoute( AIControllable ) + + local PatrolZone = AIControllable:GetState( AIControllable, "PatrolZone" ) -- PatrolCore.Zone#AI_PATROL_ZONE + PatrolZone:__Route( 1 ) +end + + +--- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_PATROL_ZONE:onafterRoute( Controllable, From, Event, To ) + + self:F2() + + -- When RTB, don't allow anymore the routing. + if From == "RTB" then + return + end + + + if self.Controllable:IsAlive() then + -- Determine if the AIControllable is within the PatrolZone. + -- If not, make a waypoint within the to that the AIControllable will fly at maximum speed to that point. + + local PatrolRoute = {} + + -- Calculate the current route point of the controllable as the start point of the route. + -- However, when the controllable is not in the air, + -- the controllable current waypoint is probably the airbase... + -- Thus, if we would take the current waypoint as the startpoint, upon take-off, the controllable flies + -- immediately back to the airbase, and this is not correct. + -- Therefore, when on a runway, get as the current route point a random point within the PatrolZone. + -- This will make the plane fly immediately to the patrol zone. + + if self.Controllable:InAir() == false then + self:E( "Not in the air, finding route path within PatrolZone" ) + local CurrentVec2 = self.Controllable:GetVec2() + --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). + local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() + local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) + local ToPatrolZoneSpeed = self.PatrolMaxSpeed + local CurrentRoutePoint = CurrentPointVec3:RoutePointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TakeOffParking, + POINT_VEC3.RoutePointAction.FromParkingArea, + ToPatrolZoneSpeed, + true + ) + PatrolRoute[#PatrolRoute+1] = CurrentRoutePoint + else + self:E( "In the air, finding route path within PatrolZone" ) + local CurrentVec2 = self.Controllable:GetVec2() + --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). + local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() + local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) + local ToPatrolZoneSpeed = self.PatrolMaxSpeed + local CurrentRoutePoint = CurrentPointVec3:RoutePointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToPatrolZoneSpeed, + true + ) + PatrolRoute[#PatrolRoute+1] = CurrentRoutePoint + end + + + --- Define a random point in the @{Zone}. The AI will fly to that point within the zone. + + --- Find a random 2D point in PatrolZone. + local ToTargetVec2 = self.PatrolZone:GetRandomVec2() + self:T2( ToTargetVec2 ) + + --- Define Speed and Altitude. + local ToTargetAltitude = math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ) + local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) + self:T2( { self.PatrolMinSpeed, self.PatrolMaxSpeed, ToTargetSpeed } ) + + --- Obtain a 3D @{Point} from the 2D point + altitude. + local ToTargetPointVec3 = POINT_VEC3:New( ToTargetVec2.x, ToTargetAltitude, ToTargetVec2.y ) + + --- Create a route point of type air. + local ToTargetRoutePoint = ToTargetPointVec3:RoutePointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true + ) + + --self.CoordTest:SpawnFromVec3( ToTargetPointVec3:GetVec3() ) + + --ToTargetPointVec3:SmokeRed() + + PatrolRoute[#PatrolRoute+1] = ToTargetRoutePoint + + --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... + self.Controllable:WayPointInitialize( PatrolRoute ) + + --- Do a trick, link the NewPatrolRoute function of the PATROLGROUP object to the AIControllable in a temporary variable ... + self.Controllable:SetState( self.Controllable, "PatrolZone", self ) + self.Controllable:WayPointFunction( #PatrolRoute, 1, "AI_PATROL_ZONE:_NewPatrolRoute" ) + + --- NOW ROUTE THE GROUP! + self.Controllable:WayPointExecute( 1, 2 ) + end + +end + +--- @param #AI_PATROL_ZONE self +function AI_PATROL_ZONE:onbeforeStatus() + + return self.CheckStatus +end + +--- @param #AI_PATROL_ZONE self +function AI_PATROL_ZONE:onafterStatus() + self:F2() + + if self.Controllable and self.Controllable:IsAlive() then + + local RTB = false + + local Fuel = self.Controllable:GetUnit(1):GetFuel() + if Fuel < self.PatrolFuelTresholdPercentage then + self:E( self.Controllable:GetName() .. " is out of fuel:" .. Fuel .. ", RTB!" ) + local OldAIControllable = self.Controllable + local AIControllableTemplate = self.Controllable:GetTemplate() + + local OrbitTask = OldAIControllable:TaskOrbitCircle( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ), self.PatrolMinSpeed ) + local TimedOrbitTask = OldAIControllable:TaskControlled( OrbitTask, OldAIControllable:TaskCondition(nil,nil,nil,nil,self.PatrolOutOfFuelOrbitTime,nil ) ) + OldAIControllable:SetTask( TimedOrbitTask, 10 ) + + RTB = true + else + end + + -- TODO: Check GROUP damage function. + local Damage = self.Controllable:GetLife() + if Damage <= self.PatrolDamageTreshold then + self:E( self.Controllable:GetName() .. " is damaged:" .. Damage .. ", RTB!" ) + RTB = true + end + + if RTB == true then + self:RTB() + else + self:__Status( 60 ) -- Execute the Patrol event after 30 seconds. + end + end +end + +--- @param #AI_PATROL_ZONE self +function AI_PATROL_ZONE:onafterRTB() + self:F2() + + if self.Controllable and self.Controllable:IsAlive() then + + self:SetDetectionOff() + self.CheckStatus = false + + local PatrolRoute = {} + + --- Calculate the current route point. + local CurrentVec2 = self.Controllable:GetVec2() + + --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). + local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() + local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) + local ToPatrolZoneSpeed = self.PatrolMaxSpeed + local CurrentRoutePoint = CurrentPointVec3:RoutePointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToPatrolZoneSpeed, + true + ) + + PatrolRoute[#PatrolRoute+1] = CurrentRoutePoint + + --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... + self.Controllable:WayPointInitialize( PatrolRoute ) + + --- NOW ROUTE THE GROUP! + self.Controllable:WayPointExecute( 1, 1 ) + + end + +end + +--- @param #AI_PATROL_ZONE self +function AI_PATROL_ZONE:onafterDead() + self:SetDetectionOff() + self:SetStatusOff() +end + +--- @param #AI_PATROL_ZONE self +-- @param Core.Event#EVENTDATA EventData +function AI_PATROL_ZONE:OnCrash( EventData ) + + if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then + self:E( self.Controllable:GetUnits() ) + if #self.Controllable:GetUnits() == 1 then + self:__Crash( 1, EventData ) + end + end +end + +--- @param #AI_PATROL_ZONE self +-- @param Core.Event#EVENTDATA EventData +function AI_PATROL_ZONE:OnEjection( EventData ) + + if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then + self:__Eject( 1, EventData ) + end +end + +--- @param #AI_PATROL_ZONE self +-- @param Core.Event#EVENTDATA EventData +function AI_PATROL_ZONE:OnPilotDead( EventData ) + + if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then + self:__PilotDead( 1, EventData ) + end +end +--- Single-Player:**Yes** / Multi-Player:**Yes** / AI:**Yes** / Human:**No** / Types:**Air** -- +-- **Provide Close Air Support to friendly ground troops.** +-- +-- ![Banner Image](..\Presentations\AI_CAS\Dia1.JPG) +-- +-- === +-- +-- # 1) @{#AI_CAS_ZONE} class, extends @{AI_Patrol#AI_PATROL_ZONE} +-- +-- @{#AI_CAS_ZONE} derives from the @{AI_Patrol#AI_PATROL_ZONE}, inheriting its methods and behaviour. +-- +-- The @{#AI_CAS_ZONE} class implements the core functions to provide Close Air Support in an Engage @{Zone} by an AIR @{Controllable} or @{Group}. +-- The AI_CAS_ZONE runs a process. It holds an AI in a Patrol Zone and when the AI is commanded to engage, it will fly to an Engage Zone. +-- +-- ![HoldAndEngage](..\Presentations\AI_CAS\Dia3.JPG) +-- +-- The AI_CAS_ZONE is assigned a @{Group} and this must be done before the AI_CAS_ZONE process can be started through the **Start** event. +-- +-- ![Start Event](..\Presentations\AI_CAS\Dia4.JPG) +-- +-- Upon started, The AI will **Route** itself towards the random 3D point within a patrol zone, +-- using a random speed within the given altitude and speed limits. +-- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. +-- This cycle will continue until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. +-- +-- ![Route Event](..\Presentations\AI_CAS\Dia5.JPG) +-- +-- When the AI is commanded to provide Close Air Support (through the event **Engage**), the AI will fly towards the Engage Zone. +-- Any target that is detected in the Engage Zone will be reported and will be destroyed by the AI. +-- +-- ![Engage Event](..\Presentations\AI_CAS\Dia6.JPG) +-- +-- The AI will detect the targets and will only destroy the targets within the Engage Zone. +-- +-- ![Engage Event](..\Presentations\AI_CAS\Dia7.JPG) +-- +-- Every target that is destroyed, is reported< by the AI. +-- +-- ![Engage Event](..\Presentations\AI_CAS\Dia8.JPG) +-- +-- Note that the AI does not know when the Engage Zone is cleared, and therefore will keep circling in the zone. +-- +-- ![Engage Event](..\Presentations\AI_CAS\Dia9.JPG) +-- +-- Until it is notified through the event **Accomplish**, which is to be triggered by an observing party: +-- +-- * a FAC +-- * a timed event +-- * a menu option selected by a human +-- * a condition +-- * others ... +-- +-- ![Engage Event](..\Presentations\AI_CAS\Dia10.JPG) +-- +-- When the AI has accomplished the CAS, it will fly back to the Patrol Zone. +-- +-- ![Engage Event](..\Presentations\AI_CAS\Dia11.JPG) +-- +-- It will keep patrolling there, until it is notified to RTB or move to another CAS Zone. +-- It can be notified to go RTB through the **RTB** event. +-- +-- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. +-- +-- ![Engage Event](..\Presentations\AI_CAS\Dia12.JPG) +-- +-- # 1.1) AI_CAS_ZONE constructor +-- +-- * @{#AI_CAS_ZONE.New}(): Creates a new AI_CAS_ZONE object. +-- +-- ## 1.2) AI_CAS_ZONE is a FSM +-- +-- ![Process](..\Presentations\AI_CAS\Dia2.JPG) +-- +-- ### 1.2.1) AI_CAS_ZONE States +-- +-- * **None** ( Group ): The process is not started yet. +-- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. +-- * **Engaging** ( Group ): The AI is engaging the targets in the Engage Zone, executing CAS. +-- * **Returning** ( Group ): The AI is returning to Base.. +-- +-- ### 1.2.2) AI_CAS_ZONE Events +-- +-- * **Start** ( Group ): Start the process. +-- * **Route** ( Group ): Route the AI to a new random 3D point within the Patrol Zone. +-- * **Engage** ( Group ): Engage the AI to provide CAS in the Engage Zone, destroying any target it finds. +-- * **RTB** ( Group ): Route the AI to the home base. +-- * **Detect** ( Group ): The AI is detecting targets. +-- * **Detected** ( Group ): The AI has detected new targets. +-- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. +-- +-- ==== +-- +-- # **API CHANGE HISTORY** +-- +-- The underlying change log documents the API changes. Please read this carefully. The following notation is used: +-- +-- * **Added** parts are expressed in bold type face. +-- * _Removed_ parts are expressed in italic type face. +-- +-- Hereby the change log: +-- +-- 2017-01-15: Initial class and API. +-- +-- === +-- +-- # **AUTHORS and CONTRIBUTIONS** +-- +-- ### Contributions: +-- +-- * **[Quax](https://forums.eagle.ru/member.php?u=90530)**: Concept, Advice & Testing. +-- * **[Pikey](https://forums.eagle.ru/member.php?u=62835)**: Concept, Advice & Testing. +-- * **[Gunterlund](http://forums.eagle.ru:8080/member.php?u=75036)**: Test case revision. +-- +-- ### Authors: +-- +-- * **FlightControl**: Concept, Design & Programming. +-- +-- @module AI_Cas + + +--- AI_CAS_ZONE class +-- @type AI_CAS_ZONE +-- @field Wrapper.Controllable#CONTROLLABLE AIControllable The @{Controllable} patrolling. +-- @field Core.Zone#ZONE_BASE TargetZone The @{Zone} where the patrol needs to be executed. +-- @extends AI.AI_Patrol#AI_PATROL_ZONE +AI_CAS_ZONE = { + ClassName = "AI_CAS_ZONE", +} + + + +--- Creates a new AI_CAS_ZONE object +-- @param #AI_CAS_ZONE self +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. +-- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h. +-- @param Core.Zone#ZONE_BASE EngageZone The zone where the engage will happen. +-- @param Dcs.DCSTypes#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO +-- @return #AI_CAS_ZONE self +function AI_CAS_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageZone, PatrolAltType ) + + -- Inherits from BASE + local self = BASE:Inherit( self, AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_CAS_ZONE + + self.EngageZone = EngageZone + self.Accomplished = false + + self:SetDetectionZone( self.EngageZone ) + + self:AddTransition( { "Patrolling", "Engaging" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. + + --- OnBefore Transition Handler for Event Engage. + -- @function [parent=#AI_CAS_ZONE] OnBeforeEngage + -- @param #AI_CAS_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone. + -- @param Dcs.DCSTypes#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. + -- @param Dcs.DCSTypes#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. + -- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. + -- @param Dcs.DCSTypes#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. + + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Engage. + -- @function [parent=#AI_CAS_ZONE] OnAfterEngage + -- @param #AI_CAS_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone. + -- @param Dcs.DCSTypes#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. + -- @param Dcs.DCSTypes#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. + -- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. + -- @param Dcs.DCSTypes#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. + + --- Synchronous Event Trigger for Event Engage. + -- @function [parent=#AI_CAS_ZONE] Engage + -- @param #AI_CAS_ZONE self + + --- Asynchronous Event Trigger for Event Engage. + -- @function [parent=#AI_CAS_ZONE] __Engage + -- @param #AI_CAS_ZONE self + -- @param #number Delay The delay in seconds. + +--- OnLeave Transition Handler for State Engaging. +-- @function [parent=#AI_CAS_ZONE] OnLeaveEngaging +-- @param #AI_CAS_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnEnter Transition Handler for State Engaging. +-- @function [parent=#AI_CAS_ZONE] OnEnterEngaging +-- @param #AI_CAS_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + + self:AddTransition( "Engaging", "Target", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. + + self:AddTransition( "Engaging", "Fired", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. + + --- OnBefore Transition Handler for Event Fired. + -- @function [parent=#AI_CAS_ZONE] OnBeforeFired + -- @param #AI_CAS_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Fired. + -- @function [parent=#AI_CAS_ZONE] OnAfterFired + -- @param #AI_CAS_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Fired. + -- @function [parent=#AI_CAS_ZONE] Fired + -- @param #AI_CAS_ZONE self + + --- Asynchronous Event Trigger for Event Fired. + -- @function [parent=#AI_CAS_ZONE] __Fired + -- @param #AI_CAS_ZONE self + -- @param #number Delay The delay in seconds. + + self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. + + --- OnBefore Transition Handler for Event Destroy. + -- @function [parent=#AI_CAS_ZONE] OnBeforeDestroy + -- @param #AI_CAS_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Destroy. + -- @function [parent=#AI_CAS_ZONE] OnAfterDestroy + -- @param #AI_CAS_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Destroy. + -- @function [parent=#AI_CAS_ZONE] Destroy + -- @param #AI_CAS_ZONE self + + --- Asynchronous Event Trigger for Event Destroy. + -- @function [parent=#AI_CAS_ZONE] __Destroy + -- @param #AI_CAS_ZONE self + -- @param #number Delay The delay in seconds. + + + self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. + + --- OnBefore Transition Handler for Event Abort. + -- @function [parent=#AI_CAS_ZONE] OnBeforeAbort + -- @param #AI_CAS_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Abort. + -- @function [parent=#AI_CAS_ZONE] OnAfterAbort + -- @param #AI_CAS_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Abort. + -- @function [parent=#AI_CAS_ZONE] Abort + -- @param #AI_CAS_ZONE self + + --- Asynchronous Event Trigger for Event Abort. + -- @function [parent=#AI_CAS_ZONE] __Abort + -- @param #AI_CAS_ZONE self + -- @param #number Delay The delay in seconds. + + self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. + + --- OnBefore Transition Handler for Event Accomplish. + -- @function [parent=#AI_CAS_ZONE] OnBeforeAccomplish + -- @param #AI_CAS_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Accomplish. + -- @function [parent=#AI_CAS_ZONE] OnAfterAccomplish + -- @param #AI_CAS_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Accomplish. + -- @function [parent=#AI_CAS_ZONE] Accomplish + -- @param #AI_CAS_ZONE self + + --- Asynchronous Event Trigger for Event Accomplish. + -- @function [parent=#AI_CAS_ZONE] __Accomplish + -- @param #AI_CAS_ZONE self + -- @param #number Delay The delay in seconds. + + return self +end + + +--- Set the Engage Zone where the AI is performing CAS. Note that if the EngageZone is changed, the AI needs to re-detect targets. +-- @param #AI_CAS_ZONE self +-- @param Core.Zone#ZONE EngageZone The zone where the AI is performing CAS. +-- @return #AI_CAS_ZONE self +function AI_CAS_ZONE:SetEngageZone( EngageZone ) + self:F2() + + if EngageZone then + self.EngageZone = EngageZone + else + self.EngageZone = nil + end +end + + + +--- onafter State Transition for Event Start. +-- @param #AI_CAS_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_CAS_ZONE:onafterStart( Controllable, From, Event, To ) + + -- Call the parent Start event handler + self:GetParent(self).onafterStart( self, Controllable, From, Event, To ) + self:HandleEvent( EVENTS.Dead, self.OnDead ) + + self:SetDetectionDeactivated() -- When not engaging, set the detection off. +end + +--- @param Wrapper.Controllable#CONTROLLABLE AIControllable +function _NewEngageRoute( AIControllable ) + + AIControllable:T( "NewEngageRoute" ) + local EngageZone = AIControllable:GetState( AIControllable, "EngageZone" ) -- AI.AI_Cas#AI_CAS_ZONE + EngageZone:__Engage( 1 ) +end + +--- @param #AI_CAS_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_CAS_ZONE:onbeforeEngage( Controllable, From, Event, To ) + + if self.Accomplished == true then + return false + end +end + +--- @param #AI_CAS_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_CAS_ZONE:onafterTarget( Controllable, From, Event, To ) + self:E("onafterTarget") + + if Controllable:IsAlive() then + + local AttackTasks = {} + + for DetectedUnit, Detected in pairs( self.DetectedUnits ) do + local DetectedUnit = DetectedUnit -- Wrapper.Unit#UNIT + if DetectedUnit:IsAlive() then + if DetectedUnit:IsInZone( self.EngageZone ) then + if Detected == true then + self:E( {"Target: ", DetectedUnit } ) + self.DetectedUnits[DetectedUnit] = false + local AttackTask = Controllable:EnRouteTaskEngageUnit( DetectedUnit, 1, true, self.EngageWeaponExpend, self.EngageAttackQty, self.EngageDirection, self.EngageAltitude, nil ) + self.Controllable:PushTask( AttackTask, 1 ) + end + end + else + self.DetectedUnits[DetectedUnit] = nil + end + end + + self:__Target( -10 ) + + end +end + + + +--- @param #AI_CAS_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone. +-- @param Dcs.DCSTypes#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. +-- @param Dcs.DCSTypes#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. +-- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. +-- @param Dcs.DCSTypes#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. +function AI_CAS_ZONE:onafterEngage( Controllable, From, Event, To, EngageSpeed, EngageAltitude, EngageWeaponExpend, EngageAttackQty, EngageDirection ) + self:E("onafterEngage") + + self.EngageSpeed = EngageSpeed or 400 + self.EngageAltitude = EngageAltitude or 2000 + self.EngageWeaponExpend = EngageWeaponExpend + self.EngageAttackQty = EngageAttackQty + self.EngageDirection = EngageDirection + + if Controllable:IsAlive() then + + + local EngageRoute = {} + + --- Calculate the current route point. + local CurrentVec2 = self.Controllable:GetVec2() + + --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). + local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() + local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) + local ToEngageZoneSpeed = self.PatrolMaxSpeed + local CurrentRoutePoint = CurrentPointVec3:RoutePointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + self.EngageSpeed, + true + ) + + EngageRoute[#EngageRoute+1] = CurrentRoutePoint + + + if self.Controllable:IsNotInZone( self.EngageZone ) then + + -- Find a random 2D point in EngageZone. + local ToEngageZoneVec2 = self.EngageZone:GetRandomVec2() + self:T2( ToEngageZoneVec2 ) + + -- Obtain a 3D @{Point} from the 2D point + altitude. + local ToEngageZonePointVec3 = POINT_VEC3:New( ToEngageZoneVec2.x, self.EngageAltitude, ToEngageZoneVec2.y ) + + -- Create a route point of type air. + local ToEngageZoneRoutePoint = ToEngageZonePointVec3:RoutePointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + self.EngageSpeed, + true + ) + + EngageRoute[#EngageRoute+1] = ToEngageZoneRoutePoint + + end + + --- Define a random point in the @{Zone}. The AI will fly to that point within the zone. + + --- Find a random 2D point in EngageZone. + local ToTargetVec2 = self.EngageZone:GetRandomVec2() + self:T2( ToTargetVec2 ) + + --- Obtain a 3D @{Point} from the 2D point + altitude. + local ToTargetPointVec3 = POINT_VEC3:New( ToTargetVec2.x, self.EngageAltitude, ToTargetVec2.y ) + + --- Create a route point of type air. + local ToTargetRoutePoint = ToTargetPointVec3:RoutePointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + self.EngageSpeed, + true + ) + + --ToTargetPointVec3:SmokeBlue() + + EngageRoute[#EngageRoute+1] = ToTargetRoutePoint + + + Controllable:OptionROEOpenFire() + Controllable:OptionROTVertical() + +-- local AttackTasks = {} +-- +-- for DetectedUnitID, DetectedUnit in pairs( self.DetectedUnits ) do +-- local DetectedUnit = DetectedUnit -- Wrapper.Unit#UNIT +-- self:T( DetectedUnit ) +-- if DetectedUnit:IsAlive() then +-- if DetectedUnit:IsInZone( self.EngageZone ) then +-- self:E( {"Engaging ", DetectedUnit } ) +-- AttackTasks[#AttackTasks+1] = Controllable:TaskAttackUnit( DetectedUnit ) +-- end +-- else +-- self.DetectedUnits[DetectedUnit] = nil +-- end +-- end +-- +-- EngageRoute[1].task = Controllable:TaskCombo( AttackTasks ) + + --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... + self.Controllable:WayPointInitialize( EngageRoute ) + + --- Do a trick, link the NewEngageRoute function of the object to the AIControllable in a temporary variable ... + self.Controllable:SetState( self.Controllable, "EngageZone", self ) + + self.Controllable:WayPointFunction( #EngageRoute, 1, "_NewEngageRoute" ) + + --- NOW ROUTE THE GROUP! + self.Controllable:WayPointExecute( 1 ) + + self:SetDetectionInterval( 10 ) + self:SetDetectionActivated() + self:__Target( -10 ) -- Start Targetting + end +end + +--- @param #AI_CAS_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @param Core.Event#EVENTDATA EventData +function AI_CAS_ZONE:onafterDestroy( Controllable, From, Event, To, EventData ) + + if EventData.IniUnit then + self.DetectedUnits[EventData.IniUnit] = nil + end + + Controllable:MessageToAll( "Destroyed a target", 15 , "Destroyed!" ) +end + +--- @param #AI_CAS_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_CAS_ZONE:onafterAccomplish( Controllable, From, Event, To ) + self.Accomplished = true + self:SetDetectionDeactivated() +end + +--- @param #AI_CAS_ZONE self +-- @param Core.Event#EVENTDATA EventData +function AI_CAS_ZONE:OnDead( EventData ) + self:T( { "EventDead", EventData } ) + + if EventData.IniDCSUnit then + self:__Destroy( 1, EventData ) + end +end + + +--- Single-Player:**Yes** / Multi-Player:**Yes** / AI:**Yes** / Human:**No** / Types:**Air** -- **Execute Combat Air Patrol (CAP).** +-- +-- ![Banner Image](..\Presentations\AI_CAP\Dia1.JPG) +-- +-- === +-- +-- # 1) @{#AI_CAP_ZONE} class, extends @{AI_CAP#AI_PATROL_ZONE} +-- +-- The @{#AI_CAP_ZONE} class implements the core functions to patrol a @{Zone} by an AI @{Controllable} or @{Group} +-- and automatically engage any airborne enemies that are within a certain range or within a certain zone. +-- +-- ![Process](..\Presentations\AI_CAP\Dia3.JPG) +-- +-- The AI_CAP_ZONE is assigned a @{Group} and this must be done before the AI_CAP_ZONE process can be started using the **Start** event. +-- +-- ![Process](..\Presentations\AI_CAP\Dia4.JPG) +-- +-- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits. +-- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. +-- +-- ![Process](..\Presentations\AI_CAP\Dia5.JPG) +-- +-- This cycle will continue. +-- +-- ![Process](..\Presentations\AI_CAP\Dia6.JPG) +-- +-- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event. +-- +-- ![Process](..\Presentations\AI_CAP\Dia9.JPG) +-- +-- When enemies are detected, the AI will automatically engage the enemy. +-- +-- ![Process](..\Presentations\AI_CAP\Dia10.JPG) +-- +-- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. +-- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. +-- +-- ![Process](..\Presentations\AI_CAP\Dia13.JPG) +-- +-- ## 1.1) AI_CAP_ZONE constructor +-- +-- * @{#AI_CAP_ZONE.New}(): Creates a new AI_CAP_ZONE object. +-- +-- ## 1.2) AI_CAP_ZONE is a FSM +-- +-- ![Process](..\Presentations\AI_CAP\Dia2.JPG) +-- +-- ### 1.2.1) AI_CAP_ZONE States +-- +-- * **None** ( Group ): The process is not started yet. +-- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. +-- * **Engaging** ( Group ): The AI is engaging the bogeys. +-- * **Returning** ( Group ): The AI is returning to Base.. +-- +-- ### 1.2.2) AI_CAP_ZONE Events +-- +-- * **Start** ( Group ): Start the process. +-- * **Route** ( Group ): Route the AI to a new random 3D point within the Patrol Zone. +-- * **Engage** ( Group ): Let the AI engage the bogeys. +-- * **RTB** ( Group ): Route the AI to the home base. +-- * **Detect** ( Group ): The AI is detecting targets. +-- * **Detected** ( Group ): The AI has detected new targets. +-- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. +-- +-- ## 1.3) Set the Range of Engagement +-- +-- ![Range](..\Presentations\AI_CAP\Dia11.JPG) +-- +-- An optional range can be set in meters, +-- that will define when the AI will engage with the detected airborne enemy targets. +-- The range can be beyond or smaller than the range of the Patrol Zone. +-- The range is applied at the position of the AI. +-- Use the method @{AI_CAP#AI_CAP_ZONE.SetEngageRange}() to define that range. +-- +-- ## 1.4) Set the Zone of Engagement +-- +-- ![Zone](..\Presentations\AI_CAP\Dia12.JPG) +-- +-- An optional @{Zone} can be set, +-- that will define when the AI will engage with the detected airborne enemy targets. +-- Use the method @{AI_Cap#AI_CAP_ZONE.SetEngageZone}() to define that Zone. +-- +-- ==== +-- +-- # **API CHANGE HISTORY** +-- +-- The underlying change log documents the API changes. Please read this carefully. The following notation is used: +-- +-- * **Added** parts are expressed in bold type face. +-- * _Removed_ parts are expressed in italic type face. +-- +-- Hereby the change log: +-- +-- 2017-01-15: Initial class and API. +-- +-- === +-- +-- # **AUTHORS and CONTRIBUTIONS** +-- +-- ### Contributions: +-- +-- * **[Quax](https://forums.eagle.ru/member.php?u=90530)**: Concept, Advice & Testing. +-- * **[Pikey](https://forums.eagle.ru/member.php?u=62835)**: Concept, Advice & Testing. +-- * **[Gunterlund](http://forums.eagle.ru:8080/member.php?u=75036)**: Test case revision. +-- * **[Whisper](http://forums.eagle.ru/member.php?u=3829): Testing. +-- * **[Delta99](https://forums.eagle.ru/member.php?u=125166): Testing. +-- +-- ### Authors: +-- +-- * **FlightControl**: Concept, Design & Programming. +-- +-- @module AI_Cap + + +--- AI_CAP_ZONE class +-- @type AI_CAP_ZONE +-- @field Wrapper.Controllable#CONTROLLABLE AIControllable The @{Controllable} patrolling. +-- @field Core.Zone#ZONE_BASE TargetZone The @{Zone} where the patrol needs to be executed. +-- @extends AI.AI_Patrol#AI_PATROL_ZONE +AI_CAP_ZONE = { + ClassName = "AI_CAP_ZONE", +} + + + +--- Creates a new AI_CAP_ZONE object +-- @param #AI_CAP_ZONE self +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. +-- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h. +-- @param Dcs.DCSTypes#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO +-- @return #AI_CAP_ZONE self +function AI_CAP_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) + + -- Inherits from BASE + local self = BASE:Inherit( self, AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_CAP_ZONE + + self.Accomplished = false + self.Engaging = false + + self:AddTransition( { "Patrolling", "Engaging" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAP_ZONE. + + --- OnBefore Transition Handler for Event Engage. + -- @function [parent=#AI_CAP_ZONE] OnBeforeEngage + -- @param #AI_CAP_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Engage. + -- @function [parent=#AI_CAP_ZONE] OnAfterEngage + -- @param #AI_CAP_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Engage. + -- @function [parent=#AI_CAP_ZONE] Engage + -- @param #AI_CAP_ZONE self + + --- Asynchronous Event Trigger for Event Engage. + -- @function [parent=#AI_CAP_ZONE] __Engage + -- @param #AI_CAP_ZONE self + -- @param #number Delay The delay in seconds. + +--- OnLeave Transition Handler for State Engaging. +-- @function [parent=#AI_CAP_ZONE] OnLeaveEngaging +-- @param #AI_CAP_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnEnter Transition Handler for State Engaging. +-- @function [parent=#AI_CAP_ZONE] OnEnterEngaging +-- @param #AI_CAP_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + + self:AddTransition( "Engaging", "Fired", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAP_ZONE. + + --- OnBefore Transition Handler for Event Fired. + -- @function [parent=#AI_CAP_ZONE] OnBeforeFired + -- @param #AI_CAP_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Fired. + -- @function [parent=#AI_CAP_ZONE] OnAfterFired + -- @param #AI_CAP_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Fired. + -- @function [parent=#AI_CAP_ZONE] Fired + -- @param #AI_CAP_ZONE self + + --- Asynchronous Event Trigger for Event Fired. + -- @function [parent=#AI_CAP_ZONE] __Fired + -- @param #AI_CAP_ZONE self + -- @param #number Delay The delay in seconds. + + self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_CAP_ZONE. + + --- OnBefore Transition Handler for Event Destroy. + -- @function [parent=#AI_CAP_ZONE] OnBeforeDestroy + -- @param #AI_CAP_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Destroy. + -- @function [parent=#AI_CAP_ZONE] OnAfterDestroy + -- @param #AI_CAP_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Destroy. + -- @function [parent=#AI_CAP_ZONE] Destroy + -- @param #AI_CAP_ZONE self + + --- Asynchronous Event Trigger for Event Destroy. + -- @function [parent=#AI_CAP_ZONE] __Destroy + -- @param #AI_CAP_ZONE self + -- @param #number Delay The delay in seconds. + + + self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_CAP_ZONE. + + --- OnBefore Transition Handler for Event Abort. + -- @function [parent=#AI_CAP_ZONE] OnBeforeAbort + -- @param #AI_CAP_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Abort. + -- @function [parent=#AI_CAP_ZONE] OnAfterAbort + -- @param #AI_CAP_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Abort. + -- @function [parent=#AI_CAP_ZONE] Abort + -- @param #AI_CAP_ZONE self + + --- Asynchronous Event Trigger for Event Abort. + -- @function [parent=#AI_CAP_ZONE] __Abort + -- @param #AI_CAP_ZONE self + -- @param #number Delay The delay in seconds. + + self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_CAP_ZONE. + + --- OnBefore Transition Handler for Event Accomplish. + -- @function [parent=#AI_CAP_ZONE] OnBeforeAccomplish + -- @param #AI_CAP_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Accomplish. + -- @function [parent=#AI_CAP_ZONE] OnAfterAccomplish + -- @param #AI_CAP_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Accomplish. + -- @function [parent=#AI_CAP_ZONE] Accomplish + -- @param #AI_CAP_ZONE self + + --- Asynchronous Event Trigger for Event Accomplish. + -- @function [parent=#AI_CAP_ZONE] __Accomplish + -- @param #AI_CAP_ZONE self + -- @param #number Delay The delay in seconds. + + return self +end + + +--- Set the Engage Zone which defines where the AI will engage bogies. +-- @param #AI_CAP_ZONE self +-- @param Core.Zone#ZONE EngageZone The zone where the AI is performing CAP. +-- @return #AI_CAP_ZONE self +function AI_CAP_ZONE:SetEngageZone( EngageZone ) + self:F2() + + if EngageZone then + self.EngageZone = EngageZone + else + self.EngageZone = nil + end +end + +--- Set the Engage Range when the AI will engage with airborne enemies. +-- @param #AI_CAP_ZONE self +-- @param #number EngageRange The Engage Range. +-- @return #AI_CAP_ZONE self +function AI_CAP_ZONE:SetEngageRange( EngageRange ) + self:F2() + + if EngageRange then + self.EngageRange = EngageRange + else + self.EngageRange = nil + end +end + +--- onafter State Transition for Event Start. +-- @param #AI_CAP_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_CAP_ZONE:onafterStart( Controllable, From, Event, To ) + + -- Call the parent Start event handler + self:GetParent(self).onafterStart( self, Controllable, From, Event, To ) + +end + +--- @param Wrapper.Controllable#CONTROLLABLE AIControllable +function _NewEngageCapRoute( AIControllable ) + + AIControllable:T( "NewEngageRoute" ) + local EngageZone = AIControllable:GetState( AIControllable, "EngageZone" ) -- AI.AI_Cap#AI_CAP_ZONE + EngageZone:__Engage( 1 ) +end + +--- @param #AI_CAP_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_CAP_ZONE:onbeforeEngage( Controllable, From, Event, To ) + + if self.Accomplished == true then + return false + end +end + +--- @param #AI_CAP_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_CAP_ZONE:onafterDetected( Controllable, From, Event, To ) + + if From ~= "Engaging" then + + local Engage = false + + for DetectedUnit, Detected in pairs( self.DetectedUnits ) do + + local DetectedUnit = DetectedUnit -- Wrapper.Unit#UNIT + self:T( DetectedUnit ) + if DetectedUnit:IsAlive() and DetectedUnit:IsAir() then + Engage = true + break + end + end + + if Engage == true then + self:E( 'Detected -> Engaging' ) + self:__Engage( 1 ) + end + end +end + + + +--- @param #AI_CAP_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_CAP_ZONE:onafterEngage( Controllable, From, Event, To ) + + if Controllable:IsAlive() then + + local EngageRoute = {} + + --- Calculate the current route point. + local CurrentVec2 = self.Controllable:GetVec2() + + --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). + local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() + local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) + local ToEngageZoneSpeed = self.PatrolMaxSpeed + local CurrentRoutePoint = CurrentPointVec3:RoutePointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToEngageZoneSpeed, + true + ) + + EngageRoute[#EngageRoute+1] = CurrentRoutePoint + + + --- Find a random 2D point in PatrolZone. + local ToTargetVec2 = self.PatrolZone:GetRandomVec2() + self:T2( ToTargetVec2 ) + + --- Define Speed and Altitude. + local ToTargetAltitude = math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) + local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) + self:T2( { self.PatrolMinSpeed, self.PatrolMaxSpeed, ToTargetSpeed } ) + + --- Obtain a 3D @{Point} from the 2D point + altitude. + local ToTargetPointVec3 = POINT_VEC3:New( ToTargetVec2.x, ToTargetAltitude, ToTargetVec2.y ) + + --- Create a route point of type air. + local ToPatrolRoutePoint = ToTargetPointVec3:RoutePointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true + ) + + EngageRoute[#EngageRoute+1] = ToPatrolRoutePoint + + Controllable:OptionROEOpenFire() + Controllable:OptionROTPassiveDefense() + + local AttackTasks = {} + + for DetectedUnit, Detected in pairs( self.DetectedUnits ) do + local DetectedUnit = DetectedUnit -- Wrapper.Unit#UNIT + self:T( { DetectedUnit, DetectedUnit:IsAlive(), DetectedUnit:IsAir() } ) + if DetectedUnit:IsAlive() and DetectedUnit:IsAir() then + if self.EngageZone then + if DetectedUnit:IsInZone( self.EngageZone ) then + self:E( {"Within Zone and Engaging ", DetectedUnit } ) + AttackTasks[#AttackTasks+1] = Controllable:TaskAttackUnit( DetectedUnit ) + end + else + if self.EngageRange then + if DetectedUnit:GetPointVec3():Get2DDistance(Controllable:GetPointVec3() ) <= self.EngageRange then + self:E( {"Within Range and Engaging", DetectedUnit } ) + AttackTasks[#AttackTasks+1] = Controllable:TaskAttackUnit( DetectedUnit ) + end + else + AttackTasks[#AttackTasks+1] = Controllable:TaskAttackUnit( DetectedUnit ) + end + end + else + self.DetectedUnits[DetectedUnit] = nil + end + end + + --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... + self.Controllable:WayPointInitialize( EngageRoute ) + + + if #AttackTasks == 0 then + self:E("No targets found -> Going back to Patrolling") + self:__Abort( 1 ) + self:__Route( 1 ) + self:SetDetectionActivated() + else + EngageRoute[1].task = Controllable:TaskCombo( AttackTasks ) + + --- Do a trick, link the NewEngageRoute function of the object to the AIControllable in a temporary variable ... + self.Controllable:SetState( self.Controllable, "EngageZone", self ) + + self.Controllable:WayPointFunction( #EngageRoute, 1, "_NewEngageCapRoute" ) + + self:SetDetectionDeactivated() + end + + --- NOW ROUTE THE GROUP! + self.Controllable:WayPointExecute( 1, 2 ) + + end +end + +--- @param #AI_CAP_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @param Core.Event#EVENTDATA EventData +function AI_CAP_ZONE:onafterDestroy( Controllable, From, Event, To, EventData ) + + if EventData.IniUnit then + self.DetectedUnits[EventData.IniUnit] = nil + end + + Controllable:MessageToAll( "Destroyed a target", 15 , "Destroyed!" ) +end + +--- @param #AI_CAP_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_CAP_ZONE:onafterAccomplish( Controllable, From, Event, To ) + self.Accomplished = true + self:SetDetectionOff() +end + + +---Single-Player:**Yes** / Multi-Player:**Yes** / AI:**Yes** / Human:**No** / Types:**Ground** -- +-- **Management of logical cargo objects, that can be transported from and to transportation carriers.** +-- +-- ![Banner Image](..\Presentations\AI_CARGO\CARGO.JPG) +-- +-- === +-- +-- Cargo can be of various forms, always are composed out of ONE object ( one unit or one static or one slingload crate ): +-- +-- * AI_CARGO_UNIT, represented by a @{Unit} in a @{Group}: Cargo can be represented by a Unit in a Group. Destruction of the Unit will mean that the cargo is lost. +-- * CARGO_STATIC, represented by a @{Static}: Cargo can be represented by a Static. Destruction of the Static will mean that the cargo is lost. +-- * AI_CARGO_PACKAGE, contained in a @{Unit} of a @{Group}: Cargo can be contained within a Unit of a Group. The cargo can be **delivered** by the @{Unit}. If the Unit is destroyed, the cargo will be destroyed also. +-- * AI_CARGO_PACKAGE, Contained in a @{Static}: Cargo can be contained within a Static. The cargo can be **collected** from the @Static. If the @{Static} is destroyed, the cargo will be destroyed. +-- * CARGO_SLINGLOAD, represented by a @{Cargo} that is transportable: Cargo can be represented by a Cargo object that is transportable. Destruction of the Cargo will mean that the cargo is lost. +-- +-- * AI_CARGO_GROUPED, represented by a Group of CARGO_UNITs. +-- +-- # 1) @{#AI_CARGO} class, extends @{Fsm#FSM_PROCESS} +-- +-- The @{#AI_CARGO} class defines the core functions that defines a cargo object within MOOSE. +-- A cargo is a logical object defined that is available for transport, and has a life status within a simulation. +-- +-- The AI_CARGO is a state machine: it manages the different events and states of the cargo. +-- All derived classes from AI_CARGO follow the same state machine, expose the same cargo event functions, and provide the same cargo states. +-- +-- ## 1.2.1) AI_CARGO Events: +-- +-- * @{#AI_CARGO.Board}( ToCarrier ): Boards the cargo to a carrier. +-- * @{#AI_CARGO.Load}( ToCarrier ): Loads the cargo into a carrier, regardless of its position. +-- * @{#AI_CARGO.UnBoard}( ToPointVec2 ): UnBoard the cargo from a carrier. This will trigger a movement of the cargo to the option ToPointVec2. +-- * @{#AI_CARGO.UnLoad}( ToPointVec2 ): UnLoads the cargo from a carrier. +-- * @{#AI_CARGO.Dead}( Controllable ): The cargo is dead. The cargo process will be ended. +-- +-- ## 1.2.2) AI_CARGO States: +-- +-- * **UnLoaded**: The cargo is unloaded from a carrier. +-- * **Boarding**: The cargo is currently boarding (= running) into a carrier. +-- * **Loaded**: The cargo is loaded into a carrier. +-- * **UnBoarding**: The cargo is currently unboarding (=running) from a carrier. +-- * **Dead**: The cargo is dead ... +-- * **End**: The process has come to an end. +-- +-- ## 1.2.3) AI_CARGO state transition methods: +-- +-- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. +-- There are 2 moments when state transition methods will be called by the state machine: +-- +-- * **Leaving** the state. +-- The state transition method needs to start with the name **OnLeave + the name of the state**. +-- If the state transition method returns false, then the processing of the state transition will not be done! +-- If you want to change the behaviour of the AIControllable at this event, return false, +-- but then you'll need to specify your own logic using the AIControllable! +-- +-- * **Entering** the state. +-- The state transition method needs to start with the name **OnEnter + the name of the state**. +-- These state transition methods need to provide a return value, which is specified at the function description. +-- +-- # 2) #AI_CARGO_UNIT class +-- +-- The AI_CARGO_UNIT class defines a cargo that is represented by a UNIT object within the simulator, and can be transported by a carrier. +-- Use the event functions as described above to Load, UnLoad, Board, UnBoard the AI_CARGO_UNIT objects to and from carriers. +-- +-- # 5) #AI_CARGO_GROUPED class +-- +-- The AI_CARGO_GROUPED class defines a cargo that is represented by a group of UNIT objects within the simulator, and can be transported by a carrier. +-- Use the event functions as described above to Load, UnLoad, Board, UnBoard the AI_CARGO_UNIT objects to and from carriers. +-- +-- This module is still under construction, but is described above works already, and will keep working ... +-- +-- @module Cargo + +-- Events + +-- Board + +--- Boards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo to the Carrier. +-- The cargo must be in the **UnLoaded** state. +-- @function [parent=#AI_CARGO] Board +-- @param #AI_CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. + +--- Boards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo to the Carrier. +-- The cargo must be in the **UnLoaded** state. +-- @function [parent=#AI_CARGO] __Board +-- @param #AI_CARGO self +-- @param #number DelaySeconds The amount of seconds to delay the action. +-- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. + + +-- UnBoard + +--- UnBoards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo from the Carrier. +-- The cargo must be in the **Loaded** state. +-- @function [parent=#AI_CARGO] UnBoard +-- @param #AI_CARGO self +-- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Point#POINT_VEC2) to where the cargo should run after onboarding. If not provided, the cargo will run to 60 meters behind the Carrier location. + +--- UnBoards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo from the Carrier. +-- The cargo must be in the **Loaded** state. +-- @function [parent=#AI_CARGO] __UnBoard +-- @param #AI_CARGO self +-- @param #number DelaySeconds The amount of seconds to delay the action. +-- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Point#POINT_VEC2) to where the cargo should run after onboarding. If not provided, the cargo will run to 60 meters behind the Carrier location. + + +-- Load + +--- Loads the cargo to a Carrier. The event will load the cargo into the Carrier regardless of its position. There will be no movement simulated of the cargo loading. +-- The cargo must be in the **UnLoaded** state. +-- @function [parent=#AI_CARGO] Load +-- @param #AI_CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. + +--- Loads the cargo to a Carrier. The event will load the cargo into the Carrier regardless of its position. There will be no movement simulated of the cargo loading. +-- The cargo must be in the **UnLoaded** state. +-- @function [parent=#AI_CARGO] __Load +-- @param #AI_CARGO self +-- @param #number DelaySeconds The amount of seconds to delay the action. +-- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. + + +-- UnLoad + +--- UnLoads the cargo to a Carrier. The event will unload the cargo from the Carrier. There will be no movement simulated of the cargo loading. +-- The cargo must be in the **Loaded** state. +-- @function [parent=#AI_CARGO] UnLoad +-- @param #AI_CARGO self +-- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Point#POINT_VEC2) to where the cargo will be placed after unloading. If not provided, the cargo will be placed 60 meters behind the Carrier location. + +--- UnLoads the cargo to a Carrier. The event will unload the cargo from the Carrier. There will be no movement simulated of the cargo loading. +-- The cargo must be in the **Loaded** state. +-- @function [parent=#AI_CARGO] __UnLoad +-- @param #AI_CARGO self +-- @param #number DelaySeconds The amount of seconds to delay the action. +-- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Point#POINT_VEC2) to where the cargo will be placed after unloading. If not provided, the cargo will be placed 60 meters behind the Carrier location. + +-- State Transition Functions + +-- UnLoaded + +--- @function [parent=#AI_CARGO] OnLeaveUnLoaded +-- @param #AI_CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable +-- @return #boolean + +--- @function [parent=#AI_CARGO] OnEnterUnLoaded +-- @param #AI_CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable + +-- Loaded + +--- @function [parent=#AI_CARGO] OnLeaveLoaded +-- @param #AI_CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable +-- @return #boolean + +--- @function [parent=#AI_CARGO] OnEnterLoaded +-- @param #AI_CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable + +-- Boarding + +--- @function [parent=#AI_CARGO] OnLeaveBoarding +-- @param #AI_CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable +-- @return #boolean + +--- @function [parent=#AI_CARGO] OnEnterBoarding +-- @param #AI_CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable + +-- UnBoarding + +--- @function [parent=#AI_CARGO] OnLeaveUnBoarding +-- @param #AI_CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable +-- @return #boolean + +--- @function [parent=#AI_CARGO] OnEnterUnBoarding +-- @param #AI_CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable + + +-- TODO: Find all Carrier objects and make the type of the Carriers Wrapper.Unit#UNIT in the documentation. + +CARGOS = {} + +do -- AI_CARGO + + --- @type AI_CARGO + -- @extends Core.Fsm#FSM_PROCESS + -- @field #string Type A string defining the type of the cargo. eg. Engineers, Equipment, Screwdrivers. + -- @field #string Name A string defining the name of the cargo. The name is the unique identifier of the cargo. + -- @field #number Weight A number defining the weight of the cargo. The weight is expressed in kg. + -- @field #number ReportRadius (optional) A number defining the radius in meters when the cargo is signalling or reporting to a Carrier. + -- @field #number NearRadius (optional) A number defining the radius in meters when the cargo is near to a Carrier, so that it can be loaded. + -- @field Wrapper.Controllable#CONTROLLABLE CargoObject The alive DCS object representing the cargo. This value can be nil, meaning, that the cargo is not represented anywhere... + -- @field Wrapper.Controllable#CONTROLLABLE CargoCarrier The alive DCS object carrying the cargo. This value can be nil, meaning, that the cargo is not contained anywhere... + -- @field #boolean Slingloadable This flag defines if the cargo can be slingloaded. + -- @field #boolean Moveable This flag defines if the cargo is moveable. + -- @field #boolean Representable This flag defines if the cargo can be represented by a DCS Unit. + -- @field #boolean Containable This flag defines if the cargo can be contained within a DCS Unit. + AI_CARGO = { + ClassName = "AI_CARGO", + Type = nil, + Name = nil, + Weight = nil, + CargoObject = nil, + CargoCarrier = nil, + Representable = false, + Slingloadable = false, + Moveable = false, + Containable = false, + } + +--- @type AI_CARGO.CargoObjects +-- @map < #string, Wrapper.Positionable#POSITIONABLE > The alive POSITIONABLE objects representing the the cargo. + + +--- AI_CARGO Constructor. This class is an abstract class and should not be instantiated. +-- @param #AI_CARGO self +-- @param #string Type +-- @param #string Name +-- @param #number Weight +-- @param #number ReportRadius (optional) +-- @param #number NearRadius (optional) +-- @return #AI_CARGO +function AI_CARGO:New( Type, Name, Weight, ReportRadius, NearRadius ) + + local self = BASE:Inherit( self, FSM:New() ) -- Core.Fsm#FSM_CONTROLLABLE + self:F( { Type, Name, Weight, ReportRadius, NearRadius } ) + + self:SetStartState( "UnLoaded" ) + self:AddTransition( "UnLoaded", "Board", "Boarding" ) + self:AddTransition( "Boarding", "Boarding", "Boarding" ) + self:AddTransition( "Boarding", "Load", "Loaded" ) + self:AddTransition( "UnLoaded", "Load", "Loaded" ) + self:AddTransition( "Loaded", "UnBoard", "UnBoarding" ) + self:AddTransition( "UnBoarding", "UnBoarding", "UnBoarding" ) + self:AddTransition( "UnBoarding", "UnLoad", "UnLoaded" ) + self:AddTransition( "Loaded", "UnLoad", "UnLoaded" ) + + + self.Type = Type + self.Name = Name + self.Weight = Weight + self.ReportRadius = ReportRadius + self.NearRadius = NearRadius + self.CargoObject = nil + self.CargoCarrier = nil + self.Representable = false + self.Slingloadable = false + self.Moveable = false + self.Containable = false + + + self.CargoScheduler = SCHEDULER:New() + + CARGOS[self.Name] = self + + return self +end + + +--- Template method to spawn a new representation of the AI_CARGO in the simulator. +-- @param #AI_CARGO self +-- @return #AI_CARGO +function AI_CARGO:Spawn( PointVec2 ) + self:F() + +end + + +--- Check if CargoCarrier is near the Cargo to be Loaded. +-- @param #AI_CARGO self +-- @param Core.Point#POINT_VEC2 PointVec2 +-- @return #boolean +function AI_CARGO:IsNear( PointVec2 ) + self:F( { PointVec2 } ) + + local Distance = PointVec2:DistanceFromPointVec2( self.CargoObject:GetPointVec2() ) + self:T( Distance ) + + if Distance <= self.NearRadius then + return true + else + return false + end +end + +end + +do -- AI_CARGO_REPRESENTABLE + + --- @type AI_CARGO_REPRESENTABLE + -- @extends #AI_CARGO + AI_CARGO_REPRESENTABLE = { + ClassName = "AI_CARGO_REPRESENTABLE" + } + +--- AI_CARGO_REPRESENTABLE Constructor. +-- @param #AI_CARGO_REPRESENTABLE self +-- @param Wrapper.Controllable#Controllable CargoObject +-- @param #string Type +-- @param #string Name +-- @param #number Weight +-- @param #number ReportRadius (optional) +-- @param #number NearRadius (optional) +-- @return #AI_CARGO_REPRESENTABLE +function AI_CARGO_REPRESENTABLE:New( CargoObject, Type, Name, Weight, ReportRadius, NearRadius ) + local self = BASE:Inherit( self, AI_CARGO:New( Type, Name, Weight, ReportRadius, NearRadius ) ) -- #AI_CARGO + self:F( { Type, Name, Weight, ReportRadius, NearRadius } ) + + return self +end + +--- Route a cargo unit to a PointVec2. +-- @param #AI_CARGO_REPRESENTABLE self +-- @param Core.Point#POINT_VEC2 ToPointVec2 +-- @param #number Speed +-- @return #AI_CARGO_REPRESENTABLE +function AI_CARGO_REPRESENTABLE:RouteTo( ToPointVec2, Speed ) + self:F2( ToPointVec2 ) + + local Points = {} + + local PointStartVec2 = self.CargoObject:GetPointVec2() + + Points[#Points+1] = PointStartVec2:RoutePointGround( Speed ) + Points[#Points+1] = ToPointVec2:RoutePointGround( Speed ) + + local TaskRoute = self.CargoObject:TaskRoute( Points ) + self.CargoObject:SetTask( TaskRoute, 2 ) + return self +end + +end -- AI_CARGO + +do -- AI_CARGO_UNIT + + --- @type AI_CARGO_UNIT + -- @extends #AI_CARGO_REPRESENTABLE + AI_CARGO_UNIT = { + ClassName = "AI_CARGO_UNIT" + } + +--- AI_CARGO_UNIT Constructor. +-- @param #AI_CARGO_UNIT self +-- @param Wrapper.Unit#UNIT CargoUnit +-- @param #string Type +-- @param #string Name +-- @param #number Weight +-- @param #number ReportRadius (optional) +-- @param #number NearRadius (optional) +-- @return #AI_CARGO_UNIT +function AI_CARGO_UNIT:New( CargoUnit, Type, Name, Weight, ReportRadius, NearRadius ) + local self = BASE:Inherit( self, AI_CARGO_REPRESENTABLE:New( CargoUnit, Type, Name, Weight, ReportRadius, NearRadius ) ) -- #AI_CARGO_UNIT + self:F( { Type, Name, Weight, ReportRadius, NearRadius } ) + + self:T( CargoUnit ) + self.CargoObject = CargoUnit + + self:T( self.ClassName ) + + return self +end + +--- Enter UnBoarding State. +-- @param #AI_CARGO_UNIT self +-- @param #string Event +-- @param #string From +-- @param #string To +-- @param Core.Point#POINT_VEC2 ToPointVec2 +function AI_CARGO_UNIT:onenterUnBoarding( From, Event, To, ToPointVec2 ) + self:F() + + local Angle = 180 + local Speed = 10 + local DeployDistance = 5 + local RouteDistance = 60 + + if From == "Loaded" then + + local CargoCarrierPointVec2 = self.CargoCarrier:GetPointVec2() + local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. + local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) + local CargoDeployPointVec2 = CargoCarrierPointVec2:Translate( DeployDistance, CargoDeployHeading ) + local CargoRoutePointVec2 = CargoCarrierPointVec2:Translate( RouteDistance, CargoDeployHeading ) + + -- if there is no ToPointVec2 given, then use the CargoRoutePointVec2 + ToPointVec2 = ToPointVec2 or CargoRoutePointVec2 + + local FromPointVec2 = CargoCarrierPointVec2 + + -- Respawn the group... + if self.CargoObject then + self.CargoObject:ReSpawn( CargoDeployPointVec2:GetVec3(), CargoDeployHeading ) + self.CargoCarrier = nil + + local Points = {} + Points[#Points+1] = FromPointVec2:RoutePointGround( Speed ) + Points[#Points+1] = ToPointVec2:RoutePointGround( Speed ) + + local TaskRoute = self.CargoObject:TaskRoute( Points ) + self.CargoObject:SetTask( TaskRoute, 1 ) + + self:__UnBoarding( 1, ToPointVec2 ) + end + end + +end + +--- Leave UnBoarding State. +-- @param #AI_CARGO_UNIT self +-- @param #string Event +-- @param #string From +-- @param #string To +-- @param Core.Point#POINT_VEC2 ToPointVec2 +function AI_CARGO_UNIT:onleaveUnBoarding( From, Event, To, ToPointVec2 ) + self:F( { ToPointVec2, From, Event, To } ) + + local Angle = 180 + local Speed = 10 + local Distance = 5 + + if From == "UnBoarding" then + if self:IsNear( ToPointVec2 ) then + return true + else + self:__UnBoarding( 1, ToPointVec2 ) + end + return false + end + +end + +--- UnBoard Event. +-- @param #AI_CARGO_UNIT self +-- @param #string Event +-- @param #string From +-- @param #string To +-- @param Core.Point#POINT_VEC2 ToPointVec2 +function AI_CARGO_UNIT:onafterUnBoarding( From, Event, To, ToPointVec2 ) + self:F( { ToPointVec2, From, Event, To } ) + + self.CargoInAir = self.CargoObject:InAir() + + self:T( self.CargoInAir ) + + -- Only unboard the cargo when the carrier is not in the air. + -- (eg. cargo can be on a oil derrick, moving the cargo on the oil derrick will drop the cargo on the sea). + if not self.CargoInAir then + + end + + self:__UnLoad( 1, ToPointVec2 ) + +end + + + +--- Enter UnLoaded State. +-- @param #AI_CARGO_UNIT self +-- @param #string Event +-- @param #string From +-- @param #string To +-- @param Core.Point#POINT_VEC2 +function AI_CARGO_UNIT:onenterUnLoaded( From, Event, To, ToPointVec2 ) + self:F( { ToPointVec2, From, Event, To } ) + + local Angle = 180 + local Speed = 10 + local Distance = 5 + + if From == "Loaded" then + local StartPointVec2 = self.CargoCarrier:GetPointVec2() + local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. + local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) + local CargoDeployPointVec2 = StartPointVec2:Translate( Distance, CargoDeployHeading ) + + ToPointVec2 = ToPointVec2 or POINT_VEC2:New( CargoDeployPointVec2:GetX(), CargoDeployPointVec2:GetY() ) + + -- Respawn the group... + if self.CargoObject then + self.CargoObject:ReSpawn( ToPointVec2:GetVec3(), 0 ) + self.CargoCarrier = nil + end + + end + + if self.OnUnLoadedCallBack then + self.OnUnLoadedCallBack( self, unpack( self.OnUnLoadedParameters ) ) + self.OnUnLoadedCallBack = nil + end + +end + + + +--- Enter Boarding State. +-- @param #AI_CARGO_UNIT self +-- @param #string Event +-- @param #string From +-- @param #string To +-- @param Wrapper.Unit#UNIT CargoCarrier +function AI_CARGO_UNIT:onenterBoarding( From, Event, To, CargoCarrier ) + self:F( { CargoCarrier.UnitName, From, Event, To } ) + + local Speed = 10 + local Angle = 180 + local Distance = 5 + + if From == "UnLoaded" then + local CargoCarrierPointVec2 = CargoCarrier:GetPointVec2() + local CargoCarrierHeading = CargoCarrier:GetHeading() -- Get Heading of object in degrees. + local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) + local CargoDeployPointVec2 = CargoCarrierPointVec2:Translate( Distance, CargoDeployHeading ) + + local Points = {} + + local PointStartVec2 = self.CargoObject:GetPointVec2() + + Points[#Points+1] = PointStartVec2:RoutePointGround( Speed ) + Points[#Points+1] = CargoDeployPointVec2:RoutePointGround( Speed ) + + local TaskRoute = self.CargoObject:TaskRoute( Points ) + self.CargoObject:SetTask( TaskRoute, 2 ) + end + +end + +--- Leave Boarding State. +-- @param #AI_CARGO_UNIT self +-- @param #string Event +-- @param #string From +-- @param #string To +-- @param Wrapper.Unit#UNIT CargoCarrier +function AI_CARGO_UNIT:onleaveBoarding( From, Event, To, CargoCarrier ) + self:F( { CargoCarrier.UnitName, From, Event, To } ) + + if self:IsNear( CargoCarrier:GetPointVec2() ) then + self:__Load( 1, CargoCarrier ) + return true + else + self:__Boarding( 1, CargoCarrier ) + end + return false +end + +--- Loaded State. +-- @param #AI_CARGO_UNIT self +-- @param #string Event +-- @param #string From +-- @param #string To +-- @param Wrapper.Unit#UNIT CargoCarrier +function AI_CARGO_UNIT:onenterLoaded( From, Event, To, CargoCarrier ) + self:F() + + self.CargoCarrier = CargoCarrier + + -- Only destroy the CargoObject is if there is a CargoObject (packages don't have CargoObjects). + if self.CargoObject then + self:T("Destroying") + self.CargoObject:Destroy() + end +end + + +--- Board Event. +-- @param #AI_CARGO_UNIT self +-- @param #string Event +-- @param #string From +-- @param #string To +function AI_CARGO_UNIT:onafterBoard( From, Event, To, CargoCarrier ) + self:F() + + self.CargoInAir = self.CargoObject:InAir() + + self:T( self.CargoInAir ) + + -- Only move the group to the carrier when the cargo is not in the air + -- (eg. cargo can be on a oil derrick, moving the cargo on the oil derrick will drop the cargo on the sea). + if not self.CargoInAir then + self:Load( CargoCarrier ) + end + +end + +end + +do -- AI_CARGO_PACKAGE + + --- @type AI_CARGO_PACKAGE + -- @extends #AI_CARGO_REPRESENTABLE + AI_CARGO_PACKAGE = { + ClassName = "AI_CARGO_PACKAGE" + } + +--- AI_CARGO_PACKAGE Constructor. +-- @param #AI_CARGO_PACKAGE self +-- @param Wrapper.Unit#UNIT CargoCarrier The UNIT carrying the package. +-- @param #string Type +-- @param #string Name +-- @param #number Weight +-- @param #number ReportRadius (optional) +-- @param #number NearRadius (optional) +-- @return #AI_CARGO_PACKAGE +function AI_CARGO_PACKAGE:New( CargoCarrier, Type, Name, Weight, ReportRadius, NearRadius ) + local self = BASE:Inherit( self, AI_CARGO_REPRESENTABLE:New( CargoCarrier, Type, Name, Weight, ReportRadius, NearRadius ) ) -- #AI_CARGO_PACKAGE + self:F( { Type, Name, Weight, ReportRadius, NearRadius } ) + + self:T( CargoCarrier ) + self.CargoCarrier = CargoCarrier + + return self +end + +--- Board Event. +-- @param #AI_CARGO_PACKAGE self +-- @param #string Event +-- @param #string From +-- @param #string To +-- @param Wrapper.Unit#UNIT CargoCarrier +-- @param #number Speed +-- @param #number BoardDistance +-- @param #number Angle +function AI_CARGO_PACKAGE:onafterOnBoard( From, Event, To, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) + self:F() + + self.CargoInAir = self.CargoCarrier:InAir() + + self:T( self.CargoInAir ) + + -- Only move the CargoCarrier to the New CargoCarrier when the New CargoCarrier is not in the air. + if not self.CargoInAir then + + local Points = {} + + local StartPointVec2 = self.CargoCarrier:GetPointVec2() + local CargoCarrierHeading = CargoCarrier:GetHeading() -- Get Heading of object in degrees. + local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) + self:T( { CargoCarrierHeading, CargoDeployHeading } ) + local CargoDeployPointVec2 = CargoCarrier:GetPointVec2():Translate( BoardDistance, CargoDeployHeading ) + + Points[#Points+1] = StartPointVec2:RoutePointGround( Speed ) + Points[#Points+1] = CargoDeployPointVec2:RoutePointGround( Speed ) + + local TaskRoute = self.CargoCarrier:TaskRoute( Points ) + self.CargoCarrier:SetTask( TaskRoute, 1 ) + end + + self:Boarded( CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) + +end + +--- Check if CargoCarrier is near the Cargo to be Loaded. +-- @param #AI_CARGO_PACKAGE self +-- @param Wrapper.Unit#UNIT CargoCarrier +-- @return #boolean +function AI_CARGO_PACKAGE:IsNear( CargoCarrier ) + self:F() + + local CargoCarrierPoint = CargoCarrier:GetPointVec2() + + local Distance = CargoCarrierPoint:DistanceFromPointVec2( self.CargoCarrier:GetPointVec2() ) + self:T( Distance ) + + if Distance <= self.NearRadius then + return true + else + return false + end +end + +--- Boarded Event. +-- @param #AI_CARGO_PACKAGE self +-- @param #string Event +-- @param #string From +-- @param #string To +-- @param Wrapper.Unit#UNIT CargoCarrier +function AI_CARGO_PACKAGE:onafterOnBoarded( From, Event, To, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) + self:F() + + if self:IsNear( CargoCarrier ) then + self:__Load( 1, CargoCarrier, Speed, LoadDistance, Angle ) + else + self:__Boarded( 1, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) + end +end + +--- UnBoard Event. +-- @param #AI_CARGO_PACKAGE self +-- @param #string Event +-- @param #string From +-- @param #string To +-- @param #number Speed +-- @param #number UnLoadDistance +-- @param #number UnBoardDistance +-- @param #number Radius +-- @param #number Angle +function AI_CARGO_PACKAGE:onafterUnBoard( From, Event, To, CargoCarrier, Speed, UnLoadDistance, UnBoardDistance, Radius, Angle ) + self:F() + + self.CargoInAir = self.CargoCarrier:InAir() + + self:T( self.CargoInAir ) + + -- Only unboard the cargo when the carrier is not in the air. + -- (eg. cargo can be on a oil derrick, moving the cargo on the oil derrick will drop the cargo on the sea). + if not self.CargoInAir then + + self:_Next( self.FsmP.UnLoad, UnLoadDistance, Angle ) + + local Points = {} + + local StartPointVec2 = CargoCarrier:GetPointVec2() + local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. + local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) + self:T( { CargoCarrierHeading, CargoDeployHeading } ) + local CargoDeployPointVec2 = StartPointVec2:Translate( UnBoardDistance, CargoDeployHeading ) + + Points[#Points+1] = StartPointVec2:RoutePointGround( Speed ) + Points[#Points+1] = CargoDeployPointVec2:RoutePointGround( Speed ) + + local TaskRoute = CargoCarrier:TaskRoute( Points ) + CargoCarrier:SetTask( TaskRoute, 1 ) + end + + self:__UnBoarded( 1 , CargoCarrier, Speed ) + +end + +--- UnBoarded Event. +-- @param #AI_CARGO_PACKAGE self +-- @param #string Event +-- @param #string From +-- @param #string To +-- @param Wrapper.Unit#UNIT CargoCarrier +function AI_CARGO_PACKAGE:onafterUnBoarded( From, Event, To, CargoCarrier, Speed ) + self:F() + + if self:IsNear( CargoCarrier ) then + self:__UnLoad( 1, CargoCarrier, Speed ) + else + self:__UnBoarded( 1, CargoCarrier, Speed ) + end +end + +--- Load Event. +-- @param #AI_CARGO_PACKAGE self +-- @param #string Event +-- @param #string From +-- @param #string To +-- @param Wrapper.Unit#UNIT CargoCarrier +-- @param #number Speed +-- @param #number LoadDistance +-- @param #number Angle +function AI_CARGO_PACKAGE:onafterLoad( From, Event, To, CargoCarrier, Speed, LoadDistance, Angle ) + self:F() + + self.CargoCarrier = CargoCarrier + + local StartPointVec2 = self.CargoCarrier:GetPointVec2() + local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. + local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) + local CargoDeployPointVec2 = StartPointVec2:Translate( LoadDistance, CargoDeployHeading ) + + local Points = {} + Points[#Points+1] = StartPointVec2:RoutePointGround( Speed ) + Points[#Points+1] = CargoDeployPointVec2:RoutePointGround( Speed ) + + local TaskRoute = self.CargoCarrier:TaskRoute( Points ) + self.CargoCarrier:SetTask( TaskRoute, 1 ) + +end + +--- UnLoad Event. +-- @param #AI_CARGO_PACKAGE self +-- @param #string Event +-- @param #string From +-- @param #string To +-- @param #number Distance +-- @param #number Angle +function AI_CARGO_PACKAGE:onafterUnLoad( From, Event, To, CargoCarrier, Speed, Distance, Angle ) + self:F() + + local StartPointVec2 = self.CargoCarrier:GetPointVec2() + local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. + local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) + local CargoDeployPointVec2 = StartPointVec2:Translate( Distance, CargoDeployHeading ) + + self.CargoCarrier = CargoCarrier + + local Points = {} + Points[#Points+1] = StartPointVec2:RoutePointGround( Speed ) + Points[#Points+1] = CargoDeployPointVec2:RoutePointGround( Speed ) + + local TaskRoute = self.CargoCarrier:TaskRoute( Points ) + self.CargoCarrier:SetTask( TaskRoute, 1 ) + +end + + +end + +do -- AI_CARGO_GROUP + + --- @type AI_CARGO_GROUP + -- @extends AI.AI_Cargo#AI_CARGO + -- @field Set#SET_BASE CargoSet A set of cargo objects. + -- @field #string Name A string defining the name of the cargo group. The name is the unique identifier of the cargo. + AI_CARGO_GROUP = { + ClassName = "AI_CARGO_GROUP", + } + +--- AI_CARGO_GROUP constructor. +-- @param #AI_CARGO_GROUP self +-- @param Core.Set#Set_BASE CargoSet +-- @param #string Type +-- @param #string Name +-- @param #number Weight +-- @param #number ReportRadius (optional) +-- @param #number NearRadius (optional) +-- @return #AI_CARGO_GROUP +function AI_CARGO_GROUP:New( CargoSet, Type, Name, ReportRadius, NearRadius ) + local self = BASE:Inherit( self, AI_CARGO:New( Type, Name, 0, ReportRadius, NearRadius ) ) -- #AI_CARGO_GROUP + self:F( { Type, Name, ReportRadius, NearRadius } ) + + self.CargoSet = CargoSet + + + return self +end + +end -- AI_CARGO_GROUP + +do -- AI_CARGO_GROUPED + + --- @type AI_CARGO_GROUPED + -- @extends AI.AI_Cargo#AI_CARGO_GROUP + AI_CARGO_GROUPED = { + ClassName = "AI_CARGO_GROUPED", + } + +--- AI_CARGO_GROUPED constructor. +-- @param #AI_CARGO_GROUPED self +-- @param Core.Set#Set_BASE CargoSet +-- @param #string Type +-- @param #string Name +-- @param #number Weight +-- @param #number ReportRadius (optional) +-- @param #number NearRadius (optional) +-- @return #AI_CARGO_GROUPED +function AI_CARGO_GROUPED:New( CargoSet, Type, Name, ReportRadius, NearRadius ) + local self = BASE:Inherit( self, AI_CARGO_GROUP:New( CargoSet, Type, Name, ReportRadius, NearRadius ) ) -- #AI_CARGO_GROUPED + self:F( { Type, Name, ReportRadius, NearRadius } ) + + return self +end + +--- Enter Boarding State. +-- @param #AI_CARGO_GROUPED self +-- @param Wrapper.Unit#UNIT CargoCarrier +-- @param #string Event +-- @param #string From +-- @param #string To +function AI_CARGO_GROUPED:onenterBoarding( From, Event, To, CargoCarrier ) + self:F( { CargoCarrier.UnitName, From, Event, To } ) + + if From == "UnLoaded" then + + -- For each Cargo object within the AI_CARGO_GROUPED, route each object to the CargoLoadPointVec2 + self.CargoSet:ForEach( + function( Cargo ) + Cargo:__Board( 1, CargoCarrier ) + end + ) + + self:__Boarding( 1, CargoCarrier ) + end + +end + +--- Enter Loaded State. +-- @param #AI_CARGO_GROUPED self +-- @param Wrapper.Unit#UNIT CargoCarrier +-- @param #string Event +-- @param #string From +-- @param #string To +function AI_CARGO_GROUPED:onenterLoaded( From, Event, To, CargoCarrier ) + self:F( { CargoCarrier.UnitName, From, Event, To } ) + + if From == "UnLoaded" then + -- For each Cargo object within the AI_CARGO_GROUPED, load each cargo to the CargoCarrier. + for CargoID, Cargo in pairs( self.CargoSet:GetSet() ) do + Cargo:Load( CargoCarrier ) + end + end +end + +--- Leave Boarding State. +-- @param #AI_CARGO_GROUPED self +-- @param Wrapper.Unit#UNIT CargoCarrier +-- @param #string Event +-- @param #string From +-- @param #string To +function AI_CARGO_GROUPED:onleaveBoarding( From, Event, To, CargoCarrier ) + self:F( { CargoCarrier.UnitName, From, Event, To } ) + + local Boarded = true + + -- For each Cargo object within the AI_CARGO_GROUPED, route each object to the CargoLoadPointVec2 + for CargoID, Cargo in pairs( self.CargoSet:GetSet() ) do + self:T( Cargo.current ) + if not Cargo:is( "Loaded" ) then + Boarded = false + end + end + + if not Boarded then + self:__Boarding( 1, CargoCarrier ) + else + self:__Load( 1, CargoCarrier ) + end + return Boarded +end + +--- Enter UnBoarding State. +-- @param #AI_CARGO_GROUPED self +-- @param Core.Point#POINT_VEC2 ToPointVec2 +-- @param #string Event +-- @param #string From +-- @param #string To +function AI_CARGO_GROUPED:onenterUnBoarding( From, Event, To, ToPointVec2 ) + self:F() + + local Timer = 1 + + if From == "Loaded" then + + -- For each Cargo object within the AI_CARGO_GROUPED, route each object to the CargoLoadPointVec2 + self.CargoSet:ForEach( + function( Cargo ) + Cargo:__UnBoard( Timer, ToPointVec2 ) + Timer = Timer + 10 + end + ) + + self:__UnBoarding( 1, ToPointVec2 ) + end + +end + +--- Leave UnBoarding State. +-- @param #AI_CARGO_GROUPED self +-- @param Core.Point#POINT_VEC2 ToPointVec2 +-- @param #string Event +-- @param #string From +-- @param #string To +function AI_CARGO_GROUPED:onleaveUnBoarding( From, Event, To, ToPointVec2 ) + self:F( { ToPointVec2, From, Event, To } ) + + local Angle = 180 + local Speed = 10 + local Distance = 5 + + if From == "UnBoarding" then + local UnBoarded = true + + -- For each Cargo object within the AI_CARGO_GROUPED, route each object to the CargoLoadPointVec2 + for CargoID, Cargo in pairs( self.CargoSet:GetSet() ) do + self:T( Cargo.current ) + if not Cargo:is( "UnLoaded" ) then + UnBoarded = false + end + end + + if UnBoarded then + return true + else + self:__UnBoarding( 1, ToPointVec2 ) + end + + return false + end + +end + +--- UnBoard Event. +-- @param #AI_CARGO_GROUPED self +-- @param Core.Point#POINT_VEC2 ToPointVec2 +-- @param #string Event +-- @param #string From +-- @param #string To +function AI_CARGO_GROUPED:onafterUnBoarding( From, Event, To, ToPointVec2 ) + self:F( { ToPointVec2, From, Event, To } ) + + self:__UnLoad( 1, ToPointVec2 ) +end + + + +--- Enter UnLoaded State. +-- @param #AI_CARGO_GROUPED self +-- @param Core.Point#POINT_VEC2 +-- @param #string Event +-- @param #string From +-- @param #string To +function AI_CARGO_GROUPED:onenterUnLoaded( From, Event, To, ToPointVec2 ) + self:F( { ToPointVec2, From, Event, To } ) + + if From == "Loaded" then + + -- For each Cargo object within the AI_CARGO_GROUPED, route each object to the CargoLoadPointVec2 + self.CargoSet:ForEach( + function( Cargo ) + Cargo:UnLoad( ToPointVec2 ) + end + ) + + end + +end + +end -- AI_CARGO_GROUPED + + + +--- (SP) (MP) (FSM) Accept or reject process for player (task) assignments. +-- +-- === +-- +-- # @{#ACT_ASSIGN} FSM template class, extends @{Fsm#FSM_PROCESS} +-- +-- ## ACT_ASSIGN state machine: +-- +-- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur. +-- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below. +-- Each derived class follows exactly the same process, using the same events and following the same state transitions, +-- but will have **different implementation behaviour** upon each event or state transition. +-- +-- ### ACT_ASSIGN **Events**: +-- +-- These are the events defined in this class: +-- +-- * **Start**: Start the tasking acceptance process. +-- * **Assign**: Assign the task. +-- * **Reject**: Reject the task.. +-- +-- ### ACT_ASSIGN **Event methods**: +-- +-- Event methods are available (dynamically allocated by the state machine), that accomodate for state transitions occurring in the process. +-- There are two types of event methods, which you can use to influence the normal mechanisms in the state machine: +-- +-- * **Immediate**: The event method has exactly the name of the event. +-- * **Delayed**: The event method starts with a __ + the name of the event. The first parameter of the event method is a number value, expressing the delay in seconds when the event will be executed. +-- +-- ### ACT_ASSIGN **States**: +-- +-- * **UnAssigned**: The player has not accepted the task. +-- * **Assigned (*)**: The player has accepted the task. +-- * **Rejected (*)**: The player has not accepted the task. +-- * **Waiting**: The process is awaiting player feedback. +-- * **Failed (*)**: The process has failed. +-- +-- (*) End states of the process. +-- +-- ### ACT_ASSIGN state transition methods: +-- +-- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. +-- There are 2 moments when state transition methods will be called by the state machine: +-- +-- * **Before** the state transition. +-- The state transition method needs to start with the name **OnBefore + the name of the state**. +-- If the state transition method returns false, then the processing of the state transition will not be done! +-- If you want to change the behaviour of the AIControllable at this event, return false, +-- but then you'll need to specify your own logic using the AIControllable! +-- +-- * **After** the state transition. +-- The state transition method needs to start with the name **OnAfter + the name of the state**. +-- These state transition methods need to provide a return value, which is specified at the function description. +-- +-- === +-- +-- # 1) @{#ACT_ASSIGN_ACCEPT} class, extends @{Fsm.Assign#ACT_ASSIGN} +-- +-- The ACT_ASSIGN_ACCEPT class accepts by default a task for a player. No player intervention is allowed to reject the task. +-- +-- ## 1.1) ACT_ASSIGN_ACCEPT constructor: +-- +-- * @{#ACT_ASSIGN_ACCEPT.New}(): Creates a new ACT_ASSIGN_ACCEPT object. +-- +-- === +-- +-- # 2) @{#ACT_ASSIGN_MENU_ACCEPT} class, extends @{Fsm.Assign#ACT_ASSIGN} +-- +-- The ACT_ASSIGN_MENU_ACCEPT class accepts a task when the player accepts the task through an added menu option. +-- This assignment type is useful to conditionally allow the player to choose whether or not he would accept the task. +-- The assignment type also allows to reject the task. +-- +-- ## 2.1) ACT_ASSIGN_MENU_ACCEPT constructor: +-- ----------------------------------------- +-- +-- * @{#ACT_ASSIGN_MENU_ACCEPT.New}(): Creates a new ACT_ASSIGN_MENU_ACCEPT object. +-- +-- === +-- +-- @module Assign + + +do -- ACT_ASSIGN + + --- ACT_ASSIGN class + -- @type ACT_ASSIGN + -- @field Tasking.Task#TASK Task + -- @field Wrapper.Unit#UNIT ProcessUnit + -- @field Core.Zone#ZONE_BASE TargetZone + -- @extends Core.Fsm#FSM_PROCESS + ACT_ASSIGN = { + ClassName = "ACT_ASSIGN", + } + + + --- Creates a new task assignment state machine. The process will accept the task by default, no player intervention accepted. + -- @param #ACT_ASSIGN self + -- @return #ACT_ASSIGN The task acceptance process. + function ACT_ASSIGN:New() + + -- Inherits from BASE + local self = BASE:Inherit( self, FSM_PROCESS:New( "ACT_ASSIGN" ) ) -- Core.Fsm#FSM_PROCESS + + self:AddTransition( "UnAssigned", "Start", "Waiting" ) + self:AddTransition( "Waiting", "Assign", "Assigned" ) + self:AddTransition( "Waiting", "Reject", "Rejected" ) + self:AddTransition( "*", "Fail", "Failed" ) + + self:AddEndState( "Assigned" ) + self:AddEndState( "Rejected" ) + self:AddEndState( "Failed" ) + + self:SetStartState( "UnAssigned" ) + + return self + end + +end -- ACT_ASSIGN + + + +do -- ACT_ASSIGN_ACCEPT + + --- ACT_ASSIGN_ACCEPT class + -- @type ACT_ASSIGN_ACCEPT + -- @field Tasking.Task#TASK Task + -- @field Wrapper.Unit#UNIT ProcessUnit + -- @field Core.Zone#ZONE_BASE TargetZone + -- @extends #ACT_ASSIGN + ACT_ASSIGN_ACCEPT = { + ClassName = "ACT_ASSIGN_ACCEPT", + } + + + --- Creates a new task assignment state machine. The process will accept the task by default, no player intervention accepted. + -- @param #ACT_ASSIGN_ACCEPT self + -- @param #string TaskBriefing + function ACT_ASSIGN_ACCEPT:New( TaskBriefing ) + + local self = BASE:Inherit( self, ACT_ASSIGN:New() ) -- #ACT_ASSIGN_ACCEPT + + self.TaskBriefing = TaskBriefing + + return self + end + + function ACT_ASSIGN_ACCEPT:Init( FsmAssign ) + + self.TaskBriefing = FsmAssign.TaskBriefing + end + + --- StateMachine callback function + -- @param #ACT_ASSIGN_ACCEPT self + -- @param Wrapper.Unit#UNIT ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ASSIGN_ACCEPT:onafterStart( ProcessUnit, From, Event, To ) + self:E( { ProcessUnit, From, Event, To } ) + + self:__Assign( 1 ) + end + + --- StateMachine callback function + -- @param #ACT_ASSIGN_ACCEPT self + -- @param Wrapper.Unit#UNIT ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ASSIGN_ACCEPT:onenterAssigned( ProcessUnit, From, Event, To ) + env.info( "in here" ) + self:E( { ProcessUnit, From, Event, To } ) + + local ProcessGroup = ProcessUnit:GetGroup() + + self:Message( "You are assigned to the task " .. self.Task:GetName() ) + + self.Task:Assign() + end + +end -- ACT_ASSIGN_ACCEPT + + +do -- ACT_ASSIGN_MENU_ACCEPT + + --- ACT_ASSIGN_MENU_ACCEPT class + -- @type ACT_ASSIGN_MENU_ACCEPT + -- @field Tasking.Task#TASK Task + -- @field Wrapper.Unit#UNIT ProcessUnit + -- @field Core.Zone#ZONE_BASE TargetZone + -- @extends #ACT_ASSIGN + ACT_ASSIGN_MENU_ACCEPT = { + ClassName = "ACT_ASSIGN_MENU_ACCEPT", + } + + --- Init. + -- @param #ACT_ASSIGN_MENU_ACCEPT self + -- @param #string TaskName + -- @param #string TaskBriefing + -- @return #ACT_ASSIGN_MENU_ACCEPT self + function ACT_ASSIGN_MENU_ACCEPT:New( TaskName, TaskBriefing ) + + -- Inherits from BASE + local self = BASE:Inherit( self, ACT_ASSIGN:New() ) -- #ACT_ASSIGN_MENU_ACCEPT + + self.TaskName = TaskName + self.TaskBriefing = TaskBriefing + + return self + end + + function ACT_ASSIGN_MENU_ACCEPT:Init( FsmAssign ) + + self.TaskName = FsmAssign.TaskName + self.TaskBriefing = FsmAssign.TaskBriefing + end + + + --- Creates a new task assignment state machine. The process will request from the menu if it accepts the task, if not, the unit is removed from the simulator. + -- @param #ACT_ASSIGN_MENU_ACCEPT self + -- @param #string TaskName + -- @param #string TaskBriefing + -- @return #ACT_ASSIGN_MENU_ACCEPT self + function ACT_ASSIGN_MENU_ACCEPT:Init( TaskName, TaskBriefing ) + + self.TaskBriefing = TaskBriefing + self.TaskName = TaskName + + return self + end + + --- StateMachine callback function + -- @param #ACT_ASSIGN_MENU_ACCEPT self + -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ASSIGN_MENU_ACCEPT:onafterStart( ProcessUnit, From, Event, To ) + self:E( { ProcessUnit, From, Event, To } ) + + self:Message( "Access the radio menu to accept the task. You have 30 seconds or the assignment will be cancelled." ) + + local ProcessGroup = ProcessUnit:GetGroup() + + self.Menu = MENU_GROUP:New( ProcessGroup, "Task " .. self.TaskName .. " acceptance" ) + self.MenuAcceptTask = MENU_GROUP_COMMAND:New( ProcessGroup, "Accept task " .. self.TaskName, self.Menu, self.MenuAssign, self ) + self.MenuRejectTask = MENU_GROUP_COMMAND:New( ProcessGroup, "Reject task " .. self.TaskName, self.Menu, self.MenuReject, self ) + end + + --- Menu function. + -- @param #ACT_ASSIGN_MENU_ACCEPT self + function ACT_ASSIGN_MENU_ACCEPT:MenuAssign() + self:E( ) + + self:__Assign( 1 ) + end + + --- Menu function. + -- @param #ACT_ASSIGN_MENU_ACCEPT self + function ACT_ASSIGN_MENU_ACCEPT:MenuReject() + self:E( ) + + self:__Reject( 1 ) + end + + --- StateMachine callback function + -- @param #ACT_ASSIGN_MENU_ACCEPT self + -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ASSIGN_MENU_ACCEPT:onafterAssign( ProcessUnit, From, Event, To ) + self:E( { ProcessUnit.UnitNameFrom, Event, To } ) + + self.Menu:Remove() + end + + --- StateMachine callback function + -- @param #ACT_ASSIGN_MENU_ACCEPT self + -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ASSIGN_MENU_ACCEPT:onafterReject( ProcessUnit, From, Event, To ) + self:E( { ProcessUnit.UnitName, From, Event, To } ) + + self.Menu:Remove() + --TODO: need to resolve this problem ... it has to do with the events ... + --self.Task:UnAssignFromUnit( ProcessUnit )needs to become a callback funtion call upon the event + ProcessUnit:Destroy() + end + +end -- ACT_ASSIGN_MENU_ACCEPT +--- (SP) (MP) (FSM) Route AI or players through waypoints or to zones. +-- +-- === +-- +-- # @{#ACT_ROUTE} FSM class, extends @{Fsm#FSM_PROCESS} +-- +-- ## ACT_ROUTE state machine: +-- +-- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur. +-- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below. +-- Each derived class follows exactly the same process, using the same events and following the same state transitions, +-- but will have **different implementation behaviour** upon each event or state transition. +-- +-- ### ACT_ROUTE **Events**: +-- +-- These are the events defined in this class: +-- +-- * **Start**: The process is started. The process will go into the Report state. +-- * **Report**: The process is reporting to the player the route to be followed. +-- * **Route**: The process is routing the controllable. +-- * **Pause**: The process is pausing the route of the controllable. +-- * **Arrive**: The controllable has arrived at a route point. +-- * **More**: There are more route points that need to be followed. The process will go back into the Report state. +-- * **NoMore**: There are no more route points that need to be followed. The process will go into the Success state. +-- +-- ### ACT_ROUTE **Event methods**: +-- +-- Event methods are available (dynamically allocated by the state machine), that accomodate for state transitions occurring in the process. +-- There are two types of event methods, which you can use to influence the normal mechanisms in the state machine: +-- +-- * **Immediate**: The event method has exactly the name of the event. +-- * **Delayed**: The event method starts with a __ + the name of the event. The first parameter of the event method is a number value, expressing the delay in seconds when the event will be executed. +-- +-- ### ACT_ROUTE **States**: +-- +-- * **None**: The controllable did not receive route commands. +-- * **Arrived (*)**: The controllable has arrived at a route point. +-- * **Aborted (*)**: The controllable has aborted the route path. +-- * **Routing**: The controllable is understay to the route point. +-- * **Pausing**: The process is pausing the routing. AI air will go into hover, AI ground will stop moving. Players can fly around. +-- * **Success (*)**: All route points were reached. +-- * **Failed (*)**: The process has failed. +-- +-- (*) End states of the process. +-- +-- ### ACT_ROUTE state transition methods: +-- +-- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. +-- There are 2 moments when state transition methods will be called by the state machine: +-- +-- * **Before** the state transition. +-- The state transition method needs to start with the name **OnBefore + the name of the state**. +-- If the state transition method returns false, then the processing of the state transition will not be done! +-- If you want to change the behaviour of the AIControllable at this event, return false, +-- but then you'll need to specify your own logic using the AIControllable! +-- +-- * **After** the state transition. +-- The state transition method needs to start with the name **OnAfter + the name of the state**. +-- These state transition methods need to provide a return value, which is specified at the function description. +-- +-- === +-- +-- # 1) @{#ACT_ROUTE_ZONE} class, extends @{Fsm.Route#ACT_ROUTE} +-- +-- The ACT_ROUTE_ZONE class implements the core functions to route an AIR @{Controllable} player @{Unit} to a @{Zone}. +-- The player receives on perioding times messages with the coordinates of the route to follow. +-- Upon arrival at the zone, a confirmation of arrival is sent, and the process will be ended. +-- +-- # 1.1) ACT_ROUTE_ZONE constructor: +-- +-- * @{#ACT_ROUTE_ZONE.New}(): Creates a new ACT_ROUTE_ZONE object. +-- +-- === +-- +-- @module Route + + +do -- ACT_ROUTE + + --- ACT_ROUTE class + -- @type ACT_ROUTE + -- @field Tasking.Task#TASK TASK + -- @field Wrapper.Unit#UNIT ProcessUnit + -- @field Core.Zone#ZONE_BASE TargetZone + -- @extends Core.Fsm#FSM_PROCESS + ACT_ROUTE = { + ClassName = "ACT_ROUTE", + } + + + --- Creates a new routing state machine. The process will route a CLIENT to a ZONE until the CLIENT is within that ZONE. + -- @param #ACT_ROUTE self + -- @return #ACT_ROUTE self + function ACT_ROUTE:New() + + -- Inherits from BASE + local self = BASE:Inherit( self, FSM_PROCESS:New( "ACT_ROUTE" ) ) -- Core.Fsm#FSM_PROCESS + + self:AddTransition( "None", "Start", "Routing" ) + self:AddTransition( "*", "Report", "Reporting" ) + self:AddTransition( "*", "Route", "Routing" ) + self:AddTransition( "Routing", "Pause", "Pausing" ) + self:AddTransition( "*", "Abort", "Aborted" ) + self:AddTransition( "Routing", "Arrive", "Arrived" ) + self:AddTransition( "Arrived", "Success", "Success" ) + self:AddTransition( "*", "Fail", "Failed" ) + self:AddTransition( "", "", "" ) + self:AddTransition( "", "", "" ) + + self:AddEndState( "Arrived" ) + self:AddEndState( "Failed" ) + + self:SetStartState( "None" ) + + return self + end + + --- Task Events + + --- StateMachine callback function + -- @param #ACT_ROUTE self + -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ROUTE:onafterStart( ProcessUnit, From, Event, To ) + + + self:__Route( 1 ) + end + + --- Check if the controllable has arrived. + -- @param #ACT_ROUTE self + -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit + -- @return #boolean + function ACT_ROUTE:onfuncHasArrived( ProcessUnit ) + return false + end + + --- StateMachine callback function + -- @param #ACT_ROUTE self + -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ROUTE:onbeforeRoute( ProcessUnit, From, Event, To ) + self:F( { "BeforeRoute 1", self.DisplayCount, self.DisplayInterval } ) + + if ProcessUnit:IsAlive() then + self:F( "BeforeRoute 2" ) + local HasArrived = self:onfuncHasArrived( ProcessUnit ) -- Polymorphic + if self.DisplayCount >= self.DisplayInterval then + self:T( { HasArrived = HasArrived } ) + if not HasArrived then + self:Report() + end + self.DisplayCount = 1 + else + self.DisplayCount = self.DisplayCount + 1 + end + + self:T( { DisplayCount = self.DisplayCount } ) + + if HasArrived then + self:__Arrive( 1 ) + else + self:__Route( 1 ) + end + + return HasArrived -- if false, then the event will not be executed... + end + + return false + + end + +end -- ACT_ROUTE + + + +do -- ACT_ROUTE_ZONE + + --- ACT_ROUTE_ZONE class + -- @type ACT_ROUTE_ZONE + -- @field Tasking.Task#TASK TASK + -- @field Wrapper.Unit#UNIT ProcessUnit + -- @field Core.Zone#ZONE_BASE TargetZone + -- @extends #ACT_ROUTE + ACT_ROUTE_ZONE = { + ClassName = "ACT_ROUTE_ZONE", + } + + + --- Creates a new routing state machine. The task will route a controllable to a ZONE until the controllable is within that ZONE. + -- @param #ACT_ROUTE_ZONE self + -- @param Core.Zone#ZONE_BASE TargetZone + function ACT_ROUTE_ZONE:New( TargetZone ) + local self = BASE:Inherit( self, ACT_ROUTE:New() ) -- #ACT_ROUTE_ZONE + + self.TargetZone = TargetZone + + self.DisplayInterval = 30 + self.DisplayCount = 30 + self.DisplayMessage = true + self.DisplayTime = 10 -- 10 seconds is the default + + return self + end + + function ACT_ROUTE_ZONE:Init( FsmRoute ) + + self.TargetZone = FsmRoute.TargetZone + + self.DisplayInterval = 30 + self.DisplayCount = 30 + self.DisplayMessage = true + self.DisplayTime = 10 -- 10 seconds is the default + end + + --- Method override to check if the controllable has arrived. + -- @param #ACT_ROUTE self + -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit + -- @return #boolean + function ACT_ROUTE_ZONE:onfuncHasArrived( ProcessUnit ) + + if ProcessUnit:IsInZone( self.TargetZone ) then + local RouteText = "You have arrived within the zone." + self:Message( RouteText ) + end + + return ProcessUnit:IsInZone( self.TargetZone ) + end + + --- Task Events + + --- StateMachine callback function + -- @param #ACT_ROUTE_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ROUTE_ZONE:onenterReporting( ProcessUnit, From, Event, To ) + + local ZoneVec2 = self.TargetZone:GetVec2() + local ZonePointVec2 = POINT_VEC2:New( ZoneVec2.x, ZoneVec2.y ) + local TaskUnitVec2 = ProcessUnit:GetVec2() + local TaskUnitPointVec2 = POINT_VEC2:New( TaskUnitVec2.x, TaskUnitVec2.y ) + local RouteText = "Route to " .. TaskUnitPointVec2:GetBRText( ZonePointVec2 ) .. " km to target." + self:Message( RouteText ) + end + +end -- ACT_ROUTE_ZONE +--- (SP) (MP) (FSM) Account for (Detect, count and report) DCS events occuring on DCS objects (units). +-- +-- === +-- +-- # @{#ACT_ACCOUNT} FSM class, extends @{Fsm#FSM_PROCESS} +-- +-- ## ACT_ACCOUNT state machine: +-- +-- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur. +-- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below. +-- Each derived class follows exactly the same process, using the same events and following the same state transitions, +-- but will have **different implementation behaviour** upon each event or state transition. +-- +-- ### ACT_ACCOUNT **Events**: +-- +-- These are the events defined in this class: +-- +-- * **Start**: The process is started. The process will go into the Report state. +-- * **Event**: A relevant event has occured that needs to be accounted for. The process will go into the Account state. +-- * **Report**: The process is reporting to the player the accounting status of the DCS events. +-- * **More**: There are more DCS events that need to be accounted for. The process will go back into the Report state. +-- * **NoMore**: There are no more DCS events that need to be accounted for. The process will go into the Success state. +-- +-- ### ACT_ACCOUNT **Event methods**: +-- +-- Event methods are available (dynamically allocated by the state machine), that accomodate for state transitions occurring in the process. +-- There are two types of event methods, which you can use to influence the normal mechanisms in the state machine: +-- +-- * **Immediate**: The event method has exactly the name of the event. +-- * **Delayed**: The event method starts with a __ + the name of the event. The first parameter of the event method is a number value, expressing the delay in seconds when the event will be executed. +-- +-- ### ACT_ACCOUNT **States**: +-- +-- * **Assigned**: The player is assigned to the task. This is the initialization state for the process. +-- * **Waiting**: the process is waiting for a DCS event to occur within the simulator. This state is set automatically. +-- * **Report**: The process is Reporting to the players in the group of the unit. This state is set automatically every 30 seconds. +-- * **Account**: The relevant DCS event has occurred, and is accounted for. +-- * **Success (*)**: All DCS events were accounted for. +-- * **Failed (*)**: The process has failed. +-- +-- (*) End states of the process. +-- +-- ### ACT_ACCOUNT state transition methods: +-- +-- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. +-- There are 2 moments when state transition methods will be called by the state machine: +-- +-- * **Before** the state transition. +-- The state transition method needs to start with the name **OnBefore + the name of the state**. +-- If the state transition method returns false, then the processing of the state transition will not be done! +-- If you want to change the behaviour of the AIControllable at this event, return false, +-- but then you'll need to specify your own logic using the AIControllable! +-- +-- * **After** the state transition. +-- The state transition method needs to start with the name **OnAfter + the name of the state**. +-- These state transition methods need to provide a return value, which is specified at the function description. +-- +-- # 1) @{#ACT_ACCOUNT_DEADS} FSM class, extends @{Fsm.Account#ACT_ACCOUNT} +-- +-- The ACT_ACCOUNT_DEADS class accounts (detects, counts and reports) successful kills of DCS units. +-- The process is given a @{Set} of units that will be tracked upon successful destruction. +-- The process will end after each target has been successfully destroyed. +-- Each successful dead will trigger an Account state transition that can be scored, modified or administered. +-- +-- +-- ## ACT_ACCOUNT_DEADS constructor: +-- +-- * @{#ACT_ACCOUNT_DEADS.New}(): Creates a new ACT_ACCOUNT_DEADS object. +-- +-- === +-- +-- @module Account + + +do -- ACT_ACCOUNT + + --- ACT_ACCOUNT class + -- @type ACT_ACCOUNT + -- @field Set#SET_UNIT TargetSetUnit + -- @extends Core.Fsm#FSM_PROCESS + ACT_ACCOUNT = { + ClassName = "ACT_ACCOUNT", + TargetSetUnit = nil, + } + + --- Creates a new DESTROY process. + -- @param #ACT_ACCOUNT self + -- @return #ACT_ACCOUNT + function ACT_ACCOUNT:New() + + -- Inherits from BASE + local self = BASE:Inherit( self, FSM_PROCESS:New() ) -- Core.Fsm#FSM_PROCESS + + self:AddTransition( "Assigned", "Start", "Waiting") + self:AddTransition( "*", "Wait", "Waiting") + self:AddTransition( "*", "Report", "Report") + self:AddTransition( "*", "Event", "Account") + self:AddTransition( "Account", "More", "Wait") + self:AddTransition( "Account", "NoMore", "Accounted") + self:AddTransition( "*", "Fail", "Failed") + + self:AddEndState( "Accounted" ) + self:AddEndState( "Failed" ) + + self:SetStartState( "Assigned" ) + + return self + end + + --- Process Events + + --- StateMachine callback function + -- @param #ACT_ACCOUNT self + -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ACCOUNT:onafterStart( ProcessUnit, From, Event, To ) + + self:HandleEvent( EVENTS.Dead, self.onfuncEventDead ) + + self:__Wait( 1 ) + end + + + --- StateMachine callback function + -- @param #ACT_ACCOUNT self + -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ACCOUNT:onenterWaiting( ProcessUnit, From, Event, To ) + + if self.DisplayCount >= self.DisplayInterval then + self:Report() + self.DisplayCount = 1 + else + self.DisplayCount = self.DisplayCount + 1 + end + + return true -- Process always the event. + end + + --- StateMachine callback function + -- @param #ACT_ACCOUNT self + -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ACCOUNT:onafterEvent( ProcessUnit, From, Event, To, Event ) + + self:__NoMore( 1 ) + end + +end -- ACT_ACCOUNT + +do -- ACT_ACCOUNT_DEADS + + --- ACT_ACCOUNT_DEADS class + -- @type ACT_ACCOUNT_DEADS + -- @field Set#SET_UNIT TargetSetUnit + -- @extends #ACT_ACCOUNT + ACT_ACCOUNT_DEADS = { + ClassName = "ACT_ACCOUNT_DEADS", + TargetSetUnit = nil, + } + + + --- Creates a new DESTROY process. + -- @param #ACT_ACCOUNT_DEADS self + -- @param Set#SET_UNIT TargetSetUnit + -- @param #string TaskName + function ACT_ACCOUNT_DEADS:New( TargetSetUnit, TaskName ) + -- Inherits from BASE + local self = BASE:Inherit( self, ACT_ACCOUNT:New() ) -- #ACT_ACCOUNT_DEADS + + self.TargetSetUnit = TargetSetUnit + self.TaskName = TaskName + + self.DisplayInterval = 30 + self.DisplayCount = 30 + self.DisplayMessage = true + self.DisplayTime = 10 -- 10 seconds is the default + self.DisplayCategory = "HQ" -- Targets is the default display category + + return self + end + + function ACT_ACCOUNT_DEADS:Init( FsmAccount ) + + self.TargetSetUnit = FsmAccount.TargetSetUnit + self.TaskName = FsmAccount.TaskName + end + + + + function ACT_ACCOUNT_DEADS:_Destructor() + self:E("_Destructor") + + self:EventRemoveAll() + + end + + --- Process Events + + --- StateMachine callback function + -- @param #ACT_ACCOUNT_DEADS self + -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ACCOUNT_DEADS:onenterReport( ProcessUnit, From, Event, To ) + self:E( { ProcessUnit, From, Event, To } ) + + self:Message( "Your group with assigned " .. self.TaskName .. " task has " .. self.TargetSetUnit:GetUnitTypesText() .. " targets left to be destroyed." ) + end + + + --- StateMachine callback function + -- @param #ACT_ACCOUNT_DEADS self + -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ACCOUNT_DEADS:onenterAccount( ProcessUnit, From, Event, To, EventData ) + self:T( { ProcessUnit, EventData, From, Event, To } ) + + self:T({self.Controllable}) + + self.TargetSetUnit:Flush() + + if self.TargetSetUnit:FindUnit( EventData.IniUnitName ) then + local TaskGroup = ProcessUnit:GetGroup() + self.TargetSetUnit:RemoveUnitsByName( EventData.IniUnitName ) + self:Message( "You hit a target. Your group with assigned " .. self.TaskName .. " task has " .. self.TargetSetUnit:Count() .. " targets ( " .. self.TargetSetUnit:GetUnitTypesText() .. " ) left to be destroyed." ) + end + end + + --- StateMachine callback function + -- @param #ACT_ACCOUNT_DEADS self + -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ACCOUNT_DEADS:onafterEvent( ProcessUnit, From, Event, To, EventData ) + + if self.TargetSetUnit:Count() > 0 then + self:__More( 1 ) + else + self:__NoMore( 1 ) + end + end + + --- DCS Events + + --- @param #ACT_ACCOUNT_DEADS self + -- @param Event#EVENTDATA EventData + function ACT_ACCOUNT_DEADS:onfuncEventDead( EventData ) + self:T( { "EventDead", EventData } ) + + if EventData.IniDCSUnit then + self:__Event( 1, EventData ) + end + end + +end -- ACT_ACCOUNT DEADS +--- (SP) (MP) (FSM) Route AI or players through waypoints or to zones. +-- +-- === +-- +-- # @{#ACT_ASSIST} FSM class, extends @{Fsm#FSM_PROCESS} +-- +-- ## ACT_ASSIST state machine: +-- +-- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur. +-- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below. +-- Each derived class follows exactly the same process, using the same events and following the same state transitions, +-- but will have **different implementation behaviour** upon each event or state transition. +-- +-- ### ACT_ASSIST **Events**: +-- +-- These are the events defined in this class: +-- +-- * **Start**: The process is started. +-- * **Next**: The process is smoking the targets in the given zone. +-- +-- ### ACT_ASSIST **Event methods**: +-- +-- Event methods are available (dynamically allocated by the state machine), that accomodate for state transitions occurring in the process. +-- There are two types of event methods, which you can use to influence the normal mechanisms in the state machine: +-- +-- * **Immediate**: The event method has exactly the name of the event. +-- * **Delayed**: The event method starts with a __ + the name of the event. The first parameter of the event method is a number value, expressing the delay in seconds when the event will be executed. +-- +-- ### ACT_ASSIST **States**: +-- +-- * **None**: The controllable did not receive route commands. +-- * **AwaitSmoke (*)**: The process is awaiting to smoke the targets in the zone. +-- * **Smoking (*)**: The process is smoking the targets in the zone. +-- * **Failed (*)**: The process has failed. +-- +-- (*) End states of the process. +-- +-- ### ACT_ASSIST state transition methods: +-- +-- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. +-- There are 2 moments when state transition methods will be called by the state machine: +-- +-- * **Before** the state transition. +-- The state transition method needs to start with the name **OnBefore + the name of the state**. +-- If the state transition method returns false, then the processing of the state transition will not be done! +-- If you want to change the behaviour of the AIControllable at this event, return false, +-- but then you'll need to specify your own logic using the AIControllable! +-- +-- * **After** the state transition. +-- The state transition method needs to start with the name **OnAfter + the name of the state**. +-- These state transition methods need to provide a return value, which is specified at the function description. +-- +-- === +-- +-- # 1) @{#ACT_ASSIST_SMOKE_TARGETS_ZONE} class, extends @{Fsm.Route#ACT_ASSIST} +-- +-- The ACT_ASSIST_SMOKE_TARGETS_ZONE class implements the core functions to smoke targets in a @{Zone}. +-- The targets are smoked within a certain range around each target, simulating a realistic smoking behaviour. +-- At random intervals, a new target is smoked. +-- +-- # 1.1) ACT_ASSIST_SMOKE_TARGETS_ZONE constructor: +-- +-- * @{#ACT_ASSIST_SMOKE_TARGETS_ZONE.New}(): Creates a new ACT_ASSIST_SMOKE_TARGETS_ZONE object. +-- +-- === +-- +-- @module Smoke + +do -- ACT_ASSIST + + --- ACT_ASSIST class + -- @type ACT_ASSIST + -- @extends Core.Fsm#FSM_PROCESS + ACT_ASSIST = { + ClassName = "ACT_ASSIST", + } + + --- Creates a new target smoking state machine. The process will request from the menu if it accepts the task, if not, the unit is removed from the simulator. + -- @param #ACT_ASSIST self + -- @return #ACT_ASSIST + function ACT_ASSIST:New() + + -- Inherits from BASE + local self = BASE:Inherit( self, FSM_PROCESS:New( "ACT_ASSIST" ) ) -- Core.Fsm#FSM_PROCESS + + self:AddTransition( "None", "Start", "AwaitSmoke" ) + self:AddTransition( "AwaitSmoke", "Next", "Smoking" ) + self:AddTransition( "Smoking", "Next", "AwaitSmoke" ) + self:AddTransition( "*", "Stop", "Success" ) + self:AddTransition( "*", "Fail", "Failed" ) + + self:AddEndState( "Failed" ) + self:AddEndState( "Success" ) + + self:SetStartState( "None" ) + + return self + end + + --- Task Events + + --- StateMachine callback function + -- @param #ACT_ASSIST self + -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ASSIST:onafterStart( ProcessUnit, From, Event, To ) + + local ProcessGroup = ProcessUnit:GetGroup() + local MissionMenu = self:GetMission():GetMissionMenu( ProcessGroup ) + + local function MenuSmoke( MenuParam ) + self:E( MenuParam ) + local self = MenuParam.self + local SmokeColor = MenuParam.SmokeColor + self.SmokeColor = SmokeColor + self:__Next( 1 ) + end + + self.Menu = MENU_GROUP:New( ProcessGroup, "Target acquisition", MissionMenu ) + self.MenuSmokeBlue = MENU_GROUP_COMMAND:New( ProcessGroup, "Drop blue smoke on targets", self.Menu, MenuSmoke, { self = self, SmokeColor = SMOKECOLOR.Blue } ) + self.MenuSmokeGreen = MENU_GROUP_COMMAND:New( ProcessGroup, "Drop green smoke on targets", self.Menu, MenuSmoke, { self = self, SmokeColor = SMOKECOLOR.Green } ) + self.MenuSmokeOrange = MENU_GROUP_COMMAND:New( ProcessGroup, "Drop Orange smoke on targets", self.Menu, MenuSmoke, { self = self, SmokeColor = SMOKECOLOR.Orange } ) + self.MenuSmokeRed = MENU_GROUP_COMMAND:New( ProcessGroup, "Drop Red smoke on targets", self.Menu, MenuSmoke, { self = self, SmokeColor = SMOKECOLOR.Red } ) + self.MenuSmokeWhite = MENU_GROUP_COMMAND:New( ProcessGroup, "Drop White smoke on targets", self.Menu, MenuSmoke, { self = self, SmokeColor = SMOKECOLOR.White } ) + end + +end + +do -- ACT_ASSIST_SMOKE_TARGETS_ZONE + + --- ACT_ASSIST_SMOKE_TARGETS_ZONE class + -- @type ACT_ASSIST_SMOKE_TARGETS_ZONE + -- @field Set#SET_UNIT TargetSetUnit + -- @field Core.Zone#ZONE_BASE TargetZone + -- @extends #ACT_ASSIST + ACT_ASSIST_SMOKE_TARGETS_ZONE = { + ClassName = "ACT_ASSIST_SMOKE_TARGETS_ZONE", + } + +-- function ACT_ASSIST_SMOKE_TARGETS_ZONE:_Destructor() +-- self:E("_Destructor") +-- +-- self.Menu:Remove() +-- self:EventRemoveAll() +-- end + + --- Creates a new target smoking state machine. The process will request from the menu if it accepts the task, if not, the unit is removed from the simulator. + -- @param #ACT_ASSIST_SMOKE_TARGETS_ZONE self + -- @param Set#SET_UNIT TargetSetUnit + -- @param Core.Zone#ZONE_BASE TargetZone + function ACT_ASSIST_SMOKE_TARGETS_ZONE:New( TargetSetUnit, TargetZone ) + local self = BASE:Inherit( self, ACT_ASSIST:New() ) -- #ACT_ASSIST + + self.TargetSetUnit = TargetSetUnit + self.TargetZone = TargetZone + + return self + end + + function ACT_ASSIST_SMOKE_TARGETS_ZONE:Init( FsmSmoke ) + + self.TargetSetUnit = FsmSmoke.TargetSetUnit + self.TargetZone = FsmSmoke.TargetZone + end + + --- Creates a new target smoking state machine. The process will request from the menu if it accepts the task, if not, the unit is removed from the simulator. + -- @param #ACT_ASSIST_SMOKE_TARGETS_ZONE self + -- @param Set#SET_UNIT TargetSetUnit + -- @param Core.Zone#ZONE_BASE TargetZone + -- @return #ACT_ASSIST_SMOKE_TARGETS_ZONE self + function ACT_ASSIST_SMOKE_TARGETS_ZONE:Init( TargetSetUnit, TargetZone ) + + self.TargetSetUnit = TargetSetUnit + self.TargetZone = TargetZone + + return self + end + + --- StateMachine callback function + -- @param #ACT_ASSIST_SMOKE_TARGETS_ZONE self + -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ASSIST_SMOKE_TARGETS_ZONE:onenterSmoking( ProcessUnit, From, Event, To ) + + self.TargetSetUnit:ForEachUnit( + --- @param Wrapper.Unit#UNIT SmokeUnit + function( SmokeUnit ) + if math.random( 1, ( 100 * self.TargetSetUnit:Count() ) / 4 ) <= 100 then + SCHEDULER:New( self, + function() + if SmokeUnit:IsAlive() then + SmokeUnit:Smoke( self.SmokeColor, 150 ) + end + end, {}, math.random( 10, 60 ) + ) + end + end + ) + + end + +end--- A COMMANDCENTER is the owner of multiple missions within MOOSE. +-- A COMMANDCENTER governs multiple missions, the tasking and the reporting. +-- @module CommandCenter + + + +--- The REPORT class +-- @type REPORT +-- @extends Core.Base#BASE +REPORT = { + ClassName = "REPORT", +} + +--- Create a new REPORT. +-- @param #REPORT self +-- @param #string Title +-- @return #REPORT +function REPORT:New( Title ) + + local self = BASE:Inherit( self, BASE:New() ) + + self.Report = {} + self.Report[#self.Report+1] = Title + + return self +end + +--- Add a new line to a REPORT. +-- @param #REPORT self +-- @param #string Text +-- @return #REPORT +function REPORT:Add( Text ) + self.Report[#self.Report+1] = Text + return self.Report[#self.Report+1] +end + +function REPORT:Text() + return table.concat( self.Report, "\n" ) +end + +--- The COMMANDCENTER class +-- @type COMMANDCENTER +-- @field Wrapper.Group#GROUP HQ +-- @field Dcs.DCSCoalitionWrapper.Object#coalition CommandCenterCoalition +-- @list Missions +-- @extends Core.Base#BASE +COMMANDCENTER = { + ClassName = "COMMANDCENTER", + CommandCenterName = "", + CommandCenterCoalition = nil, + CommandCenterPositionable = nil, + Name = "", +} +--- The constructor takes an IDENTIFIABLE as the HQ command center. +-- @param #COMMANDCENTER self +-- @param Wrapper.Positionable#POSITIONABLE CommandCenterPositionable +-- @param #string CommandCenterName +-- @return #COMMANDCENTER +function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName ) + + local self = BASE:Inherit( self, BASE:New() ) + + self.CommandCenterPositionable = CommandCenterPositionable + self.CommandCenterName = CommandCenterName or CommandCenterPositionable:GetName() + self.CommandCenterCoalition = CommandCenterPositionable:GetCoalition() + + self.Missions = {} + + self:HandleEvent( EVENTS.Birth, + --- @param #COMMANDCENTER self + --- @param Core.Event#EVENTDATA EventData + function( self, EventData ) + self:E( { EventData } ) + local EventGroup = GROUP:Find( EventData.IniDCSGroup ) + if EventGroup and self:HasGroup( EventGroup ) then + local MenuReporting = MENU_GROUP:New( EventGroup, "Reporting", self.CommandCenterMenu ) + local MenuMissionsSummary = MENU_GROUP_COMMAND:New( EventGroup, "Missions Summary Report", MenuReporting, self.ReportSummary, self, EventGroup ) + local MenuMissionsDetails = MENU_GROUP_COMMAND:New( EventGroup, "Missions Details Report", MenuReporting, self.ReportDetails, self, EventGroup ) + self:ReportSummary( EventGroup ) + end + local PlayerUnit = EventData.IniUnit + for MissionID, Mission in pairs( self:GetMissions() ) do + local Mission = Mission -- Tasking.Mission#MISSION + local PlayerGroup = EventData.IniGroup -- The GROUP object should be filled! + Mission:JoinUnit( PlayerUnit, PlayerGroup ) + Mission:ReportDetails() + end + + end + ) + + -- When a player enters a client or a unit, the CommandCenter will check for each Mission and each Task in the Mission if the player has things to do. + -- For these elements, it will= + -- - Set the correct menu. + -- - Assign the PlayerUnit to the Task if required. + -- - Send a message to the other players in the group that this player has joined. + self:HandleEvent( EVENTS.PlayerEnterUnit, + --- @param #COMMANDCENTER self + -- @param Core.Event#EVENTDATA EventData + function( self, EventData ) + local PlayerUnit = EventData.IniUnit + for MissionID, Mission in pairs( self:GetMissions() ) do + local Mission = Mission -- Tasking.Mission#MISSION + local PlayerGroup = EventData.IniGroup -- The GROUP object should be filled! + Mission:JoinUnit( PlayerUnit, PlayerGroup ) + Mission:ReportDetails() + end + end + ) + + -- Handle when a player leaves a slot and goes back to spectators ... + -- The PlayerUnit will be UnAssigned from the Task. + -- When there is no Unit left running the Task, the Task goes into Abort... + self:HandleEvent( EVENTS.PlayerLeaveUnit, + --- @param #TASK self + -- @param Core.Event#EVENTDATA EventData + function( self, EventData ) + local PlayerUnit = EventData.IniUnit + for MissionID, Mission in pairs( self:GetMissions() ) do + local Mission = Mission -- Tasking.Mission#MISSION + Mission:AbortUnit( PlayerUnit ) + end + end + ) + + -- Handle when a player leaves a slot and goes back to spectators ... + -- The PlayerUnit will be UnAssigned from the Task. + -- When there is no Unit left running the Task, the Task goes into Abort... + self:HandleEvent( EVENTS.Crash, + --- @param #TASK self + -- @param Core.Event#EVENTDATA EventData + function( self, EventData ) + local PlayerUnit = EventData.IniUnit + for MissionID, Mission in pairs( self:GetMissions() ) do + Mission:CrashUnit( PlayerUnit ) + end + end + ) + + return self +end + +--- Gets the name of the HQ command center. +-- @param #COMMANDCENTER self +-- @return #string +function COMMANDCENTER:GetName() + + return self.CommandCenterName +end + +--- Gets the POSITIONABLE of the HQ command center. +-- @param #COMMANDCENTER self +-- @return Wrapper.Positionable#POSITIONABLE +function COMMANDCENTER:GetPositionable() + return self.CommandCenterPositionable +end + +--- Get the Missions governed by the HQ command center. +-- @param #COMMANDCENTER self +-- @return #list +function COMMANDCENTER:GetMissions() + + return self.Missions +end + +--- Add a MISSION to be governed by the HQ command center. +-- @param #COMMANDCENTER self +-- @param Tasking.Mission#MISSION Mission +-- @return Tasking.Mission#MISSION +function COMMANDCENTER:AddMission( Mission ) + + self.Missions[Mission] = Mission + + return Mission +end + +--- Removes a MISSION to be governed by the HQ command center. +-- The given Mission is not nilified. +-- @param #COMMANDCENTER self +-- @param Tasking.Mission#MISSION Mission +-- @return Tasking.Mission#MISSION +function COMMANDCENTER:RemoveMission( Mission ) + + self.Missions[Mission] = nil + + return Mission +end + +--- Sets the menu structure of the Missions governed by the HQ command center. +-- @param #COMMANDCENTER self +function COMMANDCENTER:SetMenu() + self:F() + + self.CommandCenterMenu = self.CommandCenterMenu or MENU_COALITION:New( self.CommandCenterCoalition, "Command Center (" .. self:GetName() .. ")" ) + + for MissionID, Mission in pairs( self:GetMissions() ) do + local Mission = Mission -- Tasking.Mission#MISSION + Mission:RemoveMenu() + end + + for MissionID, Mission in pairs( self:GetMissions() ) do + local Mission = Mission -- Tasking.Mission#MISSION + Mission:SetMenu() + end +end + + +--- Checks of the COMMANDCENTER has a GROUP. +-- @param #COMMANDCENTER self +-- @param Wrapper.Group#GROUP +-- @return #boolean +function COMMANDCENTER:HasGroup( MissionGroup ) + + local Has = false + + for MissionID, Mission in pairs( self.Missions ) do + local Mission = Mission -- Tasking.Mission#MISSION + if Mission:HasGroup( MissionGroup ) then + Has = true + break + end + end + + return Has +end + +--- Send a CC message to a GROUP. +-- @param #COMMANDCENTER self +-- @param #string Message +-- @param Wrapper.Group#GROUP TaskGroup +-- @param #sring Name (optional) The name of the Group used as a prefix for the message to the Group. If not provided, there will be nothing shown. +function COMMANDCENTER:MessageToGroup( Message, TaskGroup, Name ) + + local Prefix = Name and "@ Group (" .. Name .. "): " or '' + Message = Prefix .. Message + self:GetPositionable():MessageToGroup( Message , 20, TaskGroup, self:GetName() ) + +end + +--- Send a CC message to the coalition of the CC. +-- @param #COMMANDCENTER self +function COMMANDCENTER:MessageToCoalition( Message ) + + local CCCoalition = self:GetPositionable():GetCoalition() + --TODO: Fix coalition bug! + self:GetPositionable():MessageToCoalition( Message, 20, CCCoalition, self:GetName() ) + +end + +--- Report the status of all MISSIONs to a GROUP. +-- Each Mission is listed, with an indication how many Tasks are still to be completed. +-- @param #COMMANDCENTER self +function COMMANDCENTER:ReportSummary( ReportGroup ) + self:E( ReportGroup ) + + local Report = REPORT:New() + + for MissionID, Mission in pairs( self.Missions ) do + local Mission = Mission -- Tasking.Mission#MISSION + Report:Add( " - " .. Mission:ReportOverview() ) + end + + self:GetPositionable():MessageToGroup( Report:Text(), 30, ReportGroup ) + +end + +--- Report the status of a Task to a Group. +-- Report the details of a Mission, listing the Mission, and all the Task details. +-- @param #COMMANDCENTER self +function COMMANDCENTER:ReportDetails( ReportGroup, Task ) + self:E( ReportGroup ) + + local Report = REPORT:New() + + for MissionID, Mission in pairs( self.Missions ) do + local Mission = Mission -- Tasking.Mission#MISSION + Report:Add( " - " .. Mission:ReportDetails() ) + end + + self:GetPositionable():MessageToGroup( Report:Text(), 30, ReportGroup ) +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 +-- @field #MISSION.Clients _Clients +-- @field Core.Menu#MENU_COALITION MissionMenu +-- @field #string MissionBriefing +-- @extends Core.Fsm#FSM +MISSION = { + ClassName = "MISSION", + Name = "", + MissionStatus = "PENDING", + _Clients = {}, + TaskMenus = {}, + TaskCategoryMenus = {}, + TaskTypeMenus = {}, + _ActiveTasks = {}, + GoalFunction = nil, + MissionReportTrigger = 0, + MissionProgressTrigger = 0, + MissionReportShow = false, + MissionReportFlash = false, + MissionTimeInterval = 0, + MissionCoalition = "", + SUCCESS = 1, + FAILED = 2, + REPEAT = 3, + _GoalTasks = {} +} + +--- This is the main MISSION declaration method. Each Mission is like the master or a Mission orchestration between, Clients, Tasks, Stages etc. +-- @param #MISSION self +-- @param Tasking.CommandCenter#COMMANDCENTER CommandCenter +-- @param #string MissionName is the name of the mission. This name will be used to reference the status of each mission by the players. +-- @param #string MissionPriority is a string indicating the "priority" of the Mission. f.e. "Primary", "Secondary" or "First", "Second". It is free format and up to the Mission designer to choose. There are no rules behind this field. +-- @param #string MissionBriefing is a string indicating the mission briefing to be shown when a player joins a @{CLIENT}. +-- @param Dcs.DCSCoalitionWrapper.Object#coalition MissionCoalition is a string indicating the coalition or party to which this mission belongs to. It is free format and can be chosen freely by the mission designer. Note that this field is not to be confused with the coalition concept of the ME. Examples of a Mission Coalition could be "NATO", "CCCP", "Intruders", "Terrorists"... +-- @return #MISSION self +function MISSION:New( CommandCenter, MissionName, MissionPriority, MissionBriefing, MissionCoalition ) + + local self = BASE:Inherit( self, FSM:New() ) -- Core.Fsm#FSM + + self:SetStartState( "Idle" ) + + self:AddTransition( "Idle", "Start", "Ongoing" ) + self:AddTransition( "Ongoing", "Stop", "Idle" ) + self:AddTransition( "Ongoing", "Complete", "Completed" ) + self:AddTransition( "*", "Fail", "Failed" ) + + self:T( { MissionName, MissionPriority, MissionBriefing, MissionCoalition } ) + + self.CommandCenter = CommandCenter + CommandCenter:AddMission( self ) + + self.Name = MissionName + self.MissionPriority = MissionPriority + self.MissionBriefing = MissionBriefing + self.MissionCoalition = MissionCoalition + + self.Tasks = {} + + return self +end + +--- FSM function for a MISSION +-- @param #MISSION self +-- @param #string Event +-- @param #string From +-- @param #string To +function MISSION:onbeforeComplete( From, Event, To ) + + for TaskID, Task in pairs( self:GetTasks() ) do + local Task = Task -- Tasking.Task#TASK + if not Task:IsStateSuccess() and not Task:IsStateFailed() and not Task:IsStateAborted() and not Task:IsStateCancelled() then + return false -- Mission cannot be completed. Other Tasks are still active. + end + end + return true -- Allow Mission completion. +end + +--- FSM function for a MISSION +-- @param #MISSION self +-- @param #string Event +-- @param #string From +-- @param #string To +function MISSION:onenterCompleted( From, Event, To ) + + self:GetCommandCenter():MessageToCoalition( "Mission " .. self:GetName() .. " has been completed! Good job guys!" ) +end + +--- Gets the mission name. +-- @param #MISSION self +-- @return #MISSION self +function MISSION:GetName() + return self.Name +end + +--- Add a Unit to join the Mission. +-- For each Task within the Mission, the Unit is joined with the Task. +-- If the Unit was not part of a Task in the Mission, false is returned. +-- If the Unit is part of a Task in the Mission, true is returned. +-- @param #MISSION self +-- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player joining the Mission. +-- @param Wrapper.Group#GROUP PlayerGroup The GROUP of the player joining the Mission. +-- @return #boolean true if Unit is part of a Task in the Mission. +function MISSION:JoinUnit( PlayerUnit, PlayerGroup ) + self:F( { PlayerUnit = PlayerUnit, PlayerGroup = PlayerGroup } ) + + local PlayerUnitAdded = false + + for TaskID, Task in pairs( self:GetTasks() ) do + local Task = Task -- Tasking.Task#TASK + if Task:JoinUnit( PlayerUnit, PlayerGroup ) then + PlayerUnitAdded = true + end + end + + return PlayerUnitAdded +end + +--- Aborts a PlayerUnit from the Mission. +-- For each Task within the Mission, the PlayerUnit is removed from Task where it is assigned. +-- If the Unit was not part of a Task in the Mission, false is returned. +-- If the Unit is part of a Task in the Mission, true is returned. +-- @param #MISSION self +-- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player joining the Mission. +-- @return #boolean true if Unit is part of a Task in the Mission. +function MISSION:AbortUnit( PlayerUnit ) + self:F( { PlayerUnit = PlayerUnit } ) + + local PlayerUnitRemoved = false + + for TaskID, Task in pairs( self:GetTasks() ) do + if Task:AbortUnit( PlayerUnit ) then + PlayerUnitRemoved = true + end + end + + return PlayerUnitRemoved +end + +--- Handles a crash of a PlayerUnit from the Mission. +-- For each Task within the Mission, the PlayerUnit is removed from Task where it is assigned. +-- If the Unit was not part of a Task in the Mission, false is returned. +-- If the Unit is part of a Task in the Mission, true is returned. +-- @param #MISSION self +-- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player crashing. +-- @return #boolean true if Unit is part of a Task in the Mission. +function MISSION:CrashUnit( PlayerUnit ) + self:F( { PlayerUnit = PlayerUnit } ) + + local PlayerUnitRemoved = false + + for TaskID, Task in pairs( self:GetTasks() ) do + if Task:CrashUnit( PlayerUnit ) then + PlayerUnitRemoved = true + end + end + + return PlayerUnitRemoved +end + +--- Add a scoring to the mission. +-- @param #MISSION self +-- @return #MISSION self +function MISSION:AddScoring( Scoring ) + self.Scoring = Scoring + return self +end + +--- Get the scoring object of a mission. +-- @param #MISSION self +-- @return #SCORING Scoring +function MISSION:GetScoring() + return self.Scoring +end + +--- Get the groups for which TASKS are given in the mission +-- @param #MISSION self +-- @return Core.Set#SET_GROUP +function MISSION:GetGroups() + + local SetGroup = SET_GROUP:New() + + for TaskID, Task in pairs( self:GetTasks() ) do + local Task = Task -- Tasking.Task#TASK + local GroupSet = Task:GetGroups() + GroupSet:ForEachGroup( + function( TaskGroup ) + SetGroup:Add( TaskGroup, TaskGroup ) + end + ) + end + + return SetGroup + +end + + +--- Sets the Planned Task menu. +-- @param #MISSION self +function MISSION:SetMenu() + self:F() + + for _, Task in pairs( self:GetTasks() ) do + local Task = Task -- Tasking.Task#TASK + Task:SetMenu() + end +end + +--- Removes the Planned Task menu. +-- @param #MISSION self +function MISSION:RemoveMenu() + self:F() + + for _, Task in pairs( self:GetTasks() ) do + local Task = Task -- Tasking.Task#TASK + Task:RemoveMenu() + end +end + + +--- Gets the COMMANDCENTER. +-- @param #MISSION self +-- @return Tasking.CommandCenter#COMMANDCENTER +function MISSION:GetCommandCenter() + return self.CommandCenter +end + +--- Sets the Assigned Task menu. +-- @param #MISSION self +-- @param Tasking.Task#TASK Task +-- @param #string MenuText The menu text. +-- @return #MISSION self +function MISSION:SetAssignedMenu( Task ) + + for _, Task in pairs( self.Tasks ) do + local Task = Task -- Tasking.Task#TASK + Task:RemoveMenu() + Task:SetAssignedMenu() + end + +end + +--- Removes a Task menu. +-- @param #MISSION self +-- @param Tasking.Task#TASK Task +-- @return #MISSION self +function MISSION:RemoveTaskMenu( Task ) + + Task:RemoveMenu() +end + + +--- Gets the mission menu for the coalition. +-- @param #MISSION self +-- @param Wrapper.Group#GROUP TaskGroup +-- @return Core.Menu#MENU_COALITION self +function MISSION:GetMissionMenu( TaskGroup ) + + local CommandCenter = self:GetCommandCenter() + local CommandCenterMenu = CommandCenter.CommandCenterMenu + + local MissionName = self:GetName() + + local TaskGroupName = TaskGroup:GetName() + local MissionMenu = MENU_GROUP:New( TaskGroup, MissionName, CommandCenterMenu ) + + return MissionMenu +end + + +--- Clears the mission menu for the coalition. +-- @param #MISSION self +-- @return #MISSION self +function MISSION:ClearMissionMenu() + self.MissionMenu:Remove() + self.MissionMenu = nil +end + +--- Get the TASK identified by the TaskNumber from the Mission. This function is useful in GoalFunctions. +-- @param #string TaskName The Name of the @{Task} within the @{Mission}. +-- @return Tasking.Task#TASK The Task +-- @return #nil Returns nil if no task was found. +function MISSION:GetTask( TaskName ) + self:F( { TaskName } ) + + return self.Tasks[TaskName] +end + + +--- Register a @{Task} to be completed within the @{Mission}. +-- Note that there can be multiple @{Task}s registered to be completed. +-- Each Task can be set a certain Goals. The Mission will not be completed until all Goals are reached. +-- @param #MISSION self +-- @param Tasking.Task#TASK Task is the @{Task} object. +-- @return Tasking.Task#TASK The task added. +function MISSION:AddTask( Task ) + + local TaskName = Task:GetTaskName() + self:F( TaskName ) + + self.Tasks[TaskName] = self.Tasks[TaskName] or { n = 0 } + + self.Tasks[TaskName] = Task + + self:GetCommandCenter():SetMenu() + + return Task +end + +--- Removes a @{Task} to be completed within the @{Mission}. +-- Note that there can be multiple @{Task}s registered to be completed. +-- Each Task can be set a certain Goals. The Mission will not be completed until all Goals are reached. +-- @param #MISSION self +-- @param Tasking.Task#TASK Task is the @{Task} object. +-- @return #nil The cleaned Task reference. +function MISSION:RemoveTask( Task ) + + local TaskName = Task:GetTaskName() + + self:F( TaskName ) + self.Tasks[TaskName] = self.Tasks[TaskName] or { n = 0 } + + -- Ensure everything gets garbarge collected. + self.Tasks[TaskName] = nil + Task = nil + + collectgarbage() + + self:GetCommandCenter():SetMenu() + + return nil +end + +--- Return the next @{Task} ID to be completed within the @{Mission}. +-- @param #MISSION self +-- @param Tasking.Task#TASK Task is the @{Task} object. +-- @return Tasking.Task#TASK The task added. +function MISSION:GetNextTaskID( Task ) + + local TaskName = Task:GetTaskName() + self:F( TaskName ) + self.Tasks[TaskName] = self.Tasks[TaskName] or { n = 0 } + + self.Tasks[TaskName].n = self.Tasks[TaskName].n + 1 + + return self.Tasks[TaskName].n +end + + + +--- old stuff + +--- 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 + +function MISSION:HasGroup( TaskGroup ) + local Has = false + + for TaskID, Task in pairs( self:GetTasks() ) do + local Task = Task -- Tasking.Task#TASK + if Task:HasGroup( TaskGroup ) then + Has = true + break + end + end + + return Has +end + +--- Create a summary report of the Mission (one line). +-- @param #MISSION self +-- @return #string +function MISSION:ReportSummary() + + local Report = REPORT:New() + + -- List the name of the mission. + local Name = self:GetName() + + -- Determine the status of the mission. + local Status = self:GetState() + + -- Determine how many tasks are remaining. + local TasksRemaining = 0 + for TaskID, Task in pairs( self:GetTasks() ) do + local Task = Task -- Tasking.Task#TASK + if Task:IsStateSuccess() or Task:IsStateFailed() then + else + TasksRemaining = TasksRemaining + 1 + end + end + + Report:Add( "Mission " .. Name .. " - " .. Status .. " - " .. TasksRemaining .. " tasks remaining." ) + + return Report:Text() +end + +--- Create a overview report of the Mission (multiple lines). +-- @param #MISSION self +-- @return #string +function MISSION:ReportOverview() + + local Report = REPORT:New() + + -- List the name of the mission. + local Name = self:GetName() + + -- Determine the status of the mission. + local Status = self:GetState() + + Report:Add( "Mission " .. Name .. " - State '" .. Status .. "'" ) + + -- Determine how many tasks are remaining. + local TasksRemaining = 0 + for TaskID, Task in pairs( self:GetTasks() ) do + local Task = Task -- Tasking.Task#TASK + Report:Add( "- " .. Task:ReportSummary() ) + end + + return Report:Text() +end + +--- Create a detailed report of the Mission, listing all the details of the Task. +-- @param #MISSION self +-- @return #string +function MISSION:ReportDetails() + + local Report = REPORT:New() + + -- List the name of the mission. + local Name = self:GetName() + + -- Determine the status of the mission. + local Status = self:GetState() + + Report:Add( "Mission " .. Name .. " - State '" .. Status .. "'" ) + + -- Determine how many tasks are remaining. + local TasksRemaining = 0 + for TaskID, Task in pairs( self:GetTasks() ) do + local Task = Task -- Tasking.Task#TASK + Report:Add( Task:ReportDetails() ) + end + + return Report:Text() +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 + + +--- 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 -- Wrapper.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 + +--- This module contains the TASK class. +-- +-- 1) @{#TASK} class, extends @{Base#BASE} +-- ============================================ +-- 1.1) The @{#TASK} class implements the methods for task orchestration within MOOSE. +-- ---------------------------------------------------------------------------------------- +-- The class provides a couple of methods to: +-- +-- * @{#TASK.AssignToGroup}():Assign a task to a group (of players). +-- * @{#TASK.AddProcess}():Add a @{Process} to a task. +-- * @{#TASK.RemoveProcesses}():Remove a running @{Process} from a running task. +-- * @{#TASK.SetStateMachine}():Set a @{Fsm} to a task. +-- * @{#TASK.RemoveStateMachine}():Remove @{Fsm} from a task. +-- * @{#TASK.HasStateMachine}():Enquire if the task has a @{Fsm} +-- * @{#TASK.AssignToUnit}(): Assign a task to a unit. (Needs to be implemented in the derived classes from @{#TASK}. +-- * @{#TASK.UnAssignFromUnit}(): Unassign the task from a unit. +-- * @{#TASK.SetTimeOut}(): Set timer in seconds before task gets cancelled if not assigned. +-- +-- 1.2) Set and enquire task status (beyond the task state machine processing). +-- ---------------------------------------------------------------------------- +-- A task needs to implement as a minimum the following task states: +-- +-- * **Success**: Expresses the successful execution and finalization of the task. +-- * **Failed**: Expresses the failure of a task. +-- * **Planned**: Expresses that the task is created, but not yet in execution and is not assigned yet. +-- * **Assigned**: Expresses that the task is assigned to a Group of players, and that the task is in execution mode. +-- +-- A task may also implement the following task states: +-- +-- * **Rejected**: Expresses that the task is rejected by a player, who was requested to accept the task. +-- * **Cancelled**: Expresses that the task is cancelled by HQ or through a logical situation where a cancellation of the task is required. +-- +-- A task can implement more statusses than the ones outlined above. Please consult the documentation of the specific tasks to understand the different status modelled. +-- +-- The status of tasks can be set by the methods **State** followed by the task status. An example is `StateAssigned()`. +-- The status of tasks can be enquired by the methods **IsState** followed by the task status name. An example is `if IsStateAssigned() then`. +-- +-- 1.3) Add scoring when reaching a certain task status: +-- ----------------------------------------------------- +-- Upon reaching a certain task status in a task, additional scoring can be given. If the Mission has a scoring system attached, the scores will be added to the mission scoring. +-- Use the method @{#TASK.AddScore}() to add scores when a status is reached. +-- +-- 1.4) Task briefing: +-- ------------------- +-- A task briefing can be given that is shown to the player when he is assigned to the task. +-- +-- === +-- +-- ### Authors: FlightControl - Design and Programming +-- +-- @module Task + +--- The TASK class +-- @type TASK +-- @field Core.Scheduler#SCHEDULER TaskScheduler +-- @field Tasking.Mission#MISSION Mission +-- @field Core.Set#SET_GROUP SetGroup The Set of Groups assigned to the Task +-- @field Core.Fsm#FSM_PROCESS FsmTemplate +-- @field Tasking.Mission#MISSION Mission +-- @field Tasking.CommandCenter#COMMANDCENTER CommandCenter +-- @extends Core.Fsm#FSM_TASK +TASK = { + ClassName = "TASK", + TaskScheduler = nil, + ProcessClasses = {}, -- The container of the Process classes that will be used to create and assign new processes for the task to ProcessUnits. + Processes = {}, -- The container of actual process objects instantiated and assigned to ProcessUnits. + Players = nil, + Scores = {}, + Menu = {}, + SetGroup = nil, + FsmTemplate = nil, + Mission = nil, + CommandCenter = nil, + TimeOut = 0, +} + +--- FSM PlayerAborted event handler prototype for TASK. +-- @function [parent=#TASK] OnAfterPlayerAborted +-- @param #TASK self +-- @param Wrapper.Unit#UNIT PlayerUnit The Unit of the Player when he went back to spectators or left the mission. +-- @param #string PlayerName The name of the Player. + +--- FSM PlayerCrashed event handler prototype for TASK. +-- @function [parent=#TASK] OnAfterPlayerCrashed +-- @param #TASK self +-- @param Wrapper.Unit#UNIT PlayerUnit The Unit of the Player when he crashed in the mission. +-- @param #string PlayerName The name of the Player. + +--- FSM PlayerDead event handler prototype for TASK. +-- @function [parent=#TASK] OnAfterPlayerDead +-- @param #TASK self +-- @param Wrapper.Unit#UNIT PlayerUnit The Unit of the Player when he died in the mission. +-- @param #string PlayerName The name of the Player. + +--- FSM Fail synchronous event function for TASK. +-- Use this event to Fail the Task. +-- @function [parent=#TASK] Fail +-- @param #TASK self + +--- FSM Fail asynchronous event function for TASK. +-- Use this event to Fail the Task. +-- @function [parent=#TASK] __Fail +-- @param #TASK self + +--- FSM Abort synchronous event function for TASK. +-- Use this event to Abort the Task. +-- @function [parent=#TASK] Abort +-- @param #TASK self + +--- FSM Abort asynchronous event function for TASK. +-- Use this event to Abort the Task. +-- @function [parent=#TASK] __Abort +-- @param #TASK self + +--- FSM Success synchronous event function for TASK. +-- Use this event to make the Task a Success. +-- @function [parent=#TASK] Success +-- @param #TASK self + +--- FSM Success asynchronous event function for TASK. +-- Use this event to make the Task a Success. +-- @function [parent=#TASK] __Success +-- @param #TASK self + +--- FSM Cancel synchronous event function for TASK. +-- Use this event to Cancel the Task. +-- @function [parent=#TASK] Cancel +-- @param #TASK self + +--- FSM Cancel asynchronous event function for TASK. +-- Use this event to Cancel the Task. +-- @function [parent=#TASK] __Cancel +-- @param #TASK self + +--- FSM Replan synchronous event function for TASK. +-- Use this event to Replan the Task. +-- @function [parent=#TASK] Replan +-- @param #TASK self + +--- FSM Replan asynchronous event function for TASK. +-- Use this event to Replan the Task. +-- @function [parent=#TASK] __Replan +-- @param #TASK self + + +--- Instantiates a new TASK. Should never be used. Interface Class. +-- @param #TASK self +-- @param Tasking.Mission#MISSION Mission The mission wherein the Task is registered. +-- @param Core.Set#SET_GROUP SetGroupAssign The set of groups for which the Task can be assigned. +-- @param #string TaskName The name of the Task +-- @param #string TaskType The type of the Task +-- @return #TASK self +function TASK:New( Mission, SetGroupAssign, TaskName, TaskType ) + + local self = BASE:Inherit( self, FSM_TASK:New() ) -- Core.Fsm#FSM_TASK + + self:SetStartState( "Planned" ) + self:AddTransition( "Planned", "Assign", "Assigned" ) + self:AddTransition( "Assigned", "AssignUnit", "Assigned" ) + self:AddTransition( "Assigned", "Success", "Success" ) + self:AddTransition( "Assigned", "Fail", "Failed" ) + self:AddTransition( "Assigned", "Abort", "Aborted" ) + self:AddTransition( "Assigned", "Cancel", "Cancelled" ) + self:AddTransition( "*", "PlayerCrashed", "*" ) + self:AddTransition( "*", "PlayerAborted", "*" ) + self:AddTransition( "*", "PlayerDead", "*" ) + self:AddTransition( { "Failed", "Aborted", "Cancelled" }, "Replan", "Planned" ) + self:AddTransition( "*", "TimeOut", "Cancelled" ) + + self:E( "New TASK " .. TaskName ) + + self.Processes = {} + self.Fsm = {} + + self.Mission = Mission + self.CommandCenter = Mission:GetCommandCenter() + + self.SetGroup = SetGroupAssign + + self:SetType( TaskType ) + self:SetName( TaskName ) + self:SetID( Mission:GetNextTaskID( self ) ) -- The Mission orchestrates the task sequences .. + + self.TaskBriefing = "You are invited for the task: " .. self.TaskName .. "." + + self.FsmTemplate = self.FsmTemplate or FSM_PROCESS:New() + + Mission:AddTask( self ) + + return self +end + +--- Get the Task FSM Process Template +-- @param #TASK self +-- @return Core.Fsm#FSM_PROCESS +function TASK:GetUnitProcess() + + return self.FsmTemplate +end + +--- Sets the Task FSM Process Template +-- @param #TASK self +-- @param Core.Fsm#FSM_PROCESS +function TASK:SetUnitProcess( FsmTemplate ) + + self.FsmTemplate = FsmTemplate +end + +--- Add a PlayerUnit to join the Task. +-- For each Group within the Task, the Unit is check if it can join the Task. +-- If the Unit was not part of the Task, false is returned. +-- If the Unit is part of the Task, true is returned. +-- @param #TASK self +-- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player joining the Mission. +-- @param Wrapper.Group#GROUP PlayerGroup The GROUP of the player joining the Mission. +-- @return #boolean true if Unit is part of the Task. +function TASK:JoinUnit( PlayerUnit, PlayerGroup ) + self:F( { PlayerUnit = PlayerUnit, PlayerGroup = PlayerGroup } ) + + local PlayerUnitAdded = false + + local PlayerGroups = self:GetGroups() + + -- Is the PlayerGroup part of the PlayerGroups? + if PlayerGroups:IsIncludeObject( PlayerGroup ) then + + -- Check if the PlayerGroup is already assigned to the Task. If yes, the PlayerGroup is added to the Task. + -- If the PlayerGroup is not assigned to the Task, the menu needs to be set. In that case, the PlayerUnit will become the GroupPlayer leader. + if self:IsStatePlanned() or self:IsStateReplanned() then + self:SetMenuForGroup( PlayerGroup ) + self:MessageToGroups( PlayerUnit:GetPlayerName() .. " is planning to join Task " .. self:GetName() ) + end + if self:IsStateAssigned() then + local IsAssignedToGroup = self:IsAssignedToGroup( PlayerGroup ) + self:E( { IsAssignedToGroup = IsAssignedToGroup } ) + if IsAssignedToGroup then + self:AssignToUnit( PlayerUnit ) + self:MessageToGroups( PlayerUnit:GetPlayerName() .. " joined Task " .. self:GetName() ) + end + end + end + + return PlayerUnitAdded +end + +--- Abort a PlayerUnit from a Task. +-- If the Unit was not part of the Task, false is returned. +-- If the Unit is part of the Task, true is returned. +-- @param #TASK self +-- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player aborting the Task. +-- @return #boolean true if Unit is part of the Task. +function TASK:AbortUnit( PlayerUnit ) + self:F( { PlayerUnit = PlayerUnit } ) + + local PlayerUnitAborted = false + + local PlayerGroups = self:GetGroups() + local PlayerGroup = PlayerUnit:GetGroup() + + -- Is the PlayerGroup part of the PlayerGroups? + if PlayerGroups:IsIncludeObject( PlayerGroup ) then + + -- Check if the PlayerGroup is already assigned to the Task. If yes, the PlayerGroup is aborted from the Task. + -- If the PlayerUnit was the last unit of the PlayerGroup, the menu needs to be removed from the Group. + if self:IsStateAssigned() then + local IsAssignedToGroup = self:IsAssignedToGroup( PlayerGroup ) + self:E( { IsAssignedToGroup = IsAssignedToGroup } ) + if IsAssignedToGroup then + self:UnAssignFromUnit( PlayerUnit ) + self:MessageToGroups( PlayerUnit:GetPlayerName() .. " aborted Task " .. self:GetName() ) + self:E( { TaskGroup = PlayerGroup:GetName(), GetUnits = PlayerGroup:GetUnits() } ) + if #PlayerGroup:GetUnits() == 1 then + PlayerGroup:SetState( PlayerGroup, "Assigned", nil ) + self:RemoveMenuForGroup( PlayerGroup ) + end + self:PlayerAborted( PlayerUnit ) + end + end + end + + return PlayerUnitAborted +end + +--- A PlayerUnit crashed in a Task. Abort the Player. +-- If the Unit was not part of the Task, false is returned. +-- If the Unit is part of the Task, true is returned. +-- @param #TASK self +-- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player aborting the Task. +-- @return #boolean true if Unit is part of the Task. +function TASK:CrashUnit( PlayerUnit ) + self:F( { PlayerUnit = PlayerUnit } ) + + local PlayerUnitCrashed = false + + local PlayerGroups = self:GetGroups() + local PlayerGroup = PlayerUnit:GetGroup() + + -- Is the PlayerGroup part of the PlayerGroups? + if PlayerGroups:IsIncludeObject( PlayerGroup ) then + + -- Check if the PlayerGroup is already assigned to the Task. If yes, the PlayerGroup is aborted from the Task. + -- If the PlayerUnit was the last unit of the PlayerGroup, the menu needs to be removed from the Group. + if self:IsStateAssigned() then + local IsAssignedToGroup = self:IsAssignedToGroup( PlayerGroup ) + self:E( { IsAssignedToGroup = IsAssignedToGroup } ) + if IsAssignedToGroup then + self:UnAssignFromUnit( PlayerUnit ) + self:MessageToGroups( PlayerUnit:GetPlayerName() .. " crashed in Task " .. self:GetName() ) + self:E( { TaskGroup = PlayerGroup:GetName(), GetUnits = PlayerGroup:GetUnits() } ) + if #PlayerGroup:GetUnits() == 1 then + PlayerGroup:SetState( PlayerGroup, "Assigned", nil ) + self:RemoveMenuForGroup( PlayerGroup ) + end + self:PlayerCrashed( PlayerUnit ) + end + end + end + + return PlayerUnitCrashed +end + + + +--- Gets the Mission to where the TASK belongs. +-- @param #TASK self +-- @return Tasking.Mission#MISSION +function TASK:GetMission() + + return self.Mission +end + + +--- Gets the SET_GROUP assigned to the TASK. +-- @param #TASK self +-- @return Core.Set#SET_GROUP +function TASK:GetGroups() + return self.SetGroup +end + + + +--- Assign the @{Task}to a @{Group}. +-- @param #TASK self +-- @param Wrapper.Group#GROUP TaskGroup +-- @return #TASK +function TASK:AssignToGroup( TaskGroup ) + self:F2( TaskGroup:GetName() ) + + local TaskGroupName = TaskGroup:GetName() + + TaskGroup:SetState( TaskGroup, "Assigned", self ) + + self:RemoveMenuForGroup( TaskGroup ) + self:SetAssignedMenuForGroup( TaskGroup ) + + local TaskUnits = TaskGroup:GetUnits() + for UnitID, UnitData in pairs( TaskUnits ) do + local TaskUnit = UnitData -- Wrapper.Unit#UNIT + local PlayerName = TaskUnit:GetPlayerName() + self:E(PlayerName) + if PlayerName ~= nil or PlayerName ~= "" then + self:AssignToUnit( TaskUnit ) + end + end + + return self +end + +--- +-- @param #TASK self +-- @param Wrapper.Group#GROUP FindGroup +-- @return #boolean +function TASK:HasGroup( FindGroup ) + + return self:GetGroups():IsIncludeObject( FindGroup ) + +end + +--- Assign the @{Task} to an alive @{Unit}. +-- @param #TASK self +-- @param Wrapper.Unit#UNIT TaskUnit +-- @return #TASK self +function TASK:AssignToUnit( TaskUnit ) + self:F( TaskUnit:GetName() ) + + local FsmTemplate = self:GetUnitProcess() + + -- Assign a new FsmUnit to TaskUnit. + local FsmUnit = self:SetStateMachine( TaskUnit, FsmTemplate:Copy( TaskUnit, self ) ) -- Core.Fsm#FSM_PROCESS + self:E({"Address FsmUnit", tostring( FsmUnit ) } ) + + FsmUnit:SetStartState( "Planned" ) + FsmUnit:Accept() -- Each Task needs to start with an Accept event to start the flow. + + return self +end + +--- UnAssign the @{Task} from an alive @{Unit}. +-- @param #TASK self +-- @param Wrapper.Unit#UNIT TaskUnit +-- @return #TASK self +function TASK:UnAssignFromUnit( TaskUnit ) + self:F( TaskUnit ) + + self:RemoveStateMachine( TaskUnit ) + + return self +end + +--- Sets the TimeOut for the @{Task}. If @{Task} stayed planned for longer than TimeOut, it gets into Cancelled status. +-- @param #TASK self +-- @param #integer Timer in seconds +-- @return #TASK self +function TASK:SetTimeOut ( Timer ) + self:F( Timer ) + self.TimeOut = Timer + self:__TimeOut( self.TimeOut ) + return self +end + +--- Send a message of the @{Task} to the assigned @{Group}s. +-- @param #TASK self +function TASK:MessageToGroups( Message ) + self:F( { Message = Message } ) + + local Mission = self:GetMission() + local CC = Mission:GetCommandCenter() + + for TaskGroupName, TaskGroup in pairs( self.SetGroup:GetSet() ) do + local TaskGroup = TaskGroup -- Wrapper.Group#GROUP + CC:MessageToGroup( Message, TaskGroup, TaskGroup:GetName() ) + end +end + + +--- Send the briefng message of the @{Task} to the assigned @{Group}s. +-- @param #TASK self +function TASK:SendBriefingToAssignedGroups() + self:F2() + + for TaskGroupName, TaskGroup in pairs( self.SetGroup:GetSet() ) do + + if self:IsAssignedToGroup( TaskGroup ) then + TaskGroup:Message( self.TaskBriefing, 60 ) + end + end +end + + +--- Assign the @{Task} from the @{Group}s. +-- @param #TASK self +function TASK:UnAssignFromGroups() + self:F2() + + for TaskGroupName, TaskGroup in pairs( self.SetGroup:GetSet() ) do + + TaskGroup:SetState( TaskGroup, "Assigned", nil ) + + self:RemoveMenuForGroup( TaskGroup ) + + local TaskUnits = TaskGroup:GetUnits() + for UnitID, UnitData in pairs( TaskUnits ) do + local TaskUnit = UnitData -- Wrapper.Unit#UNIT + local PlayerName = TaskUnit:GetPlayerName() + if PlayerName ~= nil or PlayerName ~= "" then + self:UnAssignFromUnit( TaskUnit ) + end + end + end +end + +--- Returns if the @{Task} is assigned to the Group. +-- @param #TASK self +-- @param Wrapper.Group#GROUP TaskGroup +-- @return #boolean +function TASK:IsAssignedToGroup( TaskGroup ) + + local TaskGroupName = TaskGroup:GetName() + + if self:IsStateAssigned() then + if TaskGroup:GetState( TaskGroup, "Assigned" ) == self then + return true + end + end + + return false +end + +--- Returns if the @{Task} has still alive and assigned Units. +-- @param #TASK self +-- @return #boolean +function TASK:HasAliveUnits() + self:F() + + for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do + if self:IsStateAssigned() then + if self:IsAssignedToGroup( TaskGroup ) then + for TaskUnitID, TaskUnit in pairs( TaskGroup:GetUnits() ) do + if TaskUnit:IsAlive() then + self:T( { HasAliveUnits = true } ) + return true + end + end + end + end + end + + self:T( { HasAliveUnits = false } ) + return false +end + +--- Set the menu options of the @{Task} to all the groups in the SetGroup. +-- @param #TASK self +function TASK:SetMenu() + self:F() + + self.SetGroup:Flush() + for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do + if self:IsStatePlanned() or self:IsStateReplanned() then + self:SetMenuForGroup( TaskGroup ) + end + end +end + + +--- Remove the menu options of the @{Task} to all the groups in the SetGroup. +-- @param #TASK self +-- @return #TASK self +function TASK:RemoveMenu() + + for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do + self:RemoveMenuForGroup( TaskGroup ) + end +end + + +--- Set the Menu for a Group +-- @param #TASK self +function TASK:SetMenuForGroup( TaskGroup ) + + if not self:IsAssignedToGroup( TaskGroup ) then + self:SetPlannedMenuForGroup( TaskGroup, self:GetTaskName() ) + else + self:SetAssignedMenuForGroup( TaskGroup ) + end +end + + +--- Set the planned menu option of the @{Task}. +-- @param #TASK self +-- @param Wrapper.Group#GROUP TaskGroup +-- @param #string MenuText The menu text. +-- @return #TASK self +function TASK:SetPlannedMenuForGroup( TaskGroup, MenuText ) + self:E( TaskGroup:GetName() ) + + local Mission = self:GetMission() + local MissionMenu = Mission:GetMissionMenu( TaskGroup ) + + local TaskType = self:GetType() + local TaskTypeMenu = MENU_GROUP:New( TaskGroup, TaskType, MissionMenu ) + local TaskMenu = MENU_GROUP_COMMAND:New( TaskGroup, MenuText, TaskTypeMenu, self.MenuAssignToGroup, { self = self, TaskGroup = TaskGroup } ) + + return self +end + +--- Set the assigned menu options of the @{Task}. +-- @param #TASK self +-- @param Wrapper.Group#GROUP TaskGroup +-- @return #TASK self +function TASK:SetAssignedMenuForGroup( TaskGroup ) + self:E( TaskGroup:GetName() ) + + local Mission = self:GetMission() + local MissionMenu = Mission:GetMissionMenu( TaskGroup ) + + self:E( { MissionMenu = MissionMenu } ) + + local TaskTypeMenu = MENU_GROUP_COMMAND:New( TaskGroup, "Task Status", MissionMenu, self.MenuTaskStatus, { self = self, TaskGroup = TaskGroup } ) + local TaskMenu = MENU_GROUP_COMMAND:New( TaskGroup, "Abort Task", MissionMenu, self.MenuTaskAbort, { self = self, TaskGroup = TaskGroup } ) + + return self +end + +--- Remove the menu option of the @{Task} for a @{Group}. +-- @param #TASK self +-- @param Wrapper.Group#GROUP TaskGroup +-- @return #TASK self +function TASK:RemoveMenuForGroup( TaskGroup ) + + local Mission = self:GetMission() + local MissionName = Mission:GetName() + + local MissionMenu = Mission:GetMissionMenu( TaskGroup ) + MissionMenu:Remove() +end + +function TASK.MenuAssignToGroup( MenuParam ) + + local self = MenuParam.self + local TaskGroup = MenuParam.TaskGroup + + self:E( "Assigned menu selected") + + self:AssignToGroup( TaskGroup ) +end + +function TASK.MenuTaskStatus( MenuParam ) + + local self = MenuParam.self + local TaskGroup = MenuParam.TaskGroup + + --self:AssignToGroup( TaskGroup ) +end + +function TASK.MenuTaskAbort( MenuParam ) + + local self = MenuParam.self + local TaskGroup = MenuParam.TaskGroup + + self:Abort() +end + + + +--- Returns the @{Task} name. +-- @param #TASK self +-- @return #string TaskName +function TASK:GetTaskName() + return self.TaskName +end + + + + +--- Get the default or currently assigned @{Process} template with key ProcessName. +-- @param #TASK self +-- @param #string ProcessName +-- @return Core.Fsm#FSM_PROCESS +function TASK:GetProcessTemplate( ProcessName ) + + local ProcessTemplate = self.ProcessClasses[ProcessName] + + return ProcessTemplate +end + + + +-- TODO: Obscolete? +--- Fail processes from @{Task} with key @{Unit} +-- @param #TASK self +-- @param #string TaskUnitName +-- @return #TASK self +function TASK:FailProcesses( TaskUnitName ) + + for ProcessID, ProcessData in pairs( self.Processes[TaskUnitName] ) do + local Process = ProcessData + Process.Fsm:Fail() + end +end + +--- Add a FiniteStateMachine to @{Task} with key Task@{Unit} +-- @param #TASK self +-- @param Wrapper.Unit#UNIT TaskUnit +-- @return #TASK self +function TASK:SetStateMachine( TaskUnit, Fsm ) + self:F( { TaskUnit, self.Fsm[TaskUnit] ~= nil } ) + + self.Fsm[TaskUnit] = Fsm + + return Fsm +end + +--- Remove FiniteStateMachines from @{Task} with key Task@{Unit} +-- @param #TASK self +-- @param Wrapper.Unit#UNIT TaskUnit +-- @return #TASK self +function TASK:RemoveStateMachine( TaskUnit ) + self:F( { TaskUnit, self.Fsm[TaskUnit] ~= nil } ) + + self.Fsm[TaskUnit] = nil + collectgarbage() + self:T( "Garbage Collected, Processes should be finalized now ...") +end + +--- Checks if there is a FiniteStateMachine assigned to Task@{Unit} for @{Task} +-- @param #TASK self +-- @param Wrapper.Unit#UNIT TaskUnit +-- @return #TASK self +function TASK:HasStateMachine( TaskUnit ) + self:F( { TaskUnit, self.Fsm[TaskUnit] ~= nil } ) + + return ( self.Fsm[TaskUnit] ~= nil ) +end + + +--- Gets the Scoring of the task +-- @param #TASK self +-- @return Functional.Scoring#SCORING Scoring +function TASK:GetScoring() + return self.Mission:GetScoring() +end + + +--- Gets the Task Index, which is a combination of the Task type, the Task name. +-- @param #TASK self +-- @return #string The Task ID +function TASK:GetTaskIndex() + + local TaskType = self:GetType() + local TaskName = self:GetName() + + return TaskType .. "." .. TaskName +end + +--- Sets the Name of the Task +-- @param #TASK self +-- @param #string TaskName +function TASK:SetName( TaskName ) + self.TaskName = TaskName +end + +--- Gets the Name of the Task +-- @param #TASK self +-- @return #string The Task Name +function TASK:GetName() + return self.TaskName +end + +--- Sets the Type of the Task +-- @param #TASK self +-- @param #string TaskType +function TASK:SetType( TaskType ) + self.TaskType = TaskType +end + +--- Gets the Type of the Task +-- @param #TASK self +-- @return #string TaskType +function TASK:GetType() + return self.TaskType +end + +--- Sets the ID of the Task +-- @param #TASK self +-- @param #string TaskID +function TASK:SetID( TaskID ) + self.TaskID = TaskID +end + +--- Gets the ID of the Task +-- @param #TASK self +-- @return #string TaskID +function TASK:GetID() + return self.TaskID +end + + +--- Sets a @{Task} to status **Success**. +-- @param #TASK self +function TASK:StateSuccess() + self:SetState( self, "State", "Success" ) + return self +end + +--- Is the @{Task} status **Success**. +-- @param #TASK self +function TASK:IsStateSuccess() + return self:Is( "Success" ) +end + +--- Sets a @{Task} to status **Failed**. +-- @param #TASK self +function TASK:StateFailed() + self:SetState( self, "State", "Failed" ) + return self +end + +--- Is the @{Task} status **Failed**. +-- @param #TASK self +function TASK:IsStateFailed() + return self:Is( "Failed" ) +end + +--- Sets a @{Task} to status **Planned**. +-- @param #TASK self +function TASK:StatePlanned() + self:SetState( self, "State", "Planned" ) + return self +end + +--- Is the @{Task} status **Planned**. +-- @param #TASK self +function TASK:IsStatePlanned() + return self:Is( "Planned" ) +end + +--- Sets a @{Task} to status **Assigned**. +-- @param #TASK self +function TASK:StateAssigned() + self:SetState( self, "State", "Assigned" ) + return self +end + +--- Is the @{Task} status **Assigned**. +-- @param #TASK self +function TASK:IsStateAssigned() + return self:Is( "Assigned" ) +end + +--- Sets a @{Task} to status **Hold**. +-- @param #TASK self +function TASK:StateHold() + self:SetState( self, "State", "Hold" ) + return self +end + +--- Is the @{Task} status **Hold**. +-- @param #TASK self +function TASK:IsStateHold() + return self:Is( "Hold" ) +end + +--- Sets a @{Task} to status **Replanned**. +-- @param #TASK self +function TASK:StateReplanned() + self:SetState( self, "State", "Replanned" ) + return self +end + +--- Is the @{Task} status **Replanned**. +-- @param #TASK self +function TASK:IsStateReplanned() + return self:Is( "Replanned" ) +end + +--- Gets the @{Task} status. +-- @param #TASK self +function TASK:GetStateString() + return self:GetState( self, "State" ) +end + +--- Sets a @{Task} briefing. +-- @param #TASK self +-- @param #string TaskBriefing +-- @return #TASK self +function TASK:SetBriefing( TaskBriefing ) + self.TaskBriefing = TaskBriefing + return self +end + + + + +--- FSM function for a TASK +-- @param #TASK self +-- @param #string Event +-- @param #string From +-- @param #string To +function TASK:onenterAssigned( From, Event, To ) + + self:E("Task Assigned") + + self:MessageToGroups( "Task " .. self:GetName() .. " has been assigned to your group." ) + self:GetMission():__Start( 1 ) +end + + +--- FSM function for a TASK +-- @param #TASK self +-- @param #string Event +-- @param #string From +-- @param #string To +function TASK:onenterSuccess( From, Event, To ) + + self:E( "Task Success" ) + + self:MessageToGroups( "Task " .. self:GetName() .. " is successful! Good job!" ) + self:UnAssignFromGroups() + + self:GetMission():__Complete( 1 ) + +end + + +--- FSM function for a TASK +-- @param #TASK self +-- @param #string From +-- @param #string Event +-- @param #string To +function TASK:onenterAborted( From, Event, To ) + + self:E( "Task Aborted" ) + + self:GetMission():GetCommandCenter():MessageToCoalition( "Task " .. self:GetName() .. " has been aborted! Task may be replanned." ) + + self:UnAssignFromGroups() + + self:__Replan( 5 ) +end + +--- FSM function for a TASK +-- @param #TASK self +-- @param #string From +-- @param #string Event +-- @param #string To +function TASK:onafterReplan( From, Event, To ) + + self:E( "Task Replanned" ) + + self:GetMission():GetCommandCenter():MessageToCoalition( "Replanning Task " .. self:GetName() .. "." ) + + self:SetMenu() + +end + +--- FSM function for a TASK +-- @param #TASK self +-- @param #string From +-- @param #string Event +-- @param #string To +function TASK:onenterFailed( From, Event, To ) + + self:E( "Task Failed" ) + + self:GetMission():GetCommandCenter():MessageToCoalition( "Task " .. self:GetName() .. " has failed!" ) + + self:UnAssignFromGroups() +end + +--- FSM function for a TASK +-- @param #TASK self +-- @param #string Event +-- @param #string From +-- @param #string To +function TASK:onstatechange( From, Event, To ) + + if self:IsTrace() then + MESSAGE:New( "@ Task " .. self.TaskName .. " : " .. Event .. " changed to state " .. To, 2 ):ToAll() + end + + if self.Scores[To] then + local Scoring = self:GetScoring() + if Scoring then + self:E( { self.Scores[To].ScoreText, self.Scores[To].Score } ) + Scoring:_AddMissionScore( self.Mission, self.Scores[To].ScoreText, self.Scores[To].Score ) + end + end + +end + +--- FSM function for a TASK +-- @param #TASK self +-- @param #string Event +-- @param #string From +-- @param #string To +function TASK:onenterPlanned( From, Event, To) + if not self.TimeOut == 0 then + self.__TimeOut( self.TimeOut ) + end +end + +--- FSM function for a TASK +-- @param #TASK self +-- @param #string Event +-- @param #string From +-- @param #string To +function TASK:onbeforeTimeOut( From, Event, To ) + if From == "Planned" then + self:RemoveMenu() + return true + end + return false +end + +do -- Reporting + +--- Create a summary report of the Task. +-- List the Task Name and Status +-- @param #TASK self +-- @return #string +function TASK:ReportSummary() + + local Report = REPORT:New() + + -- List the name of the Task. + local Name = self:GetName() + + -- Determine the status of the Task. + local State = self:GetState() + + Report:Add( "Task " .. Name .. " - State '" .. State ) + + return Report:Text() +end + + +--- Create a detailed report of the Task. +-- List the Task Status, and the Players assigned to the Task. +-- @param #TASK self +-- @return #string +function TASK:ReportDetails() + + local Report = REPORT:New() + + -- List the name of the Task. + local Name = self:GetName() + + -- Determine the status of the Task. + local State = self:GetState() + + + -- Loop each Unit active in the Task, and find Player Names. + local PlayerNames = {} + for PlayerGroupID, PlayerGroup in pairs( self:GetGroups():GetSet() ) do + local Player = PlayerGroup -- Wrapper.Group#GROUP + for PlayerUnitID, PlayerUnit in pairs( PlayerGroup:GetUnits() ) do + local PlayerUnit = PlayerUnit -- Wrapper.Unit#UNIT + if PlayerUnit and PlayerUnit:IsAlive() then + local PlayerName = PlayerUnit:GetPlayerName() + PlayerNames[#PlayerNames+1] = PlayerName + end + end + local PlayerNameText = table.concat( PlayerNames, ", " ) + Report:Add( "Task " .. Name .. " - State '" .. State .. "' - Players " .. PlayerNameText ) + end + + -- Loop each Process in the Task, and find Reporting Details. + + return Report:Text() +end + + +end -- Reporting +--- This module contains the DETECTION_MANAGER class and derived classes. +-- +-- === +-- +-- 1) @{DetectionManager#DETECTION_MANAGER} class, extends @{Base#BASE} +-- ==================================================================== +-- The @{DetectionManager#DETECTION_MANAGER} class defines the core functions to report detected objects to groups. +-- Reportings can be done in several manners, and it is up to the derived classes if DETECTION_MANAGER to model the reporting behaviour. +-- +-- 1.1) DETECTION_MANAGER constructor: +-- ----------------------------------- +-- * @{DetectionManager#DETECTION_MANAGER.New}(): Create a new DETECTION_MANAGER instance. +-- +-- 1.2) DETECTION_MANAGER reporting: +-- --------------------------------- +-- Derived DETECTION_MANAGER classes will reports detected units using the method @{DetectionManager#DETECTION_MANAGER.ReportDetected}(). This method implements polymorphic behaviour. +-- +-- The time interval in seconds of the reporting can be changed using the methods @{DetectionManager#DETECTION_MANAGER.SetReportInterval}(). +-- To control how long a reporting message is displayed, use @{DetectionManager#DETECTION_MANAGER.SetReportDisplayTime}(). +-- Derived classes need to implement the method @{DetectionManager#DETECTION_MANAGER.GetReportDisplayTime}() to use the correct display time for displayed messages during a report. +-- +-- Reporting can be started and stopped using the methods @{DetectionManager#DETECTION_MANAGER.StartReporting}() and @{DetectionManager#DETECTION_MANAGER.StopReporting}() respectively. +-- If an ad-hoc report is requested, use the method @{DetectionManager#DETECTION_MANAGER#ReportNow}(). +-- +-- The default reporting interval is every 60 seconds. The reporting messages are displayed 15 seconds. +-- +-- === +-- +-- 2) @{DetectionManager#DETECTION_REPORTING} class, extends @{DetectionManager#DETECTION_MANAGER} +-- ========================================================================================= +-- The @{DetectionManager#DETECTION_REPORTING} class implements detected units reporting. Reporting can be controlled using the reporting methods available in the @{DetectionManager#DETECTION_MANAGER} class. +-- +-- 2.1) DETECTION_REPORTING constructor: +-- ------------------------------- +-- The @{DetectionManager#DETECTION_REPORTING.New}() method creates a new DETECTION_REPORTING instance. +-- +-- === +-- +-- 3) @{#DETECTION_DISPATCHER} class, extends @{#DETECTION_MANAGER} +-- ================================================================ +-- The @{#DETECTION_DISPATCHER} class implements the dynamic dispatching of tasks upon groups of detected units determined a @{Set} of FAC (groups). +-- The FAC will detect units, will group them, and will dispatch @{Task}s to groups. Depending on the type of target detected, different tasks will be dispatched. +-- Find a summary below describing for which situation a task type is created: +-- +-- * **CAS Task**: Is created when there are enemy ground units within range of the FAC, while there are friendly units in the FAC perimeter. +-- * **BAI Task**: Is created when there are enemy ground units within range of the FAC, while there are NO other friendly units within the FAC perimeter. +-- * **SEAD Task**: Is created when there are enemy ground units wihtin range of the FAC, with air search radars. +-- +-- Other task types will follow... +-- +-- 3.1) DETECTION_DISPATCHER constructor: +-- -------------------------------------- +-- The @{#DETECTION_DISPATCHER.New}() method creates a new DETECTION_DISPATCHER instance. +-- +-- === +-- +-- ### Contributions: Mechanist, Prof_Hilactic, FlightControl - Concept & Testing +-- ### Author: FlightControl - Framework Design & Programming +-- +-- @module DetectionManager + +do -- DETECTION MANAGER + + --- DETECTION_MANAGER class. + -- @type DETECTION_MANAGER + -- @field Set#SET_GROUP SetGroup The groups to which the FAC will report to. + -- @field Functional.Detection#DETECTION_BASE Detection The DETECTION_BASE object that is used to report the detected objects. + -- @extends Base#BASE + DETECTION_MANAGER = { + ClassName = "DETECTION_MANAGER", + SetGroup = nil, + Detection = nil, + } + + --- FAC constructor. + -- @param #DETECTION_MANAGER self + -- @param Set#SET_GROUP SetGroup + -- @param Functional.Detection#DETECTION_BASE Detection + -- @return #DETECTION_MANAGER self + function DETECTION_MANAGER:New( SetGroup, Detection ) + + -- Inherits from BASE + local self = BASE:Inherit( self, BASE:New() ) -- Functional.Detection#DETECTION_MANAGER + + self.SetGroup = SetGroup + self.Detection = Detection + + self:SetReportInterval( 30 ) + self:SetReportDisplayTime( 25 ) + + return self + end + + --- Set the reporting time interval. + -- @param #DETECTION_MANAGER self + -- @param #number ReportInterval The interval in seconds when a report needs to be done. + -- @return #DETECTION_MANAGER self + function DETECTION_MANAGER:SetReportInterval( ReportInterval ) + self:F2() + + self._ReportInterval = ReportInterval + end + + + --- Set the reporting message display time. + -- @param #DETECTION_MANAGER self + -- @param #number ReportDisplayTime The display time in seconds when a report needs to be done. + -- @return #DETECTION_MANAGER self + function DETECTION_MANAGER:SetReportDisplayTime( ReportDisplayTime ) + self:F2() + + self._ReportDisplayTime = ReportDisplayTime + end + + --- Get the reporting message display time. + -- @param #DETECTION_MANAGER self + -- @return #number ReportDisplayTime The display time in seconds when a report needs to be done. + function DETECTION_MANAGER:GetReportDisplayTime() + self:F2() + + return self._ReportDisplayTime + end + + + + --- Reports the detected items to the @{Set#SET_GROUP}. + -- @param #DETECTION_MANAGER self + -- @param Functional.Detection#DETECTION_BASE Detection + -- @return #DETECTION_MANAGER self + function DETECTION_MANAGER:ReportDetected( Detection ) + self:F2() + + end + + --- Schedule the FAC reporting. + -- @param #DETECTION_MANAGER self + -- @param #number DelayTime The delay in seconds to wait the reporting. + -- @param #number ReportInterval The repeat interval in seconds for the reporting to happen repeatedly. + -- @return #DETECTION_MANAGER self + function DETECTION_MANAGER:Schedule( DelayTime, ReportInterval ) + self:F2() + + self._ScheduleDelayTime = DelayTime + + self:SetReportInterval( ReportInterval ) + + self.FacScheduler = SCHEDULER:New(self, self._FacScheduler, { self, "DetectionManager" }, self._ScheduleDelayTime, self._ReportInterval ) + return self + end + + --- Report the detected @{Unit#UNIT}s detected within the @{Detection#DETECTION_BASE} object to the @{Set#SET_GROUP}s. + -- @param #DETECTION_MANAGER self + function DETECTION_MANAGER:_FacScheduler( SchedulerName ) + self:F2( { SchedulerName } ) + + return self:ProcessDetected( self.Detection ) + +-- self.SetGroup:ForEachGroup( +-- --- @param Wrapper.Group#GROUP Group +-- function( Group ) +-- if Group:IsAlive() then +-- return self:ProcessDetected( self.Detection ) +-- end +-- end +-- ) + +-- return true + end + +end + + +do -- DETECTION_REPORTING + + --- DETECTION_REPORTING class. + -- @type DETECTION_REPORTING + -- @field Set#SET_GROUP SetGroup The groups to which the FAC will report to. + -- @field Functional.Detection#DETECTION_BASE Detection The DETECTION_BASE object that is used to report the detected objects. + -- @extends #DETECTION_MANAGER + DETECTION_REPORTING = { + ClassName = "DETECTION_REPORTING", + } + + + --- DETECTION_REPORTING constructor. + -- @param #DETECTION_REPORTING self + -- @param Set#SET_GROUP SetGroup + -- @param Functional.Detection#DETECTION_AREAS Detection + -- @return #DETECTION_REPORTING self + function DETECTION_REPORTING:New( SetGroup, Detection ) + + -- Inherits from DETECTION_MANAGER + local self = BASE:Inherit( self, DETECTION_MANAGER:New( SetGroup, Detection ) ) -- #DETECTION_REPORTING + + self:Schedule( 1, 30 ) + return self + end + + --- Creates a string of the detected items in a @{Detection}. + -- @param #DETECTION_MANAGER self + -- @param Set#SET_UNIT DetectedSet The detected Set created by the @{Detection#DETECTION_BASE} object. + -- @return #DETECTION_MANAGER self + function DETECTION_REPORTING:GetDetectedItemsText( DetectedSet ) + self:F2() + + local MT = {} -- Message Text + local UnitTypes = {} + + for DetectedUnitID, DetectedUnitData in pairs( DetectedSet:GetSet() ) do + local DetectedUnit = DetectedUnitData -- Wrapper.Unit#UNIT + if DetectedUnit:IsAlive() then + local UnitType = DetectedUnit:GetTypeName() + + if not UnitTypes[UnitType] then + UnitTypes[UnitType] = 1 + else + UnitTypes[UnitType] = UnitTypes[UnitType] + 1 + end + end + end + + for UnitTypeID, UnitType in pairs( UnitTypes ) do + MT[#MT+1] = UnitType .. " of " .. UnitTypeID + end + + return table.concat( MT, ", " ) + end + + + + --- Reports the detected items to the @{Set#SET_GROUP}. + -- @param #DETECTION_REPORTING self + -- @param Wrapper.Group#GROUP Group The @{Group} object to where the report needs to go. + -- @param Functional.Detection#DETECTION_AREAS Detection The detection created by the @{Detection#DETECTION_BASE} object. + -- @return #boolean Return true if you want the reporting to continue... false will cancel the reporting loop. + function DETECTION_REPORTING:ProcessDetected( Group, Detection ) + self:F2( Group ) + + self:E( Group ) + local DetectedMsg = {} + for DetectedAreaID, DetectedAreaData in pairs( Detection:GetDetectedAreas() ) do + local DetectedArea = DetectedAreaData -- Functional.Detection#DETECTION_AREAS.DetectedArea + DetectedMsg[#DetectedMsg+1] = " - Group #" .. DetectedAreaID .. ": " .. self:GetDetectedItemsText( DetectedArea.Set ) + end + local FACGroup = Detection:GetDetectionGroups() + FACGroup:MessageToGroup( "Reporting detected target groups:\n" .. table.concat( DetectedMsg, "\n" ), self:GetReportDisplayTime(), Group ) + + return true + end + +end + +do -- DETECTION_DISPATCHER + + --- DETECTION_DISPATCHER class. + -- @type DETECTION_DISPATCHER + -- @field Set#SET_GROUP SetGroup The groups to which the FAC will report to. + -- @field Functional.Detection#DETECTION_BASE Detection The DETECTION_BASE object that is used to report the detected objects. + -- @field Tasking.Mission#MISSION Mission + -- @field Wrapper.Group#GROUP CommandCenter + -- @extends Tasking.DetectionManager#DETECTION_MANAGER + DETECTION_DISPATCHER = { + ClassName = "DETECTION_DISPATCHER", + Mission = nil, + CommandCenter = nil, + Detection = nil, + } + + + --- DETECTION_DISPATCHER constructor. + -- @param #DETECTION_DISPATCHER self + -- @param Set#SET_GROUP SetGroup + -- @param Functional.Detection#DETECTION_BASE Detection + -- @return #DETECTION_DISPATCHER self + function DETECTION_DISPATCHER:New( Mission, CommandCenter, SetGroup, Detection ) + + -- Inherits from DETECTION_MANAGER + local self = BASE:Inherit( self, DETECTION_MANAGER:New( SetGroup, Detection ) ) -- #DETECTION_DISPATCHER + + self.Detection = Detection + self.CommandCenter = CommandCenter + self.Mission = Mission + + self:Schedule( 30 ) + return self + end + + + --- Creates a SEAD task when there are targets for it. + -- @param #DETECTION_DISPATCHER self + -- @param Functional.Detection#DETECTION_AREAS.DetectedArea DetectedArea + -- @return Set#SET_UNIT TargetSetUnit: The target set of units. + -- @return #nil If there are no targets to be set. + function DETECTION_DISPATCHER:EvaluateSEAD( DetectedArea ) + self:F( { DetectedArea.AreaID } ) + + local DetectedSet = DetectedArea.Set + local DetectedZone = DetectedArea.Zone + + -- Determine if the set has radar targets. If it does, construct a SEAD task. + local RadarCount = DetectedSet:HasSEAD() + + if RadarCount > 0 then + + -- Here we're doing something advanced... We're copying the DetectedSet, but making a new Set only with SEADable Radar units in it. + local TargetSetUnit = SET_UNIT:New() + TargetSetUnit:SetDatabase( DetectedSet ) + TargetSetUnit:FilterHasSEAD() + TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection. + + return TargetSetUnit + end + + return nil + end + + --- Creates a CAS task when there are targets for it. + -- @param #DETECTION_DISPATCHER self + -- @param Functional.Detection#DETECTION_AREAS.DetectedArea DetectedArea + -- @return Tasking.Task#TASK + function DETECTION_DISPATCHER:EvaluateCAS( DetectedArea ) + self:F( { DetectedArea.AreaID } ) + + local DetectedSet = DetectedArea.Set + local DetectedZone = DetectedArea.Zone + + + -- Determine if the set has radar targets. If it does, construct a SEAD task. + local GroundUnitCount = DetectedSet:HasGroundUnits() + local FriendliesNearBy = self.Detection:IsFriendliesNearBy( DetectedArea ) + + if GroundUnitCount > 0 and FriendliesNearBy == true then + + -- Copy the Set + local TargetSetUnit = SET_UNIT:New() + TargetSetUnit:SetDatabase( DetectedSet ) + TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection. + + return TargetSetUnit + end + + return nil + end + + --- Creates a BAI task when there are targets for it. + -- @param #DETECTION_DISPATCHER self + -- @param Functional.Detection#DETECTION_AREAS.DetectedArea DetectedArea + -- @return Tasking.Task#TASK + function DETECTION_DISPATCHER:EvaluateBAI( DetectedArea, FriendlyCoalition ) + self:F( { DetectedArea.AreaID } ) + + local DetectedSet = DetectedArea.Set + local DetectedZone = DetectedArea.Zone + + + -- Determine if the set has radar targets. If it does, construct a SEAD task. + local GroundUnitCount = DetectedSet:HasGroundUnits() + local FriendliesNearBy = self.Detection:IsFriendliesNearBy( DetectedArea ) + + if GroundUnitCount > 0 and FriendliesNearBy == false then + + -- Copy the Set + local TargetSetUnit = SET_UNIT:New() + TargetSetUnit:SetDatabase( DetectedSet ) + TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection. + + return TargetSetUnit + end + + return nil + end + + --- Evaluates the removal of the Task from the Mission. + -- Can only occur when the DetectedArea is Changed AND the state of the Task is "Planned". + -- @param #DETECTION_DISPATCHER self + -- @param Tasking.Mission#MISSION Mission + -- @param Tasking.Task#TASK Task + -- @param Functional.Detection#DETECTION_AREAS.DetectedArea DetectedArea + -- @return Tasking.Task#TASK + function DETECTION_DISPATCHER:EvaluateRemoveTask( Mission, Task, DetectedArea ) + + if Task then + if Task:IsStatePlanned() and DetectedArea.Changed == true then + self:E( "Removing Tasking: " .. Task:GetTaskName() ) + Task = Mission:RemoveTask( Task ) + end + end + + return Task + end + + + --- Assigns tasks in relation to the detected items to the @{Set#SET_GROUP}. + -- @param #DETECTION_DISPATCHER self + -- @param Functional.Detection#DETECTION_AREAS Detection The detection created by the @{Detection#DETECTION_AREAS} object. + -- @return #boolean Return true if you want the task assigning to continue... false will cancel the loop. + function DETECTION_DISPATCHER:ProcessDetected( Detection ) + self:F2() + + local AreaMsg = {} + local TaskMsg = {} + local ChangeMsg = {} + + local Mission = self.Mission + + --- First we need to the detected targets. + for DetectedAreaID, DetectedAreaData in ipairs( Detection:GetDetectedAreas() ) do + + local DetectedArea = DetectedAreaData -- Functional.Detection#DETECTION_AREAS.DetectedArea + local DetectedSet = DetectedArea.Set + local DetectedZone = DetectedArea.Zone + self:E( { "Targets in DetectedArea", DetectedArea.AreaID, DetectedSet:Count(), tostring( DetectedArea ) } ) + DetectedSet:Flush() + + local AreaID = DetectedArea.AreaID + + -- Evaluate SEAD Tasking + local SEADTask = Mission:GetTask( "SEAD." .. AreaID ) + SEADTask = self:EvaluateRemoveTask( Mission, SEADTask, DetectedArea ) + if not SEADTask then + local TargetSetUnit = self:EvaluateSEAD( DetectedArea ) -- Returns a SetUnit if there are targets to be SEADed... + if TargetSetUnit then + SEADTask = Mission:AddTask( TASK_SEAD:New( Mission, self.SetGroup, "SEAD." .. AreaID, TargetSetUnit , DetectedZone ) ) + end + end + if SEADTask and SEADTask:IsStatePlanned() then + self:E( "Planned" ) + --SEADTask:SetPlannedMenu() + TaskMsg[#TaskMsg+1] = " - " .. SEADTask:GetStateString() .. " SEAD " .. AreaID .. " - " .. SEADTask.TargetSetUnit:GetUnitTypesText() + end + + -- Evaluate CAS Tasking + local CASTask = Mission:GetTask( "CAS." .. AreaID ) + CASTask = self:EvaluateRemoveTask( Mission, CASTask, DetectedArea ) + if not CASTask then + local TargetSetUnit = self:EvaluateCAS( DetectedArea ) -- Returns a SetUnit if there are targets to be SEADed... + if TargetSetUnit then + CASTask = Mission:AddTask( TASK_A2G:New( Mission, self.SetGroup, "CAS." .. AreaID, "CAS", TargetSetUnit , DetectedZone, DetectedArea.NearestFAC ) ) + end + end + if CASTask and CASTask:IsStatePlanned() then + --CASTask:SetPlannedMenu() + TaskMsg[#TaskMsg+1] = " - " .. CASTask:GetStateString() .. " CAS " .. AreaID .. " - " .. CASTask.TargetSetUnit:GetUnitTypesText() + end + + -- Evaluate BAI Tasking + local BAITask = Mission:GetTask( "BAI." .. AreaID ) + BAITask = self:EvaluateRemoveTask( Mission, BAITask, DetectedArea ) + if not BAITask then + local TargetSetUnit = self:EvaluateBAI( DetectedArea, self.CommandCenter:GetCoalition() ) -- Returns a SetUnit if there are targets to be SEADed... + if TargetSetUnit then + BAITask = Mission:AddTask( TASK_A2G:New( Mission, self.SetGroup, "BAI." .. AreaID, "BAI", TargetSetUnit , DetectedZone, DetectedArea.NearestFAC ) ) + end + end + if BAITask and BAITask:IsStatePlanned() then + --BAITask:SetPlannedMenu() + TaskMsg[#TaskMsg+1] = " - " .. BAITask:GetStateString() .. " BAI " .. AreaID .. " - " .. BAITask.TargetSetUnit:GetUnitTypesText() + end + + if #TaskMsg > 0 then + + local ThreatLevel = Detection:GetTreatLevelA2G( DetectedArea ) + + local DetectedAreaVec3 = DetectedZone:GetVec3() + local DetectedAreaPointVec3 = POINT_VEC3:New( DetectedAreaVec3.x, DetectedAreaVec3.y, DetectedAreaVec3.z ) + local DetectedAreaPointLL = DetectedAreaPointVec3:ToStringLL( 3, true ) + AreaMsg[#AreaMsg+1] = string.format( " - Area #%d - %s - Threat Level [%s] (%2d)", + DetectedAreaID, + DetectedAreaPointLL, + string.rep( "■", ThreatLevel ), + ThreatLevel + ) + + -- Loop through the changes ... + local ChangeText = Detection:GetChangeText( DetectedArea ) + + if ChangeText ~= "" then + ChangeMsg[#ChangeMsg+1] = string.gsub( string.gsub( ChangeText, "\n", "%1 - " ), "^.", " - %1" ) + end + end + + -- OK, so the tasking has been done, now delete the changes reported for the area. + Detection:AcceptChanges( DetectedArea ) + + end + + -- TODO set menus using the HQ coordinator + Mission:GetCommandCenter():SetMenu() + + if #AreaMsg > 0 then + for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do + if not TaskGroup:GetState( TaskGroup, "Assigned" ) then + self.CommandCenter:MessageToGroup( + string.format( "HQ Reporting - Target areas for mission '%s':\nAreas:\n%s\n\nTasks:\n%s\n\nChanges:\n%s ", + self.Mission:GetName(), + table.concat( AreaMsg, "\n" ), + table.concat( TaskMsg, "\n" ), + table.concat( ChangeMsg, "\n" ) + ), self:GetReportDisplayTime(), TaskGroup + ) + end + end + end + + return true + end + +end--- This module contains the TASK_SEAD classes. +-- +-- 1) @{#TASK_SEAD} class, extends @{Task#TASK} +-- ================================================= +-- The @{#TASK_SEAD} class defines a SEAD task for a @{Set} of Target Units, located at a Target Zone, +-- based on the tasking capabilities defined in @{Task#TASK}. +-- The TASK_SEAD is implemented using a @{Statemachine#FSM_TASK}, and has the following statuses: +-- +-- * **None**: Start of the process +-- * **Planned**: The SEAD task is planned. Upon Planned, the sub-process @{Process_Fsm.Assign#ACT_ASSIGN_ACCEPT} is started to accept the task. +-- * **Assigned**: The SEAD task is assigned to a @{Group#GROUP}. Upon Assigned, the sub-process @{Process_Fsm.Route#ACT_ROUTE} is started to route the active Units in the Group to the attack zone. +-- * **Success**: The SEAD task is successfully completed. Upon Success, the sub-process @{Process_SEAD#PROCESS_SEAD} is started to follow-up successful SEADing of the targets assigned in the task. +-- * **Failed**: The SEAD task has failed. This will happen if the player exists the task early, without communicating a possible cancellation to HQ. +-- +-- === +-- +-- ### Authors: FlightControl - Design and Programming +-- +-- @module Task_SEAD + + + +do -- TASK_SEAD + + --- The TASK_SEAD class + -- @type TASK_SEAD + -- @field Set#SET_UNIT TargetSetUnit + -- @extends Tasking.Task#TASK + TASK_SEAD = { + ClassName = "TASK_SEAD", + } + + --- Instantiates a new TASK_SEAD. + -- @param #TASK_SEAD self + -- @param Tasking.Mission#MISSION Mission + -- @param Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. + -- @param #string TaskName The name of the Task. + -- @param Set#SET_UNIT UnitSetTargets + -- @param Core.Zone#ZONE_BASE TargetZone + -- @return #TASK_SEAD self + function TASK_SEAD:New( Mission, SetGroup, TaskName, TargetSetUnit, TargetZone ) + local self = BASE:Inherit( self, TASK:New( Mission, SetGroup, TaskName, "SEAD" ) ) -- Tasking.Task_SEAD#TASK_SEAD + self:F() + + self.TargetSetUnit = TargetSetUnit + self.TargetZone = TargetZone + + local Fsm = self:GetUnitProcess() + + Fsm:AddProcess ( "Planned", "Accept", ACT_ASSIGN_ACCEPT:New( self.TaskBriefing ), { Assigned = "Route", Rejected = "Eject" } ) + Fsm:AddProcess ( "Assigned", "Route", ACT_ROUTE_ZONE:New( self.TargetZone ), { Arrived = "Update" } ) + Fsm:AddTransition( "Rejected", "Eject", "Planned" ) + Fsm:AddTransition( "Arrived", "Update", "Updated" ) + Fsm:AddProcess ( "Updated", "Account", ACT_ACCOUNT_DEADS:New( self.TargetSetUnit, "SEAD" ), { Accounted = "Success" } ) + Fsm:AddProcess ( "Updated", "Smoke", ACT_ASSIST_SMOKE_TARGETS_ZONE:New( self.TargetSetUnit, self.TargetZone ) ) + Fsm:AddTransition( "Accounted", "Success", "Success" ) + Fsm:AddTransition( "Failed", "Fail", "Failed" ) + + function Fsm:onenterUpdated( TaskUnit ) + self:E( { self } ) + self:Account() + self:Smoke() + end + +-- _EVENTDISPATCHER:OnPlayerLeaveUnit( self._EventPlayerLeaveUnit, self ) +-- _EVENTDISPATCHER:OnDead( self._EventDead, self ) +-- _EVENTDISPATCHER:OnCrash( self._EventDead, self ) +-- _EVENTDISPATCHER:OnPilotDead( self._EventDead, self ) + + return self + end + + --- @param #TASK_SEAD self + function TASK_SEAD:GetPlannedMenuText() + return self:GetStateString() .. " - " .. self:GetTaskName() .. " ( " .. self.TargetSetUnit:GetUnitTypesText() .. " )" + end + +end +--- (AI) (SP) (MP) Tasking for Air to Ground Processes. +-- +-- 1) @{#TASK_A2G} class, extends @{Task#TASK} +-- ================================================= +-- The @{#TASK_A2G} class defines a CAS or BAI task of a @{Set} of Target Units, +-- located at a Target Zone, based on the tasking capabilities defined in @{Task#TASK}. +-- The TASK_A2G is implemented using a @{Statemachine#FSM_TASK}, and has the following statuses: +-- +-- * **None**: Start of the process +-- * **Planned**: The SEAD task is planned. Upon Planned, the sub-process @{Process_Fsm.Assign#ACT_ASSIGN_ACCEPT} is started to accept the task. +-- * **Assigned**: The SEAD task is assigned to a @{Group#GROUP}. Upon Assigned, the sub-process @{Process_Fsm.Route#ACT_ROUTE} is started to route the active Units in the Group to the attack zone. +-- * **Success**: The SEAD task is successfully completed. Upon Success, the sub-process @{Process_SEAD#PROCESS_SEAD} is started to follow-up successful SEADing of the targets assigned in the task. +-- * **Failed**: The SEAD task has failed. This will happen if the player exists the task early, without communicating a possible cancellation to HQ. +-- +-- === +-- +-- ### Authors: FlightControl - Design and Programming +-- +-- @module Task_A2G + + +do -- TASK_A2G + + --- The TASK_A2G class + -- @type TASK_A2G + -- @extends Tasking.Task#TASK + TASK_A2G = { + ClassName = "TASK_A2G", + } + + --- Instantiates a new TASK_A2G. + -- @param #TASK_A2G self + -- @param Tasking.Mission#MISSION Mission + -- @param Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. + -- @param #string TaskName The name of the Task. + -- @param #string TaskType BAI or CAS + -- @param Set#SET_UNIT UnitSetTargets + -- @param Core.Zone#ZONE_BASE TargetZone + -- @return #TASK_A2G self + function TASK_A2G:New( Mission, SetGroup, TaskName, TaskType, TargetSetUnit, TargetZone, FACUnit ) + local self = BASE:Inherit( self, TASK:New( Mission, SetGroup, TaskName, TaskType ) ) + self:F() + + self.TargetSetUnit = TargetSetUnit + self.TargetZone = TargetZone + self.FACUnit = FACUnit + + local A2GUnitProcess = self:GetUnitProcess() + + A2GUnitProcess:AddProcess ( "Planned", "Accept", ACT_ASSIGN_ACCEPT:New( "Attack the Area" ), { Assigned = "Route", Rejected = "Eject" } ) + A2GUnitProcess:AddProcess ( "Assigned", "Route", ACT_ROUTE_ZONE:New( self.TargetZone ), { Arrived = "Update" } ) + A2GUnitProcess:AddTransition( "Rejected", "Eject", "Planned" ) + A2GUnitProcess:AddTransition( "Arrived", "Update", "Updated" ) + A2GUnitProcess:AddProcess ( "Updated", "Account", ACT_ACCOUNT_DEADS:New( self.TargetSetUnit, "Attack" ), { Accounted = "Success" } ) + A2GUnitProcess:AddProcess ( "Updated", "Smoke", ACT_ASSIST_SMOKE_TARGETS_ZONE:New( self.TargetSetUnit, self.TargetZone ) ) + --Fsm:AddProcess ( "Updated", "JTAC", PROCESS_JTAC:New( self, TaskUnit, self.TargetSetUnit, self.FACUnit ) ) + A2GUnitProcess:AddTransition( "Accounted", "Success", "Success" ) + A2GUnitProcess:AddTransition( "Failed", "Fail", "Failed" ) + + function A2GUnitProcess:onenterUpdated( TaskUnit ) + self:E( { self } ) + self:Account() + self:Smoke() + end + + + + --_EVENTDISPATCHER:OnPlayerLeaveUnit( self._EventPlayerLeaveUnit, self ) + --_EVENTDISPATCHER:OnDead( self._EventDead, self ) + --_EVENTDISPATCHER:OnCrash( self._EventDead, self ) + --_EVENTDISPATCHER:OnPilotDead( self._EventDead, self ) + + return self + end + + --- @param #TASK_A2G self + function TASK_A2G:GetPlannedMenuText() + return self:GetStateString() .. " - " .. self:GetTaskName() .. " ( " .. self.TargetSetUnit:GetUnitTypesText() .. " )" + end + + end + + + +--- The main include file for the MOOSE system. + +--- Core Routines +Include.File( "Utilities/Routines" ) +Include.File( "Utilities/Utils" ) + +--- Core Classes +Include.File( "Core/Base" ) +Include.File( "Core/Scheduler" ) +Include.File( "Core/ScheduleDispatcher") +Include.File( "Core/Event" ) +Include.File( "Core/Menu" ) +Include.File( "Core/Zone" ) +Include.File( "Core/Database" ) +Include.File( "Core/Set" ) +Include.File( "Core/Point" ) +Include.File( "Core/Message" ) +Include.File( "Core/Fsm" ) + +--- Wrapper Classes +Include.File( "Wrapper/Object" ) +Include.File( "Wrapper/Identifiable" ) +Include.File( "Wrapper/Positionable" ) +Include.File( "Wrapper/Controllable" ) +Include.File( "Wrapper/Group" ) +Include.File( "Wrapper/Unit" ) +Include.File( "Wrapper/Client" ) +Include.File( "Wrapper/Static" ) +Include.File( "Wrapper/Airbase" ) +Include.File( "Wrapper/Scenery" ) + +--- Functional Classes +Include.File( "Functional/Scoring" ) +Include.File( "Functional/CleanUp" ) +Include.File( "Functional/Spawn" ) +Include.File( "Functional/Movement" ) +Include.File( "Functional/Sead" ) +Include.File( "Functional/Escort" ) +Include.File( "Functional/MissileTrainer" ) +Include.File( "Functional/AirbasePolice" ) +Include.File( "Functional/Detection" ) + +--- AI Classes +Include.File( "AI/AI_Balancer" ) +Include.File( "AI/AI_Patrol" ) +Include.File( "AI/AI_Cap" ) +Include.File( "AI/AI_Cas" ) +Include.File( "AI/AI_Cargo" ) + +--- Actions +Include.File( "Actions/Act_Assign" ) +Include.File( "Actions/Act_Route" ) +Include.File( "Actions/Act_Account" ) +Include.File( "Actions/Act_Assist" ) + +--- Task Handling Classes +Include.File( "Tasking/CommandCenter" ) +Include.File( "Tasking/Mission" ) +Include.File( "Tasking/Task" ) +Include.File( "Tasking/DetectionManager" ) +Include.File( "Tasking/Task_SEAD" ) +Include.File( "Tasking/Task_A2G" ) + + +-- The order of the declarations is important here. Don't touch it. + +--- Declare the event dispatcher based on the EVENT class +_EVENTDISPATCHER = EVENT:New() -- Core.Event#EVENT + +--- Declare the timer dispatcher based on the SCHEDULEDISPATCHER class +_SCHEDULEDISPATCHER = SCHEDULEDISPATCHER:New() -- Core.Timer#SCHEDULEDISPATCHER + +--- Declare the main database object, which is used internally by the MOOSE classes. +_DATABASE = DATABASE:New() -- Database#DATABASE + + + + +BASE:TraceOnOff( false ) env.info( '*** MOOSE INCLUDE END *** ' ) diff --git a/Moose Test Missions/EVT - Event Handling/EVT-200 - GROUP OnEventShot Example/EVT-200 - GROUP OnEventShot Example.miz b/Moose Test Missions/EVT - Event Handling/EVT-200 - GROUP OnEventShot Example/EVT-200 - GROUP OnEventShot Example.miz index 0f10d126eb9d78611153972b240c31b912c7ea83..c14d90c321f0f20ab779ef24efeabf8103481fc9 100644 GIT binary patch delta 211986 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

D9op9Nxd+w-kc308T^*k2r5q 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 f%`6?w delta 1190 zcmeynk#Fq<#tj8x)5~Wt3v6p(&Jfz(P{&-r$k!{wG5Z-K0|OHa1A_^}^u%e*X7y`> z9P@4)2<(ymAiwOLDyz=HGHDip3(b$MI{7=BqfVM8ES-8}ni}t^AvQ#_No?{IbBtWT*DT($ABt=XmRhrAbWzJ9tUKOp+@ zI~kwWxk9sKT2w<`DU|ZAd??*CLCk4-L2sg~Q?mBl$z2b4S+=eH#1k8IfFtA1%ht6g zcbjaI`B_{f*IMvo)4!@&Nq^5ytX+1ZWQss%Q$sn~USIO?pA?62b@-ECd+8-q6Nw-H^-`P^%gR=of0|NUUQ!OioTDduy;T@8<=clhOA zn(_Qb#MWILo;Ham{hZDVKb-EYyQ}U4+p1I3&g?$)eOhsdr+2Ep&9P$EjYs_=cxE@wR3Ruvg>bu9nL&~ zox6Vb`p;FsG?32BF!`*ibbW8IqyHfT5xe}8{|=wl(E>?emQf*}mXqV~%hOlsWV#)ihQwZ9~La zx%tl(kDUCX$vF4Dqr8l?eAl!&(3cByyYUZ@-?s z>(WL|f5BGsC9Bjn)LwAso_0y=^2g`l+TY$6uj2PS8sz)dak9bl+T+`bn0mZ39-qFh ze1+`^pE2v!K1ba%Zk}75dToCc&pva5^}c;l*g|fX4@|c->vezZb~q|^a_-JWp}$qV zSf;9qT{6B`dn(XPr2l&4&4o`dbM0K!-WF0X8QvqDVENZHBT;b&!}+-%yV8~!MlGof zTFW|DO)BZ}>}fleS#4*GVoZPj<)BbRSQF#NEX`>v&#pBo$2fm%*xYuOlKC5L5h6_F|b+& z28bROlgSrUC8mFv3e>X6baI`TCdMz%P}!9OfQUQmf{6zg6KHw#4!Eo zbY>%Nu4o2^{DP9q{Jdh2JGE3UB{DFSCl;k<V1H+UQ5Dx$> CxCD~` diff --git a/docs/Documentation/AI_Patrol.html b/docs/Documentation/AI_Patrol.html index 51fe4286e..14a6a30b6 100644 --- a/docs/Documentation/AI_Patrol.html +++ b/docs/Documentation/AI_Patrol.html @@ -129,13 +129,16 @@ When the fuel treshold has been reached, the airplane will fly towards the neare

1.2.2) AIPATROLZONE Events

+

Note that for a UNIT, the event will be handled for that UNIT only! +Note that for a GROUP, the event will be handled for all the UNITs in that GROUP only!

+ +

For all objects of other classes, the subscribed events will be handled for all UNITs within the Mission! +So if a UNIT within the mission has the subscribed event for that object, +then the object event handler will receive the event for that UNIT!

+

1.3.2 Event Handling of DCS Events

Once the class is subscribed to the event, an Event Handling method on the object or class needs to be written that will be called @@ -252,7 +259,8 @@ YYYY-MM-DD: CLASS:NewFunction( Params ) added

Hereby the change log:

    -
  • 2016-02-07: Did a complete revision of the Event Handing API and underlying mechanisms.
  • +
  • 2017-03-07: Added the correct event dispatching in case the event is subscribed by a GROUP.

  • +
  • 2017-02-07: Did a complete revision of the Event Handing API and underlying mechanisms.


@@ -427,6 +435,12 @@ YYYY-MM-DD: CLASS:NewFunction( Params ) added

EVENT:OnEngineStartUpRemove(EventClass)

Stop listening to SEVENTENGINE_STARTUP event.

+ + + + EVENT:OnEventForGroup(GroupName, EventFunction, EventClass, EventID) + +

Set a new listener for an SEVENTX event for a GROUP.

@@ -436,9 +450,9 @@ YYYY-MM-DD: CLASS:NewFunction( Params ) added

- EVENT:OnEventForUnit(EventDCSUnitName, EventFunction, EventClass, EventID) + EVENT:OnEventForUnit(UnitName, EventFunction, EventClass, EventID) -

Set a new listener for an SEVENTX event

+

Set a new listener for an SEVENTX event for a UNIT.

@@ -574,9 +588,15 @@ YYYY-MM-DD: CLASS:NewFunction( Params ) added

- EVENT:RemoveForUnit(EventClass, EventID, UnitName) + EVENT:RemoveForGroup(GroupName, EventClass, EventID) -

Removes an Events entry for a Unit

+

Removes an Events entry for a GROUP.

+ + + + EVENT:RemoveForUnit(UnitName, EventClass, EventID) + +

Removes an Events entry for a UNIT.

@@ -1704,6 +1724,50 @@ The self instance of the class for which the event is.

#EVENT:

+ + +
+
+ + +EVENT:OnEventForGroup(GroupName, EventFunction, EventClass, EventID) + +
+
+ +

Set a new listener for an SEVENTX event for a GROUP.

+ +

Parameters

+
    +
  • + +

    #string GroupName : +The name of the GROUP.

    + +
  • +
  • + +

    #function EventFunction : +The function to be called when the event occurs for the GROUP.

    + +
  • +
  • + +

    Core.Base#BASE EventClass : +The self instance of the class for which the event is.

    + +
  • +
  • + +

    EventID :

    + +
  • +
+

Return value

+ +

#EVENT:

+ +
@@ -1753,24 +1817,25 @@ The instance of the class for which the event is.

-EVENT:OnEventForUnit(EventDCSUnitName, EventFunction, EventClass, EventID) +EVENT:OnEventForUnit(UnitName, EventFunction, EventClass, EventID)
-

Set a new listener for an SEVENTX event

+

Set a new listener for an SEVENTX event for a UNIT.

Parameters

  • -

    #string EventDCSUnitName :

    +

    #string UnitName : +The name of the UNIT.

  • #function EventFunction : -The function to be called when the event occurs for the unit.

    +The function to be called when the event occurs for the GROUP.

  • @@ -2470,18 +2535,24 @@ The self instance of the class for which the event is.

    - -EVENT:RemoveForUnit(EventClass, EventID, UnitName) + +EVENT:RemoveForGroup(GroupName, EventClass, EventID)
    -

    Removes an Events entry for a Unit

    +

    Removes an Events entry for a GROUP.

    Parameters

    • +

      #string GroupName : +The name of the GROUP.

      + +
    • +
    • +

      Core.Base#BASE EventClass : The self instance of the class for which the event is.

      @@ -2491,9 +2562,42 @@ The self instance of the class for which the event is.

      Dcs.DCSWorld#world.event EventID :

    • +
    +

    Return value

    + +

    #EVENT.Events:

    + + +
    +
    +
    +
    + + +EVENT:RemoveForUnit(UnitName, EventClass, EventID) + +
    +
    + +

    Removes an Events entry for a UNIT.

    + +

    Parameters

    +
    • -

      UnitName :

      +

      #string UnitName : +The name of the UNIT.

      + +
    • +
    • + +

      Core.Base#BASE EventClass : +The self instance of the class for which the event is.

      + +
    • +
    • + +

      Dcs.DCSWorld#world.event EventID :

    diff --git a/docs/Documentation/Group.html b/docs/Documentation/Group.html index 73c99a059..eb3d67814 100644 --- a/docs/Documentation/Group.html +++ b/docs/Documentation/Group.html @@ -162,6 +162,9 @@ Use the following Zone validation methods on the group:

    Hereby the change log:

    +

    2017-03-07: GROUP:HandleEvent( Event, EventFunction ) added.
    +2017-03-07: GROUP:UnHandleEvent( Event ) added.

    +

    2017-01-24: GROUP:SetAIOnOff( AIOnOff ) added.

    2017-01-24: GROUP:SetAIOn() added.

    @@ -376,6 +379,12 @@ Use the following Zone validation methods on the group:

    GROUP.GroupName

    The name of the group.

    + + + + GROUP:HandleEvent(Event, EventFunction) + +

    Subscribe to a DCS Event.

    @@ -490,6 +499,12 @@ Use the following Zone validation methods on the group:

    GROUP:SetTemplateCountry(CountryID, Template)

    Sets the CountryID of the group in a Template.

    + + + + GROUP:UnHandleEvent(Event) + +

    UnSubscribe to a DCS event.

    @@ -1139,6 +1154,38 @@ Current Vec3 of the first DCS Unit of the GROUP.

    The name of the group.

    +
    +
    +
    +
    + + +GROUP:HandleEvent(Event, EventFunction) + +
    +
    + +

    Subscribe to a DCS Event.

    + +

    Parameters

    +
      +
    • + +

      Core.Event#EVENTS Event :

      + +
    • +
    • + +

      #function EventFunction : +(optional) The function to be called when the event occurs for the GROUP.

      + +
    • +
    +

    Return value

    + +

    #GROUP:

    + +
    @@ -1613,6 +1660,32 @@ The country ID.

    #table:

    +
+
+
+
+ + +GROUP:UnHandleEvent(Event) + +
+
+ +

UnSubscribe to a DCS event.

+ +

Parameter

+ +

Return value

+ +

#GROUP:

+ +
diff --git a/docs/Documentation/Spawn.html b/docs/Documentation/Spawn.html index ecc39c847..5b520310c 100644 --- a/docs/Documentation/Spawn.html +++ b/docs/Documentation/Spawn.html @@ -1759,6 +1759,9 @@ The group that was spawned. You can use this group for further actions.

+ +

Don't repeat the group from Take-Off till Landing and back Take-Off by ReSpawning.

+
@@ -2233,7 +2236,7 @@ when nothing was spawned.

- #number + SPAWN.SpawnMaxGroups @@ -2250,7 +2253,7 @@ when nothing was spawned.

- #number + SPAWN.SpawnMaxUnitsAlive From 02c44c158fbb7442d2d5dc187a5939d96fb8a696 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Tue, 7 Mar 2017 09:26:25 +0100 Subject: [PATCH 4/5] Updated mission briefing --- .../EVT-200 - GROUP OnEventShot Example.miz | Bin 235773 -> 240705 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/Moose Test Missions/EVT - Event Handling/EVT-200 - GROUP OnEventShot Example/EVT-200 - GROUP OnEventShot Example.miz b/Moose Test Missions/EVT - Event Handling/EVT-200 - GROUP OnEventShot Example/EVT-200 - GROUP OnEventShot Example.miz index c14d90c321f0f20ab779ef24efeabf8103481fc9..16bd5254f57004735df4faa1010e127fcea9e25b 100644 GIT binary patch delta 239328 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 z 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%0AWrD!6 zg1^epBvl@SU4JKYZSweR!6Q(||Lq#<(<3PqnmCsPWP1@N(KoGl$NSmQPr0h4pD^*}6oU9L%B?y^2KfI8{Gt0)Ob;Tg>8+NVWFP1NoxkeIJ-6 z3f#zVB|f^&ZUBrP54$f_xZZ6#h53PFFCc0v%7r}aWIn;bPOD+?(=OHAHRXfj9y*Y* z3di?JmjYE+DleGfO*9DpInDs}U2VB9^ZAql26`IZOCJ}kA01Kl_~b=tyLyj!Dppbh zOXZNuGk@f+%~JrhxA{&UC$IB-ko<3c{8;m{DsZxj*;_eeNPs!|La19V&#PElITqD& zEcd%x@mwSA#XDaPfOaFYm8T;Cn`177|+xNw_}VY*@H9SpX_WsKj|MzUv+`7Jqxqn~ZjhRhy367WGqX>Tx@1UM6do zqPsC!(aCu_fhhM1*190P!)bcr2U2>O7FWIt1kZ;xOc?MG8{q5N*z!Ou(~L1G^KSA| zCuHPf7)4Bv(k%RZn7>!lkQdSA7PLl}cClg|Ql=Cvh(R7f3-l+&?nGbdFdVLV{mA9n z#(y#$&#Bv8yp70hgL^1YIGbd%8%oq_5ZUK0CT%3o=^lm)zw6cBP72TE?nmz_ft@8g zGNG{l@ErQ=_WeN&qaW`Xi9BX0h=vCb@&r%bE(n{%9tm+$6=O0P#W%f#r8qCakeGr$ z4hseUiPIM%)hU*uy|1x+)`m%%sd4{kfPZu7ZY29V{k{D|AH!5r6rxG1+oBcc2DXlE zW7AE?cv4$R&Ky*QyE3glaL;BdBWeLt)mtE(o_oeMJjLhqw||7eRlsyFo$1(d8e}tE zTUrj3M1@JRGf7Pog*v(EFm5Al#g{ycLR6k#5ZWppIC7qD4{w#zCk_Pl2Z$hyAR(Ht1-O`xFYo!%aW>RfRWHcazFq ziz4n?Ce~ifh+My_26}Oysd4!EiTd%o7w>Hibp92;o>6WXgPb>#8RUfTo#c~~)Z)`% z&D{jtT(UG@OEJKUr{;}JS9}#vuzwm{9`VFhVp`%NYnOq9r);m4B7&QdhH)dYZjS*su<+x4$I50*PT@nA7O@EDaj=^M* zG^PY62FOZzG_kLle&w-&Tr80Il5@<9fE;J%>BnI{pL$6M&A3EH7j_QQ`N=Gww$Apb zG6FaAv}e{1%oA)^L%@qD6GGYAPBM?=mobuUKU)%vrM8)!74j`PBB}1e4elK8a?J~Y zhO>DgTlCy6uvnF0LXYPi)qkN9k6+}Y%W*!vI3J!w2M^O}7+Vx@3NZV-&b7>IrW<}1 zH`$Vf#24+TH%k|FKr+r_2Z)>;l?Eh;gsk&`tNfeej?^Yhv{{^Im|lC*+7jFn3`&&H#8QoDBJEypMOn{=hLC$hZ@EG zaD)&Dgp(5%v)rt^*B>pY$~FMxL!~W0%`dz!&2AM^^cgmfE-hXopp6Wko#!W_|As1y zao2Kc0meU}M&IV2?BH5qmtbKhQ%>?RGmzQ!eEVFcKo7qu$zOp98{81R_{M%vv2?v` zSh~m@-hKNzvUs&vUVoOhs@-6z3%j7zP@wfJLwjY><@rDIBvy|A$!_}5SSnz1iBYq_ zGOPyarN4ODJ}kU^EyyEYyhN%gn`u#6!yG;SDkf>|JsJBNWK1=-y?fB#Jly&rY7yO* z5Gg*?k)nkIjgm{V;WKL#%&BxnX`O9~>p5h5<~y_IVIWKc*?;a0@I=(;_$w&&YI7^; z&_}B-dp119^_VK5D!TOOOza&wKZmzj0?j~q?LrwW~NG~XPL9)x+<+W zbxG3MO7#Ln3z%Ta{{oKWK>x#5LK?XzD@cNSG2jDKSxJ2L zS7W&cH0cSlq$Fx&t)Z3Dx(DCSxaa$A_e+lRMGm>#T}l3E8YB;~q#cs8L(Xt!IGo|) zB6HaEMJF(H#3<6~ShamKpBQpXuecpECXn3NeP&}@NPmi7Lg>5p{v9K9beyE{EyPTf zODV+ewA6uozgIC_}2YXbMu2Xixp^TB8^&ty>ibByTP`Q(K{h1**#CAgmYg3B@3 zMvAvDz@m)1qQp{&tRS+WKsZ(epPVnz4BAN!@vLEzEikJ6P5*@v^G2Euo4Eqw<`bqr z`zGlbU4KF17+lTqe97@=b7L<5n$F)#@$qkx^4jB}D0jY7`^E{2PiJ^<7ufeE9JXUg<&H=@z z#)nyp+|pFss>MiHM{5p*&esH)rXs|am-?D(7xLvAnT!>x9O7@P!ePZ5szsgXBb!L| zHuY3bpGO|u$4<_IY$Q)@t}P!znO=?L$zKJ?J24UBdhRoKBi<>Wo~69W%Bjzm7w1O6 z`+wLYx5W%_+-t<%^YVX#$ES5G%O*n006R@rokmQ~-~#a7pnX8uV(N-o2CjbhsAdkg(4a%r-?WZ#{) z$4wUFi%fi~Xm+?g;d+y0d;dFz??!BdTYttEIU0rn66}^GY23K4I=lV`Y`Zu1Rt|jA zO9r%_!6aD6t(C>BX)*={CL(Pm?W>aaY4Q#Zt!CV8JVL+KZAbzpUBkU@Gb(q#C=>$H zg`?N5>ui-RGcM-U5YGlf83fK0PmNZ;cXq(41}O!EiUyPs0rBY+UmM-?#@c-l)tg(tfX zWbOBHO>t$&SM?=$=~$?YI@%0OJb&kglae*tai3wCk*E6j)XluO6%eCXmBnWvYrY{@ zIt!~9cMkkc%)9o>Zu@kV-8zp)L}FN=&1}2^o2=5{TgSvCldxW}-2I)%ZheoN%4{na z@g3`K;?O$ws|D&978g^3{rV>6J*FuTu>gtkPIbJL;8D=eMMmv3tW=XbPv|} zR~=SF5NY^|pdop$U=+FYtwV;q(o7JNYH9LHKK-1_85@JyO&+WCuQrEdS%cWGfAP90 z&LZn)!-lq!5gT9JO^%ymJ%1yIy;t?a9>ql2Mi`!C*bbwQT(^;NWq0*Lq7HjH66zi- ztuhf`sKl@boN~q~HP98C3T@T5Y3n9igAyw6?dB3i>IuGdDh@2(t8{7Xrr9EI;lP3G zWVRf)%(PNsVN@`NShFGkpQh<$t5Q1UG`9G5rPSM?IFbvr_j2(1f`8&Ldus)gFO=Gb zh*~JY_oz_f)F~P+D-})vp~QymxY`|x)85hzWd!v;uj=Xxu^&VP5)taic>X&gQ||b; zPf9ihXw*`>17|aK?`hOaan434WWMNISBVABm7b`%V|~lUd?rd-?au2NBJIlv_4Y-hayzSM5W^-kny(G<$uhR;JqPUOrXbo-in=oR`>Qcp5k+WW5_z zq1_<8R|j(V_Jo?JDucp-$|;gd!|4u@JS>M8QK__DWMjP5vgFbi!I{Hk%4K!=HD{@PQ#wX4&0E#0 zxBFo*T|!a@Lq31E^feXm!+Cot@50wf&c4#N*lunEjzRteLEHf-{3dm9yqhgtHUy{x zH%u{-L|z^1m46u{YLvq511op*@@Q!UUP9wex}0fD!CHfydq9ybRV_&Y|BWx?$x}q} z1x&@SeTaGrSv%abEBp-*c4C9>V|0V0J~N{IajV{yOPOcpR+j!Ds}Lz=PRxFoh^42#CX* z9oBMa`39@agQt)PF0y#ih4W6@JG8!7fzYu(hg1<20{(LO*;Sd}yyQp8`*+O!&oOij zi!aATfPVw9dOsTr6jV1EXGi0~{JSejPwXs*{VasZQ`kzD5D^eB5Qdo_hD-0{WDnH! zl!_{UlpMHw7GHzO3t2p-J3u(*C(q>OCH*DXl=XFGb2@zH`;V6I$v^d(@o$0gg(EU{ z@m-{F-mWtM+d`CVtqALP1~byd(&2D9x8lN|;eSb%boiQB`)JSJ#P;rV1CY&cu%TdC z(IeplTfxD(Q9njWMfn||p)#!L=59<$@roLTRrcVwe1SAzGEefzg!vn+R|v0*zCDAP zq!5DJl8H^Q9IKe+DyFKkB>L^}(N^(|Yi}{jgxS?B>zQ)tWPHB@;_t^(x*uWhz259R z;eW>a+s&N**u{;!{eF&1`^ z;@Lc;H1j;L%k|`%5xvU9y8y>Uygh`rea23=LygB9HRj+-RC%94A`K^RA|?R&)Swge z;&`Xy{_&fkKk z{*u%~5h=zMa&<7r6y`x8Pnc(MmT--#If7v;7(#knn;#-RpL=14Jl%Y5Xg_&g!3+U) z>6cnLO9avw%&Kv=fMdt?ctx-WR)68oRh+WDEr6Tzg7gq)1z4j+oWLz_&Iod4HOC0% zK_MfUXK_Ywjj9=eVJjFxdR&_kB7Twl(H5n|l+2e~_rxoC(yq%Z!35AR_rxoLJ@5*D zuI3d1+?-dWhd8gmGB&VrX5prrF^hR)K}nqt`Jr^Y9N&S}b1?oeo`%sTJ%9Ve%x#h9 zD6T#KI37M#S|7|L^awv|^5q+zq?6f#Tb~^7ANBe_b~{gLVTA*zM!ko}xA9y}Rs8J6&C*?+J6FR+kqp%Ml7@A?|+RvPp$s~92Yiw z_z30n3^QavK6a0Y6x3wwdAZg#06Fd(lDL@+A&CItpB0fX>XG6Cn&^&)(zH68*NY#L zVVcfIP+2XVdyz)5#L`}RZRN}p)(&JTxg~4`S2Bg9$ZJsd(q8R}#$H>^!KvBJL?mgEb)#>Xt4QZK3CFRy8Fdtu)DV}XLVvt6&nRmUh-S>P2B&QN zh;&CB2o8`&HZu?lui@Zwa6SPQdMrbZfMxZ|xpYO2ToaWt$)?6a)PPqns46BF9>y*x zgAqtd7@YFQU^EV9!!`U7W(PVlctfTeO!p>-G+YE&f!Wel6r|-SW$=@#4apfCVKF3? zkTiS5Hh1OzEgN$(mfcW@dzV>}E{Oj}awOg*lO zR0JuV+Spcuud}b8FqQUIsJ={>i!owqTKBpkhI4pKI}IfWKGRu{h~N0oVX#;XhVPT# z>5jWgIV9ERBJbgfm`tyrW+xUP5n6M@vKN5P>RgkWmw&yFKCO{$gQOb@QSfS&JTpih|3ZL0n;KmrH&Crm|MPe=Tp7B^z*{W zRg+i@d4E1kvM74mJ8N_!#v`Nc^IASr&OYXET1-@)Ckq}Pleuwjctl$gGtZ%*96IkZ zq1oeP&LAkOGsH0{*ou8d(~K8f1q7l)y;5QV9}r8wgY2*WbbQpMIBNow6O*vSmQO7& zfqKjt?%WxIf8f7ps`2~_GbItzn6ABgAauRgB!6gHFEo-X-rBW+GhxPp&;b<*Ur=y- z`s5QaxUG_1sNC()3rjl}u?m&i+oO>rz)lk7tZxE9jr4!~lG}ucH2K12Tae^Hwhdq- z+!|_-!QmG=L^Qv3;bSeiwu)5@7pqpo__iCy1-F?#EGQ2VH_M2|oYK~s5zj71SS#*2 z7=N)hAIvgW0*Fwg5eZ46M0ki1Yt5r^3m&lPKBIk5{JH{UNe_pA@C`}d=^URO_4<2e zA)V&l*e-KfjQH}@1HHbj7G|3=`qUMlFzOBeSEOG< z*PW3R3;pho-J_n05jdVv%KC%x;KLY(W_tFq_(|}L=<0GiM0(pB4@O=W@WTr2I`cvH zZj**5lWDrB7_~IBm~5=Yn`UGic<%Xtwd*xjHq!U=Zu>U_Rb&o6Zg^#w%DuhRVt=dV z#oqA!L3pP|dw(lwoSvBf4$XhON8m2qp3FfLqc}gQMNnk~6~j zw1nFV?nm5-nMe8O_)T`@o$@B2p_{V%s;ogD`o@M~txd8VmK=sVu4x!|Ac9w+D@7X!EjFdI&nBV%+y9SR9NI6OW!szG*@8C$JoOL#){HxTAvdw%uhJ~eDT zn?Ul1+S!7w#76>xOTQ2bK7VCg%i8wdp%d0zGkEw2Qt`+7#D_N5XeGp+)GwLW3LtT@ z6!rs?Tlf&fU*SW!qF0RKxOiBME-FC^gBJ>)8!@bC&1r55$<5Cn9iP*_zf}maKQg46 zyqUmRe;M3KN)odQj!l|r6&&h%WdOhwr2|bT1(Pfgz@?L68AN+BFMpa&+sSY|8_%y4 z?cJ@ILh?J1M_hM1HiUxez(uOF%L=H|%fYX+3Yf$61Nv;Lx-ycq0;HQ6)T{bI^?f=v zJE{>F^9o=l_enaP4969)Hdsng-dImX6mRc8YVY^I8%)6*kK#cPCj(fW#m_r~%d&@A z*~9tUR3l3@HuKQ*`+pURZ@=%iI(X2fiv&_vVs#Q-in8%wo_yljL& z@&c}6V+kOeb!8y%x9oX7lXrPveLkZp2Le|Gy}k8$rt^^zr8c?iaB36=s*g%eP{Rdq zoe{#zTge{Yu3n_e;XBGqKxEv*X5@~;JJ+tt*4+2YqOK!OnSW68F~$J?!ZlBLS8d^b zR>vo?0inXdyl_BCp&n+IGowcy?MC1OY#^j-UeANYP(ux6#M>rcByF@6aWGM2mk;8AOPPLccwwN{#OH3 z1AoHm+}*h7eh2l~1eM=VwJV-=dIvbGm*{=hPba7>r1R$hI_Y{uUmZ{v&4rAEWFm}` zv7N8qz1ENWv^wO!Cgg0DwIl@Pcdi4v(R>R$niyS&mjBms*N?X{yAo;DHV^u>Mi0u3 z&gS|YE`OWH96EgkE-ejq>eEng^)O3s_y)P;_(8IJ@LYQiVT2ZOPO z`_6R4;~vyN7!86nq=rp7T!tcU0(*VJ%Ti#62jyEsu17{hFT#!zc~)W=&0|srS10BR z?$bPi3*b658KwDLO#nV$VQPUZTma1vR+nrdVwhZ}`UTFx#|SVEN+blW{nO8H*ni+C zJynOK-U{-4uWrhGavp6uZMqQTaIjCY-h_jMuT6HAz@i+>%?w?eF}Jp|(ku$??|k{J zXE~J8$I}S9s4x)4h%ZfsI|vr{mw1^TWp`{0jK5TTD!me@vN2qh(|a|o^%C7=8D_#G zB+3X5>p8W`YDOtTID`Yeh5FDf*ndh5U#Ei52+BPHULX+IjaQowT}pyR7uJiGTI|_N=Gd zvz~3w`eA$4^X*wLwrBmgJ?rK6tiQJQc6adg?+P$o*-PlN#_bq$^I}>4AN1KvqB8A$ zGb}^YUPk$HAh;KFliXG8$d(0wzlpi>pzn}XjEaI6Xl+2#SVzQ`R0XP4SQpYv0cFCI zV+qzlVW~)&UUKqcsc&-TCV!P)VGgFxhPL0Pyqs6#9gY=Gpt!(2=pMDWQ-5g?60k&ao^qUnJ`89r76 zkilbZTFZ&4ozpV9Rvy;%_{h(%IYl(Gj6)if5W-5(A$tavJXZYJa+doEmNtH(3VVZzvk)F|S?ZwARdB0hN;G-G(tv-8N z)5c$DSSR-@zWw#SRE@h(uBk42ltVJsDXX4l-SY^qJcWl@M?HzCvd6I-0hqth6fpRj z`oEryIhtbTz0ERjC#@8FyZ-@DO9KQH00ICA0000W0F4|7o8|!k00IO605kxXVU`0n zf2~qMZrd;ryc_TzrtT#LEWvgg1Q5`J<2s2CiGy0nHDINc#Fi!vl8Wo!XGyt{(_UJj zCyCtMnc3kaNirNH3GT#}GAtkSWHy-~!S&bm!#&n|`Aphx8soA2DYiRBv3uV7#@Gy3 zO4f~E811c5vGPTD!om`N$_$eW+zN}?e`I4mMyyEL*K`Ezm} zg>dAu{(72X{mqemrpR=Z5Q&0-aCao@ux5S#z9Id`kUJe2v%0cysWLU#PkVrLf7n~; zJx?BY<66y39s0)T@$f7;xv^A5a&_>>~=HF0zI zq@(L#Gw#DR=A&^?+o}>qs{ zgZ$hvWLHv`mxmD9v)}i@hcMaCe`GhD7ez6~XNXR1(wOJiShczcNjzc6PD0NU;g>@# zx_+tkyTbRDb5u!7WiLF&a|N-*PFNpI86i!3m~^FOqzUQ+Cf?Q_wDj3+XHRbC zO8pSD8)ug;e@)#FEX zy??t}Y^#gKa$f1*t97+puP*1CYJ0W5UY@5n&d(R=GxgJ}+4=cmby=35&(5mZ^=_?3EMCo)`uVS}=C6LPUadFty(KSa+xh-;_CGUy<#!7+ zH`TkVxqipZ`no!wUo2Mpmz$5DyjXpFy?@*P(0^TTcX!|W(_;SaY`)v6VcTl9I#Hc0P*;^H;ZaUk)c&?YXPyX=tvp@ay zk55$8lV|JI#p3cw`p_pIKHV&@XOAC!{9)f#|M~Tkf4}(pX$|)2*Z)Wt9zS{k`+pZ| z$eZQtCi!={-c|7U(O0)u^wn2y^^H$gmn!hv%CoWi|Guf~@BjQ$x_q_2_LW|=ch~ms z`tar2`A;wW^3UC7w%Rt{aMSYjf46*nvtBOm%=+}gujzj;T-JPw#ycNEFY}`{9>GOw znh#OiBqP8wS?1;d+}nQ2 z+Rtnp3j1)X0bpT=_a1;A_H*9<$HYIV{S3mH_(vDKa~bx*= zI)pD+!QN3PlHtWryu3$TE3)eC((mE-l&xuNt z0s&(95>@U%)n;*dwL3;tJt|S;1bXT9%XF~=;Vo=Dj_(-fdhYbI6i17uQfoP!dF`_Zlvn501tbo;>7$cBy8moCgFyZ?_a3f{zm;1iDbD+87jJ zA9;Ct!KekKqIE+0>69s@gtQDKg1@P8V;~;y!{ZBee?tb<^}deK(-LNd!0rk*d4=L5 z^5&aXBDQE9v$n2#VYYt$wY+3Z_#YpdnTF9>g?*v51VoS<{QBYnHXDu#8g-Gcrue0T zLBQid{UgkcUkep*>Ym_h>^}!7r>raUkKp%l!w`9C>D~M36|yPs(FE?avPyd254Dh! zlG!-9Zfv2Dy1+{j6yQx`r?0YejClgGrPnd?i>z+kmGuTZdG}{gqYE@TqLZ@tiCp@8 z-{qm^(M`O*;1{C3$_5XF>8J^AVhbi;)1B&#AI!fGsGrW8d#?9Pyg2Z@Jy#x)F*|tF z?TfSC_qLt8Mu)gmJ6(F`A_u#y-Z%`?k=a=xKh7Yhj8xE`T%kV0r+6+xl@@a+IE;^H z;Jj6l2Oj3ma(tb7SRFiNF0VhW&#Ccs$=ecY?JullMY!4fsg;z+YxSl&fba$7Yjbi{ zH6k*oiTLN&;*W^~1!Ef?ImV6y17f;Ah6E}W?}l9cZ5jL7Kipj9Fl>`6t{%SJabsSO z#3|-4_9YBk3hHOuXQLRPtUo+h90|H=N}(v`5<#~b8;ln+O;=Q}19fL%qJpQi9ac!8 zbJgQNlz~00Q~lno0GRFDi@+gV7cAbSiA>tDj#ft0&=7gb4yo1s2Vd=z^hYFt7US>I zk#F?vv`3i8IwV#d0-kaEh92C(2VFGYD`+UZJ`gM_Vl?_rfSD#{3WUk4Jw=;xT zloN%k5|fI_eb>?U#b##8pMgg%Pbp*Nk2~|p7gG5=fh*`&ceDMIWIK^M1CwM(TOQ||5lxbLoWAlr zo#DjsSb4lh+^}n@{;0={5;1apagNk}Z6nTqof-_;qoL%+_bTIOtJ5%61>(kLPgm3$ zxfg?x(*5ecq=ek9@6J6=@E9^Q9>zbjY(M>w;Ciuox%gVNn@+-#%HO@3F{(Au{qC?@ z0fUP{I!Dnf9m|_m1W~vs16}#mGfEX_zdT&&p<}mo+B1M(u^0$ll3TiK{P6bP9P@-! z>MXp%TNnCBPItdKJcGBeUhQJlzd19BesYQu-Ib}e4~ll%0C`fYfA26qZz%0YBb*KE zuRmNmKUcHQ74k>F`KBpa!oU|MZ!}bvI}|we+{VYyV_G<}Z!uPh3|q;1et{^~QufUG zgu1>)ai3M@%EwSEi59$-AToxoX+qhBb?k9Y4T65u;#i2y?w`Es962 z4!s8`3a}rapoeM*$%;vvOZn<)^-6gwX6yKEaenS`d5d0bKr9wuR`n|&Ofm8**`mzC z%Ip>)mp1@@DD*ka%1|dPosN#`A-6c?ycF}(YR3yA685X*_x8ZpmXNz7dI#m!EHQ6i zNP~)rR_A8;^XH?Ef8+IQUKJRL%B^%nB0{HHWE*NVAZ_Ki5^a15LJb;&Af+wi@|~BX z_ICv+8BO~+InXtJ9Plu-iecS+UofkyB)Qh(S+wM#>{5b-qo%BYGIJfzanQhp}aK$0;p2%ALwK<#39*`$n4U`@A|w^M^FBCq2)L)44u<|7)x`+e2+egB#v{+=>U?a#aZ`T9AI+24*w zkbVUPu|jQ{JR?rgGM;#Zo2u2DJBRwCO)J!m!D_j4Zkk|ux*?)PV$m!NTQC@s_;g?b zX3ZKY|Pl`7wGHJ@VmR?fBCDqk=yIXbG^>D5s+0owW``h62 zw^3398tC=xG1}Qh|0t=D%Zba+pY2>v_wi<3{!mv}t*FkYb<%aSyuim@8h_W;+wYN) z%XhQ-m#~h2Tn(tt)AsS*py(Iwo8dd|Q)=g*;v~6`I}P0T(}(j-E;K*aIy(KXzgxw6 z4~)@;El1(OiJqIRe?C8r9S=49 z@6?xRmQ0zCtRB~|c8-5T=0YgJ$P8x*eoK#)JLD=o?(|HCK1&Q z(h_6BF@7GzzF_OlLdq!s`~gpOmSo~gl?P210t{HYxlqWoKn66p)a3vw%%Vbo9~2DF zBtu?sXpw`6vrI4!??Y3uziiDL-fqqLM+EQel4-^Q)y$>+u)WRddf}0XoO)<;$%ibZ zImo7H$qO87Bq>GXfNB=fq1O0yDH986ugJPZ=s)&kaH4wP8loDzQMlNGYIxxo*xvEM zM=D-q_rD#?_AI3BdEvy^&Cx)pgJoqtvt%4lBNk(&2QbQ$KZ?ICuu?|5Mfo8oK?22l zg3a2d+>!|C6I}uZx-}kTC0&38bwdLmgo+gmlco(oXH}|UL3N`<${azNp&=A5;rruZ ze4>W0$D2fCHGm9<4ZrCahQLHV5K;C?pgEY$Y|;8sf=6t!pE<4Jog6}`cWAFf@Y!4H zHfXO%Fl{71LyGmW!i7_DHUP-<(-gJB06Jz&cwdGVaAb@jL8l%=rV^q^L|Y<(-paig z{LW{IlmdT588=spt~w~8m6q}6Pk$5{(LDSaT^U6xTsSN#rG6GsArGGj&61_BBvj_O z(X>G@8*Hx@tgcz$hY7vtEKv%+-Ir!dRzho)3HD<9kPvog0g2Zs&;i@0hamRG!!KXI zS^KhWQT#AUCW04!g2a($S6ME*7{#*RV!#-wBp9FA-z=wW;8Az~gx8iR$e1O> zSh$}ZOhVluiO88&mIX@MNE3(@B!;nVrc)N9Ke2#q;L?hw<1dKhQ^UxO2-#l~Hn6{` zfzaJXPc_@T?(Iu@ff3-UZAsq!rW|PliOBtc5qnE135L)xiV{T)ksmuU`FG1(fRX7!Ue)7-6L4C{*M=$A^=YPl;H~%RF%og zJhFJG{;C9?Q{*Z=U76{Dx^3N{edLnX(8g5WsG{%4?56jFQUX6<9aBhWing}Vju7pa zDp1hJ(q_Jd#nv_&8CO2OZ^U_dT)faOoDU0lsz$b44ErkE{iaJT9Gb}QDnHSuJR$pO zBuq6QVh#pSt;jHe-&K7=_P}q;a7TDcaFtvfy`H&A3N;Fh_)YEM z1z3Kvk6eO?LxRXk?+3NSLSzwk;;>9@iJhg{Bq!F#KIYfIy02OLWCB)@c9GS#I>|{? zcA`w7#=3ir^u+bS)X+sS*Fx-V;`Yk7tT6>2`9qlXLzv4dVYzI5k@V2h2n)R%VQ>IM z$1TNvARYxmw0mV(bjct7g}BA_%Wz$cHoLQ?L^(R;vApRKW|lp&S`L9gEunx`RbAP6 zaBT~{-e_M^!AI9G9qcJL-s62C2s`fMf{-LwZum`WJ+0gT^zEN=B3*6?frz2K?@s&{ zm#R6_{IxUuyoiC?Q(SwTRNQpAcbK_60Zy~eKo~ZAnw4@kCR6;p2C_A(9kGr^U0UPV&_8@;pqu1b@Aoq59{i;4!# zUkHeHw^X5XfkoY9^}cHF=f*D1Kt8-*voLJ7q|Y5G7$5KdY;5dvo?UgF9W1Xs?Q}g} zkGniwwz)i>et&wrT9&&1(*(WV%zwQ2e%kqv|8)N#^clW!Lu-+u>BLq@V`gD6Erk>F zmCQI@LeT4>H#;&rc{wy7_Cf~wRDZb3g6T$y?0*40>3wL!n{PuAntM1ymHs_Jcg3_w z;v%8swH!dONICWmMTsPq@ma~epvfObBaz*=+4dJ%Cr+pk50A7XwUpAaftf|hr^Js{3&(G) zjMCkqiPs%M-#^$ox>H%Iqh$wCqD44Q(NVEK@hSddH#8myRJuQ-wYS*##g`Y)B-=lpr1w z<&@H4pzy#Oi%B3=nsg*^FGH>)qc}3kBF-JU=T7~SFBvza2N#FwMKK__D$+*rpmUXE z4<0hH3wXBYU0Hbr5n>l#=iFP6tc#6-9z@g24OQiI&;Pw3`g(csCP5+h1ZxoDBYa7Z3>JgKKUTl zx+{7`a74;6CV;3wXPH6vA+hUzn)q;c#K7MbP~LOAdNwukoM7wSi3t$|3!|g!wV3PB z)SSTH452^@&cK z9N%)Mc5_~#Niv!!^{GZuigYJ;LnTE~3%O?$1JZE@V-Sm?MP6RTHV zSW84C|Fr=6yL@C=NISXzWnNhGC1c-o@|RpgPfOpd7-b`>A6cq7*-L$o#RYU3$@XBh zd6NW&23Z%(&q2UqgRj3?`y9^CYb) zGY|c3Covuvo>YZ#DTuLFacFWe4?IATJIgvjPu=*LS&^sNLN%DQ1=V|7Tf=pD!3gjE zRJoLoP9B|V`&@tIxY!{@tOQR&KkLHK6CXa|u1&n9cmeECkxUoYr|^b_*`KHtp~R=| zi0h98%%p-U<~oqSLG&fY|ChiVDVl$@%-{6?Lb>f)Ocb>1&S5o~Q=U3UhiNXb2$ z3;}j<+dLtpY-qS@36T3bJ{69E*Z_U$#j!9n__Ax^kHq}%lGsk2Gj2sQfwWp0D zGJAPUR-9S(D`|LyAz9FlKjT-(@Q6DzM=#*AM80aPW;hGl)WHKDukM9co0N)w-}CB_ z6CF#I6fj!MfQi4U<75zR&3QxjZcp2O>u6-=EoJF zz~!u+vG<;^wV^DW7@)B{8ci)ghxZ%aeMIWp(tDeDvJB(ef0>;cJ3dnXf zSvWbOBgp~9Ls~KNtqEyg8-~MF{toW`%kosa9Q^`FkV>mQ*39mI%tcCHi<9I${A3EK|6+Ezi+ocq%oC=+<1^Dz-L@(Hev)+Z z^9L)d;R_hk=uG=k)*BU)s=7xIuf=el*Z zrtVFG?({MJIIJM<9FHwwG;eWS?<3P4QmYzZ{I8(*{`zr5L4$Pg0ikW9wG^E@E;n@q zxD&D6T2MJt_bdVn(@{rY4a>#ta@|C6y$?*2CN25Av(VbDvfZ)(;y6GdXST`KQj@k) z!_=<;n$}X-AaI_k`!_mVC4volhy=%qU^}FQ`%<3enHm4cvF2-@0R22yHevzLWR*Cn zYUc?mi8Mi70lT>Hm&qx=T)Te}!WAZpE|N4xu_Mc?t$QX6Z$=iT2Ko=<5}N@0 z*QY*>wMZ9+3<6KH3+b}4;}C4;X!c-qXOrxG-~Rp3mcNz;g?j$l={+7LO*ilu_+}$+hAx?@EZiXu z8f<(G`Z=vM1a~BuCZ}Q2ukz_962)G$5r3xr$%zrif5HN)+8YKmZn=VUH>c1GeD2Wy zwW={JE8DBld+NlP9DlxtW}F~E!7KgTa?@(D+7?Q&wx-Q%aglFN`O<3KzRE|*PIc-P zGxGK{X_{h>Led}sD><7sIMY;?Y>!mFpZM9VKuUrUU`Fony`SxP%~xK(UWRU#{G&xj z2{FFQUF9-pd8mtE=(=@+su5Y`CwJi%I}d7wkELr&KX&n&LMOW4qc*XOl+5OhOKTj@ z$7m-SK6Q}dicG`r2SVM~+dvy4OOk>5!elFp750QouE~<(v^B#ywE$Zv%e%^-yK95= z`b*#d6x|s*wS~pjTwYQq+6rs?i6%XdkXS3;V(lX6RSpJ!Z1Y?ztrZzxMn8}-TDFdy zVwV&1TX_U5p_+wB*{XNr^ntuQ*kU z465O}lhz@(!uJnL6xLg6p~makZntZ0yTn)InC_=s< z|2Zf4Q#?KNp$6vA8nDG5H~guLiZLq?dxNC6w035AzeO2mZhCQ6x|OU$d%3Y)aP>Xb zmB>zF*f^S}R?n$p|j7DuU+<`kGfB)HgxazNhC?%)i5u~aWX=m65a@_ zOD5u^MSMsMGs>!N=|u0n?mS+mPt$$gSomPKJghN4qry$}e@pd@rC%+?%Ma-7#26@a3a9CnQ?se$y&pF~zMA zS}_)DSnCugppt-z-{)8sta?4tRjBenT#qbO_Y^X$D;vdLT^3kB@WquPPF7S*;?=Ok zfm*k=*r0&;WambGWORJXmV(@13~t4yF_k6$=zfj489Qlgj;BtlhNiKZ4%bx{_+4YQ zdQaHzacBFWq#}AdQ4S~F)z`_U6quo7(CVEavgshI3m3x4y0)b>m#>HN?-rmZf-hev zSQgV4gm3RueP$tLuH{zm+^%)rxJa$|^E+{vwES$iUeRJp7p5i!r~(_9&676sFX)P= zti>K<|9OwSHCdFhs&q@W7^(e(?Z`G1bFrNVrWdiDnKa^%bXvRQJf4xmOw7q<2~|kD z)w{a}Cpo)%Bp2yndy3jHxm8XJ49 z+X*3&jcv6-+vEeuMnQ}*ObNSyEkkNHz!KtDC;)*t#P0^OGj|y{^(W9a=+usP^ca>o;=!)jyUGung3`THKoQt~iR}d5w0w{0@`< zY~s=R)}>gFSq_K3vuT<=S)F%UWQv_TUoUp5UBQFc4RfP|X*r-O+viUuo4a`9+S#A7 z*-_v6uad3GdjV91kBw7}3wE;%27{{H$y=nNp7)3c`qq5sq4lxfybhrX4YYGR`0Y{G zm()Lk8{b=*ByW&jF_-FuZHON}w))#fC$z~)b?&AV`}$aP!CtZ&?da7Xd9y8Qs11s_ zr7sOs9)LZ5H?@K`$&P~$I9HwZdHr5-{VcJ(9T9_gw`ct{Lz+CEK6j0-c>VPC%+=R9 zLSz1k=IosB^ydVU8A^V(6*ZLC*+*4ZNNDh3TKgfV%e8yX39c`Zzv%Nr#HtGxyevW# z=%??bv1Jzy4XZB-gFObE*gHvep7uJOQnrwJ!t&-v_fC?2a+ol^9v!A^sGU6S@MM$G zHA#BSw3_wvO{fk~qpf=VQ&{!)dlXF-AQAxZ|Mh?j-@AF$u808uXFVVQ4IvTyt09yf z|J~j?xVt;JIR6{vt5sQ)fdv2vX#oJF|9$D-pl2k>fh6R3{{p0sq=B9x?a`(0rh%xT z|GR(gN3ReM9@M-0|3zd0|4^^rK1~y z=>HGopE9YxF@JNz83;;VqZj-?^{XZ>DdPgeg8E7sTl)jt_qGI}o zs0P6kWx`)#&i@^JB?6Qln+jt4pG*HY%;eg(?YFjV+qP}}*0yc)tRYdv+Cb7$6nwf95t`@`e(uov*yN1s8ne?1n*041P~6+ z0M_ns+h=G`BVn}PR)3uK7#EccMl7zkr9FI|f5X*IV8vq*q%+<((XKr)#NgXeVKRHj zcSI2?Hf>(+E_0zc<3kO4xI>LD-7^_4MoNi6gN~Z26NY<2Ej|d7vqX`UqysuHd=BSp z5})15=s6_bOO|_~YstgM{_vw)1gs9UW&K!k$5_xpEiIAf@G*Br46ZHR8b2O(eHwY3 zt^fOEFkk<>1emxr|K zIgo+)*k{72aoco@icD=6)$2=h9x-xNWNR@9jtfJBo%KkIo@R==*)!SN>T{Vu?kuc8N&$0KQflinAdR6fh7@yULD|K&x}f70cSv84Cc$e zUq}$u!gM6al(sRuN|x4911S0mkcw;=5)>hXAl5tMl?*+E9S)a5DT8}f>D|vW(r04Q z1#=xA>JY6IM=~lQ(v10hi}{?lED_aMVcp(&_E5i&IhD zR37MR^$Ke@=_=FdkalG z9W2c{J8DR{`c^43+FTdq)@%_AA(LIKIr^Z1DjJQmh?G=rSqpoqPVed=f5$K{EL?A$ z*SRHMXXTw6GBNJY|I1ry&XmDkk&`Kb2q&J{rwS$<$9wNZVsR%JEXssOO}^|?gTtQ< zGh*D_J}AHsoGDL@35c5L#-~oBOE70r2L6(qW=(+3f5&vK>@!Z=llU-T%#2V}DqUW3 zkcP`98_*3a8N2*uDRrWl9J8qra&UmAT?ds2QQDTFA z*#k7pCHb*$y%d7Q=+%iVm4mwoDB3AH*RqyL@rAvV_p&-x3BaBWawy~DDghAOUbej0 z%G=~(3l{!up|4`fLdH%5qOEDy`{CicdEZ`}ada3Je4&M1&n?+a^rJDo-m>SeNSKBZsbsF9Pf-<5wvAtQ zw;3^Itu&M(0Pr*}Rd0-{NOlg-%SEX(E!|+Gj@5}D5{bZ;CcOp~rm}X|rH27D-@-#F z8}BeCoylQ={R6}EtdeuW)0!Frmcd7owF5XK$P&_lOtb07fj6A*K7f#Ug+4bD+M2ra z4^{Sk1bA;N_O&6Shg^o;*zfa)J<3W>RnM&-gKjF@ z?7SR=_AR-PUB}ZcxjTPOa~&wF*|e%ay!$Bo zUi2vA0lgl%Ntz#bbO=|q%c%^u!+x>t65A*Wz5N(cR;Pt)T2$rR1_o&_wygwQ(GzdH8%`ICR@#eRCY!scb$ zU)m%5sdm~}Rf5i0md9;wfWOUP6e3qS3JG1JMcFfTOMizUy$HsW8y{YQ+_v@v@xP;h z^#5E1t2}d}=k(h!P&$B}18xWMr~##5@4o~6mVUZ_#1?NrPcUE|`ucbXbQdo>U}A+s zjkS3s;7K`4bT?fd6>E`AJi3+~JE;qYz#W|0GI_s_oL^3Fd!0K|3@h0jP22YV`aQW0 z7njS%8o9IXP1bF7`qR}MmygyyP21DEs+ifszHYtUm3h2%m)n44V;w!(w8yAl{o3kd zZ#S-8)y?YX=Lg;%C;wi_{PBGK>6ux~>Pc%hfPo&ZgL(a%T+{H$v$=aib#HAiz+Zj3 z&clY?_};tqXcpkd2KbhF_U_5NrSoFjoO#V}`prB~-KAU7RlBV}`NHS8yu9%BZ_vE% z+w%Q58opRu?E-uwey={ed%X4tFr3`Zo^;{kw$_?gKVI0f`DE*F`M;f9-zHC3v+U|K zcr1Se3EVipoCMd9h0(3g>@*0vFRuSYV}5ylJ|O}GJ{Jvs-%`Z*{Cf5EXzBCI5dA*x z7ndKrUykm}319S{cMWd8KaV+Pt>fVfA;D#FyeO-)7(C;be=`E8*8?e%G{Zi(St? z#p4Hnx!z%A@;F!Pzk1fs>q5kdiRujJtxqOli!+AoQ0qGq>&O~w-wEW3O}O0Wdtf0b zxTaD7V0l7(0jdYAppgOxe*>h3gCK}V5Y@WK9Td-2AnFB`+60vAR}o$?Fov%k*n3T( z@i%-(;O{qBV8~nB2#*QaXDp()4|M?2x80Z|pm-r$@x7F)SPr@U6`Gkfap{eb!bmb`56d1uN6Th$uUL%X8Y9V$v$1CIf(QP9 zRuz~rL6h_V-J?Pc;Bii1oY5wOF!4(?%ksn{Q6$y7z;2tC02O{q`8O4sga#(C;R1Xl za`1i_$?(03LLgcbT1!_In1lvoAu#|d)rLOt&|007_M@5o)f2!^8=ffHq&x^{!sstO z0m9AUI>UePo6C8Kb--Oc<2Ba89|FINl*$slm@-JkGC==KQr;@hxj>RUfW~sNvTdDl zLfb{-Ekjz3(Dh1Ht%(+FXb$YVc#IniKiri7fp65I1Abg;sd@Z&VQ`+%Cb0 zw6Qdt3!5thsQ9M7fhz9Uq$){mZernw#EK&? zs`U*G0}pK>3}!%wbn4X-)NZVuf8($!@Q$6S_rwY0NpVRoU+CW`h+ReCgtL&2hQDsL z44pagLV;{|KKhRj{3$j8`&a-=nW#XTvV+VLzQ9r?4#4zqb6?bV*i1g`+Uo&XJVG&iK>psg%v> zD^qM?&a@?;nbqJt!fey5oTqZ6TSC3b^e<*i6I@9@k7bM()ato&Tvwd@YYviS)pBYq zFC>7=kV)1%CajiO3}Eb@$0q~3RJ@)~%_jce9%GAsRdP7YO z*})UCZ%1H9vh>BZu9J3?`~w|M0TpJ@y+TvVPpS%bbQ0{#^jA@g!!SV8rft@=Y?w1H z9G(aJKj2_hAzI)^eYRI~+sNOZ!!Gt6zT&?*`92nQzegDb{zd7XSa`4yiV2>aO%}YJ zuL1gbv%WKv8AJ|Fu}&=Fdf}q;O`sBtoD{z@&TG?+oFt#{2HxQQAi(CCtQlzD|3;y2 zCzSXw*LbSaM3**t;%U>S4l8TnA^Zn4X`=sGHK=01(xpupBWi!i$lZM#s>21t=(zq4 zoNngGiL(zg*oa`G^GyG4O_6Rg6dw@10zOGI7?O?4-=C%41$;)t%HHWOY=$F`!H$!0c^Dk zkQiN?RllL!hn3t=7=*3OoJkq%w0U}~VPOoykL~;2>98W_NpX^U*;Tdjl!@CV5)-DH ztV-A>i9^9sD}Ue;WiMr$;c38pINS-F>LSi*gq@jd7^Mo}(>8t-UL(Lz%WJB~nk_va z2>8s)Ue2BS4J-7pTFzAspjeL!0p6JydaCNU3AjX}!wWZcjO_rTjR5eh#%7XalJGu{ z1cdUJF|D{@0~-M|B|VB^`Q6nkUrb`-MSoRMPP;1$dV?$Ge7ISVn&<1@YU55R*NLc3 zxTu{9!I`m>)2OJNx7$UF% z7p%m+bJxAcj880i<&$Y}% z)Xe%6im`mNTsqqb)wUnAHmbRL!J=ZMy#4@NV9R~Njx;7-Rz@964zq!2>z{{i>WhE_ zxm7o9VGanvs!8B=s%Ljf;c)L=HqurgK(eJ+(U+0MSbf1Freznmg$8 zlTOgajCB&_R)tg=ccjR!(0C76Zm}6`*9N>(Ms0eL#$KxEB<72%5kY8}v!n{@GX}`L zvp2bTI3I={LzfqUUR%CTG?TZV@qCOVvc1C2QNMBiYSk!Yx{jmdIA=C~p<=~R;Hy^)YYxLL zH8^9=ddF53H7jm-*e^n(NJ*>Ec|FiprqBkwO&=T<7HOOXwPY@eDOoYreQ$~?B7bn6 z3)`PK%4n;}EN}X+I!l0X2b!J9->&N2j6!+fio{1n>wyFTlm<$7LD!ysk^P}f%twvW z-!sql&+TM^IZ7X0+Sjo8ul!x9eYJzx+hF~9S=^$!$5{IJI9-qYmT2HT=VGdo1$I%; zIfLLfDb)sw?CcQ`b1X@7cDSDs@)ldnkUg=U<(RCnwM~W~^`lx_|A(rCqeK5=h&Zx> z{|zwUl|nK4Ahf>-|3BZPZ9fnc7##3_ToiZ7@9RHsKtL*rKtP!Pxhl3yjP?woV&Xz- zvZ@TGRwk}i4)#XQUZuM0_M4Izey2gD z%4yBg^9F9Uo1umx$Un$ksO3Ek(as#Z>hYg$?~3$gP1~yq%7x`B>sG*Z6M8+{z6@9<0R<3Is1tn#&7X*Fz7_vE+LV9)_<>D(x@g|cAG>Wc#8DL4?V_!&VW>=3~#D4!1xuLFx#4qXv8L#(PwrQ^sSzdsocxogjpily%E)@c$ zv&FCB!eKW6S8Ywf?9yYja<{H;YUTNb=N0z)>!4Ft4e&T^vd#InAAEi?JDt^ZE~+GO z%l_@GHnY}x!B34 zPQ?y`9~*BY%-jgs3jb(`YQ@f|nZ@HUlMLx)52B49l|zqPJJ& z8CeJAZtTyXQPJHU)u%Ua)om-ZC4Ay+n$&AD0hFce9E%Z}Y^W)sXcBUVQ6(Y#ouVjb z{s^B|QAsqgC-@8~=n@Qy;;A%T2%1-u!O~N^5P!^wbD=4INDM_$ka7Ng;WIFkiJh#U zs%H6IF}!wEhm&?4@z+j2KS26EN}~0Ga~C5JbB0i-O$P42SjFXNdM?u5b*Z%Be5FL4 z1%!VR|2z08J2r3w^^5O|ctO<_J?uJ%MIN1xuAmO>O}+GPw5{X*v3a>`rxC~#D=zo$ zSl1V=*I52;?A>Bz^lGx^@7qXLPv1OAoER2M5v!k^-7M!jr^(*>xv#;#S7EKZC|~~> z#0*CipFcDbtI~Tid+yN+uF#fvZj3Qs2C!SJlW)**WP7iEv3OUwcU491KT~9hy-tI0 z?5;Fj`)T#rQgF-i+g*`m&AK1o9K0oZV)uM@|7qKu_*XJ&gI~5~VEsF9wepR&0Ozlt zX7h{5X3pkw) zV(VKM%Vr4w1yoKf7k%y?Exy_N@_e&)_;c{deWhJHmbaKJ&lWME)zGP~fSE__Zc?H?SSHlj3&$KQ-QbEhXKmu=fV0mr8z@^9~} z@fJM}6t@z9Er`DN$L;I2?}M^0^*Vp#zyZS9dhx~ipT+pozD8OTM>8LSXAk>0bsL~m zo3=v`b#{=BKoXJsZ(HZNCcQK~WjBV+Syr`jmfcL|SEOrcNge<!tus_>uiYAYY5pZADU#5WW6%0>4&V|g z!X!H?*a74E1IKn8svVcL)}vdUiy z#BEY;d#&t)HFaJ8y0I>))svWr6!{-A4u~i5_un~>HG4=y5s^S~sKDg_^<18SUkzEX z(9)`aMx~ffUPGIxvep4}2eC2cV5F28^90Kg(*jj}-@w=5~2@avFtYgX*QkoFhbqv)c zC16a32+D9{T@ak0H*C08Yv_*b@*0=1l8o_ zroD4#R{_j*UXvJb_DLZc%r+^UH>!tBI?k9>+wH*yKQ_jy zRtYX*U1Y2(5OsI$cW7xdL+ADVY{W@KA=Q1BbscQ(&`M!uNk}qd zc(6F2{XC{TgCIjYC*^r%`!w}YY_@q^^`Lj z$^&CtqH3^tnnQ$&V%D25@Fjg^Zap`dj;-_@;tp6~0e@k1GrUes$dUt`)EH86J;_$9 zr1F^l9%TGysctVBTkyS>Yk*Ib9OY)-&}QH`3vf9v>%bbnZquzE;*Ma6-aJb#jEg7y zU8y?go(MQ^50S4Tg9HEDjl{SCF=v`# z4+M;v922V!8LpATvN~y`K-5eeu{jjrA_gk|1Rx$9xNvzWHJ~>+DJyCbhF0tlg~&3c z9T-fA_{rl6HUbWT6fqQQ2t>y5>>k8JQCM0b$+h~}b7~MDmezau z3-B+ySt;plBV-HsD}@2o$u$In_$Q4k8HGYC6|m>1};?#-0^ z$7>wu@Emog+##HC1Da4hj!~pG-`~gpnoaIbTcB7x$gE1NNA#m=C0lE5(FGQUp?Q*} z-62<~0aY>`9RiZn1lH_f8gu#4dHo#Y18_8%Jz_F3HJmQ5mGc9xA#Dm+WkkzQ-G%G~o&>9X#V>gf6+EvA{B-#gIQ9a#tDD9s zT8>GukR}BD12bhQ#k;rpbZn|~b3lclKz(6hfre+JfC`+*f(#;Of3;Y}Y0UnLlQA0e ztKYT&0~mN+@}j8?*UYRsF)ONh>* z59={{a>PFTFfj6Wfd3C}k0)~KZCBvVdFl$y0T<|9Ng=4{!7y^qh29|W6BfyCx)ajm1ib$bRX)mfcQs@#3)yUqq! z@>+Eg(_SVSO-tuKntw+iZL%ipnfib?+lsdc_%^yG-SI0fO*}N~+KryT4P%B3GSD(; zop~p%V41bB{&t9DblKt*H_EGaCS9Z&kbiaPD?%Daj38On&9bYeayXiCU*Oo$N!ZL< zgn4C}zlkz$6t2|D#7gq%EW=gE0?5nUis8 zi2?Xl8b_jcEWM`y%lmh8agiMGSv6H%MK9-?fDg1(ObYA>p5z!Q2rS>X&D!EwJQtwJ zgTOyz%gbv26CRY(^k1Ck<#zMlpVw`}H$Ol7X`H+poE!cWt*?34zI{^lkP;xtIz^=V zfa!^*>@qL1-nb+x*eFi9Atm0J2NEnmTwc`jO)_Ga%{HP!Ct-b^(~QstEW`n80Pp3& z(dDXv&v(mp2?tjA+s|pN|6^Yiq>_Fpu{I#*DM3wV3@pe)u?7jcuBIk}Fo;%uv}0_+ z37JwUPVs)25q>n_0wmbUDbM_%TFBR3|7oFQYymi-Q20ctA=S*P75CW75yKCVsc%O< za83lX`E8gWfj?}2Sv3}KcJ zh7-vI%!h==ySEf2gl5>}#y<^!Fzlrilh^xsV~AH1<5I9+zP{d-ejoj|ZE-sfnF_&`U*8EEzataA}+g zMO&;y7SWg@Hq0plR!l?&;u8|X-e)5J-I7y{0lhdLm{#H|<$4~~qbdXN*(M^9C!FGy zA3U2t#T+oi8;HgNcS^fl2%(NFa=~|br&fVHzvnr=v8-&q+DrOZ0Yzn_gL*%d39ivi z#B-V^X8f0|Szn2ll2a2+wHXk%0C({VH|fe5V7M?LltYUEaqs4uL~7b%OlF00-G5m) z*c4mJiSHT$G#CXFsu%+x2{c@W1Wn)$T*Z4E`~;HpB9R(c3jaq%-QO851T{c{uEmN2 z)+1RGjFc+m4O|%o=WLy(nNZ4|UBe_p=d2S@fMrN`Xu7_-Di%G~JT_85MZ+V>mk}|^ z>XQiD3_UopD+r_dZKeY5ydiIaOS4MDFI7p*gQ1ox&RprNsgVJIkOGO@Rb|~lRzPZa zwusiW%zo)7i{%1k-)PkHF}wm#VUL3_LcmUS%veR9p+Z)cjGP?z^u#gS=(~qp(4_Zg zbjEKpH$DNv(tu+8f&p-ab}} z8PUWSv4-5L5*7e4z+h8SE}3#OnV6MEwhiRR!l+UI)!G0+EI2)Y08T-E5+8UQ==?{q zkVA~ngoi-AtALJjRMdQw8!Kr(w#hRUzN;oMCQO$zVApRj;Jb_{aG&a0_tP70TwS37V0mPG9wUa7XpMYC6S>clJ@Uazelo`mAK zai9Qg6?>PPi)Pqy1)94QR{L|n6=D!OJNlmmo8;(nl&pBUvjvBMInjM^Y2Z>Et|Fo( z3gO$591o>3abqH>Pz(?{<+qPIx&(Wb2{TxIQ327OW%e)aWFb((?@Q;gw?~*aB^0=G z%8U08afrmsjAUG#{gKdiIA`EwQxmL3c|(9TECN^koQ+)yo~C4nkRJ%K zxI1`m22|jKYwE9tnZ6pc0K zR-kERT_e%*39L#0is0-_5uCZpBSF=j1&|B9)xuj1GaJknl~eIp>(nzHSq95kag0<0b>7WwX#CyEw%D_DNzodqosUTiV;GvvGSj7E=}Rb7ooR-YFK z6D(fHJS$b^VHbEESj*vcZRutt3l1LzkMq80v#7ojMic?*^bc0P4|; z(+&Rnq%uo;dkZW}H;+=;pjwkLEaJJ0h@{DKX<4hHy3C9zqQFy-$7I$#$}sHDSH*TW zY@FRfy&8ANkDXgHdtcm&F8+24E3=U?3dLgz;_snzom~{+8giHrO8iK00Hui?*Hcso zDIVPVl_O*UrRkTum7-@z;Thm|4-~d802QKf4#sm@5G~`VmT_jZ9F|GH(xh8?)YA;Y z#PO486CW`O1(0z8VF>T@dtoO#pYt8*+n08``oy2QJDz~e9-%yr9%pVHj{e9l++bWH z|K?Y;Q~&=Pe;DhzL*FwJ;tum5C`~~-BOQRLa&x> zyxXP9Tq-;*K08$B?sooswy@3z_bD8J=$46YG{X+HUX0$g72ziB>q)l2h3Lon?NwV+ z8KwIk{*{lhA=rvR)eiwoW2`5$v`-rEPkGsn$=w;~vh6$qT6^qm^@J^T3ts>=ag z`TzIEzr@?+Gwq+|D=s-shX9cu!Yws4_&tXvlD;1w zHgjkWH~gU;?fSfM5L=x_se|glPQbi|8W6^KD+Nc$vdy6p%+ik;+GhobwcP5-gAlyl z_yunL2Pax6uZQ{xh7Oms^nwBZ(ENMaF6wJx8tJD3Nrw#pwGAn`_?z+&z(f9Leo3{S zROBft5(tvt6abu&kU&IJ87TC8tunF(M;fPka=KD50bA;8WayI9gaD7w&Qi&gJppa9 zgyx%zyc10r1dmAg&{bt4!#Zi=C!+zDWNi3B`xrDNT6NQ~nN31hc=VKRSutj2s3e%E zrl8xTC|X{cOc?o|lZ3KaX9cZjiC}sYOy_rm^eT@MUx2cr>>;HFPEev?nQr(R?~;z+ zrlNjgijpQeuP(Idx+yVJY(CA0BRNn%Ei_}as)icKTo(Os=Ue~UbP~Nzx`7`0J{m`V z%ev%Zb>Tlnkx?4U%s6_Cl?9FUznrwpHRJjiLWdG{6N!+dX)$6@188auK`(?rpsNdQ$Y0n!h$6r=E;J9Q5>Tdl_j>555CT1Omvx$fYUNd4EyzL%0ef0v=7Fs7Or zF-9U0d6VcmgX}DWxy6}%l@Sn5dr{>->1weTc~gWLVK1rUnF~7!6jlfhzm=n{M4w8` z7+;tVs@jKrLo74ZjbUb4GZk7{wM1K4oy=GYRRLjKlrR<6t9Ic|4-~iis_q!)2`YcN zS}wGKXuH5fT4c`d#u4G`)I=nNVr`i4{VQDP=zojqhH1KTZG?8ho#+gotaT?D>AN(N zN4JKx;MyAhrT7+Xlm|^~0b-1jXnkow@bb)>$(}xJ&bldLDTIkqPX!e>Tvg|6Ax&{J z&;Y_d3T9aWSp}(A;dy%0-hF^a0VbSyaXwT?(CgmW3KT%u$uRF49IArxDuW z)YK~JH|lN5>v|%Ouyv*9z&X>1m19K()~Z~x?Xv)mB5T54Z__vgM*I{+M5 zxj`$&ii)9a^e(b+2`7v)T@uTGL6W@ZNa4P7ze%>&t-4rNXs4jz{oqaz3~x-D!sI}e zn3^D7u3A&qr)UnIVsafzQR}Y8M;MoxhxT_o$HaivCMJyF=5fR|l_E9^iCA-jlM#yG zP}D;AWDhaQ{QgPJkk~>lVTJ!K6a_#ga=128SY^t2MsaZTgn!c!48$MpA0ZO$CQ$AWgfX9HSAYvm}cZ~=gjxLW{P zVF!5xja4x(#_wUi6q`;EKsGzS(VA@Gw1O*Ydq==#e7FV5*S{YR>OHUD&thV{OVmUn*RAf|dNqpE1b)M+EFaRbB{p7S=pZqj)zurk%omrd&m`E@#fbS&l>n0$vF zhGR^U$uI#pnI+Yv@nBZ|*z6k@}u6kVuFTZ>4$r%C|c0Dckf7@w0d)OujV1`!EJilR9W zn^bSqEp04If!?S=LLq?Damb@35=)JAhyus=Tla!X0M{|>-3-=Dm=NV;H+~!u$i|~Z zI_a{lUYzfwX=xjG9t_e@qpen!Ke>@|)iOVHBB@I{V^D=!|KB}ntA-TIOZT#c6A;Sk zrsVp#xB>PQX)3ww_>F44)^%m6zY$eLaSv>OLCv-~a+BnfqiTS-oM@vAYK6WDtS!5B z>R6`#3sDc)s!+y;h^Ilw*`gopq3Ywdt#f z>MY%bPy|pdViABJy(@_XM+HKzyOq<=f<|OZXS=_Fi#mJYL9oeIk#lA?L1Zopp>;Na zi8i*2GtbDh-&X>uA35FINpM&6c@nvIoOW50m%R7q4X<*tK%CU$!wsw-GN#_!Hl{F~ zl(?I9L_kzm!c}A~K*Q+nc24bh9_`47t1-ivL93(WF$~}&?LG%H6VpOnM_Cnyz|P+G z>NcGokpq!gl&Da6e>@UMmY9*TGggT+;8MO^Y3E#(38Nz*z@M%wtIZaL%l_EPdF#=Y z`yn3nqptNAuSHI57#mJ+O08BmB&+k7W9OMa!_XxZT?Hf>;thHrRnx4aHz?*azz~dd zT(=AyRsrxqjNiVV4lSo+;z%0R)bB)76(}|1*O*j+TmW@P|*$(tq=6)kJ0!$cIb#h zv!8)c#yNSgd0D#4!+8&KfJ8nfeB;qLP5B$d(`?Sx5Yz@SoZc zL;MArQ2W$F7Mq!-%p6T7O?2XE)(t%hV{|Sr?hqNfDOLg6pc<-%wMY=d#LEuq{?n}= zwM76)MjjC>xy>K)+Ck{Kw!&1@+cV~U+db{kIXm40mB-|)8=l2;c{074{X>rXHg4Dr zMyn-t?z^jZY@$NzB`nlUY_>N$W$yX`NszmR!q{5o^9}jVo3Y%w0~Rd>A}-qcKZ}}z zI|A`K8g!zJzD%&Rl=y9R+aWg6?f-pH5!3*^!=Bdo`*W$7Fp;w{Ndq~<-dnvW*@b;Cwr+QguE47*;s3Y)Ld0w0$VoVl;zRl2oqPzR+ z-lXH1K{5&y2@%QAgV)1dNZO^;!O@>@s>5R6Qa3Z$boEBrUgB~?b4F^gj+*uN_LBiz zW{HoTLVjJbs;*q<>mQ9Xh0@v9xg_=3k=f5ck$QbZ{Z^^yz^UWxGoM^MVLxowoOo<) zt7RW26#llW^h1RlkstN;(9++xnYj+a6}CAAHv!BXi{PM8Hgdu(z#GR(R*g*|0JMsQ z;Y6U!o0m^=MIGBiV6jrKF~-trsc*pKIR^A3qvHyFL^9!CMV|{&#b{v3o+)0(al0hL zDqN*{v1!b=kXN#*H122-a!;kNDQe@a7+S3pQ_mrkW@&pgPRcX3Lsf()*C>;gPGi;i z11-f)dI5qn=6D-l zLyCkdI$1FZZH=ok>$oPy96MhXZu=Qeb+L6Bj9gJUW#y(co&8U|<;q#y-gFhIa{<@K zCDqJ6ajH9{9YcXV^|*0WnXx4~Ly6gvgV^?0_JmJhZ;PDA%6*fHX*z3qx^%TXx6TGp z{Siqzx?2skM%|%GFczt%)jFW1p|prmc~#OV#M<(PZ@a!_^Us9h?x3Qw-=BhqhNXI< zja>~^ zoBa-6$>p23;;if8D|$c$=w{sA*(xzMWKIbQ&&h@!6CVgzLS=v6LC-g-x5Oi-Fc>wR zhJt@y9Asg4cI%-`&|GoBwvrfFq7dQ6O%jPABgWx(x0f$+riqT)J`IaKx(RFJC7EFj z%3h*8QdV;o))_1;a`66H+6v{-D+sm?V`saD zj^?73B1$Smya!t%^=CjVzHAeDe-(jz4GVS^l)aH{Hk75NhY!Ni1J%F-iYO7VlEv(;^A5_AF z!Nrp)WY-JW2rhJY^SPSs_ZWDU_k|+M2CyNhiY0drR}o3wrb9KnALj-$sj9Q&ICi~X z0PQ{7pr?}l_LjxVuuBbZ&3GwVdWTDvR%f3$_An9tfDWKH1`jQlLgFwRq-Lc)DARR; znN3`KEtBoA0B3Q9<0f>m?6n(mT;!T8M@icg8)vJum#X6eTYw<8iTBT-7P>5Zr>OR# zIpfYMAHueYxdF;OP(9jv#j^Y*C-AIZ`>go7A~5c9lFq#0ED7VDsVGF;kC?JbLuFp! zYbQQ(djWhAAfYedEEBc`IH3q`*3CRY8a@E)rk2S6{^8W`!yYI*6a*>NgI~mh2T@wm zoYti}R=5^IW>0gCL#P{?t5yvQmsQICHv5GtSl%zWwtY8?(YbY|b4{$eiF0bKs%Ud= z`e56toE$ejg3IU_S$8xu>!R@ZPC%Y;bLCC|bq5f{48YDnsc;}5TOg6 zjbgvSeiaIl70Ys%UE(=94u}0Ujy0MbXG9T`LVJKAL?f`c3(vqBG;||^khRW{M_W~( zB>`MYuB%4snb%V@svQPVTj0H7o0a*su#9M zauOQPcRk#_hWH!!Ad$g%vCgXe0s+Lq3jv(dazTk>sP=TK3LN{0FyJ|7vpn_)zY}|7 zEB49*nOJ!1FM1k|b>wmM^a2GwD>LKt=jxvdg&{uak7w>4MZi(DU|XEH;Y3E($Et|= zE^*2(91{PU=D;TZwF+3X){J}k+U8k5*Rk8iRJC4MMcd^4?N}_CJOi+^ z(9>Sd%6bi|z$?hM?H`cwhXXLs7{Sgs7df-2r%%RgK;D9Y1T9;` z*Ik%jt>A5PG;^CW`EHpz~^mO;5)?eTH)Md35Qy4~P>k<$KaRYX>bEJH}<+fmyJHLR@gd6AOb<*GNGI z_&Wty_Ni8TM>>z308SDPvjA-l4&Fsx zxu+bQto0q=uE@f74TXJBnbli+ifojY{r+n>rf7%e}ljSB|x$NzRYUflL1Xp`1+14vuh zVPG(xR&=~3X=mnFN5%wqiU7m`Q+mCB#mr~C#G_vciQs)`LMYm|a9CfB_k%|XO$+N$ zLWK_oi*K35)m@Pj*)Hl_c)7>uZG=x=kd9UYpT#S+pTB?#vNLD$L|nXge@He2#NLYy z+Pnn`x7W3XM7+$=XVNK{h~Nr{H`3Y!&5(PmZ3KWMH_|Wfz@La9iUD|ugL|xD=;x6F z)9clGO&aK+?(XKIK=I(iyT6dZU0|Tt2tNjbOg+ceL;)e^frj$+zYlwAK0TX0E1Oac z#&0uii<0^VXhrrH>WMeau8;jrA*OXB$dwSlgAdtP#Ft4#6fU&pT*Vn<5*MmY7fM>vH;)`r;ODsQpLoVFF&^( zSH{QWKT}wrnWaHI(}6p`TjiG3+0;BL>H;_G?NwD;p5Di1u7~z*I(rxvy@aDF2}{0@ zQ;Nq=E#+=Rgx%5|di}qPGOw%m#YF593G3?}DiL1)EnB_9Yp;bn`cVeawB8@#LM^;O zf_c5P>X6uY6Yz^_@rrR$L-0ikZ=%lHaXir=W;8Em`sZ@%a-4zdpy%Vg4Kms@w#jih zvn!pM>&@EuEFF@8DJ-oF^14!lrSJ`)*dwDz&OUS6=;9@c+eaJ$Np~Ji^W=ozz1}Gi zIx6$&B2Tn@$dgZJPPBdLfd4vjSQO&&Me*Yg`HBh(0$^HwI^=g*x(_mEXWSc%U2R9^ z7}*tMo;}Vfg!|khq7?)J9$0)*Zh?}Tkf0%7XOrLxCOSgQ50dy|Kk#0$Q0rcd>pstS z3CfDFft(zyDuQt!8{xDcCl^DZ54)kGx3SJ^O*7d#lm^NimRy;~gZ@*dR|v6WaXs3X z7G*6-4FEf%AeoG)9~W1K+68Jv15@d=l!aX~99m|-)~U2=#ry%SCB zCqsXb60l)0+Hlfom0?p}iTHGZu&0(I@jb^vGj%$|5%UnnGuHDmBtQ$EjmOKYct`Bs z00L{_Q`Y(7Me_6ZYT(g5K9!)PNnPNWib#i!x7!($jmS_gE}ES&tt`wNkm&?qg@{5KSU4dTIk@H9 zq(*lHc_Aa53Sp3dieMM4I8QaRn# zjKZXUKfZQHh}wpSrMYd>gL&s}d<6a6wfzzDXW`JrZIB_*&<+Xx^C)7R5$C}|o$Wd` zqlUTKB0Ga@kqm$wz4~GvIk4Qap2^`KT@5~l+G}W_%IY@{Q`#w&X};Ls+*FwgUyP#N zUX`St)@CVIi!qYT7H6{?u6c&VhXusuUUh_j#PFHJ;(zHdNEfTD5E8o#thPLhjCtZy-8rB8u8RcUtX0EqI?CF`NkiOFYEI<+yBKmOYuX^D1lvInF-;E`&P)9?m%dE<}aPOk)H+z>js`*z_8G z>X63IW6+n=OHhQgT-bFj<~4_s`8{HPVR1DTg4;5el8oS$1S5T2lCmi6s-}oj{8i&D z>a)gcYy7q}j3YOm?ajuYt6s%R$8E^Lp5dt{>NhE;syZ-fgZh0Yuk7>j+O3&uKva!G z7|uKd-E)VHQiE*zB0Ld4op?RRy{_Bfvcfpl*48Q;XJ$ zdzJT;mY9*T?Oh-!D>?UQ*UY_EB5%auikeefEXCZ~Vh;DOtvU2X&b_$0mK-~#QJe0D zDXrA=nnNi8k-G@s6vZ2IVz#7_r8yO{w4n7=yW5K>#Lo8$mGZ!&(6~Q))WQSLvQFmK z3X)*e7@9^q_+bS0qzJE&Q`)J2=tqysnucTZl_f71+%$GJVY;r~QnX zybsiT$ewnhArmYXWP@9m&S(_ChqqICaFQknXgd*nAp`ah(KrK^%X;UyZ;AyWf%x@8ig*g>e(6Gg0trOmrq)K(UMvZpY;GKFe7SMB?orNJJ}D>HTAOM@(JfVnsx2p(jfdbeb2YU}qWxwK8eVGJk=saB zowl$_DfJC4tg04sD~8hEY)I5szuFA!@44zv(U>28F`8pU(i}e;r8RJs z7G2>;IM{|&n5xsH%z()lxy_HJV1NF)38(apYx6_hp2l31L+7O5Xkl^D$^iyZjN8kA zx3TVSi5?w79>R4VYEQ8uffu`Cv8dJX9gZA5>m|U3MhPH)KNZ}BTK%+iBh_G_ONGx< z&`#`6PisJ9hByl|m|xpAau`rkCX8vLAT)Utq&bTt)D(fF#-=JT?o%&dx``o%qCzh- zDv*u;Si1}EuGZ85wK=Q(qPnh3O{G+G=`StW>1e1^qZ9`JGE6tJ1cNvF#Ihj@H18#V zGO5SZa*%9)MzmMc`qmSr^&vf8H8S3cVb=UPa^&Uiq?&Gt(IaAwqQn@U^0uI zQ^T$OMBr+Op{&K5Q10_>v^i2auRi?fFE%2pZWw?R_1g3CjV9XY)$Fvv|IJrs`U)>_dD=<7-cbZr>>*67HJCS zjv)`Pd?XV;odm~w8QO6=RR}_Lki~{b;KvNl?uHvw4{%X7rr<|Lhx~aBs$Z@ol~3h7 z>}Bz&Xn71XdFUO#2tFL5FRaVTA<|}lWOf7jW)?e?}=3f2eaHX?Z{$zf|k}4h03Wjas)vjRW)5iQ(0jrJ){Z&Ax zxueoGeeD?jO|k6C7mBB>9DL3DbiRq=v-1MBdf-RL5#TKP*xQsbK`R?E<=3l!O;*qq z1KS-NQE${yUx#6!-k#_Hu7wE~Mm`5+V&tHeHie?T=&j1+@RriAtmrK_u<8SQSKwm$ zjPakk#ArRoKzKuq$zYI5z>Z>6RUxkQkcw4)T3@n@xngs4oTUj%Zv)`}4##Wn;jYUu z$BJA(-k@GjQ`l9$o1Yy-p_L4O02F1}#@HAP2%~@l{dW4{D7Z`x`6f-K%RG0ZWDJq< zyaYm&ZfR!@rfn1jw78(S!4GiD0XVo`60)ps2n6`>R9%*3V`G#VacYK*)?$;%0O1&Y zW)NkZ%TU0a&va^A!%NJJ7&R=Hzdsa+ASA|{-R={T)@AGGQL#Vb#aK6g#S9K$Q$9P)v9_sSg zzcyF?wf|~(A`LuXd?J2#3{W_eYkaz{Ko1zAh~FJ!)a(MSJ#c~6?zuoSqVC#DiMk1v zm@|6 zYt%S1Mh}ciHRNExV*zXJiDlG;^qj%|!N#+#lOy>3t$=~M2gaC=fMIB+QNnQ+$d?bg zUBJTsIG{Ut;mO9{=E>IKvyJCl^BB-aw=y8Cv=ykeTbsu>=^&hK8;JG4VZ0i{c>h+0 z0X0VLCbsE!2_FYXU(PlUKw6%oBp=+WBnR?h%~X7w1$mN&L3ZoXY(~H_okVk(yhfd; zWFOwDWSRwkm1k|i@{r?g4C0HD&sB?KFv~uC1QN{jVK>Vb9<<3-S5TguGSqZG)?Uvy)*sv>>zX!e7W)tHk-ctMW- zc43%MG8?a)EVXyd?z$x~<+Jk+ovU4YorGVw!cnjEE30#Iop5W*F*z*gTT!Hg{MVXE-Whtwkm`ra)z+WHFjR}4LfFDX( zoKEB0-p_s<0ZIeW_ZWVD2){o=(X7%=sY)W`E`t<@a~?l8vZ-g4ha( zjQZ>(_@&4f72cX}MT=hVgoUMlKASW9byvq4LxT$Rc6-aFUsLFVLGj00^7r~sc@LR? zM6rvt%mwAamrjBSu@Z8(Gvr=p$RBlvG+jIw%EmZ&aNeRQfzojZ4I2QwsAUTkv46y| zStUNXFkIXq0GQ&D4&(3X5H6iCDAPp3-0e70IUBK|Im1SW6 zg>nb(+qzQY{^fJYOYYse0@LFEw;?flT^MUmxz-`{yuj|lHqYlATw(K6d0LC20et|HXp;o^a+gk} z&w@kuJ%kn(SH$C^YDZq62`brh>M%rnQ^X%2uMq z>HxY%gId${)`4bISlXnlnY1@5V<`;Xp&y`j)d8-hVYQ3PabTLj;1Y>BLMqlG3+U!3-}jI~U*@A!$$;@VPq+HCbi zf))QTqu*aejQ3hL`|hcikdSdIsYo)>_-xqLVWfi<>O6<&3H2*{Opa9)u3!09-EWI%>9*f+904qO+~zQ&AM&_w> z+Nio-n}~z%jt&A^ZX6OA3TGMdq#Q$A(eC_?(;7IZtvzrq*Qow_8<*AZ<9j-&rPEHb zTUb1v$*ej~%PP))#_~Ki2&1?FGr_}f<*Mey2yTN~3G=untfen4@MG0A{U2&rRKsR&_z_B5{a{sVk!FC&1X^Rh#U z&)Ukw+g$f*QeUb5(nnrkvc8dFK{qnE2->r5K)PCd9AxXNPkeu8H}Q2C0o(o(CiKJA zlImUpMR>QUUA;C3iFKE_0c%S)Irfscz9rWD(;eyhn15d%1^-a`2l0CZPy7STJ(TMs zIrC+!=#lGxDZm5j$^DBLRjyjZV@a_EvAsRSeB3|2<^tQ=BMCj+CZDjz@NbI4^Sq+z z2)YqRcX4(rlk+%R;G6ePR0sHH`%HfX;h_E}VBi*o``qHLikNCBG~xw0H8m?XZd6Zt+LIyG(Y!44t8 z1iz?`tTqkOUka@G@c<8{ev-;Cpxou;Rrsw+e6@5!qYD)ltIiSPcbFUg!dMmlkZb{!Y(=H(#oS*Hq{A(3bX{NN?%@9l9uyne2?m52>j(iKy(&t zmLgGpWeI;FVEqLpXrfMqeign5?9}TDVpW@e>s@B7XxwJ**<$qTOPxW5XfrNlMq8{{ z3#&z=70>No5VY2b-BM_`KOn~Uv=E{7%}--Kf9+{Wo~o9X>kY1F>8+=%^ONE5@Yg5% z@8p*z%ne0)L-hAYx!uMW;XeKfh&OLPoHWJ$xiN%KEc+-IKvWo+Iu&Axgm0z``Sc=x zBGpIB2u;qrZpa@k9x(V|A5L}(l(U|J^c#EM51RLvj}XSX`3?Pqi^74mv^;2Wg1e&v zjPMF6PWNw-;xv1altac-S{?MBId-{HsxFQa_S?Hx%G{jo=WaVB?S{35{u%uR5kG!* zyTj1!vaqMSAgp8US_-v_UH7Ii#(K7Y%fi;U;_I4XsO_-kr3_BfsVp1Pf4h*2HuxjG z?B5iseVdjQy1c_Ixs;O#|3w`7F-n{JLRV=XnSDh@>w>>P2`iD1r@`XSE@&hnqp43u zBQcxx5Tx>gbR$N2)d0;~?46+ptPfU2cPv(j)#_X|RdRvn@@Iw4fKi5!i-?bfKxzw0%+s1%pO^WuuT-Sk?_j zo@h2J`6#5g6mmAN?gxSfMqF-)E5UpG9{U%}IgG_^)F)y2@ki7(Xd-QR+!g48g*`dr ztD!=V6<(RGRr`;q*q{8XLsb}I+35HaG|~apD3g*2zr8f{#f}o{h)6IB^7M8KEcfta z6`2f7$GQ0hsL?6`m~yy(2xr^xCR#L+Bd%e3RFmZ>-GUR+IkKPLS4tdu6M9}#P8!NT}Lq_%Vn#F(BUU;%DU@b z>O6<-zSUW-7sH!zu=w~_n<;N@Moqm-#Np%p<)ehRMk$JePNS-h1h~IGo67j`{G!Z>1`%{Al?`GEr>BgOUWkQ_p0A3#wh*T6O=)60;<^NZh{99_Je?kN-Ea!+coR&2P(L1YU1G|vww#OF+d{4diRxx5`U1?` zpvRbj-BB#K2I48mo55!Z4B>kU#3GU9(Bsaq`zs?cgWj2sNzbP+P>Xz;pdwf&*BobI zm|_V;v#BJn=sI{_S?~dRSolh zhb{&X!s43(x=cfk2O7Qxs*|r?svqVFsK0MAN5}Fbuv%mEteqrxD_9C#V5SbRBwQ;m z;s^3tRYNYRdV}LtP48g-0D$G>A6Cw*%`RY7&Za_)x*%+RJTQd0GlGK77G;Gab}X1$ zCqmr>ti9TG6QjGN)%sAW*=qcHfI4h{s0aJjFXVd<zNR5P=%_#=t)f`0t_VVu5`9sB2ZLzrVudak^-zvKyJzTVoRi*3r0jORr+s<%B zrR#B>>%EIx^E{8KC}@TAzf$N z{L}6>8?7MjO2>J7yTNPTDC3D_0X77w+SH7hd%eNrM*q1areLI{zvJN)Z{v^fAL>iE z?EXMrwhiN@KZu7pN`|H^(tkrb&N1;7cgSL&psgmjSbhsETgO){d~m&g^ItY2!DQS? zQ@wgk-Dp*h@?&{hNKeTzPCMM%rG%~5Kizpno2B!V$hbr<<0Or<4n!1zS3s;rl`27k6u>HiI2LjRW_h`su40yTjRM?>pHKWO1)C2&zNouuO!2qfu$ZCZft3s{f(TZ1cE=f7lUM4<+hDm z17bJ-Zc}$+LDq27v}dM-bbU&gqQOrLr6B9~E+yijLn7jvvIn1PwJURz;j>;k=tVTd z1SZ-Ap?VPKDbm1ybg`{4`ddHQ@#y4PoeuHxAQT-GX&b*iI_U&ePg)=!GNI0`jI;$s zwAo;7#m)fpY4TPJ$I?=bYf)32WQ-I#ujSFr1&c007`FvR;{hEks@dK6unaW6g?GT zt(_LLYFN?g@VJn1C8;^{7)Xu=w@pmJ99_|ZR>`z<)UJOHB3LuYa@|ER=s-(nd17Os zqw~IBWySlFCUos9ycdofi0;no!QSf~u??K!YO@qELt(G-lZd30e~oAJX*)x7G8u`y z&jEfTdHR}vYzC_0KDOo^u;7tyb%ZOBQVSecF2nt?5z>koDJx;<4TRM_74s3oo5?QW znrGdl%d>T)%QIg(+Xd@R(d#LUA=oj;8h)*TaU!=T(_BL^PHbv+kQ_D=onae^?O^%G zl4jRJku&^e#}xTTTeD+5tlR8Zb&qIvK;YYLcEIp|`I?<&UT=CWt?6;?$g-=qCBJ#N zQwFOR0Q#;5K39-dUKNLHIAB(vQ(4=7CywLX-swz#o?V*V$s(nz8PKf8cj+|IoCY2$F2`^j zfLgo&NfxMleLFhz5pujB$}Ad5y|s0GG#pIx+pR4zp}0`6H(98>jbV(D5WkLRviu_e z{_$;oTW%m|J_RVJSnu*m?!*5VGrFGRK3|A0`2vK^@`@UbUn{@`G*A*8$4pd`8U@0C zvCeGB0m+M-t-9o1yo7+5jy{pL>dOlYahTl`o(=54t#d+%!2U}m9@RH2)JUrH0UVF) zyY#Hie7G~NpgvB)@A728Jcklv*>$fh;;M=MSA`?)@-Jp(Fv&AnFW#;y-UzDLUNDV^ zFz+qo$k(rJ4TQv;;44=Na||7U8qno`ihVP&d$?m+AR%Q`15?=`F&C?pJ`b!MbM|jo0|d$oFVrs{}o1wpIt9<8(?ZwX9zf)Nk;*dl{U6e%&X= zT!0&Sn`W76$uUY|dIKuKEvO))dwO`$X8;B+P6b*IG@vnQD+a~o)5R=NFd zfUac@^ixQ?C$kT&e)mykPr9;dVdjI7%ifc7*=uM$yHDMGIu>uV_iZqL-fOV$y}oXl z<~~6SrA?oD<{JaU;g2QIEI6Nhi^^FB6GOi(fRuXBTN*yllaHa`L>XWf{l^5t})*Yuk z?8!x$!Yn7Q0OSN(cO||-_Zuj*`A+B?eNGcux?a;b2FpZpOr#ue$IQ+vrI15}-#YwL zyDipp^u>voIY^^s1c^~HOhiRVq02bW>rT@wff7Kpw==P0bEC;rmE&B1r*b(JUK)Gh z#_7&0*yz9Hd@a+yupW|K2pq-Pr`Ndh`tEGBkL>jYVH=nu4mU)<31zn(<&B48f5E@s zU>o@&y*9vqwIkz1^(O;rm+1s*UzZ#k4p1&dl1grvO6vLeP}Ej%l!gPhQ%DQ^Y-vBqpDk zA7+VFhrS7@L8+i}IKPZE~euoiG(AFJvY@S%0-&oRg|>y_2r93lb`v8DY<@^5vdg-*wpl18t&zTD*N{_<`wwJ) zV4`|Y4fq}`iX{6aBRsnn$}tGuv#0=9fAfMW^-ZON#Z zXcM=pYbD1@T7j{KN*;#!J@6Us+r;F)@VXCs%r8m<;o?4r%s6w&-Sol?bU^U89chGuOx%^badfqfjl zKNCma9KQSYiTDQ2pk7@5VQb6N6K^a{?}48(Zh>p8Nj_fSD8W)A70xX2Mg$2>T42NH z{gE-qGg&|nHt-dX)=tBpHUwX}C>@4aLyrL^s#JgY*LfjJbtLyDpC%kYJ&DK8ICXtV zZ-yq5XzwPsX^X=nfxIFmn;c&RWwFSFA`r@&zt)6(#x9#`!xwldQ zUSCWT4R>w9x{$Td*gs;awM6XX-|#|@tpG<=VV!;$eRFX+}|7nmG>yK05I=YBE{ z<9y48g||y-e4NUr`%7#p*W@b~OJ7;I?JhJ?o;l{riSEH--Pn%-I3NPg%>OAH;qbK z=U-hFo@#eD(BtXF<@q6hny6P|94Qa6OdRB+SE~)M5*-0aYW#OY5L5aO)6bLKEN!LP zX9>gpEl8+GvqT-Rrt=*V{VY9f4+i3xj!*F+kTM_GSPHCpzKqv~P|)loJX)q1^frUy5;eoRit@9ox5N!8l{@a%uPEG1H11pgurg+CZIR5(fl-0;!Hc>z^EE#qeP+ z-KSx1>|io9zp9u2`dI3XcG3eBjb>;ky$vU81G1byV4-+FXOV#kh_g)~z|K1enWW`-K?4!U zOu(k4($Q*MTe?>~uC9!AvFLp8oqqPPckI{l(I5cEJB>~mtUn7h!14m5Q95eaJ1?|x zmXT7h?eRG(s=i?IWzP|%69O4Hb?=|nqHN!4!jmCfa&p;!NhkXB3OPfj^rFD|GM0~( z?088`K-F(r^0Z!Ws>a1ld@HwM?^wN)1%PTvlS_LC$FrG&UM<01Bmac;l_lI|82Qc0 zs1QZ`1x!pg%+2d-5_2}N`ar|S#8b_Mx0U}(v7}7eRR`45^;1JR>-hkAvG>sKT9j- zz(;AQBc4*>q{7vVEdt!TAx`fCgoO~*YS zp9fw=be%f*kTukTe7ag#u9pv4sF@C`V4Lebtjr7_27K5MkMCC z8+)YF!4PJu*g@ibpSEI*mBjON-oi*L zD2Y{$;+=vLQ;PZ9M-Hr1#=_=BDoyAY<1w20fa_Q3UyBe|$cW(iEmVeAAI#!^!U6sg zlVk(YQJdeS&!9|HWSkb`BA!%q#VO8bCOnLC7Mf5bCUG&R9is9soi9}|PwQdb1L!au zZ)Wc+u+(5c3qH870Sm*!If$=~Pt0?OxOq0LSd- zD6K6S?%-rpW)2*o*h3l+5`RG%;DGi#I^m(| z;r5=EI7nHH&gFiF;$jBFgaYXCj9}pGoeIc@cJE9S=rfPgCgADZfTz# zzj*APphgARMuf&r?r&u27;A-+4GcXDsympQB-0v1OK& z>&blbOpb`x`%J*h$z|Yir{kdWC4T&=yS)R~I`i84ZlrMeJ<)Cj?Jj)#ABK-zwm4fq~vIT0B4r`F#FJB>;Ee z{XFjYz4oc%vRur6Bsw$&^ar{vkdKBX?$8oaJVTGF;0IPLh0glm=VDH>3SaqrrbQH^ zkU~yzL%IxAq4yOI&sHnN@mi;H(tQnyRqE0?VN%W0MrOBaMS?W}n!7LYSv0 z=!REoE$%Y@2-M&YlKOzefaV&$KL~Q zMX%LRIeU%il3rYWHWy3P33WSMCEWGs1yiQw-D@$2V-^!ZaE&81K8yl;Y$C@Q94sR@fS%@!4K5pY_4}9 zS(yzq^l3SNUa@=0|A;fKZX~CbPir2O-%3?S2Ot6SY_d;+t1g2K7m?>uP6F-60>A^=5JV$ zbvBzOu0a0hnr5rwRvMw)z# zn3$=5Llp^Z;wMpB!aF;T_y^mbeXxk&c^D|&R}Woi&OV{16q4q*-^HdXfEXc^hdt!* zeE+?KIzV6amk`wj4#?ty!Z53b#kMkfO$63})Hw zon#eXWlqKBg+Azeue)Am%hz1-Fsy3n4wF?3uS;&(AmK6xpJ_eBh7*$^hdb#Le_$wp*kR+%8YI|g@;V^t|(Akl#Y(Z`eIH$jJn=~>jT3*y=RQnBSpi#FTZ}I z>_8zj#QicF_t8YWyz(L7L71hvc>#xim{yRu0y%*WONy}EQg^Yb`k-EszS`g*R_jV0 zy6M?KUfq=3CdNCowk7?iH8X5sHC=D3bkQBIGp%Pa3-0hZQSIGJx`=sV8-IV@_bJforo2K}157u~B# zoe`Xo?DdC!f8q1Qe0)rmALZhaf2NLI!O)L~Dxja>%UEJ$u2bV|%q@pEPS5kaBzjYN zDVqrKHMf!I#aJ{+*P-?!_X56uk}hnN2}*%|LLw*eVp{22+zhCGQq#g`0wh=kcvl0!g*fUpbgiY?Vc*bC&2jyf4eE~Ec`p)O5pSE&jwisRJ)GyWh zTEZSTH@8^c$?zsiMRHL>KTbd4zYXvg8D?^WvXp!3MRdwrf(@XJ=-eZJh`w#IA&C9W zoq>Oj-hR}EzLV@P3ye|??s*hFZ-UwHbdE(SwJxW+71DN{Z1gnf_1pve2dpT5z|1|U zf8`&;!EQ1<#~%Roo3E^IyRDv68a#9NrrZvurOpw&MexQdqnsWCxV6Oz2O3jHI`_ne zcO@~M#nsrJtO!O20Q8N2CgwJx4r1FD!PUWa0aemUG#F?#YR?ccu9;iSCRgXB6Ps=7 zEG&Mb5TFhF{(z4*bbGmK2@N9AUiJD(k(k>;2VJrc!~WCD$d3hp#%d1ue7;EXEqjr!1 ze;G(p)JpIx{>Ro+SPe>J{>|yMP?u@KVR5T&>^T%K z`EY#BM;GY2ji5DOpY|+HB#yDqKFcQg(V{3IiSDa$lJP+nN)yl3&pQrxAgwrPE&vMG zl9LxK{{WVgcpYzlHxe7!6nhKH$yWfgvEPP=pWMb^4;WbbE*HJnMDN-4y>{^At*28+GGqX_?h+M`pN2z@Bbv}!TvSJ|_)lJ+<@3AfO)qYZldf}je$e%=~9SyF*^jC)Xhy;!@tUeN!oShRYva?PrX!vj#N z?kvx>{f59<*nUmq+P#H!oCQa|2Y3ss)^|=;dJK_A1)LW(x0*{s?+f?n>QQ`&Oje7zx+L0w zgwLwXfpl6{^j4gDDOV=^O1U3<#DT_L1mK8&jx}Tei~nTrVPkYY+wBKQ`H@jJVhW)G z`d&1Y1;#;%kK;YIm>`U8BledwhS2$+NDKypx-zWRL%pzsh|0jmDs|YlVWqErHqXL_ zLg0Jz=A-DT~ z`fMxVHgyCvurs1Ey+RU_yv#2FnWfwexq zEtX$T;d?1IGu;K?|BuljPPEUIHR9fHju!M-ZP?tQ^9|#7Gifv&u96Vj( zJqlvM*x?A9r2lw?&2b)&&|x@++SxJHg1uU|)NBTfru+{+j%Zwv?|#QnZ+s|!!{pLy z8HSsB4#U2G(d>QrlK}$~%m{cEU^SzL(;>%f$i%*`#6fq?m3iFpQLr3#;@tdHkZnG} z5zuXf&~;nI=$6Hp`d%G-i;-=PemQgtu=1Mjex%QlExTMZW|{rhMH^7~+X^^wRHOTR(lBAHhx{?2m0 zj`&}(%R}$iK~rQIcJ8+F+4GKP`66Tf5)3RU{r4Wf#3{Y!N6%Hb=C1=Oxy$E~y9zXo zAY4O@0$s|vjO!Amc(@t?6pw7jS=kzw$02y#C!%*meUxUki#{97KO@-N$3d! z+{o&S1im|1IJ#)qz_BV={1#Q`^Oc%zE0rCSyqNWRL+8x!+GlHxrqj|xgW=?^1sJL{ z($qVPC~S>RQt0`yQlh%LBUVZ9=#Brh10_bz8##nPskD*%_ikLi`f=x09X1ABlO3GL zWo0_XgL>(0q+zHAyN=|4abRh6%D16Uo@M*TSRzLzXL%~DbpKTqglT<^$GWE18r|Nw zuI|w_Js` z%q;S;{WlDm3QZ#aKid9(wEh2R`#(3@{*%39ZEl+g{uL2E-~?BHP~K$7FrjrxpfsJt zX(VI+90{DH#U-75|Uh-Fs>!>*Pn;QeXyR>2_~#pLctwy$uA^yNm)W1xW=q z{t&=;|Mf^p#OpQ8y`6e!r1v1f`ZJqqZhuQlt&DbV{Jz?;#c>-l6&fvXaF9E?b+o@T zbUGQ@Z|T{1c85`aZ=UKLK=<5&Pf{A=>)~{2)yaObFjRkYeWK@FsPuvcM1Rw$)DJc^ zda4Q#(g4{Y%k#JW-|yas^X+y}?gXWIYI4s>XO*?QkpHED&3ULGnOcSNb3?(An=kt9 zoemuyU{E0Qq~h#$cmqnVxu|x1<#b*d%j3FWFtLfA|0w`}itNA2$$Y*cM&|QQKGMv# z{4X$$hO!aX5vrxkYyX$;2T~rN)Rp(9Hv4Gy>}E)lXd<$zNc3&|Y68?maolqAG29t| zcTL+%zf{#F2vz1jD?0jW)3#>*gO|9le06EMqjB1uK^bHw&u}U&!34 z+=&CZCT41XWl$3ey5C=aW^d^6$M$<(Bo${^AeLYUvKTR5tlys3yRV6Sae_WJhSXn{07^pH1!ziuZ zfzcLa71??0t{s-zl+~LqJ`c2}L{85aRtYa~nQDdyT1gfJgskQRarR%E2Yw9U!wgXlBI1N2TT-_9kO!@H8wHK1xPD4ax+ z{FX1i$7>X$>3&si8TY!Ajf9=#E;<5!H_;QRgfHq+7k~XDd z%YXgSyi;SNxJ`psm0;+REwdzFT;?=*wtDtMGyl=HgL#i_NA}!+tnVG#o5667(yDc~ zdSAnSqI(S`d)B-i$ePiOqaNwvDl#pD`ws?kMD0Bg*Yly@&H;^9W;gnal`rwMXoDkTIAL_5-DxtjX=C0{BJzLd zMTfYbqqrHOb3=d}V(Tl?sb#=5X7ya%n2P(tBdH39I(8|K;kaDDt8Siztel@+#Wpl^=ExCQ36Bwt?BlLNN@` zeC6>s9*D5M(ML;-V*F&~*JSb>XN$Z510`EUZ}@jKS!waI8@>2@Ps^htVRNOVhT` z{qK1IB{TTQryAJ)#~gB}1EOT;yD&u;O;$EChP$MhP;kM~hgB^xmMgGo=z+Z(d75m6 zuV|&O4F-l{bk!W5rS^g}@4KCrEGe`hyh@?T52q!blVjwwPyMN=1xm(MG_>co7JQQvPbhVHtC z7yq{Jc&X!n^Nx%cEQLZ4R$G?MJ!mbfS}H>eWq!vD#IJ-LG*;wf$PlsNdLs;;Q2FOu z^x&L;Yus|l0tmqR(0l?vG_B~R`OOzp9FKMEwRJjZIAf2!&ZKp;e|WcvXGiTqiSW<@ z9LP+6w-x&O2|S;N$vdClskn*tn02z^T&4tDWPd(igZthYh)G@uItYRP6Lc?i{pf+b zh6nfXXw$G9fyJfm)_z*vn&KD&zMIck?400U7?Jl^j?-d89@4Tl&x($3Nc`Jr= z&s|hj3n;1%SIJzbRU|4Xw%{L6(Z_it-snb5u9GY+Z=@8Ae~4fkpcEb@l3dzorh4aW z2mnq}zPuE6gBQRHsa0iF;E^7$SbE%(pC8uvsX*XZv{fJ=#1cV6i0%D(W{mp8?0oU- zEG=mdk?u7XQ&$c&E~cRNObSZWp8L*=sopd?&S?Kd7RMMaLqhU>F6I>=D{r$2vKo0M z$;fAL)pDKVf7FHdZ9fh0p|!J~{Qb!1FEZ@{-&$vC_wDsL6JI5jlF;~uhzIB2*BFL6 zJOQa1=o$g0>cmPa_ru{LB`yrjLAO0#0VyAXnomSo0y-;|aCUsm*rCrEd`v}aN}&FE znoZ^ZH6i9}56-y~oXA9YpM~y47jV<#SBFL!_ML~{4$UprdT-jggQ<{{ zPdY@gm{0K;T^O1PVvhVFA18R}yr$r6G`^6K1!%tZyZh0ZT;UxS`|OljE@-=DzofTevmQI2y@Kc>BPdgQuH)XSdqIRbd8k?Wm92 zi=pG|f76p^PYogTa~qU+icHCQmheK3PJx<_6Z+ zRew^mokw_?MOit7kxNW|m2)35PpN98LegLjZ4~;&v@Zdl)(H4&QNe79jwkv=7zP+e zL8qN$&@c4|?Px^{b4=heQPxN7@xQd$+Y?11e1@PiJ|!9dbkTA|t6(=^<6^-V^`oA^g>z%l3=Hu5J}mHG+2 z$|tDZ>QUSpTrjG@HOh0@MMfM}MCnhb;b;Bm*5Vxyin2@_7RTVK79g zf1N1NIS-r-OmK!|gO>J$Nug&2qt0e@(Y}p5^w;3$*k;&>Py;+FyWDt9jTZih%&) zS^%$Jse5~GHR3`u?nb6QtwVfp!3d%te_(^& zh6Vz#>xD}SZh9)hrp~0(tZUh^>>e;tx7OHnK@Q#RS&m;iU^~;y>afIEHuec!xL%*k z)}AV%r|#Xh$`16x!eN}p^SA2e_+k8k8yMI`*;xZ+6GUrHtyLnkUwPULe=U&GZl|>T@_W|(5@*nrNdu@|fc}>~nW#%)@TRbM29=ke{K`V2kjtV zc_8*L`Trv33zowT``g-Kx043%!nx@+Hi0r%|Vw8$-j~kor31Z*z zV1o!{dKP2nmSjR1)Y2W?Jziue$!ZFw-!|$i>MD^5z_Krwe0!!*Ulk5nn72U&bPQED z4ZpzOKv=SfNhcY$0SX4Te>n{tfxLZ+oBX->x)358IGhm~+TWHK^ubGI&<86p=&RX6 z)cC{w>xa$$h~{qmGaAj7#XPqwEiX~jbK-gbeLnMa@|~}a(ew9?Pty|z^&X*KPsXi;0St(_0A4NBk zO|aAwS9y_&vKHv=owz=cmet-Te$k#fZY-fNqmD^1IVXoTelpWinC27Q%+|`08khyZ z6apmU&#GeE!x*n!?}5HTfFRsIh(wyULi<1zphSv^MrEQGf7r-nlY`+Y)R^f&Fo!k_ z{*EKBaq${Am#q)*11cV{&BE&Y_;E1@F@YEkQzi_bFC~_R2YWV-<&i&Xo7CoN--^Nk zlQ%uJ@^%ewzi&LNZyxOJ9e?-q#esUv%f-RaY|7$*u3z}i!2aTkv`(WMYxamErk=&)n9@# z61A#Kud!xgoMd|{nWtm5+aY?$7kTzhMc)uQPTqQ8Y7b1yQsVXl5`|nV0|NPGd{wbG zDxLsAngm^yNkXWt2|ff$P1ZflPx%6*VysDIa)1S3f0VeW>sa?4gQOH`{3WrDGCeZV zOrFE?l8p?3*{IalowOWNpDtyoG|XzhX)%yqsuT!A4~M~;sK)iWMF2&QJd0WtHx0TA zyRlXCc`#iK-5?}SM5J_I7x`iid;@=eD+;YQq+VRLXnWh=-TbxA5$>5cMy8TAyDLn#M=w%s8dIXqFA(E;3=q!YFV+o1~6rwDNCBNpgDa^Yt3ol{=ctIzUrWb91X(eZ| zlR7A=zWnFjci-K6_)td2$A@QU(p$+73iX+cf4#jygm)alk<3D1oGewti<8+@x4W>- zLeSdQU`a4)O%W~MY2M}tpvI~X-oO9pz4t%7_x`8*5i-r&{q(1Mls31nlHU61BH5ox z@x@IYeSjT3Os6VG4_l3}O_D;=4ATeRR8sbcDkA@M)X5$=D4kBYhCUk~ot&Q0(an=5 zf8*2Rubz#MPoA79MeVKvyrit}`QD!QVeQ`DFN5o-m?9gU>$y_`^?jn?J*qzr2tTvy#5Z8go|0u5+3`zoj=`DPMdNtBrE{-}0Kh z2+;U}F230rn&b@yGKl>&|5jEHB`34>jMg)Y7v zuAHf%o$0ZiUFVhj+x-W&ie}T8qT3nmGhLh=os4nNM=7-L1~@v#_E=OUhdbmu+K&&v z9i2RSgfNfgSVD+;eE8%cNdk`YLn@}GrEQc6`{`=Bukg^<>=XWhfgjZ9UecH zEG6~IuF^~(Ll;ZPl>GSm_{oSE`s}F-dcCNo`3tF^U7$zL#t%<^AO)qF%?L_4;u7wl zO>-U}{xmw3D8}-?FtKx>AB*I75d!^qbol+K4*ej%k!~DemqJUSe)paHe>qf6D2JuJ zQNVU5bTXDQMSl69N&~w=Ndv93)^XSRU4Qqk@!C_7=yggo6|}`n2l+4`LyK=(Q^0d~ zU4zRSceV)_RP7aiBZ($k{)Coj)w6jUl@FsqeM-HH^YwlC3jXkMmd$A6rTA!$ju`3B zFr5D=q<$o#Wq(t-{d9^le@UkjIF+y9XiI&y<-$Fjl(9@+V&Va{)-GfRF!J|^*!RvJ zjGl~6f7&JN)JF1L#?U&3zoNXbkI!(4LEL!T{qa}aK!$2pV;*0>b<>s?rjVI|XRwY*Dch52e16VEaMf6i|D>EOo;1P0O# z_+MJLQa{5yCb;bJl(}!qfyKV;EwxGiI z?Hs3GaD4*^9ilq9?SOKAQ@5WjYl=&IA3#rN%U4+*@YezW338dwA6|hPI7a zW9htWtODmx{9;%FTs5T`xgF^jC9K1=;F&!Dx^vUO)HNul5E{MeKuwq1cVTe}N( z!V=$7x&s_AHSF}hvZnyDndb$#KAsnwbI(io!@RD80=ndAfBJY_iL8Z~Aa6@eyIn1I z(@roV<&5q2v$9dQpQTQ=?PDb=VLq0~33RX$YlwqIoUN=6@~$ol7Z2@UA`JaW8_HB6 z)J~s$O~Z!#2@t`bS`(r)9G$VE)zx>S@u5yAMdxklqhCyuC^~_o;_~eVYG$mGSyyk{S0K<|(a z>(w>n*;v0Wwt!ACrPkEzz1D6qTgUqK5C(LtGjqL0tkZt1_d3=90!)gj7X>DP#YkXy z189KcUq9wUUXgcg(S^v|Bp&;z8|5|I`zOS~RJeJ}e}05GnNm(m6Z#Nx+~jCKzvHG~ z897bpLtK%`xc2l3Hly1~$|%S|xGCITlsGwJMoJU%09yR4a4%}S#B;apVg~^fVY0aw zA(+Ny1i1J@yr-A4?$y7W6lY{Kq7NX)%Y*k5jF)O>1T}ULA{alt-ZRR0Df~{FsEd%0 zQ_I`;f1Kacb_W<;ExiQIeAu2qzo_JFJ%o^k-Si}lHXK&r80~3v5=9+^P^32_R)`hu zRtcx5-a&A*&;Y%|g|q+CO^p*UXb*<%!j$bB`Y|cS+|x6!K_^f`-6e`LH*lm zjKs!dI(45Dwy3#!HYX23fnDlIBULG}^qTjwe?Pm_I#+FfiQ$!By~u)4pTuIXW^fr* zoZa?jbyN(mfHii^YF4Y|@DIR-w;i3*BXQgM$1A7>o}Tb8T65*hcqIp5*)();u#0Qj zIOPI)wP>=uaztDjDEAgJNvF96{D;xux2n{CmAl^ov=m6jOJ78id24aJ^`35{^Xo55 ze^>*d3wSk?cMx16*Hpx!bHa%}(Td;eC2-8d++1LJB+4Ou(@lp}$Rp~?14c>x=dxRi z0P041ZwI>_96$G(lK_wbL9MAuFP#&j&4eZweT|p+K{C)drzLzS~W(`*% z-3AzOt6_pm#@6^9ev(b>ok;N7-eBsTe~63wJBT3LrvVNe`i90~GV^yDwYq}(eTEjs zQ{Xj}yr(>=6kCW?olleF014|1qfUOa#493tZFS;Nj&s?88Q<`AQW6I&I_Rr15&?ng zFCES`5Bh5p=JxxjlS=t>eIit^wL;<81~kMsSfz0M=fs5;rFtOeC@|6Zh8f z*89e3vh0;eFe{fumiijydz#gqe~cycB=bmZ@GeX#HAr4)9t?bz4E^DI-lpE6zc%&p z#NdUYfu~F|CV)cOA4Z|-;F5i=V%57Q{c{jOf6It(o8|gf(~3&ndN#wN{aVSgNM{dz zG+*-4K}q?yF5&_dN6=brdWN`@tt;Jzqg^yn8x?W%gHOmc?D=Qq&^$6#fBS0Rjq37s z*iZ9?wA%6XQ0T8gSm|WYt^n$9UM*HXw~m%at*-Sxg(N`zO?v?z_FqF?hOiv!i_4pgPdG4_=He;{1O>Zg6!XJXlU zlwxZ_43-lGKBU$=W=&r;k;xJJ{Vg{To-N53zmC$^b2c;hfVo>;EjZ&^6=jv~)>0j# zI8{C;ouMtgd>w_^0N>U2NH1ekFq>ixykoHz4th4mT6*<=tX)0U0{>pKSPLY3v35gF z8V(Hiw7EObh4suDe|Ys$gDEhz-Rz?~6I4qyeI>+sXGY#T%Doan)u_;VA$0|<*?{eB zGo-?Z0zxVarw>IUeC$Y z@PXPYTzpAJ6)q^`#>;k;z}-GF-MZA?Xora^@fv$gN9t=YLRQtF4H@M&U1%SpV@?#l zAZP0ldcOX8q~ZM7-J9MW8k}U>mRBL0UV{L|TiHF6#Rqy?+MDgGr668cH!VfI0SDb* zVb*ialHptRf68(^LH?L|WK~XtIc59mf4|(*xD?-3a+NR5H7laGkReh9Y5G~)o9&*J zFjjGzuYiZv0$C z+P5;lu9Y6?XOVBVFZZRhikE!_TzXCZ-OAPN+}Bd6fAyTaFOqHg`W5)bJiaMc^laWI z_3T$L>c`&yE!MDn6&rem43?xHqIaS8x{5w+#9T=pcVVsJOsg3yR3QzwP$)~y0N4ZZ?Cx3?bEeeD{aVLcBEOy8zs z4d)DVf2p#_a-Uhx(Q!}4{a5YiD1C_@_2@WI+}jiD8|_4o(v{rWE8s(eG^g8v_STp6 z{O3`+RreXdlqT2d)o<(>f3>R6H(^!C+B>kSevETViFMZpF_%Ch zF}#*rPNG+|)@oZ6MPArS%o=hKiK4hEuXJOe?Dsg$F7rXOgD_DljrYOP=;5=+qf-O7 zzZ1o=1FP<@pNd}>SJB^CFU=Ig@UT*oxGIu~_%)e4*R`XGC7fZ@g~KxFhfGlp#;xQA zf4_NUw>X{J+Oz=g-L!9RUc)BTxgxp!F8(^~{*^Fq2gt`&?(+wgF@)AAP_s$ib$eU7 zdz(F$wBjpCCkJhXqsI)wUKrTY6TWNhN&>;4_aCL%^s8H>s(Ba&L1%Fx`bU}!KA$8R z1cF3fhJOr2ETix85^6|47qeRll$xSne}I85&x&JoN-gvfqHcF8&6a#MBsB_e>GP` z7X-d`&PY*3)Cbt;Wil&8w0ViICbe}`MoN6uu%qr#er<(KwyQJvR#|;hbG{HXWo+kJ=kh-v)Dka;r z!qMX3dPkWh}zY$&%+e}h9g>&QM23FWd`Ybdu(0z$b)8n}dYboayB}@_U z^%OCS-c9gBKN!A35?dC!sl2u)7JBgmy*O~&H8vr7yhqKn+`H$tv6MFfe?=fSSXi-Kb#+OY{UrABQ!PT+AMRpkPK8E7s_nRO`@@+fZeSrH|8i~!!jqu_s zPMvpi)v$d+$Q7;D`(J(v?=mmg4F%q?hwfNv$V`cNYvs+!s`l%gENp~H_~*&3Oht<5 zDz9wXnd-dDLB-LN{Dow3e?G^TXD6rQ(L<-pv%ILp^!yjPl*=T<3o#LR+k_@)2<*q# zBAHyE+^WY6bDxjB4lI4ejiq+WVW%Sk$sf$C&GP zi7cJsOON^oP#eTn5>A5_7 z``>vW`3~C+oW;0y*>Sf#nrO`JR$DWNx zA7a?XBG7sZV-nXkB!uTkr#q7ubTni1G6 zze*?0;&di{Ph`U~UlbE@FOttGD?}vA-}YLwC|VvXf3Z7Y0iRnpnpKO{+8b3497Ohc zUlZMOw5&(W&Y;N1ZFR#**6xNqJ>tsK?QT4Q{n13lFlZ#?fFuLq1|v(cXQ8 z-A0t%N~EnZa8nU?-Ke`s>tKW2fR+5q-mx|}P6Iz5GyDhB8=winTw7jq&>70R14kKJ zV0hlRf3eaU(zw_Ty}UE!$Q|CeigOC$W|1g0}wQr&uL^Q>ipUW$mKBz&+8hD2HDgKrmLYh;=aKbDP;f8_7$R>H>ljA+XXR{E-ekSY9|ah7(i zSsv=gUx_RGnR!VF%;oXn>|992S08G{X9O4xzG%FVDStM^dinBDFj0jHp(Xi zf9*knyTu?ZlW|y<3it)|LAdoy8awn#MS_0iDyz<;3rU3<{enIpD<@T`amJFOQc@_{ zm6y>Kt_+KOuA?}jsuUGsp<-OPyrqZ=ETD$5FGlt;fa0?#Ig{*)qaX75tJ5j2A`*Kx z6>3KFQpn|}xJahRDf(14G-xT8Bb+55e}v~S1dSEXKMbR z^>8TvUcwY`jgVL`06!8&6Ew4v}w0X;0xK4VfXqV3?=Fp@S%jhVtv^U@s4`@yX%w@!`=| z*N>woh`WpoYW-S9K9x3=l_ds_e^RmHMnoSS9ejCs5`b!~YX_R~;C8ni2R4_@U8%ab zy)U(wvGO+r)>PJUUo=CrHq#G@juLgIRqo`d+)IG3hmQ(QAgpOng9tAWZ6$P9R4~KNpyo*W-ZKox?)KSR>?4Vtz^=#H_PQV{s<;CyO zb}79|K6|wSe?h));8^?4AVS9A!W`)#z$;nR+V=Wi3}`|t{9`6RpUFv17vLsgB+to% zHOSe1b#x*pN{8F?(##P&e_)|Cu#7dvlALJ??s>tdVVQ;Nmz4JWa*B)kBootA+g+iC zI;HaiK(3`hTr#*SW7Oo@9frm`1F3SPZot%Q>NJW}uqPW$q<4*7Uhn;nVI&_&&68pg zQXRXLP*g#=4L_1`6c5$To6)YBk+z&0n;D;>Kk(`^)d*UrJc4j6e=kyUxTUDHQ)mq= zheM!nCZ~^Tpb26TrCGZn00k9mR_5Ulu@Y{{b3~!svqpt&;nt;kk+KqD)`KM~CaaoU zIuYS2v!aAa-Sw{<5WST=;pJoN=~%%pom8wi`h((sl9HRZ&bvOvSZ+|RB}{dmr)rJ1 z`=(M3;Q~{Jg>s#=e|afU)HeAdA7fBA7O@xNW+ej{X)S^7GacCttT#7fD6YB+iY^>l?&(?Mm1fHxC7tNm|2fLgIZiz|zJwfv@3Q8MHVl#-jKN-MM%s z#`8+pO;420;c^e%Q30%^PnzgndZjzvBmL4O`+COx=AQCxe-tK$dAC&@TrIFHV}}IS zvk@J);M=%zy99(eI6Ivlibw+lyEX#9C(g~zjjP=rxHrz}JK*FxRiU4!vs!Vx9*)TE z?c=E1J@axzbPqrGCiywqk#_sI-u?~m+Ad>z8ca`2o->;zQqq4)en{nz9LNjZk_su1 zd?C`sf7^23uuIf)<^$i9D@5dBT8g0eK{Jt1ri~!d-3F09XV<4i-bvUUb$Li? zKX->+?vtxSf=hO*XHHf#+YUUw?;CL9h&OSjEK8oZ)SCy4}DYq zjIf94A;I1U1tdzD=7C^$n*{rueVP$`H)(g&s}ZgJf7}}NxlewLC=PXOaD4MoKC6oC z6xmrmj$Stn2Xo1sLO%L*nw_0j2$xTz$I-{4%+6-fE78|QekSwq7m_B6^8cS7BM{B|tf z(}Kr(z6fhy6n=nXhSoAb59|d^vCl%c!Lm{R)iyFB{Raq$|7*GE*1tl~^>|#dcr#Zp z4x?M~)WlvDh=f7`l`8(J>!b0A8i|LwQ#mc1e^t_t$Z-N@U9&{Y=IsZ?*&I&n-S#@g zwvB6V#nqS7->$}^CR#^L1gg98!jvz_fa+K2#?5SNcy|6MQF*KMHXF(G-Lx=O_nQONS3$RGt_9g*dgW7Nl`K{t*(&Lh&(?#6F+rKd zf3V{`$GZg5#o2zTwb3Z#UIl-FG4z0O{E6^%jBX2MOp4-cmKP$`l|om^ ztTJQJ=`Z^JN`fTRaw!bbD4#&d65SRVZDR-!eY*3Aj{OGgc2G8RX4AU0&#JT;BlWJt z{1oaD8=ehL8Y)SH&N zLBpUvdOI>-@8w|i_1t?YfHnT)q5dV>$FUrr6LOzC#f~@-M4>7sEd<nWC#tn+t(kp%o*TvjiITI&i*O(ZLsl-mPe<1ABrk)Ykf^TUrv_-FkF0mO#)e+0PU zmKv>g6GjIki5*I)eOcR=b8^M+;#Dw@prM(+%q(axb6ANBhEHoZvt37c-HL&a1ni_FbuSkW6Def5d$a3=kMggVZni}|$5qn4;`@_>b|6T2kXx~Fhrh=yJqf5XCyP|=I6uOpj z`0=@>MT*#5GiBX*R!ZvO$OTXfg+d|H_lrUQo#xBCx3DqV*#Z?|opzUY+Qm+GygH|^ z^7&M6g%ziPznq(t95w+a?isg^U_#}VyZpI%MstDTHxko+O+Co7e_CmkSt{ahK9TY9 z78R5uijvcB2eFoVESH?)Dyd||7}1~PzzhXG7L2byIX*u4j2yg$8$~;BMT5sDxg>de zV4+DqkDrJV^wk{%d(pL0}C3vSl_jjh2+-=7vOMbZ%6Um zAD;$CdyuBhIQ8Gwf73;#X3)#Nu*)aVM-Qn7&_Gf}W#l-C^@Nu*eF=W-M<}*N4!)72 z&z6jQqP+~9_5A*l?6Re_45BTVyy3)Fke*4|tU@+)BbpG))`&@AFLT@Qr`^Lw3L5ouGCDhhii7WnB5!;=G00VQ{Q%cmoU1`J!$v8l&)u51~JN>bKHkA>4x5JG3mgoOG?dI~d#L@0dErSb+lkP>@_QnFH z+fMB=&rkhA!>9Y2`X$v|DfvrXZzTQOEsR@Sio=&-%rwG7o3|nJG#9yKCEWl}!F_t; zbGHuN-o{TU994a8Tn|#Ku3AVX=aNg26zFp`q*ONNf6W_JCtFrYQCa_coWtE^#lqcJ zQ+KuNPK~#OD-M2_zssB){Uw!~sv8}X@i`n8b3vE$5DprPLdN96+&yxYjfE~H zrVn{dv%eCMC*h0@<3z)q4RpY2y(}Q3L9_{jW>QK z02cs}f2^H^nVYo#M5?5MzzaV0w!>7UXZgERJ+T&4S7MmLzg zrx7-hnFmAbmrTdrM1UPgce8D@ZEzmIb%^k=e>offli%}~?(Dqt0q5^AQY>QX|BwvS z89FGTzsY&s>N!B2bwHi0cFr28;{zZt=Bu!MgrG5!I7ZQT4}fca=PQi}!LW>QAx zR!By+u0;i(T2lPtb_EaR8Rubn0I%7q?+V(EzEY_JLM}2F7e7d}=vqX^aPZTPNKb3z ze^638?-;o#RveZrIbdW03>w@>U;P+KHzr-*Bls^G+_GZq{yP<8_qi#??oBF&LtI;? z%R;d9mfO8M3x8*r<+!KXQH3A5NNe|I}G zKsZMy`_rmwqvnD5Vb_Czbf7vb>OX$7MdGlHa?n6WW#p&zN zg{=4LaQBZ}Lk?GQE`52#lD8JVZ^pLqO!b~S*=*9{K(S}5xAPY)Z$2Br$MeuZY4aXV zi%EHv963*o6Jz=DGYO>g>=~hge-EB(;dju1GeNeWHgllvu)HZdjQn0LHLNY0lXAzx zc@vq2g!DIdrmU2(?P3(ZGBf~n?;k?WW0&nL*4|z`xWV0p;zGbKWBDO5$fHdTNY1}u zw{eeGyTIUb-7Q7;DgD2Cvk|`CP=D`F0dcu+5keHY6L=DGzMh^Ox{{H-a& z?!QeLLh3SZKp6syGS*NAOJ7Sy*aEObu+zCa|641|prC<-V+t;1Ww}IhXx{wIJ$Y#Qe4X+aQ?boT~eY&8yo5{|f7b_F5$&Eo@Umil2+ida~Sw8_ff8S2I;uc5%@;*i1nOY8g zdpqJI`e`Es<3%34fsnXU@F0lq$bR0F5(D+vPUo1rbH`oTV}SA!vFMbw%|=@t?-}Xz zY2eFE^=bw-(NXtX;V@k9Twve20{#hg>lB{YzNe~24-mmED@poj12Ht&9| zg7<3pO>wK1ndKa+bLMZR-KZcS&E{4Vf3Ij8fKi?W#u=JLYnqRxYu!Ag^_I9#+X@k{YJ!QHi*P&r^o zFkjX<%Ey`H+H4HxE7&>{Nr8Q4P=hM50Y5Ht`BU!U0{V{ybZmx=V=DkUohGo-SJFtz zSLz*2b0(ebh&Gm&Vw_E~u~Q-&rDBrJJc&VQe@{%04BNSuKSueR-!!t1?KCNs3!Y9% zmpqk-8`3(6n?&hMB9S@>WJ?U+&a&zucE%BxPlc8uHH>`_GLED6 ze?cRCee~rIpB;T=#4MpcMoUK#U&HhsR-H{7O8j%uDvC=_S$hd zliwRe2QHH@i~KY>&8Atkh$aafIpJ!nZ+|TX3a^k7%`dXQ1nTZo0AHZJx)NeWe+%R? zJ{*hc8;#*>J0MO&wgI_j2=r1OS@}fUC#ew;Akq;mhhznRCeyhv^gKcZ+=;g3-}a+N zFOTHk_vKmLqnD#Wn<}*v_}Ll+UP^=57=ZM+GnL7JsW#f<1pf2~L61F+u-(4MlxVnONa7At{d>C=f^Sj&gl9*y(7 zNV8c|`G(vfJcIjVwAUz6^XK?|Ghvf^%Qq4V#(=@)AM>Q>a!Ma6=wkr(g_X}CcDcyQ zs)lo^!+KkjLskhaFMb#4<7k=5UF5$D<16r2*%Vz$ox!`Mrg_oA-cP4Ve_=7fuRu&q ztC94*Je*|9yzhX)w^XQpby6$+H03BSmHJe zpJ@!t{yCC5d-hz?jZpg#e-DQ5lFq$O=p@2iSU9r14rF^CGPCN@2e7eJEty8=0zTVp zw=OjXg1-jsqMeq%E?CI#XlLnYy?Bw+)!DEf365vi+J}Iq>-u+mSmd%3r zM##Q~E;Wo27#~0U`sh%;Rq@>?AAvOV2e~1sFJ`dm+|<~6OUK^pe;9kOX>9+#a4lO> z0u5e9vUMtvP;0x(-Sa#P_qE*XnBF(B5nRFR{*7*zk-5I-t#Rtj0o$Uk#xA~_O;_2? zuE44f{=RuOy*1!_>=pU6iS0OI8_x?o37m=~HQ6CbZEM6W*`qtn>bD0?Nd}BNRtlP2 z1ipUWXkOwDaQ7CIe;IGbJI`yeb>+LEC6}Y*3opekYc0KM_(>%doqswouh- zt?x8iD>qaQ(`B&#En6ktddwXT557K(KKlINtIs}(K0iD@Ir`>@;m`o`e>`5&t1i<8 zP&7Wr>k_8-!}>@C#_z z`dvytIRJ#GsYr)IJZ7OYAn{C1bVI8C3nbVcHqxN zAt*$1rt1*4`)KE#*B|Y^zWeTDRI{I{)L}Su6PDY9;Sj84Dsh)C*we!jt;+>{8b=|h z_rg){1)wHne+!_S0Q*u-y#i<_z`oQg+YTUKLH4CzbqkmPAArxmA7!ULik359B5=S2 ztRP+o&t4!N5=JuyX*V-bj8qZQMM^qKk4*^W6}W@5@G8QEj~$S zz{hw@#{qSq>MI|OejZy*H0@L&<<*zDbwL5AuFF_OSPidEnvgWI!njU5r(O7AN%d!h zH2V~16X$He9-Q`|-hO(^OZ?KQ?unD6*0nt~f03W&`BWsbwV0c?&p1IHobUn%8rowD zJ@Q#INv5S{31_GT_ywFh#4$62zkF4nsuqqX-G4pTfX!fyB z{A?60^xyxAu6=$I5|5#)pKXSi%PgC=ngxvDn}u%)KIwP{>=M4>!D#^1%uY}t@Gq7h ze`V{uDDI-!`SdR%pQi!Vm;tWxHFPu!eAsg;*r#(o>e#q`R@|R>P@3q3V``<+3mD8j z6NC*+V+SH6X%_S+tkCou|BSAe%n30!ogeG8Zj}|HbY~W{JR(}OT)ryU(fb^Pce}c8 zt8AKK+5-MoK0JPM^u;G19esK94UO{=e;`A)m++I2H?th!k$ktSI~BjdpVk^hG@*0d zVC>v5{h)n~!;F?z6U(p|n!!sZ?kovy)~NdF<|vJ+w{)+{vI z0Gn*vi6$c}4W1Q9qB^&@(L=iRPa7GdT|~5L=EAY@$u}PQ{YWNE&tn>sK(k2=f9C!D zMuFPbBYcIY_oBKkl>Ng z>?a?|e;q^Q0g??-Ff78LsE^Fm((*N(0&VA)R3 zFEyyQ)1qg1e};WV96_XJ??Rn4ihcDL)O`9YEZloXDlJIot^0D-A@{7ea2{^d0yVVsxS zs*6NDG?OMBfoa>`3*CF6dzaZPptnoziPRUJex!eX+QMb4SZw_QxyEmg54J{;*PK-1 zEZ6Oy=0%lFGP3w`Yve=De?Cfzvm78Po!~W>=gFmb3Qmkreg1m2D8$nbpML+Tj|Ub3 z0{L(%Z}fb60$?r>i0h}V`7EmrQv@XDza~Q=`HzPS$<_??TAn#A&%72xB6-M|jwH9E zt(VK!k%13iew1#Fh~4(ROhv`O!*wna*e@YEP1&_19{@~1v%eNnX=xGyi+@qJe$g*A z+HbTcbpKU0KEe~~?MMkK&eB+lX+**tm{qwsY+ZMULw3BeJOYY`Z%F+cqN{_y zvWvNl3-q5Bt$sx4>F*SA5r2&<+LU<hgy+tkPm>KoVJ4y=W_UfI&3=zm+=xUHdx zwwojsV06;tf-3o!+ZthR-aoW$9Gdmiav&rPj;$xCs7~n4GW<@PJMr$9AVNIM+MA|L zaP8P|O^+PNz4~rv16=L*Rz-isV*A+;202dX!7{Eqs81ex!PMEF4}V)8xHIqbW=e0( z^0)Fcab>0-s97O>TXc(9hog7t75QPlVK%)uYgt0mv<0e_dB4Ubdqp-I`}>>BPckC~Wo%gg-dBHt zh4)eC^6dfa!IB@DRezDrSdlI9nWzZuwvPg`Gq0%;N6}w)NxWtX0t0Z}pvP7()~*Q% zQHfiv3Lt{Q>jK1d8@H${UE3yIA00PTB#nQGe9cxvMryR95M3YMx}X z>_Sc`RsIzV&)eSUfjWR>K1JR>YTS(F^QwkFdDeoTKt*~cTF0pRMrpSQX$^%rFRH!I() z0;_Vu_zYOhD?qE;VM|3g`R8D3^fDr3fVB<$rv=$OA*)wL`PboH!d9LwpxXzjm)$jpqR0z*mWPkw zs$tU;Z+{;)Uk0SNZsRS$*HQ)(RBPW}pFoi|{*Eto{&nF!fv9^#zm>LyR_rS@@NCJ! z$mZ`RK$W%#o!T37Ex18|6vAJEwV#OOCScl(9YhhggDSV*KeO1f8F}`WdK=9FIBo8z zv_X8`*0Z$ErkzPBDWbx?v*4zyZtR?_p_+!8Mt>Vxbp=e|`zXvtI7YkKAl%#({toMP z%}j-K%Jea9UnZrFwm80S!!n7*_WCr>n!5F=ch(y35>2Cg#s<^SEG}TPY+n33azrs_ zSbL&~Gn+s>ziS`z+C(3N=bp`u%np3r@UlbH9O?$_!`Bg6H+C)?8!-h)$jZldshy~H z3xCa7N`1lUzU|x(+RG1NCm%T!_S?@lopII>_S@RGBJ}miqUV9B)41AM14yA;x$fmT z1I(d*$`E$?({{FS|IQa)XLlWi1YUTRhhG+){3(j$Ov}x1AFqzCRJh>fkrBtJBXC2C z{K$=e&;{g!e+v)Zs3VWwY-7Lh4ZDQKZ-4$(C>@a;q25p^*T^-Hzme(fMs#*=j&0f& z7J+G!HyhL;%KlLub_)vYl(yMkn+n-TW&ujGcDzO-8PK>rK*fJYb+%Tb5;6mgSd3)oqu(YZzQjx7VUr-p6KI@58B+^ns%x$}O zwVgYih(4z({OsYP4kB%p89XZ!w`5veTG5mjVwVm@L~ii2?kxP%+r2r~nbv@KuRMaZ{D$*zx#Zt8{wFMskMZCROv z>ea)5NzIT;3Av2ADf`iMQQg=c^SuRtVVF@6J-7j`dpn&L*YSe5L9ZiC;uWgq!c_xns;quW3bJ1e0igz)1h{bSc>g@vG~ zQF-!yE#Jv1Po?=N`Ds*_yMJ23Pd@^W<PJiW0Wlgkyobq4CVa2q%v@zqUi;ZyQM|u8^WekOJH+B3q@<- zx|t(n&$8l8CTG}y30f>#*M#YNfvNeyUF4IyK9BA?pWXF%co+Hfu7CEb?t7ahpVfVb zS)0GB8w_$SXm5kLY1s@LGPW0nwQA&(yVgf{b_lNh@DBb7t{&1j&&?G`_zBMvJ7pFMD&x%DkR^W^tLMs8|F>>P! z?}fxJj{(t)Guc;bWDlKpGR81Cdeo{P9w-8L}&0ou_H&0ci5sYu%Ji95% z`Q?|zTxZHDv)Abp--os6+UwJp3yQ!f^N?ad+eY+eFL~re8b;w*s1$mLT zs*^j8OtA@@AM|ANg9tXSb1k@q%?}`N#g*UIcO%yC!5G(<~k|==YAOcN6PXvYnvD$E+RDo?oAn^`T zh=KdaMNhoJ!+bMuorPP)t?SD&M(m{X{_xAKrP5zhqvo?|KKQopI~ znry=psJUa8aFrUaYYkPaKm!N2Gpv(Cx3C*#8DV1Q%tfRV;Bf@@Of7Kd|E8# zTK)F9!PPtb!hQ z?=;Bf(BjwkmE(A8rlWJzEoEV#=jqDX)l4> zhJR<~gT&K)9e#dt{3T}|oSZ)T>Xg49eSYx$@#)#8+Dth<3KI#2>i7yaM{Y+nJ;LoR z2)g>kS6^w;vWZrbxgKK13+$r2Eg*87Z2-{kfK*8x_O*RxU`EyP7rM^#zy zSLnJluy_F;UG1A(SYM%+l^#si(BO3_{GjoxVNf zHszYdndVW2d2clTgL&T*HKv3Lh6>hiu3l#Oln!PIv!+MQZ;FWvEGToRKYq=oKYxe( zuGD;loeo_DqC|VOilVG}Oa#azvvJlSDI@OPmIdtbV%1?FGD$>My4!8jCAWjCVajAg zbJy|uOzKz@3G(_($CuWGR~}`q5xNY$70TzhM^H6Yldh@(2SP^`II=gW7LbP?5Z033?oVV?@yV?`aA5lM;J zLd$lkFH*oFSQ~`%vY6V*kub^-QZ8e4*d~B#joM^0$s|2jPjrXPRb^ol)aUs53?UNI z>p==}uljIlkjjjcBTmNu)4axsXXgj!Cx>1F4ha4>6PHY&>5Y@AXBZBqMSp0Ym-1!y zlq?yWgoZfUNUW|8?O|%N4`mE9^nwzYcB&@8FHCvI95yc$B{d>5hv6Zv9nI<)o+l%d zQwx=9l6C9!dshp1qo(<|xSgxA0s52U&rc4&dUSq#>foQMn|wTNLJe&F@(ABBIOyso zzij|L`{d*i!{Bjp6V&T%?)cp9V9^{Y8yeKie#oM*bu1_@mQw2a8 z*YP(nI@1sHa(-jh2$z`k)eO_HYAc0>tpY3u#e$J9A1#v!x-erj zZ5mcU8)Oa;MBJdqRCBCBy@qq%g$4eR+s&Z0P2Yn_SL+jLb$_&TL$={yLQMeW610Nf zO!~BYP(QjO9KXG`hjo+jot*s^6(mV3;)t@sKUy2Eke_fBL(P?CDQdugd+UgpWGdAn z`Ra6VYr{q0GAjWz&O-7iZA(%>$dF$8DJ6k;Y%H}41`LIv6^a^28DZZNM9{mf{YJ1| zJx5UM_nqX1?0-Mm#IO&g5~C7Lly(v-^%xaw)nt^tOI3Q>j1pS)nQ6GeE_E8E8?6?0 zHD%Q_n_r+wuFXsk@Wdu=NwN(#>`e2!O}6b?;etVhbhpA$=bJ1~z!@e^!VPeTh+DAr zWxRtgAmz55`$Sr>UO zBYN3#h*qf~gUHHSgFjTTl|7WbOI3Qzp@df6P_co*E;&Q#0b|VC`+~CSY~cdZ%86jg z=88bHNaz6B2r>NIK=uv!Jn+MM5t~biXo-*!LA{U=dR(-lu@SKkcSelRT^?w|wNHgu zlBjGTNJ%2>-zHp*wj?cQLk7}fZA&vQA>(Dy$e<6|=_R(ak7$JrSlk~1MhdKwr)F>P zf&dIx3AJ3biIQ$tu~weH{3%>c;6!uR{7u-_pMSm$lhkfb1gbZp)3NLzL=l|Bhgm7!0})OTLKwjZnaf<^Gkmb}!|ul2y4EvD1_YDt||Do9sN==j zI1q-F8J`QnHH#d9#y4vGb22WAssigx!QO90hmyZ#ngG>kmg%a_u%5a_Gr~o=2!Dmc zi8eWP`^G6La?i3z4fwt(7e-dTzgB+S*2`+1>Bev>jN%s4w2RDDflz{ z1|oUE>%g@wufRMqct9b@GFMqSKI)^sm>8`NdWe1ANy{?Zh0+sbE!Xt>yDccrn zPr6xgRw85A2pFa5ih29`X!J18h@zidA9zk&a{qkfhs#mTHzi zLO`kO-B$=I)x7&n)%RdMYl(d+e7!K4uR5ZBNUkC2WKzdr(|h59WeqY0UVjCK=9Q@>ZZO7F4&AiXx^2HlFdj$14g$Jeh|AgH3GG9id+VIrEHA5p94ap6Hs7b>32fC| zCm_8C`$FQ8&I?h5CZ=8`Fn`17Mv>bOll=@a^+eLCt9zoVYwQiEYFF9)khQ4Co=BSY zV}CTQ3KFe}ph)#|KAoa5F=$BNhrx^c+BWA*}_mweFpfLPxD_1KPMEcsX1t0D(5__OV=95cCqshVN)qgPrYpO+uwJc|YoJ(9= z?{aluVpjt=WrT(7)>!LdUqH_`&Sx1*%t^6@$C2WG*8PKY`$g{_S~Op}Ua>o^cR$F2 z>(||54Z)j68D_K&GR(!{6sS}qudr>aT87X&j*QlUDKCY$ zk48G^kd;#RhOy`#Cc1LhWU5+Vbc4IoiCx^lh6dodG_5$v0!fO}XqtN)=iVa9avsWgC1 za%r+!MbhM>P)3&}dE~-hs>Qs_rqSiUJpTIp^x*TYi?pc_Q1DlzP!n^x7zwPGz!D3E z+NITk?|=A2u)K1?m5%`8Ws9T>AKq2=i(c%J;|u$OQ2_*S3R7vxM#lh^ian*rC)`-o zF1`Zsq21h=r;OdNxLyYLZCOL=H?%%)D$o z_qU1Y!q33?eIaoJV9=rZ!v8c^li{68UX9{4q_3Cg*$(ZmGx})^x!n>H47+{dp36Er zE`PP1lCTQ7WwM!6;Y0FJs>X0ryiKfP-<@nCH%Qa{%gqM8+oxxPyZpU-60h7_w@_h1 z=w*z3Gi5XOr({|0jFszLH<6qP?kZAiREYYKwB6Bkw?ysIn}|0T$%dj4g@DvRKGcxNb_MCo^y#KhK{a ztfs!W6)1(w`)EbxKuAuvs%L`1n?Q(dAO+}lGnvqx?kYf2N-owbZ>t=R<~NMVMa&nYGb)T+nW1vYky+EosM*3 zuivFEIQabJo8!GbV|3Izo>I{v+4I!>L{`t958Jb@Jw?{;op!vj!tZ(>AYN$Z7RgOk z8P}Koc~V^%aA8thd5nB+Wa3wNX1ktPY-yhGU5ws`bQl+soxaF6kq~tAAS?t*HLikP zspm!%?Ch|vG+^^Kmj^nSynjjHbOJ+{EKROIMg0Tx&>yjH5oYp`uCa~@9@ z>a`(|gw8hyw#p&q;>OTx!J>eC37H$203AL};H8c@dppY~y3X3U)_;WwzqtT+B_ZWQ z?MTGc+Up-KMZQV(%=R=fJ$CdV<<`6Mg6kFy%CB5kq#D&Fl|Z)WI%XC{cvl1f^VwvC zKf+#YK<4+Mt(1BMU{>}>L~AY#G0-e0kNVG4OQlk`-~zzaqXm5(%oneLAiM@OSWGEs zB5;_0HZQR=0D|ImX@3m!slu>bPl{4u9dD@Lz>6-7>t(tg37G#HbspA;3BJ>Zv$JEZ zC$zK`rB0u3R?o$2zl9C{Z9{ZRMEBN)Sbh(QS`xZo3sU}EZ=~U1lY_*>sI?=6XQ9OA zpd5z-W(*mZ`D8@D#FzBw3XIrE*BP1Hhm2CE!AWM*JCe1Xm=xfCvf^ z0X4qY>!c_p06MO2&LY>^EqUY}yCv6py0&6i-4u)I1WY6d-Z7hvRaIr>lDrj&hB%?1 zg9R&6VKuT8O@F}y>=O$rx`ig(Ae&yBVY(?Sb0vzwZWEFZlDCFKYPB7GufgEM+?Rct z`$^^4aDrr7;IV>4_0Y>o+z^mg;Tot8q)DA@&1ks0~R}xqThn-JlO;*@5w&Xd7j3&$>Fx zr%SyLjE9^EXZ9d;&lpU2vX(e%Cc5knX6o6a+S%K)TS8B=e414Glf~4PA_`AnD+YJ# zmZM~7vQjD?3O84_jQ5;~rZ1PxJB{pav<9`G6GiW9lHIR$=Eq{TwY%!e!ZPXM? zArJ@xf2BY5d9UT2#iS;UIN(&V5d|IBX<5{wHmU0IuMQ@7d9GAXil@p|Mb2=j3jj8D z31XtyYf64r%es7IWnHLJUHdZ_llAmdB*tW5~qg4$~ zlfjwNBl)rB`>;(Nz2mDRJO>i`i1XJ~Ns%%Cnh60Jq8+D7;Y$@VWNM*s#;OE090mvE zd)h9=#mHe8n3OmHZuysUzzTh5@!EJfWPb#jL1;J#qtFqZj7_josMhr7!&uQnI0_6z zk#rcZ892T`F{e^};BSS;=KwTEz&$iIR4rAco^Yh^I0Zu^Izh-mHN*kTWgjN7Z4BB< z2ai@MXA|qDisRRx|@F*sqGSVdeZ!@de!gnt3| zq2{S+uZbF4;*Nh=W=oJl&LI&c`aNAYsqrXa>;q3`>}}I=2^u8N;+QFvL=6iN#i3Ub zg2xVDOsmuPQHTYVKF1bbDP2^Q2b~r*+^(;}dYlY54P;Dr2ALMLbY*33Ja15$eQ}h6lW*^Y4FYe=AmsSpI`;JNF0Yv+g1TaBLTus(m`QyppC{i65d!<*tE zG6_Iq#7sunUD06yu3+bSayyxU0O^WPntCesV7_);3-n2)K#s<)qQsNeXT9!okyf;) z>#p{QWkL5Cw9LSa$W;cC;*GlW0n5l*Yk!ROmKZQ}#QX&` zY_Pz9;t}f$Y-#Y4ulMQ#P(&;);%efhMMqWV%iFbUSy*k-*&fhCn=}W?$FzplF0u98 z)5-;^?z3rl2(3E60!1tet-8C9B-zk8*x6DDIHG8JDQCi@(-5TACchd}cy7aEU@WV^w&&SdaN%@-=e|TMg z!{y(*u>5lkxq}5D;%!?3Qf|y55CO$9P}Cw9g73jds&F68W)C?6}vPvBzYqE7S|d&FW$;6wx}+VVgD%aKyEZmXCdEyQd{2R=?gN5{vd(Mn(*i zdt6FVwY-?@(|5>v_h+kA9`1Y5Ti1s;MSQ2e3W!Qgq7_+=YIdBWi zZix=h4&L)kNaHsCu58GDMr25gv?4R(cGB6=)8!gAB{2c*J+3n___}9mVdMF@8jU>8 zjSS##DBgq17lAD67>dlU31SMYqIxj zUZlZi$?^sgT6~;UpE2UtyH%UdP_lv1XD~!qeFoBrxgDO7cHg=tgQ<2co57UrVKmq_ zNk&U)csG!S*jabYiPzK;J@Xp)txvrMZ_C_k`S&-?y@t3BVW8R9D4NyNFTvQEH$p1H zsdsmGH-9-;%x^#_l8~s6rup@a-f`0=eKAcQCPz3|Ca)%s%HmpY7Cq5?AYev?(D~y9 z5E&pOw+H4uh1RS5(8A8>G{q7eELmIzC1DVI8@iW9i^w3%GWGMk@{`FIqjRgZ>zryK zpIthYFy;r9!ABcJlcf%x^ucSAL!jO~b=~jCEW!vg^)V!Ed|N8_^C`Qj{n*ZdB8nie{nEYf)v_ z%4ivTy`Nj-$84sTHb)=QCyr;kNvum~N>r&(5fvPy#KkD#u+Wxxe*k=OY!wvvygogr zVSm7N?K@)7o!XH7TZ9=lQnw$p{bY8d%G|zk08r<#4Vj~nV|fE-eYJ6A$q>+>Sowx+ z5XaZmyYEQw+-ZCd!fam_Xppzxa#`p36U907y2LB#gf2))FX2VJ`c3D!v|166H?8q9 z99Q78&pBvJo{c=fVbBa1RvljKf}s1FAAjg*^ii1?ns>`V@=lk#vX4;tWwYRAl{I^T zZtr)d9;90XWl3$ba3A*v)wyuPbxkmKiwEND0mES08?Qe0aWX;iBBZ zDzQqLO*PL@=MIEjfs0I0-g)G%0Z9240sd$S-O?j8m&!h&_4q`US`SR@JPH-t{SX;6DUaP43Y0}I$89V90P*bR3J<$nNdf>jW< zfDB*b5By&p5v+0tn zk*(>h$!4<3Lt-VFf_pKl@eNXqgtk>Z5<;c`ez2wR+>XgsJ=tQM+|gJ)f6y>kg~!CX zVC|`bwHD|++~atH9mcQi^?!sC%iT^MS`%)zc=A}4WSciJ2{Q)5qV`2fC~ocRET=Yz zT|AHwYo+1hc5sE;mAp))g`e^)@oYv>uXHwK3_IIa&jBgW1*PrUCK(#ouAv6Dv=swt z-?DMvkG9=x3C~8?sU@+Z-8-DCDJ4jj$J7?}5kaP*=gZ<(XSL((Zn(afN_U=Q1mH_7^2#*e5CrqDeoO#&y? z$!t7bOjOCmgO}c+#NWe17i5`jf`>b%QWwf|Gp=JP3Iv3E zPJ;x)^PHs^NzUwP1<~aYS9>`KGG>a&xMN*~5vHTGogqHL!+(fcKy;1Mp2V%Js1}f_ zR%I@iWxlxmJ4Bu&9%>)>8ZU>sVDZ2Kmap%dt z%oLK&t~5#Aq7!yH#$Of77YL*RP4Rpwr#+l6m&TC3vBhnKX&`RXfV_Z#aG3b=__wX3 zC>Sj5cWlvzJB$Vl42I6HTn5av?n6YTrUE=lIAlGS;TXRJE+Zon1Ch@cVdPCNB5frj zambYgg@3zu^^d&j!{B|;P=U3ThW$1?^+0CuDL(8^{z3mId3RE-bJqA7ZCo^kL`!%xgwi~=yIVFI+_O`z}tZ6VqqeBFX=#5*A6ujmB zWt;ncdKnBA(aoX{p(OO9E&Qi21_+xxv*^_Y_kR#xcgx+?9bR|)UAFna{0EH2t;EE= zlg>t(dUSTZJCl9U`Q0*6%cZ+WGGs2ML(wYPmjz(P#LPis9OWS7skLq{M~S6|?HaDe zTQLxs1{zdEdwV1}Pv(Q6&@S$ukWTsvj_AGlzdhb}V7honyhTA+lV*t{Mku?^P}Qqq z2!D5k5OyNibSM+?QrCqQc8KmWwlLJO?VwnFH=?@&PMyKkT${$f4$A8Vw7UuiGe@*? zwmOQKo>crFX@B|O8)>;CKC--A`q2JrjEyW$uo{x$VkWoD z5aDbvi*eil#&Sb!%vNn5wR5zw#%~LQxF8cGXmn=~#9BzqHMY0@V()5vTc?5ezY_K& z)QwW{%BLoVC~pHM7@PLS=RhcHv#p|Qrlti38h<;!J3D9R;>JnSt%LFLUSprn_J7^a zj$_}Q^$SHWQ*bxJdl@2d5HF8>m!Zy;h`Ku3{U9d3jSBdyWnj>Xdc|u~9}Otfw#S1? z<;}WnfyCY+V;+8#w1yNq<`eg+Vrs4XRG%6~(3`j&lvLiU+7?QvqO58XVY8eeT)P5- z1F_+yE3{2ueJ=zq`{9e+6sft6_kV3pxXW|b;MaNCyFMC|MRtu_3%RRYUKP6hpo9nw zU_k>qAX~CgavdOhnAp45*17B5Zh{ePq*DZWk$#uAydSG032U2&{4e1EnRgqt3d z+WvC3EnZIH^Od9AqO%-Hg^zvcJQax|4#LFA`DcVk#q9u!IO62oTQwdf^SG>5P6L_2 z8JRo{>l%U!eGDcKymFFhvY5}}iKP1G;M=p)!PlDbUi8y$2wqX960l=EO>@^%&YtQ% z?{By=z6MPae|hlr;j815vwwrrocwAAPDGLv)bq_g#x2Wf;Q}>D`TEPl6DGmv1&diy zLU(rh?W?nIPY;~#8Xri)+_F1Nq#GpPeDP3zkk}YG<8WSxCnOR!bcLjsDBZ6jJR5W7 z!nM)OS=pn}@s#8{6?@dKXxr<##|}Dt=%QZHNZOeq7Gn9mc#o4@lCxI;cbX60q$lMlI|_jk~|A`s%$dr zl_Q(OFCRW&aLBqOAv~Tc>o958{$Tejy7YFT=Gr#CRoIpnL_;;KPx;KB^>3v~2)MZt z1+ugRf}lYx*r^f@oPUdm;70)p%BjEw5eed6{woj#@UP>pORojjX@ru1YxR`t4*a}1 zwl>RvLLV6TuEW&gvX4QJ*&XKWXqS?OmoBCjophO0nBY?o6Y&yLzxc&PqBu#jYB^3KCj{_1+SSqu`j@UFUfYJEq_%XtVE$k_*+-F)$_a$ z{R68Q+NHy-zyamj#SnFmNqDQ#Ly-Hrqv-kaZ>O5*A0Bzk&}CvjGQ*-3(|(s9N-dap zfO#6sx7CBKP5!_&it$yArv*Im0?30YCO}NlQnPEVB)fW<*u^DaZFCv#)#!qn5m#I{ zJW=iN3+itUwtu`K9d48#Y)m4+CJ20BV-g?OTqgV|!Dd#{fHm`*p~fWz)I@X7U-9ka zIjdzrtDLs<@|o7xexz|e0&RYsW=ZthH2q_$&pqK(I{G7>O-8j*Mw}UBntgsfG0|n7 z>KbFpRNLVecoD@EtbdWe98W~yhm*sCp9GjFhpM6vmWcV97)oKH+&IQ<~Zqr^oZy2LHI|Y^) zPi;1Nh%<`p0dA6GT1+Ox7@}fk=}iI$wNyEG59`sDUM2I)Z1_)Na?Xr-YrjbUMx`36 zjl|?euj+&O+Pmbt6kJN$5I^HgsaMy!FVMuywto&Ift%MUeAK;UEI7l~RB~Qw!1Rk% zDheyGr$5?_M?j@%lranlV_)UJSX!(b#Jeuq?+8f*bCNiDm1k|?I~>_Wsph-i$BN(Y z$t;UE8{z8nwm!sL)^VHHtJJzgrTQJPwnI;{GxdY)QgWgs_#6>=Dh|~|v6l-+xB&=5IvJ-)8p}!B zXJrUy^S^(bp}N*^l7Arzg84rU&*|DOsCyop4acUsB<^`&ih-%5 zvT36dv>Z4xS1PCBk!SPK;~zheD@RZRW8nTuW}a;t1J?u0x~zep08)bN zfkUBE!yu_j*%H;Ai=4cPed+hX1}!hJb%B) zCnt*bc)DJ8{_fhlI2DTXco`BTCjm=ZZUwj;=2@L>tx+~4Ej`J{?XP0X?(LUbI2#zj zHK_OY{Ec_tl^#*^t5bT?MrCT2Y1m{=EwgDLP*k703s4ZzoxnutF}Xy9tj&)I!$ktZ z|-43y8@m+?XVLvoTVO}CY z8RR4agaJMxK(?}r2-LqJ7iB?HgjES=g}8?R-AdjeIE;jIh+!>!LrlhcT|=OTK+g~$ zWycU~Tcx0dI|vAYeE}>URDZKzBE_MZbPPA=>nBg;7Xjou7o|v&F7sMuHNO^N1b9aP z8v=GiS+R!<2Hw@{^n-zZJ**qdvU&5_31*#6Fo30p^ML_?YW9)G*P&~va8%rcnKH!mZM`PX-T2|yFp*1<~s7{XlEw#f4%2Xeij z$&11-2^CBQn84o*5h7-E$kv?0p9&O6*$yDq=@JFnctDS+08l*pL$%=#RXVGbdqZK? zI-H?Esm*<%pj9ERPyvvt;RwZ>c{Zj5$;P`uK_i=bLHY4?9e@8$8)LJ06we?XFali` zm@20?H~wLfwh%>*3#U*8Ou7gusu8NGzZXFw{4&mvWrsQIZ%lJgTl%(KA5K; zAr9Za`B9*RvlLMHfw97A0xhDc=lM^73qAzA$Vw2S#5vf9J|Pt9Z**2`2P){4C2ZCcJgLjacI|`dypCrd)9+!IgkK+%)pqNH zLiN4+@?Fjl&UGRF^bJrByw7?e-ieP|t{aRWVv@VFn- zv*;!U@1Fbsp@w;pD*3TqS9bLk_Mb!p%@EPYOzF^`8S>E?Zo&Zqj*<)vqoOlqO~?c}6QFYr^34&xy#CX@ zBZzlRh$U74g{&(rp?(29^L1wm0EXxu3KuhOL!&*W|02nS+R4Wge=>6C&do3P6EP~K zP+y+{nSSEZRJygxe@hn2m`WHqyIWJtnq{msdVl`n=7(eorRhlN?pIOytP+Yvj5_>IKp^=M~^u zbAM)mB7t)Yx4BzPP_ThInHx0H$|rHimeL2hC@JPEA++;Cg2GS$4Q`|dv+R|3vLv-0 z%Pgd=MA@Xp*|{s-UmWrBR4Yf6sK6OXuj0vYF){1)oA}T1;wBpOzi@_=o&>{eMde7! z>#$m(s?b>Ai7WooC4dX`c=%AiA7B7+DSvKC$S@dR77KkM`(zKp(jL1BWs2nuS;@e4 z!r)xy=6U~(NyGFSi>_n1e|2`1$n&~R;lOUYa;l}A{b2^zU^y~}Klq%9PnO&%nwFl? znY~)X#_(PEs?mHz&21|KW62So`o( z8Q5`)9dt3FX3zEQavr_Kd-#MrdZ&Z^!D-hOgIN`}!n<%rz6*tWY;b@6~i!CjxX~9mCFqv z8qC!)_hK=J1!$fKZP`z_WBw_gTxlBn8(t^5A4awq`FGHF z4JjK7)qxjTU=qq|oQRh}%Hices^`Jf7gzu!xom06RILImeBRRl!^!1PH@bi@*+xzY zn;4kBj`}WVV?{T))3UE=*U*j@Ki6#_Jx1n{ZKl1d&1pAAxu!v7-EcHs)_?RoEnMrO zM8O;+Y(qQfY;hg8a6(faE2K)(ZD(r$$UX>wKk2|bwUDx|xH}tW<)N&7&F_X-B%5a+;pADPhIXI>6#lqv}8do?<{9(QXl_14(Lx(Q<JVNs*p@q0P zz~<3H=AhL0t6`|~Ha=vIr+?jL8V`?_6QuB<@Hp&RBUBApwwk3=jH_P3*RNT~MXkyM zF5)az#K|C4SxyR+deDwAfm7o+L%`rMLJUE@twa|RjLL)^98)Mn-T^^`gqwztxi5SW z5Pc9YsSfEs;D1vF=G(dmbX*X1 zET37B1k{kzx?;t~1Z-Mb3?O0v36XoIxv^f5y5mwt+RT`~2h`uwWC%yJF#D%b0)`R% z`7qd5WVi9fSiPn1dR&Rsy)D{KMJM%1jT<4I(Di;cXZ9FMk zx642zdd$qJY?E$5@qf}#S4=OWTyX{mxZ)63^kE}XMA$3N3JZLNq~6e1luf~}==1U6 z!(U;w&jGNWb^t8Gkv{|$?N~fm5G-nr+(qTgpg>qi*)$Xu;#!8mV#Xp6mdIL9AS|pQ zG!zyQQ~W1xpFao?le}6bn2LF*IW$YiO;Ltyu zCL{lF9xPax&{?zR=>egin669G65*eaqHzEy);>UQ75$qHD{9XRqSDo@6%bqnV?xwh z8WhTp3`yV*3?=X(lFNcaF-rJZ0L97*4iUxFq>AS8qOiCdG0LO?&wxqUof)_}wq`xR z;n-1_F3!8G!GAym9gDxCY)Y3=q%c{;k;2r#NK*L9TWc&S?BxSTlXB{mGJlRJwbqDI zh^YsRDTO+H+^ABafP0QC)plelp1{nqo08TUT?%#jAn~OFarGQyDij)Pj5389dB`|Z zf#7(KHPuF}Dc?Nfi8n==Yl%38%3UGm6t*lV>Qo@WYJWtYa`({~6q0E3qfcQo8^@oj z8=og&x2RH4&ry0-iPD3jY(1a}a`l8X;asWHX3=`8!;}2aW7ce%l0R?hF8bzLwrl{o zVc}MQY%huhMKl^rtr3p~=^rE_jjm}tb-Uis{;%$Y9ccg8N;HC~W&{juV}lqNSgwAA z8Z5SYIDZUe^CSXdY!33CZe$F1D;A#u;$skak zEVs#T+bZ8Zd9%1id;Im`7`cD0PM?YTEV;RbuYZtSpnQH=Ve&6(YscPy-4_3%*@a;i zN!1aIq=g^^v4zxAlh`YJfm3+C^61Z*xbW@bavhFE!mHQU zpLtGGc(RLvsHzRug!MWn2;m4^x)dwHQuAO`nv|xX)Wt(EX&>%LI0{dPm3!sI#9IeLSZVi>yontWt2`9)?6W` z3gMYisHN+3z9{wAsPxzX`TM;&L>DE7m|(>rPb?Ton9zK{Q)vrNjHa+-6o!;VQHChY zXCD#gS>j4o%HRt36e+9$xl(o(agvI7sDI5)YThPUMe)Re^-j+X)lP-W!_2eQJ~I$@ z`)-!Jnp#>VD$b7z@NxV^?-zGk$N|qSK({yYEAhzte5^;7Za(_eY z-3>h(yz?Q_>!jM`ousDQ>6F~F`AfTZ&ReJ9yRIV8@?BHIHj~{zkL5EHEHi!A0tzs7 z*31HJosICgirmzhlcWGsK&-!^vzsZ+oH^TX0na3C;>?NCz?m~)b<9u2H8N*Zo55Rz zT_ZWi5T0q8TEc6t1jW_xU^bw1i(VKys%d}S5OHcL7{q&lBJw3xB!%B5ULZ!Kms`a% zZbSiT7hj1UA}b*`nrUnrugg2O@p&R}4VlOblLt4+*0dK)Y#>iLB5ticOf*TyuEo*6 z$q-iZT);MyH{@Y<@-Y#w(@(rk)x#OfSE4N3T3#=25n_L| zdd+R*SA*KKm3(t6`EZNEI?M7hZLQs?FxblMNt+dFX|`>0rrDO-oN;!!W}i(9*I`DZ|y1+vi7MiNY(_*hHEl4yS_oU%_mP|eL3V74Z3w0~O zWnrQuH&PH9U)~wbPLp_+WOHSWRQP|KxTnwFSmm6gV>Oyijt{>%`}MnnSN;2NedA~> zmX9#uyJXmp24(E^{uwpk4^X&>e>0qDc=*TnHzY7!1WtDH-gLNQwzg$D ze`LAb^k>0nU~bt z=7ZuU9W5q_Id7sb2wWK8$!dRo%!Gfu+?H#HckhX?8|i~qMe-TE#PGF_#rA6dqrZ0& ze|6-;ibsL(?nk#kWR|1Z_+~MI)tuGxot?v*+jKUMA#O@KPj;1=_)(haK<~U_rB5%A zOP0_p^^ath;3m?P7Dq>iug|)urdgu=ntw}1uu?ic1_{C%SIg^g8l``;Q8I(f-;(5( zmQb@~G^X%@x?=o4Ud*r4SvNXVANmwoT$!v~$JzL=gtr2S9;;UkiEprS*ckw>h{20H zb!7s41$Na6E{*Qe1>TOL#FX!jAH;CXv45vNzsAEXQtRKvjjiApFQS({rDYa3bc^Dc zr8A}*z+t9MHC9Bi>c4-vkA>PquE4uoF4`&4Xips=-gmZlEmiiGQ^Q=Q)?)7#N|{=D z8Ld9r(DLgAQWrbg-&PkgO&7O(+tv@==*MpKlj^im-gv5#Gc98TF`9$5GdI91xe8R5CN*j zfGU{cavm%hhz`)}^B_-ify1_fP8g`|$gV z1oO3AcnrR~t&KX=RCb4BEW=gOEQRZaeB&*~@B`}OsaSuE?u@|&3ySEQSV<8N6mB!^ zW|OEJUi4ln+JT%xO(f<*+uq-fUf-m@CEe(9qN;*7-0dVCsUPFXWO0LI&}!kF=Lt9- zktH*IPXrf-gp2g%!iZjt(W|Ruri34Tn-z{NjGA_>R#G#2FLgt073~yYi3B)do~6_{ zZZ3I84U>Nvnw$*N3BEi1Y(ASTmaxK;!lmU;lhKmm(EVklk7qD)moP|?U@4F9ugCLc zEdGG}r5wJPCCPLNhbzd}92{-IYv*%7#WHOnf#!}FYNjnOf1ZDvtN+Q@;W%^ak(#mIo^0<)$je@jiFydw_BVtE$4KgXA2gJ_bIv!3mX_3YdJf5cK3 z7_5J?)a~a>U6xH3J}%H;aSELoQ@>1)c$<6)Q(h7G4fCKCaX)NDJebVK^98*2ek@rS zLtXy0alg1W?(4O27v+VWCiWOB2;vM?teUOXtZVUXRncS(-RCv5NLH-NGk&bMith6& zS|pd&fy?VhT}StM9W9ben~V$W&00zK*Sdd_?prJAesLxJV<+mr^A0bi`&uw&I=A;a z1}BHnt1k!Nd~pzcsrCzwPk-FmSu*Xr13|5yli37Ls|;2q=~XmJhl`tJs*N(B0?N#K z(a9u3QkghhX36D34OyDzc?CO7=OiaZw8)Z?BZ(boILzj7Gas6NC^qmWR@#45 zw79TLCnJhOco`3^1;ejoTsZpGS8zO!C*z@2lq>!+nI#u@*emWXH%tlr<%e&-^RD`D zmvHbizDCssQod^FMETD{vo46XcXsII)QhSZ6;}ZrQPyJ6fBWS}MF!Rjpo2i_Mqevf ze~Ir9z&0~<*nRN!J0HFMvR`C(rq+MY8TnW4kLhKnCPlE2UCQX44|dhtW0dgMAdW5g z(Pdm?8valdyXtkS#0^si8@CM8QG%~&RW}^IyC6ADZYN5EX1qqfBIjS};}WKcogMhS zv!kByAk*-~p&DP|!UQOK%u2mp*vuZk6L}Kr1=QTPYVKFo++VroQzgUnrlf!8l(B~X z_nc{lSGxn;y3BSXS4p2w0C}&}OMI42qTOge$;MaH=*{S4mR_k9jney7ap4TqaLX`^ z0*B$Jv_(x2qN7!ssd!N%l1%z;8=g!2asM9S@Y| zU_!2=BBi>hTQ0EBfcDgj2_=qNc!WT9Kg_ArNsF_|N9zbyX+wRO11Yk1?Bev<;Pe=n zz~5B--Yrs~Ka17=!{y~kJcBhmlyp;o2hyn}gnC2{NJYDzH2XS2jdI8tT|hjWm}eITauH`US^?a(gDvgoU4XeBGg=`9;XIdv!SK}7O zBB;36<%e|b^}F&CrQPpd%SyK7IBZB}y+}Z51rKp-`QO+--wUAvkmODPAGmNkOF0=x zsIeb?iU&wLtDApVZh{Lq`MRSi%zY!n`2eW)-GsT>;e5~&q|)NJ?~uuTCx6e}U!F~I zpIz)&@XAgq;&Zw1c3vvs8?w{1i^xQsQMXx-afR;Wt+_kwzR#NQIr*qwZ7f3cx5$z$ zo%!d|$SXE+^V&_cIo`2}n_Ze{&59Zpu0Cn zUa8g8gqVL9^~as$W5?!yalX8;`DfQ@e$2n_2Q~k!WAnc_FJIXFPuFRF%=7x=PR#r9 z3XmoxAAdsY$0H=-Te-WKXYz`5zNpNd3U`VxcK1EVo#}K5d5ykn+_QlU;JzGKC^6u6 zbwwK6x-KoxgEx1kz}EBW82I(n6wuG@fak&1gc*NCLK&aw3PR>)hES&q^RgQ;Z#g?_X^B&9*ut zTpW`l0Nn`tGFe}ec_$oA1n6zU8sJ>Npm8>|u31))`|F<3?tzNu_ebsgQBn0UdQ5Lj z%pHGl12jd^W>K!nrya}vg+}%~Y;sIYR7+eu_OmuL7l{I!4&PRRVAWnC1>nr~MMOa` zFh);s!XemJ7fDI~GD-j7TRO1KM;4b$`UhRQH5Me8kXIZfL*~!kH<4KPOAUZ8Mqw>k z3J9Y{ch&C9z%sDKHa_&wbQ2KYRzA8^@d|$h#A+NQb+jIXNi$bx&gGns#%!FI&MW7A z#U>VgJiCG9 z=2@p<;)SE+Yyi^^e7@0XO5V(zin)Ko&=_+trd4>Oj-pqi&4x6tZOb1r%XXt=2{tes zt20IuV}|y98Smytamil6qGVog^ZG1em4TqlU0bGGCFr+GPI zgR&?ZR8&jLX)l`+kFolh%>YMvoqxzOfr#ZbFXpSum78B2maDe@5Dal^+IfF(^NALX z*Mb(|REKMlQ+vgt9A(YI%tok}^g_9@VFiU-lfM&V+zFvR8I*hiO8&h!{88H=KlooB zUq2H4kBH zbgM_Nl6|sTY9819Vox9+z3P7j#0;zwXxGR!>HB(r=h+^cx6B zdm3#J1H4ngEY5$B!s-igTl!r#KfgENp|L}psANRdb7AZK;>B|hS08BNgDh^Ei;Iu- zx|iJk{=Em|+k;x8lHoA9Enc3t`KMwQT)@J~%FpV=!2?09rjhy+HQav-Z;&%j@j1l+ z5w4aFN7KUdNjagX%!i@r!v6?=l6J~ryNE5H?>Ua-?_^8r>mzIy$PrUrKEY54v1>XPo((2TxVyS%Q>;tbaH9Pt z%jN;T#4t(+#=V#_6QF*g}U~ddX-!dd1_=vNT4Tx3}JP-A_9lbwMmRI&T0J ziOEbU-${uJ?z}p#9O32x*MD z^Kh>(uL3iF*dvw;v<{wCpE@^+$he+^B{3U-XfqS;F#9xBTmdG-fDjo#O3ghiJhlUr|+q@K?IEl6aH}mc%GZ%Z5 z=->TdwWgHe#-1_eH?#Zx8DgyRd zG2&oQCQ4*By3$oLk?dg{X&&dl&oF!8wJZS z7J;g6dj z4lsrmC(b#%yfFnD`IvJ!;^XOIvCn`5KU-L-5)ex^1b~tXLZC*# z^FczN8Q*_c9a$-ciSw;)0cb4DSy69s1{fetALiq*txYhfR#3EO%Wng!2|g<$(k+r~ zctYUn!mU*x?D^85yD0h%?`)f-k>%WoXh8g@ zCm@5yYFhdzjuVEy>L$#Znj1wL2wK?GlDu9bY*2Jr4}aw60qtDBV@6~qOrk;w$S=yDBxX)dXpT@-Wg%2ZL=6=-7UkRpjGaMP2zC1gR{ z8Nw%u9YzNrJvGJZCoTzFO#EJk^JRo~n?Gb2j!Tx3k*dP-rtDseLL~(CJh}#2TO>{l zrcIpW$3!lJ#X_a220qf!-{P;?Fcw#uN?(6Z&tmSjbZL~TqXCAo*}UwrE**q0hW|A^ z1ahy_MZmC9A9Y!?zMd`;YOm8nf?%Z%y4w1wK{&5ND;19^#h*Bw#in=I*An`$r5T$Y zkA=UgunB&2gAUk3hj?RHp9wYmVt%xIZA+sEYsmk$_>@ZO$3f`Kn7lzeb{C}b410f| zd6~L$cBn3O`DnX^ZD_Rr-?WGgR}U}nnRc5Wd2>s6=;xXITyA~7bkKpOh!Sx`3J(Q$ ze8171{%Rp5HiIApU1LgYUTVsC#h4O6+&I|(jWH#<3~y_+2^r`ciZ!u$rC1Y}5Azjc zO>`S(7i;3mp;%DB_r?-@>Sp20v(A54=9>TX5O)o)u-Rc)uMl2g^G3rfT#?ALE=0a* zh+J@~)lG3^%TqeP5n_Q06||8H100{=i2vH-ZzTQI12S}J%j}8)8Jd^tFBXtt^`>+W z$k5W1I3UBPt^pYwBiuwYwiQ*(Wyy}b#eBj_WN(_23Yb^0t`L*JAO>()lmUOMY`%Jj zF#j4!fvdkI7v+k4-6=4@*T(7pj&KjFH|yG5Z($tbI0h?(aWJ;CorM=HedTuf)4)Cu z#NppQ5vZLPOoWw~Ke-dkn|p2DyVU*6R?0WVhY6$w!3`skpLg%>rYBclIjqw8t#x$q zB-oE=o?n2m#l^tJ!QqXbRE>WK4>YiL0RDeu?AS}13N*SSnzukpiO4OHONiru_zi@w z;u#UXh}1;kjMcfq*X&o|i+JxBF~;T9T$43?oGUU1_~Ig#ScMw`*x|=!9*E%1ZhYhe z1$SDAwK4k_TwevLSu;O9ozXQfK3aOa!MNOPz;9pGj&w8OBe!BoRu+HPG$ifP>aW(2 zbn`+((#_@}xxOLk8{ekw-(^VprjsG*oA#(3YDmhcqLb!J9u9r}bZ>9_w@)RYbo6lO z^Q@}U~9D*=XoJukwLg z?eT=3uH}_tVqbJ^04G-xKTVa%{elz53Q!H?LDOl5)A0pf$76peow^>s;U%LzzqpXC zv{13tdZWEtK=r(M$T|A{fpS+{U+otFI3HRGW5f1B3S;G%VUw4{j#+s@K?2eu;gbpu z6lr*dHhjCmX1on#7%hI8esK_g2ZRvq-w7hLyR5ZALOGzwf0Oy%z>;^KxC9aqk-d}% z_>kLthQxC{VLpF`a9Rl8>CDc?jZ+;rIH!whuH0v=N4>rrQx+rqX`_V{3_~&pUn@B# zlENpM_~$5}F~(p)jFwx~F@TAc2cBb$%y*8{>|w*+Q1hNF%H``kY2xzosY*1)L~@Wl z^p5?B&C+o(87GGq$;n|m$u`z@7Y7mmrZ4%Z2wYPcxK@8}n{cV-itII_(h4J%m(^T4 zhFfM5R9`8=Lsn_B_AptiX%hjPFP78BzfY*LGMDJYU`1ed!)J1X*Z`hO?y={?r4W&a zSZ0{b16f)&e-th#d4=gGuiSB?*7im;)7I*Byn6bir4DH{XS;f6xF%<)?EbyWDtij! z)8~0r$t!>S5IHm}*Z!1GBO)*%?zc8J0l;c4wK|dTAFb6J(Jh@1P@vZKsVbAr50gjF zUO!DvQho|=`rYJdIy!M9z#S@?A6Sc`$8zzU?RTk8B5x3NIxA(tK*v?vAZzt3yI7)R z@+Mj`%V?I(&!}HUDG4)PT5nUvJDM^g{dm{IiDiGnb7wKl=hfN()VxGdAtUPah?K`9 zEJ&)j_tC0>*7Oe+qh!RBq|PH$TZ`^(#J;&2cGnoIz{lS=l|R7gi_t+{Fb*P#MQFaW z;+@-fuw|xGtrhE11O25d`?QE)?AY!D3E*OSdoJQH;f}7jEAsgy8xDuxAv*G-s5s4l zDD8irv_h{rpXTo*NnGk-wK(i;-0utIUx`1>|Jj}CMQeIg$#5tVmrKD1dbRRG0!KyK z;1dBs#)JqGI6xcLHk|N20JIyWVQoxMv8BLk;YT&-{anm`oh+4~HH;-9M-cs+de1d4baS?xQE!9cjOUOiOsi{y%6g^xY)W&vFwmWlq@RqkY*f;1geb`eDUD#nd#!Ol$&Y1GzeO7)bbGN^12fh#F_;7!t zk6M|<9-Xl+%>vQ#od~^YA+76SD6Ai<_S^GG{31g93IxEcc$I2zpwZncyU{j&>5=}o zjc7x2R^YNW(0JL8AcIBPlCWapTNXayk5tCf}%DRb+$uXbO*MEPJnX<0#6$;P;x}AGD96oo{n8|B@s^k+vMIwyti`{jgGa~wo?%&+GfhYiBj)t3*moY>{uF@+WEf2 z4y1kX>;mu7(Yf-v5Z3c=xrvHy{#X#ntsmqk)f=cVUj+&O!7iy0VB|$}Ky!@49ZPrB zn>s*p3)(%Vy0PZ1BUwwle@FF*#%U)&2EL-Ez4_0GJkme^tERxW41D0 zHfIFjTL>5R9q+Ua0hWITk{k#rWN^Ct!~eG)HKdN=$%?kjsuK=5RFVTT_;b~pODeIv zyNocNJBTINunw_RFT66#bp1zV6qX$~6X{`WF@)?D{g;jr$wX~nq;S!sJ6o=xA zdBNe(`14Gb5kQw0%z)=+=M=!bv{Fxi#3fQ0w3xJ-qlap2UQD=VEf5}UNo+kc&Mz;7RH6=gIi z7VD&-yseoKrw)p_7QY}VybJDU*%1Tx;@Xg(+_{*JWFxvD*CB|i`NfvLOA`}MG?2ei zNU!~nzsCCego8O>?eC23nTIl7_1WB(;`uMqiR0Tra=3q>8NnPcIx|6xCW~=4X12#9 zumKJbat&q(dcuWaI#>7;Y^cONayvpRW!S`&O|Oa_NL*(Rm8Nr7-lm&NSYAO4{Dj8I zX4$JxH7mQ~M3XX2Gwt#Vs`^!Ut-&-)aCh;wPClXZTQ>W?gMY;!f;=xyve7$M<&42) zT8Y{nu0MY^Q9@1+jXQ>5@0(suIx=~u^#mhlz^#TGHFh;t$&YHKMgI+xn73`Y>GK4t z^#@!KfU|Y>Ez2~`z;E3gWIk^1f?y^ZZ~H(0t@EtMK>w_YhM&qm`jdqN}e?6e$lBX8iw{9iB0j52{@?0?RLQCL% zS^r6N~b$m$R{>qkIHSm}S`p;pH>4eljbIQO0$^Kd30*9Z?bAcjdcp zAEkfts_f1wb2h~`5eHKJ3YfWfk4>?#|@S9 zBuByZbyw}q9L`WDA^uhSAS^FG4bBmp_pJOR}~ZId^7$I-6vtJPgR^qHRYgtUyw3CMQuvRplHEJfKdAi%F9<6{l28&USy9 zBChB|L3~;=@`1Q2xODWdp-xJgT$K`-PV30q3!YGcNmg=MD+I&FqVge)+lASr37D1D zVm2#?Mj4EgJS)?Z_>cyuMuo6(t#Ie*X?-8Vq)AK2hFHZwz<7~p?U7lf< zq{nH_Y$f~+!NLVc1ylFY1Vb{)4&QGI2y5QrFus#Ch8BhE7Bm7c8k*uWvAma8o04zDd(2-5DiwBITueq zg*!C1W1m>ka6eAr{&T`4FAI#bIWKcc6(r8~j*wSo%&?3_%2ZlN>E_PSmStlP2jJc5 zsujZ|V(j_!C?y}fGjz>tQk;K5C=x#6;k0}Q(oeEUJ}PF+t2_*ODb3>F?=u>QNoEb7 zWOGOPEM@0D*KnUFWUbuY-Fr%PkJ9qvjs+GY>R5bY?y}vnhW_)GavD@F&V| zr_(3|8ITaQ>a$Xaqxg@bcZ61It<6Jf)2o2%HV#t~yOBt7)vd2X@&vZ>VRwt*BDA~~ zW@(jU^@dCtqeG=3r29;YG8+@h!?PyBPl(TtPh6+R;&a~w_ctoI(VcSt!60x^j3>Nn zjjVCR`&rI2QJVBHm}!6F7(FVB>DeifvAx48mXi+fYt}gTaTOXIc6ux&lM&oeC(_Lp#m@e@`2STrcZ>twcsnbKTU61(B_y0#C$4jsP5K>;!VU#q&01jRHE~1BD^xrlU_9`=6w-HIx=z`{Es4^_O-=K?nMoL z`D*tuZ3fPxNCHnXW_($}S9@8XU9R96$GM~??hj;Jddq{Hj z?mNOW5|no*i*!Q}Z8$`IXy|OJQi_xkT|N(6(wnF6-0pvm@QkwAye~?mZVn@*E+m$C zu2?wrd60;jHV(atC=Lj(6zo@^5sbITxCrD6lK}`!r5OCs(?gCqe-=_PWmW-yL+mpn zpH8x>YM9i+XoH18=8zWP)l1`8I91IPxUX2n7NTM)jP$3XC+=foP_=7y4@|3z`OKNJ z3xBzk;)s9Sj8(e677l3(X01ojQ_@M7H&vf(3D6tQkQgdt^n@MFooi&tjlyn2vxLCR!oHtaG9Bgiy zQMwL_!8k!ueEe}nPHsMiAL}U`olKlTwSCV4P1v|jaK05jHcB`?GUAN84fsySp=$+ z(~^G$t+1)?cP&Jo1JSnQU;(rM<$(GRfeegS(=zqL9_+BuIvRUiTCu3Im{29s z*)J89#;TC*@W5%GAx%QSq6(LVm{XMKK~jGej__r5-f1O^6Xw08t~{?Vpk5bkS5P^) z?btXEup72Ri}m$ql$(}Lv>06`EWYuUza}t@*ddJ*IGQ{3BXqD+T3wS=BwCrBU@O+L zV99_&?Fsc2zHE`hA98)7y7{n8c8+Kn_XX;2az026 zFp_qmGyVQCxF`>-QK!WYWTCNhKU&-DkdgP6bh40BN9u8#oGDZO`Z`#mSM}`-gBw_I zj>*?l8_~qLVQz~~7Tu~gcam7t9Z;RlnbFdUGe4x0cMO{8 z8gFOQRR%;(5aGNG!^`cA#@7@Bbe)aj6!OsW{o$-HnAQ)zjD`w0BsJ7}84L#?*%9RkIwmX%>rs%$cvb1X<^4gKi z3n$IcD#AA_K*8Wb%nCldghc9`U2Q=>O3ogUU3!W7XEqPA3=g; zS5>|-Q;wTkx$S?&+P09*Gc4X_DYH4Y{ua5}M2<0@fk^2>PghGmxX1PBLlKk%blQN1 zsWzmxb%mVLPU%3t*l1B?BtE#X0^1wcM%cAkY-+F(yGi3&_MXXla0euuN%*kd$(gEHIcY{?HtXS}VSANy ztAa|&%Ey0&ZzLn16)8v~637+5|WG1e@vDY)OLW>%LF6bM5yNSjxqeCa`!`4tX8*D1ucI=w74sOrq+AXmKxR7mRYsHA&~L5unx`E^}KL zmxN%Fd6-Yxe{|0(Y@-s%#$l`+nY6zYU@RNGP8 zf~?b(OT2eBBok7eWRH0&5v^rTS)kb?eRn{|!8+0zkyLFB4UeAXKjxXktmc*~@LOHh z4w7xiAKS_KQG7mY_Llp>Zcy~1^oXbqabO-6j5*|LgrAhX{}+a zBinzO%7vef&A@Hrg6>&g@AaFO&QSFNBa#)2u(;Zcnqm>FhZrEY*neOP5}ZSx^7mLS zga>Tga+}Rdi+AoxGHBsHWIfz?)sb(fNv5lt><9z{7c=VQd=~3Vg}y>$Ow z4!=Qdr;~3^h|ax*Hak2v$mr-;AnoLse^Em_6=gWTU|6fF1h)G%y}$+p7Py#a(QMUO zS(N^Cl!YB=b8jh~l5QeAAKHJXc zs*2S`hI`F2P7p=vVmthKSi=9EAGt05mJX8h3NnyH@wk`e>yO|YZVkVU@3;o=kKyl7 z1C=oRk=7f1N^g*Yt6lgb>ja)Bw);`G0H6#$?65hoCwqt(vth*J%A$X1(6L^AMPh}D zmuwtxhsVkg`ZJO6k;)jVWTrUUO@%NFY>MN`UkCD2ZB!|(SJSlmG#{LuM!p69M*p_RZSZYN^~h5rOrj08AA=U0*jR0DcUDZ9p0?Gz|E{U6dW#& zk6Fl-Sy~lvg43=X*NzRj8%L2P<#1B?JKrnFB8+Qj_GE6p;n||mmRD*puF|R~Ay(|g zELjTb@8OJ!2|*f{fEsDSw^JUfvXxhMYGn6vp7dc5xDR=}3)_E&4|!EvlRxm9ES4Q& z62&|OK>$Z3lW!P$?8ULDhWAxh5uB?D?D*2n3h;PaMd*R{JLPvzMRw26grE^L{*Tj4o=M*{np&xHul;AMCkJwJBvfjVW>mgrd6jz zJ)Bk9V-0@Olra%*jJ<7zui*&0zV5t()aM5*0s(|J;~teDV-v|&$m&rjygsguy=$o_ z*({TT9NvG!b9vFG)Lp!Ccr&)hFkb^lo!(K{WjmF3Y!cW{tHbnV=6UWSge>A%(CcZ3 zYrC2yF)WB*zCY^m8BxFn0*}AhenEi^zS-H^-+lFFeLbicS2~k3rV*DVY>gmBxaL&B zSddu_lb4e$b?l}pW|_(H;V`@wSt{15afZ%zOx=HY%)KdY-lF;G(|I_4JS>Q{BnsV8 zI?BwC?BEs6GE&VB+6v}#l4T>Ws8#%i{n;COAzjXPRyUQ0uDAd-s35aes5;{!bqg z3(J21_@X)<_SgI0?7rHQ*&yzNJC9!P@9e&y_!8lPv7Eu?hYJxYb-&}c>+3J`cNtwZ z+9@tchs9|%%u<@G!*X%=lxJl*oSn>mwb=Uc=1(8|sLv$6&oi+$35~gbKy81YkC;c~ zlBJ|k6fUb>UT@38o=x-wC&!)X`svSp^x%KTMAs8I>HdIk8QFtetR8+45-Fs3Z;pys z=a7lxB7sl6K5DjMj!Q^D$V5)Xcl+a_2?HT~A}^M~>!OsiHsJJg4d)s^8fxAiLjkZr zmYl;|A|zWFOAuxIIn}1Pb^G@GxfF6i72OV3DNecvUQ7}zFvJ^~6&2K9H29y-V~BrY zwK!z3C@m`B)R;P~vp;6=+tXs2-S&z&(}Jx45o3?ic)*}{TZWg-p{(2T4ovgpkz^Xy z{A^pkqgoG~ywBnsTB`(QoFlZv$1H?SloFn8hq7YOX!Sf1MVG5AWAZ zk2*H@w?T9J(RfA8#e52z#HPlIAlRc@p=#L3ziPJ;n4h(P?iT{LU$lRXakv4C zPZBYVwr@6IoBv+o5a&UHC(3iibU+{FiizBQp%vN*oWZE%Xpwzy?f2gZmwkEo4rI7b z(^=1#--DeI zJkGam!igqEI*%W;K|7C4+A7*$X}5tN;&wV7b1KEQqu!b*>Miz;V27m-v*SkI#u0J{ zBY5PU_Q2{fY30i1M(i)~HcAiI#+vAGse`ad%+LPOdY3ZikqlyLO!$BBS0r^fF%~yS zKB^ehRc6VujA@ZI`C7BwfTXQ}LI5?YTyLo;Ng~FeP$r0oBUlWX!Z={K0f#G$<=jPb z8VnrE3T>jvh7R%!BYi`%Ogo}ok|{(i$6D}3yyi7r=7)pXd9q}vTEx>DVJ=LmUTA0` z$5P-a7JQnaY~}bWc;tWCSR24gj*mx$*${cI#la8v$^NHwD9VR3iqK3s;P`^$f*UA>}Wdzi*~hYgdS z8t|9dgdC;aNxKH;!8WyqwLfejhwG46w{w~N{0nFfIorsPGf=aj98uyJ5NO6CS}hSq z6Ey%kQcR^V^g?Zn34`W$Y0QU=Uzu=V?QuVYMdmk~yvnpep2sXK8OEGVsoC&jBtuTd9 zpxaD>lICi3bEtjwMwz-|Xr*<%JmAfVt1n1LilZ&rifE*yh~BM`?nJRg0wBtWWPn1f zAO>8C)g(cr&9!NAPz027Op9wi-(isUWfR$G?J6~s#&Umgy$lf78XwWtDveM*E7CC^ zIX)tTRpKoDxzFU(HNz=0keF|=d8=uKVJS4$v_i+0Z@Dxxgq9C9whQ!D|HMHL8*q@S zFUuL7HhXOE;zL6B3?an>2Ah_H15^CsGS(DH;kMY9a5VNMFgre&+ipW3`T+{A)sWb? zp&4f!+Hrr}hQ}uRn%vf@pVh;<@;X3*HdBkcz6{$k3=(A|6Y6-`EaKY3wqNO(Xe$nM zT&&oTlHwt{bd+^FCp$U*cOVR>1lxmAmF!*(V2l5lC=vv3E4QiTmW2u6&n8g$W&P2b zX9(_q)Z@H)70GT1xqK9fU_(Gzoc}~pcjEF+wDwK01 zqp2%2Fm=WJ7!jGM6X_nab%LiZou%G&*kz*lfF8Oin@Xm)pwwmzY( zN&a?uFtm;k#Mb%&k8G?iJx+Om3ssl?#oqCEHB!Vrf!|?;KP+dhdZ#`;#U}zP>KzEE zPkriJ+67ii7k66~efZf;CYxrmndxpR+~I!@eM)yGlgVU~Oy*Yx_BDD!#(6#{})sPU)|_^1eePv^LVGZi7Qkx3!-ShtN&i_xn&CLc{M4Xc}VQ_l)W>QTF_g(ScAN9Rt6zbaV7Nu}{QKXoG#9xl#g zanb|u=5%WUwMjCni*DF*-zkXNW@>*xWSM3CkZibX2JJXI4CL$i!?3QHM|Lw58Hl!V z*%bIrDy6n?hF87xW?U@9fHH|ZvE?4lD_-;LDJzhlN*RfCY=xI+;AsIg-6UR?F9k;d^fiV2LT4H}BWqFy& zPZ`jtC`D6~T#BA+FOu+e-0YWGiXkafB$*eJeZKSn>HZt5)w#R&Fu_}hR0}|B8$!;& zUz>!KsJk`6x&_giTbUQ`p|rhlTM|cY6vxC2X3+T}CUT5|Mm2MV$+&l7r=(F7rU*K~ zd5N3u|7=%8gk`uS=Q`>H2N!<E52g@GOH5l?Q8x6xjY=qD#g%{bJv8!~d3i}Dsct1ikbIB+SUBf94}EyS;?Ed0{V>LUmx`{N zD-j(E+0lcFLhHMAw(q%a)1aX^ickl>TP}V&vsXZbrE&y-kVg6FpioG8!17N}YgCj+ z@X=a?i9!~ShEk0lG6;%>Pg|-=FLQ zS`|M%CC9jh!ODLlY+K;ntN7$k90%}^U>4DmS3M^bi_ugqXxthp;jk(sdqLUvMi~5> zj8G6Jbc88K=H|FD^qrKYtrRcor3Wv4@3#n%kVA=%bxTV?iFyr&umi{ql~c*|M{W+O zr`-4&k4J6LP!mdFOSVP_bbb7GHwGy%1M=}fv#<=WU+ zq;GB z_?fHqv*CYN1G^q3quF*FXVcJWnfvjxV=i&0ox6*<^|9hsaGZ=cIgWSPZBaFx;nb32 zhi=M@3g?^`)tM9^!T%L5DxMZF6?Nsn@sP`RRF*feRC!W83Z!G!KQv|n8n`Grcm$@m z5u9i?rW;h5($22?zaKriiw)pI_jq!Dz7hQUC3b&NPx#Bz)ZiAe6@~e(F>jJ?(9E^3 zIxqlBK(xQUmrMqC#rgnoyYJ(U z`2qsb$qlGsqaJ`jSOcQ+wN6VqjclqbT)T<=-A70}BAbEICq^6Uc=d8!#NERRgLWAo zLv4pkscIDNZ8R;8a7$sXO6&Z|_mSZT+5nV#d9U@6#`4{#TKx?Nme#aN|hynK{( zE~F3?W^bpr5jp^iVHzr4b&ttSywe38A?nzr80x$jUtJU>K3!igO&9!xR{n?cs|?qf z-Huj2$ESe$pFq@cuKKp2+fF5i*)OJVO;h>i)#65f3$R0U)Eb^uOCnzIk*~lVAIVg= ze1tB2rK)*}wQp{&s+8lxv$&S{`30j}x|n26Mw7tlfL3y7np-H>^VGejIR)G>?^4nN zZ4fxUjuqzvUfdyx@EE~&agY=IJHQIgim*b8ZrFb1JOjm4Ht6)v@k5J+S~{MOq|%Q7 zotPefB@#0&g>s2SW0ynYe!{}J4PlJWZs@jczEfMRbFzs9crHtvh^o>M@JBcXA#Xc=pwQi-7_P9 z8=imzUw7LbAb`o*lP6nGcedg6{N2MvO;N>vA`OymQjHH4m)27Bs{Nh)$H`MVvrBP2 zDMEJsd;Ny4A}Gl#I$0_^htoz$rgNDa6d!Ueq1R%)+`Mw@ zwY0zgbCz8u`IJ&Pil~dGo1By|`^u&y_j0-bJ+xR;INxU*1lUHY+LXdX^#?TD<*N*ZOBhH(<*U_-vb<{WZOE5uO8e3h=uCRaY#T0UyskdLMGVMBF;Z!cM^J0Q(VKa=(PcJS*ZNVKkK5{hB$!B@ zn7LGqN3$W;w7AL|C{ib*2wbKocb7K|__ml3M8?Y_Ybo_;UF7r|wcvJc{N`^gj!=}V z>P(wf9~(M*XLfOMWfZDQp+14{RmZse*qn0br#f4o0)GN@Zhc(ue5`%I)sc~m>oH zfeEJI8cM!_#9?rBK1?T&ptg21cz^!t(IJ2te$|9`z}h-%m)-sl*q$J zj@K!~Evk;8^b$0uKK>DAPPHqCNa{(H@4EJtEE(!$h84?;olJ;tzS3lWf%HaFf@`Ui zwdz7D<}t45!YO5US~W$$=#nXAFWxL&X1*91!W~R({5I1uPLz$t)fZr6`;}6?=<#=r zlf3f_sQVLV{NoJH)*SeF`7Vp$w^K_don} zcblxFAN2b54Va#jQ@2ha*96n&XG0)=9zXh;f_~rp9}fN=sg#1;{6<56oiyba)%iJ| zgi7)om5=2O_y-VHKfMIqgfNw~#NC-e4#79WFHAT*F0|~myj0ddm zBt0Xl*4$9PMr(0@ULgV|Mh}41+zH?txWExbN#WLCS@;hztxu`5Tl>396CVLBolJbRON%INIM?X4Bgk3|=3WE$_F~&<0 zLJkh|g<>o#+YmKO%~B-9j8J@ng(uG1fq)E6DrDA*|N$F zk{zZ4jUe-Xa_d&`Nyb2RgM1*%8cqk9a$=<~s&47}r3(N2GRs&G@~l)V{F+&ovsBeW z)()VP>DS(l<|Ur;I@Sh|@00$XS1#Bru2YTS!qFf}EW4cw_7t}acl>Px5f^VaVV)+d zm-(qc%6MJl;HD&-0`&MyC}H-`0r~6+5#rw|fK={(n4PQw@xSuN(Lqe6<4lC9>yj+2 z>wQ*CBnAoJMpC_FlHrkvZ>#sQw*y;tdmrubA#>}4totC=2hj$8Wj{pwBD(UWe389c zHPJM7@<{vwalb@?MZFV)a7iDYo zMZq8)a?VYV&cK7YvM4Eid4yn^hXxx555+5O92agO>V=M{E<+YJqE_OB>P+Q|hTVmL zN2=-#`lY#Uj9+EYNA&^7Q}wGb{%Reza2wQr_pB(d>ij?50QH?a=fGKmt-Zs8&$piL zeoymjz_OK>__PyJo;qsfQVwkj?VMTFzE5&m)pl*9=b*lC`%8)Ux%D+Ed}Co`;;js#fNWmzkMWs z>;6YjHtaBDU2|KzxI11RdD*6xj&3@MbPR=d-7SDP@K!nvbkLQjZd$^yM*BuH@1!Wk zSvqkcqX26JW7PFj@Eu)H)hG3+;X9&_dX`l|chm>y;*|RE#=rM7i73F?ECshmcd>EW zaRdEbzKc_}_mI^;t!lVb7;AB7p!Kgje%mL)K2qG&Jhu}pv@@TDEYqiA zD*`Cz?ZtlR(2=Aw&qjmwEJ4p2zLR=@DZP!soS`ZVLl~dv>$4gTJbCCc+3nV`wX)8q z)eWtk*E6W{5qb0O2h}QM-0kpxFsH8T`0$Kmi^&QN4~A+n6JaEFfUq#n+5_=Ku6;3N zDS@l8t=E4C`G`gY`t-Im?pm^3efLeS98nReqD(7O7J-o{1MzI-DKX=k?!u7_P2Mm? zzUjz4fsYqu!eihKM6h)=V}L}Uv_;6gW7)*o@0VaAMPaEFS|v`PDH@f3N6LNI%r z!Pt-a84stbFZ%QxCT;kfz~bT0@D3?Ii@J>5i4(QB!a+nR>?NQY@k=kwTeWDCb&l(U zpF?ytcfQ;!|5_gw6^Gd*E3eiF)8g&AgZ-mDe6>HmU3XB>HD{+&3?A9K)A}bw-|BIq zAVEuQ=lO*?ZRJIG?-Z(kg}Zl4Y}olY8=G~UwbEuCBUxdyPEbTQ>*D=nH|``izH!Ge zcfWCWk)_qFjBMMrw5|0(l0iP`jJI`bP;hrV@FdmgeJ82e>^n(jeBUW%27;%em2p+c1}+Tjk2L*E0VE@;k#^q=qL&DKf;VwsO@Hs zWZTUg&Xdg?u3^c|9FdSXBKC~v5~S@>PFiNyhAvtI?{L+wnltOsb#ht%On%MKx>k+1 z#UVehqd|`H5) z+3-=w_2vAi>&pOtU0q*>54yg7CjSPcsu;Py{0(bJmwH}ikl3ky5X5taJIu`+LJ919 zcwP5kPzq7r9zGX&L())MuzDhe$b7bPhE&`y`9g{BDq9P1Je1(%CTjq30!*EQ-y^_R zA>cm{SkLlI$*6Es_Hu%aUf$l*;~&4;-F{fRC(mD?-J$J&1FUv^PTf5aYNY62D%sRD z5^rHS4*Pi5+l>~s&J8Q97O9n33$v2gK3XKU&ve`miR~jA_wR$)-m?K8DPn}iGwCL+ zSY9}NOVawNgS0+sm)54dmf)8b7Y!%k5jzpJG!h&{e0l!D+;l4Lfgm3`7E<>frlWjT zCHG3AopdaJV~s5eX(IU<9vH~QIOE5ONSfd}%mezq%pW{_m!QMw$Tu;>QqIeSWLQ+a zx*9^PD_bfsvtBEIh$&Ab$28Ia4;CFIdpYA zKilAo2hEBYk<9-jY|^YeO^5D~A;67c=oo;gc&RSFs+DDKGP^j*N|u~vO$*G#Cj`;kSi0{Q z_WY`glN{C}T$rURTu0p`_V7}$#1F%(n}gp6fAnKHzx$6y1S{azF&QiglMzgPIe929 zrwYThET#v^>ut?Rk(Onm)OEU<1f8Nl3a+{Y=>T?Mkq?-!c zi$>H49hKHnM#a^cK{_2tAncyouIVW?{KT})S>=4CZ?adDiefJph({jUS012GY=8%wc=qe89+ia#w} za(!EhfY99(j{+vQN1=&Gu*mO!SA4%&O?+!Kb4JRgA*O=vuP{#eDzjmh{|e;(6I5eP zc1RdiOaB2XBaJbXw+{vpWkOni0DSbnO0BF?_eksyv(Z0qL;#|1IwBy4PR6tBe<}Vq z`86LxkN)$-7^%Lg9D`>w&f!G;|I&Ly>7Bon;@G&p{rMgoI8W#ckr015O6)((mvN*A zg^#lCxneN=^tat(u`0q(Pr1jBuK!n(6_v!Xaoue#aP=<9A#%z8NIh|XZCK>*JAh}e zEPk@Yvzh}etxvk0?#qiRe>3fJ=K|fEbG$?Kzq+leZudqt{&~I>iMT7%K3;SRTFegs z#cx4J9$B-upktRfZ~GQ>kf5b*LF1)l#?m>cu__S17R_MoL5+P#b4U6UqL!pfLGnjd zcDPWj>ah`!g?su^b^D!vYVMCYiWRO@Gm3@rj~OU@jaqV}*QgbYdn%c4M7Ot=upDJ@ z1|c1*%dQF$n)xLCHO*ZXJuGvcjW3g{Vup>rEHf~HRHl+BidDu4SdG(!Y@}lo?TJ#4 zQdy4PLDqo+I}B4NS?+508Ele`86Hqqq<#$5&nA!Cu=~VtJ%p2yur%Jc5zDG z`#9m3MCsenyE{35EM`-}>Koei^>vg{R7okNC6pk;*+9n6nZ1Xd{hkf6Hf{&;0zS#E z_~Uf%vH-+TXagWiIJZ#w^5z=*Gwc(ibOF~g_=6%j%BEm8vpHXKXM0Ea=_%ARk!KZk zyZb^%u(AogIgNtM71Y%V48<_u7LJ6K_;?tCgUtc4AA|0HFzyErQlEDZ9;i?XR5)a7 zP>i>xQ+Tb&*Np4ONe`rOBcZG$)n>K>wkPQ&IRgz;b6+mKD*)- zKcg%i++vh}yB8;E`Lj>%lj2va7tV;$aZz_}800a{`Jr--KFE%&Q`^iThTLK2J|uSc{k%LW%1NfFUM%Pz zZEgP;6?Qm*!Yw#vMMTR}85Y0lW+rI@=o|x!5h+f!O)%&NP5{MySS=_N1@w* zZgGO&&XSMvGTWjv-k)I4hm0=5#y3q?Bda^Eq@mi;P60-$*ds zv1;mnTM6J{nVeIIdUtxMfY2_WYJu~mpR%4o@hW#2;^l?u2FgjDSVRkyp#=hB|r-%nI$-sxGfPD~o3@7z}PYd_-A01d*jNiEq)&w20tBh|R1L^FH z(u4!j$qO#A^xUkUT#?Q1Qsr@h+_n`zVQ0%Xk*Jn5vQWKUY>CwJ=b)-I3A-N_j&Ap|{KmqivsjRE2WjEam_H^{^G z`or`-Km7Kp5esrlVUqDKo<{v?RslMH)Z{8&CUZnbt4k|zBFAI=OJlQ4037r(oH?cv zmMJ24Y-HSeWJlazA1?2gb9r*tR`$)t*;IRG=S5Kg^SEUT{j$_0-V&$L`6O=Ns`D9c zU{}xccA9qVhP@UmP!Em)D-hIZWjP-~m_WC>L>9qXPrY(6%*tt+YdE)FMLLInY=Zo8 zQHlXP$^H#oJDH~X z@;doi+C*j-mt$P&5)|9PeBKj(fWMHEO$Dj9lm=3GRDi7Hu&Lu#G=EDcQ<}?NUL~n1 zO9bgbgBnPqM2>}d-qc|c<_V#VdxBIkOFz{vY1CG=DEvq-3bP1yTo<x}27NI}?DhF&acOs4 zuT;`)BBM!f;DYPn44|MqjXW1HuWMm4QFOS!`?Xu4`l!sZ>kiQSm)QiDA+Fm&oj`X5 z-u*QlWdtjR9!I%s=vI7xY0#P^0GbrUOkU3>t0v3yzNYpuKC#xo znPm$xhop=Zgoi8$Xj9899d&H_@l!abnYuBeos#1NG=0B9pzu?eW^^F^*pc2%5RSis zF+GRvLM;bi-KSa%j_*ngJYIC~9nF&^x|MjdoG8qBBt;g72^NEYqQe9Wb`vI8GEJD^ z_M4Snl-*`{TeUBz3>^UZ-yFVQ^`q?Vn=r&&)`TIBto(8FqJfARKmrl#DhP^lc%1o& zH-}wJCddju2q~yl{9wj!;s-N)m-xYP23rn=w(xBXVOzT#NZ~vQ{maOfj3F%I6Gx;D zv{(Si9@w5T(Ei(h2F@`%g0Tj*r3B!k?N4@hzWjXmkVv7ED)ErSmWwIYXgyemeaHP8xS8%kI8=OKhgR^s_vEX8pmW!&pRRC+eduuJ#xi}s zj$6a;vFc+z>qy4?$+0155R(OHmLu@_O9A9NN`n0^L`ngImhg9|N_(*V_thpFTeghAVao2#{Ku6Adv%@TH>g<7BEVCq4ii>Q-02~ zbUZ!B=XO$my?Rq6FhAh@G6$W7lpvzltY|TIC4CO>dJO=*AvjfOC``fyQUxqD zN?Ml?3J6|UQ_I}A$_#P`aa>H{i|HoucbR)TB>$d&>}$b~kh+k}#MG<5Uk*{vI-?Po zvain}*ALc(=+gX5-Yo z@CGY%Xt-%V63fltFi-m1B+~@a41Fk=T7~q@_=Ph4&$c|1tmSd8qsT5oNBXpZqzm=-QJR zYpYv{oLjmZY1Z~QOG{pGbtu0;y+S5ZZ&KF%(>x%qlj8Y|zL5v8Pz#sULp#f$0{^RM ztBMXP>T#`GR$J+MjMiAIN{mbph~}-L;DEP_e&0x6oyOj9S8kaVAf$Aim{t^bU9V&8tw~qgd84EnC{2K_MXQCu26L;&FtNHh?%gC;r^mKl+!{6my@*&d7T*3_Qhq3>ArWgTbFG$&%IyJHat_|R;*y-WgT6(5DD@BmnV(|vTd&o#TdLJfjUC+xC_AfirG11b za)NH`ykdX$>+=vS>pf`)ktxzv%gW*LhsmZuJq{iT>m?8S$=iZKzd-7r`+Tk|#6O<4H$n;>%&m*`_xvIq!%BBLfop;|qhh-0p{m^7S9sMBhplk;FeqvRmBAjF%)2nd zYh?Lp^qLOwp@M`ffTnw#ks7NhR7soqaDlgQJQX%_!7@gFD^bTp%qD;hc*#BI5mA(s z+ky~pr6iAwAqCo@#8nxmDP2FX62NS~D;XB>NSOUHJJ^2I!V;X=on4OFn^%*UEUy;!4V6H_zJ zi-T1&gTpn}K`ZKNR0s>Msgvi!2S{BRiRm#|v@+q1!aaXT?t9~FUkzM+>BGKQp8i~w z`prJ>bdIfmN0rY#Hk>P%t|gC+a@lFKKri(P#5iy?Q=t=Ddsx^}KY(>IFZVZY3RXsA zsGm)L<1b`oke7tq1`k@qDuhcc-|l=xr`Ee56lB*Uh@Z{+YjA{sAeU{a%2u5aLUCRH zx>jo)hJKuI($a=x%A!W707$)@OcK;AAR)}0=ON~#R=l+ydh2f}9E(NC$dM8vUlDMb zXK$ysfu*c4iIQ->NK;dQjP!^eajumf!(R)3>jQDF6`>|S4WtLcc~*$DYU_zoKenhC zUPsiM!Qs2oH<<1G&EBy#H&Vm?%2Ga%!Z!4Az;O(Z(B_~6v_~3X80Mz4*KF?=(rnpH z4+?Yt9m!fnR_sl@Y0?K5U})BsWm&dm$(H4bmtG``Mbxjf@U<1Qwqn*+%-V{%i50Vd z)}xo9M{8cH;X%aN{6sem3GYY()f#pe7*ga#N>M{75)4f4$>1x!*WDlxYZ;~I=g`9@ zFcpm}i3=&wAo3{}wixF$CF2F}p-jP5+X&cUtw??^prq^d+r#%}w!+G&P)38)rPY2? z@g!HQgc>u!EMi8(LvYJV2r&}^?1Tz`I>1htWda21@9@u=!#EdEG=2CHpf|&J4Qn-9 z9Zl0Y!qyxYT&z-us+q9fkvy8MMI+emY&ASGQV~wjH+<^v?)LV!t6sb95@Uht)>+8u z8XEzt-`W0T?~_M6k9T)xYw`%o)Hv=v-6?O+K<&Cwkk2Sw_nv-1c$b23?jCG^o#lBk z$VNtN=jT$FIpid2CZw#j^LwwIvnnWhIFePCX-RsxLn4w^nuz`*-_{Rm2_hF*6s?}`Os!~#0MrJkFWfs?*gp%g zDi}NcJ8Vx)9!9F?| zxVn(3osHNDok!x*29f}+X5vXULc zfbFzPGGN!Y7wPz~G#yD{K3)_AWP+nvY~KGeA@d44z{Ks!sEQjM9apstc<1E`cad-d z0?M5w@D){IcB3u`{=6`;-AC_@IUbv$c>v=TBSt0wSgyC_?)$df`p|rRXujT!{9o)w zPCX&_|JaqBYLsVxcXA41y-VrrQhNEc-mMHj>QwIyHc$cCv9LpL8@&tYFHAijYE@eNpw^M^!?5)qRfSJpb024Z!?K}d1HPS@S{F>x>=N68

0GX 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�+=06F2YpXY)oP5_Y*hQxGNfk_yG~Dy6VU9yb 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


3W}{6w)4l6g=9Xztn5l8U;At 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 From ef6885b63b16e35445e09c9d54db30eb18cced6b Mon Sep 17 00:00:00 2001 From: FlightControl Date: Tue, 7 Mar 2017 09:38:20 +0100 Subject: [PATCH 5/5] Updated mission naming, documentation, briefing and tested all --- .../EVT-100 - OnEventShot Example.miz | Bin 24734 -> 0 bytes .../EVT-100 - UNIT OnEventShot Example.lua} | 4 ++-- .../EVT-100 - UNIT OnEventShot Example.miz | Bin 0 -> 241674 bytes .../EVT-101 - OnEventHit Example.miz | Bin 232862 -> 0 bytes .../EVT-101 - UNIT OnEventHit Example.lua} | 4 ++-- .../EVT-101 - UNIT OnEventHit Example.miz | Bin 0 -> 241720 bytes .../EVT-102 - OnEventTakeoff Example.miz | Bin 232049 -> 0 bytes .../EVT-102 - UNIT OnEventTakeoff Example.lua} | 4 ++-- .../EVT-102 - UNIT OnEventTakeoff Example.miz | Bin 0 -> 238278 bytes .../EVT-103 - OnEventLand Example.miz | Bin 232596 -> 0 bytes .../EVT-103 - UNIT OnEventLand Example.lua} | 4 ++-- .../EVT-103 - UNIT OnEventLand Example.miz | Bin 0 -> 238919 bytes .../EVT-104 - OnEventCrash Example.miz | Bin 233818 -> 0 bytes .../EVT-104 - UNIT OnEventCrash Example.lua} | 4 ++-- .../EVT-104 - UNIT OnEventCrash Example.miz | Bin 0 -> 240129 bytes 15 files changed, 10 insertions(+), 10 deletions(-) delete mode 100644 Moose Test Missions/EVT - Event Handling/EVT-100 - OnEventShot Example/EVT-100 - OnEventShot Example.miz rename Moose Test Missions/EVT - Event Handling/{EVT-100 - OnEventShot Example/EVT-100 - OnEventShot Example.lua => EVT-100 - UNIT OnEventShot Example/EVT-100 - UNIT OnEventShot Example.lua} (88%) create mode 100644 Moose Test Missions/EVT - Event Handling/EVT-100 - UNIT OnEventShot Example/EVT-100 - UNIT OnEventShot Example.miz delete mode 100644 Moose Test Missions/EVT - Event Handling/EVT-101 - OnEventHit Example/EVT-101 - OnEventHit Example.miz rename Moose Test Missions/EVT - Event Handling/{EVT-101 - OnEventHit Example/EVT-101 - OnEventHit Example.lua => EVT-101 - UNIT OnEventHit Example/EVT-101 - UNIT OnEventHit Example.lua} (90%) create mode 100644 Moose Test Missions/EVT - Event Handling/EVT-101 - UNIT OnEventHit Example/EVT-101 - UNIT OnEventHit Example.miz delete mode 100644 Moose Test Missions/EVT - Event Handling/EVT-102 - OnEventTakeoff Example/EVT-102 - OnEventTakeoff Example.miz rename Moose Test Missions/EVT - Event Handling/{EVT-102 - OnEventTakeoff Example/EVT-102 - OnEventTakeoff Example.lua => EVT-102 - UNIT OnEventTakeoff Example/EVT-102 - UNIT OnEventTakeoff Example.lua} (91%) create mode 100644 Moose Test Missions/EVT - Event Handling/EVT-102 - UNIT OnEventTakeoff Example/EVT-102 - UNIT OnEventTakeoff Example.miz delete mode 100644 Moose Test Missions/EVT - Event Handling/EVT-103 - OnEventLand Example/EVT-103 - OnEventLand Example.miz rename Moose Test Missions/EVT - Event Handling/{EVT-103 - OnEventLand Example/EVT-103 - OnEventLand Example.lua => EVT-103 - UNIT OnEventLand Example/EVT-103 - UNIT OnEventLand Example.lua} (94%) create mode 100644 Moose Test Missions/EVT - Event Handling/EVT-103 - UNIT OnEventLand Example/EVT-103 - UNIT OnEventLand Example.miz delete mode 100644 Moose Test Missions/EVT - Event Handling/EVT-104 - OnEventCrash Example/EVT-104 - OnEventCrash Example.miz rename Moose Test Missions/EVT - Event Handling/{EVT-104 - OnEventCrash Example/EVT-104 - OnEventCrash Example.lua => EVT-104 - UNIT OnEventCrash Example/EVT-104 - UNIT OnEventCrash Example.lua} (94%) create mode 100644 Moose Test Missions/EVT - Event Handling/EVT-104 - UNIT OnEventCrash Example/EVT-104 - UNIT OnEventCrash Example.miz diff --git a/Moose Test Missions/EVT - Event Handling/EVT-100 - OnEventShot Example/EVT-100 - OnEventShot Example.miz b/Moose Test Missions/EVT - Event Handling/EVT-100 - OnEventShot Example/EVT-100 - OnEventShot Example.miz deleted file mode 100644 index 22f687d8a4844daa9de69b17f0eb63a8c7c47dd3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24734 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 a_hms*2JYp7fPzAO`FOv~IV0=+6a61h$9nnz diff --git a/Moose Test Missions/EVT - Event Handling/EVT-100 - OnEventShot Example/EVT-100 - OnEventShot Example.lua b/Moose Test Missions/EVT - Event Handling/EVT-100 - UNIT OnEventShot Example/EVT-100 - UNIT OnEventShot Example.lua similarity index 88% rename from Moose Test Missions/EVT - Event Handling/EVT-100 - OnEventShot Example/EVT-100 - OnEventShot Example.lua rename to Moose Test Missions/EVT - Event Handling/EVT-100 - UNIT OnEventShot Example/EVT-100 - UNIT OnEventShot Example.lua index 92b008506..1ea3b6dba 100644 --- a/Moose Test Missions/EVT - Event Handling/EVT-100 - OnEventShot Example/EVT-100 - OnEventShot Example.lua +++ b/Moose Test Missions/EVT - Event Handling/EVT-100 - UNIT OnEventShot Example/EVT-100 - UNIT OnEventShot Example.lua @@ -1,7 +1,7 @@ --- --- Name: EVT-100 - OnEventShot Example +-- Name: EVT-100 - UNIT OnEventShot Example -- Author: FlightControl --- Date Created: 7 February 2017 +-- Date Created: 7 Feb 2017 -- -- # Situation: -- diff --git a/Moose Test Missions/EVT - Event Handling/EVT-100 - UNIT OnEventShot Example/EVT-100 - UNIT OnEventShot Example.miz b/Moose Test Missions/EVT - Event Handling/EVT-100 - UNIT OnEventShot Example/EVT-100 - UNIT OnEventShot Example.miz new file mode 100644 index 0000000000000000000000000000000000000000..2d5fc77b2234bc22d9ecd070df4bd186cd49da12 GIT binary patch literal 241674 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

u!o8u#MEwy0X*1u1AQPJ+fV7_)oy?L?hV! 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

FO4V`LJw%mi%5dKzOD#LH zln`Jhln`Grt}MD#4Z|%lzEnfL`W9oVDQm1S%2e$}x9m7mjd!_kv8EP}H5HdL;doQJ z{_-PE)$mzu%qd2@LDZ>+4Avs@lxIha@R)CGdzjIuTr}IppK2SQr-8MocA~yT=~*O7 zPh{OK7|=u-`pO|qbZNwjw2Um}_8|%~_dj^dnuj*Y%gRMR|1A4HfJ~wG{-V^d{yx&gK8tR$w>u|NAP@X+kW}oC8`%sHtX=#^ErkmiEI6i1BbZ z@9{>)a80rFt3E!4tmimY$O3dpeRu&fR<%m&V%2t8*sVrs@~=8x~ejQHd)*DK$> zc@t1or{?tGIJv)^d3c{T&>hfReZ8`q_tM>RW6Ukj>NoeZiqH{d_ z#Ggbcq|-io6AtV!!3EfdD@I*nT<-&V%6aY0!nU`}&0Y}rJQ{pOu*`P^9)lW~CH~}+ zdD&AuQSiH3#?yq=K29hRC&riIhY9W2l3@t0)KKSSN^vh%$CLi$j>nGWmktZQb%?xn zpx&CicPPJj2(^vPSK!@4tGR;q7GK|qy2Tvn%c)wK5x7?=ZEJxC6ia(T;qPY*u)LdL zK|ojtSrOgI%*za~mu~FwMfAN!<%$Eq5XWA@?DIIqxoH?x!4! zdzqVJ*C-xKh2DoSM~QNHBjVJL@Ri$XTpGk)ig-Gp+bKU5aquW@6D`5TW3E#U|K-o3I_ zIKFci4L#qvcA*{L`FGs&xlHYn>pK&ufvd9y*38#g6VG-b-QS&FegAh^7;u1h+Xg-0 zohsS|-udYJz;hux!HcS{7knADh#S19*wvKt9pPm!b9llVZZMAUbMu2XKru#7bUo^6 z)hy!l;yJ{}8Kqnvc8RZJmq0Dy6dz|Ar`)>5hgm0vY^7tosNQpUr4gsNuJPR7^^Mmb z!_M)w!o#8zbdUF$$vVjEtt`S1fHPO=B;LShf%2?mV2Qd!0pvgG4XJqSZH{0r)PGYDZQtd zCO>`ljuT(#*b9D2Z*#>mPu?CK9G<@a^ZxEuAdgAqYV_MD_~Fl4e@mTq1K`)QmQ6x9y*?*G85KoDn#mG;${Fb3y2g6-F-u13z}eLTFEGeL*V(KE|&sEV7Z^z!)! z&Qu9g%bIr_IunAuIbX)YXACC($!Y+_J6iS2WRqGq2F0it%+Wi;=S^%ukYd0mtHzB( z|9P@n)g3mkxnegl2d&?Ie2SYGZR^x*d+Gm)y_3~>EtGu~3)kuH?J0Y#XUqn8KACtaGR zY4)y|zJbXHP3iCuDTq3*(Cc6f*3eu`FL7c{scxhm%LItm6G81MLg zipLy#S73hS{W1alcRtb+{NO?IqzAL?$c@9|JZ8nz)!U)NOjQdw2WX2oV)#84gA?A% z-E!@cwaAS2US;pDJF8o2DtpbfQI>%&wi&dty;?OIj5dn=dO)?qfo`lOFVpdS;$GYO zdpG&1oBRxehRMsvbk=O10>BJ67zS=(kqrn&-0%twscKJ* z9muuI0u~WD0+1C(9)@(=-r}YPMB24@SPks-JtelJ2?1dMa`LTA35{qZU48dalNYv( zzh7g_Tjldl@y|cQX8>PLaKXIIhF3j{P`roZcn`<%9*(7l{$)D8;8}8=@et+tM`THI z?e;1Zp=#{*wH!+DXw}S1W17Vg-Wd#H2*c+X!tfapCLGZ69SqQ~QqIp{8Kr%+f%&;r zV$zvHG8~o~3yLf!3eX~zQ(=p-xd))&DF!vs5oIAv1YDI9SD}bAFdSttI{+Hc&(KNn zKE+FJdKEkM1J@N^7#Hhk6CH1V?<7wiKTcNh_s{VATl@`-g5^RNw+1&?C8z^L*%hs^ zlC-30!OsoF*IP>I2PSxa;5YkTjt*eYBNDwzfr@mXFe&AlO*T5q#Ft8OAV=7fT1gJK zZ&#D!QSm10Cg(%w3U|24uo%FPd^ntsXbmb1XHHJw@kq60naev7wmE({D@JFA^sq)R zE?~nM^pYRS@Yq78X-i{CXrja>MD-M{h3g0_kia}C7&(P51)B7;DTSQ$i=o*&Yrs4O z#%us}X8E?J{ZTe(Xg)O$dDXN&oZ`xD08mhcMi@U{=CdXc@36cP!uQiG8#h3>z;Xk@ zn=ROOo*`E>%@$QK(20j`G+P8lQ9A;uf!fsirwsY80l*biXoTTWI|8DS+62L)b_9Z= zwnu9aFZ;Y`o2b2SR;TGf7P{)gJTWX($IX0PavJFB4F730IfzJzYR3mk5IhArVET9R z#i8IvM?W?qy7)#!7q95gH&n?NYUUeN&3vO)Gba$2S$=UjQwmMe!PC(!1L?eKUD4QZ zh*DFke}n3i^@#Jam=xjunV;u|qnS>gKD$xTXV>fh7gKdNZlmgMC{@?cri;b}1uTwn zFu9CNMijWoYk`VJyb%Cz)`&OSX~a9jSw5TNw)ay^F*bcg_v(#MH{KB4xGO6Pj}#a@ z1{7ePp#@rV9Gg2;o^=;D(4jZtIy8L-(D8zwmeZm);#xF)j`tzz^>3#~Z^ZRz`W#1D z6ztDPlis*jO?pFV(i@>B{Zl8|c>LI@(i>WpHiB7v9XqcNlHKP!ufE?;o*x{a93K6& zw$>o_{e=Mfb2c5)X_fHGu((JDMSniZ#uAi)9dKdUOI{B%JP80vGho+AAGYW7IV@Q@ z4YV5UxR^1Wpwhg|1|B8W5OI{|_%a`bf8Z_;J2A{=$)OKt#c;rR2+z^Hwhd1?xNvY< z*D#-@!@M6hrT;t!b^Gj!r`rx!A-3`O$-k~Y-h}_Z#eDD&+D5n5)~wdhN$k%9qZcHr zFbvw9ddVU{p%&1ov{e8$)}Q<(1fVv64lz!$woc0jpZ3oZ;_viJ;e_4OB5rR7b z{ki1(m3ujFb|4ffLbeIe<3De~?g4__T7$H<@JCmjpUJn41Oz*b8r0W|E8+&DCG=7A z!fhJ*&a~b`bA6OehA@L>y(Xsw`IL=I6ccM}_ z>}|pa?x5GA=MB~K#=@RAZr$?{(6AUqM$QHIF#et=W}LRWL$1qmEAc1k*$_Ez7dG)} zF-*3Sy{ybH#>u}VucyTYXf!bI7op-zwegiMFw`B`TM-I=N=)DQamntkqpN45e)VkV zDc$RrJ!?LgjiF{Oc|l4|&3Wey*Ok}Y7~B3e&~SMk1^d z7b?{yq(@3j#Vi#8+zM(4GT^(A>%6L(7XnU*jg!y477kM_RPHC_&j@!ehZgPt#x(D7 zn25XcCw582r|ri@sp2rkrZxA9SxfqhSm^YRTpYx*SH;w(n{9#~*)*D<|1f!%P6uTR z!2Z_K7VRV4l>K0>=$Z7ckUtQ_$TwbfHH*yuJM1{)LC{SN5+D z@Yjc?!0N?(!A2qJnH8IVS#BXH^q&)^@P_ut-GY=0ly*&CR|K>{cUbQtoAC#3ZL0e) zwu8UH3VrO?@~R}?`1N(Pq=}Rx79chDy~eo9->$+C%MIp&MIn@JW|NuwE~aynL7jLD z=B}o5v%^4F-J^9O?KY8i^zW7Y`yq(RGc;?uSa75~6;;2ScRJ4)ofp$l+D2qbpH;G1 zib03g^A;nAI`6YX_?Wymx7tvInBOEzw#~}F4(^36HN=fu4^gf0c0=4~GekYC%D`f~ zkJ^J<55?FWc1K%;{ogwwgZ19mQ9rhc-4@BKHc0+dMe?c+$=HT?TO>cULGtG+k{|pF zP1)>j=32^X4cQ=^UaPn<+|i2+8+vSCuDzQjL%B6a_(Q4cO3M%Br%rOEhGLb9MMJqY z-}pnRYG=z2<>yYKwgjgUHRVzeHSgYeOx*6`k~is5#aHf2o@GYO4)2f`<;OS8OsR%{ z?|j)f{HxmxKf=HAQw{UTz%zhuaK4!t?Uun?~N39Ux7nl?fAFxmI1l|J3eF)*rlaFlJ~S zjYn7a6DXh`>nmRSrU@_zr8L?|))LUYE_;*Q{Nf4W%~4WZ0El#mSCG95z~Gez`MItA z@5vJtzU^RaBRL(<&$BaO$DeP3bKR#`>)%2*p)sVEXv8DtFHUBM6bP| z`HerDIzev)s)o7NI5;!4GA!Nj-;>84?xrf;{C;nU-!JePE1ErruR`V<<_6XjS(~MH zWj=1%4)h|qU9N^)L8j76wRz>9wH|(hC{WJ9Kc+xX=j98efIWe`b5Y<;jKjzGaF{t= zP9!1tW=X&K2c6is%VM#l-)uL%F+rkM-trp<1JkSbr;1qDt-7ATT8-edX$6GC##q(j zGf)L&V#fr;W;+4#N9Ci1idRKIEWttHnh)Gy6>e$;TCMqD&Ib8ZZsokq@RUHjT2p@> zMJ2t7(yPi&(&{y8-7^SV5S1ZRd~-E(@ zZX-2+@@oEsH9y2_{_NG19a}B9?=i#*tb(Z-H^v;J#xS-EW}3mWAEg)LY?k+vF&?hm zh4*IH_IV`zQ7BsslX|k#QBMVu)@Yd~BCiY`obQt>4bxc{evT;}Cs ztSS=)PA5f~&s_Z%Q2($4a6qO$Ay|6KNy?@m31FvH%|R-zD+GHVx?bsfzw~{Zm8Yi9 z#@(se;26NjQeD~yUFW&K$(o9AhXtdVBR0$yOVV&ffSa1qN^jfVlzHuNmi!Om6Iox6 zfT?_P+IJK0g27N}`B%?sXxEoE`tVGBGM^z8nSK~e2MhTzisX$osL_$jmYCo)pZ14- z;y)usgI?;qy&HSblk9#rW*|AsX74i2T2htQw3&Yfn}gl8?GNU@a5VM2x3OoM*6all z;S`bHp`1D{=F@&wE6ijpouuQ-jdj%!xi$Gez!=~7wLbW@K1eP6@8jOLb(6gL&pgSS zzq?7k@@rk)niXDCQX~pme43?{Kk5Q& z$2Qv_P4Gdx)+fcQmkQRZ1LHfgaA+nk zKJC|?W<_awGeEkw+oft7npSNFafQdc-C7io^|y& zTw={_{e|`{%Vr)tB>^&(8`x9nfeB>rayjgCFrfo3ZJv#%M>G%dt+Aqc2m4(cd)g-1 z@da@eMw0>H5V11Tv|p&id1qcNcMku2y9^Dq5r;>^`hE4X{j8o9#V||9Cb$|ys{s;{ zgW+{K&yjp1$TbJsvP=vlcid;=w)scsL?|iOZ4mTRE;}5*p zM}K+j>|dpj|DB9wq1Ku@ZF9=ZH-Lg_@pGgFowIo@ey%%gk;vSWzjys@1>mlRX$d)n zH(k92L!CmtDcgn@1-X=@kK8CR|DJfeHGHXqA6_l7trc!<0mxN~9hc=)14*mz_j>Gl zJjsC1_`f^(w4@)vt#3w;{Ge|uMX2tJqEE@7oG8K7D6LDJ=QrlsTqMh_KLatoR>RAu z9YbJ#o31Nk?J_D|52NrfuYJqlpkLh@Flor$r{4SOf>Nbd)kURBfkXU*qiXr-j4$HY z`xd+qR3)ikN_BH{wZ`0H2+Dbqwq?#AC7ZoJ$7-DN9I8BVG@HpUE3>qGvw}I!`NK#4 z2mklWN!m@HD0)Vg zZA=>3k~@49$sb0vBubc&U_=80gsUiU|lDJD4yO%kzz6YdR(@mU4)jVOERtty{wO1;c z2J{GHBgw-S5toj4WJvdw%SyH?m!+!3EVjH39knpAyrZgBCQ%i|C$D4>;l%kMwUup? zf2kpT?-EiiHA619SY&SK4>LkblO9!XgVd2#K$k`dnzl#ih_%i|==|u?el3p>HExd* zRd0jTp-LXNJXYMgJz8A94PH-e3Y+Dz!nW;k!rB2wlvCi27j;9*f|w8CLq3|%E|ZQ4 z#ad0wCo(bRw@K$q8RG0|KIfqsWasG|3=LBkjIzfy3Kb#LQFwK2Zxuz#)~ORG`30aW z5JRC7>wyvynej~7j~mX#O_tKiewlSKsjNo6Vg9Z-nTy$y*N z!csJ9vuAxiB;rl-78D`?OA+X1`=?35c^h1h(3-1aD*puOEH<>WPes7@z5EKXVQjQ? z3x8H=6C&9SnxOj^;uXVcCY1Dx`MG^<%VZC>DgW#8QwpjSsH|=PCO=6&c1}`x#Wv7v znwoansV+lOU>9FbkH-IR=!^|FO)u~ftj+gi+yb0mR<7vhm5%+t&!hvfj1r;WJIF+E zm8=G9j9`UV6)G3W;znU!VUSXrfwB8`S!p5DZSGXaP zi_$A>)TCGVurTUI5Txx%JW{tsmSIT;_2}*lXCA`Ut*S2nMm03zZn)#72^ng(>-$Q` zu>4TkCuFD}N^qh5L)(N5A*bD97&fx2%4Nv`y~TWqN@Qo8g9->X2Sry)WS=_zgJyQ>1(N4Y_`BRI>Ej zEdv+rNJ$(%`9k1~-?a;28RiF)>UHzDj=Wa)^J1lJFusI9+9bGsCi3g{s{UX%zFy%xZq%NykBr@6(ope-XBe7SLTvKlWrRXmzx;yhc}HQ1)Fv{yDY{S zWpTSe()K&)XBbGj(HKa&Q8OjC4kZ1l8q;?FfuujR3MBohE|7G?1(E`&il>=5u=e55 zm;1-ZJKyhfLTR22`VT!E`Z6oa^dfs8mG0r)P--RJB*$0fEE^$C+8jMSUVkGt+OJ#YQk<#G4orAWa0+}88o`zb?ivd z>}(KS>U@GTnwK-neeTuZ?>^c&MKMBu+SxK+a2k+#iLV76C6ZWQvo9A-vwl7yHu5&a zip3#1hCo8)0T&Yj)0g93YC#d$hEldSpW4@XfS6pCkBvk_NFWFChTes@ut7S`hXdTm z>*Zvx0cAVa>MV{>kZSUhj}d_@Mh32gW8?fDR)os=93z!7NzU^rz6?f08fytEuN3h^ zR%x=bm8=BXgvaJ{yKMA%LYdJ!?Zr8>qReg2)RPamWa;LTJobEMg$O;wGU3(SrLAS- zTYp1A>4|HHUb(|TtsM7ZrLBa09PRJb^+9K)P%QnxbPcb#vnP)ib+*sJr!VsoBN$gi zmxNWh@?$=(Ai)dbN$p@oOfEN4)n|O@pR!jqquXXZumZJm!m}x|c`JGP;)ng@GR3Fx z#$PAl+hfscS3teyEdpOt_PVe^`s251GeqBaw~TSx@~(x0ka~lZ*ZD` zl`TX>z;SXJ2ek$!-#!pK=PO;wZj9TNtrA4Yw?XZboYa~b{4+%_CjQ-74J+Xew5>S+ zDgAQ%~yB5-RKlFt4meGDY9*XxN#BYFrgcX0`r`_ zlKeJ$7umYN%?~!-|8J$cva%ZdNtvf^nOyegG87}C}sJ~oQ=pQ?uc9-qQ(I?ab?3U zMmPDyvya^_nt%ES85X(p{^viI@LRD!4Flk;(cP84(SYi;i)1O)AhlkCc3J_Ma{(4I zOe(4|*r@DIEjDYdRkspzC#Y_J{7i8AoTwIJq%e*M1XE=8l2vP0Y$@kpNAXFS53(K& zXLV`mM+QHP$pT&|2k(jZ(d!Ur{ZHwtXkaqA$+&>;e_M<`AA$w=IBX*vPj_6F=Xe~; z@(iCY!RlOK>mNg}o&|d@w3*Kmc?#axw0wFtpV}}0JXjs1Qz+?A^9k8)+4utL!fNox zb=|w)P3E&-XbWfjGg^Yn;QILxxw<$Pjw5FNxHkS0@eHA`MC>8&vv>(VGaIIEe4niy z<2-nh@{R*_z^oqR=70_}oc!M8UK8&foL6SYjn^}$BAiIZl&enGx&3RK)4?dWbah9n zz+7N~)K8u}z`H*MEa>M`oJ3Vm{zwqvsUOKt=y!oq^&6n@8wd|_-CEmhQ`L~ooeDKXmis|6|M|oI4=Pe7 zjFq8cEbyvtbGoRnl(&r&EOq&_7E&hj()#+far1{1C7{gkN7?x_D=#tS(DZFOWd2VN()N=k;?d*8>`TpJyFZPd^WK4m&ZW=nEA$g3lzAkPn$0)FB>~vww zLCQ|?`K-WnXnlHVO15N=MGPT0j4(dqA-bJaMd{=jMZmms@j1B0?Wq@?TBU9wICVjE z*dC}yJHrwc-2(L2cbA0?q2dTec93^=cWi(t$HGCwRYLkJjJxo78C}ZWhrM)`BDhHk z$jV#?+`pO(3W7%9YgAx5C1Wy zgx`^BZIsbSGp&;{=C#9=)Ol1(xb`b^j-d{?Uu5Sqisp|Yi3i}{G~u^^U_FD}Q?Urusj`Y)g*9r9ils`m&klDA z3z*yM<>q!2Jc8)z57EV@cc5jLnh$!3g(m0Pv6a^9fI=OG2LpyxV$Lo;&&6ks^#4e! zk9C2sC?Mip0vlKVjZq@aF)A)ARJ;BC$1N0})BWI%Ue7V*9d0Lgxu2NxUsJg@a3f$> z$12*z!mU~V)t`{J-FCBz3n-umUqLW1YgV6%8;Y&KKiC?Ku0kC;rBFdX-Ei?}ZSC3d%eA$n13%W*@-c3cYiq}AYqOMoK^biCU`wnDM!1XyVC%dEq^a>g zPN6xKnTYKN4?tNaNXG=_B}0IX7OJn({tu!fzd)fd0}B99Nil|;d}HV5d7m|5lx6Dx z7ky@{$z)peGwg%!2Nlyd=g>soUHST#rzyQEdoTjh{MzaD{_z^D41mSqH2*+k_ww{f z?|;Ake$}-z_n+uLI|^l4@*M)@zT0a<+@*j7Fjz9oOS*88gT}`s#g2d<@%0kIcxVRA;pWgI z-qQ%MgI1(KuC0+yIw{IB$7J9hb_TgBYBZ)|1kJ>^3m8F1?+W14>5Ndqs|xa6=xaeI zQ&?Q3K@gJ=`@riP!o&^0GNrb*5eGJ+mh5V3ZbxP#R;5*0&L@*%iq~?WgUdXdrc>ZU zl(GR9#Ky7JiJcQ*(j;=Q)iJ6KmnB)TKvLlOL6W`CX+cKF$4m?>pu|U@NvtrwSk>Z` z$zo^I!LD|!GtJgPk;i#QtN1c~D@p3$dp>?k*AOnza?Y|L&Nu9Nv`W$okY+IR#NRAf zup!RmFnjL=hbJn26akX#zfZAVy|~y2#03&QJp-d68RY2&-7LC9BBcrZF=3)lvxx#F zM6dE2-GPMRVnT7ZvM%QGF52_?2-s*=yi2F#U;G)Cbypm<1N1Cj=6p>vD`(T_LMon7Dzklw3>NO5CXN${gp@Px(4Dn`0kP z(jXnjeEsJVQML^R*$lTigB44hodZG-NCXUvP^e958yUQjI5IL6fDQ)LH{Mm-j296? z&&TKKc!u18q?rtXGAtBf(4(_-`i4=z%!YZtm>{p>G~`nr7W{sj!8`yg3wxN&1asK1 zro~VO?(+ofm4}DN`_LEYY`8xGOU`k>qMLmsNcAE6rQE93dnxUtW*?QMXbwAB+FgK> zMSgSYCZ}atB1d6T#1T8w&BiLsk4e&zj;8bspLu}AVOHkTE4KszyE$eW{$N6?<9J#O z84ZyTe)q_mfZTu2^Zpy$MCoyfU>&Zl9eG^0ewL!x4MmD{2ec%~D`AUyT5bodZ;{?7qwQnmNyXqzg$73v-AqN!bIA9nyHe z`$+Mc*GPv)bhq+{R|hB8bpNl!wc|KY=3AmD9zaK@yN9n%jt*bE*m?TG+(GZfF+SNj z{*PsF&faJJ`HbJc{_Z2TzLsySB8Wc)C~J=h8Ds9K0cVEQgzL`kNiC|!^Rx9Tx?PXP zB&17u8wvT8*w9?JKBL=28SjiTG<1%W1|}c*R>pPGDTl>um4uO*8FB~zSCNlb?HnPx z4Fi6Cbhrz~z;z{)z+Q%oPdhjYI$2(nGW+#`J|)gXf5JP2G?i<8W0F(8 zEC|}dLh4WRGaeo*FkEd=^ygX|s@y$)@Zc0Z@WT`>xvkUFh?+BiZGTuyMsj_HMFyU2 z8O@w^E31V2Y(}C6*lD*I<97KUS3@~{JGl&7q7cDzetHZCn*MSDi*$bWYv*z{o0MCR z9`WBjkh7280GvL7d^DV=>jY?B0I}Y1@Q|)0qm&|DKKDn`nWb+`uBc+%&nB}~Nh>18 zVMVF)kOiI-Wi_956H?O#{;i_D-DBJ-@Lqw*V7xQngFrkm=`wsN< zOsv8u9W3^Nkw>6Il#xZPCRPkAo@7pGfxNmfkA9Lg>o?1eov!R+X{=?L>um}91tXi4e_nmvT5$hWwq?AS7@ z9tg#FmyWcrc08TqA~9C8*^5O1H48YI%%^Dlpa>#6Xl(C-u^t~a>OoIO4JDi}dnFwwg(j zO{c}w-oBrvc_|{-C}UJq&WeehUrviuOEpdMX~y%M4owCRj~>b(C-x20qOK5(_h=jJ zbOp=my(Ngj)1(8=IV?h=FVR~vhaEOG_O42iCjdl1yT4V;*u}AMX7%MR(2o-tI9|no z36=Kf#DT#f&Dafr>{(3m zkdukp9+i1#^hiv@Vx=QSaT*1kYlVm!Bqe^SC~d9^G>5yQefl^N0;mYIcV%;ICsSwwoZ$+wHHvDPDQjhyqvIjuD8EP zHJ-6Em?y*0qx?PVU?bRF>Ev((M)@V1VznCn;e=QGe04* zt6B(hTj*JoU2P_knAIJ^8qJk1<|%-}cj@qryf?(#qw`IJP+&kJ7Rg^G(@P>Vm|vwB zn0--ZaV+vMD3|FBHq1a;ii~Czum@_WqsDb-)qu2Vi7ZovGh+e;5K7{A;dzY14Q_7p z;rN&wXR~9p%1O2WCjQ$hY_K!H;@q0CWwP=LxNXH(k_I9{=%EVovatuSvi5i+gjwz! zf@8;;VzSz&(4Uo~&0~<@uxD&zEr~Hh7K5U_Y*e@w@PVstc|+?YD~IRjCekvf=}m{! z#ISiPJ;YGWmBaB(+P=&OAj`PCA}Sar|v+9U7!tOF4EbNe8%lX9F-CfSU{_Yk>o za~%XrQZm&mQ|LKvk~MP}gL!kP1qrpoF*}>W^9np^6Ve)yR|iDS^w-vSQ~(cFM$Ybt zCdj6GGju@l{Cp9PSVMtv$r=jEND0^pYv!dHGo8g46zD`jpR9%52NLtR7?Dvbb}DY* zWM3(gI~?L`s}z4At$*}m7_ZAYh?citE~KJ9gwsfZ*jfaoAsC)j@%yrL_J>sBQyZOZ zP|{a~8|+hpf4__9qZMBRR}8 zTlBuMGtV-bIlIFmQH*1K&{O1&niI0|+n_z}$Zg|n8Ht5p?3d6ZA2S5Wg~-NWqV){} zLz(SlN+>25?ElRkc38KeGoZNUfgDpaHA?r6l%Yn?I*oWO6y{I}M3}5DPd_Ircg3i& z-O*N$!=qb-hvOF+Iyiu2hvR+3fP8e&VC-KtWSe9RHfb-8f@rokLE&izQzkgAXQvtm|FIL|VcdTx?eui#dG3GmzT9Y zfik4WwNGxkls7%xz+B>8MJpz7FFDS5u^RO?w+{Q{7Zi<~zjhwbzyDZWCoRFmbC;_T zvuATP@J47E+SXzFZJonbvY1+-#sG0Q2@GaK4vbK-Fp#yTy!nv9PTP?P7}t9ZbaWQxHdFp+P=@epfP+-g!b<&;r>0QbxxlT{tC$@vY0>nf8T<%|-WwUn~ zI{{0~A{L!6njzSRis?+!Qi3%OxZ=&q9`2(WBXS-x-Yc-5t*B*Zz{-P!=75pBT$rw$ z2f^5ibD4!Oz@ijPxDN9%-XA?y9oFLv+u8YiNPa{P&Jt}>ZGUR!SlFQembE&>n1`vM zJ$aEvle8aN)NVsI%N1I@l^BvCC{JW#jwqwGoMM|qv*+gB0W%MpkQ78x)(zBmJ@fzU zwHjnK*{X!!0$JNlc3A!>OHO6+`Dlu_%@2IBL{*gtOZiLvWINfmexiyz{{Pltki;Q{ z7<^L!CZISCWn?_;T^jR&pUh2H%s7ucYipgVu8BE>{qsv=#EWcZ{Ntn@I>n-@9!ii0 zZ}#snf`t8$qk`Pgcp%)x;FgCNURt~{Pm+-q{wLAHg;xcA73pYobtgMK!5{1DLH7|*g39mBxeggcX4b0wN=bhTveybiCJSg3GmBm^pH1fC zbH~73USVX}oKMq>k?J?x0xVB$)P9pGV_g_8jZw-{8BJOlcEi8~FgC*2Lzwps5L^Wx zuuMrZ5eF&xV&({cqkmNnTTRaIJP@_6<=53+|4*wCqgl*uMUf0v8`qM~}{i#o42g zd?~;j`5zmPPoQ)7NA4|`kDyQZa~=MVPe$PfSpiswk3k*@T2jrS27f5zT2|`~=c&?w z4wRD$6JBtt0Xig`A^Q>SpQf|e-6}q-}z10w()%9hl<~ua0{KsR5NrPb4}|g z!e21ZR!y;<*U;W|w$d@COJ<(e@hQ0r zb95YXDIP9oF^G<07^iMhF#w}}M*lT7qfS#4GJ0J_)J3TG1Ijp0Uv%nXZT)M%h5ntN zi;2H&Cut_Pf~-Pn;KS1|m;lbua*}Adrf%vBizo7?c;cLf%)*Jpw)HppgB>Ywp zEg{#8SU-j>#9Pmkk8#+T*b{GvIA-fP_+Dq#Wzeo)enp~n$Ciyf?$E4ELVwZ=6jI}x z_Ea*fII5Ehqa>2UGwA#WUw%rADtzk%n${|>T^~nTL++N067Ze=`LxFtSTR4Nlk*(C zLItm!ufP5}S=m}y?bQrlKucZ$-v>tjZfaR>wOCR3%cQcpl2sKRjYkdY(8V>k{wh|H z?S-|>s-`hViP=i~sa(+}#*hFlFCyh|%;Te>hNk5$lr1h!=`b?9d0poz9sW9w@F5GnvBq=zGUjN8f{q4JX$ z=td^PelXzEARERNsHZxd2UTUOkOD37kSjP^IJSm}31P9VwT_DZUIK(*j8R~nda=`0 z&+q0|6LDf;msM@##qVDEY-;+^HkKxIObV1;k$)!+g_hGnCKev^^Btv}f@4d%Ptl){$bT2kLfKBR;dP3T9M$cGN^d?Hk6;`Zz2|LBL!w_2YoU z3!h5UHpi$oGT7Bcbmk?!_~({Ra_uynL0u1#N?mn#KdqqXQke^a0L{i?)?d*>St9*22U}w%K z++jQ3&bBc<_N?Drm|hf~CkG)In+Jm4cstC^D6u&y2#l)@WkY)X_ zDgEas_}}`=m+O0bTgmQ=o#W%JwKdpPOj@!mV~Ce2?$8F@%YJ~*Pm>xKl#fRurN)47S+{h|Ka5M;n6YYUcu{w z2Ty-EIXHZEtY;q`#^i&|&&a?k!+XcStgXGy-(**7YtD-c(qTR-dszx=wKttV8sVy( z_9mB;|25zK^TuB`zg_u@t1d zlR`>Z0;4-|&!1BxNvbFouE98IpLo&0qQph~Mkc0zG{0c-KVDaB^vd~JDKAj_-@*n) zjS1?S_+zO4a12|@L)An=3$}eg(xdkcCye!OSD=b1m32+osna}hCJ~1fU97k7px4va zcIymFV6Qq35t8mgT%-Yx^;i-w)}Xoz8?dP|jVF^dZ>_lb^pZq+8HWv<9%r&;1U_~b zD%8lW{&TBLNE?mA@!FzmDfqn#V+S?>!wLD)pDg+}%3`)8GF(wr&B_(v*1!AMlOQ|%r&4Bu>FD1|DTTM^P3%IF|YEZnH{kTC|LL%w$XA+7og`(v9n*Hsw=*eKxxe^sRp zevF74{Cr+wRq4!qrftkLS)spHT&aiBwufO=Bw3Z~7ifx-2>uCcZJh!~I5C6@V;Adn zFKR@GX zfS9ByW$z+r7E;tR2OIP+3O-XHJhE-WYTYBxjlqxP!4(qhR~yTVMv&>KaolpJu1Sm9lG-R^-ASU57Ey!P?)Z|*69hxUZ_=ktwHe% z{qt)?{k29=q)LF4)`2ZzWmHSG9oO=ivvp#8=F=f5(frXDQYFF|W59sGgPT|zxmD(! z8gKRkd-Td3>xXIRY_^wlOaT8h8)k1)>uKlYJjVR>psv>JWn|mYtJ^s(e*7gleymYI zXqYoyiJejjV5CFis| z$g5YE1OorjzVVmkav#ejuBEkKLJJy1lr*zpZHJp~WcLm&zK^Q4*)LByU%z!t+3);C za5MXf!ci|ngYUMFcM?K#0-&Zq$$)r`CNa>|qE-^bwYiukXRi;IOe@ji;{F|~Qt#Vg z2z54QZO_N(|7`3RD%AC=U9^#n+5`2XNC)V0e46&(U^aUBc|zy}n&Frj2*bB1yjA6} z_gc|lJ*^P2`UP7fLzwoS#@3{_DvyJ4Hn1brzAUHkG^@w6U2M&sGG$CnxdDSl%jv08 z{A$|JQ0R-<-OOJwY_>}{?D*6N zYnMNmVcO zz{ye+Z6#T+uEI9|V?_}H_@xx1_E)Z|KWO#CE1_SG9<^l(0RIAn79Q*Casg7jGb($iWP>j#o0>$m@+D?_(4PtK_s|^0vmS2L)#{49$0(VJ zTA;3&tu1juQE|s%i$w7$O2$*k1=n#&;YiytLhqGY5->WF9a8gG&y=>(0fjS;mZX4G zO*|nI9!&jWd-~P9{-6cs-|!xkkz1y3bKW=~Pe8VJ80S^jkL*v>)|31-@}TDk!GJAJ zU7Fd7y0n|(1Q$kK>ZjvQNHI$_thSA@QXa)@V{^t-*`fFWK}n|P6Xl#8Xw~MX0#gK+ zCe`%@G%I*eC}K@3ncSXs0N40RR)QQ_xL3O;ZZ!b!4H_&jw8&=7WhhLe4tyX6j%DY_6QeUm>8`&6>-$C zFV_K(d^tA

;fNYO0=s$wEl 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¥3kdO8Z~;C8EZ)YP&QElJ5a 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

;JYP@|gm9l5< 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

`a%q#()A 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 diff --git a/Moose Test Missions/EVT - Event Handling/EVT-101 - OnEventHit Example/EVT-101 - OnEventHit Example.lua b/Moose Test Missions/EVT - Event Handling/EVT-101 - UNIT OnEventHit Example/EVT-101 - UNIT OnEventHit Example.lua similarity index 90% rename from Moose Test Missions/EVT - Event Handling/EVT-101 - OnEventHit Example/EVT-101 - OnEventHit Example.lua rename to Moose Test Missions/EVT - Event Handling/EVT-101 - UNIT OnEventHit Example/EVT-101 - UNIT OnEventHit Example.lua index 26c89daf5..88781fc39 100644 --- a/Moose Test Missions/EVT - Event Handling/EVT-101 - OnEventHit Example/EVT-101 - OnEventHit Example.lua +++ b/Moose Test Missions/EVT - Event Handling/EVT-101 - UNIT OnEventHit Example/EVT-101 - UNIT OnEventHit Example.lua @@ -1,7 +1,7 @@ --- --- Name: EVT-101 - OnEventHit Example +-- Name: EVT-101 - UNIT OnEventHit Example -- Author: FlightControl --- Date Created: 7 February 2017 +-- Date Created: 7 Feb 2017 -- -- # Situation: -- diff --git a/Moose Test Missions/EVT - Event Handling/EVT-101 - UNIT OnEventHit Example/EVT-101 - UNIT OnEventHit Example.miz b/Moose Test Missions/EVT - Event Handling/EVT-101 - UNIT OnEventHit Example/EVT-101 - UNIT OnEventHit Example.miz new file mode 100644 index 0000000000000000000000000000000000000000..730bfc5b589f361788ca05cbb70c55e1650a956f GIT binary patch literal 241720 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%0D~!J96`CNrR=r8C>0PzNJ9=R{7339IRerj{c+ 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%0AD4xxo|a6F7XR32SYwuHq~w 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�Htt&4-<|6zWbncQwqmGB$aUoFii7p}q*!BY zO+>_N7~KKc26e!xuMkBB(e!W<2lKdB+UmDDBIkLrJ^K`EUNUg3q3d3G5k&zANu*=( zI2y!WRuk^$uBcC)x{kIFC>f;d0r(XGwsAyXxVB0gDb;Y=(p!M=+23~O&h(7(Q9mPA z_F>^5aH?AcPkw`?5r*i$?~(wVd*nqxFCUz8W-pfj_AMc71Hh$)9sLFO~L^F+wlO; z9-`iN0Kofl008dIaeHeQm$w1N3DJkd04ZnTvz)*=ffjPZsEU0Qb z-2 zw1lFG-lt#lR&01Z-9#a_8S+?%8x+VGWV=9sxoKHf;-E~k{Zf~m{EcC8;P+Z?YuVeI zo}!Nvm`S2*tH?TV(*U zv(h#*zkSmhYy%_|*7RZ<&D0Fd`0lp?$Q{lv+f=}4!P_`6T?sZ(XgOw_PV-*PWq_&$ zXszu^xiDrLTZnp^fId1uY{H&$e$ui!`INGBh;5E z+dvP%(-gTY;YMi6qydCT%w?HpiBhgCA6Oec(PAZ|yH1Il*tlez|8{FJjYMe|F~)CB z)duEH@1L16`<>kztJdG+t$D|wjr1>e$C6FdG(%JfJq>D4f9yn?aoGK|!U(S)o?)T* z{*=4#8oRjB46-PpP{`2bZ2(ff4IW_U&GE*LgPi5IM9BFFHJxbj23R zF(>*MlYwbZ#;oamXJ(R?J`fN5ZnDjXLQSpGeL3$u;QT{G$qmD}67>~t&an7=L%Oeu zvf@7AiKp{c9~YK?#MKHjfN^v1f>HQK3jQ;$nv4FB4`LmP`ezs|xA{5w&oGYKj`r^# zH5XP`D*6=D)1Ma~IdN_cYOD>MG-&k^tw~j9KLhD@Mrq({Gy?8SU?^8uVw^bjsoF}& ziJcvpt(0wqO^olS6`_aT?L_LRo_Li^Ea@O?_iR7vR|(Ql@5W*4&%J5cyyP*9vOyFL zRV)AxLH)`;6vMy>vhcoKzg^7cXi~@@*I^;nH1&*fPW7dM_x2L(`|sADiO~zDvxuIL zj|sa@Db2WVJ9%NYYcTyUf}r!YC@Q3ajIe}`2d}lM?E9~V@DHFuiLY=Dw(n*lJNTw< z6GWj2?4UsGTeS;V2~1MKqLEf;O?0htzCfNVfiYxI|N6}qbP{^40F{9dK4k}?Us@}Q z*KgKj_1{*!)MAB~sQooO&h%~upK>v&L_%IbxLDek{kz!^a*W5sugd3FcNkw$!!Po* zTk#;UtTZ6t;jbTKCF2W#h$tsQ2n@ba&73--{<|R<-$-!vtxA*na3D@ z7tX_CdSZAbE2g0?(L&3C6K#3b-bF3 z;b|DF=%Z=!GjC4D#EWc^W?=kO^p}tXB}GgB37TR=kHGr2pb`s^vBnw~RV$E>?#s;d zax1LWDIIV$gz{RspMIE&-V`XmE30u!z}2g8e&Kp@f6j>WNTTg-CRxQ*9n;nA+75FO zj2>3oBRhZVsXR}TtGCioVpy7Cg@e?h1B#KCr^GUlC1Q_C_j|H9MpUak0?=J~8f~i5 z_K`VBIrg$xXl#<#Y;Uti)K|QT_Ny1{`zLmh?K4=gi~?IjImwh zi?eN*v?QZ9$*TnUz1KbuK|uFl_-KhCoTxIAmFvqh@YIOGf?2$*o5-lm;2{Qdx4oqGpvcH^5XLXw z%}oxaZD?cO<*1Yqk+GwC9~*XE$2$05RqtpbgA-Q~XcRXR1`ZKn^#X|T<7;`P-*GG3 zR+RXY1}A1SfsY9ZdP5;pSJS_06|ixE=<&9@Z}nO=B-mG*LjIp zYn3*VR9zI=uBu`zygLZPJdpgptt3APUD*TMAWi}LegQslqNQv}=@v-WpB`RvZnLcw zs*&s#lOkB-ptRoyZh=G1p_IiV7RxFtUEw`<6I0sQXmVJ>R(ZLLbLQD&2i3I0+yWI zMHbg;S_DtCox%niQf0`6%XlQdilDG^qtYj!BX$(FU9x{V>2dqkbfD#lt$Rmu;A_?} ztzDQDY@sq6Z@#=EsP*_YaB+dd_R!Y4x=Q-T$0U&(i~JZI_-4VK7HV5kQoA}yGTH7A zU-+s>^37=Z69^m}8K4^VlWWz39Hq`Fi?SKPJyUErz_nAf>@!^F6(2=atTT(-o2Glu z-)l-~&>=mR(h86SoqXX;B2FRgLrnWzYpLjFO~}J7KCJ9T5v5bE66~H@nw}E6c5GQ{ zc9%F?!VV*?%-D{fYoT9Cv?)3U@Qr_u2d$R(uOKofYCIm>ud=qVuy_$r-!2`^c{p@H zXCYa{2VC2kc;L~Yi(|0R)M^zUUJ`D>uYfG^k1OLWT^lX5YI_ONhQovr!-m?U-4g9J z!t^CU_NNP`zJVnpbsj%;g94lslty#hqkB&d1ccGjU)^I!nfX`3r3{(vKJ~)q_iqjJgff{nfxKblUxI71?J#gb+*&{upGt<#>EUFd{Q zFzXMP6kOe7?b66OjYFM;Yfj|dJPJJN2<(BiBW;PE7!ywn_L^lnRs*Bc@b)-f1aiim zxso!o%8WhZUJKAfB^*oM2*;Qtb3yx!R(=g6P2|ou@XmZRgPz7|Ex22Y3-ABn8xQwX57q$hjB=8p&f7GB z0u6fSQFWzLsYUJrZoo+`A56jh%9&v_jwhBQ0XdJ?{a%D`f2|*2l;~N|V{G&z7^x@i zcCiRdamzhsr6Q0+tEM8$+Qod*n+gt*`bNXBaQk(cxsbk9Z}@CY3AosHhSCxe**!U> zn%`N{M%QaP{1w-OAPl>7Ah&i%6>$WIt*ESC&G;mEO9VB<*DSAv{f9$euxGsBK$drn zK_aGdDzSGWhRBq!qMSNCy+MyGk*LuJtjz81__-IifU}`BNw&peIdJPS+d>Bg^|AVG zXIc#=l|)L*1o8Uq5pCk(uSkqECQ z!HT@6F!sHn6EhiP|#4b#7MfgR!v-p=Z|fPtHYBjGRH#4~e7POk?~Zg{@` z3htl8BUIa>59Cjawo7GaSE9-qv6_^fF zkT@42XCnfq9SrHI`07v7wz#&C$i_!ygyu|+5qIta9h~6C7D@;`71T{+&nu|BD8|Bl z(K%eZn~hWJ5eoB!5JZdYz-#Dni0Yyg@g4UTBNpqM#CeSA3K-`$ft1df+39;z*v&-v zW6YwRZH+(<$^KO^~6%;n%SLZ_Za4QRR7twe9s+D85Pc3>Q94Y4^%pBt+Q zMS9e8!K3lm6za7vE!6>R*PdIVqWX5@F9GHjl`(IF(GU|`b~i50qM{Z-9c=1*QY z4$)Z;fpmC5)J4ALHYw)vMGNZb2e36XjV1<4OecposxA69T8pXA*0VkLGI`Xz(l!$ zY3hhAUCfo*34{fGSIqM5h{B17@1L$zzDaW2lMvbdRW=V)Z5e4ztz_5k!9qsu;^H8` z*Vw^PT@fPKg!63OdtatQk?~ah_7>moqsdY#_Ji!H)zNkID($`VcZu)k;suS;9za`^}^u}=pJPD5@d$>bO zHMCfE*Mp}9V$0kdt+eZ!%vb|G>lV{NN-ZVzd)znRQ z52sJ3o5W8uTVhSYzFa!iS04{q6i%hiwt&=(%BE3_OJjTYtCzSxvmcd8yGVh9#yDU3 z*I;*=l6CzCNWHm<2dIKmbEXG;g7Q>iN`*dB;Jm#Z6eG})YenE`2Wm#qu+-f@Rh1Uj zO!ncRx~@wMcpSxsQ7VY4-q*ni#UE~v5MKx}fZo#uS@N18h(ny$rBtwI^km~Tio<&P zMdSivAHPKQntBj>ctVZqY2I=#>#>t>Q9~@QGpyEip0c7=rI`Nfoo8 z$zu6@Z*Xxr&HCmha?d`!b;uTvkA}Aenfd-^j9N?q)i**348}8%w1w@ER2cii^O7`gdwy#$G=cxj z?I7+TZ`q(!Q&tT~d{QKrV@cAa&d5VA9!Ms;Q@2{3+!Grz&=Pch=#IY;DM=LoH-)^I zuI0Lf0v*&V4~PChwR~b!iXI#{uCbAf=guVs-K=j?aw7AkHbE0$3;!&6U8w#JGnW-u z1A5yQYz%n4q#dDAK#OcVOpcZDQx|eO)NWmxIndI)uMm+{n_LnJQ7QluhmfKgVyPV7 zlnUli(LpvZd52TBPP-~N$+@DO)XJ{xu&*d6;7YYoO&#I8FO1Id{8WNCeHl!b4%q;!xmS@$<}sprtV@eKr!vjc zFdLN5`_kyOxH^rB=l57eW-&rotjQ%TjJOy#7vO-jL!6`=%^#ZP2^1`iSjv9b$x@XY%XfXy9OMX4r zPc3~Wo$rj?BB2%jy(0>{zV?@c^l^HkE+4f2vYNYTRi*!P-?Pa1f*r*v9X zDjJU`yHUcD2!wv^Sv|N8}ZTGWs>PR@jG6Kh${|X*eoJcUy{n zZ!TSpvo)rvrng)IHEyks?8U_?2ky9-zBrU?Y%%=6>iY0lsb{j2h0IUn$dQI4W+$X* z(Xp0iHJn{)y04+StvejOz#D22^NFYOwn3jfVc-%^^@hXS+Hs@m}x5=64rxT_=i2C7~pra#05{zOCL6B{mcCMv*crGgV^ zLY?Gt|7h0eYN#zlYWy?)c3%m%@WD8~QJ~0o$(m@e=jgEmJcXh2euerbPW(W3Qz~DS zi7a1m{3p!W)%}1(X_at|{Hz3}uK*|w*twl|+nRQpz)wtG33)Ft>A|gDXOa(l`@Htz z{c)m7XoNBZgruKZonaHADU1IO27eCGt-F7A@kutnJAqU(J3n?!Ho-My&h=RiQg$`5 z+H1@!It9UMZ%l(qv1MO^pPNq{>v?ck^zplaG*5b z|7A}s$hdCxgal1d#i!RH#K@`LXp#Dst9zh>}yZc)RIAS?ANojc`M;q%U+gqbB7Qtock#yh8E{Sa}{6Wj~JO z8Qec_zAi3TzNtCKs-piAd8TH;`a5VhoC9g;WP4|~K89qU8#;c9)x(%a-_LT{n|1qJEO)t}4#Gp6W zKj?jm%6#4z?O1xhD@xPEq+A|VwT6ptW2ZMmMG3Bp45LFsElK?T4?hqeFhp%}Vh9}l zWPAaOF9)9U2?ysdGos(r4ACDe)Q59dKV*>yXSoc*n=pBU@&w*CLgqous<1Vr!Ei%% zAU@}#iP(5uurq9gO?_1|S<};Ex&KwNMEs@MF6QSpGgXjHL3ZVES{NaF14c{H_wxeP zYs44*K-_O`{oRSpWzh2j)#x+KZ}qIG4p?-&IYV;Qd&T}0#_WK-{iU??(4M=ZWs{M_ zR~UDQDMR8)Wk_gp06qe?#-~xXW{-~MwTmoz_xF9_2fvT-NMG7(U0n)GdnN~#9mnBrSBd11GVJ_3NEdaY1`miJx^a%dBZJP z@f*yOSqdAcGS(1Pns>0+G_S<1LmJOY=A}4_d(piEfq0G+nm<=`POerzK5j~FmS>&{ zJt6(~cZ~VP{XPQ{0H87i01)586&}W5b1O$T7ju`BpGrzAEI`Jf;L&ZOoKpzS}sCiIBAw#Y1;zne{>q3k8DW3G3$OJxH$w zX98Y+H?64TWLNTKwU>6h7L$m^tr6<6Q4i?+Z1Mx`9)Jk{d0oza#T4;_DITL3Yf)MwHMfVr%mC z$KB?NJ$rq>nt%JWW2Lg?%wErzT#HS&{I$c`EQ@O!D)urJZM17@F`6w^?sUEUX3~r5 zCi1je_p{g?LJ!*Ax1=2At8&NGIv8lMLT((QD|{d+I&_fmAQ%qk;4KUNAK7q3ju2t= zVbwq<_#JW~wb3coI|!ka%V1{V7(`cjnD>Fg=y%yb%=)#}`oL5Szs zS)CL=*dkm+j8qS%7nce zn-lQYRsWhp6YGa@0Bv*+vu-E9W=A`_UA8e0XK-Z7W1)xW;JuLyE&N2&dvBwhIO_D>CM{%Ko|!Akp8db-wc|! zUfzE(9E_d*g1LyBTNu09x&9kX#>zlq0U7|1NB!H+zkq+!bl*aQ|4I9c^lyX-#DR8U z3;+Ne1OU8sG5=5cn_%*Gi)yA|YbRG1W-Ujsof)%~vYMfkqMF*jao`Vy&G_C7DWLuq zKmG;!o3l*&C&$DZZ07QBkjnRu literal 0 HcmV?d00001 diff --git a/Moose Test Missions/EVT - Event Handling/EVT-102 - OnEventTakeoff Example/EVT-102 - OnEventTakeoff Example.miz b/Moose Test Missions/EVT - Event Handling/EVT-102 - OnEventTakeoff Example/EVT-102 - OnEventTakeoff Example.miz deleted file mode 100644 index 6e755d8882872be7655116a911e50767cfc32498..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 232049 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�Ci;ijq@pKHMw4>98_5JDd+0T}eNO@H zt-&DvZiKgb@bP$GY3h8!m)gc3uxaXU`Xd~1QDOIQNYwI@qMPgV6+1Y?Z#O}_&9gb) zBRTg19+wR~kfYq@3pwHnpY_loDgq_!d5M$lGD4){b^y5<@pA5+8o$WrS)kTG*ouXN zF?j?wG62u@*_S-!vLLg3F|V?zgnD-J)#dr&Qw?}8+UXL4amkVcYFMgamZ^H-se*Vv zd$ z=WP6p(R&`rffQ4sE7|ROuBUdjIn2zwWb#t6RqTa(30@dCv0_N&9cpI_T|r%RZ4Q;K zENcX!8Tz$(o6UaKA=Y`23_;dpISd+zGxZ?B5Qshi?q)TT?w$K2HVbyDTrg+Kk>v2? z!v_o=*oZiU$5U+$rrvcI?BR}X4JovKZ<_~INXrhvPz?{)`HY|Y-$|1q;O9yykfo&{ z2pE(FH&u!Q=b}XLrw|It>5vNo62Phb6)6SqpZ7+WrUn0{2qgj5Zk6i~d`phon{9$Z z3k;a+s7%xBW1+|FA!GJ>my*TPTug1g%wH|v@YJ|VK!a*9C55ohi;m^gp z;sY%AFT)UY${@Ve7yz&j+>!Mh@!P8=`IAQ;BlMBDpNz11b?JD-AFbDxcz^{6<`DJj zf(;ks8pZhPmZt@Lu>lm2DF#59B2#nZSdt@cCXUbq>@8ged$)8!%_vt~H+)g;@C)i+ zU)U@mLnbOLYz!iVO#p<1jX^@f<|E-xDQw1)23a$|S=6|s5H&&Wl_?IAW`3^{v`)39 z&1bT&!;;4NC}{KBvd)tqXXURmeeMaT(#fx7HJ$X9GQ!M4rqSD&!~|Cds#{y8OtnL% zASh7`!Tm4tSIZMYgvH52!Cyj{D2HT;ezGW0ij*Wt=N0}!IifINGwjR>B6Q^)FrvDx7uH==xRxCSln?f0&v9wxAyPsq|cGu=K0hba~iE1$z;Eyi2 zUAVfWw&hr1TV$E)dR(y&SN36t#FnJ-21(UqPj^V}TxPh#@O zjQME4O#fc38tRS2-#|sYSB7S)Uzi3_p_2Q$B$vaQ-Iq=VBgZ}~E${*e7S(52@ zjK#hVJLXdoP2b!eB42)DjK3}NLXb|!Y%<$Z(N0%)a+t#Rk{^a>#SjI-{GW#Bw6o94J&etkV^e+N?O|YwfvKdjX`>Rf z95^ypu;)cFv^VI*#@v;)X8Y0OA3u;QM^Fo^-r-92n(MUcZ3dXG%zEzuQi2S7L!r{Z zw0AYAG&AiDe0f$)xQAJmCz#Oq1tbi4nV0j9kr`y^z^R%~M80L_;IsZ>zRaf}igtOr zUUrf08m>4Mii>p_(4!y$lUQyA_zo6Xy)CU#wg|1flE>q(a?5V)ms>a+7{LvwcX$1b zci)vBQG?QaF0oOSnq?X`nNzEb69^Rb7w7^MM6|Fm;lJ;eh>*4U5n;GUK$x6LJji=< z)C{*O1Pzi&0fum5fd!K-pdba@4H7hS0fgFnZ-E1(kuZ=q)e;1$)m$5PUeyszCz>l=baVXaVXvaB72sFq+Gh>LH`awOhsh{? z??`@_#t?cuIw+GII!qjle{R;`-=V&P8|3i;mHrnUKA>Nr{vJRZ=I#NMLEauf7~t#y zWGnl6K>Zu?nJZ|DFc0CZ5I+x~TglA>hmr8|Fsy}>hso&H#{+5zbnyUE_VB>A)e2hp zb$}4qsNdp2H47$E9GXebaC3G)MJm4tAm91Si6rSWuVv=%od_eq%>mdDuv^NCU0vu| zx;b!;baCK3pPviovzrU2+>}33BkS5?Cm76v)ge>y>aZwV?*YR>5>XQD|5D_(b$>N+VKb}i1~_$@Zq3f&QRbauhWYc5S(4(@=}5YCwd)TwD!%D1OR7* zdb%$`JLKHbJ z977c_=^~`4MyRIwSp=++AM=#fN2lFi$^19KQeXqd*B~DWLEJ zV};XvXkmia#ZQ0>J_NkTDiEW@xwj8}LMSvp=&aWcRM4wX2&7wAB(l9tUG`RM>3aP2 z{;z&|Ajoz5J^@dU>l5WkU3TN62a|NTiyl+&Vl#z<9+Nfn&x0bt?s#8AvgZLXxety9cZ;(Bg4Z73alo&7 zw*#P(UI&OZ@vHriFCVUDB??15H|UeSp^Wmdd2X} zdk(ss%izNkJWT@x940wfI7KJQERYEbCP3%h$eX)(`4vw0t37=DK`gNXC}e$E66zPw zGhZDs1pq_zWe68DZbPHpf*zWng<5=g#s2grckZ?M^}CD4N{ZcRM$sUH4J$4hS6wBwZl7Z_@!MV)M^ZrXo!}J=9E@Jp1 z>g+s`7jM1N&D&P(R7*MgqZ~d=<;WcV#}`I?MdVJ=wDgQl?A0PRhCfNK8qIgj+_ow( zR@}w8+Y~pQ;R(t7mD8rvdI^+3uQH~2Haki3M!-}Zpn|!99k`%fbjuX-f7xE_J9`X{p$~8w_b26hGDQw&x!)I%MBpfo2zB+o9j6&K#N3Z%YKrN zdRv3I$cOSS4=o6*pd%aePuVL^)7X1>o#bweY%%ihpnVM~8w)jo7nxEL${Jj4S3%0* z#wG+x#8A}w6&qD0BABkVvs=x(u#?_xqz?kYA) z+hb>I0LVTFfZyu~I*pLhRREoh$4F54oG_H!m*6>p?s13kv*A^Z44EVSMLA8+*_5zi zj2vKbr{Qs+0rxR7j%c@txPc@M;^8>jbRMC3l(2>XIKbx4V&9qG2*RNT~MXkyUF5)az#K|C4SxyR+daxd00;kpi zgMh(Z#0r9X*Aga3Fsc%EaB!dyc?U!T68;uK=Ds*UkjG(w_=$xsDry}B$h{4r+Bja1 zP6&wpQ^Ti9A)z6ELi9nrq&lP{fKM5iZ|fq^1Hn^=PY*}}YA9%3v0`Ha4lOMP;QCKO z%B$tXUuqzvN26dc2c(_{!A&VvOD6FO`5B0V666VvrcS|W}U zQnZfb#M%ewt)hQ(U`6eDL{z$(P61(6QI758gN4ZU#CNIzheae~JozBvWOJOSDxla| z!Lgp0npDv~@DmnyLqC}`;2AI}yE6kf$JVR|I2;5D)5U$4H5iDXXYqHGP3bZm6ef#+ zP?#DR5(-~=od$)%Ufyz8D5p*-^M}w-orZ=&Ox4Inox+zOEGMX{iWMuVvh;?W@etwf~JHKkLx z>n-j7noih(_J3=MMi4cOfPrmn5d#CuHIGn(#a0i8fo$$YK#a{n-u;b?;U2}}Q$TzS zqLUFaD1&2UY+`_nu12X^!#@nq%^aThI(ALBBXl#P*o30KT1JQT*a9`uh@M!LK*6be z_oNd&#`GqaU0eK%W|xLpBvnT+k`{jalLduxdZDce$K1{L`;ssBS_0Qi zfNqNNSk}E|4)Y*zF)AgWr2YeKiq2|_pmmoCLhu+%&l zl_sSvD0T4=Ou{9JACAJ)VP#(jkGBKX7Ub_BJRT~`dh~@9tLO^Kb9>>brH-Izh<@a~ zikvc}1H~F$C`@H`U2?XdjLONvnk#%$1(oHQf}8DgzNqxqsP)(Y`TM;&L>DE7m|(>r zPb?Ton9zQ}Q)vrNjHa+_6o!;VQH3bXXCD#gS>j4o%HRt36e+C%xl(o(agxe-Xv|J( z-X>W^@x+1kPR}jXPKAYAzw^CKUYu9j?NgTT8`H|F8^(7yD-U{_)YFYiWNKk0mWx+P~b}cWmb+9huKJGl@6q z^V`J7RLK&fc$$ul0K}0!S0e5%MQ(__yP;=;cfLe=mDHQOlhkxOoszpYe`)v5d0iU5 z>nZ{*-!(NHGuaLFSUxksGShc0pa4^6%`DK?*$9sSO+d20t0+vJIY}BiyP49=nX~;C z@Jzxc&YUO>oH-L#$NW@YBXdT*8N5Zyi{5J6dF>booDxPs83P`*7TJ#WE1-a2oYtwjL-m#4@5`k;TL|&Ra zxJkCAePCh(`QlaP!2`5ell1Ib9Q~UNVI|K6Y%_U79%d&W6LFV@^18FPYbno_`Uc~J zt$dfZ@)}pSXDnZfvT$p8y}U(;(dxCZkzWmJ4_5N+t>nWk3Y#p;tF+a*QDLx^*}XO^ z)Y4qH$(d&Bu{q=HD$PEa7ItA;xQUA1Jo7@`vS9l{OIcD(}I9Rpt}fp-yNuM1;kR z;pPa+O=c;XRFBb&$(B|#_U3lCc;vWPEe&ThpIfz@(RGHJ&S-L$Xt2?YroVSqGqTPxMsrt?QF@l!_ylg>__^d zRgruJFEM3(ztL}odfrQ_6FoLT@am+GYMLd=uldhp3@fFhBak4h zakab-rcpK-C{5pyfQ*k=}B8F>@{e{|orK3Dj>)-23 zTfvVWMLPqfWfnJdi{hANGp5^v%S@XpRYbAszxf^uwTWDTpK-ZpyF#M_b%A)(+j?fH za0dA5;Bq#*%1MSJL;!!eC8v`C#xCfl7eboXsQnDs$otoD!vT=?F0SJXlr-gZ9 zw~dPE!NMo3hIg!Z7=_Fdu&DY=tAsu+MpY>>+2U2Nj9-*o`!e~oI{8Z;43M zVG=f2CZAR(LxZsm^9;`I;9MoYexrs_JW_3;h7d^S+(^+a?h8&?JQXOBm|NkZ#WdMF z{J2epbwr*KhyYb7pbDnAf(J_mq5}*D1DGHb5@T?3<56rcKVV!%yT$J4=`)a3FWUL= z!)ObBpZ22RhxqVKf&{%SW>FfBA8$pkF0((Aesng8X9>Jet|r-7{YWR1>q~^ZH2~*4 zEa5gnmdy2M8MrtkyvZ)#n4YR3aeki6R9~YX^U^JWk;*fxmDJ3^j&7(ek?j%+kO1eW zlZ+b2%_V1~Q8Ggdk5M+kSCya5SCi`{tngxPY5C)1yrejEe_83H84Q#q3^F8G%H!vY zbiRzm-;lqQ!#A@enJ(dQ4*8mcqb-=_zXnt+(-snF?uemw+5$EtZRQj%Z7hA70REOR zSRlbN9!A>CqKUMT152AZNNLNF@pLPCfrECbv|s4aX=*S7o$J6Uou!WN&S_3jm!*b3 z+KLW%hFDVZfi)3)rRD)sKhH1R1nwRFP$-heW@70pmn&^oz_knEp~#Po@1#tp(`NFb`UQ4#O6pdy{!OzlPV|X~jAe z>hdo|hvlW{P%lOMC@<_ZvB%In5Z0$=k!rQ3tF@|CMVlq-ke93?S+TCpIBm9Q9g0P( zN%qu%tJynUwhnpODv~RkjO*FWTDT57UAPXdh3l|x;X2fUG1Iwyw6OQ`AbS4o-Vfir zh`v?Z^P}U_?QQ?G&qeje=Euow0+&MuE0gRz8fT;H%Ver;AfN)u%m>lSNfPIPrg*F} zak$KqvuibEX`1I1>@=H`oD|V@o{SwyY(v9gHir-4p|yo#18-ubO+||f%WN{HuzzRq z$XYP`LAHG3KYRt#c|1u+R#C3_+hmr!SzxcYyWB7(^qo(ieE3xT`;2h#8@@)>c}=lu z=tafP1G6rOwzjwFa-qs$fRfT=!;43|t^7GIc{@!|HA-N6*kd zdWO#Eu@z_O6TjPE>?_P0!!wYyu~2<+dL=J>#kq;gK{0%v9k#|9co%UWj5JsWkp@Wh zR5DmL6bMQLqzcACT+1w7YzeGcq%Hzn{Z}=Iz*gHJBD3ldsFav&*sSO|Z^-czR<>gu z?MCFmv1@$9*=0kPaA?wI7GI2dUrv9Zx%z$hLpGaV4C=Y-*Fh-vAFuw7XX8A8v3_ZU zGS79Cd z=eJ%C!3AywagAl~^xL5YxF9Q#I{rY>t}e=&rMfL%&Wd*N5NvRs%qf9N8*-P&Zs|Fj z;V0W2cJ{+#hu-m%*5+uaNLswUwOib$v_;vDaY!hDAoWUF2?wj=66i*|YGj=p6;U(L!4P zi_>&!v`_kr4jQ=#Y2>MFwbSm*u>}U0DwzFt5CIUa2eUWK_R@cu^73m6dUWdF3trN?rN22Ma8BMN}8CV4mF$ z#^@BC4&td+J|QQwPa3r@+I5DRJxB?daR}lop`>_z7>^t;xv+#SI!p+mTv(T@0>L z^NSSH+hSs15l?bO?hr(eWr6gpp#CD-VH;_aid8-_Py99JQeB7FlX-eINs}2drLHg> zoafJX?QV0Of)@n7a7drHvW&KiWR(?G`DzSHCsF zA7y!uvN(o^XK?_c(gv^{R7_L&d-2R8+}yKJ^?%F7W5 z&?W@}ZBv?*tCT#>u9KKZx{S(a4kG|KRD`ZOxwqyM}&k`I|nxQ(r7A97P&G z@}bL55o;c8jR0ccA{sG^Xj)9sjm)H6GOcu+_M!vvj8{K(2*6aT)%zqN2&=DgF<)P= z-2UQFuiEm1FxcL7^WN5;RJChChjMDdwdtwDd|6DBc4ekBG=TpGdSg>761PTw0mk@l z2=(_6>hBCi|2~*})iueF{+B2D(SO_|UxrYZpUes`Df1AyDn3bKO44*6ZTmC%;CJtt zWxdbdtG>>U@rBb@^tPo|`xJJjkb3eeIs#9r_IW+Z4+-+gt3g1_SQ}yMnoCvtzCPN2 zc8D%Hn4z5?j`F<(1D--mnCtz4r1awn-Y?l1&j0Fzwin{o_xI81{J|KfMh|gJO+(dl zV(X*)#d8nWkkQzKEbN*~KF{m*AiDSM2ajg=#*I!zlSy=s@>JZzpMsfiF&2C+{4Ajx zI1$imn5o}G$Gsc^De@GaQ;a!{R2^oN0DdxrkwtRl*I1-PT9;k@*sT&H~FOsAR@I{J|2GEXTj7-3tcOhY4#1=Z_(iOAus zQcVukfC-0#@r^l|5Whprofq3qOazl&_ldW*D`_4glu>G(e#&*b!^3%N{Sm3R?jQW# zT++tTp0WR0_t6kbk^UV+ALlL&u|^u6rI7fMmxZyUa-3?MCOFXDL~whUuJXVi1$6

zI>kVwQXx_z%IwlV9?u@CHgtR0AM7 zGLL@vK=;22cm&9Gv_{@-n;J9U1rod(e=;@ro-M5Lr{J>1c;-UG<6v1qyulARC z790szpr1E+LH0kHxjt4z{vHLZHN2Uu#=#Z2c0gNS1TKJr{}?1Oz3yh$!$ksohW}FT z{1Sg4x4yf$e?Do`iVXWKpJK)(RV5^iu67yCsd!>j$(mkv@U_>yv>3<$k0bt<`#NS) z#y&;O%e*yeHZJ;=f;!i?zM3>{avP<=wXP`z4hb)gZ22)U>W)y401Y4qz@=ez38;}M zhDJG0(hm0>#QM?X`)v@%JcpW$&4szWfsE$myA4bv&M!VnNH3P9Gc3WhiWevO5dx0= zVq&F|K=idDU?^%J1ZoAm8skPM!5gbnt%WeQT-zpq*2-99Mhgp=0qpxpIt%;S2E)w~ zitb|itw0UMr*%lX8O_Fd`fsk>Iu*j9uMPUEqF><7woMyp$*rh{UsAsZPM8(BQ36~d zg8$iNy+a_C47A`jh+CrQ|ML0YDxdX7(lS2}mY*&wKW)pc(_3!kJj90Kw@c2ok^4qy zEQoV}k#t>-u_txsnkOLR)@C}!$YuxQvFfMH+PZ7<3j{rE>q*+I5q8L2)^j9sX>wFy zq;c>23X)B|{emfemaHE(4$m|;|! z>k;cSK%BhrKW2oO+)o)I#<12H^;xsNoFO7=KV^gn!CC`!v-ML_Z+;4`P~B3>e{4F7 zE$^_eBjI63H@12n3x82*6a4H3J+OxX@yf716H3y>^tAq@l{@rKB(Hu~R!SlD!#E6P zjQ+?;&s}QDOWp%jMeOS7p*pqYqwSZrq1FF?OEWgyJiWk2*lm8m%`M=8pO^Hr-uis* zqyudkC2U3t4+ReVur-|iW+^4Mf+Pfeb4qNjbY;9@P6+8H&ldpRv2KdrA{a=ypVf}7>GS_Pw2b;%WjWiC*cDl3h zQlzilFJBDwfs;7=%LM`Yd5s{f#r)BC1G#z7HN3U$r@m6YGd{aOYUMoHRcH-I)M$Z@8YV0e zYf#_-#SMg?o>L-x6)7)?GuGz{-?CqWFYMmWFvjX;K9MzioNF=%^Tl;6u?{x`u#*q% zJYe81Z++$i12@ab+Zf{uKHUUqcr!n}SX7r0KXi5im-^nKd?RgR>8?Bz)M$L_2h;z)|9%t`Z#uX8?sdU*Kw zmrp5;wD~&c^Q0`}v*fM=`V1fEq*|_-=}dx6k|9# znn`maTVdnEMFGi&Dh1mDglUfIg6y)3e97J`Wirj7Hz-hiMobnI*1`{I$3=)9PlS00 z`9{@!Ru#X4HKi+%G@GUH)_qPz0i0I4U_3h1^pQ-R2Y2G{f0BftHdp<}3_|dABRiY) zJ)?W*yts&V^UmG z5rCvh_#p)c%J2AFui+OPRN{3a!yNJT`|Cfi;D9(q`!|9N?XGBJ96Y00x<>)qTO+@S>Z5|@erk`jf3DgDW0_K4W(>vS=8U;i6d8* zW}?v$6X-#V&^z-58@1zdK8sGS)02~Kl69{2E{;iHF!_FusK5=;fg8=f5tm9S>0Tu& zE_ue%qO5pe(#+GK@@*pid=(_K-;OpK)GQNK=~MTtazH9KexGKB2o{Kkot=#sV7;DdoBZP6+Nw9BJ0cHkKy4gB zQ>vS{qbJW^KaI{~`~Y5dCwdxB&)p2*fKuUmw4%Oxz!ya29cUBC8=FB#qb!(c+q6!y zG>^IqHA+HXLW>p&NLh6W<1&pwnc>oU4+`Fa$dLBKT@Su2B9xtFmR2Q@L)5(Fszinv zbVJH6C=1Xk?yG3!SX%lks!?#_iDDNKD!t|5ZN8sIkQ!$Q> z#sW0oSo6;98@y$PQ>``I5(oV{ls(H0j2_#)2L+sG_bO6<5e{^TLy=bVWHOn21L#Ok zO?8j}^X?xz;Fs@Wn!gc6w$^sJJQ-|091`T8fj>3=*`4W`HQh8anUKcCS{wttUVS0~ zvm$Nci2xx(LKq3{Toh{;PL4icXg5of#+-1&`i7$xzG^@prDWDr@Tb62One>;YapF- zM5j*x(0~E^6=5h4wNZp&9*d5$+V0SDfatt&A!{ubA@DV1M6EPTD1J$Hc*&ZTzw+uf z2C3Nmddg6uAeKoLZ7|vm3iV*cv=Q0`jtFDp{Jy}wQ!!x#cXyxdKYei|RHmiUB^Ch} z^L3y;(T@$PD8TP=f zQg)qet|HrKxcI@s`>l=f#>Oyw_7(^)d1?hMz~ zemD{sc2^@_sRut3@8hY1cIn@BRB;WH!6^0M`+qM*U(SFv=uv77$8$-vKF0&v8!&vl z2CGwn!XHAb!4CG+jm@%(=mYS&r{&wTWl_5U;Kk}JE}&#uqzl@&CD~b;p)`d3x|)36 zMdphaw3Rdd1&QD~w0=2*+@PmVKaAK0bwUwQQMM(#9|~WSCHv*=VY>SF`R&*n?ql=> zFFjR99eAD1-0PAuocyQhz3X^y|J2Nm_E)!4h7)x&rQkrR_q4<5VDwm;F?I8Oj~z(& zqYCv({K)8Ebn|(lK*z{JY+q1mkB=g(XPhWeUGU_Yi6=OjF zzQgIFzTus=ae}2Wy$+NV7vTFVIp)_9RiuvL!Hc#?%5zLPR6qbT^XIxZmr$d=yEGWD z97GK^X+rEY>mVqBZ*EeBJ$4xRg>++wGM}YcQclY0d4dj0QszvHV^`&Q?IZS)3NTgH zgK>MWqo|{?!`)v!-FyA)=_~3nk3eTLoegl8d`7`%Vihi`{U>F*$mbyDL-A#mV>&c^ zyrd<$$x$*8#iV=2o8%6ZEKF=b9x(6KKL^^RS1D{%q|?_v2el{Jk6hv8h?=xmVKW_{lKbA}1OeXwJR#> ztbYVJkn_d=PT3xLD8W@9-EASC|1zFCz8yy=OOOcUc(R!RVmb#W{TZ@73xQ5>fDl_y zN6>Su%&v2}FJKcP?upwGYAwY^u55Bu>_BXr18y{~TzwTc_oBXn82c|;g-wgs7d5TB zrl3)sHc4B3LDjqpZ*&->gzqlCRN*5^|I4KB8~iUhL_p-lc`|($rJjEVixrvO;rgS3 z5^{Rz+%aMHzU}3tCzJPDPk81OxYKc?!LG_G>1m@j*}vf{9P8vWw%`Dhdk;Kyi`W@&}*4o9OZre9D7 z%X^w7L#BwtXqFUU(ayN?pO2wBm5Imp-Mi5*7}FtPc}^H`p#|_Et^c4p(lb;F73=^q zz=}TPbT)Q+noijgMqRcEc=60^7%hr?nqV6oACwpGPN9k)yK?8vlbF6LyMF;g^L8+q z{P|^(p!+hr)BG8U-Am(#lV895C2wgYT1p=tM*xMvMdaSic8eS@$I&Twi(UNjRf!$s z6DywQ3+^jLATbWC&2SNJ5r|QLOk>Y4(~5zqG`r+;0)o;Fj+-hMNzN5VBiHPH#lcYL z5Rk|btlA_QO&Fn`6wG4SffNeZT|Wb)$H!@glMv63kI}>M_&E7cKReY$GXrE*qtRGG ztaS4QNGc5ZBMnfK^%#-kV5S#~d2)f%z~xw#j!T&cV6bSOmK1Q2R;3$KXb4L1nJJ!2 zr^($MTRcw;bx;cpNGMI{Bq}e<6qA7m)CqBxH0o1+0oCYi7b(Jq-sixl1tK3)sDgXX z=@dgN zB|2(3mw2cH(gq3jn}TkjIG$a?#LXN_xv->d?y!W+xqSLDhePvr?C+>)a2zuZ|2apJ z>k6}^!evgVVvX~7pU5i`WLU&PbxNrubmQPi&ytykgW=ujs#aqmLhNaF8bb`;B@fMF zo?o(31bo25aq*5xKTqcAG+!XE;xwcMbqjxgoxnWI6YcmssT}2t7=!y<$9>huC5P#jpw;InMw&Ty{Ebcb3OZGUe5qb;^tflsZzy={zrz z8K6A5Y$N;}`26hL4Z2mIN2<7gC5oGelkPtdghLdqz`Nei8V9_eq$r8hrR_k{C@^|j z5^!Up)ss|#qXS* z;?7g@Q;~Y17(uV483C@R_3`deC#i?alTAzRI2dD6(xv-067ngrp@dtXP&5%5k=nI! zRFcgvFF7jXDjJmYyc&`+l9{37;J@Z+Hk2MCxz~00<*S2T*bH2mOagleGCr-~sOVU{ zIA!+BeV1D`opN07>9Ro1r1OOLh^cU4eHrPgP#1)Cp$sp&Pq=$*z%bY>pDulC$i$xC zy?cx&@begVa@)tp*31Fqphfe=g$rL{m62y^nWJr`MtC@Hq-ubj@D>c0%imnilZiAE z)*}mB!lte z8D9kAg~^x+jHw#Op~okfbN(`{-sGp)XI`b7YvBa8VA^}GdJ(5t zeNz?FSXQ~)_8_66-`aP|%9gF>0go7RQhR;daIr)okzKQ_#cY7q0vs)t1#W&&1<^hz zgYM`&wv&o>I$9{IPD_@s!p5@S)f9OSMEcGL3rq{69K-l= zA_L{sxQP9Zj)eb$I>*ba&Jq1>l*X!T}og?{o=v&$4B8z8M8){7Cn$6Y(F)P@>)}g=2A5sjzhx^OfwZ+UniqRQ3Z}ug-Y6=9GV@OA2Zm-3PEl& zCyQ=XtDGh#bBC#pD`d2|#KQOS{2hWOvc~J#beS+Br-=BxjHeg-Y35fA10Fh^#fzdu-~`N29gVBOP*6%*B4H}Sp|pz1G)6%a;fZl`fm?Du9774@7(T>Z zIq4RlB>c8YiA9VM`A4>~kmlqCaN9;`B+XcZAQYdnaUCOj+!LlO)dNAo?O}}cQ|P}a zFCHC&28T6c2cevg8L}7@?Ijnv)_@lby6txjgWV5KPs!8LLI%^pkm#_(kqaMff?|nELSd(HJD8_Y zv@aHwt3gWbAfv-3b6){PGPJDnx{*}5(`MW&z_-Ymg7LMO1$?rCL>QG7G!lfO@sc&* zo|UL`teKY5=e+PJrW*ylS+mVIH0EKBhx_$D6&G;YU+Iu54IQ$Dgw*nn=NNM8a+#(&~>Xy$bXsAGA8i98Kdu^1wJ|@qKfCLLm`;edTVGbWEeL85~*2y z?TN$p^u)otg4Cg>`!kr2?}6&M;LA)|qMb3H;$+~==?WWXdayp5$Z&UUCwL81r#)^+ zmvP=1@eV<@0_H^CZUpHFQ8t50L#cZpZ`)2=3FQ-^{>O6JA;*SVSs?e_H8qb$DdM4{ zK}_MvLhg2&sKT6d>1fL+k9kw0&Cybi-J>YNgX~#?2ON0;Kc6B6bVdgivM|(;_aqtY zNhj?j5Y;y$;Yk8h=4g0)+D!wqiPGRMT96-HKZs!jp^~DreGy-}NC$Rha$8g$>?!8P zoj0`GozX}e>)ukzk+|a@Lc+_gn0zy&oNsRFwwKqoC2gMB<82WmoAcIRGdHWsQN~jc zs-4T}YJms$e0{o2igL_@HioHctN~PvnTDOB7rz-2z(CFFgfp`4N;*T5>@_Zxe*?zp zMuLT2yL_jM_r!~Fl;{nVN)XBZIn6#TdbEIP;$0mpH^38!XklR$^EIsx?4viR8XvtK z+@-&N9&VDAApYFtuw~oxXxKQ7@O5b41KY1U4s2O;)DxMT!qp_)Fe@jbhf0m3NRUAg zoGdN0w&>zO-P!>NW3 z+nryEiIsw7G()!@d^PEA5^hzvQSkC{=^MexXH5!XLjw3>@YxgaGo|DL^dF+SfyNFH zMj2p;n*eq7K1njiuUk>;gwYJ87kF0W(o!xn5MYX|oZvd@QN&(JO!o$?XOvYhM9cSb zdcjDiT=TeconUI+;WG9GaZw5;lEXB^_|Zdcu#09sa64Vju|0Oqau%Dv{CP7+$_^E< z$ST;Yh;gB$z#zoa#*SJSWD!@a@wM|InS=2}d&~>UXiYMuiDpmby8|*0I*`tZr0nWw zc=SyF(a0QRHMdkTzcqF3IC{+bM|X057N3t&yyJdgn3T3eab^1C3Ur^WwI6y#0snuc z6hy6<35p3!J9Y+E`*(~B7qykLmM#b;Cv3u_+o z1i8ijJ$jJvIphUC&*(yUjLt3h(7iN&=RQfsR`~bO4p&}vyyUlsY<} z<@KfHvEs~{y87sm-=kut*c$|BW1$+-mK=%r)TX~;vY*22wL*|&0X3BQ@`jhK3%YOR ziDe0+oeespoLzYBHkJV8sjZH81gtSF7&L|{OQkol>2`O=7%^8W6H=R7pty3rU@1^D zF$bxBadX7K@qCppTkD+PX+za&%{zA0|K8AU%EjBxA5fJI9Dkr{eyNNP{R+H&Kzmm8 z-Uj~n%l)HYy?(MCy+IG6hYub^n?Y;p>d<;~A7pIbd`^yw_o<=#C-eN|{)Ky!=VkKk ztp`WYIQ(&O%jJD&6CO9=|8z5oAhZiW7`_I%@5pjsN*X+P#PtN5z=5}C0qyG{6(zjj zR0DKLRw4V**guJ@kjDv6&}T=~|R z1+_7oS&n?isdVB0P&8%xKuAeTUiH$-TcVs5W)DpDtokHjuQI0MSF~ORI;_l>#gwIz zPjdAIQpWoU@pIApDL$o0S!avQ8_Iv(ziE&^-<`h(-$G^A0ljE&dZA31#_&*CFwlTG z?|T~m`ZnZ!-gV5S->EQT#T}?6uRwSPMt>i9x?HVT zXYiMik*;Xyxku-jf9ayt^}LEqWDQtL#58R^C`l2BM!|(>BV&kzR^X9xka7R0tCQQ+ z7ko2I0R<<^voln3MG}`e2f@iu&Kk#tIE+){NjaJ4{?7LXx`^jBbbHb;-|%cvX-i9S z7#D4o7pzwFi&?Z3G{1*4&F28AT*A~)7ydi-p%`0fsi#JIFXw3=1mW-@k9XO(;eA>* z*5voNCX;6exkMoku_E9=CDm^{^}HA7MK!#yx{2akPhs0jH`RmUZR_`K>>1xcw7T?+ zM#_n$UZH}l^F1iXKrjB&kg zTKKY=Ct)SkaH-u*rdle8xg4a_h!7U0(4Lu*&?{lrZjoTbka1|lP#_%v5@H(q0c&Mr zX}fDw^$b>6BxhNU5rK-1r5XlY9h)4@B0hBG%KBnnHC+b3VwW`Ey|UGDuoD8NnbEsY zD+(sW;H&uSIGyuVQ|85Wu`hcq)6^9>i%G~-psfy{nzzQSxxKB8+G2<>=FFCE?lJu)=bz)>f66nfbX^&Oi8{?igLy~;h$U4(!~ z94dM_?Qne;w0JMOBOR-ntKUR7=QkMN((=g&9y_O_$lXO9mLw?`vbRV1w% z+z3Q3(HJeGA%@D%p?2v2j}PWEfjd^PK4=vhPjRrI#>JAP>uGO z5P&AK#1h;R@oP#nk6wLEbq zk%kSwSeI|p>ao-Ji7kP(ics1TqQ$m)Pz)bK+Ba9DaDe z;d<1wyT1&&J2dlUy36tucFBFHpB0R8Os(=}haMXkwx&yQo|L^{cbtO>Z*b8K-9Zg2 z)t!4ydt^TI*bTl{&Z8T8<2t}nhQ_)Y3xZ&eu9eD3EB}h$M&N$d0eX}p9RBH;<8T9J zPZD_=b>FPOD*xWg37-cM9#iI==@`8!B^SA)TpH99IE4|-(V_diJB+u0OP@S^$28p+ z@nYcX@3CowIJiwDbr##a!b|fJNagN8E(uAS#CiR(B4_*DDRYN-DX9@JumcdwKdKVg zRUuQ7qbV3-5f`zkz|KaFg{5Ry@`m`KNX}GniKzJSt)vYcr)GyXokd0a#w&@GtROwk zf1ALOB?dN+pR_>+4_(?y*kH-9F+bRTIv%4oppv6ryGZ6O?;V*P>KJCnjkt{iRcHegw&|=!6&12@TFBZ{Q9&+ zP}i8gre#d3Y|zt&mIfs41Qaq*tI72Ss)8m$3<9N!2si@OkQ$6*gd1bJLaXPl)6>kr zR#!+DRX6m|XFSulq|0O=x;2?VSUuK(&+wYpagq-Qv%gm}R3qbQgD{t-R4z26l4B|6 zDO7w?AZ+FMD)`8=vNi+P9B*d@*^tXzkAolX5#vwcP!tbm1fjZe&}U$XJo+vSO%kgE zDq-@d2oFp+?oYguCWuK)Ed668;Upd=dBvKW)oyVlqhQhD`91;l`0K) z^O-6BU=!sg3i6_k;Qp<;kUzl*L{c%5oMdlY-mVM(Phzw+-xWm-O*c06{yxx^UwVU5cF7njc=F0EGD8Y09M`biQS zX|6>lL)Wj~%^lxItzEB|$GACh?FH}ojiW8vF*L#}L+?&VcO=-10x$(e2MEL(YQWW4 zPZJnxuFH~RLqIr3t+@8{9R%rKHo2I4yH3ksv0UFS0@zmV5pAPUgX&ox&+y3c2{^0* zXUWeaB&TT^X2?L~zJ>0sY88gHxU*^%I<|a+wUHsDeqga(W4HPz4szJQ2dVO9Ii*vj zNB1s1B-@@LBt^jRrseop4L@5)yFzcUFZx6r&HEB|J3g*-w?PoY00p+HNc36J%o0rP zIP1cr!oESbbrNT_-85du{6e}Z)!kTzeVGI=X(l5Wc-k!TwTEuM;u+AEAIrRGwF$hk zQ?zoHWjjYZIsA7_3>WHBRBa{gUKL;m|EMY=1aC^Wsl}d!FPJ~uKDT?mcOiDn3TdcXzQ}*B?TK`8$k*f`XuJxo=!%3()NYrrF&&>iWcWRr7c2gNY6V zA=ciHab~T#v>W3D=VC5R<806zF&Q-i+d*F`f5hl*aL1%{dX=#uttQR$v4X9x06Zcn zngknksLHuhsABabg^~FOz}jv&#gaoyUC(H?ErQ?y+vekqZ3nHi(|ZuZpE4pH$nbqd z7aZ!{x{|irX(^jYGx*9IF=m#AByHr`2}OK{4YUeY{lyw>EiXFILe_}Sfi}$Khtq~n_e_Q3&Y!h!W{%VdXN0rOV>W=H`Sq`rT z4Uy7OAz3&kb3*M^#iB+kug~cztw`uxoS4H&1n~BB8w$0+Hn#0ud6ahuCV(Y{Q%5aQvk;UHzi}L+Z`S9T^i%+ih;lYHE#>UFHjqTZag)C-V@ydAWRkfP z9Z1kaR0ZEyEzc?JA;Ty{Y2{9+L)aMj2Wq&Qx?5wcTVSnP%Y5?-vD<4b+-}RdI(_q%DC#9VlnSw z-9rw8QKQ-G7%pOa+yEox^~5%yHuikJzX$z#pmG>A15A%y3b@_^{ltB4E}}e5^Gg;` z4V^o9vHR2$sVJUYFr=hFnNkorIcJ<(@(EEOq?OU$9v}lAQicl|ovs~?T8D0g^ zAypRuLfSo!1cgj03zqLe9iq}QEi$d>ASzkjer~tz*h2>wAWW?$%?fQ6VW!G>L)jyL z1E@W?p4EINYxAJu88O=oWen|~Lw4QK2yIDr*_O;JKFxI2aH2U8ar^3=RmT44Tm>5H z=Jthui2uAkwhzzTA0erJ>6IQpQYg=WUehTkog>`itKWP6AQ6H$3Wy6EKON;p&}7$+ zfD|^w`*y8e@7oo%LE1VzI{sa~>h_*eH*Ou^z#?3Az`I|n=T8?796y4&j+V3>5K~-_ zrd)xB&PW-D6(PL~O5Pe~@LLLpPu-kznG4;Ul3sTROpCcGGJZq#)$B!{X0x_LmbpubBv+^-)_hAAvxAsx;3+b?bsP+ z=FF`7hn+=bFoT5xaj1{ufV7^$S}Y&kAzO%9n}~*-`#^cYlB#zTU6%!f8KZA}fI$eZ z7HnT9Dvl}oT;@(Ycb9YPV~<NxI21*S^<*@x5d=xHqmcG^w+$f}9W)FTqQ+ z4uYHO`v&;NHO36azZ>`tDy3K-Aa3_v+%aE3Av(DMHEh%aPzY;4ynJ7$HJwK`uPdnC z%>M2pq#cpXKI>VTD1vjE}*#A55_gK%9TOL06@}ERqAhE1g%VZn5%? z_bNT7FTvu1mJ4tfeY#s*UWHg??<>hs)`gHlP?&?g-e%|kEQV>QWYs+;H}Otabd0ED zmtv^%Vth3zN_@J$UI1-ClE0cRlDzJgzsRpLTo-l|t!|D>0rkJV!kr}bZA02lmD!&b z^KVUa`R3K)Mhmb*{HQfDtJXxk;GORRl!bG6^Nt*RE+62G`$ zbW0bL%*p8M#`2M?mfPAwy`JaZYg$ks4RbCfEzt&rGwWCpKH$Y2k_eX(au)|VA-@Bx zkgNzRwCJYoSIIL_LS=(a_Z&a8SgGaX`A91L2+@h@Q6e$ZQmB_$eC&E?+)Y?Iw_%L2 z*^S(`ZCrNfYD6hJGSaVOXn`gy+6#_Zgg@D)ye>l&%`$GGkz9jqWwaF^*R3!EsWu}@ zs=nlOJJi;(%QwC_I6j6rJj-aVL6?y&=$;z+yTxPzzV0p^Ab`o*7hmjrxwi|i=kFga zYl>Ga(je_7ukoSc+FFiYb+~u~b7Wj*y-IS-+v{5tQT=pV{EYxAC3-0X}3` ze}8;*`1O-PGnxs-UY`Kt(sQ}n1GnXo-fKC_@o`zqFC|YSe8xE(Li4~5YjA|Fus zNc4lc>_O7QX`>{|xy((D5BY%Bxz60&yrT75-rxT<%Py0AhAA3Fl%nY-rzOn3vKh<0 zo-RNSE!Gstci9F3wo$6KwJ=lt0nc_N)o%jRE}Ya3WMtGpJ_#Xt00!MCqHt1hUr26W zI}dx_34GxTE&HFY(o2vB6wYg+JjDpLFxswT8g#H3)aJn-Z0tH3*1LiFCVmMf*m22L z7t$2*uU%Y`X#l16T|o8!qpOuJxQo#Yik^2ul2n%z{c;egX0^`hmdxww16<62Y~+z@ z=6DSCAUJvp4>!BaChJ;1O7n4BoeUGH6Ejz;@pwMOnwD2t14Zf-6oJe1?C#2jf!r2T zg2;HeWUZtgt&5yS6~ec3<2Qd}afG5=_0F{U>SIG^@69KZE2B_d3iTQMpgO_j$L5qf zKl8KwQ{Z0!ojV`bJ0A}|km|@t#^>~Wx4;BbpoX$SGf8y)H@*q2VZ_P$9P-0J zaQH^pYROf``V&xg-&IMP`GA5%i9LMec%9O7)}aN;UxJp@$3Mf&sdnWMNj-^jUDv)R zONM%#Va4)dCllhEuRK{`z0nlAmb%Eg=|U>z39iV(sbqIrHATS4lBr}b-z;5az8D$8 z2_`mvo9UP&>PF+u7hq%im0G>(@ehrYyo(E{hf^~DyO`W5nuBT5svAaK_EB#69$kE^ zqT9X%=}=R9RG*IbPil421glMw<0%vO8+cS3kn=NkeF7g{bP3>Ns>HdSGzs8hip06M zbm#yY^|Q1POTu^KWC4QM?N%ltevW|ija)(+s|Z)s@uWb9GXh52lmL-!sWWsOKnS|Y z=D)D9GqYMd)XE@JxeB*U<8;n$QwR9Ht+kl7o4aT$x573y*c_nXCL8L2Vg<8!FYTiA z(;61CwudiEe0>fdEujp#XAeL8bbptvq#N}5wN03wvs1UhAlD4jmuEvDe-S_WnuC6? z|4)LyM=IqYH-GTZUnkAvMRj?Ohka17UDacK1O6d|6}iOcU(nliKrxJbH4vEnDc~D& zED`~bAo&Q%x2D(`ZXiKdS@3}Mou=nZ)dL#pAIMsq79wC~^Z-~bodCW81&%1n$CUZ< z(tpAm6wV<9oA0u%*&73}yw|W2T9040u-mLHY1Se@2W50< zjuKs$lv@q+if2#Iyy+}IyGqij-|7AdNLM-dx}4VOrei3i~Hd_ADqR$2g6GardS1C`pSF?(4B z;(z6iql1`C$C(II*A-b<*ZZ`X$P7}xjkJ1VlHrkxZ>#rlum@Xq2Ok~CA#>}4Y`7rS z2k{O3jr|bc7x61!$rm}ORTE8PCy&G}5cf+wu&8%p5U%K>xJ7+W#p|%O{S_Cl#aiUi1~OEPqF|5?C1)L^bMRoUEJ{va9U++Jp}{7>L-9(R#D!amy2bHy z%dmyDYGs^IH`8-P!|p=BBlYSH`lY$w7`MuxkLm)Dr|MN<{M9;a;V!7}c~M@~`G3;@ z^#h%AkgUPZ!O@e?cfQ>Jk*3#(WhXE3`TPv9S@2KxKR?(#d;*6@eyqVi$;SDx0Ja3- z@o2`Edq3my^N$RVd#TfrM}M&7@h1mQm?$Ca_#(dygPt6Hy?gTY5i~+TMS2!Xiq_fo zr9fS7mom>uapBwCZx_ji`w^54I}BOZ+}5t{j+4h;wyC9~o6aH~L*cvb7C;?%E1d?~ zxbo^Ru3%VO`bL;{S`_0fol?jsz#7G9T~FIL9bHhbPwG*_bwnTaEUSX@nyYR>DQRw$B%yK`-Z|x^?A~v9EzD1)0gEa z%&0Dp8O2I+!S>pi=LUUuDH*kE;!RPWxn!`)hN-Y3p_xV=h@}0xJv^ex0Wm!pU>j;* zRESNSRIJo->eQDj%STtlcO)d}DXt9` z-o|an)sh7zb!$j^q*joJCBjh`x$T9H4l6}cv%g-X6q%0qi}_vN$ZTIV@s!{8iL#Fs zH#N`g1PkpgW+AKesbecF%0+vzA3Ag->&(;9V1Jh2&l-NvdVndt&B219UKoZjKGoN! zH5@qd&}FjQtz&CtT}-PRTDz!cP~{``=IIC5s$|^l@UWn+>-g}LWJ|~j4G)HDH4|YZ zc7U)jPul}=M6P`?WF>*Cv2E0U2l)Qjk}gCs_(wZ6%iGYD$cYrWf2&OG7wK! zo)R;z`CT~Dp~)Gh*f$-~6Zm*hCOihNA%d-|83QB&r7c3{9m^)xe!m10DGE!a(yDO+ zO;KMSP4+VwPBq8~#(vDtWjOWvqR-D^@`lgpIGz5QoL7q~w7IYd{Z^W{$YSNpK2ILxM5dG&xWwQSctIXphVSNr4Jbx#Vu=1e*z z;E}C6U;mWoTRlz`Bxt4WJhxD%t-R>&orh}a?%fI-b}r7wW}Rf+XtPd`++ed#QA9WE z;{9Yd?kqaHaVIc$zi~Io(rR8tw(VNl)_NexARlzb+qyNVxH}$ra;qalY9z$$J85Qo z-zjDWf~TT{2D1lcTLsOL%A-3_0-!rkrh?#!DHlo`+swJ0(^E>LZs^#GbSx71E*mJ-R5d%B~Gxv zvi_O;o}qQE8gGk3eqKj|9OWf|_@K$7y^tvn+`I}N@(S93xch79$9@G8Od~=xS-00n zMMsxciinFM)O?c-9~sxz_Mvrs1)!_z>%d#r*Uen;Y1=I(?yr2q8q%ddFEdE&RNsR5 z5fnPi%^N}q?AyAodoU=4C~sTOMBb1zloqU>NntXduACti_e;J|;=9V$5*%9^oZMs$ zAWnd(bMR*b_$mba7XtgUJX6vaZj-&5V566J@a4(R-|X*h)$Yl&7ic=P{RFGsSWtHl zgc>ROk4pBUX@E#sj>A6Q^`_Cn*12hg)grYLYhi9AwvU#H?Nc51Lt^`g$Nl>tw)bqn zM~VcY@l3i&E0$MI-;%UG>L9I;+NHHAuO;}U#iZdxJQ62Dq>)ua=B86|4+QxL zSSa0ll#cRwmE0?dCh1tk8e0_F#M;kLFp!IJCXW-bG{JS4hx~n+Kiqnkpd;wWH?hQW z&P$ABSX8~b8bE9+VwNwjx%|}_NcWzZI_Hth1O-{PidIvJGQ`hRt#Zwc2 zRuc}{6z9&UQ@hVFxLV%zN%;J-hYYzYafH%Shy{Qi!+iQjdUaDlXdZGl)%w-lkp#fO zV65hd>Uegx!4(ghz$hY_|0%%nygW;XbjT3k#xQgYKvbO69^>!|c@2eP9+Dc1lQuZ| z`pa*3zOyc@kH|MYUS5auaK`fZ-FcCvBj`r-LPyzonPt_DWo|m3oMxp+PP3*ZX5tfu z=xwgt_e*>Jri+sj)*@V3q$^xU-6ZkwQn18Z;nmH-pM$^pv7A5MMf0p2Dpw&eE=6d<2n)qdwq znG|$xo0Z%Rw{S?{dY<8U6|yHnRot6Vx{XI`(X@KD47Q$!82DDf{!+=TzXI(P%TWn?$KpIarb zK=s3LefslZ@@ViTmYWuTBpkLuguxe8E#ix&B(Ell8t@zE-+XvL0z79RLtx=YNskAK zGUKq~+2)}8sQ@`$At9kr*rduCoY!z+{h8UkOi9k<2NfO32Tsux)gV+QNtqvkj?$!T z1^fWLN)G5L)V+%$+Mzg`JuV>{@u;%#Av_>k@To20`yH8rn~8^YTf3E%5wc->fvJscNuf1vo= z!X?+Yr3eV!P4PHja(fh-hy<(r{(r^yyPJt`jb_0}y)-0L@ck9Rsb6I_%<|uX+<${= zEXWQCqu0`Zfy!uO4CU>EK}4C5762bTSE(DT)IAdWqipo=8xer$bw>o`(CK)dJ(uGD zl3(*7^yt4&jFIYf0P{(LTudF{rmt9oG1K+NSMD8CH7zD%Mj^7 z;p41(t{6f;|84hJtcu9fQ}p=J^>Zb;p^^|AH)v}?s;4AEM9Ke3J)t%%^Y;nh=_`wy ztnsWC04wX0-%k2+qROwQU34yx-kg&is^{uPl#HUE+0}pva-X4 zYOfv}0a>}{U#f1uQ!V|mKyiaB)q-Ma{9^~mUZb`s>@{i+hMr268`15pWh}=ToIyy( z-ep%05t{iV{XNYoiyxM`$i|n+RWZj#UzQn|KrT~h6vZkN1fs@i!ZwoF#CxLLqg+;^ zcOW+=dg7;#R{_=lFB4qrZs~n^Xh`%+%3u`;BhwNEGCc~PI}!^X_*zq zkg#B4L1Kxj!C6rvup6n(6z+qaa?13n5X8XkCmg9Z;ZGVCIi=?0emcA`>;dj%Wrm~j zI3Hq@kwpoeOCW1lvJsszxx?PBo+O-8IYe1o^`RNF}!HQ(NIOFbpoXAU} z{7v-kPmhZEjIp|gc4K1$WfWCXN*M_y$Z$50@oQ%9VP}74L#$2OLA=0EvMb&=-McIR zF%;Sa$P&&iRIa>HV}FHxVw5i6S_XemBuCi{%x1RWOHQ_Tl%JhJJyU&F(QkKO=?GRf z7`6JdZ+Vzj80x(gKopHJD3fIn_a5H zSj;r@Lq<;GRQRjk)@X)K z`;*hO{M9A*i{f{y7i7fvxG0?)26;?#d8k~X4~ip8YMT*a$Q^d>W2WV#I4RRGaRpCvJki6CwFGSa{dI7GhAOmRz4}lBbQkG4zYpl z>7>AghjxtOiwmuc_L5RnZ-?`eOtqxj2_~nk(w@(u%SB`~8vSO1>BOp;YbAh(WpcqG z>gn`SF&$sESFkhtzL<(+GHydAbuC~($c^t0zuf=%XlL)>>*E%qd03w2v)#NLI%b~I zZpJjCM*JF7^l{GjFgjDSLnq@cO^Q`JI9eN;HtYq>RBK057&ixhWi6KcC_9^HW7q61 zQq%*OW@!mJ47xu36({vy7WDKVA6RR_kFJ9?K}X^$Nk%98@QZ7cploGsTxqFUm}BK3B$C0fg)yRQXxz}7;UMpE4x{(^EX6;MaOMo(Hv z4NDPE$wsCXqxJmXtxeZewC=B$i?gNQ=J~3gW0h>wLeVn%foN47_nft6ebz( z;%U@h<`tkrO|If)G9o%!U0Q(?IUeI*9-DOnK+vmj=7dXFrl{PBk)id-j<~-)THP;~ z^6akN*f$$zGv712D2fW0CoNm*m!*_=OPogMlevAX&S$uRT|Lj+Y1#=5do5O=9vlN! zz-Y8tId}wN0%>)LErPY4{>sHLD`#o$!_j&b>m0HP^2bG~2JkeyNPo|ZIY(SvIwJ6f z+GuCU{-+MrUPgkTfE#CQG=H8<(zaP60G$>tMoRE8L2_B9xVA*;RU1ZdN}F^JB@xC0 zqLunY?O(ly(2BVRc2R4pPc?s~v;pPO73l2O!S_3zrT*o0_O*NynN2RoxYVU6c7yq{ zCjfsTC7UTyXDJP&@VEe3$zfB6Ry6-er!$_*U0x-rDa!=;g9bKGMu{8?%e<+>BFqh#UlMwyQWE7y+z?i{-UsmV8?Y~%g=`ohdzdf4|UD=qIMo+!cc=&2{|8_Z(0qj zuQf@s0NcW*8hWodrcMJ^?G*|Cmih*5g^$6#7+!pvUdbf-Nmin(fztxYf_}duztcxq z^YrxwPY+3Uy0J9G(Ym}cbO@U*Jm{-Q60grKi%Yv>S*fJkL`IWd#|77;IY2>q9(g9& zbk@RTrs(K!|65w2`l!sZ>kiO|m)R7TA+Fm&Nuav|@Bf~TGKN({kE2{Rq!nKtw5ADw zCIuB*tY@=T)8%@1sB9Y{YR(z^-5$u}^j=dfMq%K=#TnJ)$>cO?c6FS_@R z&yy9pm3gz8C@grSMV5yNR)gY)30CYTOt5B}Fv0CNE1f92&G5F~zMN<10LcB8@cmvt z>fXKyL+qBdVTkR@pENHTh*$tL5V5HOpg2dynU7?1m|`+PR`@|kMeW587W^iDu)ueT z9~@_} zKHmLgfA8zh_m7YiIjIs4No=_&laY##UksdA4m@Gxd6M0F?$Z6uXM+UehW?E8)XzYN zLiHL4g(5t>cDvH4v0Drc2ijgcb~BYOK-wty_}tR1S9~LA{0qMkpch&X)=}Sy188ri zQwWDj4(!w`c>11v6?E=;_0tt}*oO~iZm!bz8@M(6o~S<7vw>v1pPU$i1~FNHW)XqU zUJ4+eC<*qv5~&3kTEpL=DjiHm*`IyN+Dm6C9#y(b^RntGdf137xIn@ve%PQtFTM67 zAVao2#{Ku6BGChwzQl2HU%)u+h1O$jP5HUV((&v9pW8|O)tf4T`GMpo?`?yp_v%VQ zqEQW8yn+$Er#XlsZ}^mQToVroooAaia)^g#g*SDYLic%o7JOtfWY$-fxFqQ)bI@5x z2_kych?ZdYRfOL`8>wx)k zf@kuO_lQD{!2CAP=Gimvfcmf?4GQ?94=#K@JITS^+^60kcv4F!++2h_ezNoJmsOuu zAZZJMH_%c*25_`x2*ak~vK-#6^2vOh(hF~}LWhQ%_am{~d|F+me@rufAks6j0FeE8 z$yHkS8kORAf$AInFs$1-Bp@bS^3d8J$LdnYM^9(1Ji*}X(fC~-Ew!z2a*}c zIIU)~R7Uyk5(cv!Dx$vI!2iXJh_|!YqvrM(^!NRi^p)=Zg1Ej%O4IV(9HBK?$Ft&c zL9$F)O~wFghFts;%oj=kMt;WNlwtzb?=a?}5~mFeLQbvnQV6~yz!>=N;=jQQ85=DCes>2KSAG2`s=3ZNNQ>^avg`X0_)d8nKl^(xwhZ< z`$f$`xEK4JM_j2CHX#EC-$KvAa{EeR$YeOuoNFUOhB#k_Y`!Z*YaGS6k=X1I!aAo) zb_D+ie}|y2Z$Vz5RXwdRA{kXk5TrPzdnu;-F|obpF+nQyx|C=3G#+9mtYgq$bcb?X z#G`V$mLabb6R6%Nwc??48_CctS18sv@w19h)CD)>yoM&-xVt@idk*G4uaBDkSBQWF z7>tx3%xOr(126MIEfpE<7(zZpDy|c27h#(TtP|7u42A401@|s0QmNGJG_w|~e#}%@ z%8g7|5KlJt=@bmQ^m2ftCgiTQjZ{z`57j^ z`D#78iBew$$4p2JU~Hlhtfx6jOD-!y zbf~74B7x#jW7{W00M84g$Olv!TWoQVWZxKvVM2ON9wc9uSwq2PCX7BONqk2IUf$<| ztfzP*qp$(6-Rq+?Ju4aUPUX$MZLO+f_XxP(GbWolMK@x0jz2q2J~}+YtL`3mjs@oB z_6x|JIGlp9Knwz-=ig=9RPI~Qx%COiA0{W`d_so?rh=A6h(R=v&CUTy6<+OlCNTox#HA(->C+Qegy3^?_pW%#3=%y}DbGonaiXjeL zLH95?Y6F+S9+@t>FvDwP`C0Uu4)dXcge!oidz`Tvs~J>DoBDWxw;-M>8>L_wqm`)> zBIZ-T2E3G>bBHKP&TT_j&Zh||FK~C5W6 z<-jD1&yY%>fCX&>PbpuKWTKL~hh&D!RSUFQ(;zv-6Y293e zF^ySS(j9^@B`V2s=3j9b#@Xl6Iv;wW2g0!N!FiUfoj?~PyrV5S8DO)A9bYhn{U(FLPbUd(u3LcR8 zc%V@)9`Le>sTt?R!K&SEur<~}E9z>z5EfEXC(lO?kWv|q=`mQgGU1HEJ$Fd%JL7BL z8@T$~hkdm?{e>#^n|<8r99#d6DqnbPI9D)T%N`r&iqmF+UhE5qI&fMl;S*X1SeU3E zz&e?e`-l1|+x4o7o6X}dWMz<-gxm%PTE!}aOD*62VnwIcyC4)~*CU9VE&6M5SRibY z*iuWTbV3Nlb^YtwTkA0N&)UX9m`}Jg!q80%OVU|2ayhv)rTkD~-{)WP_ zT$GF)c|znW0xt6$?DaOWlocjb63JI-Y6_5%9@QhswbEnwYhhg=$+aTXo%AAK8s zbc2^JjZB=&Pduif;T_qas@Q>BEl>elvbs)>^OGc$#Jl z+vm9A5|_HGn+f9`+oR=PG=gm{cf%91it}0VAmsjTz542_*1fjv(#ZnX?PnpUBXb0- z{F#?sUl86(=zm*#FAs~N9OY9o z+r^pIMVp*V&4QHY-ub=v&czi}KOD)bs;nYE+>ppewke|j)Gqz-*?hGN;)*OL;OoZJ1G(!Z)j2QrC0&Pd1x;$}yv@Wxa@;jS zMBP0j#$_ENMHTmpFuRk-vw-6uHTbuK!{hNM=;v6zELjLdDq7Y7!~P2vjJgAu8EwCn ziGFe`OLO2lJe!cTYq}kLQs%_|XpBDaW$h2AD-(k&^|} zXTksu%Yb}wI!PhN6p_GbjzL(~GG#&3YVe!$@7pWC=uhzC-#+rPC~)fqeg zv#ebki`NxvW9ia$tK+?1q1bgN+NX7$f5h}IzWANGz#xs4k5D@Qs5PH;@p zhF6#ZCUL<$lHeFRz%ZGseMt^%U%0bV4AXySAGcn~(%EOnPFiXntxsC5Rx4?>tH*8* zQZgN8=VGhV+MKwTT9DrC)ugiQ0y0%>9j}x9evxR>D?VNg-COYm%chIUX-c_RY%mVj z6^UV?VP^*HtjSM(Se0${e;mV4c|Zsh-BH*zY3-2Y=&a;j0D-N`A4%`T<0OX=m)X1B8ZQKuDeZ~+xiE*91~w$UR% zkKTHN{9FE3dAiY;Hu};=U)tzP8-3{>^d;+xVMT=TN)(uq3ezg9bWd`*VIGai%Acn% z@uJ7qu;R1}<2Pz{-zvq)V%HqCNxD?-slA;V)N-*t%Q?(kr39GJx!BGlP@_%s%&$qF zcW&XxD|?}G3ZFapR@`-W3(Z?fC!T-uo=1XD_*8c8Zgs&fbv6vg_O-;jY*QmIrjE>o zh_b?dx|j|o;xzWA4kPGwOoR@1#?M?+Rv1Dnyox{<05Z|Z1&Agggx8mv5L5Cl z%`zdaOHIq81Z3p|L4dz-c2TI`*LX%EofV?Cs{J3dATQ}-N0olRT!_MT3q*-LAuKSR z0{s0Hk&-6BP;$hmL(zp~nBteJ`H5v2nY8dI%SYORQS2p(Xt4n<(YHYgb>+-bkWLu^ zSYJOHWC-yB0Ickcuu)Px7)3eS`~WXb2|p?mOi|NcM-bzRg#&rcu0g{00v5i|KHqf? zbJmH@)p0%Oa0}^TTk!(=B%nXe^oZDKn8kEgaHLTW#>;Iqzi)6ZUqba*<`SdSZQuCB(f46Cw0vmc&Qquk>t@h)iY-ec0FH{O;4YllwXk~ zO1QKU0)f=CPLdAa(1m9vMKGum3@R`LQ=RtQEEqAD@u~)rqIb$@bHALOTy_~^CYM=; zB7)p3LvPqJWYeOdjO8};58>Yd{7(lLT~5nKpmex17}4QXj(H{fmB&!%j33~s^k zJd7s+&ogNk?u=jI=*rCVG^VE(tZl95iMh~PU-fiv)ol72l!5tm8rlN4$<-D({q@ln zG~XR}<}(8Y8)}0AAoPx~>YJj3n4R(lsUHAX+-L46MR2$8%%QtF??mJp`sA3Mw@$Pi zP6Q)6PV?QHpBqPyd5ejUPVqo=G@zwaMf)9}IN=}m3#Uu2zl5vBR@jY48I?}yCokl@ zLBvn8Z+giXlLV{(P#w;Bk6^*HoSyus$L2=60V|%2gmi_VA1lEg22u49ELq!Yw85b6 z`jB>7hn0dGjiv6xGQ>7B^{|^&Gl?o;DbVQT|aZdw3oOA zPCA@Mx#Gudj@=gKDW*Hk*asgCNKbdv?~mDcvw}J85w{U;M2+}Td(7SVt}(?mqh4{VRDobEo+!oNZaD)Z&=E6^oGf^PqY_5enXt@Xk3xW zYkT$n>a+!$?{!!&xaTN*^7#kTzP|Om?)@B)97hq?>AFhh*jOKs{SZQEfg=t)mx9Sk zV3agW1lqg=Qi%ppf9xAL+kD$+M15))zJG$6&)2hVdf_-qcQpdWXNG= zc_)?0d!4W#fo2t`9u^Fj}E33o7G&av6af0ZqK-_mGzBaU>Q*jqSv6iH=J6^W(h zfG%6PdYcP1S821hMl_uUB2#xK@l{TK-AR0LJ4Hb8r6bi>l_eXIFQpX&n*Qcvws# zu3_42^G?^-H__4)rGZAY>ztu^$qyaTCLlWp=+%dFH=>}MyRmv~gaG2S-D+s1hN zzZ!392Z@l;8KoCs{YJ{qaTWum240jM>h9&1^EVDzz%~7CDEg+zx$2%pd#A9BWRbHjzaD zCQ?g47LL?5OO5A6I2|eaJ8SeV$~!9e6NDw>*-`EyK7dwa8(7wXxfr6(j0Z@#PxibH zV)YUmlEzT7H_kQO3IX1va2uKJ&OY4*wXOw*FF?x5RnctwHSi;32!3i=rJNWxkAtb4 z=Kz?}SACnAkgn}I^X$&gy+3(6|Mlv~Hj%kRPjW#|S8~Rx`;xJ)tuup$WmHqQ@m+B4 zG|NWFoJR*=D`-$@Jl{>jZkTRQi-X4V*8fr%K)aS0Kv2pvdLVVxLkCda>$s6O{Irr4 z529c-uN!*N(0bdzrdZjh4f8KNY#uz!fAH}Cx`=Vlw=XttHtryL@d?19$A0U1#36N+ ze9qxJs&iKZg?w|~b3kUX<+}VThOD^%1Esq>_VAj>t9S^q#${9QV$WPPDeS+49#fYv%}7_Z9stS$8K<=gPIh*YCx$W+kG= zHuir=@?q#)Mh?`$Zxzl~;vw4t6J4ll`jd_NFTxabhh<~gE#O%gNm<&Dsax3gtA)+L z@m|Wc76ISUbKp#8LPCw>Bed$LwCr3ehYg!ye&fiRJLJ_!6_oDC5B?TcyftqlXlZn_wRRISF5dN?`5AaBFy0wo|Y^ zFmfL`Ipfp&ka~p9C#5XU>qO_PHJx?Jx~Fe^7a>ejaO&2jH98(B8kSP!HfUBiwF^Di zMAFaf%ZBwB%%MuwbY7@)vz<)3L*^*cTH0xsRj6Qw!FiC6^y2l#A?^-y(DOWJ72~@= z>1{D|81y-6C3OiYU1nO;2c2`c$oleVNJAS*ka(E&fYQWsoCFg!D=s&D3WteSRxxX~ zhXyk|DIXZJ@CiXlw%wc^6}=JxWnLan6x#qhb|UPS@0ziSnaRdNW+WFfR3Vg|&tM{*LMTBO$mOPHxV+Tt z*NBrufc9}!NOa_sh({yYmZokv@8xCJ+ak1{tVn1fsu?ot$?`G_OFJUV2lLOAH0lj~ zP-6$`LZ;$Z_15XuM(nLx?EPaVbFaAa(CvtguzVCU_10>SeWV6x|#pH)ER$ zEtl)_tJ3lnu}~YO(y|L^nxzPJ4+^v8(zMOH|C}wPB|E#I&`OoSuqJY=N=p2hy|g#@``qmP?$_ z-wsgK%|A3wN3JyYhNKA}d|0(JaL!TA7KSa{n#$#AqWREZ2L$X4Mk1ERG{mICFJyC{ zVW;8LDxiGHbXwzB3wEY!H)yVFL3j`sXS-H~=Ds2q_Vmo^(U+ z*!$$kr&Be& zA~=Au!9(j^e(0YgNW)t$S^&HaOX{1XuQ_l&#m(FHvPv8ad10_E`IXZ?U5kMV)Gf^~ z*0@CW+gZ|h75sL-3MiLczPE{Mehw5gk4-J6;v$(C%=X6C-fDFv#?qQ$>)V|4$$uil7aahxy)TkF92${36EjXYb5S0;!Y^Gq_36-JoL zEl%Akyw%1{LQuhAzf*K$94iURLNk5pwybKP&b@&Aa>(L$(S(6Rx%M^LD9JJNw#Di3 z=^@nrGD-jB`oW4%y5bfLkfd>hjvthuTCK>c#))8F6tL7g|D_Ro8?ku5>*BcHuA@d< z6Y?_)SN5A1{qF(}6%o%1IZk!2l-+{Yxmv)Ja2^wBCcaiItnofAlV8YB<)eKS3tK+Q z=w=F8DtI5AOcf#PRnX~$*+N$?auepO2>jWqVZscii#EKS#XSm4pzFfyp|3{pK!9B+ z&Vs#yu$5h>1|M?Z38AI}f|FpNak3}2iwOz=1?>FmFk{m8=Iy@8uTW#1<}_C6?i*=R zv#y~%N1)Wi!XXZ-h{?^6-8;FwRy>Sx(D4}N{;6Vnt4ey|aos3!F*`|z+2{ov zXM!U8IMB%ZMWt;@3c_zW1*f?+k$g`}O#R|nFPgLB)0W5V2#DvOCI_5C>WuCIX#wn8 z6P&(nVERdywA<9{&0_#ytnVWqANhsSE}`lsTwKU@0e=&$zbhTQm&MtS`0w}kf1hy( zGZ*!?;w{{wRIcRn!O0CI15D_>J#*azeNy2dlRBDi;g-F=;rGq9qH z?WQe=l?m6M`a?BQ#%<4)C{2^J5^M?Mk!|d0jE0J3*c}F2psEC()pG7(kiDSMTa?aF z9}X4j2qea3BW>~oCd;E|j};k0ev>a%&b>1!q*?Qh8 z2YCE_^W-}V)YbGDGV11G17;|7Q;-#lcU7A^RkQt7qlHy%L)D^k0Y!14ZIe>x)yFaj zxiNstZXMpyw-cVy9uJU&S?0m+1Xb)Nl&kRctld1C zT_cZ2t&hIBHvKMBSF3(pZ_TQShO<_LpM*_6%T$AKvueSqpq0QEqSCK2RRGwm8hCVS z9rVq$=y#cV2+*oY{}egyznXA(RW$i@$X1N{v?620T*j# z^%zDk$UXUHZ$?(q`E`5)30hj1O+wYNsyL61sY-11?gq#gu#<)NORJ8IzI2h2z4eg2 zRQ__DuUG9JQ~1Ly8ger$=-+~W;^}`a?$)5?)u~i?Nsr03=Z*T=>B+@6r{5n3#?=QJp+Q}$iJC4!?>plqqyIVD<7t}1HeaR~!J^>`&WVi@U1sIj=f#MQI;97(c z+n*b8YC;n425o#-xRt*r-`?8c`6!Y(ONX&_8C`lOm)0f%T|MD-4vVGyoA~Oeo#KYNfYm^ zetzEkxp8sac-A;MTGv8HlW+aITkQtc^SYX!>zHmb;fTHWv3bQTej829WoKC9=lXGD zoz_@n^uDZc;AevGC-lQX5{+)tH7#;K=k>hB^R1BM>((-b_YcYQH)$e%^$RX`7xk0l zi^lm;{cHvEu|@9~T<^eoOFtZtt8aStfL_4SJ+^e~b&~vPUVWBA7^RUeePV(9Mek2p zWWJ;L^54Q^&co;7m>>1z_26S&ekVXEsM-k;9KD2Z62_fL%~^?Bzx3AH_&e&1hS4@u z$~WQ(L4>L1UGhQnQkrYD@+6^qz+oQjRd6<8UnvfI@jn#C5=I<`6DBm`F4Da^NuaI9 zdAr_fH;&&aGZif_Fj990_a*hiz9l)pdy#rIeuiV1qRH&GuwqnZOl+9fnxS(!HfYny z#1cR=o+6NY@zsT+1m~rQGY?!JW>ADXV3elOV5s#0sNuw3l6Cl1w9hSwOs=lNl*2H= zF#bkYgajmRz17f>y1kH?U8Hllj7?Sv*SEln33$coLhUqWyxVh9r=4F;6kWv`#)S$( z;f=DH`72KCBc7sNnqKUeZaTmF*6}Q7ghokuYw7^4p`yPksZlWjPqL6;kq-b$0kH zhVwZdO~IB3P?s>$v>v(1p9gy?UW+N2YIy+FZB=}9Rr&n~4@;P+op4xjsR5`eJn|wm z+gq~eu8M{LQRE9`#i$WbhfOy~{N{IHc?sj( znI88sj@n#58?c94?80u;gW=E>HUS{(jOLi47lvBCd5k2<1fqAk`}CMl=Up1%GC#e( zRRONxou&$vt|a9mqRc0lkP}E1JFcfiEX~O zra$(#!E&TfWRD2o6+={E*bbqRY?d|4Ua_9udG0KD%-W4 zok_j>H5d^DtWffnkN|f5|4`mDm|0QSxpw*mx&Y>(24hJ_16Kbf;zP)FN@q*4N<83) z2ip@d-N8d*+o$Bkm;b6m0WQf*moQ{qG$YuS0Tfp;RnE*;VN>A+MpIZOW!oHXraK;l z{TQu*F)c;7%(&w(2tn>-`J?oT>HFaPtp5A}S3s!0Bp=w@+RtrkW+JH(4LY8?#H_=U z=w)R)?}TcQhU9ZI9!iAbEMPrMKr+jV2LLVW76|>+*GjFoD86e&zh2ZA$c&g*0uJm2^x-7ctu1+7 z>&GQb2Y9g4kqdFgn;SzvZEELp?84*<{)W<>h;}*+iBZG6%eWU6ce!*U6=65YNWZMu zc6a_YiaUSy#sr(fatEH=g!M94Oc@9Rk40(?hsY~<7#HA=Ak;E;TtM|3Ak5)$61tt| zt>UXOj+qrzG_Z!>qemc}9*cBBOHevRM<{3V!eOsmuWG@0L}O_>!$O5B7j>Zoi_1vW z)_qJ1+8~aps~v0yr!R2CyNb*ewMl&HWV!}_ZD+{Thrb~u8Y&utnhctg02QDhWTa!i z#DOqO*eyxumStUviJ(Et!6}27N`*YN50xnD+2XaoW;p$Mb$ItMNZP|sq=$2BXhoge zVQl65d8z;pcO~;c$V{lzil}5{<%wkYA{svw8p(4p9KFQdaGF?+h{{8Qw{``XcfMr7 ziL=SL>W#^@_z*-yJ$`Hmw5K(&Lo84U@V}b zX+#tuk7gxiiF5D^(xXtkR9U^09-?}0lHRzVq&L@bhojnZP0)fU!_krqrU&6iG6?HU zXWYpwA@4_;517S9#~L7qc64t;UCLd?f@xK6o(@hgFKP8@BVVz!)_yv`u?hgS!qUY> zi^>$&XVx^alnmy8j*mLZ(Kq#zuN%QP&GYtY>laqVwHrqR=%?NoCr$@Um8*!9jc(GR zy)3Q}v2Z4=3TM5D99%_V7c%Kc4m7C|sI9astrHO^EiqJfPwYvD)OA6f+S#Fb&b=S~ zeUnBRz@mopf}ZG(Zvbc5iIhJVEi#z97exhQj7r@xifYB*fXerRwHs}&LD=KH{f~F| z_IDp29Ohe)J3HF(l_38SYy;ULl3xpgQl%@4mVny)MchvQsr}7q>zo!{T0)y&{e;Kg z=hD}rKj2azfjG0n!0wLUc6J(&4XgXk&hM4$;qWFq{NMvv7)G!y9)(x9Q8D_UAJO^E z&GpR}qo@1(`v)KIZ;JYl{H2Rl25!BL0;>_$)i(g?K5G{ONzD~)h-yJpi@AHnQQ zrzr#M&w3EhS?&~I6&#m5csDppldClB_hGzgp)YxRg7qFw2?=#zA)*ZqS6}7=ICwaf zzw_;i`7ezb{4$PuU2#LHwhq>XTBj|8feloX_=h|~G7ua$$tef@NWKz!kH|s|Lb4Dk zn`A6&w$f(Z{v(K@m{8Ij-W;JnZEB~qWqS)yQR@a^K8)o^vSOgy)^h?Z1FgfQ;IKJ> zbow^ePr2;)@oXZRq> zbQMQ6QkK(oOteu>J3#rYyE9ascZ&g0mf@~vZaR3DVJI7@Q|((5WBw!WSex4>f}i=o ze-P;mIKk1R<=r97m^{Ek(sYsl107)0SV;wrRU{h-4E!^OpTu8aA6oCN?qo}j8(utG z>uzsvZ+CC+y{qKvjGp%-MMGr**hTxe#s%tUIpwjzzemPxS54ckN`gKKyKL9h4Sy}< z-II`m@oV(F`WgKIRObbD-%)m`b?-hyi)QJFGMj z@ue+fU@$u}4ixOH$YAcm8v_QrU&8l7B>9bAhy-Bq{YBLwg%8HY(oFdcNNdF&UQrA| zyx8;P{#W=VxiFJ??s?7jQCbf=K@ibp_^X9BXZ8KYtgPe&aD4EkvkaCd08DXvd>eOm z`aFYY+u`m!va|e%_H2I4%9Jn2wlW)XeV9-*Q|McbVL8AIPa8bD;MtSq?)j|+F}9ei zHN#w19w4iF>^V%5Mf^QD!1!$!Wnx8c&{q@O=ifW1RJE-J(Jyi2r_!x6%6v`c1BCRF}nU3xmRJ*WYG0tGio$k8~vH~y3fMI4| z1EgmC2we#*5(@BsomO?RVBAVgz=J1Nt76#De|$7}bU-)3q-7iZCh(6Bj)r^SAMfCl z3MgBei|}Cqfjj(ahClmw_?V!Pp#t{n`nAs0JIk*p2KK9$TRliIikK&3nn$drgq1be z6D(pDSortm*yA!A@p?%s#3ZV9>Dpk9{?3$=Qs>oL!6`Xm$!Rg)U})ycT`dM)zq+N- zu5jF|{*E(@CZMQ69+PPX8&O4g#;=t&PWQv&L;QDc>Z?ZWhNffqlL7qRr=>lUuUXwK z{-=+I=XSe?w>zu5c1YMG`l%NK>}I>n-R#`}zI|pUY%*OAfvZfz$n!9iIh^Y zRx3{-#L7^CtpFvx&7$7#_ZdI@djVqXFUL=(vXk3OR4Qqd?2rNe9sx_`zMLheQQQjP zOI@?u2*26lm#a+xH6oZ|T4Y7bJ7v678ZeZl$7%r%TE^wCADOiSzPyo^aviYs)>Boo zt!Ba+{pu<$s=$u$7ndfuUWi-vT+z?3H3TwQLGbw59u&7%4h^c@jK)`$a{yiQPQlA< zdZ^UfJhc~ya5t4)t3aCBGD45x{$4VMj(sJoXms^ZPUl^1#Xc)ySqZ-{FT_!1SL<9v zx0098-YhSr%$Jvz_iU=>bze=>d`KeQ^)SwBbpp28z~XGOf(Xsg8&$GbRg528`)x#u zSUQLk5!+zGA_l~v*)D1$O3EeDcN?HpcD8^qYc^}CknqT_lFnk`s3i|efDrAuSetpT z=)!z|qXXY58O!*5S8P7>Fx};wrV7jG-%%dnysyN2tokyB`FZ=RsEZeVdwG$@_M#?{ zp%*@ayAr{ey_P)|Vqo!lM~wRAa%Kr(R4yF8TBa6fqVBck-r zeYCqpyIK+$;O&cuqZ_IlC`aiE%?PCW5vl(heIFe1NJN$}cq~k8cDvget`h$%^#dmG(k~P^D3gSoi(!f2DfBx%huwH*RGuGFSE)=>iA?8aF@Jl)VTGmqgFzr*L#+ z5b^0G5%?l+mJA%y&;#Ch#`nyNw4^YUz6wBz4g>sc=gCyxDG}3nNQmL-8csxkm+cGb zxu~6NyF}Qjwwu#sqU~Mfk=NhO8FkrBkX1)tj?)w9Jo_%?+Wq+VL5E8ROND!TL4n*K zV;5bYm;_@`NRnwq^zRmlbkg+*gzqH)gY*3P`h?W zO7o19*rZ!W2prT5n<1(p&)jJBgOk*h&BWB!1^OxZ%pwZ_2)N?rcPNx!Q?tOEMP1Oe z(=w=1_S>9mp&B4Ltbqo*I;p7{AkENv4E#O)saj`IT9uPLbt3Gyo&n8qOi&;F7|?nr zw^3)hDob}^O2;=1a3wAAt|PMm^r>A%w>y)2dAXck(sv=)_SHo@$k)kF+4bx?`k@L5 z95^F8053skm&I*>=95D?Sv963a=wGPKGxnYr8Aai86d<_CYJ|6mr03x8Jn2O4eWT1 zeq68c5-nPAhZ;a7{_zVOH7Hf;fhqyd&TU5wYZEX}DyVkQ1sfLCHZ~W7BG=%dC{CW1 zJr7^}6IA>9B7mD^06(LpP@7}x{=nyvwL(4%4T6xlv7N-j@N$PKmFXnegtY5ficg-` z)I|)=U3BSQB+Y$g7Lb?az-UTt)EfJPpugB46@h6spO}Y zoJxLL&Z&sJi47lpq>MwSW3uO3Infh`O1&wB@%|2}7neK2MLk=^Aw#pTXC*qDoV%SG zsj~p%s4vEg30w$$KFvoOAEhq1U}Zpv*atK~V_FHH7h_l)i~>(G*?$(J%=w08IYm(l z^0E)M%(|JWIVpfr&oM0GO+G84_&WQUaDs(I-$&JpQCv1L%ih`kIRmyx*{4PRW0}ga zrvl4kXZC}_7`L`6qPMmbK@O2W6$B|S)1aD?CZy;Nq4|#dWN{xAzCKxQM@f>IqI^=SmTaZYtH)sG=N|{2t z9Ye7Kk(#0V2W^td)HNHSNBT`c52T+V)!~j@>#k~3q-bx`vpALE=qY+LWpR$t?b*42 z$2f%Y&~B1%B>88ap}O!iKHAh9O`Ti4gI;uR4!|PZT*G+}v8w%{PeqCr1Ki?eVN=#w znyb%b{Uw(|F2gr$AG|Sc`?ysdKAd^IqG_ID4YK+-be)p+>dp=~+f5$0 z=WC}8b?Kl_5W=OJs(q@#e}%Rj3lWIape5>DfTdz-{Aq*XLC3Z;^0<%)^V+%lkX@NQ z#l^d}rGviDOH#fYt;umKuAM2@v=S9@->ur(IJYh|RQc3_b(kmx!d$AJG7b055Z0Yl zjnB)p2J_HtxW58_sHn>v#cg?`>AMQkfhW}iy>)S=<=aT7Ui(gRHEvaf4ZkEYz%k9) zRk%f5^LAX!WKN9FDwmO~xrK&n(wcpvBR)Udz76F{C{Rp>XOQsMxQs_>3eWnU-}Kbz z#8PX?NXjP`;a9P5h{MdF+HT1Si;1IijnLt+gx~U5qd}S4-OqjjjEOnS6_eP^CE<>^ zg2iUJZmMxM^fW2cdqP3wF0xG*l(p`M=wcfa94H(A@G$GHHtNp1=Gid_Lb&7SM3L)QRw<} zJqbxhA!K+akG6jdodo^fow5IOo;QT2Cw$pHzD8dqz1n02;Mv}1`y|uHWIm*@`?X(o~(`1{P`(#gWZ$;bpwM#9+dK18U(-Er@ z?jMoKC=&`Um=;A|^r8)6JhF{9B}r9yJK-UPQ{s>IuXNy%$i<9s%1$e6dk!9aRK@p? z#wi@Ah-vKRh-i-boKPJhCcw;{SbHdJp;`d$61sDMjag~$fYG+!Jm69uF^UE&bFzE7 zqfjgKf!``uzk_YI391|+i{~S7mxq4t!duMx$YZ7tJk%e0HDCeDJM4uuTfG93IKDE`C8B0twTGDrG=4@uz!XFAyL=8(-25s@y+0B8E6hU*`BZBB2_6SM|KK@gq5FtA{1tGFAhy;845JXtvzcB(4 zwsMsKL=r8)bO|`YYK%d&wbdMj2n&+G76iLZBqDB2VZPPbZ;L=g*p}sSh#aQHmJiKg zh_vLIq7Wgfc0q`cu=T_ss!4rqL5K*M^cG`{#I|`ch%m8hWFS{=K7R4k9e6A&vmc!IS;r?QuyU;_0_}BN zstr|S6jZ3d6VH4?hc>K@axRaS5r;<#XwOrqSyny+JY@!TW)nIT1r#G{Kfqj8jfRqN z2Araqkyc{VWm0Qb9FpFr8g4Z9>;luF^>Fe9nj|z|qCY#g~Sejl1pik3LQ=&U|lks92 z_0kRV&*~R^jdM2?ITfNVN`-V-4PQ<0W*Lz>U3Swch&Su}k5EgB!MqD#m+ZMotxWe+ z?ptJ&S^A2{O%$c_%_Ji){KU_nWz*D8{vt&S4CzF)G5o0YxPyyZxGZ5^;853Y!%8Vi z(@bFEHsULv|%=JS}hC@2g5qbFECx*X7_pH!O`ej>UGX> z)qIB%dC@F%LGJd(xi196_v#29(umEW>bADVRV%l)>NW9+6=IEg5B4{as00cSg#azN zZ$!N}Ah+_bfFgolWR_)epwZlHo8^HmAay*+6Y7icY{W~}?9V@bpKw^8urK^RDlgsH zpSn!39^bXg1}MZoIJVe%T2A?y2xK?N+2en8KZ04WX;g*Q3tbxnJ742&VU$@;>m4Eh zP9_u!ol|A)&Lan*G&H}>G-F>!4TBqb+4QEX$(5y~P%4k6`A|W6fm=hATw#GJN6Nx1 zn!d^$)kfJtGI90uCBkz3j9&t=VLieJUCwN6A@Z%QJ_p)146mY%^++jgmc}Ae*2gfv zzH#G+X>2bTK4;WW%9d+_OGAnI4ScYvGaqtr-_w361^mXJAVSO< zPI&WF+Zp=-i4bsMJR&`a_4Go+5>aC3s6<$Z{V~5Zk1w>lfvFr^NUCMCb_SEVyg)xI zTCbm@k&fVz>Tu6ZI34T{PThpz>Djpf`21Arj5t`(-0r{R$2p=lll*1>$!_Xmc4LSa z1P?`DLR2)=n|(V*rkhAqwZ=0dY_|$$qzIg-MUq(U`?iQ#^u{e>7L_s=_E#(JGDX)y zZP?xD>{BoeCTc;YOLvn0S zO^RHv)_-5)@1)D(PFQX`I~DSG)qPKhOP@IUm-Mkzn>+f?6CC4sqy27gY@_M@2VdAO zl%4Q<8^-Vy@8g-)Ebq0x%Q8Asz+YJ1oCvvHl4CyC$^#!VDVYZEbjYn;z~i!9cq=XN z`imTgC@FoxWh^P@o7Y5%Tg2=<{>q zmeiNo-&t1l^auK~n`M{<04y6MZ)H{@3epx+mU41Fqorak3Ijd@? zAc4xwx2LV)T2*1C_j_1knkndsb2xu@S1pRGl!|y`F#2{z)2t0Z)*<1fTu{TCJ0Q&SeT%@2&+x{m&(Yw+sql=kf)Dz7()EV8Z6CE- zfz~gCed=Lp46j76ErY3&1*47Xa1cygG=^51Z7zM8^<`akq-JN&BvWFX1*wI8d8vPX z??D?Oc&((PR*K@jEj!zMj9T5@ZY7(VPxVrLs#7g%l#JtX{lyE%6^!~@7ck$)y0G39 z2t7tTsL0g_^Kl$)@HJd*hotMJ-B;kYe5>FV?j8^H%A2&H^FVoGr`<3w5R2`Z`Vx+)&!|UB zfGF2PhI1i6a=A#ETuYLm;CC>1apa&XHbn0nHzcd4DMugDn@`R?HUY=2)tgG0tWy9R z8b@j_qg+pguo$0!n+jWw5{yu{WcfRe)~bmdVd}+ep=*D*POE{O=Gh(rUQvBWR!C09 z454{97E}*?!Ta1;aab&&E8oO_`hIaE1);xNG0El_wUG};9Ek}6Cqu zBqQ91EmC`V@8Vf{@f}iGB%_47IqwCt!y7r~Nv9WaNClq5`A_x}4Ilo!nt_{5Q!m`K zo(dqPK22eNB88mS?!C5+BO^;xkd$&~l@5Hp8>@Qk?qI-H0Uxsqd20g9kUD$#TyN7} zEfh*ZEpyg$VD~C}E9emqefK zlGBFszV%P^bP%{%$zdw@LP$6dVL~Uh*DjC5bU~!gCg~K03{D-KZ7y2813nQrzcIf^ z3$of+ZyObuujUKyNtsq5FICbJ;ialX&1&bO^a7~h@hdI(elpA&xmhmnql_=!jq=HB z)A{u~fPf(eMy~7XO5M|Z#*b=rUL;lXBMscp=HfEucA|cLD*myG78Z+f?R9rjT&-8T zr8-q^t6r4@Hm7#zKL52cu`aRme|IOja={JMZ(dUOP{6Yt;vdOzOT^tPX}X(iMNv}u}5zAIyTQ^&I& z{C_mU={;l%16*wm!%Vou5lm~wE@rEw z+mTs`u>5CFJCw@bPsp2!f6dm5Z66BV+#znWzX^!f*v8Z0a8y525xlwIDz*?=Q5s?o?+X;4WB;0j%aIVQ2)nR6?$y+ZPW*}rr5{NM2fu+t!c zos~u4JGMbXRstQ>Q{($I3hdw-Otv5OeYGjduXS;ov`o{-d9Li$R_BXu?h5nf+mwWbo4_C8^q(Zsni=6!=IoZLa7|ry?4hn*iT( z0r7rsF(G(*9Dn%(?-dWuKRRwg@V1L!Fc>t!z2m~24UVyb>jYM;a2z!}jo>BosF}s@ zn!@x*@)QrI5K*7!l}j+Z=VCZcN7?MUBZ~VwE{dakav<;9E~?8c?WKWr zr5Z~VWcowrDg!mWwSfOriO+uMs!Lf9y|^>S7m@kq`p5)rUJt1OGT&&Do0>|WP_UeV zM4321R;Z>2jWvm(URRs$hk}inA<2v*Pn-|%bm>kebR^SOP760> z@cCfx%zzZhq(r}99KILB^V*T=)4^xXDDh#AYXx^`nHCsLGP{VpERY7qSJAk??oc&> zZ*)tPyQ#(h>oMtoY2d873~o>k3Ap4Olh1_Om4u~ zmEd<9@v|o6bVjh@5UU5G`qj*sym*7}1DT<6*fp%~M-YfiY z6Oj#!N+<%8+h6-!WUrWIWh=-k^hYb$!y>x^?Qa3Omlr>z(^jB<78e<~z!85^RQ>(* z5-%)4BA11nyh!QQw}bWw&D1iq8;|mdDK4c4UOAMB=Ht*4hUkZh>be3W%A!O9lyYD=33D5+T0i&mU>5YkuT?2-5C%2`1!SkQw)$p z8e4}z5q<(y>_kZ1C&j^&<2K|#lJ9iLf4Recj(S}C!h@bZ!ie909l5-Rg=*3Sd$dbM z0TLhKNsTso8rN+fc>SUneznInni1Th=KC2-opH}})Bc|(L?Ef7AmJGQj${e6wUy_} z5|!&UR-htl`eT7)i?rIV-`dLYb8AZuX;jb+PO~2Di~BrE@8s!*QU^aK868=Y8c_8C z7s?hNJK@3tLKRSWjjrCfJ1ZrwTSy?cuxC=l1M#FA_8P{8ZQ~moz0Mn6p3kwKvdWF& z@KU4BD!isi!C%*NSms>W4#s#?7$!ym_1i*I37qQmdOdx#9Bnharfh!w0N#-*$j<_z zDFCwh^-la2o+op&H5e=mzFX9D(@X04NedCPL57a*){AQdhe`j8RhgG0BZ;7V+xb2 ztF(v{Di@bTG?ws;Sl>m|k9PF2u5Ip*=&tk@jkax252(G{5ihWp^G3vkc2QGq$i?b< z+rn;J*WVb%yQno+=yP_xjUhL->u(F8UDOm-_s?BhM1^`(maFJjC%t|PvrEoe&;~{(t>?f>!~;RvcWTn0T5XUK8n3+0 zVVgM-k&1NTM_W}5QYcWAZIpC#5s@I-E+hi@Itc`Hi^(JV*M-2gcDOZ(gm5vD?Dbb^2MPE-Q?T4@A)mcnQ__dmS&Q3SuBCg0!}xO<-$;%M4}CC+9x`>v*g z3803(0cKJkzrc1TTYY{0OH-+C&o2fJVNr^g&-kTHk)o68*kAoYfdFT3KOuH+kL^?B zd_1oTy_{fZ4HZ}`5=(Qc$5VU4_QCM<@zLPXK|THBgQMXdIOsh%)up0K5w-c0_yOef znySN>4dDtSbo|_T;QXsrKT-^+)^PU{k_sD(zcEQyt`CjZ@sd(n;nT^bXc@MFwEw8%%0SdF+@s~8r<8L}vf(pM$UT1Aa`V^$ZbErMN2 z*bRyZ(9*-%(twtTr^}8d#&5jicW6|l>^P$?;-6_KVdm(M6icYoxYpW_t*FSs!@ z&!0U-Ey5TVIs4RS&=>)N{Z!KRV1x?XgD7B+&a2Y3!oqCMJe03q{+wT_ZIaLg3Me7x zMhUsunui2TsRj83Y~NF`BC$k5s&y@oX;#J@tC1+^F(G}WCunpjcRwqiqYDpoCj@~y zxkOIe+g{vmi?2`FRifLgZA=6!Kn`#bkV?8fsVwHxrxTx3Em*_;BpW!>krkx73nQ5B z9Sxyf;TWcS;|yC)AfZ^M^QUnu#V(~XTm`>Zfl;r-m_&%SgyW}CdRcdub6iEUJQ!uu zymC1AEU6+wmfyjMS*BAdh>_F2`G#xw<{PkD@Wr0bzWIims!^;i96BDIoU7w9z(MrT z^7Uq(KS?jrvP_C>e8aX-pX6A+tsaK;7OgWc#A$#igH;e~8FTge11dnW7!e}n3gQcrsGMD(_M}3R2C5~M=@zPXQN&tO zAE4P4u_xPplyJTO%!su%(x^39QDc1BMbTxe*s_0XWLav(%CTjs^glDYERnYU__Bm{ zeGz6EU2}|C%J-wpQu1mMV5MVtb*L-zHPv*BShG~?;%KuB(<0uiL_%XDL+cw~mKy6! zlEcx?WTpYx1Sav#P9A-DGC18o8}1)eDK9;OM&kRwK7Je>93tp_`Zyha^f7)tpsx=H zd!OJXYWg@lJt3TL!pE^E@fP)6Zib+9)AHdNrn-#{6P#Hb?!k-64Eohfp*Jr*c^F$a){F z=5R2DOUmW5xVQl`ql>O(=55^$lU8N5^G0<++czF0W=Dxl|KlElkvF z5eFicwmbyFKgyFa_=12ZCHc+S;B6)r6B*WnXV&ys>r8kCsO3}O&Gx&;=bt^;`uOC0 zo3RQ5?nCtt=gq}Ca4RD}9UMQdVSi(N*nQF;tPgpB^fv?8WrP!ap34qVORBd;WITck z2Y^%B6_>Lm;}+Fz88`LeppGk`%FM48h)iF8lR6Jz=@B)_$+{t ztT7t&Y&=2^7DF59DR6$%t&y%RdCQeQ!(7qxWDvYv+XZ+Gwm;} zV8Pm2_Em7VHtee)Z)?|A!F>y}F8I_s^lP2DU-hzEne#5j{Fep9;Ax9j2w5cny?Oy3 zJg#Q8XJFzkp3OZd(tX;c!$x*VLj|)$ny|uX;@++EB%`r`xm1_vz_5C0DvucS#Kh)F zdTfSt{gC`1^R%!oQ69)jLpRC^gfjQg&FmR$MB~z}0LUVrO)+wMTvmy)C*Nbl-vo;F zt6S*NudpSMU8e=Q$34#|fYa;fqx&ssR%5kuOG~>s%)!MKZd8LfWqX1JN~GR;gp_b> zWt~x|x#kg=H*j@i&Ol*W4e>|63JWegQIn2A}yttOJ zTZ2@%=2bkxEMh$ZdlN5Vcqdh9-~v`4lo^$g7b1=+Z)^=4b+xXIfNF4?K~x{bSW3PI zs)2e!2psg0Q%Zlvaj2Niw zyRPdclg+%X3~(>f0s8G7cwQ}^8_w|3N;jl7qa!jGt>PTFQyJ3>rFRKk9kr@fx>rXR zE!H-pQ(9Ez9O9_JS;mT$=)zzpfOa}9+jhBD3pP5*YPV8zBGX*Y%Ic6W(YJPN7H^*G zBFhs6Ho%+By;QMXe#XvDV(rTBVg0o(4XLGFdsXi8&}+$dNe4ePjcIA1YL`t6oi{S;R&DFv96VMc#ki+|GNR^NNJonR&x9@)pnUu>Pn)!J9 zt@|HJCVyZaKhAIW43r5(s)%^K>%tilITMCElPQ9Z0)(3gpXTPKg zd^04e4#xSwm9I|V84ic^a|1)be+eyMv&k$6m4m1`S=p{zsUiT<55eJ#=%%dCwHls# z@nT;uw)pcCJ~1EX)41LAibdxU(67fkV4>7z08%R2^$^q`AIlyRswd0}7YqbpzWJ1g z=EJ>c{*$|lHUMwY+1$Q2AMlq9%~OPah!M-)NUJl8z~ZEuLHSXh{V9zi@hFheHcTvL zLunGzQv?%kI)P(D=wt;Vktf0;-3$cR4zIubZV4@JW=bH^K=Zn8Nqyp;#!RD)LAU170$waSKfV6d%)=_;g0Qg@_a`H@p?U(Q4EX*Zd}&8~y3)K3^B~ zLa@=ZJ49*?a1|<-z;Q(?J1pMdf<^K9u@)nfr=JhA@&M1i#nuD7JPr2H@f7nRhrH7e zn_Pz5>|my3R0hu1gLBRhVcLdCU~KqnNxNWLP!*K>(dA$49cyzVHS9CY@E?}m45THu z^vb(KVM1G=yn53H7#=q_OS5eQX>#l)Kw*yGjvlMXT6?qJwB-#m(0C`{Z*xRPq!!gZW1JIvVx$ zhkb9p&OE;|FC9PzZo{DNZq#U@eL@U*=N>Af|2NfRsxkE-7FQt@lsc-Al*!8?Vf9hl zbN_x7ny7W2nkV%K>>+&k$I%ISBv3%@+kk?Aq!ilFa!$G%f*e`#RZGw;No}bWfU;7P zMZ$!9N{DiF08o2-Au{sWw86CEGkkez!r5Xl{(PADkt;o6Ck*BF2w#8kdAD_16s*rj z!cv1K_Cmq*BNzwyK^8>6P@?0{$PZ&+0k@1*v<8frX?1# zhY@zHgSJS|G-`u(nEH_PU}tcM;5ZI^BJs*c{{oa`d2!hA_~3Y#mz?7J(dw3czVzsT zom6o3oL8QY4#ehg&&CBLg5-lzRuip7&Ppwdqz^ZNsWVQ*PlLK5cT#ctW4PQe- z#pNSwAWOxf&%dgO){##Y-wI2_nzwaiVjW~l?*=kK^wkdn&CcT^rCf=hci1Kr1T=5s z8gConurXac^`O8s)kjR37Rn+;OFT!@&Y=k6Z4+Z*kRvfNJ3T=cMxp38!&d{n^3K# z5RBC7cBIO+VUR9Cy-kj?Izllxdc2QYr1$m*Cm$R?4Lp>st>aw}nb$MmRE~N`oFYVV z%sa7DN&X_BPZ{str-!GfgAd^B!f0Ib?VNP_;JTdWS3op25Qi2m2=#fD%F#E{StpUkpRK02C`tlfeBj8>;Lrz!uP(tp)Kz~Qt@ zPKx^-OZoekIo?$&`4S4j0Aym%W)rCBYin5CZh!RRVp1pN+oAq%+Ak69ZP@Qq^AT`H43`-VR1F2hlEjMDX*H z!k=JUFD^ZyAPpE{?V~5KE6EJlqySD*tj4VHXvy+3WP*OM;2bz1Gg|(JAx+PlYW{}c zXab^$B_E|LSd=5-Qiq%wm-$ROqetx+&nNljDwgR@12E=7ZY;l*WngXobI#|Zgh#2( zH)OmaqP=C$j<0Ij(ZQ055eyX!@D{aJF>#R-o9HFW0j1Y5T>a6ncU8YG*M40xT5#a* zawfmsfTrwGh*7m;&P3eV^m@I&N6Q;ExZd|IY9Sue0GmP0{0x4n^hhjpB@b%0c-HyCx>g@dl0+_fmz^V=386* zD~Y}cp>~^rp_WaqX5~C|p}}_xD!#@8+Am5Hk{m*q-XaeCYrKth{!>UO1rV@40=qbN-&$pkx)1Et79PX8#u+t7q2!!ejN7xH_GpXQtav z(HqT5YmGi@I!AA@Z|NjG6N@`b&&|8_v-I5RTRTh7TsEJj=kE>Y-1%eIbM7p}o1Ap# zdRLuv7nt~y?#vYzWee!D0G0Lea^RLMVe&K77b_B6Z{hfl*d&PUHZ%V_BT*AKDt zmyLe#v^al^JTA%Yyo-~P%tm8~fGch|DajwZot5OlB=tILS1vFIv7~Z);D<}YC$9LL z!E_I7IK_JojE0vDaSb3lrMw7MTv^oWfhh$JpR*Oc2%e0xj1#UeBR$f@;2n-cxvGov zizSh4#)!&Ko(wT*FP1a+&oQ|rU4rp908HDohZJA>M>x_?j@-jn%lZ6-BiNMrcSL?>UfYJhb%gMOp}H#?F7 zvD%XqAbW*X?P4eChIKs4`-$-_{f0_*w!YG{AYV{iJ}|>l|3Bh}C!?}p*&Dslt*vH% z6cNb^BiOn=sS$NiFY4r~R*h=+Rc+U%o>&^aRf~U1?rKFX&R?xKymg1QqI+u|tI%t7 zSu240{ZE3})m^Q1X`mu?G;XoF8@R_(^D6&XfZ8482kFixawZt4b1MuV_q%UiDV{;s z@9~UOj$v-R7DM&v@i7Q8pf8sh?0c#4G$bkBQ4~3BP)|oFK~~+$y3y^aIMq>-J70dg z7?+nosvJYjQ0w(b%Nd#cdWjM-?P3ATlw-b37*q)M4^e!sv^|*lXHOc35-GO1= zV8oH<3nB$$=%CyYClrpHRt zf?oSsN0hgi5Ar@fcaZ(%ync&W&%?hh+fn<6pBqNg`&OuNyEK5gsM+E%Sl5Jw=X|U6)loRSfuf?b;cP&kf(4%B_c?uTcWdjb9CvVEPU!T$S4 zMK&t%&cFOSx?(Qf+ESf={+su*=@e=|x`M6wN4+%t7OzbKVj20}-=6LtpB#Pm!M6wF z>_Pg`!w+~35r{P26B|bi-K+2dHts!~ktpWG$ZBY6NJ+L7S-FnP%1EIB2EUdQD?sJD zVmJXSF}nhZ;bQL^mwaanIvB<&kECmSOS^coVtp?;zZ^3n!LnR)N-8%_8UzpOo?Xfg z3qQ{Aw7=ZCr%WL=(h|pWnPf}z6Sq6(*R9#^Ulb5Fk((9(|LPhWJk1yL0y9&x;v#2A zsM-!2sC`vUJ6x*E@+?HM?6XXBCPvjZM1^I3qR7L1FM5glTU*xG`1 zudBYm6^xAqDVTD)wUw?nv|rVsE$mf{;c0}t&QMmWv#_;g^xHIKYm2nA8H{BwY!94D zxO(ix=20YyG#W$TkOh?ia})3c;8!zS{3uS6BwTBW4>w%)(Nk=$NHz8XK(SRy&!8X> zgf0PwVDo}oA^{8@5~wLts=l#SrT(*IF(Wf&xb}ZdoxE8l7wo%Sj6^XXqyLDGVN-3Xi-nr3GKkBJt(391Tpy@=M7m zaPSJloteFM};{gZa!XRsWco`i(1mYnN;sa<ZQ+SNrG_e09=}uTq%W+A3iPWXBJBoqs&LsrRFR_(fy4xN$rrX)6fTsOS?DFaY5w7Bq@UdnjNB@uP?v9^)(M zbQ)jljRU|`?<}D1Rk^A;aV{f@t5MhbnkRr-&xB_MX}#y?k7!AYH{V83a zrNH7T)_1@N$N}ySk!CXY%nt~ImlZ;~OnNd=EsHkOq+4|lSq=>qN~7F5X)3gJy7oYB z^dnL7O`=uR9Q4?b^7lcB@R|id6NKH6KzcAyM!;6B=_Rka(CUU~-_3ra4lT8r*ta;b zjz`kN^BGOcr!y%q?k1TXM&<2;?FvC^k*PZl@zrgsw!9Bavlz$Rbn&Ru1`)(CK#?(z z#=00RqsA);SnJCZ5zpN1>Rh@CHW<=J~kmx@*sFCog!rL8(Zb$a$mI$-Cv4J0xd)wO& z0K1>;YzGIZm3=RiG`hh6$vq+jBWcChW$TOcxG$L5IWfVG6p3lcA$7RLd7z+xxMTsA zkYTO2hevQ(UDT!@n9`;^g%zYvOh<+n48J}yrRfJU!thc3SvGOwZ*+tRT$w9MMxseF z)dV-qUl^<3i*;vZmFpvW173Hqrbl@$z*YWy=b(LW!paBS@?C@tSr&w zVJH-&oP3}-r+rX5q0WmY7CCE$s1(Z!zR2lzqKvM!RVdc|B_jYWT;2N`vY{T+GqxN_ zML?B5gF-ui7b@siBkr0K{k)Ddi>=^7NM}<@7}PAAjISJ!FpgPuWr## zhtI}{5m-E#;hIHWmoE4y8F#fqWFSmpUKSY#TDh1AeY%V*>Rkeo_J`24C&wdn0j;I zo69+HwwOOz%r$nB+Km7y2l_l)c~_Hhf(m~Wo8^cHoVTaH(fvO>N6w0VHR-$v$N~q{}(XBJzY8wgAVI20<1TAE$Y4laginQJ2McUf;(|Pkn04I_}vduW$Xy z%C9eY%vZ^m+-dMs)=HdCMZq4au@>U{5+kWTHJZ<`H+j-XYw+1(G{yuru>_^Pphnxv z4Nf08O?E-c=E}sDSe257nI={K_6&)QRHDW%z<3DA%*w)=D!C2d!|@!m(zOBgnj~15 zRlP$)=esT0;J5-oJU>=yu&v)q3g&ttziggdL-S zJ2eed6}A`rl-;#&B{gqy@{|yWnNX;8W}{SeI2%kRZ4$QCq^%^Ysk&Wj=%GeBWHKu? z%ETR`d!#!5W~CF3lJ;qjNeUi!A52^5?(Tm$`0Rtj7I5$j78@&2K=#1!EMd65{*|jlBITmdp(nD_Pw>kr?a_Ss6~4uWIaW6ysw7AeLVEb za|Br2C-|UJ&i4|-gvu}3oM&gb%KR}e7)Otr5b0>cnMLJ#Kg^4qodKt@%GS>IgE#JP z@7&*ceK$Fpg21zhqzO$&Ci&CKF`LFAd;{|j1jvdY`_DPDoehLl1-8ZLRTMe({E;u zA6b%gOZLwmS9nW9lI#Hj(@zq(=nrY1;mS&rc2+klg-7q0M(skCc3TL==P_Q=UD-Ig`BfH@R{)lU^t_WYXgb!8>50`b692 zTr6j@bn|)63nJo$Rwp*~-q!6uO)N_;ZgFW`aj?#IWA+313XU&~4Bk+dp><5)-ip8h z-*BIwHDKEI<(1Y+mGxV_Sd7CaDmxg<_`oh-^DIr*%zLd&p9do3bopU#2_x_2*auz* z*eBdc{HB_0O>rEOQ=LOw^ZII>@Kd$c$rxcla$lC3sq9Lb$A1_d-9L>CTWtI$xQyv| z8sdux(r(@5ah|=g4ttNUFVxSz$Gr-Kf?lftfNlM*jiwDtWw&{|Q$fPJoN|*ZlRaSn zQG-Vs;-DZB9!mXXe6_feVjC$gagyS+0*RzZp4VmW2CX(z<;#tJ$t4gzqcA_dq7qP6 zJH&6a!fkk*hAbD_(=vv`I^;)X{JXkdpX0uf4p&~92hoHW# zhig)yBzY{*Hs~tM2N!(wyaX(+`@B~p$Pr^BG}MM`0w-2OFj!g6VU_Q+hkeCh*r8yt zmPYG>$$C)U`uwLoK8qjN@lwFu5A^74gBSFT;617kTXly60p+de&tK6&?PBK^h9rOc zBF96hk%N`} zO@U)HiXqn?#?5fW5qDgLv9Qekg$cI z%9?jc#inMA09}2jLuAQR&tWh)K*Yw<(p_P(W2jVm2HK;rM+bfVp)a+##Q3$AB#AZa zNz!iC2~olnk@lT7oiwYUaU^D%gys%IIX9v^O4izI6~6UO#ySW1=AM)M`z9Z&hoN6e zUgfrW7XEoXsFG0{AnS2;+VjlI&x4)R*<^t~nFE`Y6Q}hQBGwc!jAJo(Se6Wxeb#pt zG_M5=KRoVW{EYdf=rpN`Gb5HIUV+Jclj{pWoE0|p>~6-)GHb7=tqYDA{J@&5fUE2n zelNcfEPj=u1tt~3rjjV6ZZlPTQlc#Nq<~ej%(DWzdVEgfM$-vdsdX%njbm2^wVII& zs#=GY-q9nK{;%cJc-`fm45;s|>uuEByGHf`r%4TlGWvT&#{5LKd;Bx^o9Fd0q(4~Zz!%V%D^oRtAYaD zR1q%Oug^5s-)j~2BR9ES+y8WZfN3kN4*LDpwc(a!)jm`0gO!nF&w*L7?D_xPjGmk6q$A<>Sbed2BF;jmPH##n36~pXXaL!3zzr*j z2}Z%eC$G%127 zwg>pQhCH4bj_V4v4eJUZGZ6sQ4+eo@^771rQ;`qoyf-U&shkq_V5R3QWi9u~<0Ifc z>r^a^yYJfLdqmo-{!&PN!)xDi)7P5JBSzR0tcy&t{6DkOfBh(==snk`OZ*+H3>D)TcPWYE2slEXS`H zSinIdXvIY0uy0{+lh>__T4FKnUnrH8t@%dDvj}4Fs>ZFt1=G>mY^c5}juVqCgST5D zD}&YrcB#r;kiN_cx@AaEsM)Ic)7iY4gc(0H23l+N4S@b8Y)z|W4q{2cBi3Pd;>3{b zwS&BrytW-ramh&pX@z}>*I$9BYBwso@glv7rbO`9#*3A6F;Yi?!}RM)IH~DgK@)@Ya^AL2vZ9cjwbv#|*@>{MC ze*a_ur`Gn54h}I0C(jO71B%yLa@#E3EK4=nMsjt1szEm4_m4k6877|&PKHN=PYw@= z_QBEVr$?u!@T=Oy951g0d_MVh`W5a&^3BDXSGyZ$kIblfWxLg+#K^vVCJ;!S2h1qC znc_Izu4C8P-RP_iRgYu5Ksz1R*(*D3T(0al*OosJxvSKTEa;EmD(_-ubB#=7Yp4r+ zy|bAchjOu*T6)2ZN!QZ%7={(hFuR~Vc)4K$h=A0mbIi>yWcf@LPVsJ`nK=R=o9}dJ zc0Lo#YXqXkNY40qk>~z9wnCmR+YIJ?YjTDleg`-I=vBPXGja=WWTeSk2&)KfChP3n z5pbFu!;bYiEfsbAAA z!LCSpCBE@7tF8pq%gX46)Xg-+#SnVT?Y)Q&YfP~z8+*}QrQA!F5tYyx3HNHDo-1FG zSRa|TiP^zuf};BIgk^-|5EtbNsXmiKtD)wndU!^GrTz5qqg0947TFkH+=AX`Tq7WD}RWyaR`aLR+#s2)`I1MP>MB&O%|bXoHv9L zt7?%e9=F1XE3w-cw-thrXAJp;;K2dz1N7U+{6BN3Cy9xXD3++oHzGQOcU`YmSd1h^ z!t#JcbwH!h38^YUJhMnMfmBygo90VfB@7<6IV%KPw?NU&VHvarC>t=X$?NI`V+sO( zAyuX_=jM1ul3$92P#Q3y+8K2Y{O}a7uJ9V>b~rfw_;J1J-HUSZT`zetd9k0o`1r-! zu(=g=vAz7f0~En^@ZNhJ$kX3Oc~#FX8s->na2k@6Z#z(XU9|e zQmekl@Z5&+IsZzJrepj_I;g4~+=;aaX4{b-iP!_x`0Vq?bkvBnLouo&Jug<-+Amt{ z6MA5QU``iTS6OjwXg%IP{`AwqX9o>51*IS=&Xr`m-h^I6?)oB~lxw5@bgSghaKA>s zkoD6xzN2l%qaf8r#&mv4inXy5-O-pVD!3KvJd}6dFmY_ zB1D_|@mp2+)GpRQOmqlPkA7%5`ZB?C-ACEju%B386~}&PK+^d|FwJ+}aq$W3o{INP zH&Ofsv=KFXH^J|gxPBdC!myF3`JyuQzgVf-k5;b!rzXUpY%PQ@2>V&Z;C~e%_+PAy z8$nvg`*OBc0d6!#5aSA}Mtg7yz}=t#=BsvvC1E!WRoyKVmDNC$m<6-cq^~TjmTC)) zXcg3MO@gHx9v>XTcG|4?p}O&YpC6Ed`jht-7XnrrMoDES~Ouyc=Qs%jMDVA+x4gGLK_DAMm~L&L*H+% z7Ad@8@u@T6*9Ob*VjdVQ#$0)j0-|`5+OK*ERJE@!l_MG82ar=Az~oar1i+bZXHaPV-J;C++JGTA9v@D|{^g{*cE(`tbe<@blsX7|u)hi{AJ6IlE3Z?Sn-S z-$ZGe#KpN-vrAXClj|}qb%gDz$i|0NHK!uM4aIO$EWZ?i=fc>#Uelzv#qVc)p)inR zU>CDF(&)=UuX$?VxG5BR%*IparZjpGXpT>}>emnPobOEka6X+7466h5uT2T5kww}A_ z2|tA`Z|r3>OjEC4glH!|m5Q>cq%>%0OR#m@0eH5w{o+)01IUHZ* zr}U!8UZ}JYkDZNL0Ya;BAbX(wd<@6X_*psVF}c6iUe$F4MVt#pm(; z!cM(zB0|7*k>d0*rag=M!4|z^6W^NUQKwhe-@CHCLcQ5gh}oIXGOdj2JAX6^CUsI}nbB#MqpEg4u zc*p1HoOkBx7mCnyC?D)8=|*zP!Gc8X0=GjQOC-N0KC*FlN~h;g#o7B`v;H%hW#=J( z2hNcWkRnZC0XjOye+?k5~8S3>n(4>tGOwZsx*a+*3)fW8` zKDfqe_E|Jue|H`Cwz?-=2Bk3fVxmZk7UA)MhEb#ch@XV2908fR5iRb@L1& zH#Kj%3yK)dR;21zs(g>{l?gg|q~jEX4iS;ROR@{9F2um!BTDZMVc!A9jswRKBFe+KvY@Ns>6;IxG%tz>__ zaTOKaO$vXALRKb%pJUkIUE5IMtmrU6Qr+H_aoM^fx85#Zr45((R^lgkZziMkxrln4 zOM;P~yMGc74HMJezRKp8=~+IW^pc+63?Sv#zL{(_zJbSe z+h*tqF_&1fXL;!dYTt_$Y^|stl$l?aJQY^e2zJ|`HBDgTmholFZ$K(?w0U|^-@A_5 zc-K8~JNMhB;X zdMnX&8BG5Me}_6}U2iAk?hwJCv(v@(Au4~RC-w6i*DPxM*HvTtc2YZ zsU+-+*yjhrY2&Nkn)PNL!lJpQTdLf0LR@!V#s7D<8K?Z{v8-pvcHC&_Z>XVpe&qmf zvsd~=+BAXdYl@7i>DaGs)qcLR0Lfo%0{&p{NSj-yfuCW9|6sxZNpVei+~o|fg;$Q2 zY0C}MaTBLCq;atmpfKgPvxhgUjpW2>%UyWzVja7y)oQg`tt7(vCo2HLaf9K%_IUq? zk91F+dFN3co@av}^yE$o|IsU@{IrGl7AtT5Wy8b(!tE2PM#Sq|sPfDEq# zoTAZN7OGg~4caJFCD9ygxBob8GrR^^eh{seg7;lM&jt_nCiu~9(5YAgg>bG0Z>cUlkCjyDF>kD z7;mIE|01pV57C)xg!YndyHTvKcl{dN_dlFrG}^+IfyKxy} zjBwabp>v7{O?bu&@c`DN6GV#g(Pj z2xraIlF(+Z*&NHm7vN7Xj<%M)dzO!UgukOoa7UvzgJ3b%xL$1+zWk++-%xQLTNt;oFWOlbmt0G< z-Dx&RXB8R={Jb$R%XtyF^?NaYx&1Mp3?CxZ*}WZdZ-=aFhb)K?_jaDCU1EArrvmdJ zx_KkGE%;W&0%|SV1g+ME|LRx2d!0ByU%ZTF6pWS{+5_UL#&!)^sVdDU`ooo4fl_PH zRtWEU-PTU8;-jVi>?K?MTG73#yjPX$s!DpEnFYcYken?qLmo`j1s)dFCuo}&ESPIH zRShQn{hYwDtBL^OUJ`H2kst_FoU|=z3YT-|im~e75@E zqvif1*5kb@yjO)cRE4$Z1ht`Lwp569Y0`=E(X18)wo!~0seOXc1(~>6T3gA|-(EdV zmr6ligi=+<`j0i2so&0PrD)}?R;vFLJ^WUqqZHDlV$WhiP@jfhLhKqqffuojGR0g~N(_?QBW+`- zoe(4(q_B)~5n{X!C3m5``bW{1taZ$|l0>yMd?e-pZ_tVUTen#61`mI;D1_e0*4)KM z8F5Rhb_w!N-MOslDW!Ep1pybhTwgzU;85&M=iA993A&qIlKX~$}}D-8Tv%0?`(ly?F_zrtM7d}57$`uu1~bz^hI{*BGd zrq$~NY4=(-wA*xrV-p$}V2c!`P4U^VZfmZ+7m`9Iy(+EfIXiIq+ZQ-@#Rngjj1VSo z$BMBn>IF?%fpM{=&H-s}_LcO=b1zC1>(kYfWJ zIstoc(%qYM_a@yfjF@|qPBYA$es9>dH-+vEyL-cKiDCD2CI9|=OuXiD?)=qM#_IQ# z5)LWJ z<-iap$3~uRZt}J=KGyRFC)Ej^t*veP)1h>Yv!6UFW@Y6aN9a7g^&lB_V!m&5G%kv= zD_Ea@@s+KuUav+l`6QpLS^t$Skbo{@-gpH-2`ntJ9xMPFiu0`R0ReC}h{>-vLy@GPH8DWT zB>Q;@B@!idhSK^0;E6s5D?PhPqL1-S2pdbKa89!`uwsACr{^O3tOTkKH_cog7riAS zTwkW);DNE>SJ+0<=@teH918oxLJKRO%+p?8dOak@ql)I3yr92|N{0nzYb__8fhJDg zTTVf%%}2w$gls^Q{9NYK3_kf%kZ->ExHzl6{q8&X@Zh1w;2E!|l3_Z{@KDNEn@?Y$ zc76KM=F^{SK79m?pn}Pu`iWO2N*e%qAG#>!udGBG@**jKOI@M~)YZ?=bSji7 zNoRxRy5hb*nfEnS69sHLq+jbjdGrN-nsLg5_hME}VZ=sfIXvQ(bj-A)aMg6`z=~SJ zn2rWzemOpkYec!hL5Z^UuNb@u zexaLDPBhEsu4aduuKAWeh3uOHVH#Fa(2TzkyK*`FnrJY!D9I@wenK>m=v7b}Y=T!% z8DJBnV2kBMz}}Vq0L3w128aPHvXmr}{cMbX**eyQTv_MYNQ2d-g4UtzFdkR)MXT{} zq8d)^>jXq+ojP?#uY(5(T$eWBL(4iWF6*_2>#WxvuCrcyxK7*lEXYpPMs_ICwH`PJ z^j_nbKzFaM4VJmWV4|+bg6XY0`UY5&s`(F<9PRYgB!*(JioTVYd3 z%Th7{2W48(Gq?pl@H-yoL;Ohd6716nzVon$d!1DgU6%aLx#mTqvbX?y1noQYp8yzo zu9Ps;RvfLFfD}ac*(Il?v*|ftdw-mtolkMKR~B%PPycOfLk$z{M#k9WZG{D|F$E=KZvxA;pXlV|i(6ZmTDRh3 zu*;<6wGjKT%s|MgtH4v218>=1^z(T>IFEoS=g^m9S=18y;X};dVRl;Pl`^qjCPheD zM5I}xS{ml95T|%fEz$<<;d+x20To{I%Bu*u5m<2RvAWZmI9Ss~edu?nUIC#{UzX?T z1PfgPFS8-L%>b%`wuDY*7pGYn8Gd5F=JEl}t;l30k|xTXD1~A2Q$EG#+J4CB4ctKn zgPkZIrL!@<=8HWe^<$_ztMD~`P&6+`7x`oc9b#7#!nj2jV+8+U?-Krs8OT*dbo(In zMM*ozH4JtD{uVt;-_h}DKF+KB*;o=jnoUzoh^}L}adxYA4i=>gwXU=P)B`E?wIT!e zD=KQP2yS85(P&Dmn8db&dXJIyPSOP!a4c=_O+aAsX_pdXN<$#LqIgW)lfN~>yry#S zCm+$m&%qw`(3AfxNvSL$9wU&6GcvV2{yNBBcllSs+dd?h4P7WsYt~{l&!?JF%C~fYzqrrKGDzU`%2*BC- z>|=lhoD|M7w8heK^c#wiWvZ~W+3PglU`sikgj6>BeDaIgIGq+HADSKD&EXX)^JIknxkc^;D_rVvjv|R*F}YC0Qo1atq2SWt_C#^zFRbMGnGH5xIjlot*)5QX)7R zW~A$2{$Ns_$~`QuFQ#`~d^^IiPHgDlp{5prCkpTW#vP;~-}t}lzg{@zxP`~?`=+8{ zJY}EF{f5g$1Iu+?2(ba@2}W~KD;I8-^g1h5BTbG|gnQnpRxhg24m#L5fND8t7wC>N zoHAcXUQS*Fn2bsRqU@OjU-&|F$iV|s^UOJU@EBowDp2RG=mmwWpzVP1A+z6rD{XEP zf7yZ}CHfGJQqOl~7#`J?4d7Z(7_b>P^RW_f$xd)sQ$3zs7`WnV1)B_n5EQ2pmxokFVFy1;5RYORfw;P=p86t7FN=w%A1-je zWSsqV`oN_o`h>BwGFvis>vCON!H!p(=L}GrLBmEc&yIHvzSwW>s;IX%%;Wyy@n@ed1Ig>=79bz(9WR4}lbp#1kz60i z!^8g3Bfp##qlgI7QnQWV-B6u z3|gE;VrqAKoXr;N$yw)bQ@bBiWYHm^T^s8v0t2rsCapci9|n; z^A!D;;sE`bEMW>Ul87gj>py92ftDof`@yrxlL^zj=a49d*zISt`!T z5}LG?#+ZDpB%wNQp40SOl$qHKTuFCEPG<uH=kZUkvy7*6!&G8js`E?R5WZp(U= zuY2>LxaVG4ODi+-HxHPKtNKOMcVWBn_-wj%H$t7e=tc!&x=}O3y{HA+^r95Ay{MUY z(2D@&?s^e`wd_S_t_0&4sc!T=PYcXmv{2(&-; zs!dr3N*llPn$R$1Da5(McP-c))#y5b*M;y&0inqNVw93c7puxh*YU!VS-6si3ZPYU znmoNE`D@I*3oH4kC~rM8r}cOv@?~_%gba=Rv!?2WK}l)t=H+&_di0i2-YuqrwPtjd z5UW~3fI_t0VlwmFE`@K`_O+(LVB38_n$o82Iu^{{(9x_Mr2~qpJBH{Tj?jrBn1K8H zJDr$+A3f@S{PDpTCtviBcMm1ed>kJ1=cNxn+yA2f6-GN=&nF#VGRKPmi-xF5H1cFV zc6FGz0XUMoVyO7Tz}vd-+PSAAXy*kw>IgABdPpZHcroP6h0C72@&t))Vu1S50#N(; zT=W8zFNdjQSUJc9Tkr<43~6x z#0g>c1bd$=1Ul|2+Vn9ooED=*z|wld$0s92TJ9Hk6QcN}@hx%I>k<#;{Ghr#$^d`k zNxmu_WzjjJV*wbZ7wH+e(n$4Mv^F+m*-Akw>b`y*JkXkT?VAA;OgWptQwAaOZ%4fl zufO!iJS-AxRdq0HTB0{B$6fyV5F>?LjtlsvDb~)QXmTVBF06O^uY1vNCDe1&b8&OX9gcqA0VVY~KB-k)LM#NW)jI2&J^oOe|i`vPJ=42>8L} z8gYR_;*$jBcD;C={bDMtlW_oM^5OBW+Q*8}_ujA}xjY z=X1&Hh8i`KZcTcj+BG3fBr37otbvaxguFhLx4p6PRaR|mM9)W`Kp1!KC-?W9k4LkM zbOPV|1%1*|6h8aoF?`QAPd7Hmrs7RGx(b3f28UM=2AB`O4oiOv`}<$)^Z%0%KmCBVZn^4PZ@r}-Pr-Yd z;O2UY3SRTcg*d8@C_u*ie8RQXftba`Dtrc=PMcTQGSd>g2!mS+5m^9l2onUJ(Abov z4(}6=^IwVfsLH0OuAwv>$bkJzIfrnXou@zLz!MI>XOO7i8K-PE!Fn*t;H)u8*wb&%pD`XY<4z2x3tNK|E` z!+m|6U0h=A&W7wX5d-VsQ6{|)qhgem*<^qUjv?9Fz|3qYzXxc@(K3bP4C%6WUB~Gjn8!2@XuU$lzqmuu4=gzR>ChH>Th)MK=;JX(#CzsmXrf z9ce!9GcD7>4@eNt+%^VW>rpxd7ScAC^nFbNazX_TceJGha$r^&=13c&Zk5&67r4oC z01VNA$l;$rDu?NmS&Xfqx*z8kI9j_h2og%T?u9y*G(gCPN)wua7n#cSF&QCLBEc_6U-1=f`FW1!I2C&Z2%K4ow7GVRR+B^p&)4(#Tj9>qRm9;eHDNJOIhqxD29B7Fz*4_ z54d)vlUw?z_!&>w#56#>3Mj7qah_JDCJGhdkEsF`AgtVQ)>#dNF;;fZP;uZ5Dn=^K zKY`;|6#rt3vod4ygzxDTt>;ZN9JaN;BIv#7NV-NbC^wM+OVd+lXJ^!QKmk8#076q| zC@eg%wvgYizy5ks<9MJra7Jo}VskbVEVEfN5-TT_gN8<4{S;lH>PV%PUN%E7F`ea1 z$2g#}igX7lM3x#fDKb34l?@H!Hs&J6vqcI_$B9YUDAQ2$)Swdp>U;3}p}6+vvp_uo z^g$WT2Iny!%W~(Z2}&UgwU30tl(85T(6O!)ET2kD=8f5-0OtGxPk7ZMN?P#V?)wLa zyExdGzh>%9alU|!tcB^;C@sjV0O){}^$^xV+VbL(3=1=oXO+^$I&GMtQXrFD+qVx} z1BWa;Obk@yBAur?!jbTdWK~4B0`**!N^i{^yjv?DK>XF@S&qUI(@>~mTL+At{T3wnuOx-0G1@7rNI!^f0Ln;}UZdFnh9Fr+Q&qJ1UuZPUU=A#+ zDFQub-OIGIsY>!u$&3kVfR1U})~Ve$9?KO^PK_uf^8*{r@=Pt}(rRmBt0;q&&KO`0 zuZ*dRh|OqJNn~Jb|B@ZJh_Nbaq-05ODq1_0n!2zbVeVrAAVLLn4QvpIug-ETD$&p`ziIHE9Gg5lSG*i~Gr9&GM8L*Jfi(}R(Kbw^)YZZU<%cmQ|&G7B=+!^9{J@K{5Z=8KR{80lZ{6C0FNaR{i+Zih(b{0VU^Hl9I6MX zw{dP%;RIZ=!XbvxpK#~CP=!T64A7oV*|sF(E9d)iOfg-`sq}_PIE1RjMgD7c2>W~)(K1gWBqgTf zde<{Dp%}a8^0H3jgZw<}AZ`F4UeQ(PS3FU$0b>VJ~{Ft7MiUgdb4 zp5uv;uq$fWJ(T^e1ya+pJtyhZy2l zv@;;WcMj$q4ID5(L3qdMe07P`hJYVT>UhX&q&r-~o_e`nB{k$Vwjxv2Gde~6RI&(` zX^ACA1VwGT6b5dY4)YnjwhWLqHe#CGh;0Wj07@cxkK8w0ZWU?hl@5|7S`RvS`g4XT zP*>CvCsPIhMF7{8&PKxw=0YgQh8Sy5*&K#2ESQ8T%vV$Zk`NGt<6A3BOfXx-WKd63>YsvS{J-dEHBE$A{FhF zqANw=vMvC1LLrq*Gv+G$k<^AN5Y$Dg&g`rNW{`SC$YT&-O}+1HM{&9cE@jyeC}FDD ziEs6rD0v`6a%mk@ihvL2!4FjueO1hmOSs?&!{&o$03TV^Sfsdw66B=B(p0p9L}H4N z5rNfunW0+D##c=(dLOhs?nY{Z%;bTS0VH4vFmwv^vw)6GqAnVI>f-S`9>*B&nv1G| z>9Dtgh6gKYc(9BHi=VWz>^&~Ue1wkX!BX7n;E0{|0yMCaJ00nGe+_9FGSt6eRLcNk ztwyjMdlUvrQ=Di`8P>e#kg*Q6uyhVGBZn_oWl*&I;Wt!hr6;aY0w;Dd42MMra2Cla z5z#V4iWv}4N-zjqatain(o1q$^3@F-F=T*p;~b^+QV$0Ew1j(7#XcmsmE%P9T%2ZS zz$~@WAvn7DCRw&}8QM`)M-}1%vBZt9Za%@0IQRjV(Uc`iu;C2vO;Cr-FWF#*jbX}n zX)~A2RznlhD@#|xDl88$*%Y94(r`##CD&oCHxqFqhyq3`p7=_XG*-=_T1o-~oIX3} zo)%yRqte4Tnp8YQ?7WZ1*!$Qq)PE$uUy!3QXIC&Eba(qsn62h4lw{Gw3Z|&|tPQL^ z)a~rx(^S72VWG>3dABbKHef&dWjXy5C=dWnIz^@%CdstKc4~9O6p3w@E#ImsAEzt2 zVw?u~M0At#4HK_*0}W8nwgH^jzF$IGM5K3x#PPhD{Wzlq@ABRa4g|~VBVa{%--ugV zi26$KTDy!X)k#{2=H(-5h!j>{nK)P^-;hbv;lx55L^8EEn2zx9)ZHi}hQlnyr4u(Q zNfr&kmwQ$<$$e_py-kLmZa&fRJJf#0mpb1W;E98#s|n#Ie&oES%gY)Sc}ye#N5u|x zF)a!KHs+pXY6s<|++~&nIaHDy>wL*MHBk-2^X>pjNw{R~Y_4xcGArXYn+ z@Ji%kZiBk6^w=Q##4ie{EbD2C)tR!7Hzu|bv>b}1SIjPkHz{}~SND5SdX^TfYtc(W z`F3mWcz$~zEsKC_cDcZT1Ss2KkCBcxS%#@|u*%0YW}szIwRRX+6od)wG|o?V3*-Pq z&+JY^5R*n6ZBfT6S{v;oL}(;k+wvpciG0p8?}MfbKYNzwl{mY z$(M?#DXI0ClF1Zh6W0zpoiT_-2bF;l!;}Qk+3Ev(mlT9L zbhw$Jy;fP^Aconr0Ea0%yygwh=|=r*T*1PS$i&7W;z3r8W>C^Jl*W;?HO7=Oqa_FD zK!Yy8pW+NO?q>?&#Uf1-hkYtW9l3TFRn05C=ppuu^;7WLjbxflTWs4YZnn#spD)1C zt!dpSw+pyh#$&Vor9mVHn{IR3RTYR4rl}X$-3T|}jOz_{n{a%C5XhbrTY#ChX;pBf zs<@V%w#)c7CTcaiq^lCG4+JIhN+vAZBL@>=Nj<@Vt4Hc|W$UzlV+gM~h3rUne#Fts zH$_U%f@DlNkdes?$Z`sHyDAoUTFM9Z>zyHB*^l0zs_~7uXjSaI};=QUdg*fqx z9`sDmuo%oP=38e7g)=L>4>?OKnzFD5sY}5;naMtu4nI_$sr37-^7h1mI$Y#HQ=lt& zA_V5)o*k4$HE~*uhy3gThKH*Od~(QB#7R@dd2+>#TW$|XaW_MLMO{BH`8H#K75&L9G#rZTSvs5oWu;Dta?LCpt9XIh}p4! zNZ$OIeEafo;s9+!q_`HlHt^J-KZ@p9sb*^k!)<>mb5V9lk^t?PJV)Tp2mA2o(@P(sP0L- z%B_f@tQn30K54Grk|1Lr_!!;n6JtO4M&4(~ zj}8ux_^jT+{_)}7d!HTe9qb>uw%${)JR^wtDY>t!?Z`97(h}oBwxdsrDyzeK!Lvkb zK9XWoUa()tSZ6Z7DVPzS-E=MNV8^ZESqkJ)cf5 ztL^8X2cl*dRg$G3^GP{-o&ztF%k#^3W^cXl!V53G`a&EjLJ#HS}0 z^wkDVUG`=J=Z*3V4CVQI4NjsyF6YZ?V=)T9^UKKN1gQ8}IfrsV6)rEJ^TV?0BoMZ~ zPHZG)Jk-o5w44gb@p$us1?AA;_YgDqSdZo^4>apqx=Xvqr-(_WxQ92XuQ2DSuMgn9 z3hc1Am12R`_!x$sWl43-sopf%MuR7c?TLFLtLZhwVB_=3i=!lTH5S0Y%X3D~g#|P`c-VHoe|!L&!?3t3E@@9Mo1CQ>wk?!`Q)>!hK_Wvi z8QIBuyC)y@;Z=yYxYO$AAKu%IKI(72y!Gy3|C3KQKVVn(4*1$9Rfg}mDFOOG+a$9< zI+WJot21r9D%bfbEpwC%4QZ=yCCsQ0KMq5TQGPMdr-s8}M?GO0_9ZyeQ0O?zgLn@d zQbT;lMy~gK1vXRqNAGC}!j`+21%qVEr8@Mt-ha?cU_P>S?TZxQ;T%XcHCP9s_u3}U zuaS7n{=@DP-9D8A2F1w6IhrUP^3k5-?M>K6(2;}8v2&6+W>uMC%nxOI9$*S)3N5~X zFmpaA!7k8Q$w8pJO|sD7s`jGlEk6rr4ztnNy?jTSf^#GJdYZJySplWwpxYb?R00=J z67DDh9BfpYX6KKbO6^EPp)pT|!*9ZTl{N-Cq#pG?p(SV)`b=Anz=>tI5%ozO9^+2F0J z<2fK-nd#bxVf5^#rD*e7Lz+6cn-F8rFRLIDAaOMY3u_Fgc5=z8y;8dTMbe^3%dTkp z3*3``(I!F|@&Y2^1r93_c2&j%+>``~IJUqwsJDUlM;SX$jaAFXHRYU}J?Jzo7|;Tu z&nbYm5zYGmkI0p?l^Kp+>Q=p|N0gNM!JI=&5~9odR!57i6=x)$43C!ogM8)C#T*9; z%C5XVWg{5V`R*cC)7A#08Vw~OMjOD7k9oaH>tXXIwspJfE+;`@4rrtGsU&%4dp><` z>*nL6VCpr9HF#*N(b&OT5%I`+C=Zi6kUPJawBf;`P0!k}$EUDmydmc7MVlZ@F&$Kg zrV|OrWr!&n!GXh-f2+9}M4~3Uk3l!TC5{+*qe%I*BkGU8!3doncUobM;yY=N<|w27 zbP96>_tZn!C~P)X=Lo`B#gvSr&U*!}z}7TmQP@`5vJCicjah9$NWx-AB5rEuy4Gz= z9RF;4*Dh%_>E1TJw~g;@<9plqCM5h{ZDTZA@2%s1fOWj|Jx})>>L;E!;~;Ev9JP*B zT7O6Y0al4hd?x(Lei>wXu+$tLKTXGIz#~R;_!-bSTuh=J(44FnivWH=fxnu+&~X^5 zl}!ggvlK+SpafunXrw`gXCnND@Gx_p-^lFkuk&X$s&gO|V`C+rmft~^I7zw;iy13> zMisG@xG-r%t*?0~ml(3##gZf@T+d#+l2;OxH=>l?2{0dI{X6F61Qx z+b8C7fw&GGu8R5crOFttlnWix*3jZs%O}|_@n?*%a-KCwv#xGXmn_4<-L<~*r#?}h zhuDgOA!mNMUe(B7C*oCO^f-hJ!_4%yk&ra$bL+8$rUTOpIN^rSfqEYKptpc!r4?RDA3qn5M@7t=9iJV0MxthA_>tb(Ln1_h%poF6aeJ3c>Fmn zhgFL?CjF_%hZY(n13*3{t*EoK9OoQ0uZP<;o3%O%LbzcX*Cfp<>#s>fgwZdaXlx5r z0?2L=oK@^xnpNh|N9ouhXk;?PO~78mscWtehhu&b+^5Rhbm8YLLixUYtl%blChUp4-Toe0WAcmz8pvWxST=ZuHg` z@D%ia)Qds&;M4#Cu`k6D$gGrkD|#8r?0ni&;9tzaJs{ilMVjTvVn?>hrew+_PC z_V4xKc{gS>;mP;z0vFern{R@^pRq0NwQFHX38QKk=vY#XY12yZy{Zb+FBSM}NpWtW z6qkGQ>&bepd3vEcrSfV&KwxVAY=7@qFQW0U&T%fzX?JVRiaGYQDCkaj8`uoXZ0gsT zg_cH@EcdOJx2J`W`s>P!a3ou&QN4$Ui7)1q=Pu<@P zVH*wRnro}^B8S6xu?cJ8-9I>XaNo}o_p`)&mbl&71N6+cnHE|@oWE%lP^Pl^wUAJh z>;bm56a!Y#FDk!f%en--MdaABSh}^YM)%T6Z5^Qjjep^PO$z(i`bx_br5UVvU~Nt& zs=Z?qAGe-Hd(Wc4l~;U17dj!Q5ONFcGy^D^nEB*&r-QEI@i#Zc(Xe1ipBu?2xNF?n ziv)Om`s-ZI5S%V<>Hb4wPfs9&e_T>!#NX z+f8S)1r{j39X+Cz6+4?`Ti)=zjcr+$Wm%SGi5<6hIna#t6%Yez4+_(DA)k$kiCT!x zFN>ngCK6S#c{_@F>MuJgi)x+V(({i!jo>(pu2wN`EiJNeZ;!jdlJ)dIyxbUJv~GoazGr?5Vx6*26{s zrfD5>J@c<@*lr?5kAw5+qy0ym?pFd@`P!L8okLk!4_#8|reGbW#N|`sZ^}_sqQzq1 zS5^gd>%{j~-HzKc`!3AaHYBW~M5xk?z+6Wnj_~EB6B0>u2fb^qry2I*-AW&Sw3^{) z8AQEKs)P(9j;54~Wt^3wfDsTPdJ=hyEfki)(!2Zl$hPxNL8kCwt+uUaF_INT!1^sA zJ;wwG+2=5$FFYwJlsJ6;g9HX-gwHLjVi0mMZ<3oL?DikAobGpO%IqK-W~>iF||_ zMy8SNSJ(iSJm#OekXvfJZz^x5dap*RH7{@XPE`7h#Sv2i2^PT)%b?O z<8LNzx=}d$<`KKaNgrHXghdZJ@u~3AkHSPV0*b*+xw)4ay-;0t>FO%zqVCM7*_4}AdHQ=653kgcPlWK+1Q!?++`^f z*q@m~l(8svB$1(rq450>q(OPIG_llRSq}$Ok+@{(A#og`IKEeE*-`!RL}RLiX~raN zC7PsN^|Ce4T@AW}^NF5)s3gLo`UXlvTU~8xl?iasQc}56+(@cMO>c11pn`LWnSq6K z%5>^FD_X0P?2PHm3ww#0TxIR(rpT%V&5N8xHh2L{NMhVZ+7x75t`So^?&2s`9mw>x z(N5Jn9_FH96zMF5^m==9f?h1pO#0XPtj|LdWMt_%NQHisz^=wXgDV7pq^r8SP$N;_gP+`f7SoeQlpb=6+R$xx|&W4pFM1x zkWOMpAO)WECvqND{8UOQ_6?p^8b9ipXSiGALH(hsogPGe zT>51MM~^ZLUt?0~z(|gq>xtHRg&+_&IzasDnP)l!{0aef5{aU-?vqSkBp*_0)b20& zdeXnahi&tJ>R$;f0tOYc4$Dq+~-4_)!mNY`Gv9`E94i>%%hZXfBpufkw>an)c|<+ zp<7jI)w|r(k4kU;W$=)s@Zyr|&eL&G%y6c=kU&^G`6&S8tG+`Vr(pUlvEaCv zdB)e+K{j^!jD7CVEntYiyH4){E&EEdCzC9o{9t&jBO-OiWW(ASBx3#abpH@U*C}WN z$2*TE@8{FmC0NI#yFo9?ivm%>;h)j4vA4ZxTHZ%*`&l}AG(~9bePZ1|s!Q$^R-AL_ zqBTYZVSMoUEIeAg2b3yEf=*+fbPT)?8Xy?k8^{5rLG$UfjV8ACJz#19dN_AnMG`vT zY=8H3m%rNY;%^;O&5(Wra8mxI zhE#|+I2VLCkLG@gkMbYVda$N#|u^JYnvYXN6pA( z(;KQ?3+JVt!p$?(phW6yT=b0A@WBT8V9nP`7VZLLzEGQi1;V-^w&Kard6@Re8hM|y zU`=?6D9!=*sV8&Kw=4O}X7V`WxpXk@cSNKnYLUt>@L{-HB-+1z!n2cXC^T^LJK*~} zPO5)}HXMp{dXBme`?5zP3Lo7Fav)M4Q$2QqK34Jf=mK~+EL{Dpa27#tL zg+iU`&qQk#Qw$HwgVhR-y5S%sNe>NZ#0&}^k*H#EB(SuBf{Way`+P6St%!yqqQaE0 ziz!G3>08IGB$4ThB8d|~uX5o(S_ijLHjv{L-vJ-v;dfCwl`uw(8HkkVgUo>ga@bTI z!ONZ7>&B_Y4g-`zlgL2^Vu04_3_?CKT3w}0Q0j|wqP||^WzQBXTH?N}c{(nhq~mJW z`frFF%Zv?@XS*rF?89yX>7o9B0^Mt*y?*f?R7(%$tC4VoB1US`KI||SLXXWTzUp4v z%@${8wUx9tVHU2=`Wf+V6|DvTWTcRKnRP(Lv|vzT%`!y^`ftmDl#gSk0uM%u1tVI< zm^=iq`Rvi*r>AG1AM8B{#4Aqt1uegV`DW|8M5eg0LOPzcRhjFV)!w;qtgvYDVsD647z%anJi&*$x1w{ z>CK(S!HlESxt>Q~LDmSGx8Xc5GK zLq?z1qG1I02@$9lPo1u_Jtr z@S_W{IYAGX45TzErDym;4s&+eM|Gw8T|2>S@(X<9D(P$43l6u_d~%b|Bq zdIoTwO)2&1VE1q+9rNnNOq-}356HD{7=qBv#m#l!K+hsPW!e$Rf>Y-?ebnkdP*oBo zOZ)j;o)=Pw`q+?%2NG|b`(e^8USTAtTMLv$Nri#5+T;g_`0}D6raU-&X=9c?#r4;M zmPMzJ6HCAF2GYu(nVd{Lh#Yl?L@;0th2GGop7Jh~z}Zk%uW!EOb?8!ZA_-?ped#OR zy!QH{2O{p9`a6kC7(0S6KEHuQ%=x_eIksf?lDCbY6hHFQ(w!(G{9lg1&J=Bc-`VTY ztI;;d+K1SyZS@hni<4$ft5-}bz)c#xPw30*kJ*m}-Aa+)i@ZHo*DD#HedSEoK&|l` z?jgp4I>*DnVj7X}j%3qy4|LJRf%Rvt%Pn=~Vj#cJQt&)Mc}X+wH<41+5mX3wbxf=~ zBX$^2I9i;4H$jJc>qA>gckZ2peMdtAU#-x>5 z2`52jl;nrcmZ_nX>99eW-Ss74DuvX6k{N%_-2P=Hm`*ze0NUrk)r=P3%bp*S-sk{&Jp{4} zy4o$BStI$e`0R=G2fPD3@OJ=%-qM8GW)ZKs&DK=91dmk@!(g>Vh@r$eKZ>g~)6^HX2_AGR4H(FUS)ey&D#$HQN1KMwMZ0}Bwr`v-? zs|r?r;dUXbluzX>$n<6}DfGh=j>~0$=x<=>L$BgjQt@Z5_!U;9XCqzi-(X&$uWof= z(xAI;b>U)H@R|pBa2Ma({ME*jaktC4MsMy9=EE4vh}(x`yY|ARgaEykS>LYJ_nw*idEgNZvc>xdi zqbV50*y?=&0H<^K0|3=`ga7=S*QKyIZc8sa>GW@2q)3dxf-LbW?$OdXyjJrIslb*d`LT1zDOv`6Kmq067Q>5t!fk7~lTUW@ zDb#ydJ?zvI&9YT3keU|tU3Xbo>*WlC_51;={mIQ3xsUFAG|^=l4Y)@r&$+_zFI-wD zgv$#jr@NvmhZKr&K%|aq#9C>O zYz5@p=H(YuU6(C>u}!sX$)et7{H;@SXi>8PvIjT;bcfUH;N_RW+4kj^hgM*a^zXD=wQ0ZeV+~k6oGRs_L9myNP#I!IfZV!o88pI`M!njt}|PoC~bwoD)iy%N|h4D=a$@ zk0osJy}Y`#&;D9pNwGJLmVz>rmQ$kDPc*fV&E?`5}7)2aYP&a%IKr6h!5=r^F zUb|3_0)0` zuu7q77ggs%{3*w;B;bbe0noga7Ov0BXSCy(r~g|297PhZK3>60M?dF-&Gx{YOE3t0 z9LdF_(`7J>A2vq~Zk##I#UpK0P;o@=CWSnY6jj)?;DIDuzY8XxWVa80 zJ-?)(8nX{+kDy4Yp$Ba)U6#7U&vxn7dhRh&PUoai;DyP#v}q497;Z0Okw+-x6cmK0 z{|n8(nb52^vFoIJ>d>GNjXiK8RlS0T-WTwJ@x`xS?_Vm6eNjA4o0bB}T{<&{g|9Xb z3t;WQN(}z^r)qG}T?g6L7t{NaZ9S#eIQ{zzuTRo|?8+~krgCZHd=mRD5aaphpI=Fy z1;i1fvw{lOqQGgf&_F!ts5=$`!;q}0H>IZ&8(V*wgz-nBKN_WwCwam|!O64&$un`l zun-+{IcB0y9aGz~Udft6N=h)q4GuB35<_rO7UeS|%v2Fzm^pnyD61Mqnxn~Zm#qp} z&^p#W?Hzcz!%o^BnhGa86r4(0c`l&De!h)NT|z6s-Ckk`o}*ezg?v;ncmV|3rAThBEZku_IW){kDqTTb%;`;W zuj#TO8h0Y&y@3CbE-R}>_3Kv5bu_L9dmS}lTpBMo89|SK&xC<#?vRVCjt7E|!^96( z;$nd#!r2v}SWp?(v_qIK`ZGHUU^xoKp@~>#TU$+)L>yP9|FgIpe02UgE+=;UQ#l+yugu?0+mIaQo;~iyR?6GS%P*`6H@nT0n=LHo z7q5pcSr5d$t+wY5JpI&h8Sr!j)qAN1h-e7(un_p2Hvan*u1(S8==UldNDcpZ27G48 zT}qdgVATabbe&-oWZFR(EmbhIBn)HNsA{-MdDp$FyS}2pRB6$B7i~WbDl^RiFpT=F z+J-fss&|xmfNBJKFszA)`LKlN4arw*d|wFjstBn`qjCp+XTA(%`{}wzS0P44Jn~lk zqHfg)uJdjz!#I6l%3`&a)wk=A0F(2*Gr8Bymj&I6<*NHuLc0AxNzoQU*L)YI1 zb-cZ&u%t_*Z2(~nXX-R@k2d$i8ukL1k2c-TB+Dw%bJ?~^n&*MtBUi>}c{`+-{sUT6&aYc`0wxAo`Qgt=`__iP(zJxx{VSK8R{)0c#R>AG1q zkdP6C4ICO?(7INL<%>kSQIDDx0flAn_*Je6O}GyUX9ybO`*ReEwwdnSwwWv#9=9z- z?NiVg3?1Kt;L+TtMgbB6hjuUI5MX~bJp_-H2m*Hz_rx6x$`a##3`*`s^2{GYTmw=< z7W=fOf`Ox5nI`z24{H%AR$@^5Vpt86M+a`sXLgvUIrq{((}qrMG~Bgb!H}CPc^IcT zm_Tlf)ZM;nu=!31>WrtGj`F&Ks(hAM(Q091y3~tk4fTr-jdq$(_hoTA;cM4Eu&)8jSW+-jfLHO0rH+#%sVrx~gmQ|XD71uV z=jamxHvGJP5_B~6=ZqfpZ@jd*4bG?1@2~|p{gU}o?9EDDlcqasagSvSXBE42aWWTX z>S?9G!;s`z3%dw9Ge+-ed)HIE4nP!aHv!ZxVeP znyS*u=oKoa@rdU_A=tDj0EicwV>Gy-YH+}Hg)jWVA; zDL{*gSn|=kmQ@1&OWgjR;r=yGK*jUj!u`)UhzN|VkuV=+5G-}(KXB z2Y!R+cNpCn)p-l5^ShxsXH+5X+?_GyExH+3s?l-BBjw@Vcf2ajLlpx{?XU83yH~{K z``83|V_zvWM|cY~kXuh%R4IJg!Xj318 zvSo1;_$bi{6iF;Xfn~PWAV6uU=x|&8?hH88`8U$wJG|@1KOd#@C+H7ln3=g#ds(C1 zSPgq%4j@2lv397=1A>h=5gcDkn)uNaLS6r2o64V&iYvrnZCwB1u4(Cpj}a~cE5t%w zHI(Q*MNoA+UL{_oX-~?3$2h0$QBM2EIPD+dRO6c}NBKccZHQA-cOHy$YKEj?P5mMp z#Z;0n$uVC}^_qry42$HFj-N@AXtVYmY=QC4bI;yq_|rdze^~nXp+oI7g{sU=6`1Gt z(4`%q)|&XeD9ZkJj-RO1-!q&2CS)@_E{fTg&Y!=k{w&|*_wMBBI9utcxk5J@R>wq- zUU=Z@Cpr!=q);iYC5G?7V_H-{&_50y=Y5}FV{*5h=yEo@E_YsiHJcsK0xsym~n3&1Ns z2h2D>Xg-6l>_U{HU+WLqG#zu)?lSBpOgOh@l<&5o23&({A5jqSqtn>WC;vi|v7r{t zZV>7SuVXg(NsAfCJK?nS`s7tWhS4-&YZ$}aH=`{5`XC7!7g~>9&co>-X7hW{^9J|l zyKWaCUisG2w2!ZStN}eW#f!)Ao4Ov67?jb)V}l?i1A|tWz;%omx{b#od@8>IeUSUFeAW- zetXMV@@x;dTz%ZpSUtrJ9sRsF!8-Ade%gIf;@5$`h{-RweFC(XS1$H7BA0i1=Ho#c z5*R0o3!%*Q!2QDo9WPH-$LWMEE+KivJ)4T*j3}=`X7mg$`8JfvsjyXJ=lsD5VyTEa_L97S;bF$AIEsPWL20@b% zG$BNJ-$(-MWtNpQs;$)s67=u!w`S@eL|HH1&J1t!&xAd>7Wrd2 z4M}tdHuFyRK z9rGV85TfYcArUOqWHak38rl}Ph5m=GbKBKU^cOF9+9MZCn#=Nw(wkWUcCZ|O&Z8%D zdHWqq)qY4uDh^TrD{cZL`>~jQpG{@O%SI~?a5OcXq@1GjD>wfk9fwR?C(`GjX3ARA zqP4j1lbGuBszxt;Q3GW79LyRc=n$9Oj67IU__v&a*b?L4IJ;P+&3b~N3P4E6Xm}FM z8)Mp~%zVjEtJk6!zW?zxS{ND8F3}VhVgFA7IPmhGq&HcR(I-CPR`nK|2%3kgT;Tvi zw`S4zmRKBdXE)QbnCen%j?;YF*m)yV>6_hSv=-fFc+MAc6tinnU!F!0{YSkeV*3An zI!ke`uT#K$;DQ=LKh&gZ8Ia@(VPnHjQ>Lx_C|LSiC5U#W|C5=~i3PI}KA5;-d%41d zXb62O0~vm~H5gL8UO;>gB~u3S67r`}`=f3#oLa1wFiiJFO$Y|4=d=08 zM<0=SZOZ_Yfk7%k?8p*b(w}H$gH?i-0Bp%WxN9w)ODX@5UIi3KKh%^TWc`u+A*^ZT zwX~aYm6|WgXOcq#g>6ZFCW)ZQB%+TLJ>nIAx(+Mx?3x>mtx+JgE=<|-0O_V(k0)0l z+MP}pvv*tOP*a{H*m4~tmuoMMAsl`HW9Tp2tUiA2LOW>U46UslYIo9^g0MJ-khhTR zvyJ6^&(qc)bB5e~$?#v@X{ZySmj5O5*JbV%r2er|jqu<04JZ)vT6vJcv@?G5jwB!$ zC#wSBvS(dY;tH`g@JW8fc2rJ1Gy{oH+?1382*VPnUZ5$~rZ*E(;g0gMtc5Em085l8 zs*@E_esw(sXe@EEDLo~foTH{Dk%LQ;I&`f$MZoE3lyf>S%y~4<%PZO$t4Fmq+wW%z zaR=oZVF-tW|(Z%e{l$b70#y_YG;_? zDJPYP52a~7!9Jtxr|kTuO1}oZ7L7FdtW-*I6bg=8fpocq$sfy;Tpk$C z_j#slXb#-ui}DM8> zU9&rxy`IbsZTK{6abuTp!sLkKb1@?Poq+Q^2|>7$&bDB~Y96D&b1osb(ZI;_XkQl# zT7kgepLFCZVkf)xBMR43ItwG)bC*Zew46OYIr|J0@}iiab}JZQeq@wi>yJ;O({%EE zRMm+-fwRkc?0-MXN275TDme!4PHD3D(Z*|U?!%d*D6Y%(=oW%vza9;ZC!xW>@I!3o zM8o-@v<1}hIt=zU5Ex)vppP+qBS1szO4tln6b`nc;(D=2M6_?N6IZU=C}O7CL(@tl z&IGGke4<;~0JO5)@^3z4|01!iCvHypr3t%~R}hFSPz(ZpiH8XkHDwNR7mN9sbfUc~ zO&+=ws&&67QEO;jd20drkyj*3ampQwEU78#{QE8)X1E&}bY(a|1Il=mus!dDA)dW|czpU%Y@l$O4hO=)1o@MI z@aa^TU(^tqYb*(6rW2_Tlf}cA#fc*>v855BLIJ$H2`ps4Pg#c=agFBZ18M2JQ551z zSw%fH_QaJN6UY+pI6{y8x3)KVMTh!1ftlHV{K>&F3JDP!eh&a!Aa$ZpaotQHH62f~ zg?2S&O${VG-hO=m{=da+IW3`&8Yyd-T~iupwlNWpWW)_*BPRgUA zyew|UBj9!LOT$bIC3z_%D0vW)32CK@5Gw_wG`bOa(-G`^Ic)bS{tOP%I=ooPU46S< z#SI4bQM}w!-2k*y79BzXQXyxWfvA$BqDO*wz(10q6BAT(gB)!&Fu7T)o6X`DipD2e z6kD*pW}4iu4pg%~cwg0;uf0+{8I@pYnbKd5ZOYr3#6(q@1tgG64mX%VY+bmxV))de znDq3o;mRtyzcGEPS<0sZ7BCE@dDtaKEaLhHYbg(jadLx!HHvT=2*JG>zZw1}b_x$H z*S!N&Mz~oBKqK0ru$dJGOpkN=UQB5h9c3`t#r*Fi5_HgA1w+$1)Mbb;JFmjKucG@| zL+vYwyn}kIAF`0Nua*0|LXv59u8^W#loMrw%%XdDC`PhX{p-4} z@4(C-!T@XJ}&PlE~;GJB$@o~eF zPOy}NqmKkBns-A02j1Mdta(MkmnqW9mnXi!2;68&Q~Po z($jz^Nf$fWJ&MYFmNlC)tzGN{1U!F8Z(!e=*77unodnj%&6TM)I4_3L2V*o{RMw+t za8!))i@fq2$R^VqK@cJfv6Uj*3{@rq1_!(QH47Yn7j{AOEW-suweXJ2O z&*674Aj9X=bP9{^O-DJYLVWIEbNoe)PE3Q3KiYU5Xb%5-u=QE>|6Th3_UlKVJ&vP~ zc8`ywT{$GnICMwq7rAgb?pxSVIxRK*1Nv)$YM8o15ZA7~OEl24k>yE_h9o{HNnZqk zesr_+`gCV{TBENN;-7gWV&rB23AraICfvM^y<-NZ(b@EDghasDy0^FY8L!-Zn$fJ3 z68b%2UO2vifu|G12@F#+P;gS}lj1{j^K>>#&%bAi(3i90$7dkK&KNE-8+~4wTaOi)p^j+KI>{?ZBt`ovf$m$(L(Kvti4G^#MHTEZad4q)PzEx8(8e-`b0V_F zO!ixjdY-69B(EULYm1R}Ij9l$Q9Jf|@lHiv9o`FrrzcbQi!8kcl%yvAB;Sb<{LU}< zEb=3LX7S1vg8&fIrG7%wG0&HP;R`nWe&X7Xwn4&75Y>jPdv^8WPdf0UCZdoZc)??6F zhP$;@;G@A-4Ep;J;qVu^XhXShPMZ!=5u4(+!OZHwoRJ=MlwZ~62mL@imQY{^ZnvfRlv|+CR#voA)l!(J-FihY2SM_HNHALk!*kthnbsA_`R$5~) zBtGl)mMR(yKcTh&(HaXvJB!u{j#SZ75&8k%SIYa5l{N>9O}h;yfSCc`Y=`lX=0i>3 zoJJMa!2hH2j{jdM-fPhs+i>B1ilEA)dhX^AZ0Rx8P&XYNfD zJ#z=40)H)T>ZlU4;y&1LPJqCJ8i@G8=4QOUpTq4itcQ3TPNv%=`ROjW;?(inV0ChH z4ZaH{XeO-pZ_)7#+uWeKP%eu3FoVw(Ic9VPj_!h7h-zXK(C}vn^}RL7M>Hs6u2FJ6 zn~rxt^Lcbl_l~=V3I8cUJSL+As_cFOXRWi7)7{g9*eeI`9k&AH6F4~dbkTt~tZ(uW zlh8BD;=(Zl$VW(n>eG^)Bd)68NTAftZ&h|{Adm2@yj8QXwX7{Ubwe1bIO)2wtSAJRwb>NMkZAdb+#E9Sw+4edU~M)uPldm(ILvWi(T&FB;;`iiXz9jG;1 z7*Kk;{hhsITW*~OKEn+EU;+$DG3Ib*I>1n93*}a(4KNJ1aT}*Kq;ZK8ATZ_67(R(# zU@zXR*OFr;ZMmO;b6QKv5ctGd`$TZE{KP4&e#>S7wMEvD<0#pb$VBURC^DOwdpo1VC}n%HQ@`X_AXe7*na zI&GV+JXynz<@NND{OpvefS*;R6WuW=$tWu!f_DxD9Kky#XFhu_*nRQyV+W^i9iI$u zYX14x8h_+jU8gojvXuy%Vt86}IvXLPI67O97kWv&y5@5vvI>hC767D+OAdl6uFg20MP=OMX%pUv zN#`0DTwuI;H4LRP=wxsI=s1G2<&R7AN-xKfk9NJH;}xYtd`vk70)aWRCBOh7s(2}( zi&6T0nor09ElS=0zyJ4 z&8gTUpl2i({?0LJ_ z4mJc{MH#z(+QX%Wd>IJi1jv=>9-nrWzq`cB@c|3 zyfBm&9=96tBI1wo%a zvzXGql;{HAY+O*d291Q^paUGH16N(w3> zqL5$=s}UG-+nt@nhmcKInvT*JY9@9Q^~c&mQiOa~1tF@u{wh^=kx&iMLxL@pJ9&!M z;KN2_16C-Zo{*I?FI0o7!A1t8v>=XP$l4G>4lmOmzFoyZ+>$hA1skhsq(gPGDiJ1n zBZYv;WC8K4=KK|7Kd3H-6Be{0XgJBW_JK7%cblyZurO<)cYNTAS;R^#n@d!}gu^8u z$4Gj?_X_&ZaDd-jox}Ggb$4X73fUx(b0PEFf4nF|9XdX>3Ym^YyV~;MsoVPaI_qp( zKpkp{6JJevuVy2MH$a?a>mX*tF&af(zm+K%Y*e<`W?X~U8`jgz>#7Az>C0)3lBHws z;Fqhla!ZiM1SRJjBqw!8=a9-5FeMNv_+b2f(+$)VyD~`3}BwYZblmeW!FZI16E$9-S z*lldq4f_(+0S_GqY{BX!7D3`$Mp&cyEia;j;RrRX-O}g+dI2E}>5^WAAlh|bLvbUV zV-KJMQ`0F}Akbl0g~Na=4z)=V^`t@ zE2vx~V4302g#2-j;Z5O>ZqS&bk8jax`q*3VA3@yEO)Q!SR2>JG;^q2mOpF2#o5P4) zvMQ&Wu|nNkK#2?m3KmUC@p9BdfYc#^ql!xwzPVkkf|o^Exm09fRd&_6g|;+%8usRf zZ6wq4!r4VIarA}lj>HQS7p=WH&!dm3ouCgstz32Zv`s6;wpr~W*I{`-1AQ^u6id~E zZX{@R!=0eYoO8g7Eur9f!%@m2=!Z(~7B8)%yaeSYOF&C=bfUC3PoX)xOZ=^Qr?yFt zc+aSPY6Yvqy%Ah@%tbSvLAr-BTekEB-%~(IXPY|&BO8@er)n#=FjKS8C}cnU%H#>d;aWRnUn&xQ#$XCFhHZ})q`YKSYt&6UZp&h| zpLz8VU2B&${mX{@?q}yH@}jf}7#sYey3?{!KqW8W(vnh=MWB^=mXk_fwW&U`M(3{; zA$)?yh*T4(-ZK%u+Ojw0 zjRX)ms@U)(LwhpjP<3(c<=hEqYtAd+h$hmboCLVMVhjDR*Ia~s<^l`dU6Ftti6Q20 zJJ(V8^STQuTm~EO$!L{_&KN6(i@w@5d#BFhdez7HP_QaSDKe#^AddGBswy7ta@TdS+NhXiN;yi6btdI-Z=KBl1saKuJ&1?V{7Px#ZPB!2i) zUXt==p{|dM5E%sgVDGKr(ea00!+*nf;J2aRs`i?YNSCy|Aa?2p(t(3W3&dk+d46pq z&#oJEKFtEd2mDcFu2FGOFUA5DH;$u-Nt>xsVnK|IgmX=9uEaWDKPdQe5`99!6=I_3 z@c8}rk3U@%Lh;e*DKG`TJV#${=x-JXj!6;1YHlzQ45;czvJzuui$hAWKt%#RdW9xA z{^;PtuMdw-4z#{Atk5__D3U$w@z;BA0_VT3dP`8rp4`u8?5zC+{wZb>16(no{PrVU z!X-B6#wxS~`Rr;n)fc`A%LCvkk&zG_C~F_>oxsgQ1gi+Nz@w(3kMQ;r20@xa(GOfK zznLoHoa8DJ-Qm%bT;YSPWHZLq@VY)0Gu~oOVW4;!HYsAcQS;p_>lBU~bBR-@YUqxzTBa!vrq^Z?|wxO6L`iquZ#&8||!1VK>kU>kwpWt$6YEt`} zn<@Od*1q-%eP>NtsO8nQa15WzFD47{m*I$801!R50bmEy8Q(hq&BNUkfLj4plg|zY z!&AcJTxWQ^5}#4GHzN z=RBOklaz>iC>$>MDu#mI>>1?aKrC3-EIBIqEd{kUTsv04rZ@#1@Qlf4 z&gnKW2WVVI&?mW61SkLJ>{Ny}V1`y7=2iqpR_PiG&g=#HhV1b7(8X(w9$#Al%2E)!_c5| zMj(m$Fmp9;G<;rW^Fa&UtC;N==uai~+-Or8I3@%U;zJW0R*0y{K%C1J8Gy*)VqSMD zPA0tkcS!g8T3EpT)ly@ZdFw*1SLy>hVHqS@f&>k^Nzr1Gdlvdci`D#xB6y|*&`T|2hjW)h|d zJ#lj&ttYLs`b`wPWVWqELaSYX-15-HaixYfT_3rxh7A5h*$`V=31{lfag$$rgSyTsU@7z`_N#{6uNcMm$+%sVYdjhtCg>wpjkt-x|Q)Uzg zaY2yXwo{f*+jq#q38H>F>Xv4iaa~d^^@41wdIK@2yoIgSlu$y`N7>`fBbRmZ$xl|T zR4l4GL0asytF$oU!edyVT0O9Cb(y9zcuobKkkwCG<3@fQbQ)z_nq|0K`KxTQJKD4T zZ;<_Oka-NUVv0_z|Lw9Z>@s#z{co53r|q(n>>F{=UEZOIRtNjPwH9Kn$$Sj8+NA!* z)|)g`D=whYztx&!;{GyoF5H;In>O`(@YaXUMh}+j$8Bu2`lbB;t+oHHwg0-c<`_@T zf#F|iHu)D6q0uZLZh7!Y&uK&R+?Cz*?1L>0r|173mebzdSW0{UTSgs%tdp%vq_t*{ zHDsHp9po+kb1fjm?XiIHb4AaF1`xK~U+|-MG>5jbS2nX*z9;}R650uRdzNL>Xwy)JlWdZuB}^1* zJtjuRegpv&#{A_ioa6B65K;{vymkEk@kv6|#gVyC1T_RdxC-hdJlcz*^}RiTCehZt z1+Afdy@%ccbp*J*w@8BawX3&qsd1id-P<3q^*xR)QMYz%nS{`>{Vc;cm~DHzq}I~i zyYDSi7q9Crlc3$b30PydT3<1o^zPPIn`YFkY$bu9oTFyVBHOi;th$mSlis^d5FVAM z`HVm5GL8uTU&b-;bIWCXvyI{3Ff{NMd$)Qc&bDheMV-rk$PNVI8myiQp+-POKq=eOkRMnAr2tUL1f*6_uZkHft%YBgfNQ_M@0?*o#xpMCX^jD(x!Rmt|*zK90w? z?c{j;7xa>|^ypO7Eb#5J$`!VnYut4GT&vywHPi;)<27xZ>%6Ly?qXTw6#+9)I_j6R zC;Jmcrqg<=$Oz+lu*Re;npq>m7D{hIdtEV2B4ExctM~6<`MQSNtl6lRxkV~DDm}UV zWmV^DrL!S#&(ZI^?-NDBoxH>}&P%*bKVD2^sK!Z_0Jrs?KKWCey!16=z|`iXDoK)f zCINF~PY4X&uC{c|oeCRDs3*Q9qkOn->QE7@y~)H^=n9!SaX{LR4?4qGLgWD8tn$U!y%x}?{=nUkVHwUIep9`Cv}blD0Sx2E2R33EH`#`1-11Lin~>>e z9G3#?$ZNYf-ypZjW?X{?Tk#Cq{aGAi68-$bGU1m06^TbTXCl8f0Z&bYvvJjAV@B%3 z7JmXOS7Zh;Q|;2IptRR`Xs~>=x0#D!!?j9MP-EWddZy{ytXP^t|$f{Mr+H&RE z`i!AMtM#f?uJ#xR=khG2)B|9H^9HKke%R18hlU>(LYj9DwG+5%xbc?JeM}{8!BL-6 z!&}a&T2I|M#YCYYS*J^9P&J2wIQkL4&9cu=vuwy!(|BVYwf0GwKZnb<13peu5m_C) z?_%-{#oUMO*iSFmz%sNc=c(N`D{S-0oIi)pLavv^P$=)TDP7yKQvs7AoCjxxF$>D(dK&$KqwVM59iq}mS;|rWU$=H zQf<>O1s(i5&FrR9K5`XK<#$pPy0i4c*<{`I@S@Wn-DY(u`!oYKHYd4LmU*uN^|dBq zYbI{h(quIov`%-XOEQ2;%HnQxYD)on=$zXu03|4iAS0q=orfSR6r!!)sdJFK?s>^D>vR`?#0oq; zEFAMC`7wG;;Uz!X!lfzv$rhH^;A+IQ_%A-N1(<}LbI)Uf_~g?{ui&{@GW_`D!=n%1 z{`#ZiqYsC+Svr{;bW5lB>jvb#$vmGgMj1hVbMNHXkt}QuP*pxE#ykg%v1}d|!W{XX zSffyoH(Iu|Wy@NbVT^v>f-TU9m{J$dT45kdY7{_?hR9$eg8o)MJD+5G%$!=+;yUs; zdSKOtX8+fbidPK1;i?^eCae-<9J!pEq1xSlM0eL5z@eY%=j>mGdGtv=c9H&gvqJG~;}Hktm7W&S zgF=5NPCfeoS@q(BzmiweQHbe>D9G~TqfN@JK}MfJPKMtq_O)jzbC*MwY1hE@S-3iA z>&#f~lIoNVwk#|4@-=%CjYJ5)zEAS$xadbuR+XN#+J;-WZX+OYZUKVRl7H(pLHFw1 z0FLTwAays57F?RhG-Sh@pp`nkViXR+*QeDF$L17bT@%|a5|{3&e9^T zwPldB&z6rz7t>%@|Vj{;AT zKo&rE+u$e(jbf*QKqiMm((9DQlk6fdtH;u^6|BJb0f}?&xuU?7@iIYBp#s?&R-*ur zp+WLeml$ni?>f~AltIC#yG<4}ANK6qV^i-lqc>55)SsQXhgK}VbM5epnWV2#x5j=? zau9QVI?{nJPd@Utc`WgTtiOx;b#eK)bOUh z!eU|&rFKOz+FEUJ)M8dHGg86dEPKucWUDX}q)W}R=K>hS-9}tK?tTJa8&LP@UDQ67 zK`7MINCNyj`0Bansp{*p`noh-ia| z05EnC_CEZEPH7P*9s1{J?5j!AivdCm)njNIBS|sR6C*5k6ZW^~?`(RsUT zbcg{kH~=7q+A%bZk)jyM!Qb$w=HNUH0wRwXYf>}kso*BVDIg8P_k=qv!*K*rd%_wP z;5hmTJ_%n~dT(0Lzx~d0G5)1ny)wO8rDab`YHlc@+DeL$;G(`+(__edGsz(C8oM4c zrx)@X^I10kA*-HhJ|~w#lv3t8=nnG)c` zWs_Y1b%fIOV;^7+qNkdm&Wo~7y&C`{`vBBar_86TIzJH{N0lDQps-xHFm{-CicY;P zIDpihrGCSaWZ`KAB~&S>wn6v5g}wg?ih#!P^F#n!G{c+m)Tfl~%d-@IPkC72n+0d( z6QNp8JdKcynDQqPSJm%Fj@SYzke8$(NsDPZTl%`QfCajs^aU(_@Tvi1smZS=ODxnm zM!~c(X1zMU#AE>3kJmG>+Ov^9gaC@COd5nhBbY6Qn7kD1Pyj$i`8nT=AamT|Ex{=D zrSvF0heEo^kxv8BI9{#bx?^HRWM? z%^=H91kfm70hX3O6($6k?h_zLxP7Q-Vb1Xz`5$N|l=+niP&!vBs!HeSx0znt z-}oPARwJ!!6Qk+1wwj8U&UC&8R@aX6LUbEtpk=+r`dUA$epF-D>!=wPXbkgYW7>ef z&nGWMbY%xZt=Tjnh099+gX=Qte~{yCG;my0qT%LBWtRiZcF(mc&W}_NdUTR`MJvyO zi9zphN|=Z;zi+~~HK(0&pDrrgm0v+cldGxo;S)WN;(V@)isbZKDU7HcT`G5HCVEA}V?VhQ6ol`#>Z;=-O zm3hRVy_vQ7ykw9OsNlsGR7g=F;-_IhoiaW07I~}JHq-)>l0yi&v07hJQVY02q%iX zBXCJ!sn-+R5{ml1im{&TM`;|LMFTOc(?sV+n?$?PLqoN@AI z*FR0sR$@)h4Zd{uu@%cicL9 zM(LLAgPUCU16sk^6$lE0#O_v1pn?V5vGEZko$3am(hk92q$=Ir6)Nnt8!6gHb=3tg zM|M%nmb_C4Am(k;28as905)DQ2CS>l131Y`i>s<5KV!UKlcbe1eD9>+(>hMyxXys? zDhmy|`rL}D-pA8#!RD2;e?An`QMXu#r4~8}b>s7vcI#c+uD7yZZ(+l>+p(M3vO)ZB z8n*$>K&uDOn3aLYiN^0>_B7vmeIj{o+^gqA;AwQY09mU98z zII}%qfJ^@fmo&s*OhKpU!eOg)OaF#;X;ac&NW)0ZxHo=UFCXfv_z6O}H^>qMm(EPRV zy1ETkd^&*o$qb|?Zkth>h;apKL-_YKbYC~E3XAur_v45bya5J$t@3h1zws6e3gmJ( z(f`UUTB2z~6=XePZLl5VwG$}z-%44NpH}aO)Jd~BWP=XJqp0P0{OQ$8H3c4KM{wSb zujX=S*#}3br$_G}3{UosK0G*qxRbQ3UX#JUK=Zza|I*Kph4YP{sY8=&I0FdT>@nN? zhTF#xBv}X4CDea2Ewjho+&ewEVR*OuBX8*fU_WY%rlXEg=)n)g41!RuNb%AmGfS93 zSJmq}=_DiHWr10!m(dk>tmKDz>A^Y9cArX771MGF>+`Z$%vNDuF;ay%i3EOuPOcH+ zXK62skG@;X4<}M^+V@31ibm-btPt?KMZn{6GzUj1u8Q2h@hU^wcy;dr&Z~A;!vzs9 z!x`+P(X7=rCC>DY)+P$@h+G!#hT&Wj(?*{GEG zHwtSl6Cvayg33JSrwfE9lN6}G%*q&`pHCKu85hG8%0?c9{HAi%3hGL;8o_Zz?eD++ z>PPP#R*v5LHlH9~%l$P_>O7!H&Evi}pKIR_rYUY1pFvfEKe6Mln2z%c2>s$oSV5>} z6{kw4lJB8Q#iFdg)GUzs?@y76*@E(?aG&IzhM6dc)V{7<(4uHjdfHi`2yXj6FA)Z>T|PP+8pIuqvE@FkK>k6L`qeVk zzA+C7u18RG57jWO?6|+vRtfF@W*pPfn*a%q6asSB7>oNQm zaQ$ILG0w8e!kYqFjE|>A9E}!4r_BF^>}NLY@+&lvvS*+V6I2Bxza~l=E*dPL;S!G3 z7jv*HSf|YL@>?)s*v12x=B9vaOn^2*(KWSz^l{PbCffBJMLtR*GYBs};0qQs0^DP( z-UO)vn->Nl0_UeWU%aF9263(G2!lJD}lZCO@&=I9LOhs)!hnR zxfb6%#ks~TO@D-Y1y_cX#wf=R89bN0NatZQJrOb+$azwqgE5RG;H0B11~i1t9O|K5 z=OwaSo>Ko7n@zIsvx!9Yv-h(15vh0t?G5sSWva&XDKO zJcrHQPw6cNy@J2z=e<0?PMe@nJ|1WIbope7x6yEm%sg1ovR91TDPk;sK{$d&VuK$~ zVPycZI7Q#**-{@lzg#`qzS@^_lWIJVKg79ne0Z3wMeHkfIo zx_Lr|qRj5{k|HAt-*APE2;&CpG*l#4#%dWn0E~l;(O}VM+jLV@WGDt=aP3DGImS2+ zz=)P!xI|Y5;H|OJ>4f0iqZcO}OHiB<_)sNY)1+_e>rkC5ennmvPY(8vhNoPhPDd~R zPUnkn)2P3nPN&Pk&JK89NWkbGWeNQ{zZ5-rkm8l^{pB>h%FppW^=xrfO@dF08833h zMTYJao<#(cEX7$?0^4-i@w#W3Qf*XT(n>|oZI56s)6%0Da5+V{RrL&O)qE)BGLNB) zFTDH`1SQPoWSL|sy!RDj^EqA2Yo#aVYl;Qz-xk1xYkI2+){!x+!)KsI9#!tXQMsV4 z=ZK5a1t`~9bU4AQu~oujz1uZxLC$R{0?Zb3!8p$*=xv*2m~!Z`(}Q7kY!L0g$;);H zK|Iz}b>*~v+bsWT5JNlC#!!`1tT76^DBWBXJQLKPJuC zOVC1oCuo_?G42_B5{(6Ukn}vgs?(Z$@CSQG+T2PF`^>*s$^b2duy+y$m?`%gjxv;E zxF@vX5hzKgc?Ajsza7b5t*n)ub6dNA{W!Z10QDXl|cwb--7tv19^aV!49R*uWB$Wfx%e;9x*z|=qS zVWrpsg0?sY<}EC?W2QELy6L&i}NGwuFv)n!Pbcz5}LZS zXA!9<+CgFxj4QtNO?`u`S4SO2V^!5|OAXJew0aNrChia^)28-NQblwz4u-5AE7qp^ zV1jM%MfCuGnz-Z-7SbXxZ*qO&Iz$6irBQ;QjTpi$k;IFE8wD7fTMmEU!d{lf@t388 zGnl1u97ako8(xH2oDB0w8Ro;wFjq3<%(05A09YknWR zCNAOTN=Mxn?z@qq _&`n#aVve`^_zXKCCN;Q@en_5>8ODxc<~9YVYP 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 diff --git a/Moose Test Missions/EVT - Event Handling/EVT-102 - OnEventTakeoff Example/EVT-102 - OnEventTakeoff Example.lua b/Moose Test Missions/EVT - Event Handling/EVT-102 - UNIT OnEventTakeoff Example/EVT-102 - UNIT OnEventTakeoff Example.lua similarity index 91% rename from Moose Test Missions/EVT - Event Handling/EVT-102 - OnEventTakeoff Example/EVT-102 - OnEventTakeoff Example.lua rename to Moose Test Missions/EVT - Event Handling/EVT-102 - UNIT OnEventTakeoff Example/EVT-102 - UNIT OnEventTakeoff Example.lua index 4b36e09ec..21d8204c5 100644 --- a/Moose Test Missions/EVT - Event Handling/EVT-102 - OnEventTakeoff Example/EVT-102 - OnEventTakeoff Example.lua +++ b/Moose Test Missions/EVT - Event Handling/EVT-102 - UNIT OnEventTakeoff Example/EVT-102 - UNIT OnEventTakeoff Example.lua @@ -1,7 +1,7 @@ --- --- Name: EVT-102 - OnEventTakeoff Example +-- Name: EVT-102 - UNIT OnEventTakeoff Example -- Author: FlightControl --- Date Created: 7 February 2017 +-- Date Created: 7 Feb 2017 -- -- # Situation: -- diff --git a/Moose Test Missions/EVT - Event Handling/EVT-102 - UNIT OnEventTakeoff Example/EVT-102 - UNIT OnEventTakeoff Example.miz b/Moose Test Missions/EVT - Event Handling/EVT-102 - UNIT OnEventTakeoff Example/EVT-102 - UNIT OnEventTakeoff Example.miz new file mode 100644 index 0000000000000000000000000000000000000000..1ab1cfd5666cce75b2bb17811d5e7f32aed394f6 GIT binary patch literal 238278 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"QlVrM+O)q`qbUT*gZ%ewjfHk)0ON1jkRztI2 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

#zSpT#@ddPL}FeMayLJ3Q<5F=-}$R0IO? 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

dQ zasrI%MH0D=oBcPw(I{~g=A8d)eiQ^RJ#Z|g)lJGvy7n;6-qJl;>gDS|hLAGwf42pw z!tM?j>ke4UN}11*+wP3x+@X+SBfXi?T`Wq>96wK94^*FT&%P}-PO6nSG)UPYmWC?ZzrR4cG*kMU?cYm zyhTEm-)y0I6E4rhjjF!ND%ukt%{B%YW29U8sx~iayx=D7 zy{cND2-|(iu_{D-G|V9_@_QwBk!WFZ@oNgQI-gRot=E^mrHD60PdN0QZa;BKP|j92 z&Ms2?{dIC&OsA4uh+F{3DCTSgg}oL53T?Y+K03n5k8q9GuIH+6u=R4L1?+p50~1++ zGSwI1BkJ>O+J^a9fq#3dq}9-j?rq#l#z-Ykhy(H~=X{Qp{G!0e9Tp$!u<6Dj)_nFm zQp%mCU}K}|?jcRh=ob-JYp%Ou_IMP9TCcTQ?(}p^URTAE)P(>D&^kJna-g$oHY^=M z?Spbk%ww}&Gx-xTy)g*dC)l}(tiibX?(F8@kJJA!6b(@YkNppqGh znHjo0}ZG2c;MJ6NQ2Fiv{#}fbz0KOwkOcTm%Mvq?sp> zyd@-yC;PR+3^6tM+81r42y=}+IBl1e(cZ!0>TzH;mu(E%g;*IpF=XQPILDVItt#>&|qDir= zf==x{C9hhWOG`f7+8a!Mwfb=5Rf#Oi`(_x9{?ZG`K_WzbEy0A1#uWtHMC%6l z#x>>)#(y8+JE-)fNe8HiF5PpzE+92psR0IBy#`=vLBM`P znR73}(&>e&a36EJTTHI%N#(gDB$U-M>CSj?um^J&34p~k4W;qA*Yi#Nq#JMysO6Lb zwAaP(YE(?|>3VTZ7v>4A{6i+`r+lhgaue4ESXc~r{`H}+?GtYs(s8Q3@gdEEThoSy z`f7170-O*(YYi5lRqa0F1s@eOQoZBjU8MAm$_+vND^tAN->PtxttHITa`LUvOz>7P4@x~Ft+{Hmk znBNgns7pOjQrM=e)p>PSmBL!V6)S_MisQ!-D{LtQ8c~%v{R*BYggT0(7f-J-G?!RB z=)Evdz2~)$*)|NZ$-2SYw!vkGwnCJPn|JzU1TEl%O*_#&i{Rt6Z2{iKg!}#u25Fo% z;V5fI@x6?qCVbuCUx%vAc77MwO78LpFAt874-Q{7;Ve~~&^F*c8Tz}$Xau_ME(H*P zvG(%i&a1uM{Z}XZM@>WVn$--*O_MkHn&x)2faJ;H-r*K3ERNabJf4&xJAcoXYw96{ zffy9C!H;kM@ao{ChY#7w-`^h{{_wgNW;3DO>mwjsdSUPOeD@LYy*9BN-%pGA#FV)V zpY;@uQtKu6(MjY7uR5biezc#r?JwF{u#y~_@2V?25z2|b6fkFm&1tOKKd>bgx~Y=W z6+FD3in1SV=4x<$#zRMFOp%YEJRs^1oU)aqLuRA;jB%N(oF8(9{9Na{6oYEeUrXov zZ?bHHc@5}`%_53mb*;McCMSiMSG^}gxqtA53B zQ*_!1agww&>q`NOtgN~%v%#*D0InU$-BFJ!-PWO6E79HC(6H$wD(v-HnhzKC$#rV_ z#KNU|Jf8PGl{ng^RI2IIfC#%K;g0I2IWVxd%`8GJydrLuR?B0t3puhBBDZtnMJ9hn zERRqFRaHz|uzV`1v-jqs(N%RpwP?g%u&smg1l^CJ$t}J_Rji$fF9FY;@8#rU{hk<&%o<~*>T}N2o02TG! zsq#TNf{WJ2BH zD;o=D`I|2_zEopb+{TfLWwx7o$0-)K@|qHydY35`Hy)NQV=vZi;aI0`|Bjkt_)#A; zx|Mb_2W?{O{2<9aXeBm!LHit{%6Zj`vUe zJKrB_Wjhg@mPd6LbKBI<+H&mgs&B zA`9?S?6?(?Xo@2cl6rgz-|P}Wxa}eZTEqy%R+}JTd0YC7g#-8q-R$t+lCdqZ7EWlD z2N}y{s2v%cI;Ic4VrP6PFTK?HZtildI%S+5Y;-t)!EFxI5kz~j_*u$DGfy{=kRlIp zCP_7v^wE;akZ1Pr>Hpr}6)CBbUO!m{^PH8s2VCWv>vZk2A>Vzp`J=CGviBSQ&nAD5 zcgjg_-tpEipJw(&^%+0MBRZ&gqjJlp%}&;m{iFw^Hj_Xvugv?iV#?{( z*Bk%mUWHO~=$`zYd8zN6#*eN^(-BZr8t15%!03!w$;VY{l{vb^b_Gkn;N8G`lnII2XT{?S{ zg`F$A1?JBKS0#OeKl6vW{Oe8;F@`gPC%J^MDJt4S6Y!G`Mg8!U4=!TeLxF-4{B~{7 z-a)1`Be56<-x#rca7vgsa`km=7c2~DX5d;5K?O(9lpMxO4sR6h`(D57K?9@p zo9X0v3P>bhIZuV&FFn|Hf8ejIo;XO|1Rr-zl&iHt8wr!l^G>^=;kkmIHx2F&R#`oz zLqs`~QnM<$t{;~8&wtM|vkr3eN?KqddAzSED_u2_wg;2UzwiQkG2hN)^Z{)k`=<4UjS zGg?)sOl2I@LAitC#sigCA@FtKd4M3A`xR&=c_>|JGkM{fKrc`{bsMdIR+p7h>2#s5ibmCim`1Amjq0VjKNt_6 zn2xF|u%=q+LhGxNws04g??o}alJbwVLH$B;4yLHLb8z(f#m=k!U)jC}C_DLdGEB!A z_Y+iqzW?H2_we<}{?V^e{dqRb`vs6CS|39*UhSQt`}tR{k7uc%FKGOuEsvicy#6(@ z|HFjwm-!@K_2lS>-IE`VWDt~FUbx4)6eG&EM|G!i`%I&h6tDgQ^;-jZ;0cB(Z&bFS zs%jqT-KsmfjMel?FYVNHt?5@m1gd+y57Mq8+!=nVQ{}ZHzgVy?m>Wa9v!WPg^o|W0 z1LutpW2$FZzl93w&57(9S`pps)Q~DvN4>sVD5YNgP`}rOf)uOzx?n3W_i=E6${%U# zw139HPw`dJZq2;b=zaX?N8;21LrbgkqKaTk2m=uk3h4RMd{ z6+JDr%wd|`v7@7fr^q53_=|ju)WiqPVooJfxY%nFF8<0|v*e?Nn?|48sWjTNR>)0d zMFpfCFEjDNiF9k`{#S5dC3sfDFRmXTN@ula3Hod8TGj1?6UX~rvEF)JZ0s(zSglzd ztXkVouLKo^rk{RT&%Y_Y*X_Y#_jxH27OnX95MW)iKR6)u8-%U%sWlLvTRWXk5fwxdlYP}gfM1oyvHE7-|Z}O(}IWZ>DBg&NySCuEQ(zKo*vG_OGPF<1_ z%>9VBQ;c*|bJ3OOFzLYOY?zMU;5(%9Eb2{_sHP5kjXIRR1SEhTK2&OBy^(c^{=pa7 z+l=DNRr|-?WaYS@jkD?1ia=^Su6uoWd~kAb_^ST6?sdV}oM}zz>XD;6fBor`wmS?b zT7=Nz<2-NBZI1HlPwzZXji-0*59~C|;IPi0t{WcKRp)%^!#WQ{&0$^bJXH_wqTqsq zJFm{Y9^8$xw46^HkL~KmwkRN}0=(B6?!u$N8rb#cPP#jN?o{Ovb?&4K7m_WPW4o%3dm?idV>Ko$~PxbY9w}tBK)elu)U1;gF9X3Pt*ZkdC?M(u^EDFR| zgkx0#Wq5&Y9xx@4Z{u2KFwTX*Q5(MrzaeVGEgO4+ilF&P`3$l6y!Z<_zAbMxz}Qg9 z$-|=F`#|JjpS|}rm9JuzKe#|ZYvolisAq00UXHO*%R6{=a{9OZ-3@b&`kON}1#Q2^ zZXYP@esiWKjQ*IMZHIF}Q1T4}=@(^3y0L&&M@;hOUP7hD^>WqN?n7<64+gt3AGgM?!9-9L zu3zV_(rwLz%{nsyR#N>M`sY~BpY5J>a|3h%U)_wO(%~$h&0SDHWOnX`E%tKu@zMmh z==C0WVn=%Q`Qq~t;ZRI@?kdY^vCh!DYM%O}CVtsN9;j;a2&HGmTl8H{=i_(j)omG} z6673q8}06{IpCn&!ETPIkKaxAcKfi-l=~eQA(gOtY-K zHP4ObqqA&kI4A6>K}ODGfO=V?J*2hd$zL8Pk3v{c)6BH7Ba4XH z=Z0q#M$1@eM(kDCJ}=FAahgj&ipt;KrR1`2v*|P+3^K}cPWfc%7=kj*-nCo~k-2*r zJl@Ht%gEjcZIcDEapC<=cdatR`uG9k9Vy`tH3mAwyxUp z8g867fYaZM9$nt$E)&AOcb@ox0tr7zI=o3tGfpd>t@;sajZpz6y27M{KA}Jg{yodydzo5%%WOWS z=UNi`1Dg)aA2ud~K=oo>5|;%T=w7LK4tz=$))i*kmgkBw^k_hdwiAs-}s* z8<8(uhWS9UyEZJBFbR6s4z!!x<>eEJOE5{gP2=#QkqpAiN*lqhpYB&L}Sqv5IkHMOp5jc@4x_+-7#i+`~;wt~QE~)p}v-|C=uaQ+x5k(+QJrz`@}a zmVZX}{poh>Yj1Q2A>E}RgN3`}f-v1*)2yH6ZyN{u`78~0ohQbO;YdI8B~u+ta1Uwm z)x_#zLYhuN1^Ke1Zo8yDBep-nw-tVGI|3qo!yN%BbcXMGe&OOjkY4i?X7u;R#)S3F zHJf3cjb~rD_lDeSUrHe|KG=P6u>a~L;lKE&YLd zeabC7t6Ex-n$Vkb_Jr!oa{HrbRKGu6N?4q3l(n#MTO=*^UQLVMg06jJEpiLG_QdEe zZ$U3Ys9jQH4W-phXs<#2wP>$eL`fd8nsu4o=TP*(ThjRP>!>=*)$#cmt8d=6!S^? zHqE67AC|eyhLhx~nB$-)(`=GXxlQS^D3+N8Aj>#SL`EW+%$X?9DD`KucaU`8#143? z8>I?f?Y_w0!Vl;$1CGq5q$Ad@n%?VqdC70?7SphzJj;142$I6-UO**WWS8_n^2l(M zL=w9U@YzS+==@A=x^WlelxowZMmq;b-GnpMCj2LDi~L0>xt{_VLh=B0@E8$f!7%T0 zm>tIm1A8K1A)cO5i<})I2$Z+!Fdx`0e@Z)gvXP8HkS68)YyjF|NxY3C!hvrk<|R@7 zoABKq9~JYNVE`v+A3S(~Jc>)wYvpk${U=%sXuRR&?+qk-pY`YR3@ue>THv1)->CNK z&ZGdq&AWgZ%s)`WdTd6}@qH z6&|t9#`5O$S!O#h-P$^m!$3wjP^5eY^9w~FbAauKuyzdWwY5~!Zf(s^rNE*lB5RP1 zcV@G+|3#d{%TnO_;MJR$oF}ncT%XGrZKmf|>a_PjF8=bRxK13z2>ymE6w%eQa z`m1faqNj-igs}_vo_1aPr^VZx}$Q(D> z0AE1rh}b%i`=hgT`bMMs62oaL>w>Ii3KylgVc^F!HxHGY?1NU3#buijU^-`OJI9R6 zNpUhw$0h0!h!cye)r1Qf4_kraQ8DBgw?wl(7j#ng1u~76{{)N+miqbhteB4d!S|XO z{n^g$X)UuS-y~)PCuSj{9%@cmaG;XDrE|S?#8o z>Wwg;7sG)@7XOZsf#vC_K*z&kh(;H;xDwcj36zy}+n-NKRNL=W12dB1qq+ANFy)42 zTsHdE1iurhW?D)B4X3CjVTyW+UMj}J1q&Sk?aY2I#!d{sFjlfhFq+!$K4Q%WKOer@ zKRw#nJNV&vfzUjhp5?RMeA-uHp1$3VXpD{MHMr36WCrhHw8mzKBIBz#X{dUFqrIVM zqghaeR@%wi%B}={WeX(vQFcDhhFa{}FHQp(XP`p>aX>MDgGT+Uf}j4QfVBniQFW+_ zpd+Iy?Mvc&bW zdH=nr)uM@57zMGqLvjxvpxvVR$B8&8@C7)>0`-8$BqO1+v(JqF3s#y|6|3s6r$~ae z^xyM*Sxxe)idAP`(xnS%US{L6n3k)HG|mM|qu@y@*pEBA$4>i#g*;8qXW}eG`xi}v zgldrvaz4#7u>G${j+9A18Q@yc$D=cRs+Dn&;gN?S!_y1yVpZ$A}b&u4qqkZmuS;$-Bf7Auc_g z=c1oY(Fg2%5y)RfYmQcGnh(WYL$%Jb%k*tt%%{pTSFW>ZD>B)C0^RS7=kL;9UT0m)7m?X$GDKIG1F;(|H)jIyFMI(Wvp|(A z4Y=?aET~yNf(Gc(e3ya_Ot#$Qiii`mmHd9bOeSeTyF@-?b%2&t3n$D=2HT(snvRKI zrFZ*H#;Tpdn}JhUSg@sExJu8pwZ0EwZO!?7FOBjb27^>B6IMR>Xwe__F=oo9TE+pp zfJ{<*uQ;Yj15)i33I8_z)$^=0J_hxofB9p2WsK<8*%Va`Gz+K*=KYG@JswD*>7N3c z9k*!bL3`eLMqt-1 zATncgbh!T`d8nREv+TM>?ZZhnMmNOu;;PKhEi3Q8O$V8)%Yq(vxvWnfU*5FF2~gkc^Qh>Hp@_EiAI(}ejASwHVh-6Or2)wqLpTVpS&uL zvXnQv#g$*9=zE*(W#?dKv_SnNnB2y?e}g?e@25kb4`yg}du(uWUu>Xg(SCLUnk><< zW?Ea(D8dQRG{IV+=rqCAQ%`P2p$NK=KZj|8YtPBKI%nAp(%bq6cv{fW(t7jgR5nVIW~5m@9+KaV*iLyagi$Vl*Cmk8e^p7@Qa>$<-lV`v6JFQ&kLqM zd^X5HeBgf8&D1Hqw_<8_!a$)o4X^c7hO2d94h>&r53U{SLWdXMMb~Bg>WYuIy5d^{ z#=oi=ue3O@j^@rPfIOZ0W;&*DC{4glsY6$OAQxCnx$DT&6&&nC!I`Ve%>4sA8vd(U z`q<9{MC0FSY5NHE>UaT$MF{@(Tma=vs_48cBuxWT+oZq4rF1YJWbeCFw3p6O3{{$> z`Lygf@TA+`d!Zk5S=)zuE5Az=QnetoZ zJqVfZMSdQAWHKh!m*zN~@Su_BaIz2+0Q5MV?b|NEOL_Fl@L zV5F|?BOzz7%G7$ox^43GxR5Qg#9kMZ3B{nvY3GUcs>F|j+pVd%w$)Zq0-Sak9mJ+N zf!#gqvb3`jpqL(zsCrwIV&YH?)StzX=ophnuPhRAxDnZy*!ehnKjEXuX;y*;Q}!&bGRGP2u0r9Ei2Hh&T3ag0MUYVu1|Pa%Nm*R30SdgJEM( zApZLR|5GPKypzQa4YzCY@A@O@I=s6UxvqywMe^JlU{z!t&p_w1GjB}Ru%L%MLn%*S zq}*Tvb=+h&)FX^lWtrSai@rim&2*%u4YS);lO3t&QDi%qd`oeZs2{5oy7v5SVR0lU z+szN9ms(>XiyCp^L8^?VihtFCw=DO|P}NRa>9%G^jnu3%tltY4Rz4fYImmDP{m_+Q zy@y>cBdV0HZo)^~+ZG=yxr0SgB^%bFm2(|qp#ZrG4~1ijGf~S>j9anI4gr*sGTAZk z`ALAV_Vpd`3yiAcI;`Q0vX&8`dB6imrpolg$Hg;Wx~VVyDP_wZ_d_g1KL$+%s6)AP z;>ifCFN21$-$|T4V0Ysfdm!F5oI!((F-7Z$pJkXr8a^=b8t&7rr`yA~qab(LKN_2V zgGQVKJcu4A__XmkV#O<5=^8Z(l|?MaOvUn1RD^AD;PS_GK0_uuPeHw_WvR69#Dy8A zuvNEFRQQ-jc_J9L;*EU{p)EONh$xr#D~it@^1)IzmH^OGAI!O5@EN(B$_DJ0_dqR* z`e{E~Mv29TqdX-xC_gFu(uK#Qb?S%sHXNHW$({$I~myXPT@$x3^a)3#kbrAVFy3;N2CMaMg85JWDugOgX7&#uHn7+&wFajZ(2RT}gU03a<$95T?8AmbY^hi#2j{oC0 zd3Jb2SKT#rj-6v<;RWPYlegF_$vwDKNdCS>*K7FpMi%IDBG)-Oke*#p>0X}Zvl01N z&&^d|x;Ft5jjkVH7&_nH0Rc!!U~K%mIX1QTEii6<1pJ4elVLuhpnGNr69%z|LbOP#q>V8(WX)>70Wtt3Zak@iLi;1~gBzL7<_qBwx&!w41 z%VLBR+@L1q>ycZvHzSE_esJ!6plnkG$QM}m=)s4dm-)q|NGO{Qp`KY}@ckkDw{&=# zNgXr~MzFE96jp}&8wgLA^VAHM>A4zebT7Bk;{}R{Fj3bzx>5(eG=zbGc9BgdAd{pw z6h2ni*g;U^T;2nk(dmz^P4QeLl~B?dxg@w-@q~_!m1?4b6%rpSF6+gL$+Z*oOm=a! z>tGuE9wljobmOJ4Fg>->d~5(Ix$|sJkJ?Bxa(ua6xTCk^uG(L_-p18+bJ%r~r)!r| zH=N^Eactdt>fCBi`=Q_Pv9>t}1=!^ptJ! zs_0_bf49MqnU9i~+MsJYwu-^Bl5gL7bY$H{YeBK>AYKaO>1~=T5Udr0BQF@8nEJ!NPEAYNpF<-2hFILPKD`q(>=8GPE7kYF+ zOO2!K0g9d!-r>?al0a2#H^P)6n8JlF`cfts$^<5JTSl+DCC}3`$Ps;JZp)J)v(^m%uU7LC<5n$_@xq~d&5ti;scjmM84H>u*@xX4(rH1~?u z*K2G9?EcB)f8G4oKRx-&#zwOyk97?;mGGYZAlkE~cD+=R&t&ydO=s%9Reo#wL!d+C6 zFKX&ts;OU8t7g?ol++H%tmSo?n`=;a)}QD~xWUU{WFyyFl9W8`+V5^Fx1SP`&fAQf z`y5k^RI64V)CR>!fr{xxs<=_(vGq)n8vM7VqtMbPuIB`FsnA4EqlEM|_yOZyKxr6v z>`7sC{8gSX)o!?0S2QMvexBtQmwcN7v{u-(_=8Zz3_`KKY7`IbRZ@Y7 zU6l!n9*z`HQ%KRzEL=b7J;FaoUt4P1BhKeFtXJ9 zpBeyeK#{-Ptd<7Rma+fch*Yhi>y9di*KQ$hJVWu1fs-su#)JVs|24^gfcfFBK2{``k@BH{C#LcaHnci)3PX4LsTnt5NZF z+hzq+%@(@oEjGu8cm5K?Qf%iv*hdEguvOcoosUfrdJ`w!h>7&86wBmHQZ$XtQzzMc z+)bW5#;V@acj0;Qjb7HdH_w)j2-v$OFl<(^mt82 zi&ExI`2Z8$m5JnyPUQJP*s;a)GDj~GIUvrmeu{x?l4dvA2f<%VDYyIh;mqV?^Kc#j zzhcHnyl7v3am#(~Zn-aq=3frYzj!165Z=h(3AsOpS27q>#5)<(_~NB(3};ky`r@r@ zd(;_uZ*Zx=!X19w2y}}h%+{O8-}YZCPrv9(U-YFf`qCGD>5IPf=}vDs#IR$8@uyUn z?y}43NiGkp{ydk<7wJp9>2VlVJ{H+$=IsRaFZ zA^|L(%XuD=f7}qO>&uo!*OukL)(oiN#hLHUrtWr6^WM?PySxNn@Tu%VZ*`5Ax}0GS zu%!iVxkX~RvvS)^F%vyhOtWd&2xc8WKI~TcXQ3%;2qKvcT}*FS0SZq{ zZp(xiQus27GA5p|=oirKAOD^_*0=g9{L3Rr?2)$KW|G7oi%`iG1f!v7*+{WEezfP| z*H*OAQH)J$dcPQQxN%a0L?xG+7mT~`wDKlTTDMx3yI_9wEPw=m(dxpdem~V4n$_6^A~=+kd^xlrbTzjG%;6#eMNNQfxg$o$ieXJvCwf!WoLIJy zNlSB?&uML6vA3^?)*ImR`t}f?;xK2H2B>cYV1J`*OxDF2R0y&$vk?TJ@-gP#wip7W zd`j@?K#MRsi1&eJ2+(I})=@3e1;+&}9niiwbq-V3JE=YiS2SnP;SmObZQ%yGnb3b; z2S>y*FpuaJ!7miK7>s z9arVh+RJ9CPFPoq+aZo@ik_zizY#olEgIc${_VNE>gZq z2q)|7+ItxXlDI}ycX!IsFsAKm`c)^dazo0aXDv0O!-zJBRSqKyZ)rd7mwgN?_+c2;Y|GkpAh$= zl%DjyP1XdYK^zcny5%*SP4lz)jJ7sT1AapOY$|q%!M%FC4s-k9d1=Olp79Ihu2P?; zF+K)~d)VuFVJ@}TcO%o=^@e`;%E012m9`)fy`nAf_s>ULP~{cA^RF40YBM$9ZI{~K znZ<~jhq}rbR~N-EUrv+as{eA zR3D9sUDzN(42GCyyU@5ud?)G`iy4&6R3yJUVTJ|DpyaowGAmaeVg}R&TD^ zkgx7$_b-i{q00sAbB!1rqv9%_rK+AHOlonv=KNn!J8i z@27`HulAE2*e_(;z-)BKk1Dyo?RnidiJ9UP6K64^r}07?>k-)xY4U7mmmi*+qRC3t z@DqqusjY)RI@vgbl@ei`RgU(VOZ1C1eBb2sIt9i^4b!V^x^5cLwWuLlvb#IS>o%;# zsNNFylJ;kOw^#7$8CPyy6iLODbY`CkOQJE~tp``-coeOn;l2546yGNf-D@zHBpd@H zs4m8Qa-e?)L00B7VjB-R!soq*`Pd6I+QIRZCMEK~)Sw3u#ljYL%g6ZYKx+1W37K*+ zT1GsAaVamo-=h@A{ZUrNXlAo6MbmQIe6s&)eP{RN6!w9W z{n&O=yL+s%ZNv|u;tC|C-pZCGJ&tj^!TyMQn*89*R2~}{W*5%KB(j0zT526c7L1bg zK=Wdn4er1{Wa4aFIDmX9|9YB3ebD(p>On;w4&^4|uv@yWXX>crjTBZ)IT8g>XNaM! z{JzLKC6$MdrsXONew89Jm1kzjlgenmqlixAAVZ%CKkI_mg7N<7JqgeKF}+Hi+PE2ub~)hjr_cYf*K5? zPSV8(u*ehQf?^!TmehBm1V;r@S}T&!U(%;bzU$>>I)TsVg1E1XxdgjjpoIRBql$0j zGm=K@U&N6XN4^i{RwAhls5-H9Fs5Lus@@i5|C-aQ zGHij>wUINqy6(~v)L%Ti2D!%P@zwz6Is2?07W-Ywye}92zFheGa^dgGg}*Ovqg9vj zm$%XW3Ep(LZ>2xx!!?@1Z~R!=J)q&`-(&)KYEq{u~VlDb#fT)E!suD4fWN6m z-vIJv%6${SM*F304xl@-4Ls|>+YB+X6J$C1$$s1cv5M}YbG*1W0IALzZigyPrp4Qw z?amR$g6goq^b9o+2^B3@KZ86%O7PRjI_<=;m*bQu63J|e-|sq_S%BVCICJylOZKP8 z=@e07Q$A0&7iNp2P>N-cwQC;LdN9+{kbH2}^ zZ|p}b*PHHQb)>l81Et$s_Gmnsxf|{K&bLh$i`}?u^2dL+&9gBa+&!liz4;Gyw^lk2S)Mge&RBoKdwA$Fut3|^Aes7gpiz?qT zb6{gKA%jgNM`*VO)0?F;u7QTb@Sw+^&GlK{8L0+j6(Q`%Oh~G$aW2 zOwVV<)b>X`#0RARIM@@HsqRQ8Dt3^*XvNPaS(;b-C_;KuIJp*J`_=UkGOW4 zjO5iyni5jF)VFLjLA_>~w^C;;$pM;q4|6BsZ<0CAJ|?OxylMV;wk8TMb$a~SYOgSmteSS=bJ86B z@g!N*N!?vjs{RZm67Kc*-G1r44^C)B!1|T0qLr@vN;CFgtegOKP&84=ieD!+=i807 zyhrXMMW-;VH(p4vt1y`sU?tA-Oj#_yth^nkZ&!YWFD5kLmz5GIlhL=w_FIafJHfZN zv5(~}%ck^=bAny-_TUlxr0~goQuNdgra!AqOn+WLP^jU^`3x4(;afD|E0yBZGzB%Z zgyPSKk}R?89Cs~!LmB0ZM*3Wlq)K%0qP?ly`ezVWn|CCzXbAQ4YV+pu3R^D{S5)Ui zAc=;vSgor{ty8lMpY9wT27>B~$h*DB`=CGbtVCpC{X~2bl%H5oT6^6{$I#IfQ@WQ| zvA-{>N;cu>EA2NEZXE~S7is#lr(OqH&x|C45_$hx8sIglptify6igLT2A$tM_-9!5 z23a=QEhbkXa;BZ=@pq-&JHQ58iY20vox;%E*N&jEzV5#D%4~+1bX4oEhoUqhF9Loz z#^myIsJ3N(`E9GPyMVuj2zWm+VE=#i-nF}}8%Y%X>{;vlhaGzLm{KT4vg0JJGwNhmig6(rU;pZa{sBF=T75(1Tp2>@w40iKQdp zyKDflwsIB_+jFt&oV>x6%R~)taSiWr&BU6xV52~0L9sqfA8sb!BwO9@^l?=tFzD}q zdMwk`P2Mx0N6SkSTQe!oeI~P?#alRaH5YN^7t_qaC`&Q6@Y*db!=jm$B|bT;Kx8r1 z!uf>ajaUjX8PyqWx>!en*c0y~Tu-l)CFGD&Nu){^S&HY&?66oET;qVxPm{m>#wm2~ zLjcQOfB)mB=GWRB&Kn1ho4+Fa3&)C_oadKU@LDfAgk-x~ej<)a?-hXkO7V%tHpqEA zZFjI`m&Ir$`zF5sA-%ks==`9szXefOKVXae94ItT<|qk&`?%v4z@EdB7Njd|d8M>X zY&pb=@T>4WgDT-o!>eHy{H<2%Q(eEFy%n_NOoG`^ZRfV&7rOTbztp`f1c0lH-CKn% z{fMFK(Y@u&n~Xcn{Xq|#KYsXq_lK>`$3OhA_5JtXKJw9Oa(UAvAA052G=y&-Z7DDh z|NQ+U_5ZvoAF#^C<8L>=>puKp^U=58ZvOD-&t9czddbVNHy+OaGrNBK)72cFt=>L< z4Z-9I?+ij{pF=XlX0@y%ai_aHogM) z7c5yqo5O;|UQQ5PsRQ>bLyXHCT&$b74WQt9W(tfF4izLV`l`J%Lu)pklKP|}xc-Tr z>P+_Jy@T30I##km0`=_yqHgF-kgE|mH|AbiQNoZ(ZE>`Bw5|GopU)_A$ECW(=OY~s zOaYmVvv+86s|VEWmfotLkyb_<2)N>1+}FreBbMsB-tP4VJ&1nSFkBQ^c;L$U|QZru^+9Ks|zkwY( zURCt4J|@FSS}bL1!B?0k+4*>CFD9&bLIWFCXW@!oU6YwGM&_f)DtL2}jmAZ?S(2`D z6!Z`SoIZwO0zv&6@OVGYlr|95DA$0+u$_vpHPkoGI9S>% zUf-wxe!cak>Jg?6^qPfRs-hHF;r-y3CW$dse7w0SOAK0LV}D~GR-JEOCyL%Ly0o*k z>q4@gY;3tZvJC2V1nTvYP{qK_pXLuf#|XB&SF+hGpS9KXM3W+*4I_*psW=zA?WCnd zm2j81`ziVugEE|3hoN#A0ob@!q#4(jg0p5{3t+%4Nz&rLM64Q2udcw$$T$v7 z^PmB6VbmaYJa~BgEXetj0#&_B$83-MZ(jPPg`qN3XihC~(4*xT#>PmWhOdOmpphBqgYPKERbzJOp!_cReQA?zgX?9evf`LIJeo@5P+#KW!u)&23H|El)_0?cp} z(4aR>O13-w#r9|F)OUDvJUG~`?vd!x_D8>MZ(LsA*GX@$cT&|8lC$jyf7*Viw7wgx zS2Z6D@6Z+-L(B!r8qDOU%6SlV+CJ#F?S;$h`=N@mC;7kP;U_Rn*gh}ae1>E@0abOp zRqwcK({Yg4@h4n+%EZ~kK|ur5!$oBRZN}eYhszbEfgKcjXdUy#L|LN`kPUk29t*gF{XY2}J{B;?Xj7i}b-*YsA zlcRmes|yLA)npJU%OwF`?YXK7vDrb<8!PGszQ?~QijniFe=)vluQ?Y7{pLHv%k;05$HjQbmljtstOlZLDHd#+M*$$`!Nqw~K5}(`-Xr3;GjY|GVQ+ zSIxZReSMcs;B)t<{oe7>;oIj&FZT5QhpUtA)R4|H{ZgYy*IdxJ_Xw5N^?jHE^ssCC zUe$9z?M+QWq6~k>9Db4)yEXJNi4 z^aeXR-v8a$N=@H)##F+7?}ViYy>)&Cp%Wm&)b*XP6^c&wbe4UVjODJF2F*G-Q`5d< zzG3nEg>!iF|I?HGyV^(UK!*G(cPsJ_4)c?YtkC{d_70)^Fi)~~DcZ`Id)546mZc5j zZrpe`35@@%?WXTnEH&;-rdwBF#f4!rWUiUv_!^J#X!z2ors~%&I;#XlyKF!{n>DpNy z=4c0;a*|gK^I$rjkJAa*xQ>fNQ5>WZMld=|WW;n5s-Eok2fdTQ{@zzhSpSm&p6vr8aKBb z4mWIftgiGBH`?77uk~MVx)_3(TrBbL!t25grCIaWPU|w z>3BM76E!6*!$8AQ?S?ak+?Gl7mrpuHm(l&e-K%!jXF#fSq9yzTAh{} zFo0QWxrgrU$B-~ceI(e(%Z5nF8gA4h+Oem7;MT0AZ+Aj=4OvEB^GF50u7wWDI{uoV zbU`6fM@VN-;fe)T62z!mdQ;#lV=Ag?rq-eJ<2qlUxPa0aco$p2)5zfv`QM1S?wRIE zo$iQadNT!|1Do9 zki3i^YM5v6C^bfjbnb;{I5W=w!HMH&C*X zY#C=l+IO=Ij(PulGf@XaKTS`7lv}Z{<0Beo!2Ys+8+QT9#rx!CRzSACq<4H!Y~bcp zo8dBtNQyco0BvOC*wWt`|7UAW-5fCm*?p|Q9@!m5w(|@~ui`dKQL%Pz^Uv^;*dQo# z)O)fW=1*#&FBFR;MF%Fkpp1|?YDJ1|07yY()|37MEg#T1&ejA(b#;~f=>?|4!dnoP z8%6{bk)Bxt;D7+J(?EGnBck(?st9v^UF&5!g@po8Ek*A0tfl#pf!sC2Utd3Xd3$GVYdVVHm2=YhL3)lf4cI^MeqGuwyR`}P_yLMNG}aM4cGX~3C@i-hE`HS1m~qIf{>{<34w zc5q@AGxq~1C+%2!Q_7|Xh(Cu;ewpU)Db#^NuB0%S2hO6!(`ZGvYu7`89zU+NCBIlx z@N0#*UbGftjJNxo`<}lDGmzZK21MwdLS&blq>)lA}}HxPc&p zi9bzDox+~M&)fpD^@llxKAc-HVl;)CPOe6w1cU-*?Ds$jkS`O$lB8iGuK5#(6&vZ4p#?az(#frc` zH=o{aM zY&r2U+!xq`Dglk|yVM@#A!60ls&{ZSJvuwX(`P_LVRXNX4*cjzQR-qw! z6cj2{r^4|h!vI&1Rd{}ZPjX6D#CQVjiMQ$0=tfGMT3^R~POG1N zxSFZ^rdnr3d#C843(*zN(qU%t^Y#QU%%itu4abm;dgwRB|9P%NzEhHJ>~zJz9&T=Z zzp?pfWApoMRza?>)9q_t-Sg+|1pm{a{}_e!nwmrklj$bqU|vl_K%%8ThOLc<-`d8O z_~2C>{|I}nYu#S9h3YNdBqX#~gXc#leG0Vz$N0gMSMUVim-GfQ3?4_E1hm*rT#|Ub zzP=B;^6Gkh{Z0E~KEEoqzxhTTPK(Q;o2BQVELnVWnc*AK)y36c7du;9Ti<@arS4E_ z4DiZ*o=(@-x078E-4!b{uaC1#Nw%{Lh#F5jjm@>LJHFmRafn$kvi0?+lkxe5wrhYc zC?|UI@5%8jKcA(Smx|_Xqrb6=1Z6M!k|OBD0i;!;p3F`SkbvS=0~QE ztHgxsv4r3!Z*gIxqRoEWvE@)I6?Mn~bjs>>@@@GW5kB#H%95^L3cLo{bHP<-nQKwx zHr~D666u9@SEw6X9YfvSV2$X7$G~#lmn0T-4G6qKG3-#a>yC@IiqrflM)DHj>aa&F z3YLxsZQD7o76#8+Lqv0nsX2^VqXxV2y%~kG6hi5e?9K0v5F>_bZWW{#?FvgNBfK=E zJ~xYLjQo76^Ux65vY7`;YR?<9tLRCi%iFs4(ZMtdY&9&mYwBcP@V(3=<2%laF%10C zM!?K>I|?L!PbGNHCmon=F>YVv4Lv-IPm{sA1n__!1rnMK2G=yBj_ z;s7XH7I*x(B5DZ1BqC$-h#K@<1Ao|Kp)x~t>Ws9!D2Rlst%pI0{eU?@!nN60$u6v% zkkG>x4o^o)kO+xJLXs8AZ73}bIrV)zN?B-7y5lzk_6K53J$Vj&l+b}Wr^tHEigdQCo6g?=2Q9}tY75pT+{^cA*@M7*(9&_VjA5hh% z=wv$m*TsRkK#L{5katLcfRmLogQXV>xf#+x7V-CYeBP6yKqBti%e09|*5OW0uR zni0Apsyi5egPKN>9jeQgl_(5$cyi;?Zm_}T17Fca1E(-bOo_!+4XyM zcrftPJ*e(vdp{W1A(`7t;{>7b*Rq>upU{zRKb|>4H)*8T2!=pPNBWcLEi!zvvZLPo zT!ESo0L=T?b`S*;r58&5E`}3zn=_=-kyS&Z=UWN<09or^VDz2_%#f5b+QcELhiw7H zr6==!SzTGG2xSVS&xmk%G@5p*3C80 z;F%XcLJs&b+gq(7sE$u~d>f&Bv2n0yAb`(yqhIJ4nUFqc? zdiB=R4#Yo2uyW)Gf$t4gFWmQJ>OHxGW$e_$$Cu&h?N?NG5?xt#vHX1&Saj~Kb1MGNbWSA+fCbfPx)xw%E3jc3K&N<6nx@C|~ca z#vhIw+D(?!W+QI2oB&umUg`DZWq?Yi*aatiU6=0*|V3U-D*8(~Uktmf#&fV}AkOBhU=;utk73_$m&8|MhR99Nte43bLg& zMwK2uZ)y%(eev7PXIa-Z7Nubrqf;;oRYInxQ8IZfz2PC+&E>&Z_sUoCz&JJXF6i3@ zw`ovy*Fmhr2RHm5JpI5r9qdKle&kZR?>=_B@kYUx!UV?XMX0`-IrObY4y&uj43#W> zBr*n%^-&I^BALgd@L8CmD#fxT3ZwSw_$fOnHn4x_yi- zmRm@@(Q9D_3u=o${rzW&=ii4*7#-9u?(G*!t@|--(bX-Jgc#d(Sm-X%AA3pkCSA>f zuwBA@mgiTiTNEE^urO5NpJ`dh)?+n`0_Gp9TMP{?SOm&K^3SsbU_P_t0#saIg~zBO zWpyGdMluqaw7sSfK``EyZCnldS1pU!aX2F`lSNehqv@URKhcYkLrDIkTDP-;R6Xlf z+v8~%P&SG7FJx3dO}r<%gsojl49e@sj{ifd73r!9_0a{)XsM(tDWXZLjxeNAj53@8 zLmKQW2)7>)QZr%`bGI*gs-tp5kyiozObLFuA4-yVWES;V)E9zgR0dtLz71=UStzwP z_mv0*MgkpUd}7z;%7!Qw{n*d;tB_`v zB$nkD(?)Za?@4l{Ip=obT>!(;E>GJ_4;TD9>&8p;E(h$WLd!JBm+6P`<>E4V$7zWp zZ)E!rsc4~XYR1wH0)yJ|tr1?zQ+6#2ptg6|;s&5xj1@pQWpeEhbe0yN%UFZ2p7l$! zcl4Kjhi90?ET*AWRdo*LyA~OAKdMUwXY;lvhRYM6On1Nn~$=|D;kzfcT1>hE2#O<(RgN&!ou})-r)FFzu;YtzZ!YoD9g(_YV zrraJYSxG&n5{zheaQMt34g;UD&J}@uG(nx(^Y>Z$3yfZe{V{KD7K><(po=dT#hk4M zG4$2Q1`BY41$MzOfg62PJVj3@0@7T#;ASvK@1!%dO@b&}DY!!oC+c!AinwIj7+%RB zCkspP+#cDmO&y4NhD={a*zkiCET&hGn(;bc%#!Zq_*aCfI3&bhVvkX{HWA9c-}rTW zWrJ#*&hqy~rk_1+W$o zlcV-8>?C6jypF_4Mm2g;>JYnjAP@6=rv5+^BKIoF8ydo1KqL;n>+y={(b7jfW*dQW z!13#F9NJq>Hl)yr?u2@(OWkHBwjDp$AORtnLHGwVCA+3k2V=3PLUUiXCexstZ6!Cd zzg?~E5Z4AS62yVP8MG~UM`ZDy(e2y0uHJo1-kOnR}KkHEyzzE0?LoXmwy(+(sHa=Z)aC;m@|2 zi?7#&rZ?RbZ}?94-Rqh*AA%MiYFbg8L@+m z8K%a$4JKSjlp$;cL61!eusCW?Y+|TSkC-lwwtHpb-p8?Iu%pA#o?a(1R5nOMr)iPh zw}+PqTi?tt?FoeWMody~S3zb5+;LaeBt&DVZwiojis2JQBgN=9yRkm@r=y^U7;uFJ z0u2oeOjLrTg| zu}2-8cwL$~P{8DepCN(*akHBtfRH$WqKGnSVLC9;jvaGHckSC<+1kFlo=J=L+efW$ z2i`w{><@~t`W%DZJV->u6EwE(UP7#rwkND~l2&*9ek;+Q$F`l7s3k1(_ja*3Z9_td z4K{$bcAM3rQJ6;JH_^hIyx~MURogVOLydyhK(O=S>>adoP%pq3|24scBEPHSG0dW6RxAWl*mpP_LJSDh3`t zk?|=LOK_OYX8Ek0v|HjAC?FengBj2R1oZ+SDabaK7-!8&Ke!e2me865>yEsn?ts8HS02b#VlWmAiVn88 zZ8ZvBqrXIECFwghXZJ&R3wAhO2tZyBgVIVX=DBh5SojVeaA@&?rBJ?vF09O?$EZF1 z?u@=M=*|zcVFu<0*?`}rarF+StF>B{(eUE$f9!M__w15S{^a)`S3lvfOPs^Tc1lL~ zBK&Qkp(%8IZBd*k8?gfp-JQ42&Z@)<67D6lyIJ6PP&QM?IqU})e`2`tR3 zX=G`#6)sI3uHs?HYtch7*UPc96grrQE7OT#WS3|*F+6)dX&y22(&u*GEPojJ|B3Wr z=E2F&ABHu{(}!VkW%e))ASq2AhBcPX9mWw-psM&^Oyz~kv`i*1EOHWg2?RCBgb=vL(_QVxN^+4F#1Zeo{kmDnZsT91v6o#$3Yx%$hpf9=QP|NTIW= zP#n-Y7`{9@>hE`bLI7pmS-?Yqj3G1E44&-sDZV-qH5$Vb&|P8l={yzs!G8!*~joI|&DbDLvgbDz~j*xCKB3{&9wI>FJ-AL2x=fIvm53 z$pXgLalbyBsa1i`&ex+E#8e7vpJb!5kIvamN^2aWu^Joe1oe`>hWR9)bw%7~Xhd6- z8o9@6U|Zu$88mleP-7QfNuNaX?^Q2~zs=N!Obe|v*1fW_T^kIIr;F^X0Gqg!25;_X zA3^x>r;G7C3qSuo0}c$R!!8`guc#iLAm9yxR@mOqt({p7Dos-mGZhjy0A6vCQwjhi z7_~izb9R_jIJt&mB~hw#&Cg|p8RcH&LsF~jSnnOuoK;cua&(>B1$;q`SKp{lJ*A0_Qas+0qS zz!E{S1z^iYIFKn3GKUTjFFKvfeWciw z1$N6uM#xM4Slz(EwiErFFz$Qxxum;d10M(V>Kk?;SVb~|?C6)E?v}o6uXy9L5!_{# zf*{>t(aR{G@Z>;{&X~XBF~eS06!u|!iAKxIv0QVN6r>53@@yK+yFm7kr039J>eL zFbs;XETf7cQYy~x;PwOF@r6EH5x&l5h$ju>!CYt7*8%wYdWW*rI|%2LeeE?ZR@)+* z(9fd2tK{ozCkdZPkf)B%1x*x-wVi8SKE7~*N~*EvouMNZ>*Ye52~sw6dZjaYE_q+- z2I1t703g~jO-cK2=PA&xGipIa`cRfoOIi}RbV2PfA{i;LH*#oJuq!{D^!9ou^25PNf8bR3=|uOLP_`?>_wVsb zg-q0i!^ib!1*-chfyKdtH^&UqH@qNy7_95U!qWOD!mm+2VW?Z1kjp?5rd`l;<9u8e zfJnP#KJJ)u#||z`y5&YgqPaIsx>aadIC5JN*OcHcREh4=Xtf$rZ)!QHe>*i-l*n56 zuT5_3VT2My_OdbY@Ezw|z9)y#jNMF9sBibyISf+l=hre9!6-iGg7ne|9&Sp`IQvMl ztxCM9{bJtp z%uqT8mP5x_?wA_`Ne>QQ^t*eZHC$ku{8XJTn%4QfP@{=}tvJV)h{p%XfAhjv|SY0k?Xi|6ZV&)iM*TM24N3rX;(i zua|}pWgSbjkFt~{+FN?6blQ>Y*%Ws3n!>?H#4ph!Xgn855m7$pi?&Nb4)~*FeJE6X zUW_o<5;cr3s9(wGg4&ggEvQ}3$RsUzCwG(O`bYWJm)l>-#}jAsFSUJ?w3ng0VT3)Bcz=P;&&hx6RLBfAI)uHm zIjnf6e5&I&4HF-)vE885?8+Mh>PG)iJw8|I+B+G;6IEo%^LvI=W|}GZ{OBaqju1 zcHOl!=8*{i9u)Z03UTh)y#t2=n@U`g$O4x4W@4541{HY-z;GGMifBNxU^7@(C| zev$yOHbE8u*Jm4tYc#-MK;9xda(5p1-B_J})pArl<=I)Jjz{gvYkW>f=h{cqZbLBN zRm_KOO`mE2FQz!_QZ5=Vz`8t9$zevpbkY&%VP~0Zyf?hahQGilXXz-Nkyj)xAVqBAXP#_6 z?8ZpoYL=aiKLn3+xHL)6DMphLMFGZ>1izkUXZZ{s3m@EnumL9{t*8c47@JiQDM?za z3R_eS0cNG}I38+esSHA?QlD+`H(m2g`7Ey^#c`4S&jO#|m^lD87M3}n@f>&Sn)~96 z(z7}9Mm3-Wr&OtKj>tq&bguSn#DxJWy|)5NzgI%&_e91XqJ)@>s_D~ss0JRV_l(bs zfJG%@OVD>uermwSSg=hmFJYDaSVBuaHg$WKm-$N93IKjurUX5@Cx;z_!-QzA%ib5c(#MC zG?{ttqUZrMqw_19k$tj~8qKq>!9DZ=hIgZU`e1I%?^F$S#Hf?&l1=JP^TphR;A6Rn zyCx?MP-vUx=FDw8`qh#6bGx+ave@;)6Qr~A)l#?ZP7t>sFODF{J$rJFZM+cLt0#gH>1d{Zv1emt{z6Za+K0`zCju) zhe%bOC6B@PIR!uW{PUio=K_UA*D@^S3;=%y2%RD_SuvkM!M$Cqk?d}4BKnzFKW02OHX0Iu#zVuk!vU|u>Wl;e)zh+iq{cH?Ezvw(=s*{rie{ikmp<$mT=PQp`3BcAHp^rK z%?Wxxp5dm<$iu49%0RpS2y{+=F{X)ycI|b7JZY?=5+a@^+aT6YBVL8r8no^?NyIf; z#ULd9b*LzPEEcaZ98lcM*1j9^E!xjLj^COnl%h(&i!7bYFS^NImTKEQ*p~DeUPN!O zkzPsAv`1IFWnPTWKEd07Tr%jhCjWrn<5?S@gi*hrxyFl&={hOa2|C`|g zY?iFR=8cyijLZa#tWe`%SK}=_1vcRhX4H??zgjjWZ(UH67Fjlx@*-NkiawIfZLqk04>qEn8DvWK#@vT2l3)t(s0v?c`l{F&-M<@u1Zs+X@~Cz|ejMPLf%9N8gH$ z@Nh1x_*hm1`(ChASM||3{_Gv;ihF}+FUtUYCjs<&y|Q}W%XzpbpEG)e!zHA-v2+5b|p|5L=rP#EV>!Rtr<-aGrF)rOoMtF2m#dfde~#YkkB->z<$ zJ~iKXsF7hlDqT0jD#L*)&Kdcr1qT>fq^F0aJsC#5qB*%%j+!YtN0K~8UW*@JZ@uw^ zNqcCyxg@<4OBJ@PP3Q856<|7>$uGU1db@q6$}F807#9o^OhbNY_hfq2`%#<{Pja{` z$fE^(fV1=QFbRs(YCwV@o48sYRiLbautd51`f@Ruk7v1(lb?cS>}A=HcdFh zlBbaw=viGCOPxJWl%zDtFsXdqplc5iXB*K{*zEO7jklcP+tokC)vp-zQtbUn9kfcg zk4xbyVs3g|)N)^k_nKW)ivVpvlE31m*d=Y(Mgv}oITB;IDSjm@ToD_j)X>ZaJuV1k z%mz0$UISAc&eF4avYXCkW15BHgRSRT`tCYG9+(u6g1XiD*r6u7v&Fb*RFiYpG^+b_ zHa>^_8o=A;O*`SuYfgxmhY=!d?g%;iR_>d=wUbko|(@MP1kVm}OC(We} zs~BA`u_wz9D~G0+-t6Uvir7_k|CVC@Vqe_=ZaI^dyKs59<;*m;x0=IU0W&p96-a0= zp-Tq<@x3Fu5J@n}Alq8X6ji~<^>e5R#zQfFJ~jx7fgy$6fIt}92=$;CM2N#Dc`9d< z8>D_XWGw0h9SS{nD9%xvdar!w=};Y$xIzfn`25I%a(HKri0qGtPynrQM1p|tFYFtdE*r|<~8nu@><8A z<189Egx@q-Uq>VS^>zD5V?!NBgJwP4)8X@?2FR6>ZYWOY`*cjn9MJdzoirjLlpT1S zf@5%@6Yjvn?PfnZ&HW@j-*=}HEeFX;YpTGdW9BVLCmqXMU_lK za<80bO?~l=8-85PW0^47E=^L(behahX?zvvA_oDCO zv=zaDPoYMK(c}=MeiX{uC`$D8)qD&&?6RT@e--zizUZCo?;gE4I-$_8yAZuIh!X`R zZBfDHgZM*{%!&t9^QIV0k=dSrN8_eQ{Um2>f*>z3BW%>Bw%A37UwXqCQm(J_tYQuXOwXP& zgCPzy(2nTI&k9!!Op`#+Udynhm`IRBmcd6;QjL#L*DB0NnMl{F261egQQsE%Z!p*ta6wHv3i_OCVtw<>QO@dRgMwhIU|It zy9ugZ=@dcnEFm^H*k7OLGaG1HR!fC4n_^ev!$gO)7;{XeLlL26@(B>OI@bM{M5(1L z!+T<2Ne0Ge0cLM65KHSZuwez^A* z(NaRDG{2irsstXVI0v>*bh7YPbf@lfv$3i+xt4LKn^D9;@l>~MLJYdHy29~aCY5=H6{^zoJvf2Co<*|n$Q(Jst_aCDMNr5!+tvi)jdXi#)1GJx=aa$A861oGN3S>8z>M>$93<{sY?k|@@jT~%a~CORge-nhCuWh& zO~bTE-oB+C-o6E{_4X}AK70F?rfRcT4aGPPj|Kz}WAhZ`=7!hTYg0bWhFMXhGe}5B zywJGh#PPO#8rECP&h-$71EPp`fe&Q}nY}-t12rrTu&01h2i8S=)!ASvnw6gp783_> zRKfqwfmZ3si}GJ)oPibm%^+LlcxW2ZjsqD=tc;tH zomIbmhFJzznq!vAgDkUNv)kpF6@7%6W^d8cEpp9rua((m9m-A0^Udl^XvAb>rZc&g zR26fM(P#SLXFD^J#{c>^ZcX{kjt-w3^-lKs2YdVc<=!DoawpmBz>gREPoe4-{x~^! z_8fja#9vQ(yZ;GB)cE7+$q~Z*R{a1oQ=2#qTn4Gj7JY}=Z~+j#`G!V7I?RpzY)!m_ zq9=+#)lq)lzsTQDW%1jW*>pj!`uKB@7Qfh^FZI!;J|r++C#Rdv`Ne1|&HCkRhq%EG zg9uD^IP9Q6#i18{kYqR|!lgyM6xS^Wt}&FLRUk)raH?r`wa)|*p3vyM8vGFURf>5|LPHvkQtj*Ib} zWFH;6oF$K3chiMlXiyK}8UZun(Qd1OKg-ieJK4*wCi!*0_ZJ@4M0&1U(mt(mS3 zOb(igZHiy+LUjxqZ^Zrx9-W?6itC{@q}Hq?$e+Nx5aeaUcjmyDIJYU~6@^^k|9X6s zd)4~IeOSb#2gWz&)SnGarjyTt2#D2+i+nK|0S0I37%o?=_tbJiI^`(cNC(~I$u%G& z1$C=|q3krT1ADoJWqZ94^kNdi#s?JVNVh*sc^r98W4xj~l%Y6Dk&RWba2LYN_JjUEh@CF&JY4r>`MK_hT`wc(;br zQXXZ(U}c9bKE&x7<}a)oiSE}Rnlq%7=T_z$t6|r40GqF1%&|%@oHM!6Vk`UmiV^fomV|h`>bjl${ zQHDa~YROEWym~m%^W3+YV@%cZ9Cw$6WnHge$E#UV&$-2WuZRKap>iO2X@oCDDkWHe z4h$NcY5(T4lTP_;q^}dO&eDpO;Z|VA2E-+1DKM-P)v!;MzXTS1B3qe1bxxyjp+w}3 zl7_A1c&1oq@)~tTy%&QwU%!6x1~$OEarU0&vdb&Xfp+QBWlOx|F#~QV_JICwvy-&+ z&z3k|8qtQ|8_@{MqB_rUBa(4m9@ot9RU z=$ZWLbKDsH;*nZ-cP|Q;2T@Cukbn9wL15Xiy5dMD98I;Qm1^Np?N=zOh8>4z;m-gvzAL>JNzEc++~PaI~s z8z`U5YE&$}=_h?EFqt#kYZxhy>xqnC@fp5$;l6RWr02pG%ko?5_RExThPK=EFwPII zWI2+ewd!P2$ixsBtP(D;Tjl9#D@1EB$zE*L<^&v#&rk_1WWRv;;O0KA&VT89MWsieZ{gCL4Gf8;TFxSg0k z-AlywQ?#OUN;{SE09_f%MTAZi5j8&2Hkd`^a>5dnL(K>y6lp>rW(2aeO%n^n$idZr zPobK|rc4|g0jF;uI`c$)Nkuw!kvcYZ{|dLIOpOplN|0%w@?KZ5->L7wZ5*uzK*rBR45A#35TU>>rkogCP~rn#^}Li=zzwy1bir8pjV{il=qyCx(DbWuJU3?;%~x_I}%2 zJi(p}VY*KE>Yt#8gs>OEMJ{Vo>%t6lL5Hc@K$Xcu=1}y3{-MNYPPqf3~9PAmyJ7f3yluaD9o@r#o zKyK#n3zN=~%9uIO-zH!I{6j;>_sRPVO@7EYrpPC3?c}j(iY_ji?y7wTPc2Y)q_7U@ z5FM*w6A^>eVZ>mQwl^8HqEsuSvm}AJi0#NO)RVW>8)jTEoF zRejTSnz|#XT1AuF9O>a;RNj&obJ-zjZSK-k39EO?SmTw2R2PK7uN)Ls5D?Q396OeP z=+`}?MW+>x@_=|6X_qnuT?$?&w6~&tAeZVV255^QHE8`G9c+UQA*Z=9A*D-9P+=GVB{6}+21k2G z+jQmBvLD`VN#^Sw?2(ZAEF%s?6Hy)O5JZtj=o-<|2Op6tojJPk>fMDEUMq$`?3;>6 z!Y*;P22am>aA&wgJ(1eJ9*BG5O6WHB9w#(fWct^0neliT{IGtj4kP+hS{5e}0ok(4 zo8yGoW>rFn%U>Azs z!P{MUBE7%cJ9&2WF`g)!ht97ae6FXco~Bm<>44)y9MeneR2YB#?Kgg1xc_p$-|s!! z|Co%%1x4pX?}O`No?TX;FZMQqoq8y-TmTAR7WTPI-+AFX+f=}x@&*dS@0+V9>xSSC zgf-PdyvWm0HaeIRuEj>bC%Q9#!lO7oMw>oKK!EMEjY5hnhp$xrQwFh0wY`L`LEo{? z&(oGCiQxoclbC_P#LWODI+T1oziv4V-3K6Dxsz-??!fha<9IyD)rbgV{dqcHKrG)! zo5VcrQwPWwNqcK8IY<_>bA_YHwYUbo8>6O?HPstQkC}p5Tvjo&yARxve#&Q)Q3CHO zOd|lPAghb~?96x`h5M#Uwi()fyPX`*E(e%M7$`>nc)j%|u;nmi58pVu2&nxs#khyq zAQ{cZ@6^7~*E7jkT@9Y1b=X5i@-Cf?L0>N&rl6@c`8&ts*m-VLe=6R3ei0xAV4%NI z-R_CJO7aw3qX0Bg)O*(1>AE?mA>-zI3$lUJU`FOQFeONDD(p9Om(~E0(c;Hra)+p0 z!gU|UXIvXkGT1iGV?3W^YgI$WoCb=SYj9)p+Yb3Oo4?Q4KZ?hwdy6nK<4R?Zgc9Fz zZ)Qq3)q@zRtpW<(nLjH8Ztz5EUp?dq(Thi1`lI77RmaC`$Hz=Zj88CEb#J^mO#Mco znX13%9F04&UVrs>@OonzZcv`I^i~(FQ9x3&`GH#p zw;oxmH6}{11kL&67Pe^4DD?|kiByY1r#8(gtE59BIOqeZ(KF85D5X7T-T$&vThA=< z*O0K#QOY(#RTAK;3{|%!*0^@+7T8%5J&lQQc_cQcw^brh|D{!P=0!3|Ryt~}m$Emb z$~Xu8L2q!d8;^4U;^TODpQ=RS95|6xj*t2WgM*{P-jf&mH;Q`@je|f#AUE^%b^4Vw z4us%GGmy~I$z@)UyyFElP&Xa;d<_oT4~!8KJ0VQ6nY>ZOP}D4>0ndd!ovR`fxHU^d z4sf;dpaWc?GVFj8aDXTQ{O_4cR^orp!$4yd#w~-daUxtN*7?ypt#9i|?*gm#rRV9b+LxYZy((XN9%ZR7J(ZXExqFQp`nfX_Z{g|A z{Z@LqJJi*{)15{|pVEa|vsv3Q_O{*9rL zFTCT#P9@;P9`P3EX3g9sgPpoEPe~p|nTmiqF7cG4!W;QYn&cwjJV+iy_@{L*Wczn= zKy+27dZEm{mp(X7Tz23c*@{xA3ftW{hZ9p9dpWZ=NcMy%eZVl=B<;J52e;gOKv6-QzZ!D zBT|9%xY@Zs$S~BMiHOJhOT(o)!IOkPH(m|f-r`%oo#-NA7+4zYE;8Oo4sUL_)q#wj z0J>ElajbW6f}908UM7WcEeu=9TwPni-sC>F`Q)-W4@kc%$U!udet80M@z(u= zR@bPOZ7mop<0j&NQPkDTa06MFXW&S??nK#M+~y5hkaWsIC>#vB|35;^DTid$SyL(> ztK+9aLSA2I8QE03QSd2)>Y4v(DdAa9s#bb-m$n$mLg76QBOxl&?xNxDL0-|9YGqmgk7;G^Y? z&#b%8rBTCz0RizuDiqUGOUsPv<wKY(qkNR{W6_!3j`|HLm6OH?PTU4*FcK)jOsz!M zmRAj~#&gB_f6Ycpg3CLw)w2&NI8Szh{mX3?Be&N6=D4T- zcG(8@W1AxD0BZ+eVBJVILV-L1(!ZeMX9j>+{FnJG6Pr^XGwDe2pxVO^E9?=Y!g~cQ zvbu1nh0m{GV4dW$0zPw3=ibP(ZFX_oh;blsZ{+VwUmP_&&Z29sjyo+bp?tMkDa(?t zak0}YCNoCtogBT~XU{oRK2oeA(^+rB29PP@xVMKnP_vpJZ_yZb%(P( zb7e{t{E;mH=sLimQAkooFE{O52GL*i?2(-*g4De^*F;^Z?0PB=VO%0O-0h!|mxibgBsn*ix$GV!=HbZ3QJUI4w=*<)th{;)gzH;zfQ&oEW-yeB3%#DM*-^n|K00=~&FjXo>%2KR>hJUR z>?nI6+uc9hKlyvf3)wK6X0z-2T3k%3zTGN;FJuba+Uag|TZuoBH_E!0D zh5SL<%6c#>fRt zY=aE?w%6DHp23^)HwnfQ2tPy01M^cgUtjNyF2_?7zdR|_{e+b%+6;Gi0asg)ev1xK zB*qBP64q}2N4!O&HK9IWG#HF2K=+|VK{}gE9UTxC@P8Jm5*nw;`|K2h-gVd4EzN)R z`bj!fb4?B|)AQ_2yW9O40=Xz)DCreH_jiv@4i2CFyf;p_y8q|+*_ya`I-17&n#bXQ z?sK~ck9z_q5~mW^jfv5Cjg+jfQC8dpXGKG#QEYi|&ufT4J8m9uqPRqU37T8Qq^{x0 zH(a2r8W~|Ct#RCVmSi>ey5f5U2yr$htfrS>=Jyw@pwIjHG(yqnNExVY3zoL8{} z;Aff16rjzL=b+9QVnz%w20xpOLC=)}nd@_IhIZF98p4+iirEn}c~1t~7g_oaOga7j z75r>kEM{yrq6b3uAjACfQcdzq-JE9QAvkLrf}iJeV84h!CJQEOHG4xtND=Gn>fS5K z7m_oKAt(*D(j#48@7`={e`~k486FJ`{39}dKxtuxh4u9qebYqX9g$wkN~#g=^{{)g z6Y3sTv3wUXA&thMI7CFHYH$;f1V+&Yj9*OClQ0+S6_85eqN6=PDmp@nJy%urtQelt zL{*yP??E{abyX?NL@O|8ODIa2Quh_TGW7S_t{UJes>yG-$dA26#`GWzYT2@g<0ZpzlQkeP&X)^=8@sEXE4gkO~?EKd8i8{xmy-RT!JsbHj5w$z=Qs z>5U+L9C9((>erNUwYVX+w;p-uKH``C$#utWW7=PZ9@P4Jp|(IaQX2<}nSZMdlJvDO zi(gC&NHqg^xYA6Xm;T{Sk|K>|5B=5CO76Q`cOdmp0>YMdTt^iY;AVqKrF{3 zA}@vTX!C|h{ocbVh*hI8TTIaS73#Eo1W!a!o5G_;AD@rw>m6(1Ph{FI)-+YHx;#8f zml^Dzy}|y-P9lH%?Z5JA_OY;(NVhLoMmp?yCCKow$lY^Viw+W7>Jgv(v{N ztOT61#Y74uI26F!RykbONsfu@SnSba2?i*}~~5BR#?JA=BJ+qLzFu z+5a$9>>l_J_SRw5(bf1}4uZ)kOawLzeNZTzQJ)wQRX?}#SZtsD3!nwvWqO$}0H)5Q zbf&K$x;;ID3>QTa2gzy!BuT@kW*Kl{;8b1p3mv)0bO|;f60Rc>quQQ!qC6PT%UU&v z2S??oX$1;^uqeAdkp}h^sdE(j8IAMg5=QV8%;fQ?F~K%?E2({@&N%&mE@t12O;1k_ zwztRQjK3mVSH@ap; zEFfyp`kD>^Z)HkO1&^g0Ie$z?1WV*vTKD#_MzDEf()Y8j8e$*%+oJ|Jf{)-Ckz>z< z_ml!y(Y1m>7fIV3RQl5zFG1t(jtLKC9hz%~?b95Ewol0qOpHOkykh8N?3$DwE&cte zBBGW~P=VH8RInXGNE@6s>g)1s+R3+0ZF5wor};Zv&_YZT)>a*?3e$zzn<#cL(VxB`I7 zeG7A*!~MOEoC`@{N{E1KwV`9WQk!DRCF#@v!owpKq%=L})RRRx_@fa$`0`%pBddBu zbic12lUL`4qar9}(}SYxT@Kema;N19saC>51X+_A1~qhb9)<{J==eX>tz9E`Y-)YyE zrxF0B=GJmAT9*}U>*Kr)$mQ^MU){0>3t|7V{OXb6SK{KC{FM%zhheOx&v8}EL;L4BT#gsR!LtDa0j~72ai%326J)yxp2)l(4r@G9^5tYLyocif8%X5DR+(z?AHT@deTu8?CDe zXxRP<{_PrMGGL;^L^jL>7FRjmSvcSIjGWtOsil>q86?=1sQOSaNbE^7g+J%s1s*+mr@Io&Cq~W z^i_F=KQ+T+1ds{>8;}Oly^x_y_RDzq3ux$*KvRx}ZQhJc=m=I4>Cs;Yw z(#-zy^UrvN*!)Q?T^3$)So2_6cK!!ohaWK?9-pV`E_gv9n@i&uzNq62s?CFB13gj z_E3+Witi`Xqz2IFJcHH5L!;}}PZy&xTH!G#D0s(i}KDKUSdf~ zxH6b2vAhi@qFMukQHL*pvZ(}TW^OL4((wtNtGg&zrHcu8J+ZPd)v^g}ol{WbUyKFj z;?P;Wanl~WYfO1R>NwA*b-hI7Q;_#=+c%0`chj}?`XR0g5~?1KLxB%{3N$3_h8!xE zr@l&P!Qm8cHIS@?DzsGdS&raA-kVJ7Fzj|yw`POf9Muf~=Tmet$qS)nviCTABemym zS_pNNGe7)~Ho>FigSRUs?(RPC9X{Jno*(oFM<;(L(;UOWLB^j=@O_n^Pr#K%a&-1F zAn~EA#MTU6Fcdc*>8th(-g+k2BzxNhpXPHD;tSqLwoS)?I(%2dXUHpja)2exK3qYH zCpvde)qzU6bKd)JUhye$rl>}g7Jv3qrrsNnk!T{Bu z;J(#Sf+5^b)j*GD`S~nWcNFKwXe}jkigoK)L$Z7kT0`Pqq%UXVOc4<0GXTR6h?T-Q za5&~JJ)D?rhtdm10Dni9*{DFaF{81p9n z)`R1W@n~*I^4VK%XBbJc0}Xy-CyOnJ9M(F?tlPyy`~n#rTGpw;gJlAqNl+L@ReFOQ zzvvzABeY{MPB1;V6~PN+_vm=AfAU6qfeiC2b>fuNe|B>8>TnNrCD`D{ObJ7iGU((p z;(3esbp|J|b_cIcXsmq{`xTFENuLt!JyNjyh+ZkyoZ-DZYBOn6MAlZ$i-5q8$}H*O zh7ffyL3*NRbNsA*vh3sYoG(PIEHGY;Hj+Wmx05WHuUs_a!V$&IZCGDGo~cqL_^O@3 z56Zl?tPQPmBM{YFPC{I#nwh%^vYoKiFXvFEZPUOy7{5_K8w@;s-|TQ>d2c$K+k{1E zscQ;$OdYhpx0!c1x zTdX;Ve^xW>w*D_v3{zWfX(bIUi zl~!wAIWDXH(m^1?fvWTIk%NIfPpML`zp)+0bwd@M%Z~S1w+U_)>NH9W;OF=Wi`&Wm zkNby%es@m^emcc3mK;st)6r~qmKGOcd18&R&W&3u;`G`+*8#ap+t3la503&ie0NRl z7gb$gkj8DnOx-;HUV`|{oJ^y7%RNO7VYY5qsxyOF~qzUK*+6G+Fe zguWhVu~L)Gw-n`i&sn4gHXc=Y*UkSRn2!h(^~+&+O7((h2Gej!di;inW(y^ceD+OP z*Mq2SR&N<(MC|MM05>DL}_6Y;(4ooYg(#EvlT0NMtbr_iA zJ_`lLON%RRKeMj0B~s99-R_NUSVudg2q;!UKflKU)uTNyOt77<9OdYQ+HAA3HBSR{ z>zX&(u)6UowycI#yJ@-Na#X=lS1nzz<;=iA!tc0wU27vd!srHeW2V{4O(@E?z0o#a z1!etyi`H~Qm#xNKEkn)LX5px*k)5`!TMpZtAok?w)iF(m(ohky3826=zLIL5nljq; zZEF#;InkFl?`b!^?}I+~oM zVTR#S{!Ru+)~cXvC#Kd2O>RTuH)3Cu9ByyB2)Q@u!{`i%uzNJ}?>P2YZBYHLnJ45d z;P79u35c(V;?TAcjx$PKegV z6?Qzd@6`SoQ|@Z*8*9*+(vY>jBDu;Z*XN+(p1G!Hce7`V)n41qF1ll&KvFXNuHs{; z+j^~A**qB)b-EV6qLW6AmOH8HjS|IGPfID_%#o!twbZZy)V`7Cgy^|q_5xi3Nt6Mq zW;-l4p5<+2WAu2PexdT_=WvXCS53E#&feSDN7QNtbkyXd>CxF4Mrh>ZaY$$-FYCMn z&C~>r_L#bnxDmw)Ay{6u%V2~n#DbSPFEWFJGdY1!btfm_jP&+KC!f*c8Qp&Q0&i#5 z2J6w&hI2c~*0SqyvjI0jt25phUhS}P+>H@@TSK{(t%rGRty`6cH&~UspJlT}3?^B( zln*_b_LF*{jJ4VJXX941XjS%R8cF1%k&!VLmYZa`)Zk&%PMyIuWXQ}KW!frJxH$M zB$W^Jx~8_L)}@Lxh)7ji;5NWISuymg!>6#K$bhTcb+wmpP}drwI*^@X!fgU+$sXC7EIrnWWq8(rA*3!LVwxs#2F!Ck-mvD|&;< z&(MdqQB~_Brp*oEpF$omJiR#Z#Nh;<7q1+i4YGaeKqyrce{I3L*s5yBc&KV?A~9Fv zT^ecE*4H=Y&DxrlDK@Sv_h92jKc%xN1-jrSCPP=Ev!#`Sa5ui?Vimo{c!l}R0}z>J zD|V>|>K`5~XE=@UTwCSqwZmFHTvh|*=;e4d4PG<1m{=3S`Szb83~yJ zUjVRj;4SdF8B*UMY!HJ=zy!7J*psBy)?>of&2d1TjxT*#Z>_5qz&K*-AhMW^L;C>G zzlZh*P>qX7}-?!{8bK(v1E;k}Z!nF2QEDlb?Dghl(V=0{z5w zzQCZW~G2d2d!w3a=HNkVrW>_NZR?;gE6 z86+=zCxe6Di~T)>y?4-mdC>2xU;N%VHW38BN`9XH9iA*H$gwrS-EPK@oL%#1vzDba zJ7k;I6z71ui*7?Wa@oz^6%QddICXHvLA78P9k1k7i>B77YTYC+-!gdjux@Z+)5g0O z!t>lSOOi^kfz<`r@XjM$q|@yf?ua$i0D;+E(xfP1vf{)8xoyyP8^rlepQGJKtGrqD5T#ZH0~>S~gakGe@7K1hpSn(;WO z31j7!_ufyDLX9V2&MbHMC-`i!l-!PzSLn9 z{ht40ZgoxZtRoS7h;i^*pze($dezmHnxvY#v|)i9q|T>NBdf8KiXUeq59AKk*E+YP zd=`Q$M0ie(hD>mv<#14^pe$=y&GKrbV6axe5JlW^R9uJmj3~$C%Bb?IKl60?mC@h5}3BlQ{;@xeCT6aNP`*NRa4qzB) z9lMM0qb4%P4OrMx)lFA7x$&-GuRFWlWUTi#e3E?UYB@kz5Ck|#^LB*e;>W;A(tnG8i?$(qbZhn-M2Y3!5Say_^W#`9pl*S zk&8HsJ=(eroEJ*WC!sdy{^6^)=#@m&VOp^F&jU8>e&Dt~!CVYjr@y$oOi|5fVZGfw zdik<~n@~mv8NCeen`#wdbRCVIF7;*|jkrCjhC3eo(t++}^Hkk;-M2xw-d~x z;|}qoIPksAGB}^dHjBfq{F+V`xnWe5pxoaw7K|lwmzT+UV$+0NEvV>2jBFfLJKQ)S z!!wm{x^)63Y0(kiJ~a)ZTPSE_haIWsY$H|2-$rr%9Bw2JTW^Ba6`RQ&anO{9XvZ6F zDVMA|K3Q~vrSJI5+wt($5&d;ySm;D1*YQW~?lIh99`F8#XW2QY2U}Jj>nFL3@f;rv ziX0w^F}!T$HPW0~Rzc$EA>Bc|G2Np#c5#7OS*H2y5)%u6HktB>bduAB8qWiDQ<-PQ zJi$cW*YDG7qe)O51-qU#&7Ci2J7u>qZNx5!fWHlCSYE> zT~~t*Dtp~TmIUr$kyROjqXJq=Luz_hqASjtO}i9gL0veDEZ2q1p_`A>LP>m_sMem+ z(tc61+lHl^XWvNi}&j@$WI)05v$d@gG_7+*rXpvx<#q5WFcj)e{HLp~GC!SVxK z=Oge~!whst=j6TXM)kVPaX&z2Q;27aDLzbuMD){23vAsDmk9u=QO~+?e2nqUVk0`E zzXXPFW%%&m#pw4gc&~(B!YG*S%ya!0V04z9sS4m`hdiX~!m+zRb18q=u&2RkShf?! z@BYw4j3fEz7{QNw8PZtk2^(dO4ixU@D1@DzKuq)(RLt1lg~E<|Yct-aZu2&3S+N;~ zRi6*$ZjN&Ok6$>#f+UH(dU#S3zkGmUoaZ}8)q{ak11yVmN&h0!D!z!c69E+EI{dtr zhzA)DuhwF`Ti0ugn{6i>ad-%+&<0u^o5{41A@BQ!34_KeL9x5KI22l zog8c)y1D60(UteMl3lf=%;$FHd3f4?QBn(5YlS5CDah+Hprlp{%`cBC zyMF>1g(`VJ*aAro)1-G`#Rfjc|N7UzhE89Y^lg?7e@T+P{oWp^qIOSu{pV!e5jYp! zp?QbMRI`kkN1a(hg?&N2T|zVCgRuCY&GI1fnCh4ol)RM2j0<8frGa@t*-L5Ez@YS{ zG-_f{_EH)(GPv6(PO|ChT$-0WBk3kS4{J2XkH8PHO~YBZP}z$MmV zqAo46Usn>KsO)XJ5(7Bdz}W^>#&69mXezAZ>MUveC5V~JSQ(*2tf-YT%z!?6E2-aq zg$oz2y7v^!BnPBz-!r_Q!5e3Faqw2D>G>y$nuuYYF2>qE6#Rpy@)wVnqvz>7kl6@@ z6fF(;FK8z2Q0ZfBS%vt6ZAtCdmUq;Hb}Dl>&I2Ab*O8+fj>D@ZpVHx-)|?Np1bin( zCvElw7ko$(Fb%|psA@iL3(0|D6k@Tj?h9w6h8;*nVHFhnlvGmCpmEtCykdgnyo%O6 zmEG)ljaTXd@*Tc_*q79#!=GR^=!+{mJW~s0+qBnefEx1(%{C7Y1_6xZ8%X#WC@3W` zy8@T#{GvO|#*=o^4u`Kmg}tvO>vRgn^4EQ1l8xGb)@4fo13bvFbRxa)G)1WQsT0Ln z_)QBC_FwdkxZ=gsnS$H6jOOFtmj~5w2LyBwJSI1J3Ac~b?s$0fsw=n6}76$q|yB%SoS}PAhXQ-`zJYH9JDq+UF5rI zY|XzMET5P7qMzdbDX8iTJ>8k0BHmA(!|L-CQsUx?&~&>phz=wZHgN(lvHN|H7dAL? zs8xlrb;+3;IS7|KPon_pctxKR6!ZN>9zHkQ@-53?zN)*7RPrgC&P=hX5A#_{&_p~K z3Tyop@BHNV-ue{P=x#TDWgnGBI7{yH&wPgmY|Q-y!7(nf;V;&82I8b*Mo~^f;pa`I z2RO`%CIP_{hE74+v3W~lfuWc|P$x1cgR=rC|BI^0jt4bRKRVnf)hrM6wm| z;;{S%nyX>T)`Q~5Z20hCN-6GKUJ4~Zd{OpyBL=tv7TCmgkr|njcN_EMKCn#3O<2p( zs%j10SH$D$`nf$1L@v0T(IKV%yJM@vs!l37-ai}9i*UV&Wu^S*sBRp~Gw)WhB7XT9 zaLd*5$|X7FYWU=5DJHUo>53WTr(usvFvn6DE0GXy#4kS)xBN_a73OxPi8Z;aFyu3}rG;on3_v!9Ad;pj==@1JC%9f5r>)t!ZbV!^ zCta9{Ovgyk!4R?!qKa>{u{K|>gmWn}P|C-_v%+n0bG zkwYEY8d@lIh)D=XM7REPZMseWgzbv4G%*b##wN~${fZ83hXY564Y&oRG&qB8KUa(U zr(*k0qXbeINs7@ycQmayM!dzUTBUx5haGfMaO*NKNxNBG{@ zXgJVvthpsF4#DGPt&@nC$uQBFmRe_-0tfoj`A9uuv@!tm{J*ze8toXckPvls(*OF6t&)IJ)Kbh^D{|!!Or}53s z=olvabJ}Vt3@nkm(9=$cjMl(lfBP7@{x?7P6CX>SEri>Wzq6GCE|nFchS$@5ol)o$%P3|nrJZYPbO8EPU|HLT{R+I=%P@< zxQ%?#OuEonF`3()X2Wz*ppbyMdqEm-Qt*DJ@`pJLcQNDe7dhmM9I~t&;*k)oHYBIC za)~Qb1Zk`P0crcnct8dCM%5y0E!j|9t*!o#(`>J`I+5|kzS%JwURTSZ10dC`v2-G} ztkU@j`fxj110}*m%Q{^3y{#Evwb^Gs*DD+5iu}SSzwpT=`6MPowmeZ(;F8Utz)epG zw-u;LQWyMHep^8zb?zs!C;Ru;29A5BhzvaVgs*Z)2zpfXY1@1gS<0Z~ey!S9xU#kM0DgdXx^q;=qRF zz)XJS{FF{Ok$14|0E*K$H+avuxPT;VIfm$`)z+eD89Jtt_c(f|V%{;XSHjUPjfQ_Sv%F zBc-u91wprPReSM+HVzoxw0@p$5TT1=g5m-9m zvUe#tX#vOkP(FE6#UQ{{;)OV9JeyK{_&P3z6n^$E`CL$ir#k(a#8(7vNkKXOf~p3O zen}ZVZ;g#oJfo!Dv2weGLAQ|#G)C{#5_cMc{8f%6DmSRXpJn){8+GY)wfx(0PBzCzQkr(2|&$%8)?56tTwXdB^wST+IeqgCsH^3i=& z6aOq#)K)T&ToO6WTGiHyKK_}1h8~=3;10?<$)n9qjeTzmlXjyd639US-F@E?H|~f$ zr+3-!CeY~ttXQJcV84iTUqreuBHb5}Zj~Y(!TuuHeG%-w2zJRT1iL>K`v3iLM7)Z6 z;{2+qV;Fi(y?{a0)TdosQ^}_+@84%4tfrF*%ajGZ1N|P7YHu=Gwc=5MHbsP(J|?;bCw4_HzqAYf zy=KLhDSv{k$$vtjk|b=2Yj(P^!OzOm@#KHgC;rgd+}y_h-@@5}cg+f)?Lrk>ZoQ=kkiRbx3=3J}AA zLBTH^4nlP?K-Ie4)^@Ag#ajAr=+rEkkaLf&vT1<;PC&80$;g0acl$pkXNo66{_RPA zZgZ|IY2*pCtp2OOxkgN3y9JOW{>g(;W0_{}D@LNNq|Sj_pQ(P5AC;7zU3Zfg8Ky*X zY^e;K)9hSHvG2$83q$+3Z?8_A3AD4l2Ek@EMAKV1TM{nPV}KfT=eQy(JDVaUf} z@vh~jW$7v)Pt_7pRE%ymgE5TrZmzJ@#xW^&_5SOYSF;F_&Y>trz6;YNZKxnC_XHzLKs|n4J+9 z^K6zVOO9o!aH~#Tlg<@eh=!~eDGeLRoHRI|iZ-zN*eJcs7gMx^WsaK;FUHwB3>#I_ zxJOL}CPx>oT=}$&Ng^~^!249g`MJA?Ofm=34A+H$uYn@HfjDa;PtNck7|9cKJ8kn`cvvZ`tQEBcuod9EDS2oUH-vxtSv0M8$@1DrRH4bBWa`w=_4C z(ci^vs{2P0EbH2_DA#@He3fliF!B$0Z<&Ek!Xkm)Xt9`jtenTEivn?=bPSavsP4a{ zcZ214`p(!_B?yoieKcohBVIUr@cHI6K(AO1;@ZJQE%h| z`jUJq6EKJEz&YkwTqKVv_p5Hb9ew%c-yZD%3>JSaE^2jEfzELY#!Lj;~Qh_M)51ba)95y{g0IXlz0Q0TNRE zoKx|h<*JS$A|I15`)-7&xk|MjSmlIRLY!J;2Xp}EP5P+1!VIqyERbn{v2PBmJ7eV3 znJ((XysP7?LUf#%@ewJ<56-RPsjLtu9_L$zN^|!i%4;)5;RYvt8_9g4c+u< z!eJY0V#Msh*sJQl;8e-flqbEfxIWAA2AN@y0q{$-g1@7|SvpRO6jW#W@b_sM!-wd$ z8*MyYXX9X&OQ_t)2U-DX+jBz(Uei!_-HpI3EDeoLZNnyAcY->PNP7qFQa!+0+B%ph zhDl4h=kL-t0D>oqN7_93r4}Y$Nj*~kgOt>20gK+F4p{P6kW2Lu;t_)E$(3pTR;8rb z1u6*yqFbm4A`$v@ka7C(*1G&@0I2_+?!DbR9PA#PyzUQnUhExj9ZjC4d3nj652r^H ze8?TYk-?Hjtc5mB+{y4V0g;&FdE~|3?wgki7ET9ImZ15yyC81x!4_wt2(@~vleu7v z$)AKoHrjmBtLZo{vz&rvhmbD30AUVsv*!>x3o@Zh$74zark4J}ck3&M4)9VFz{FFz}w^gF|ZWW%(#q;a(bu+M) z9FmSwhD}?1-%5);pkTXW_)d-K;zAK8MudB#1ZADtKL}OPxTD#Wo+xk0{&qFRT3rVZ zT57&VAXwq2FJgkoHE{(U#ra4cnSjNNcPYe2oWgwU;ka~Wf9Rlyf> zJbKnis7h`UMiH8M(X68{HKW1N?vZMiD(Rx8;{vwKjo#DV6M$*EB7neq@aw|vOjZUBwe){wrWL7@=c0j0EJyK)E~abpMYNWY^+ zWn$f!wL&Xpt5vtGRWrT07~p8|XaXB6wrUsyAme7j`;O2ifA$){%k`6VY_t+!bF(;= zN>CycNe!$IU#Sas@e>E(D%L9yPFKa_cS(E=k?@{;xPto9I0gxQ?6 z%k|oB18=>$WnzK41+Z+KFM)OAmOJcVTq#|sRzUj#xjHDRKC~nJ+r9by!XL%+o)tF2 z8oh%_s%R>C>C0{N>OGgO{5s16(vnVA!f$s3#S*0gjxtusZ6zTXTBpOZ{%E8sbLUo1oYzMg=jEI0uhYegwEQ}NU#H`&+)d(9bMPqj z7l6GJ#Q7kx(gVXhxy+WJE7Z2*rduFf_KM(Gp&i|1`FPYr3rz2XIy~Jw+&?(nJ3V>v zX0Uto?y!EU^v%)|FbDnP!JF6fLBhGY0mv5zgL!aZlXJ=yOD^BZ&T;?bMRlGUfp0{Z z-M#*92!3M>PhK1iDzmQJ3n+UNI0s`5RK2|LgWaTr2PKxh3w&d*9v=?E|q1{Ysab_L8kY9EZ$r}TVACOX{M%FDO}RY;NB?JzzsA&kT84|;Nx zhe`|YG^iVT*hm~TBnV}ciit0AY!d93WN%N1|;a;t#4-Tgv;#qh8 z0D~jznpZ2JnCs(g_6YLniP}gQrd-2t)(_ePbr;^Tr>iHX$H%7IZnWOpsF;2#oLXx0 z2>8CmlSpjS;rj=gF~h_u-Ej=#B5?;>b^UoT-n4sm6Ao2_^m)Ndttz`a|A3PV9LtTp zLl^;GrswdedACN}TLJ$7r8WAcU<-k!VBKhaA84*CtgDl#HX{TEXyHVEn+wC*YRsXP zNxP-btDNpFG!Q4HeiLpbLVl@Eh+tP^7ZJ7#IgNvha_MQbZSFOV7K}5EeqqflRx1Q& zF$BlVVi?ApRxF>Ll~^o+pEKzNmXRNL=%bRMHcSF-@E zbhBQNGc(C~C;*xar_Rz_rzO_yz1~S4d@=63GsnE%XukF8;td%F`IWY6Z&8}vdhx#8 z^aSGx?}y#di9>wRQUz4EIo@$Is~Nj1w(l_ZrMAI&W0!#Hwv^!5?OJf|hEAsWc|1g~ zxDXP9xZqxQ=w%IAr6gpFmi$pLt-zq z+6ep=%EBNwQOd;dDDr_iKfjAM$df<_$ok$`fpKu_bFU;7O47p zK3bbWw1gml07G>m2mTRX+a8pjZo&Ax%)Puwh8tM+8zELtT7ES@*2 z{ppob9XVI{Tl8ez9sI&R^$Tx)mnKl%Jj!sEKqOxgpC{2Jbq5SEim&2}WYlXp*zpD^ zeM9D*6l_C%ukSbyJm@gg29r-GH}Px^`gg?MB5(dOe;BvOH)v%iXVy3eYgk&MR{{nx zqRZtt1Ls!SV&x7BDn~E46vdneM3wrKmb0o1)FoOB*0&vGCA#!s@|FRAu%Mb9n+d|^|6}UsW%08kC z_+)kk7bZd8eDGCcc~GpM;J`yC!eDKE`YZEkQp@0;!w($SkP7G$kG^Aigbn?54ztpb zj@N-wlaB{`j()5=tXgmAyjCv|>_ykPQr$+-2V9A%4!U%m$B4Ihh8_xasnRpnQiXIq zQHkW@419tv_YcI67t^bFqJAG{_!Ez!>SupER=>^V z>FO%#RCFnasUW5(vPQE>e3cIIr5H7+SvJkdq@PWS-s-Bje*O06XNtB-9vy&i{iWUO z{quF66zZ&lv6sa^PhR)m9n!yh|NHemUESipwzs$KkH^tQC;I8jW2|^fkqg~Ohba8? z#l$Dv+BO5Uj5$px1|6f#3mlmo2_(Xxk#?iA7_N!b263GyYx<}|_k`p0GsTS-NeSXw z)xOF`>G@2@A)F1eW*`YNqL!#_!1xg_0BeT`z|qlBSkY39%&~SOfgD6(Ty~P z`;>V+`~V2i&h7btn|Aaa91HQ9OV2%{0FR$8T8C_c8Kwtojb{F+4hd4x3-9DWF1QYtQA?E5hxrpOm1!^chX2-WH%6D)3wn$vH`Gbf1JjJ(-N5q`llOJtU##Yz)5D+AV$3!^Ngx~ zo?&thykEsXf#M(<)qkkRDJZCbo~FB64@!Gp1I1y>&R3*nj|gU_hI%m6L>wKQ9$#Es zbUAoR{fP@u4Rs34LJMmh=>6Bf{( zjs&;j%VzOtgt45EZ!4qlHAO2Bh|D!grAQ!xD;YV2TQw|3yM+?pQ{0%CjCvcIvo)9v z0OWntdI{{>H@g6D0DUElro+oFhh>TVX#!kGOkM4S+?1gjR^YX6H&}{F?3uwFpaXMy z1rc7nqVx!!?L9v_-owR4+pyevW7sdQlic5K9if$~br8e$qq-P@SA-dPc8!XKvyv6U z7B7@(qXdKks^oTjhkI}Lj){4gC?tpDsP(udDN@~$ToKVOlz7f^DZnXYbDUkI!{{cP ztdJL$Rt+_Fr0XbUr>E2%YH7DiMFSS7mTSnrpM|m`&kaEw*`v-fj8|B~sB-SZV@(gr zmWj;_bX~?D>i2G340&=*S|Eu|%GqlKY#aFm*HEVy#6TkatD!>M7~B*UBpSpiUY|4s1M%E?M^Sl%uK&{85-4b3CIkO=omg?^Du`Q#^?l ziXf)EWr_BmI~3K(EIl<4#eNV8SbH zr?9GS|K&?39}7rk#zIZab)0L>QR@{w@@dpdALO-?7YG1E0;m#j5MhaohWuQq80j^5 z_}J+u)l8BO^H0NtL_$fW6k$}+d3rI;>9`V+H=#=l;tQ$kgEnWRw;3mr$LK~J@{1$* zuW35`FeWopa(0i12z8x3=+zRU>+-Hb53r<%3Z zRTsQE4H_km4OI$U0p9ag!mO^svzMysHEyxd# z$8_yfpz@V+z8qOi9_1YFt)OtMnr2t&=j1q>mI>lwK4B+t|3|40D@x{s^vF2P&Bh4< z-L&gq-vEHDqKmrMQYL@_3n-%xc+1FYu&G!`ag-wvuQk?5#)=t02?k9^xDnbecysR6 z%-fsE4rLV>CnH3dwmi&QV7AN2(A23Jd_T|Q8Mc2Fe=rR{rbT*&X(Q2}SRpoZRUqxX zJEAGb@)4bJto|}iKfpeC7MH34ijfH=X2|iW+TcVa*Ghg8^^>(inmV*1KEs;<&3u=j z-oe6A>ZfAfacoYPh;0D)(S-dWZxQcc4L9oLb`jU0)!1d3f_HTE`YH4w*lbJ4IhIkh zCKIcLo5!PcTBtEiMysn`c7sr4JDP%=1C$TYd_(mXp@!GwA=Hf2J4?!&1X)a3oy(A{Sn265FnW1!iY0xS5j(_wThs{AV3a9AL>GaCc3046V9)IW!6|z-X(K%unPcZ znL^tQqU4MuFsX9}hMoB1lJ%Pm8JDsZ2zV!`KHs9KLawyhrk7W)IJ=`T2#pcK*E zJD=ld-%S2%N(bJ#vm0Ir4y{k51<<`W!qH}0eDxr$UBV|xvq>AI&&d%v`4p>@S7Hq; zRPLaO*ulnv6a+NoY*Yk-og8x;<*?x>iAU%L4THjact;U@+VQMQa22hDnrq%JDeJGk ze8TWME&Yrw)!Yp5#sS+E9@`?MM@nZpb%v&{$YR0{ID+kzEXG422CK$bkVjnWL20gY znWYKYCQv21;w0xZM6$e1(AeG zOvdAAohH_F>v}ucJ9h0!EQoII8DUHb@)5daXnTg{ww~GE`5G58YAakX>3%KRTI;|i zU#wY8ph|1XY^I7g;p`xn{Lto|_EBT!5ir%O-8H9)YmF6Kv_Kd*Y#4Jafw}5a`Ys6w zW6JKN%NrFhn$QOPvmfNpU`fRVN3^>xjnMg)d58 z;gN$%hS}9wT#ALpk?@%G#<@iBI1c7iD|KMn^+nB(ooEMV#<5e-*^PJ_Ra+R_8Cc19q}KOYV(y$s z15A9K)-aAZ%oqy^$D`}@EmhQFdQGVk9UDlDCJwn_nHf2nAW3+Gi&j^4o3_3>C3U?6 zX--vf)GPScn{&S@=r1RP?LwUMMwonpJG%n6i|8yHkLcM0a1XaC@aZva5vO$x(^4&; zq2H7oGL#VK6QmukKx}h;Zp)mKf6AJ^9ZvbOQmZNa<1>MC<2|F-EheJbeKp3}Mp z4LWr#&rbumK zPn%7UGF87WQs}`=?Z|<90cMr@zc2L27RoK?$30Mt=Hciqa0K0sns#NE|6D@o8*IFi z=3%{xKOu0m6Liftz6TN(=Auz6k5k1_a-CxQ%@HD;Gm@{<}T>d`@mjqD8`kGfass(PB=gd z>l|!#_0xIH8zJQC2bCUW@u{OfRF%9pgBM4~Clsr9bT~LZc=l#+aCCSgb-l;xc!m(u zGc;cpa&fXc7U$p>vK76~iiF*IM@fulK2ko-1&6*QBmGXPw_=B)yF%~*00*#n$)VE2O4ZMK6gJ&-e1__vColyS}{s zU(@Z4jg24vywT~xpy2p&5#zXaz{P~U%-M*C2LjM^ag-1d0_3;gc0Lkc82A(PC zVi(;HqyIO0oo5&7fN|xR+4KVsNG|2R6_JB11RcZ;!wR`(c&iDilH znbTU+^fg-aYR`?h=R5zJ*APR4=TsQdk8)pML&A?&dvrXf&Z)8n(Tm7i!?S0(9@}Vo z((j&xQ1ITJ5RC6UYI3z%qP22A!Qg+dKVx z|LHosB7k~5J^%1*FZz3b?dkeY$Ng8Y*Y?Sjy_=jTlOkE+WRrw|eV}cUxIkL?twpIb zU4K<-^D)j-2Z;9=)_qf zylGUUNhU_=H0Bo=d#C$JX;m`fw;&$`X~7K)@d@eFK--+ue8856nsh3HAqu@DcCQyuI5i3kmphO`D|9!vz!&V;Bt zp<&z$*qVw$2HgLRh+J1GfoXa*QSs1PpXye6p&Qg%78hEpUNt0Y8e#R?CSxyPQ5j>KS|hl>gv82 zzQb{Dn%J~{;jvNx)f(6H2g(VW1{akB+c0b}$*Vq+N9J-s;33 ztu{tdWOy|HAEs;n0EB`Xt>Rt%`jqRz7@ONg^GqAtD5_kI8KDG4%%py~t;{N(hy6FP zh1uQSa+*wEXNerNjy2csY@6+K%U(WCGG52Vi8UOuTBqzlRzwV050ydK`bx_tCjBP- zV9{Dd+wj7t)X3;UOx{I1PMGF)Sj$k5JvuH58pRlSSPicHP1ViXPNd2%dvGFS=q^e7 zTw$ja?a1=u*H%JJxYHtSv>UCwFkMFdvWzj;5@ab)%UQj$YC5Rtl~u`Z)Owb|32aG4 z7MN|3E=zzPR+$we2yj@p$`MQJxr?;hKp($veb+tGR!{nW=*EBO#((I>f9S@4=*Ek6 zW23bGp&kFW+VTANJVhMps|q{Qma;e;)%q$exg^%sd?CvInd-0P(=f4trB3kpSv)2M z9s){4@N;%9sn}DRq1ri7F0wv{Tn> zUA3!*epaKnREUyqEcw&&JIL%OiOH~tu=>uZV6rMIOzOKfuX$K1*%y|X`vs?n&GVnb%Z4|6=D*Vuz7#6wuKBu>aCWi#Pl&J(?l1&RmdvWfgRC=O+AeLTA2F?!# z5UUFut^g2jF0--SbjImD<^lEX`-aw5pM4t&X3Sf=`b$Gmq^HI@Y zkBL8K>Bxfy&i@mRk~Uo@aXwDTYhDDmn_`yg$O!cVW1qHEAB*T0M)ZJ;PAA%RzcNDR z0MJ`>pM^*lmgT9MBem;VgwEXpV;QqS`OV?MKqR8knxoIgXF4U^!mK$9_Smy5!<_K0 zV>2GJ)x36DHO5uN$NhRK|I-)1{}~lKXwUkmsmRXK);r|Xj^Cu z(Y$FC5T^R_%liP4xMTsAt>miRcGj?}zwXwR2y8p(P+h3qSXOIO^;~HkMJSlT5EVsr z0(;=vO7j@F8A6t?$0@0(PL54@+`5boR)FCk7Yjt_s!2#EJ*(t1w6jD(NsOA0R(A+= z)fRu^eVr)|mIdeCv&r=AVYETkUPOS6Ju_Mpv73__56b4=kB*BJOwazaSb@?niVV*O z6I$bNgEcC;j^Q-7`<>kkG_pnFo~Ok%p-6mPO_WS2jUK}NgB-W}oXp1Y6(}4-U?U3S zbs?RNC|NM)6S&N>BAHaPD*D?|w8Wk;h$kPwtD?-{swAC!jM4nQuOzCGjyu?~A^gS@ zkx6ak&4~kJ#m_B; z_;jVKV^0GARI2|Ng7N3j$G?O={x$S*V|_UsHiC562ohrB$x3t^B=IRQv!@UQ`Q5e7 zZm8WKCb_evojcmVbMOf755CIaMTN=v4J#K4=k{fiMD3vy(ZR%=$ zps!nTox$_Qm(y}>l-+2s38tzpY<@IwXTvu`I8U1K(JoAeF3zLE{sjue$289-S2od+ zdyd1C1!khsxqM}!tEgSTQ*wO6HRPv>;9!PM9r&0=$DW8to9CqclnV>!WHfs08y9Rc zLGFp&yk_RX7MpfU!vZ*k_VOTq2J5@gvyMGm*74!Ol7_+AJbGrh#?{hTU`#GD@|^pr*1FP6gktQDLxhyQy~K`)fN&iE+M_w+CTttSg~gOZ(jlZKT%5o(R(k&mvFY z^2}6A8G}+o5;+tJQTUz&neTn5Y2r|WWIY^AwZuhB4~XLk#PNettIBj=muMkYvozBr zol7+R?5eiGnb#WduBIdl8hXBfSxskH6uyBX>!zq~ZE9o!w7D7iDObuHiPi8SEZQ_6 zh)Uf+`x^vA##9OVd^j4?G zh1LJ)jd%TetUA-Wtv8}-O8)U-@y(ycTRdfI3VZF zi@n`9FZYgVSEhKD`54@G>w1D+c@w*<)4V~PyuwtM!ldlIM?*PL+x`~l3sIAvi*Qd0Juqs7H@%x5gE3U#Oi`h}z%jACWqG6!q{aCzTwSk|RTVq7_jH8sb_5 zi0=i;ivYft02?W_yX<$&j}KXDc=wllJz2TIr0-!L(k=6bQak$VI=nTWDw;XfDeSv9-W^*07?wWr9iMx zI0imIhXBUyjZLsV4JxP81vGJMKLDmKzyQvTR*_B}aJt(c^f5gFmM@2t-b&0)C?)Tz z+{8_-_nz>TwcanfI~rx6EcFlUb8~QLT&sKAPP;Og9Cz-Mu+%!JG&=>_&r*lv_Blu^ zBS%Yc5D=Y1grq6QozSgter`H;RYTA$-M6QrD5Q~9l6mWkf=c8t|3zgEJ)H}N5C!i` zx;qc8Erugdc888+_wPxn!*~D|xa&9|)n|i&5P#T_AgB90P6tkWe~ImWp_dO$jtuc$ zAzdcCf%}}FzUYOnpIw(3A733Eoh(T9$YXeyLttf2^5Zc-eSPUXk9}ib;$8wd;+MOyn8pSDr2H&Yjp-vQp=jkAk% zh^d4N%yc>g-G_YH;d$+qGWsydZj`W?MeSKqg4O1cZ*~)wHvDZK-Kva|TE2^CugTMT zTS74h9{ky}wIGp8I;O#j`c~Z#2$`1@C1}7*6&#_cB5`D3nFdmO5lwgLUXoj}l+`Dr zI!gG9$s>b14j8u`EKeT_UCI*xQMvFhTSsl9Y#={Q*7o?V93>+QQR!O*A_k&MY(QpP zfb7&&XYhFE_SAK1afYE^#56`xGY|pPtJ4no$k^&tY2%6-c22a>YqadiVns{5B5N+j z*%`P-QsRJr36Zma8Azgon$4|f24qPly#nLRrjTgl=k#KL^mPmp&xTMPW=R=R8V1C~CrRTfnRVu{ea#59;J+r!Z zE|e=QG7+<_?i^0DW4M*@>|Z1;{a6r^k&cfXCVSB-_3?7e1`qWtJco(As1UhRz(24G zp+s9LAG;Kx6cgI-$(0SweSn2f$O0Ty@b26;^zX3xS(80XK7LWXv3u9}Zi% z5O>tSF0yTkUldoK@mj$RM;jw5Z(Zq~4WaQymZ|8TFuYL))|<%U`F6~GqT zjb4*WTB`!7I%!t*5Tx@xoy-U~tm$d@k!OZ8NM&jm4{kGYBn-AFoD+a$uq;J%n0-AIKifvUDw4k+* z_#t1(krztJ-qc3X8)ZoWXEkwiXYycn^`$wfjNpsNsl?K&t7HV3Wydo*XMF2x7S)px zV&&-cCyBMV>*QT_P^AQ(u2QEPyny;fgNtfv1T4AX z*#L*s>v-ri(17~+1;5tV@djqj(|V3TYsTq0n?={tvLp}7sCC@ZjtVj@GaqGKH&;s8 zNog%r2SEvfC65G`D)ks&873c>z#`}>9OwYn;xVkP$*aB^b9q={#a%*QRtZB=13G_W zvmBB`&DP$0!$<3o)0^2?GOTCZbfldtc#iP03!ynd94@g<;XEl_LWCT?+35t;6~gb@ zCbLl!1Tn7q`C9aXgY7h(Od(K%J;d<&R1S2*Fo0o_yqm zXhS_sT&Nf6sfPS?{ZG{n1tr<%a!BuYlk1W0=7*=&%MghBXZu^`XrPs$r1x+-#!zz1 za1Y_w`gJcI+xRlOq0rJT^#kxxPlyf*-+jGXee~ty~9RDME#Xk+WBVh+7nReSQ zx^8Iwh0?6o)zudR=9mH8@qh7kE^D53n{cVs5v&ky>X;aIMw~mKMejD4y9xauyY-

UXtAEkm=GT}@4;s!JcT3EFI3C#G(bigMJE;G!o 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 z

9+JW_{3ID2X#t^`8O|6PoA*N@NM`xI$64~>?~N5 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 VitsxC;1S{zgrM)%6d)?Ve*t`oJ+A-& literal 0 HcmV?d00001 diff --git a/Moose Test Missions/EVT - Event Handling/EVT-104 - OnEventCrash Example/EVT-104 - OnEventCrash Example.miz b/Moose Test Missions/EVT - Event Handling/EVT-104 - OnEventCrash Example/EVT-104 - OnEventCrash Example.miz deleted file mode 100644 index 29eb6eb41116d9c0cd44c7c791ab5484fd03b8e0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 233818 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�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
!YxN=7Rh)v*yktGAF?G+yH)Bj2Ni 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

!L`&@5Ld);RIAicr)AH{`sACf&HZJ$ic% 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

2{PAJAZF^db*yry+QE;3>^Fhi`fZ_OfQz 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

v(9kX>~#DS$FQF=Ju-UO84d=p$GUXy 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 literal 0 HcmV?d00001

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