From ab345f5ad2de5761d7acd406d0ed07e4095350f9 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Fri, 3 Feb 2017 22:18:51 +0100 Subject: [PATCH 1/3] New Event Dispatching based on priorities -- First _DATABASE -- then SETS -- then the rest -- changed AIRBASEPOLICE --- DCS_Folder_Sync.bat | 4 +- Moose Development/Moose/Core/Database.lua | 5 + Moose Development/Moose/Core/Event.lua | 71 +++++++- Moose Development/Moose/Core/Set.lua | 1 + .../Moose/Functional/AirbasePolice.lua | 2 +- .../Moose/Tasking/CommandCenter.lua | 7 +- Moose Development/Moose/Tasking/Mission.lua | 7 +- Moose Development/Moose/Tasking/Task.lua | 7 +- .../l10n/DEFAULT/Moose.lua | 156 +++++++++++++----- Moose Mission Setup/Moose.lua | 102 ++++++++++-- .../APL-001 - Caucasus/APL-001 - Caucasus.miz | Bin 242831 -> 243563 bytes .../TSK-010 - Task Modelling - SEAD.miz | Bin 231007 -> 226501 bytes .../TSK-020 - Task Modelling - Pickup.miz | Bin 229002 -> 229006 bytes 13 files changed, 291 insertions(+), 71 deletions(-) diff --git a/DCS_Folder_Sync.bat b/DCS_Folder_Sync.bat index d950efe12..98d4fdc55 100644 --- a/DCS_Folder_Sync.bat +++ b/DCS_Folder_Sync.bat @@ -1,4 +1,4 @@ rem This script will pull the latest changes from the remote repository, and update the submodules accordingly. -git pull -git submodule update --init +C:\Program Files (x86)\Git\bin\git pull +C:\Program Files (x86)\Git\bin\git submodule update --init diff --git a/Moose Development/Moose/Core/Database.lua b/Moose Development/Moose/Core/Database.lua index 37b76490c..4eecd065f 100644 --- a/Moose Development/Moose/Core/Database.lua +++ b/Moose Development/Moose/Core/Database.lua @@ -55,6 +55,8 @@ DATABASE = { CLIENTS = {}, AIRBASES = {}, NavPoints = {}, + EventPriority = 1, -- Used to sort the DCS event order processing (complicated). Database has highest priority. + } local _DATABASECoalition = @@ -228,6 +230,7 @@ end function DATABASE:AddGroup( GroupName ) if not self.GROUPS[GroupName] then + self:E( { "Add GROUP:", GroupName } ) self.GROUPS[GroupName] = GROUP:Register( GroupName ) end @@ -569,6 +572,8 @@ function DATABASE:_EventOnPlayerEnterUnit( Event ) self:F2( { Event } ) if Event.IniUnit 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 ) diff --git a/Moose Development/Moose/Core/Event.lua b/Moose Development/Moose/Core/Event.lua index cedee92e6..5aede04a6 100644 --- a/Moose Development/Moose/Core/Event.lua +++ b/Moose Development/Moose/Core/Event.lua @@ -21,6 +21,7 @@ EVENT = { ClassName = "EVENT", ClassID = 0, + SortedEvents = {}, } local _EVENTCODES = { @@ -50,6 +51,33 @@ local _EVENTCODES = { "S_EVENT_MAX", } +local _EVENTORDER = { + [world.event.S_EVENT_SHOT] = 1, + [world.event.S_EVENT_HIT] = 1, + [world.event.S_EVENT_TAKEOFF] = 1, + [world.event.S_EVENT_LAND] = 1, + [world.event.S_EVENT_CRASH] = -1, + [world.event.S_EVENT_EJECTION] = -1, + [world.event.S_EVENT_REFUELING] = 1, + [world.event.S_EVENT_DEAD] = -1, + [world.event.S_EVENT_PILOT_DEAD] = -1, + [world.event.S_EVENT_BASE_CAPTURED] = 1, + [world.event.S_EVENT_MISSION_START] = 1, + [world.event.S_EVENT_MISSION_END] = -1, + [world.event.S_EVENT_TOOK_CONTROL] = 1, + [world.event.S_EVENT_REFUELING_STOP] = 1, + [world.event.S_EVENT_BIRTH] = 1, + [world.event.S_EVENT_HUMAN_FAILURE] = 1, + [world.event.S_EVENT_ENGINE_STARTUP] = 1, + [world.event.S_EVENT_ENGINE_SHUTDOWN] = 1, + [world.event.S_EVENT_PLAYER_ENTER_UNIT] = 1, + [world.event.S_EVENT_PLAYER_LEAVE_UNIT] = -1, + [world.event.S_EVENT_PLAYER_COMMENT] = 1, + [world.event.S_EVENT_SHOOTING_START] = 1, + [world.event.S_EVENT_SHOOTING_END] = 1, + [world.event.S_EVENT_MAX] = 1, +} + --- The Event structure -- @type EVENTDATA -- @field id @@ -102,7 +130,7 @@ function EVENT:Init( EventID, 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" } ) - + self.SortedEvents[EventID] = setmetatable( {}, { __mode = "k" } ) end if not self.Events[EventID][EventClass] then @@ -121,6 +149,12 @@ function EVENT:Remove( EventClass, EventID ) local EventClass = EventClass self.Events[EventID][EventClass] = nil + + self.SortedEvents[EventID] = nil + self.SortedEvents[EventID] = {} + for EventClass, Event in pairs(self.Events[EventID]) do table.insert( self.SortedEvents[EventID], Event) end + table.sort( self.SortedEvents[EventID], function( Event1, Event2 ) return Event1.EventTime < Event2.EventTime end ) + end --- Clears all event subscriptions for a @{Core.Base#BASE} derived object. @@ -132,6 +166,7 @@ function EVENT:RemoveAll( EventObject ) local EventClass = EventObject:GetClassNameAndID() for EventID, EventData in pairs( self.Events ) do self.Events[EventID][EventClass] = nil + self.SortedEvents[EventID] = nil end end @@ -165,6 +200,13 @@ function EVENT:OnEventGeneric( EventFunction, EventClass, EventID ) local Event = self:Init( EventID, EventClass ) Event.EventFunction = EventFunction Event.EventClass = EventClass + Event.EventTime = EventClass.EventPriority and EventClass.EventPriority or 10 + + self.SortedEvents[EventID] = nil + self.SortedEvents[EventID] = {} + for EventClass, Event in pairs(self.Events[EventID]) do table.insert( self.SortedEvents[EventID], Event) end + table.sort( self.SortedEvents[EventID], function( Event1, Event2 ) return Event1.EventTime < Event2.EventTime end ) + return self end @@ -741,6 +783,8 @@ function EVENT:onEvent( Event ) 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 end if Event.target then @@ -763,11 +807,27 @@ function EVENT:onEvent( Event ) end self:E( { _EVENTCODES[Event.id], Event, Event.IniDCSUnitName, Event.TgtDCSUnitName } ) - -- Okay, we got the event from DCS. Now loop the self.Events[] 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] ) do + local function pairsByEventSorted( EventSorted, Order ) + local i = Order == -1 and #EventSorted or 0 + local iter = function() + i = i + Order + if EventSorted[i] == nil then + return nil + else + return EventSorted[i].EventClass, EventSorted[i] + end + end + return iter + end + + self:E( { Order = _EVENTORDER[Event.id] } ) + + -- 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 pairsByEventSorted( self.SortedEvents[Event.id], _EVENTORDER[Event.id] ) 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 - self:T( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName } ) + self:E( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName, EventData.EventTime } ) + Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) local Result, Value = xpcall( function() return EventData.IniUnit[Event.IniDCSUnitName].EventFunction( EventData.IniUnit[Event.IniDCSUnitName].EventClass, Event ) end, ErrorHandler ) --EventData.IniUnit[Event.IniDCSUnitName].EventFunction( EventData.IniUnit[Event.IniDCSUnitName].EventClass, Event ) else @@ -775,7 +835,8 @@ function EVENT:onEvent( Event ) -- 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 - self:T( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID() } ) + self:E( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID(), EventData.EventTime } ) + Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) local Result, Value = xpcall( function() return EventData.EventFunction( EventData.EventClass, Event ) end, ErrorHandler ) --EventData.EventFunction( EventData.EventClass, Event ) end diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index f20832639..9d2b167c3 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -240,6 +240,7 @@ SET_BASE = { Filter = {}, Set = {}, List = {}, + EventPriority = 2, -- Used to sort the DCS event order processing (complicated) } --- Creates a new SET_BASE object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. diff --git a/Moose Development/Moose/Functional/AirbasePolice.lua b/Moose Development/Moose/Functional/AirbasePolice.lua index 45af50d9f..a85014785 100644 --- a/Moose Development/Moose/Functional/AirbasePolice.lua +++ b/Moose Development/Moose/Functional/AirbasePolice.lua @@ -182,7 +182,7 @@ function AIRBASEPOLICE_BASE:_AirbaseMonitor() Client:SetState( self, "Warnings", SpeedingWarnings + 1 ) else MESSAGE:New( "Player " .. Client:GetPlayerName() .. " has been removed from the airbase, due to a speeding violation ...", 10, "Airbase Police" ):ToAll() - Client:GetGroup():Destroy() + Client:Destroy() Client:SetState( self, "Speeding", false ) Client:SetState( self, "Warnings", 0 ) end diff --git a/Moose Development/Moose/Tasking/CommandCenter.lua b/Moose Development/Moose/Tasking/CommandCenter.lua index 1f1eb735d..83b5d3581 100644 --- a/Moose Development/Moose/Tasking/CommandCenter.lua +++ b/Moose Development/Moose/Tasking/CommandCenter.lua @@ -81,7 +81,8 @@ function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName ) local PlayerUnit = EventData.IniUnit for MissionID, Mission in pairs( self:GetMissions() ) do local Mission = Mission -- Tasking.Mission#MISSION - Mission:JoinUnit( PlayerUnit ) + local PlayerGroup = EventData.IniGroup -- The GROUP object should be filled! + Mission:JoinUnit( PlayerUnit, PlayerGroup ) Mission:ReportDetails() end @@ -100,7 +101,8 @@ function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName ) local PlayerUnit = EventData.IniUnit for MissionID, Mission in pairs( self:GetMissions() ) do local Mission = Mission -- Tasking.Mission#MISSION - Mission:JoinUnit( PlayerUnit ) + local PlayerGroup = EventData.IniGroup -- The GROUP object should be filled! + Mission:JoinUnit( PlayerUnit, PlayerGroup ) Mission:ReportDetails() end end @@ -115,6 +117,7 @@ function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName ) 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 diff --git a/Moose Development/Moose/Tasking/Mission.lua b/Moose Development/Moose/Tasking/Mission.lua index 2566922a2..12fd65689 100644 --- a/Moose Development/Moose/Tasking/Mission.lua +++ b/Moose Development/Moose/Tasking/Mission.lua @@ -103,15 +103,16 @@ end -- 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 ) - self:F( { PlayerUnit = PlayerUnit } ) +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 ) then + if Task:JoinUnit( PlayerUnit, PlayerGroup ) then PlayerUnitAdded = true end end diff --git a/Moose Development/Moose/Tasking/Task.lua b/Moose Development/Moose/Tasking/Task.lua index 7b23f21b0..6a7af018b 100644 --- a/Moose Development/Moose/Tasking/Task.lua +++ b/Moose Development/Moose/Tasking/Task.lua @@ -234,14 +234,14 @@ end -- 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 ) - self:F( { PlayerUnit = PlayerUnit } ) +function TASK:JoinUnit( PlayerUnit, PlayerGroup ) + self:F( { PlayerUnit = PlayerUnit, PlayerGroup = PlayerGroup } ) local PlayerUnitAdded = false local PlayerGroups = self:GetGroups() - local PlayerGroup = PlayerUnit:GetGroup() -- Is the PlayerGroup part of the PlayerGroups? if PlayerGroups:IsIncludeObject( PlayerGroup ) then @@ -394,7 +394,6 @@ end -- @return #boolean function TASK:HasGroup( FindGroup ) - self:GetGroups():FilterOnce() -- Ensure that the filter is updated. return self:GetGroups():IsIncludeObject( FindGroup ) 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 31a028f31..add0090dd 100644 --- a/Moose Mission Setup/Moose Mission Update/l10n/DEFAULT/Moose.lua +++ b/Moose Mission Setup/Moose Mission Update/l10n/DEFAULT/Moose.lua @@ -1,5 +1,5 @@ env.info( '*** MOOSE STATIC INCLUDE START *** ' ) -env.info( 'Moose Generation Timestamp: 20170124_1109' ) +env.info( 'Moose Generation Timestamp: 20170203_2208' ) local base = _G Include = {} @@ -4211,6 +4211,7 @@ end EVENT = { ClassName = "EVENT", ClassID = 0, + SortedEvents = {}, } local _EVENTCODES = { @@ -4240,6 +4241,33 @@ local _EVENTCODES = { "S_EVENT_MAX", } +local _EVENTORDER = { + [world.event.S_EVENT_SHOT] = 1, + [world.event.S_EVENT_HIT] = 1, + [world.event.S_EVENT_TAKEOFF] = 1, + [world.event.S_EVENT_LAND] = 1, + [world.event.S_EVENT_CRASH] = -1, + [world.event.S_EVENT_EJECTION] = -1, + [world.event.S_EVENT_REFUELING] = 1, + [world.event.S_EVENT_DEAD] = -1, + [world.event.S_EVENT_PILOT_DEAD] = -1, + [world.event.S_EVENT_BASE_CAPTURED] = 1, + [world.event.S_EVENT_MISSION_START] = 1, + [world.event.S_EVENT_MISSION_END] = -1, + [world.event.S_EVENT_TOOK_CONTROL] = 1, + [world.event.S_EVENT_REFUELING_STOP] = 1, + [world.event.S_EVENT_BIRTH] = 1, + [world.event.S_EVENT_HUMAN_FAILURE] = 1, + [world.event.S_EVENT_ENGINE_STARTUP] = 1, + [world.event.S_EVENT_ENGINE_SHUTDOWN] = 1, + [world.event.S_EVENT_PLAYER_ENTER_UNIT] = 1, + [world.event.S_EVENT_PLAYER_LEAVE_UNIT] = -1, + [world.event.S_EVENT_PLAYER_COMMENT] = 1, + [world.event.S_EVENT_SHOOTING_START] = 1, + [world.event.S_EVENT_SHOOTING_END] = 1, + [world.event.S_EVENT_MAX] = 1, +} + --- The Event structure -- @type EVENTDATA -- @field id @@ -4292,7 +4320,7 @@ function EVENT:Init( EventID, 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" } ) - + self.SortedEvents[EventID] = setmetatable( {}, { __mode = "k" } ) end if not self.Events[EventID][EventClass] then @@ -4311,6 +4339,12 @@ function EVENT:Remove( EventClass, EventID ) local EventClass = EventClass self.Events[EventID][EventClass] = nil + + self.SortedEvents[EventID] = nil + self.SortedEvents[EventID] = {} + for EventClass, Event in pairs(self.Events[EventID]) do table.insert( self.SortedEvents[EventID], Event) end + table.sort( self.SortedEvents[EventID], function( Event1, Event2 ) return Event1.EventTime < Event2.EventTime end ) + end --- Clears all event subscriptions for a @{Core.Base#BASE} derived object. @@ -4322,6 +4356,7 @@ function EVENT:RemoveAll( EventObject ) local EventClass = EventObject:GetClassNameAndID() for EventID, EventData in pairs( self.Events ) do self.Events[EventID][EventClass] = nil + self.SortedEvents[EventID] = nil end end @@ -4355,6 +4390,13 @@ function EVENT:OnEventGeneric( EventFunction, EventClass, EventID ) local Event = self:Init( EventID, EventClass ) Event.EventFunction = EventFunction Event.EventClass = EventClass + Event.EventTime = EventClass.EventPriority and EventClass.EventPriority or 10 + + self.SortedEvents[EventID] = nil + self.SortedEvents[EventID] = {} + for EventClass, Event in pairs(self.Events[EventID]) do table.insert( self.SortedEvents[EventID], Event) end + table.sort( self.SortedEvents[EventID], function( Event1, Event2 ) return Event1.EventTime < Event2.EventTime end ) + return self end @@ -4931,6 +4973,8 @@ function EVENT:onEvent( Event ) 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 end if Event.target then @@ -4953,11 +4997,27 @@ function EVENT:onEvent( Event ) end self:E( { _EVENTCODES[Event.id], Event, Event.IniDCSUnitName, Event.TgtDCSUnitName } ) - -- Okay, we got the event from DCS. Now loop the self.Events[] 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] ) do + local function pairsByEventSorted( EventSorted, Order ) + local i = Order == -1 and #EventSorted or 0 + local iter = function() + i = i + Order + if EventSorted[i] == nil then + return nil + else + return EventSorted[i].EventClass, EventSorted[i] + end + end + return iter + end + + self:E( { Order = _EVENTORDER[Event.id] } ) + + -- 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 pairsByEventSorted( self.SortedEvents[Event.id], _EVENTORDER[Event.id] ) 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 - self:T( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName } ) + self:E( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName, EventData.EventTime } ) + Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) local Result, Value = xpcall( function() return EventData.IniUnit[Event.IniDCSUnitName].EventFunction( EventData.IniUnit[Event.IniDCSUnitName].EventClass, Event ) end, ErrorHandler ) --EventData.IniUnit[Event.IniDCSUnitName].EventFunction( EventData.IniUnit[Event.IniDCSUnitName].EventClass, Event ) else @@ -4965,7 +5025,8 @@ function EVENT:onEvent( Event ) -- 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 - self:T( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID() } ) + self:E( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID(), EventData.EventTime } ) + Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) local Result, Value = xpcall( function() return EventData.EventFunction( EventData.EventClass, Event ) end, ErrorHandler ) --EventData.EventFunction( EventData.EventClass, Event ) end @@ -5765,7 +5826,7 @@ do end end - self:F( { MenuGroup:GetName(), MenuText, ParentMenu.MenuPath } ) + --self:F( { MenuGroup:GetName(), MenuText, ParentMenu.MenuPath } ) return self end @@ -5844,7 +5905,7 @@ do end end - self:F( { MenuGroup:GetName(), MenuText, ParentMenu.MenuPath } ) + --self:F( { MenuGroup:GetName(), MenuText, ParentMenu.MenuPath } ) return self end @@ -6321,16 +6382,20 @@ end --- Returns a random location within the zone. -- @param #ZONE_RADIUS self +-- @param #number inner minimal distance from the center of the zone +-- @param #number outer minimal distance from the outer edge of the zone -- @return Dcs.DCSTypes#Vec2 The random location within the zone. -function ZONE_RADIUS:GetRandomVec2() - self:F( self.ZoneName ) +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() * self:GetRadius(); - Point.y = Vec2.y + math.sin( angle ) * math.random() * 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 } ) @@ -6757,6 +6822,8 @@ DATABASE = { CLIENTS = {}, AIRBASES = {}, NavPoints = {}, + EventPriority = 1, -- Used to sort the DCS event order processing (complicated). Database has highest priority. + } local _DATABASECoalition = @@ -6930,6 +6997,7 @@ end function DATABASE:AddGroup( GroupName ) if not self.GROUPS[GroupName] then + self:E( { "Add GROUP:", GroupName } ) self.GROUPS[GroupName] = GROUP:Register( GroupName ) end @@ -7271,6 +7339,8 @@ function DATABASE:_EventOnPlayerEnterUnit( Event ) self:F2( { Event } ) if Event.IniUnit 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 ) @@ -7721,6 +7791,7 @@ SET_BASE = { Filter = {}, Set = {}, List = {}, + EventPriority = 2, -- Used to sort the DCS event order processing (complicated) } --- Creates a new SET_BASE object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. @@ -22398,7 +22469,7 @@ function AIRBASEPOLICE_BASE:_AirbaseMonitor() Client:SetState( self, "Warnings", SpeedingWarnings + 1 ) else MESSAGE:New( "Player " .. Client:GetPlayerName() .. " has been removed from the airbase, due to a speeding violation ...", 10, "Airbase Police" ):ToAll() - Client:GetGroup():Destroy() + Client:Destroy() Client:SetState( self, "Speeding", false ) Client:SetState( self, "Warnings", 0 ) end @@ -24867,13 +24938,14 @@ AI_PATROL_ZONE = { -- @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 ) +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 @@ -24885,6 +24957,9 @@ function AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltit self.PatrolMinSpeed = PatrolMinSpeed self.PatrolMaxSpeed = PatrolMaxSpeed + -- defafult PatrolAltType to "RADIO" if not specified + self.PatrolAltType = PatrolAltType or "RADIO" + self:SetDetectionOn() self.CheckStatus = true @@ -25390,7 +25465,7 @@ function AI_PATROL_ZONE:onafterRoute( Controllable, From, Event, To ) local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) local ToPatrolZoneSpeed = self.PatrolMaxSpeed local CurrentRoutePoint = CurrentPointVec3:RoutePointAir( - POINT_VEC3.RoutePointAltType.BARO, + self.PatrolAltType, POINT_VEC3.RoutePointType.TakeOffParking, POINT_VEC3.RoutePointAction.FromParkingArea, ToPatrolZoneSpeed, @@ -25405,7 +25480,7 @@ function AI_PATROL_ZONE:onafterRoute( Controllable, From, Event, To ) local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) local ToPatrolZoneSpeed = self.PatrolMaxSpeed local CurrentRoutePoint = CurrentPointVec3:RoutePointAir( - POINT_VEC3.RoutePointAltType.BARO, + self.PatrolAltType, POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, ToPatrolZoneSpeed, @@ -25431,7 +25506,7 @@ function AI_PATROL_ZONE:onafterRoute( Controllable, From, Event, To ) --- Create a route point of type air. local ToTargetRoutePoint = ToTargetPointVec3:RoutePointAir( - POINT_VEC3.RoutePointAltType.BARO, + self.PatrolAltType, POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, ToTargetSpeed, @@ -25519,7 +25594,7 @@ function AI_PATROL_ZONE:onafterRTB() local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) local ToPatrolZoneSpeed = self.PatrolMaxSpeed local CurrentRoutePoint = CurrentPointVec3:RoutePointAir( - POINT_VEC3.RoutePointAltType.BARO, + self.PatrolAltType, POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, ToPatrolZoneSpeed, @@ -25709,12 +25784,13 @@ AI_CAS_ZONE = { -- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. -- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. -- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h. +-- @param Dcs.DCSTypes#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO -- @param Core.Zone#ZONE EngageZone -- @return #AI_CAS_ZONE self -function AI_CAS_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageZone ) +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 ) ) -- #AI_CAS_ZONE + local self = BASE:Inherit( self, AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_CAS_ZONE self.EngageZone = EngageZone self.Accomplished = false @@ -25953,7 +26029,7 @@ function AI_CAS_ZONE:onafterEngage( Controllable, From, Event, To ) local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) local ToEngageZoneSpeed = self.PatrolMaxSpeed local CurrentRoutePoint = CurrentPointVec3:RoutePointAir( - POINT_VEC3.RoutePointAltType.BARO, + self.PatrolAltType, POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, ToEngageZoneSpeed, @@ -25979,7 +26055,7 @@ function AI_CAS_ZONE:onafterEngage( Controllable, From, Event, To ) -- Create a route point of type air. local ToEngageZoneRoutePoint = ToEngageZonePointVec3:RoutePointAir( - POINT_VEC3.RoutePointAltType.BARO, + self.PatrolAltType, POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, ToEngageZoneSpeed, @@ -26006,7 +26082,7 @@ function AI_CAS_ZONE:onafterEngage( Controllable, From, Event, To ) --- Create a route point of type air. local ToTargetRoutePoint = ToTargetPointVec3:RoutePointAir( - POINT_VEC3.RoutePointAltType.BARO, + self.PatrolAltType, POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, ToTargetSpeed, @@ -26220,11 +26296,12 @@ AI_CAP_ZONE = { -- @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 ) +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 ) ) -- #AI_CAP_ZONE + local self = BASE:Inherit( self, AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_CAP_ZONE self.Accomplished = false self.Engaging = false @@ -26501,7 +26578,7 @@ function AI_CAP_ZONE:onafterEngage( Controllable, From, Event, To ) local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) local ToEngageZoneSpeed = self.PatrolMaxSpeed local CurrentRoutePoint = CurrentPointVec3:RoutePointAir( - POINT_VEC3.RoutePointAltType.BARO, + self.PatrolAltType, POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, ToEngageZoneSpeed, @@ -26525,7 +26602,7 @@ function AI_CAP_ZONE:onafterEngage( Controllable, From, Event, To ) --- Create a route point of type air. local ToPatrolRoutePoint = ToTargetPointVec3:RoutePointAir( - POINT_VEC3.RoutePointAltType.BARO, + self.PatrolAltType, POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, ToTargetSpeed, @@ -28739,7 +28816,8 @@ function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName ) local PlayerUnit = EventData.IniUnit for MissionID, Mission in pairs( self:GetMissions() ) do local Mission = Mission -- Tasking.Mission#MISSION - Mission:JoinUnit( PlayerUnit ) + local PlayerGroup = EventData.IniGroup -- The GROUP object should be filled! + Mission:JoinUnit( PlayerUnit, PlayerGroup ) Mission:ReportDetails() end @@ -28758,7 +28836,8 @@ function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName ) local PlayerUnit = EventData.IniUnit for MissionID, Mission in pairs( self:GetMissions() ) do local Mission = Mission -- Tasking.Mission#MISSION - Mission:JoinUnit( PlayerUnit ) + local PlayerGroup = EventData.IniGroup -- The GROUP object should be filled! + Mission:JoinUnit( PlayerUnit, PlayerGroup ) Mission:ReportDetails() end end @@ -28773,6 +28852,7 @@ function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName ) 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 @@ -29040,15 +29120,16 @@ end -- 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 ) - self:F( { PlayerUnit = PlayerUnit } ) +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 ) then + if Task:JoinUnit( PlayerUnit, PlayerGroup ) then PlayerUnitAdded = true end end @@ -30125,14 +30206,14 @@ end -- 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 ) - self:F( { PlayerUnit = PlayerUnit } ) +function TASK:JoinUnit( PlayerUnit, PlayerGroup ) + self:F( { PlayerUnit = PlayerUnit, PlayerGroup = PlayerGroup } ) local PlayerUnitAdded = false local PlayerGroups = self:GetGroups() - local PlayerGroup = PlayerUnit:GetGroup() -- Is the PlayerGroup part of the PlayerGroups? if PlayerGroups:IsIncludeObject( PlayerGroup ) then @@ -30285,7 +30366,6 @@ end -- @return #boolean function TASK:HasGroup( FindGroup ) - self:GetGroups():FilterOnce() -- Ensure that the filter is updated. return self:GetGroups():IsIncludeObject( FindGroup ) end @@ -30911,7 +30991,7 @@ end end -- Reporting --- This module contains the DETECTION_MANAGER class and derived classes. +--- This module contains the DETECTION_MANAGER class and derived classes. -- -- === -- diff --git a/Moose Mission Setup/Moose.lua b/Moose Mission Setup/Moose.lua index b55fff7e4..add0090dd 100644 --- a/Moose Mission Setup/Moose.lua +++ b/Moose Mission Setup/Moose.lua @@ -1,5 +1,5 @@ env.info( '*** MOOSE STATIC INCLUDE START *** ' ) -env.info( 'Moose Generation Timestamp: "20170202_2257"' ) +env.info( 'Moose Generation Timestamp: 20170203_2208' ) local base = _G Include = {} @@ -4211,6 +4211,7 @@ end EVENT = { ClassName = "EVENT", ClassID = 0, + SortedEvents = {}, } local _EVENTCODES = { @@ -4240,6 +4241,33 @@ local _EVENTCODES = { "S_EVENT_MAX", } +local _EVENTORDER = { + [world.event.S_EVENT_SHOT] = 1, + [world.event.S_EVENT_HIT] = 1, + [world.event.S_EVENT_TAKEOFF] = 1, + [world.event.S_EVENT_LAND] = 1, + [world.event.S_EVENT_CRASH] = -1, + [world.event.S_EVENT_EJECTION] = -1, + [world.event.S_EVENT_REFUELING] = 1, + [world.event.S_EVENT_DEAD] = -1, + [world.event.S_EVENT_PILOT_DEAD] = -1, + [world.event.S_EVENT_BASE_CAPTURED] = 1, + [world.event.S_EVENT_MISSION_START] = 1, + [world.event.S_EVENT_MISSION_END] = -1, + [world.event.S_EVENT_TOOK_CONTROL] = 1, + [world.event.S_EVENT_REFUELING_STOP] = 1, + [world.event.S_EVENT_BIRTH] = 1, + [world.event.S_EVENT_HUMAN_FAILURE] = 1, + [world.event.S_EVENT_ENGINE_STARTUP] = 1, + [world.event.S_EVENT_ENGINE_SHUTDOWN] = 1, + [world.event.S_EVENT_PLAYER_ENTER_UNIT] = 1, + [world.event.S_EVENT_PLAYER_LEAVE_UNIT] = -1, + [world.event.S_EVENT_PLAYER_COMMENT] = 1, + [world.event.S_EVENT_SHOOTING_START] = 1, + [world.event.S_EVENT_SHOOTING_END] = 1, + [world.event.S_EVENT_MAX] = 1, +} + --- The Event structure -- @type EVENTDATA -- @field id @@ -4292,7 +4320,7 @@ function EVENT:Init( EventID, 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" } ) - + self.SortedEvents[EventID] = setmetatable( {}, { __mode = "k" } ) end if not self.Events[EventID][EventClass] then @@ -4311,6 +4339,12 @@ function EVENT:Remove( EventClass, EventID ) local EventClass = EventClass self.Events[EventID][EventClass] = nil + + self.SortedEvents[EventID] = nil + self.SortedEvents[EventID] = {} + for EventClass, Event in pairs(self.Events[EventID]) do table.insert( self.SortedEvents[EventID], Event) end + table.sort( self.SortedEvents[EventID], function( Event1, Event2 ) return Event1.EventTime < Event2.EventTime end ) + end --- Clears all event subscriptions for a @{Core.Base#BASE} derived object. @@ -4322,6 +4356,7 @@ function EVENT:RemoveAll( EventObject ) local EventClass = EventObject:GetClassNameAndID() for EventID, EventData in pairs( self.Events ) do self.Events[EventID][EventClass] = nil + self.SortedEvents[EventID] = nil end end @@ -4355,6 +4390,13 @@ function EVENT:OnEventGeneric( EventFunction, EventClass, EventID ) local Event = self:Init( EventID, EventClass ) Event.EventFunction = EventFunction Event.EventClass = EventClass + Event.EventTime = EventClass.EventPriority and EventClass.EventPriority or 10 + + self.SortedEvents[EventID] = nil + self.SortedEvents[EventID] = {} + for EventClass, Event in pairs(self.Events[EventID]) do table.insert( self.SortedEvents[EventID], Event) end + table.sort( self.SortedEvents[EventID], function( Event1, Event2 ) return Event1.EventTime < Event2.EventTime end ) + return self end @@ -4931,6 +4973,8 @@ function EVENT:onEvent( Event ) 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 end if Event.target then @@ -4953,11 +4997,27 @@ function EVENT:onEvent( Event ) end self:E( { _EVENTCODES[Event.id], Event, Event.IniDCSUnitName, Event.TgtDCSUnitName } ) - -- Okay, we got the event from DCS. Now loop the self.Events[] 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] ) do + local function pairsByEventSorted( EventSorted, Order ) + local i = Order == -1 and #EventSorted or 0 + local iter = function() + i = i + Order + if EventSorted[i] == nil then + return nil + else + return EventSorted[i].EventClass, EventSorted[i] + end + end + return iter + end + + self:E( { Order = _EVENTORDER[Event.id] } ) + + -- 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 pairsByEventSorted( self.SortedEvents[Event.id], _EVENTORDER[Event.id] ) 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 - self:T( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName } ) + self:E( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName, EventData.EventTime } ) + Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) local Result, Value = xpcall( function() return EventData.IniUnit[Event.IniDCSUnitName].EventFunction( EventData.IniUnit[Event.IniDCSUnitName].EventClass, Event ) end, ErrorHandler ) --EventData.IniUnit[Event.IniDCSUnitName].EventFunction( EventData.IniUnit[Event.IniDCSUnitName].EventClass, Event ) else @@ -4965,7 +5025,8 @@ function EVENT:onEvent( Event ) -- 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 - self:T( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID() } ) + self:E( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID(), EventData.EventTime } ) + Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) local Result, Value = xpcall( function() return EventData.EventFunction( EventData.EventClass, Event ) end, ErrorHandler ) --EventData.EventFunction( EventData.EventClass, Event ) end @@ -6761,6 +6822,8 @@ DATABASE = { CLIENTS = {}, AIRBASES = {}, NavPoints = {}, + EventPriority = 1, -- Used to sort the DCS event order processing (complicated). Database has highest priority. + } local _DATABASECoalition = @@ -6934,6 +6997,7 @@ end function DATABASE:AddGroup( GroupName ) if not self.GROUPS[GroupName] then + self:E( { "Add GROUP:", GroupName } ) self.GROUPS[GroupName] = GROUP:Register( GroupName ) end @@ -7275,6 +7339,8 @@ function DATABASE:_EventOnPlayerEnterUnit( Event ) self:F2( { Event } ) if Event.IniUnit 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 ) @@ -7725,6 +7791,7 @@ SET_BASE = { Filter = {}, Set = {}, List = {}, + EventPriority = 2, -- Used to sort the DCS event order processing (complicated) } --- Creates a new SET_BASE object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. @@ -22402,7 +22469,7 @@ function AIRBASEPOLICE_BASE:_AirbaseMonitor() Client:SetState( self, "Warnings", SpeedingWarnings + 1 ) else MESSAGE:New( "Player " .. Client:GetPlayerName() .. " has been removed from the airbase, due to a speeding violation ...", 10, "Airbase Police" ):ToAll() - Client:GetGroup():Destroy() + Client:Destroy() Client:SetState( self, "Speeding", false ) Client:SetState( self, "Warnings", 0 ) end @@ -28749,7 +28816,8 @@ function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName ) local PlayerUnit = EventData.IniUnit for MissionID, Mission in pairs( self:GetMissions() ) do local Mission = Mission -- Tasking.Mission#MISSION - Mission:JoinUnit( PlayerUnit ) + local PlayerGroup = EventData.IniGroup -- The GROUP object should be filled! + Mission:JoinUnit( PlayerUnit, PlayerGroup ) Mission:ReportDetails() end @@ -28768,7 +28836,8 @@ function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName ) local PlayerUnit = EventData.IniUnit for MissionID, Mission in pairs( self:GetMissions() ) do local Mission = Mission -- Tasking.Mission#MISSION - Mission:JoinUnit( PlayerUnit ) + local PlayerGroup = EventData.IniGroup -- The GROUP object should be filled! + Mission:JoinUnit( PlayerUnit, PlayerGroup ) Mission:ReportDetails() end end @@ -28783,6 +28852,7 @@ function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName ) 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 @@ -29050,15 +29120,16 @@ end -- 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 ) - self:F( { PlayerUnit = PlayerUnit } ) +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 ) then + if Task:JoinUnit( PlayerUnit, PlayerGroup ) then PlayerUnitAdded = true end end @@ -30135,14 +30206,14 @@ end -- 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 ) - self:F( { PlayerUnit = PlayerUnit } ) +function TASK:JoinUnit( PlayerUnit, PlayerGroup ) + self:F( { PlayerUnit = PlayerUnit, PlayerGroup = PlayerGroup } ) local PlayerUnitAdded = false local PlayerGroups = self:GetGroups() - local PlayerGroup = PlayerUnit:GetGroup() -- Is the PlayerGroup part of the PlayerGroups? if PlayerGroups:IsIncludeObject( PlayerGroup ) then @@ -30295,7 +30366,6 @@ end -- @return #boolean function TASK:HasGroup( FindGroup ) - self:GetGroups():FilterOnce() -- Ensure that the filter is updated. return self:GetGroups():IsIncludeObject( FindGroup ) end diff --git a/Moose Test Missions/ABP - Airbase Police/APL-001 - Caucasus/APL-001 - Caucasus.miz b/Moose Test Missions/ABP - Airbase Police/APL-001 - Caucasus/APL-001 - Caucasus.miz index 3973875f271d14189e0f0ee754d3c7a170f4ea08..a03c8d2db5ddd5646b931098c447cc8f110a3fe5 100644 GIT binary patch delta 197869 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 _} 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∓9DECp$lZ7Kj6ij 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 Qu9r!-0~`j*umb=90Cx>q-~a#s diff --git a/Moose Test Missions/TSK - Task Modelling/TSK-010 - Task Modelling - SEAD/TSK-010 - Task Modelling - SEAD.miz b/Moose Test Missions/TSK - Task Modelling/TSK-010 - Task Modelling - SEAD/TSK-010 - Task Modelling - SEAD.miz index 8e56e629c4a41334ada7675c85754d71a0f7d837..4ba073b8454175daa125363058b8dccf35eb3323 100644 GIT binary patch literal 226501 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�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

};F<0u-`je_W|#a}O6eP?a_5puO8u%u=UNsqJ& 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 literal 231007 zcmZU419T?O@^)-H8{4++Y;4=MZ6_OJW81cEJK5NF^3VS6z2|)Qe(yQcr>3TRdir^v zs;=&-k&^-jK>+{)fB*mhzz29GkH40o{gya@0l>dC*5;0m=C(E$+SZP1l69ZIb$4%z z9m@>RJpgmtocSQ)aF+Z!fD(T3xJr>2t`-IOqwZi|Eqy+ft71$>7GP35lBZc+*Z>K7 zw6V}_@_ab)(YdxCn0P;So8o=m?NUGA9ldPn^8Wr}dMAE&eK&uPZeIakzLe25p$nr; z=%bs~KJwW=NBeYWcg0Pnx@s1){aV$=;a*?2hWqS0>)djy?*7@a49BAO@#s$i z)5kZnZMJ`Y))ab#4{!5d)$Qo`F($*CPT#4Ljm zQ%M(=Z=KEVeO|{=@#M~&?C-Z|eRKc^wg<~y zPR5+)XcyRpoqdRJ5JlaB(kRcdjr9O@YTQ2GRK8C{dycsbP}V($e`QsH$|2cqN0$uf zigvmMd=ud|`~~@bYzuex=2L_AoPp|tOXu1=zG#xIb+_uNL93!e{VY@?;J)JrOM89$ z)OV+srCFu&`|FgNb$a^pb?YvYIFwZAjOcLP34vd??{%058>mI=D&WJZcDsXs_BstA zb8>U)PIpT|qL{2N-)Z01^~3Y%f*@Cx6z<&(WG{r5c?qz3Z}!ghx%ca8a*axV1C^3s z%a5xpul&}yxl*ERW3lGd?kXkpIXq#_Lf;uJK(fi8v~s6P%}S+ZH&VU-H2_p(9nKM%;8%Gp#+}95BeKR5T5Hs&*t0A z&`Ba6p-uNooBAo=JI^ckvlp8kNRRLrStpjcb^jt$`YmibJCmW!?xot;9A`#6Ivw=# zjwHn2>%-%GJEVIl2Od{1c+)`becIUOh6rT;M$V)bOHc68)fMCNyv9385Cs7R73U z!9?|Ks%0uh_AV33wz#49=e-ck7BuP0_PD2r60i`#>≫X9pD}5ZR-^dEOW21r;R| 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%0An62Qz4LGTX&&id1_Gw4k7ZC71WWG>nX;5e| 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

  • ?Z*BG8|Rik3FUh>=g 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 diff --git a/Moose Test Missions/TSK - Task Modelling/TSK-020 - Task Modelling - Pickup/TSK-020 - Task Modelling - Pickup.miz b/Moose Test Missions/TSK - Task Modelling/TSK-020 - Task Modelling - Pickup/TSK-020 - Task Modelling - Pickup.miz index e7f90ff5be7ed9f790f11011b488a19c25ebeeed..84ecb897a072092cc564982fce17228cfe0f9363 100644 GIT binary patch delta 195824 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

    +JWd51Od1I*v`#jJdm30{#lDv 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%0D5Q1iXjc3N= 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 From be4d51237bf403fdbfd8a0115391f08509eaf67c Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sat, 4 Feb 2017 11:35:58 +0100 Subject: [PATCH 2/3] Improved the event handling -- Removed the sort -- Implemented the order in the existing table. -- 5 levels -- _DATABASE = 1 -- SET and SET_ derived classes = 2 -- UNIT = 3 -- GROUP = 4 -- all other = 5 --- Moose Development/Moose/Core/Base.lua | 21 + Moose Development/Moose/Core/Database.lua | 4 +- Moose Development/Moose/Core/Event.lua | 81 +- Moose Development/Moose/Core/Set.lua | 3 +- Moose Development/Moose/Wrapper/Group.lua | 2 + Moose Development/Moose/Wrapper/Unit.lua | 2 + .../l10n/DEFAULT/Moose.lua | 31744 +--------------- Moose Mission Setup/Moose.lua | 31744 +--------------- .../DET-001 - Detection Areas.miz | Bin 218906 -> 25089 bytes .../TSK-010 - Task Modelling - SEAD.miz | Bin 226501 -> 233956 bytes 10 files changed, 98 insertions(+), 63503 deletions(-) diff --git a/Moose Development/Moose/Core/Base.lua b/Moose Development/Moose/Core/Base.lua index 6bb7aa043..c5fe0394c 100644 --- a/Moose Development/Moose/Core/Base.lua +++ b/Moose Development/Moose/Core/Base.lua @@ -181,6 +181,7 @@ local _ClassID = 0 BASE = { ClassName = "BASE", ClassID = 0, + _Private = {}, Events = {}, States = {} } @@ -287,6 +288,26 @@ function BASE:GetClassID() return self.ClassID end +--- Get the Class @{Core.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 @{Core.Event} processing Priority. +function BASE:GetEventPriority() + return self._Private.EventPriority or 5 +end + +--- Set the Class @{Core.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 @{Core.Event} processing Priority. +-- @return self +function BASE:SetEventPriority( EventPriority ) + self._Private.EventPriority = EventPriority +end + + --- Set a new listener for the class. -- @param self -- @param Dcs.DCSTypes#Event Event diff --git a/Moose Development/Moose/Core/Database.lua b/Moose Development/Moose/Core/Database.lua index 4eecd065f..1277a1ba4 100644 --- a/Moose Development/Moose/Core/Database.lua +++ b/Moose Development/Moose/Core/Database.lua @@ -55,8 +55,6 @@ DATABASE = { CLIENTS = {}, AIRBASES = {}, NavPoints = {}, - EventPriority = 1, -- Used to sort the DCS event order processing (complicated). Database has highest priority. - } local _DATABASECoalition = @@ -102,6 +100,8 @@ function DATABASE:New() self:_RegisterPlayers() self:_RegisterAirbases() + self:SetEventPriority( 1 ) + return self end diff --git a/Moose Development/Moose/Core/Event.lua b/Moose Development/Moose/Core/Event.lua index 5aede04a6..6f2c82d4b 100644 --- a/Moose Development/Moose/Core/Event.lua +++ b/Moose Development/Moose/Core/Event.lua @@ -21,7 +21,6 @@ EVENT = { ClassName = "EVENT", ClassID = 0, - SortedEvents = {}, } local _EVENTCODES = { @@ -122,6 +121,7 @@ end --- Initializes the Events structure for the event -- @param #EVENT self -- @param Dcs.DCSWorld#world.event EventID +-- @param #number EventPriority The priority of the EventClass. -- @param Core.Base#BASE EventClass -- @return #EVENT.Events function EVENT:Init( EventID, EventClass ) @@ -130,8 +130,13 @@ function EVENT:Init( EventID, 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" } ) - self.SortedEvents[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][EventClass] then self.Events[EventID][EventClass] = setmetatable( {}, { __mode = "k" } ) @@ -148,13 +153,8 @@ function EVENT:Remove( EventClass, EventID ) self:F3( { EventClass, _EVENTCODES[EventID] } ) local EventClass = EventClass - self.Events[EventID][EventClass] = nil - - self.SortedEvents[EventID] = nil - self.SortedEvents[EventID] = {} - for EventClass, Event in pairs(self.Events[EventID]) do table.insert( self.SortedEvents[EventID], Event) end - table.sort( self.SortedEvents[EventID], function( Event1, Event2 ) return Event1.EventTime < Event2.EventTime end ) - + local EventPriority = EventClass:GetEventPriority() + self.Events[EventID][EventPriority][EventClass] = nil end --- Clears all event subscriptions for a @{Core.Base#BASE} derived object. @@ -164,9 +164,9 @@ 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][EventClass] = nil - self.SortedEvents[EventID] = nil + self.Events[EventID][EventPriority][EventClass] = nil end end @@ -200,12 +200,6 @@ function EVENT:OnEventGeneric( EventFunction, EventClass, EventID ) local Event = self:Init( EventID, EventClass ) Event.EventFunction = EventFunction Event.EventClass = EventClass - Event.EventTime = EventClass.EventPriority and EventClass.EventPriority or 10 - - self.SortedEvents[EventID] = nil - self.SortedEvents[EventID] = {} - for EventClass, Event in pairs(self.Events[EventID]) do table.insert( self.SortedEvents[EventID], Event) end - table.sort( self.SortedEvents[EventID], function( Event1, Event2 ) return Event1.EventTime < Event2.EventTime end ) return self end @@ -807,38 +801,33 @@ function EVENT:onEvent( Event ) end self:E( { _EVENTCODES[Event.id], Event, Event.IniDCSUnitName, Event.TgtDCSUnitName } ) - local function pairsByEventSorted( EventSorted, Order ) - local i = Order == -1 and #EventSorted or 0 - local iter = function() - i = i + Order - if EventSorted[i] == nil then - return nil - else - return EventSorted[i].EventClass, EventSorted[i] - end - end - return iter - end + local Order = _EVENTORDER[Event.id] + self:E( { Order = Order } ) - self:E( { Order = _EVENTORDER[Event.id] } ) + for EventPriority = Order == -1 and 5 or 1, Order == -1 and 1 or 5, Order do - -- 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 pairsByEventSorted( self.SortedEvents[Event.id], _EVENTORDER[Event.id] ) 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 - self:E( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName, EventData.EventTime } ) - Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) - local Result, Value = xpcall( function() return EventData.IniUnit[Event.IniDCSUnitName].EventFunction( EventData.IniUnit[Event.IniDCSUnitName].EventClass, Event ) end, ErrorHandler ) - --EventData.IniUnit[Event.IniDCSUnitName].EventFunction( EventData.IniUnit[Event.IniDCSUnitName].EventClass, Event ) - 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 - self:E( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID(), EventData.EventTime } ) + 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 + self:E( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName } ) Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) - local Result, Value = xpcall( function() return EventData.EventFunction( EventData.EventClass, Event ) end, ErrorHandler ) - --EventData.EventFunction( EventData.EventClass, Event ) + local Result, Value = xpcall( function() return EventData.IniUnit[Event.IniDCSUnitName].EventFunction( EventData.IniUnit[Event.IniDCSUnitName].EventClass, Event ) end, ErrorHandler ) + --EventData.IniUnit[Event.IniDCSUnitName].EventFunction( EventData.IniUnit[Event.IniDCSUnitName].EventClass, Event ) + 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 + self:E( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID() } ) + Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) + local Result, Value = xpcall( function() return EventData.EventFunction( EventData.EventClass, Event ) end, ErrorHandler ) + --EventData.EventFunction( EventData.EventClass, Event ) + end + end end end end diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index 9d2b167c3..1309c543c 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -240,7 +240,6 @@ SET_BASE = { Filter = {}, Set = {}, List = {}, - EventPriority = 2, -- Used to sort the DCS event order processing (complicated) } --- Creates a new SET_BASE object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. @@ -265,6 +264,8 @@ function SET_BASE:New( Database ) self.CallScheduler = SCHEDULER:New( self ) + self:SetEventPriority( 2 ) + return self end diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index 84a9d2912..bcd0027f8 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -113,6 +113,8 @@ function GROUP:Register( GroupName ) local self = BASE:Inherit( self, CONTROLLABLE:New( GroupName ) ) self:F2( GroupName ) self.GroupName = GroupName + + self:SetEventPriority( 4 ) return self end diff --git a/Moose Development/Moose/Wrapper/Unit.lua b/Moose Development/Moose/Wrapper/Unit.lua index 2b9d30b89..7f0b430b8 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -97,6 +97,8 @@ UNIT = { function UNIT:Register( UnitName ) local self = BASE:Inherit( self, CONTROLLABLE:New( UnitName ) ) self.UnitName = UnitName + + self:SetEventPriority( 3 ) return self 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 add0090dd..da1821733 100644 --- a/Moose Mission Setup/Moose Mission Update/l10n/DEFAULT/Moose.lua +++ b/Moose Mission Setup/Moose Mission Update/l10n/DEFAULT/Moose.lua @@ -1,31741 +1,31 @@ -env.info( '*** MOOSE STATIC INCLUDE START *** ' ) -env.info( 'Moose Generation Timestamp: 20170203_2208' ) +env.info( '*** MOOSE DYNAMIC INCLUDE START *** ' ) +env.info( 'Moose Generation Timestamp: 20170203_2219' ) + 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 @{Core.Base#BASE.New) constructor within the @{Core.Base#BASE.Inherit) method. --- See an example at the @{Core.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. --- Therefore, the BASE class exposes the following event handling functions: --- --- * @{#BASE.EventOnBirth}(): Handle the birth of a new unit. --- * @{#BASE.EventOnBaseCaptured}(): Handle the capturing of an airbase or a helipad. --- * @{#BASE.EventOnCrash}(): Handle the crash of a unit. --- * @{#BASE.EventOnDead}(): Handle the death of a unit. --- * @{#BASE.EventOnEjection}(): Handle the ejection of a player out of an airplane. --- * @{#BASE.EventOnEngineShutdown}(): Handle the shutdown of an engine. --- * @{#BASE.EventOnEngineStartup}(): Handle the startup of an engine. --- * @{#BASE.EventOnHit}(): Handle the hit of a shell to a unit. --- * @{#BASE.EventOnHumanFailure}(): No a clue ... --- * @{#BASE.EventOnLand}(): Handle the event when a unit lands. --- * @{#BASE.EventOnMissionStart}(): Handle the start of the mission. --- * @{#BASE.EventOnPilotDead}(): Handle the event when a pilot is dead. --- * @{#BASE.EventOnPlayerComment}(): Handle the event when a player posts a comment. --- * @{#BASE.EventOnPlayerEnterUnit}(): Handle the event when a player enters a unit. --- * @{#BASE.EventOnPlayerLeaveUnit}(): Handle the event when a player leaves a unit. --- * @{#BASE.EventOnBirthPlayerMissionEnd}(): Handle the event when a player ends the mission. (Not a clue what that does). --- * @{#BASE.EventOnRefueling}(): Handle the event when a unit is refueling. --- * @{#BASE.EventOnShootingEnd}(): Handle the event when a unit starts shooting (guns). --- * @{#BASE.EventOnShootingStart}(): Handle the event when a unit ends shooting (guns). --- * @{#BASE.EventOnShot}(): Handle the event when a unit shot a missile. --- * @{#BASE.EventOnTakeOff}(): Handle the event when a unit takes off from a runway. --- * @{#BASE.EventOnTookControl}(): Handle the event when a player takes control of a unit. --- --- The EventOn() methods provide the @{Core.Event#EVENTDATA} structure to the event handling function. --- The @{Core.Event#EVENTDATA} structure contains an enriched data set of information about the event being handled. --- --- Find below an example of the prototype how to write an event handling function: --- --- CommandCenter:EventOnPlayerEnterUnit( --- --- @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 --- Mission:JoinUnit( PlayerUnit ) --- Mission:ReportDetails() --- end --- end --- ) --- --- Note the function( self, EventData ). It takes two parameters: --- --- * self = the object that is handling the EventOnPlayerEnterUnit. --- * EventData = the @{Core.Event#EVENTDATA} structure, containing more information of the Event. --- --- ## 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, - 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 - ---- Set a new listener for the class. --- @param self --- @param Dcs.DCSTypes#Event Event --- @param #function EventFunction --- @return #BASE -function BASE:AddEvent( Event, EventFunction ) - self:F( Event ) - - self.Events[#self.Events+1] = {} - self.Events[#self.Events].Event = Event - self.Events[#self.Events].EventFunction = EventFunction - self.Events[#self.Events].EventEnabled = false - - return self -end - ---- Returns the event dispatcher --- @param #BASE self --- @return Core.Event#EVENT -function BASE:Event() - - return _EVENTDISPATCHER -end - ---- Remove all subscribed events --- @param #BASE self --- @return #BASE -function BASE:EventRemoveAll() - - _EVENTDISPATCHER:RemoveAll( self ) - - return self -end - ---- Subscribe to a S_EVENT\_SHOT event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnShot( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_SHOT ) - - return self -end - ---- Subscribe to a S_EVENT\_HIT event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnHit( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_HIT ) - - return self -end - ---- Subscribe to a S_EVENT\_TAKEOFF event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnTakeOff( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_TAKEOFF ) - - return self -end - ---- Subscribe to a S_EVENT\_LAND event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnLand( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_LAND ) - - return self -end - ---- Subscribe to a S_EVENT\_CRASH event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnCrash( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_CRASH ) - - return self -end - ---- Subscribe to a S_EVENT\_EJECTION event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnEjection( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_EJECTION ) - - return self -end - - ---- Subscribe to a S_EVENT\_REFUELING event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnRefueling( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_REFUELING ) - - return self -end - ---- Subscribe to a S_EVENT\_DEAD event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnDead( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_DEAD ) - - return self -end - ---- Subscribe to a S_EVENT_PILOT\_DEAD event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnPilotDead( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_PILOT_DEAD ) - - return self -end - ---- Subscribe to a S_EVENT_BASE\_CAPTURED event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnBaseCaptured( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_BASE_CAPTURED ) - - return self -end - ---- Subscribe to a S_EVENT_MISSION\_START event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnMissionStart( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_MISSION_START ) - - return self -end - ---- Subscribe to a S_EVENT_MISSION\_END event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnPlayerMissionEnd( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_MISSION_END ) - - return self -end - ---- Subscribe to a S_EVENT_TOOK\_CONTROL event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnTookControl( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_TOOK_CONTROL ) - - return self -end - ---- Subscribe to a S_EVENT_REFUELING\_STOP event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnRefuelingStop( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_REFUELING_STOP ) - - return self -end - ---- Subscribe to a S_EVENT\_BIRTH event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnBirth( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_BIRTH ) - - return self -end - ---- Subscribe to a S_EVENT_HUMAN\_FAILURE event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnHumanFailure( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_HUMAN_FAILURE ) - - return self -end - ---- Subscribe to a S_EVENT_ENGINE\_STARTUP event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnEngineStartup( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_ENGINE_STARTUP ) - - return self -end - ---- Subscribe to a S_EVENT_ENGINE\_SHUTDOWN event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnEngineShutdown( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_ENGINE_SHUTDOWN ) - - return self -end - ---- Subscribe to a S_EVENT_PLAYER_ENTER\_UNIT event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnPlayerEnterUnit( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_PLAYER_ENTER_UNIT ) - - return self -end - ---- Subscribe to a S_EVENT_PLAYER_LEAVE\_UNIT event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnPlayerLeaveUnit( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_PLAYER_LEAVE_UNIT ) - - return self -end - ---- Subscribe to a S_EVENT_PLAYER\_COMMENT event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnPlayerComment( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_PLAYER_COMMENT ) - - return self -end - ---- Subscribe to a S_EVENT_SHOOTING\_START event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnShootingStart( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_SHOOTING_START ) - - return self -end - ---- Subscribe to a S_EVENT_SHOOTING\_END event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnShootingEnd( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_SHOOTING_END ) - - return self -end - - - - - - - ---- Enable the event listeners for the class. --- @param #BASE self --- @return #BASE -function BASE:EnableEvents() - self:F( #self.Events ) - - for EventID, Event in pairs( self.Events ) do - Event.Self = self - Event.EventEnabled = true - end - self.Events.Handler = world.addEventHandler( self ) - - return self -end - - ---- Disable the event listeners for the class. --- @param #BASE self --- @return #BASE -function BASE:DisableEvents() - self:F() - - world.removeEventHandler( self ) - for EventID, Event in pairs( self.Events ) do - Event.Self = nil - Event.EventEnabled = false - end - - return self -end - - -local BaseEventCodes = { - "S_EVENT_SHOT", - "S_EVENT_HIT", - "S_EVENT_TAKEOFF", - "S_EVENT_LAND", - "S_EVENT_CRASH", - "S_EVENT_EJECTION", - "S_EVENT_REFUELING", - "S_EVENT_DEAD", - "S_EVENT_PILOT_DEAD", - "S_EVENT_BASE_CAPTURED", - "S_EVENT_MISSION_START", - "S_EVENT_MISSION_END", - "S_EVENT_TOOK_CONTROL", - "S_EVENT_REFUELING_STOP", - "S_EVENT_BIRTH", - "S_EVENT_HUMAN_FAILURE", - "S_EVENT_ENGINE_STARTUP", - "S_EVENT_ENGINE_SHUTDOWN", - "S_EVENT_PLAYER_ENTER_UNIT", - "S_EVENT_PLAYER_LEAVE_UNIT", - "S_EVENT_PLAYER_COMMENT", - "S_EVENT_SHOOTING_START", - "S_EVENT_SHOOTING_END", - "S_EVENT_MAX", -} - ---onEvent( {[1]="S_EVENT_BIRTH",[2]={["subPlace"]=5,["time"]=0,["initiator"]={["id_"]=16884480,},["place"]={["id_"]=5000040,},["id"]=15,["IniUnitName"]="US F-15C@RAMP-Air Support Mountains#001-01",},} --- Event = { --- id = enum world.event, --- time = Time, --- initiator = Unit, --- target = Unit, --- place = Unit, --- subPlace = enum world.BirthPlace, --- weapon = Weapon --- } - ---- Creation of a Birth Event. --- @param #BASE self --- @param 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) @{Core.Scheduler#SCHEDULER} class, extends @{Core.Base#BASE} --- --- The @{Core.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: --- --- * @{Core.Scheduler#SCHEDULER.New}( nil ): Setup a new SCHEDULER object, which is persistently executed after garbage collection. --- * @{Core.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. --- * @{Core.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. --- * @{Core.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: --- --- * @{Core.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. --- * @{Core.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 @{Core.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 @{Core.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 module contains the EVENT class. --- --- === --- --- Takes care of EVENT dispatching between DCS events and event handling functions defined in MOOSE classes. --- --- === --- --- The above menus classes **are derived** from 2 main **abstract** classes defined within the MOOSE framework (so don't use these): --- --- === --- --- ### Contributions: - --- ### Authors: FlightControl : Design & Programming --- --- @module Event - ---- The EVENT structure --- @type EVENT --- @field #EVENT.Events Events -EVENT = { - ClassName = "EVENT", - ClassID = 0, - SortedEvents = {}, -} - -local _EVENTCODES = { - "S_EVENT_SHOT", - "S_EVENT_HIT", - "S_EVENT_TAKEOFF", - "S_EVENT_LAND", - "S_EVENT_CRASH", - "S_EVENT_EJECTION", - "S_EVENT_REFUELING", - "S_EVENT_DEAD", - "S_EVENT_PILOT_DEAD", - "S_EVENT_BASE_CAPTURED", - "S_EVENT_MISSION_START", - "S_EVENT_MISSION_END", - "S_EVENT_TOOK_CONTROL", - "S_EVENT_REFUELING_STOP", - "S_EVENT_BIRTH", - "S_EVENT_HUMAN_FAILURE", - "S_EVENT_ENGINE_STARTUP", - "S_EVENT_ENGINE_SHUTDOWN", - "S_EVENT_PLAYER_ENTER_UNIT", - "S_EVENT_PLAYER_LEAVE_UNIT", - "S_EVENT_PLAYER_COMMENT", - "S_EVENT_SHOOTING_START", - "S_EVENT_SHOOTING_END", - "S_EVENT_MAX", -} - -local _EVENTORDER = { - [world.event.S_EVENT_SHOT] = 1, - [world.event.S_EVENT_HIT] = 1, - [world.event.S_EVENT_TAKEOFF] = 1, - [world.event.S_EVENT_LAND] = 1, - [world.event.S_EVENT_CRASH] = -1, - [world.event.S_EVENT_EJECTION] = -1, - [world.event.S_EVENT_REFUELING] = 1, - [world.event.S_EVENT_DEAD] = -1, - [world.event.S_EVENT_PILOT_DEAD] = -1, - [world.event.S_EVENT_BASE_CAPTURED] = 1, - [world.event.S_EVENT_MISSION_START] = 1, - [world.event.S_EVENT_MISSION_END] = -1, - [world.event.S_EVENT_TOOK_CONTROL] = 1, - [world.event.S_EVENT_REFUELING_STOP] = 1, - [world.event.S_EVENT_BIRTH] = 1, - [world.event.S_EVENT_HUMAN_FAILURE] = 1, - [world.event.S_EVENT_ENGINE_STARTUP] = 1, - [world.event.S_EVENT_ENGINE_SHUTDOWN] = 1, - [world.event.S_EVENT_PLAYER_ENTER_UNIT] = 1, - [world.event.S_EVENT_PLAYER_LEAVE_UNIT] = -1, - [world.event.S_EVENT_PLAYER_COMMENT] = 1, - [world.event.S_EVENT_SHOOTING_START] = 1, - [world.event.S_EVENT_SHOOTING_END] = 1, - [world.event.S_EVENT_MAX] = 1, -} - ---- The Event structure --- @type EVENTDATA --- @field id --- @field initiator --- @field target --- @field weapon --- @field IniDCSUnit --- @field IniDCSUnitName --- @field Wrapper.Unit#UNIT IniUnit --- @field #string IniUnitName --- @field IniDCSGroup --- @field IniDCSGroupName --- @field TgtDCSUnit --- @field TgtDCSUnitName --- @field Wrapper.Unit#UNIT TgtUnit --- @field #string TgtUnitName --- @field TgtDCSGroup --- @field TgtDCSGroupName --- @field Weapon --- @field WeaponName --- @field WeaponTgtDCSUnit - ---- The Events structure --- @type EVENT.Events --- @field #number IniUnit - -function EVENT:New() - local self = BASE:Inherit( self, BASE:New() ) - self:F2() - self.EventHandler = world.addEventHandler( self ) - return self -end - -function EVENT:EventText( EventID ) - - local EventText = _EVENTCODES[EventID] - - return EventText -end - - ---- Initializes the Events structure for the event --- @param #EVENT self --- @param Dcs.DCSWorld#world.event EventID --- @param Core.Base#BASE EventClass --- @return #EVENT.Events -function EVENT:Init( EventID, EventClass ) - self:F3( { _EVENTCODES[EventID], 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" } ) - self.SortedEvents[EventID] = setmetatable( {}, { __mode = "k" } ) - end - - if not self.Events[EventID][EventClass] then - self.Events[EventID][EventClass] = setmetatable( {}, { __mode = "k" } ) - end - return self.Events[EventID][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, _EVENTCODES[EventID] } ) - - local EventClass = EventClass - self.Events[EventID][EventClass] = nil - - self.SortedEvents[EventID] = nil - self.SortedEvents[EventID] = {} - for EventClass, Event in pairs(self.Events[EventID]) do table.insert( self.SortedEvents[EventID], Event) end - table.sort( self.SortedEvents[EventID], function( Event1, Event2 ) return Event1.EventTime < Event2.EventTime end ) - -end - ---- Clears all event subscriptions for a @{Core.Base#BASE} derived object. --- @param #EVENT self --- @param Core.Base#BASE EventObject -function EVENT:RemoveAll( EventObject ) - self:F3( { EventObject:GetClassNameAndID() } ) - - local EventClass = EventObject:GetClassNameAndID() - for EventID, EventData in pairs( self.Events ) do - self.Events[EventID][EventClass] = nil - self.SortedEvents[EventID] = 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 - Event.EventTime = EventClass.EventPriority and EventClass.EventPriority or 10 - - self.SortedEvents[EventID] = nil - self.SortedEvents[EventID] = {} - for EventClass, Event in pairs(self.Events[EventID]) do table.insert( self.SortedEvents[EventID], Event) end - table.sort( self.SortedEvents[EventID], function( Event1, Event2 ) return Event1.EventTime < Event2.EventTime end ) - - 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 and Event.initiator:getCategory() == Object.Category.UNIT then - Event.IniDCSUnit = Event.initiator - Event.IniDCSGroup = Event.IniDCSUnit:getGroup() - Event.IniDCSUnitName = Event.IniDCSUnit:getName() - Event.IniUnitName = Event.IniDCSUnitName - Event.IniUnit = UNIT:FindByName( Event.IniDCSUnitName ) - 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 - end - if Event.target then - if Event.target and Event.target:getCategory() == Object.Category.UNIT then - Event.TgtDCSUnit = Event.target - Event.TgtDCSGroup = Event.TgtDCSUnit:getGroup() - Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() - Event.TgtUnitName = Event.TgtDCSUnitName - Event.TgtUnit = UNIT:FindByName( Event.TgtDCSUnitName ) - Event.TgtDCSGroupName = "" - if Event.TgtDCSGroup and Event.TgtDCSGroup:isExist() then - Event.TgtDCSGroupName = Event.TgtDCSGroup:getName() - end - end - end - if Event.weapon then - Event.Weapon = Event.weapon - Event.WeaponName = Event.Weapon:getTypeName() - --Event.WeaponTgtDCSUnit = Event.Weapon:getTarget() - end - self:E( { _EVENTCODES[Event.id], Event, Event.IniDCSUnitName, Event.TgtDCSUnitName } ) - - local function pairsByEventSorted( EventSorted, Order ) - local i = Order == -1 and #EventSorted or 0 - local iter = function() - i = i + Order - if EventSorted[i] == nil then - return nil - else - return EventSorted[i].EventClass, EventSorted[i] - end - end - return iter - end - - self:E( { Order = _EVENTORDER[Event.id] } ) - - -- 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 pairsByEventSorted( self.SortedEvents[Event.id], _EVENTORDER[Event.id] ) 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 - self:E( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName, EventData.EventTime } ) - Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) - local Result, Value = xpcall( function() return EventData.IniUnit[Event.IniDCSUnitName].EventFunction( EventData.IniUnit[Event.IniDCSUnitName].EventClass, Event ) end, ErrorHandler ) - --EventData.IniUnit[Event.IniDCSUnitName].EventFunction( EventData.IniUnit[Event.IniDCSUnitName].EventClass, Event ) - 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 - self:E( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID(), EventData.EventTime } ) - Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) - local Result, Value = xpcall( function() return EventData.EventFunction( EventData.EventClass, Event ) end, ErrorHandler ) - --EventData.EventFunction( EventData.EventClass, Event ) - end - end - end - end - else - self:E( { _EVENTCODES[Event.id], Event } ) - end -end - ---- This module contains the MENU classes. --- --- There is a small note... When you see a class like MENU_COMMAND_COALITION with COMMAND in italic, it acutally represents it like this: `MENU_COMMAND_COALITION`. --- --- === --- --- 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_**: --- --- * @{Core.Menu#MENU_MISSION}: Manages main menus for whole mission file. --- * @{Core.Menu#MENU_COALITION}: Manages main menus for whole coalition. --- * @{Core.Menu#MENU_GROUP}: Manages main menus for GROUPs. --- * @{Core.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_**: --- --- * @{Core.Menu#MENU_MISSION_COMMAND}: Manages command menus for whole mission file. --- * @{Core.Menu#MENU_COALITION_COMMAND}: Manages command menus for whole coalition. --- * @{Core.Menu#MENU_GROUP_COMMAND}: Manages command menus for GROUPs. --- * @{Core.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) @{Core.Menu#MENU_BASE} class, extends @{Core.Base#BASE} --- -------------------------------------------------- --- The @{#MENU_BASE} class defines the main MENU class where other MENU classes are derived from. --- --- 1.2) @{Core.Menu#MENU_COMMAND_BASE} class, extends @{Core.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#MENU_MISSION} class, extends @{Core.Menu#MENU_BASE} --- --------------------------------------------------------- --- The @{Core.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#MENU_MISSION_COMMAND} class, extends @{Core.Menu#MENU_COMMAND_BASE} --- ------------------------------------------------------------------------- --- The @{Core.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#MENU_COALITION} class, extends @{Core.Menu#MENU_BASE} --- ------------------------------------------------------------ --- The @{Core.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 @{Core.Menu#MENU_COMMAND_BASE} --- ---------------------------------------------------------------------------- --- The @{Core.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 @{Core.Menu#MENU_BASE} --- -------------------------------------------------------- --- The @{Core.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 @{Core.Menu#MENU_COMMAND_BASE} --- ------------------------------------------------------------------------ --- The @{Core.Menu#MENU_GROUP_COMMAND} class manages the command menus for coalitions, which allow players to execute functions during mission execution. --- You can add menus with the @{#MENU_GROUP_COMMAND.New} method, which constructs a MENU_GROUP_COMMAND object and returns you the object reference. --- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_GROUP_COMMAND.Remove}. --- --- === --- --- 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 @{Core.Menu#MENU_BASE} --- --------------------------------------------------------- --- The @{Core.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 @{Core.Menu#MENU_COMMAND_BASE} --- ------------------------------------------------------------------------- --- The @{Core.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. - -- @param CommandMenuArgument An argument for the function. - -- @return Menu#MENU_CLIENT_COMMAND self - function MENU_CLIENT_COMMAND:New( MenuClient, 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 = MenuClient - self.MenuClientGroupID = MenuClient:GetClientGroupID() - self.MenuParentPath = MenuParentPath - self.MenuText = MenuText - self.ParentMenu = ParentMenu - - if not _MENUCLIENTS[self.MenuClientGroupID] then - _MENUCLIENTS[self.MenuClientGroupID] = {} - end - - local MenuPath = _MENUCLIENTS[self.MenuClientGroupID] - - self:T( { MenuClient:GetClientGroupName(), MenuPath[table.concat(MenuParentPath)], MenuParentPath, MenuText, CommandMenuFunction, 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 - - ParentMenu.Menus[self.MenuPath] = self - - 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 module contains the ZONE classes, inherited from @{Core.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 @{Wrapper.Group#GROUP} objects). --- * Test if not in the zone. --- * Distance to the nearest intersecting point of the zone. --- * Distance to the center of the zone. --- * ... --- --- Each of these ZONE classes have a zone name, and specific parameters defining the zone type: --- --- * @{Core.Zone#ZONE_BASE}: The ZONE_BASE class defining the base for all other zone classes. --- * @{Core.Zone#ZONE_RADIUS}: The ZONE_RADIUS class defined by a zone name, a location and a radius. --- * @{Core.Zone#ZONE}: The ZONE class, defined by the zone name as defined within the Mission Editor. --- * @{Core.Zone#ZONE_UNIT}: The ZONE_UNIT class defines by a zone around a @{Wrapper.Unit#UNIT} with a radius. --- * @{Core.Zone#ZONE_GROUP}: The ZONE_GROUP class defines by a zone around a @{Wrapper.Group#GROUP} with a radius. --- * @{Core.Zone#ZONE_POLYGON}: The ZONE_POLYGON class defines by a sequence of @{Wrapper.Group#GROUP} waypoints within the Mission Editor, forming a polygon. --- --- === --- --- 1) @{Core.Zone#ZONE_BASE} class, extends @{Core.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 @{Core.Zone#ZONE_BASE}: --- --- * @{#ZONE_BASE.IsPointVec2InZone}(): Returns if a @{Core.Point#POINT_VEC2} is within the zone. --- * @{#ZONE_BASE.IsPointVec3InZone}(): Returns if a @{Core.Point#POINT_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 @{Dcs.DCSTypes#Vec2} coordinate of the zone. --- * @{#ZONE_BASE.GetRandomVec2}(): Define a random @{Dcs.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) @{Core.Zone#ZONE_RADIUS} class, extends @{Core.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) @{Core.Zone#ZONE_RADIUS} constructor: --- --- * @{#ZONE_BASE.New}(): Constructor. --- --- ### 2.2) Manage the radius of the zone: --- --- * @{#ZONE_BASE.SetRadius}(): Sets the radius of the zone. --- * @{#ZONE_BASE.GetRadius}(): Returns the radius of the zone. --- --- ### 2.3) Manage the location of the zone: --- --- * @{#ZONE_BASE.SetVec2}(): Sets the @{Dcs.DCSTypes#Vec2} of the zone. --- * @{#ZONE_BASE.GetVec2}(): Returns the @{Dcs.DCSTypes#Vec2} of the zone. --- * @{#ZONE_BASE.GetVec3}(): Returns the @{Dcs.DCSTypes#Vec3} of the zone, taking an additional height parameter. --- --- === --- --- 3) @{Core.Zone#ZONE} class, extends @{Core.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) @{Core.Zone#ZONE_UNIT} class, extends @{Core.Zone#ZONE_RADIUS} --- ======================================================= --- The ZONE_UNIT class defined by a zone around a @{Wrapper.Unit#UNIT} with a radius. --- This class implements the inherited functions from @{Core.Zone#ZONE_RADIUS} taking into account the own zone format and properties. --- --- === --- --- 5) @{Core.Zone#ZONE_GROUP} class, extends @{Core.Zone#ZONE_RADIUS} --- ======================================================= --- The ZONE_GROUP class defines by a zone around a @{Wrapper.Group#GROUP} with a radius. The current leader of the group defines the center of the zone. --- This class implements the inherited functions from @{Core.Zone#ZONE_RADIUS} taking into account the own zone format and properties. --- --- === --- --- 6) @{Core.Zone#ZONE_POLYGON_BASE} class, extends @{Core.Zone#ZONE_BASE} --- ======================================================== --- The ZONE_POLYGON_BASE class defined by a sequence of @{Wrapper.Group#GROUP} waypoints within the Mission Editor, forming a polygon. --- This class implements the inherited functions from @{Core.Zone#ZONE_RADIUS} taking into account the own zone format and properties. --- This class is an abstract BASE class for derived classes, and is not meant to be instantiated. --- --- === --- --- 7) @{Core.Zone#ZONE_POLYGON} class, extends @{Core.Zone#ZONE_POLYGON_BASE} --- ================================================================ --- The ZONE_POLYGON class defined by a sequence of @{Wrapper.Group#GROUP} waypoints within the Mission Editor, forming a polygon. --- This class implements the inherited functions from @{Core.Zone#ZONE_RADIUS} taking into account the own zone format and properties. --- --- ==== --- --- **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: --- --- 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 --- @author FlightControl - - ---- 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:IsPointVec2InZone( 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:IsPointVec3InZone( Vec3 ) - self:F2( Vec3 ) - - local InZone = self:IsPointVec2InZone( { x = Vec3.x, y = Vec3.z } ) - - return InZone -end - ---- Returns the @{Dcs.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 @{Dcs.DCSTypes#Vec2} within the zone. --- @param #ZONE_BASE self --- @return Dcs.DCSTypes#Vec2 The Vec2 coordinates. -function ZONE_BASE:GetRandomVec2() - 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 - - ---- 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 ) ) - self:F( { ZoneName, Vec2, Radius } ) - - self.Radius = Radius - self.Vec2 = Vec2 - - 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 @{Dcs.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 @{Dcs.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 @{Dcs.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:IsPointVec2InZone( 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:IsPointVec3InZone( Vec3 ) - self:F2( Vec3 ) - - local InZone = self:IsPointVec2InZone( { x = Vec3.x, y = Vec3.z } ) - - return InZone -end - ---- Returns a random location within the zone. --- @param #ZONE_RADIUS self --- @param #number inner minimal distance from the center of the zone --- @param #number outer minimal distance from the outer edge 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 - - - ---- 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 @{Wrapper.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 @{Wrapper.Unit#UNIT}. --- @param #ZONE_UNIT self --- @return Dcs.DCSTypes#Vec2 The location of the zone based on the @{Wrapper.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 @{Dcs.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 @{Wrapper.Group#GROUP} and a radius. --- @param #ZONE_GROUP self --- @param #string ZoneName Name of the zone. --- @param Wrapper.Group#GROUP ZoneGROUP The @{Group} as the center of the zone. --- @param Dcs.DCSTypes#Distance Radius The radius of the zone. --- @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 @{Dcs.DCSTypes#Vec2}, forming a polygon. --- @type ZONE_POLYGON_BASE --- @field #ZONE_POLYGON_BASE.ListVec2 Polygon The polygon defined by an array of @{Dcs.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 @{Dcs.DCSTypes#Vec2}, forming a polygon. --- The @{Wrapper.Group#GROUP} waypoints define the polygon corners. The first and the last point are automatically connected. --- @param #ZONE_POLYGON_BASE self --- @param #string ZoneName Name of the zone. --- @param #ZONE_POLYGON_BASE.ListVec2 PointsArray An array of @{Dcs.DCSTypes#Vec2}, forming a polygon.. --- @return #ZONE_POLYGON_BASE self -function ZONE_POLYGON_BASE:New( ZoneName, PointsArray ) - local self = BASE:Inherit( self, ZONE_BASE:New( ZoneName ) ) - self:F( { ZoneName, PointsArray } ) - - local i = 0 - - self.Polygon = {} - - for i = 1, #PointsArray do - self.Polygon[i] = {} - self.Polygon[i].x = PointsArray[i].x - self.Polygon[i].y = PointsArray[i].y - end - - return self -end - ---- Flush polygon coordinates as a table in DCS.log. --- @param #ZONE_POLYGON_BASE self --- @return #ZONE_POLYGON_BASE self -function ZONE_POLYGON_BASE:Flush() - self:F2() - - self:E( { Polygon = self.ZoneName, Coordinates = self.Polygon } ) - - return self -end - - ---- Smokes the zone boundaries in a color. --- @param #ZONE_POLYGON_BASE self --- @param 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:IsPointVec2InZone( 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 @{Dcs.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:IsPointVec2InZone( Vec2 ) then - Vec2Found = true - end - end - - self:T2( Vec2 ) - - return Vec2 -end - ---- Get the bounding square the zone. --- @param #ZONE_POLYGON_BASE self --- @return #ZONE_POLYGON_BASE.BoundingSquare The bounding square. -function ZONE_POLYGON_BASE:GetBoundingSquare() - - local x1 = self.Polygon[1].x - local y1 = self.Polygon[1].y - local x2 = self.Polygon[1].x - local y2 = self.Polygon[1].y - - for i = 2, #self.Polygon do - self:T2( { self.Polygon[i], x1, y1, x2, y2 } ) - x1 = ( x1 > self.Polygon[i].x ) and self.Polygon[i].x or x1 - x2 = ( x2 < self.Polygon[i].x ) and self.Polygon[i].x or x2 - y1 = ( y1 > self.Polygon[i].y ) and self.Polygon[i].y or y1 - y2 = ( y2 < self.Polygon[i].y ) and self.Polygon[i].y or y2 - - end - - return { x1 = x1, y1 = y1, x2 = x2, y2 = y2 } -end - - - - - ---- The ZONE_POLYGON class defined by a sequence of @{Wrapper.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 @{Wrapper.Group#GROUP} defined within the Mission Editor. --- The @{Wrapper.Group#GROUP} waypoints define the polygon corners. The first and the last point are automatically connected by ZONE_POLYGON. --- @param #ZONE_POLYGON self --- @param #string ZoneName Name of the zone. --- @param Wrapper.Group#GROUP ZoneGroup The GROUP waypoints as defined within the Mission Editor define the polygon shape. --- @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) @{Core.Database#DATABASE} class, extends @{Core.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 = {}, - EventPriority = 1, -- Used to sort the DCS event order processing (complicated). Database has highest priority. - -} - -local _DATABASECoalition = - { - [1] = "Red", - [2] = "Blue", - } - -local _DATABASECategory = - { - ["plane"] = Unit.Category.AIRPLANE, - ["helicopter"] = Unit.Category.HELICOPTER, - ["vehicle"] = Unit.Category.GROUND_UNIT, - ["ship"] = Unit.Category.SHIP, - ["static"] = Unit.Category.STRUCTURE, - } - - ---- Creates a new DATABASE object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. --- @param #DATABASE self --- @return #DATABASE --- @usage --- -- Define a new DATABASE Object. This DBObject will contain a reference to all Group and Unit Templates defined within the ME and the DCSRTE. --- DBObject = DATABASE:New() -function DATABASE:New() - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) - - _EVENTDISPATCHER:OnBirth( self._EventOnBirth, self ) - _EVENTDISPATCHER:OnDead( self._EventOnDeadOrCrash, self ) - _EVENTDISPATCHER:OnCrash( self._EventOnDeadOrCrash, self ) - - - -- Follow alive players and clients - _EVENTDISPATCHER:OnPlayerEnterUnit( self._EventOnPlayerEnterUnit, self ) - _EVENTDISPATCHER:OnPlayerLeaveUnit( self._EventOnPlayerLeaveUnit, self ) - - self:_RegisterTemplates() - self:_RegisterGroupsAndUnits() - self:_RegisterClients() - self:_RegisterStatics() - self:_RegisterPlayers() - self:_RegisterAirbases() - - return self -end - ---- Finds a Unit based on the Unit Name. --- @param #DATABASE self --- @param #string UnitName --- @return 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 - self:AddUnit( Event.IniDCSUnitName ) - self:AddGroup( Event.IniDCSGroupName ) - self:_EventOnPlayerEnterUnit( Event ) - end -end - - ---- Handles the OnDead or OnCrash event for alive units set. --- @param #DATABASE self --- @param Core.Event#EVENTDATA Event -function DATABASE:_EventOnDeadOrCrash( Event ) - self:F2( { Event } ) - - if Event.IniDCSUnit then - if self.UNITS[Event.IniDCSUnitName] then - self:DeleteUnit( Event.IniDCSUnitName ) - -- add logic to correctly remove a group once all units are destroyed... - end - end -end - - ---- Handles the OnPlayerEnterUnit event to fill the active players table (with the unit filter applied). --- @param #DATABASE self --- @param Core.Event#EVENTDATA Event -function DATABASE:_EventOnPlayerEnterUnit( Event ) - self:F2( { Event } ) - - if Event.IniUnit 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 - - ---- 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 - local PlayerName = Event.IniUnit:GetPlayerName() - if self.PLAYERS[PlayerName] then - self:DeletePlayer( PlayerName ) - end - end -end - ---- Iterators - ---- Iterate the DATABASE and call an iterator function for the given set, providing the Object for each element within the set and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called when there is an alive player in the database. --- @return #DATABASE self -function DATABASE:ForEach( IteratorFunction, FinalizeFunction, arg, Set ) - self:F2( arg ) - - local function CoRoutine() - local Count = 0 - for ObjectID, Object in pairs( Set ) do - self:T2( Object ) - IteratorFunction( Object, unpack( arg ) ) - Count = Count + 1 --- if Count % 100 == 0 then --- coroutine.yield( false ) --- end - end - return true - end - --- local co = coroutine.create( CoRoutine ) - local co = CoRoutine - - local function Schedule() - --- local status, res = coroutine.resume( co ) - local status, res = co() - self:T3( { status, res } ) - - if status == false then - error( res ) - end - if res == false then - return true -- resume next time the loop - end - if FinalizeFunction then - FinalizeFunction( unpack( arg ) ) - end - return false - end - - local Scheduler = SCHEDULER:New( self, Schedule, {}, 0.001, 0.001, 0 ) - - return self -end - - ---- Iterate the DATABASE and call an iterator function for each **alive** UNIT, providing the UNIT and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the database. The function needs to accept a UNIT parameter. --- @return #DATABASE self -function DATABASE:ForEachUnit( IteratorFunction, FinalizeFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, FinalizeFunction, arg, self.UNITS ) - - return self -end - ---- Iterate the DATABASE and call an iterator function for each **alive** GROUP, providing the GROUP and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the database. The function needs to accept a GROUP parameter. --- @return #DATABASE self -function DATABASE:ForEachGroup( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.GROUPS ) - - return self -end - - ---- Iterate the DATABASE and call an iterator function for each **ALIVE** player, providing the player name and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called when there is an player in the database. The function needs to accept the player name. --- @return #DATABASE self -function DATABASE:ForEachPlayer( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.PLAYERS ) - - return self -end - - ---- Iterate the DATABASE and call an iterator function for each player who has joined the mission, providing the Unit of the player and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called when there is was a player in the database. The function needs to accept a UNIT parameter. --- @return #DATABASE self -function DATABASE:ForEachPlayerJoined( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.PLAYERSJOINED ) - - return self -end - ---- Iterate the DATABASE and call an iterator function for each CLIENT, providing the CLIENT to the function and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called when there is an alive player in the database. The function needs to accept a CLIENT parameter. --- @return #DATABASE self -function DATABASE:ForEachClient( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.CLIENTS ) - - return self -end - - -function DATABASE:_RegisterTemplates() - self:F2() - - self.Navpoints = {} - self.UNITS = {} - --Build routines.db.units and self.Navpoints - for CoalitionName, coa_data in pairs(env.mission.coalition) do - - if (CoalitionName == 'red' or CoalitionName == 'blue') and type(coa_data) == 'table' then - --self.Units[coa_name] = {} - - ---------------------------------------------- - -- build nav points DB - self.Navpoints[CoalitionName] = {} - if coa_data.nav_points then --navpoints - for nav_ind, nav_data in pairs(coa_data.nav_points) do - - if type(nav_data) == 'table' then - self.Navpoints[CoalitionName][nav_ind] = routines.utils.deepCopy(nav_data) - - self.Navpoints[CoalitionName][nav_ind]['name'] = nav_data.callsignStr -- name is a little bit more self-explanatory. - self.Navpoints[CoalitionName][nav_ind]['point'] = {} -- point is used by SSE, support it. - self.Navpoints[CoalitionName][nav_ind]['point']['x'] = nav_data.x - self.Navpoints[CoalitionName][nav_ind]['point']['y'] = 0 - self.Navpoints[CoalitionName][nav_ind]['point']['z'] = nav_data.y - end - end - end - ------------------------------------------------- - if coa_data.country then --there is a country table - for cntry_id, cntry_data in pairs(coa_data.country) do - - local CountryName = string.upper(cntry_data.name) - --self.Units[coa_name][countryName] = {} - --self.Units[coa_name][countryName]["countryId"] = cntry_data.id - - if type(cntry_data) == 'table' then --just making sure - - for obj_type_name, obj_type_data in pairs(cntry_data) do - - if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then --should be an unncessary check - - local CategoryName = obj_type_name - - if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! - - --self.Units[coa_name][countryName][category] = {} - - for group_num, GroupTemplate in pairs(obj_type_data.group) do - - if GroupTemplate and GroupTemplate.units and type(GroupTemplate.units) == 'table' then --making sure again- this is a valid group - self:_RegisterTemplate( - GroupTemplate, - coalition.side[string.upper(CoalitionName)], - _DATABASECategory[string.lower(CategoryName)], - country.id[string.upper(CountryName)] - ) - end --if GroupTemplate and GroupTemplate.units then - end --for group_num, GroupTemplate in pairs(obj_type_data.group) do - end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then - end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then - end --for obj_type_name, obj_type_data in pairs(cntry_data) do - end --if type(cntry_data) == 'table' then - end --for cntry_id, cntry_data in pairs(coa_data.country) do - end --if coa_data.country then --there is a country table - end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then - end --for coa_name, coa_data in pairs(mission.coalition) do - - return self -end - - - - ---- This module contains the SET classes. --- --- === --- --- 1) @{Core.Set#SET_BASE} class, extends @{Core.Base#BASE} --- ============================================== --- The @{Core.Set#SET_BASE} class defines the core functions that define a collection of objects. --- A SET provides iterators to iterate the SET, but will **temporarily** yield the ForEach interator loop at defined **"intervals"** to the mail simulator loop. --- In this way, large loops can be done while not blocking the simulator main processing loop. --- The default **"yield interval"** is after 10 objects processed. --- The default **"time interval"** is after 0.001 seconds. --- --- 1.1) Add or remove objects from the SET --- --------------------------------------- --- Some key core functions are @{Core.Set#SET_BASE.Add} and @{Core.Set#SET_BASE.Remove} to add or remove objects from the SET in your logic. --- --- 1.2) Define the SET iterator **"yield interval"** and the **"time interval"** --- ----------------------------------------------------------------------------- --- Modify the iterator intervals with the @{Core.Set#SET_BASE.SetInteratorIntervals} method. --- You can set the **"yield interval"**, and the **"time interval"**. (See above). --- --- === --- --- 2) @{Core.Set#SET_GROUP} class, extends @{Core.Set#SET_BASE} --- ================================================== --- Mission designers can use the @{Core.Set#SET_GROUP} class to build sets of groups belonging to certain: --- --- * Coalitions --- * Categories --- * Countries --- * Starting with certain prefix strings. --- --- 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 @{Core.Set#SET_GROUP.AddGroupsByName} and @{Core.Set#SET_GROUP.RemoveGroupsByName} respectively. --- These methods take a single GROUP name or an array of GROUP names to be added or removed from SET_GROUP. --- --- 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 @{Core.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) @{Core.Set#SET_UNIT} class, extends @{Core.Set#SET_BASE} --- =================================================== --- Mission designers can use the @{Core.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 @{Core.Set#SET_UNIT.AddUnitsByName} and @{Core.Set#SET_UNIT.RemoveUnitsByName} respectively. --- These methods take a single UNIT name or an array of UNIT names to be added or removed from SET_UNIT. --- --- 3.3) SET_UNIT filter criteria: --- ------------------------------ --- 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 @{Core.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) @{Core.Set#SET_CLIENT} class, extends @{Core.Set#SET_BASE} --- =================================================== --- Mission designers can use the @{Core.Set#SET_CLIENT} class to build sets of units belonging to certain: --- --- * Coalitions --- * Categories --- * Countries --- * Client types --- * Starting with certain prefix strings. --- --- 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 @{Core.Set#SET_CLIENT.AddClientsByName} and @{Core.Set#SET_CLIENT.RemoveClientsByName} respectively. --- These methods take a single CLIENT name or an array of CLIENT names to be added or removed from SET_CLIENT. --- --- 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 @{Core.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) @{Core.Set#SET_AIRBASE} class, extends @{Core.Set#SET_BASE} --- ==================================================== --- Mission designers can use the @{Core.Set#SET_AIRBASE} class to build sets of airbases optionally belonging to certain: --- --- * Coalitions --- --- 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 @{Core.Set#SET_AIRBASE.AddAirbasesByName} and @{Core.Set#SET_AIRBASE.RemoveAirbasesByName} respectively. --- These methods take a single AIRBASE name or an array of AIRBASE names to be added or removed from SET_AIRBASE. --- --- 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 = {}, - EventPriority = 2, -- Used to sort the DCS event order processing (complicated) -} - ---- 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 ) - - return self -end - ---- Finds an @{Core.Base#BASE} object based on the object Name. --- @param #SET_BASE self --- @param #string ObjectName --- @return Core.Base#BASE The Object found. -function SET_BASE:_Find( ObjectName ) - - local 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 @{Core.Base#BASE} object in the @{Core.Set#SET_BASE}, using a given ObjectName as the index. --- @param #SET_BASE self --- @param #string ObjectName --- @param Core.Base#BASE Object --- @return Core.Base#BASE The added BASE Object. -function SET_BASE:Add( ObjectName, Object ) - self:F2( ObjectName ) - - 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 @{Core.Base#BASE} object in the @{Core.Set#SET_BASE}, using the Object Name as the index. --- @param #SET_BASE self --- @param Wrapper.Object#OBJECT Object --- @return Core.Base#BASE The added BASE Object. -function SET_BASE:AddObject( Object ) - self:F2( Object.ObjectName ) - - self:T( Object.UnitName ) - self:T( Object.ObjectName ) - self:Add( Object.ObjectName, Object ) - -end - - - ---- Removes a @{Core.Base#BASE} object from the @{Core.Set#SET_BASE} and derived classes, based on the Object Name. --- @param #SET_BASE self --- @param #string ObjectName -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 @{Core.Base#BASE} object from the @{Core.Set#SET_BASE} and derived classes, based on the Object Name. --- @param #SET_BASE self --- @param #string ObjectName --- @return Core.Base#BASE -function SET_BASE:Get( ObjectName ) - self:F( ObjectName ) - - local t = self.Set[ObjectName] - - self:T3( { ObjectName, t } ) - - return t - -end - ---- Retrieves the amount of objects in the @{Core.Set#SET_BASE} and derived classes. --- @param #SET_BASE self --- @return #number Count -function SET_BASE:Count() - - return self.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 - - _EVENTDISPATCHER:OnBirth( self._EventOnBirth, self ) - _EVENTDISPATCHER:OnDead( self._EventOnDeadOrCrash, self ) - _EVENTDISPATCHER:OnCrash( self._EventOnDeadOrCrash, self ) - - -- Follow alive players and clients - _EVENTDISPATCHER:OnPlayerEnterUnit( self._EventOnPlayerEnterUnit, self ) - _EVENTDISPATCHER:OnPlayerLeaveUnit( self._EventOnPlayerLeaveUnit, self ) - - - return self -end - ---- Stops the filtering for the defined collection. --- @param #SET_BASE self --- @return #SET_BASE self -function SET_BASE:FilterStop() - - _EVENTDISPATCHER:OnBirthRemove( self ) - _EVENTDISPATCHER:OnDeadRemove( self ) - _EVENTDISPATCHER:OnCrashRemove( self ) - - return self -end - ---- Iterate the SET_BASE while identifying the nearest object from a @{Core.Point#POINT_VEC2}. --- @param #SET_BASE self --- @param Core.Point#POINT_VEC2 PointVec2 A @{Core.Point#POINT_VEC2} object from where to evaluate the closest object in the set. --- @return Core.Base#BASE The closest object. -function SET_BASE:FindNearestObjectFromPointVec2( PointVec2 ) - self:F2( PointVec2 ) - - local NearestObject = nil - local ClosestDistance = nil - - for ObjectID, ObjectData in pairs( self.Set ) do - if NearestObject == nil then - NearestObject = ObjectData - ClosestDistance = PointVec2: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 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 not self.Database[Event.IniDCSGroupName] then - self.Database[Event.IniDCSGroupName] = GROUP:Register( Event.IniDCSGroupName ) - self:T3( self.Database[Event.IniDCSGroupName] ) - end - - return Event.IniDCSGroupName, self.Database[Event.IniDCSGroupName] -end - ---- Handles the Database to check on any event that Object exists in the Database. --- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! --- @param #SET_GROUP self --- @param 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 not self.Database[Event.IniDCSUnitName] then - self.Database[Event.IniDCSUnitName] = UNIT:Register( Event.IniDCSUnitName ) - self:T3( self.Database[Event.IniDCSUnitName] ) - end - - return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] -end - ---- Handles the Database to check on any event that Object exists in the Database. --- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! --- @param #SET_UNIT self --- @param 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 @{Wrapper.Airbase#AIRBASE} from a @{Core.Point#POINT_VEC2}. --- @param #SET_AIRBASE self --- @param Core.Point#POINT_VEC2 PointVec2 A @{Core.Point#POINT_VEC2} object from where to evaluate the closest @{Wrapper.Airbase#AIRBASE}. --- @return Wrapper.Airbase#AIRBASE The closest @{Wrapper.Airbase#AIRBASE}. -function SET_AIRBASE:FindNearestAirbaseFromPointVec2( PointVec2 ) - self:F2( PointVec2 ) - - local NearestAirbase = self:FindNearestObjectFromPointVec2( PointVec2 ) - return NearestAirbase -end - - - ---- --- @param #SET_AIRBASE self --- @param Wrapper.Airbase#AIRBASE MAirbase --- @return #SET_AIRBASE self -function SET_AIRBASE:IsIncludeObject( MAirbase ) - self:F2( MAirbase ) - - local MAirbaseInclude = true - - if MAirbase then - local MAirbaseName = MAirbase:GetName() - - if self.Filter.Coalitions then - local MAirbaseCoalition = false - for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do - local AirbaseCoalitionID = _DATABASE:GetCoalitionFromAirbase( MAirbaseName ) - self:T3( { "Coalition:", AirbaseCoalitionID, self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) - if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == AirbaseCoalitionID then - MAirbaseCoalition = true - end - end - self:T( { "Evaluated Coalition", MAirbaseCoalition } ) - MAirbaseInclude = MAirbaseInclude and MAirbaseCoalition - end - - if self.Filter.Categories then - local MAirbaseCategory = false - for CategoryID, CategoryName in pairs( self.Filter.Categories ) do - local AirbaseCategoryID = _DATABASE:GetCategoryFromAirbase( MAirbaseName ) - self:T3( { "Category:", AirbaseCategoryID, self.FilterMeta.Categories[CategoryName], CategoryName } ) - if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == AirbaseCategoryID then - MAirbaseCategory = true - end - end - self:T( { "Evaluated Category", MAirbaseCategory } ) - MAirbaseInclude = MAirbaseInclude and MAirbaseCategory - end - end - - self:T2( MAirbaseInclude ) - return MAirbaseInclude -end ---- This module contains the POINT classes. --- --- 1) @{Core.Point#POINT_VEC3} class, extends @{Core.Base#BASE} --- ================================================== --- The @{Core.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_VEC3.New}(): a 3D point. --- * @{#POINT_VEC3.NewFromVec3}(): a 3D point created from a @{Dcs.DCSTypes#Vec3}. --- --- --- 2) @{Core.Point#POINT_VEC2} class, extends @{Core.Point#POINT_VEC3} --- ========================================================= --- The @{Core.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_VEC2.New}(): a 2D point, taking an additional height parameter. --- * @{#POINT_VEC2.NewFromVec2}(): a 2D point created from a @{Dcs.DCSTypes#Vec2}. --- --- === --- --- **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: --- --- 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 --- @extends Core.Base#BASE --- @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 -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 --- @extends #POINT_VEC3 --- @field Dcs.DCSTypes#Distance x The x coordinate in meters. --- @field Dcs.DCSTypes#Distance y the y coordinate in meters. -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 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 #number x The x coordinate. -function POINT_VEC3:SetX( x ) - self.x = x -end - ---- Set the y coordinate of the POINT_VEC3. --- @param #number y The y coordinate. -function POINT_VEC3:SetY( y ) - self.y = y -end - ---- Set the z coordinate of the POINT_VEC3. --- @param #number z The z coordinate. -function POINT_VEC3:SetZ( z ) - self.z = z -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 - - ---- 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 #number x The x coordinate. -function POINT_VEC2:SetX( x ) - self.x = x -end - ---- Set the y coordinate of the POINT_VEC2. --- @param #number y The y coordinate. -function POINT_VEC2:SetY( y ) - self.z = y -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 @{Dcs.DCSTypes#Vec2}. --- @param #POINT_VEC2 self --- @param Dcs.DCSTypes#Vec2 Vec2Reference The reference @{Dcs.DCSTypes#Vec2}. --- @return Dcs.DCSTypes#Distance The distance from the reference @{Dcs.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) @{Core.Message#MESSAGE} class, extends @{Core.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 @{Core.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 @{Core.Message#MESSAGE.ToClient}. --- * Coalitions with @{Core.Message#MESSAGE.ToCoalition}. --- * All Players with @{Core.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 all players. --- @param #MESSAGE self --- @return #MESSAGE --- @usage --- -- Send a message created to all players. --- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ):ToAll() --- or --- MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ):ToAll() --- or --- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ) --- MessageAll:ToAll() -function MESSAGE:ToAll() - self:F() - - self:ToCoalition( coalition.side.RED ) - self:ToCoalition( coalition.side.BLUE ) - - return self -end - - - ------ The MESSAGEQUEUE class ----- @type MESSAGEQUEUE ---MESSAGEQUEUE = { --- ClientGroups = {}, --- CoalitionSides = {} ---} --- ---function MESSAGEQUEUE:New( RefreshInterval ) --- local self = BASE:Inherit( self, BASE:New() ) --- self:F( { RefreshInterval } ) --- --- self.RefreshInterval = RefreshInterval --- --- --self.DisplayFunction = routines.scheduleFunction( self._DisplayMessages, { self }, 0, RefreshInterval ) --- self.DisplayFunction = SCHEDULER:New( self, self._DisplayMessages, {}, 0, RefreshInterval ) --- --- return self ---end --- ------ This function is called automatically by the MESSAGEQUEUE scheduler. ---function MESSAGEQUEUE:_DisplayMessages() --- --- -- First we display all messages that a coalition needs to receive... Also those who are not in a client (CA module clients...). --- for CoalitionSideID, CoalitionSideData in pairs( self.CoalitionSides ) do --- for MessageID, MessageData in pairs( CoalitionSideData.Messages ) do --- if MessageData.MessageSent == false then --- --trigger.action.outTextForCoalition( CoalitionSideID, MessageData.MessageCategory .. '\n' .. MessageData.MessageText:gsub("\n$",""):gsub("\n$",""), MessageData.MessageDuration ) --- MessageData.MessageSent = true --- end --- local MessageTimeLeft = ( MessageData.MessageTime + MessageData.MessageDuration ) - timer.getTime() --- if MessageTimeLeft <= 0 then --- MessageData = nil --- end --- end --- end --- --- -- Then we send the messages for each individual client, but also to be included are those Coalition messages for the Clients who belong to a coalition. --- -- Because the Client messages will overwrite the Coalition messages (for that Client). --- for ClientGroupName, ClientGroupData in pairs( self.ClientGroups ) do --- for MessageID, MessageData in pairs( ClientGroupData.Messages ) do --- if MessageData.MessageGroup == false then --- trigger.action.outTextForGroup( Group.getByName(ClientGroupName):getID(), MessageData.MessageCategory .. '\n' .. MessageData.MessageText:gsub("\n$",""):gsub("\n$",""), MessageData.MessageDuration ) --- MessageData.MessageGroup = true --- end --- local MessageTimeLeft = ( MessageData.MessageTime + MessageData.MessageDuration ) - timer.getTime() --- if MessageTimeLeft <= 0 then --- MessageData = nil --- end --- end --- --- -- Now check if the Client also has messages that belong to the Coalition of the Client... --- for CoalitionSideID, CoalitionSideData in pairs( self.CoalitionSides ) do --- for MessageID, MessageData in pairs( CoalitionSideData.Messages ) do --- local CoalitionGroup = Group.getByName( ClientGroupName ) --- if CoalitionGroup and CoalitionGroup:getCoalition() == CoalitionSideID then --- if MessageData.MessageCoalition == false then --- trigger.action.outTextForGroup( Group.getByName(ClientGroupName):getID(), MessageData.MessageCategory .. '\n' .. MessageData.MessageText:gsub("\n$",""):gsub("\n$",""), MessageData.MessageDuration ) --- MessageData.MessageCoalition = true --- end --- end --- local MessageTimeLeft = ( MessageData.MessageTime + MessageData.MessageDuration ) - timer.getTime() --- if MessageTimeLeft <= 0 then --- MessageData = nil --- end --- end --- end --- end --- --- return true ---end --- ------ The _MessageQueue object is created when the MESSAGE class module is loaded. -----_MessageQueue = MESSAGEQUEUE:New( 0.5 ) --- ---- 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) @{Core.Fsm#FSM} class, extends @{Core.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.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) - if self[handler] then - self:T( "Calling " .. handler ) - 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) == false ) - or ( self:_call_handler("OnBefore" .. EventName, params) == false ) - or ( self:_call_handler("onleave" .. from, params) == false ) - or ( self:_call_handler("OnLeave" .. from, params) == 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) - self:_call_handler("OnEnter" .. to, params) - self:_call_handler("onafter" .. EventName, params) - self:_call_handler("OnAfter" .. EventName, params) - self:_call_handler("onstatechange", params) - 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) - self:_call_handler("OnEnter" .. to, params) - --end - - self:_call_handler("onafter" .. EventName, params) - self:_call_handler("OnAfter" .. EventName, params) - - self:_call_handler("onstatechange", params) - 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 = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1 ) - 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 ) - - 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 ) - 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 ) - if self[handler] then - self:T( "Calling " .. handler ) - 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 ) - if self[handler] then - self:T( "Calling " .. handler ) - return self[handler]( self, self.Set, unpack( params ) ) - end - end - -end -- FSM_SET - ---- This module contains the OBJECT class. --- --- 1) @{Wrapper.Object#OBJECT} class, extends @{Core.Base#BASE} --- =========================================================== --- The @{Wrapper.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: --- --- * @{Wrapper.Object#OBJECT.New}(): Create a OBJECT instance. --- --- 1.2) OBJECT methods: --- -------------------------- --- The following methods can be used to identify an Object object: --- --- * @{Wrapper.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 @{Wrapper.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 - - - - - - - - - ---- This module contains the POSITIONABLE class. --- --- 1) @{Wrapper.Positionable#POSITIONABLE} class, extends @{Wrapper.Identifiable#IDENTIFIABLE} --- =========================================================== --- The @{Wrapper.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: --- --- * @{Wrapper.Positionable#POSITIONABLE.New}(): Create a POSITIONABLE instance. --- --- 1.2) POSITIONABLE methods: --- -------------------------- --- The following methods can be used to identify an measurable object: --- --- * @{Wrapper.Positionable#POSITIONABLE.GetID}(): Returns the ID of the measurable object. --- * @{Wrapper.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 @{Dcs.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:E( 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 @{Dcs.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 @{Dcs.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 @{Dcs.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) @{Wrapper.Controllable#CONTROLLABLE} class, extends @{Wrapper.Positionable#POSITIONABLE} --- =========================================================== --- The @{Wrapper.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 @{Wrapper.Controllable#CONTROLLABLE.PushTask} or @{Wrapper.Controllable#SetTask} method to assign the task to the CONTROLLABLE. --- Tasks are specific for the category of the CONTROLLABLE, more specific, for AIR, GROUND or AIR and GROUND. --- Each task description where applicable indicates for which controllable category the task is valid. --- There are 2 main subdivisions of tasks: Assigned tasks and EnRoute tasks. --- --- ### 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 @{Core.Zone#ZONE_RADIUS). --- * @{#CONTROLLABLE.TaskOrbitCircle}: (AIR) Orbit at the current position of the first unit of the controllable at a specified alititude. --- * @{#CONTROLLABLE.TaskOrbitCircleAtVec2}: (AIR) Orbit at a specified position at a specified alititude during a specified duration with a specified speed. --- * @{#CONTROLLABLE.TaskRefueling}: (AIR) Refueling from the nearest tanker. No parameters. --- * @{#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 @{Dcs.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 @{Core.Zone#ZONE_RADIUS). --- @param #CONTROLLABLE self --- @param Core.Zone#ZONE Zone The zone where to land. --- @param #number Duration The duration in seconds to stay on the ground. --- @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 AttackUnit The UNIT. --- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. --- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. --- @param 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:EnRouteTaskEngageUnit( AttackUnit, Priority, WeaponType, WeaponExpend, AttackQty, Direction, AttackQtyLimit, ControllableAttack ) - self:F2( { self.ControllableName, AttackUnit, Priority, WeaponType, WeaponExpend, AttackQty, Direction, AttackQtyLimit, ControllableAttack } ) - - -- EngageUnit = { - -- id = 'EngageUnit', - -- params = { - -- unitId = Unit.ID, - -- weaponType = number, - -- expend = enum AI.Task.WeaponExpend - -- attackQty = number, - -- direction = Azimuth, - -- attackQtyLimit = boolean, - -- controllableAttack = boolean, - -- priority = number, - -- } - -- } - - local DCSTask - DCSTask = { id = 'EngageUnit', - params = { - unitId = AttackUnit:GetID(), - weaponType = WeaponType, - expend = WeaponExpend, - attackQty = AttackQty, - direction = Direction, - attackQtyLimit = AttackQtyLimit, - controllableAttack = ControllableAttack, - priority = Priority, - }, - }, - - self:T3( { DCSTask } ) - return DCSTask -end - - - ---- (AIR) Aircraft will act as an AWACS for friendly units (will provide them with information about contacts). No parameters. --- @param #CONTROLLABLE self --- @return 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:TaskRouteToVec2( 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:TaskRouteToVec3( 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 @{Wrapper.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 @{Wrapper.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 @{Core.Database#DATABASE} class. --- @param #CONTROLLABLE self --- @param #number Begin The route point from where the copy will start. The base route point is 0. --- @param #number End The route point where the copy will end. The End point is the last point - the End point. The last point has base 0. --- @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 @{Wrapper.Controllable#CONTROLLABLE:WayPointFunction} to define the hook functions for specific waypoints. --- Use the method @{Controllable@CONTROLLABLE:WayPointExecute) to start the execution of the new mission plan. --- Note that when WayPointInitialize is called, the Mission of the controllable is RESTARTED! --- @param #CONTROLLABLE self --- @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) @{Wrapper.Group#GROUP} class, extends @{Wrapper.Controllable#CONTROLLABLE} --- ============================================================= --- The @{Wrapper.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: --- --- * @{Wrapper.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 @{Core.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 - 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 @{Dcs.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() - 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:T3( UnitFound.UnitName ) - self:T2( UnitFound ) - return UnitFound - end - - return nil -end - ---- Returns the DCS Unit with number UnitNumber. --- If the underlying DCS Unit does not exist, the method will return nil. . --- @param #GROUP self --- @param #number UnitNumber The number of the DCS Unit to be returned. --- @return 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 @{Core.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 - -- TODO: Rename IsPointVec3InZone to IsVec3InZone - if Zone:IsPointVec3InZone( 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 @{Core.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:IsPointVec3InZone( 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 @{Core.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:IsPointVec3InZone( 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 @{Wrapper.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 @{Core.Database#DATABASE} class. --- @param #GROUP self --- @param #number Begin The route point from where the copy will start. The base route point is 0. --- @param #number End The route point where the copy will end. The End point is the last point - the End point. The last point has base 0. --- @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 @{Wrapper.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 @{Dcs.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 @{Core.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 - 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. -function UNIT:GetThreatLevel() - - local Attributes = self:GetDesc().attributes - local ThreatLevel = 0 - - 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" - } - - self:T2( Attributes ) - - 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"] then ThreatLevel = 2 - elseif Attributes["Infantry"] then ThreatLevel = 1 - end - - self:T2( ThreatLevel ) - return ThreatLevel, ThreatLevels[ThreatLevel+1] - -end - - --- Is functions - ---- Returns true if the unit is within a @{Zone}. --- @param #UNIT self --- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the unit is within the @{Core.Zone#ZONE_BASE} -function UNIT:IsInZone( Zone ) - self:F2( { self.UnitName, Zone } ) - - if self:IsAlive() then - local IsInZone = Zone:IsPointVec3InZone( 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 @{Core.Zone#ZONE_BASE} -function UNIT:IsNotInZone( Zone ) - self:F2( { self.UnitName, Zone } ) - - if self:IsAlive() then - local IsInZone = not Zone:IsPointVec3InZone( 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 - ---- This module contains the CLIENT class. --- --- 1) @{Wrapper.Client#CLIENT} class, extends @{Wrapper.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 @{Wrapper.Client#CLIENT} class is a wrapper class to handle the DCS Unit objects that have the skillset defined as __Client__ or __Player__: --- --- * Wraps the DCS Unit objects with skill level set to Player or Client. --- * Support all DCS Unit APIs. --- * Enhance with Unit specific APIs not in the DCS Group API set. --- * When player joins Unit, execute alive init logic. --- * Handles messages to players. --- * Manage the "state" of the DCS Unit. --- --- Clients are being used by the @{MISSION} class to follow players and register their successes. --- --- 1.1) CLIENT reference methods --- ----------------------------- --- For each DCS Unit having skill level Player or Client, a CLIENT wrapper object (instance) will be created within the _@{DATABASE} object. --- This is done at the beginning of the mission (when the mission starts). --- --- The CLIENT class does not contain a :New() method, rather it provides :Find() methods to retrieve the object reference --- using the DCS Unit or the DCS UnitName. --- --- Another thing to know is that CLIENT objects do not "contain" the DCS Unit object. --- The CLIENT methods will reference the DCS Unit object by name when it is needed during API execution. --- If the DCS Unit object does not exist or is nil, the CLIENT methods will return nil and log an exception in the DCS.log file. --- --- The CLIENT class provides the following functions to retrieve quickly the relevant CLIENT instance: --- --- * @{#CLIENT.Find}(): Find a CLIENT instance from the _DATABASE object using a DCS Unit object. --- * @{#CLIENT.FindByName}(): Find a CLIENT instance from the _DATABASE object using a DCS Unit name. --- --- IMPORTANT: ONE SHOULD NEVER SANATIZE these CLIENT OBJECT REFERENCES! (make the CLIENT object references nil). --- --- @module Client --- @author FlightControl - ---- The CLIENT class --- @type CLIENT --- @extends 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 CallBack Function. --- @return #CLIENT -function CLIENT:Alive( CallBackFunction, ... ) - self:F() - - self.ClientCallBack = CallBackFunction - self.ClientParameters = arg - - return self -end - ---- @param #CLIENT self -function CLIENT:_AliveCheckScheduler( SchedulerName ) - self: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.AI_Cargo#CARGO} contained within the CLIENT to the player as a message. --- The @{AI.AI_Cargo#CARGO} is shown using the @{Core.Message#MESSAGE} distribution system. --- @param #CLIENT self -function CLIENT:ShowCargo() - self:F() - - 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 @{Core.Message#MESSAGE} when the CLIENT is in the air. --- @param #string MessageID is the identifier of the message when displayed with intervals. -function CLIENT:Message( Message, MessageDuration, MessageCategory, MessageInterval, MessageID ) - self:F( { Message, MessageDuration, MessageCategory, MessageInterval } ) - - 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) @{Wrapper.Static#STATIC} class, extends @{Wrapper.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 @{Wrapper.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. --- @return #STATIC -function STATIC:FindByName( StaticName ) - local StaticFound = _DATABASE:FindStatic( StaticName ) - - self.StaticName = StaticName - - if StaticFound then - StaticFound:F( { StaticName } ) - - return StaticFound - end - - error( "STATIC not found for: " .. StaticName ) -end - -function STATIC:Register( StaticName ) - local self = BASE:Inherit( self, 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 ---- This module contains the AIRBASE classes. --- --- === --- --- 1) @{Wrapper.Airbase#AIRBASE} class, extends @{Wrapper.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 @{Dcs.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 - - - ---- Scoring system for MOOSE. --- This scoring class calculates the hits and kills that players make within a simulation session. --- Scoring is calculated using a defined algorithm. --- With a small change in MissionScripting.lua, the scoring can also be logged in a CSV file, that can then be uploaded --- to a database or a BI tool to publish the scoring results to the player community. --- @module Scoring --- @author FlightControl - - ---- The Scoring class --- @type SCORING --- @field Players A collection of the current players that have joined the game. --- @extends 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() ) - - if GameName then - self.GameName = GameName - else - error( "A game name must be given to register the scoring results" ) - end - - - _EVENTDISPATCHER:OnDead( self._EventOnDeadOrCrash, self ) - _EVENTDISPATCHER:OnCrash( self._EventOnDeadOrCrash, self ) - _EVENTDISPATCHER:OnHit( self._EventOnHit, self ) - - --self.SchedulerId = routines.scheduleFunction( SCORING._FollowPlayersScheduled, { self }, 0, 5 ) - self.SchedulerId = SCHEDULER:New( self, self._FollowPlayersScheduled, {}, 0, 5 ) - - self:ScoreMenu() - - self:OpenCSV( GameName) - - return self - -end - ---- Creates a score radio menu. Can be accessed using Radio -> F10. --- @param #SCORING self --- @return #SCORING self -function SCORING:ScoreMenu() - self.Menu = MENU_MISSION:New( 'Scoring' ) - self.AllScoresMenu = MENU_MISSION_COMMAND:New( 'Score All Active Players', self.Menu, SCORING.ReportScoreAll, self ) - --- = COMMANDMENU:New('Your Current Score', ReportScore, SCORING.ReportScorePlayer, self ) - return self -end - ---- Follows new players entering Clients within the DCSRTE. --- TODO: Need to see if i can catch this also with an event. It will eliminate the schedule ... -function SCORING:_FollowPlayersScheduled() - self:F3( "_FollowPlayersScheduled" ) - - local ClientUnit = 0 - local CoalitionsData = { AlivePlayersRed = coalition.getPlayers(coalition.side.RED), AlivePlayersBlue = coalition.getPlayers(coalition.side.BLUE) } - local unitId - local unitData - local AlivePlayerUnits = {} - - for CoalitionId, CoalitionData in pairs( CoalitionsData ) do - self:T3( { "_FollowPlayersScheduled", CoalitionData } ) - for UnitId, UnitData in pairs( CoalitionData ) do - self:_AddPlayerFromUnit( UnitData ) - end - end - - return true -end - - ---- Track DEAD or CRASH events for the scoring. --- @param #SCORING self --- @param 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.IniDCSUnit - TargetUnitName = Event.IniDCSUnitName - TargetGroup = Event.IniDCSGroup - TargetGroupName = Event.IniDCSGroupName - TargetPlayerName = TargetUnit:getPlayerName() - - TargetCoalition = TargetUnit:getCoalition() - --TargetCategory = TargetUnit:getCategory() - TargetCategory = TargetUnit:getDesc().category -- Workaround - TargetType = TargetUnit:getTypeName() - - TargetUnitCoalition = _SCORINGCoalition[TargetCoalition] - TargetUnitCategory = _SCORINGCategory[TargetCategory] - TargetUnitType = TargetType - - self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType } ) - end - - for PlayerName, PlayerData in pairs( self.Players ) do - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Something got killed" ) - - -- Some variables - local InitUnitName = PlayerData.UnitName - local InitUnitType = PlayerData.UnitType - local InitCoalition = PlayerData.UnitCoalition - local InitCategory = PlayerData.UnitCategory - local InitUnitCoalition = _SCORINGCoalition[InitCoalition] - local InitUnitCategory = _SCORINGCategory[InitCategory] - - self:T( { InitUnitName, InitUnitType, InitUnitCoalition, InitCoalition, InitUnitCategory, InitCategory } ) - - -- What is he hitting? - if TargetCategory then - if PlayerData and PlayerData.Hit and PlayerData.Hit[TargetCategory] and PlayerData.Hit[TargetCategory][TargetUnitName] then -- Was there a hit for this unit for this player before registered??? - if not PlayerData.Kill[TargetCategory] then - PlayerData.Kill[TargetCategory] = {} - end - if not PlayerData.Kill[TargetCategory][TargetType] then - PlayerData.Kill[TargetCategory][TargetType] = {} - PlayerData.Kill[TargetCategory][TargetType].Score = 0 - PlayerData.Kill[TargetCategory][TargetType].ScoreKill = 0 - PlayerData.Kill[TargetCategory][TargetType].Penalty = 0 - PlayerData.Kill[TargetCategory][TargetType].PenaltyKill = 0 - end - - if InitCoalition == TargetCoalition then - PlayerData.Penalty = PlayerData.Penalty + 25 - PlayerData.Kill[TargetCategory][TargetType].Penalty = PlayerData.Kill[TargetCategory][TargetType].Penalty + 25 - PlayerData.Kill[TargetCategory][TargetType].PenaltyKill = PlayerData.Kill[TargetCategory][TargetType].PenaltyKill + 1 - MESSAGE:New( "Player '" .. PlayerName .. "' killed a friendly " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. - PlayerData.Kill[TargetCategory][TargetType].PenaltyKill .. " times. Penalty: -" .. PlayerData.Kill[TargetCategory][TargetType].Penalty .. - ". Score Total:" .. PlayerData.Score - PlayerData.Penalty, - 5 ):ToAll() - self:ScoreCSV( PlayerName, "KILL_PENALTY", 1, -125, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - else - PlayerData.Score = PlayerData.Score + 10 - PlayerData.Kill[TargetCategory][TargetType].Score = PlayerData.Kill[TargetCategory][TargetType].Score + 10 - PlayerData.Kill[TargetCategory][TargetType].ScoreKill = PlayerData.Kill[TargetCategory][TargetType].ScoreKill + 1 - MESSAGE:New( "Player '" .. PlayerName .. "' killed an enemy " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. - PlayerData.Kill[TargetCategory][TargetType].ScoreKill .. " times. Score: " .. PlayerData.Kill[TargetCategory][TargetType].Score .. - ". Score Total:" .. PlayerData.Score - PlayerData.Penalty, - 5 ):ToAll() - self:ScoreCSV( PlayerName, "KILL_SCORE", 1, 10, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - end - end - end - end - end -end - - - ---- Add a new player entering a Unit. -function SCORING:_AddPlayerFromUnit( UnitData ) - self:F( UnitData ) - - if UnitData and UnitData:isExist() then - local UnitName = UnitData:getName() - local PlayerName = UnitData:getPlayerName() - local UnitDesc = UnitData:getDesc() - local UnitCategory = UnitDesc.category - local UnitCoalition = UnitData:getCoalition() - local UnitTypeName = UnitData:getTypeName() - - self:T( { PlayerName, UnitName, UnitCategory, UnitCoalition, UnitTypeName } ) - - if self.Players[PlayerName] == nil then -- I believe this is the place where a Player gets a life in a mission when he enters a unit ... - self.Players[PlayerName] = {} - self.Players[PlayerName].Hit = {} - self.Players[PlayerName].Kill = {} - self.Players[PlayerName].Mission = {} - - -- for CategoryID, CategoryName in pairs( SCORINGCategory ) do - -- self.Players[PlayerName].Hit[CategoryID] = {} - -- self.Players[PlayerName].Kill[CategoryID] = {} - -- end - self.Players[PlayerName].HitPlayers = {} - self.Players[PlayerName].HitUnits = {} - self.Players[PlayerName].Score = 0 - self.Players[PlayerName].Penalty = 0 - self.Players[PlayerName].PenaltyCoalition = 0 - self.Players[PlayerName].PenaltyWarning = 0 - end - - if not self.Players[PlayerName].UnitCoalition then - self.Players[PlayerName].UnitCoalition = UnitCoalition - else - if self.Players[PlayerName].UnitCoalition ~= UnitCoalition then - self.Players[PlayerName].Penalty = self.Players[PlayerName].Penalty + 50 - self.Players[PlayerName].PenaltyCoalition = self.Players[PlayerName].PenaltyCoalition + 1 - MESSAGE:New( "Player '" .. PlayerName .. "' changed coalition from " .. _SCORINGCoalition[self.Players[PlayerName].UnitCoalition] .. " to " .. _SCORINGCoalition[UnitCoalition] .. - "(changed " .. self.Players[PlayerName].PenaltyCoalition .. " times the coalition). 50 Penalty points added.", - 2 - ):ToAll() - self:ScoreCSV( PlayerName, "COALITION_PENALTY", 1, -50, self.Players[PlayerName].UnitName, _SCORINGCoalition[self.Players[PlayerName].UnitCoalition], _SCORINGCategory[self.Players[PlayerName].UnitCategory], self.Players[PlayerName].UnitType, - UnitName, _SCORINGCoalition[UnitCoalition], _SCORINGCategory[UnitCategory], UnitData:getTypeName() ) - end - end - self.Players[PlayerName].UnitName = UnitName - self.Players[PlayerName].UnitCoalition = UnitCoalition - self.Players[PlayerName].UnitCategory = UnitCategory - self.Players[PlayerName].UnitType = UnitTypeName - - if self.Players[PlayerName].Penalty > 100 then - if self.Players[PlayerName].PenaltyWarning < 1 then - MESSAGE:New( "Player '" .. PlayerName .. "': WARNING! If you continue to commit FRATRICIDE and have a PENALTY score higher than 150, you will be COURT MARTIALED and DISMISSED from this mission! \nYour total penalty is: " .. self.Players[PlayerName].Penalty, - 30 - ):ToAll() - self.Players[PlayerName].PenaltyWarning = self.Players[PlayerName].PenaltyWarning + 1 - end - end - - if self.Players[PlayerName].Penalty > 150 then - ClientGroup = GROUP:NewFromDCSUnit( UnitData ) - ClientGroup:Destroy() - MESSAGE:New( "Player '" .. PlayerName .. "' committed FRATRICIDE, he will be COURT MARTIALED and is DISMISSED from this mission!", - 10 - ):ToAll() - end - - end -end - - ---- Registers Scores the players completing a Mission Task. --- @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 OnHit event for the scoring. --- @param #SCORING self --- @param Core.Event#EVENTDATA Event -function SCORING:_EventOnHit( Event ) - self:F( { Event } ) - - local InitUnit = nil - local InitUnitName = "" - local InitGroup = nil - local InitGroupName = "" - local InitPlayerName = nil - - local InitCoalition = nil - local InitCategory = nil - local InitType = nil - local InitUnitCoalition = nil - local InitUnitCategory = nil - local InitUnitType = nil - - local TargetUnit = nil - local TargetUnitName = "" - local TargetGroup = nil - local TargetGroupName = "" - local TargetPlayerName = "" - - local TargetCoalition = nil - local TargetCategory = nil - local TargetType = nil - local TargetUnitCoalition = nil - local TargetUnitCategory = nil - local TargetUnitType = nil - - if Event.IniDCSUnit then - - InitUnit = Event.IniDCSUnit - InitUnitName = Event.IniDCSUnitName - InitGroup = Event.IniDCSGroup - InitGroupName = Event.IniDCSGroupName - InitPlayerName = InitUnit:getPlayerName() - - InitCoalition = InitUnit:getCoalition() - --TODO: Workaround Client DCS Bug - --InitCategory = InitUnit:getCategory() - InitCategory = InitUnit:getDesc().category - InitType = InitUnit:getTypeName() - - InitUnitCoalition = _SCORINGCoalition[InitCoalition] - InitUnitCategory = _SCORINGCategory[InitCategory] - InitUnitType = InitType - - self:T( { InitUnitName, InitGroupName, InitPlayerName, InitCoalition, InitCategory, InitType , InitUnitCoalition, InitUnitCategory, InitUnitType } ) - end - - - if Event.TgtDCSUnit then - - TargetUnit = Event.TgtDCSUnit - TargetUnitName = Event.TgtDCSUnitName - TargetGroup = Event.TgtDCSGroup - TargetGroupName = Event.TgtDCSGroupName - TargetPlayerName = TargetUnit:getPlayerName() - - TargetCoalition = TargetUnit:getCoalition() - --TODO: Workaround Client DCS Bug - --TargetCategory = TargetUnit:getCategory() - TargetCategory = TargetUnit:getDesc().category - TargetType = TargetUnit:getTypeName() - - TargetUnitCoalition = _SCORINGCoalition[TargetCoalition] - TargetUnitCategory = _SCORINGCategory[TargetCategory] - TargetUnitType = TargetType - - self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType, TargetUnitCoalition, TargetUnitCategory, TargetUnitType } ) - end - - if InitPlayerName ~= nil then -- It is a player that is hitting something - self:_AddPlayerFromUnit( InitUnit ) - if self.Players[InitPlayerName] then -- This should normally not happen, but i'll test it anyway. - if TargetPlayerName ~= nil then -- It is a player hitting another player ... - self:_AddPlayerFromUnit( TargetUnit ) - self.Players[InitPlayerName].HitPlayers = self.Players[InitPlayerName].HitPlayers + 1 - end - - self:T( "Hitting Something" ) - -- What is he hitting? - if TargetCategory then - if not self.Players[InitPlayerName].Hit[TargetCategory] then - self.Players[InitPlayerName].Hit[TargetCategory] = {} - end - if not self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName] then - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName] = {} - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score = 0 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty = 0 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit = 0 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit = 0 - end - local Score = 0 - if InitCoalition == TargetCoalition then - self.Players[InitPlayerName].Penalty = self.Players[InitPlayerName].Penalty + 10 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty + 10 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit + 1 - MESSAGE:New( "Player '" .. InitPlayerName .. "' hit a friendly " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit .. " times. Penalty: -" .. self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty .. - ". Score Total:" .. self.Players[InitPlayerName].Score - self.Players[InitPlayerName].Penalty, - 2 - ):ToAll() - self:ScoreCSV( InitPlayerName, "HIT_PENALTY", 1, -25, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - else - self.Players[InitPlayerName].Score = self.Players[InitPlayerName].Score + 1 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score + 1 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit + 1 - MESSAGE:New( "Player '" .. InitPlayerName .. "' hit a target " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit .. " times. Score: " .. self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score .. - ". Score Total:" .. self.Players[InitPlayerName].Score - self.Players[InitPlayerName].Penalty, - 2 - ):ToAll() - self:ScoreCSV( InitPlayerName, "HIT_SCORE", 1, 1, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - end - end - end - elseif InitPlayerName == nil then -- It is an AI hitting a player??? - - end -end - - -function SCORING:ReportScoreAll() - - env.info( "Hello World " ) - - local ScoreMessage = "" - local PlayerMessage = "" - - self:T( "Score Report" ) - - for PlayerName, PlayerData in pairs( self.Players ) do - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) - - -- Some variables - local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] - local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local PlayerScore = 0 - local PlayerPenalty = 0 - - ScoreMessage = ":\n" - - local ScoreMessageHits = "" - - for CategoryID, CategoryName in pairs( _SCORINGCategory ) do - self:T( CategoryName ) - if PlayerData.Hit[CategoryID] then - local Score = 0 - local ScoreHit = 0 - local Penalty = 0 - local PenaltyHit = 0 - self:T( "Hit scores exist for player " .. PlayerName ) - for UnitName, UnitData in pairs( PlayerData.Hit[CategoryID] ) do - Score = Score + UnitData.Score - ScoreHit = ScoreHit + UnitData.ScoreHit - Penalty = Penalty + UnitData.Penalty - PenaltyHit = UnitData.PenaltyHit - end - local ScoreMessageHit = string.format( "%s:%d ", CategoryName, Score - Penalty ) - self:T( ScoreMessageHit ) - ScoreMessageHits = ScoreMessageHits .. ScoreMessageHit - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageHits ~= "" then - ScoreMessage = ScoreMessage .. " Hits: " .. ScoreMessageHits .. "\n" - end - - local ScoreMessageKills = "" - for CategoryID, CategoryName in pairs( _SCORINGCategory ) do - self:T( "Kill scores exist for player " .. PlayerName ) - if PlayerData.Kill[CategoryID] then - local Score = 0 - local ScoreKill = 0 - local Penalty = 0 - local PenaltyKill = 0 - - for UnitName, UnitData in pairs( PlayerData.Kill[CategoryID] ) do - Score = Score + UnitData.Score - ScoreKill = ScoreKill + UnitData.ScoreKill - Penalty = Penalty + UnitData.Penalty - PenaltyKill = PenaltyKill + UnitData.PenaltyKill - end - - local ScoreMessageKill = string.format( " %s:%d ", CategoryName, Score - Penalty ) - self:T( ScoreMessageKill ) - ScoreMessageKills = ScoreMessageKills .. ScoreMessageKill - - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageKills = ScoreMessageKills .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageKills ~= "" then - ScoreMessage = ScoreMessage .. " Kills: " .. ScoreMessageKills .. "\n" - end - - local ScoreMessageCoalitionChangePenalties = "" - if PlayerData.PenaltyCoalition ~= 0 then - ScoreMessageCoalitionChangePenalties = ScoreMessageCoalitionChangePenalties .. string.format( " -%d (%d changed)", PlayerData.Penalty, PlayerData.PenaltyCoalition ) - PlayerPenalty = PlayerPenalty + PlayerData.Penalty - end - if ScoreMessageCoalitionChangePenalties ~= "" then - ScoreMessage = ScoreMessage .. " Coalition Penalties: " .. ScoreMessageCoalitionChangePenalties .. "\n" - end - - local ScoreMessageMission = "" - local ScoreMission = 0 - local ScoreTask = 0 - for MissionName, MissionData in pairs( PlayerData.Mission ) do - ScoreMission = ScoreMission + MissionData.ScoreMission - ScoreTask = ScoreTask + MissionData.ScoreTask - ScoreMessageMission = ScoreMessageMission .. "'" .. MissionName .. "'; " - end - PlayerScore = PlayerScore + ScoreMission + ScoreTask - - if ScoreMessageMission ~= "" then - ScoreMessage = ScoreMessage .. " Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ")\n" - end - - PlayerMessage = PlayerMessage .. string.format( "Player '%s' Score:%d (%d Score -%d Penalties)%s", PlayerName, PlayerScore - PlayerPenalty, PlayerScore, PlayerPenalty, ScoreMessage ) - end - end - MESSAGE:New( PlayerMessage, 30, "Player Scores" ):ToAll() -end - - -function SCORING:ReportScorePlayer() - - env.info( "Hello World " ) - - local ScoreMessage = "" - local PlayerMessage = "" - - self:T( "Score Report" ) - - for PlayerName, PlayerData in pairs( self.Players ) do - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) - - -- Some variables - local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] - local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local PlayerScore = 0 - local PlayerPenalty = 0 - - ScoreMessage = "" - - local ScoreMessageHits = "" - - for CategoryID, CategoryName in pairs( _SCORINGCategory ) do - self:T( CategoryName ) - if PlayerData.Hit[CategoryID] then - local Score = 0 - local ScoreHit = 0 - local Penalty = 0 - local PenaltyHit = 0 - self:T( "Hit scores exist for player " .. PlayerName ) - for UnitName, UnitData in pairs( PlayerData.Hit[CategoryID] ) do - Score = Score + UnitData.Score - ScoreHit = ScoreHit + UnitData.ScoreHit - Penalty = Penalty + UnitData.Penalty - PenaltyHit = UnitData.PenaltyHit - end - local ScoreMessageHit = string.format( "\n %s = %d score(%d;-%d) hits(#%d;#-%d)", CategoryName, Score - Penalty, Score, Penalty, ScoreHit, PenaltyHit ) - self:T( ScoreMessageHit ) - ScoreMessageHits = ScoreMessageHits .. ScoreMessageHit - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageHits ~= "" then - ScoreMessage = ScoreMessage .. "\n Hits: " .. ScoreMessageHits .. " " - end - - local ScoreMessageKills = "" - for CategoryID, CategoryName in pairs( _SCORINGCategory ) do - self:T( "Kill scores exist for player " .. PlayerName ) - if PlayerData.Kill[CategoryID] then - local Score = 0 - local ScoreKill = 0 - local Penalty = 0 - local PenaltyKill = 0 - - for UnitName, UnitData in pairs( PlayerData.Kill[CategoryID] ) do - Score = Score + UnitData.Score - ScoreKill = ScoreKill + UnitData.ScoreKill - Penalty = Penalty + UnitData.Penalty - PenaltyKill = PenaltyKill + UnitData.PenaltyKill - end - - local ScoreMessageKill = string.format( "\n %s = %d score(%d;-%d) hits(#%d;#-%d)", CategoryName, Score - Penalty, Score, Penalty, ScoreKill, PenaltyKill ) - self:T( ScoreMessageKill ) - ScoreMessageKills = ScoreMessageKills .. ScoreMessageKill - - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageKills = ScoreMessageKills .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageKills ~= "" then - ScoreMessage = ScoreMessage .. "\n Kills: " .. ScoreMessageKills .. " " - end - - local ScoreMessageCoalitionChangePenalties = "" - if PlayerData.PenaltyCoalition ~= 0 then - ScoreMessageCoalitionChangePenalties = ScoreMessageCoalitionChangePenalties .. string.format( " -%d (%d changed)", PlayerData.Penalty, PlayerData.PenaltyCoalition ) - PlayerPenalty = PlayerPenalty + PlayerData.Penalty - end - if ScoreMessageCoalitionChangePenalties ~= "" then - ScoreMessage = ScoreMessage .. "\n Coalition: " .. ScoreMessageCoalitionChangePenalties .. " " - end - - local ScoreMessageMission = "" - local ScoreMission = 0 - local ScoreTask = 0 - for MissionName, MissionData in pairs( PlayerData.Mission ) do - ScoreMission = ScoreMission + MissionData.ScoreMission - ScoreTask = ScoreTask + MissionData.ScoreTask - ScoreMessageMission = ScoreMessageMission .. "'" .. MissionName .. "'; " - end - PlayerScore = PlayerScore + ScoreMission + ScoreTask - - if ScoreMessageMission ~= "" then - ScoreMessage = ScoreMessage .. "\n Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ") " - end - - PlayerMessage = PlayerMessage .. string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties ):%s", PlayerName, PlayerScore - PlayerPenalty, PlayerScore, PlayerPenalty, ScoreMessage ) - end - end - MESSAGE:New( PlayerMessage, 30, "Player Scores" ):ToAll() - -end - - -function SCORING:SecondsToClock(sSeconds) - local nSeconds = sSeconds - if nSeconds == 0 then - --return nil; - return "00:00:00"; - else - nHours = string.format("%02.f", math.floor(nSeconds/3600)); - nMins = string.format("%02.f", math.floor(nSeconds/60 - (nHours*60))); - nSecs = string.format("%02.f", math.floor(nSeconds - nHours*3600 - nMins *60)); - return nHours..":"..nMins..":"..nSecs - end -end - ---- Opens a score CSV file to log the scores. --- @param #SCORING self --- @param #string ScoringCSV --- @return #SCORING self --- @usage --- -- Open a new CSV file to log the scores of the game Gori Valley. Let the name of the CSV file begin with "Player Scores". --- ScoringObject = SCORING:New( "Gori Valley" ) --- ScoringObject:OpenCSV( "Player Scores" ) -function SCORING:OpenCSV( ScoringCSV ) - self:F( ScoringCSV ) - - if lfs and io and os then - if ScoringCSV then - self.ScoringCSV = ScoringCSV - local fdir = lfs.writedir() .. [[Logs\]] .. self.ScoringCSV .. " " .. os.date( "%Y-%m-%d %H-%M-%S" ) .. ".csv" - - self.CSVFile, self.err = io.open( fdir, "w+" ) - if not self.CSVFile then - error( "Error: Cannot open CSV file in " .. lfs.writedir() ) - end - - self.CSVFile:write( '"GameName","RunTime","Time","PlayerName","ScoreType","PlayerUnitCoaltion","PlayerUnitCategory","PlayerUnitType","PlayerUnitName","TargetUnitCoalition","TargetUnitCategory","TargetUnitType","TargetUnitName","Times","Score"\n' ) - - self.RunTime = os.date("%y-%m-%d_%H-%M-%S") - else - error( "A string containing the CSV file name must be given." ) - end - else - self:E( "The MissionScripting.lua file has not been changed to allow lfs, io and os modules to be used..." ) - end - return self -end - - ---- Registers a score for a player. --- @param #SCORING self --- @param #string PlayerName The name of the player. --- @param #string ScoreType The type of the score. --- @param #string ScoreTimes The amount of scores achieved. --- @param #string ScoreAmount The score given. --- @param #string PlayerUnitName The unit name of the player. --- @param #string PlayerUnitCoalition The coalition of the player unit. --- @param #string PlayerUnitCategory The category of the player unit. --- @param #string PlayerUnitType The type of the player unit. --- @param #string TargetUnitName The name of the target unit. --- @param #string TargetUnitCoalition The coalition of the target unit. --- @param #string TargetUnitCategory The category of the target unit. --- @param #string TargetUnitType The type of the target unit. --- @return #SCORING self -function SCORING:ScoreCSV( PlayerName, ScoreType, ScoreTimes, ScoreAmount, PlayerUnitName, PlayerUnitCoalition, PlayerUnitCategory, PlayerUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - --write statistic information to file - local ScoreTime = self:SecondsToClock( timer.getTime() ) - PlayerName = PlayerName:gsub( '"', '_' ) - - if PlayerUnitName and PlayerUnitName ~= '' then - local PlayerUnit = Unit.getByName( PlayerUnitName ) - - if PlayerUnit then - if not PlayerUnitCategory then - --PlayerUnitCategory = SCORINGCategory[PlayerUnit:getCategory()] - PlayerUnitCategory = _SCORINGCategory[PlayerUnit:getDesc().category] - end - - if not PlayerUnitCoalition then - PlayerUnitCoalition = _SCORINGCoalition[PlayerUnit:getCoalition()] - end - - if not PlayerUnitType then - PlayerUnitType = PlayerUnit:getTypeName() - end - else - PlayerUnitName = '' - PlayerUnitCategory = '' - PlayerUnitCoalition = '' - PlayerUnitType = '' - end - else - PlayerUnitName = '' - PlayerUnitCategory = '' - PlayerUnitCoalition = '' - PlayerUnitType = '' - end - - if not TargetUnitCoalition then - TargetUnitCoalition = '' - end - - if not TargetUnitCategory then - TargetUnitCategory = '' - end - - if not TargetUnitType then - TargetUnitType = '' - end - - if not TargetUnitName then - TargetUnitName = '' - end - - if lfs and io and os then - self.CSVFile:write( - '"' .. self.GameName .. '"' .. ',' .. - '"' .. self.RunTime .. '"' .. ',' .. - '' .. ScoreTime .. '' .. ',' .. - '"' .. PlayerName .. '"' .. ',' .. - '"' .. ScoreType .. '"' .. ',' .. - '"' .. PlayerUnitCoalition .. '"' .. ',' .. - '"' .. PlayerUnitCategory .. '"' .. ',' .. - '"' .. PlayerUnitType .. '"' .. ',' .. - '"' .. PlayerUnitName .. '"' .. ',' .. - '"' .. TargetUnitCoalition .. '"' .. ',' .. - '"' .. TargetUnitCategory .. '"' .. ',' .. - '"' .. TargetUnitType .. '"' .. ',' .. - '"' .. TargetUnitName .. '"' .. ',' .. - '' .. ScoreTimes .. '' .. ',' .. - '' .. ScoreAmount - ) - - self.CSVFile:write( "\n" ) - end -end - - -function SCORING:CloseCSV() - if lfs and io and os then - self.CSVFile:close() - end -end - ---- 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 @{Dcs.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 @{Dcs.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 - ---- This module contains the SPAWN class. --- --- # 1) @{Functional.Spawn#SPAWN} class, extends @{Core.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-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.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.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 - 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 @{Wrapper.Group#GROUP} that was spawned. --- @param #SPAWN self --- @param #function SpawnCallBackFunction The function to be called when a group spawns. --- @param SpawnFunctionArguments A random amount of arguments to be provided to the function when the group spawns. --- @return #SPAWN --- @usage --- -- Declare SpawnObject and call a function when a new Group is spawned. --- local SpawnObject = SPAWN --- :New( "SpawnObject" ) --- :InitLimit( 2, 10 ) --- :OnSpawnGroup( --- function( SpawnGroup ) --- SpawnGroup:E( "I am spawned" ) --- end --- ) --- :SpawnScheduled( 300, 0.3 ) -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 @{Core.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 mode... --- This will be similar to the uncontrolled flag setting in the ME. --- @param #SPAWN self --- @return #SPAWN self -function SPAWN:InitUnControlled() - self:F( { self.SpawnTemplatePrefix } ) - - self.SpawnUnControlled = true - - for SpawnGroupID = 1, self.SpawnMaxGroups do - self.SpawnGroups[SpawnGroupID].UnControlled = true - end - - return self -end - - - ---- Will return the SpawnGroupName either with with a specific count number or without any count. --- @param #SPAWN self --- @param #number SpawnIndex Is the number of the Group that is to be spawned. --- @return #string SpawnGroupName -function SPAWN:SpawnGroupName( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex } ) - - local SpawnPrefix = self.SpawnTemplatePrefix - if self.SpawnAliasPrefix then - SpawnPrefix = self.SpawnAliasPrefix - end - - if SpawnIndex then - local SpawnName = string.format( '%s#%03d', SpawnPrefix, SpawnIndex ) - self:T( SpawnName ) - return SpawnName - else - self:T( SpawnPrefix ) - return SpawnPrefix - end - -end - ---- 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 - - if SpawnTemplate.CategoryID == Group.Category.HELICOPTER or SpawnTemplate.CategoryID == Group.Category.AIRPLANE then - SpawnTemplate.uncontrolled = false - end - - for UnitID = 1, #SpawnTemplate.units do - SpawnTemplate.units[UnitID].name = string.format( SpawnTemplate.name .. '-%02d', UnitID ) - SpawnTemplate.units[UnitID].unitId = nil - 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 - 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].y = self.SpawnTemplate.units[1].y - 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 @{Wrapper.Group#GROUP} for a @{Wrapper.Client#CLIENT}, with an optional briefing text. --- --- ESCORT initialization methods. --- ============================== --- The following menus are created within the RADIO MENU 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:TaskRouteToVec3( GDV, Speed / 3.6 ) -- DCS models speed in Mps (Miles per second) - end - - return true - end - - return false -end - - ---- Report Targets Scheduler. --- @param #ESCORT self -function ESCORT:_ReportTargetsScheduler() - self:F( self.EscortGroup:GetName() ) - - if self.EscortGroup:IsAlive() and self.EscortClient:IsAlive() then - local EscortGroupName = self.EscortGroup:GetName() - local EscortTargets = self.EscortGroup:GetDetectedTargets() - - local ClientEscortTargets = self.EscortClient._EscortGroups[EscortGroupName].Targets - - local EscortTargetMessages = "" - for EscortTargetID, EscortTarget in pairs( EscortTargets ) do - local EscortObject = EscortTarget.object - self:T( EscortObject ) - if EscortObject and EscortObject:isExist() and EscortObject.id_ < 50000000 then - - local EscortTargetUnit = UNIT:Find( EscortObject ) - local EscortTargetUnitName = EscortTargetUnit:GetName() - - - - -- local EscortTargetIsDetected, - -- EscortTargetIsVisible, - -- EscortTargetLastTime, - -- EscortTargetKnowType, - -- EscortTargetKnowDistance, - -- EscortTargetLastPos, - -- EscortTargetLastVelocity - -- = self.EscortGroup:IsTargetDetected( EscortObject ) - -- - -- self:T( { EscortTargetIsDetected, - -- EscortTargetIsVisible, - -- EscortTargetLastTime, - -- EscortTargetKnowType, - -- EscortTargetKnowDistance, - -- EscortTargetLastPos, - -- EscortTargetLastVelocity } ) - - - local 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) @{Functional.MissileTrainer#MISSILETRAINER} class, extends @{Core.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. - SCHEDULER:New( TrainerWeapon, TrainerWeapon.destroy, {}, 2 ) - if ( TrainerWeapon:getTypeName() == "9M311" ) then - SCHEDULER:New( TrainerWeapon, TrainerWeapon.destroy, {}, 2 ) - 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) @{Functional.AirbasePolice#AIRBASEPOLICE_BASE} class, extends @{Core.Base#BASE} --- ================================================================== --- The @{Functional.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) @{Functional.AirbasePolice#AIRBASEPOLICE_CAUCASUS} class, extends @{Functional.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) @{Functional.AirbasePolice#AIRBASEPOLICE_NEVADA} class, extends @{Functional.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() - 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) @{Functional.Detection#DETECTION_BASE} class, extends @{Core.Base#BASE} --- ========================================================== --- The @{Functional.Detection#DETECTION_BASE} class defines the core functions to administer detected objects. --- The @{Functional.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 @{Functional.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: --- --- * @{Functional.Detection#DETECTION_BASE.InitDetectVisual}(): Detected using Visual. --- * @{Functional.Detection#DETECTION_BASE.InitDetectOptical}(): Detected using Optical. --- * @{Functional.Detection#DETECTION_BASE.InitDetectRadar}(): Detected using Radar. --- * @{Functional.Detection#DETECTION_BASE.InitDetectIRST}(): Detected using IRST. --- * @{Functional.Detection#DETECTION_BASE.InitDetectRWR}(): Detected using RWR. --- * @{Functional.Detection#DETECTION_BASE.InitDetectDLINK}(): Detected using DLINK. --- --- 1.3) Obtain objects detected by DETECTION_BASE --- ---------------------------------------------- --- DETECTION_BASE builds @{Set}s of objects detected. These @{Core.Set#SET_BASE}s can be retrieved using the method @{Functional.Detection#DETECTION_BASE.GetDetectedSets}(). --- The method will return a list (table) of @{Core.Set#SET_BASE} objects. --- --- === --- --- 2) @{Functional.Detection#DETECTION_AREAS} class, extends @{Functional.Detection#DETECTION_BASE} --- =============================================================================== --- The @{Functional.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 @{Core.Set#SET_UNIT}s containing the @{Wrapper.Unit#UNIT}s detected. --- The class is group the detected units within zones given a DetectedZoneRange parameter. --- A set with multiple detected zones will be created as there are groups of units detected. --- --- 2.1) Retrieve the Detected Unit sets and Detected Zones --- ------------------------------------------------------- --- The DetectedUnitSets methods are implemented in @{Functional.Detection#DECTECTION_BASE} and the DetectedZones methods is implemented in @{Functional.Detection#DETECTION_AREAS}. --- --- Retrieve the DetectedUnitSets with the method @{Functional.Detection#DETECTION_BASE.GetDetectedSets}(). A table will be return of @{Core.Set#SET_UNIT}s. --- To understand the amount of sets created, use the method @{Functional.Detection#DETECTION_BASE.GetDetectedSetCount}(). --- If you want to obtain a specific set from the DetectedSets, use the method @{Functional.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 @{Functional.Detection#DETECTION_BASE.GetDetectionZones}(). --- To understand the amount of zones created, use the method @{Functional.Detection#DETECTION_BASE.GetDetectionZoneCount}(). --- If you want to obtain a specific zone from the DetectedZones, use the method @{Functional.Detection#DETECTION_BASE.GetDetectionZone}() with a given index. --- --- 1.4) Flare or Smoke detected units --- ---------------------------------- --- Use the methods @{Functional.Detection#DETECTION_AREAS.FlareDetectedUnits}() or @{Functional.Detection#DETECTION_AREAS.SmokeDetectedUnits}() to flare or smoke the detected units when a new detection has taken place. --- --- 1.5) Flare or Smoke detected zones --- ---------------------------------- --- Use the methods @{Functional.Detection#DETECTION_AREAS.FlareDetectedZones}() or @{Functional.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 @{Core.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 @{Wrapper.Unit#UNIT}s in an array of @{Core.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( 0, 30 ) - - 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 @{Core.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 @{Core.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** / Mulit-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.AI_Balancer#AI_BALANCER} class, extends @{Core.Fsm#FSM_SET} --- --- The @{AI.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 @{Core.Fsm#FSM_SET} manages the functionality to control the Finite State Machine (FSM). --- The mission designer can tailor the behaviour of the AI_BALANCER, by defining event and state transition methods. --- An explanation about state and event transition methods can be found in the @{FSM} module documentation. --- --- 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** @{Wrapper.Airbase#AIRBASE}. --- * @{#AI_BALANCER.ReturnToNearestAirbases}: Returns the AI to the **nearest friendly** @{Wrapper.Airbase#AIRBASE}. --- --- Note that when AI returns to an airbase, the AI_BALANCER will trigger the **Return** event and the AI will return, --- otherwise the AI_BALANCER will trigger a **Destroy** event, and the AI will be destroyed. --- --- === --- --- # **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 - - 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 @{Wrapper.Airbase#AIRBASE}. --- @param #AI_BALANCER self --- @param Dcs.DCSTypes#Distance ReturnTresholdRange If there is an enemy @{Wrapper.Client#CLIENT} within the ReturnTresholdRange given in meters, the AI will not return to the nearest @{Wrapper.Airbase#AIRBASE}. --- @param Core.Set#SET_AIRBASE ReturnAirbaseSet The SET of @{Core.Set#SET_AIRBASE}s to evaluate where to return to. -function AI_BALANCER:ReturnToNearestAirbases( ReturnTresholdRange, ReturnAirbaseSet ) - - self.ToNearestAirbase = true - self.ReturnTresholdRange = ReturnTresholdRange - self.ReturnAirbaseSet = ReturnAirbaseSet -end - ---- Returns the AI to the home @{Wrapper.Airbase#AIRBASE}. --- @param #AI_BALANCER self --- @param Dcs.DCSTypes#Distance ReturnTresholdRange If there is an enemy @{Wrapper.Client#CLIENT} within the ReturnTresholdRange given in meters, the AI will not return to the nearest @{Wrapper.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 - 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 - ---- @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** / Mulit-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 @{Core.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:SetDetectionOn() - - self.CheckStatus = true - - self:ManageFuel( .2, 60 ) - self:ManageDamage( 1 ) - - self:SetDetectionInterval( 30 ) - - 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", "Ejected" ) - self:AddTransition( "*", "Crash", "Crashed" ) - self:AddTransition( "*", "PilotDead", "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.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.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 @{Wrapper.Unit#UNIT}s that were detected by the AI. --- No filtering is applied, so, ANY detected UNIT can be in this list. --- It is up to the mission designer to use the @{Unit} class and methods to filter the targets. --- @param #AI_PATROL_ZONE self --- @return #table The list of @{Wrapper.Unit#UNIT}s -function AI_PATROL_ZONE:GetDetectedUnits() - self:F2() - - return 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:EventOnPilotDead( self.OnPilotDead ) - self:EventOnCrash( self.OnCrash ) - self:EventOnEjection( self.OnEjection ) - - - Controllable:OptionROEHoldFire() - Controllable:OptionROTVertical() - - self.Controllable:OnReSpawn( - function( PatrolGroup ) - self:E( "ReSpawn" ) - self:__Reset() - self:__Route( 5 ) - end - ) - -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 - self:T( TargetObject ) - 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 } ) - self.DetectedUnits[TargetUnit] = TargetUnit - Detected = true - end - else - self.DetectedUnits[TargetUnit] = TargetUnit - 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:__Crash( 1, EventData ) - 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** / Mulit-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.AI_Patrol#AI_PATROL_ZONE} --- --- @{#AI_CAS_ZONE} derives from the @{AI.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 Dcs.DCSTypes#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO --- @param Core.Zone#ZONE EngageZone --- @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. - -- @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. - - --- 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", "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:EventOnDead( self.OnDead ) - -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: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 - - - if self.Controllable:IsNotInZone( self.EngageZone ) then - - -- Find a random 2D point in EngageZone. - local ToEngageZoneVec2 = self.EngageZone:GetRandomVec2() - self:T2( ToEngageZoneVec2 ) - - -- Define Speed and Altitude. - local ToEngageZoneAltitude = math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) - local ToEngageZoneSpeed = self.PatrolMaxSpeed - self:T2( ToEngageZoneSpeed ) - - -- Obtain a 3D @{Point} from the 2D point + altitude. - local ToEngageZonePointVec3 = POINT_VEC3:New( ToEngageZoneVec2.x, ToEngageZoneAltitude, ToEngageZoneVec2.y ) - - -- Create a route point of type air. - local ToEngageZoneRoutePoint = ToEngageZonePointVec3:RoutePointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToEngageZoneSpeed, - 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 ) - - --- 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 ToTargetRoutePoint = ToTargetPointVec3:RoutePointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, - true - ) - - ToTargetPointVec3:SmokeBlue() - - EngageRoute[#EngageRoute+1] = ToTargetRoutePoint - - - Controllable:OptionROEOpenFire() - Controllable:OptionROTPassiveDefense() - - 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, 2 ) - 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:SetDetectionOff() -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** / Mulit-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.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.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.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 DetectedUnitID, DetectedUnit 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 DetectedUnitID, DetectedUnit 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:__Accomplish( 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 / Mulit-Player:Yes / AI:Yes / Human:No / Types:Ground -- Management of logical cargo objects, that can be transported from and to transportation carriers. --- --- === --- --- 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.AI_Cargo#AI_CARGO} class, extends @{Core.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) @{Core.Point#POINT_VEC2) to where the cargo should run after onboarding. If not provided, the cargo will run to 60 meters behind the Carrier location. - ---- UnBoards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo from the Carrier. --- The cargo must be in the **Loaded** state. --- @function [parent=#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) @{Core.Point#POINT_VEC2) to where the cargo should run after onboarding. If not provided, the cargo will run to 60 meters behind the Carrier location. - - --- Load - ---- Loads the cargo to a Carrier. The event will load the cargo into the Carrier regardless of its position. There will be no movement simulated of the cargo loading. --- The cargo must be in the **UnLoaded** state. --- @function [parent=#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) @{Core.Point#POINT_VEC2) to where the cargo will be placed after unloading. If not provided, the cargo will be placed 60 meters behind the Carrier location. - ---- UnLoads the cargo to a Carrier. The event will unload the cargo from the Carrier. There will be no movement simulated of the cargo loading. --- The cargo must be in the **Loaded** state. --- @function [parent=#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) @{Core.Point#POINT_VEC2) to where the cargo will be placed after unloading. If not provided, the cargo will be placed 60 meters behind the Carrier location. - --- State Transition Functions - --- UnLoaded - ---- @function [parent=#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 @{Core.Fsm#FSM_PROCESS} --- --- ## ACT_ASSIGN state machine: --- --- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur. --- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below. --- Each derived class follows exactly the same process, using the same events and following the same state transitions, --- 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 @{Core.Fsm#FSM_PROCESS} --- --- ## ACT_ROUTE state machine: --- --- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur. --- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below. --- Each derived class follows exactly the same process, using the same events and following the same state transitions, --- 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 @{Core.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:EventOnDead( 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 @{Core.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:EventOnBirth( - --- @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:EventOnPlayerEnterUnit( - --- @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:EventOnPlayerLeaveUnit( - --- @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:EventOnCrash( - --- @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 @{Core.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. --- --- 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, -} - ---- 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: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() - - -- Handle the birth of new planes within the assigned set. - - - -- Handle when a player crashes ... - -- The Task is UnAssigned from the Unit. - -- When there is no Unit left running the Task, and all of the Players crashed, the Task goes into Failed ... --- self:EventOnCrash( --- --- @param #TASK self --- -- @param Core.Event#EVENTDATA EventData --- function( self, EventData ) --- self:E( "In LeaveUnit" ) --- self:E( { "State", self:GetState() } ) --- if self:IsStateAssigned() then --- local TaskUnit = EventData.IniUnit --- local TaskGroup = EventData.IniUnit:GetGroup() --- self:E( self.SetGroup:IsIncludeObject( TaskGroup ) ) --- if self.SetGroup:IsIncludeObject( TaskGroup ) then --- self:UnAssignFromUnit( TaskUnit ) --- end --- self:MessageToGroups( TaskUnit:GetPlayerName() .. " crashed!, and has aborted Task " .. self:GetName() ) --- end --- end --- ) --- - - 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 - ---- 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() -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() - -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 - -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) @{Tasking.DetectionManager#DETECTION_MANAGER} class, extends @{Core.Base#BASE} --- ==================================================================== --- The @{Tasking.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: --- ----------------------------------- --- * @{Tasking.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 @{Tasking.DetectionManager#DETECTION_MANAGER.ReportDetected}(). This method implements polymorphic behaviour. --- --- The time interval in seconds of the reporting can be changed using the methods @{Tasking.DetectionManager#DETECTION_MANAGER.SetReportInterval}(). --- To control how long a reporting message is displayed, use @{Tasking.DetectionManager#DETECTION_MANAGER.SetReportDisplayTime}(). --- Derived classes need to implement the method @{Tasking.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 @{Tasking.DetectionManager#DETECTION_MANAGER.StartReporting}() and @{Tasking.DetectionManager#DETECTION_MANAGER.StopReporting}() respectively. --- If an ad-hoc report is requested, use the method @{Tasking.DetectionManager#DETECTION_MANAGER#ReportNow}(). --- --- The default reporting interval is every 60 seconds. The reporting messages are displayed 15 seconds. --- --- === --- --- 2) @{Tasking.DetectionManager#DETECTION_REPORTING} class, extends @{Tasking.DetectionManager#DETECTION_MANAGER} --- ========================================================================================= --- The @{Tasking.DetectionManager#DETECTION_REPORTING} class implements detected units reporting. Reporting can be controlled using the reporting methods available in the @{Tasking.DetectionManager#DETECTION_MANAGER} class. --- --- 2.1) DETECTION_REPORTING constructor: --- ------------------------------- --- The @{Tasking.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 @{Wrapper.Unit#UNIT}s detected within the @{Functional.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 @{Functional.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 @{Functional.Detection#DETECTION_BASE} object. - -- @return #boolean Return true if you want the reporting to continue... false will cancel the reporting loop. - function DETECTION_REPORTING:ProcessDetected( Group, Detection ) - self:F2( Group ) - - 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 @{Functional.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 @{Tasking.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 @{Tasking.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 @{Wrapper.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 @{Tasking.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 @{Tasking.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 @{Wrapper.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" ) - ---- 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 add0090dd..da1821733 100644 --- a/Moose Mission Setup/Moose.lua +++ b/Moose Mission Setup/Moose.lua @@ -1,31741 +1,31 @@ -env.info( '*** MOOSE STATIC INCLUDE START *** ' ) -env.info( 'Moose Generation Timestamp: 20170203_2208' ) +env.info( '*** MOOSE DYNAMIC INCLUDE START *** ' ) +env.info( 'Moose Generation Timestamp: 20170203_2219' ) + 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 @{Core.Base#BASE.New) constructor within the @{Core.Base#BASE.Inherit) method. --- See an example at the @{Core.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. --- Therefore, the BASE class exposes the following event handling functions: --- --- * @{#BASE.EventOnBirth}(): Handle the birth of a new unit. --- * @{#BASE.EventOnBaseCaptured}(): Handle the capturing of an airbase or a helipad. --- * @{#BASE.EventOnCrash}(): Handle the crash of a unit. --- * @{#BASE.EventOnDead}(): Handle the death of a unit. --- * @{#BASE.EventOnEjection}(): Handle the ejection of a player out of an airplane. --- * @{#BASE.EventOnEngineShutdown}(): Handle the shutdown of an engine. --- * @{#BASE.EventOnEngineStartup}(): Handle the startup of an engine. --- * @{#BASE.EventOnHit}(): Handle the hit of a shell to a unit. --- * @{#BASE.EventOnHumanFailure}(): No a clue ... --- * @{#BASE.EventOnLand}(): Handle the event when a unit lands. --- * @{#BASE.EventOnMissionStart}(): Handle the start of the mission. --- * @{#BASE.EventOnPilotDead}(): Handle the event when a pilot is dead. --- * @{#BASE.EventOnPlayerComment}(): Handle the event when a player posts a comment. --- * @{#BASE.EventOnPlayerEnterUnit}(): Handle the event when a player enters a unit. --- * @{#BASE.EventOnPlayerLeaveUnit}(): Handle the event when a player leaves a unit. --- * @{#BASE.EventOnBirthPlayerMissionEnd}(): Handle the event when a player ends the mission. (Not a clue what that does). --- * @{#BASE.EventOnRefueling}(): Handle the event when a unit is refueling. --- * @{#BASE.EventOnShootingEnd}(): Handle the event when a unit starts shooting (guns). --- * @{#BASE.EventOnShootingStart}(): Handle the event when a unit ends shooting (guns). --- * @{#BASE.EventOnShot}(): Handle the event when a unit shot a missile. --- * @{#BASE.EventOnTakeOff}(): Handle the event when a unit takes off from a runway. --- * @{#BASE.EventOnTookControl}(): Handle the event when a player takes control of a unit. --- --- The EventOn() methods provide the @{Core.Event#EVENTDATA} structure to the event handling function. --- The @{Core.Event#EVENTDATA} structure contains an enriched data set of information about the event being handled. --- --- Find below an example of the prototype how to write an event handling function: --- --- CommandCenter:EventOnPlayerEnterUnit( --- --- @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 --- Mission:JoinUnit( PlayerUnit ) --- Mission:ReportDetails() --- end --- end --- ) --- --- Note the function( self, EventData ). It takes two parameters: --- --- * self = the object that is handling the EventOnPlayerEnterUnit. --- * EventData = the @{Core.Event#EVENTDATA} structure, containing more information of the Event. --- --- ## 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, - 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 - ---- Set a new listener for the class. --- @param self --- @param Dcs.DCSTypes#Event Event --- @param #function EventFunction --- @return #BASE -function BASE:AddEvent( Event, EventFunction ) - self:F( Event ) - - self.Events[#self.Events+1] = {} - self.Events[#self.Events].Event = Event - self.Events[#self.Events].EventFunction = EventFunction - self.Events[#self.Events].EventEnabled = false - - return self -end - ---- Returns the event dispatcher --- @param #BASE self --- @return Core.Event#EVENT -function BASE:Event() - - return _EVENTDISPATCHER -end - ---- Remove all subscribed events --- @param #BASE self --- @return #BASE -function BASE:EventRemoveAll() - - _EVENTDISPATCHER:RemoveAll( self ) - - return self -end - ---- Subscribe to a S_EVENT\_SHOT event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnShot( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_SHOT ) - - return self -end - ---- Subscribe to a S_EVENT\_HIT event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnHit( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_HIT ) - - return self -end - ---- Subscribe to a S_EVENT\_TAKEOFF event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnTakeOff( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_TAKEOFF ) - - return self -end - ---- Subscribe to a S_EVENT\_LAND event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnLand( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_LAND ) - - return self -end - ---- Subscribe to a S_EVENT\_CRASH event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnCrash( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_CRASH ) - - return self -end - ---- Subscribe to a S_EVENT\_EJECTION event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnEjection( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_EJECTION ) - - return self -end - - ---- Subscribe to a S_EVENT\_REFUELING event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnRefueling( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_REFUELING ) - - return self -end - ---- Subscribe to a S_EVENT\_DEAD event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnDead( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_DEAD ) - - return self -end - ---- Subscribe to a S_EVENT_PILOT\_DEAD event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnPilotDead( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_PILOT_DEAD ) - - return self -end - ---- Subscribe to a S_EVENT_BASE\_CAPTURED event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnBaseCaptured( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_BASE_CAPTURED ) - - return self -end - ---- Subscribe to a S_EVENT_MISSION\_START event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnMissionStart( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_MISSION_START ) - - return self -end - ---- Subscribe to a S_EVENT_MISSION\_END event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnPlayerMissionEnd( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_MISSION_END ) - - return self -end - ---- Subscribe to a S_EVENT_TOOK\_CONTROL event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnTookControl( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_TOOK_CONTROL ) - - return self -end - ---- Subscribe to a S_EVENT_REFUELING\_STOP event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnRefuelingStop( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_REFUELING_STOP ) - - return self -end - ---- Subscribe to a S_EVENT\_BIRTH event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnBirth( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_BIRTH ) - - return self -end - ---- Subscribe to a S_EVENT_HUMAN\_FAILURE event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnHumanFailure( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_HUMAN_FAILURE ) - - return self -end - ---- Subscribe to a S_EVENT_ENGINE\_STARTUP event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnEngineStartup( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_ENGINE_STARTUP ) - - return self -end - ---- Subscribe to a S_EVENT_ENGINE\_SHUTDOWN event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnEngineShutdown( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_ENGINE_SHUTDOWN ) - - return self -end - ---- Subscribe to a S_EVENT_PLAYER_ENTER\_UNIT event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnPlayerEnterUnit( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_PLAYER_ENTER_UNIT ) - - return self -end - ---- Subscribe to a S_EVENT_PLAYER_LEAVE\_UNIT event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnPlayerLeaveUnit( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_PLAYER_LEAVE_UNIT ) - - return self -end - ---- Subscribe to a S_EVENT_PLAYER\_COMMENT event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnPlayerComment( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_PLAYER_COMMENT ) - - return self -end - ---- Subscribe to a S_EVENT_SHOOTING\_START event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnShootingStart( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_SHOOTING_START ) - - return self -end - ---- Subscribe to a S_EVENT_SHOOTING\_END event. --- @param #BASE self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @return #BASE -function BASE:EventOnShootingEnd( EventFunction ) - - _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_SHOOTING_END ) - - return self -end - - - - - - - ---- Enable the event listeners for the class. --- @param #BASE self --- @return #BASE -function BASE:EnableEvents() - self:F( #self.Events ) - - for EventID, Event in pairs( self.Events ) do - Event.Self = self - Event.EventEnabled = true - end - self.Events.Handler = world.addEventHandler( self ) - - return self -end - - ---- Disable the event listeners for the class. --- @param #BASE self --- @return #BASE -function BASE:DisableEvents() - self:F() - - world.removeEventHandler( self ) - for EventID, Event in pairs( self.Events ) do - Event.Self = nil - Event.EventEnabled = false - end - - return self -end - - -local BaseEventCodes = { - "S_EVENT_SHOT", - "S_EVENT_HIT", - "S_EVENT_TAKEOFF", - "S_EVENT_LAND", - "S_EVENT_CRASH", - "S_EVENT_EJECTION", - "S_EVENT_REFUELING", - "S_EVENT_DEAD", - "S_EVENT_PILOT_DEAD", - "S_EVENT_BASE_CAPTURED", - "S_EVENT_MISSION_START", - "S_EVENT_MISSION_END", - "S_EVENT_TOOK_CONTROL", - "S_EVENT_REFUELING_STOP", - "S_EVENT_BIRTH", - "S_EVENT_HUMAN_FAILURE", - "S_EVENT_ENGINE_STARTUP", - "S_EVENT_ENGINE_SHUTDOWN", - "S_EVENT_PLAYER_ENTER_UNIT", - "S_EVENT_PLAYER_LEAVE_UNIT", - "S_EVENT_PLAYER_COMMENT", - "S_EVENT_SHOOTING_START", - "S_EVENT_SHOOTING_END", - "S_EVENT_MAX", -} - ---onEvent( {[1]="S_EVENT_BIRTH",[2]={["subPlace"]=5,["time"]=0,["initiator"]={["id_"]=16884480,},["place"]={["id_"]=5000040,},["id"]=15,["IniUnitName"]="US F-15C@RAMP-Air Support Mountains#001-01",},} --- Event = { --- id = enum world.event, --- time = Time, --- initiator = Unit, --- target = Unit, --- place = Unit, --- subPlace = enum world.BirthPlace, --- weapon = Weapon --- } - ---- Creation of a Birth Event. --- @param #BASE self --- @param 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) @{Core.Scheduler#SCHEDULER} class, extends @{Core.Base#BASE} --- --- The @{Core.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: --- --- * @{Core.Scheduler#SCHEDULER.New}( nil ): Setup a new SCHEDULER object, which is persistently executed after garbage collection. --- * @{Core.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. --- * @{Core.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. --- * @{Core.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: --- --- * @{Core.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. --- * @{Core.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 @{Core.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 @{Core.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 module contains the EVENT class. --- --- === --- --- Takes care of EVENT dispatching between DCS events and event handling functions defined in MOOSE classes. --- --- === --- --- The above menus classes **are derived** from 2 main **abstract** classes defined within the MOOSE framework (so don't use these): --- --- === --- --- ### Contributions: - --- ### Authors: FlightControl : Design & Programming --- --- @module Event - ---- The EVENT structure --- @type EVENT --- @field #EVENT.Events Events -EVENT = { - ClassName = "EVENT", - ClassID = 0, - SortedEvents = {}, -} - -local _EVENTCODES = { - "S_EVENT_SHOT", - "S_EVENT_HIT", - "S_EVENT_TAKEOFF", - "S_EVENT_LAND", - "S_EVENT_CRASH", - "S_EVENT_EJECTION", - "S_EVENT_REFUELING", - "S_EVENT_DEAD", - "S_EVENT_PILOT_DEAD", - "S_EVENT_BASE_CAPTURED", - "S_EVENT_MISSION_START", - "S_EVENT_MISSION_END", - "S_EVENT_TOOK_CONTROL", - "S_EVENT_REFUELING_STOP", - "S_EVENT_BIRTH", - "S_EVENT_HUMAN_FAILURE", - "S_EVENT_ENGINE_STARTUP", - "S_EVENT_ENGINE_SHUTDOWN", - "S_EVENT_PLAYER_ENTER_UNIT", - "S_EVENT_PLAYER_LEAVE_UNIT", - "S_EVENT_PLAYER_COMMENT", - "S_EVENT_SHOOTING_START", - "S_EVENT_SHOOTING_END", - "S_EVENT_MAX", -} - -local _EVENTORDER = { - [world.event.S_EVENT_SHOT] = 1, - [world.event.S_EVENT_HIT] = 1, - [world.event.S_EVENT_TAKEOFF] = 1, - [world.event.S_EVENT_LAND] = 1, - [world.event.S_EVENT_CRASH] = -1, - [world.event.S_EVENT_EJECTION] = -1, - [world.event.S_EVENT_REFUELING] = 1, - [world.event.S_EVENT_DEAD] = -1, - [world.event.S_EVENT_PILOT_DEAD] = -1, - [world.event.S_EVENT_BASE_CAPTURED] = 1, - [world.event.S_EVENT_MISSION_START] = 1, - [world.event.S_EVENT_MISSION_END] = -1, - [world.event.S_EVENT_TOOK_CONTROL] = 1, - [world.event.S_EVENT_REFUELING_STOP] = 1, - [world.event.S_EVENT_BIRTH] = 1, - [world.event.S_EVENT_HUMAN_FAILURE] = 1, - [world.event.S_EVENT_ENGINE_STARTUP] = 1, - [world.event.S_EVENT_ENGINE_SHUTDOWN] = 1, - [world.event.S_EVENT_PLAYER_ENTER_UNIT] = 1, - [world.event.S_EVENT_PLAYER_LEAVE_UNIT] = -1, - [world.event.S_EVENT_PLAYER_COMMENT] = 1, - [world.event.S_EVENT_SHOOTING_START] = 1, - [world.event.S_EVENT_SHOOTING_END] = 1, - [world.event.S_EVENT_MAX] = 1, -} - ---- The Event structure --- @type EVENTDATA --- @field id --- @field initiator --- @field target --- @field weapon --- @field IniDCSUnit --- @field IniDCSUnitName --- @field Wrapper.Unit#UNIT IniUnit --- @field #string IniUnitName --- @field IniDCSGroup --- @field IniDCSGroupName --- @field TgtDCSUnit --- @field TgtDCSUnitName --- @field Wrapper.Unit#UNIT TgtUnit --- @field #string TgtUnitName --- @field TgtDCSGroup --- @field TgtDCSGroupName --- @field Weapon --- @field WeaponName --- @field WeaponTgtDCSUnit - ---- The Events structure --- @type EVENT.Events --- @field #number IniUnit - -function EVENT:New() - local self = BASE:Inherit( self, BASE:New() ) - self:F2() - self.EventHandler = world.addEventHandler( self ) - return self -end - -function EVENT:EventText( EventID ) - - local EventText = _EVENTCODES[EventID] - - return EventText -end - - ---- Initializes the Events structure for the event --- @param #EVENT self --- @param Dcs.DCSWorld#world.event EventID --- @param Core.Base#BASE EventClass --- @return #EVENT.Events -function EVENT:Init( EventID, EventClass ) - self:F3( { _EVENTCODES[EventID], 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" } ) - self.SortedEvents[EventID] = setmetatable( {}, { __mode = "k" } ) - end - - if not self.Events[EventID][EventClass] then - self.Events[EventID][EventClass] = setmetatable( {}, { __mode = "k" } ) - end - return self.Events[EventID][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, _EVENTCODES[EventID] } ) - - local EventClass = EventClass - self.Events[EventID][EventClass] = nil - - self.SortedEvents[EventID] = nil - self.SortedEvents[EventID] = {} - for EventClass, Event in pairs(self.Events[EventID]) do table.insert( self.SortedEvents[EventID], Event) end - table.sort( self.SortedEvents[EventID], function( Event1, Event2 ) return Event1.EventTime < Event2.EventTime end ) - -end - ---- Clears all event subscriptions for a @{Core.Base#BASE} derived object. --- @param #EVENT self --- @param Core.Base#BASE EventObject -function EVENT:RemoveAll( EventObject ) - self:F3( { EventObject:GetClassNameAndID() } ) - - local EventClass = EventObject:GetClassNameAndID() - for EventID, EventData in pairs( self.Events ) do - self.Events[EventID][EventClass] = nil - self.SortedEvents[EventID] = 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 - Event.EventTime = EventClass.EventPriority and EventClass.EventPriority or 10 - - self.SortedEvents[EventID] = nil - self.SortedEvents[EventID] = {} - for EventClass, Event in pairs(self.Events[EventID]) do table.insert( self.SortedEvents[EventID], Event) end - table.sort( self.SortedEvents[EventID], function( Event1, Event2 ) return Event1.EventTime < Event2.EventTime end ) - - 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 and Event.initiator:getCategory() == Object.Category.UNIT then - Event.IniDCSUnit = Event.initiator - Event.IniDCSGroup = Event.IniDCSUnit:getGroup() - Event.IniDCSUnitName = Event.IniDCSUnit:getName() - Event.IniUnitName = Event.IniDCSUnitName - Event.IniUnit = UNIT:FindByName( Event.IniDCSUnitName ) - 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 - end - if Event.target then - if Event.target and Event.target:getCategory() == Object.Category.UNIT then - Event.TgtDCSUnit = Event.target - Event.TgtDCSGroup = Event.TgtDCSUnit:getGroup() - Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() - Event.TgtUnitName = Event.TgtDCSUnitName - Event.TgtUnit = UNIT:FindByName( Event.TgtDCSUnitName ) - Event.TgtDCSGroupName = "" - if Event.TgtDCSGroup and Event.TgtDCSGroup:isExist() then - Event.TgtDCSGroupName = Event.TgtDCSGroup:getName() - end - end - end - if Event.weapon then - Event.Weapon = Event.weapon - Event.WeaponName = Event.Weapon:getTypeName() - --Event.WeaponTgtDCSUnit = Event.Weapon:getTarget() - end - self:E( { _EVENTCODES[Event.id], Event, Event.IniDCSUnitName, Event.TgtDCSUnitName } ) - - local function pairsByEventSorted( EventSorted, Order ) - local i = Order == -1 and #EventSorted or 0 - local iter = function() - i = i + Order - if EventSorted[i] == nil then - return nil - else - return EventSorted[i].EventClass, EventSorted[i] - end - end - return iter - end - - self:E( { Order = _EVENTORDER[Event.id] } ) - - -- 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 pairsByEventSorted( self.SortedEvents[Event.id], _EVENTORDER[Event.id] ) 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 - self:E( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName, EventData.EventTime } ) - Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) - local Result, Value = xpcall( function() return EventData.IniUnit[Event.IniDCSUnitName].EventFunction( EventData.IniUnit[Event.IniDCSUnitName].EventClass, Event ) end, ErrorHandler ) - --EventData.IniUnit[Event.IniDCSUnitName].EventFunction( EventData.IniUnit[Event.IniDCSUnitName].EventClass, Event ) - 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 - self:E( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID(), EventData.EventTime } ) - Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) - local Result, Value = xpcall( function() return EventData.EventFunction( EventData.EventClass, Event ) end, ErrorHandler ) - --EventData.EventFunction( EventData.EventClass, Event ) - end - end - end - end - else - self:E( { _EVENTCODES[Event.id], Event } ) - end -end - ---- This module contains the MENU classes. --- --- There is a small note... When you see a class like MENU_COMMAND_COALITION with COMMAND in italic, it acutally represents it like this: `MENU_COMMAND_COALITION`. --- --- === --- --- 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_**: --- --- * @{Core.Menu#MENU_MISSION}: Manages main menus for whole mission file. --- * @{Core.Menu#MENU_COALITION}: Manages main menus for whole coalition. --- * @{Core.Menu#MENU_GROUP}: Manages main menus for GROUPs. --- * @{Core.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_**: --- --- * @{Core.Menu#MENU_MISSION_COMMAND}: Manages command menus for whole mission file. --- * @{Core.Menu#MENU_COALITION_COMMAND}: Manages command menus for whole coalition. --- * @{Core.Menu#MENU_GROUP_COMMAND}: Manages command menus for GROUPs. --- * @{Core.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) @{Core.Menu#MENU_BASE} class, extends @{Core.Base#BASE} --- -------------------------------------------------- --- The @{#MENU_BASE} class defines the main MENU class where other MENU classes are derived from. --- --- 1.2) @{Core.Menu#MENU_COMMAND_BASE} class, extends @{Core.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#MENU_MISSION} class, extends @{Core.Menu#MENU_BASE} --- --------------------------------------------------------- --- The @{Core.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#MENU_MISSION_COMMAND} class, extends @{Core.Menu#MENU_COMMAND_BASE} --- ------------------------------------------------------------------------- --- The @{Core.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#MENU_COALITION} class, extends @{Core.Menu#MENU_BASE} --- ------------------------------------------------------------ --- The @{Core.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 @{Core.Menu#MENU_COMMAND_BASE} --- ---------------------------------------------------------------------------- --- The @{Core.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 @{Core.Menu#MENU_BASE} --- -------------------------------------------------------- --- The @{Core.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 @{Core.Menu#MENU_COMMAND_BASE} --- ------------------------------------------------------------------------ --- The @{Core.Menu#MENU_GROUP_COMMAND} class manages the command menus for coalitions, which allow players to execute functions during mission execution. --- You can add menus with the @{#MENU_GROUP_COMMAND.New} method, which constructs a MENU_GROUP_COMMAND object and returns you the object reference. --- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_GROUP_COMMAND.Remove}. --- --- === --- --- 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 @{Core.Menu#MENU_BASE} --- --------------------------------------------------------- --- The @{Core.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 @{Core.Menu#MENU_COMMAND_BASE} --- ------------------------------------------------------------------------- --- The @{Core.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. - -- @param CommandMenuArgument An argument for the function. - -- @return Menu#MENU_CLIENT_COMMAND self - function MENU_CLIENT_COMMAND:New( MenuClient, 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 = MenuClient - self.MenuClientGroupID = MenuClient:GetClientGroupID() - self.MenuParentPath = MenuParentPath - self.MenuText = MenuText - self.ParentMenu = ParentMenu - - if not _MENUCLIENTS[self.MenuClientGroupID] then - _MENUCLIENTS[self.MenuClientGroupID] = {} - end - - local MenuPath = _MENUCLIENTS[self.MenuClientGroupID] - - self:T( { MenuClient:GetClientGroupName(), MenuPath[table.concat(MenuParentPath)], MenuParentPath, MenuText, CommandMenuFunction, 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 - - ParentMenu.Menus[self.MenuPath] = self - - 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 module contains the ZONE classes, inherited from @{Core.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 @{Wrapper.Group#GROUP} objects). --- * Test if not in the zone. --- * Distance to the nearest intersecting point of the zone. --- * Distance to the center of the zone. --- * ... --- --- Each of these ZONE classes have a zone name, and specific parameters defining the zone type: --- --- * @{Core.Zone#ZONE_BASE}: The ZONE_BASE class defining the base for all other zone classes. --- * @{Core.Zone#ZONE_RADIUS}: The ZONE_RADIUS class defined by a zone name, a location and a radius. --- * @{Core.Zone#ZONE}: The ZONE class, defined by the zone name as defined within the Mission Editor. --- * @{Core.Zone#ZONE_UNIT}: The ZONE_UNIT class defines by a zone around a @{Wrapper.Unit#UNIT} with a radius. --- * @{Core.Zone#ZONE_GROUP}: The ZONE_GROUP class defines by a zone around a @{Wrapper.Group#GROUP} with a radius. --- * @{Core.Zone#ZONE_POLYGON}: The ZONE_POLYGON class defines by a sequence of @{Wrapper.Group#GROUP} waypoints within the Mission Editor, forming a polygon. --- --- === --- --- 1) @{Core.Zone#ZONE_BASE} class, extends @{Core.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 @{Core.Zone#ZONE_BASE}: --- --- * @{#ZONE_BASE.IsPointVec2InZone}(): Returns if a @{Core.Point#POINT_VEC2} is within the zone. --- * @{#ZONE_BASE.IsPointVec3InZone}(): Returns if a @{Core.Point#POINT_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 @{Dcs.DCSTypes#Vec2} coordinate of the zone. --- * @{#ZONE_BASE.GetRandomVec2}(): Define a random @{Dcs.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) @{Core.Zone#ZONE_RADIUS} class, extends @{Core.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) @{Core.Zone#ZONE_RADIUS} constructor: --- --- * @{#ZONE_BASE.New}(): Constructor. --- --- ### 2.2) Manage the radius of the zone: --- --- * @{#ZONE_BASE.SetRadius}(): Sets the radius of the zone. --- * @{#ZONE_BASE.GetRadius}(): Returns the radius of the zone. --- --- ### 2.3) Manage the location of the zone: --- --- * @{#ZONE_BASE.SetVec2}(): Sets the @{Dcs.DCSTypes#Vec2} of the zone. --- * @{#ZONE_BASE.GetVec2}(): Returns the @{Dcs.DCSTypes#Vec2} of the zone. --- * @{#ZONE_BASE.GetVec3}(): Returns the @{Dcs.DCSTypes#Vec3} of the zone, taking an additional height parameter. --- --- === --- --- 3) @{Core.Zone#ZONE} class, extends @{Core.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) @{Core.Zone#ZONE_UNIT} class, extends @{Core.Zone#ZONE_RADIUS} --- ======================================================= --- The ZONE_UNIT class defined by a zone around a @{Wrapper.Unit#UNIT} with a radius. --- This class implements the inherited functions from @{Core.Zone#ZONE_RADIUS} taking into account the own zone format and properties. --- --- === --- --- 5) @{Core.Zone#ZONE_GROUP} class, extends @{Core.Zone#ZONE_RADIUS} --- ======================================================= --- The ZONE_GROUP class defines by a zone around a @{Wrapper.Group#GROUP} with a radius. The current leader of the group defines the center of the zone. --- This class implements the inherited functions from @{Core.Zone#ZONE_RADIUS} taking into account the own zone format and properties. --- --- === --- --- 6) @{Core.Zone#ZONE_POLYGON_BASE} class, extends @{Core.Zone#ZONE_BASE} --- ======================================================== --- The ZONE_POLYGON_BASE class defined by a sequence of @{Wrapper.Group#GROUP} waypoints within the Mission Editor, forming a polygon. --- This class implements the inherited functions from @{Core.Zone#ZONE_RADIUS} taking into account the own zone format and properties. --- This class is an abstract BASE class for derived classes, and is not meant to be instantiated. --- --- === --- --- 7) @{Core.Zone#ZONE_POLYGON} class, extends @{Core.Zone#ZONE_POLYGON_BASE} --- ================================================================ --- The ZONE_POLYGON class defined by a sequence of @{Wrapper.Group#GROUP} waypoints within the Mission Editor, forming a polygon. --- This class implements the inherited functions from @{Core.Zone#ZONE_RADIUS} taking into account the own zone format and properties. --- --- ==== --- --- **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: --- --- 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 --- @author FlightControl - - ---- 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:IsPointVec2InZone( 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:IsPointVec3InZone( Vec3 ) - self:F2( Vec3 ) - - local InZone = self:IsPointVec2InZone( { x = Vec3.x, y = Vec3.z } ) - - return InZone -end - ---- Returns the @{Dcs.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 @{Dcs.DCSTypes#Vec2} within the zone. --- @param #ZONE_BASE self --- @return Dcs.DCSTypes#Vec2 The Vec2 coordinates. -function ZONE_BASE:GetRandomVec2() - 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 - - ---- 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 ) ) - self:F( { ZoneName, Vec2, Radius } ) - - self.Radius = Radius - self.Vec2 = Vec2 - - 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 @{Dcs.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 @{Dcs.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 @{Dcs.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:IsPointVec2InZone( 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:IsPointVec3InZone( Vec3 ) - self:F2( Vec3 ) - - local InZone = self:IsPointVec2InZone( { x = Vec3.x, y = Vec3.z } ) - - return InZone -end - ---- Returns a random location within the zone. --- @param #ZONE_RADIUS self --- @param #number inner minimal distance from the center of the zone --- @param #number outer minimal distance from the outer edge 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 - - - ---- 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 @{Wrapper.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 @{Wrapper.Unit#UNIT}. --- @param #ZONE_UNIT self --- @return Dcs.DCSTypes#Vec2 The location of the zone based on the @{Wrapper.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 @{Dcs.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 @{Wrapper.Group#GROUP} and a radius. --- @param #ZONE_GROUP self --- @param #string ZoneName Name of the zone. --- @param Wrapper.Group#GROUP ZoneGROUP The @{Group} as the center of the zone. --- @param Dcs.DCSTypes#Distance Radius The radius of the zone. --- @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 @{Dcs.DCSTypes#Vec2}, forming a polygon. --- @type ZONE_POLYGON_BASE --- @field #ZONE_POLYGON_BASE.ListVec2 Polygon The polygon defined by an array of @{Dcs.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 @{Dcs.DCSTypes#Vec2}, forming a polygon. --- The @{Wrapper.Group#GROUP} waypoints define the polygon corners. The first and the last point are automatically connected. --- @param #ZONE_POLYGON_BASE self --- @param #string ZoneName Name of the zone. --- @param #ZONE_POLYGON_BASE.ListVec2 PointsArray An array of @{Dcs.DCSTypes#Vec2}, forming a polygon.. --- @return #ZONE_POLYGON_BASE self -function ZONE_POLYGON_BASE:New( ZoneName, PointsArray ) - local self = BASE:Inherit( self, ZONE_BASE:New( ZoneName ) ) - self:F( { ZoneName, PointsArray } ) - - local i = 0 - - self.Polygon = {} - - for i = 1, #PointsArray do - self.Polygon[i] = {} - self.Polygon[i].x = PointsArray[i].x - self.Polygon[i].y = PointsArray[i].y - end - - return self -end - ---- Flush polygon coordinates as a table in DCS.log. --- @param #ZONE_POLYGON_BASE self --- @return #ZONE_POLYGON_BASE self -function ZONE_POLYGON_BASE:Flush() - self:F2() - - self:E( { Polygon = self.ZoneName, Coordinates = self.Polygon } ) - - return self -end - - ---- Smokes the zone boundaries in a color. --- @param #ZONE_POLYGON_BASE self --- @param 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:IsPointVec2InZone( 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 @{Dcs.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:IsPointVec2InZone( Vec2 ) then - Vec2Found = true - end - end - - self:T2( Vec2 ) - - return Vec2 -end - ---- Get the bounding square the zone. --- @param #ZONE_POLYGON_BASE self --- @return #ZONE_POLYGON_BASE.BoundingSquare The bounding square. -function ZONE_POLYGON_BASE:GetBoundingSquare() - - local x1 = self.Polygon[1].x - local y1 = self.Polygon[1].y - local x2 = self.Polygon[1].x - local y2 = self.Polygon[1].y - - for i = 2, #self.Polygon do - self:T2( { self.Polygon[i], x1, y1, x2, y2 } ) - x1 = ( x1 > self.Polygon[i].x ) and self.Polygon[i].x or x1 - x2 = ( x2 < self.Polygon[i].x ) and self.Polygon[i].x or x2 - y1 = ( y1 > self.Polygon[i].y ) and self.Polygon[i].y or y1 - y2 = ( y2 < self.Polygon[i].y ) and self.Polygon[i].y or y2 - - end - - return { x1 = x1, y1 = y1, x2 = x2, y2 = y2 } -end - - - - - ---- The ZONE_POLYGON class defined by a sequence of @{Wrapper.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 @{Wrapper.Group#GROUP} defined within the Mission Editor. --- The @{Wrapper.Group#GROUP} waypoints define the polygon corners. The first and the last point are automatically connected by ZONE_POLYGON. --- @param #ZONE_POLYGON self --- @param #string ZoneName Name of the zone. --- @param Wrapper.Group#GROUP ZoneGroup The GROUP waypoints as defined within the Mission Editor define the polygon shape. --- @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) @{Core.Database#DATABASE} class, extends @{Core.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 = {}, - EventPriority = 1, -- Used to sort the DCS event order processing (complicated). Database has highest priority. - -} - -local _DATABASECoalition = - { - [1] = "Red", - [2] = "Blue", - } - -local _DATABASECategory = - { - ["plane"] = Unit.Category.AIRPLANE, - ["helicopter"] = Unit.Category.HELICOPTER, - ["vehicle"] = Unit.Category.GROUND_UNIT, - ["ship"] = Unit.Category.SHIP, - ["static"] = Unit.Category.STRUCTURE, - } - - ---- Creates a new DATABASE object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. --- @param #DATABASE self --- @return #DATABASE --- @usage --- -- Define a new DATABASE Object. This DBObject will contain a reference to all Group and Unit Templates defined within the ME and the DCSRTE. --- DBObject = DATABASE:New() -function DATABASE:New() - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) - - _EVENTDISPATCHER:OnBirth( self._EventOnBirth, self ) - _EVENTDISPATCHER:OnDead( self._EventOnDeadOrCrash, self ) - _EVENTDISPATCHER:OnCrash( self._EventOnDeadOrCrash, self ) - - - -- Follow alive players and clients - _EVENTDISPATCHER:OnPlayerEnterUnit( self._EventOnPlayerEnterUnit, self ) - _EVENTDISPATCHER:OnPlayerLeaveUnit( self._EventOnPlayerLeaveUnit, self ) - - self:_RegisterTemplates() - self:_RegisterGroupsAndUnits() - self:_RegisterClients() - self:_RegisterStatics() - self:_RegisterPlayers() - self:_RegisterAirbases() - - return self -end - ---- Finds a Unit based on the Unit Name. --- @param #DATABASE self --- @param #string UnitName --- @return 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 - self:AddUnit( Event.IniDCSUnitName ) - self:AddGroup( Event.IniDCSGroupName ) - self:_EventOnPlayerEnterUnit( Event ) - end -end - - ---- Handles the OnDead or OnCrash event for alive units set. --- @param #DATABASE self --- @param Core.Event#EVENTDATA Event -function DATABASE:_EventOnDeadOrCrash( Event ) - self:F2( { Event } ) - - if Event.IniDCSUnit then - if self.UNITS[Event.IniDCSUnitName] then - self:DeleteUnit( Event.IniDCSUnitName ) - -- add logic to correctly remove a group once all units are destroyed... - end - end -end - - ---- Handles the OnPlayerEnterUnit event to fill the active players table (with the unit filter applied). --- @param #DATABASE self --- @param Core.Event#EVENTDATA Event -function DATABASE:_EventOnPlayerEnterUnit( Event ) - self:F2( { Event } ) - - if Event.IniUnit 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 - - ---- 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 - local PlayerName = Event.IniUnit:GetPlayerName() - if self.PLAYERS[PlayerName] then - self:DeletePlayer( PlayerName ) - end - end -end - ---- Iterators - ---- Iterate the DATABASE and call an iterator function for the given set, providing the Object for each element within the set and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called when there is an alive player in the database. --- @return #DATABASE self -function DATABASE:ForEach( IteratorFunction, FinalizeFunction, arg, Set ) - self:F2( arg ) - - local function CoRoutine() - local Count = 0 - for ObjectID, Object in pairs( Set ) do - self:T2( Object ) - IteratorFunction( Object, unpack( arg ) ) - Count = Count + 1 --- if Count % 100 == 0 then --- coroutine.yield( false ) --- end - end - return true - end - --- local co = coroutine.create( CoRoutine ) - local co = CoRoutine - - local function Schedule() - --- local status, res = coroutine.resume( co ) - local status, res = co() - self:T3( { status, res } ) - - if status == false then - error( res ) - end - if res == false then - return true -- resume next time the loop - end - if FinalizeFunction then - FinalizeFunction( unpack( arg ) ) - end - return false - end - - local Scheduler = SCHEDULER:New( self, Schedule, {}, 0.001, 0.001, 0 ) - - return self -end - - ---- Iterate the DATABASE and call an iterator function for each **alive** UNIT, providing the UNIT and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the database. The function needs to accept a UNIT parameter. --- @return #DATABASE self -function DATABASE:ForEachUnit( IteratorFunction, FinalizeFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, FinalizeFunction, arg, self.UNITS ) - - return self -end - ---- Iterate the DATABASE and call an iterator function for each **alive** GROUP, providing the GROUP and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the database. The function needs to accept a GROUP parameter. --- @return #DATABASE self -function DATABASE:ForEachGroup( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.GROUPS ) - - return self -end - - ---- Iterate the DATABASE and call an iterator function for each **ALIVE** player, providing the player name and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called when there is an player in the database. The function needs to accept the player name. --- @return #DATABASE self -function DATABASE:ForEachPlayer( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.PLAYERS ) - - return self -end - - ---- Iterate the DATABASE and call an iterator function for each player who has joined the mission, providing the Unit of the player and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called when there is was a player in the database. The function needs to accept a UNIT parameter. --- @return #DATABASE self -function DATABASE:ForEachPlayerJoined( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.PLAYERSJOINED ) - - return self -end - ---- Iterate the DATABASE and call an iterator function for each CLIENT, providing the CLIENT to the function and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called when there is an alive player in the database. The function needs to accept a CLIENT parameter. --- @return #DATABASE self -function DATABASE:ForEachClient( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.CLIENTS ) - - return self -end - - -function DATABASE:_RegisterTemplates() - self:F2() - - self.Navpoints = {} - self.UNITS = {} - --Build routines.db.units and self.Navpoints - for CoalitionName, coa_data in pairs(env.mission.coalition) do - - if (CoalitionName == 'red' or CoalitionName == 'blue') and type(coa_data) == 'table' then - --self.Units[coa_name] = {} - - ---------------------------------------------- - -- build nav points DB - self.Navpoints[CoalitionName] = {} - if coa_data.nav_points then --navpoints - for nav_ind, nav_data in pairs(coa_data.nav_points) do - - if type(nav_data) == 'table' then - self.Navpoints[CoalitionName][nav_ind] = routines.utils.deepCopy(nav_data) - - self.Navpoints[CoalitionName][nav_ind]['name'] = nav_data.callsignStr -- name is a little bit more self-explanatory. - self.Navpoints[CoalitionName][nav_ind]['point'] = {} -- point is used by SSE, support it. - self.Navpoints[CoalitionName][nav_ind]['point']['x'] = nav_data.x - self.Navpoints[CoalitionName][nav_ind]['point']['y'] = 0 - self.Navpoints[CoalitionName][nav_ind]['point']['z'] = nav_data.y - end - end - end - ------------------------------------------------- - if coa_data.country then --there is a country table - for cntry_id, cntry_data in pairs(coa_data.country) do - - local CountryName = string.upper(cntry_data.name) - --self.Units[coa_name][countryName] = {} - --self.Units[coa_name][countryName]["countryId"] = cntry_data.id - - if type(cntry_data) == 'table' then --just making sure - - for obj_type_name, obj_type_data in pairs(cntry_data) do - - if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then --should be an unncessary check - - local CategoryName = obj_type_name - - if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! - - --self.Units[coa_name][countryName][category] = {} - - for group_num, GroupTemplate in pairs(obj_type_data.group) do - - if GroupTemplate and GroupTemplate.units and type(GroupTemplate.units) == 'table' then --making sure again- this is a valid group - self:_RegisterTemplate( - GroupTemplate, - coalition.side[string.upper(CoalitionName)], - _DATABASECategory[string.lower(CategoryName)], - country.id[string.upper(CountryName)] - ) - end --if GroupTemplate and GroupTemplate.units then - end --for group_num, GroupTemplate in pairs(obj_type_data.group) do - end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then - end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then - end --for obj_type_name, obj_type_data in pairs(cntry_data) do - end --if type(cntry_data) == 'table' then - end --for cntry_id, cntry_data in pairs(coa_data.country) do - end --if coa_data.country then --there is a country table - end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then - end --for coa_name, coa_data in pairs(mission.coalition) do - - return self -end - - - - ---- This module contains the SET classes. --- --- === --- --- 1) @{Core.Set#SET_BASE} class, extends @{Core.Base#BASE} --- ============================================== --- The @{Core.Set#SET_BASE} class defines the core functions that define a collection of objects. --- A SET provides iterators to iterate the SET, but will **temporarily** yield the ForEach interator loop at defined **"intervals"** to the mail simulator loop. --- In this way, large loops can be done while not blocking the simulator main processing loop. --- The default **"yield interval"** is after 10 objects processed. --- The default **"time interval"** is after 0.001 seconds. --- --- 1.1) Add or remove objects from the SET --- --------------------------------------- --- Some key core functions are @{Core.Set#SET_BASE.Add} and @{Core.Set#SET_BASE.Remove} to add or remove objects from the SET in your logic. --- --- 1.2) Define the SET iterator **"yield interval"** and the **"time interval"** --- ----------------------------------------------------------------------------- --- Modify the iterator intervals with the @{Core.Set#SET_BASE.SetInteratorIntervals} method. --- You can set the **"yield interval"**, and the **"time interval"**. (See above). --- --- === --- --- 2) @{Core.Set#SET_GROUP} class, extends @{Core.Set#SET_BASE} --- ================================================== --- Mission designers can use the @{Core.Set#SET_GROUP} class to build sets of groups belonging to certain: --- --- * Coalitions --- * Categories --- * Countries --- * Starting with certain prefix strings. --- --- 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 @{Core.Set#SET_GROUP.AddGroupsByName} and @{Core.Set#SET_GROUP.RemoveGroupsByName} respectively. --- These methods take a single GROUP name or an array of GROUP names to be added or removed from SET_GROUP. --- --- 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 @{Core.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) @{Core.Set#SET_UNIT} class, extends @{Core.Set#SET_BASE} --- =================================================== --- Mission designers can use the @{Core.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 @{Core.Set#SET_UNIT.AddUnitsByName} and @{Core.Set#SET_UNIT.RemoveUnitsByName} respectively. --- These methods take a single UNIT name or an array of UNIT names to be added or removed from SET_UNIT. --- --- 3.3) SET_UNIT filter criteria: --- ------------------------------ --- 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 @{Core.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) @{Core.Set#SET_CLIENT} class, extends @{Core.Set#SET_BASE} --- =================================================== --- Mission designers can use the @{Core.Set#SET_CLIENT} class to build sets of units belonging to certain: --- --- * Coalitions --- * Categories --- * Countries --- * Client types --- * Starting with certain prefix strings. --- --- 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 @{Core.Set#SET_CLIENT.AddClientsByName} and @{Core.Set#SET_CLIENT.RemoveClientsByName} respectively. --- These methods take a single CLIENT name or an array of CLIENT names to be added or removed from SET_CLIENT. --- --- 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 @{Core.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) @{Core.Set#SET_AIRBASE} class, extends @{Core.Set#SET_BASE} --- ==================================================== --- Mission designers can use the @{Core.Set#SET_AIRBASE} class to build sets of airbases optionally belonging to certain: --- --- * Coalitions --- --- 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 @{Core.Set#SET_AIRBASE.AddAirbasesByName} and @{Core.Set#SET_AIRBASE.RemoveAirbasesByName} respectively. --- These methods take a single AIRBASE name or an array of AIRBASE names to be added or removed from SET_AIRBASE. --- --- 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 = {}, - EventPriority = 2, -- Used to sort the DCS event order processing (complicated) -} - ---- 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 ) - - return self -end - ---- Finds an @{Core.Base#BASE} object based on the object Name. --- @param #SET_BASE self --- @param #string ObjectName --- @return Core.Base#BASE The Object found. -function SET_BASE:_Find( ObjectName ) - - local 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 @{Core.Base#BASE} object in the @{Core.Set#SET_BASE}, using a given ObjectName as the index. --- @param #SET_BASE self --- @param #string ObjectName --- @param Core.Base#BASE Object --- @return Core.Base#BASE The added BASE Object. -function SET_BASE:Add( ObjectName, Object ) - self:F2( ObjectName ) - - 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 @{Core.Base#BASE} object in the @{Core.Set#SET_BASE}, using the Object Name as the index. --- @param #SET_BASE self --- @param Wrapper.Object#OBJECT Object --- @return Core.Base#BASE The added BASE Object. -function SET_BASE:AddObject( Object ) - self:F2( Object.ObjectName ) - - self:T( Object.UnitName ) - self:T( Object.ObjectName ) - self:Add( Object.ObjectName, Object ) - -end - - - ---- Removes a @{Core.Base#BASE} object from the @{Core.Set#SET_BASE} and derived classes, based on the Object Name. --- @param #SET_BASE self --- @param #string ObjectName -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 @{Core.Base#BASE} object from the @{Core.Set#SET_BASE} and derived classes, based on the Object Name. --- @param #SET_BASE self --- @param #string ObjectName --- @return Core.Base#BASE -function SET_BASE:Get( ObjectName ) - self:F( ObjectName ) - - local t = self.Set[ObjectName] - - self:T3( { ObjectName, t } ) - - return t - -end - ---- Retrieves the amount of objects in the @{Core.Set#SET_BASE} and derived classes. --- @param #SET_BASE self --- @return #number Count -function SET_BASE:Count() - - return self.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 - - _EVENTDISPATCHER:OnBirth( self._EventOnBirth, self ) - _EVENTDISPATCHER:OnDead( self._EventOnDeadOrCrash, self ) - _EVENTDISPATCHER:OnCrash( self._EventOnDeadOrCrash, self ) - - -- Follow alive players and clients - _EVENTDISPATCHER:OnPlayerEnterUnit( self._EventOnPlayerEnterUnit, self ) - _EVENTDISPATCHER:OnPlayerLeaveUnit( self._EventOnPlayerLeaveUnit, self ) - - - return self -end - ---- Stops the filtering for the defined collection. --- @param #SET_BASE self --- @return #SET_BASE self -function SET_BASE:FilterStop() - - _EVENTDISPATCHER:OnBirthRemove( self ) - _EVENTDISPATCHER:OnDeadRemove( self ) - _EVENTDISPATCHER:OnCrashRemove( self ) - - return self -end - ---- Iterate the SET_BASE while identifying the nearest object from a @{Core.Point#POINT_VEC2}. --- @param #SET_BASE self --- @param Core.Point#POINT_VEC2 PointVec2 A @{Core.Point#POINT_VEC2} object from where to evaluate the closest object in the set. --- @return Core.Base#BASE The closest object. -function SET_BASE:FindNearestObjectFromPointVec2( PointVec2 ) - self:F2( PointVec2 ) - - local NearestObject = nil - local ClosestDistance = nil - - for ObjectID, ObjectData in pairs( self.Set ) do - if NearestObject == nil then - NearestObject = ObjectData - ClosestDistance = PointVec2: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 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 not self.Database[Event.IniDCSGroupName] then - self.Database[Event.IniDCSGroupName] = GROUP:Register( Event.IniDCSGroupName ) - self:T3( self.Database[Event.IniDCSGroupName] ) - end - - return Event.IniDCSGroupName, self.Database[Event.IniDCSGroupName] -end - ---- Handles the Database to check on any event that Object exists in the Database. --- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! --- @param #SET_GROUP self --- @param 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 not self.Database[Event.IniDCSUnitName] then - self.Database[Event.IniDCSUnitName] = UNIT:Register( Event.IniDCSUnitName ) - self:T3( self.Database[Event.IniDCSUnitName] ) - end - - return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] -end - ---- Handles the Database to check on any event that Object exists in the Database. --- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! --- @param #SET_UNIT self --- @param 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 @{Wrapper.Airbase#AIRBASE} from a @{Core.Point#POINT_VEC2}. --- @param #SET_AIRBASE self --- @param Core.Point#POINT_VEC2 PointVec2 A @{Core.Point#POINT_VEC2} object from where to evaluate the closest @{Wrapper.Airbase#AIRBASE}. --- @return Wrapper.Airbase#AIRBASE The closest @{Wrapper.Airbase#AIRBASE}. -function SET_AIRBASE:FindNearestAirbaseFromPointVec2( PointVec2 ) - self:F2( PointVec2 ) - - local NearestAirbase = self:FindNearestObjectFromPointVec2( PointVec2 ) - return NearestAirbase -end - - - ---- --- @param #SET_AIRBASE self --- @param Wrapper.Airbase#AIRBASE MAirbase --- @return #SET_AIRBASE self -function SET_AIRBASE:IsIncludeObject( MAirbase ) - self:F2( MAirbase ) - - local MAirbaseInclude = true - - if MAirbase then - local MAirbaseName = MAirbase:GetName() - - if self.Filter.Coalitions then - local MAirbaseCoalition = false - for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do - local AirbaseCoalitionID = _DATABASE:GetCoalitionFromAirbase( MAirbaseName ) - self:T3( { "Coalition:", AirbaseCoalitionID, self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) - if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == AirbaseCoalitionID then - MAirbaseCoalition = true - end - end - self:T( { "Evaluated Coalition", MAirbaseCoalition } ) - MAirbaseInclude = MAirbaseInclude and MAirbaseCoalition - end - - if self.Filter.Categories then - local MAirbaseCategory = false - for CategoryID, CategoryName in pairs( self.Filter.Categories ) do - local AirbaseCategoryID = _DATABASE:GetCategoryFromAirbase( MAirbaseName ) - self:T3( { "Category:", AirbaseCategoryID, self.FilterMeta.Categories[CategoryName], CategoryName } ) - if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == AirbaseCategoryID then - MAirbaseCategory = true - end - end - self:T( { "Evaluated Category", MAirbaseCategory } ) - MAirbaseInclude = MAirbaseInclude and MAirbaseCategory - end - end - - self:T2( MAirbaseInclude ) - return MAirbaseInclude -end ---- This module contains the POINT classes. --- --- 1) @{Core.Point#POINT_VEC3} class, extends @{Core.Base#BASE} --- ================================================== --- The @{Core.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_VEC3.New}(): a 3D point. --- * @{#POINT_VEC3.NewFromVec3}(): a 3D point created from a @{Dcs.DCSTypes#Vec3}. --- --- --- 2) @{Core.Point#POINT_VEC2} class, extends @{Core.Point#POINT_VEC3} --- ========================================================= --- The @{Core.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_VEC2.New}(): a 2D point, taking an additional height parameter. --- * @{#POINT_VEC2.NewFromVec2}(): a 2D point created from a @{Dcs.DCSTypes#Vec2}. --- --- === --- --- **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: --- --- 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 --- @extends Core.Base#BASE --- @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 -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 --- @extends #POINT_VEC3 --- @field Dcs.DCSTypes#Distance x The x coordinate in meters. --- @field Dcs.DCSTypes#Distance y the y coordinate in meters. -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 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 #number x The x coordinate. -function POINT_VEC3:SetX( x ) - self.x = x -end - ---- Set the y coordinate of the POINT_VEC3. --- @param #number y The y coordinate. -function POINT_VEC3:SetY( y ) - self.y = y -end - ---- Set the z coordinate of the POINT_VEC3. --- @param #number z The z coordinate. -function POINT_VEC3:SetZ( z ) - self.z = z -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 - - ---- 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 #number x The x coordinate. -function POINT_VEC2:SetX( x ) - self.x = x -end - ---- Set the y coordinate of the POINT_VEC2. --- @param #number y The y coordinate. -function POINT_VEC2:SetY( y ) - self.z = y -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 @{Dcs.DCSTypes#Vec2}. --- @param #POINT_VEC2 self --- @param Dcs.DCSTypes#Vec2 Vec2Reference The reference @{Dcs.DCSTypes#Vec2}. --- @return Dcs.DCSTypes#Distance The distance from the reference @{Dcs.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) @{Core.Message#MESSAGE} class, extends @{Core.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 @{Core.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 @{Core.Message#MESSAGE.ToClient}. --- * Coalitions with @{Core.Message#MESSAGE.ToCoalition}. --- * All Players with @{Core.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 all players. --- @param #MESSAGE self --- @return #MESSAGE --- @usage --- -- Send a message created to all players. --- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ):ToAll() --- or --- MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ):ToAll() --- or --- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ) --- MessageAll:ToAll() -function MESSAGE:ToAll() - self:F() - - self:ToCoalition( coalition.side.RED ) - self:ToCoalition( coalition.side.BLUE ) - - return self -end - - - ------ The MESSAGEQUEUE class ----- @type MESSAGEQUEUE ---MESSAGEQUEUE = { --- ClientGroups = {}, --- CoalitionSides = {} ---} --- ---function MESSAGEQUEUE:New( RefreshInterval ) --- local self = BASE:Inherit( self, BASE:New() ) --- self:F( { RefreshInterval } ) --- --- self.RefreshInterval = RefreshInterval --- --- --self.DisplayFunction = routines.scheduleFunction( self._DisplayMessages, { self }, 0, RefreshInterval ) --- self.DisplayFunction = SCHEDULER:New( self, self._DisplayMessages, {}, 0, RefreshInterval ) --- --- return self ---end --- ------ This function is called automatically by the MESSAGEQUEUE scheduler. ---function MESSAGEQUEUE:_DisplayMessages() --- --- -- First we display all messages that a coalition needs to receive... Also those who are not in a client (CA module clients...). --- for CoalitionSideID, CoalitionSideData in pairs( self.CoalitionSides ) do --- for MessageID, MessageData in pairs( CoalitionSideData.Messages ) do --- if MessageData.MessageSent == false then --- --trigger.action.outTextForCoalition( CoalitionSideID, MessageData.MessageCategory .. '\n' .. MessageData.MessageText:gsub("\n$",""):gsub("\n$",""), MessageData.MessageDuration ) --- MessageData.MessageSent = true --- end --- local MessageTimeLeft = ( MessageData.MessageTime + MessageData.MessageDuration ) - timer.getTime() --- if MessageTimeLeft <= 0 then --- MessageData = nil --- end --- end --- end --- --- -- Then we send the messages for each individual client, but also to be included are those Coalition messages for the Clients who belong to a coalition. --- -- Because the Client messages will overwrite the Coalition messages (for that Client). --- for ClientGroupName, ClientGroupData in pairs( self.ClientGroups ) do --- for MessageID, MessageData in pairs( ClientGroupData.Messages ) do --- if MessageData.MessageGroup == false then --- trigger.action.outTextForGroup( Group.getByName(ClientGroupName):getID(), MessageData.MessageCategory .. '\n' .. MessageData.MessageText:gsub("\n$",""):gsub("\n$",""), MessageData.MessageDuration ) --- MessageData.MessageGroup = true --- end --- local MessageTimeLeft = ( MessageData.MessageTime + MessageData.MessageDuration ) - timer.getTime() --- if MessageTimeLeft <= 0 then --- MessageData = nil --- end --- end --- --- -- Now check if the Client also has messages that belong to the Coalition of the Client... --- for CoalitionSideID, CoalitionSideData in pairs( self.CoalitionSides ) do --- for MessageID, MessageData in pairs( CoalitionSideData.Messages ) do --- local CoalitionGroup = Group.getByName( ClientGroupName ) --- if CoalitionGroup and CoalitionGroup:getCoalition() == CoalitionSideID then --- if MessageData.MessageCoalition == false then --- trigger.action.outTextForGroup( Group.getByName(ClientGroupName):getID(), MessageData.MessageCategory .. '\n' .. MessageData.MessageText:gsub("\n$",""):gsub("\n$",""), MessageData.MessageDuration ) --- MessageData.MessageCoalition = true --- end --- end --- local MessageTimeLeft = ( MessageData.MessageTime + MessageData.MessageDuration ) - timer.getTime() --- if MessageTimeLeft <= 0 then --- MessageData = nil --- end --- end --- end --- end --- --- return true ---end --- ------ The _MessageQueue object is created when the MESSAGE class module is loaded. -----_MessageQueue = MESSAGEQUEUE:New( 0.5 ) --- ---- 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) @{Core.Fsm#FSM} class, extends @{Core.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.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) - if self[handler] then - self:T( "Calling " .. handler ) - 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) == false ) - or ( self:_call_handler("OnBefore" .. EventName, params) == false ) - or ( self:_call_handler("onleave" .. from, params) == false ) - or ( self:_call_handler("OnLeave" .. from, params) == 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) - self:_call_handler("OnEnter" .. to, params) - self:_call_handler("onafter" .. EventName, params) - self:_call_handler("OnAfter" .. EventName, params) - self:_call_handler("onstatechange", params) - 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) - self:_call_handler("OnEnter" .. to, params) - --end - - self:_call_handler("onafter" .. EventName, params) - self:_call_handler("OnAfter" .. EventName, params) - - self:_call_handler("onstatechange", params) - 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 = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1 ) - 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 ) - - 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 ) - 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 ) - if self[handler] then - self:T( "Calling " .. handler ) - 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 ) - if self[handler] then - self:T( "Calling " .. handler ) - return self[handler]( self, self.Set, unpack( params ) ) - end - end - -end -- FSM_SET - ---- This module contains the OBJECT class. --- --- 1) @{Wrapper.Object#OBJECT} class, extends @{Core.Base#BASE} --- =========================================================== --- The @{Wrapper.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: --- --- * @{Wrapper.Object#OBJECT.New}(): Create a OBJECT instance. --- --- 1.2) OBJECT methods: --- -------------------------- --- The following methods can be used to identify an Object object: --- --- * @{Wrapper.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 @{Wrapper.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 - - - - - - - - - ---- This module contains the POSITIONABLE class. --- --- 1) @{Wrapper.Positionable#POSITIONABLE} class, extends @{Wrapper.Identifiable#IDENTIFIABLE} --- =========================================================== --- The @{Wrapper.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: --- --- * @{Wrapper.Positionable#POSITIONABLE.New}(): Create a POSITIONABLE instance. --- --- 1.2) POSITIONABLE methods: --- -------------------------- --- The following methods can be used to identify an measurable object: --- --- * @{Wrapper.Positionable#POSITIONABLE.GetID}(): Returns the ID of the measurable object. --- * @{Wrapper.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 @{Dcs.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:E( 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 @{Dcs.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 @{Dcs.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 @{Dcs.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) @{Wrapper.Controllable#CONTROLLABLE} class, extends @{Wrapper.Positionable#POSITIONABLE} --- =========================================================== --- The @{Wrapper.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 @{Wrapper.Controllable#CONTROLLABLE.PushTask} or @{Wrapper.Controllable#SetTask} method to assign the task to the CONTROLLABLE. --- Tasks are specific for the category of the CONTROLLABLE, more specific, for AIR, GROUND or AIR and GROUND. --- Each task description where applicable indicates for which controllable category the task is valid. --- There are 2 main subdivisions of tasks: Assigned tasks and EnRoute tasks. --- --- ### 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 @{Core.Zone#ZONE_RADIUS). --- * @{#CONTROLLABLE.TaskOrbitCircle}: (AIR) Orbit at the current position of the first unit of the controllable at a specified alititude. --- * @{#CONTROLLABLE.TaskOrbitCircleAtVec2}: (AIR) Orbit at a specified position at a specified alititude during a specified duration with a specified speed. --- * @{#CONTROLLABLE.TaskRefueling}: (AIR) Refueling from the nearest tanker. No parameters. --- * @{#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 @{Dcs.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 @{Core.Zone#ZONE_RADIUS). --- @param #CONTROLLABLE self --- @param Core.Zone#ZONE Zone The zone where to land. --- @param #number Duration The duration in seconds to stay on the ground. --- @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 AttackUnit The UNIT. --- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. --- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. --- @param 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:EnRouteTaskEngageUnit( AttackUnit, Priority, WeaponType, WeaponExpend, AttackQty, Direction, AttackQtyLimit, ControllableAttack ) - self:F2( { self.ControllableName, AttackUnit, Priority, WeaponType, WeaponExpend, AttackQty, Direction, AttackQtyLimit, ControllableAttack } ) - - -- EngageUnit = { - -- id = 'EngageUnit', - -- params = { - -- unitId = Unit.ID, - -- weaponType = number, - -- expend = enum AI.Task.WeaponExpend - -- attackQty = number, - -- direction = Azimuth, - -- attackQtyLimit = boolean, - -- controllableAttack = boolean, - -- priority = number, - -- } - -- } - - local DCSTask - DCSTask = { id = 'EngageUnit', - params = { - unitId = AttackUnit:GetID(), - weaponType = WeaponType, - expend = WeaponExpend, - attackQty = AttackQty, - direction = Direction, - attackQtyLimit = AttackQtyLimit, - controllableAttack = ControllableAttack, - priority = Priority, - }, - }, - - self:T3( { DCSTask } ) - return DCSTask -end - - - ---- (AIR) Aircraft will act as an AWACS for friendly units (will provide them with information about contacts). No parameters. --- @param #CONTROLLABLE self --- @return 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:TaskRouteToVec2( 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:TaskRouteToVec3( 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 @{Wrapper.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 @{Wrapper.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 @{Core.Database#DATABASE} class. --- @param #CONTROLLABLE self --- @param #number Begin The route point from where the copy will start. The base route point is 0. --- @param #number End The route point where the copy will end. The End point is the last point - the End point. The last point has base 0. --- @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 @{Wrapper.Controllable#CONTROLLABLE:WayPointFunction} to define the hook functions for specific waypoints. --- Use the method @{Controllable@CONTROLLABLE:WayPointExecute) to start the execution of the new mission plan. --- Note that when WayPointInitialize is called, the Mission of the controllable is RESTARTED! --- @param #CONTROLLABLE self --- @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) @{Wrapper.Group#GROUP} class, extends @{Wrapper.Controllable#CONTROLLABLE} --- ============================================================= --- The @{Wrapper.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: --- --- * @{Wrapper.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 @{Core.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 - 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 @{Dcs.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() - 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:T3( UnitFound.UnitName ) - self:T2( UnitFound ) - return UnitFound - end - - return nil -end - ---- Returns the DCS Unit with number UnitNumber. --- If the underlying DCS Unit does not exist, the method will return nil. . --- @param #GROUP self --- @param #number UnitNumber The number of the DCS Unit to be returned. --- @return 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 @{Core.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 - -- TODO: Rename IsPointVec3InZone to IsVec3InZone - if Zone:IsPointVec3InZone( 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 @{Core.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:IsPointVec3InZone( 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 @{Core.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:IsPointVec3InZone( 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 @{Wrapper.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 @{Core.Database#DATABASE} class. --- @param #GROUP self --- @param #number Begin The route point from where the copy will start. The base route point is 0. --- @param #number End The route point where the copy will end. The End point is the last point - the End point. The last point has base 0. --- @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 @{Wrapper.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 @{Dcs.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 @{Core.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 - 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. -function UNIT:GetThreatLevel() - - local Attributes = self:GetDesc().attributes - local ThreatLevel = 0 - - 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" - } - - self:T2( Attributes ) - - 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"] then ThreatLevel = 2 - elseif Attributes["Infantry"] then ThreatLevel = 1 - end - - self:T2( ThreatLevel ) - return ThreatLevel, ThreatLevels[ThreatLevel+1] - -end - - --- Is functions - ---- Returns true if the unit is within a @{Zone}. --- @param #UNIT self --- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the unit is within the @{Core.Zone#ZONE_BASE} -function UNIT:IsInZone( Zone ) - self:F2( { self.UnitName, Zone } ) - - if self:IsAlive() then - local IsInZone = Zone:IsPointVec3InZone( 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 @{Core.Zone#ZONE_BASE} -function UNIT:IsNotInZone( Zone ) - self:F2( { self.UnitName, Zone } ) - - if self:IsAlive() then - local IsInZone = not Zone:IsPointVec3InZone( 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 - ---- This module contains the CLIENT class. --- --- 1) @{Wrapper.Client#CLIENT} class, extends @{Wrapper.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 @{Wrapper.Client#CLIENT} class is a wrapper class to handle the DCS Unit objects that have the skillset defined as __Client__ or __Player__: --- --- * Wraps the DCS Unit objects with skill level set to Player or Client. --- * Support all DCS Unit APIs. --- * Enhance with Unit specific APIs not in the DCS Group API set. --- * When player joins Unit, execute alive init logic. --- * Handles messages to players. --- * Manage the "state" of the DCS Unit. --- --- Clients are being used by the @{MISSION} class to follow players and register their successes. --- --- 1.1) CLIENT reference methods --- ----------------------------- --- For each DCS Unit having skill level Player or Client, a CLIENT wrapper object (instance) will be created within the _@{DATABASE} object. --- This is done at the beginning of the mission (when the mission starts). --- --- The CLIENT class does not contain a :New() method, rather it provides :Find() methods to retrieve the object reference --- using the DCS Unit or the DCS UnitName. --- --- Another thing to know is that CLIENT objects do not "contain" the DCS Unit object. --- The CLIENT methods will reference the DCS Unit object by name when it is needed during API execution. --- If the DCS Unit object does not exist or is nil, the CLIENT methods will return nil and log an exception in the DCS.log file. --- --- The CLIENT class provides the following functions to retrieve quickly the relevant CLIENT instance: --- --- * @{#CLIENT.Find}(): Find a CLIENT instance from the _DATABASE object using a DCS Unit object. --- * @{#CLIENT.FindByName}(): Find a CLIENT instance from the _DATABASE object using a DCS Unit name. --- --- IMPORTANT: ONE SHOULD NEVER SANATIZE these CLIENT OBJECT REFERENCES! (make the CLIENT object references nil). --- --- @module Client --- @author FlightControl - ---- The CLIENT class --- @type CLIENT --- @extends 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 CallBack Function. --- @return #CLIENT -function CLIENT:Alive( CallBackFunction, ... ) - self:F() - - self.ClientCallBack = CallBackFunction - self.ClientParameters = arg - - return self -end - ---- @param #CLIENT self -function CLIENT:_AliveCheckScheduler( SchedulerName ) - self: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.AI_Cargo#CARGO} contained within the CLIENT to the player as a message. --- The @{AI.AI_Cargo#CARGO} is shown using the @{Core.Message#MESSAGE} distribution system. --- @param #CLIENT self -function CLIENT:ShowCargo() - self:F() - - 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 @{Core.Message#MESSAGE} when the CLIENT is in the air. --- @param #string MessageID is the identifier of the message when displayed with intervals. -function CLIENT:Message( Message, MessageDuration, MessageCategory, MessageInterval, MessageID ) - self:F( { Message, MessageDuration, MessageCategory, MessageInterval } ) - - 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) @{Wrapper.Static#STATIC} class, extends @{Wrapper.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 @{Wrapper.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. --- @return #STATIC -function STATIC:FindByName( StaticName ) - local StaticFound = _DATABASE:FindStatic( StaticName ) - - self.StaticName = StaticName - - if StaticFound then - StaticFound:F( { StaticName } ) - - return StaticFound - end - - error( "STATIC not found for: " .. StaticName ) -end - -function STATIC:Register( StaticName ) - local self = BASE:Inherit( self, 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 ---- This module contains the AIRBASE classes. --- --- === --- --- 1) @{Wrapper.Airbase#AIRBASE} class, extends @{Wrapper.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 @{Dcs.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 - - - ---- Scoring system for MOOSE. --- This scoring class calculates the hits and kills that players make within a simulation session. --- Scoring is calculated using a defined algorithm. --- With a small change in MissionScripting.lua, the scoring can also be logged in a CSV file, that can then be uploaded --- to a database or a BI tool to publish the scoring results to the player community. --- @module Scoring --- @author FlightControl - - ---- The Scoring class --- @type SCORING --- @field Players A collection of the current players that have joined the game. --- @extends 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() ) - - if GameName then - self.GameName = GameName - else - error( "A game name must be given to register the scoring results" ) - end - - - _EVENTDISPATCHER:OnDead( self._EventOnDeadOrCrash, self ) - _EVENTDISPATCHER:OnCrash( self._EventOnDeadOrCrash, self ) - _EVENTDISPATCHER:OnHit( self._EventOnHit, self ) - - --self.SchedulerId = routines.scheduleFunction( SCORING._FollowPlayersScheduled, { self }, 0, 5 ) - self.SchedulerId = SCHEDULER:New( self, self._FollowPlayersScheduled, {}, 0, 5 ) - - self:ScoreMenu() - - self:OpenCSV( GameName) - - return self - -end - ---- Creates a score radio menu. Can be accessed using Radio -> F10. --- @param #SCORING self --- @return #SCORING self -function SCORING:ScoreMenu() - self.Menu = MENU_MISSION:New( 'Scoring' ) - self.AllScoresMenu = MENU_MISSION_COMMAND:New( 'Score All Active Players', self.Menu, SCORING.ReportScoreAll, self ) - --- = COMMANDMENU:New('Your Current Score', ReportScore, SCORING.ReportScorePlayer, self ) - return self -end - ---- Follows new players entering Clients within the DCSRTE. --- TODO: Need to see if i can catch this also with an event. It will eliminate the schedule ... -function SCORING:_FollowPlayersScheduled() - self:F3( "_FollowPlayersScheduled" ) - - local ClientUnit = 0 - local CoalitionsData = { AlivePlayersRed = coalition.getPlayers(coalition.side.RED), AlivePlayersBlue = coalition.getPlayers(coalition.side.BLUE) } - local unitId - local unitData - local AlivePlayerUnits = {} - - for CoalitionId, CoalitionData in pairs( CoalitionsData ) do - self:T3( { "_FollowPlayersScheduled", CoalitionData } ) - for UnitId, UnitData in pairs( CoalitionData ) do - self:_AddPlayerFromUnit( UnitData ) - end - end - - return true -end - - ---- Track DEAD or CRASH events for the scoring. --- @param #SCORING self --- @param 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.IniDCSUnit - TargetUnitName = Event.IniDCSUnitName - TargetGroup = Event.IniDCSGroup - TargetGroupName = Event.IniDCSGroupName - TargetPlayerName = TargetUnit:getPlayerName() - - TargetCoalition = TargetUnit:getCoalition() - --TargetCategory = TargetUnit:getCategory() - TargetCategory = TargetUnit:getDesc().category -- Workaround - TargetType = TargetUnit:getTypeName() - - TargetUnitCoalition = _SCORINGCoalition[TargetCoalition] - TargetUnitCategory = _SCORINGCategory[TargetCategory] - TargetUnitType = TargetType - - self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType } ) - end - - for PlayerName, PlayerData in pairs( self.Players ) do - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Something got killed" ) - - -- Some variables - local InitUnitName = PlayerData.UnitName - local InitUnitType = PlayerData.UnitType - local InitCoalition = PlayerData.UnitCoalition - local InitCategory = PlayerData.UnitCategory - local InitUnitCoalition = _SCORINGCoalition[InitCoalition] - local InitUnitCategory = _SCORINGCategory[InitCategory] - - self:T( { InitUnitName, InitUnitType, InitUnitCoalition, InitCoalition, InitUnitCategory, InitCategory } ) - - -- What is he hitting? - if TargetCategory then - if PlayerData and PlayerData.Hit and PlayerData.Hit[TargetCategory] and PlayerData.Hit[TargetCategory][TargetUnitName] then -- Was there a hit for this unit for this player before registered??? - if not PlayerData.Kill[TargetCategory] then - PlayerData.Kill[TargetCategory] = {} - end - if not PlayerData.Kill[TargetCategory][TargetType] then - PlayerData.Kill[TargetCategory][TargetType] = {} - PlayerData.Kill[TargetCategory][TargetType].Score = 0 - PlayerData.Kill[TargetCategory][TargetType].ScoreKill = 0 - PlayerData.Kill[TargetCategory][TargetType].Penalty = 0 - PlayerData.Kill[TargetCategory][TargetType].PenaltyKill = 0 - end - - if InitCoalition == TargetCoalition then - PlayerData.Penalty = PlayerData.Penalty + 25 - PlayerData.Kill[TargetCategory][TargetType].Penalty = PlayerData.Kill[TargetCategory][TargetType].Penalty + 25 - PlayerData.Kill[TargetCategory][TargetType].PenaltyKill = PlayerData.Kill[TargetCategory][TargetType].PenaltyKill + 1 - MESSAGE:New( "Player '" .. PlayerName .. "' killed a friendly " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. - PlayerData.Kill[TargetCategory][TargetType].PenaltyKill .. " times. Penalty: -" .. PlayerData.Kill[TargetCategory][TargetType].Penalty .. - ". Score Total:" .. PlayerData.Score - PlayerData.Penalty, - 5 ):ToAll() - self:ScoreCSV( PlayerName, "KILL_PENALTY", 1, -125, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - else - PlayerData.Score = PlayerData.Score + 10 - PlayerData.Kill[TargetCategory][TargetType].Score = PlayerData.Kill[TargetCategory][TargetType].Score + 10 - PlayerData.Kill[TargetCategory][TargetType].ScoreKill = PlayerData.Kill[TargetCategory][TargetType].ScoreKill + 1 - MESSAGE:New( "Player '" .. PlayerName .. "' killed an enemy " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. - PlayerData.Kill[TargetCategory][TargetType].ScoreKill .. " times. Score: " .. PlayerData.Kill[TargetCategory][TargetType].Score .. - ". Score Total:" .. PlayerData.Score - PlayerData.Penalty, - 5 ):ToAll() - self:ScoreCSV( PlayerName, "KILL_SCORE", 1, 10, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - end - end - end - end - end -end - - - ---- Add a new player entering a Unit. -function SCORING:_AddPlayerFromUnit( UnitData ) - self:F( UnitData ) - - if UnitData and UnitData:isExist() then - local UnitName = UnitData:getName() - local PlayerName = UnitData:getPlayerName() - local UnitDesc = UnitData:getDesc() - local UnitCategory = UnitDesc.category - local UnitCoalition = UnitData:getCoalition() - local UnitTypeName = UnitData:getTypeName() - - self:T( { PlayerName, UnitName, UnitCategory, UnitCoalition, UnitTypeName } ) - - if self.Players[PlayerName] == nil then -- I believe this is the place where a Player gets a life in a mission when he enters a unit ... - self.Players[PlayerName] = {} - self.Players[PlayerName].Hit = {} - self.Players[PlayerName].Kill = {} - self.Players[PlayerName].Mission = {} - - -- for CategoryID, CategoryName in pairs( SCORINGCategory ) do - -- self.Players[PlayerName].Hit[CategoryID] = {} - -- self.Players[PlayerName].Kill[CategoryID] = {} - -- end - self.Players[PlayerName].HitPlayers = {} - self.Players[PlayerName].HitUnits = {} - self.Players[PlayerName].Score = 0 - self.Players[PlayerName].Penalty = 0 - self.Players[PlayerName].PenaltyCoalition = 0 - self.Players[PlayerName].PenaltyWarning = 0 - end - - if not self.Players[PlayerName].UnitCoalition then - self.Players[PlayerName].UnitCoalition = UnitCoalition - else - if self.Players[PlayerName].UnitCoalition ~= UnitCoalition then - self.Players[PlayerName].Penalty = self.Players[PlayerName].Penalty + 50 - self.Players[PlayerName].PenaltyCoalition = self.Players[PlayerName].PenaltyCoalition + 1 - MESSAGE:New( "Player '" .. PlayerName .. "' changed coalition from " .. _SCORINGCoalition[self.Players[PlayerName].UnitCoalition] .. " to " .. _SCORINGCoalition[UnitCoalition] .. - "(changed " .. self.Players[PlayerName].PenaltyCoalition .. " times the coalition). 50 Penalty points added.", - 2 - ):ToAll() - self:ScoreCSV( PlayerName, "COALITION_PENALTY", 1, -50, self.Players[PlayerName].UnitName, _SCORINGCoalition[self.Players[PlayerName].UnitCoalition], _SCORINGCategory[self.Players[PlayerName].UnitCategory], self.Players[PlayerName].UnitType, - UnitName, _SCORINGCoalition[UnitCoalition], _SCORINGCategory[UnitCategory], UnitData:getTypeName() ) - end - end - self.Players[PlayerName].UnitName = UnitName - self.Players[PlayerName].UnitCoalition = UnitCoalition - self.Players[PlayerName].UnitCategory = UnitCategory - self.Players[PlayerName].UnitType = UnitTypeName - - if self.Players[PlayerName].Penalty > 100 then - if self.Players[PlayerName].PenaltyWarning < 1 then - MESSAGE:New( "Player '" .. PlayerName .. "': WARNING! If you continue to commit FRATRICIDE and have a PENALTY score higher than 150, you will be COURT MARTIALED and DISMISSED from this mission! \nYour total penalty is: " .. self.Players[PlayerName].Penalty, - 30 - ):ToAll() - self.Players[PlayerName].PenaltyWarning = self.Players[PlayerName].PenaltyWarning + 1 - end - end - - if self.Players[PlayerName].Penalty > 150 then - ClientGroup = GROUP:NewFromDCSUnit( UnitData ) - ClientGroup:Destroy() - MESSAGE:New( "Player '" .. PlayerName .. "' committed FRATRICIDE, he will be COURT MARTIALED and is DISMISSED from this mission!", - 10 - ):ToAll() - end - - end -end - - ---- Registers Scores the players completing a Mission Task. --- @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 OnHit event for the scoring. --- @param #SCORING self --- @param Core.Event#EVENTDATA Event -function SCORING:_EventOnHit( Event ) - self:F( { Event } ) - - local InitUnit = nil - local InitUnitName = "" - local InitGroup = nil - local InitGroupName = "" - local InitPlayerName = nil - - local InitCoalition = nil - local InitCategory = nil - local InitType = nil - local InitUnitCoalition = nil - local InitUnitCategory = nil - local InitUnitType = nil - - local TargetUnit = nil - local TargetUnitName = "" - local TargetGroup = nil - local TargetGroupName = "" - local TargetPlayerName = "" - - local TargetCoalition = nil - local TargetCategory = nil - local TargetType = nil - local TargetUnitCoalition = nil - local TargetUnitCategory = nil - local TargetUnitType = nil - - if Event.IniDCSUnit then - - InitUnit = Event.IniDCSUnit - InitUnitName = Event.IniDCSUnitName - InitGroup = Event.IniDCSGroup - InitGroupName = Event.IniDCSGroupName - InitPlayerName = InitUnit:getPlayerName() - - InitCoalition = InitUnit:getCoalition() - --TODO: Workaround Client DCS Bug - --InitCategory = InitUnit:getCategory() - InitCategory = InitUnit:getDesc().category - InitType = InitUnit:getTypeName() - - InitUnitCoalition = _SCORINGCoalition[InitCoalition] - InitUnitCategory = _SCORINGCategory[InitCategory] - InitUnitType = InitType - - self:T( { InitUnitName, InitGroupName, InitPlayerName, InitCoalition, InitCategory, InitType , InitUnitCoalition, InitUnitCategory, InitUnitType } ) - end - - - if Event.TgtDCSUnit then - - TargetUnit = Event.TgtDCSUnit - TargetUnitName = Event.TgtDCSUnitName - TargetGroup = Event.TgtDCSGroup - TargetGroupName = Event.TgtDCSGroupName - TargetPlayerName = TargetUnit:getPlayerName() - - TargetCoalition = TargetUnit:getCoalition() - --TODO: Workaround Client DCS Bug - --TargetCategory = TargetUnit:getCategory() - TargetCategory = TargetUnit:getDesc().category - TargetType = TargetUnit:getTypeName() - - TargetUnitCoalition = _SCORINGCoalition[TargetCoalition] - TargetUnitCategory = _SCORINGCategory[TargetCategory] - TargetUnitType = TargetType - - self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType, TargetUnitCoalition, TargetUnitCategory, TargetUnitType } ) - end - - if InitPlayerName ~= nil then -- It is a player that is hitting something - self:_AddPlayerFromUnit( InitUnit ) - if self.Players[InitPlayerName] then -- This should normally not happen, but i'll test it anyway. - if TargetPlayerName ~= nil then -- It is a player hitting another player ... - self:_AddPlayerFromUnit( TargetUnit ) - self.Players[InitPlayerName].HitPlayers = self.Players[InitPlayerName].HitPlayers + 1 - end - - self:T( "Hitting Something" ) - -- What is he hitting? - if TargetCategory then - if not self.Players[InitPlayerName].Hit[TargetCategory] then - self.Players[InitPlayerName].Hit[TargetCategory] = {} - end - if not self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName] then - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName] = {} - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score = 0 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty = 0 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit = 0 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit = 0 - end - local Score = 0 - if InitCoalition == TargetCoalition then - self.Players[InitPlayerName].Penalty = self.Players[InitPlayerName].Penalty + 10 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty + 10 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit + 1 - MESSAGE:New( "Player '" .. InitPlayerName .. "' hit a friendly " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit .. " times. Penalty: -" .. self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty .. - ". Score Total:" .. self.Players[InitPlayerName].Score - self.Players[InitPlayerName].Penalty, - 2 - ):ToAll() - self:ScoreCSV( InitPlayerName, "HIT_PENALTY", 1, -25, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - else - self.Players[InitPlayerName].Score = self.Players[InitPlayerName].Score + 1 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score + 1 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit + 1 - MESSAGE:New( "Player '" .. InitPlayerName .. "' hit a target " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit .. " times. Score: " .. self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score .. - ". Score Total:" .. self.Players[InitPlayerName].Score - self.Players[InitPlayerName].Penalty, - 2 - ):ToAll() - self:ScoreCSV( InitPlayerName, "HIT_SCORE", 1, 1, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - end - end - end - elseif InitPlayerName == nil then -- It is an AI hitting a player??? - - end -end - - -function SCORING:ReportScoreAll() - - env.info( "Hello World " ) - - local ScoreMessage = "" - local PlayerMessage = "" - - self:T( "Score Report" ) - - for PlayerName, PlayerData in pairs( self.Players ) do - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) - - -- Some variables - local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] - local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local PlayerScore = 0 - local PlayerPenalty = 0 - - ScoreMessage = ":\n" - - local ScoreMessageHits = "" - - for CategoryID, CategoryName in pairs( _SCORINGCategory ) do - self:T( CategoryName ) - if PlayerData.Hit[CategoryID] then - local Score = 0 - local ScoreHit = 0 - local Penalty = 0 - local PenaltyHit = 0 - self:T( "Hit scores exist for player " .. PlayerName ) - for UnitName, UnitData in pairs( PlayerData.Hit[CategoryID] ) do - Score = Score + UnitData.Score - ScoreHit = ScoreHit + UnitData.ScoreHit - Penalty = Penalty + UnitData.Penalty - PenaltyHit = UnitData.PenaltyHit - end - local ScoreMessageHit = string.format( "%s:%d ", CategoryName, Score - Penalty ) - self:T( ScoreMessageHit ) - ScoreMessageHits = ScoreMessageHits .. ScoreMessageHit - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageHits ~= "" then - ScoreMessage = ScoreMessage .. " Hits: " .. ScoreMessageHits .. "\n" - end - - local ScoreMessageKills = "" - for CategoryID, CategoryName in pairs( _SCORINGCategory ) do - self:T( "Kill scores exist for player " .. PlayerName ) - if PlayerData.Kill[CategoryID] then - local Score = 0 - local ScoreKill = 0 - local Penalty = 0 - local PenaltyKill = 0 - - for UnitName, UnitData in pairs( PlayerData.Kill[CategoryID] ) do - Score = Score + UnitData.Score - ScoreKill = ScoreKill + UnitData.ScoreKill - Penalty = Penalty + UnitData.Penalty - PenaltyKill = PenaltyKill + UnitData.PenaltyKill - end - - local ScoreMessageKill = string.format( " %s:%d ", CategoryName, Score - Penalty ) - self:T( ScoreMessageKill ) - ScoreMessageKills = ScoreMessageKills .. ScoreMessageKill - - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageKills = ScoreMessageKills .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageKills ~= "" then - ScoreMessage = ScoreMessage .. " Kills: " .. ScoreMessageKills .. "\n" - end - - local ScoreMessageCoalitionChangePenalties = "" - if PlayerData.PenaltyCoalition ~= 0 then - ScoreMessageCoalitionChangePenalties = ScoreMessageCoalitionChangePenalties .. string.format( " -%d (%d changed)", PlayerData.Penalty, PlayerData.PenaltyCoalition ) - PlayerPenalty = PlayerPenalty + PlayerData.Penalty - end - if ScoreMessageCoalitionChangePenalties ~= "" then - ScoreMessage = ScoreMessage .. " Coalition Penalties: " .. ScoreMessageCoalitionChangePenalties .. "\n" - end - - local ScoreMessageMission = "" - local ScoreMission = 0 - local ScoreTask = 0 - for MissionName, MissionData in pairs( PlayerData.Mission ) do - ScoreMission = ScoreMission + MissionData.ScoreMission - ScoreTask = ScoreTask + MissionData.ScoreTask - ScoreMessageMission = ScoreMessageMission .. "'" .. MissionName .. "'; " - end - PlayerScore = PlayerScore + ScoreMission + ScoreTask - - if ScoreMessageMission ~= "" then - ScoreMessage = ScoreMessage .. " Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ")\n" - end - - PlayerMessage = PlayerMessage .. string.format( "Player '%s' Score:%d (%d Score -%d Penalties)%s", PlayerName, PlayerScore - PlayerPenalty, PlayerScore, PlayerPenalty, ScoreMessage ) - end - end - MESSAGE:New( PlayerMessage, 30, "Player Scores" ):ToAll() -end - - -function SCORING:ReportScorePlayer() - - env.info( "Hello World " ) - - local ScoreMessage = "" - local PlayerMessage = "" - - self:T( "Score Report" ) - - for PlayerName, PlayerData in pairs( self.Players ) do - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) - - -- Some variables - local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] - local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local PlayerScore = 0 - local PlayerPenalty = 0 - - ScoreMessage = "" - - local ScoreMessageHits = "" - - for CategoryID, CategoryName in pairs( _SCORINGCategory ) do - self:T( CategoryName ) - if PlayerData.Hit[CategoryID] then - local Score = 0 - local ScoreHit = 0 - local Penalty = 0 - local PenaltyHit = 0 - self:T( "Hit scores exist for player " .. PlayerName ) - for UnitName, UnitData in pairs( PlayerData.Hit[CategoryID] ) do - Score = Score + UnitData.Score - ScoreHit = ScoreHit + UnitData.ScoreHit - Penalty = Penalty + UnitData.Penalty - PenaltyHit = UnitData.PenaltyHit - end - local ScoreMessageHit = string.format( "\n %s = %d score(%d;-%d) hits(#%d;#-%d)", CategoryName, Score - Penalty, Score, Penalty, ScoreHit, PenaltyHit ) - self:T( ScoreMessageHit ) - ScoreMessageHits = ScoreMessageHits .. ScoreMessageHit - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageHits ~= "" then - ScoreMessage = ScoreMessage .. "\n Hits: " .. ScoreMessageHits .. " " - end - - local ScoreMessageKills = "" - for CategoryID, CategoryName in pairs( _SCORINGCategory ) do - self:T( "Kill scores exist for player " .. PlayerName ) - if PlayerData.Kill[CategoryID] then - local Score = 0 - local ScoreKill = 0 - local Penalty = 0 - local PenaltyKill = 0 - - for UnitName, UnitData in pairs( PlayerData.Kill[CategoryID] ) do - Score = Score + UnitData.Score - ScoreKill = ScoreKill + UnitData.ScoreKill - Penalty = Penalty + UnitData.Penalty - PenaltyKill = PenaltyKill + UnitData.PenaltyKill - end - - local ScoreMessageKill = string.format( "\n %s = %d score(%d;-%d) hits(#%d;#-%d)", CategoryName, Score - Penalty, Score, Penalty, ScoreKill, PenaltyKill ) - self:T( ScoreMessageKill ) - ScoreMessageKills = ScoreMessageKills .. ScoreMessageKill - - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageKills = ScoreMessageKills .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageKills ~= "" then - ScoreMessage = ScoreMessage .. "\n Kills: " .. ScoreMessageKills .. " " - end - - local ScoreMessageCoalitionChangePenalties = "" - if PlayerData.PenaltyCoalition ~= 0 then - ScoreMessageCoalitionChangePenalties = ScoreMessageCoalitionChangePenalties .. string.format( " -%d (%d changed)", PlayerData.Penalty, PlayerData.PenaltyCoalition ) - PlayerPenalty = PlayerPenalty + PlayerData.Penalty - end - if ScoreMessageCoalitionChangePenalties ~= "" then - ScoreMessage = ScoreMessage .. "\n Coalition: " .. ScoreMessageCoalitionChangePenalties .. " " - end - - local ScoreMessageMission = "" - local ScoreMission = 0 - local ScoreTask = 0 - for MissionName, MissionData in pairs( PlayerData.Mission ) do - ScoreMission = ScoreMission + MissionData.ScoreMission - ScoreTask = ScoreTask + MissionData.ScoreTask - ScoreMessageMission = ScoreMessageMission .. "'" .. MissionName .. "'; " - end - PlayerScore = PlayerScore + ScoreMission + ScoreTask - - if ScoreMessageMission ~= "" then - ScoreMessage = ScoreMessage .. "\n Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ") " - end - - PlayerMessage = PlayerMessage .. string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties ):%s", PlayerName, PlayerScore - PlayerPenalty, PlayerScore, PlayerPenalty, ScoreMessage ) - end - end - MESSAGE:New( PlayerMessage, 30, "Player Scores" ):ToAll() - -end - - -function SCORING:SecondsToClock(sSeconds) - local nSeconds = sSeconds - if nSeconds == 0 then - --return nil; - return "00:00:00"; - else - nHours = string.format("%02.f", math.floor(nSeconds/3600)); - nMins = string.format("%02.f", math.floor(nSeconds/60 - (nHours*60))); - nSecs = string.format("%02.f", math.floor(nSeconds - nHours*3600 - nMins *60)); - return nHours..":"..nMins..":"..nSecs - end -end - ---- Opens a score CSV file to log the scores. --- @param #SCORING self --- @param #string ScoringCSV --- @return #SCORING self --- @usage --- -- Open a new CSV file to log the scores of the game Gori Valley. Let the name of the CSV file begin with "Player Scores". --- ScoringObject = SCORING:New( "Gori Valley" ) --- ScoringObject:OpenCSV( "Player Scores" ) -function SCORING:OpenCSV( ScoringCSV ) - self:F( ScoringCSV ) - - if lfs and io and os then - if ScoringCSV then - self.ScoringCSV = ScoringCSV - local fdir = lfs.writedir() .. [[Logs\]] .. self.ScoringCSV .. " " .. os.date( "%Y-%m-%d %H-%M-%S" ) .. ".csv" - - self.CSVFile, self.err = io.open( fdir, "w+" ) - if not self.CSVFile then - error( "Error: Cannot open CSV file in " .. lfs.writedir() ) - end - - self.CSVFile:write( '"GameName","RunTime","Time","PlayerName","ScoreType","PlayerUnitCoaltion","PlayerUnitCategory","PlayerUnitType","PlayerUnitName","TargetUnitCoalition","TargetUnitCategory","TargetUnitType","TargetUnitName","Times","Score"\n' ) - - self.RunTime = os.date("%y-%m-%d_%H-%M-%S") - else - error( "A string containing the CSV file name must be given." ) - end - else - self:E( "The MissionScripting.lua file has not been changed to allow lfs, io and os modules to be used..." ) - end - return self -end - - ---- Registers a score for a player. --- @param #SCORING self --- @param #string PlayerName The name of the player. --- @param #string ScoreType The type of the score. --- @param #string ScoreTimes The amount of scores achieved. --- @param #string ScoreAmount The score given. --- @param #string PlayerUnitName The unit name of the player. --- @param #string PlayerUnitCoalition The coalition of the player unit. --- @param #string PlayerUnitCategory The category of the player unit. --- @param #string PlayerUnitType The type of the player unit. --- @param #string TargetUnitName The name of the target unit. --- @param #string TargetUnitCoalition The coalition of the target unit. --- @param #string TargetUnitCategory The category of the target unit. --- @param #string TargetUnitType The type of the target unit. --- @return #SCORING self -function SCORING:ScoreCSV( PlayerName, ScoreType, ScoreTimes, ScoreAmount, PlayerUnitName, PlayerUnitCoalition, PlayerUnitCategory, PlayerUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - --write statistic information to file - local ScoreTime = self:SecondsToClock( timer.getTime() ) - PlayerName = PlayerName:gsub( '"', '_' ) - - if PlayerUnitName and PlayerUnitName ~= '' then - local PlayerUnit = Unit.getByName( PlayerUnitName ) - - if PlayerUnit then - if not PlayerUnitCategory then - --PlayerUnitCategory = SCORINGCategory[PlayerUnit:getCategory()] - PlayerUnitCategory = _SCORINGCategory[PlayerUnit:getDesc().category] - end - - if not PlayerUnitCoalition then - PlayerUnitCoalition = _SCORINGCoalition[PlayerUnit:getCoalition()] - end - - if not PlayerUnitType then - PlayerUnitType = PlayerUnit:getTypeName() - end - else - PlayerUnitName = '' - PlayerUnitCategory = '' - PlayerUnitCoalition = '' - PlayerUnitType = '' - end - else - PlayerUnitName = '' - PlayerUnitCategory = '' - PlayerUnitCoalition = '' - PlayerUnitType = '' - end - - if not TargetUnitCoalition then - TargetUnitCoalition = '' - end - - if not TargetUnitCategory then - TargetUnitCategory = '' - end - - if not TargetUnitType then - TargetUnitType = '' - end - - if not TargetUnitName then - TargetUnitName = '' - end - - if lfs and io and os then - self.CSVFile:write( - '"' .. self.GameName .. '"' .. ',' .. - '"' .. self.RunTime .. '"' .. ',' .. - '' .. ScoreTime .. '' .. ',' .. - '"' .. PlayerName .. '"' .. ',' .. - '"' .. ScoreType .. '"' .. ',' .. - '"' .. PlayerUnitCoalition .. '"' .. ',' .. - '"' .. PlayerUnitCategory .. '"' .. ',' .. - '"' .. PlayerUnitType .. '"' .. ',' .. - '"' .. PlayerUnitName .. '"' .. ',' .. - '"' .. TargetUnitCoalition .. '"' .. ',' .. - '"' .. TargetUnitCategory .. '"' .. ',' .. - '"' .. TargetUnitType .. '"' .. ',' .. - '"' .. TargetUnitName .. '"' .. ',' .. - '' .. ScoreTimes .. '' .. ',' .. - '' .. ScoreAmount - ) - - self.CSVFile:write( "\n" ) - end -end - - -function SCORING:CloseCSV() - if lfs and io and os then - self.CSVFile:close() - end -end - ---- 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 @{Dcs.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 @{Dcs.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 - ---- This module contains the SPAWN class. --- --- # 1) @{Functional.Spawn#SPAWN} class, extends @{Core.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-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.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.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 - 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 @{Wrapper.Group#GROUP} that was spawned. --- @param #SPAWN self --- @param #function SpawnCallBackFunction The function to be called when a group spawns. --- @param SpawnFunctionArguments A random amount of arguments to be provided to the function when the group spawns. --- @return #SPAWN --- @usage --- -- Declare SpawnObject and call a function when a new Group is spawned. --- local SpawnObject = SPAWN --- :New( "SpawnObject" ) --- :InitLimit( 2, 10 ) --- :OnSpawnGroup( --- function( SpawnGroup ) --- SpawnGroup:E( "I am spawned" ) --- end --- ) --- :SpawnScheduled( 300, 0.3 ) -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 @{Core.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 mode... --- This will be similar to the uncontrolled flag setting in the ME. --- @param #SPAWN self --- @return #SPAWN self -function SPAWN:InitUnControlled() - self:F( { self.SpawnTemplatePrefix } ) - - self.SpawnUnControlled = true - - for SpawnGroupID = 1, self.SpawnMaxGroups do - self.SpawnGroups[SpawnGroupID].UnControlled = true - end - - return self -end - - - ---- Will return the SpawnGroupName either with with a specific count number or without any count. --- @param #SPAWN self --- @param #number SpawnIndex Is the number of the Group that is to be spawned. --- @return #string SpawnGroupName -function SPAWN:SpawnGroupName( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex } ) - - local SpawnPrefix = self.SpawnTemplatePrefix - if self.SpawnAliasPrefix then - SpawnPrefix = self.SpawnAliasPrefix - end - - if SpawnIndex then - local SpawnName = string.format( '%s#%03d', SpawnPrefix, SpawnIndex ) - self:T( SpawnName ) - return SpawnName - else - self:T( SpawnPrefix ) - return SpawnPrefix - end - -end - ---- 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 - - if SpawnTemplate.CategoryID == Group.Category.HELICOPTER or SpawnTemplate.CategoryID == Group.Category.AIRPLANE then - SpawnTemplate.uncontrolled = false - end - - for UnitID = 1, #SpawnTemplate.units do - SpawnTemplate.units[UnitID].name = string.format( SpawnTemplate.name .. '-%02d', UnitID ) - SpawnTemplate.units[UnitID].unitId = nil - 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 - 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].y = self.SpawnTemplate.units[1].y - 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 @{Wrapper.Group#GROUP} for a @{Wrapper.Client#CLIENT}, with an optional briefing text. --- --- ESCORT initialization methods. --- ============================== --- The following menus are created within the RADIO MENU 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:TaskRouteToVec3( GDV, Speed / 3.6 ) -- DCS models speed in Mps (Miles per second) - end - - return true - end - - return false -end - - ---- Report Targets Scheduler. --- @param #ESCORT self -function ESCORT:_ReportTargetsScheduler() - self:F( self.EscortGroup:GetName() ) - - if self.EscortGroup:IsAlive() and self.EscortClient:IsAlive() then - local EscortGroupName = self.EscortGroup:GetName() - local EscortTargets = self.EscortGroup:GetDetectedTargets() - - local ClientEscortTargets = self.EscortClient._EscortGroups[EscortGroupName].Targets - - local EscortTargetMessages = "" - for EscortTargetID, EscortTarget in pairs( EscortTargets ) do - local EscortObject = EscortTarget.object - self:T( EscortObject ) - if EscortObject and EscortObject:isExist() and EscortObject.id_ < 50000000 then - - local EscortTargetUnit = UNIT:Find( EscortObject ) - local EscortTargetUnitName = EscortTargetUnit:GetName() - - - - -- local EscortTargetIsDetected, - -- EscortTargetIsVisible, - -- EscortTargetLastTime, - -- EscortTargetKnowType, - -- EscortTargetKnowDistance, - -- EscortTargetLastPos, - -- EscortTargetLastVelocity - -- = self.EscortGroup:IsTargetDetected( EscortObject ) - -- - -- self:T( { EscortTargetIsDetected, - -- EscortTargetIsVisible, - -- EscortTargetLastTime, - -- EscortTargetKnowType, - -- EscortTargetKnowDistance, - -- EscortTargetLastPos, - -- EscortTargetLastVelocity } ) - - - local 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) @{Functional.MissileTrainer#MISSILETRAINER} class, extends @{Core.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. - SCHEDULER:New( TrainerWeapon, TrainerWeapon.destroy, {}, 2 ) - if ( TrainerWeapon:getTypeName() == "9M311" ) then - SCHEDULER:New( TrainerWeapon, TrainerWeapon.destroy, {}, 2 ) - 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) @{Functional.AirbasePolice#AIRBASEPOLICE_BASE} class, extends @{Core.Base#BASE} --- ================================================================== --- The @{Functional.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) @{Functional.AirbasePolice#AIRBASEPOLICE_CAUCASUS} class, extends @{Functional.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) @{Functional.AirbasePolice#AIRBASEPOLICE_NEVADA} class, extends @{Functional.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() - 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) @{Functional.Detection#DETECTION_BASE} class, extends @{Core.Base#BASE} --- ========================================================== --- The @{Functional.Detection#DETECTION_BASE} class defines the core functions to administer detected objects. --- The @{Functional.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 @{Functional.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: --- --- * @{Functional.Detection#DETECTION_BASE.InitDetectVisual}(): Detected using Visual. --- * @{Functional.Detection#DETECTION_BASE.InitDetectOptical}(): Detected using Optical. --- * @{Functional.Detection#DETECTION_BASE.InitDetectRadar}(): Detected using Radar. --- * @{Functional.Detection#DETECTION_BASE.InitDetectIRST}(): Detected using IRST. --- * @{Functional.Detection#DETECTION_BASE.InitDetectRWR}(): Detected using RWR. --- * @{Functional.Detection#DETECTION_BASE.InitDetectDLINK}(): Detected using DLINK. --- --- 1.3) Obtain objects detected by DETECTION_BASE --- ---------------------------------------------- --- DETECTION_BASE builds @{Set}s of objects detected. These @{Core.Set#SET_BASE}s can be retrieved using the method @{Functional.Detection#DETECTION_BASE.GetDetectedSets}(). --- The method will return a list (table) of @{Core.Set#SET_BASE} objects. --- --- === --- --- 2) @{Functional.Detection#DETECTION_AREAS} class, extends @{Functional.Detection#DETECTION_BASE} --- =============================================================================== --- The @{Functional.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 @{Core.Set#SET_UNIT}s containing the @{Wrapper.Unit#UNIT}s detected. --- The class is group the detected units within zones given a DetectedZoneRange parameter. --- A set with multiple detected zones will be created as there are groups of units detected. --- --- 2.1) Retrieve the Detected Unit sets and Detected Zones --- ------------------------------------------------------- --- The DetectedUnitSets methods are implemented in @{Functional.Detection#DECTECTION_BASE} and the DetectedZones methods is implemented in @{Functional.Detection#DETECTION_AREAS}. --- --- Retrieve the DetectedUnitSets with the method @{Functional.Detection#DETECTION_BASE.GetDetectedSets}(). A table will be return of @{Core.Set#SET_UNIT}s. --- To understand the amount of sets created, use the method @{Functional.Detection#DETECTION_BASE.GetDetectedSetCount}(). --- If you want to obtain a specific set from the DetectedSets, use the method @{Functional.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 @{Functional.Detection#DETECTION_BASE.GetDetectionZones}(). --- To understand the amount of zones created, use the method @{Functional.Detection#DETECTION_BASE.GetDetectionZoneCount}(). --- If you want to obtain a specific zone from the DetectedZones, use the method @{Functional.Detection#DETECTION_BASE.GetDetectionZone}() with a given index. --- --- 1.4) Flare or Smoke detected units --- ---------------------------------- --- Use the methods @{Functional.Detection#DETECTION_AREAS.FlareDetectedUnits}() or @{Functional.Detection#DETECTION_AREAS.SmokeDetectedUnits}() to flare or smoke the detected units when a new detection has taken place. --- --- 1.5) Flare or Smoke detected zones --- ---------------------------------- --- Use the methods @{Functional.Detection#DETECTION_AREAS.FlareDetectedZones}() or @{Functional.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 @{Core.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 @{Wrapper.Unit#UNIT}s in an array of @{Core.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( 0, 30 ) - - 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 @{Core.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 @{Core.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** / Mulit-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.AI_Balancer#AI_BALANCER} class, extends @{Core.Fsm#FSM_SET} --- --- The @{AI.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 @{Core.Fsm#FSM_SET} manages the functionality to control the Finite State Machine (FSM). --- The mission designer can tailor the behaviour of the AI_BALANCER, by defining event and state transition methods. --- An explanation about state and event transition methods can be found in the @{FSM} module documentation. --- --- 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** @{Wrapper.Airbase#AIRBASE}. --- * @{#AI_BALANCER.ReturnToNearestAirbases}: Returns the AI to the **nearest friendly** @{Wrapper.Airbase#AIRBASE}. --- --- Note that when AI returns to an airbase, the AI_BALANCER will trigger the **Return** event and the AI will return, --- otherwise the AI_BALANCER will trigger a **Destroy** event, and the AI will be destroyed. --- --- === --- --- # **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 - - 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 @{Wrapper.Airbase#AIRBASE}. --- @param #AI_BALANCER self --- @param Dcs.DCSTypes#Distance ReturnTresholdRange If there is an enemy @{Wrapper.Client#CLIENT} within the ReturnTresholdRange given in meters, the AI will not return to the nearest @{Wrapper.Airbase#AIRBASE}. --- @param Core.Set#SET_AIRBASE ReturnAirbaseSet The SET of @{Core.Set#SET_AIRBASE}s to evaluate where to return to. -function AI_BALANCER:ReturnToNearestAirbases( ReturnTresholdRange, ReturnAirbaseSet ) - - self.ToNearestAirbase = true - self.ReturnTresholdRange = ReturnTresholdRange - self.ReturnAirbaseSet = ReturnAirbaseSet -end - ---- Returns the AI to the home @{Wrapper.Airbase#AIRBASE}. --- @param #AI_BALANCER self --- @param Dcs.DCSTypes#Distance ReturnTresholdRange If there is an enemy @{Wrapper.Client#CLIENT} within the ReturnTresholdRange given in meters, the AI will not return to the nearest @{Wrapper.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 - 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 - ---- @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** / Mulit-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 @{Core.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:SetDetectionOn() - - self.CheckStatus = true - - self:ManageFuel( .2, 60 ) - self:ManageDamage( 1 ) - - self:SetDetectionInterval( 30 ) - - 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", "Ejected" ) - self:AddTransition( "*", "Crash", "Crashed" ) - self:AddTransition( "*", "PilotDead", "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.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.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 @{Wrapper.Unit#UNIT}s that were detected by the AI. --- No filtering is applied, so, ANY detected UNIT can be in this list. --- It is up to the mission designer to use the @{Unit} class and methods to filter the targets. --- @param #AI_PATROL_ZONE self --- @return #table The list of @{Wrapper.Unit#UNIT}s -function AI_PATROL_ZONE:GetDetectedUnits() - self:F2() - - return 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:EventOnPilotDead( self.OnPilotDead ) - self:EventOnCrash( self.OnCrash ) - self:EventOnEjection( self.OnEjection ) - - - Controllable:OptionROEHoldFire() - Controllable:OptionROTVertical() - - self.Controllable:OnReSpawn( - function( PatrolGroup ) - self:E( "ReSpawn" ) - self:__Reset() - self:__Route( 5 ) - end - ) - -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 - self:T( TargetObject ) - 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 } ) - self.DetectedUnits[TargetUnit] = TargetUnit - Detected = true - end - else - self.DetectedUnits[TargetUnit] = TargetUnit - 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:__Crash( 1, EventData ) - 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** / Mulit-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.AI_Patrol#AI_PATROL_ZONE} --- --- @{#AI_CAS_ZONE} derives from the @{AI.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 Dcs.DCSTypes#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO --- @param Core.Zone#ZONE EngageZone --- @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. - -- @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. - - --- 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", "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:EventOnDead( self.OnDead ) - -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: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 - - - if self.Controllable:IsNotInZone( self.EngageZone ) then - - -- Find a random 2D point in EngageZone. - local ToEngageZoneVec2 = self.EngageZone:GetRandomVec2() - self:T2( ToEngageZoneVec2 ) - - -- Define Speed and Altitude. - local ToEngageZoneAltitude = math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) - local ToEngageZoneSpeed = self.PatrolMaxSpeed - self:T2( ToEngageZoneSpeed ) - - -- Obtain a 3D @{Point} from the 2D point + altitude. - local ToEngageZonePointVec3 = POINT_VEC3:New( ToEngageZoneVec2.x, ToEngageZoneAltitude, ToEngageZoneVec2.y ) - - -- Create a route point of type air. - local ToEngageZoneRoutePoint = ToEngageZonePointVec3:RoutePointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToEngageZoneSpeed, - 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 ) - - --- 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 ToTargetRoutePoint = ToTargetPointVec3:RoutePointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, - true - ) - - ToTargetPointVec3:SmokeBlue() - - EngageRoute[#EngageRoute+1] = ToTargetRoutePoint - - - Controllable:OptionROEOpenFire() - Controllable:OptionROTPassiveDefense() - - 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, 2 ) - 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:SetDetectionOff() -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** / Mulit-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.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.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.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 DetectedUnitID, DetectedUnit 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 DetectedUnitID, DetectedUnit 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:__Accomplish( 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 / Mulit-Player:Yes / AI:Yes / Human:No / Types:Ground -- Management of logical cargo objects, that can be transported from and to transportation carriers. --- --- === --- --- 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.AI_Cargo#AI_CARGO} class, extends @{Core.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) @{Core.Point#POINT_VEC2) to where the cargo should run after onboarding. If not provided, the cargo will run to 60 meters behind the Carrier location. - ---- UnBoards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo from the Carrier. --- The cargo must be in the **Loaded** state. --- @function [parent=#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) @{Core.Point#POINT_VEC2) to where the cargo should run after onboarding. If not provided, the cargo will run to 60 meters behind the Carrier location. - - --- Load - ---- Loads the cargo to a Carrier. The event will load the cargo into the Carrier regardless of its position. There will be no movement simulated of the cargo loading. --- The cargo must be in the **UnLoaded** state. --- @function [parent=#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) @{Core.Point#POINT_VEC2) to where the cargo will be placed after unloading. If not provided, the cargo will be placed 60 meters behind the Carrier location. - ---- UnLoads the cargo to a Carrier. The event will unload the cargo from the Carrier. There will be no movement simulated of the cargo loading. --- The cargo must be in the **Loaded** state. --- @function [parent=#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) @{Core.Point#POINT_VEC2) to where the cargo will be placed after unloading. If not provided, the cargo will be placed 60 meters behind the Carrier location. - --- State Transition Functions - --- UnLoaded - ---- @function [parent=#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 @{Core.Fsm#FSM_PROCESS} --- --- ## ACT_ASSIGN state machine: --- --- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur. --- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below. --- Each derived class follows exactly the same process, using the same events and following the same state transitions, --- 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 @{Core.Fsm#FSM_PROCESS} --- --- ## ACT_ROUTE state machine: --- --- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur. --- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below. --- Each derived class follows exactly the same process, using the same events and following the same state transitions, --- 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 @{Core.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:EventOnDead( 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 @{Core.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:EventOnBirth( - --- @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:EventOnPlayerEnterUnit( - --- @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:EventOnPlayerLeaveUnit( - --- @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:EventOnCrash( - --- @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 @{Core.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. --- --- 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, -} - ---- 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: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() - - -- Handle the birth of new planes within the assigned set. - - - -- Handle when a player crashes ... - -- The Task is UnAssigned from the Unit. - -- When there is no Unit left running the Task, and all of the Players crashed, the Task goes into Failed ... --- self:EventOnCrash( --- --- @param #TASK self --- -- @param Core.Event#EVENTDATA EventData --- function( self, EventData ) --- self:E( "In LeaveUnit" ) --- self:E( { "State", self:GetState() } ) --- if self:IsStateAssigned() then --- local TaskUnit = EventData.IniUnit --- local TaskGroup = EventData.IniUnit:GetGroup() --- self:E( self.SetGroup:IsIncludeObject( TaskGroup ) ) --- if self.SetGroup:IsIncludeObject( TaskGroup ) then --- self:UnAssignFromUnit( TaskUnit ) --- end --- self:MessageToGroups( TaskUnit:GetPlayerName() .. " crashed!, and has aborted Task " .. self:GetName() ) --- end --- end --- ) --- - - 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 - ---- 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() -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() - -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 - -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) @{Tasking.DetectionManager#DETECTION_MANAGER} class, extends @{Core.Base#BASE} --- ==================================================================== --- The @{Tasking.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: --- ----------------------------------- --- * @{Tasking.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 @{Tasking.DetectionManager#DETECTION_MANAGER.ReportDetected}(). This method implements polymorphic behaviour. --- --- The time interval in seconds of the reporting can be changed using the methods @{Tasking.DetectionManager#DETECTION_MANAGER.SetReportInterval}(). --- To control how long a reporting message is displayed, use @{Tasking.DetectionManager#DETECTION_MANAGER.SetReportDisplayTime}(). --- Derived classes need to implement the method @{Tasking.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 @{Tasking.DetectionManager#DETECTION_MANAGER.StartReporting}() and @{Tasking.DetectionManager#DETECTION_MANAGER.StopReporting}() respectively. --- If an ad-hoc report is requested, use the method @{Tasking.DetectionManager#DETECTION_MANAGER#ReportNow}(). --- --- The default reporting interval is every 60 seconds. The reporting messages are displayed 15 seconds. --- --- === --- --- 2) @{Tasking.DetectionManager#DETECTION_REPORTING} class, extends @{Tasking.DetectionManager#DETECTION_MANAGER} --- ========================================================================================= --- The @{Tasking.DetectionManager#DETECTION_REPORTING} class implements detected units reporting. Reporting can be controlled using the reporting methods available in the @{Tasking.DetectionManager#DETECTION_MANAGER} class. --- --- 2.1) DETECTION_REPORTING constructor: --- ------------------------------- --- The @{Tasking.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 @{Wrapper.Unit#UNIT}s detected within the @{Functional.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 @{Functional.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 @{Functional.Detection#DETECTION_BASE} object. - -- @return #boolean Return true if you want the reporting to continue... false will cancel the reporting loop. - function DETECTION_REPORTING:ProcessDetected( Group, Detection ) - self:F2( Group ) - - 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 @{Functional.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 @{Tasking.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 @{Tasking.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 @{Wrapper.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 @{Tasking.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 @{Tasking.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 @{Wrapper.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" ) - ---- 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/DET - Detection/DET-001 - Detection Areas/DET-001 - Detection Areas.miz b/Moose Test Missions/DET - Detection/DET-001 - Detection Areas/DET-001 - Detection Areas.miz index 670aa650bfb60269a4f9d1adddddd123c00d4fb5..3d902d5a2af8a2c7c4de924d6706baf327dcacc3 100644 GIT binary patch delta 791 zcmbQWj<@j$FWk;2rc0M z#Lv5-==2%y!c||q&-h*q2w*y;)v$D`h)Lzn3c;$FAn%~b-bSB4H~fl;agLemZ1eZ; zBiZH{+2*x>f=-)0c=k#|LP|p7g@(jTDUFP%oljN7mO6b|w#?<}^QB_h3pm@JrA}jD zb)3e?5a2Dv%p$_Tz`()42qf2(7Cfm0GSZnD7{nN+-wS1ysP7GS%sylwVz>Rm{|_&Y zzFL@7E+K4V@z!NV*tK^CZ64RuHcwESv}||J-|woK=Br;H@IG|<_r32=t1Az_IecF7 zq1+d?X2vy(HTV)gZko9^yf`BFS^xQNfk!FdrZk-UzJR~4*WWe!oHvj6nGFs`&v_>8 zIk42_Xd%+pz{|`2RWEXxetU90*GO2r=6_FQtDe9? z?u_Ms);@U8BpR*4|Ew$Q?Q{*nkZBPf=PSNzx=RMy#HFXUPxIG)vPw=WD78Mu(6907 zm+K2!TOTmll%La{HS<`;#Alvc#Y$#nboAy&J(~6E7F!1EoY>3k-U*SMY)4ivjaX*3 zdDhb}uil>0+)=3YX|Yaw+J&}tZvXb)O}%Ju{xaS#{HA4b*Yy=M*I9h~tHY<>>7dyc z;nKk#6q5F@;QIZaKaC%zUHoNuKfs%rWwWXHXXfc@Vay`iHw!Y0=x(am}6i_1xDD$FlKETaMA(_F9u>QkUS88BA0>TuEyj>F&&)%Z$_YgT(J&T z3DvMj6PyO7tA{dcG5Kpx_7#_$o&#bp(VIRalv!@NTo|(&vyDE(boVf3Bj)L$4AX1E qm?fs)31gO0Nr+)!C{HX(&B!k;PAv}bW@Q5z%?*T4f!4FdgLnXkmo={d delta 196110 zcmV(%K;plF!~vSF4X|$(3C|oYN)bNlo`jR|C^dgwjj;;CFcd}iLjU2>DTNMEa0xiX zHYqNt)OK-^(7quM6R8pL?~MwjbUlZ2Z<<&?XbX~;_2i#-*hAem{fSBA#=~3b8xqK( zg`dtKPm@JcVZb36?_yVpPL$oTQCHpyer5_^WZo@cLn$p_yQ7dZY4wEv7RQTs_#i9T zI}-&W{GNRPvrjAc0}qFtGfD@C5n4m`0sy(<5tF+q8-MH_Yg6Pz@H1BVA4V)|cQwQX ze4e7>fl3L-3893E{R?K~Ab&z|mnzeTSHM*@>uq9a~^&C0WvrgWAIc)sIhBKYs5$&N)bX!9bh_kY_`jeEs@sufMjF^akS)-~YVP zZ~j;DAj+#SMC*@}9A9P^Eax z-G9&0p*Sy!(UW)I9gKsOaSk;6G&_6uGJX-i`!pR*jXUnMwn{bWP190)GNR+ zme|w>Y5HP3Iw_C<@l+`~i#X{44J+eP+!w`U6e*DSYeOs(fn_v`1W}f4TZaNf=#u;vyJ~BX|*`AkK1K znlL3%bgpAAKV|A5J$|R$9W9lX9b9r46#->!Ya*UFv*3ysWar$nNDP*QHY$qe1P6

      L{R$1jO*j_XqJM_tYONP~kQg}BWXW+>T|LYU__nzh8ZP5rLYuoO(1Z%D>KZIrWOYq;XcCs{B7w(@rRFu1pNv+rsd1+nAO&7C?&5a0 z{6t)=B^_rBog4L>F=eK@&KPjCXtL9cfq&_|X25s7+l+x(zu!zi7!?n-H%OtnmT7DB zoiP+|&UwZZnC3mxOhXN#qo6O7<9|%Ra;<8v+jXWwb_Kl*-Dkjwd$I~D;n6DcytCE2 zc0qAI8C;HXd)}lg2$>kj$!dAbB>%j>9nKx@T`6EYGY72%@)-|eGOP>QZsY&VdMB?h zPVcbFQQ*D(?1F*pbhH?rUygV-2hPbk{NVgzGM)7Zj2vt8`{X8mI4Mk^cYn-Sb?L?J zQsg?M{!Mcs*+d%aon)W&v(`ssvPdW3?JipK> z6s_8Z8a~hztS>HJ(mm=ac|FrYUn7f?%L=_^Fnpe4D?s6;cw&UKoC_y}f~krQKAzK) z3A_qy(+qBs%_o=gItn#YxqmzUMURg;_UE+x8up8f=D(Nco`5?fVzf2aNrRY8rh1%N z|DWC+=0i@Gz>x5J)Gqd&7Ufm12;WD!**g(Qcj#d8sw@U7t! zS&rp{ZM(`7#1W~o1%H?H8jDTdaVoQli@ZO4GPG~(Xghh@(p^A! zP|e$vWK>47mh96m&VLyRaB@cTg8t=sJ~msXT!3a4o$TQ#=Xp9YONL4#osvhv zY86dmk8AXR5o{h7&$aQXVMy7-0ex|@6MbPq67I!0$;HNsxQWete0qxIF z+Q5?ySrJFJa*CySWSK{U^7*1?Li(9&{)b~84vP!zUVkCVTu|msP@W6QbA!?wUxV`f zj3+&OCdj{vJ12)n-+b};(SxJo?Bsm%XZ|_$py_o8Uq0A7Rs@nuZyF-FGl63|ip-^@ z_eSa(9i%Kq_3;vs_H@qZCg{2n&{0Q2WUHUyVNPn)HwzL@32Oj1z0dZJkC+JmRrD(# zr2>CSlYiNxCr=*sraU+@rk_GI6pmI8^eGesO21Wy!-7y<6vbnwp}I#S5L4l#%$CiG z4U4;q_MUFgdywVz9#?RVJmoRWasam@MomXDJMfFqb>N!Up@YS!QXqQ^UXX)HMx~+- zwlyGw%qU3Qlyt7^3aUz~ND;zGRI4O?-3oNg0e|&1z?XlF9aXl43@MmXPN0>cPBvsK z`y4Z0i3{&~t7g6GqDc4J{T+Q|S9gcgRa2LUGBBD6u>K)-?Rfa@gbs2$i>aI8AeH*} z7k~GrgCc>j88lJ}8@7*mDE5vSqQ@Nfs{7%{Q7={Us-L&1blM+D@^OW)EH=$M?05gH z{D0zYqj~xDoW7pdy9A`@g)S(xInYYQa_YiVl+>x)#H+WZU23(#f>UpxMx3T%eZMi8crCvPx69 zRrn_(HBp^lrL(_9`*y9aTuCLDtYUZNDt~ru6}yR5?50!^)e0&Ww^t}8y()O9lW3-J z|C7hMGF{jD^9?XLPhs*=$mG0^$>$qj@_7oAKZi^{_n4#u^Tx7{FI?*RIAMtrU|^Mf zJ{pVDUkxSk@u)?B+x~b^<)g90{3TWrA3wKvVDm?!PNpL)P6Z<;}p)SF*j+FKyA{j_s2w7*m!t!)4^2|73=a1S7RH$)gdDWN~f-p45y zXi$-N?HAtDsg!kiIhqYG=oa2oiGONUpbp;m$GdK|QD#7kuP%6?cm1}^-YMC9*Ow;Z zn8c`hG~cO7vxL)5h*kik3KUd<|c{AFMqJ~_x_`T zjjwhVTl#w`%^Ei(+3u230x`GuJ_^jbi`51_p9E@B8VGP>C*)?ifef9ULk}G?X$IoI zsz*+-i3Xy921yyUD`=@^bLB*zugYvNoaV~^Upa3{nFaCGm*Z@*tQg$9Q*xBeXVNC+ zqIo81F69MXcHc5=Nd#30mVfniGV(~Z0`|i7s}Et-6Wa81Odzu4={z`^O&&aG0U5yB0>58Oihtn@@DB+7s6}+h zroJz<9FeP)?U5zuwp?_wig&>vZBVtn+-H97vy`5PMw5M(nE1tfmiOeE z-FrN@KeJCZB}-QAzPd`eHowOS#$HKk1S>AW+Dm|+9cQO>EFYKa>EuKpKRa%D%(PL6 z&aS;&erfmg;K2!>|8k5i{WzWM=V#MA_bG9-t{r0C+%$Nb zewLJlmQtbQQlIx|J8E`0$XO6nYm(HDRB_k^x%ZdxK1EwE0+!CBW4hI6km;sAz4+Ae zFUkE+^pj?%e7lj<^6&{z-Xls>wxQS7>tLEjlJq(*$n;U3CW+spX< z%aOUx%)U3vblilgYx$wiI_NbDOk-=QiLQ#q3V$8_#fSr$y!gS#d<&xWK@A6P)#5U_ z#zEel1OMb5Jh?eP`zKJyfdyc4GClfe2kTz|o)Yo0a00+-8zwXF5(^H-&mIQO*?f$j zV}~tp1EYf5-mt=Sx8o7C{KWYROvf2*E=TLzo*`4O(~tlG!#*>J>F6Y z7=LeF{Sy%BNpiiqo?PUlXYwy!wk+s}sQI7g?>+6rtfIm`nDqJ1988JyMuS~sBgyWB z!9apq*61~=M@+SB1Gh8%SL@nVR@yE{)x*37n3aQma8O2TLQA0*srF zvIQWD%vnKeaRwB?Pp6>YeqCcQS}aIJv*kB{Duxd@7-C(^rjgc`H*U2C;l#BD>3^o^ zOJ3Q=tPyh7Vj8}Zdbdnx2Jxy9K#uhIjraAM4hc4R2Di1PbKY0|^S{_XrHAURC>a`Y z&6kb``PNf%=S;kG73(RI-*Ui$j)5%e+(Hxiv$K3^l(thaKy_!M?UA%~Mvbe}`>$x; z>rX+_9MpxFAh%q`@nLI-Ht7g>3V&0_`CwU=2sND{0#0G)$EU+tzAQt6oX(Pfr!aNI zr&>#MCD;wv68I#(J{w&w%@yG`;EADqj^yUzx;u6BVr2ai^ErhHp!IRfdg^RvCP`Ny z`nwE@!31&G+hG_ELC_-~MhnYx_MMvXRv2nIe6?+9fjI$8ojDnv5xWc)8h@2o3B2Mj ze-qW6h%A=sO1GzFweQB8G)%S9P=!HnE?X?6fga@?Ea0VSA!T=!CQ=HG+9+krx;;%K z+^*6>LZDFtEjNA&DMDR^S3(at)j#3Y+OE476pwP^R|7DKyN0*w3a5M1GJ*O7vaes~ z49xqrP*_(9ZKRdqlQL$l*?$wwZQ!iBP&Ag+XKnu$PWDKn8ku-BbBUe&jAFwZJnXqj zSuWYME2OS_I1-)Pqkhf()gZF6T`9IhXBC>jgWNA$&v@-abm-??e$HLjaG&z7|1E=< z59NVm@A~kTX#MTh*zH)5;D+ChUesLfdTTeh-r7yL-r8O0>DJA>n}3X{*ISh^gk9&5 zFGQCYx{Vlm8^kOA-@c4BtzOXU5Oc~c=EY)O@)mQNn9VKbrLTo6f_qt~y!bpfzuED= zvDUn35vN|k%^?&WI@2YQim8-tp$W`Cx-c{eyF5;NkaWYbSh}YaW%AXtHFG1eO z3xawdTdG`{+A>m{Fi!)XXV*}~v35#r)HPamk78)v~*E{BYaehHDW%VQ8+A+U8xX-=4tP4&z9+bswBZt~+20^E^?zYr@XM=r4A2H5Qwx?UCOerIv;3S?jLjmc{@msioTe`t7j5@pHh!3M zq%Y?Iwl_5n)Fw)EN$r9<(`zMSvW-NY1qBr%)%Wer? zlkJ!>4Xyo7NBTL$n0^hJ4cuLxCKEW zIJnmc6eaq^MHjX$FND^tHR_v1xF(Jy>aC4VYPGAbtxhKjaMb|@)UXz*gaIBaZ1#}U zF`r(8NPj>U#SeIDKs8MRo%@kh(c>XVKbB*eetj5J1u_qLzve|!G+7j!pK_EVH|qaR z8_5Uzno$4hw6ATBLbAFPRc%xejwhmbwrA&qXjj+W&GXA*X0;EhN3CAUl+1{HLRy-l zV#sEy*OncaBr{*5ty7};7ys-JFPNe?Al5#tsDG4r^1yveh~ZvASx{_|W%Kqgr)7lc zUS{%!V{6fv64}9wpXqixHe1Kb5e2xv&Q5mfS%$2!PpvE{n2knlP2iTbfm_wOiI&=2 zvAtlbXWuKF7BguWt{E(-jkK+S`YGt-{jB=NB-Fg}hB)@x3fldVm~g6!O4*1Hc7xPF zoqxW~a{`amh}Z|jFSIweWoYSs;0-8*r`=D2y>q3uPWsf-wyI^&bM)~tRLv;cmP6ti z&80B9C$*@sFVt`6a99-b&WoBH>XlnR4#xo#+9BSrt!$PU3g_6MCn zZJkn4u+8@~e*QmuN)-Q|j9<_AVKXBEYJX78{F`S{AMEJnY4JLhN$j!F&X%Q@UF6iuW^?;QArGx@Q^udvWyJdNs)rM&8TXw_dRea^c7&UUiV8N| zMh{r|oQwro6`FF3oow5Z{$Mjo2i#fDT&A(L)#i;-?>3`uDuV)C`p(Py9!=MbwtuUJ zu^m_}BJ*0q{pi5=jpZ%C@g#s57P_b-btMBl8NWRf^Otc$7u*!X*(j&C z_g*7DhED=?>H$F-zgBA>s(mfXLah76<>S`f_qPS|*Rh4gb3C1JOs}SNI%07dORtYL z!xt7fpu}C;5l~{xi2E`RsK#k>Eq|SRov#51GO-0Cg-7-eD$u7xnbyqc&y=@q`Mb4U z0zu|X4qpUNfjJ?a#}idv7JJBE#Ped0un|P)B4=TF6XFsZ60X5l=@x&h^<7 zvoILiIX`w83hs#f9QBrx-4ZWZHB4Lnyg-?#e}PX4-1+>$0li~txf~YY%YV)YG%I-k z=SI+I3|KFC%bl6L`@%~cvu-d0sozqDoZnzp#>|>;&Y1HFJ?8rNbja(wYk}Wa;`(8w zFMnCZK9h&F*dQX`n9y|_X*(YJ#`?aU{kB{dH)6z>U_f2&niRW+N~agL);exl(l;j2 zNOc-$SsiFN><5tEAdMs)F@N=221@-YkNBa4gMD*%S`f_Kz{}5e>oFO{K41rv?vInz zQRMy^4}LW8zOmKb+S>L%bugs;#Vw##MldO~;?7Ba?@XHRH3(}hzXt6!@z$(Ftgx)5o0tEBr~BgyAW{>*?!KJ?)z2=nMpJ{Hofag{3S>D4E?oP>S>b{WXzN|E zm8-RIC^7!>7&~q!Pq(SnsvLpiszE>&#uEBzKb+0MmZK-daFBPTI@^t{pDg&@M|c<7 z!42s??>eALFKVme8h<6DR!Y$O7t7J@X|M`E;5N{B&B>Z;Jds^O;d>2KT`~ZFLZx~u z*e9lKE@zoMXNWbeoSqR+r5gYrtPXlp%IQys7qV{4$7cjfy}_^7RcEt<%&qTe3nhMw zx?mGtZ%-pv_(`cpq}u|g(nZjf!yH`?`N=Yqy?tw#y83N?9)G*sItFJz3k}=Sro}Jk zMh-KA!z0#ZIXKcTNacE?;H_Zc1vW{A0k9WzVaZuyswpNR1<=k(YZO_Px#A}9-S=O7 z@AwM9TL9UjuO9fW#u|9d*>BL>`E5z|y02z%`jy@2M9YAvkr(+9PV?gF!FZNW>3Wkk zZ_~3I!ocZGm4D)ufhfT`Rfydx4ucB#-MfOYuSe!z+G^}d*bIJCyi@d_=IpXz^Nex* z-Jl$nUNZkk2FF?0X~%1wWi^GJ{L9{zcQ;bQ@W1(k?=W;9kd!pQma|Zxv>fFsYzsWD z*Ow;K?m(I;nFJ`j^4Za0ttQi{w{6s2{u8uNRYu!Vt^t>?)I27hXqXh^kY4gD00IsXVu!4val(}~d8qUUSl@_y+xf((1#FVc zW1D3Rl5Df1jSgn6K8Blnu?WKU{1?f=hT`>y8tSe&(41IZe|_&vAJ1L&gq5>sB`SJ;UJTU}W`A3@ zMR;fQzMEZ5%VExK=n08(`olR@@w4}z_~z|_e3I<&l34Ryr{DkfQ(9#ZE=?%-mWe$m z2G93D`1HLl>d^@hJnUrz#m6fo!Bzbh3$P8cbM2NUIrFQEJE#E{mGh&s%Mz*0K7)=WHs!hZtCiE22XPN_RuadXo(dpOhMAP?zCmY(55lj1Z@ zN~%6p72^&RRGZlewkW`vWAf>FTf^U)xs59bX(xgx2B~_ECK2yXE{alisP8oSmlC|A?r}|UC@-f!OnuhXcw4i zzD15a%bhfqxbl-;VZ-l>3Cov<0jqI7W_eGqb(9P)1_cRL^;-=KR~(j1-7hd4fvNb? z8IbIqUj_`XM^&nU!yz9ov-2VI zv7HDx5^Ioazbht==Ah9v32x+6${ozu(GJ_8A$J+;Ko5lKa_m>OG|b0|9)NhrBwGa{ zY{t`^QKc4V?@-8_=6|fPghlIAR>|mQ=2*}2k%vR!U6d5nbPzH2VscLT`_pS>&DFTP zR-;JxNQVd0AC&Zm`M4OCSFEcnhGHt+;@>~zB!_X1j*s)1qx@>X$D3ToeVvfG^62RJ z9cqhwHlCkIB}tttJ*ZfbRUZ>c7`3&zRAy3}M|CLLc8{}m7k}hrv9z?ALWX6<8pT&8 zT}mw2^xu@fm|Y7Wt@MoV2&TbdBlGQR3_+r9&i4@hO6ARTf#HWhkvB!7Y~)`3G;aZ!~MP)Ze^#2?{%?N<*5O$y^%SNbU!b2UZ%fxf{`Xi zLx>ZMeg?U#`c8VXAYC^A$BNdi`$wOgJUII3qrJC3vQxPIG=Ie>d&eKHLv#L1KAa2918R(G z-{?E_Q*?pEmdZxGQOuxAX=Z@swC?SnG?IEeKii7Q?FBp*BVD~la1%|DQl+06Mkk)Q+KdwB6cdY1z}w(;}nN8?VdU?3N|W- zbKe_^PT$_Ucghj?@qkxyJEy0yn?uNdB9g8yT>`(FjB*0coP8@e;i25b)UY~HqA^dG zA6}31zH6jjlZ=RDc+gBF4SaJ!gLHoOUFYF!c7IjvJo~IZ_sPya`vc(_3d(2c{<|$f zwB- z-Gsn11!Y$2{f2rQL}A}`?kRx-sir4!2WNaE?R7}SgjGfLTg^Tz^5vL}dSX!bqXm`< znST%UtFEp*$?se>&)_~_7h8ymt+2j-NBqPST^Ll&Ufnjlx>(O#GOy^Dz9H^%pLLe5 z&xL(jf}!_X^^$zj#ZJ{gcsVQGvWE#Z`<7lQLoJi;f#Vo8l&&8)Je~6(u~oCp#p-}A z1~7fVbjph#GC{fynoxHj$26(zr?ZC=N`J`~aLLKit>8RSVGe~fQYak*^+4~Zt`qNa zkKX2yN_Hxz#1|}K7#Rbz-}X>n>&V8r-G}AXm3;^dYi9j1Cl=TulR$Nisbn6KK1}2d zw*(*=xcX5&em^O*pGXKJB|y!I?KWs_B~p-0r&MC+kEes8Qj=@Wm=;yD@@gSpB7Z#Q zTAJn4T;)6%+Z_f0Kd>O@<|4PK=NKk)G}7FzX;}SY3^6(vmjxD@q@vA9v}8^*?7-H$ zEGIR44#fH{fd$Y)D0{>|Im!)P<>VIs;+GSkp)M(ErBk8`yh0h$K@t-3xV)hHvl2rV z9Cic5NwH*mJepHqG=ef1CRX90CVzw5K9~6gSd4C|(~;yj_k!0CF`-7JOTQ^AJFPFv z;hyt8Lzov8EW&z3=!;7E= zaquH59Za2R0bMW_?|ai<3QQ$-MsiY(W`_QvKG-GAuGv5=+BAQtOR@BXrGEldwtO5! zJyFUE1Ox@QhcUxdb+kyn^W&e!i@dBBiOWn-0DcU*n1q*{nzzj=e z2&|9#V0U^sp>rUT_Ou~PK zENqC%26^bVsg)a*4G(Zbl)@VerCVvT33HtNLe&} zIkD;Pi$_YvF0VEg~Xly2ojxk&E*?0<+^h#yS+NM&2{li zwD2}OtGO=yad$0D4ipa{QoFS1>XZZbx_!DMhO(zj>yfBxbAS}{U8~N}Ti%Qez`)Ex z;EJrd5+r2TT@EVfZ=g8MNVrhj1;IzWPkhzOa)0bVVFbz8e<;$INlzv)Ts;b2(NDD_ z$GHwxb6<-^z&`tiP2=`=oqO{4x7{uA5-gv)>L!*w+ud3;!ndJq1GX=w1GaAHAQM@e z;@KozFhdh*!i14x)tbuWBOaelM^eqEAjk))FNB)R-H@UmlX8yr`2a7|K82Ft5~p^g znSV}~svGxU>s33{!ok9a?T)X_#VW`%8gW_=zv?$PNxLfAC}sJ$3+yP#XH5zQ5ebxw zp|K~@XStM!(EpMx8mO%RiOU2d+C->lKj-FfP0DKL>xamleFcQGHW#)(#X8ufV| z^(fM;B&BL#wxPMM4wJx|!eGy!G2@Um!D4*vvK+s^CW(&BT z&&S-JCTBU1&7b}}$%(OJ18ifJ{8h$y1yYC!gJEq&jS1O?E7$nR<&ccYd6G5erQGQ& zV+vPop4-jqCI>LmC6QE39i{f_P8d$+u&TMKO7*R7Yw1R8wLe(N`S+OoX2rYi2Y(-v zLQ8Vd2cBF3*2$>-yjK+H|BIj?OJa&BcqT&TP;qK4u+@8BSmZv$rwEV$+utdG^7uXMn zt~&A&O@glu+YuNBt_JEPKCAOfrGLEAy9F`(Amq0_9RYid05lewA++R3w$t;a>p)S~7S@vQ$sH-BD~Teh9g zP>c=cKT$QlhT>Ddf@~kMp0mZ;hW>l+@Z|l^-`>f-;S8eZpL;Ib3R**|LvrUnxU+ie zV@X_mRvLQtY+Rl_d+FYkv>bWz{&OePIQ`|0-B!<1oAkIv|I5xOgUBj?VRRehSx1%w z18MZ&5!Vu|PXTYw13E;IN`H?0>m?JiNj78k6W+f~7ph$rqUG{!Nh;RdNoWdtHDim$ zaJfwe1O4o!)XQ}`;LBq48N*d~jdh^bq&X>B@ANWJ`k#u1;vX0(1f#PaQI9D~y|6^U z6tU{FT(ioQiuYx{3~X3Mmmv&wRB9v_FPJi2PgtG{_NO#TQK-!}pMN(P|FA^E;0AFQ ze+|Brrl|wiXi&RAXqCn=sB9R7VJ`b#_`lc<`MGQgbK$0a^-}YxM&qTKFpNj0*mlLO zMWHGR|JP{HBw!N1dblp-u4oHyDxHvTB03-D zrxZwYgoZqH5iYD4oR6Y?X%wm?U^GkgpJ zsdPASN3>ZnEJ4f9NTE(@|D>z^M{6&1XI2groXsyT*vQdjS(SPS&R99Bg$J zY!wxvM%b70tPg|e@gYxlHMimCqN>fwKhvD7nH`c6#X6*hKo2UxzNzSSEv}PlxLd@if{n^Z;>*(PPq zZn(d%KtfA#@Dht;VrP^JNsg`fjO#M7=w`DDVI|eHsohK_JeA|p0x1v=!IB8$n3)WE z5VmxQL>yzq5svYIbOg8z`&u3_Do38S+eSrZFn?f~oO#t_1QZlHaVJ+O&&1GMVY3T~ZVsdd6 zWT~T3^W?cTx3-N>Z7BrKe5MbJtDD16XS$nKU9xjF6^f_}dTyEu#-fd>)3$?&p0Hr^ zrGKY`?97jOERlb#N=w4k>58#vVb!NA^zwmIH*+zk zxr7h<;kL-au{Mu0JZ#6*jYiB(Ip!^%pM5+`!;fbrsg~SAcTWBw&XJuypixGy*=bY7 zVn()X=v76-U(r8XAAh`caIll@f3$aeytBDUvx;4-Mmqx8r|gVYS(lH>j;VI#08fv` zBZGfC1y~!jfaWL;_u3oaG_YK1Is{N-)&1w5e`)La`&-YyvO_8?0`N_BEcVY&-amS9 zETch=4<5e#`N`qYC&wy%1dJsHTb#|sq}0zHf3vyyY4Jl&TaAd~l69D0R{eZHW3@k> zKYPiea@xOoc=hId_oe%dG*EZmx!(dHd{;v4K6-GE?Eyxg+vM|-d7jJtn-zLqcYPo_4Zk(EgW3Z z7kXtPzt!jF&nspOq}M0%WEp&4kxDjt{QkzS)Q>-#h|?Z@0l=lBR3b|veT^$el_-C# z=ggbp-ksnB&{D}=s_0g@Mse2u;>9dOg}#0zu%e3gi-iB%O$^beYjFrHdKXu4ZcH84 ztMuAt%m-#l8ox<~ENiq!^elezRLx3`8@AJ5X z<|;uMmn@@YpJv!&yL>TyTcmwX0s?>YZ-K;T9eh0`_qsw5J#r*hsbly>N$D2SZNwd; zA9eiwNOMCcU-3ZkQqoYEScg!O?5_F?cjLcE%urZGDPPWWQo7*)WS-8=3dJpSc4k-!6%uOyGVbx4Rgkn zHE}L6q=7}aSE=eJ_16+J!VJJP=t;?N{8w7W;R-7b6*{3~VGLl5fA3{q13rQ$Ds$O# ziasjk95`yy8bpW*4xX zGac@2FYJ;IU&PMa)VYal5K{x=!%x1|!OOth;PrV$pw<{(%QR+HHsomor2$F10fh>b z^r^m~s$_{6gF(R%kw(A_(L>c^xE|j}7~8p}c3OHL+Y0LbdHnZ=2uaO>lqh*xL=KRiX?=o%rD^|6Gl8j_ z&3*!v#*S}t#qf>%MVhaW(JChf0Z}O*7=%?xFV!{NsAnK8*0X<@4_QU)o8ZK$OCjbY zFJkbh<=hplHi{|;H2c#i_ExoFd~?Vh|LSKQ%g~?ZV>$`#Lg{wKfu=;_=w#0AsBMvUupno6RM*~48Mi165KBW4dA*38}(M8MYDICAJ z4!R<5xG#Tr5{=e%iRKlb&am1Lh;D#_TLlt41I;MsyBkMMcre&EWIN6dQti~8*Pgzx zZd!H6%dju~;H5}1BgV^OQTH{l{2Gjiw(`{Ef@*zw6^m%4lx;aDD>?qNCkkf-;#q2Zy>Z@DhbKc$SFe5oX7feOB=XyPjM;I81HqFz1JigZueTEP=buLj;MyLrer1 zBM2V2Z4FZFIB3u=?`bf-A&5Y0?fp!)DUgJ^lD0?F(qv@K@GFmFsw}nfuu)FVTTP_QeXk<{`~SwSZ(7BswuJBwT+~7K!Z0sK)R`9b7P6S6-}(H*$+i65D!ldpADi z=>bS!`P7*g8#yZ~(bY8TS1KoxzaAQmmUZd)#jWd>6XB00nQg?Cz_f3i)bVD0xsw1L zU^LA1A~KZf#k27l3_Ho1|2QA;Z4SXDEKg)dc3yzq`=2MR9?FS<2_N9IR-S(d?SM|u zzq6HEPV|Vr+&L}#@lZgQ3WW-?C;|Acnj>m`m zwfZ=ZYsY?tpZi_-_`lU1boNFO^m$&0ZXwNDM*|7C{V1VW*mR~WX%v2%1{)w z0w#hivpi@f9G+kAuj>S>=zf7# zcaw9(uNwAnnrtkNL(#i+&;)be`|Fsi+*!@t<=lEN;8t>;Z0>S1-r5Q~tL@Oln;GwL z`k~!TxQk^J=OtcN7pS1x{!^d6#97j+Q|ghofwMnS7mX$2Kbq6fNax<}A6^nGSgSC*2 zu0VT%Oq%z#)NtOp$@JnZobr)#hS}bGnVRTP8qV?jSzNwml>ffRpH7rF@m$_)zkS?I zb2=beP3M0Ows9s0L~ICx@^Eb8z0Od9D5T%Y{`NCnJhLVyJP;Ea+~($yfG#IH(x7eT zxm4e&8{7vXF3gukB=3_lJJjQqMK!?oknX;%BRPCLlB2dhz@H)0{qpL%iB;js)so{D zN=o-g93FJKsgycC*U90gz*`;4&ce&&SzPO!oiHL(I!yRlrY3{W*HYjpaV!PH|=s|mkMs{BaN z$?uzplu9v$NQ)1#BMsGmv2=~17@JoqdVa5#KHb)cYH@m|zm1_Knz(8wIcN3d<*t;q znWAL1@CZ7oEyTTS?8SSo7tutDZZwjLkH3FB8frgQWcNNkJU*tYJC@PxK$nqi=>9VD z_vx!qLs#wjhjKK^*`lY@Qw;PyKYmUShl*3cn{Dd>EvI(n-Y-W(kq?PQ;57NB1J zr8E=5Uf*A8Q+(On;~?L4NDv^{)L=SB8LWqjBF{n3!{bV#<}^-FUUaHKkB% zpw_4I!FYA{)71c>SGpa6(R`~mp$d`UK}5EZb$BI+*P0{N)hp#TGX20LN=cTyrga~` z{UOhaPgpAz?q#*Y4w%QRPTSWBg-CIQ&+6qXB*wg%UZRI%1ZTHvVg_gpbJX7 zaS{SlWYR(1J+?9eBQcsPoRlgHEA2seEP3xJeW1L8ft_Hb3 z;qZxX+dY_HUS3B7s!gSyf**fYCmd14k{jVczc|cb{g~+7d(Z7)Y`kU7iIHqx(`%k{ zbUa%+s=fhEb_<>Nv+mAGxks_Y$YDos!(*i<<#)yn^E~9<#o(OeH0Mha?OSq#P^oaw zBV9Xz)BkFsG1*p337$kr7azY`hq#izV z+`Z{|p=E;#qssPn=ag1X&Xr>%ZV~0XuDw8)BI>ciYAS2WFlgRH)y0C28@x%zQnl!= zfmA9(GwHyo6}O6-GGJrKREw8ydakoxY?#6YrfL3;mSgr38-{lez&7nWqIrztZ)zEj ziz97E6EXk${JPco@Th-Ftu}tz?1!vAo724I&~AT0#zK7*U|bx(bK-^tUc{pUpx&TR z{*z~Pk4jEigc&`w850EGG$gdN)rcUtrU9X)k0=R%I(2G2zE<`+w~E=7vV~7K@*CCU&o8_Dy8_phl)O z?JILT_=@u;-5Y;!#UF5B=hFk_i=eB%!Mrmed_%o#KmqVcYk`z>HT6wvNZ4~-;Q4qk z8C)n;8#425SZEBDa4Ro;;;y?80!`I&GQ6A$_1V_af0Su88RDazv*7Y-IN-Aq4P^tX zT+kctzR$M0H2P#fZzwS7rlGj9->v)Bt;Gp^DQM9h5UqbT?F+hc>9%YUj`*|A=hqp% z4utMsGo7NJjKbjo_aL|g>hQn`C`6XNkQ(Np7-TAaidnsgnhzmG>OugOl<>P7BYTgPQp-Ux-uZ+Tqf?;->>7H;lyrcrIlRyi z_EVG=cZq*$fE)&2V9eg*{lT#6Qw4LvojCQcI*att9Yn{30o-BiS>LLc0Zg=x&nubh zT6pKRYoc+05BN~f55#~6W?9X55L3?o*6u5lNb=Ja|MlZMw{cJqV|<>4%nExCwk&0B zpMf#0W7w-^E zEY1OmDOhnVx{~bzXy*cy82cB1mI@NchPtkzQNi%_>!kkI{yf@m(&=U<(n@ko7P@x- zDkds}z_$^a7bZ3Ei2q&beH9qtX(!NV5y~uZoQN$ih0Q2Yi zF@-O#*qcf9=)GJwnuj%PxE+6xPm0;aPfJm+a6J7wu&{|?1t&Bt3|!H) zyO8imgLadCX%TkDZ!+nl0>GVt37UxfRd+AhJ`LZCa(eB`f0GI78%aB`qW<3DgHJ!& z`{bSPWO>b4_KGQg06QmaG5GuMd~~>f^eN?XeCNR5&&S2EB(@~sIaA`3gHw(Ma|M6- z&>>%g8_=JudHnw2r%IF&cKoooN`sy}_27Gn{J@EK&Y;&Cns6rXsBpD1<&(hu(4h!HmXx?%1f0nn+%mo?d@AL9O68 zwUXu9`q38gpQI%0DXya^S@zecilZxKMK0=8{uoVfEi~h14S_o3d%wK;qg2PopxN7(Y?GRHs-Ts21!R-cVd>F5P)pX$LLz~5J zJHfiax>#1%M(v`VepQUro0lKjD#-YAr-ub~ZRdx-NVklv)btQh4KtBO(mX)`=CAfZ zJe6A=3|ULz9JYJz?|v~7g|SAkrSWHLmbCXz(<>n=tyQkDS*L)YNuz%N@z?587R_s+ z2uGNjylF~((~*3{n$NAI=fGR2;L2!5pNSA{OOyF0>n6AU&r(dLD6N&C71!@YRg?ON z;=f@!#oZlz9bs{S>C}@&PrrAk8$M?gJp6$^G-%&FTu1NVL@jTy->eikljC>m>8ESq zh?d)KpB^0_^7pfwx7~j}E%o%Uj17P%x8TBY1iBw-T2PXpwYKH_O0Bl!8h7CWR7-c^ z*4TLSafX|2i=sPix*3u?Y`OtOLCTaT z4m2f`wURcV*^PgV=x4bS2~j6fG$q~F5TvE+iYn@*7&Twy!|hHt^5(3#(StRBwjM0t z6CSKzxM0(6z1q$N$fi)xRk*y0ki;wYB#7tJwpe1}ZZA{YzjWG9sqMB-`~L*B-Tp_aEhCH_lXlw5^}*>!q^;X6v~_#6 zw$}A^N^XB?d0BHJw#|uf+Nf}JkILV#%yp;YAE3w&goP^J2ZK>Dud;uXMvHc=V~t}J z=%Vv807iNt^j;2T5BsW3bgE@jpRbC|`>!c<8XbI-fLJYgMv)B5s^gkjAVx!u<;z>S zZfxaxX}w-}ofRmG$=bxlhIG}=7Q0n~1wFJFHqY$ulqI+C>DzdN4)v2#+Z!JE{s z6?%0$jT($+#cV#xQ_^{)YH?6#h&MZF*Xp036ML@JP%r*61z2gqsaS50i-yx`pP_%f zT=ZEw4LLx@T(#SI24{3KpFM{8cNW> zAi{t850??mr{{yAhhnU&StLh$u8Y zL0OBmw#f2?;(VA7Ml42}mqz);G|#I$>)d31d6rL2bLuTEF%$1AM5nv9?3YUZuE9yW za3Wb5h9}vE>ymkR1G2>T)6vcTFa2M8O(lQ7_+M8@Ht>4(TtG^eCL{Uwne0AX&fs#Z zbun#b&%g3q_H2j?dY!5hUB?l_;BybFr`b^5&Fsx$D`wlpgM#x)!duaMT+TP7Op6h1 zuMw!;(#pFKa)0Zc$4K2SgP1?d22bcH);hK~!P{(wE%_;@0Z`1Yo!@y^CMBKQdLw^- z4!3wp(02aD`6^{kjH+gD26Q)iv_P}cvsJL$dALd4U`_ww^4W)XRSs{KiE#hvI`QGh z@BjU35y%k8ApaCF0d`PR0|kGucdX5g)4*TAfAG)`q=zpY$NO;%r7h6manJ@BV1RKG zr!^#Tu@jEM^uMzYZ&tG8SWeP%g}{HnU98n=wOXxKtJUgfQ2#I6AFq8M|Duok@c&2s zH^5p3c4LG`T9lyi%XH{(PyhNLdaeH^)|=(eBFes?!eEK2RK6evEO{0zbZ;l<1 zw8<&R5NP;O)Z;~>NI6+|y5A2E3XtFmNwkc@CQZ&ET*HL5cV-Z2L2@R4p{jo)`GH$B zMb-B;Nm6F#gQIU!w*-E`yn-CoDDDz@G>Wm10W;&KQ|@j}`mQg$>gHaRGj+vusizi? zmmQ6xZQdc_u!O4Pg;du2LY5Snxn8}LNp%hV!yrdV6mF&nZ%xJFrgF#dC%~(b{A_O1 zz^Rx504l_!V7owz5{PW+yTE_v=>-5f;w=MufeE0|c5#M!oE7Dq(%X=zkQ+;p@8*Ea z0l$b>ou!Pv%|dQ36o0#L(ap9d0z!vPJWfO0o=m1ef>nP1zvBD-X5w3+Su#@Z{sghe0GTIr3^7h4`VKN~t06yAHsf~Z9)N>^Er|Iba ztq4H$x+?;5=zOwBcT)UcvTHtq5xsw6j8v~H$6(n^G6<;ON$;A{yL^9y*tmE2`3c0E zNBoPBFn_D?u6t(75b0jwDHFhw93f}Rw**-erSCke4}kB%0kdHO_% z=<~gtC)9^k_C5jJ{bYYJKWkd5B|zu-r2W*9)8z2$%`S=yq?g>}glae4Hl1$I&D6Mm zwiJsv)oEWZf-XVpvpP+ep#4vm`7c3xPmH#G3EE3gr%TXyCt0ur=O)$)D{~VISbJ__ zh4kbm{*9Qf^M_L5jr?%R06B#S_>^s%%SE{S=PyyQDMzw#USdqq9fqb{9wY2dT zwFN^D5X*(=_TDmgF~#0h63 z{z=Osf2lZm4DU8M>;dkifDLrPBpYIvBt|$b5wS2&`>e!i7eS%?0e+NG)i1^f|2>At z8ko@K;(P?wU`eu_k9m0CjpTh!{*Cm4rZ~+PbH-{a+P#0hJ(N+Hl3L3YEQRzI0~x=i z?ltW6&vXc_X+Mw`_>=UCwNCf0azG4~_5rehxP{V`H|p$fa88WU1zbzv4;0B!ItR6x zF8Pv??j2?0F|;$&#|rI!$4ZB{u^GSQjDjo;RLv?3r!bHnj+B-7lox`7&H=HXz}yks z4;~~Y?;d|V5bnLJm3_Jf#rR-8hewCZnQ`-O)B`EpkGLpGY&9Ez?Th4!je#1fr7f3S z$#$c6zQ`tMF?S2?Jv zxp8?0ev92Y8wFwJxXEVxf>I-5n^7K5&y(UeliYtV@;}^pK}w9nMJa9=Hp3z?230L7!GBV=f@oS!X?l*7Exk>#I2 zan4#lE6(#`mfBRW7W9t~4u5GBc8EZc9t>n6qUI?b7QaF}lOzIk#K61&OP;{!(>xbw zRRVt?<>@S)URT0nq1%UHA;E8F$;Vld9#9Oq#c})`l{rNukVThFr$8g(V}|2ZOcvr6 z{AA87S^lwDHxAc3$jYbrWMmSHzkO_AeLBt2@z9P@Y;mEL(O%J4owvh9fvQ@+I~z>? z(xpG2!;p)}Xj1w82)`4n=BAba9v0ChXPSS9=p}xfzrBHj`49O_B$IJFWYW+A287)E z;pvyhznmT%oqTiFqBNft=h^%)D~5)er*E4vji?d31}FL?<9isv)ErP`yi1c})d6m7 z4OJVAf)=W_V^@S(f`3a}H2G;dUZfLK?JmEl1u#p&hA4r3kU$JK^j8+%5gS z$humR*Dh7PMM;+~<|R6v%DgD|+bn<17NfDElT5Jh4i3-M`htmEB;$GIEX4h9i7X)~ zvO&g0mcriun)FB+^$3u+)I`pdCuueV;Av%}oI_*OB|up22hT2p%H!DuxDdP+E)@ZP z>h_Ns0i!%6?}qFRbv&HWm-pLVam0enQrKj?hta6NE=oX$Gr4T8lM>O5%_V;&7?H^3 zGUByaHvk0Pg);-L;W9<#PLvG!BLi`NciP=9*ZOF#ZETxO(z$J!UFLZS%#)t2w969u zxFt@b@yXoYs`2UXV5sG}ovsaN*=x}P)#B*U0^S*IY0lRmOCWidSR?qgr+O}iX#qZ9 z8;<-{Jm-*2P(H~ERes0)57b5nL$*J4skRCt1O?o6!<{U&|2oWif!PGJ$j zK)#!KFZH%(12A(dBlI0 z_WEs=&%nGGUVfKc%O-#NtF%B<1GfcK1mk{9cBhT9;_2%(o<1en31g{`qg8!n=nxKD zc+pptBvzkU7hSs(@l?`nB4;MOjtY9I5Jrbe1^NwUTg1>$rmOSOfueDx4~f0=qu=65=!yS|i#i8GHq*4+QZj9m6&w zfb;>89wrEbZ(&W(;JDD11JLesTMP!bB?bmB!bivENrz!&-gFa%C6Bbq>MX&kQ0y$h zirvW)tc6aN;P!u;l}3`?dUjj;08f9=A+PycvhZ8usAv057GkaIWFhu?{4TdY>~hF=gzu&hc3ang6e-ip zzk;k|3SkwWW<*v%iwmH|eRrfBXoMAbGR)3kt-;w+0Pug=;itz(-+X?2%A{zdia#T< z%SG9YlpKE1Hy$gngpv12?$L9|@Yi4e5sZ8K*QSyB1?W(yUOiCA&%+yYD2*1wa%d>D z=i0GZsB{6+TE%xST)Op&Zxb5(s-_?4h1S4296K3T&&_m7;ZRDzPUXSVXW>h)ao4L} zprBwM4$gnv?=tp#cr^UJOnq!;56So-8aM>?8)N~8j0pVqr2xu_67RSxky?SFHT@k< zr4u-Q{4=Jaqhy|7sL~a9YRjIY-v?J8SBMzJk9+jT%dh?f$dGOC;`w_|k!S#>Epc4k z7BK$yLTgxCO@1(y&18OwujVAS_oj?se<0=2`%izs(tFfNNHnW~iB~Y9^|Sy{EfNAP{5t|T*Z z+O>awzwD)6=ZxD^t@}#FPMu}ud=l993=R0Nglx$a`zpV>LZOt?&VhJU9HX$yobp*) z;f4~>bjai&HTEP}N7@5rT~|9nKEJ%w2{v`$zKAR6RP&d!hXb?P2OQ?TbcS4?hb@1JnWlSDOIzr$Bv;>d=932_L zvgvSHf_KYox|k&NP#bvYP;v8lB-UF%Fi-Mqmf8!E#>4_Z`sWoNMHXoZ7EIX}TFF1p z$BxgS6|$zFFIo%C``~)CH=A8lUV7Fm&C;uDRs@WIYL)uoz3&BCCRd7bqe~}GL+&U z-g2P?VC1h&a7r-&>u*2ip%SMZ^g>df8BqS}g|j&Fq&E0aLpAE(E{ASRf17_(Y_5eH zA4+$cV|5V~bzxzuk?YdW(3?qr)m2?domL#VZiaLP)^3Fg-OnhM0~LALHjYA62_s zZk4viL^lHJ$*NvyA0dj2p=&3vIG%A84* zRtySmP9Mc6kSQwBzZD$!iSImOR_!;p5j$^i#ulm^RWE_ za?s$ldnLIAO@)7CLRQyn5&}s_eV<mOakTNyU6!s4< z?Xu>9$an@Oc!0as+>KsSm5` zkpq*=HiK{(z?Zc3tx5Tcd?qS+cyN@tUb#f8E%l?P7?HjxK+ZJxKFTQCeR@C3Qx#lU zDbtu11>GU=Q=*bAG5?CAk#YK5nmk(OVVuwgka&Ony#id5C0hMtY<}QuQ+XbNP=kEX zUoW$Z%SuCu7HAR{?fh^8|1XtJ8Prj8XOtR+QmRDy{fyUHYK~x;jE%1P)^gPzpQDPX zC+ZrbD|KXPL+nU4gD-I90a}yls%)eaRGHXWu;|Pbn&P=wE1{+ny~O3Jp3u?pqMNAT zMN)qsFKX6{7ujv%w2b>=Z_~fWcf${Db>-O9{p9G31b!RnM-nP ztgo@PaK$=?v1*=pIhEr27zf3%#kbV>(qhBCf-|&iv2m#wZ7$GDv4E%nr?n!E&^p1& zM13FpWJc*TwNsu{UKO*L*Iy_~p)B&54F-R-ij@zSYQE#8N2i*1UMMK8RuHpT=I;s| z3IwI1OC2(G5IiVus=wV>z7B_e+;FmMLkrcZ;i~{ruaQZLS_H(0S;`dg@>45bIc`w@ z8w$s2QF7$S5+ZXVpquC9sJD-`Tw&5lBIPPgbpdjuN9U20y3*tD*M&8Kq|_CmIzN9M zNCnZpI6`DkZF{2>H#rskenk5)*nd}gjor>NN-xsIA{tay_-+@o+r{j5F}q#NCcBuO z9lZ@Zy2eYBMkdbWCx&TgdPi2MDt4ge$~jCB&Kg3QU~qCTg|GBpcY{Q%Wt5J`Fv66W zisef4LTWaMycWV1;UT!OO9$Nj{@++q?dP@Z*U7_xR}H zFwcuoHY2s2kF_m2xZla)eB;Z z?99|{nyEX}YPD%q8L2(eS>1nane{EGt+iL#O1QvRov9Xb(@LsJUW(jIK^D&v?uPz`e+C#l7Js~Uj?0%7jzFZM#SiGWU!6ohUp2_L;l+K4(tjfHZ5lL8UD1$&gWlIneI27 z$zpwe?0#pSxyKm(Lk?p`Q*!7}Q#h>U+Z156!lK0=Dy~Q;RP?oy6D#Iw1!A_V6O)`ZA>m7+_|q+0qgS0G<|6obPIL7Cmk@XcS=31aO!o-Lb@ ztdzhoU^{zBMiTmVp3a}8=}arj@uDKAw&1gzw&53)GOwTmOmbI7Ro>|6Y1P_DJTD9M zBB28U>J1b46;*#}cB5lZ{CPpS-DfY&B_Eqb^9bY>Ge#x=Snk|%&%0Z0cWAylG~an6 z{}bNG+7ojB4X-i=O!l)TV@ z${Idb__jQBn?21NWD~cayjL&5=X@&L>aA|@QiGrL+73(H#7&L7s2!OL5uwVyUQLCG zIE|yJVg!Gqjvryb9r^pvprSygXxo4cz^e!mmaO21&!kbG?hzX3PS?5XXplNxS zgRWd6i12SXyXdLk=XgdUojsy9*71){p)8-`Km~u_Uv5U>gnH0No)8w8P67V?1d);@ zz)*6;sG{gXDvbZ8YIR~6qLVsHStV&5U$K{0M4LC@^7=Mtp^=r|yf+;%jw-IHyVxb_ncnxxn3+Nos-kv&#IqSU6HFPuR za1Vbqu+6)Gz82B{zS1LNvt!D1cf&@`7Lb=e()O{zxjcsEvCbH@0iHqT_#Q34pIvQ! zp>{i}{&vU4r;a}7?6{gk3q4PXCahaQ+f(dWkpghDU#S|x<-QiYCs_&bx3^d2274aa ziD~#_x67fLuM0}GS*mJckf`y?-y02==9GU6YSjQVQ=lIwNXivc;n+1kpyduyH-1s% zvAo?*n#(wAc^5`?y}y1v<3JR(n)>+L=3z*-0cY^6ou|vsCT{w>;jQ2U4oM{(ya|Da z)XO1+72MN>XC*}-R5?B>Fo&%=`P?iR*)C(K29lyNW#rrs+b4%EL(JrmWhf=c-7?Y|K{Ae2ft5WMP^*L)6P<%>BjZJY*tK>lnh*2Lfz9M8jS`QUje z{X)<91xixOja%CC+F89n zdU^`Z_XAijc;x8$ z^IV|KAdp@6)Gch{kNQETkpQHmVplLPbb zxeiduij5kBt?2i1R8pwR*Lr<9b)BY}Y5CCmd;+_D+II>2pc$dtxtk$U9KW%bhg zE$+fVXnqMxuWPU-77m1m0CmG^Xh0><#Ol$Ben7vL4BU13f_}7gq@-)$k0WVXVJQ@9 zMXax_vA1`hxz+Bn-AkvBGMe!g))0I<>C1-)hl5{W9T*(@mXm+R)nlLcMle-)29nQ_ zb;}waNB_P-{^EF=e8HJX_1MTHy&%1(rIk-E5eJc?F*H5!JcmHgE%=A1(J%*)t>wQe zGJv07qt*jJ>JH~7qOe=Ot|u^>yph6+IY(lJs0VK6vU*?SAtjZEk5Y4=6~Cs4tRBlM z*^|m-J_8VHddYvED}L4mit+yFZ7I*=8NFSCRe9^p94>p2*Gm*Hc^0ZNThN#~(jp*w zQrC=Daf?F47;kJDzA6D7-9QSjLLmBc0|V9ShELNqcc9%9LLfH9%IYmBum**cG^(Ts zJ+pMl_hEUNT%{v)L3FwE-05O2;f~t%9B(`MkI>3@t&M*k?$nXa6WfKep-Kut)ufhA zW)y7Y+HEdWtI=k;M)W%gB2%}K`U=xu8>uhuXQ1;3+Vu2~{}QCzZeO|iRGWR&r#tsi zp7QpNcesr=e&)HH2-=rwbkhV_p1p^r(XWeg&f22%*HIPH*j!SA`8%25pL9~G8*qm1 zVkCClTd#ldu-LuSdAkdLy9M!)OBRm;5l^~2Z-k&B-;5LmBlD5U7KwW8Pm*Ddy7(|4Yt61xZL~x&IvG3o^7K^{f}Br_5tts^>ZA zrSE^+j#?(9w-&a%dhn|(PoBYlz6NqYWUeuiT+uU>T(Ir2WURL~n9;GdkEUVcm~S35 z%hE5M%LSh(Xw*CY?jc;oNQ(WsIynCB^gpQ#pkHeU5Y+NW4}9IW=m5$)l^eOMPX>9j z5JlU$#V|?d)*S}kP=#J$Xb)`IU2JH-*zkYf^2pt>8*Xf-i)-2|&(0JGZ4Sbt?W6$)Zf-ih#BDvK zaj_G)z}!z`_(}W)^su5`?>ceXl;Z^qckybqT1l(bz9cF1uJi6biya{}`(I_H8akJb0}bRG!U;(_;kCfr7aE#zW0RX_ zaSp!2U31p05J4D8xpTZyx3V2S3%h@T`rasn76IQgLg2!nK|+la3pDgo+H@>6!-m~3 zf1_qCRe4>c3QAY=gXa=*H_|XKv-vliQkMq;ww9g{zk1#twcziN`J;PGo29h@iM1>( z_`!r74%iPs!NAwy5?H%2+#4R1{Tx;wn4yn?neiEXOcJ3}Mybo=I@9rL4QGEHvhL9o zCl-zK9D=tEX^u|EiiWLJvkjW{4ei=UHHpZXW7e=AgC*3+10AO6*z9L>amW~DS{n!F zvMc#V7@VH?NUxu79OCXV20hPk)}h}uPH&H)%X_+E~kM*;Wl^c+tSLigZgfZ-j<8P5dDkwmZuOWTHYq zgd&4UCZRXu;3~!YF6_;UU3fO5o~-n8h@~PN5P@WujGC=O!`sA*gg_3cF#18bPxR8? zm}VJA1qkBa;;0OfukfnNr22~LrnL8$BfQEYG9q$Txy0v202O1+cZYvcUMw?S^sNYn zw~-npuf~;J4ojcWed5IPiXt<^r2A7;TZLj>o1~8B>k)gwmIiQl!o@!Zt3>lXeAh1# z7*-oK8(AA@<^`JBKoj1@*yvAlG*4WzVZ-oBZyA)8(B4yY0p0pRum4&AaE=2j#YvhN ztClAlKaJy`HlAVtu26qI6~LKfLCqYf3mJ-^w0nQ28~>ZVb(_6kjL*n# zuRU}H@xQTrS;kTfrdUX4!^w5N@3H{;G;^J8qOm0Lt_F_#spxH!rk_pbR=1bbl0gY4 zG!_e%UOLeFhRGHcaRxWRBlu%fM8hPxJ<4upHWga0^yk;*F`>ul{fC>kt_6ZW>N&Rw zyTb5kmP#w<&S(C^`ks&<9MhV)z>0Xx1~ueJp8HGZb<%%Z-Gl>JYPpHeV!?cZiK-!f z+*j>>_w*!qEZjhFo);d*knm(uHx!_ zqJ}HEhHJTIZjIlu0hEapJ08uW-QX?ArSPsyS6+ufzZ2`JOu*Ai6`Wy91A<(Z>rAqp z)z?s1!;0Ac3s+Nrj;a?Uh04d}UWxQdmCxvv>#``A_QwyaqL9vLW32E_Lyp^1+lDJo zyOEXrkXlcq&KFtP<{Q~yxp3kM<`a)0ZDA!l{)@lbUe{&mXS(?-TX2_FvgBp|vIVwQ z?_4=%SNtcos94N%!$S9oY3pYtJuREqn#H0YNYBPQ&tuqsESG$sPu@mXxBQ{`I7+8^ zG$Bv;?RP@E0LL7aY+!oAjY=sG6D@}hJ0P&mU?j5As6xyIej%It3|ocMRRNVtq0<}B zYC#Kq>jo`#R}db=^~LV0LQ7vzsCs(gdRgg=-OWzFP1WCd^j`G--tIf^zrXk1dvEVo z**eeN&7FLIR4Jb^32*Q30h&i2ytfbkRzRK;$j&=&@4g#7dVhES?YDQ|-~XTjG>&gd zaU7?E$rs7&0up!N@zupU`&9waC}48--;ds(|3f(K9@5&6faOHh2Ek*8CQ?oT1zYKx zXIURBc|^i)yo1h*Qk%CcEUje9S1|~r7fLCsRTSZWgon|}LOHYs7#G3YP&atlewQEm zn*_e$Ef=i-QHAB}KgnNn;Czmox9?4zI5zUaVB7L5r+vN_1J$@&T3+vQiS4(Gr1NU{ z?Q%6xF1dVf6X*OADCi!WUQEkHGG8#;8(X{U^_3V)Z^2sM_M8}Wx$6{weYycQ%^%ww zXTkY@AWv^$eaj`y-5YT%jx)w!dmT7l1*6~JD2t_fYmUfSo=Jgj!lgqLIDg%!z17BD zLQuhAzf*K`87m3OLi2m&wyb)f&b@&0a>(j;(S(5$x%M^RC@C@Xw#Di3=>fF=bC&>P zi~gyALqo!IKgX%H=cl?vWR zr=^OJ^&%LI;$o#M7v&P>ljJI;{U7tL1zoh^?JSlkFoCWMb0ofq;DG?UQd$JNHR&mT zhfWJVp_rHAbal9cKB2*xyh|uf1HR(6aP%XT#Fu(nK^J4f(?S`EC3& z4lz@MwX!p84rPxJ7Hfm+*4$;oLU@FK5o|=;n;T4Bdg)fzjS?5L^XZ_NK7->-aAY3^ znqj}LwoOS*nk}bdm0NSmci3R)*N=MfoE4w;JmqjcJpS}^z+cFn(LErofU-5g>9-9` zf2B*>eUf_j7yuaSo5;sazfsyDRKtv`3)wE<6T$ja>EOLAF5cun-`sn;;1FhiDeBV4 zTewB3T#@lmEe)gqme9MqTB2xSZ5xrvlX#2 z;rdtoi+ZAr+a4=Po@aS0*bv4O+b;2%EUJpVdv_3QfU6RCX3M3ALH2}3Z&3yleK=I> zBaj)FjkNg_n4-**JyT=|QF>Z`9KA=j24SObl*-N<16c~=o}Tr)M~hoz zebn~oH@Bw0o3zzyAGdppS|WAUhVUn0)1OV6LAXV&;84&;;1{COUrm|-uthDfc4`~+ zn_JP}P1+$qt0n!v*m3`V)r5nqy1}O^8#-ELe9=*;vN4NgENX|B&oX{9(JhcbB^T(F z!T2&pxG7O4tXl|a;rt75Foinu0kdEC)0_G4FCfM$Y((pYG%ugT7auj*Bq$q{Nxg2&F$KNT4(8Ft1kcbJV#;bsf7dtacuBz3 zx&WOES7|7% zmno&@m*ad=wI!y1@CQXQ;cm7tz6JjkPyeHI7(vg&%2#+vkDsG&JMFX6lZ($zzdR0% zt1aB9s?Lh-awTcnDQMn0YUzq?pTvM&ns&bXTNZ7<$XeC9UpnU%i zVvKnG`M)`GCB&RT&abvJzq*Oxmi{*~rjhA&`d{}>E*^J(d!3hHWR|x6U`EDh`C*Mr z8PrGjn;yp5IlWE(w=N#hAYc9h(*n)>Z&*yj0_&;HOEW9b_DeA;(C%MhQjC4;>=r{P zV5F(pgP0YLQua7c{+5R2p^OIg8sPKfq3y<;zb}=qe=o~X`u~r6orfAnoVfpiUH<+_ z)=Odn_^*tA9b!2~OHO`@>8^~dtDvxbtW-IQ}v@{W3;Vr+tLar!L^X{%7#3 z?;Bh$RCIEBAzvS8tbr;Flk%p}Ee1{ei7rni<05$wPq=@^jT}=O|EX!K!=tXk<_Dg_ zY<|B~KL3*j-dX$ny!%b(;<)p;b8_@h6CDk{hu__Q>UXf6zw7ezA%>d_IAZT#v3bQT z{x2>u?>fRd-?WcA54jpk4C%`Z2XQ9&ega%D&ywj+`2$UI-<0kAhsRqj#~<3u6yCo~ zp8t~u;wRtXWOva%IlkzeAGOccFdlpKj=_x%the;zF@^f(XAf|>5W{1OSg*6}2lMK) zRKi?;i*)G|E0iyKzb+Ev9R=KTuRZ2G_%@#Lqn@%Ie5}jw1PBE+J12s(m+(!(bTDtZ zXwd4%-c~!$lL6dX-GWB>Mm!-%@T+;3e4P9&-8Fi7l1&o2xX72x1!oubmC|^W{zAzp z2f$CfYcSEbC!VX51lsGI_uIXG=lHcIL(z(V0wZk~a9>h?*taCdcrQ|~#xDpAQ#Y91 z9=43ejDZc~S_^b6rv`00nAien!Jz=@#aBNZeQ;lj$T+BvVFp!L0Mk5A#uIH1K#d^w z@>$idr82i53c0!pLoS371o}6efnTb`{}r9hKf~bMTfWLKPZ+ zGv1Y)v}u<&GeuWvfq9{VSa_psX8uZ0`-F$+kOmoxtr}{s2;hJDyz46lo0GR7QWY;S zD^Xk+*@9Lom7f8^{008#3cAVM&elyAcg+pIe}+dI#ixhSwZq%?|; zyj4*Z{JP~nP?q>>zHrU<9wEZt7`2e+k6AVa8%o|7zb+QX zQ(bC+iq*{(Og^vEqR2{fhWLl!+t)Y0%?mNS1L^`*QEr5FWKKLprFl%4XKI;}R zcscdbO*WirY5*F@Y_ZLM_tx~ky)9TdQYx}d0(ij?A*k&H8Yvd7W;rU>;eGxQd*US` zNXcR5yTkkm1$_-Bk~TUx=>^pbGTId>4iF(Bg@=geQ@Z(pYkY+yMnwYB83VRK!mA)I z7ehpVq|dwqV52~{bB^URrikZD2GI?ZIn-D2IKE=KA;J=dhyr_mWMhuz3q}a1?TM(2!;542WuB1+S5-y?;Q1m9-J2R@VwyswV;K$ z*Hro?mq3*+lN^2J0+%XV(f0Pd-Mt7jf`AoD-VzePj{hGjdkiBhi3it?TtEac4mBA| zJ{`0BFBu(W&&CT=x4~gxVk{2KUssRPKCNo{qkPUGe!D|^nag}o8 z*`;M{T0Ft%7q&^o7N?sT&c^XgimQPcO+`4(xcV2wV0Vi0QF_aaeQ@AgZ z-x`@nYC=lKW0#n9aFYDo+A4(*26;>|H`9q^C@uo_!vv&%u%dVXup@SAS&1`xk=q|| zb5dz-k25Gm5UQC4Ka64TEam5{mWE+Js23Y$H#WS(HL_VC^pmWWT5nZ+*NSnyNEXP9 zlxG4?>;?4UAl0ocd0p$rB}@l+Sf?Ww(t@y)aF}GIUuJAuoIg&}!4IPu!R9dCK_oX}y^Iyp27=!|I8t*!PGFQ|miK$cQ9Q>(WAVVMigfGcN z(HPVe(47pZ00*HUANyxigh|F>Nn)*Q`%+8_b3{t8U^Uyv}qDZp!bAMz6{dsY4 z|1?M{;eC;Cxf)uNkOz#dxtuE%SlyM(w}s4%YORS%M%J82f%nt+j?hSvi{a=c9mZ8+ zH6tp2s|IiF3NY_{$%K;@bG;hPD75$lOi`AT?c@oWI*ZM{&{GP_uGP^(yNW`Et%qu1 zy2a7F{qC5WIVhE_;Mc_ICAoW?$e)=cI-e&ljlL$%uv_)$3mA6y!VgQ@TI6H+Y@uDX zCHg}md$TZkM|3x%NiY9fD+o#{Yg>;dUXSqR_*TT`1JCUW}iOt z6>Dqjr(@Jr0ElX9*H^8pv#337X}(f2m;*jO`Y1=AwNF0n1fO-!`=`C{*cDeXPRAgB zr_l@t&H$DwR|z>A!)!o%S==CE;X;@d&PE9ZxJu$76w;9#cv4evTWMEX2O%&65IP(ZhL$Pjo4|;$=KYls^|eS}+eU>MEv~m3m-Q)rLO< zm+u*SH`-l;ut&Rl@9pgF?L2z>pxlCg+}>8fw}SF7!4{AmBl%GfG^*WNwFb22ui^^% z>;7k_y>ps$X$tLr@--fRpNp);f553g5^)v>fh~?F+uI!|hS`04`)TWXGPx}d-g*lr zhAC`|C-D_-R7~HxN$C9M_WJgt>EYhq-rMi(ZG}h!o-kj<{d^I{_+I8 z$~-}ZqM1gxxfR*;wWlyT^D1S4{qYDAI?J5`Y=ZNW2d_is-|PxDxo$w;w9?1CJ;8nt ze~AfoU?HLn4%c7i3aC8%m4BDp70X{5Gx%kijD`}1Qr9}z7J8kw4F)z)OA;UQ2+2Tj z+$5(Gj3fC%=sltpY7o;3k&4NG#c1?1}yAu4L1srl-EWf5`e|`7e$8@-g}3C(JY*^^srhD%pc|u z#EU&o_MYM|DFsjF)`^FEctJ#$;g1H|oL2W6)1s6U!12MCdKoND0GQzR_zv#u zgzUky?J##9Iaq!`dp19(MZyrwQ7Q8J>vuNo(Vv22^6yQG2Dt1|JjN-5bJ);8BxXM{Ii*3j_M0_Bj zEXZS!7O;3;n4qI$%A8T1XVeatj?hyZCwaN~3`TFjj=PN{yQFrTOd}Q1mc9K;M2iay zzR*If!;sKupKs$dFVxvF#&WQw!CkpAcCg7sJj&w&;`kze<8qv?`YF0U2I?(4(x^5t zH2WMF9YS4bq9QWL2SCeJmd}9yYn{cTBS8%2Ut8%O`z(A3yG{`{7@N|n<%23Q{sAhb z&Ir}A20_G3mqEEhYoNOcLj27m3)La)NQ_e0Q>P0paDg3f!GPuEU;?COVSup$76}D- zzfQBcSTJUPqgFK&XyL1?S`55jbx)&R;@B+zjM8Dy zH&BB-#*-9wnsUEOuZ`CF^M3Im{=eMRmz_Fvrs&olMO$S6cl2p($K-3#>K6a&lWyPM z&*A;d@-iI~c0zyElYeVBU3|Cd;eTknsi~gzM?s^F3x@<0wh)$G){)Ow*toT7B0jML zh;x&F>h`@zeEPydGRSdm9NVfBnxHRMQ=}!TM3`lB1*z99E>$Uq+Q3pNpjfe3`lR1i zRVLAef0PPI>#pLsmder}s?;|y)a?-cjT8q84Bc6xFt!EKx{k(R(Vdh=o>CR*FLmx_ zmrshT(kd))InI@p94BLS(aI#jXJW%flawfbku}p0YOlP{p`f)`z$n!9iIl!#s}*@F zAu2-!t_V=RH(AuX<38n=eGeeUa5;WDb{*eeqS}aGvhsfMhIa9AzCUdy=rc_Xv7&9^g>Lhk!D-g+u)Hq}gj zSfgKEC3%^-5&q!X1lRI$%RV@C%PSp$LQ)XAoM4{>H&+hLnp}^@Rh2$~u6d{6)g~iU z+O2o(1;Rf}rPNj+Eo_*A$Kw7`Jc5CJC97zRX2A{I0wZ2i$U4=OVh1 zyoC08c`0SCytKY!Q?+dM)in7-lFF`sg^66tBd)C)7N_Goh|oG)qehO(hVcitejkw{ zmKDT_i0y2_A_l~v#U^S?lvGNjANrwHc9ja2S+k3l3JK5jO6e>Xj#_fR1PIZQiM5{h ziq6mX{T295pRtb1cfn>ePivTb)33rZ`gfQG==T+TQB`g%GC%Ks*L3m1?IfXH|0fjWUUnlj z6(A*r0Es{k=cl8xqhZ6v!S(zsj}9IJKI0?;AMyf9+aL`C<&CC%9X(G9%2w&>0n}K* zO}MYOP3`rFnA)R8%wkt_t_r+tUP(_^ZI|67!gkorl2zRG>Wv?LB(6t)y2WaOEJyot zoQ^=}#ebz-S3g94w8GVerNX_xpg`_tI7HVcCc)?xk`&q^`e%zo2I+bPQr>QWt>5f+ zeL^v@dIiG(|0c2Ujgj>T1ul`SQ5b^F5ds$cn?!(@a8?vR{UTT&b)u%?j0lEgsL8IY zRnwxbZpfv={+Ni6VW?BYK%C4Ppxm5Eg`!&#i!mhDwDh5akv>gk6 zQ~#=3XQQ-LPRcaau-khEv<`K=`sk*L)|tS(F$_*Mr@NlRMnz*+#h)ZRt6 zS0?xJayh-E??SLWRu}C<-^9<;o9RvPQ<*u~bJEs!8Ab84qUGR=o1Na3kh1wju?GIfZ z*;dG9p@qO_ZtOwgV0d)}m&zI>>6oDFGYZ@XJPVWGJp)U|807gcDQHBt~J%)(g zVEq6Az|RQA86L|S6+d4;qvGd{7)>@M@R+y3Rka#=T6fKv8w}gs+0w54aIjYdJWh`aBI%_a9Xvnq` zYF^&EQfID{7<0>O6O9JYQ0OW9HQOWl4=|>6X(|uBhz)RYYEcs_wg- z7P<#-jN5)9t8pRD-BIE9Qz`~o!yuZmOIy{Lh+EB^2Ojx4XnjN48#;t=six|fstI$U zJ;zD}A}wf*Di>g>SXg1V!SG=G+bIQRh=qC0qJqe-%#q^aUE9JyAM%ovZ&>T+s1etH z&XsH0h>AEgo3?G7*%lhAY+}F~Oq2?KE|m|Wy2sWOwmPe7AE{{z)^oM)@g4X>RZZq7 zZkspy{ZL_6;7R$+Z(Uqz_`=d?*M4nzH*S@cEq=*I5686RP~jGF&Dn9WoH;Q*ce&DB z`WBkiNn3V}j)Vqn`ofeOp?oqaoezwGoq8&n4lGM2y95nbEA#8+sNO$)-?H zMwM(b1ZA!J2}a@uPD*RHPVxjCanMn6qB$COMDWnERS6CKZscpDGafoE_>cpC7YH>~ z5S#=9y^?#)JNQ8%xPq2r5KAV_rbTZ@!7?O+MP)MoTdYsTOBzEYkB_vx9qNJGGc9}aSohthQQ@|8kYAN|cUm;L%e_S@0+k6@5s zc)W8Clg@sF-#y_g`_T#0ZWjWu^rOrNzd=fe(Pqt+Xo zOI#y2)5EV?qtbbG2@e+yQ_l#M4PL>Z{gDJ4%yM(#z+Y&iSDjd`fWk4s>9-B6+rxd$ zi@{jmL|$$B4ag3nvd0g|2b6V*e>8vV15Zr`Q^F}bt+4Ice)K8)#Qm}Wwo;Rkn8ww@8O>4m z3DuW~2{7{@))5L@s5St94+-5lz|P!h?|{)Z-#p-24l#-b8#9Vqx}#93^r71;Q@_1^ zwm+zH_$;1_z+4`>r3r;_w^_d7vTC$M}EMH+StmaA&BXgV6{YGuhAFe|Pb3=9D z@A~k2-SK*FtwIyco`3q^hl%0{RNeLyk?ltzmqoai>V{cS3Pmx0v}mqh%R}i^IC(nm zCFOog`IpB6z#L@0R?7~n(8gkEVZtOFpZ z4GB2Gs!dh2yX_xn7|G46=7eNrzU!9)9%C({hwAXQ|HdK*O z?x7q{JhL$!+ORgtxjb4%oE|CfJ-blTw73R5MG9@EV>%QC6hrDiz+9G{hMI5&oS>PJ zUSe`)QfpHj@;#&$)#)20lZi%8VygI@xyYcxZ91VG??cXf@@xjYi1EQ%XbnS6x9vy`t8C(W(OuO0B@TFD5n{<6^FrpGs9ju$&I*{3iPW$astwABc$MnfPjkt!Z!z z#ESOglJN4$DT~3dbV41`@X2V3^o91WvZqlmkRmy#f zbUaO7bGV73RK8iCkr(dAZ?Dry;(q=vK?@AuiD=94qt@dNE^gtngmr;KL%Rtpr7F3Z zz{DNIS6pWtXq^=1@)AuGzc&P)%&<70;JjAJ*26iozRU*rU88GC{&Si3bH<4jY}`tvtBkYNmJ#2{Rn2gexoL|UA(;nM*RkN3xm|=wB8{C z;CM_a)A_5c-FfC9lorkJGR-*FQN!Ry9^Bzv)#S=jQYe*|(?p}_M_{%P#8+5h!U?so zh}>84QSD?rNG7g+zDHPYpYcn+Hfl%sz*9MC8Y17`4mlsU#qcsIS&Nj?F4B}`%K8X@ z{Og&SW=vCn!Sp$$j#9B)6I_~c%C=OB`1*{q#|GneHU ztwq~)OEe)8JW}l*nIC%X<95&d(CwXn_bq^Ld(vjWIfdqS{}neK67`wnFZ)mSrY;uO zW`aR*Uy>$7MN_@CZ^y`V6N#GEv`2*PURjS6fsx!uK9>8w4N@EJ-y^kAA-=G?S}~U? zx)y4}?%I%{f~h@L3o1j(S)zDkQQfT90I-7E-U(G+O1UODvZ4I>da%TnvoBJAtnN=& z7J^=WP7KwHD}1<*I#ciA*GhcjHEgwu55gL`8eoq+BE{|U;@E2lA!}y4Pbyla#~xPZ z*6q?p>lC@#toy#k-AR|lov_@Ziq+QLRd?elE`8$YUed=}P44L5#yH3EM*ClTV;fEH z-}u7zpX?oLZzDDEGf4c0B}iO<3fom%-W3Ifw#P;Er6hn*IjX&8TNcvf$RDByh|on z&aLvohfGRmUY`!}+66o&%Z0bn182U-b%>G@3oc8Oa=GQLi6TAdp7fJ{Be`5(;@bHb zRu`mSMZcV?R=&SM+tZGp4!zxyrmUMS1*3T+B^76Lg{rzmyJRFkO9k~%_QKsOocbl( z9^S8MHJ{u0O2>1p?z=)>?SQ_xHx6_=4{|(T!}WY^=W~o}qZ)b}{0H>mA#P;XFs zwX?d%U413m)e!2!PD_k`|DIK)Q;l)i6_p`tn(*RQCQ#`RU6mb`h;Dl3IaKCKoig;e2E=OCpZ z7>i1TU(Z!d+s2qxKYCOj8Wn#tMd(Zkui~qR_bIx1XrE%MhYnDGWFiZ~>xRSid)?Hx zr~eqNxW(iTQp09T!;DTtIdDplN1PEJBH~o$u)8g;CxnL@;r+H+s|Db`23wx+=g`(J z`~@sQdOI3@cu;RMkPq++;68wD_v;(a=gnjN{sZG*Euz@*ONrTZrwLe#;ZW?_%%@C(A zYkyf49ckG(Gs%@0V?$~~zp^sQzw@At5IidAs+E$xZ_CLxU!#`3+d*SfiOgPTWOiz0 zgCk?aZGZ8DV-Ac)ZP&vrrNtQ=t>Al%cwCaJiRhCgYOx!CuJ%K|tJChQ;4L4NBEvl6 zfmwN`UpA0`X0mq}&`%;+fpa6D)9cj6UCVDG`)%XV7YA|q>V1#-k%fFLoqmsyZ zs$RSln)-*^v?WmKQ1LN<*AyQbDLXu{7$YVWMedLVW!mQ~)fvYkNr!k2JI!o==e_H1v+M7X%4s@GX`1s{FbBMm zYo2U&o%lrHIo$u`KhyHz`qgyIVsjR02Bq|+4M_S+HG=+<$~iaad*P>TE*|(f5B6S? zCkG}jE@6fk-&a2~s4;BQySq7uVhLF}>Mom-&fVQ)qho~#|6HYnEm|vw(x6I60Gsoh z=2Aia2*ra~-Deyd<#vhl&t-VosKs;n=(1gZN?QmTlevpA0y^F~82Hq_wf#5D{4H=BLh+GHpU$s$@gLOHGNG zRqjRU1<;h&uWZWx$zU~dw@ltgIXm7BC*w!6h5H>qz!C#1*InsS_iQrfM>T2}Nz?p) zNCOYFxw@>mji}$8h(FPxjglm(yzWk?x0{u2X-K}KG zV2C#)Xa>Sfj|-r+_0C3z0zYCCof#~_;IEfe^vzLLT!Sxqx+vz#CW?|sGb%JS>7u@D zk)i-kzq6y=tHjqB>4d|W_HLR548!3ZoBp@gaYFRfiWDdM`B0WhPKj;PbS$(*x0-psR6)TS$# zon!|W(A{Kb%Q=3^XXT)d(-92I`1>NEH+5|F;D5s*n)gslC>ch>xYL={Gdz3BQouq6 z0k#w*(Rz*)-h<5w&0}ce&y^W}Y|)Yrr9s6*02}H@@f*w+&%Z`_fxM(inb8ZYFkOT7 zK6K8UK3cdY7V@;WNIYDvPv%UxBoIt6W0$d2YuhWc5@GXSBJIesSX@#x71zzy$&l|$ z0No(_vwH}LZfqk6PZAwvsrc@Fo62Xn5nX2@eQu)WIl9>?bCTcu33mg3#*k5q*4Qcy zIuZi7#*|@>Nrrgl+?s4Jk-T8`@BI1v-*E?Er)C6p)*b>kunj7*0_Z588t>97u!DOr zIeygl)mBx0t&7K`MV5{C?fBILvyEtHU&aCEU^=~%OAr@wS0P~|n__szI?z1@O~(sp zcsK?J#G|aP)>~SuOs$iDPVtdsR^QOuMnkkX*8#p`0-}HKbWHH{IR55GJ}d4FK0U2N z@UDrV)9KW~y=TJpJEz#dT?#waVH|aP5xis`*0cD1t1>;3?BT@}B8u6rT!G;O6T@jX zgosHEQS9!RC{8Bh+gb$91a%@1=~UG1l+H%){?UzeBr%t~g|wo7@l0%e&H~(uqw+(y zlsx@%J{m>?GU8r`SKx<*mdMxc<1hHF7@W_LL;y(S0V0?Zpe#LQxgwy&N061p-$4LA zCDtJ`6{O#@f1g%;1SFiG+f^!8>50vTI}C=Wqz`T4Fo$R!C> zlD6f?x>ti$blrNi?y4DY=oSk|-GZ#jv89bKjUN!H(Mt1wL@D$2Dp^gT+GA5EL3O~} zxfhY2x+EtwCw1xIw>lrSSgo3lTDE&suKvf4Wh&UCgW7D;cBTbg8)SHa3Ptmqy|vSV zb#sst+tk4SyNx7})NnL|5{z$TK!LVKgSz&x(ir-3i)Z?@vwnD|S!v(D@x}yyzVU|a z>U6YgUfJ(|9Z9?m`rhbiEPd_#l;(8CL9#&2dt$EH`1sX6RuHOy!aIx&!4q34G2_Sp zxl?-+OFWR!TmCQuT-ok?ZKpSRCFBGtXe+DCpsz303a-NIH>vpBW_H(xkB2uYjKVN6 z3aGzrXsUrWZ@b;rR}RrO!|Ru=Z}!0PTtR+uKz;##kgacal6UX`io3m~gsW3Y3xxpu zPWZDS81#W>mR$iSms0~jGNkPW|I!i%YxMX{M-q33V;1S-(%@&;{PlC#U_-sENbqmP zYmZMlz2n34lk*K)t<%wz;dKSG30*FRp2is8XBiauacR zH01U$zu}@KeLyTaY8;83cpyk)qa}^PAA*d3&^YzgkgexLL?Y6JAEBb^rBI-#*b>sM zWkkZrHX#wfH%TC%TTUM7za|8R>fvA#3E^@gDfnO`Dyhas$s~*`=!AfZji?0pjnWAC zYzm|9B4F8aFDia=LBRykVdHi(mB@59SPBEAk%Y9RMZ zoZ@zcP6{_5cd(zE<^VRj$4_C7Dk2fx;xX3^69xfpNAkBt>bU4dONh2)3W$F8ONo}p zF3oi^A%H3sRSFLL45)_lQB+mG)Aky@q?_?zh%tnm+j)iC`h z-3z}Makth{EQr&mQ52;sN?f##7#Rn%G#&xjaU@PJg%Ax4iLf#Q4&rHYVu=Y~{$$5h zYSN5;pd{!hDWp9_`aNuXYE#D?o^+4TYLdqsjzFKxULz1+5}Ach;#x^#s17&5jzXY4 z3JeB~Xp1-AkUW*fQMRP?jE;wY`D{|Y^#3+3Q*jjEgXSr+nN-A(^;cKi!qpXwKK$Y* z=T}$URSh40ZP0OcKF}BAfP?6vZNUAae4JfpMUhVP(LKwQ`a;=!TR;EJ1A1q<=F>zg zyiE{m8FT4&0}UWqj0ll(1IY_Z!^6*rH4l$QY?N>g-%au{4?csxX+rYr zN&+7cbsU}BjCjm=ipKeS907fnp%$4J94vw_>`uG=e)s(BHyu*;g_Bn4F~_uw<%@M- zCKvN-l%7mT(hNZ)GR<-XElU+d;sZ3BL+p%g9Hrdu-!oN^o#Mq-SW#_apJi!%%A`Jj zX-XgJ#o9@IsP^A8tq+lZw)w<9gm!bOeHdMRavv&p())0=^-}MYW9YV_G2}MX(gTwG zP^-(+{4h*Fq8|xC&q_w^5W(OCnWxWAn0Y4)C4os``SY_+&O5!Me)s6O{PNNnbP`=P zWBhS){1`!Z=|`{o=@b05N54Ml9Dau0QS{?+@0@VH4L^R1C1SmQgs(5hO+UP@s^lC7D z4YtnQtPi(tPY1u_1EHvo3U09CtP=KtZ8$LD(qgq0m((Fw4n+eH2v(d!+VeasC;u}9 za90ynbEzzKpj7C8fz^ay7h@LP?5Mw* z$eFunr*YB1VTiRq){ zjEp8_;#y78GQsYpuD+QR++8*Kdtxo=`a{aS%+t!cMcFfBpm6MH0-?e~jPKKjC_lJ$ zn*!v2bTXe|%DSW|KT2xyHY2|BREtY*-srbDn&o%d6yp*Na193yuntvh*PmMut15lTMbR?PimzWg{o^_f%yPOS3tf8Q?Nw;THw<56D(lU+j^70 zw^TWTXKD;XQlvXN#7eD5fZI3enK3&t!bD+z*)k^shjiY{6%ZI(=FRUKy^M+MGitZ0cQ3^p>CWwRo5$^~uM=q4*{ zr=}r+DdQ+#@}>G_iPhr$bKPV)LxCNCaICqPO18^i6XPVY;yU@b`u>rY)S|NY;y4Ac z)5^CCy7(b8CQw0DF6}Kmo*s3JQv~Q!xuvrl8G^#9(QDMjxQ=`}%Cd9`l7a>~JV}MG z@*`f9e*R>8_X!kIDgV&Ii|u!IKaoQI5iK6ThrOv|0+A{rUVR3*KvI^#(vBa01x`b) z*Yxnx#{1N0S7hG~{ucU%k|criho|MmczNNP{YZZw4>^~D8IcqIO(l)^1Vj$P73W3R zv{FF;q@O^1GLcQ$pKCK@d+B8)z-L3BeZn{9qsc4@Gp~5)90KBMph+H-iUlCGq5}s( z1@c19kWf6qDqOM<`1$5i?z4w~+tK2$JYBQ{=#6BT#*RJUZW-!LgxJT3RS!-hk!;`Q zq_m*i^jfY-qevVIq_&-(2aBOL4bw9O6K-n89`DG?4n#tJ`iFGOlhw?>|8l1#^thfW zo)C}q`??kL9TEASOv{<$B2ay59`N~s&<@LxuM=4C(h;p&r~;t+h}Oh^XLC9&M5LHq zJ$Wl=)sUEoRvdl$zbpIfcICvrr{_zE)K?QFd1C3HV-s_qa}8*26RyL3HZap?6b3HWi*qiJ!L&Q(PyFg%G|QvSg`a6ra^+Xv zIP*bo(aZ4s=vsgfYgy8NHog}5FiU#JN6}Vn)yAh9q18V*`5c||e*?;3y-~Wl!&raV ziT4o?L(y(XBhH&#NJ{s|6G{xnJXhh!>rUijk#J+5W*?ha@@8Z7h{Ide4|^GO~97%^VbwT`52$Kd6C3fb`c(a*EFN zTpduKzstaE%cY^~^z_!OAG;?9g0eJg1hxDjK?~Rs5^#xs3#oYbLYZHCHdLK8dUHLc zX(RLL!vHQE`-R)=x0{OwWCs! zx6Z&=a~P4V8W;ffk4+SEh-lk)OC-4(lUQ2tYdx(mwAtDXXQ0u=*%~d zYF!NONv&Q-Dqm|R=>pWbt0`8yrF@n4M{D3RY^Y(2}fA=m>kBn-sKBpqhM?wELOPV|?wIc+&f`0ok*UkwDD zPWxn}nC1AD%fHO>+iz#$FQE|ponXEmw=7GHJBTDJA=vQ+fhIe^KAzpTtcvypkO0Fq z{yQ;l@3$}V(F7=Bj`e5hY>q*6-=a7(ENNhgDg zOUto!-3WgL-Fo}2!xY2;Gi>$j@!U!>0j5)cI4PEMR%kTQO8j@Jel%wvI6*U9{Dvt_ zd1cgpL+YplqOc_!CQC?^r?t|CyuFq6Oa`M({m5sdY-<_GU3B&XWiIG)`K=@aX|ta) zejg%^T^uQU3b^;i|IQy#Tss;g=wOxl~@6^HBwEozF( z1?*F+LD}~CLFvMG1&n;_!cnhM6j*7H;}J*IyDPA=43e~~UkHJ+NJq=*dq6XPS{K{} zc8n8P5vQD@b1#^T9rDUduZSN0KN$2o61 zDniEnql1-j(q;7b+cBCAWmV+xs)kh|{Q{7QjSkx$_0oRn5 z&c4V5en@}F0R^iMI=}!+!w%R12lV|t_ir=r@0kp8RuSGZ`ub<~f8n!#dM52Jd{$qw ztI;cZCVKTNdP}mBN~f=vy`$IK8+u94$im*zbN4p>mY#dPvA6V0W&K-vF0Xm#&V^0i zxib?V^3t8#UG~ylV8UOzGgbU4ox_+-A*3`OkQ}l~75`J?ZGpC8zlN93{@3sTQ$Vc0 zwd>I4W2|9qDw}hjUw6#VU_DV=^@e$(4}#_7~KLd!MGOyR^N1lf3&`gk5HsV z2H&e8x%~=9e12{>=ruJrKNIeayrLe)~9$?=}%gYR00a|V0?k0O%NSV|?nOmaxc-&p} zN(T}YKaW?W0+in9UKG{o`8hDskrz(Ke51CF4Q3pfzEI(LJ_F7FOEv^sYZAj$2fn2E)-ZPFFV_W;Go0gPxi9}jqotHUA_G-B zAR}i$cBz>;yiE&Je~Ld91m@tMPNtbLIc1qihoFO2E`9_6M|+}>X^_D_hk>6wN5bOh zZVCP*a<*B*k+6N%woe1;HcP%Y-e;BdB-`65k5%nVe2)H^>t zE+3jf`4Ch^##pq19iSc(fdV{Lc#@5BNbc){*D$~Y%rBqt8Hf;h_vzs#);b>6tVM`UE>y%Zoa@E=wx^zz>jfmmC=C(w! z*!vRm`+-9FcP7H$ssWi)H{;4(cQ>x9guNz(v0fh6`1Oa0wKLYR$*P7Ndn;^&hz@s{ z*t*kJDHqS&0WT84o6gY(+b}r~mSV>Hl$%w?^3Q9|DJJNR3OlMtY^RP%JXD(e~9B%p$%q6c3x#NEr(FxaCn_(YFC zd2or%V59OU)*);_-dIhQxFF2`<3$0d-+B1HbLGSa!!RNhW z2Aoaj*D&3+`=)(Uuz@22BFi00^$;A>$*8cT7q%p@O(hRO5f59}3#K|<|7m*Xm_RB| zq+#f@>>6}dA{5R`q9vo*X)g|T+ivu`Bhnq*W9g9e7!;j#((@^lMqi*aleWQp*5unGwz`Fzx%|)kIX9VjL8Ytg|4K4Zrb`lDQse1N!Qm7m!84!2-s7hOMXL z9UKQ97z)*)w->XChIRMovzC?WPyV}tz53bUsN28j3?Rf+Z~uIJl%+!nrgaR6XZ-1W zdN@ssZ)5Rz@K+|7wQE1gXI5SMqIHbme_)G<0o~!!pkOk-$<<>z*C`d3R3Dn5!FDtd znA=2s0I+(@ic_CYpqOUptZPSZ*oT;mW0*TCgIjJa21(KAs0bQs&Z|6$?f^MDMblcC za$tX1pL+zc{ZaIIcV~rK@K0KI;%JAIb@&sV)Q-%(WToZxAxCS#z$ni|806JY28W^Ztu7_ZXK*iRf#bJ7_!d*Hv> zHCiyDj}Zfz{Wyb?s9T07fprcRe+-Zp!YvbpUe~&VS|ihTP=lyd14gQ*CMW>;JwEi! zzx(9B!Ha*-gMWDMuVl^X0lxhk3Dhq}OV}$mgSf{Jiy2@iX{zUM1+B)qTqA&h5s>5x zLe+6wk72RH`fbIs)g+9EL@LCPCehJx=ZLa<^g8_~ES7kFYOPX9kc~q2e=DsFuhHUx z`@o2P`()T%e%cINi3r9Yt6AI!=@f)UpN(22^8Qwe+)83bx4iXgOPT$(#ft&-H>`%T zTgri)>IqiV^&pDg4`DUICJ!?=>5g+d^%O~y#bgIkL6oHq7=IQ}9T*#qzBxdQ&;?YC zoWhiYSXe~`@|!D2qk!1AXT zp7%ec_X#6aUbOmfRL{4@x04wj^_Z(Ni(G^-^x6t<6ACVn84Z)coN`KQB3~VN9VHu{ zzq;_sb*F+BxwT|y+)7g7)Cmw=i=L+rhLyr=Qub82tg>k`G`83kyCm`_{ppYp+pPGl zGO}M%eETBe<32oBfBRUYlU0?6o=ZA{iRXkSmtiyJ`Ba^`0U(z;RW(u|QJ&H%C3=Yi z>9Xt33pB#?mz>Ex%iv44*!5<^k3arcyCOkiOihU+ zwD-;f>5mT9s<Vwk0qD1P_uh$pi&Mz@qbr!-^0JY5#x$;={6oZKlHEs7bonCrfrB)<5W`*e>FLRxaWW|;HZg@XY8-|JvfM^ zTJXA)QE2tqP(T?v8`RzAyP4N*aQfr`ni~kIx7edQuriP~X8mrXmmzgYcX#u;E3rgX z#mSZ#Tdb^>bmM`;V(oP=6}1&7fUeZQa38V5G<$o?Jv(~Iqw%i^zHT`t{wTl6=-S`T z0|vE7f7AekHwcMNeLpc`F|Po^JxdrMs-!STdGEu(x?7`Njj+3W6x%L(dXiAta?)Xf zVp-aiOGVH952=SeTT&4Y!{{m;gUxOhv%R7e+JQMr#I+!PL7WV3%Jandj$MKtwctY*hQ~K zUysR)VTL=M=y}Pdyx0%Z8WaDhTIt9mm<&)irfsA?6|QF@=L+_Ot>aGA>@mw` zktzh!)LpUBSWKv6M#Nhh?~YJw?0@R)3GMvrTtj{#xVZ<@IO7h0NtdpL1|2rBzo}oG z0V81z=p7|TW7S++DOBm9V&52!!QL1BwG0>5VGs25!e%h^LC+a`!T(t{&Q)WSM zcbEq&_p3EEr9q~-sZzuTlvqNdOy7*h09;9aX|2RMYUFOSbqnHXVc{tP@1BTQj^1#XLUibojYiarp7ZH3Mi?2LpRp`K6R6WqdcMmkWx zmS;a278D0qA5999f3i-t^|7Oaqv)YZ)zOp+RdvjGm`4ZJRm?`ME%o*YQg^Ss+~l%@ z{#;XqH3DBWwT=O_fw^wYe|9+mlnJju@9LDN0bRXAEhj>gS?~Bk+7I-eQ1}qGsK|Sr z{T=^AcSmTmiJPO`ET~VPes}#CrO503s0B5Zl=o88DtBEFy^cR_na{J)y;t!9tUzr0 zG&XS$UF`$P^&aRR`fqm-Rq=G-764(+p$J&-8Y&>6j-ie?!-r)~f1!?iE4PnH zX;X4p0VNS6q!KV|VhB>d&!`Jp=P#=8G+GMly-s?7hus>Ubz04$&hHTFUbLz+Xk`R_ zE_Zqfrp#F5zSedewWWS=I}3D+2FMV%XaEaxiw3wbw`c(T3*Dl_X2A>fp8nrmdM$V9 z^}P~wy2xayYTMr{fA`7g?5g>eXYBnduCGxczR-xQ{-uj9HCz7UMw^JIoAcThpWN^+ zO4sDDhCl1|_tJhUyA1~rx2tvcxWSNenkyq|IjBD=d%%wBnk&EFe|fd{GM_B1q|fXB z8_tM&XJGE?1F5OFMn9A~2OUzoIdpmlzd@YkS^kEx5~TpRe_^yGv5MH11F?V&KCSDE z?2sC8^IijnbMpl-;SctXwYhN`_!(yS4<-y;Qe4xPcj16?EnMNa-t@tn>9~p08q&B} z2~e2w+u4UVD_L^l^uc2&&xqj=!w+o%eyN9({>VIb_HAVonoJf_DYEucs zz9rB$qR^)zR)hE}jebe;fAO&BQnsd1Q9bTg%Q8W&%(BE>umk4GQJijbbuW`?JzMsOrXnVtpvMwl{?Ki(pyy&{J z+k&g8RURm`w;|Hnld?^GX4Hjk*FlYFu%1?G8}EHQC*WH}!EN1ZxX9}Tf$Pp9yh#Zc z0hDm5f3m8%&}-~^1Nc18aI#9`5vz`Xa4Q6=Cqo>{cruR%!cYB6`Y%?5z~e>3t1AqV zKiVzio6|`7B!9A-RG<77!aT34!guv|lQ1$AKDkeZsDLKG{Flw<%L7Kn<5zgdsb6+KpDccaESh~PRpz}0gIbrn;ke|Fl)$6HcYjLZ4uw^V$d1kRJffCh`AThn2P!KoFR1>QGL ze~oe9{vDdEiRgbqmm%KkDl=-!+RTHwtU8;Su#f0g4f8Y4RKt0y#k*O9y;yTQ(H2Fk zYJtItR$*+q@Nab&sMm=D6vp$Y#t~~RWgeKt1KiM^>yEk7Es!b+Xoqw;p^%s~q|~n^ z#r}4|4=HAKZ%35H@}pVl3w%e{*HOE1e|a|0#4ghI7IFG#za@XyW$f_4N7KTw%;MhlfL%d^(VkzRw?dAhtFjJ01zF@1T!!vG7+d$6?h=)I8cOYrL@{JCg@ zC0Aqgx3D3$d$M>RcJE8Nll|AVeM#Ss7kiVUwubh-GpW7Oymu!3&CaAdSB8i;w!5=f z8{T`R?o=yY-z)X~xQ$<`wPL(?e@nFyvwzwxg@Zl=xU4&>wJh9wqyAsssPD&j>W_-< z${oe)WH%CzRP4H>!0q!NaZ1f|OC4lz2Y+A=i$^fYe(IX)x0Ux}%%4z?N-1Anu}V}k z^LnVyJih0=4B1&*+oXRw^j(+H@ngvCV?3j!^YYq8G6W{WkF#VrD&VCsf5H0N>u;^C z^?DV8@fZ1c$@Xup!5DQJ^U7NQN?>7)T-aqB}r$qlO$M6{!B9Q~YV6m6cESrE~v#H6=zv!+DMh`d?vmT41rZ^79~C zWe=^2?ix581;Q-sc1x!CGFeeaJ2sc73EUeXQa}EotZblDfut9TO z@>n0w#=7Sx(bRVse_2IPa+Wm5{AKPu^pb?luKh=AjE7e z);1+=!dJp@{9|iM@e8#E%IQ&DlMRw2(N`e7m+HdlYDA@y2`TZs%PCP2ApcEK1?RUC zN#jQFO0G0wdYaqN=u@Ng94-w{pqXe~c^tL5@fJ5Z6f`_?f5G~$3{#US{Ak-hZLYhZ zLpwvK3xi+>0r@kI;Z#4C2jeikZF8N3!GWGoahv81+_(my5I2?w<8UL?eEHWh>xpPo z)?-VAV_PEQnz+Rw`H2IvAm8rj-oWZAts$Tc(+R#Z--p$REJ41&SdiXQD_MFcmwoDn zHC-Gj6F`HNe`FC0B;m8O^mI5&k0yw(@ro3pevQV%6#3lM;&9is+^0{9d~+a7!$Jz0fAKeC7cL8jL)t*u94c)C$rpRS zumv0>+=H=1kBFJt-Ysz3x7erzENK~OP)V}E(3idWFZeFS4`TeE-$sqog>4>0>MD{3 z+J=h5cx;^-Eylwi)g)`*CLol{w5fz+89Yefva|soTDD=KS*}0aX1V@wo8|h$ZQAyy zCOf8$f9%jl+b-b}(5Q`bf+8@ZUW?m*U;-Gjje5(@zCO0ZX1-tO5cgzHcD9}#J&XFI z)R^d}EW1FsQCemxe9KZaE;$u0ntnwIR_ADx5Aa8tmtgshGlLIl^ZVKtB|l-TWt^gU zn4{&W4|wN>FjV#-?Z$ud5>cmj+Z80WY#Sd`c7nF=$d}HO8 z6c0;ZUt7bkRWF`^#V0kdYLC*BnS7q+z+3i2{d$)7&nT-4rbv|SGO-_ih+#d*PRiUU ze-o<>ScH^CM4D@^OUJyu=L7@YBJI#F?qumBqWT@djj4cNoDtj_n|D$X2fM{+3&%Z; z3=j(Kd5a)9mM>C#U!NUv098RJp}eRzKWSWZ`2pQEYz))mHp-nS#kcPB$t95gQ)c*~ z9GL7x@i3i^@Zv3wjI@uT?bKl6Nl-Lze@5`?`xFMm-KPlSYR*fvMM*{pzhMS)QxV;* zyu=7e6p;!ByB7b59?z6hA~)F=c0Gw_{P-P^(5YpdLtRuMHWv z-%wF=LvT3TE=;Gkib?DaQ13a~7m#!T2AoTqyJHZTe8N$mF?~ZIyjOWnvu!rPf4ri4 z@J~LZEv1D$>fsr|1CmnNoIFP$6KiH_eJEv{(XV6^5M5!#@qlnT#JD)zx$abC08IBW z=OW!bc(!%;=<&|}=HB?@yqugdN0Q^cF=dXUZ!vwVdMrwt2ToqIFL9>>aJG-004(67 zaF(IfYsaW=qIH+4!qR4cR+Ymaf0e)~W;i32&EBLOk}ju{qU6Y1z|;T9pfWcTT!V6& zU1kD@`5a0FQ~$+QDrmcJ5=c}h8I>uq+Dn{;kN(RJcbvzfBH2?nU{b?gDiM2vAf6WP z66u^I3CQ?y)q85xwXw$@JXeb6*JW8Iv9c>ls536w_IK-rHjz8%)9EQNeYBW+W8>Qvj&)*(4xVai5qP5T(H}fOn(`X|bM^NN#|*dd82)@+(Qtfl zM>CJ)e9^#qT^mAd|L_H4epQtVyGTZzm8v66mb-;V-fE_K;V>Pfq<}n`p5l_Zp1hg7 z4lo&&0z}z!KKQ~HqC;LEe{)(^Hnr&@2ME(kfi@4Lb%7iNO^op&vqyp(t*#P(*@7Y^ z`Us6u&np%?T&)^AfGa^^z|Opx&6S92c0$vdVe;a_z!hIB*i^CO(CI+t#e^FtmNofZ zt-%kRE10bkwiI=8j-UZTQR*}qv!HM$KT8nLVwr)syD~3*m8BQOf7sOz^4j1i`}_2P zO-=L#V`pVHXYN+@x^4q^zShcgg1QAXZS+f6X*=bvILsn&wT`QxU4)5&)6>-dwR zq@^SXLu@)KgUqB*f4M>~cH(>-aOyHSy0{w@5XF8sfS=`~0(|W0pgnlf`U}8b3*vl` zROyys9-I|R@D*!ozY%1$Bm%>+0>Jncul*$%K{{ZB$2(6y-F>=qeDL__aC`5or_DnZ z^_GTt2oG}|J(~xT_suOpLJqKbaBz`x%0VQTNAhTY>)^3Je@?Byw_?op&epaMe+dc? z9`7A`qb}_&C_7`^2U88h@p&~fegaF2!DTmP=Uw&GM$zw2OYvD$?^{jDMmsIg-BLh( z(TZ|4q|D9>*hjYWEjep7(MOm=OSE#xUdx_qc#}g@p7SNq$#e)d1%-SdC-DuR*)v@c zP=H2o0*TQ1fAu`>W1k{~>YD{HE#+cC=Olw3XOWoNogNpn#d>^N`o=hqUk=}Lw0(L| zdv^ez@*()7Wcx{Zx6u>=cDo4EA?(sUe27VnhfoXM4LS{L)YvfEuX^{kz%Mtk1)`{N zE|MIg6SbIFCN$5i-68wMd5Vgcw-`%Y1vyXAe<=>oe_zQGrr;%srBb=RquGk;sKsPO zQ9J}vZgT|xbHO4ggYkt81_0SL zI5N9qEHeHwTPa)1&B&I2)D2@*OCLj7e@ZXoxg8AmES7PiVy@4lnK0zz16W8Zm^z1P zuOIdYe+r7)wkMW>NqF0YUgouAz2^GyB~>3qJWJ^~$XcfT^9LO<7Giz3zMq=R=`2lv zJCaRZv)f2$)8jhqFS&vI$nyjY%yj}h$%mNoHEyu>9^g59966;Dp#e?FyU}?~n|l@3 zx;=3tNC(4kqW9;)P}+6VTCsLh_N#o^!h>Spe;REqt<1K9Sp1r4M9TFlbJ z2yJd-7zvC&j9M5TMXk_g6tS8=iduLJqXPFx5q`>03g(|;; zK3r!bar<7}Hcq-fuG&;|%_tk;vaQ)Vsgt}6ls10nC86P$xe#X#-=$!4yrk^}UKYX^ ze+2|R1BhWdGUUn8nXdDNC9|+44;4VG?lgINN%EIiduuy+I3&LD%A8j7jmVeTB?~fi z@~)QZ+N7j!?bhXXwtDo?QQj?EgK>DRk z-*qlHXF~_ma+vlhstzAW

      ?jZ2x5If9ZB7=06`i-g@$6@2lglwhnjp<)iase{XA6 zd;jR^S6kn7;KwW|XA79j@gl&Y0jd&>Jekd1ohEhwj+8E#D*iO^j_$ke+S3uVvkDz` zgg88UM0YZHGi0rW^Accqf>d2(elDs3b$;@*49_`7&CS@vfN^LI2aijW`V!`}fA<#> zZRM5)v3iZXwXx|uRyrFVlo&qZITvgYn~(Ge_Ax$CivcnCG&6YXCsV_VhSxcsbe;X| z;3EBX+)+yWZhe{cU)N!pJup~+`0BZ-bfDg@*9>%ntQIb>l_b9jLIsul7Or|3sQUYS zvNn{gup$7EBiNCn{8(SRH7I`Pe|OWM|>Xfq!}aG7n?drU${#p4XaB`qHDhcJ7Bqc0G=fxC=WeM}6e#Vir9wBPXY z$xM-+`xV}YC_d?YOPtld#6vktdjKA@emXT7{2%WKnsf-)2oW6%z#u(Of8i2gLaNtA z>&Ax6+bKvxJ=d>-2U@ePeK%l%DW_w|hZrLNX4DJu`X~LvEG&|vJvf*(Ezuj6(^DsW zJ(5B$M+N+*CDvU*(d0-NTv!n|s4Dmb(VxB!l#*_5QCoT(2LRBW{Pe_h19g($deN)% zYg{g@@q16zLL(jTYov_Sf0aec-LOa{zm{%{aLImWEqdSEA6;`I@%aiJxxkSSKeBmw z4hJSd*}VN(gMakqk92(Dj!ef890dg=*V`bRtoS z<<1`XfI`SutE8hWhHto`eHiDC;sQHUF*l=>3KSa-%ktrNn26)3F%AV_w4X= zWrb`iK9r**6MQf@xP&mkypQQ-u~V0jV1r%y#n>b(E7tMri{E_=yv@pJ7nSP|-6Z+` zS(%w^3>F>M{vOWvf4_Rl|4lx7_9-3Ra?^(oAL@^nARQODxn81*SA26Jj;bpPP%yin zaHVY^X0hIcZ@1Guc7r{$dqOETxTg@21@H#2K;R9HRoUwBIpHY(ooJ6tMlVyD7_Y~} z`K8=LILXe^Uvl6H2j4SDRPc;b4x3;<7-evaoGWltrqqb7e@&BcHp&J$MT@1}g8Uua zs!vgVe279#IwSba!TwC^aFm{Qkm4lzDvPkcL& zcJ!SB3+b3k#=ar}IiUiFC)!d2IWRR2bEFSZ*JPFT1#Yq&07G;jau~MW`5>Jzi?J8f z_DOz@v$ZReAfXRez0kmte4-L8Q6_;-B@=}uH;e(qx;zDcqzMRJouaVt#9Bjszx(dHNrhuW zao~#74#nzpDp;n|W+bkh7z+)}yzwasW1;Fuf2EaDqobFY?sBFhoKTq|-9ZYGr3Ouk z3~z8{1BY=dGZEv}A_b=7!X#{zWvF>+&;PItb6po$zLl6rXR1dW^K-o6Rm~{r!N)tF!1I+j*_gl1(wpLX0S8&N zf92LuT2N*HXrGkz0QN%K^WuUG3uh)zjnc(BZIGc-Ad_7C_Y{r>_E~tC7-+~bWSD-ikSxwJYyK{MGmYM`4L+DAcj71IA9W-s6@!<$**|s46ua-_J_f z(dR_2uY0;Zp)06ipj>dcqFINsDR(mif2i?pVdL7(lcCHm*a|f4DBj;YdZr4tOL9VU zsHfu=B>3+ng_bcoDKexVL^l9sq)M;QYyd-$ETf4kTKqRO8>X-ZLa;vqJ!9R=w6m#7 z@?m7f1T{d{G~M2*-8ULZ@+hlCl#=;@oo0Ea9&>HAHL+Jzz)EKXFb9{8sY-}-e>U2Z z$iUeCB|ET=(G(R@vLyH`T02HfT{w?$GQ9v0p#p{m4hY0@6uTm!m`0iUq_06=AOKPc zXiDHlq|(R$8{mM6kzb55QhJVQrmSO2hYln%PFFP^V9_u?ot7zU6^NQEeY}=L^s7SH5QU(~!zy8mai|`k(Z;n=g%fbe z4u=>*mv&*)V8`czhuxqUacu4qk3eWk)iBU#7aBp;3tDV9qo_f5U^TKqaoUP`I??P} zuAmBwfEb`Xov>|5##hdnc${jwl~d^rldum>i}U>VY#+|~GNNVniI9}|e;xO`zL7a* zJ2K5nA(bxj^Qwcm0f2Z#7ole@<5Qpts;b~kBeh_2ok;M#Bp|icR3{sOFk}O^bTo`m zQOM$)G>qHP_z`Cn7-a(@n3p_gJl5rM+?P5{yx%Iz^pe_tk^VGm_)BhbyiU*Y#7Nks zYXV91?tq7&C_#0)_RA;-e{d;?^#mG_8W~e!2E^Ai5ZMLjPv(BMYHU}B7~+?7G9bct z2Id`2956pYct`1McZt-7fbWf~c*rZHJ6yxP^>VdHYRGHsij1jNbc*^hvI&-Di6ut_ zMQytjCT^Jy@~MeV0Mg1zOp6<_ZEp&Cj!51m_YK!uL>hXfL!_zJe>=%0zh;O6bwwp{ zvSa{I1aM_^H5z2F7D7cf#8`vM;V^_@!6HmyzM=w%d^R9YQn=@juI^!~`z&7?clX9b z?;&{DnAEM^Xm9MUH~1Z=oN|5vflIWXaid@1CM;JC=JOlX1s>UkC1=g4pywFlr10RVgLPi8u z>qUlYF&kYr_2?7O_IMhp9g@XEomOdtHNem*(9Z%oHi^1ufAFb`#~*l{V|Z#Vss@(B z-U1pn7SgaWj|LY%X=OQkoQoMl2lD=0-0I+nt^ERY&`7pOy53(wnuiSaZy41)z}Tu0 zEXN*&fzlEuT2rPqpE+c#LtR+32Kh6GQ@1iG+Wx#m|D~OtxJC(_*vT**>JH#6l3OC8 zWr!4eLSQYye;{zlDNuk)FUe`isT(+A$N|covy|2wJ(%p15;CxgeMoSP0NKS$~S1Vaj*t zFqh3%hbE?1wyuO#SRP=qQz6Qc@Bw+1Y=^aGCgM&Ie+7(GJn=@9G&aqlSxN!}oIX9{ zkrrSMMx}>wG^u!q*!cvnv2WqP(D;%3enpPPoL#|u(cK<9VYZsHP?JRy8)#LfCv{-$ zqHbpgpMLe55f(~&%%^=xumexC-{#Xlh6(}Tq*G+MVUnDl_=6hmm?Cle<<@W2l_%+f zp%|q>e?Af2q|Ih2>d@hChR#}LgM%2RlL8#3?C_d(Jf~d!*~q}gkjTW&A>w{!hEu3%ca-Om^fiVr zP)b`4E`bi6gFnTZXgtmo!i!a!Bo4<^j5>1d)>X}0z335+jO(Z1y&K6io3_}ue-k`x zmpwlxz|o^=JtucJaFvY5&ia^}Pq zVCLGiDmXgDiP?7FWqvy*s+nF;szlcZf)aTt3zj<~dt+irwZMU^N7{7d=#$cBGVZzl<8C%!+p8rf0bE|n@5F+qcDx;T{Gi!ky1QRj8J4H@LXAV@=Xd` zq2z{u)M>1uRd~N*AiE_ERB1C?Q-}(Fs0#jgsE@7*g~eyL;*kl?uWLZ zF1iC{`3K6yC>K${)rFqx;4R`UjjA;RAG(oMP;2YiZuIE!*3(aSqQ|=je}{Yf-%t>j z?w0thYD6JU{8k7(Kr|@&)AQNU8A9PggZYrN)X16_fk;Dr#Fhi7(B7uCc`F&glL3K$-)Ch+k-FA>L04f6?1pMJe52V{T}=N+Wg zU5l`dwe`1G*EUwy-q_?>f4W(Bk>l9dziBASj1sPzrZ&B#j$(avLr_3?p5+GYM%5pp z9)PBOV9UncB^v;)h8AzFuD!Fm{-(CDR)Lk3N+PS@l3vs2g52#nZfej1sq2BR0MVbL zxwKoowP%(R4|?chSb#pMW?76;pn)z>WvO&Lzi|T{p3jGqxyI4)fAOq$MBMRl%<##o zw}}Zgc8?8WcI@wyH$NudzWg}0fVLr0T#H?DA2yQYSG_(_n{A2OAWR!=h^ngIHBkt& z5)1s}Ldt6O|83;4FSKgGKW;!`G>=A|;0n4MwMC`Ve{(UCl@&aa7DjxL{zkybPY}<( z@mYwc1CnEEd)%&Ze=B0>sI-E&1)<&o=c{}$IjjD>osA~x*Y=NZ0;Cb7j(ye6_9uxicQvKV`7!2VYtWQ0{_0z6tE*x*})u6-N2V;e?0eVfCr?S|2wzZ%L42 zANUyU;$z2t@Qu8q!^eC32Ygp=@9E+G?#D-myL(R$z}C}>f3Q6xi22Es_AQ%{XO5*M z#)WJ~&kB=OVZDPar9B_Xw;piFAsO3u>BL^4_^c527(Nr>ffilKaOv>) z6fwyZ_wYWA6_&j8?H=T-zybR}DHdprk74LtmQ?qg>P?elGU-uHNCSK?0jy# zI7&iSVF3)hJ!j-xSwJLFwBv@}`w`oG+AZ&%gh+7To!~A6p3Hr^b+`wI!?3w4F6c}z z8=s~Wf40q(Vvx`T#DYYIU^24fk9Uqg--1^m9`c}7&p&*;6Meq5`sUh4`&(Z;Tm6(> z+1uc2ADawcic9=v_0_o7V4yVp^<5juMFKL;hWN1iRm6b4~O8htsF-G~tKwlaR z1|9W;Y1o(GOhcjLBoCTsK}ikq9Xq+)^A*@le;FUm(-4F$xt9fl6iZSa`oHEsXeKZp zx^3MTDT32E1WQ|p4#J9S?&nz=iRawE*j=K>r*grd7`kzePLvM$XwNWv6OIuKWIwa) zoMeVsRc09TBRQT2n1Y!?i_am1TLT?JWxOaXLp6uF!?6(7Lz;OM*lkyTCQ5w}JNu zS>-$xt5&aT$~`ytqSK^cKs7{PQUGlunoj^8kt=sAGn~CNteU7tl$83xtV>H0f1=Ch zRtNRoiZhaLhDY=NLcVh7VvZ99WmjIGaw8bi+3Dg8krKxbi+LkPdtK)xZP%r)Czu3< zd#4@JXOQG|?%C3~sRxVWf+^Dg)!>D!MneWO9^!TMP#J#iKk$siIn$9)WXuq`pd$Qwn?+d&OB3HfdptuKZA~Vi|9PYy!69I?Av!}4#wuoF9Cbb}aCfz&k&41L-%b_4w`-_s z4?+?aTM}_y8`id5TjKb4+p_L_7e|yK4@7#Ad z?fau&c%q6Etj#sjD#mDeaR&k%1t~H0$fWGIex?^folD;*=?IN;#Ashy26Q$TlV}^X zBI~@O;x7ywhFE2jeyoAENEg%qED%N1&+smT-w~cd*0ULzo3(ZJ9ELdqLNTUQ;$Zpx zVu^30Bv#B=IqNaRR^r0Ae-X95!l7JZ$dW@PNt`e{H(g3@B$#d>gT+y?!Za6~kdzw< zm}T_P3(e@k+X1#coTLD874mC}+1A8l3|H!fu2*YFaI3A6oO1Z9L0Iulo1|HHE~s9b zaHz6hUa`}dD9=M|MZu7}y4=n*^4E$~)z~@?{lf4t%@PulCVeJ7f0mGPV0r-;v+dnj z48oOP7@R43)fM9>(eMM>gCqd?IA>X%z?=Y8z9un#x(iKZVsN%3p^&@A3^F`_v>Jca zX-q9>P+O`fZ&NBFbvrv#VTHEh?r=D>A-MO(xH$Bavpu8#!WFtlzdfx66 zt1J;I;_M@v_HS_;Q1XgGvnlC+T-Kml+NKPfl$+W zSMEJz+SYU(KnuNWY-;FrcE@i9Feb7}1o?I9I<+)$3El_ve^nkF-kT{4z6ZK)pN_Nr*;?;#rPD zjCJ4*KOm=#;jd{qFfG=Y^p_$ZxX|!(#4Q$*#&A#wZzvOyQwkZ?(NUGgRvmPLVPYCy5n zVw;V%wY6%DZyY?a>F49&bvI@-;l=m%0uGm1o3DewU$I@4O`XL>6p`uY12mV zdr=dne_tB#uQ}DZhf-bg+gFSAQtNbWIHmDoKR{q)L$8$;Y_wJqnbabBEFbWp4*g1jn55j=X^S7 zM@(BPp0yAbdCQcp!d?bgHP;s7MGlAYVi%UefBSUr(87JcO5Cp!vsL0|cLvbC+h$p4 z4RQ7sQ9zl>!B*`uPi_E!5T#8qoMZ{I^MA zAG*HMGDT?yD;`*zlZoo)#>7XdC(-T$6u9z|Z`Q&fJ7?GKlA3> ze;M9*TCk+gjARttC9>@z0pjeroFK-H0}u!55*)^(K;9dn2TJn* zz>Cl(8%s9C=H)0_Vof+q$3J1j#H1+3f4Kbol9K!VQ_!f1RD48;XN+He-O7OW;OM}C z@t~GF)MY!7;yxy*bYoR26TEfU(UWXAc@X>Gzo2d10~9j9a34*y2cc2R%&dz$E`_|v z_}MY5O=uc^2d-KGKyZKu1H*OSSbLdkse_~IEH#|5MF?T|>^>F27T3}GMs%dEfBwqR zn2Dp2z^ua}!}^PPyU>YfuTn<`$HFu?YV$d{9^(B*=;Q05k8gxNz6l?RgwS7p^|wMW z-VS|yC-m{%(8u+)K;i2ln#R z3XZ!rGb}5ExYx>*kZ#0LNl&PZG9v~U0U@R*kvG|z(kUpv+wYIucA;I67Cx-icI&wm z=?sEO%S|EO#{@gM%w|UZUggJ}Bv1^%I;YWOrMkJQPYX&VO;w@MK)3JsSrRfas9{>d z`8IDc#uzO!!lP6Qo|u3&eRrpa@u-YW}qR)eA5yrbtBD#?tfv4PUCR$YB+>;jy%lqXmz-y>C{ zrc+!rsGu*oF0k;DUMRJL6ZzOIL}OT;=U(XUM7t&C7U2K6n94L}I>0S`?&T zwi8nkw+R%he>P-#lW4QTj+?ot7ezWsA-mo^Il(9vXeRh|JnQ3-1O-_z2f47^F0R(t zRhGdT4}%SzCi+4yp2D72ao22h^8FsQ8HJ8%X^Il+5#@eb;;z`vjSmRpyZ8zgJHnG{ zb=7`#_7maeL$VoA)g*H3K+3w+6%xBIGc<9<)95Ppe_lI=XOQFhW0KUtAv+&E-q}8S zva`>tGFFQhPyo0aFyX4qZ`=FIMAZpCA%iAT(>pSp%d}g68m3oIx~%6}UV?tL|5Bq_ z#kC4=5c!#zRui8iY%~>5;zfXsufa0&%qk#BZK^t~J1K5MV2n zC^qXh>Gav7LCTHV`6VAu`X`wDJs3mEpQ?mj@2=q+;W_YpJm|m-Si^teKW{eqzOJ1J zY#9x@3}Df&{yiMF61-bhI&rABI44Y8yNY99f4n(Q2U&1JS23ZM2L2kO5ZzDT_@BOJ zJKC>0`+kF9Q_|*)Ca%E7Hd^_pwtVnr&v!QB9}<_XEgHzBDYeJwHFmfE)-TlcSRo%; zBad3f{`n`cMxVf^stVwPhjvw|RY$p|AC=$wi|DW)bC-}@dz|+FV(-|S+oxfF!yVrt ze{68HBMWRiZ^t;wHekRQw~aR(m$m5@=$etVK!JUC^oS}ucABIsZ+QM1+mdX{vMkF| zmd$Xc+YEuQc=WRe$QNCKED(sAU$4OQSzy6MHgk-xp+RhD;0#^v&@G^kz}rsmk}UfL zW=AGjLiy3+v6h5X8Iuib-$DY_kFJhSe?fFzfi`e)a6WxIg(%=)9TRlDP9rZ0L9lD~?AjZk)CTC#+;SC3Sb%Sj2Ui0MJwWB%h(fd~<16BQ z$A8?!m$kl7;p^1;qUr8vlm)wJ_R}bf~5wRS>w6n?wS#N^w6!{ zH9n;}BwJnQVedC~V*@O(Za>E3Rvlsf(0`r~r`tR(2TsbHRFh(GX2idcJcD|f@JW#r zFaWa9C0)81sqyUm{PLugh48Z+A6DipKeo%1)plCzpEQ$}ot9ATS~xG&)N78R1|?E! z<6_llEgv+12W!5zWMLE-&xP8U(r>97Vk@2;orkPX^EcJ^bDeah6;s{>d#1PWjTg*<<4pa zL)|b4l4OMzXrv4Z&Pi0UI1*UeK!3pnX47N7m*iG7h9aWsM?x2qcNhe>_FIQUrY{I2 zP5`~ih5u+B+(y|zj#qpqe2jQ!DfjgtcLJ=)vO!@)Te0+ZT(bcz~ zo*cd8i8CDW3tD~!^VQxrk$+5aZH0I;YpSx)Gpl{)!g0WY<%`*^a0RDnA0AA2@+Xm& z_VW#qk(Q4f1$&{@A@At*@ld4jEGDwS5>&0p&QDk(R?2OAMCMSWeoyw?Fl5TRe5jqo z{Ypzt%V&T+{}Yt3=Cw)R#^<5wD<&G|8IUW!=;!t=7k0~qw6v%DIe#CB=c0(AMA36h z!gE0st)>$zfM0VnxJ4Uq7Hg`R!~J2gg$?lt{&SP%3!HliEhyij_f9@MJv#sR>f{1+ zm@W+mrxzc8IQZy9Sgm$0)KkV1HD+Kmw@+#w;jXo|8n!nJwloZOF~bExTiah3s06p( zyw}OV*Uf_0&W6|8NPoAS-j0U4uBN&*Y;i)GAroGz+uOSA#Xh5*W0iY?l>$dqZ?!dZ z&D~v}zNVnXk^7@08L6vCI=0h9!VZuWc%ahjH4Q`=2C;F6jt+wo?<>8#OGX&lb+X{Y zz|+<3-#xw{Rkq@NnW(A7vW@li!biJ92M^8PYU}m4;6N#gbAMQRk_b(_!xlR$FYvvW`)Qlq~0j;(WC7+ zYbe6f>*e5NwSRSD>m!aqrVqZjckD}}X9K^o?}h;4Gryhy8AxH}Yo6gVHcW_VzkDmy zIogOglaBErs^o@cS2f%})9E~&Nn|XnlFMr}^3@+EIpr1Y4Nm)lm{%{Ru|yTNNB(bp zAB1*(Xr_We_`9!R`N>c%Oz>GBgn zXlWre1mRSqGsxl2gg%TIh3JLG$t}OAIu)ujy}zb)YK)r1x+RiwGh^v2;^v`~ZOm{VnK^8U;2thsAp<^NS$n_brb7A*%(S@G% zXPbZ<>WcL~9cwALhoHQq8F!mVsp<$Sgxh)#R)0MPTXe`D(#^d_U+_BrTjMRGhO~cWQyd*0fm$O57+~IaeI(r`Pg%l}v_rL=Oa%(`lr{qHpBsdLy0L zeK#_Lpq5aEW&4Q_NE+b4a(t0Ez+}3X9_T#&=WY}#|0S=s>Ex*s<+QiblBd(*WIiH^ zJbx=+3_3aCuf(ZMOJvR4YHM1^^{1-v4;4#m%fPWREB*|_Op^R?*)n!VN+8!LvjbNG z#!|=#c$hW1o8p(1U?FW8rRSKVOEXz~d4d9lb^cg!7?b#}FV3J?Miq<*-13=wnV!w4 z0J^V~*?ydRFKUUIExK2J)N5$dBTU*C76)55-v(>o4&R@RGX& zSmdqDMr|$PEwkB{N*C|3>d+5XTlg4CJm`ZUB}US1VRa*{TV8&lB=Nt<^k3I#N`aWt zPooO+(lNUaM}hAp<6Txzj))O>#_>B54V_{u@*H>~o*BCbTU^B}JcT_Az0WPJEPu;2 zj&UWBII9q5_(KUAQ((eSS!9aOdYhQcq}YlDscLcME+^qZv|dfhl@6=iN&yc-si(CQ zR#yBk9C{jmlu`VLVV30}Vqy3s#+StCQK@0!ag&2O5ybE#2aMtN3etJajgO$4Z^pb* zTICn2HXmj5Zb)nS_W8z~>FsvY(0^$uYC!Xqi;dmDc(vbKwklzzV-tm}Qa+WlAk*tN zpT9=MEvUmqni#z|cBzHnT3lJJeLo~uLkbEQL|laZ4@!7r5J0kM5fCuWnio4vKOP~- zRbD7Sv@I?z{_;Bzo@^}Axn9+}+VT=SAFbxm&rX#uDpi1jYMr+MOw-Mz(|_iR#x{#; zn=8P>6OPNJhve@f^1f5?3#s@ESNsAi(%X(Umt-)nfU8|ym^47wt}fiZ3SA2i4-W8A z>_3YI^`1bOqKOZdGuW>FVmXoR>idS++p>`o7jyM?v5PmJdO25we8qO!N*&w(gViy@ zC&?EiX9S~9b5_<$4eiGSw10-le@i3VrzsGPZ2!Njk?r?r0M^#X_Sf02zJ_y~ynv&B>w9y6UTU&NG9OeZ)+>dfFim}yu3;^>5TDCJI zrl3R@zz9A6=5;A-j@!~U#()mqgE_!o{5qH7`ik_zfz_eQaHJ<01%HwHH!o5gjNWos z!gYL@rLljlremqVmL~aeaLRd9dVD|u<=!raXY)kcUjHDS4$>Uz9hR>z^+dC3RSTrL zMg7=aR@Qnk!-zV6z-qsqPmufQ&POv%7D10OLV3y+`hVflx+Ge*FRunyr$;pA{Dt_R z3H|RaDjtHKF&yjdfPdP)c5P`DViWzT>#O~8;g%a*Ff_^<#hfNZ!u+YRzx*IMO|mh@ zjG;Jsz^OU3Vb!v%xBcb$MoV2l)_B{42YBoT204IPRaeD3T?0`q?&Q9MJ7-YQ{PPVfF9UqrGAlEt@N>o*0;(Jt?KJX7+LWgd=6O1QwboO4F!V$~}rc`9Xh#9;}$cq30E?YDW2 zPnp=8#-F5L9623GZe7FY#dmjOIU!?L0S=E)#II7oX!~rj{p^b`jl{a@>0jRrccY;C z>f)(i)qjaoTPRq!jopnWTwQGp8?pux;=q5p;d7K&;RTjR%For>nR*l`MDC;LDT>E! z$+)f)^!vgy`MQs`L*aYa7#ti7I+VmmHhJn892Z>CRVG4KDL!MBLe(y+&V~3>j$e_- z4dWdc^Tx3-eO^AM9mjf~ul0v0l6dv;3g&Ti6Mr+$TVRO{r zCW+BpJkmxP6$j*QlF1uJRfV-h*^!9rx1r>t!Km-sba+^aM&j$Y%; z6spyY=7?msTqCaixSDmp`Ull|#`7tJTF(?V&3amBC~JO6F@$Ao(DK|L^u^WiOZ)iV2#_6UlU z8hX&?(q^ekkZW_?8qYnJl+!t>6?p#PT-mgP81%OnvB*7?;S>^NQ2!U2e>0(3ZDOyJ z_W3=%LNs*1iBxq84xKOH1LKNcx8A>07=OE>coNqw1(LgTX7meRZSEJqioli_{P9oK z-~e3<+18cPP06;7(rXg`{e{i-5jQ)>NC)(}~@^zf8ioJ<%VvQpk}!{z1XXv;~r5;(%@;Io9Qv6@Mb? zg4!0Cty z^Xm^sF4}vRL(T2oJ4o>3Zk03P1%D7|w<5W@vT%omVA_gi6Lnmgp;5^+!HF_`$JwYXT|h;TMV zC>E54HAM*1MSo^T0W3$M*muH;X=|HPSy7~sQG*>=>xQ>IyImM;ZAUx1TYvCGu$W)d zbCq3=pARTZFoM9s0I+8n3!0u;nE(j$bAdhv=l|BfnZ6JXl?kN_Qzm-@Tt5TVU&4Yb z)Bjmq4n8`69hVb3{;3=epV#JZqpeR4GiHaov6b?6w6nb>+-x>eZnj{VRd_w@$$B8} zZL~eN;OQH~Wx&x9RPUu4Ab+AE(8E&Tx7zqODO~G9zR~Y>IFK6t@eKIHio29HE5TM5 z-1u@9qaf4v{A8(up(TD8{aRJSRLa}#Rqgc^1*S@i-n(e~zE_#44}jsQ&#J(v`Bc55 ztOuy3K=+3=5i#$V@Vp_picREGU8yt6(WG9P9e*@mdQ$SV-J{DmpnoDBd8>X#w`v5} zdN-DCT)kwKg=#IUZr45mCg*!esLiuVXLLCo&kK*&iGA#BdB9zLMnP@%El# zhpv#e9)dOIsL{kc+H8h3%nM*S+O#{9tg1xMWt%E#G6r^!Xslnx=V@JE9SS-$()bW% z_K%h=+J*q_25u>Mp?@*#uSqXx-`1UHD;DkQoXpxl>uEkox6_#!YYhn8MQn&W7?eF2n}0DVnH$Lyf6U-oASJG_ zJJwXtGqfwS3BBjTS~L`EF{u4wSPheBCuU-1c9^F*cXBk-hE8oXCbeF{kee%c7{}?f z2nHX17!aVC;`(S%BLKlo){k4Q6YQLS-_qt@eyRxtxj~MqNtWt;ijH$erIqiRkrDnk zDWXx5z#_O`^nWyDWF)nVNQAbF{hMVi<R-uQ?H|`J7@H$fA`rJ_rdv8@b(*k(=VAXrruepThbH)fp%E7a8|LGE>7ms zOr4hsJb!daUZ%BtAJgeBug*U+iWxp{hPIlMG2q1 z6@RX?juS1fc5hW>Tt_l(CHpp^AP7paTXk<)>wnSMer~|?^P@2=8Zay#je&QddK^@vr9h{> zapMBL3}aA+cvADCJWTEska!a^p7*V_B2Uyxc!k_}n4n7j!vq$wJ^`wt#y8N=4%C8Q zOVEvI&*mVW-9vbG58$ccJC&o{ z7@kf9Pp!M~hVitzq)|KFB0ET@+|XH5Kv%7RfE0uA(T?X3aa;lH!A>p4?<~ z&p(ELSo*k;G3{ycRaux-aGskZbGCq5Y2sp0lHKhbKT)Z>XV$v~$YywvWwXzXKYvmF zS-#2d!SrU5taXxF1C2(Fv4SHn9ZdDp2v-(E@|EIRV)zC;9*gn^x_`mjvw!c?d(2&S z5ZumY_r<{rFC^3GC;1v>`OORX>ji9z6sjMLI2ie_hsGP))N0*89S2`E{Tj1N748U26<>ct?j;J=xnwJ!|4B zNj9H12T7U(5i6T36*9JgNq%<7@X>tFP^8LCO>XE5z@vM%@ccZ~?OEfoT zHE*|}dOTk%A5jqSo66A5X}(=&Y^Vfl>-iAjZNrQFV#Dgl8~)Vj`l3@n`pGoGRw;(L zuP0gXx*!P}7upV~oPTE0Agr3c0#so0=bMN2qkF!Tv)Ttv4R0H*uHfC8##Ka@r?Bip z+??;iLwK#%vm47@X{(K}$G0h3$@}inzie%E=8FP8xGZy`j*>}22KKawqDUR3D03}+ zhQatLfHG?BwMzv~EXosbQk-p5j$s5C&~IlsOB(4Om#Ygm+J9nm+|bdD`9-i!yrG{4 z*9Cs<>4T4S%dwIlRKeprYRcu1$;2VA<|f zj0473Q3naTU<-pok9gSAs$@gr^-rY-!4`0V(?(P7+V2ysP2>E!7-q<4%qX*Ll>J>8 zP!og;8`?I|`hShYV%mBh+zbm=66GM(L4x&oTDo!JU&n;aJh+QIxCRR<>p7uNM#&8Q zX2Au3!C(MR3N}3+BgcIv+45*Su`nq(`(a3*#r^hO4P$?k7GQ|s1ghep5Jgr3VsZ#$ z4o{}VJTHUiUnIN4dVrsES*B=VoRYHxnv9?c5z6~UPZW^NEU)}84U$y zJdJ5vPN%Q|Zeted%4sRH$REpTI7E+NGv{<499HMC@@yZuP=6hy#mUbw zQoTNX=YN!vk@-4hSrronNChd4@MZa$oVx_vmS>5ZM1Uj5DhMC1x_ro*x1WWzd`p!M zqY#J3%v#SgXGSPk{f4&fqfcL#54JSbJ^b1KOw+;pV8JV!_o~%Y73sP>i`zNJdmd#R za5>E}F?fo%lOopZ*R2L&=3_2g^UA=U5Rgj%e}4>+A1M3*r-gMEyW9fHvSqAq&u6@H zC8aA-OmI{#NMd_h)lIj@MUn)%Eg0XJ4IVEB>^}d|0wDzezG)W`(-Om-rwgq&GZ?))QLVrL6gqu9lA>F*^Mx_h?~cLfZrj zTt>CO0^q>QN0Pl`IgIY$6Ky3n-$c+nlz-(42N=3BNxZYf!hn&@^CHW2snv%ezN$rD z3srEldyLki+YHC~LXKkg+SG@YK|udeZwY=XavaZMoa_4-DDN4d$Z4LMlr24yTp_^B zIT3!el^f(qf2#yhWcoju8J$=#8{vC^JGPfAOnB_yrE->YXaJ><>U9F*^-$9Ckbgnq zJlcIq$6OMTKaJWSb&KK5S(SWPx-V+JD^nTk;P^t)&aIbfH*FP#oM(Gg^@KN77@krj=H*N`__TxgecMiUbt41>uY$A(MxQ zK2mUm=iTW#Eyc5GZZvF-3aN2n%72yzMAz+lIJpAR-RX2OJGW)VXsXA(TaJWtxneO4 z;qU_(Lx0)Ls{L0KilB}&w6-Etbb?Gl*fiD=uaN9l39I>@r>#5X47vT1;lJ9Cp-zNa z{#VRjo4Hqz`o~f=!hf4LAWukV)IdVyvT%jZ*4;m*>csJzC{fJ&4os*`0wdUu}#G?q9?PA?^=L)6qH zFm6Fohpshq1RRe>DQBy~ls=QRxTBqs-;7q0*?z1L4^XZVg>Xo?xH>U3s2HRz^@XLJ zfZ_+H#AMtMM<7MHxJYuLE`MYT`DdpPH(;12sGVT~qm)!4K9t7k6mbU0&&hCJW)}mj z8KoG^gQJf>2}nmqMHtHQ>S_e1j;sVdoWw<$I%%?;j`%K{^Z45n<92}P$fh#pssabS zA!mCBw0P~ifG$nJcdK8XU(4w_JOA|L46OK~AR>18#CAmEN@%nBVT4zj>u8E{-~Ve%*P;*>jv^F2(I4b1~L`=b0>aBL{peja{< zKNCYjZB`AF0s+TdRc_ChMLm3%TlDF>qGUEF%O2VL%4>E_T*YS?9UDH)+O)CDI8k!O z;is4o{!YMoo@79{l7G&&P{V2-lfZK>5v9?<$n$7Fo)xk@aj!pY$yLNo_STOi+~;%_ zMz&{^XXUhf`^n|EAA>@kWmD8{g#t|fi}LHaPcDP2c=~-%)(Jj@v&(a#`~55(jV6h& zQSnm7s5W65144{1BRRqJPn9NHhcKcpV0N8wiYa z#@3UNz7e3l?Mhe=R}>DmqT+h7i3n)le2%#CIXg&}RqfDfr2$pERc(5L2iX9$vfT2& z_=NoziETY`b4Dx8N2R=iKxBbpjPqMOOrWSKQjoh?%&&kW#j0a+XpT??{gDJ^p>^e~ z1>{Fwku1e2HGe9yq-J1q@4K{{el*g{Te$LsePh8`2!V>r7y)W;6!@U6#JeMwZZa(9r7dQV5rq}aXawP(Z~0pF6wc09NAI0n94xQ#iJPp($~eM_O^4%_k$^b?{5W zL<0qRDMToF5Rw(KNfjYh3P@=*1J1@H*!fb}?qmGv?WF7QqLRD(_OOl{4D6$Lxu?1T zXsIkZWCTcsoN0ocQjUrq3E~0&NQO?#zswC%w13sWYr#)Y{7P#Y3{lz zP)%y!d{t|{Vx@2?D#6e)rN0c@l(RF5iK;XUNSu@uZZN&nns9N&@To;9>F8hcL`8Ie zW#Lq_lsgJoz%X>o{Vq9T5$8X+mU5pMCpQ>aqX?&FAQ+qFH^bk=PT_#%x{rX$1lJ1z zXn#aI6gIPk0guO!zGpe@qN4;RJ6rsnM1pp@tzf8Ihq??AX6se>_*Jx-HPpO<$UCUR z`XLKR^IF;56_QM=afK8NkZ()~s#p=5vWhC7pqxg=%PhKg`$7QQs()42bsU(r3hmK+ z#5A*nIHUDu@M7gclu>5a@pU=@+Ze^DcYl=Z9eC$}KFKds0h%oP9JUA)mb^Q2P|%bK z+Y0f@WL9_ZlaGU~dkX1`w)BUubV2JTCPlh|$(Q=gOR=8kp!EPsCl&>bo6W(`X#*J{ z{lb-*BK`Qrp)-g|g#u>UCE2~4yA_Gsc?JLr|K^D_5Sl0>g8oDNzA{hEYN`$7EUn<6~I$_{S7XZ=|OIPm(Tn zIXDZ7be60(WvX555(GSdi080x<&`u9Vkdz$GQYFx^@dqLcxQs9i_&@&^v<$TI!;T^ zfn=Je2!aS%ime#grmr#)C^#7$SAQ&U{E>fxH5o>sV0YZN<09GSXZu*gXP(3FTOnE} zuHqaP-R1YVt#ZXD24En>XSM-+!2K{DVVhT83}N#JdnkCaP{AI2*ncH>kk8ZQ*Oy)l z9^mf~fxVk$BY;!^_EHtt>j3M;Dy)5|^|Ar$Js3w9*4`@>SnzxAwF)f4_*VKQ zGMNC!*|4f~y@X=H2kTXa1)v^i9C1X6S*}wwB=JE>a1jLh(azHA zjysR1ZS<8w{4@7NjJ)hWA@>B?l$%$nH_X7)J3IRr5#cem9vvNh%q#bpW;7|Jgno~h z7cS<|@py_frPE>t3QkNoDf}@vk7u)Z_&tvZefjp{lW#$YeJgN-Lx1|bG`C)S0^oLE z4L+D(KNspxsa}3NJKf!Xm941qX+W5Qce6Zwjp@@5Bgxr(-^g z{6wEwyt2i>1H?k9pU`y7^Ce*Tf(^f4n)ai8kgyY2t%kSK)_++kH$RN$Q_>_3=>%HE z+NUMu&;pfdE7*cv#aAdBLS6ab-Lu`-Yg_ZSva&Gtz_3(!0w1pdTpfNWH!_>D<5v zp^OJiDX_$gRDVnRz9yta8*SJtfGLQS0v*JDJs2L}>R0t=^)*E0)Z1kF0yXzEEK04> z8}Xg>dV5qf)_X*40itaz@a-&G$JZ9mlz%QqvX*dhT9k__gr$^a=;OvE zmSZ!1;A_aO@(6*>S$F`z|D*Db|6eNJTfqZ2w<#&%*r6i#!j~}=r1n+^$7*eRXd7pn zugq$im5A#0N`dlbL#v2R34Pyj-j4mfg%!z#j+>XGNOIY(X~OUKOY*%kX@xA{R%^+j zCpIREo_~0RP=UXeH+58rS@Ae6W|uI)y$Xo%r59fepF2+Bb{N(}xDO}OeUkii7hH1c zWMNTtIll+rg#t7a*8BJ9c!q7}s4f)CVm{2^b43n`T!N#!AQPgR7zH%^86sKl_0kb_ zikNE@4QKh}05qTTd%AZVoJRbo25P!~E-(Fq~u1-Ry9K3hT3J@;;j&;TV*kR_q*MSp|CnrS|-mWe*nQ5uTOzDi*eewI!!+2qP5+ zZR_rT)7N{Y#<9>WBJGC{$4BexG~;z3_Rzg6=FW#T?LNXr_NOEELfFGx46`gx=oBdW ziho@d5U4d<7*H@hJRyJ5^%W`S+4B|#8|FkB2=|`7vN%HEy0B@cBF|9wada3sZDY>B zREGoVS1gqvVXnYV<_PNxTJXS59{J3Z+3%xus_(3$l@f*oOdE;u2MdggljB-(v97de zBQCnii*>}sw7s;Lt`-*EWkn}ZQ7QMK~fB}deDUk{$>q`c#npphpFgbpC0S#g z+U&_zAZ+sC!5NN+Y$s9FNJlm`2|!~jn_q-nCg~&|n(b(kB-iN>qW(cMxon6*!+*~v ziQI{xu_u!{MbP8la3?blQVaF+3GOs_XYWP!AeR1Bps+Fb$2ecWfg5NV$@mV-XPRL`X1d=?Ch0J{pCzTqd64EG;#qFU2{B(Qn|<5CVUYS&(tqCLx1yf z8VVhti^1{fc>s6IpA=RSTJ|L$9e7#C%S!L0(6JZ@gu%%d00Wq)97}Os4B{W-bV3Gj z8N-7mB_F}1*bSTZq9|g-yY^qqPSk~xji%a2R<8jvfSc!?ovrt?bh`Tq-+#)!lOc1X zC)CftNH}3z2Rl17%ZUkij8ikF%E)NgAimD#+HA>k@HX478=1J@nE;az3b_en{+0gY z3Y&09HJg9}4z++Y7$X3=5{T2B{J7A?B+D&Cmv`Cs>mkC1KIoW@oMTB)9W#!E&f{1U zVo|z6D48&O8%*|I1`aj%S$~l_v%8rI9?XQp@ZB2Krjfuq;KJ>(J#n`g7zXfukmnVq zS>A(4zj)Auer{R-XozSURmqs7e-#zHoD-4`A_nS*+Fze9x>aF;M<6plVed|=8=QB;FEPUCSzY0c2S&LCGw`zs#16a99f^wj?0y&f<;cY>ml-So_!L#0fWuPD z@6c%_3FcvM;0@FAJb%q{eUKFw-#Zg(b^=#Ok$~1dV!2Ia`pqD(h-zWH>Vfg97lzWp zQ`TZE;hXW~OvaQ(faVspD~CyR)$F~Z%jP(_PdG3OulMrKDjUm`!{^@j!06K#=2QAV z2fFw%E#~B|K{FvZ=mLl7h$jVJ8xuXY^!Q<(Ac-(BtjP0)uYdM-#b2B0bqL6nMHJG` zPB{Zt+;(Rt@*!l^m6oIQg_?=pME$XKAt}OfRskWR+)TQZ+(cAEY_VgMmvJKliX_`SmJZHS=#^&(_p>x6PL{b7GlXvyb=iAIDi-< z@eRK#=t09iesg6Gznj$4ku6n7CxMs?iC_QYO&RLY^{Fk8=~}d@ZBDh^*5~(0W7`bs zP)l6+s>*vM8yUO-VxFvmn9zvPEb9KPgM!XRMU8I8HGg=&VL8oXUABNFeLGE&vvlqq z{PMI`UJ3G?Am9NOKZNCY(O29>b3QbIGqpwcYf2l##~OXfdPl{_im-&S-+_VP-m z$=xCtn}1x9dnU0`H@N0}BI6VX)dKr&#_ADY9T0`wT5+tLC+PwxrR3nOeKF6Dw4sZ8 zVzaVoD)t461D-l|*n-_lG=j*tjj%-XYhFYL!x3s&y2WL>{GZ|lWrQHw^;|=KBOGIo ze7MK)6f_X%FziBX|Cj(;QA^ZlC)vFjDI*#dgKou5Si+7Yq@iXy$8;QRUjSL zp=E;%!Y|;Kl-Zi!ClpcRksKR@g5$8VHUDfatfd|w>3Ewze?Nn+Uh`(S7 zm5T%{4>&X-d)zj@6#n=Dl_~moM7!zs(TAttH}nvSBmz;#&ZYRcJ{i+MfrZT(h*Y8~ z$A8_1Ld{%2iA)7D7A;A!a+oeaY7oI$#Vrd{N|(FfZB~>f71>yIxXjT)U79TogPA#O zgc`WOIYlsW^o8|~$O{uUt-*}f(Z}UU&)dTHvQkNP%C@5Gxf(Yuw?N`K=#8gRi1V@Ty2N+OGUzI45qMRSof$w%3Ef+M?KWz_J0jA zaKG#xENd&<8vn<2`8`gC$nqkz2^cH z{sxJ6SOSQoEK6HHRdI+y%TWPW)CLgZ&s-m!UX zgFeTHf~FXy$SD;Cae8!YKz}{~){@)-8;Y8i8BH>)Hg{)vE%3SJy?=Amu`B^}&H*i9 zRcPXzgIHb|WjsgX5$datM9h|fDYL6<8b)VXr0j0fd6Cwh_paFhnD9CX<4*1Ae}yYWc??atbd};V(Hf@oYv+Nr%qY(vZD1&dt9r0FHI_DuCO~OV5Gw;QY+gMX*Wn! zeLbiZZIeX*&~leC45J^Iem)d(nx*4Ua5>a9sr#CnQ~1!TZ@og_3P}sKyt)?O!dLQ# zlLh#j_=X!05Iwg+;0~rYzFR=e!(A7ETOnqX&mRoNr+zJM7MY@)${id%kCSCmO*B_7ZB+}IFncW%?^ekY~C(c1Rzc?)#VpJVSJdtCcJ84!N zE?lEg*?;0sS!m1|fwqJin*T8mYGi|-eGbn0lnTXGKYo+?0?UflZ%0D)si20uLiUBa!&6OV`BuP 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 I_}>rz3%M(BXQJFy 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�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

        XkYQV>vT*rN#kh${c==dFKi+nbo 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

        Yoxs%?xldi{|RM^RSlNyGf)=$-O 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 QpO?R?0~`j4q5}W`0B~1jnE(I) diff --git a/Moose Test Missions/SPA - Spawning/SPA-130 - Uncontrolled Spawning/SPA-130 - Uncontrolled Spawning.lua b/Moose Test Missions/SPA - Spawning/SPA-130 - Uncontrolled Spawning/SPA-130 - Uncontrolled Spawning.lua new file mode 100644 index 000000000..49d3f0951 --- /dev/null +++ b/Moose Test Missions/SPA - Spawning/SPA-130 - Uncontrolled Spawning/SPA-130 - Uncontrolled Spawning.lua @@ -0,0 +1,47 @@ +--- +-- Name: SPA-130 - Uncontrolled Spawning +-- Author: FlightControl +-- Date Created: 04 Feb 2017 +-- +-- # Situation: +-- +-- A plane will be spawned Uncontrolled and later one will be spawned Controlled. +-- Only the Controlled plane will move, the other will remain idle at the parking spot. +-- +-- # Test cases: +-- +-- 1. Observe the spawning of the UnControlled Plane. +-- 2. Observe the spawning of the Controlled Plane. + + +-- Create the SPAWN object looking for the group (template) "Plane". +local SpawnPlane = SPAWN:New( "Plane" ) + +-- Set the spawn mode to UnControlled. +SpawnPlane:InitUnControlled( true ) + +-- Spawn the UnControlled Group +local UnControlledPlane = SpawnPlane:Spawn() + +-- Set the spawn mode back to Controlled. +SpawnPlane:InitUnControlled( false ) + +local ControlledPlane = SpawnPlane:Spawn() + +-- Now, let's create a menu option at a player slot plane... +-- We can only create the menu option if the player has joined the slot ... +local PlayerPlane = CLIENT:FindByName( "PlayerPlane", "Select Menu item to activate UnControlled plane" ) + +PlayerPlane:Alive( + function( Client, SpawnPlane ) + + --- @param Functional.Spawn#SPAWN SpawnPlane + local function ActivatePlane( SpawnPlane ) + SpawnPlane:InitUnControlled( false ) + SpawnPlane:ReSpawn( 1 ) + end + + local Menu = MENU_CLIENT_COMMAND:New( Client, "Select to activate UnControlled plane", nil, ActivatePlane, SpawnPlane ) + end + , SpawnPlane +) \ No newline at end of file diff --git a/Moose Test Missions/SPA - Spawning/SPA-130 - Uncontrolled Spawning/SPA-130 - Uncontrolled Spawning.miz b/Moose Test Missions/SPA - Spawning/SPA-130 - Uncontrolled Spawning/SPA-130 - Uncontrolled Spawning.miz new file mode 100644 index 0000000000000000000000000000000000000000..13bde46acc0f978f2a833473e68be8bbf59c3fe0 GIT binary patch literal 220318 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 z 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ξ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 WAi>`Y1OycLM<6~OL~V`V5d0545rmTf literal 0 HcmV?d00001 diff --git a/Moose Test Missions/TSK - Task Modelling/TSK-010 - Task Modelling - SEAD/TSK-010 - Task Modelling - SEAD.miz b/Moose Test Missions/TSK - Task Modelling/TSK-010 - Task Modelling - SEAD/TSK-010 - Task Modelling - SEAD.miz index 5766313494f7948c537f9d4202d4cf5c2f442962..6f1e3197804bf206ad80e841f5db14f4729a2d17 100644 GIT binary patch delta 201245 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

        `zq37^@>7LuHn58cT* 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%0Dtlna>?paWve8^zR0rK4UlmYu7bl3fT5A5 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

        e|M1s!%7e-RIy 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

        PeBD7=n9Rp&N>$6 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

        (CVY1%hsAt61)%pt{v{Bo%^;aA=Y8 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.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 delta 196325 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

        ^Rv`;{v|VUXQnW1HC)jtU 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 diff --git a/docs/Documentation/AI_Balancer.html b/docs/Documentation/AI_Balancer.html index 02a4952eb..7b1a36cfe 100644 --- a/docs/Documentation/AI_Balancer.html +++ b/docs/Documentation/AI_Balancer.html @@ -33,6 +33,7 @@

      1. Controllable
      2. Database
      3. Detection
      4. +
      5. DetectionManager
      6. Escort
      7. Event
      8. Fsm
      9. @@ -76,18 +77,17 @@ even when there are hardly any players in the mission.

        Banner Image

        -
        -

        1) AI.AIBalancer#AIBALANCER class, extends Core.Fsm#FSM_SET

        +

        1) AIBalancer#AIBALANCER class, extends Fsm#FSM_SET

        -

        The AI.AIBalancer#AIBALANCER class monitors and manages as many replacement AI groups as there are +

        The AIBalancer#AIBALANCER 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 Core.Fsm#FSM_SET manages the functionality to control the Finite State Machine (FSM). +

        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.

        @@ -136,8 +136,8 @@ However, there are 2 additional options that you can use to customize the destro When a human player joins a slot, you can configure to let the AI return to:

        Note that when AI returns to an airbase, the AIBALANCER will trigger the Return event and the AI will return, @@ -240,13 +240,13 @@ otherwise the AIBALANCER will trigger a Destroy event, and AI_BALANCER:ReturnToHomeAirbase(ReturnTresholdRange) -

        Returns the AI to the home Wrapper.Airbase#AIRBASE.

        +

        Returns the AI to the home Airbase#AIRBASE.

        AI_BALANCER:ReturnToNearestAirbases(ReturnTresholdRange, ReturnAirbaseSet) -

        Returns the AI to the nearest friendly Wrapper.Airbase#AIRBASE.

        +

        Returns the AI to the nearest friendly Airbase#AIRBASE.

        @@ -495,14 +495,14 @@ The default Spawn object to spawn new AI Groups when needed.

        -

        Returns the AI to the home Wrapper.Airbase#AIRBASE.

        +

        Returns the AI to the home Airbase#AIRBASE.

        Parameter

        @@ -517,20 +517,20 @@ If there is an enemy Wrapper.Client#CLIE
        -

        Returns the AI to the nearest friendly Wrapper.Airbase#AIRBASE.

        +

        Returns the AI to the nearest friendly Airbase#AIRBASE.

        Parameters

        diff --git a/docs/Documentation/AI_Cap.html b/docs/Documentation/AI_Cap.html index c71293951..ec34c1a56 100644 --- a/docs/Documentation/AI_Cap.html +++ b/docs/Documentation/AI_Cap.html @@ -33,6 +33,7 @@
      10. Controllable
      11. Database
      12. Detection
      13. +
      14. DetectionManager
      15. Escort
      16. Event
      17. Fsm
      18. @@ -74,10 +75,9 @@

        Banner Image

        -
        -

        1) #AICAPZONE class, extends AI.AICAP#AIPATROL_ZONE

        +

        1) #AICAPZONE class, extends AICAP#AIPATROL_ZONE

        The #AICAPZONE 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.

        @@ -151,7 +151,7 @@ When the fuel treshold has been reached, the airplane will fly towards the neare 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.AICAP#AICAP_ZONE.SetEngageRange() to define that range.

        +Use the method AICAP#AICAP_ZONE.SetEngageRange() to define that range.

        1.4) Set the Zone of Engagement

        @@ -159,7 +159,7 @@ Use the method AI.AIC

        An optional Zone can be set, that will define when the AI will engage with the detected airborne enemy targets. -Use the method AI.AICap#AICAP_ZONE.SetEngageZone() to define that Zone.

        +Use the method AICap#AICAP_ZONE.SetEngageZone() to define that Zone.


        @@ -281,7 +281,7 @@ Use the method AI.AICa - AI_CAP_ZONE:New(PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed) + AI_CAP_ZONE:New(PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType)

        Creates a new AICAPZONE object

        @@ -640,7 +640,7 @@ Use the method AI.AICa
        -AI_CAP_ZONE:New(PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed) +AI_CAP_ZONE:New(PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType)
        @@ -678,6 +678,12 @@ The minimum speed of the Controllable in km/h.Dcs.DCSTypes#Speed PatrolMaxSpeed : The maximum speed of the Controllable in km/h.

        + +
      19. + +

        Dcs.DCSTypes#AltitudeType PatrolAltType : +The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO

        +
      20. Return value

        diff --git a/docs/Documentation/AI_Cas.html b/docs/Documentation/AI_Cas.html index 5089134a9..bae974427 100644 --- a/docs/Documentation/AI_Cas.html +++ b/docs/Documentation/AI_Cas.html @@ -33,6 +33,7 @@
      21. Controllable
      22. Database
      23. Detection
      24. +
      25. DetectionManager
      26. Escort
      27. Event
      28. Fsm
      29. @@ -70,16 +71,16 @@

        Module AI_Cas

        -

        Single-Player:Yes / Mulit-Player:Yes / AI:Yes / Human:No / Types:Air -- Provide Close Air Support to friendly ground troops.

        +

        Single-Player:Yes / Mulit-Player:Yes / AI:Yes / Human:No / Types:Air -- +Provide Close Air Support to friendly ground troops.

        Banner Image

        -
        -

        1) #AICASZONE class, extends AI.AIPatrol#AIPATROL_ZONE

        +

        1) #AICASZONE class, extends AIPatrol#AIPATROL_ZONE

        -

        #AICASZONE derives from the AI.AIPatrol#AIPATROL_ZONE, inheriting its methods and behaviour.

        +

        #AICASZONE derives from the AIPatrol#AIPATROL_ZONE, inheriting its methods and behaviour.

        @@ -276,7 +277,7 @@ It can be notified to go RTB through the RTB event.

        - AI_CAS_ZONE:New(PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageZone) + AI_CAS_ZONE:New(PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType, EngageZone)

        Creates a new AICASZONE object

        @@ -601,7 +602,7 @@ It can be notified to go RTB through the RTB event.

        -AI_CAS_ZONE:New(PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageZone) +AI_CAS_ZONE:New(PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType, EngageZone)
        @@ -642,6 +643,12 @@ The maximum speed of the Controllable in km/h.
      30. +

        Dcs.DCSTypes#AltitudeType PatrolAltType : +The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO

        + +
      31. +
      32. +

        Core.Zone#ZONE EngageZone :

      33. diff --git a/docs/Documentation/AI_Patrol.html b/docs/Documentation/AI_Patrol.html index 36cd59344..9510146aa 100644 --- a/docs/Documentation/AI_Patrol.html +++ b/docs/Documentation/AI_Patrol.html @@ -33,6 +33,7 @@
      34. Controllable
      35. Database
      36. Detection
      37. +
      38. DetectionManager
      39. Escort
      40. Event
      41. Fsm
      42. @@ -70,14 +71,14 @@

        Module AI_Patrol

        -

        Single-Player:Yes / Mulit-Player:Yes / AI:Yes / Human:No / Types:Air -- Air Patrolling or Staging.

        +

        Single-Player:Yes / Mulit-Player:Yes / AI:Yes / Human:No / Types:Air -- +Air Patrolling or Staging.

        Banner Image

        -
        -

        1) #AIPATROLZONE class, extends Core.Fsm#FSM_CONTROLLABLE

        +

        1) #AIPATROLZONE class, extends Fsm#FSM_CONTROLLABLE

        The #AIPATROLZONE class implements the core functions to patrol a Zone by an AI Controllable or Group.

        @@ -317,7 +318,7 @@ Use the method AIPATROLZONE.M AI_PATROL_ZONE:GetDetectedUnits() -

        Gets a list of Wrapper.Unit#UNITs that were detected by the AI.

        +

        Gets a list of Unit#UNITs that were detected by the AI.

        @@ -333,7 +334,7 @@ Use the method AIPATROLZONE.M - AI_PATROL_ZONE:New(PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed) + AI_PATROL_ZONE:New(PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType)

        Creates a new AIPATROLZONE object

        @@ -450,6 +451,12 @@ Use the method AIPATROLZONE.M AI_PATROL_ZONE:OnPilotDead(EventData) + + + + AI_PATROL_ZONE.PatrolAltType + + @@ -868,7 +875,7 @@ Use the method AIPATROLZONE.M
        -

        Gets a list of Wrapper.Unit#UNITs that were detected by the AI.

        +

        Gets a list of Unit#UNITs that were detected by the AI.

        No filtering is applied, so, ANY detected UNIT can be in this list. @@ -877,7 +884,7 @@ It is up to the mission designer to use the Unit class a

        Return value

        #table: -The list of Wrapper.Unit#UNITs

        +The list of Unit#UNITs

        @@ -957,7 +964,7 @@ self

        -AI_PATROL_ZONE:New(PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed) +AI_PATROL_ZONE:New(PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType)
        @@ -995,6 +1002,12 @@ The minimum speed of the Controllable in km/h.Dcs.DCSTypes#Speed PatrolMaxSpeed : The maximum speed of the Controllable in km/h.

        + +
      43. + +

        Dcs.DCSTypes#AltitudeType PatrolAltType : +The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO

        +
      44. Return value

        @@ -1751,6 +1764,22 @@ Return false to cancel Transition.

        +
        + +
        +
        + + +AI_PATROL_ZONE.PatrolAltType + +
        +
        + + + + +

        defafult PatrolAltType to "RADIO" if not specified

        +
        diff --git a/docs/Documentation/Account.html b/docs/Documentation/Account.html index 51eb80f32..5b145b1df 100644 --- a/docs/Documentation/Account.html +++ b/docs/Documentation/Account.html @@ -33,6 +33,7 @@
      45. Controllable
      46. Database
      47. Detection
      48. +
      49. DetectionManager
      50. Escort
      51. Event
      52. Fsm
      53. @@ -76,7 +77,7 @@
        -

        #ACT_ACCOUNT FSM class, extends Core.Fsm#FSM_PROCESS

        +

        #ACT_ACCOUNT FSM class, extends Fsm#FSM_PROCESS

        ACT_ACCOUNT state machine:

        diff --git a/docs/Documentation/Airbase.html b/docs/Documentation/Airbase.html index edb97e0d9..65dbb7cab 100644 --- a/docs/Documentation/Airbase.html +++ b/docs/Documentation/Airbase.html @@ -33,6 +33,7 @@
      54. Controllable
      55. Database
      56. Detection
      57. +
      58. DetectionManager
      59. Escort
      60. Event
      61. Fsm
      62. @@ -76,7 +77,7 @@
        -

        1) Wrapper.Airbase#AIRBASE class, extends Wrapper.Positionable#POSITIONABLE

        +

        1) Airbase#AIRBASE class, extends Positionable#POSITIONABLE

        The AIRBASE class is a wrapper class to handle the DCS Airbase objects:

        -

        The EventOn() methods provide the Core.Event#EVENTDATA structure to the event handling function. -The Core.Event#EVENTDATA structure contains an enriched data set of information about the event being handled.

        +

        The EventOn() methods provide the Event#EVENTDATA structure to the event handling function. +The Event#EVENTDATA structure contains an enriched data set of information about the event being handled.

        Find below an example of the prototype how to write an event handling function:

        @@ -189,7 +190,7 @@ The Core.Event#EVENTDATA structure co
        • self = the object that is handling the EventOnPlayerEnterUnit.
        • -
        • EventData = the Core.Event#EVENTDATA structure, containing more information of the Event.
        • +
        • EventData = the Event#EVENTDATA structure, containing more information of the Event.

        1.4) Class identification methods

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

        BASE:GetClassNameAndID()

        Get the ClassName + ClassID of the class instance.

        + + + + BASE:GetEventPriority() + +

        Get the Class Event processing Priority.

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

        BASE:New() + + + + BASE:SetEventPriority(EventPriority) + +

        Set the Class Event processing Priority.

        @@ -628,6 +641,12 @@ When Moose is loaded statically, (as one file), tracing is switched off by defau BASE:_F(Arguments, DebugInfoCurrentParam, DebugInfoFromParam)

        Trace a function call.

        + + + + BASE._Private + + @@ -1731,6 +1750,28 @@ The ClassName + ClassID of the class instance.

        + +BASE:GetEventPriority() + +
        +
        + +

        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.

        + +

        Return value

        + +

        #number: +The Event processing Priority.

        + +
        +
        +
        +
        + BASE:GetParent(Child) @@ -1867,6 +1908,37 @@ Child

        + +BASE:SetEventPriority(EventPriority) + +
        +
        + +

        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.

        + +

        Parameter

        +
          +
        • + +

          #number EventPriority : +The Event processing Priority.

          + +
        • +
        +

        Return value

        + + +

        self

        + +
        +
        +
        +
        + BASE:SetState(Object, Key, Value) @@ -2176,6 +2248,20 @@ A #table or any field.

        + +
        +
        +
        + + + +BASE._Private + +
        +
        + + +
        diff --git a/docs/Documentation/Cargo.html b/docs/Documentation/Cargo.html index 749df60ca..fb98d54a5 100644 --- a/docs/Documentation/Cargo.html +++ b/docs/Documentation/Cargo.html @@ -33,6 +33,7 @@
      63. Controllable
      64. Database
      65. Detection
      66. +
      67. DetectionManager
      68. Escort
      69. Event
      70. Fsm
      71. @@ -70,24 +71,32 @@

        Module Cargo

        -

        Single-Player:Yes / Mulit-Player:Yes / AI:Yes / Human:No / Types:Ground -- Management of logical cargo objects, that can be transported from and to transportation carriers.

        +

        Single-Player:Yes / Mulit-Player:Yes / AI:Yes / Human:No / Types:Ground --
        +Management of logical cargo objects, that can be transported from and to transportation carriers.

        - +

        Banner Image


        Cargo can be of various forms, always are composed out of ONE object ( one unit or one static or one slingload crate ):

          -
        • AICARGOUNIT, 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.

        • -
        • AICARGOPACKAGE, 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.

        • -
        • AICARGOPACKAGE, 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.

        • -
        • AICARGOGROUPED, represented by a Group of CARGO_UNITs.

        • +
        • AICARGOUNIT, represented by a Unit in a Group: Cargo can be represented by a Unit in a Group.
        -

        1) AI.AICargo#AICARGO class, extends Core.Fsm#FSM_PROCESS

        + +

        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. + * AICARGOPACKAGE, 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. + * AICARGOPACKAGE, 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.

        + +
          +
        • AICARGOGROUPED, 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.

        @@ -132,10 +141,12 @@ There are 2 moments when state transition methods will be called by the state ma

        2) #AICARGOUNIT class

        +

        The AICARGOUNIT 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 AICARGOUNIT objects to and from carriers.

        5) #AICARGOGROUPED class

        +

        The AICARGOGROUPED 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 AICARGOUNIT objects to and from carriers.

        @@ -1263,7 +1274,7 @@ The cargo must be in the Loaded state.

      72. Core.Point#POINT_VEC2 ToPointVec2 : -(optional) @{Core.Point#POINT_VEC2) to where the cargo should run after onboarding. If not provided, the cargo will run to 60 meters behind the Carrier location.

        +(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.

      73. @@ -1289,7 +1300,7 @@ The cargo must be in the Loaded state.

      74. Core.Point#POINT_VEC2 ToPointVec2 : -(optional) @{Core.Point#POINT_VEC2) to where the cargo will be placed after unloading. If not provided, the cargo will be placed 60 meters behind the Carrier location.

        +(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.

      75. @@ -1399,7 +1410,7 @@ The amount of seconds to delay the action.

      76. Core.Point#POINT_VEC2 ToPointVec2 : -(optional) @{Core.Point#POINT_VEC2) to where the cargo should run after onboarding. If not provided, the cargo will run to 60 meters behind the Carrier location.

        +(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.

      77. @@ -1431,7 +1442,7 @@ The amount of seconds to delay the action.

      78. Core.Point#POINT_VEC2 ToPointVec2 : -(optional) @{Core.Point#POINT_VEC2) to where the cargo will be placed after unloading. If not provided, the cargo will be placed 60 meters behind the Carrier location.

        +(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.

      79. @@ -2413,7 +2424,6 @@ The UNIT carrying the package.

        - AI_CARGO_UNIT.CargoCarrier diff --git a/docs/Documentation/CleanUp.html b/docs/Documentation/CleanUp.html index 99477fdf2..41e24b2d7 100644 --- a/docs/Documentation/CleanUp.html +++ b/docs/Documentation/CleanUp.html @@ -33,6 +33,7 @@
      80. Controllable
      81. Database
      82. Detection
      83. +
      84. DetectionManager
      85. Escort
      86. Event
      87. Fsm
      88. @@ -119,7 +120,7 @@ CLEANUP:_AddForCleanUp(CleanUpUnit, CleanUpUnitName) -

        Add the Dcs.DCSWrapper.Unit#Unit to the CleanUpList for CleanUp.

        +

        Add the DCSWrapper.Unit#Unit to the CleanUpList for CleanUp.

        @@ -143,7 +144,7 @@ CLEANUP:_DestroyUnit(CleanUpUnit, CleanUpUnitName) -

        Destroys a Dcs.DCSWrapper.Unit#Unit from the simulator, but checks first if it is still existing!

        +

        Destroys a DCSWrapper.Unit#Unit from the simulator, but checks first if it is still existing!

        @@ -291,7 +292,7 @@
        -

        Add the Dcs.DCSWrapper.Unit#Unit to the CleanUpList for CleanUp.

        +

        Add the DCSWrapper.Unit#Unit to the CleanUpList for CleanUp.

        Parameters

          @@ -385,7 +386,7 @@ The groupname...

        -

        Destroys a Dcs.DCSWrapper.Unit#Unit from the simulator, but checks first if it is still existing!

        +

        Destroys a DCSWrapper.Unit#Unit from the simulator, but checks first if it is still existing!

        Parameters

        diff --git a/docs/Documentation/CommandCenter.html b/docs/Documentation/CommandCenter.html index a840108a0..23bc8a52b 100644 --- a/docs/Documentation/CommandCenter.html +++ b/docs/Documentation/CommandCenter.html @@ -33,6 +33,7 @@
      89. Controllable
      90. Database
      91. Detection
      92. +
      93. DetectionManager
      94. Escort
      95. Event
      96. Fsm
      97. diff --git a/docs/Documentation/Controllable.html b/docs/Documentation/Controllable.html index f631f02e5..c2a5442eb 100644 --- a/docs/Documentation/Controllable.html +++ b/docs/Documentation/Controllable.html @@ -33,6 +33,7 @@
      98. Controllable
      99. Database
      100. Detection
      101. +
      102. DetectionManager
      103. Escort
      104. Event
      105. Fsm
      106. @@ -74,8 +75,8 @@ -

        1) Wrapper.Controllable#CONTROLLABLE class, extends Wrapper.Positionable#POSITIONABLE

        -

        The Wrapper.Controllable#CONTROLLABLE class is a wrapper class to handle the DCS Controllable objects:

        +

        1) Controllable#CONTROLLABLE class, extends Positionable#POSITIONABLE

        +

        The Controllable#CONTROLLABLE class is a wrapper class to handle the DCS Controllable objects:

        +
        +
        + + + +EVENTDATA.IniGroup + +
        +
        + + +
        diff --git a/docs/Documentation/Fsm.html b/docs/Documentation/Fsm.html index e509e364b..712e2566e 100644 --- a/docs/Documentation/Fsm.html +++ b/docs/Documentation/Fsm.html @@ -33,6 +33,7 @@
      107. Controllable
      108. Database
      109. Detection
      110. +
      111. DetectionManager
      112. Escort
      113. Event
      114. Fsm
      115. @@ -77,6 +78,8 @@

        Banner Image

        +
        +

        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. @@ -124,7 +127,7 @@ Additionally, I've added extendability and created an API that allows seamless F


        -

        1) Core.Fsm#FSM class, extends Core.Base#BASE

        +

        1) #FSM class, extends Base#BASE

        Transition Rules and Transition Handlers and Event Triggers

        @@ -1482,7 +1485,7 @@ A string defining the start state.

        - #string + FSM._StartState @@ -1776,6 +1779,7 @@ A string defining the start state.

        + FSM.current diff --git a/docs/Documentation/Group.html b/docs/Documentation/Group.html index 9feadfaf4..519182d0f 100644 --- a/docs/Documentation/Group.html +++ b/docs/Documentation/Group.html @@ -33,6 +33,7 @@
      116. Controllable
      117. Database
      118. Detection
      119. +
      120. DetectionManager
      121. Escort
      122. Event
      123. Fsm
      124. @@ -74,8 +75,8 @@ -

        1) Wrapper.Group#GROUP class, extends Wrapper.Controllable#CONTROLLABLE

        -

        The Wrapper.Group#GROUP class is a wrapper class to handle the DCS Group objects:

        +

        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.
        • @@ -113,7 +114,7 @@ If the DCS Group object does not exist or is nil, the GROUP methods will return

          Group templates contain complete mission descriptions. Sometimes you want to copy a complete mission from a group and assign it to another:

          1.3) GROUP Command methods

          @@ -135,7 +136,7 @@ Use the following Zone validation methods on the group:

        • 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 Core.Zone#ZONE_BASE. So, these methods are polymorphic to the zones tested on.

        +

        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

        @@ -221,7 +222,7 @@ Use the following Zone validation methods on the group:

        GROUP:CopyRoute(Begin, End, Randomize, Radius) -

        Return the route of a group by using the Core.Database#DATABASE class.

        +

        Return the route of a group by using the Database#DATABASE class.

        @@ -319,7 +320,7 @@ Use the following Zone validation methods on the group:

        GROUP:GetPositionVec3() -

        Returns the Dcs.DCSTypes#Position3 position vectors indicating the point and direction vectors in 3D of the POSITIONABLE within the mission.

        +

        Returns the DCSTypes#Position3 position vectors indicating the point and direction vectors in 3D of the POSITIONABLE within the mission.

        @@ -584,7 +585,7 @@ All units on the ground result.

        -

        Return the route of a group by using the Core.Database#DATABASE class.

        +

        Return the route of a group by using the Database#DATABASE class.

        Parameters

          @@ -947,7 +948,7 @@ Minimum height found.

        -

        Returns the Dcs.DCSTypes#Position3 position vectors indicating the point and direction vectors in 3D of the POSITIONABLE within the mission.

        +

        Returns the DCSTypes#Position3 position vectors indicating the point and direction vectors in 3D of the POSITIONABLE within the mission.

        Return values

          @@ -1250,7 +1251,7 @@ The zone to test.

          Return value

          #boolean: -Returns true if the Group is completely within the Core.Zone#ZONE_BASE

          +Returns true if the Group is completely within the Zone#ZONE_BASE

        @@ -1313,7 +1314,7 @@ The zone to test.

        Return value

        #boolean: -Returns true if the Group is completely within the Core.Zone#ZONE_BASE

        +Returns true if the Group is completely within the Zone#ZONE_BASE

        @@ -1340,7 +1341,7 @@ The zone to test.

        Return value

        #boolean: -Returns true if the Group is completely within the Core.Zone#ZONE_BASE

        +Returns true if the Group is completely within the Zone#ZONE_BASE

        @@ -1422,7 +1423,7 @@ self

        Respawn the GROUP using a (tweaked) template of the Group.

        -

        The template must be retrieved with the Wrapper.Group#GROUP.GetTemplate() function. +

        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:

        diff --git a/docs/Documentation/Identifiable.html b/docs/Documentation/Identifiable.html index 4f55a41e4..d40418a42 100644 --- a/docs/Documentation/Identifiable.html +++ b/docs/Documentation/Identifiable.html @@ -33,6 +33,7 @@
      125. Controllable
      126. Database
      127. Detection
      128. +
      129. DetectionManager
      130. Escort
      131. Event
      132. Fsm
      133. @@ -74,7 +75,7 @@ -

        1) #IDENTIFIABLE class, extends Wrapper.Object#OBJECT

        +

        1) #IDENTIFIABLE class, extends Object#OBJECT

        The #IDENTIFIABLE class is a wrapper class to handle the DCS Identifiable objects:

        ?j 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�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

      U3fiIc($<=r-P0VKv+?s5PynM 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 T#62TS7v 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ⅈ3Jv6q7 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 ykT_mdtK) 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 YO9ci10000C01N;y0{{S?jspMy09H25T>t<8 delta 200995 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ℜnj{(0qxYU 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 Zzq7xwJ`wT1rQZIV!9V~&?%Cf4_!lMUR0#k8 From 52f4051901522ab19d1b4d022552fd9104f1b774 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sat, 4 Feb 2017 15:16:32 +0100 Subject: [PATCH 3/3] Various Fixes -- Documentation fixes with links not working. -- MENU_CLIENT_COMMAND had a small glitch, fixed that one too. -- Implemented new Event Dispatcher. -- --- Moose Development/Moose/AI/AI_Balancer.lua | 23 +- Moose Development/Moose/AI/AI_CAP.lua | 7 +- Moose Development/Moose/AI/AI_CAS.lua | 8 +- Moose Development/Moose/AI/AI_Cargo.lua | 25 +- Moose Development/Moose/AI/AI_Patrol.lua | 10 +- .../Moose/Actions/Act_Account.lua | 2 +- .../Moose/Actions/Act_Assign.lua | 2 +- .../Moose/Actions/Act_Assist.lua | 2 +- Moose Development/Moose/Actions/Act_Route.lua | 2 +- Moose Development/Moose/Core/Base.lua | 18 +- Moose Development/Moose/Core/Database.lua | 2 +- Moose Development/Moose/Core/Event.lua | 2 +- Moose Development/Moose/Core/Fsm.lua | 4 +- Moose Development/Moose/Core/Menu.lua | 67 +- Moose Development/Moose/Core/Message.lua | 10 +- Moose Development/Moose/Core/Point.lua | 22 +- .../Moose/Core/ScheduleDispatcher.lua | 2 +- Moose Development/Moose/Core/Scheduler.lua | 18 +- Moose Development/Moose/Core/Set.lua | 60 +- Moose Development/Moose/Core/Zone.lua | 102 +- .../Moose/Functional/AirbasePolice.lua | 8 +- .../Moose/Functional/CleanUp.lua | 4 +- .../Moose/Functional/Detection.lua | 60 +- Moose Development/Moose/Functional/Escort.lua | 2 +- .../Moose/Functional/MissileTrainer.lua | 2 +- Moose Development/Moose/Functional/Spawn.lua | 37 +- .../Moose/Tasking/DetectionManager.lua | 32 +- Moose Development/Moose/Tasking/Task.lua | 2 +- Moose Development/Moose/Tasking/Task_A2G.lua | 6 +- .../Moose/Tasking/Task_Pickup.lua | 6 +- Moose Development/Moose/Tasking/Task_SEAD.lua | 6 +- Moose Development/Moose/Wrapper/Airbase.lua | 4 +- Moose Development/Moose/Wrapper/Client.lua | 13 +- .../Moose/Wrapper/Controllable.lua | 20 +- Moose Development/Moose/Wrapper/Group.lua | 21 +- .../Moose/Wrapper/Identifiable.lua | 2 +- Moose Development/Moose/Wrapper/Object.lua | 8 +- .../Moose/Wrapper/Positionable.lua | 18 +- Moose Development/Moose/Wrapper/Static.lua | 4 +- Moose Development/Moose/Wrapper/Unit.lua | 10 +- .../l10n/DEFAULT/Moose.lua | 31770 +++++++++++++++- Moose Mission Setup/Moose.lua | 31770 +++++++++++++++- Moose Presentations/SPAWN.pptx | Bin 5137485 -> 5137369 bytes .../AIB-001 - Spawned AI.lua | 1 + .../DET-001 - Detection Areas.lua | 22 +- .../DET-001 - Detection Areas.miz | Bin 25089 -> 220336 bytes .../DET-101 - Detection Reporting.lua | 4 +- .../DET-101 - Detection Reporting.miz | Bin 239372 -> 240353 bytes .../SPA-130 - Uncontrolled Spawning.lua | 47 + .../SPA-130 - Uncontrolled Spawning.miz | Bin 0 -> 220318 bytes .../TSK-010 - Task Modelling - SEAD.miz | Bin 233956 -> 226749 bytes .../TSK-020 - Task Modelling - Pickup.miz | Bin 229006 -> 229254 bytes docs/Documentation/AI_Balancer.html | 26 +- docs/Documentation/AI_Cap.html | 18 +- docs/Documentation/AI_Cas.html | 19 +- docs/Documentation/AI_Patrol.html | 45 +- docs/Documentation/Account.html | 3 +- docs/Documentation/Airbase.html | 5 +- docs/Documentation/AirbasePolice.html | 9 +- docs/Documentation/Assign.html | 3 +- docs/Documentation/Base.html | 94 +- docs/Documentation/Cargo.html | 38 +- docs/Documentation/CleanUp.html | 9 +- docs/Documentation/Client.html | 26 +- docs/Documentation/CommandCenter.html | 1 + docs/Documentation/Controllable.html | 25 +- docs/Documentation/Database.html | 3 +- docs/Documentation/Detection.html | 65 +- docs/Documentation/DetectionManager.html | 1101 + docs/Documentation/Escort.html | 3 +- docs/Documentation/Event.html | 35 +- docs/Documentation/Fsm.html | 8 +- docs/Documentation/Group.html | 25 +- docs/Documentation/Identifiable.html | 3 +- docs/Documentation/MOVEMENT.html | 1 + docs/Documentation/Menu.html | 70 +- docs/Documentation/Message.html | 11 +- docs/Documentation/MissileTrainer.html | 3 +- docs/Documentation/Mission.html | 13 +- docs/Documentation/Object.html | 9 +- docs/Documentation/Point.html | 25 +- docs/Documentation/Positionable.html | 27 +- docs/Documentation/Process_JTAC.html | 1 + docs/Documentation/Process_Pickup.html | 1 + docs/Documentation/Route.html | 3 +- docs/Documentation/ScheduleDispatcher.html | 3 +- docs/Documentation/Scheduler.html | 19 +- docs/Documentation/Scoring.html | 1 + docs/Documentation/Sead.html | 1 + docs/Documentation/Set.html | 77 +- docs/Documentation/Smoke.html | 3 +- docs/Documentation/Spawn.html | 52 +- docs/Documentation/Static.html | 5 +- docs/Documentation/Task.html | 15 +- docs/Documentation/Task_A2G.html | 7 +- docs/Documentation/Task_PICKUP.html | 7 +- docs/Documentation/Task_SEAD.html | 7 +- docs/Documentation/Unit.html | 11 +- docs/Documentation/Utils.html | 1 + docs/Documentation/Zone.html | 146 +- docs/Documentation/index.html | 55 +- docs/Documentation/routines.html | 1 + docs/Presentations/AI_CARGO/CARGO.JPG | Bin 0 -> 197358 bytes docs/Presentations/SPAWN/SPAWN.JPG | Bin 0 -> 249941 bytes 104 files changed, 65657 insertions(+), 749 deletions(-) create mode 100644 Moose Test Missions/SPA - Spawning/SPA-130 - Uncontrolled Spawning/SPA-130 - Uncontrolled Spawning.lua create mode 100644 Moose Test Missions/SPA - Spawning/SPA-130 - Uncontrolled Spawning/SPA-130 - Uncontrolled Spawning.miz create mode 100644 docs/Documentation/DetectionManager.html create mode 100644 docs/Presentations/AI_CARGO/CARGO.JPG create mode 100644 docs/Presentations/SPAWN/SPAWN.JPG diff --git a/Moose Development/Moose/AI/AI_Balancer.lua b/Moose Development/Moose/AI/AI_Balancer.lua index 52adb5978..c866dd48b 100644 --- a/Moose Development/Moose/AI/AI_Balancer.lua +++ b/Moose Development/Moose/AI/AI_Balancer.lua @@ -3,17 +3,16 @@ -- even when there are hardly any players in the mission.** -- -- ![Banner Image](..\Presentations\AI_Balancer\Dia1.JPG) --- --- +-- -- === -- --- # 1) @{AI.AI_Balancer#AI_BALANCER} class, extends @{Core.Fsm#FSM_SET} +-- # 1) @{AI_Balancer#AI_BALANCER} class, extends @{Fsm#FSM_SET} -- --- The @{AI.AI_Balancer#AI_BALANCER} class monitors and manages as many replacement AI groups as there are +-- 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 @{Core.Fsm#FSM_SET} manages the functionality to control the Finite State Machine (FSM). +-- 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. -- @@ -55,8 +54,8 @@ -- However, there are 2 additional options that you can use to customize the destroy behaviour. -- When a human player joins a slot, you can configure to let the AI return to: -- --- * @{#AI_BALANCER.ReturnToHomeAirbase}: Returns the AI to the **home** @{Wrapper.Airbase#AIRBASE}. --- * @{#AI_BALANCER.ReturnToNearestAirbases}: Returns the AI to the **nearest friendly** @{Wrapper.Airbase#AIRBASE}. +-- * @{#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. @@ -152,10 +151,10 @@ function AI_BALANCER:InitSpawnInterval( Earliest, Latest ) return self end ---- Returns the AI to the nearest friendly @{Wrapper.Airbase#AIRBASE}. +--- Returns the AI to the nearest friendly @{Airbase#AIRBASE}. -- @param #AI_BALANCER self --- @param Dcs.DCSTypes#Distance ReturnTresholdRange If there is an enemy @{Wrapper.Client#CLIENT} within the ReturnTresholdRange given in meters, the AI will not return to the nearest @{Wrapper.Airbase#AIRBASE}. --- @param Core.Set#SET_AIRBASE ReturnAirbaseSet The SET of @{Core.Set#SET_AIRBASE}s to evaluate where to return to. +-- @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 @@ -163,9 +162,9 @@ function AI_BALANCER:ReturnToNearestAirbases( ReturnTresholdRange, ReturnAirbase self.ReturnAirbaseSet = ReturnAirbaseSet end ---- Returns the AI to the home @{Wrapper.Airbase#AIRBASE}. +--- Returns the AI to the home @{Airbase#AIRBASE}. -- @param #AI_BALANCER self --- @param Dcs.DCSTypes#Distance ReturnTresholdRange If there is an enemy @{Wrapper.Client#CLIENT} within the ReturnTresholdRange given in meters, the AI will not return to the nearest @{Wrapper.Airbase#AIRBASE}. +-- @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 diff --git a/Moose Development/Moose/AI/AI_CAP.lua b/Moose Development/Moose/AI/AI_CAP.lua index 94dab5dd7..5c7d2f3c9 100644 --- a/Moose Development/Moose/AI/AI_CAP.lua +++ b/Moose Development/Moose/AI/AI_CAP.lua @@ -2,10 +2,9 @@ -- -- ![Banner Image](..\Presentations\AI_CAP\Dia1.JPG) -- --- -- === -- --- # 1) @{#AI_CAP_ZONE} class, extends @{AI.AI_CAP#AI_PATROL_ZONE} +-- # 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. @@ -71,7 +70,7 @@ -- that will define when the AI will engage with the detected airborne enemy targets. -- The range can be beyond or smaller than the range of the Patrol Zone. -- The range is applied at the position of the AI. --- Use the method @{AI.AI_CAP#AI_CAP_ZONE.SetEngageRange}() to define that range. +-- Use the method @{AI_CAP#AI_CAP_ZONE.SetEngageRange}() to define that range. -- -- ## 1.4) Set the Zone of Engagement -- @@ -79,7 +78,7 @@ -- -- An optional @{Zone} can be set, -- that will define when the AI will engage with the detected airborne enemy targets. --- Use the method @{AI.AI_Cap#AI_CAP_ZONE.SetEngageZone}() to define that Zone. +-- Use the method @{AI_Cap#AI_CAP_ZONE.SetEngageZone}() to define that Zone. -- -- ==== -- diff --git a/Moose Development/Moose/AI/AI_CAS.lua b/Moose Development/Moose/AI/AI_CAS.lua index adeaddfd3..988ed11f8 100644 --- a/Moose Development/Moose/AI/AI_CAS.lua +++ b/Moose Development/Moose/AI/AI_CAS.lua @@ -1,13 +1,13 @@ ---- Single-Player:**Yes** / Mulit-Player:**Yes** / AI:**Yes** / Human:**No** / Types:**Air** -- **Provide Close Air Support to friendly ground troops.** +--- Single-Player:**Yes** / Mulit-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.AI_Patrol#AI_PATROL_ZONE} +-- # 1) @{#AI_CAS_ZONE} class, extends @{AI_Patrol#AI_PATROL_ZONE} -- --- @{#AI_CAS_ZONE} derives from the @{AI.AI_Patrol#AI_PATROL_ZONE}, inheriting its methods and behaviour. +-- @{#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. diff --git a/Moose Development/Moose/AI/AI_Cargo.lua b/Moose Development/Moose/AI/AI_Cargo.lua index 903181661..63c6e4117 100644 --- a/Moose Development/Moose/AI/AI_Cargo.lua +++ b/Moose Development/Moose/AI/AI_Cargo.lua @@ -1,4 +1,7 @@ ---- Single-Player:Yes / Mulit-Player:Yes / AI:Yes / Human:No / Types:Ground -- Management of logical cargo objects, that can be transported from and to transportation carriers. +---Single-Player:**Yes** / Mulit-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) -- -- === -- @@ -12,8 +15,8 @@ -- -- * AI_CARGO_GROUPED, represented by a Group of CARGO_UNITs. -- --- 1) @{AI.AI_Cargo#AI_CARGO} class, extends @{Core.Fsm#FSM_PROCESS} --- ========================================================================== +-- # 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. -- @@ -52,13 +55,13 @@ -- 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 --- ==================== +-- # 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 --- ======================= +-- # 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. -- @@ -90,14 +93,14 @@ -- The cargo must be in the **Loaded** state. -- @function [parent=#AI_CARGO] UnBoard -- @param #AI_CARGO self --- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Core.Point#POINT_VEC2) to where the cargo should run after onboarding. If not provided, the cargo will run to 60 meters behind the Carrier location. +-- @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) @{Core.Point#POINT_VEC2) to where the cargo should run after onboarding. If not provided, the cargo will run to 60 meters behind the Carrier location. +-- @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 @@ -122,14 +125,14 @@ -- The cargo must be in the **Loaded** state. -- @function [parent=#AI_CARGO] UnLoad -- @param #AI_CARGO self --- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Core.Point#POINT_VEC2) to where the cargo will be placed after unloading. If not provided, the cargo will be placed 60 meters behind the Carrier location. +-- @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) @{Core.Point#POINT_VEC2) to where the cargo will be placed after unloading. If not provided, the cargo will be placed 60 meters behind the Carrier location. +-- @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 diff --git a/Moose Development/Moose/AI/AI_Patrol.lua b/Moose Development/Moose/AI/AI_Patrol.lua index 43a101b01..48e27b2ba 100644 --- a/Moose Development/Moose/AI/AI_Patrol.lua +++ b/Moose Development/Moose/AI/AI_Patrol.lua @@ -1,11 +1,11 @@ ---- Single-Player:**Yes** / Mulit-Player:**Yes** / AI:**Yes** / Human:**No** / Types:**Air** -- **Air Patrolling or Staging.** +--- Single-Player:**Yes** / Mulit-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 @{Core.Fsm#FSM_CONTROLLABLE} +-- # 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}. -- @@ -524,11 +524,11 @@ function AI_PATROL_ZONE:SetDetectionZone( DetectionZone ) end end ---- Gets a list of @{Wrapper.Unit#UNIT}s that were detected by the AI. +--- 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 @{Wrapper.Unit#UNIT}s +-- @return #table The list of @{Unit#UNIT}s function AI_PATROL_ZONE:GetDetectedUnits() self:F2() diff --git a/Moose Development/Moose/Actions/Act_Account.lua b/Moose Development/Moose/Actions/Act_Account.lua index 684d1742d..18854aa18 100644 --- a/Moose Development/Moose/Actions/Act_Account.lua +++ b/Moose Development/Moose/Actions/Act_Account.lua @@ -2,7 +2,7 @@ -- -- === -- --- # @{#ACT_ACCOUNT} FSM class, extends @{Core.Fsm#FSM_PROCESS} +-- # @{#ACT_ACCOUNT} FSM class, extends @{Fsm#FSM_PROCESS} -- -- ## ACT_ACCOUNT state machine: -- diff --git a/Moose Development/Moose/Actions/Act_Assign.lua b/Moose Development/Moose/Actions/Act_Assign.lua index aa3162ffb..e78ca3b62 100644 --- a/Moose Development/Moose/Actions/Act_Assign.lua +++ b/Moose Development/Moose/Actions/Act_Assign.lua @@ -2,7 +2,7 @@ -- -- === -- --- # @{#ACT_ASSIGN} FSM template class, extends @{Core.Fsm#FSM_PROCESS} +-- # @{#ACT_ASSIGN} FSM template class, extends @{Fsm#FSM_PROCESS} -- -- ## ACT_ASSIGN state machine: -- diff --git a/Moose Development/Moose/Actions/Act_Assist.lua b/Moose Development/Moose/Actions/Act_Assist.lua index d10e29953..23c5fc056 100644 --- a/Moose Development/Moose/Actions/Act_Assist.lua +++ b/Moose Development/Moose/Actions/Act_Assist.lua @@ -2,7 +2,7 @@ -- -- === -- --- # @{#ACT_ASSIST} FSM class, extends @{Core.Fsm#FSM_PROCESS} +-- # @{#ACT_ASSIST} FSM class, extends @{Fsm#FSM_PROCESS} -- -- ## ACT_ASSIST state machine: -- diff --git a/Moose Development/Moose/Actions/Act_Route.lua b/Moose Development/Moose/Actions/Act_Route.lua index 1efef0ef8..53fa5d9b9 100644 --- a/Moose Development/Moose/Actions/Act_Route.lua +++ b/Moose Development/Moose/Actions/Act_Route.lua @@ -2,7 +2,7 @@ -- -- === -- --- # @{#ACT_ROUTE} FSM class, extends @{Core.Fsm#FSM_PROCESS} +-- # @{#ACT_ROUTE} FSM class, extends @{Fsm#FSM_PROCESS} -- -- ## ACT_ROUTE state machine: -- diff --git a/Moose Development/Moose/Core/Base.lua b/Moose Development/Moose/Core/Base.lua index c5fe0394c..a25db9edb 100644 --- a/Moose Development/Moose/Core/Base.lua +++ b/Moose Development/Moose/Core/Base.lua @@ -13,8 +13,8 @@ -- -- ## 1.1) BASE constructor -- --- Any class derived from BASE, must use the @{Core.Base#BASE.New) constructor within the @{Core.Base#BASE.Inherit) method. --- See an example at the @{Core.Base#BASE.New} method how this is done. +-- 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 -- @@ -84,8 +84,8 @@ -- * @{#BASE.EventOnTakeOff}(): Handle the event when a unit takes off from a runway. -- * @{#BASE.EventOnTookControl}(): Handle the event when a player takes control of a unit. -- --- The EventOn() methods provide the @{Core.Event#EVENTDATA} structure to the event handling function. --- The @{Core.Event#EVENTDATA} structure contains an enriched data set of information about the event being handled. +-- The EventOn() methods provide the @{Event#EVENTDATA} structure to the event handling function. +-- The @{Event#EVENTDATA} structure contains an enriched data set of information about the event being handled. -- -- Find below an example of the prototype how to write an event handling function: -- @@ -105,7 +105,7 @@ -- Note the function( self, EventData ). It takes two parameters: -- -- * self = the object that is handling the EventOnPlayerEnterUnit. --- * EventData = the @{Core.Event#EVENTDATA} structure, containing more information of the Event. +-- * EventData = the @{Event#EVENTDATA} structure, containing more information of the Event. -- -- ## 1.4) Class identification methods -- @@ -288,20 +288,20 @@ function BASE:GetClassID() return self.ClassID end ---- Get the Class @{Core.Event} processing Priority. +--- 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 @{Core.Event} processing Priority. +-- @return #number The @{Event} processing Priority. function BASE:GetEventPriority() return self._Private.EventPriority or 5 end ---- Set the Class @{Core.Event} processing Priority. +--- 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 @{Core.Event} processing Priority. +-- @param #number EventPriority The @{Event} processing Priority. -- @return self function BASE:SetEventPriority( EventPriority ) self._Private.EventPriority = EventPriority diff --git a/Moose Development/Moose/Core/Database.lua b/Moose Development/Moose/Core/Database.lua index 1277a1ba4..e40bcdf9d 100644 --- a/Moose Development/Moose/Core/Database.lua +++ b/Moose Development/Moose/Core/Database.lua @@ -2,7 +2,7 @@ -- -- ==== -- --- 1) @{Core.Database#DATABASE} class, extends @{Core.Base#BASE} +-- 1) @{#DATABASE} class, extends @{Base#BASE} -- =================================================== -- Mission designers can use the DATABASE class to refer to: -- diff --git a/Moose Development/Moose/Core/Event.lua b/Moose Development/Moose/Core/Event.lua index 6f2c82d4b..bb341f103 100644 --- a/Moose Development/Moose/Core/Event.lua +++ b/Moose Development/Moose/Core/Event.lua @@ -157,7 +157,7 @@ function EVENT:Remove( EventClass, EventID ) self.Events[EventID][EventPriority][EventClass] = nil end ---- Clears all event subscriptions for a @{Core.Base#BASE} derived object. +--- Clears all event subscriptions for a @{Base#BASE} derived object. -- @param #EVENT self -- @param Core.Base#BASE EventObject function EVENT:RemoveAll( EventObject ) diff --git a/Moose Development/Moose/Core/Fsm.lua b/Moose Development/Moose/Core/Fsm.lua index acf7a73ce..fe4ad1d95 100644 --- a/Moose Development/Moose/Core/Fsm.lua +++ b/Moose Development/Moose/Core/Fsm.lua @@ -3,6 +3,8 @@ -- -- ![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**. @@ -46,7 +48,7 @@ -- -- === -- --- # 1) @{Core.Fsm#FSM} class, extends @{Core.Base#BASE} +-- # 1) @{#FSM} class, extends @{Base#BASE} -- -- ![Transition Rules and Transition Handlers and Event Triggers](..\Presentations\FSM\Dia3.JPG) -- diff --git a/Moose Development/Moose/Core/Menu.lua b/Moose Development/Moose/Core/Menu.lua index 5d4ab402e..6defc55d5 100644 --- a/Moose Development/Moose/Core/Menu.lua +++ b/Moose Development/Moose/Core/Menu.lua @@ -1,7 +1,5 @@ --- This module contains the MENU classes. -- --- There is a small note... When you see a class like MENU_COMMAND_COALITION with COMMAND in italic, it acutally represents it like this: `MENU_COMMAND_COALITION`. --- -- === -- -- DCS Menus can be managed using the MENU classes. @@ -15,17 +13,17 @@ -- -- ### To manage **main menus**, the classes begin with **MENU_**: -- --- * @{Core.Menu#MENU_MISSION}: Manages main menus for whole mission file. --- * @{Core.Menu#MENU_COALITION}: Manages main menus for whole coalition. --- * @{Core.Menu#MENU_GROUP}: Manages main menus for GROUPs. --- * @{Core.Menu#MENU_CLIENT}: Manages main menus for CLIENTs. This manages menus for units with the skill level "Client". +-- * @{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_**: -- --- * @{Core.Menu#MENU_MISSION_COMMAND}: Manages command menus for whole mission file. --- * @{Core.Menu#MENU_COALITION_COMMAND}: Manages command menus for whole coalition. --- * @{Core.Menu#MENU_GROUP_COMMAND}: Manages command menus for GROUPs. --- * @{Core.Menu#MENU_CLIENT_COMMAND}: Manages command menus for CLIENTs. This manages menus for units with the skill level "Client". +-- * @{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". -- -- === -- @@ -37,11 +35,11 @@ -- These are simply abstract base classes defining a couple of fields that are used by the -- derived MENU_ classes to manage menus. -- --- 1.1) @{Core.Menu#MENU_BASE} class, extends @{Core.Base#BASE} +-- 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) @{Core.Menu#MENU_COMMAND_BASE} class, extends @{Core.Base#BASE} +-- 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. -- @@ -53,15 +51,15 @@ -- ====================== -- The underlying classes manage the menus for a complete mission file. -- --- 2.1) @{Menu#MENU_MISSION} class, extends @{Core.Menu#MENU_BASE} +-- 2.1) @{#MENU_MISSION} class, extends @{Menu#MENU_BASE} -- --------------------------------------------------------- --- The @{Core.Menu#MENU_MISSION} class manages the main menus for a complete mission. +-- 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#MENU_MISSION_COMMAND} class, extends @{Core.Menu#MENU_COMMAND_BASE} +-- 2.2) @{#MENU_MISSION_COMMAND} class, extends @{Menu#MENU_COMMAND_BASE} -- ------------------------------------------------------------------------- --- The @{Core.Menu#MENU_MISSION_COMMAND} class manages the command menus for a complete mission, which allow players to execute functions during mission execution. +-- 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}. -- @@ -71,15 +69,15 @@ -- ========================= -- The underlying classes manage the menus for whole coalitions. -- --- 3.1) @{Menu#MENU_COALITION} class, extends @{Core.Menu#MENU_BASE} +-- 3.1) @{#MENU_COALITION} class, extends @{Menu#MENU_BASE} -- ------------------------------------------------------------ --- The @{Core.Menu#MENU_COALITION} class manages the main menus for coalitions. +-- 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 @{Core.Menu#MENU_COMMAND_BASE} +-- 3.2) @{Menu#MENU_COALITION_COMMAND} class, extends @{Menu#MENU_COMMAND_BASE} -- ---------------------------------------------------------------------------- --- The @{Core.Menu#MENU_COALITION_COMMAND} class manages the command menus for coalitions, which allow players to execute functions during mission execution. +-- 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}. -- @@ -89,15 +87,15 @@ -- ===================== -- 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 @{Core.Menu#MENU_BASE} +-- 4.1) @{Menu#MENU_GROUP} class, extends @{Menu#MENU_BASE} -- -------------------------------------------------------- --- The @{Core.Menu#MENU_GROUP} class manages the main menus for coalitions. +-- 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 @{Core.Menu#MENU_COMMAND_BASE} +-- 4.2) @{Menu#MENU_GROUP_COMMAND} class, extends @{Menu#MENU_COMMAND_BASE} -- ------------------------------------------------------------------------ --- The @{Core.Menu#MENU_GROUP_COMMAND} class manages the command menus for coalitions, which allow players to execute functions during mission execution. +-- 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}. -- @@ -107,15 +105,15 @@ -- ====================== -- The underlying classes manage the menus for units with skill level client or player. -- --- 5.1) @{Menu#MENU_CLIENT} class, extends @{Core.Menu#MENU_BASE} +-- 5.1) @{Menu#MENU_CLIENT} class, extends @{Menu#MENU_BASE} -- --------------------------------------------------------- --- The @{Core.Menu#MENU_CLIENT} class manages the main menus for coalitions. +-- 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 @{Core.Menu#MENU_COMMAND_BASE} +-- 5.2) @{Menu#MENU_CLIENT_COMMAND} class, extends @{Menu#MENU_COMMAND_BASE} -- ------------------------------------------------------------------------- --- The @{Core.Menu#MENU_CLIENT_COMMAND} class manages the command menus for coalitions, which allow players to execute functions during mission execution. +-- 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}. -- @@ -622,9 +620,8 @@ do -- MENU_CLIENT -- @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. - -- @param CommandMenuArgument An argument for the function. -- @return Menu#MENU_CLIENT_COMMAND self - function MENU_CLIENT_COMMAND:New( MenuClient, MenuText, ParentMenu, CommandMenuFunction, ... ) + function MENU_CLIENT_COMMAND:New( Client, MenuText, ParentMenu, CommandMenuFunction, ... ) -- Arrange meta tables @@ -635,8 +632,8 @@ do -- MENU_CLIENT local self = BASE:Inherit( self, MENU_COMMAND_BASE:New( MenuText, MenuParentPath, CommandMenuFunction, arg ) ) -- Menu#MENU_CLIENT_COMMAND - self.MenuClient = MenuClient - self.MenuClientGroupID = MenuClient:GetClientGroupID() + self.MenuClient = Client + self.MenuClientGroupID = Client:GetClientGroupID() self.MenuParentPath = MenuParentPath self.MenuText = MenuText self.ParentMenu = ParentMenu @@ -647,7 +644,7 @@ do -- MENU_CLIENT local MenuPath = _MENUCLIENTS[self.MenuClientGroupID] - self:T( { MenuClient:GetClientGroupName(), MenuPath[table.concat(MenuParentPath)], MenuParentPath, MenuText, CommandMenuFunction, arg } ) + self:T( { Client:GetClientGroupName(), MenuPath[table.concat(MenuParentPath)], MenuParentPath, MenuText, CommandMenuFunction, arg } ) local MenuPathID = table.concat(MenuParentPath) .. "/" .. MenuText if MenuPath[MenuPathID] then @@ -657,7 +654,9 @@ do -- MENU_CLIENT self.MenuPath = missionCommands.addCommandForGroup( self.MenuClient:GetClientGroupID(), MenuText, MenuParentPath, self.MenuCallHandler, arg ) MenuPath[MenuPathID] = self.MenuPath - ParentMenu.Menus[self.MenuPath] = self + if ParentMenu and ParentMenu.Menus then + ParentMenu.Menus[self.MenuPath] = self + end return self end diff --git a/Moose Development/Moose/Core/Message.lua b/Moose Development/Moose/Core/Message.lua index 15a627890..08ea61bdc 100644 --- a/Moose Development/Moose/Core/Message.lua +++ b/Moose Development/Moose/Core/Message.lua @@ -1,6 +1,6 @@ --- This module contains the MESSAGE class. -- --- 1) @{Core.Message#MESSAGE} class, extends @{Core.Base#BASE} +-- 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. @@ -8,16 +8,16 @@ -- -- 1.1) MESSAGE construction methods -- --------------------------------- --- Messages are created with @{Core.Message#MESSAGE.New}. Note that when the MESSAGE object is created, no message is sent yet. +-- 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 @{Core.Message#MESSAGE.ToClient}. --- * Coalitions with @{Core.Message#MESSAGE.ToCoalition}. --- * All Players with @{Core.Message#MESSAGE.ToAll}. +-- * Clients with @{Message#MESSAGE.ToClient}. +-- * Coalitions with @{Message#MESSAGE.ToCoalition}. +-- * All Players with @{Message#MESSAGE.ToAll}. -- -- @module Message -- @author FlightControl diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index 39801e785..e31653d0e 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -1,8 +1,8 @@ --- This module contains the POINT classes. -- --- 1) @{Core.Point#POINT_VEC3} class, extends @{Core.Base#BASE} +-- 1) @{Point#POINT_VEC3} class, extends @{Base#BASE} -- ================================================== --- The @{Core.Point#POINT_VEC3} class defines a 3D point in the simulator. +-- 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. @@ -11,20 +11,20 @@ -- --------------------------- -- A new POINT_VEC3 instance can be created with: -- --- * @{#POINT_VEC3.New}(): a 3D point. --- * @{#POINT_VEC3.NewFromVec3}(): a 3D point created from a @{Dcs.DCSTypes#Vec3}. +-- * @{Point#POINT_VEC3.New}(): a 3D point. +-- * @{Point#POINT_VEC3.NewFromVec3}(): a 3D point created from a @{DCSTypes#Vec3}. -- -- --- 2) @{Core.Point#POINT_VEC2} class, extends @{Core.Point#POINT_VEC3} +-- 2) @{Point#POINT_VEC2} class, extends @{Point#POINT_VEC3} -- ========================================================= --- The @{Core.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. +-- 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_VEC2.New}(): a 2D point, taking an additional height parameter. --- * @{#POINT_VEC2.NewFromVec2}(): a 2D point created from a @{Dcs.DCSTypes#Vec2}. +-- * @{Point#POINT_VEC2.New}(): a 2D point, taking an additional height parameter. +-- * @{Point#POINT_VEC2.NewFromVec2}(): a 2D point created from a @{DCSTypes#Vec2}. -- -- === -- @@ -672,10 +672,10 @@ function POINT_VEC2:DistanceFromPointVec2( PointVec2Reference ) return Distance end ---- Calculate the distance from a reference @{Dcs.DCSTypes#Vec2}. +--- Calculate the distance from a reference @{DCSTypes#Vec2}. -- @param #POINT_VEC2 self --- @param Dcs.DCSTypes#Vec2 Vec2Reference The reference @{Dcs.DCSTypes#Vec2}. --- @return Dcs.DCSTypes#Distance The distance from the reference @{Dcs.DCSTypes#Vec2} in meters. +-- @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 ) diff --git a/Moose Development/Moose/Core/ScheduleDispatcher.lua b/Moose Development/Moose/Core/ScheduleDispatcher.lua index 277fb7b8e..5abb966b5 100644 --- a/Moose Development/Moose/Core/ScheduleDispatcher.lua +++ b/Moose Development/Moose/Core/ScheduleDispatcher.lua @@ -22,7 +22,7 @@ -- -- The SCHEDULEDISPATCHER allows multiple scheduled functions to be planned and executed for one SCHEDULER object. -- The SCHEDULER object therefore keeps a table of "CallID's", which are returned after each planning of a new scheduled function by the SCHEDULEDISPATCHER. --- The SCHEDULER object plans new scheduled functions through the @{Core.Scheduler#SCHEDULER.Schedule}() method. +-- 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. -- -- === diff --git a/Moose Development/Moose/Core/Scheduler.lua b/Moose Development/Moose/Core/Scheduler.lua index 2b68e54bd..3ea8b2e4b 100644 --- a/Moose Development/Moose/Core/Scheduler.lua +++ b/Moose Development/Moose/Core/Scheduler.lua @@ -1,28 +1,28 @@ --- This module contains the SCHEDULER class. -- --- # 1) @{Core.Scheduler#SCHEDULER} class, extends @{Core.Base#BASE} +-- # 1) @{Scheduler#SCHEDULER} class, extends @{Base#BASE} -- --- The @{Core.Scheduler#SCHEDULER} class creates schedule. +-- 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: -- --- * @{Core.Scheduler#SCHEDULER.New}( nil ): Setup a new SCHEDULER object, which is persistently executed after garbage collection. --- * @{Core.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. --- * @{Core.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. --- * @{Core.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. +-- * @{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: -- --- * @{Core.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. --- * @{Core.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. +-- * @{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 @{Core.Scheduler#SCHEDULER.Schedule}() a new time event can be scheduled. This function is used by the :New() constructor when a new schedule is planned. +-- 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. -- -- === -- diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index 1309c543c..0966b39c8 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -2,9 +2,9 @@ -- -- === -- --- 1) @{Core.Set#SET_BASE} class, extends @{Core.Base#BASE} +-- 1) @{Set#SET_BASE} class, extends @{Base#BASE} -- ============================================== --- The @{Core.Set#SET_BASE} class defines the core functions that define a collection of objects. +-- 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. @@ -12,18 +12,18 @@ -- -- 1.1) Add or remove objects from the SET -- --------------------------------------- --- Some key core functions are @{Core.Set#SET_BASE.Add} and @{Core.Set#SET_BASE.Remove} to add or remove objects from the SET in your logic. +-- 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 @{Core.Set#SET_BASE.SetInteratorIntervals} method. +-- Modify the iterator intervals with the @{Set#SET_BASE.SetInteratorIntervals} method. -- You can set the **"yield interval"**, and the **"time interval"**. (See above). -- -- === -- --- 2) @{Core.Set#SET_GROUP} class, extends @{Core.Set#SET_BASE} +-- 2) @{Set#SET_GROUP} class, extends @{Set#SET_BASE} -- ================================================== --- Mission designers can use the @{Core.Set#SET_GROUP} class to build sets of groups belonging to certain: +-- Mission designers can use the @{Set#SET_GROUP} class to build sets of groups belonging to certain: -- -- * Coalitions -- * Categories @@ -38,7 +38,7 @@ -- -- 2.2) Add or Remove GROUP(s) from SET_GROUP: -- ------------------------------------------- --- GROUPS can be added and removed using the @{Core.Set#SET_GROUP.AddGroupsByName} and @{Core.Set#SET_GROUP.RemoveGroupsByName} respectively. +-- 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: @@ -57,7 +57,7 @@ -- -- Planned filter criteria within development are (so these are not yet available): -- --- * @{#SET_GROUP.FilterZones}: Builds the SET_GROUP with the groups within a @{Core.Zone#ZONE}. +-- * @{#SET_GROUP.FilterZones}: Builds the SET_GROUP with the groups within a @{Zone#ZONE}. -- -- 2.4) SET_GROUP iterators: -- ------------------------- @@ -72,9 +72,9 @@ -- -- ==== -- --- 3) @{Core.Set#SET_UNIT} class, extends @{Core.Set#SET_BASE} +-- 3) @{Set#SET_UNIT} class, extends @{Set#SET_BASE} -- =================================================== --- Mission designers can use the @{Core.Set#SET_UNIT} class to build sets of units belonging to certain: +-- Mission designers can use the @{Set#SET_UNIT} class to build sets of units belonging to certain: -- -- * Coalitions -- * Categories @@ -90,7 +90,7 @@ -- -- 3.2) Add or Remove UNIT(s) from SET_UNIT: -- ----------------------------------------- --- UNITs can be added and removed using the @{Core.Set#SET_UNIT.AddUnitsByName} and @{Core.Set#SET_UNIT.RemoveUnitsByName} respectively. +-- 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: @@ -110,7 +110,7 @@ -- -- Planned filter criteria within development are (so these are not yet available): -- --- * @{#SET_UNIT.FilterZones}: Builds the SET_UNIT with the units within a @{Core.Zone#ZONE}. +-- * @{#SET_UNIT.FilterZones}: Builds the SET_UNIT with the units within a @{Zone#ZONE}. -- -- 3.4) SET_UNIT iterators: -- ------------------------ @@ -130,9 +130,9 @@ -- -- === -- --- 4) @{Core.Set#SET_CLIENT} class, extends @{Core.Set#SET_BASE} +-- 4) @{Set#SET_CLIENT} class, extends @{Set#SET_BASE} -- =================================================== --- Mission designers can use the @{Core.Set#SET_CLIENT} class to build sets of units belonging to certain: +-- Mission designers can use the @{Set#SET_CLIENT} class to build sets of units belonging to certain: -- -- * Coalitions -- * Categories @@ -148,7 +148,7 @@ -- -- 4.2) Add or Remove CLIENT(s) from SET_CLIENT: -- ----------------------------------------- --- CLIENTs can be added and removed using the @{Core.Set#SET_CLIENT.AddClientsByName} and @{Core.Set#SET_CLIENT.RemoveClientsByName} respectively. +-- 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: @@ -168,7 +168,7 @@ -- -- Planned filter criteria within development are (so these are not yet available): -- --- * @{#SET_CLIENT.FilterZones}: Builds the SET_CLIENT with the clients within a @{Core.Zone#ZONE}. +-- * @{#SET_CLIENT.FilterZones}: Builds the SET_CLIENT with the clients within a @{Zone#ZONE}. -- -- 4.4) SET_CLIENT iterators: -- ------------------------ @@ -180,9 +180,9 @@ -- -- ==== -- --- 5) @{Core.Set#SET_AIRBASE} class, extends @{Core.Set#SET_BASE} +-- 5) @{Set#SET_AIRBASE} class, extends @{Set#SET_BASE} -- ==================================================== --- Mission designers can use the @{Core.Set#SET_AIRBASE} class to build sets of airbases optionally belonging to certain: +-- Mission designers can use the @{Set#SET_AIRBASE} class to build sets of airbases optionally belonging to certain: -- -- * Coalitions -- @@ -194,7 +194,7 @@ -- -- 5.2) Add or Remove AIRBASEs from SET_AIRBASE -- -------------------------------------------- --- AIRBASEs can be added and removed using the @{Core.Set#SET_AIRBASE.AddAirbasesByName} and @{Core.Set#SET_AIRBASE.RemoveAirbasesByName} respectively. +-- 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 @@ -269,7 +269,7 @@ function SET_BASE:New( Database ) return self end ---- Finds an @{Core.Base#BASE} object based on the object Name. +--- Finds an @{Base#BASE} object based on the object Name. -- @param #SET_BASE self -- @param #string ObjectName -- @return Core.Base#BASE The Object found. @@ -289,7 +289,7 @@ function SET_BASE:GetSet() return self.Set end ---- Adds a @{Core.Base#BASE} object in the @{Core.Set#SET_BASE}, using a given ObjectName as the index. +--- 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 @@ -315,7 +315,7 @@ function SET_BASE:Add( ObjectName, Object ) end ---- Adds a @{Core.Base#BASE} object in the @{Core.Set#SET_BASE}, using the Object Name as the index. +--- 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. @@ -330,7 +330,7 @@ end ---- Removes a @{Core.Base#BASE} object from the @{Core.Set#SET_BASE} and derived classes, based on the Object Name. +--- 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 ) @@ -369,7 +369,7 @@ function SET_BASE:Remove( ObjectName ) end ---- Gets a @{Core.Base#BASE} object from the @{Core.Set#SET_BASE} and derived classes, based on the Object Name. +--- 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 @@ -384,7 +384,7 @@ function SET_BASE:Get( ObjectName ) end ---- Retrieves the amount of objects in the @{Core.Set#SET_BASE} and derived classes. +--- Retrieves the amount of objects in the @{Set#SET_BASE} and derived classes. -- @param #SET_BASE self -- @return #number Count function SET_BASE:Count() @@ -477,9 +477,9 @@ function SET_BASE:FilterStop() return self end ---- Iterate the SET_BASE while identifying the nearest object from a @{Core.Point#POINT_VEC2}. +--- 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 @{Core.Point#POINT_VEC2} object from where to evaluate the closest object in the set. +-- @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 ) @@ -2226,10 +2226,10 @@ function SET_AIRBASE:ForEachAirbase( IteratorFunction, ... ) return self end ---- Iterate the SET_AIRBASE while identifying the nearest @{Wrapper.Airbase#AIRBASE} from a @{Core.Point#POINT_VEC2}. +--- 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 @{Core.Point#POINT_VEC2} object from where to evaluate the closest @{Wrapper.Airbase#AIRBASE}. --- @return Wrapper.Airbase#AIRBASE The closest @{Wrapper.Airbase#AIRBASE}. +-- @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 ) diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index d8aef9bdd..d096962aa 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -1,4 +1,4 @@ ---- This module contains the ZONE classes, inherited from @{Core.Zone#ZONE_BASE}. +--- This module contains the ZONE classes, inherited from @{Zone#ZONE_BASE}. -- There are essentially two core functions that zones accomodate: -- -- * Test if an object is within the zone boundaries. @@ -7,7 +7,7 @@ -- The object classes are using the zone classes to test the zone boundaries, which can take various forms: -- -- * Test if completely within the zone. --- * Test if partly within the zone (for @{Wrapper.Group#GROUP} objects). +-- * 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. @@ -15,16 +15,16 @@ -- -- Each of these ZONE classes have a zone name, and specific parameters defining the zone type: -- --- * @{Core.Zone#ZONE_BASE}: The ZONE_BASE class defining the base for all other zone classes. --- * @{Core.Zone#ZONE_RADIUS}: The ZONE_RADIUS class defined by a zone name, a location and a radius. --- * @{Core.Zone#ZONE}: The ZONE class, defined by the zone name as defined within the Mission Editor. --- * @{Core.Zone#ZONE_UNIT}: The ZONE_UNIT class defines by a zone around a @{Wrapper.Unit#UNIT} with a radius. --- * @{Core.Zone#ZONE_GROUP}: The ZONE_GROUP class defines by a zone around a @{Wrapper.Group#GROUP} with a radius. --- * @{Core.Zone#ZONE_POLYGON}: The ZONE_POLYGON class defines by a sequence of @{Wrapper.Group#GROUP} waypoints within the Mission Editor, forming a polygon. +-- * @{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) @{Core.Zone#ZONE_BASE} class, extends @{Core.Base#BASE} +-- 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. -- @@ -32,10 +32,10 @@ -- -- * @{#ZONE_BASE.GetName}(): Returns the name of the zone. -- --- ### 1.2) Each zone implements two polymorphic functions defined in @{Core.Zone#ZONE_BASE}: +-- ### 1.2) Each zone implements two polymorphic functions defined in @{Zone#ZONE_BASE}: -- --- * @{#ZONE_BASE.IsPointVec2InZone}(): Returns if a @{Core.Point#POINT_VEC2} is within the zone. --- * @{#ZONE_BASE.IsPointVec3InZone}(): Returns if a @{Core.Point#POINT_VEC3} is within the zone. +-- * @{#ZONE_BASE.IsPointVec2InZone}(): Returns if a @{Point#POINT_VEC2} is within the zone. +-- * @{#ZONE_BASE.IsPointVec3InZone}(): Returns if a @{Point#POINT_VEC3} is within the zone. -- -- ### 1.3) A zone has a probability factor that can be set to randomize a selection between zones: -- @@ -45,8 +45,8 @@ -- -- ### 1.4) A zone manages Vectors: -- --- * @{#ZONE_BASE.GetVec2}(): Returns the @{Dcs.DCSTypes#Vec2} coordinate of the zone. --- * @{#ZONE_BASE.GetRandomVec2}(): Define a random @{Dcs.DCSTypes#Vec2} within the zone. +-- * @{#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: -- @@ -59,12 +59,12 @@ -- -- === -- --- 2) @{Core.Zone#ZONE_RADIUS} class, extends @{Core.Zone#ZONE_BASE} +-- 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) @{Core.Zone#ZONE_RADIUS} constructor: +-- ### 2.1) @{Zone#ZONE_RADIUS} constructor: -- -- * @{#ZONE_BASE.New}(): Constructor. -- @@ -75,45 +75,45 @@ -- -- ### 2.3) Manage the location of the zone: -- --- * @{#ZONE_BASE.SetVec2}(): Sets the @{Dcs.DCSTypes#Vec2} of the zone. --- * @{#ZONE_BASE.GetVec2}(): Returns the @{Dcs.DCSTypes#Vec2} of the zone. --- * @{#ZONE_BASE.GetVec3}(): Returns the @{Dcs.DCSTypes#Vec3} of the zone, taking an additional height parameter. +-- * @{#ZONE_BASE.SetVec2}(): Sets the @{DCSTypes#Vec2} of the zone. +-- * @{#ZONE_BASE.GetVec2}(): Returns the @{DCSTypes#Vec2} of the zone. +-- * @{#ZONE_BASE.GetVec3}(): Returns the @{DCSTypes#Vec3} of the zone, taking an additional height parameter. -- -- === -- --- 3) @{Core.Zone#ZONE} class, extends @{Core.Zone#ZONE_RADIUS} +-- 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) @{Core.Zone#ZONE_UNIT} class, extends @{Core.Zone#ZONE_RADIUS} +-- 4) @{Zone#ZONE_UNIT} class, extends @{Zone#ZONE_RADIUS} -- ======================================================= --- The ZONE_UNIT class defined by a zone around a @{Wrapper.Unit#UNIT} with a radius. --- This class implements the inherited functions from @{Core.Zone#ZONE_RADIUS} taking into account the own zone format and properties. +-- 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) @{Core.Zone#ZONE_GROUP} class, extends @{Core.Zone#ZONE_RADIUS} +-- 5) @{Zone#ZONE_GROUP} class, extends @{Zone#ZONE_RADIUS} -- ======================================================= --- The ZONE_GROUP class defines by a zone around a @{Wrapper.Group#GROUP} with a radius. The current leader of the group defines the center of the zone. --- This class implements the inherited functions from @{Core.Zone#ZONE_RADIUS} taking into account the own zone format and properties. +-- 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) @{Core.Zone#ZONE_POLYGON_BASE} class, extends @{Core.Zone#ZONE_BASE} +-- 6) @{Zone#ZONE_POLYGON_BASE} class, extends @{Zone#ZONE_BASE} -- ======================================================== --- The ZONE_POLYGON_BASE class defined by a sequence of @{Wrapper.Group#GROUP} waypoints within the Mission Editor, forming a polygon. --- This class implements the inherited functions from @{Core.Zone#ZONE_RADIUS} taking into account the own zone format and properties. +-- 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. -- -- === -- --- 7) @{Core.Zone#ZONE_POLYGON} class, extends @{Core.Zone#ZONE_POLYGON_BASE} +-- 7) @{Zone#ZONE_POLYGON} class, extends @{Zone#ZONE_POLYGON_BASE} -- ================================================================ --- The ZONE_POLYGON class defined by a sequence of @{Wrapper.Group#GROUP} waypoints within the Mission Editor, forming a polygon. --- This class implements the inherited functions from @{Core.Zone#ZONE_RADIUS} taking into account the own zone format and properties. +-- The ZONE_POLYGON class defined by a sequence of @{Group#GROUP} waypoints within the Mission Editor, forming a polygon. +-- This class implements the inherited functions from @{Zone#ZONE_RADIUS} taking into account the own zone format and properties. -- -- ==== -- @@ -204,7 +204,7 @@ function ZONE_BASE:IsPointVec3InZone( Vec3 ) return InZone end ---- Returns the @{Dcs.DCSTypes#Vec2} coordinate of the zone. +--- Returns the @{DCSTypes#Vec2} coordinate of the zone. -- @param #ZONE_BASE self -- @return #nil. function ZONE_BASE:GetVec2() @@ -212,7 +212,7 @@ function ZONE_BASE:GetVec2() return nil end ---- Define a random @{Dcs.DCSTypes#Vec2} within the zone. +--- Define a random @{DCSTypes#Vec2} within the zone. -- @param #ZONE_BASE self -- @return Dcs.DCSTypes#Vec2 The Vec2 coordinates. function ZONE_BASE:GetRandomVec2() @@ -374,7 +374,7 @@ function ZONE_RADIUS:SetRadius( Radius ) return self.Radius end ---- Returns the @{Dcs.DCSTypes#Vec2} of the zone. +--- Returns the @{DCSTypes#Vec2} of the zone. -- @param #ZONE_RADIUS self -- @return Dcs.DCSTypes#Vec2 The location of the zone. function ZONE_RADIUS:GetVec2() @@ -385,7 +385,7 @@ function ZONE_RADIUS:GetVec2() return self.Vec2 end ---- Sets the @{Dcs.DCSTypes#Vec2} of the zone. +--- 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. @@ -399,7 +399,7 @@ function ZONE_RADIUS:SetVec2( Vec2 ) return self.Vec2 end ---- Returns the @{Dcs.DCSTypes#Vec3} of the ZONE_RADIUS. +--- 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. @@ -501,7 +501,7 @@ function ZONE:New( ZoneName ) end ---- The ZONE_UNIT class defined by a zone around a @{Wrapper.Unit#UNIT} with a radius. +--- 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 @@ -526,9 +526,9 @@ function ZONE_UNIT:New( ZoneName, ZoneUNIT, Radius ) end ---- Returns the current location of the @{Wrapper.Unit#UNIT}. +--- Returns the current location of the @{Unit#UNIT}. -- @param #ZONE_UNIT self --- @return Dcs.DCSTypes#Vec2 The location of the zone based on the @{Wrapper.Unit#UNIT}location. +-- @return Dcs.DCSTypes#Vec2 The location of the zone based on the @{Unit#UNIT}location. function ZONE_UNIT:GetVec2() self:F( self.ZoneName ) @@ -567,7 +567,7 @@ function ZONE_UNIT:GetRandomVec2() return RandomVec2 end ---- Returns the @{Dcs.DCSTypes#Vec3} of the ZONE_UNIT. +--- 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. @@ -593,7 +593,7 @@ ZONE_GROUP = { ClassName="ZONE_GROUP", } ---- Constructor to create a ZONE_GROUP instance, taking the zone name, a zone @{Wrapper.Group#GROUP} and a radius. +--- 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. @@ -644,9 +644,9 @@ end -- Polygons ---- The ZONE_POLYGON_BASE class defined by an array of @{Dcs.DCSTypes#Vec2}, forming a polygon. +--- 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 @{Dcs.DCSTypes#Vec2}. +-- @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", @@ -656,11 +656,11 @@ ZONE_POLYGON_BASE = { -- @type ZONE_POLYGON_BASE.ListVec2 -- @list ---- Constructor to create a ZONE_POLYGON_BASE instance, taking the zone name and an array of @{Dcs.DCSTypes#Vec2}, forming a polygon. --- The @{Wrapper.Group#GROUP} waypoints define the polygon corners. The first and the last point are automatically connected. +--- Constructor to create a ZONE_POLYGON_BASE instance, taking the zone name and an array of @{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 @{Dcs.DCSTypes#Vec2}, forming a polygon.. +-- @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 ) ) @@ -757,7 +757,7 @@ function ZONE_POLYGON_BASE:IsPointVec2InZone( Vec2 ) return InPolygon end ---- Define a random @{Dcs.DCSTypes#Vec2} within the zone. +--- 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() @@ -809,15 +809,15 @@ end ---- The ZONE_POLYGON class defined by a sequence of @{Wrapper.Group#GROUP} waypoints within the Mission Editor, forming a polygon. +--- 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 @{Wrapper.Group#GROUP} defined within the Mission Editor. --- The @{Wrapper.Group#GROUP} waypoints define the polygon corners. The first and the last point are automatically connected by ZONE_POLYGON. +--- 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. diff --git a/Moose Development/Moose/Functional/AirbasePolice.lua b/Moose Development/Moose/Functional/AirbasePolice.lua index a85014785..e0b14d61b 100644 --- a/Moose Development/Moose/Functional/AirbasePolice.lua +++ b/Moose Development/Moose/Functional/AirbasePolice.lua @@ -2,9 +2,9 @@ -- -- === -- --- 1) @{Functional.AirbasePolice#AIRBASEPOLICE_BASE} class, extends @{Core.Base#BASE} +-- 1) @{AirbasePolice#AIRBASEPOLICE_BASE} class, extends @{Base#BASE} -- ================================================================== --- The @{Functional.AirbasePolice#AIRBASEPOLICE_BASE} class provides the main methods to monitor CLIENT behaviour at airbases. +-- 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. @@ -12,7 +12,7 @@ -- * Avoid to hit other planes on the airbase. -- * Obey ground control orders. -- --- 2) @{Functional.AirbasePolice#AIRBASEPOLICE_CAUCASUS} class, extends @{Functional.AirbasePolice#AIRBASEPOLICE_BASE} +-- 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. @@ -39,7 +39,7 @@ -- * TbilisiLochini -- * Vaziani -- --- 3) @{Functional.AirbasePolice#AIRBASEPOLICE_NEVADA} class, extends @{Functional.AirbasePolice#AIRBASEPOLICE_BASE} +-- 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. diff --git a/Moose Development/Moose/Functional/CleanUp.lua b/Moose Development/Moose/Functional/CleanUp.lua index c9fb36b23..feb6a6d55 100644 --- a/Moose Development/Moose/Functional/CleanUp.lua +++ b/Moose Development/Moose/Functional/CleanUp.lua @@ -62,7 +62,7 @@ function CLEANUP:_DestroyGroup( GroupObject, CleanUpGroupName ) end end ---- Destroys a @{Dcs.DCSWrapper.Unit#Unit} from the simulator, but checks first if it is still existing! +--- 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 ... @@ -199,7 +199,7 @@ function CLEANUP:_EventHitCleanUp( Event ) end end ---- Add the @{Dcs.DCSWrapper.Unit#Unit} to the CleanUpList for CleanUp. +--- Add the @{DCSWrapper.Unit#Unit} to the CleanUpList for CleanUp. function CLEANUP:_AddForCleanUp( CleanUpUnit, CleanUpUnitName ) self:F( { CleanUpUnit, CleanUpUnitName } ) diff --git a/Moose Development/Moose/Functional/Detection.lua b/Moose Development/Moose/Functional/Detection.lua index bd5df0de6..e458e8cb5 100644 --- a/Moose Development/Moose/Functional/Detection.lua +++ b/Moose Development/Moose/Functional/Detection.lua @@ -2,14 +2,14 @@ -- -- === -- --- 1) @{Functional.Detection#DETECTION_BASE} class, extends @{Core.Base#BASE} +-- 1) @{Detection#DETECTION_BASE} class, extends @{Base#BASE} -- ========================================================== --- The @{Functional.Detection#DETECTION_BASE} class defines the core functions to administer detected objects. --- The @{Functional.Detection#DETECTION_BASE} class will detect objects within the battle zone for a list of @{Group}s detecting targets following (a) detection method(s). +-- 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 @{Functional.Detection#DETECTION_BASE.New}() method. +-- Construct a new DETECTION_BASE instance using the @{Detection#DETECTION_BASE.New}() method. -- -- 1.2) DETECTION_BASE initialization -- ---------------------------------- @@ -20,46 +20,46 @@ -- -- Use the following functions to report the objects it detected using the methods Visual, Optical, Radar, IRST, RWR, DLINK: -- --- * @{Functional.Detection#DETECTION_BASE.InitDetectVisual}(): Detected using Visual. --- * @{Functional.Detection#DETECTION_BASE.InitDetectOptical}(): Detected using Optical. --- * @{Functional.Detection#DETECTION_BASE.InitDetectRadar}(): Detected using Radar. --- * @{Functional.Detection#DETECTION_BASE.InitDetectIRST}(): Detected using IRST. --- * @{Functional.Detection#DETECTION_BASE.InitDetectRWR}(): Detected using RWR. --- * @{Functional.Detection#DETECTION_BASE.InitDetectDLINK}(): Detected using 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 @{Core.Set#SET_BASE}s can be retrieved using the method @{Functional.Detection#DETECTION_BASE.GetDetectedSets}(). --- The method will return a list (table) of @{Core.Set#SET_BASE} objects. +-- 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) @{Functional.Detection#DETECTION_AREAS} class, extends @{Functional.Detection#DETECTION_BASE} +-- 2) @{Detection#DETECTION_AREAS} class, extends @{Detection#DETECTION_BASE} -- =============================================================================== --- The @{Functional.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 @{Core.Set#SET_UNIT}s containing the @{Wrapper.Unit#UNIT}s detected. +-- 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 @{Functional.Detection#DECTECTION_BASE} and the DetectedZones methods is implemented in @{Functional.Detection#DETECTION_AREAS}. +-- 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 @{Functional.Detection#DETECTION_BASE.GetDetectedSets}(). A table will be return of @{Core.Set#SET_UNIT}s. --- To understand the amount of sets created, use the method @{Functional.Detection#DETECTION_BASE.GetDetectedSetCount}(). --- If you want to obtain a specific set from the DetectedSets, use the method @{Functional.Detection#DETECTION_BASE.GetDetectedSet}() with a given index. +-- 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 @{Functional.Detection#DETECTION_BASE.GetDetectionZones}(). --- To understand the amount of zones created, use the method @{Functional.Detection#DETECTION_BASE.GetDetectionZoneCount}(). --- If you want to obtain a specific zone from the DetectedZones, use the method @{Functional.Detection#DETECTION_BASE.GetDetectionZone}() with a given index. +-- 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 @{Functional.Detection#DETECTION_AREAS.FlareDetectedUnits}() or @{Functional.Detection#DETECTION_AREAS.SmokeDetectedUnits}() to flare or smoke the detected units when a new detection has taken place. +-- 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 @{Functional.Detection#DETECTION_AREAS.FlareDetectedZones}() or @{Functional.Detection#DETECTION_AREAS.SmokeDetectedZones}() to flare or smoke the detected zones when a new detection has taken place. +-- 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. -- -- === -- @@ -245,7 +245,7 @@ function DETECTION_BASE:GetDetectedObject( ObjectName ) return nil end ---- Get the detected @{Core.Set#SET_BASE}s. +--- Get the detected @{Set#SET_BASE}s. -- @param #DETECTION_BASE self -- @return #DETECTION_BASE.DetectedSets DetectedSets function DETECTION_BASE:GetDetectedSets() @@ -308,12 +308,12 @@ function DETECTION_BASE:Schedule( DelayTime, RepeatInterval ) self.ScheduleDelayTime = DelayTime self.ScheduleRepeatInterval = RepeatInterval - self.DetectionScheduler = SCHEDULER:New(self, self._DetectionScheduler, { self, "Detection" }, DelayTime, RepeatInterval ) + self.DetectionScheduler = SCHEDULER:New( self, self._DetectionScheduler, { self, "Detection" }, DelayTime, RepeatInterval ) return self end ---- Form @{Set}s of detected @{Wrapper.Unit#UNIT}s in an array of @{Core.Set#SET_BASE}s. +--- 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 } ) @@ -433,7 +433,7 @@ function DETECTION_AREAS:New( DetectionSetGroup, DetectionRange, DetectionZoneRa self._SmokeDetectedZones = false self._FlareDetectedZones = false - self:Schedule( 0, 30 ) + self:Schedule( 10, 10 ) return self end @@ -487,7 +487,7 @@ function DETECTION_AREAS:GetDetectedAreaCount() return DetectedAreaCount end ---- Get the @{Core.Set#SET_UNIT} of a detecttion area using a given numeric index. +--- 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 @@ -501,7 +501,7 @@ function DETECTION_AREAS:GetDetectedSet( Index ) return nil end ---- Get the @{Core.Zone#ZONE_UNIT} of a detection area using a given numeric index. +--- 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 diff --git a/Moose Development/Moose/Functional/Escort.lua b/Moose Development/Moose/Functional/Escort.lua index 96a28c453..247ede8b9 100644 --- a/Moose Development/Moose/Functional/Escort.lua +++ b/Moose Development/Moose/Functional/Escort.lua @@ -80,7 +80,7 @@ -- ============================ -- Create a new SPAWN object with the @{#ESCORT.New} method: -- --- * @{#ESCORT.New}: Creates a new ESCORT object from a @{Wrapper.Group#GROUP} for a @{Wrapper.Client#CLIENT}, with an optional briefing text. +-- * @{#ESCORT.New}: Creates a new ESCORT object from a @{Group#GROUP} for a @{Client#CLIENT}, with an optional briefing text. -- -- ESCORT initialization methods. -- ============================== diff --git a/Moose Development/Moose/Functional/MissileTrainer.lua b/Moose Development/Moose/Functional/MissileTrainer.lua index 0d4d3f32f..23bd215d2 100644 --- a/Moose Development/Moose/Functional/MissileTrainer.lua +++ b/Moose Development/Moose/Functional/MissileTrainer.lua @@ -2,7 +2,7 @@ -- -- === -- --- 1) @{Functional.MissileTrainer#MISSILETRAINER} class, extends @{Core.Base#BASE} +-- 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. diff --git a/Moose Development/Moose/Functional/Spawn.lua b/Moose Development/Moose/Functional/Spawn.lua index 4db8a032f..b921ad8f0 100644 --- a/Moose Development/Moose/Functional/Spawn.lua +++ b/Moose Development/Moose/Functional/Spawn.lua @@ -1,6 +1,11 @@ ---- This module contains the SPAWN class. +--- Single-Player:**Yes** / Mulit-Player:**Yes** / AI:**Yes** / Human:**No** / Types:**All** -- +-- **Spawn groups of units dynamically in your missions.** +-- +-- ![Banner Image](..\Presentations\SPAWN\SPAWN.JPG) -- --- # 1) @{Functional.Spawn#SPAWN} class, extends @{Core.Base#BASE} +-- === +-- +-- # 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. @@ -42,7 +47,7 @@ -- * @{#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.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. @@ -112,6 +117,8 @@ -- -- 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. @@ -646,6 +653,10 @@ function SPAWN:SpawnWithIndex( SpawnIndex ) 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 + SpawnTemplate.uncontrolled = self.SpawnUnControlled + end end _EVENTDISPATCHER:OnBirthForTemplate( SpawnTemplate, self._OnBirth, self ) @@ -734,7 +745,7 @@ 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 @{Wrapper.Group#GROUP} that was spawned. +-- 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. @@ -874,7 +885,7 @@ function SPAWN:SpawnFromStatic( HostStatic, SpawnIndex ) end --- Will spawn a Group within a given @{Zone}. --- The @{Zone} can be of any type derived from @{Core.Zone#ZONE_BASE}. +-- 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 @@ -897,17 +908,20 @@ function SPAWN:SpawnInZone( Zone, RandomizeGroup, SpawnIndex ) return nil end ---- (AIR) Will spawn a plane group in uncontrolled mode... +--- (**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() - self:F( { self.SpawnTemplatePrefix } ) +function SPAWN:InitUnControlled( UnControlled ) + self:F2( { self.SpawnTemplatePrefix, UnControlled } ) - self.SpawnUnControlled = true + self.SpawnUnControlled = UnControlled for SpawnGroupID = 1, self.SpawnMaxGroups do - self.SpawnGroups[SpawnGroupID].UnControlled = true + self.SpawnGroups[SpawnGroupID].UnControlled = UnControlled end return self @@ -1227,9 +1241,6 @@ function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) SpawnTemplate.visible = false end - if SpawnTemplate.CategoryID == Group.Category.HELICOPTER or SpawnTemplate.CategoryID == Group.Category.AIRPLANE then - SpawnTemplate.uncontrolled = false - end for UnitID = 1, #SpawnTemplate.units do SpawnTemplate.units[UnitID].name = string.format( SpawnTemplate.name .. '-%02d', UnitID ) diff --git a/Moose Development/Moose/Tasking/DetectionManager.lua b/Moose Development/Moose/Tasking/DetectionManager.lua index 0ecb7fc3a..b7bcc07b5 100644 --- a/Moose Development/Moose/Tasking/DetectionManager.lua +++ b/Moose Development/Moose/Tasking/DetectionManager.lua @@ -2,37 +2,37 @@ -- -- === -- --- 1) @{Tasking.DetectionManager#DETECTION_MANAGER} class, extends @{Core.Base#BASE} +-- 1) @{DetectionManager#DETECTION_MANAGER} class, extends @{Base#BASE} -- ==================================================================== --- The @{Tasking.DetectionManager#DETECTION_MANAGER} class defines the core functions to report detected objects to groups. +-- 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: -- ----------------------------------- --- * @{Tasking.DetectionManager#DETECTION_MANAGER.New}(): Create a new DETECTION_MANAGER instance. +-- * @{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 @{Tasking.DetectionManager#DETECTION_MANAGER.ReportDetected}(). This method implements polymorphic behaviour. +-- 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 @{Tasking.DetectionManager#DETECTION_MANAGER.SetReportInterval}(). --- To control how long a reporting message is displayed, use @{Tasking.DetectionManager#DETECTION_MANAGER.SetReportDisplayTime}(). --- Derived classes need to implement the method @{Tasking.DetectionManager#DETECTION_MANAGER.GetReportDisplayTime}() to use the correct display time for displayed messages during a report. +-- The time interval in seconds of the reporting can be changed using the methods @{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 @{Tasking.DetectionManager#DETECTION_MANAGER.StartReporting}() and @{Tasking.DetectionManager#DETECTION_MANAGER.StopReporting}() respectively. --- If an ad-hoc report is requested, use the method @{Tasking.DetectionManager#DETECTION_MANAGER#ReportNow}(). +-- 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) @{Tasking.DetectionManager#DETECTION_REPORTING} class, extends @{Tasking.DetectionManager#DETECTION_MANAGER} +-- 2) @{DetectionManager#DETECTION_REPORTING} class, extends @{DetectionManager#DETECTION_MANAGER} -- ========================================================================================= --- The @{Tasking.DetectionManager#DETECTION_REPORTING} class implements detected units reporting. Reporting can be controlled using the reporting methods available in the @{Tasking.DetectionManager#DETECTION_MANAGER} class. +-- 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 @{Tasking.DetectionManager#DETECTION_REPORTING.New}() method creates a new DETECTION_REPORTING instance. +-- The @{DetectionManager#DETECTION_REPORTING.New}() method creates a new DETECTION_REPORTING instance. -- -- === -- @@ -148,7 +148,7 @@ do -- DETECTION MANAGER return self end - --- Report the detected @{Wrapper.Unit#UNIT}s detected within the @{Functional.Detection#DETECTION_BASE} object to the @{Set#SET_GROUP}s. + --- 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 } ) @@ -198,7 +198,7 @@ do -- DETECTION_REPORTING --- Creates a string of the detected items in a @{Detection}. -- @param #DETECTION_MANAGER self - -- @param Set#SET_UNIT DetectedSet The detected Set created by the @{Functional.Detection#DETECTION_BASE} object. + -- @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() @@ -231,7 +231,7 @@ do -- DETECTION_REPORTING --- 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 @{Functional.Detection#DETECTION_BASE} object. + -- @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 ) @@ -392,7 +392,7 @@ do -- DETECTION_DISPATCHER --- 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 @{Functional.Detection#DETECTION_AREAS} object. + -- @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() diff --git a/Moose Development/Moose/Tasking/Task.lua b/Moose Development/Moose/Tasking/Task.lua index 6a7af018b..694cd269e 100644 --- a/Moose Development/Moose/Tasking/Task.lua +++ b/Moose Development/Moose/Tasking/Task.lua @@ -1,6 +1,6 @@ --- This module contains the TASK class. -- --- 1) @{#TASK} class, extends @{Core.Base#BASE} +-- 1) @{#TASK} class, extends @{Base#BASE} -- ============================================ -- 1.1) The @{#TASK} class implements the methods for task orchestration within MOOSE. -- ---------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Tasking/Task_A2G.lua b/Moose Development/Moose/Tasking/Task_A2G.lua index bb9002d5b..3eee41d0d 100644 --- a/Moose Development/Moose/Tasking/Task_A2G.lua +++ b/Moose Development/Moose/Tasking/Task_A2G.lua @@ -1,14 +1,14 @@ --- (AI) (SP) (MP) Tasking for Air to Ground Processes. -- --- 1) @{#TASK_A2G} class, extends @{Tasking.Task#TASK} +-- 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 @{Tasking.Task#TASK}. +-- 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 @{Wrapper.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. +-- * **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. -- diff --git a/Moose Development/Moose/Tasking/Task_Pickup.lua b/Moose Development/Moose/Tasking/Task_Pickup.lua index a0c41eb02..9754ca417 100644 --- a/Moose Development/Moose/Tasking/Task_Pickup.lua +++ b/Moose Development/Moose/Tasking/Task_Pickup.lua @@ -1,14 +1,14 @@ --- This module contains the TASK_PICKUP classes. -- --- 1) @{#TASK_PICKUP} class, extends @{Tasking.Task#TASK} +-- 1) @{#TASK_PICKUP} class, extends @{Task#TASK} -- =================================================== -- The @{#TASK_PICKUP} class defines a pickup task of a @{Set} of @{CARGO} objects defined within the mission. --- based on the tasking capabilities defined in @{Tasking.Task#TASK}. +-- based on the tasking capabilities defined in @{Task#TASK}. -- The TASK_PICKUP is implemented using a @{Statemachine#FSM_TASK}, and has the following statuses: -- -- * **None**: Start of the process -- * **Planned**: The SEAD task is planned. Upon Planned, the sub-process @{Process_Fsm.Assign#ACT_ASSIGN_ACCEPT} is started to accept the task. --- * **Assigned**: The SEAD task is assigned to a @{Wrapper.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. +-- * **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. -- diff --git a/Moose Development/Moose/Tasking/Task_SEAD.lua b/Moose Development/Moose/Tasking/Task_SEAD.lua index fe68d5b97..cd40cd71e 100644 --- a/Moose Development/Moose/Tasking/Task_SEAD.lua +++ b/Moose Development/Moose/Tasking/Task_SEAD.lua @@ -1,14 +1,14 @@ --- This module contains the TASK_SEAD classes. -- --- 1) @{#TASK_SEAD} class, extends @{Tasking.Task#TASK} +-- 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 @{Tasking.Task#TASK}. +-- 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 @{Wrapper.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. +-- * **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. -- diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index 063bf33a8..a73189948 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -2,7 +2,7 @@ -- -- === -- --- 1) @{Wrapper.Airbase#AIRBASE} class, extends @{Wrapper.Positionable#POSITIONABLE} +-- 1) @{Airbase#AIRBASE} class, extends @{Positionable#POSITIONABLE} -- ================================================================= -- The @{AIRBASE} class is a wrapper class to handle the DCS Airbase objects: -- @@ -33,7 +33,7 @@ -- --------------------- -- 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 @{Dcs.DCSWrapper.Airbase#Airbase.getName}() +-- 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 diff --git a/Moose Development/Moose/Wrapper/Client.lua b/Moose Development/Moose/Wrapper/Client.lua index e603afa20..4d08b3fea 100644 --- a/Moose Development/Moose/Wrapper/Client.lua +++ b/Moose Development/Moose/Wrapper/Client.lua @@ -1,10 +1,10 @@ --- This module contains the CLIENT class. -- --- 1) @{Wrapper.Client#CLIENT} class, extends @{Wrapper.Unit#UNIT} +-- 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 @{Wrapper.Client#CLIENT} class is a wrapper class to handle the DCS Unit objects that have the skillset defined as __Client__ or __Player__: +-- 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. @@ -35,7 +35,6 @@ -- IMPORTANT: ONE SHOULD NEVER SANATIZE these CLIENT OBJECT REFERENCES! (make the CLIENT object references nil). -- -- @module Client --- @author FlightControl --- The CLIENT class -- @type CLIENT @@ -226,7 +225,7 @@ end --- Checks for a client alive event and calls a function on a continuous basis. -- @param #CLIENT self --- @param #function CallBack Function. +-- @param #function CallBackFunction Create a function that will be called when a player joins the slot. -- @return #CLIENT function CLIENT:Alive( CallBackFunction, ... ) self:F() @@ -399,8 +398,8 @@ function CLIENT:IsTransport() return self.ClientTransport end ---- Shows the @{AI.AI_Cargo#CARGO} contained within the CLIENT to the player as a message. --- The @{AI.AI_Cargo#CARGO} is shown using the @{Core.Message#MESSAGE} distribution system. +--- 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() @@ -433,7 +432,7 @@ end -- @param #string Message is the text describing the message. -- @param #number MessageDuration is the duration in seconds that the Message should be displayed. -- @param #string MessageCategory is the category of the message (the title). --- @param #number MessageInterval is the interval in seconds between the display of the @{Core.Message#MESSAGE} when the CLIENT is in the air. +-- @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 } ) diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index bc5104782..514a0681c 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -1,8 +1,8 @@ --- This module contains the CONTROLLABLE class. -- --- 1) @{Wrapper.Controllable#CONTROLLABLE} class, extends @{Wrapper.Positionable#POSITIONABLE} +-- 1) @{Controllable#CONTROLLABLE} class, extends @{Positionable#POSITIONABLE} -- =========================================================== --- The @{Wrapper.Controllable#CONTROLLABLE} class is a wrapper class to handle the DCS Controllable objects: +-- 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. @@ -18,7 +18,7 @@ -- 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 @{Wrapper.Controllable#CONTROLLABLE.PushTask} or @{Wrapper.Controllable#SetTask} method to assign the task to the CONTROLLABLE. +-- These methods return a string consisting of the task description, which can then be given to either a @{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. @@ -44,7 +44,7 @@ -- * @{#CONTROLLABLE.TaskHold}: (GROUND) Hold ground controllable from moving. -- * @{#CONTROLLABLE.TaskHoldPosition}: (AIR) Hold position at the current position of the first unit of the controllable. -- * @{#CONTROLLABLE.TaskLand}: (AIR HELICOPTER) Landing at the ground. For helicopters only. --- * @{#CONTROLLABLE.TaskLandAtZone}: (AIR) Land the controllable at a @{Core.Zone#ZONE_RADIUS). +-- * @{#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. @@ -344,7 +344,7 @@ end --- Return a Combo Task taking an array of Tasks. -- @param #CONTROLLABLE self --- @param Dcs.DCSTasking.Task#TaskArray DCSTasks Array of @{Dcs.DCSTasking.Task#Task} +-- @param Dcs.DCSTasking.Task#TaskArray DCSTasks Array of @{DCSTasking.Task#Task} -- @return Dcs.DCSTasking.Task#Task function CONTROLLABLE:TaskCombo( DCSTasks ) self:F2( { DCSTasks } ) @@ -829,7 +829,7 @@ function CONTROLLABLE:TaskLandAtVec2( Point, Duration ) return DCSTask end ---- (AIR) Land the controllable at a @{Core.Zone#ZONE_RADIUS). +--- (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. @@ -1638,11 +1638,11 @@ function CONTROLLABLE:TaskRouteToZone( Zone, Randomize, Speed, Formation ) return nil end ---- (AIR) Return the Controllable to an @{Wrapper.Airbase#AIRBASE} +--- (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 @{Wrapper.Airbase#AIRBASE} to return to. +-- @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 ) @@ -1762,7 +1762,7 @@ function CONTROLLABLE:GetTaskRoute() return routines.utils.deepCopy( _DATABASE.Templates.Controllables[self.ControllableName].Template.route.points ) end ---- Return the route of a controllable by using the @{Core.Database#DATABASE} class. +--- 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. @@ -2187,7 +2187,7 @@ function CONTROLLABLE:OptionROTVertical() end --- Retrieve the controllable mission and allow to place function hooks within the mission waypoint plan. --- Use the method @{Wrapper.Controllable#CONTROLLABLE:WayPointFunction} to define the hook functions for specific waypoints. +-- 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 diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index bcd0027f8..5fcd0cdb2 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -1,8 +1,8 @@ --- This module contains the GROUP class. -- --- 1) @{Wrapper.Group#GROUP} class, extends @{Wrapper.Controllable#CONTROLLABLE} +-- 1) @{Group#GROUP} class, extends @{Controllable#CONTROLLABLE} -- ============================================================= --- The @{Wrapper.Group#GROUP} class is a wrapper class to handle the DCS Group objects: +-- 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. @@ -36,7 +36,7 @@ -- -- Group templates contain complete mission descriptions. Sometimes you want to copy a complete mission from a group and assign it to another: -- --- * @{Wrapper.Controllable#CONTROLLABLE.TaskMission}: (AIR + GROUND) Return a mission task from a mission template. +-- * @{Controllable#CONTROLLABLE.TaskMission}: (AIR + GROUND) Return a mission task from a mission template. -- -- ## 1.3) GROUP Command methods -- @@ -55,7 +55,7 @@ -- * @{#GROUP.IsPartlyInZone}: Returns true if some units of the group are within a @{Zone}. -- * @{#GROUP.IsNotInZone}: Returns true if none of the group units of the group are within a @{Zone}. -- --- The zone can be of any @{Zone} class derived from @{Core.Zone#ZONE_BASE}. So, these methods are polymorphic to the zones tested on. +-- 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 -- @@ -156,7 +156,7 @@ function GROUP:GetDCSObject() return nil end ---- Returns the @{Dcs.DCSTypes#Position3} position vectors indicating the point and direction vectors in 3D of the POSITIONABLE within the mission. +--- 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. @@ -297,7 +297,6 @@ function GROUP:GetUnit( UnitNumber ) if DCSGroup then local UnitFound = UNIT:Find( DCSGroup:getUnit( UnitNumber ) ) - self:T3( UnitFound.UnitName ) self:T2( UnitFound ) return UnitFound end @@ -450,7 +449,7 @@ 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 @{Core.Zone#ZONE_BASE} +-- @return #boolean Returns true if the Group is completely within the @{Zone#ZONE_BASE} function GROUP:IsCompletelyInZone( Zone ) self:F2( { self.GroupName, Zone } ) @@ -469,7 +468,7 @@ 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 @{Core.Zone#ZONE_BASE} +-- @return #boolean Returns true if the Group is completely within the @{Zone#ZONE_BASE} function GROUP:IsPartlyInZone( Zone ) self:F2( { self.GroupName, Zone } ) @@ -486,7 +485,7 @@ 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 @{Core.Zone#ZONE_BASE} +-- @return #boolean Returns true if the Group is completely within the @{Zone#ZONE_BASE} function GROUP:IsNotInZone( Zone ) self:F2( { self.GroupName, Zone } ) @@ -703,7 +702,7 @@ end -- SPAWNING --- Respawn the @{GROUP} using a (tweaked) template of the Group. --- The template must be retrieved with the @{Wrapper.Group#GROUP.GetTemplate}() function. +-- 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: -- @@ -805,7 +804,7 @@ function GROUP:GetTaskRoute() return routines.utils.deepCopy( _DATABASE.Templates.Groups[self.GroupName].Template.route.points ) end ---- Return the route of a group by using the @{Core.Database#DATABASE} class. +--- 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. diff --git a/Moose Development/Moose/Wrapper/Identifiable.lua b/Moose Development/Moose/Wrapper/Identifiable.lua index c11b5cee7..abfa0c607 100644 --- a/Moose Development/Moose/Wrapper/Identifiable.lua +++ b/Moose Development/Moose/Wrapper/Identifiable.lua @@ -1,6 +1,6 @@ --- This module contains the IDENTIFIABLE class. -- --- 1) @{#IDENTIFIABLE} class, extends @{Wrapper.Object#OBJECT} +-- 1) @{#IDENTIFIABLE} class, extends @{Object#OBJECT} -- =============================================================== -- The @{#IDENTIFIABLE} class is a wrapper class to handle the DCS Identifiable objects: -- diff --git a/Moose Development/Moose/Wrapper/Object.lua b/Moose Development/Moose/Wrapper/Object.lua index 14650e584..6641c80fb 100644 --- a/Moose Development/Moose/Wrapper/Object.lua +++ b/Moose Development/Moose/Wrapper/Object.lua @@ -1,8 +1,8 @@ --- This module contains the OBJECT class. -- --- 1) @{Wrapper.Object#OBJECT} class, extends @{Core.Base#BASE} +-- 1) @{Object#OBJECT} class, extends @{Base#BASE} -- =========================================================== --- The @{Wrapper.Object#OBJECT} class is a wrapper class to handle the DCS Object objects: +-- 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. @@ -12,13 +12,13 @@ -- ------------------------------ -- The OBJECT class provides the following functions to construct a OBJECT instance: -- --- * @{Wrapper.Object#OBJECT.New}(): Create 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: -- --- * @{Wrapper.Object#OBJECT.GetID}(): Returns the ID of the Object object. +-- * @{Object#OBJECT.GetID}(): Returns the ID of the Object object. -- -- === -- diff --git a/Moose Development/Moose/Wrapper/Positionable.lua b/Moose Development/Moose/Wrapper/Positionable.lua index 7ce5f0d04..8c0be85a9 100644 --- a/Moose Development/Moose/Wrapper/Positionable.lua +++ b/Moose Development/Moose/Wrapper/Positionable.lua @@ -1,8 +1,8 @@ --- This module contains the POSITIONABLE class. -- --- 1) @{Wrapper.Positionable#POSITIONABLE} class, extends @{Wrapper.Identifiable#IDENTIFIABLE} +-- 1) @{Positionable#POSITIONABLE} class, extends @{Identifiable#IDENTIFIABLE} -- =========================================================== --- The @{Wrapper.Positionable#POSITIONABLE} class is a wrapper class to handle the POSITIONABLE objects: +-- 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. @@ -12,14 +12,14 @@ -- ------------------------------ -- The POSITIONABLE class provides the following functions to construct a POSITIONABLE instance: -- --- * @{Wrapper.Positionable#POSITIONABLE.New}(): Create 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: -- --- * @{Wrapper.Positionable#POSITIONABLE.GetID}(): Returns the ID of the measurable object. --- * @{Wrapper.Positionable#POSITIONABLE.GetName}(): Returns the name of the measurable object. +-- * @{Positionable#POSITIONABLE.GetID}(): Returns the ID of the measurable object. +-- * @{Positionable#POSITIONABLE.GetName}(): Returns the name of the measurable object. -- -- === -- @@ -49,7 +49,7 @@ function POSITIONABLE:New( PositionableName ) return self end ---- Returns the @{Dcs.DCSTypes#Position3} position vectors indicating the point and direction vectors in 3D of the POSITIONABLE within the mission. +--- 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. @@ -67,7 +67,7 @@ function POSITIONABLE:GetPositionVec3() return nil end ---- Returns the @{Dcs.DCSTypes#Vec2} vector indicating the point in 2D of the POSITIONABLE within the mission. +--- 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. @@ -133,7 +133,7 @@ function POSITIONABLE:GetPointVec3() end ---- Returns a random @{Dcs.DCSTypes#Vec3} vector within a range, indicating the point in 3D of the POSITIONABLE within the mission. +--- 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. @@ -157,7 +157,7 @@ function POSITIONABLE:GetRandomVec3( Radius ) return nil end ---- Returns the @{Dcs.DCSTypes#Vec3} vector indicating the 3D vector of the POSITIONABLE within the mission. +--- 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. diff --git a/Moose Development/Moose/Wrapper/Static.lua b/Moose Development/Moose/Wrapper/Static.lua index ca81d064e..efadcaf4d 100644 --- a/Moose Development/Moose/Wrapper/Static.lua +++ b/Moose Development/Moose/Wrapper/Static.lua @@ -1,10 +1,10 @@ --- This module contains the STATIC class. -- --- 1) @{Wrapper.Static#STATIC} class, extends @{Wrapper.Positionable#POSITIONABLE} +-- 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 @{Wrapper.Static#STATIC} class is a wrapper class to handle the DCS Static objects: +-- The @{Static#STATIC} class is a wrapper class to handle the DCS Static objects: -- -- * Wraps the DCS Static objects. -- * Support all DCS Static APIs. diff --git a/Moose Development/Moose/Wrapper/Unit.lua b/Moose Development/Moose/Wrapper/Unit.lua index 7f0b430b8..6f58e404c 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -1,6 +1,6 @@ --- This module contains the UNIT class. -- --- 1) @{#UNIT} class, extends @{Wrapper.Controllable#CONTROLLABLE} +-- 1) @{#UNIT} class, extends @{Controllable#CONTROLLABLE} -- =========================================================== -- The @{#UNIT} class is a wrapper class to handle the DCS Unit objects: -- @@ -33,7 +33,7 @@ -- ------------------ -- The DCS Unit APIs are used extensively within MOOSE. The UNIT class has for each DCS Unit API a corresponding method. -- To be able to distinguish easily in your code the difference between a UNIT API call and a DCS Unit API call, --- the first letter of the method is also capitalized. So, by example, the DCS Unit method @{Dcs.DCSWrapper.Unit#Unit.getName}() +-- 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 @@ -60,7 +60,7 @@ -- 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 @{Core.Zone#ZONE_BASE}. +-- To test whether the Unit is within a **zone**, use the @{#UNIT.IsInZone}() or the @{#UNIT.IsNotInZone}() methods. Any zone can be tested on, but the zone must be derived from @{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. @@ -584,7 +584,7 @@ end --- Returns true if the unit is within a @{Zone}. -- @param #UNIT self -- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the unit is within the @{Core.Zone#ZONE_BASE} +-- @return #boolean Returns true if the unit is within the @{Zone#ZONE_BASE} function UNIT:IsInZone( Zone ) self:F2( { self.UnitName, Zone } ) @@ -601,7 +601,7 @@ end --- Returns true if the unit is not within a @{Zone}. -- @param #UNIT self -- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the unit is not within the @{Core.Zone#ZONE_BASE} +-- @return #boolean Returns true if the unit is not within the @{Zone#ZONE_BASE} function UNIT:IsNotInZone( Zone ) self:F2( { self.UnitName, Zone } ) 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 da1821733..c89d1ec84 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,31767 @@ -env.info( '*** MOOSE DYNAMIC INCLUDE START *** ' ) -env.info( 'Moose Generation Timestamp: 20170203_2219' ) - +env.info( '*** MOOSE STATIC INCLUDE START *** ' ) +env.info( 'Moose Generation Timestamp: 20170204_1514' ) 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. +-- Therefore, the BASE class exposes the following event handling functions: +-- +-- * @{#BASE.EventOnBirth}(): Handle the birth of a new unit. +-- * @{#BASE.EventOnBaseCaptured}(): Handle the capturing of an airbase or a helipad. +-- * @{#BASE.EventOnCrash}(): Handle the crash of a unit. +-- * @{#BASE.EventOnDead}(): Handle the death of a unit. +-- * @{#BASE.EventOnEjection}(): Handle the ejection of a player out of an airplane. +-- * @{#BASE.EventOnEngineShutdown}(): Handle the shutdown of an engine. +-- * @{#BASE.EventOnEngineStartup}(): Handle the startup of an engine. +-- * @{#BASE.EventOnHit}(): Handle the hit of a shell to a unit. +-- * @{#BASE.EventOnHumanFailure}(): No a clue ... +-- * @{#BASE.EventOnLand}(): Handle the event when a unit lands. +-- * @{#BASE.EventOnMissionStart}(): Handle the start of the mission. +-- * @{#BASE.EventOnPilotDead}(): Handle the event when a pilot is dead. +-- * @{#BASE.EventOnPlayerComment}(): Handle the event when a player posts a comment. +-- * @{#BASE.EventOnPlayerEnterUnit}(): Handle the event when a player enters a unit. +-- * @{#BASE.EventOnPlayerLeaveUnit}(): Handle the event when a player leaves a unit. +-- * @{#BASE.EventOnBirthPlayerMissionEnd}(): Handle the event when a player ends the mission. (Not a clue what that does). +-- * @{#BASE.EventOnRefueling}(): Handle the event when a unit is refueling. +-- * @{#BASE.EventOnShootingEnd}(): Handle the event when a unit starts shooting (guns). +-- * @{#BASE.EventOnShootingStart}(): Handle the event when a unit ends shooting (guns). +-- * @{#BASE.EventOnShot}(): Handle the event when a unit shot a missile. +-- * @{#BASE.EventOnTakeOff}(): Handle the event when a unit takes off from a runway. +-- * @{#BASE.EventOnTookControl}(): Handle the event when a player takes control of a unit. +-- +-- The EventOn() methods provide the @{Event#EVENTDATA} structure to the event handling function. +-- The @{Event#EVENTDATA} structure contains an enriched data set of information about the event being handled. +-- +-- Find below an example of the prototype how to write an event handling function: +-- +-- CommandCenter:EventOnPlayerEnterUnit( +-- --- @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 +-- Mission:JoinUnit( PlayerUnit ) +-- Mission:ReportDetails() +-- end +-- end +-- ) +-- +-- Note the function( self, EventData ). It takes two parameters: +-- +-- * self = the object that is handling the EventOnPlayerEnterUnit. +-- * EventData = the @{Event#EVENTDATA} structure, containing more information of the Event. +-- +-- ## 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 + +--- 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 + + +--- Set a new listener for the class. +-- @param self +-- @param Dcs.DCSTypes#Event Event +-- @param #function EventFunction +-- @return #BASE +function BASE:AddEvent( Event, EventFunction ) + self:F( Event ) + + self.Events[#self.Events+1] = {} + self.Events[#self.Events].Event = Event + self.Events[#self.Events].EventFunction = EventFunction + self.Events[#self.Events].EventEnabled = false + + return self +end + +--- Returns the event dispatcher +-- @param #BASE self +-- @return Core.Event#EVENT +function BASE:Event() + + return _EVENTDISPATCHER +end + +--- Remove all subscribed events +-- @param #BASE self +-- @return #BASE +function BASE:EventRemoveAll() + + _EVENTDISPATCHER:RemoveAll( self ) + + return self +end + +--- Subscribe to a S_EVENT\_SHOT event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnShot( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_SHOT ) + + return self +end + +--- Subscribe to a S_EVENT\_HIT event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnHit( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_HIT ) + + return self +end + +--- Subscribe to a S_EVENT\_TAKEOFF event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnTakeOff( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_TAKEOFF ) + + return self +end + +--- Subscribe to a S_EVENT\_LAND event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnLand( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_LAND ) + + return self +end + +--- Subscribe to a S_EVENT\_CRASH event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnCrash( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_CRASH ) + + return self +end + +--- Subscribe to a S_EVENT\_EJECTION event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnEjection( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_EJECTION ) + + return self +end + + +--- Subscribe to a S_EVENT\_REFUELING event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnRefueling( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_REFUELING ) + + return self +end + +--- Subscribe to a S_EVENT\_DEAD event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnDead( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_DEAD ) + + return self +end + +--- Subscribe to a S_EVENT_PILOT\_DEAD event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnPilotDead( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_PILOT_DEAD ) + + return self +end + +--- Subscribe to a S_EVENT_BASE\_CAPTURED event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnBaseCaptured( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_BASE_CAPTURED ) + + return self +end + +--- Subscribe to a S_EVENT_MISSION\_START event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnMissionStart( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_MISSION_START ) + + return self +end + +--- Subscribe to a S_EVENT_MISSION\_END event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnPlayerMissionEnd( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_MISSION_END ) + + return self +end + +--- Subscribe to a S_EVENT_TOOK\_CONTROL event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnTookControl( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_TOOK_CONTROL ) + + return self +end + +--- Subscribe to a S_EVENT_REFUELING\_STOP event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnRefuelingStop( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_REFUELING_STOP ) + + return self +end + +--- Subscribe to a S_EVENT\_BIRTH event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnBirth( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_BIRTH ) + + return self +end + +--- Subscribe to a S_EVENT_HUMAN\_FAILURE event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnHumanFailure( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_HUMAN_FAILURE ) + + return self +end + +--- Subscribe to a S_EVENT_ENGINE\_STARTUP event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnEngineStartup( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_ENGINE_STARTUP ) + + return self +end + +--- Subscribe to a S_EVENT_ENGINE\_SHUTDOWN event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnEngineShutdown( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_ENGINE_SHUTDOWN ) + + return self +end + +--- Subscribe to a S_EVENT_PLAYER_ENTER\_UNIT event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnPlayerEnterUnit( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_PLAYER_ENTER_UNIT ) + + return self +end + +--- Subscribe to a S_EVENT_PLAYER_LEAVE\_UNIT event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnPlayerLeaveUnit( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_PLAYER_LEAVE_UNIT ) + + return self +end + +--- Subscribe to a S_EVENT_PLAYER\_COMMENT event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnPlayerComment( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_PLAYER_COMMENT ) + + return self +end + +--- Subscribe to a S_EVENT_SHOOTING\_START event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnShootingStart( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_SHOOTING_START ) + + return self +end + +--- Subscribe to a S_EVENT_SHOOTING\_END event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnShootingEnd( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_SHOOTING_END ) + + return self +end + + + + + + + +--- Enable the event listeners for the class. +-- @param #BASE self +-- @return #BASE +function BASE:EnableEvents() + self:F( #self.Events ) + + for EventID, Event in pairs( self.Events ) do + Event.Self = self + Event.EventEnabled = true + end + self.Events.Handler = world.addEventHandler( self ) + + return self +end + + +--- Disable the event listeners for the class. +-- @param #BASE self +-- @return #BASE +function BASE:DisableEvents() + self:F() + + world.removeEventHandler( self ) + for EventID, Event in pairs( self.Events ) do + Event.Self = nil + Event.EventEnabled = false + end + + return self +end + + +local BaseEventCodes = { + "S_EVENT_SHOT", + "S_EVENT_HIT", + "S_EVENT_TAKEOFF", + "S_EVENT_LAND", + "S_EVENT_CRASH", + "S_EVENT_EJECTION", + "S_EVENT_REFUELING", + "S_EVENT_DEAD", + "S_EVENT_PILOT_DEAD", + "S_EVENT_BASE_CAPTURED", + "S_EVENT_MISSION_START", + "S_EVENT_MISSION_END", + "S_EVENT_TOOK_CONTROL", + "S_EVENT_REFUELING_STOP", + "S_EVENT_BIRTH", + "S_EVENT_HUMAN_FAILURE", + "S_EVENT_ENGINE_STARTUP", + "S_EVENT_ENGINE_SHUTDOWN", + "S_EVENT_PLAYER_ENTER_UNIT", + "S_EVENT_PLAYER_LEAVE_UNIT", + "S_EVENT_PLAYER_COMMENT", + "S_EVENT_SHOOTING_START", + "S_EVENT_SHOOTING_END", + "S_EVENT_MAX", +} + +--onEvent( {[1]="S_EVENT_BIRTH",[2]={["subPlace"]=5,["time"]=0,["initiator"]={["id_"]=16884480,},["place"]={["id_"]=5000040,},["id"]=15,["IniUnitName"]="US F-15C@RAMP-Air Support Mountains#001-01",},} +-- Event = { +-- id = enum world.event, +-- time = Time, +-- initiator = Unit, +-- target = Unit, +-- place = Unit, +-- subPlace = enum world.BirthPlace, +-- weapon = Weapon +-- } + +--- Creation of a Birth Event. +-- @param #BASE self +-- @param 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 module contains the EVENT class. +-- +-- === +-- +-- Takes care of EVENT dispatching between DCS events and event handling functions defined in MOOSE classes. +-- +-- === +-- +-- The above menus classes **are derived** from 2 main **abstract** classes defined within the MOOSE framework (so don't use these): +-- +-- === +-- +-- ### Contributions: - +-- ### Authors: FlightControl : Design & Programming +-- +-- @module Event + +--- The EVENT structure +-- @type EVENT +-- @field #EVENT.Events Events +EVENT = { + ClassName = "EVENT", + ClassID = 0, +} + +local _EVENTCODES = { + "S_EVENT_SHOT", + "S_EVENT_HIT", + "S_EVENT_TAKEOFF", + "S_EVENT_LAND", + "S_EVENT_CRASH", + "S_EVENT_EJECTION", + "S_EVENT_REFUELING", + "S_EVENT_DEAD", + "S_EVENT_PILOT_DEAD", + "S_EVENT_BASE_CAPTURED", + "S_EVENT_MISSION_START", + "S_EVENT_MISSION_END", + "S_EVENT_TOOK_CONTROL", + "S_EVENT_REFUELING_STOP", + "S_EVENT_BIRTH", + "S_EVENT_HUMAN_FAILURE", + "S_EVENT_ENGINE_STARTUP", + "S_EVENT_ENGINE_SHUTDOWN", + "S_EVENT_PLAYER_ENTER_UNIT", + "S_EVENT_PLAYER_LEAVE_UNIT", + "S_EVENT_PLAYER_COMMENT", + "S_EVENT_SHOOTING_START", + "S_EVENT_SHOOTING_END", + "S_EVENT_MAX", +} + +local _EVENTORDER = { + [world.event.S_EVENT_SHOT] = 1, + [world.event.S_EVENT_HIT] = 1, + [world.event.S_EVENT_TAKEOFF] = 1, + [world.event.S_EVENT_LAND] = 1, + [world.event.S_EVENT_CRASH] = -1, + [world.event.S_EVENT_EJECTION] = -1, + [world.event.S_EVENT_REFUELING] = 1, + [world.event.S_EVENT_DEAD] = -1, + [world.event.S_EVENT_PILOT_DEAD] = -1, + [world.event.S_EVENT_BASE_CAPTURED] = 1, + [world.event.S_EVENT_MISSION_START] = 1, + [world.event.S_EVENT_MISSION_END] = -1, + [world.event.S_EVENT_TOOK_CONTROL] = 1, + [world.event.S_EVENT_REFUELING_STOP] = 1, + [world.event.S_EVENT_BIRTH] = 1, + [world.event.S_EVENT_HUMAN_FAILURE] = 1, + [world.event.S_EVENT_ENGINE_STARTUP] = 1, + [world.event.S_EVENT_ENGINE_SHUTDOWN] = 1, + [world.event.S_EVENT_PLAYER_ENTER_UNIT] = 1, + [world.event.S_EVENT_PLAYER_LEAVE_UNIT] = -1, + [world.event.S_EVENT_PLAYER_COMMENT] = 1, + [world.event.S_EVENT_SHOOTING_START] = 1, + [world.event.S_EVENT_SHOOTING_END] = 1, + [world.event.S_EVENT_MAX] = 1, +} + +--- The Event structure +-- @type EVENTDATA +-- @field id +-- @field initiator +-- @field target +-- @field weapon +-- @field IniDCSUnit +-- @field IniDCSUnitName +-- @field Wrapper.Unit#UNIT IniUnit +-- @field #string IniUnitName +-- @field IniDCSGroup +-- @field IniDCSGroupName +-- @field TgtDCSUnit +-- @field TgtDCSUnitName +-- @field Wrapper.Unit#UNIT TgtUnit +-- @field #string TgtUnitName +-- @field TgtDCSGroup +-- @field TgtDCSGroupName +-- @field Weapon +-- @field WeaponName +-- @field WeaponTgtDCSUnit + +--- The Events structure +-- @type EVENT.Events +-- @field #number IniUnit + +function EVENT:New() + local self = BASE:Inherit( self, BASE:New() ) + self:F2() + self.EventHandler = world.addEventHandler( self ) + return self +end + +function EVENT:EventText( EventID ) + + local EventText = _EVENTCODES[EventID] + + return EventText +end + + +--- Initializes the Events structure for the event +-- @param #EVENT self +-- @param Dcs.DCSWorld#world.event EventID +-- @param #number EventPriority The priority of the EventClass. +-- @param Core.Base#BASE EventClass +-- @return #EVENT.Events +function EVENT:Init( EventID, EventClass ) + self:F3( { _EVENTCODES[EventID], 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][EventClass] then + self.Events[EventID][EventClass] = setmetatable( {}, { __mode = "k" } ) + end + return self.Events[EventID][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, _EVENTCODES[EventID] } ) + + local EventClass = EventClass + local EventPriority = EventClass:GetEventPriority() + self.Events[EventID][EventPriority][EventClass] = 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 and Event.initiator:getCategory() == Object.Category.UNIT then + Event.IniDCSUnit = Event.initiator + Event.IniDCSGroup = Event.IniDCSUnit:getGroup() + Event.IniDCSUnitName = Event.IniDCSUnit:getName() + Event.IniUnitName = Event.IniDCSUnitName + Event.IniUnit = UNIT:FindByName( Event.IniDCSUnitName ) + 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 + end + if Event.target then + if Event.target and Event.target:getCategory() == Object.Category.UNIT then + Event.TgtDCSUnit = Event.target + Event.TgtDCSGroup = Event.TgtDCSUnit:getGroup() + Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() + Event.TgtUnitName = Event.TgtDCSUnitName + Event.TgtUnit = UNIT:FindByName( Event.TgtDCSUnitName ) + Event.TgtDCSGroupName = "" + if Event.TgtDCSGroup and Event.TgtDCSGroup:isExist() then + Event.TgtDCSGroupName = Event.TgtDCSGroup:getName() + end + end + end + if Event.weapon then + Event.Weapon = Event.weapon + Event.WeaponName = Event.Weapon:getTypeName() + --Event.WeaponTgtDCSUnit = Event.Weapon:getTarget() + end + self:E( { _EVENTCODES[Event.id], Event, Event.IniDCSUnitName, Event.TgtDCSUnitName } ) + + local Order = _EVENTORDER[Event.id] + self:E( { Order = Order } ) + + for EventPriority = Order == -1 and 5 or 1, Order == -1 and 1 or 5, Order 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 + self:E( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName } ) + Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) + local Result, Value = xpcall( function() return EventData.IniUnit[Event.IniDCSUnitName].EventFunction( EventData.IniUnit[Event.IniDCSUnitName].EventClass, Event ) end, ErrorHandler ) + --EventData.IniUnit[Event.IniDCSUnitName].EventFunction( EventData.IniUnit[Event.IniDCSUnitName].EventClass, Event ) + 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 + self:E( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID() } ) + Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) + local Result, Value = xpcall( function() return EventData.EventFunction( EventData.EventClass, Event ) end, ErrorHandler ) + --EventData.EventFunction( EventData.EventClass, Event ) + end + end + end + end + end + end + else + self:E( { _EVENTCODES[Event.id], Event } ) + end +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 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.IsPointVec2InZone}(): Returns if a @{Point#POINT_VEC2} is within the zone. +-- * @{#ZONE_BASE.IsPointVec3InZone}(): Returns if a @{Point#POINT_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_BASE.New}(): Constructor. +-- +-- ### 2.2) Manage the radius of the zone: +-- +-- * @{#ZONE_BASE.SetRadius}(): Sets the radius of the zone. +-- * @{#ZONE_BASE.GetRadius}(): Returns the radius of the zone. +-- +-- ### 2.3) Manage the location of the zone: +-- +-- * @{#ZONE_BASE.SetVec2}(): Sets the @{DCSTypes#Vec2} of the zone. +-- * @{#ZONE_BASE.GetVec2}(): Returns the @{DCSTypes#Vec2} of the zone. +-- * @{#ZONE_BASE.GetVec3}(): Returns the @{DCSTypes#Vec3} of the zone, taking an additional height parameter. +-- +-- === +-- +-- 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. +-- +-- === +-- +-- 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: +-- +-- 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 +-- @author FlightControl + + +--- 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:IsPointVec2InZone( 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:IsPointVec3InZone( Vec3 ) + self:F2( Vec3 ) + + local InZone = self:IsPointVec2InZone( { 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 + +--- 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 + + +--- 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 ) ) + self:F( { ZoneName, Vec2, Radius } ) + + self.Radius = Radius + self.Vec2 = Vec2 + + 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:IsPointVec2InZone( 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:IsPointVec3InZone( Vec3 ) + self:F2( Vec3 ) + + local InZone = self:IsPointVec2InZone( { x = Vec3.x, y = Vec3.z } ) + + return InZone +end + +--- Returns a random location within the zone. +-- @param #ZONE_RADIUS self +-- @param #number inner minimal distance from the center of the zone +-- @param #number outer minimal distance from the outer edge 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 + + + +--- 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 +-- @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:IsPointVec2InZone( 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:IsPointVec2InZone( Vec2 ) then + Vec2Found = true + end + end + + self:T2( Vec2 ) + + return Vec2 +end + +--- Get the bounding square the zone. +-- @param #ZONE_POLYGON_BASE self +-- @return #ZONE_POLYGON_BASE.BoundingSquare The bounding square. +function ZONE_POLYGON_BASE:GetBoundingSquare() + + local x1 = self.Polygon[1].x + local y1 = self.Polygon[1].y + local x2 = self.Polygon[1].x + local y2 = self.Polygon[1].y + + for i = 2, #self.Polygon do + self:T2( { self.Polygon[i], x1, y1, x2, y2 } ) + x1 = ( x1 > self.Polygon[i].x ) and self.Polygon[i].x or x1 + x2 = ( x2 < self.Polygon[i].x ) and self.Polygon[i].x or x2 + y1 = ( y1 > self.Polygon[i].y ) and self.Polygon[i].y or y1 + y2 = ( y2 < self.Polygon[i].y ) and self.Polygon[i].y or y2 + + end + + return { x1 = x1, y1 = y1, x2 = x2, y2 = y2 } +end + + + + + +--- The ZONE_POLYGON class defined by a sequence of @{Group#GROUP} waypoints within the Mission Editor, forming a polygon. +-- @type ZONE_POLYGON +-- @extends 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() ) + + _EVENTDISPATCHER:OnBirth( self._EventOnBirth, self ) + _EVENTDISPATCHER:OnDead( self._EventOnDeadOrCrash, self ) + _EVENTDISPATCHER:OnCrash( self._EventOnDeadOrCrash, self ) + + + -- Follow alive players and clients + _EVENTDISPATCHER:OnPlayerEnterUnit( self._EventOnPlayerEnterUnit, self ) + _EVENTDISPATCHER:OnPlayerLeaveUnit( self._EventOnPlayerLeaveUnit, self ) + + self:_RegisterTemplates() + self:_RegisterGroupsAndUnits() + self:_RegisterClients() + self:_RegisterStatics() + self:_RegisterPlayers() + self:_RegisterAirbases() + + self:SetEventPriority( 1 ) + + 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 + self:AddUnit( Event.IniDCSUnitName ) + self:AddGroup( Event.IniDCSGroupName ) + self:_EventOnPlayerEnterUnit( Event ) + end +end + + +--- Handles the OnDead or OnCrash event for alive units set. +-- @param #DATABASE self +-- @param Core.Event#EVENTDATA Event +function DATABASE:_EventOnDeadOrCrash( Event ) + self:F2( { Event } ) + + if Event.IniDCSUnit then + if self.UNITS[Event.IniDCSUnitName] then + self:DeleteUnit( Event.IniDCSUnitName ) + -- add logic to correctly remove a group once all units are destroyed... + end + end +end + + +--- Handles the OnPlayerEnterUnit event to fill the active players table (with the unit filter applied). +-- @param #DATABASE self +-- @param Core.Event#EVENTDATA Event +function DATABASE:_EventOnPlayerEnterUnit( Event ) + self:F2( { Event } ) + + if Event.IniUnit 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 + + +--- 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 + local PlayerName = Event.IniUnit:GetPlayerName() + if self.PLAYERS[PlayerName] then + self:DeletePlayer( PlayerName ) + end + end +end + +--- Iterators + +--- Iterate the DATABASE and call an iterator function for the given set, providing the Object for each element within the set and optional parameters. +-- @param #DATABASE self +-- @param #function IteratorFunction The function that will be called when there is an alive player in the database. +-- @return #DATABASE self +function DATABASE:ForEach( IteratorFunction, FinalizeFunction, arg, Set ) + self:F2( arg ) + + local function CoRoutine() + local Count = 0 + for ObjectID, Object in pairs( Set ) do + self:T2( Object ) + IteratorFunction( Object, unpack( arg ) ) + Count = Count + 1 +-- if Count % 100 == 0 then +-- coroutine.yield( false ) +-- end + end + return true + end + +-- local co = coroutine.create( CoRoutine ) + local co = CoRoutine + + local function Schedule() + +-- local status, res = coroutine.resume( co ) + local status, res = co() + self:T3( { status, res } ) + + if status == false then + error( res ) + end + if res == false then + return true -- resume next time the loop + end + if FinalizeFunction then + FinalizeFunction( unpack( arg ) ) + end + return false + end + + local Scheduler = SCHEDULER:New( self, Schedule, {}, 0.001, 0.001, 0 ) + + return self +end + + +--- Iterate the DATABASE and call an iterator function for each **alive** UNIT, providing the UNIT and optional parameters. +-- @param #DATABASE self +-- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the database. The function needs to accept a UNIT parameter. +-- @return #DATABASE self +function DATABASE:ForEachUnit( IteratorFunction, FinalizeFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, FinalizeFunction, arg, self.UNITS ) + + return self +end + +--- Iterate the DATABASE and call an iterator function for each **alive** GROUP, providing the GROUP and optional parameters. +-- @param #DATABASE self +-- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the database. The function needs to accept a GROUP parameter. +-- @return #DATABASE self +function DATABASE:ForEachGroup( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self.GROUPS ) + + return self +end + + +--- Iterate the DATABASE and call an iterator function for each **ALIVE** player, providing the player name and optional parameters. +-- @param #DATABASE self +-- @param #function IteratorFunction The function that will be called when there is an player in the database. The function needs to accept the player name. +-- @return #DATABASE self +function DATABASE:ForEachPlayer( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self.PLAYERS ) + + return self +end + + +--- Iterate the DATABASE and call an iterator function for each player who has joined the mission, providing the Unit of the player and optional parameters. +-- @param #DATABASE self +-- @param #function IteratorFunction The function that will be called when there is was a player in the database. The function needs to accept a UNIT parameter. +-- @return #DATABASE self +function DATABASE:ForEachPlayerJoined( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self.PLAYERSJOINED ) + + return self +end + +--- Iterate the DATABASE and call an iterator function for each CLIENT, providing the CLIENT to the function and optional parameters. +-- @param #DATABASE self +-- @param #function IteratorFunction The function that will be called when there is an alive player in the database. The function needs to accept a CLIENT parameter. +-- @return #DATABASE self +function DATABASE:ForEachClient( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self.CLIENTS ) + + return self +end + + +function DATABASE:_RegisterTemplates() + self:F2() + + self.Navpoints = {} + self.UNITS = {} + --Build routines.db.units and self.Navpoints + for CoalitionName, coa_data in pairs(env.mission.coalition) do + + if (CoalitionName == 'red' or CoalitionName == 'blue') and type(coa_data) == 'table' then + --self.Units[coa_name] = {} + + ---------------------------------------------- + -- build nav points DB + self.Navpoints[CoalitionName] = {} + if coa_data.nav_points then --navpoints + for nav_ind, nav_data in pairs(coa_data.nav_points) do + + if type(nav_data) == 'table' then + self.Navpoints[CoalitionName][nav_ind] = routines.utils.deepCopy(nav_data) + + self.Navpoints[CoalitionName][nav_ind]['name'] = nav_data.callsignStr -- name is a little bit more self-explanatory. + self.Navpoints[CoalitionName][nav_ind]['point'] = {} -- point is used by SSE, support it. + self.Navpoints[CoalitionName][nav_ind]['point']['x'] = nav_data.x + self.Navpoints[CoalitionName][nav_ind]['point']['y'] = 0 + self.Navpoints[CoalitionName][nav_ind]['point']['z'] = nav_data.y + end + end + end + ------------------------------------------------- + if coa_data.country then --there is a country table + for cntry_id, cntry_data in pairs(coa_data.country) do + + local CountryName = string.upper(cntry_data.name) + --self.Units[coa_name][countryName] = {} + --self.Units[coa_name][countryName]["countryId"] = cntry_data.id + + if type(cntry_data) == 'table' then --just making sure + + for obj_type_name, obj_type_data in pairs(cntry_data) do + + if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then --should be an unncessary check + + local CategoryName = obj_type_name + + if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! + + --self.Units[coa_name][countryName][category] = {} + + for group_num, GroupTemplate in pairs(obj_type_data.group) do + + if GroupTemplate and GroupTemplate.units and type(GroupTemplate.units) == 'table' then --making sure again- this is a valid group + self:_RegisterTemplate( + GroupTemplate, + coalition.side[string.upper(CoalitionName)], + _DATABASECategory[string.lower(CategoryName)], + country.id[string.upper(CountryName)] + ) + end --if GroupTemplate and GroupTemplate.units then + end --for group_num, GroupTemplate in pairs(obj_type_data.group) do + end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then + end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then + end --for obj_type_name, obj_type_data in pairs(cntry_data) do + end --if type(cntry_data) == 'table' then + end --for cntry_id, cntry_data in pairs(coa_data.country) do + end --if coa_data.country then --there is a country table + end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then + end --for coa_name, coa_data in pairs(mission.coalition) do + + return self +end + + + + +--- This module contains the SET classes. +-- +-- === +-- +-- 1) @{Set#SET_BASE} class, extends @{Base#BASE} +-- ============================================== +-- The @{Set#SET_BASE} class defines the core functions that define a collection of objects. +-- A SET provides iterators to iterate the SET, but will **temporarily** yield the ForEach interator loop at defined **"intervals"** to the mail simulator loop. +-- In this way, large loops can be done while not blocking the simulator main processing loop. +-- The default **"yield interval"** is after 10 objects processed. +-- The default **"time interval"** is after 0.001 seconds. +-- +-- 1.1) Add or remove objects from the SET +-- --------------------------------------- +-- Some key core functions are @{Set#SET_BASE.Add} and @{Set#SET_BASE.Remove} to add or remove objects from the SET in your logic. +-- +-- 1.2) Define the SET iterator **"yield interval"** and the **"time interval"** +-- ----------------------------------------------------------------------------- +-- Modify the iterator intervals with the @{Set#SET_BASE.SetInteratorIntervals} method. +-- You can set the **"yield interval"**, and the **"time interval"**. (See above). +-- +-- === +-- +-- 2) @{Set#SET_GROUP} class, extends @{Set#SET_BASE} +-- ================================================== +-- Mission designers can use the @{Set#SET_GROUP} class to build sets of groups belonging to certain: +-- +-- * Coalitions +-- * Categories +-- * Countries +-- * Starting with certain prefix strings. +-- +-- 2.1) SET_GROUP construction method: +-- ----------------------------------- +-- Create a new SET_GROUP object with the @{#SET_GROUP.New} method: +-- +-- * @{#SET_GROUP.New}: Creates a new SET_GROUP object. +-- +-- 2.2) Add or Remove GROUP(s) from SET_GROUP: +-- ------------------------------------------- +-- GROUPS can be added and removed using the @{Set#SET_GROUP.AddGroupsByName} and @{Set#SET_GROUP.RemoveGroupsByName} respectively. +-- These methods take a single GROUP name or an array of GROUP names to be added or removed from SET_GROUP. +-- +-- 2.3) SET_GROUP filter criteria: +-- ------------------------------- +-- You can set filter criteria to define the set of groups within the SET_GROUP. +-- Filter criteria are defined by: +-- +-- * @{#SET_GROUP.FilterCoalitions}: Builds the SET_GROUP with the groups belonging to the coalition(s). +-- * @{#SET_GROUP.FilterCategories}: Builds the SET_GROUP with the groups belonging to the category(ies). +-- * @{#SET_GROUP.FilterCountries}: Builds the SET_GROUP with the gruops belonging to the country(ies). +-- * @{#SET_GROUP.FilterPrefixes}: Builds the SET_GROUP with the groups starting with the same prefix string(s). +-- +-- Once the filter criteria have been set for the SET_GROUP, you can start filtering using: +-- +-- * @{#SET_GROUP.FilterStart}: Starts the filtering of the groups within the SET_GROUP and add or remove GROUP objects **dynamically**. +-- +-- Planned filter criteria within development are (so these are not yet available): +-- +-- * @{#SET_GROUP.FilterZones}: Builds the SET_GROUP with the groups within a @{Zone#ZONE}. +-- +-- 2.4) SET_GROUP iterators: +-- ------------------------- +-- Once the filters have been defined and the SET_GROUP has been built, you can iterate the SET_GROUP with the available iterator methods. +-- The iterator methods will walk the SET_GROUP set, and call for each element within the set a function that you provide. +-- The following iterator methods are currently available within the SET_GROUP: +-- +-- * @{#SET_GROUP.ForEachGroup}: Calls a function for each alive group it finds within the SET_GROUP. +-- * @{#SET_GROUP.ForEachGroupCompletelyInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence completely in a @{Zone}, providing the GROUP and optional parameters to the called function. +-- * @{#SET_GROUP.ForEachGroupPartlyInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence partly in a @{Zone}, providing the GROUP and optional parameters to the called function. +-- * @{#SET_GROUP.ForEachGroupNotInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence not in a @{Zone}, providing the GROUP and optional parameters to the called function. +-- +-- ==== +-- +-- 3) @{Set#SET_UNIT} class, extends @{Set#SET_BASE} +-- =================================================== +-- Mission designers can use the @{Set#SET_UNIT} class to build sets of units belonging to certain: +-- +-- * Coalitions +-- * Categories +-- * Countries +-- * Unit types +-- * Starting with certain prefix strings. +-- +-- 3.1) SET_UNIT construction method: +-- ---------------------------------- +-- Create a new SET_UNIT object with the @{#SET_UNIT.New} method: +-- +-- * @{#SET_UNIT.New}: Creates a new SET_UNIT object. +-- +-- 3.2) Add or Remove UNIT(s) from SET_UNIT: +-- ----------------------------------------- +-- UNITs can be added and removed using the @{Set#SET_UNIT.AddUnitsByName} and @{Set#SET_UNIT.RemoveUnitsByName} respectively. +-- These methods take a single UNIT name or an array of UNIT names to be added or removed from SET_UNIT. +-- +-- 3.3) SET_UNIT filter criteria: +-- ------------------------------ +-- You can set filter criteria to define the set of units within the SET_UNIT. +-- Filter criteria are defined by: +-- +-- * @{#SET_UNIT.FilterCoalitions}: Builds the SET_UNIT with the units belonging to the coalition(s). +-- * @{#SET_UNIT.FilterCategories}: Builds the SET_UNIT with the units belonging to the category(ies). +-- * @{#SET_UNIT.FilterTypes}: Builds the SET_UNIT with the units belonging to the unit type(s). +-- * @{#SET_UNIT.FilterCountries}: Builds the SET_UNIT with the units belonging to the country(ies). +-- * @{#SET_UNIT.FilterPrefixes}: Builds the SET_UNIT with the units starting with the same prefix string(s). +-- +-- Once the filter criteria have been set for the SET_UNIT, you can start filtering using: +-- +-- * @{#SET_UNIT.FilterStart}: Starts the filtering of the units within the SET_UNIT. +-- +-- Planned filter criteria within development are (so these are not yet available): +-- +-- * @{#SET_UNIT.FilterZones}: Builds the SET_UNIT with the units within a @{Zone#ZONE}. +-- +-- 3.4) SET_UNIT iterators: +-- ------------------------ +-- Once the filters have been defined and the SET_UNIT has been built, you can iterate the SET_UNIT with the available iterator methods. +-- The iterator methods will walk the SET_UNIT set, and call for each element within the set a function that you provide. +-- The following iterator methods are currently available within the SET_UNIT: +-- +-- * @{#SET_UNIT.ForEachUnit}: Calls a function for each alive unit it finds within the SET_UNIT. +-- * @{#SET_GROUP.ForEachGroupCompletelyInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence completely in a @{Zone}, providing the GROUP and optional parameters to the called function. +-- * @{#SET_GROUP.ForEachGroupNotInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence not in a @{Zone}, providing the GROUP and optional parameters to the called function. +-- +-- Planned iterators methods in development are (so these are not yet available): +-- +-- * @{#SET_UNIT.ForEachUnitInUnit}: Calls a function for each unit contained within the SET_UNIT. +-- * @{#SET_UNIT.ForEachUnitCompletelyInZone}: Iterate and call an iterator function for each **alive** UNIT presence completely in a @{Zone}, providing the UNIT and optional parameters to the called function. +-- * @{#SET_UNIT.ForEachUnitNotInZone}: Iterate and call an iterator function for each **alive** UNIT presence not in a @{Zone}, providing the UNIT and optional parameters to the called function. +-- +-- === +-- +-- 4) @{Set#SET_CLIENT} class, extends @{Set#SET_BASE} +-- =================================================== +-- Mission designers can use the @{Set#SET_CLIENT} class to build sets of units belonging to certain: +-- +-- * Coalitions +-- * Categories +-- * Countries +-- * Client types +-- * Starting with certain prefix strings. +-- +-- 4.1) SET_CLIENT construction method: +-- ---------------------------------- +-- Create a new SET_CLIENT object with the @{#SET_CLIENT.New} method: +-- +-- * @{#SET_CLIENT.New}: Creates a new SET_CLIENT object. +-- +-- 4.2) Add or Remove CLIENT(s) from SET_CLIENT: +-- ----------------------------------------- +-- CLIENTs can be added and removed using the @{Set#SET_CLIENT.AddClientsByName} and @{Set#SET_CLIENT.RemoveClientsByName} respectively. +-- These methods take a single CLIENT name or an array of CLIENT names to be added or removed from SET_CLIENT. +-- +-- 4.3) SET_CLIENT filter criteria: +-- ------------------------------ +-- You can set filter criteria to define the set of clients within the SET_CLIENT. +-- Filter criteria are defined by: +-- +-- * @{#SET_CLIENT.FilterCoalitions}: Builds the SET_CLIENT with the clients belonging to the coalition(s). +-- * @{#SET_CLIENT.FilterCategories}: Builds the SET_CLIENT with the clients belonging to the category(ies). +-- * @{#SET_CLIENT.FilterTypes}: Builds the SET_CLIENT with the clients belonging to the client type(s). +-- * @{#SET_CLIENT.FilterCountries}: Builds the SET_CLIENT with the clients belonging to the country(ies). +-- * @{#SET_CLIENT.FilterPrefixes}: Builds the SET_CLIENT with the clients starting with the same prefix string(s). +-- +-- Once the filter criteria have been set for the SET_CLIENT, you can start filtering using: +-- +-- * @{#SET_CLIENT.FilterStart}: Starts the filtering of the clients within the SET_CLIENT. +-- +-- Planned filter criteria within development are (so these are not yet available): +-- +-- * @{#SET_CLIENT.FilterZones}: Builds the SET_CLIENT with the clients within a @{Zone#ZONE}. +-- +-- 4.4) SET_CLIENT iterators: +-- ------------------------ +-- Once the filters have been defined and the SET_CLIENT has been built, you can iterate the SET_CLIENT with the available iterator methods. +-- The iterator methods will walk the SET_CLIENT set, and call for each element within the set a function that you provide. +-- The following iterator methods are currently available within the SET_CLIENT: +-- +-- * @{#SET_CLIENT.ForEachClient}: Calls a function for each alive client it finds within the SET_CLIENT. +-- +-- ==== +-- +-- 5) @{Set#SET_AIRBASE} class, extends @{Set#SET_BASE} +-- ==================================================== +-- Mission designers can use the @{Set#SET_AIRBASE} class to build sets of airbases optionally belonging to certain: +-- +-- * Coalitions +-- +-- 5.1) SET_AIRBASE construction +-- ----------------------------- +-- Create a new SET_AIRBASE object with the @{#SET_AIRBASE.New} method: +-- +-- * @{#SET_AIRBASE.New}: Creates a new SET_AIRBASE object. +-- +-- 5.2) Add or Remove AIRBASEs from SET_AIRBASE +-- -------------------------------------------- +-- AIRBASEs can be added and removed using the @{Set#SET_AIRBASE.AddAirbasesByName} and @{Set#SET_AIRBASE.RemoveAirbasesByName} respectively. +-- These methods take a single AIRBASE name or an array of AIRBASE names to be added or removed from SET_AIRBASE. +-- +-- 5.3) SET_AIRBASE filter criteria +-- -------------------------------- +-- You can set filter criteria to define the set of clients within the SET_AIRBASE. +-- Filter criteria are defined by: +-- +-- * @{#SET_AIRBASE.FilterCoalitions}: Builds the SET_AIRBASE with the airbases belonging to the coalition(s). +-- +-- Once the filter criteria have been set for the SET_AIRBASE, you can start filtering using: +-- +-- * @{#SET_AIRBASE.FilterStart}: Starts the filtering of the airbases within the SET_AIRBASE. +-- +-- 5.4) SET_AIRBASE iterators: +-- --------------------------- +-- Once the filters have been defined and the SET_AIRBASE has been built, you can iterate the SET_AIRBASE with the available iterator methods. +-- The iterator methods will walk the SET_AIRBASE set, and call for each airbase within the set a function that you provide. +-- The following iterator methods are currently available within the SET_AIRBASE: +-- +-- * @{#SET_AIRBASE.ForEachAirbase}: Calls a function for each airbase it finds within the SET_AIRBASE. +-- +-- ==== +-- +-- ### 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 + + _EVENTDISPATCHER:OnBirth( self._EventOnBirth, self ) + _EVENTDISPATCHER:OnDead( self._EventOnDeadOrCrash, self ) + _EVENTDISPATCHER:OnCrash( self._EventOnDeadOrCrash, self ) + + -- Follow alive players and clients + _EVENTDISPATCHER:OnPlayerEnterUnit( self._EventOnPlayerEnterUnit, self ) + _EVENTDISPATCHER:OnPlayerLeaveUnit( self._EventOnPlayerLeaveUnit, self ) + + + return self +end + +--- Stops the filtering for the defined collection. +-- @param #SET_BASE self +-- @return #SET_BASE self +function SET_BASE:FilterStop() + + _EVENTDISPATCHER:OnBirthRemove( self ) + _EVENTDISPATCHER:OnDeadRemove( self ) + _EVENTDISPATCHER:OnCrashRemove( self ) + + 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 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 not self.Database[Event.IniDCSGroupName] then + self.Database[Event.IniDCSGroupName] = GROUP:Register( Event.IniDCSGroupName ) + self:T3( self.Database[Event.IniDCSGroupName] ) + end + + return Event.IniDCSGroupName, self.Database[Event.IniDCSGroupName] +end + +--- Handles the Database to check on any event that Object exists in the Database. +-- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! +-- @param #SET_GROUP self +-- @param 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 not self.Database[Event.IniDCSUnitName] then + self.Database[Event.IniDCSUnitName] = UNIT:Register( Event.IniDCSUnitName ) + self:T3( self.Database[Event.IniDCSUnitName] ) + end + + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] +end + +--- Handles the Database to check on any event that Object exists in the Database. +-- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! +-- @param #SET_UNIT self +-- @param 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}. +-- +-- +-- 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}. +-- +-- === +-- +-- **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: +-- +-- 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 +-- @extends Core.Base#BASE +-- @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 +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 +-- @extends #POINT_VEC3 +-- @field Dcs.DCSTypes#Distance x The x coordinate in meters. +-- @field Dcs.DCSTypes#Distance y the y coordinate in meters. +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 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 #number x The x coordinate. +function POINT_VEC3:SetX( x ) + self.x = x +end + +--- Set the y coordinate of the POINT_VEC3. +-- @param #number y The y coordinate. +function POINT_VEC3:SetY( y ) + self.y = y +end + +--- Set the z coordinate of the POINT_VEC3. +-- @param #number z The z coordinate. +function POINT_VEC3:SetZ( z ) + self.z = z +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 + + +--- 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 #number x The x coordinate. +function POINT_VEC2:SetX( x ) + self.x = x +end + +--- Set the y coordinate of the POINT_VEC2. +-- @param #number y The y coordinate. +function POINT_VEC2:SetY( y ) + self.z = y +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 all players. +-- @param #MESSAGE self +-- @return #MESSAGE +-- @usage +-- -- Send a message created to all players. +-- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ):ToAll() +-- or +-- MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ):ToAll() +-- or +-- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ) +-- MessageAll:ToAll() +function MESSAGE:ToAll() + self:F() + + self:ToCoalition( coalition.side.RED ) + self:ToCoalition( coalition.side.BLUE ) + + return self +end + + + +----- The MESSAGEQUEUE class +---- @type MESSAGEQUEUE +--MESSAGEQUEUE = { +-- ClientGroups = {}, +-- CoalitionSides = {} +--} +-- +--function MESSAGEQUEUE:New( RefreshInterval ) +-- local self = BASE:Inherit( self, BASE:New() ) +-- self:F( { RefreshInterval } ) +-- +-- self.RefreshInterval = RefreshInterval +-- +-- --self.DisplayFunction = routines.scheduleFunction( self._DisplayMessages, { self }, 0, RefreshInterval ) +-- self.DisplayFunction = SCHEDULER:New( self, self._DisplayMessages, {}, 0, RefreshInterval ) +-- +-- return self +--end +-- +----- This function is called automatically by the MESSAGEQUEUE scheduler. +--function MESSAGEQUEUE:_DisplayMessages() +-- +-- -- First we display all messages that a coalition needs to receive... Also those who are not in a client (CA module clients...). +-- for CoalitionSideID, CoalitionSideData in pairs( self.CoalitionSides ) do +-- for MessageID, MessageData in pairs( CoalitionSideData.Messages ) do +-- if MessageData.MessageSent == false then +-- --trigger.action.outTextForCoalition( CoalitionSideID, MessageData.MessageCategory .. '\n' .. MessageData.MessageText:gsub("\n$",""):gsub("\n$",""), MessageData.MessageDuration ) +-- MessageData.MessageSent = true +-- end +-- local MessageTimeLeft = ( MessageData.MessageTime + MessageData.MessageDuration ) - timer.getTime() +-- if MessageTimeLeft <= 0 then +-- MessageData = nil +-- end +-- end +-- end +-- +-- -- Then we send the messages for each individual client, but also to be included are those Coalition messages for the Clients who belong to a coalition. +-- -- Because the Client messages will overwrite the Coalition messages (for that Client). +-- for ClientGroupName, ClientGroupData in pairs( self.ClientGroups ) do +-- for MessageID, MessageData in pairs( ClientGroupData.Messages ) do +-- if MessageData.MessageGroup == false then +-- trigger.action.outTextForGroup( Group.getByName(ClientGroupName):getID(), MessageData.MessageCategory .. '\n' .. MessageData.MessageText:gsub("\n$",""):gsub("\n$",""), MessageData.MessageDuration ) +-- MessageData.MessageGroup = true +-- end +-- local MessageTimeLeft = ( MessageData.MessageTime + MessageData.MessageDuration ) - timer.getTime() +-- if MessageTimeLeft <= 0 then +-- MessageData = nil +-- end +-- end +-- +-- -- Now check if the Client also has messages that belong to the Coalition of the Client... +-- for CoalitionSideID, CoalitionSideData in pairs( self.CoalitionSides ) do +-- for MessageID, MessageData in pairs( CoalitionSideData.Messages ) do +-- local CoalitionGroup = Group.getByName( ClientGroupName ) +-- if CoalitionGroup and CoalitionGroup:getCoalition() == CoalitionSideID then +-- if MessageData.MessageCoalition == false then +-- trigger.action.outTextForGroup( Group.getByName(ClientGroupName):getID(), MessageData.MessageCategory .. '\n' .. MessageData.MessageText:gsub("\n$",""):gsub("\n$",""), MessageData.MessageDuration ) +-- MessageData.MessageCoalition = true +-- end +-- end +-- local MessageTimeLeft = ( MessageData.MessageTime + MessageData.MessageDuration ) - timer.getTime() +-- if MessageTimeLeft <= 0 then +-- MessageData = nil +-- end +-- end +-- end +-- end +-- +-- return true +--end +-- +----- The _MessageQueue object is created when the MESSAGE class module is loaded. +----_MessageQueue = MESSAGEQUEUE:New( 0.5 ) +-- +--- 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.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) + if self[handler] then + self:T( "Calling " .. handler ) + 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) == false ) + or ( self:_call_handler("OnBefore" .. EventName, params) == false ) + or ( self:_call_handler("onleave" .. from, params) == false ) + or ( self:_call_handler("OnLeave" .. from, params) == 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) + self:_call_handler("OnEnter" .. to, params) + self:_call_handler("onafter" .. EventName, params) + self:_call_handler("OnAfter" .. EventName, params) + self:_call_handler("onstatechange", params) + 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) + self:_call_handler("OnEnter" .. to, params) + --end + + self:_call_handler("onafter" .. EventName, params) + self:_call_handler("OnAfter" .. EventName, params) + + self:_call_handler("onstatechange", params) + 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 = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1 ) + 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 ) + + 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 ) + 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 ) + if self[handler] then + self:T( "Calling " .. handler ) + 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 ) + if self[handler] then + self:T( "Calling " .. handler ) + 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 + + + + + + + + + +--- 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:E( 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 AttackUnit The UNIT. +-- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. +-- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. +-- @param 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:EnRouteTaskEngageUnit( AttackUnit, Priority, WeaponType, WeaponExpend, AttackQty, Direction, AttackQtyLimit, ControllableAttack ) + self:F2( { self.ControllableName, AttackUnit, Priority, WeaponType, WeaponExpend, AttackQty, Direction, AttackQtyLimit, ControllableAttack } ) + + -- EngageUnit = { + -- id = 'EngageUnit', + -- params = { + -- unitId = Unit.ID, + -- weaponType = number, + -- expend = enum AI.Task.WeaponExpend + -- attackQty = number, + -- direction = Azimuth, + -- attackQtyLimit = boolean, + -- controllableAttack = boolean, + -- priority = number, + -- } + -- } + + local DCSTask + DCSTask = { id = 'EngageUnit', + params = { + unitId = AttackUnit:GetID(), + weaponType = WeaponType, + expend = WeaponExpend, + attackQty = AttackQty, + direction = Direction, + attackQtyLimit = AttackQtyLimit, + controllableAttack = ControllableAttack, + priority = Priority, + }, + }, + + self:T3( { DCSTask } ) + return DCSTask +end + + + +--- (AIR) Aircraft will act as an AWACS for friendly units (will provide them with information about contacts). No parameters. +-- @param #CONTROLLABLE self +-- @return 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:TaskRouteToVec2( 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:TaskRouteToVec3( 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() + 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 + -- TODO: Rename IsPointVec3InZone to IsVec3InZone + if Zone:IsPointVec3InZone( 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:IsPointVec3InZone( 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:IsPointVec3InZone( 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. +function UNIT:GetThreatLevel() + + local Attributes = self:GetDesc().attributes + local ThreatLevel = 0 + + 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" + } + + self:T2( Attributes ) + + 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"] then ThreatLevel = 2 + elseif Attributes["Infantry"] then ThreatLevel = 1 + end + + self:T2( ThreatLevel ) + return ThreatLevel, ThreatLevels[ThreatLevel+1] + +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:IsPointVec3InZone( 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:IsPointVec3InZone( 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 + +--- 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. +-- @return #STATIC +function STATIC:FindByName( StaticName ) + local StaticFound = _DATABASE:FindStatic( StaticName ) + + self.StaticName = StaticName + + if StaticFound then + StaticFound:F( { StaticName } ) + + return StaticFound + end + + error( "STATIC not found for: " .. StaticName ) +end + +function STATIC:Register( StaticName ) + local self = BASE:Inherit( self, 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 +--- 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 + + + +--- Scoring system for MOOSE. +-- This scoring class calculates the hits and kills that players make within a simulation session. +-- Scoring is calculated using a defined algorithm. +-- With a small change in MissionScripting.lua, the scoring can also be logged in a CSV file, that can then be uploaded +-- to a database or a BI tool to publish the scoring results to the player community. +-- @module Scoring +-- @author FlightControl + + +--- The Scoring class +-- @type SCORING +-- @field Players A collection of the current players that have joined the game. +-- @extends 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() ) + + if GameName then + self.GameName = GameName + else + error( "A game name must be given to register the scoring results" ) + end + + + _EVENTDISPATCHER:OnDead( self._EventOnDeadOrCrash, self ) + _EVENTDISPATCHER:OnCrash( self._EventOnDeadOrCrash, self ) + _EVENTDISPATCHER:OnHit( self._EventOnHit, self ) + + --self.SchedulerId = routines.scheduleFunction( SCORING._FollowPlayersScheduled, { self }, 0, 5 ) + self.SchedulerId = SCHEDULER:New( self, self._FollowPlayersScheduled, {}, 0, 5 ) + + self:ScoreMenu() + + self:OpenCSV( GameName) + + return self + +end + +--- Creates a score radio menu. Can be accessed using Radio -> F10. +-- @param #SCORING self +-- @return #SCORING self +function SCORING:ScoreMenu() + self.Menu = MENU_MISSION:New( 'Scoring' ) + self.AllScoresMenu = MENU_MISSION_COMMAND:New( 'Score All Active Players', self.Menu, SCORING.ReportScoreAll, self ) + --- = COMMANDMENU:New('Your Current Score', ReportScore, SCORING.ReportScorePlayer, self ) + return self +end + +--- Follows new players entering Clients within the DCSRTE. +-- TODO: Need to see if i can catch this also with an event. It will eliminate the schedule ... +function SCORING:_FollowPlayersScheduled() + self:F3( "_FollowPlayersScheduled" ) + + local ClientUnit = 0 + local CoalitionsData = { AlivePlayersRed = coalition.getPlayers(coalition.side.RED), AlivePlayersBlue = coalition.getPlayers(coalition.side.BLUE) } + local unitId + local unitData + local AlivePlayerUnits = {} + + for CoalitionId, CoalitionData in pairs( CoalitionsData ) do + self:T3( { "_FollowPlayersScheduled", CoalitionData } ) + for UnitId, UnitData in pairs( CoalitionData ) do + self:_AddPlayerFromUnit( UnitData ) + end + end + + return true +end + + +--- Track DEAD or CRASH events for the scoring. +-- @param #SCORING self +-- @param 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.IniDCSUnit + TargetUnitName = Event.IniDCSUnitName + TargetGroup = Event.IniDCSGroup + TargetGroupName = Event.IniDCSGroupName + TargetPlayerName = TargetUnit:getPlayerName() + + TargetCoalition = TargetUnit:getCoalition() + --TargetCategory = TargetUnit:getCategory() + TargetCategory = TargetUnit:getDesc().category -- Workaround + TargetType = TargetUnit:getTypeName() + + TargetUnitCoalition = _SCORINGCoalition[TargetCoalition] + TargetUnitCategory = _SCORINGCategory[TargetCategory] + TargetUnitType = TargetType + + self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType } ) + end + + for PlayerName, PlayerData in pairs( self.Players ) do + if PlayerData then -- This should normally not happen, but i'll test it anyway. + self:T( "Something got killed" ) + + -- Some variables + local InitUnitName = PlayerData.UnitName + local InitUnitType = PlayerData.UnitType + local InitCoalition = PlayerData.UnitCoalition + local InitCategory = PlayerData.UnitCategory + local InitUnitCoalition = _SCORINGCoalition[InitCoalition] + local InitUnitCategory = _SCORINGCategory[InitCategory] + + self:T( { InitUnitName, InitUnitType, InitUnitCoalition, InitCoalition, InitUnitCategory, InitCategory } ) + + -- What is he hitting? + if TargetCategory then + if PlayerData and PlayerData.Hit and PlayerData.Hit[TargetCategory] and PlayerData.Hit[TargetCategory][TargetUnitName] then -- Was there a hit for this unit for this player before registered??? + if not PlayerData.Kill[TargetCategory] then + PlayerData.Kill[TargetCategory] = {} + end + if not PlayerData.Kill[TargetCategory][TargetType] then + PlayerData.Kill[TargetCategory][TargetType] = {} + PlayerData.Kill[TargetCategory][TargetType].Score = 0 + PlayerData.Kill[TargetCategory][TargetType].ScoreKill = 0 + PlayerData.Kill[TargetCategory][TargetType].Penalty = 0 + PlayerData.Kill[TargetCategory][TargetType].PenaltyKill = 0 + end + + if InitCoalition == TargetCoalition then + PlayerData.Penalty = PlayerData.Penalty + 25 + PlayerData.Kill[TargetCategory][TargetType].Penalty = PlayerData.Kill[TargetCategory][TargetType].Penalty + 25 + PlayerData.Kill[TargetCategory][TargetType].PenaltyKill = PlayerData.Kill[TargetCategory][TargetType].PenaltyKill + 1 + MESSAGE:New( "Player '" .. PlayerName .. "' killed a friendly " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. + PlayerData.Kill[TargetCategory][TargetType].PenaltyKill .. " times. Penalty: -" .. PlayerData.Kill[TargetCategory][TargetType].Penalty .. + ". Score Total:" .. PlayerData.Score - PlayerData.Penalty, + 5 ):ToAll() + self:ScoreCSV( PlayerName, "KILL_PENALTY", 1, -125, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + else + PlayerData.Score = PlayerData.Score + 10 + PlayerData.Kill[TargetCategory][TargetType].Score = PlayerData.Kill[TargetCategory][TargetType].Score + 10 + PlayerData.Kill[TargetCategory][TargetType].ScoreKill = PlayerData.Kill[TargetCategory][TargetType].ScoreKill + 1 + MESSAGE:New( "Player '" .. PlayerName .. "' killed an enemy " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. + PlayerData.Kill[TargetCategory][TargetType].ScoreKill .. " times. Score: " .. PlayerData.Kill[TargetCategory][TargetType].Score .. + ". Score Total:" .. PlayerData.Score - PlayerData.Penalty, + 5 ):ToAll() + self:ScoreCSV( PlayerName, "KILL_SCORE", 1, 10, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + end + end + end + end + end +end + + + +--- Add a new player entering a Unit. +function SCORING:_AddPlayerFromUnit( UnitData ) + self:F( UnitData ) + + if UnitData and UnitData:isExist() then + local UnitName = UnitData:getName() + local PlayerName = UnitData:getPlayerName() + local UnitDesc = UnitData:getDesc() + local UnitCategory = UnitDesc.category + local UnitCoalition = UnitData:getCoalition() + local UnitTypeName = UnitData:getTypeName() + + self:T( { PlayerName, UnitName, UnitCategory, UnitCoalition, UnitTypeName } ) + + if self.Players[PlayerName] == nil then -- I believe this is the place where a Player gets a life in a mission when he enters a unit ... + self.Players[PlayerName] = {} + self.Players[PlayerName].Hit = {} + self.Players[PlayerName].Kill = {} + self.Players[PlayerName].Mission = {} + + -- for CategoryID, CategoryName in pairs( SCORINGCategory ) do + -- self.Players[PlayerName].Hit[CategoryID] = {} + -- self.Players[PlayerName].Kill[CategoryID] = {} + -- end + self.Players[PlayerName].HitPlayers = {} + self.Players[PlayerName].HitUnits = {} + self.Players[PlayerName].Score = 0 + self.Players[PlayerName].Penalty = 0 + self.Players[PlayerName].PenaltyCoalition = 0 + self.Players[PlayerName].PenaltyWarning = 0 + end + + if not self.Players[PlayerName].UnitCoalition then + self.Players[PlayerName].UnitCoalition = UnitCoalition + else + if self.Players[PlayerName].UnitCoalition ~= UnitCoalition then + self.Players[PlayerName].Penalty = self.Players[PlayerName].Penalty + 50 + self.Players[PlayerName].PenaltyCoalition = self.Players[PlayerName].PenaltyCoalition + 1 + MESSAGE:New( "Player '" .. PlayerName .. "' changed coalition from " .. _SCORINGCoalition[self.Players[PlayerName].UnitCoalition] .. " to " .. _SCORINGCoalition[UnitCoalition] .. + "(changed " .. self.Players[PlayerName].PenaltyCoalition .. " times the coalition). 50 Penalty points added.", + 2 + ):ToAll() + self:ScoreCSV( PlayerName, "COALITION_PENALTY", 1, -50, self.Players[PlayerName].UnitName, _SCORINGCoalition[self.Players[PlayerName].UnitCoalition], _SCORINGCategory[self.Players[PlayerName].UnitCategory], self.Players[PlayerName].UnitType, + UnitName, _SCORINGCoalition[UnitCoalition], _SCORINGCategory[UnitCategory], UnitData:getTypeName() ) + end + end + self.Players[PlayerName].UnitName = UnitName + self.Players[PlayerName].UnitCoalition = UnitCoalition + self.Players[PlayerName].UnitCategory = UnitCategory + self.Players[PlayerName].UnitType = UnitTypeName + + if self.Players[PlayerName].Penalty > 100 then + if self.Players[PlayerName].PenaltyWarning < 1 then + MESSAGE:New( "Player '" .. PlayerName .. "': WARNING! If you continue to commit FRATRICIDE and have a PENALTY score higher than 150, you will be COURT MARTIALED and DISMISSED from this mission! \nYour total penalty is: " .. self.Players[PlayerName].Penalty, + 30 + ):ToAll() + self.Players[PlayerName].PenaltyWarning = self.Players[PlayerName].PenaltyWarning + 1 + end + end + + if self.Players[PlayerName].Penalty > 150 then + ClientGroup = GROUP:NewFromDCSUnit( UnitData ) + ClientGroup:Destroy() + MESSAGE:New( "Player '" .. PlayerName .. "' committed FRATRICIDE, he will be COURT MARTIALED and is DISMISSED from this mission!", + 10 + ):ToAll() + end + + end +end + + +--- Registers Scores the players completing a Mission Task. +-- @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 OnHit event for the scoring. +-- @param #SCORING self +-- @param Core.Event#EVENTDATA Event +function SCORING:_EventOnHit( Event ) + self:F( { Event } ) + + local InitUnit = nil + local InitUnitName = "" + local InitGroup = nil + local InitGroupName = "" + local InitPlayerName = nil + + local InitCoalition = nil + local InitCategory = nil + local InitType = nil + local InitUnitCoalition = nil + local InitUnitCategory = nil + local InitUnitType = nil + + local TargetUnit = nil + local TargetUnitName = "" + local TargetGroup = nil + local TargetGroupName = "" + local TargetPlayerName = "" + + local TargetCoalition = nil + local TargetCategory = nil + local TargetType = nil + local TargetUnitCoalition = nil + local TargetUnitCategory = nil + local TargetUnitType = nil + + if Event.IniDCSUnit then + + InitUnit = Event.IniDCSUnit + InitUnitName = Event.IniDCSUnitName + InitGroup = Event.IniDCSGroup + InitGroupName = Event.IniDCSGroupName + InitPlayerName = InitUnit:getPlayerName() + + InitCoalition = InitUnit:getCoalition() + --TODO: Workaround Client DCS Bug + --InitCategory = InitUnit:getCategory() + InitCategory = InitUnit:getDesc().category + InitType = InitUnit:getTypeName() + + InitUnitCoalition = _SCORINGCoalition[InitCoalition] + InitUnitCategory = _SCORINGCategory[InitCategory] + InitUnitType = InitType + + self:T( { InitUnitName, InitGroupName, InitPlayerName, InitCoalition, InitCategory, InitType , InitUnitCoalition, InitUnitCategory, InitUnitType } ) + end + + + if Event.TgtDCSUnit then + + TargetUnit = Event.TgtDCSUnit + TargetUnitName = Event.TgtDCSUnitName + TargetGroup = Event.TgtDCSGroup + TargetGroupName = Event.TgtDCSGroupName + TargetPlayerName = TargetUnit:getPlayerName() + + TargetCoalition = TargetUnit:getCoalition() + --TODO: Workaround Client DCS Bug + --TargetCategory = TargetUnit:getCategory() + TargetCategory = TargetUnit:getDesc().category + TargetType = TargetUnit:getTypeName() + + TargetUnitCoalition = _SCORINGCoalition[TargetCoalition] + TargetUnitCategory = _SCORINGCategory[TargetCategory] + TargetUnitType = TargetType + + self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType, TargetUnitCoalition, TargetUnitCategory, TargetUnitType } ) + end + + if InitPlayerName ~= nil then -- It is a player that is hitting something + self:_AddPlayerFromUnit( InitUnit ) + if self.Players[InitPlayerName] then -- This should normally not happen, but i'll test it anyway. + if TargetPlayerName ~= nil then -- It is a player hitting another player ... + self:_AddPlayerFromUnit( TargetUnit ) + self.Players[InitPlayerName].HitPlayers = self.Players[InitPlayerName].HitPlayers + 1 + end + + self:T( "Hitting Something" ) + -- What is he hitting? + if TargetCategory then + if not self.Players[InitPlayerName].Hit[TargetCategory] then + self.Players[InitPlayerName].Hit[TargetCategory] = {} + end + if not self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName] then + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName] = {} + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score = 0 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty = 0 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit = 0 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit = 0 + end + local Score = 0 + if InitCoalition == TargetCoalition then + self.Players[InitPlayerName].Penalty = self.Players[InitPlayerName].Penalty + 10 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty + 10 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit + 1 + MESSAGE:New( "Player '" .. InitPlayerName .. "' hit a friendly " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit .. " times. Penalty: -" .. self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty .. + ". Score Total:" .. self.Players[InitPlayerName].Score - self.Players[InitPlayerName].Penalty, + 2 + ):ToAll() + self:ScoreCSV( InitPlayerName, "HIT_PENALTY", 1, -25, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + else + self.Players[InitPlayerName].Score = self.Players[InitPlayerName].Score + 1 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score + 1 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit + 1 + MESSAGE:New( "Player '" .. InitPlayerName .. "' hit a target " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit .. " times. Score: " .. self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score .. + ". Score Total:" .. self.Players[InitPlayerName].Score - self.Players[InitPlayerName].Penalty, + 2 + ):ToAll() + self:ScoreCSV( InitPlayerName, "HIT_SCORE", 1, 1, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + end + end + end + elseif InitPlayerName == nil then -- It is an AI hitting a player??? + + end +end + + +function SCORING:ReportScoreAll() + + env.info( "Hello World " ) + + local ScoreMessage = "" + local PlayerMessage = "" + + self:T( "Score Report" ) + + for PlayerName, PlayerData in pairs( self.Players ) do + if PlayerData then -- This should normally not happen, but i'll test it anyway. + self:T( "Score Player: " .. PlayerName ) + + -- Some variables + local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] + local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] + local InitUnitType = PlayerData.UnitType + local InitUnitName = PlayerData.UnitName + + local PlayerScore = 0 + local PlayerPenalty = 0 + + ScoreMessage = ":\n" + + local ScoreMessageHits = "" + + for CategoryID, CategoryName in pairs( _SCORINGCategory ) do + self:T( CategoryName ) + if PlayerData.Hit[CategoryID] then + local Score = 0 + local ScoreHit = 0 + local Penalty = 0 + local PenaltyHit = 0 + self:T( "Hit scores exist for player " .. PlayerName ) + for UnitName, UnitData in pairs( PlayerData.Hit[CategoryID] ) do + Score = Score + UnitData.Score + ScoreHit = ScoreHit + UnitData.ScoreHit + Penalty = Penalty + UnitData.Penalty + PenaltyHit = UnitData.PenaltyHit + end + local ScoreMessageHit = string.format( "%s:%d ", CategoryName, Score - Penalty ) + self:T( ScoreMessageHit ) + ScoreMessageHits = ScoreMessageHits .. ScoreMessageHit + PlayerScore = PlayerScore + Score + PlayerPenalty = PlayerPenalty + Penalty + else + --ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) + end + end + if ScoreMessageHits ~= "" then + ScoreMessage = ScoreMessage .. " Hits: " .. ScoreMessageHits .. "\n" + end + + local ScoreMessageKills = "" + for CategoryID, CategoryName in pairs( _SCORINGCategory ) do + self:T( "Kill scores exist for player " .. PlayerName ) + if PlayerData.Kill[CategoryID] then + local Score = 0 + local ScoreKill = 0 + local Penalty = 0 + local PenaltyKill = 0 + + for UnitName, UnitData in pairs( PlayerData.Kill[CategoryID] ) do + Score = Score + UnitData.Score + ScoreKill = ScoreKill + UnitData.ScoreKill + Penalty = Penalty + UnitData.Penalty + PenaltyKill = PenaltyKill + UnitData.PenaltyKill + end + + local ScoreMessageKill = string.format( " %s:%d ", CategoryName, Score - Penalty ) + self:T( ScoreMessageKill ) + ScoreMessageKills = ScoreMessageKills .. ScoreMessageKill + + PlayerScore = PlayerScore + Score + PlayerPenalty = PlayerPenalty + Penalty + else + --ScoreMessageKills = ScoreMessageKills .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) + end + end + if ScoreMessageKills ~= "" then + ScoreMessage = ScoreMessage .. " Kills: " .. ScoreMessageKills .. "\n" + end + + local ScoreMessageCoalitionChangePenalties = "" + if PlayerData.PenaltyCoalition ~= 0 then + ScoreMessageCoalitionChangePenalties = ScoreMessageCoalitionChangePenalties .. string.format( " -%d (%d changed)", PlayerData.Penalty, PlayerData.PenaltyCoalition ) + PlayerPenalty = PlayerPenalty + PlayerData.Penalty + end + if ScoreMessageCoalitionChangePenalties ~= "" then + ScoreMessage = ScoreMessage .. " Coalition Penalties: " .. ScoreMessageCoalitionChangePenalties .. "\n" + end + + local ScoreMessageMission = "" + local ScoreMission = 0 + local ScoreTask = 0 + for MissionName, MissionData in pairs( PlayerData.Mission ) do + ScoreMission = ScoreMission + MissionData.ScoreMission + ScoreTask = ScoreTask + MissionData.ScoreTask + ScoreMessageMission = ScoreMessageMission .. "'" .. MissionName .. "'; " + end + PlayerScore = PlayerScore + ScoreMission + ScoreTask + + if ScoreMessageMission ~= "" then + ScoreMessage = ScoreMessage .. " Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ")\n" + end + + PlayerMessage = PlayerMessage .. string.format( "Player '%s' Score:%d (%d Score -%d Penalties)%s", PlayerName, PlayerScore - PlayerPenalty, PlayerScore, PlayerPenalty, ScoreMessage ) + end + end + MESSAGE:New( PlayerMessage, 30, "Player Scores" ):ToAll() +end + + +function SCORING:ReportScorePlayer() + + env.info( "Hello World " ) + + local ScoreMessage = "" + local PlayerMessage = "" + + self:T( "Score Report" ) + + for PlayerName, PlayerData in pairs( self.Players ) do + if PlayerData then -- This should normally not happen, but i'll test it anyway. + self:T( "Score Player: " .. PlayerName ) + + -- Some variables + local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] + local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] + local InitUnitType = PlayerData.UnitType + local InitUnitName = PlayerData.UnitName + + local PlayerScore = 0 + local PlayerPenalty = 0 + + ScoreMessage = "" + + local ScoreMessageHits = "" + + for CategoryID, CategoryName in pairs( _SCORINGCategory ) do + self:T( CategoryName ) + if PlayerData.Hit[CategoryID] then + local Score = 0 + local ScoreHit = 0 + local Penalty = 0 + local PenaltyHit = 0 + self:T( "Hit scores exist for player " .. PlayerName ) + for UnitName, UnitData in pairs( PlayerData.Hit[CategoryID] ) do + Score = Score + UnitData.Score + ScoreHit = ScoreHit + UnitData.ScoreHit + Penalty = Penalty + UnitData.Penalty + PenaltyHit = UnitData.PenaltyHit + end + local ScoreMessageHit = string.format( "\n %s = %d score(%d;-%d) hits(#%d;#-%d)", CategoryName, Score - Penalty, Score, Penalty, ScoreHit, PenaltyHit ) + self:T( ScoreMessageHit ) + ScoreMessageHits = ScoreMessageHits .. ScoreMessageHit + PlayerScore = PlayerScore + Score + PlayerPenalty = PlayerPenalty + Penalty + else + --ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) + end + end + if ScoreMessageHits ~= "" then + ScoreMessage = ScoreMessage .. "\n Hits: " .. ScoreMessageHits .. " " + end + + local ScoreMessageKills = "" + for CategoryID, CategoryName in pairs( _SCORINGCategory ) do + self:T( "Kill scores exist for player " .. PlayerName ) + if PlayerData.Kill[CategoryID] then + local Score = 0 + local ScoreKill = 0 + local Penalty = 0 + local PenaltyKill = 0 + + for UnitName, UnitData in pairs( PlayerData.Kill[CategoryID] ) do + Score = Score + UnitData.Score + ScoreKill = ScoreKill + UnitData.ScoreKill + Penalty = Penalty + UnitData.Penalty + PenaltyKill = PenaltyKill + UnitData.PenaltyKill + end + + local ScoreMessageKill = string.format( "\n %s = %d score(%d;-%d) hits(#%d;#-%d)", CategoryName, Score - Penalty, Score, Penalty, ScoreKill, PenaltyKill ) + self:T( ScoreMessageKill ) + ScoreMessageKills = ScoreMessageKills .. ScoreMessageKill + + PlayerScore = PlayerScore + Score + PlayerPenalty = PlayerPenalty + Penalty + else + --ScoreMessageKills = ScoreMessageKills .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) + end + end + if ScoreMessageKills ~= "" then + ScoreMessage = ScoreMessage .. "\n Kills: " .. ScoreMessageKills .. " " + end + + local ScoreMessageCoalitionChangePenalties = "" + if PlayerData.PenaltyCoalition ~= 0 then + ScoreMessageCoalitionChangePenalties = ScoreMessageCoalitionChangePenalties .. string.format( " -%d (%d changed)", PlayerData.Penalty, PlayerData.PenaltyCoalition ) + PlayerPenalty = PlayerPenalty + PlayerData.Penalty + end + if ScoreMessageCoalitionChangePenalties ~= "" then + ScoreMessage = ScoreMessage .. "\n Coalition: " .. ScoreMessageCoalitionChangePenalties .. " " + end + + local ScoreMessageMission = "" + local ScoreMission = 0 + local ScoreTask = 0 + for MissionName, MissionData in pairs( PlayerData.Mission ) do + ScoreMission = ScoreMission + MissionData.ScoreMission + ScoreTask = ScoreTask + MissionData.ScoreTask + ScoreMessageMission = ScoreMessageMission .. "'" .. MissionName .. "'; " + end + PlayerScore = PlayerScore + ScoreMission + ScoreTask + + if ScoreMessageMission ~= "" then + ScoreMessage = ScoreMessage .. "\n Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ") " + end + + PlayerMessage = PlayerMessage .. string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties ):%s", PlayerName, PlayerScore - PlayerPenalty, PlayerScore, PlayerPenalty, ScoreMessage ) + end + end + MESSAGE:New( PlayerMessage, 30, "Player Scores" ):ToAll() + +end + + +function SCORING:SecondsToClock(sSeconds) + local nSeconds = sSeconds + if nSeconds == 0 then + --return nil; + return "00:00:00"; + else + nHours = string.format("%02.f", math.floor(nSeconds/3600)); + nMins = string.format("%02.f", math.floor(nSeconds/60 - (nHours*60))); + nSecs = string.format("%02.f", math.floor(nSeconds - nHours*3600 - nMins *60)); + return nHours..":"..nMins..":"..nSecs + end +end + +--- Opens a score CSV file to log the scores. +-- @param #SCORING self +-- @param #string ScoringCSV +-- @return #SCORING self +-- @usage +-- -- Open a new CSV file to log the scores of the game Gori Valley. Let the name of the CSV file begin with "Player Scores". +-- ScoringObject = SCORING:New( "Gori Valley" ) +-- ScoringObject:OpenCSV( "Player Scores" ) +function SCORING:OpenCSV( ScoringCSV ) + self:F( ScoringCSV ) + + if lfs and io and os then + if ScoringCSV then + self.ScoringCSV = ScoringCSV + local fdir = lfs.writedir() .. [[Logs\]] .. self.ScoringCSV .. " " .. os.date( "%Y-%m-%d %H-%M-%S" ) .. ".csv" + + self.CSVFile, self.err = io.open( fdir, "w+" ) + if not self.CSVFile then + error( "Error: Cannot open CSV file in " .. lfs.writedir() ) + end + + self.CSVFile:write( '"GameName","RunTime","Time","PlayerName","ScoreType","PlayerUnitCoaltion","PlayerUnitCategory","PlayerUnitType","PlayerUnitName","TargetUnitCoalition","TargetUnitCategory","TargetUnitType","TargetUnitName","Times","Score"\n' ) + + self.RunTime = os.date("%y-%m-%d_%H-%M-%S") + else + error( "A string containing the CSV file name must be given." ) + end + else + self:E( "The MissionScripting.lua file has not been changed to allow lfs, io and os modules to be used..." ) + end + return self +end + + +--- Registers a score for a player. +-- @param #SCORING self +-- @param #string PlayerName The name of the player. +-- @param #string ScoreType The type of the score. +-- @param #string ScoreTimes The amount of scores achieved. +-- @param #string ScoreAmount The score given. +-- @param #string PlayerUnitName The unit name of the player. +-- @param #string PlayerUnitCoalition The coalition of the player unit. +-- @param #string PlayerUnitCategory The category of the player unit. +-- @param #string PlayerUnitType The type of the player unit. +-- @param #string TargetUnitName The name of the target unit. +-- @param #string TargetUnitCoalition The coalition of the target unit. +-- @param #string TargetUnitCategory The category of the target unit. +-- @param #string TargetUnitType The type of the target unit. +-- @return #SCORING self +function SCORING:ScoreCSV( PlayerName, ScoreType, ScoreTimes, ScoreAmount, PlayerUnitName, PlayerUnitCoalition, PlayerUnitCategory, PlayerUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + --write statistic information to file + local ScoreTime = self:SecondsToClock( timer.getTime() ) + PlayerName = PlayerName:gsub( '"', '_' ) + + if PlayerUnitName and PlayerUnitName ~= '' then + local PlayerUnit = Unit.getByName( PlayerUnitName ) + + if PlayerUnit then + if not PlayerUnitCategory then + --PlayerUnitCategory = SCORINGCategory[PlayerUnit:getCategory()] + PlayerUnitCategory = _SCORINGCategory[PlayerUnit:getDesc().category] + end + + if not PlayerUnitCoalition then + PlayerUnitCoalition = _SCORINGCoalition[PlayerUnit:getCoalition()] + end + + if not PlayerUnitType then + PlayerUnitType = PlayerUnit:getTypeName() + end + else + PlayerUnitName = '' + PlayerUnitCategory = '' + PlayerUnitCoalition = '' + PlayerUnitType = '' + end + else + PlayerUnitName = '' + PlayerUnitCategory = '' + PlayerUnitCoalition = '' + PlayerUnitType = '' + end + + if not TargetUnitCoalition then + TargetUnitCoalition = '' + end + + if not TargetUnitCategory then + TargetUnitCategory = '' + end + + if not TargetUnitType then + TargetUnitType = '' + end + + if not TargetUnitName then + TargetUnitName = '' + end + + if lfs and io and os then + self.CSVFile:write( + '"' .. self.GameName .. '"' .. ',' .. + '"' .. self.RunTime .. '"' .. ',' .. + '' .. ScoreTime .. '' .. ',' .. + '"' .. PlayerName .. '"' .. ',' .. + '"' .. ScoreType .. '"' .. ',' .. + '"' .. PlayerUnitCoalition .. '"' .. ',' .. + '"' .. PlayerUnitCategory .. '"' .. ',' .. + '"' .. PlayerUnitType .. '"' .. ',' .. + '"' .. PlayerUnitName .. '"' .. ',' .. + '"' .. TargetUnitCoalition .. '"' .. ',' .. + '"' .. TargetUnitCategory .. '"' .. ',' .. + '"' .. TargetUnitType .. '"' .. ',' .. + '"' .. TargetUnitName .. '"' .. ',' .. + '' .. ScoreTimes .. '' .. ',' .. + '' .. ScoreAmount + ) + + self.CSVFile:write( "\n" ) + end +end + + +function SCORING:CloseCSV() + if lfs and io and os then + self.CSVFile:close() + end +end + +--- 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** / Mulit-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.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.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 + SpawnTemplate.uncontrolled = self.SpawnUnControlled + 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 + 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].y = self.SpawnTemplate.units[1].y + 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:TaskRouteToVec3( GDV, Speed / 3.6 ) -- DCS models speed in Mps (Miles per second) + end + + return true + end + + return false +end + + +--- Report Targets Scheduler. +-- @param #ESCORT self +function ESCORT:_ReportTargetsScheduler() + self:F( self.EscortGroup:GetName() ) + + if self.EscortGroup:IsAlive() and self.EscortClient:IsAlive() then + local EscortGroupName = self.EscortGroup:GetName() + local EscortTargets = self.EscortGroup:GetDetectedTargets() + + local ClientEscortTargets = self.EscortClient._EscortGroups[EscortGroupName].Targets + + local EscortTargetMessages = "" + for EscortTargetID, EscortTarget in pairs( EscortTargets ) do + local EscortObject = EscortTarget.object + self:T( EscortObject ) + if EscortObject and EscortObject:isExist() and EscortObject.id_ < 50000000 then + + local EscortTargetUnit = UNIT:Find( EscortObject ) + local EscortTargetUnitName = EscortTargetUnit:GetName() + + + + -- local EscortTargetIsDetected, + -- EscortTargetIsVisible, + -- EscortTargetLastTime, + -- EscortTargetKnowType, + -- EscortTargetKnowDistance, + -- EscortTargetLastPos, + -- EscortTargetLastVelocity + -- = self.EscortGroup:IsTargetDetected( EscortObject ) + -- + -- self:T( { EscortTargetIsDetected, + -- EscortTargetIsVisible, + -- EscortTargetLastTime, + -- EscortTargetKnowType, + -- EscortTargetKnowDistance, + -- EscortTargetLastPos, + -- EscortTargetLastVelocity } ) + + + local 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. + SCHEDULER:New( TrainerWeapon, TrainerWeapon.destroy, {}, 2 ) + if ( TrainerWeapon:getTypeName() == "9M311" ) then + SCHEDULER:New( TrainerWeapon, TrainerWeapon.destroy, {}, 2 ) + 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() + 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** / Mulit-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 + + 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 + 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 + +--- @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** / Mulit-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:SetDetectionOn() + + self.CheckStatus = true + + self:ManageFuel( .2, 60 ) + self:ManageDamage( 1 ) + + self:SetDetectionInterval( 30 ) + + 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", "Ejected" ) + self:AddTransition( "*", "Crash", "Crashed" ) + self:AddTransition( "*", "PilotDead", "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.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.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 + + +--- 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:EventOnPilotDead( self.OnPilotDead ) + self:EventOnCrash( self.OnCrash ) + self:EventOnEjection( self.OnEjection ) + + + Controllable:OptionROEHoldFire() + Controllable:OptionROTVertical() + + self.Controllable:OnReSpawn( + function( PatrolGroup ) + self:E( "ReSpawn" ) + self:__Reset() + self:__Route( 5 ) + end + ) + +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 + self:T( TargetObject ) + 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 } ) + self.DetectedUnits[TargetUnit] = TargetUnit + Detected = true + end + else + self.DetectedUnits[TargetUnit] = TargetUnit + 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:__Crash( 1, EventData ) + 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** / Mulit-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 Dcs.DCSTypes#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO +-- @param Core.Zone#ZONE EngageZone +-- @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. + -- @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. + + --- 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", "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:EventOnDead( self.OnDead ) + +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: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 + + + if self.Controllable:IsNotInZone( self.EngageZone ) then + + -- Find a random 2D point in EngageZone. + local ToEngageZoneVec2 = self.EngageZone:GetRandomVec2() + self:T2( ToEngageZoneVec2 ) + + -- Define Speed and Altitude. + local ToEngageZoneAltitude = math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) + local ToEngageZoneSpeed = self.PatrolMaxSpeed + self:T2( ToEngageZoneSpeed ) + + -- Obtain a 3D @{Point} from the 2D point + altitude. + local ToEngageZonePointVec3 = POINT_VEC3:New( ToEngageZoneVec2.x, ToEngageZoneAltitude, ToEngageZoneVec2.y ) + + -- Create a route point of type air. + local ToEngageZoneRoutePoint = ToEngageZonePointVec3:RoutePointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToEngageZoneSpeed, + 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 ) + + --- 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 ToTargetRoutePoint = ToTargetPointVec3:RoutePointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true + ) + + ToTargetPointVec3:SmokeBlue() + + EngageRoute[#EngageRoute+1] = ToTargetRoutePoint + + + Controllable:OptionROEOpenFire() + Controllable:OptionROTPassiveDefense() + + 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, 2 ) + 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:SetDetectionOff() +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** / Mulit-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 DetectedUnitID, DetectedUnit 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 DetectedUnitID, DetectedUnit 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:__Accomplish( 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** / Mulit-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:EventOnDead( 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:EventOnBirth( + --- @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:EventOnPlayerEnterUnit( + --- @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:EventOnPlayerLeaveUnit( + --- @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:EventOnCrash( + --- @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. +-- +-- 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, +} + +--- 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: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() + + -- Handle the birth of new planes within the assigned set. + + + -- Handle when a player crashes ... + -- The Task is UnAssigned from the Unit. + -- When there is no Unit left running the Task, and all of the Players crashed, the Task goes into Failed ... +-- self:EventOnCrash( +-- --- @param #TASK self +-- -- @param Core.Event#EVENTDATA EventData +-- function( self, EventData ) +-- self:E( "In LeaveUnit" ) +-- self:E( { "State", self:GetState() } ) +-- if self:IsStateAssigned() then +-- local TaskUnit = EventData.IniUnit +-- local TaskGroup = EventData.IniUnit:GetGroup() +-- self:E( self.SetGroup:IsIncludeObject( TaskGroup ) ) +-- if self.SetGroup:IsIncludeObject( TaskGroup ) then +-- self:UnAssignFromUnit( TaskUnit ) +-- end +-- self:MessageToGroups( TaskUnit:GetPlayerName() .. " crashed!, and has aborted Task " .. self:GetName() ) +-- end +-- end +-- ) +-- + + 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 + +--- 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() +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() + +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 + +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" ) + +--- 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 da1821733..c89d1ec84 100644 --- a/Moose Mission Setup/Moose.lua +++ b/Moose Mission Setup/Moose.lua @@ -1,31 +1,31767 @@ -env.info( '*** MOOSE DYNAMIC INCLUDE START *** ' ) -env.info( 'Moose Generation Timestamp: 20170203_2219' ) - +env.info( '*** MOOSE STATIC INCLUDE START *** ' ) +env.info( 'Moose Generation Timestamp: 20170204_1514' ) 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. +-- Therefore, the BASE class exposes the following event handling functions: +-- +-- * @{#BASE.EventOnBirth}(): Handle the birth of a new unit. +-- * @{#BASE.EventOnBaseCaptured}(): Handle the capturing of an airbase or a helipad. +-- * @{#BASE.EventOnCrash}(): Handle the crash of a unit. +-- * @{#BASE.EventOnDead}(): Handle the death of a unit. +-- * @{#BASE.EventOnEjection}(): Handle the ejection of a player out of an airplane. +-- * @{#BASE.EventOnEngineShutdown}(): Handle the shutdown of an engine. +-- * @{#BASE.EventOnEngineStartup}(): Handle the startup of an engine. +-- * @{#BASE.EventOnHit}(): Handle the hit of a shell to a unit. +-- * @{#BASE.EventOnHumanFailure}(): No a clue ... +-- * @{#BASE.EventOnLand}(): Handle the event when a unit lands. +-- * @{#BASE.EventOnMissionStart}(): Handle the start of the mission. +-- * @{#BASE.EventOnPilotDead}(): Handle the event when a pilot is dead. +-- * @{#BASE.EventOnPlayerComment}(): Handle the event when a player posts a comment. +-- * @{#BASE.EventOnPlayerEnterUnit}(): Handle the event when a player enters a unit. +-- * @{#BASE.EventOnPlayerLeaveUnit}(): Handle the event when a player leaves a unit. +-- * @{#BASE.EventOnBirthPlayerMissionEnd}(): Handle the event when a player ends the mission. (Not a clue what that does). +-- * @{#BASE.EventOnRefueling}(): Handle the event when a unit is refueling. +-- * @{#BASE.EventOnShootingEnd}(): Handle the event when a unit starts shooting (guns). +-- * @{#BASE.EventOnShootingStart}(): Handle the event when a unit ends shooting (guns). +-- * @{#BASE.EventOnShot}(): Handle the event when a unit shot a missile. +-- * @{#BASE.EventOnTakeOff}(): Handle the event when a unit takes off from a runway. +-- * @{#BASE.EventOnTookControl}(): Handle the event when a player takes control of a unit. +-- +-- The EventOn() methods provide the @{Event#EVENTDATA} structure to the event handling function. +-- The @{Event#EVENTDATA} structure contains an enriched data set of information about the event being handled. +-- +-- Find below an example of the prototype how to write an event handling function: +-- +-- CommandCenter:EventOnPlayerEnterUnit( +-- --- @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 +-- Mission:JoinUnit( PlayerUnit ) +-- Mission:ReportDetails() +-- end +-- end +-- ) +-- +-- Note the function( self, EventData ). It takes two parameters: +-- +-- * self = the object that is handling the EventOnPlayerEnterUnit. +-- * EventData = the @{Event#EVENTDATA} structure, containing more information of the Event. +-- +-- ## 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 + +--- 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 + + +--- Set a new listener for the class. +-- @param self +-- @param Dcs.DCSTypes#Event Event +-- @param #function EventFunction +-- @return #BASE +function BASE:AddEvent( Event, EventFunction ) + self:F( Event ) + + self.Events[#self.Events+1] = {} + self.Events[#self.Events].Event = Event + self.Events[#self.Events].EventFunction = EventFunction + self.Events[#self.Events].EventEnabled = false + + return self +end + +--- Returns the event dispatcher +-- @param #BASE self +-- @return Core.Event#EVENT +function BASE:Event() + + return _EVENTDISPATCHER +end + +--- Remove all subscribed events +-- @param #BASE self +-- @return #BASE +function BASE:EventRemoveAll() + + _EVENTDISPATCHER:RemoveAll( self ) + + return self +end + +--- Subscribe to a S_EVENT\_SHOT event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnShot( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_SHOT ) + + return self +end + +--- Subscribe to a S_EVENT\_HIT event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnHit( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_HIT ) + + return self +end + +--- Subscribe to a S_EVENT\_TAKEOFF event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnTakeOff( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_TAKEOFF ) + + return self +end + +--- Subscribe to a S_EVENT\_LAND event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnLand( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_LAND ) + + return self +end + +--- Subscribe to a S_EVENT\_CRASH event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnCrash( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_CRASH ) + + return self +end + +--- Subscribe to a S_EVENT\_EJECTION event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnEjection( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_EJECTION ) + + return self +end + + +--- Subscribe to a S_EVENT\_REFUELING event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnRefueling( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_REFUELING ) + + return self +end + +--- Subscribe to a S_EVENT\_DEAD event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnDead( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_DEAD ) + + return self +end + +--- Subscribe to a S_EVENT_PILOT\_DEAD event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnPilotDead( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_PILOT_DEAD ) + + return self +end + +--- Subscribe to a S_EVENT_BASE\_CAPTURED event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnBaseCaptured( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_BASE_CAPTURED ) + + return self +end + +--- Subscribe to a S_EVENT_MISSION\_START event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnMissionStart( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_MISSION_START ) + + return self +end + +--- Subscribe to a S_EVENT_MISSION\_END event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnPlayerMissionEnd( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_MISSION_END ) + + return self +end + +--- Subscribe to a S_EVENT_TOOK\_CONTROL event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnTookControl( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_TOOK_CONTROL ) + + return self +end + +--- Subscribe to a S_EVENT_REFUELING\_STOP event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnRefuelingStop( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_REFUELING_STOP ) + + return self +end + +--- Subscribe to a S_EVENT\_BIRTH event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnBirth( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_BIRTH ) + + return self +end + +--- Subscribe to a S_EVENT_HUMAN\_FAILURE event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnHumanFailure( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_HUMAN_FAILURE ) + + return self +end + +--- Subscribe to a S_EVENT_ENGINE\_STARTUP event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnEngineStartup( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_ENGINE_STARTUP ) + + return self +end + +--- Subscribe to a S_EVENT_ENGINE\_SHUTDOWN event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnEngineShutdown( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_ENGINE_SHUTDOWN ) + + return self +end + +--- Subscribe to a S_EVENT_PLAYER_ENTER\_UNIT event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnPlayerEnterUnit( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_PLAYER_ENTER_UNIT ) + + return self +end + +--- Subscribe to a S_EVENT_PLAYER_LEAVE\_UNIT event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnPlayerLeaveUnit( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_PLAYER_LEAVE_UNIT ) + + return self +end + +--- Subscribe to a S_EVENT_PLAYER\_COMMENT event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnPlayerComment( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_PLAYER_COMMENT ) + + return self +end + +--- Subscribe to a S_EVENT_SHOOTING\_START event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnShootingStart( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_SHOOTING_START ) + + return self +end + +--- Subscribe to a S_EVENT_SHOOTING\_END event. +-- @param #BASE self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @return #BASE +function BASE:EventOnShootingEnd( EventFunction ) + + _EVENTDISPATCHER:OnEventGeneric( EventFunction, self, world.event.S_EVENT_SHOOTING_END ) + + return self +end + + + + + + + +--- Enable the event listeners for the class. +-- @param #BASE self +-- @return #BASE +function BASE:EnableEvents() + self:F( #self.Events ) + + for EventID, Event in pairs( self.Events ) do + Event.Self = self + Event.EventEnabled = true + end + self.Events.Handler = world.addEventHandler( self ) + + return self +end + + +--- Disable the event listeners for the class. +-- @param #BASE self +-- @return #BASE +function BASE:DisableEvents() + self:F() + + world.removeEventHandler( self ) + for EventID, Event in pairs( self.Events ) do + Event.Self = nil + Event.EventEnabled = false + end + + return self +end + + +local BaseEventCodes = { + "S_EVENT_SHOT", + "S_EVENT_HIT", + "S_EVENT_TAKEOFF", + "S_EVENT_LAND", + "S_EVENT_CRASH", + "S_EVENT_EJECTION", + "S_EVENT_REFUELING", + "S_EVENT_DEAD", + "S_EVENT_PILOT_DEAD", + "S_EVENT_BASE_CAPTURED", + "S_EVENT_MISSION_START", + "S_EVENT_MISSION_END", + "S_EVENT_TOOK_CONTROL", + "S_EVENT_REFUELING_STOP", + "S_EVENT_BIRTH", + "S_EVENT_HUMAN_FAILURE", + "S_EVENT_ENGINE_STARTUP", + "S_EVENT_ENGINE_SHUTDOWN", + "S_EVENT_PLAYER_ENTER_UNIT", + "S_EVENT_PLAYER_LEAVE_UNIT", + "S_EVENT_PLAYER_COMMENT", + "S_EVENT_SHOOTING_START", + "S_EVENT_SHOOTING_END", + "S_EVENT_MAX", +} + +--onEvent( {[1]="S_EVENT_BIRTH",[2]={["subPlace"]=5,["time"]=0,["initiator"]={["id_"]=16884480,},["place"]={["id_"]=5000040,},["id"]=15,["IniUnitName"]="US F-15C@RAMP-Air Support Mountains#001-01",},} +-- Event = { +-- id = enum world.event, +-- time = Time, +-- initiator = Unit, +-- target = Unit, +-- place = Unit, +-- subPlace = enum world.BirthPlace, +-- weapon = Weapon +-- } + +--- Creation of a Birth Event. +-- @param #BASE self +-- @param 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 module contains the EVENT class. +-- +-- === +-- +-- Takes care of EVENT dispatching between DCS events and event handling functions defined in MOOSE classes. +-- +-- === +-- +-- The above menus classes **are derived** from 2 main **abstract** classes defined within the MOOSE framework (so don't use these): +-- +-- === +-- +-- ### Contributions: - +-- ### Authors: FlightControl : Design & Programming +-- +-- @module Event + +--- The EVENT structure +-- @type EVENT +-- @field #EVENT.Events Events +EVENT = { + ClassName = "EVENT", + ClassID = 0, +} + +local _EVENTCODES = { + "S_EVENT_SHOT", + "S_EVENT_HIT", + "S_EVENT_TAKEOFF", + "S_EVENT_LAND", + "S_EVENT_CRASH", + "S_EVENT_EJECTION", + "S_EVENT_REFUELING", + "S_EVENT_DEAD", + "S_EVENT_PILOT_DEAD", + "S_EVENT_BASE_CAPTURED", + "S_EVENT_MISSION_START", + "S_EVENT_MISSION_END", + "S_EVENT_TOOK_CONTROL", + "S_EVENT_REFUELING_STOP", + "S_EVENT_BIRTH", + "S_EVENT_HUMAN_FAILURE", + "S_EVENT_ENGINE_STARTUP", + "S_EVENT_ENGINE_SHUTDOWN", + "S_EVENT_PLAYER_ENTER_UNIT", + "S_EVENT_PLAYER_LEAVE_UNIT", + "S_EVENT_PLAYER_COMMENT", + "S_EVENT_SHOOTING_START", + "S_EVENT_SHOOTING_END", + "S_EVENT_MAX", +} + +local _EVENTORDER = { + [world.event.S_EVENT_SHOT] = 1, + [world.event.S_EVENT_HIT] = 1, + [world.event.S_EVENT_TAKEOFF] = 1, + [world.event.S_EVENT_LAND] = 1, + [world.event.S_EVENT_CRASH] = -1, + [world.event.S_EVENT_EJECTION] = -1, + [world.event.S_EVENT_REFUELING] = 1, + [world.event.S_EVENT_DEAD] = -1, + [world.event.S_EVENT_PILOT_DEAD] = -1, + [world.event.S_EVENT_BASE_CAPTURED] = 1, + [world.event.S_EVENT_MISSION_START] = 1, + [world.event.S_EVENT_MISSION_END] = -1, + [world.event.S_EVENT_TOOK_CONTROL] = 1, + [world.event.S_EVENT_REFUELING_STOP] = 1, + [world.event.S_EVENT_BIRTH] = 1, + [world.event.S_EVENT_HUMAN_FAILURE] = 1, + [world.event.S_EVENT_ENGINE_STARTUP] = 1, + [world.event.S_EVENT_ENGINE_SHUTDOWN] = 1, + [world.event.S_EVENT_PLAYER_ENTER_UNIT] = 1, + [world.event.S_EVENT_PLAYER_LEAVE_UNIT] = -1, + [world.event.S_EVENT_PLAYER_COMMENT] = 1, + [world.event.S_EVENT_SHOOTING_START] = 1, + [world.event.S_EVENT_SHOOTING_END] = 1, + [world.event.S_EVENT_MAX] = 1, +} + +--- The Event structure +-- @type EVENTDATA +-- @field id +-- @field initiator +-- @field target +-- @field weapon +-- @field IniDCSUnit +-- @field IniDCSUnitName +-- @field Wrapper.Unit#UNIT IniUnit +-- @field #string IniUnitName +-- @field IniDCSGroup +-- @field IniDCSGroupName +-- @field TgtDCSUnit +-- @field TgtDCSUnitName +-- @field Wrapper.Unit#UNIT TgtUnit +-- @field #string TgtUnitName +-- @field TgtDCSGroup +-- @field TgtDCSGroupName +-- @field Weapon +-- @field WeaponName +-- @field WeaponTgtDCSUnit + +--- The Events structure +-- @type EVENT.Events +-- @field #number IniUnit + +function EVENT:New() + local self = BASE:Inherit( self, BASE:New() ) + self:F2() + self.EventHandler = world.addEventHandler( self ) + return self +end + +function EVENT:EventText( EventID ) + + local EventText = _EVENTCODES[EventID] + + return EventText +end + + +--- Initializes the Events structure for the event +-- @param #EVENT self +-- @param Dcs.DCSWorld#world.event EventID +-- @param #number EventPriority The priority of the EventClass. +-- @param Core.Base#BASE EventClass +-- @return #EVENT.Events +function EVENT:Init( EventID, EventClass ) + self:F3( { _EVENTCODES[EventID], 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][EventClass] then + self.Events[EventID][EventClass] = setmetatable( {}, { __mode = "k" } ) + end + return self.Events[EventID][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, _EVENTCODES[EventID] } ) + + local EventClass = EventClass + local EventPriority = EventClass:GetEventPriority() + self.Events[EventID][EventPriority][EventClass] = 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 and Event.initiator:getCategory() == Object.Category.UNIT then + Event.IniDCSUnit = Event.initiator + Event.IniDCSGroup = Event.IniDCSUnit:getGroup() + Event.IniDCSUnitName = Event.IniDCSUnit:getName() + Event.IniUnitName = Event.IniDCSUnitName + Event.IniUnit = UNIT:FindByName( Event.IniDCSUnitName ) + 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 + end + if Event.target then + if Event.target and Event.target:getCategory() == Object.Category.UNIT then + Event.TgtDCSUnit = Event.target + Event.TgtDCSGroup = Event.TgtDCSUnit:getGroup() + Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() + Event.TgtUnitName = Event.TgtDCSUnitName + Event.TgtUnit = UNIT:FindByName( Event.TgtDCSUnitName ) + Event.TgtDCSGroupName = "" + if Event.TgtDCSGroup and Event.TgtDCSGroup:isExist() then + Event.TgtDCSGroupName = Event.TgtDCSGroup:getName() + end + end + end + if Event.weapon then + Event.Weapon = Event.weapon + Event.WeaponName = Event.Weapon:getTypeName() + --Event.WeaponTgtDCSUnit = Event.Weapon:getTarget() + end + self:E( { _EVENTCODES[Event.id], Event, Event.IniDCSUnitName, Event.TgtDCSUnitName } ) + + local Order = _EVENTORDER[Event.id] + self:E( { Order = Order } ) + + for EventPriority = Order == -1 and 5 or 1, Order == -1 and 1 or 5, Order 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 + self:E( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName } ) + Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) + local Result, Value = xpcall( function() return EventData.IniUnit[Event.IniDCSUnitName].EventFunction( EventData.IniUnit[Event.IniDCSUnitName].EventClass, Event ) end, ErrorHandler ) + --EventData.IniUnit[Event.IniDCSUnitName].EventFunction( EventData.IniUnit[Event.IniDCSUnitName].EventClass, Event ) + 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 + self:E( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID() } ) + Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) + local Result, Value = xpcall( function() return EventData.EventFunction( EventData.EventClass, Event ) end, ErrorHandler ) + --EventData.EventFunction( EventData.EventClass, Event ) + end + end + end + end + end + end + else + self:E( { _EVENTCODES[Event.id], Event } ) + end +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 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.IsPointVec2InZone}(): Returns if a @{Point#POINT_VEC2} is within the zone. +-- * @{#ZONE_BASE.IsPointVec3InZone}(): Returns if a @{Point#POINT_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_BASE.New}(): Constructor. +-- +-- ### 2.2) Manage the radius of the zone: +-- +-- * @{#ZONE_BASE.SetRadius}(): Sets the radius of the zone. +-- * @{#ZONE_BASE.GetRadius}(): Returns the radius of the zone. +-- +-- ### 2.3) Manage the location of the zone: +-- +-- * @{#ZONE_BASE.SetVec2}(): Sets the @{DCSTypes#Vec2} of the zone. +-- * @{#ZONE_BASE.GetVec2}(): Returns the @{DCSTypes#Vec2} of the zone. +-- * @{#ZONE_BASE.GetVec3}(): Returns the @{DCSTypes#Vec3} of the zone, taking an additional height parameter. +-- +-- === +-- +-- 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. +-- +-- === +-- +-- 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: +-- +-- 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 +-- @author FlightControl + + +--- 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:IsPointVec2InZone( 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:IsPointVec3InZone( Vec3 ) + self:F2( Vec3 ) + + local InZone = self:IsPointVec2InZone( { 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 + +--- 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 + + +--- 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 ) ) + self:F( { ZoneName, Vec2, Radius } ) + + self.Radius = Radius + self.Vec2 = Vec2 + + 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:IsPointVec2InZone( 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:IsPointVec3InZone( Vec3 ) + self:F2( Vec3 ) + + local InZone = self:IsPointVec2InZone( { x = Vec3.x, y = Vec3.z } ) + + return InZone +end + +--- Returns a random location within the zone. +-- @param #ZONE_RADIUS self +-- @param #number inner minimal distance from the center of the zone +-- @param #number outer minimal distance from the outer edge 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 + + + +--- 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 +-- @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:IsPointVec2InZone( 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:IsPointVec2InZone( Vec2 ) then + Vec2Found = true + end + end + + self:T2( Vec2 ) + + return Vec2 +end + +--- Get the bounding square the zone. +-- @param #ZONE_POLYGON_BASE self +-- @return #ZONE_POLYGON_BASE.BoundingSquare The bounding square. +function ZONE_POLYGON_BASE:GetBoundingSquare() + + local x1 = self.Polygon[1].x + local y1 = self.Polygon[1].y + local x2 = self.Polygon[1].x + local y2 = self.Polygon[1].y + + for i = 2, #self.Polygon do + self:T2( { self.Polygon[i], x1, y1, x2, y2 } ) + x1 = ( x1 > self.Polygon[i].x ) and self.Polygon[i].x or x1 + x2 = ( x2 < self.Polygon[i].x ) and self.Polygon[i].x or x2 + y1 = ( y1 > self.Polygon[i].y ) and self.Polygon[i].y or y1 + y2 = ( y2 < self.Polygon[i].y ) and self.Polygon[i].y or y2 + + end + + return { x1 = x1, y1 = y1, x2 = x2, y2 = y2 } +end + + + + + +--- The ZONE_POLYGON class defined by a sequence of @{Group#GROUP} waypoints within the Mission Editor, forming a polygon. +-- @type ZONE_POLYGON +-- @extends 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() ) + + _EVENTDISPATCHER:OnBirth( self._EventOnBirth, self ) + _EVENTDISPATCHER:OnDead( self._EventOnDeadOrCrash, self ) + _EVENTDISPATCHER:OnCrash( self._EventOnDeadOrCrash, self ) + + + -- Follow alive players and clients + _EVENTDISPATCHER:OnPlayerEnterUnit( self._EventOnPlayerEnterUnit, self ) + _EVENTDISPATCHER:OnPlayerLeaveUnit( self._EventOnPlayerLeaveUnit, self ) + + self:_RegisterTemplates() + self:_RegisterGroupsAndUnits() + self:_RegisterClients() + self:_RegisterStatics() + self:_RegisterPlayers() + self:_RegisterAirbases() + + self:SetEventPriority( 1 ) + + 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 + self:AddUnit( Event.IniDCSUnitName ) + self:AddGroup( Event.IniDCSGroupName ) + self:_EventOnPlayerEnterUnit( Event ) + end +end + + +--- Handles the OnDead or OnCrash event for alive units set. +-- @param #DATABASE self +-- @param Core.Event#EVENTDATA Event +function DATABASE:_EventOnDeadOrCrash( Event ) + self:F2( { Event } ) + + if Event.IniDCSUnit then + if self.UNITS[Event.IniDCSUnitName] then + self:DeleteUnit( Event.IniDCSUnitName ) + -- add logic to correctly remove a group once all units are destroyed... + end + end +end + + +--- Handles the OnPlayerEnterUnit event to fill the active players table (with the unit filter applied). +-- @param #DATABASE self +-- @param Core.Event#EVENTDATA Event +function DATABASE:_EventOnPlayerEnterUnit( Event ) + self:F2( { Event } ) + + if Event.IniUnit 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 + + +--- 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 + local PlayerName = Event.IniUnit:GetPlayerName() + if self.PLAYERS[PlayerName] then + self:DeletePlayer( PlayerName ) + end + end +end + +--- Iterators + +--- Iterate the DATABASE and call an iterator function for the given set, providing the Object for each element within the set and optional parameters. +-- @param #DATABASE self +-- @param #function IteratorFunction The function that will be called when there is an alive player in the database. +-- @return #DATABASE self +function DATABASE:ForEach( IteratorFunction, FinalizeFunction, arg, Set ) + self:F2( arg ) + + local function CoRoutine() + local Count = 0 + for ObjectID, Object in pairs( Set ) do + self:T2( Object ) + IteratorFunction( Object, unpack( arg ) ) + Count = Count + 1 +-- if Count % 100 == 0 then +-- coroutine.yield( false ) +-- end + end + return true + end + +-- local co = coroutine.create( CoRoutine ) + local co = CoRoutine + + local function Schedule() + +-- local status, res = coroutine.resume( co ) + local status, res = co() + self:T3( { status, res } ) + + if status == false then + error( res ) + end + if res == false then + return true -- resume next time the loop + end + if FinalizeFunction then + FinalizeFunction( unpack( arg ) ) + end + return false + end + + local Scheduler = SCHEDULER:New( self, Schedule, {}, 0.001, 0.001, 0 ) + + return self +end + + +--- Iterate the DATABASE and call an iterator function for each **alive** UNIT, providing the UNIT and optional parameters. +-- @param #DATABASE self +-- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the database. The function needs to accept a UNIT parameter. +-- @return #DATABASE self +function DATABASE:ForEachUnit( IteratorFunction, FinalizeFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, FinalizeFunction, arg, self.UNITS ) + + return self +end + +--- Iterate the DATABASE and call an iterator function for each **alive** GROUP, providing the GROUP and optional parameters. +-- @param #DATABASE self +-- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the database. The function needs to accept a GROUP parameter. +-- @return #DATABASE self +function DATABASE:ForEachGroup( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self.GROUPS ) + + return self +end + + +--- Iterate the DATABASE and call an iterator function for each **ALIVE** player, providing the player name and optional parameters. +-- @param #DATABASE self +-- @param #function IteratorFunction The function that will be called when there is an player in the database. The function needs to accept the player name. +-- @return #DATABASE self +function DATABASE:ForEachPlayer( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self.PLAYERS ) + + return self +end + + +--- Iterate the DATABASE and call an iterator function for each player who has joined the mission, providing the Unit of the player and optional parameters. +-- @param #DATABASE self +-- @param #function IteratorFunction The function that will be called when there is was a player in the database. The function needs to accept a UNIT parameter. +-- @return #DATABASE self +function DATABASE:ForEachPlayerJoined( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self.PLAYERSJOINED ) + + return self +end + +--- Iterate the DATABASE and call an iterator function for each CLIENT, providing the CLIENT to the function and optional parameters. +-- @param #DATABASE self +-- @param #function IteratorFunction The function that will be called when there is an alive player in the database. The function needs to accept a CLIENT parameter. +-- @return #DATABASE self +function DATABASE:ForEachClient( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self.CLIENTS ) + + return self +end + + +function DATABASE:_RegisterTemplates() + self:F2() + + self.Navpoints = {} + self.UNITS = {} + --Build routines.db.units and self.Navpoints + for CoalitionName, coa_data in pairs(env.mission.coalition) do + + if (CoalitionName == 'red' or CoalitionName == 'blue') and type(coa_data) == 'table' then + --self.Units[coa_name] = {} + + ---------------------------------------------- + -- build nav points DB + self.Navpoints[CoalitionName] = {} + if coa_data.nav_points then --navpoints + for nav_ind, nav_data in pairs(coa_data.nav_points) do + + if type(nav_data) == 'table' then + self.Navpoints[CoalitionName][nav_ind] = routines.utils.deepCopy(nav_data) + + self.Navpoints[CoalitionName][nav_ind]['name'] = nav_data.callsignStr -- name is a little bit more self-explanatory. + self.Navpoints[CoalitionName][nav_ind]['point'] = {} -- point is used by SSE, support it. + self.Navpoints[CoalitionName][nav_ind]['point']['x'] = nav_data.x + self.Navpoints[CoalitionName][nav_ind]['point']['y'] = 0 + self.Navpoints[CoalitionName][nav_ind]['point']['z'] = nav_data.y + end + end + end + ------------------------------------------------- + if coa_data.country then --there is a country table + for cntry_id, cntry_data in pairs(coa_data.country) do + + local CountryName = string.upper(cntry_data.name) + --self.Units[coa_name][countryName] = {} + --self.Units[coa_name][countryName]["countryId"] = cntry_data.id + + if type(cntry_data) == 'table' then --just making sure + + for obj_type_name, obj_type_data in pairs(cntry_data) do + + if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then --should be an unncessary check + + local CategoryName = obj_type_name + + if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! + + --self.Units[coa_name][countryName][category] = {} + + for group_num, GroupTemplate in pairs(obj_type_data.group) do + + if GroupTemplate and GroupTemplate.units and type(GroupTemplate.units) == 'table' then --making sure again- this is a valid group + self:_RegisterTemplate( + GroupTemplate, + coalition.side[string.upper(CoalitionName)], + _DATABASECategory[string.lower(CategoryName)], + country.id[string.upper(CountryName)] + ) + end --if GroupTemplate and GroupTemplate.units then + end --for group_num, GroupTemplate in pairs(obj_type_data.group) do + end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then + end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then + end --for obj_type_name, obj_type_data in pairs(cntry_data) do + end --if type(cntry_data) == 'table' then + end --for cntry_id, cntry_data in pairs(coa_data.country) do + end --if coa_data.country then --there is a country table + end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then + end --for coa_name, coa_data in pairs(mission.coalition) do + + return self +end + + + + +--- This module contains the SET classes. +-- +-- === +-- +-- 1) @{Set#SET_BASE} class, extends @{Base#BASE} +-- ============================================== +-- The @{Set#SET_BASE} class defines the core functions that define a collection of objects. +-- A SET provides iterators to iterate the SET, but will **temporarily** yield the ForEach interator loop at defined **"intervals"** to the mail simulator loop. +-- In this way, large loops can be done while not blocking the simulator main processing loop. +-- The default **"yield interval"** is after 10 objects processed. +-- The default **"time interval"** is after 0.001 seconds. +-- +-- 1.1) Add or remove objects from the SET +-- --------------------------------------- +-- Some key core functions are @{Set#SET_BASE.Add} and @{Set#SET_BASE.Remove} to add or remove objects from the SET in your logic. +-- +-- 1.2) Define the SET iterator **"yield interval"** and the **"time interval"** +-- ----------------------------------------------------------------------------- +-- Modify the iterator intervals with the @{Set#SET_BASE.SetInteratorIntervals} method. +-- You can set the **"yield interval"**, and the **"time interval"**. (See above). +-- +-- === +-- +-- 2) @{Set#SET_GROUP} class, extends @{Set#SET_BASE} +-- ================================================== +-- Mission designers can use the @{Set#SET_GROUP} class to build sets of groups belonging to certain: +-- +-- * Coalitions +-- * Categories +-- * Countries +-- * Starting with certain prefix strings. +-- +-- 2.1) SET_GROUP construction method: +-- ----------------------------------- +-- Create a new SET_GROUP object with the @{#SET_GROUP.New} method: +-- +-- * @{#SET_GROUP.New}: Creates a new SET_GROUP object. +-- +-- 2.2) Add or Remove GROUP(s) from SET_GROUP: +-- ------------------------------------------- +-- GROUPS can be added and removed using the @{Set#SET_GROUP.AddGroupsByName} and @{Set#SET_GROUP.RemoveGroupsByName} respectively. +-- These methods take a single GROUP name or an array of GROUP names to be added or removed from SET_GROUP. +-- +-- 2.3) SET_GROUP filter criteria: +-- ------------------------------- +-- You can set filter criteria to define the set of groups within the SET_GROUP. +-- Filter criteria are defined by: +-- +-- * @{#SET_GROUP.FilterCoalitions}: Builds the SET_GROUP with the groups belonging to the coalition(s). +-- * @{#SET_GROUP.FilterCategories}: Builds the SET_GROUP with the groups belonging to the category(ies). +-- * @{#SET_GROUP.FilterCountries}: Builds the SET_GROUP with the gruops belonging to the country(ies). +-- * @{#SET_GROUP.FilterPrefixes}: Builds the SET_GROUP with the groups starting with the same prefix string(s). +-- +-- Once the filter criteria have been set for the SET_GROUP, you can start filtering using: +-- +-- * @{#SET_GROUP.FilterStart}: Starts the filtering of the groups within the SET_GROUP and add or remove GROUP objects **dynamically**. +-- +-- Planned filter criteria within development are (so these are not yet available): +-- +-- * @{#SET_GROUP.FilterZones}: Builds the SET_GROUP with the groups within a @{Zone#ZONE}. +-- +-- 2.4) SET_GROUP iterators: +-- ------------------------- +-- Once the filters have been defined and the SET_GROUP has been built, you can iterate the SET_GROUP with the available iterator methods. +-- The iterator methods will walk the SET_GROUP set, and call for each element within the set a function that you provide. +-- The following iterator methods are currently available within the SET_GROUP: +-- +-- * @{#SET_GROUP.ForEachGroup}: Calls a function for each alive group it finds within the SET_GROUP. +-- * @{#SET_GROUP.ForEachGroupCompletelyInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence completely in a @{Zone}, providing the GROUP and optional parameters to the called function. +-- * @{#SET_GROUP.ForEachGroupPartlyInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence partly in a @{Zone}, providing the GROUP and optional parameters to the called function. +-- * @{#SET_GROUP.ForEachGroupNotInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence not in a @{Zone}, providing the GROUP and optional parameters to the called function. +-- +-- ==== +-- +-- 3) @{Set#SET_UNIT} class, extends @{Set#SET_BASE} +-- =================================================== +-- Mission designers can use the @{Set#SET_UNIT} class to build sets of units belonging to certain: +-- +-- * Coalitions +-- * Categories +-- * Countries +-- * Unit types +-- * Starting with certain prefix strings. +-- +-- 3.1) SET_UNIT construction method: +-- ---------------------------------- +-- Create a new SET_UNIT object with the @{#SET_UNIT.New} method: +-- +-- * @{#SET_UNIT.New}: Creates a new SET_UNIT object. +-- +-- 3.2) Add or Remove UNIT(s) from SET_UNIT: +-- ----------------------------------------- +-- UNITs can be added and removed using the @{Set#SET_UNIT.AddUnitsByName} and @{Set#SET_UNIT.RemoveUnitsByName} respectively. +-- These methods take a single UNIT name or an array of UNIT names to be added or removed from SET_UNIT. +-- +-- 3.3) SET_UNIT filter criteria: +-- ------------------------------ +-- You can set filter criteria to define the set of units within the SET_UNIT. +-- Filter criteria are defined by: +-- +-- * @{#SET_UNIT.FilterCoalitions}: Builds the SET_UNIT with the units belonging to the coalition(s). +-- * @{#SET_UNIT.FilterCategories}: Builds the SET_UNIT with the units belonging to the category(ies). +-- * @{#SET_UNIT.FilterTypes}: Builds the SET_UNIT with the units belonging to the unit type(s). +-- * @{#SET_UNIT.FilterCountries}: Builds the SET_UNIT with the units belonging to the country(ies). +-- * @{#SET_UNIT.FilterPrefixes}: Builds the SET_UNIT with the units starting with the same prefix string(s). +-- +-- Once the filter criteria have been set for the SET_UNIT, you can start filtering using: +-- +-- * @{#SET_UNIT.FilterStart}: Starts the filtering of the units within the SET_UNIT. +-- +-- Planned filter criteria within development are (so these are not yet available): +-- +-- * @{#SET_UNIT.FilterZones}: Builds the SET_UNIT with the units within a @{Zone#ZONE}. +-- +-- 3.4) SET_UNIT iterators: +-- ------------------------ +-- Once the filters have been defined and the SET_UNIT has been built, you can iterate the SET_UNIT with the available iterator methods. +-- The iterator methods will walk the SET_UNIT set, and call for each element within the set a function that you provide. +-- The following iterator methods are currently available within the SET_UNIT: +-- +-- * @{#SET_UNIT.ForEachUnit}: Calls a function for each alive unit it finds within the SET_UNIT. +-- * @{#SET_GROUP.ForEachGroupCompletelyInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence completely in a @{Zone}, providing the GROUP and optional parameters to the called function. +-- * @{#SET_GROUP.ForEachGroupNotInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence not in a @{Zone}, providing the GROUP and optional parameters to the called function. +-- +-- Planned iterators methods in development are (so these are not yet available): +-- +-- * @{#SET_UNIT.ForEachUnitInUnit}: Calls a function for each unit contained within the SET_UNIT. +-- * @{#SET_UNIT.ForEachUnitCompletelyInZone}: Iterate and call an iterator function for each **alive** UNIT presence completely in a @{Zone}, providing the UNIT and optional parameters to the called function. +-- * @{#SET_UNIT.ForEachUnitNotInZone}: Iterate and call an iterator function for each **alive** UNIT presence not in a @{Zone}, providing the UNIT and optional parameters to the called function. +-- +-- === +-- +-- 4) @{Set#SET_CLIENT} class, extends @{Set#SET_BASE} +-- =================================================== +-- Mission designers can use the @{Set#SET_CLIENT} class to build sets of units belonging to certain: +-- +-- * Coalitions +-- * Categories +-- * Countries +-- * Client types +-- * Starting with certain prefix strings. +-- +-- 4.1) SET_CLIENT construction method: +-- ---------------------------------- +-- Create a new SET_CLIENT object with the @{#SET_CLIENT.New} method: +-- +-- * @{#SET_CLIENT.New}: Creates a new SET_CLIENT object. +-- +-- 4.2) Add or Remove CLIENT(s) from SET_CLIENT: +-- ----------------------------------------- +-- CLIENTs can be added and removed using the @{Set#SET_CLIENT.AddClientsByName} and @{Set#SET_CLIENT.RemoveClientsByName} respectively. +-- These methods take a single CLIENT name or an array of CLIENT names to be added or removed from SET_CLIENT. +-- +-- 4.3) SET_CLIENT filter criteria: +-- ------------------------------ +-- You can set filter criteria to define the set of clients within the SET_CLIENT. +-- Filter criteria are defined by: +-- +-- * @{#SET_CLIENT.FilterCoalitions}: Builds the SET_CLIENT with the clients belonging to the coalition(s). +-- * @{#SET_CLIENT.FilterCategories}: Builds the SET_CLIENT with the clients belonging to the category(ies). +-- * @{#SET_CLIENT.FilterTypes}: Builds the SET_CLIENT with the clients belonging to the client type(s). +-- * @{#SET_CLIENT.FilterCountries}: Builds the SET_CLIENT with the clients belonging to the country(ies). +-- * @{#SET_CLIENT.FilterPrefixes}: Builds the SET_CLIENT with the clients starting with the same prefix string(s). +-- +-- Once the filter criteria have been set for the SET_CLIENT, you can start filtering using: +-- +-- * @{#SET_CLIENT.FilterStart}: Starts the filtering of the clients within the SET_CLIENT. +-- +-- Planned filter criteria within development are (so these are not yet available): +-- +-- * @{#SET_CLIENT.FilterZones}: Builds the SET_CLIENT with the clients within a @{Zone#ZONE}. +-- +-- 4.4) SET_CLIENT iterators: +-- ------------------------ +-- Once the filters have been defined and the SET_CLIENT has been built, you can iterate the SET_CLIENT with the available iterator methods. +-- The iterator methods will walk the SET_CLIENT set, and call for each element within the set a function that you provide. +-- The following iterator methods are currently available within the SET_CLIENT: +-- +-- * @{#SET_CLIENT.ForEachClient}: Calls a function for each alive client it finds within the SET_CLIENT. +-- +-- ==== +-- +-- 5) @{Set#SET_AIRBASE} class, extends @{Set#SET_BASE} +-- ==================================================== +-- Mission designers can use the @{Set#SET_AIRBASE} class to build sets of airbases optionally belonging to certain: +-- +-- * Coalitions +-- +-- 5.1) SET_AIRBASE construction +-- ----------------------------- +-- Create a new SET_AIRBASE object with the @{#SET_AIRBASE.New} method: +-- +-- * @{#SET_AIRBASE.New}: Creates a new SET_AIRBASE object. +-- +-- 5.2) Add or Remove AIRBASEs from SET_AIRBASE +-- -------------------------------------------- +-- AIRBASEs can be added and removed using the @{Set#SET_AIRBASE.AddAirbasesByName} and @{Set#SET_AIRBASE.RemoveAirbasesByName} respectively. +-- These methods take a single AIRBASE name or an array of AIRBASE names to be added or removed from SET_AIRBASE. +-- +-- 5.3) SET_AIRBASE filter criteria +-- -------------------------------- +-- You can set filter criteria to define the set of clients within the SET_AIRBASE. +-- Filter criteria are defined by: +-- +-- * @{#SET_AIRBASE.FilterCoalitions}: Builds the SET_AIRBASE with the airbases belonging to the coalition(s). +-- +-- Once the filter criteria have been set for the SET_AIRBASE, you can start filtering using: +-- +-- * @{#SET_AIRBASE.FilterStart}: Starts the filtering of the airbases within the SET_AIRBASE. +-- +-- 5.4) SET_AIRBASE iterators: +-- --------------------------- +-- Once the filters have been defined and the SET_AIRBASE has been built, you can iterate the SET_AIRBASE with the available iterator methods. +-- The iterator methods will walk the SET_AIRBASE set, and call for each airbase within the set a function that you provide. +-- The following iterator methods are currently available within the SET_AIRBASE: +-- +-- * @{#SET_AIRBASE.ForEachAirbase}: Calls a function for each airbase it finds within the SET_AIRBASE. +-- +-- ==== +-- +-- ### 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 + + _EVENTDISPATCHER:OnBirth( self._EventOnBirth, self ) + _EVENTDISPATCHER:OnDead( self._EventOnDeadOrCrash, self ) + _EVENTDISPATCHER:OnCrash( self._EventOnDeadOrCrash, self ) + + -- Follow alive players and clients + _EVENTDISPATCHER:OnPlayerEnterUnit( self._EventOnPlayerEnterUnit, self ) + _EVENTDISPATCHER:OnPlayerLeaveUnit( self._EventOnPlayerLeaveUnit, self ) + + + return self +end + +--- Stops the filtering for the defined collection. +-- @param #SET_BASE self +-- @return #SET_BASE self +function SET_BASE:FilterStop() + + _EVENTDISPATCHER:OnBirthRemove( self ) + _EVENTDISPATCHER:OnDeadRemove( self ) + _EVENTDISPATCHER:OnCrashRemove( self ) + + 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 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 not self.Database[Event.IniDCSGroupName] then + self.Database[Event.IniDCSGroupName] = GROUP:Register( Event.IniDCSGroupName ) + self:T3( self.Database[Event.IniDCSGroupName] ) + end + + return Event.IniDCSGroupName, self.Database[Event.IniDCSGroupName] +end + +--- Handles the Database to check on any event that Object exists in the Database. +-- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! +-- @param #SET_GROUP self +-- @param 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 not self.Database[Event.IniDCSUnitName] then + self.Database[Event.IniDCSUnitName] = UNIT:Register( Event.IniDCSUnitName ) + self:T3( self.Database[Event.IniDCSUnitName] ) + end + + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] +end + +--- Handles the Database to check on any event that Object exists in the Database. +-- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! +-- @param #SET_UNIT self +-- @param 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}. +-- +-- +-- 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}. +-- +-- === +-- +-- **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: +-- +-- 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 +-- @extends Core.Base#BASE +-- @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 +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 +-- @extends #POINT_VEC3 +-- @field Dcs.DCSTypes#Distance x The x coordinate in meters. +-- @field Dcs.DCSTypes#Distance y the y coordinate in meters. +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 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 #number x The x coordinate. +function POINT_VEC3:SetX( x ) + self.x = x +end + +--- Set the y coordinate of the POINT_VEC3. +-- @param #number y The y coordinate. +function POINT_VEC3:SetY( y ) + self.y = y +end + +--- Set the z coordinate of the POINT_VEC3. +-- @param #number z The z coordinate. +function POINT_VEC3:SetZ( z ) + self.z = z +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 + + +--- 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 #number x The x coordinate. +function POINT_VEC2:SetX( x ) + self.x = x +end + +--- Set the y coordinate of the POINT_VEC2. +-- @param #number y The y coordinate. +function POINT_VEC2:SetY( y ) + self.z = y +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 all players. +-- @param #MESSAGE self +-- @return #MESSAGE +-- @usage +-- -- Send a message created to all players. +-- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ):ToAll() +-- or +-- MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ):ToAll() +-- or +-- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ) +-- MessageAll:ToAll() +function MESSAGE:ToAll() + self:F() + + self:ToCoalition( coalition.side.RED ) + self:ToCoalition( coalition.side.BLUE ) + + return self +end + + + +----- The MESSAGEQUEUE class +---- @type MESSAGEQUEUE +--MESSAGEQUEUE = { +-- ClientGroups = {}, +-- CoalitionSides = {} +--} +-- +--function MESSAGEQUEUE:New( RefreshInterval ) +-- local self = BASE:Inherit( self, BASE:New() ) +-- self:F( { RefreshInterval } ) +-- +-- self.RefreshInterval = RefreshInterval +-- +-- --self.DisplayFunction = routines.scheduleFunction( self._DisplayMessages, { self }, 0, RefreshInterval ) +-- self.DisplayFunction = SCHEDULER:New( self, self._DisplayMessages, {}, 0, RefreshInterval ) +-- +-- return self +--end +-- +----- This function is called automatically by the MESSAGEQUEUE scheduler. +--function MESSAGEQUEUE:_DisplayMessages() +-- +-- -- First we display all messages that a coalition needs to receive... Also those who are not in a client (CA module clients...). +-- for CoalitionSideID, CoalitionSideData in pairs( self.CoalitionSides ) do +-- for MessageID, MessageData in pairs( CoalitionSideData.Messages ) do +-- if MessageData.MessageSent == false then +-- --trigger.action.outTextForCoalition( CoalitionSideID, MessageData.MessageCategory .. '\n' .. MessageData.MessageText:gsub("\n$",""):gsub("\n$",""), MessageData.MessageDuration ) +-- MessageData.MessageSent = true +-- end +-- local MessageTimeLeft = ( MessageData.MessageTime + MessageData.MessageDuration ) - timer.getTime() +-- if MessageTimeLeft <= 0 then +-- MessageData = nil +-- end +-- end +-- end +-- +-- -- Then we send the messages for each individual client, but also to be included are those Coalition messages for the Clients who belong to a coalition. +-- -- Because the Client messages will overwrite the Coalition messages (for that Client). +-- for ClientGroupName, ClientGroupData in pairs( self.ClientGroups ) do +-- for MessageID, MessageData in pairs( ClientGroupData.Messages ) do +-- if MessageData.MessageGroup == false then +-- trigger.action.outTextForGroup( Group.getByName(ClientGroupName):getID(), MessageData.MessageCategory .. '\n' .. MessageData.MessageText:gsub("\n$",""):gsub("\n$",""), MessageData.MessageDuration ) +-- MessageData.MessageGroup = true +-- end +-- local MessageTimeLeft = ( MessageData.MessageTime + MessageData.MessageDuration ) - timer.getTime() +-- if MessageTimeLeft <= 0 then +-- MessageData = nil +-- end +-- end +-- +-- -- Now check if the Client also has messages that belong to the Coalition of the Client... +-- for CoalitionSideID, CoalitionSideData in pairs( self.CoalitionSides ) do +-- for MessageID, MessageData in pairs( CoalitionSideData.Messages ) do +-- local CoalitionGroup = Group.getByName( ClientGroupName ) +-- if CoalitionGroup and CoalitionGroup:getCoalition() == CoalitionSideID then +-- if MessageData.MessageCoalition == false then +-- trigger.action.outTextForGroup( Group.getByName(ClientGroupName):getID(), MessageData.MessageCategory .. '\n' .. MessageData.MessageText:gsub("\n$",""):gsub("\n$",""), MessageData.MessageDuration ) +-- MessageData.MessageCoalition = true +-- end +-- end +-- local MessageTimeLeft = ( MessageData.MessageTime + MessageData.MessageDuration ) - timer.getTime() +-- if MessageTimeLeft <= 0 then +-- MessageData = nil +-- end +-- end +-- end +-- end +-- +-- return true +--end +-- +----- The _MessageQueue object is created when the MESSAGE class module is loaded. +----_MessageQueue = MESSAGEQUEUE:New( 0.5 ) +-- +--- 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.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) + if self[handler] then + self:T( "Calling " .. handler ) + 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) == false ) + or ( self:_call_handler("OnBefore" .. EventName, params) == false ) + or ( self:_call_handler("onleave" .. from, params) == false ) + or ( self:_call_handler("OnLeave" .. from, params) == 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) + self:_call_handler("OnEnter" .. to, params) + self:_call_handler("onafter" .. EventName, params) + self:_call_handler("OnAfter" .. EventName, params) + self:_call_handler("onstatechange", params) + 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) + self:_call_handler("OnEnter" .. to, params) + --end + + self:_call_handler("onafter" .. EventName, params) + self:_call_handler("OnAfter" .. EventName, params) + + self:_call_handler("onstatechange", params) + 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 = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1 ) + 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 ) + + 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 ) + 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 ) + if self[handler] then + self:T( "Calling " .. handler ) + 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 ) + if self[handler] then + self:T( "Calling " .. handler ) + 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 + + + + + + + + + +--- 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:E( 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 AttackUnit The UNIT. +-- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. +-- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. +-- @param 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:EnRouteTaskEngageUnit( AttackUnit, Priority, WeaponType, WeaponExpend, AttackQty, Direction, AttackQtyLimit, ControllableAttack ) + self:F2( { self.ControllableName, AttackUnit, Priority, WeaponType, WeaponExpend, AttackQty, Direction, AttackQtyLimit, ControllableAttack } ) + + -- EngageUnit = { + -- id = 'EngageUnit', + -- params = { + -- unitId = Unit.ID, + -- weaponType = number, + -- expend = enum AI.Task.WeaponExpend + -- attackQty = number, + -- direction = Azimuth, + -- attackQtyLimit = boolean, + -- controllableAttack = boolean, + -- priority = number, + -- } + -- } + + local DCSTask + DCSTask = { id = 'EngageUnit', + params = { + unitId = AttackUnit:GetID(), + weaponType = WeaponType, + expend = WeaponExpend, + attackQty = AttackQty, + direction = Direction, + attackQtyLimit = AttackQtyLimit, + controllableAttack = ControllableAttack, + priority = Priority, + }, + }, + + self:T3( { DCSTask } ) + return DCSTask +end + + + +--- (AIR) Aircraft will act as an AWACS for friendly units (will provide them with information about contacts). No parameters. +-- @param #CONTROLLABLE self +-- @return 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:TaskRouteToVec2( 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:TaskRouteToVec3( 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() + 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 + -- TODO: Rename IsPointVec3InZone to IsVec3InZone + if Zone:IsPointVec3InZone( 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:IsPointVec3InZone( 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:IsPointVec3InZone( 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. +function UNIT:GetThreatLevel() + + local Attributes = self:GetDesc().attributes + local ThreatLevel = 0 + + 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" + } + + self:T2( Attributes ) + + 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"] then ThreatLevel = 2 + elseif Attributes["Infantry"] then ThreatLevel = 1 + end + + self:T2( ThreatLevel ) + return ThreatLevel, ThreatLevels[ThreatLevel+1] + +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:IsPointVec3InZone( 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:IsPointVec3InZone( 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 + +--- 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. +-- @return #STATIC +function STATIC:FindByName( StaticName ) + local StaticFound = _DATABASE:FindStatic( StaticName ) + + self.StaticName = StaticName + + if StaticFound then + StaticFound:F( { StaticName } ) + + return StaticFound + end + + error( "STATIC not found for: " .. StaticName ) +end + +function STATIC:Register( StaticName ) + local self = BASE:Inherit( self, 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 +--- 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 + + + +--- Scoring system for MOOSE. +-- This scoring class calculates the hits and kills that players make within a simulation session. +-- Scoring is calculated using a defined algorithm. +-- With a small change in MissionScripting.lua, the scoring can also be logged in a CSV file, that can then be uploaded +-- to a database or a BI tool to publish the scoring results to the player community. +-- @module Scoring +-- @author FlightControl + + +--- The Scoring class +-- @type SCORING +-- @field Players A collection of the current players that have joined the game. +-- @extends 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() ) + + if GameName then + self.GameName = GameName + else + error( "A game name must be given to register the scoring results" ) + end + + + _EVENTDISPATCHER:OnDead( self._EventOnDeadOrCrash, self ) + _EVENTDISPATCHER:OnCrash( self._EventOnDeadOrCrash, self ) + _EVENTDISPATCHER:OnHit( self._EventOnHit, self ) + + --self.SchedulerId = routines.scheduleFunction( SCORING._FollowPlayersScheduled, { self }, 0, 5 ) + self.SchedulerId = SCHEDULER:New( self, self._FollowPlayersScheduled, {}, 0, 5 ) + + self:ScoreMenu() + + self:OpenCSV( GameName) + + return self + +end + +--- Creates a score radio menu. Can be accessed using Radio -> F10. +-- @param #SCORING self +-- @return #SCORING self +function SCORING:ScoreMenu() + self.Menu = MENU_MISSION:New( 'Scoring' ) + self.AllScoresMenu = MENU_MISSION_COMMAND:New( 'Score All Active Players', self.Menu, SCORING.ReportScoreAll, self ) + --- = COMMANDMENU:New('Your Current Score', ReportScore, SCORING.ReportScorePlayer, self ) + return self +end + +--- Follows new players entering Clients within the DCSRTE. +-- TODO: Need to see if i can catch this also with an event. It will eliminate the schedule ... +function SCORING:_FollowPlayersScheduled() + self:F3( "_FollowPlayersScheduled" ) + + local ClientUnit = 0 + local CoalitionsData = { AlivePlayersRed = coalition.getPlayers(coalition.side.RED), AlivePlayersBlue = coalition.getPlayers(coalition.side.BLUE) } + local unitId + local unitData + local AlivePlayerUnits = {} + + for CoalitionId, CoalitionData in pairs( CoalitionsData ) do + self:T3( { "_FollowPlayersScheduled", CoalitionData } ) + for UnitId, UnitData in pairs( CoalitionData ) do + self:_AddPlayerFromUnit( UnitData ) + end + end + + return true +end + + +--- Track DEAD or CRASH events for the scoring. +-- @param #SCORING self +-- @param 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.IniDCSUnit + TargetUnitName = Event.IniDCSUnitName + TargetGroup = Event.IniDCSGroup + TargetGroupName = Event.IniDCSGroupName + TargetPlayerName = TargetUnit:getPlayerName() + + TargetCoalition = TargetUnit:getCoalition() + --TargetCategory = TargetUnit:getCategory() + TargetCategory = TargetUnit:getDesc().category -- Workaround + TargetType = TargetUnit:getTypeName() + + TargetUnitCoalition = _SCORINGCoalition[TargetCoalition] + TargetUnitCategory = _SCORINGCategory[TargetCategory] + TargetUnitType = TargetType + + self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType } ) + end + + for PlayerName, PlayerData in pairs( self.Players ) do + if PlayerData then -- This should normally not happen, but i'll test it anyway. + self:T( "Something got killed" ) + + -- Some variables + local InitUnitName = PlayerData.UnitName + local InitUnitType = PlayerData.UnitType + local InitCoalition = PlayerData.UnitCoalition + local InitCategory = PlayerData.UnitCategory + local InitUnitCoalition = _SCORINGCoalition[InitCoalition] + local InitUnitCategory = _SCORINGCategory[InitCategory] + + self:T( { InitUnitName, InitUnitType, InitUnitCoalition, InitCoalition, InitUnitCategory, InitCategory } ) + + -- What is he hitting? + if TargetCategory then + if PlayerData and PlayerData.Hit and PlayerData.Hit[TargetCategory] and PlayerData.Hit[TargetCategory][TargetUnitName] then -- Was there a hit for this unit for this player before registered??? + if not PlayerData.Kill[TargetCategory] then + PlayerData.Kill[TargetCategory] = {} + end + if not PlayerData.Kill[TargetCategory][TargetType] then + PlayerData.Kill[TargetCategory][TargetType] = {} + PlayerData.Kill[TargetCategory][TargetType].Score = 0 + PlayerData.Kill[TargetCategory][TargetType].ScoreKill = 0 + PlayerData.Kill[TargetCategory][TargetType].Penalty = 0 + PlayerData.Kill[TargetCategory][TargetType].PenaltyKill = 0 + end + + if InitCoalition == TargetCoalition then + PlayerData.Penalty = PlayerData.Penalty + 25 + PlayerData.Kill[TargetCategory][TargetType].Penalty = PlayerData.Kill[TargetCategory][TargetType].Penalty + 25 + PlayerData.Kill[TargetCategory][TargetType].PenaltyKill = PlayerData.Kill[TargetCategory][TargetType].PenaltyKill + 1 + MESSAGE:New( "Player '" .. PlayerName .. "' killed a friendly " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. + PlayerData.Kill[TargetCategory][TargetType].PenaltyKill .. " times. Penalty: -" .. PlayerData.Kill[TargetCategory][TargetType].Penalty .. + ". Score Total:" .. PlayerData.Score - PlayerData.Penalty, + 5 ):ToAll() + self:ScoreCSV( PlayerName, "KILL_PENALTY", 1, -125, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + else + PlayerData.Score = PlayerData.Score + 10 + PlayerData.Kill[TargetCategory][TargetType].Score = PlayerData.Kill[TargetCategory][TargetType].Score + 10 + PlayerData.Kill[TargetCategory][TargetType].ScoreKill = PlayerData.Kill[TargetCategory][TargetType].ScoreKill + 1 + MESSAGE:New( "Player '" .. PlayerName .. "' killed an enemy " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. + PlayerData.Kill[TargetCategory][TargetType].ScoreKill .. " times. Score: " .. PlayerData.Kill[TargetCategory][TargetType].Score .. + ". Score Total:" .. PlayerData.Score - PlayerData.Penalty, + 5 ):ToAll() + self:ScoreCSV( PlayerName, "KILL_SCORE", 1, 10, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + end + end + end + end + end +end + + + +--- Add a new player entering a Unit. +function SCORING:_AddPlayerFromUnit( UnitData ) + self:F( UnitData ) + + if UnitData and UnitData:isExist() then + local UnitName = UnitData:getName() + local PlayerName = UnitData:getPlayerName() + local UnitDesc = UnitData:getDesc() + local UnitCategory = UnitDesc.category + local UnitCoalition = UnitData:getCoalition() + local UnitTypeName = UnitData:getTypeName() + + self:T( { PlayerName, UnitName, UnitCategory, UnitCoalition, UnitTypeName } ) + + if self.Players[PlayerName] == nil then -- I believe this is the place where a Player gets a life in a mission when he enters a unit ... + self.Players[PlayerName] = {} + self.Players[PlayerName].Hit = {} + self.Players[PlayerName].Kill = {} + self.Players[PlayerName].Mission = {} + + -- for CategoryID, CategoryName in pairs( SCORINGCategory ) do + -- self.Players[PlayerName].Hit[CategoryID] = {} + -- self.Players[PlayerName].Kill[CategoryID] = {} + -- end + self.Players[PlayerName].HitPlayers = {} + self.Players[PlayerName].HitUnits = {} + self.Players[PlayerName].Score = 0 + self.Players[PlayerName].Penalty = 0 + self.Players[PlayerName].PenaltyCoalition = 0 + self.Players[PlayerName].PenaltyWarning = 0 + end + + if not self.Players[PlayerName].UnitCoalition then + self.Players[PlayerName].UnitCoalition = UnitCoalition + else + if self.Players[PlayerName].UnitCoalition ~= UnitCoalition then + self.Players[PlayerName].Penalty = self.Players[PlayerName].Penalty + 50 + self.Players[PlayerName].PenaltyCoalition = self.Players[PlayerName].PenaltyCoalition + 1 + MESSAGE:New( "Player '" .. PlayerName .. "' changed coalition from " .. _SCORINGCoalition[self.Players[PlayerName].UnitCoalition] .. " to " .. _SCORINGCoalition[UnitCoalition] .. + "(changed " .. self.Players[PlayerName].PenaltyCoalition .. " times the coalition). 50 Penalty points added.", + 2 + ):ToAll() + self:ScoreCSV( PlayerName, "COALITION_PENALTY", 1, -50, self.Players[PlayerName].UnitName, _SCORINGCoalition[self.Players[PlayerName].UnitCoalition], _SCORINGCategory[self.Players[PlayerName].UnitCategory], self.Players[PlayerName].UnitType, + UnitName, _SCORINGCoalition[UnitCoalition], _SCORINGCategory[UnitCategory], UnitData:getTypeName() ) + end + end + self.Players[PlayerName].UnitName = UnitName + self.Players[PlayerName].UnitCoalition = UnitCoalition + self.Players[PlayerName].UnitCategory = UnitCategory + self.Players[PlayerName].UnitType = UnitTypeName + + if self.Players[PlayerName].Penalty > 100 then + if self.Players[PlayerName].PenaltyWarning < 1 then + MESSAGE:New( "Player '" .. PlayerName .. "': WARNING! If you continue to commit FRATRICIDE and have a PENALTY score higher than 150, you will be COURT MARTIALED and DISMISSED from this mission! \nYour total penalty is: " .. self.Players[PlayerName].Penalty, + 30 + ):ToAll() + self.Players[PlayerName].PenaltyWarning = self.Players[PlayerName].PenaltyWarning + 1 + end + end + + if self.Players[PlayerName].Penalty > 150 then + ClientGroup = GROUP:NewFromDCSUnit( UnitData ) + ClientGroup:Destroy() + MESSAGE:New( "Player '" .. PlayerName .. "' committed FRATRICIDE, he will be COURT MARTIALED and is DISMISSED from this mission!", + 10 + ):ToAll() + end + + end +end + + +--- Registers Scores the players completing a Mission Task. +-- @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 OnHit event for the scoring. +-- @param #SCORING self +-- @param Core.Event#EVENTDATA Event +function SCORING:_EventOnHit( Event ) + self:F( { Event } ) + + local InitUnit = nil + local InitUnitName = "" + local InitGroup = nil + local InitGroupName = "" + local InitPlayerName = nil + + local InitCoalition = nil + local InitCategory = nil + local InitType = nil + local InitUnitCoalition = nil + local InitUnitCategory = nil + local InitUnitType = nil + + local TargetUnit = nil + local TargetUnitName = "" + local TargetGroup = nil + local TargetGroupName = "" + local TargetPlayerName = "" + + local TargetCoalition = nil + local TargetCategory = nil + local TargetType = nil + local TargetUnitCoalition = nil + local TargetUnitCategory = nil + local TargetUnitType = nil + + if Event.IniDCSUnit then + + InitUnit = Event.IniDCSUnit + InitUnitName = Event.IniDCSUnitName + InitGroup = Event.IniDCSGroup + InitGroupName = Event.IniDCSGroupName + InitPlayerName = InitUnit:getPlayerName() + + InitCoalition = InitUnit:getCoalition() + --TODO: Workaround Client DCS Bug + --InitCategory = InitUnit:getCategory() + InitCategory = InitUnit:getDesc().category + InitType = InitUnit:getTypeName() + + InitUnitCoalition = _SCORINGCoalition[InitCoalition] + InitUnitCategory = _SCORINGCategory[InitCategory] + InitUnitType = InitType + + self:T( { InitUnitName, InitGroupName, InitPlayerName, InitCoalition, InitCategory, InitType , InitUnitCoalition, InitUnitCategory, InitUnitType } ) + end + + + if Event.TgtDCSUnit then + + TargetUnit = Event.TgtDCSUnit + TargetUnitName = Event.TgtDCSUnitName + TargetGroup = Event.TgtDCSGroup + TargetGroupName = Event.TgtDCSGroupName + TargetPlayerName = TargetUnit:getPlayerName() + + TargetCoalition = TargetUnit:getCoalition() + --TODO: Workaround Client DCS Bug + --TargetCategory = TargetUnit:getCategory() + TargetCategory = TargetUnit:getDesc().category + TargetType = TargetUnit:getTypeName() + + TargetUnitCoalition = _SCORINGCoalition[TargetCoalition] + TargetUnitCategory = _SCORINGCategory[TargetCategory] + TargetUnitType = TargetType + + self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType, TargetUnitCoalition, TargetUnitCategory, TargetUnitType } ) + end + + if InitPlayerName ~= nil then -- It is a player that is hitting something + self:_AddPlayerFromUnit( InitUnit ) + if self.Players[InitPlayerName] then -- This should normally not happen, but i'll test it anyway. + if TargetPlayerName ~= nil then -- It is a player hitting another player ... + self:_AddPlayerFromUnit( TargetUnit ) + self.Players[InitPlayerName].HitPlayers = self.Players[InitPlayerName].HitPlayers + 1 + end + + self:T( "Hitting Something" ) + -- What is he hitting? + if TargetCategory then + if not self.Players[InitPlayerName].Hit[TargetCategory] then + self.Players[InitPlayerName].Hit[TargetCategory] = {} + end + if not self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName] then + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName] = {} + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score = 0 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty = 0 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit = 0 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit = 0 + end + local Score = 0 + if InitCoalition == TargetCoalition then + self.Players[InitPlayerName].Penalty = self.Players[InitPlayerName].Penalty + 10 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty + 10 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit + 1 + MESSAGE:New( "Player '" .. InitPlayerName .. "' hit a friendly " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit .. " times. Penalty: -" .. self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty .. + ". Score Total:" .. self.Players[InitPlayerName].Score - self.Players[InitPlayerName].Penalty, + 2 + ):ToAll() + self:ScoreCSV( InitPlayerName, "HIT_PENALTY", 1, -25, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + else + self.Players[InitPlayerName].Score = self.Players[InitPlayerName].Score + 1 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score + 1 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit + 1 + MESSAGE:New( "Player '" .. InitPlayerName .. "' hit a target " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit .. " times. Score: " .. self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score .. + ". Score Total:" .. self.Players[InitPlayerName].Score - self.Players[InitPlayerName].Penalty, + 2 + ):ToAll() + self:ScoreCSV( InitPlayerName, "HIT_SCORE", 1, 1, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + end + end + end + elseif InitPlayerName == nil then -- It is an AI hitting a player??? + + end +end + + +function SCORING:ReportScoreAll() + + env.info( "Hello World " ) + + local ScoreMessage = "" + local PlayerMessage = "" + + self:T( "Score Report" ) + + for PlayerName, PlayerData in pairs( self.Players ) do + if PlayerData then -- This should normally not happen, but i'll test it anyway. + self:T( "Score Player: " .. PlayerName ) + + -- Some variables + local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] + local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] + local InitUnitType = PlayerData.UnitType + local InitUnitName = PlayerData.UnitName + + local PlayerScore = 0 + local PlayerPenalty = 0 + + ScoreMessage = ":\n" + + local ScoreMessageHits = "" + + for CategoryID, CategoryName in pairs( _SCORINGCategory ) do + self:T( CategoryName ) + if PlayerData.Hit[CategoryID] then + local Score = 0 + local ScoreHit = 0 + local Penalty = 0 + local PenaltyHit = 0 + self:T( "Hit scores exist for player " .. PlayerName ) + for UnitName, UnitData in pairs( PlayerData.Hit[CategoryID] ) do + Score = Score + UnitData.Score + ScoreHit = ScoreHit + UnitData.ScoreHit + Penalty = Penalty + UnitData.Penalty + PenaltyHit = UnitData.PenaltyHit + end + local ScoreMessageHit = string.format( "%s:%d ", CategoryName, Score - Penalty ) + self:T( ScoreMessageHit ) + ScoreMessageHits = ScoreMessageHits .. ScoreMessageHit + PlayerScore = PlayerScore + Score + PlayerPenalty = PlayerPenalty + Penalty + else + --ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) + end + end + if ScoreMessageHits ~= "" then + ScoreMessage = ScoreMessage .. " Hits: " .. ScoreMessageHits .. "\n" + end + + local ScoreMessageKills = "" + for CategoryID, CategoryName in pairs( _SCORINGCategory ) do + self:T( "Kill scores exist for player " .. PlayerName ) + if PlayerData.Kill[CategoryID] then + local Score = 0 + local ScoreKill = 0 + local Penalty = 0 + local PenaltyKill = 0 + + for UnitName, UnitData in pairs( PlayerData.Kill[CategoryID] ) do + Score = Score + UnitData.Score + ScoreKill = ScoreKill + UnitData.ScoreKill + Penalty = Penalty + UnitData.Penalty + PenaltyKill = PenaltyKill + UnitData.PenaltyKill + end + + local ScoreMessageKill = string.format( " %s:%d ", CategoryName, Score - Penalty ) + self:T( ScoreMessageKill ) + ScoreMessageKills = ScoreMessageKills .. ScoreMessageKill + + PlayerScore = PlayerScore + Score + PlayerPenalty = PlayerPenalty + Penalty + else + --ScoreMessageKills = ScoreMessageKills .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) + end + end + if ScoreMessageKills ~= "" then + ScoreMessage = ScoreMessage .. " Kills: " .. ScoreMessageKills .. "\n" + end + + local ScoreMessageCoalitionChangePenalties = "" + if PlayerData.PenaltyCoalition ~= 0 then + ScoreMessageCoalitionChangePenalties = ScoreMessageCoalitionChangePenalties .. string.format( " -%d (%d changed)", PlayerData.Penalty, PlayerData.PenaltyCoalition ) + PlayerPenalty = PlayerPenalty + PlayerData.Penalty + end + if ScoreMessageCoalitionChangePenalties ~= "" then + ScoreMessage = ScoreMessage .. " Coalition Penalties: " .. ScoreMessageCoalitionChangePenalties .. "\n" + end + + local ScoreMessageMission = "" + local ScoreMission = 0 + local ScoreTask = 0 + for MissionName, MissionData in pairs( PlayerData.Mission ) do + ScoreMission = ScoreMission + MissionData.ScoreMission + ScoreTask = ScoreTask + MissionData.ScoreTask + ScoreMessageMission = ScoreMessageMission .. "'" .. MissionName .. "'; " + end + PlayerScore = PlayerScore + ScoreMission + ScoreTask + + if ScoreMessageMission ~= "" then + ScoreMessage = ScoreMessage .. " Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ")\n" + end + + PlayerMessage = PlayerMessage .. string.format( "Player '%s' Score:%d (%d Score -%d Penalties)%s", PlayerName, PlayerScore - PlayerPenalty, PlayerScore, PlayerPenalty, ScoreMessage ) + end + end + MESSAGE:New( PlayerMessage, 30, "Player Scores" ):ToAll() +end + + +function SCORING:ReportScorePlayer() + + env.info( "Hello World " ) + + local ScoreMessage = "" + local PlayerMessage = "" + + self:T( "Score Report" ) + + for PlayerName, PlayerData in pairs( self.Players ) do + if PlayerData then -- This should normally not happen, but i'll test it anyway. + self:T( "Score Player: " .. PlayerName ) + + -- Some variables + local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] + local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] + local InitUnitType = PlayerData.UnitType + local InitUnitName = PlayerData.UnitName + + local PlayerScore = 0 + local PlayerPenalty = 0 + + ScoreMessage = "" + + local ScoreMessageHits = "" + + for CategoryID, CategoryName in pairs( _SCORINGCategory ) do + self:T( CategoryName ) + if PlayerData.Hit[CategoryID] then + local Score = 0 + local ScoreHit = 0 + local Penalty = 0 + local PenaltyHit = 0 + self:T( "Hit scores exist for player " .. PlayerName ) + for UnitName, UnitData in pairs( PlayerData.Hit[CategoryID] ) do + Score = Score + UnitData.Score + ScoreHit = ScoreHit + UnitData.ScoreHit + Penalty = Penalty + UnitData.Penalty + PenaltyHit = UnitData.PenaltyHit + end + local ScoreMessageHit = string.format( "\n %s = %d score(%d;-%d) hits(#%d;#-%d)", CategoryName, Score - Penalty, Score, Penalty, ScoreHit, PenaltyHit ) + self:T( ScoreMessageHit ) + ScoreMessageHits = ScoreMessageHits .. ScoreMessageHit + PlayerScore = PlayerScore + Score + PlayerPenalty = PlayerPenalty + Penalty + else + --ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) + end + end + if ScoreMessageHits ~= "" then + ScoreMessage = ScoreMessage .. "\n Hits: " .. ScoreMessageHits .. " " + end + + local ScoreMessageKills = "" + for CategoryID, CategoryName in pairs( _SCORINGCategory ) do + self:T( "Kill scores exist for player " .. PlayerName ) + if PlayerData.Kill[CategoryID] then + local Score = 0 + local ScoreKill = 0 + local Penalty = 0 + local PenaltyKill = 0 + + for UnitName, UnitData in pairs( PlayerData.Kill[CategoryID] ) do + Score = Score + UnitData.Score + ScoreKill = ScoreKill + UnitData.ScoreKill + Penalty = Penalty + UnitData.Penalty + PenaltyKill = PenaltyKill + UnitData.PenaltyKill + end + + local ScoreMessageKill = string.format( "\n %s = %d score(%d;-%d) hits(#%d;#-%d)", CategoryName, Score - Penalty, Score, Penalty, ScoreKill, PenaltyKill ) + self:T( ScoreMessageKill ) + ScoreMessageKills = ScoreMessageKills .. ScoreMessageKill + + PlayerScore = PlayerScore + Score + PlayerPenalty = PlayerPenalty + Penalty + else + --ScoreMessageKills = ScoreMessageKills .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) + end + end + if ScoreMessageKills ~= "" then + ScoreMessage = ScoreMessage .. "\n Kills: " .. ScoreMessageKills .. " " + end + + local ScoreMessageCoalitionChangePenalties = "" + if PlayerData.PenaltyCoalition ~= 0 then + ScoreMessageCoalitionChangePenalties = ScoreMessageCoalitionChangePenalties .. string.format( " -%d (%d changed)", PlayerData.Penalty, PlayerData.PenaltyCoalition ) + PlayerPenalty = PlayerPenalty + PlayerData.Penalty + end + if ScoreMessageCoalitionChangePenalties ~= "" then + ScoreMessage = ScoreMessage .. "\n Coalition: " .. ScoreMessageCoalitionChangePenalties .. " " + end + + local ScoreMessageMission = "" + local ScoreMission = 0 + local ScoreTask = 0 + for MissionName, MissionData in pairs( PlayerData.Mission ) do + ScoreMission = ScoreMission + MissionData.ScoreMission + ScoreTask = ScoreTask + MissionData.ScoreTask + ScoreMessageMission = ScoreMessageMission .. "'" .. MissionName .. "'; " + end + PlayerScore = PlayerScore + ScoreMission + ScoreTask + + if ScoreMessageMission ~= "" then + ScoreMessage = ScoreMessage .. "\n Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ") " + end + + PlayerMessage = PlayerMessage .. string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties ):%s", PlayerName, PlayerScore - PlayerPenalty, PlayerScore, PlayerPenalty, ScoreMessage ) + end + end + MESSAGE:New( PlayerMessage, 30, "Player Scores" ):ToAll() + +end + + +function SCORING:SecondsToClock(sSeconds) + local nSeconds = sSeconds + if nSeconds == 0 then + --return nil; + return "00:00:00"; + else + nHours = string.format("%02.f", math.floor(nSeconds/3600)); + nMins = string.format("%02.f", math.floor(nSeconds/60 - (nHours*60))); + nSecs = string.format("%02.f", math.floor(nSeconds - nHours*3600 - nMins *60)); + return nHours..":"..nMins..":"..nSecs + end +end + +--- Opens a score CSV file to log the scores. +-- @param #SCORING self +-- @param #string ScoringCSV +-- @return #SCORING self +-- @usage +-- -- Open a new CSV file to log the scores of the game Gori Valley. Let the name of the CSV file begin with "Player Scores". +-- ScoringObject = SCORING:New( "Gori Valley" ) +-- ScoringObject:OpenCSV( "Player Scores" ) +function SCORING:OpenCSV( ScoringCSV ) + self:F( ScoringCSV ) + + if lfs and io and os then + if ScoringCSV then + self.ScoringCSV = ScoringCSV + local fdir = lfs.writedir() .. [[Logs\]] .. self.ScoringCSV .. " " .. os.date( "%Y-%m-%d %H-%M-%S" ) .. ".csv" + + self.CSVFile, self.err = io.open( fdir, "w+" ) + if not self.CSVFile then + error( "Error: Cannot open CSV file in " .. lfs.writedir() ) + end + + self.CSVFile:write( '"GameName","RunTime","Time","PlayerName","ScoreType","PlayerUnitCoaltion","PlayerUnitCategory","PlayerUnitType","PlayerUnitName","TargetUnitCoalition","TargetUnitCategory","TargetUnitType","TargetUnitName","Times","Score"\n' ) + + self.RunTime = os.date("%y-%m-%d_%H-%M-%S") + else + error( "A string containing the CSV file name must be given." ) + end + else + self:E( "The MissionScripting.lua file has not been changed to allow lfs, io and os modules to be used..." ) + end + return self +end + + +--- Registers a score for a player. +-- @param #SCORING self +-- @param #string PlayerName The name of the player. +-- @param #string ScoreType The type of the score. +-- @param #string ScoreTimes The amount of scores achieved. +-- @param #string ScoreAmount The score given. +-- @param #string PlayerUnitName The unit name of the player. +-- @param #string PlayerUnitCoalition The coalition of the player unit. +-- @param #string PlayerUnitCategory The category of the player unit. +-- @param #string PlayerUnitType The type of the player unit. +-- @param #string TargetUnitName The name of the target unit. +-- @param #string TargetUnitCoalition The coalition of the target unit. +-- @param #string TargetUnitCategory The category of the target unit. +-- @param #string TargetUnitType The type of the target unit. +-- @return #SCORING self +function SCORING:ScoreCSV( PlayerName, ScoreType, ScoreTimes, ScoreAmount, PlayerUnitName, PlayerUnitCoalition, PlayerUnitCategory, PlayerUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + --write statistic information to file + local ScoreTime = self:SecondsToClock( timer.getTime() ) + PlayerName = PlayerName:gsub( '"', '_' ) + + if PlayerUnitName and PlayerUnitName ~= '' then + local PlayerUnit = Unit.getByName( PlayerUnitName ) + + if PlayerUnit then + if not PlayerUnitCategory then + --PlayerUnitCategory = SCORINGCategory[PlayerUnit:getCategory()] + PlayerUnitCategory = _SCORINGCategory[PlayerUnit:getDesc().category] + end + + if not PlayerUnitCoalition then + PlayerUnitCoalition = _SCORINGCoalition[PlayerUnit:getCoalition()] + end + + if not PlayerUnitType then + PlayerUnitType = PlayerUnit:getTypeName() + end + else + PlayerUnitName = '' + PlayerUnitCategory = '' + PlayerUnitCoalition = '' + PlayerUnitType = '' + end + else + PlayerUnitName = '' + PlayerUnitCategory = '' + PlayerUnitCoalition = '' + PlayerUnitType = '' + end + + if not TargetUnitCoalition then + TargetUnitCoalition = '' + end + + if not TargetUnitCategory then + TargetUnitCategory = '' + end + + if not TargetUnitType then + TargetUnitType = '' + end + + if not TargetUnitName then + TargetUnitName = '' + end + + if lfs and io and os then + self.CSVFile:write( + '"' .. self.GameName .. '"' .. ',' .. + '"' .. self.RunTime .. '"' .. ',' .. + '' .. ScoreTime .. '' .. ',' .. + '"' .. PlayerName .. '"' .. ',' .. + '"' .. ScoreType .. '"' .. ',' .. + '"' .. PlayerUnitCoalition .. '"' .. ',' .. + '"' .. PlayerUnitCategory .. '"' .. ',' .. + '"' .. PlayerUnitType .. '"' .. ',' .. + '"' .. PlayerUnitName .. '"' .. ',' .. + '"' .. TargetUnitCoalition .. '"' .. ',' .. + '"' .. TargetUnitCategory .. '"' .. ',' .. + '"' .. TargetUnitType .. '"' .. ',' .. + '"' .. TargetUnitName .. '"' .. ',' .. + '' .. ScoreTimes .. '' .. ',' .. + '' .. ScoreAmount + ) + + self.CSVFile:write( "\n" ) + end +end + + +function SCORING:CloseCSV() + if lfs and io and os then + self.CSVFile:close() + end +end + +--- 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** / Mulit-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.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.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 + SpawnTemplate.uncontrolled = self.SpawnUnControlled + 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 + 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].y = self.SpawnTemplate.units[1].y + 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:TaskRouteToVec3( GDV, Speed / 3.6 ) -- DCS models speed in Mps (Miles per second) + end + + return true + end + + return false +end + + +--- Report Targets Scheduler. +-- @param #ESCORT self +function ESCORT:_ReportTargetsScheduler() + self:F( self.EscortGroup:GetName() ) + + if self.EscortGroup:IsAlive() and self.EscortClient:IsAlive() then + local EscortGroupName = self.EscortGroup:GetName() + local EscortTargets = self.EscortGroup:GetDetectedTargets() + + local ClientEscortTargets = self.EscortClient._EscortGroups[EscortGroupName].Targets + + local EscortTargetMessages = "" + for EscortTargetID, EscortTarget in pairs( EscortTargets ) do + local EscortObject = EscortTarget.object + self:T( EscortObject ) + if EscortObject and EscortObject:isExist() and EscortObject.id_ < 50000000 then + + local EscortTargetUnit = UNIT:Find( EscortObject ) + local EscortTargetUnitName = EscortTargetUnit:GetName() + + + + -- local EscortTargetIsDetected, + -- EscortTargetIsVisible, + -- EscortTargetLastTime, + -- EscortTargetKnowType, + -- EscortTargetKnowDistance, + -- EscortTargetLastPos, + -- EscortTargetLastVelocity + -- = self.EscortGroup:IsTargetDetected( EscortObject ) + -- + -- self:T( { EscortTargetIsDetected, + -- EscortTargetIsVisible, + -- EscortTargetLastTime, + -- EscortTargetKnowType, + -- EscortTargetKnowDistance, + -- EscortTargetLastPos, + -- EscortTargetLastVelocity } ) + + + local 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. + SCHEDULER:New( TrainerWeapon, TrainerWeapon.destroy, {}, 2 ) + if ( TrainerWeapon:getTypeName() == "9M311" ) then + SCHEDULER:New( TrainerWeapon, TrainerWeapon.destroy, {}, 2 ) + 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() + 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** / Mulit-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 + + 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 + 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 + +--- @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** / Mulit-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:SetDetectionOn() + + self.CheckStatus = true + + self:ManageFuel( .2, 60 ) + self:ManageDamage( 1 ) + + self:SetDetectionInterval( 30 ) + + 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", "Ejected" ) + self:AddTransition( "*", "Crash", "Crashed" ) + self:AddTransition( "*", "PilotDead", "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.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.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 + + +--- 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:EventOnPilotDead( self.OnPilotDead ) + self:EventOnCrash( self.OnCrash ) + self:EventOnEjection( self.OnEjection ) + + + Controllable:OptionROEHoldFire() + Controllable:OptionROTVertical() + + self.Controllable:OnReSpawn( + function( PatrolGroup ) + self:E( "ReSpawn" ) + self:__Reset() + self:__Route( 5 ) + end + ) + +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 + self:T( TargetObject ) + 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 } ) + self.DetectedUnits[TargetUnit] = TargetUnit + Detected = true + end + else + self.DetectedUnits[TargetUnit] = TargetUnit + 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:__Crash( 1, EventData ) + 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** / Mulit-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 Dcs.DCSTypes#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO +-- @param Core.Zone#ZONE EngageZone +-- @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. + -- @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. + + --- 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", "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:EventOnDead( self.OnDead ) + +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: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 + + + if self.Controllable:IsNotInZone( self.EngageZone ) then + + -- Find a random 2D point in EngageZone. + local ToEngageZoneVec2 = self.EngageZone:GetRandomVec2() + self:T2( ToEngageZoneVec2 ) + + -- Define Speed and Altitude. + local ToEngageZoneAltitude = math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) + local ToEngageZoneSpeed = self.PatrolMaxSpeed + self:T2( ToEngageZoneSpeed ) + + -- Obtain a 3D @{Point} from the 2D point + altitude. + local ToEngageZonePointVec3 = POINT_VEC3:New( ToEngageZoneVec2.x, ToEngageZoneAltitude, ToEngageZoneVec2.y ) + + -- Create a route point of type air. + local ToEngageZoneRoutePoint = ToEngageZonePointVec3:RoutePointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToEngageZoneSpeed, + 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 ) + + --- 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 ToTargetRoutePoint = ToTargetPointVec3:RoutePointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true + ) + + ToTargetPointVec3:SmokeBlue() + + EngageRoute[#EngageRoute+1] = ToTargetRoutePoint + + + Controllable:OptionROEOpenFire() + Controllable:OptionROTPassiveDefense() + + 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, 2 ) + 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:SetDetectionOff() +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** / Mulit-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 DetectedUnitID, DetectedUnit 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 DetectedUnitID, DetectedUnit 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:__Accomplish( 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** / Mulit-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:EventOnDead( 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:EventOnBirth( + --- @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:EventOnPlayerEnterUnit( + --- @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:EventOnPlayerLeaveUnit( + --- @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:EventOnCrash( + --- @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. +-- +-- 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, +} + +--- 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: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() + + -- Handle the birth of new planes within the assigned set. + + + -- Handle when a player crashes ... + -- The Task is UnAssigned from the Unit. + -- When there is no Unit left running the Task, and all of the Players crashed, the Task goes into Failed ... +-- self:EventOnCrash( +-- --- @param #TASK self +-- -- @param Core.Event#EVENTDATA EventData +-- function( self, EventData ) +-- self:E( "In LeaveUnit" ) +-- self:E( { "State", self:GetState() } ) +-- if self:IsStateAssigned() then +-- local TaskUnit = EventData.IniUnit +-- local TaskGroup = EventData.IniUnit:GetGroup() +-- self:E( self.SetGroup:IsIncludeObject( TaskGroup ) ) +-- if self.SetGroup:IsIncludeObject( TaskGroup ) then +-- self:UnAssignFromUnit( TaskUnit ) +-- end +-- self:MessageToGroups( TaskUnit:GetPlayerName() .. " crashed!, and has aborted Task " .. self:GetName() ) +-- end +-- end +-- ) +-- + + 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 + +--- 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() +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() + +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 + +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" ) + +--- 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 Presentations/SPAWN.pptx b/Moose Presentations/SPAWN.pptx index 060aeabea2ff8f002eb8568dfa2e897b8caec3e8..afd38a8b5d0d692d64600b164ad7899b28844c50 100644 GIT binary patch delta 21352 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 f+aULq?Kc{kTLzE#nFjk8Z1|0n#@g5@a diff --git a/Moose Test Missions/AIB - AI Balancing/AIB-001 - Spawned AI/AIB-001 - Spawned AI.lua b/Moose Test Missions/AIB - AI Balancing/AIB-001 - Spawned AI/AIB-001 - Spawned AI.lua index 5315ae2e7..a3104d156 100644 --- a/Moose Test Missions/AIB - AI Balancing/AIB-001 - Spawned AI/AIB-001 - Spawned AI.lua +++ b/Moose Test Missions/AIB - AI Balancing/AIB-001 - Spawned AI/AIB-001 - Spawned AI.lua @@ -1,3 +1,4 @@ +--- -- Name: AIB-001 - Spawned AI -- Author: FlightControl -- Date Created: 07 Dec 2016 diff --git a/Moose Test Missions/DET - Detection/DET-001 - Detection Areas/DET-001 - Detection Areas.lua b/Moose Test Missions/DET - Detection/DET-001 - Detection Areas/DET-001 - Detection Areas.lua index f48027238..be56c16f9 100644 --- a/Moose Test Missions/DET - Detection/DET-001 - Detection Areas/DET-001 - Detection Areas.lua +++ b/Moose Test Missions/DET - Detection/DET-001 - Detection Areas/DET-001 - Detection Areas.lua @@ -1,5 +1,23 @@ +--- +-- Name: DET-001 - Detection Areas +-- Author: FlightControl +-- Date Created: 04 Feb 2017 +-- +-- # Situation: +-- +-- A small blue vehicle with laser detection methods is detecting targets. +-- Targets are grouped within areas. A detection range and zone range is given to group the detected units. +-- This demo will group 5 red vehicles in areas. One vehicle is diving from one group to the other. +-- +-- # Test cases: +-- +-- 1. Observe the flaring of the areas formed +-- 2. Observe the smoking of the units detected +-- 3. Observe the areas being flexibly changed very detection run. +-- 4. The truck driving from the one group to the other, will leave the first area, and will join the second. +-- 5. While driving in between the areas, it will have a separate area. -local FACGroup = GROUP:FindByName( "FAC Group" ) +local FACSetGroup = SET_GROUP:New():FilterPrefixes( "FAC Group" ):FilterStart() -local FACDetection = DETECTION_AREAS:New( FACGroup, 1000, 250 ):FlareDetectedZones():FlareDetectedUnits() +local FACDetection = DETECTION_AREAS:New( FACSetGroup, 1000, 250 ):FlareDetectedZones():SmokeDetectedUnits() diff --git a/Moose Test Missions/DET - Detection/DET-001 - Detection Areas/DET-001 - Detection Areas.miz b/Moose Test Missions/DET - Detection/DET-001 - Detection Areas/DET-001 - Detection Areas.miz index 3d902d5a2af8a2c7c4de924d6706baf327dcacc3..fce1dfa48c8beda7e02ab1ab2e878fb6b0e9636e 100644 GIT binary patch delta 208290 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ℑDuc)KMkUeK+VEayV1-T@aV`yxUz zS`~QFE+o0@X-uZHbig1c@FHRbak*;=6BH1NHFk1vpp^Dbhz5jv3UzH>7YB&*5Pt>; zUug88prwNV`KKXT8shbsgoNln3+TiIk{a?SMIK74UL3dxP$vw@cV&@y10h;Qy&MFD z!~j};vkEf_o0bj%zWT3?h?*H{kc&XKe8|kW8S8q0{*LoLAJMY)pJX{ZMqv4{w@+Xf zy~jfurRNy+M5;opDqYwFDM9=yWTi#063}=ID`ZO)D_uU`d>kt&_EjXS zmphV`>QQ|xE8VfUv1nGxPPmJT%%q4`0=aBVE79vPo)riO5v_u(%|x`45`R)-S}Cvw zasq!f(R`49T~H@!n3NzsLMk7(K0-R7#4xB6^1>)S$gK zE_~+I?(>4aCu~g*!$ASu2?(XYi6Nn6$lGX8D5>QQhlN6!(GK@2G}J~zLy1n^U~njr z>5B~ym8x*BLPV`QM3hxFgmclPO@@gQnchgCs6@Sb6)Y-MH8vP7N`EABlL4a=mE%>= zsC5L5s;+qY@RYK?o^-9}P+o0^R;FQo`oafux!S+DX_g zS}3Sj;dWLDw_q>+wL~Mx8b-hn9+!xLA$ZLr)Ck(@ z;V^{EOA!#`_GsSA8yUmjij}W~_!xlmBV=d>Vr1OL02!O=r8I|s)je-#_k5{i*KD?j z-p(jCsahXzqeJ~%LTf@NdL>m+3Z&xf$tK2~>1{6B)yGml9)J7Q!SgAqcxnFlKFzpK z{<^*L{gXF`YPO5jhv($}C7nJi^l5&6!K;w6L2-V$z~w(EZ9V?}oA&q**sgT5EG9a1 zI&>=hbQ?`?Cu+-eRSb0q04sKRB>_#6gn#8byka~01Z_$?MuAE>5HXFC zeB2T7zM)t3g+jT~Z;DjrfG8A~MZBb{J~Sp5lRQ9H5xi1>-1U1&u`61fZ6O;3Z z-#)V9Z%mU{H}~&IQs}vlyd>9s0~`0D<(N|E2Y@r#>{8PPj2_)jd!9 z{o|uU-+$BoqBj7n?`i*VZ{PEs2b}Ld-^?Z6im%=$zNdv;;}p-T95{Jljf+Bpv&$ei z#oyh+!{Hr=NGJJXmv^g#!l#G1>(USX-jTOS$9GdiqUXDT=4~dI!5YtJ09@z#ZW<`T z)!7hB^mVq4ha%wqj#5KMXJkg*oRMAc0S^$kIDaECbZ|x{&yMj`og*V-u^YSt*tRX_ zIl=>GSx^koU1xmdE5Au!d81c1=PbWa%hJ8&&Gc4OOlPm9 zkNj$?_R33sc`x~NkHRL?@|d(X?o^oUW%g3L6`Iyux67F!8}&Iuc3iTrTnjhhT6h}; zef!)C4QI*zg$CYpN

    p@57TfAs4g%V7Z+(J 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 z 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