From 14ca38bc8b2bf03f332178757dece4b04063f85b Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sat, 25 Mar 2017 07:31:47 +0100 Subject: [PATCH 01/14] Progress --- Moose Development/Moose/AI/AI_Cargo.lua | 18 +- .../Moose/Tasking/Task_CARGO.lua | 272 ++++++++++++++++++ 2 files changed, 288 insertions(+), 2 deletions(-) create mode 100644 Moose Development/Moose/Tasking/Task_CARGO.lua diff --git a/Moose Development/Moose/AI/AI_Cargo.lua b/Moose Development/Moose/AI/AI_Cargo.lua index 2172045f7..6d44c87fa 100644 --- a/Moose Development/Moose/AI/AI_Cargo.lua +++ b/Moose Development/Moose/AI/AI_Cargo.lua @@ -244,8 +244,8 @@ function AI_CARGO:New( Type, Name, Weight, ReportRadius, NearRadius ) self.Type = Type self.Name = Name self.Weight = Weight - self.ReportRadius = ReportRadius - self.NearRadius = NearRadius + self.ReportRadius = ReportRadius or 1000 + self.NearRadius = NearRadius or 200 self.CargoObject = nil self.CargoCarrier = nil self.Representable = false @@ -288,6 +288,20 @@ function AI_CARGO:IsNear( PointVec2 ) end end +--- Get the current PointVec2 of the cargo. +-- @param #AI_CARGO self +-- @return Core.Point#POINT_VEC2 +function AI_CARGO:GetPointVec2() + return self.CargoObject:GetPointVec2() +end + +--- Get the range till cargo will board. +-- @param #AI_CARGO self +-- @return #number The range till cargo will board. +function AI_CARGO:GetBoardingRange() + return self.NearRadius +end + end do -- AI_CARGO_REPRESENTABLE diff --git a/Moose Development/Moose/Tasking/Task_CARGO.lua b/Moose Development/Moose/Tasking/Task_CARGO.lua new file mode 100644 index 000000000..b81827fd5 --- /dev/null +++ b/Moose Development/Moose/Tasking/Task_CARGO.lua @@ -0,0 +1,272 @@ +--- **Tasking** - The TASK_CARGO models tasks for players to transport @{Cargo}. +-- +-- ![Banner Image]() +-- +-- +-- +-- ==== +-- +-- # **API CHANGE HISTORY** +-- +-- The underlying change log documents the API changes. Please read this carefully. The following notation is used: +-- +-- * **Added** parts are expressed in bold type face. +-- * _Removed_ parts are expressed in italic type face. +-- +-- Hereby the change log: +-- +-- 2017-03-09: Revised version. +-- +-- === +-- +-- # **AUTHORS and CONTRIBUTIONS** +-- +-- ### Contributions: +-- +-- * **[WingThor]**: Concept, Advice & Testing. +-- +-- ### Authors: +-- +-- * **FlightControl**: Concept, Design & Programming. +-- +-- @module Task_A2G + +do -- TASK_CARGO + + --- @type TASK_CARGO + -- @field Set#SET_UNIT TargetSetUnit + -- @extends Tasking.Task#TASK + + --- + -- # TASK_CARGO class, extends @{Task#TASK} + -- + -- The TASK_CARGO class defines @{Cargo} transport tasks, + -- based on the tasking capabilities defined in @{Task#TASK}. + -- The TASK_CARGO is implemented using a @{Statemachine#FSM_TASK}, and has the following statuses: + -- + -- * **None**: Start of the process. + -- * **Planned**: The cargo task is planned. + -- * **Assigned**: The cargo task is assigned to a @{Group#GROUP}. + -- * **Success**: The cargo task is successfully completed. + -- * **Failed**: The cargo task has failed. This will happen if the player exists the task early, without communicating a possible cancellation to HQ. + -- + -- # 1.1) Set the scoring of achievements in a cargo task. + -- + -- Scoring or penalties can be given in the following circumstances: + -- + -- === + -- + -- @field #TASK_CARGO TASK_CARGO + -- + TASK_CARGO = { + ClassName = "TASK_CARGO", + } + + --- Instantiates a new TASK_CARGO. + -- @param #TASK_CARGO self + -- @param Tasking.Mission#MISSION Mission + -- @param Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. + -- @param #string TaskName The name of the Task. + -- @param AI.AI_Cargo#AI_CARGO Cargo The cargo. + -- @param #string TaskType The type of Cargo task. + -- @return #TASK_CARGO self + function TASK_CARGO:New( Mission, SetGroup, TaskName, Cargo, TaskType ) + local self = BASE:Inherit( self, TASK:New( Mission, SetGroup, TaskName, TaskType ) ) -- Tasking.Task#TASK_CARGO + self:F() + + self.Cargo = Cargo + self.TaskType = TaskType + + Mission:AddTask( self ) + + local Fsm = self:GetUnitProcess() + + + Fsm:AddProcess ( "Planned", "Accept", ACT_ASSIGN_ACCEPT:New( self.TaskBriefing ), { Assigned = "RouteToCargo", Rejected = "Reject" } ) + + Fsm:AddTransition( "Assigned", "RouteToCargo", "RoutingToCargo" ) + Fsm:AddProcess ( "RoutingToCargo", "RouteToCargoPickup", ACT_ROUTE_POINT:New(), { Arrived = "ArriveAtCargo" } ) + + Fsm:AddTransition( { "Arrived", "RoutingToCargo" }, "ArriveAtCargo", "ArrivedAtCargo" ) + + Fsm:AddTransition( { "ArrivedAtCargo", "LandAtCargo" }, "Land", "Landing" ) + Fsm:AddTransition( { "Landing", "Landed", "Landed" } ) + Fsm:AddTransition( { "OnGround", "PrepareBoarding", "AwaitBoarding" } ) + Fsm:AddTransition( { "AwaitBoarding", "Board", "Boarding" } ) + Fsm:AddTransition( { "Boarding", "Boarded", "Boarded" } ) + + Fsm:AddTransition( "Accounted", "DestroyedAll", "Accounted" ) + Fsm:AddTransition( "Accounted", "Success", "Success" ) + Fsm:AddTransition( "Rejected", "Reject", "Aborted" ) + Fsm:AddTransition( "Failed", "Fail", "Failed" ) + + + --- Route to Cargo + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_CARGO#TASK_CARGO Task + function Fsm:onafterRouteToCargo( TaskUnit, Task ) + self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + + + Task:SetCargoPickup( Task.Cargo, TaskUnit ) + self:__RouteToCargoPickup( 0.1 ) + end + + --- + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_CARGO#TASK_CARGO Task + function Fsm:OnAfterArriveAtCargo( TaskUnit, Task ) + self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + + self:__Land( 0.1 ) + end + + return self + + end + + --- @param #TASK_CARGO self + function TASK_CARGO:GetPlannedMenuText() + return self:GetStateString() .. " - " .. self:GetTaskName() .. " ( " .. self.TargetSetUnit:GetUnitTypesText() .. " )" + end + + --- @param #TASK_CARGO self + -- @param AI.AI_Cargo#AI_CARGO Cargo The cargo. + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return #TASK_CARGO + function TASK_CARGO:SetCargoPickup( Cargo, TaskUnit ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + local ActRouteCargo = ProcessUnit:GetProcess( "RoutingToCargo", "RouteToCargoPickup" ) -- Actions.Act_Route#ACT_ROUTE_POINT + ActRouteCargo:SetPointVec2( Cargo:GetPointVec2() ) + ActRouteCargo:SetRange( Cargo:GetBoardingRange() ) + return self + end + + --- @param #TASK_CARGO self + -- @param Core.Point#POINT_VEC2 TargetPointVec2 The PointVec2 object where the Target is located on the map. + -- @param Wrapper.Unit#UNIT TaskUnit + function TASK_CARGO:SetTargetPointVec2( TargetPointVec2, TaskUnit ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT + ActRouteTarget:SetPointVec2( TargetPointVec2 ) + end + + + --- @param #TASK_CARGO self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return Core.Point#POINT_VEC2 The PointVec2 object where the Target is located on the map. + function TASK_CARGO:GetTargetPointVec2( TaskUnit ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT + return ActRouteTarget:GetPointVec2() + end + + + --- @param #TASK_CARGO self + -- @param Core.Zone#ZONE_BASE TargetZone The Zone object where the Target is located on the map. + -- @param Wrapper.Unit#UNIT TaskUnit + function TASK_CARGO:SetTargetZone( TargetZone, TaskUnit ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE + ActRouteTarget:SetZone( TargetZone ) + end + + + --- @param #TASK_CARGO self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return Core.Zone#ZONE_BASE The Zone object where the Target is located on the map. + function TASK_CARGO:GetTargetZone( TaskUnit ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE + return ActRouteTarget:GetZone() + end + + --- Set a score when a target in scope of the A2G attack, has been destroyed . + -- @param #TASK_CARGO self + -- @param #string Text The text to display to the player, when the target has been destroyed. + -- @param #number Score The score in points. + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return #TASK_CARGO + function TASK_CARGO:SetScoreOnDestroy( Text, Score, TaskUnit ) + self:F( { Text, Score, TaskUnit } ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + ProcessUnit:AddScoreProcess( "Engaging", "Account", "Account", Text, Score ) + + return self + end + + --- Set a score when all the targets in scope of the A2G attack, have been destroyed. + -- @param #TASK_CARGO self + -- @param #string Text The text to display to the player, when all targets hav been destroyed. + -- @param #number Score The score in points. + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return #TASK_CARGO + function TASK_CARGO:SetScoreOnSuccess( Text, Score, TaskUnit ) + self:F( { Text, Score, TaskUnit } ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + ProcessUnit:AddScore( "Success", Text, Score ) + + return self + end + + --- Set a penalty when the A2G attack has failed. + -- @param #TASK_CARGO self + -- @param #string Text The text to display to the player, when the A2G attack has failed. + -- @param #number Penalty The penalty in points. + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return #TASK_CARGO + function TASK_CARGO:SetPenaltyOnFailed( Text, Penalty, TaskUnit ) + self:F( { Text, Score, TaskUnit } ) + + local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + ProcessUnit:AddScore( "Failed", Text, Penalty ) + + return self + end + + +end + + +do -- TASK_CARGO_TRANSPORT + + --- The TASK_CARGO_TRANSPORT class + -- @type TASK_CARGO_TRANSPORT + -- @extends #TASK_CARGO + TASK_CARGO_TRANSPORT = { + ClassName = "TASK_CARGO_TRANSPORT", + } + + --- Instantiates a new TASK_CARGO_TRANSPORT. + -- @param #TASK_CARGO_TRANSPORT self + -- @param Tasking.Mission#MISSION Mission + -- @param Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. + -- @param #string TaskName The name of the Task. + -- @param AI_Cargo#AI_CARGO Cargo The cargo. + -- @return #TASK_CARGO_TRANSPORT self + function TASK_CARGO_TRANSPORT:New( Mission, SetGroup, TaskName, Cargo ) + local self = BASE:Inherit( self, TASK_CARGO:New( Mission, SetGroup, TaskName, Cargo, "TRANSPORT" ) ) -- #TASK_CARGO_TRANSPORT + self:F() + + return self + end + +end + From 09c05057ae64b49777ca4162a603d3d33084bf84 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sat, 25 Mar 2017 07:56:37 +0100 Subject: [PATCH 02/14] Progress --- Moose Development/Moose/Moose.lua | 1 + .../Moose/Tasking/Task_CARGO.lua | 8 +- .../l10n/DEFAULT/Moose.lua | 35837 +--------------- Moose Mission Setup/Moose.lua | 35837 +--------------- ...- A2G Task Dispatching DETECTION_AREAS.lua | 2 +- .../TSK-020 - Task Modelling - Pickup.lua | 125 - .../TSK-020 - Task Modelling - Pickup.miz | Bin 255645 -> 0 bytes .../TSK-100 - Cargo Pickup.lua | 33 + .../TSK-100 - Cargo Pickup.miz | Bin 0 -> 27419 bytes 9 files changed, 73 insertions(+), 71770 deletions(-) delete mode 100644 Moose Test Missions/TSK - Task Modelling/TSK-020 - Task Modelling - Pickup/TSK-020 - Task Modelling - Pickup.lua delete mode 100644 Moose Test Missions/TSK - Task Modelling/TSK-020 - Task Modelling - Pickup/TSK-020 - Task Modelling - Pickup.miz create mode 100644 Moose Test Missions/TSK - Task Modelling/TSK-100 - Cargo Pickup/TSK-100 - Cargo Pickup.lua create mode 100644 Moose Test Missions/TSK - Task Modelling/TSK-100 - Cargo Pickup/TSK-100 - Cargo Pickup.miz diff --git a/Moose Development/Moose/Moose.lua b/Moose Development/Moose/Moose.lua index fe1d1e2bf..0312878a0 100644 --- a/Moose Development/Moose/Moose.lua +++ b/Moose Development/Moose/Moose.lua @@ -62,6 +62,7 @@ Include.File( "Tasking/Task" ) Include.File( "Tasking/DetectionManager" ) Include.File( "Tasking/Task_A2G_Dispatcher") Include.File( "Tasking/Task_A2G" ) +Include.File( "Tasking/Task_CARGO" ) -- The order of the declarations is important here. Don't touch it. diff --git a/Moose Development/Moose/Tasking/Task_CARGO.lua b/Moose Development/Moose/Tasking/Task_CARGO.lua index b81827fd5..0033a1cfd 100644 --- a/Moose Development/Moose/Tasking/Task_CARGO.lua +++ b/Moose Development/Moose/Tasking/Task_CARGO.lua @@ -90,10 +90,10 @@ do -- TASK_CARGO Fsm:AddTransition( { "Arrived", "RoutingToCargo" }, "ArriveAtCargo", "ArrivedAtCargo" ) Fsm:AddTransition( { "ArrivedAtCargo", "LandAtCargo" }, "Land", "Landing" ) - Fsm:AddTransition( { "Landing", "Landed", "Landed" } ) - Fsm:AddTransition( { "OnGround", "PrepareBoarding", "AwaitBoarding" } ) - Fsm:AddTransition( { "AwaitBoarding", "Board", "Boarding" } ) - Fsm:AddTransition( { "Boarding", "Boarded", "Boarded" } ) + Fsm:AddTransition( "Landing", "Landed", "Landed" ) + Fsm:AddTransition( "OnGround", "PrepareBoarding", "AwaitBoarding" ) + Fsm:AddTransition( "AwaitBoarding", "Board", "Boarding" ) + Fsm:AddTransition( "Boarding", "Boarded", "Boarded" ) Fsm:AddTransition( "Accounted", "DestroyedAll", "Accounted" ) Fsm:AddTransition( "Accounted", "Success", "Success" ) 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 9aca34abf..4db9338c3 100644 --- a/Moose Mission Setup/Moose Mission Update/l10n/DEFAULT/Moose.lua +++ b/Moose Mission Setup/Moose Mission Update/l10n/DEFAULT/Moose.lua @@ -1,35834 +1,31 @@ -env.info( '*** MOOSE STATIC INCLUDE START *** ' ) -env.info( 'Moose Generation Timestamp: 20170325_0623' ) +env.info( '*** MOOSE DYNAMIC INCLUDE START *** ' ) +env.info( 'Moose Generation Timestamp: 20170325_0745' ) + 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 ---- **Core** - BASE forms **the basis of the MOOSE framework**. Each class within the MOOSE framework derives from BASE. --- --- ![Banner Image](..\Presentations\BASE\Dia1.JPG) --- --- === --- --- The @{#BASE} class is the core root class from where every other class in moose is derived. --- --- === --- --- # **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 - ---- @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. - ---- # 1) #BASE class --- --- All classes within the MOOSE framework are derived from the BASE class. --- --- BASE provides facilities for : --- --- * The construction and inheritance of MOOSE classes. --- * The class naming and numbering system. --- * The class hierarchy search system. --- * The tracing of information or objects during mission execution for debuggin purposes. --- * The subscription to DCS events for event handling in MOOSE objects. --- --- Note: The BASE class is an abstract class and is not meant to be used directly. --- --- ## 1.1) BASE constructor --- --- Any class derived from BASE, will use the @{Base#BASE.New} constructor embedded in the @{Base#BASE.Inherit} method. --- See an example at the @{Base#BASE.New} method how this is done. --- --- ## 1.2) Trace information for debugging --- --- The BASE class contains trace methods to trace progress within a mission execution of a certain object. --- These trace methods are inherited by each MOOSE class interiting BASE, soeach object created from derived class from BASE can use the tracing methods to trace its execution. --- --- Any type of information can be passed to these tracing methods. See the following examples: --- --- self:E( "Hello" ) --- --- Result in the word "Hello" in the dcs.log. --- --- local Array = { 1, nil, "h", { "a","b" }, "x" } --- self:E( Array ) --- --- Results with the text [1]=1,[3]="h",[4]={[1]="a",[2]="b"},[5]="x"} in the dcs.log. --- --- local Object1 = "Object1" --- local Object2 = 3 --- local Object3 = { Object 1, Object 2 } --- self:E( { Object1, Object2, Object3 } ) --- --- Results with the text [1]={[1]="Object",[2]=3,[3]={[1]="Object",[2]=3}} in the dcs.log. --- --- local SpawnObject = SPAWN:New( "Plane" ) --- local GroupObject = GROUP:FindByName( "Group" ) --- self:E( { Spawn = SpawnObject, Group = GroupObject } ) --- --- Results with the text [1]={Spawn={....),Group={...}} in the dcs.log. --- --- Below a more detailed explanation of the different method types for tracing. --- --- ### 1.2.1) Tracing methods categories --- --- There are basically 3 types of tracing methods available: --- --- * @{#BASE.F}: Used to trace the entrance of a function and its given parameters. An F is indicated at column 44 in the DCS.log file. --- * @{#BASE.T}: Used to trace further logic within a function giving optional variables or parameters. A T is indicated at column 44 in the DCS.log file. --- * @{#BASE.E}: Used to always trace information giving optional variables or parameters. An E is indicated at column 44 in the DCS.log file. --- --- ### 1.2.2) Tracing levels --- --- There are 3 tracing levels within MOOSE. --- These tracing levels were defined to avoid bulks of tracing to be generated by lots of objects. --- --- As such, the F and T methods have additional variants to trace level 2 and 3 respectively: --- --- * @{#BASE.F2}: Trace the beginning of a function and its given parameters with tracing level 2. --- * @{#BASE.F3}: Trace the beginning of a function and its given parameters with tracing level 3. --- * @{#BASE.T2}: Trace further logic within a function giving optional variables or parameters with tracing level 2. --- * @{#BASE.T3}: Trace further logic within a function giving optional variables or parameters with tracing level 3. --- --- ### 1.2.3) Trace activation. --- --- Tracing can be activated in several ways: --- --- * Switch tracing on or off through the @{#BASE.TraceOnOff}() method. --- * Activate all tracing through the @{#BASE.TraceAll}() method. --- * Activate only the tracing of a certain class (name) through the @{#BASE.TraceClass}() method. --- * Activate only the tracing of a certain method of a certain class through the @{#BASE.TraceClassMethod}() method. --- * Activate only the tracing of a certain level through the @{#BASE.TraceLevel}() method. --- --- ### 1.2.4) Check if tracing is on. --- --- The method @{#BASE.IsTrace}() will validate if tracing is activated or not. --- --- ## 1.3 DCS simulator Event Handling --- --- The BASE class provides methods to catch DCS Events. These are events that are triggered from within the DCS simulator, --- and handled through lua scripting. MOOSE provides an encapsulation to handle these events more efficiently. --- --- ### 1.3.1 Subscribe / Unsubscribe to DCS Events --- --- At first, the mission designer will need to **Subscribe** to a specific DCS event for the class. --- So, when the DCS event occurs, the class will be notified of that event. --- There are two methods which you use to subscribe to or unsubscribe from an event. --- --- * @{#BASE.HandleEvent}(): Subscribe to a DCS Event. --- * @{#BASE.UnHandleEvent}(): Unsubscribe from a DCS Event. --- --- ### 1.3.2 Event Handling of DCS Events --- --- Once the class is subscribed to the event, an **Event Handling** method on the object or class needs to be written that will be called --- when the DCS event occurs. The Event Handling method receives an @{Event#EVENTDATA} structure, which contains a lot of information --- about the event that occurred. --- --- Find below an example of the prototype how to write an event handling function for two units: --- --- local Tank1 = UNIT:FindByName( "Tank A" ) --- local Tank2 = UNIT:FindByName( "Tank B" ) --- --- -- Here we subscribe to the Dead events. So, if one of these tanks dies, the Tank1 or Tank2 objects will be notified. --- Tank1:HandleEvent( EVENTS.Dead ) --- Tank2:HandleEvent( EVENTS.Dead ) --- --- --- This function is an Event Handling function that will be called when Tank1 is Dead. --- -- @param Wrapper.Unit#UNIT self --- -- @param Core.Event#EVENTDATA EventData --- function Tank1:OnEventDead( EventData ) --- --- self:SmokeGreen() --- end --- --- --- This function is an Event Handling function that will be called when Tank2 is Dead. --- -- @param Wrapper.Unit#UNIT self --- -- @param Core.Event#EVENTDATA EventData --- function Tank2:OnEventDead( EventData ) --- --- self:SmokeBlue() --- end --- --- --- --- See the @{Event} module for more information about event handling. --- --- ## 1.4) Class identification methods --- --- BASE provides methods to get more information of each object: --- --- * @{#BASE.GetClassID}(): Gets the ID (number) of the object. Each object created is assigned a number, that is incremented by one. --- * @{#BASE.GetClassName}(): Gets the name of the object, which is the name of the class the object was instantiated from. --- * @{#BASE.GetClassNameAndID}(): Gets the name and ID of the object. --- --- ## 1.5) All objects derived from BASE can have "States" --- --- A mechanism is in place in MOOSE, that allows to let the objects administer **states**. --- States are essentially properties of objects, which are identified by a **Key** and a **Value**. --- --- The method @{#BASE.SetState}() can be used to set a Value with a reference Key to the object. --- To **read or retrieve** a state Value based on a Key, use the @{#BASE.GetState} method. --- --- These two methods provide a very handy way to keep state at long lasting processes. --- Values can be stored within the objects, and later retrieved or changed when needed. --- There is one other important thing to note, the @{#BASE.SetState}() and @{#BASE.GetState} methods --- receive as the **first parameter the object for which the state needs to be set**. --- Thus, if the state is to be set for the same object as the object for which the method is used, then provide the same --- object name to the method. --- --- ## 1.10) Inheritance --- --- The following methods are available to implement 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. --- --- === --- --- @field #BASE BASE --- -BASE = { - ClassName = "BASE", - ClassID = 0, - _Private = {}, - Events = {}, - States = {} -} - ---- The Formation Class --- @type FORMATION --- @field Cone A cone formation. -FORMATION = { - Cone = "Cone" -} - - - ---- BASE constructor. --- --- This is an example how to use the BASE:New() constructor in a new class definition when inheriting from BASE. --- --- function EVENT:New() --- local self = BASE:Inherit( self, BASE:New() ) -- #EVENT --- return self --- end --- --- @param #BASE self --- @return #BASE -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 - - --- THIS IS WHY WE NEED LUA 5.2 ... -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. --- Note that the Parent class must be passed to call the parent class method. --- --- self:GetParent(self):ParentMethod() --- --- --- @param #BASE self --- @param #BASE Child is the Child class from which the Parent class needs to be retrieved. --- @return #BASE -function BASE:GetParent( Child ) - local Parent = getmetatable( Child ) --- env.info('Inherited class of ' .. Child.ClassName .. ' is ' .. Parent.ClassName ) - return Parent -end - ---- Get the ClassName + ClassID of the class instance. --- The ClassName + ClassID is formatted as '%s#%09d'. --- @param #BASE self --- @return #string The ClassName + ClassID of the class instance. -function BASE:GetClassNameAndID() - return string.format( '%s#%09d', self.ClassName, self.ClassID ) -end - ---- Get the ClassName of the class instance. --- @param #BASE self --- @return #string The ClassName of the class instance. -function BASE:GetClassName() - return self.ClassName -end - ---- Get the ClassID of the class instance. --- @param #BASE self --- @return #string The ClassID of the class instance. -function BASE:GetClassID() - return self.ClassID -end - -do -- Event Handling - - --- Returns the event dispatcher - -- @param #BASE self - -- @return Core.Event#EVENT - function BASE:EventDispatcher() - - return _EVENTDISPATCHER - end - - - --- Get the Class @{Event} processing Priority. - -- The Event processing Priority is a number from 1 to 10, - -- reflecting the order of the classes subscribed to the Event to be processed. - -- @param #BASE self - -- @return #number The @{Event} processing Priority. - function BASE:GetEventPriority() - return self._Private.EventPriority or 5 - end - - --- Set the Class @{Event} processing Priority. - -- The Event processing Priority is a number from 1 to 10, - -- reflecting the order of the classes subscribed to the Event to be processed. - -- @param #BASE self - -- @param #number EventPriority The @{Event} processing Priority. - -- @return self - function BASE:SetEventPriority( EventPriority ) - self._Private.EventPriority = EventPriority - end - - --- Remove all subscribed events - -- @param #BASE self - -- @return #BASE - function BASE:EventRemoveAll() - - self:EventDispatcher():RemoveAll( self ) - - return self - end - - --- Subscribe to a DCS Event. - -- @param #BASE self - -- @param Core.Event#EVENTS Event - -- @param #function EventFunction (optional) The function to be called when the event occurs for the unit. - -- @return #BASE - function BASE:HandleEvent( Event, EventFunction ) - - self:EventDispatcher():OnEventGeneric( EventFunction, self, Event ) - - return self - end - - --- UnSubscribe to a DCS event. - -- @param #BASE self - -- @param Core.Event#EVENTS Event - -- @return #BASE - function BASE:UnHandleEvent( Event ) - - self:EventDispatcher():Remove( self, Event ) - - return self - end - - -- Event handling function prototypes - - --- Occurs whenever any unit in a mission fires a weapon. But not any machine gun or autocannon based weapon, those are handled by EVENT.ShootingStart. - -- @function [parent=#BASE] OnEventShot - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs whenever an object is hit by a weapon. - -- initiator : The unit object the fired the weapon - -- weapon: Weapon object that hit the target - -- target: The Object that was hit. - -- @function [parent=#BASE] OnEventHit - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when an aircraft takes off from an airbase, farp, or ship. - -- initiator : The unit that tookoff - -- place: Object from where the AI took-off from. Can be an Airbase Object, FARP, or Ships - -- @function [parent=#BASE] OnEventTakeoff - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when an aircraft lands at an airbase, farp or ship - -- initiator : The unit that has landed - -- place: Object that the unit landed on. Can be an Airbase Object, FARP, or Ships - -- @function [parent=#BASE] OnEventLand - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any aircraft crashes into the ground and is completely destroyed. - -- initiator : The unit that has crashed - -- @function [parent=#BASE] OnEventCrash - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when a pilot ejects from an aircraft - -- initiator : The unit that has ejected - -- @function [parent=#BASE] OnEventEjection - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when an aircraft connects with a tanker and begins taking on fuel. - -- initiator : The unit that is receiving fuel. - -- @function [parent=#BASE] OnEventRefueling - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when an object is completely destroyed. - -- initiator : The unit that is was destroyed. - -- @function [parent=#BASE] OnEvent - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when the pilot of an aircraft is killed. Can occur either if the player is alive and crashes or if a weapon kills the pilot without completely destroying the plane. - -- initiator : The unit that the pilot has died in. - -- @function [parent=#BASE] OnEventPilotDead - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when a ground unit captures either an airbase or a farp. - -- initiator : The unit that captured the base - -- place: The airbase that was captured, can be a FARP or Airbase. When calling place:getCoalition() the faction will already be the new owning faction. - -- @function [parent=#BASE] OnEventBaseCaptured - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when a mission starts - -- @function [parent=#BASE] OnEventMissionStart - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when a mission ends - -- @function [parent=#BASE] OnEventMissionEnd - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when an aircraft is finished taking fuel. - -- initiator : The unit that was receiving fuel. - -- @function [parent=#BASE] OnEventRefuelingStop - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any object is spawned into the mission. - -- initiator : The unit that was spawned - -- @function [parent=#BASE] OnEventBirth - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any system fails on a human controlled aircraft. - -- initiator : The unit that had the failure - -- @function [parent=#BASE] OnEventHumanFailure - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any aircraft starts its engines. - -- initiator : The unit that is starting its engines. - -- @function [parent=#BASE] OnEventEngineStartup - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any aircraft shuts down its engines. - -- initiator : The unit that is stopping its engines. - -- @function [parent=#BASE] OnEventEngineShutdown - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any player assumes direct control of a unit. - -- initiator : The unit that is being taken control of. - -- @function [parent=#BASE] OnEventPlayerEnterUnit - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any player relieves control of a unit to the AI. - -- initiator : The unit that the player left. - -- @function [parent=#BASE] OnEventPlayerLeaveUnit - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any unit begins firing a weapon that has a high rate of fire. Most common with aircraft cannons (GAU-8), autocannons, and machine guns. - -- initiator : The unit that is doing the shooing. - -- target: The unit that is being targeted. - -- @function [parent=#BASE] OnEventShootingStart - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any unit stops firing its weapon. Event will always correspond with a shooting start event. - -- initiator : The unit that was doing the shooing. - -- @function [parent=#BASE] OnEventShootingEnd - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - -end - - ---- Creation of a Birth Event. --- @param #BASE self --- @param Dcs.DCSTypes#Time EventTime The time stamp of the event. --- @param Dcs.DCSWrapper.Object#Object Initiator The initiating object of the event. --- @param #string IniUnitName The initiating unit name. --- @param place --- @param subplace -function BASE:CreateEventBirth( EventTime, Initiator, IniUnitName, place, subplace ) - self:F( { EventTime, Initiator, IniUnitName, place, subplace } ) - - local Event = { - id = world.event.S_EVENT_BIRTH, - time = EventTime, - initiator = Initiator, - IniUnitName = IniUnitName, - place = place, - subplace = subplace - } - - world.onEvent( Event ) -end - ---- Creation of a Crash Event. --- @param #BASE self --- @param Dcs.DCSTypes#Time EventTime The time stamp of the event. --- @param Dcs.DCSWrapper.Object#Object Initiator The initiating object of the event. -function BASE:CreateEventCrash( EventTime, Initiator ) - self:F( { EventTime, Initiator } ) - - local Event = { - id = world.event.S_EVENT_CRASH, - time = EventTime, - initiator = Initiator, - } - - world.onEvent( Event ) -end - --- TODO: Complete Dcs.DCSTypes#Event structure. ---- The main event handling function... This function captures all events generated for the class. --- @param #BASE self --- @param Dcs.DCSTypes#Event event -function BASE:onEvent(event) - --self:F( { BaseEventCodes[event.id], event } ) - - if self then - for EventID, EventObject in pairs( self.Events ) do - if EventObject.EventEnabled then - --env.info( 'onEvent Table EventObject.Self = ' .. tostring(EventObject.Self) ) - --env.info( 'onEvent event.id = ' .. tostring(event.id) ) - --env.info( 'onEvent EventObject.Event = ' .. tostring(EventObject.Event) ) - if event.id == EventObject.Event then - if self == EventObject.Self then - if event.initiator and event.initiator:isExist() then - event.IniUnitName = event.initiator:getName() - end - if event.target and event.target:isExist() then - event.TgtUnitName = event.target:getName() - end - --self:T( { BaseEventCodes[event.id], event } ) - --EventObject.EventFunction( self, event ) - end - end - end - end - end -end - ---- Set a state or property of the Object given a Key and a Value. --- Note that if the Object is destroyed, nillified or garbage collected, then the Values and Keys will also be gone. --- @param #BASE self --- @param Object The object that will hold the Value set by the Key. --- @param Key The key that is used as a reference of the value. Note that the key can be a #string, but it can also be any other type! --- @param Value The value to is stored in the object. --- @return The Value set. --- @return #nil The Key was not found and thus the Value could not be retrieved. -function BASE:SetState( Object, Key, Value ) - - local ClassNameAndID = Object:GetClassNameAndID() - - self.States[ClassNameAndID] = self.States[ClassNameAndID] or {} - self.States[ClassNameAndID][Key] = Value - self:T2( { ClassNameAndID, Key, Value } ) - - return self.States[ClassNameAndID][Key] -end - - ---- Get a Value given a Key from the Object. --- Note that if the Object is destroyed, nillified or garbage collected, then the Values and Keys will also be gone. --- @param #BASE self --- @param Object The object that holds the Value set by the Key. --- @param Key The key that is used to retrieve the value. Note that the key can be a #string, but it can also be any other type! --- @param Value The value to is stored in the Object. --- @return The Value retrieved. -function BASE:GetState( Object, Key ) - - local ClassNameAndID = Object:GetClassNameAndID() - - if self.States[ClassNameAndID] then - local Value = self.States[ClassNameAndID][Key] or false - self:T2( { ClassNameAndID, Key, Value } ) - return Value - end - - return nil -end - -function BASE:ClearState( Object, StateName ) - - local ClassNameAndID = Object:GetClassNameAndID() - if self.States[ClassNameAndID] then - self.States[ClassNameAndID][StateName] = nil - end -end - --- Trace section - --- Log a trace (only shown when trace is on) --- TODO: Make trace function using variable parameters. - ---- Set trace on or off --- Note that when trace is off, no debug statement is performed, increasing performance! --- When Moose is loaded statically, (as one file), tracing is switched off by default. --- So tracing must be switched on manually in your mission if you are using Moose statically. --- When moose is loading dynamically (for moose class development), tracing is switched on by default. --- @param #BASE self --- @param #boolean TraceOnOff Switch the tracing on or off. --- @usage --- -- Switch the tracing On --- BASE:TraceOnOff( true ) --- --- -- Switch the tracing Off --- BASE:TraceOnOff( false ) -function BASE:TraceOnOff( TraceOnOff ) - _TraceOnOff = TraceOnOff -end - - ---- Enquires if tracing is on (for the class). --- @param #BASE self --- @return #boolean -function BASE:IsTrace() - - if debug and ( _TraceAll == true ) or ( _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName] ) then - return true - else - return false - end -end - ---- Set trace level --- @param #BASE self --- @param #number Level -function BASE:TraceLevel( Level ) - _TraceLevel = Level - self:E( "Tracing level " .. Level ) -end - ---- Trace all methods in MOOSE --- @param #BASE self --- @param #boolean TraceAll true = trace all methods in MOOSE. -function BASE:TraceAll( TraceAll ) - - _TraceAll = TraceAll - - if _TraceAll then - self:E( "Tracing all methods in MOOSE " ) - else - self:E( "Switched off tracing all methods in MOOSE" ) - end -end - ---- Set tracing for a class --- @param #BASE self --- @param #string Class -function BASE:TraceClass( Class ) - _TraceClass[Class] = true - _TraceClassMethod[Class] = {} - self:E( "Tracing class " .. Class ) -end - ---- Set tracing for a specific method of class --- @param #BASE self --- @param #string Class --- @param #string Method -function BASE:TraceClassMethod( Class, Method ) - if not _TraceClassMethod[Class] then - _TraceClassMethod[Class] = {} - _TraceClassMethod[Class].Method = {} - end - _TraceClassMethod[Class].Method[Method] = true - self:E( "Tracing method " .. Method .. " of class " .. Class ) -end - ---- Trace a function call. This function is private. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:_F( Arguments, DebugInfoCurrentParam, DebugInfoFromParam ) - - if debug and ( _TraceAll == true ) or ( _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName] ) then - - local DebugInfoCurrent = DebugInfoCurrentParam and DebugInfoCurrentParam or debug.getinfo( 2, "nl" ) - local DebugInfoFrom = DebugInfoFromParam and DebugInfoFromParam or debug.getinfo( 3, "l" ) - - local Function = "function" - if DebugInfoCurrent.name then - Function = DebugInfoCurrent.name - end - - if _TraceAll == true or _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName].Method[Function] then - local LineCurrent = 0 - if DebugInfoCurrent.currentline then - LineCurrent = DebugInfoCurrent.currentline - end - local LineFrom = 0 - if DebugInfoFrom then - LineFrom = DebugInfoFrom.currentline - end - env.info( string.format( "%6d(%6d)/%1s:%20s%05d.%s(%s)" , LineCurrent, LineFrom, "F", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) - end - end -end - ---- Trace a function call. Must be at the beginning of the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:F( Arguments ) - - if debug and _TraceOnOff then - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - if _TraceLevel >= 1 then - self:_F( Arguments, DebugInfoCurrent, DebugInfoFrom ) - end - end -end - - ---- Trace a function call level 2. Must be at the beginning of the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:F2( Arguments ) - - if debug and _TraceOnOff then - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - if _TraceLevel >= 2 then - self:_F( Arguments, DebugInfoCurrent, DebugInfoFrom ) - end - end -end - ---- Trace a function call level 3. Must be at the beginning of the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:F3( Arguments ) - - if debug and _TraceOnOff then - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - if _TraceLevel >= 3 then - self:_F( Arguments, DebugInfoCurrent, DebugInfoFrom ) - end - end -end - ---- Trace a function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:_T( Arguments, DebugInfoCurrentParam, DebugInfoFromParam ) - - if debug and ( _TraceAll == true ) or ( _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName] ) then - - local DebugInfoCurrent = DebugInfoCurrentParam and DebugInfoCurrentParam or debug.getinfo( 2, "nl" ) - local DebugInfoFrom = DebugInfoFromParam and DebugInfoFromParam or debug.getinfo( 3, "l" ) - - local Function = "function" - if DebugInfoCurrent.name then - Function = DebugInfoCurrent.name - end - - if _TraceAll == true or _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName].Method[Function] then - local LineCurrent = 0 - if DebugInfoCurrent.currentline then - LineCurrent = DebugInfoCurrent.currentline - end - local LineFrom = 0 - if DebugInfoFrom then - LineFrom = DebugInfoFrom.currentline - end - env.info( string.format( "%6d(%6d)/%1s:%20s%05d.%s" , LineCurrent, LineFrom, "T", self.ClassName, self.ClassID, routines.utils.oneLineSerialize( Arguments ) ) ) - end - end -end - ---- Trace a function logic level 1. Can be anywhere within the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:T( Arguments ) - - if debug and _TraceOnOff then - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - if _TraceLevel >= 1 then - self:_T( Arguments, DebugInfoCurrent, DebugInfoFrom ) - end - end -end - - ---- Trace a function logic level 2. Can be anywhere within the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:T2( Arguments ) - - if debug and _TraceOnOff then - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - if _TraceLevel >= 2 then - self:_T( Arguments, DebugInfoCurrent, DebugInfoFrom ) - end - end -end - ---- Trace a function logic level 3. Can be anywhere within the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:T3( Arguments ) - - if debug and _TraceOnOff then - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - if _TraceLevel >= 3 then - self:_T( Arguments, DebugInfoCurrent, DebugInfoFrom ) - end - end -end - ---- Log an exception which will be traced always. Can be anywhere within the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:E( Arguments ) - - if debug then - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - local Function = "function" - if DebugInfoCurrent.name then - Function = DebugInfoCurrent.name - end - - local LineCurrent = DebugInfoCurrent.currentline - local LineFrom = -1 - if DebugInfoFrom then - LineFrom = DebugInfoFrom.currentline - end - - env.info( string.format( "%6d(%6d)/%1s:%20s%05d.%s(%s)" , LineCurrent, LineFrom, "E", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) - end - -end - - - ---- **Core** - SCHEDULER prepares and handles the **execution of functions over scheduled time (intervals)**. --- --- ![Banner Image](..\Presentations\SCHEDULER\Dia1.JPG) --- --- === --- --- # 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 - ---- Clears all pending schedules. --- @param #SCHEDULER self -function SCHEDULER:Clear() - self:F3( ) - - _SCHEDULEDISPATCHER:Clear( self ) -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" } ) -- or {} - - 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] - -- Only start when there is no ScheduleID defined! - -- This prevents to "Start" the scheduler twice with the same CallID... - if not Schedule[CallID].ScheduleID then - Schedule[CallID].ScheduleID = timer.scheduleFunction( - Schedule[CallID].CallHandler, - CallID, - timer.getTime() + Schedule[CallID].Start - ) - end - 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] - -- Only stop when there is a ScheduleID defined for the CallID. - -- So, when the scheduler was stopped before, do nothing. - if Schedule[CallID].ScheduleID then - timer.removeFunction( Schedule[CallID].ScheduleID ) - Schedule[CallID].ScheduleID = nil - end - else - for CallID, Schedule in pairs( self.Schedule[Scheduler] ) do - self:Stop( Scheduler, CallID ) -- Recursive - end - end -end - -function SCHEDULEDISPATCHER:Clear( Scheduler ) - self:F2( { Scheduler = Scheduler } ) - - for CallID, Schedule in pairs( self.Schedule[Scheduler] ) do - self:Stop( Scheduler, CallID ) -- Recursive - end -end - - - ---- **Core** - EVENT models DCS **event dispatching** using a **publish-subscribe** model. --- --- ![Banner Image](..\Presentations\EVENT\Dia1.JPG) --- --- === --- --- # 1) Event Handling Overview --- --- ![Objects](..\Presentations\EVENT\Dia2.JPG) --- --- Within a running mission, various DCS events occur. Units are dynamically created, crash, die, shoot stuff, get hit etc. --- This module provides a mechanism to dispatch those events occuring within your running mission, to the different objects orchestrating your mission. --- --- ![Objects](..\Presentations\EVENT\Dia3.JPG) --- --- Objects can subscribe to different events. The Event dispatcher will publish the received DCS events to the subscribed MOOSE objects, in a specified order. --- In this way, the subscribed MOOSE objects are kept in sync with your evolving running mission. --- --- ## 1.1) Event Dispatching --- --- ![Objects](..\Presentations\EVENT\Dia4.JPG) --- --- The _EVENTDISPATCHER object is automatically created within MOOSE, --- and handles the dispatching of DCS Events occurring --- in the simulator to the subscribed objects --- in the correct processing order. --- --- ![Objects](..\Presentations\EVENT\Dia5.JPG) --- --- There are 5 levels of kind of objects that the _EVENTDISPATCHER services: --- --- * _DATABASE object: The core of the MOOSE objects. Any object that is created, deleted or updated, is done in this database. --- * SET_ derived classes: Subsets of the _DATABASE object. These subsets are updated by the _EVENTDISPATCHER as the second priority. --- * UNIT objects: UNIT objects can subscribe to DCS events. Each DCS event will be directly published to teh subscribed UNIT object. --- * GROUP objects: GROUP objects can subscribe to DCS events. Each DCS event will be directly published to the subscribed GROUP object. --- * Any other object: Various other objects can subscribe to DCS events. Each DCS event triggered will be published to each subscribed object. --- --- ![Objects](..\Presentations\EVENT\Dia6.JPG) --- --- For most DCS events, the above order of updating will be followed. --- --- ![Objects](..\Presentations\EVENT\Dia7.JPG) --- --- But for some DCS events, the publishing order is reversed. This is due to the fact that objects need to be **erased** instead of added. --- --- ## 1.2) Event Handling --- --- ![Objects](..\Presentations\EVENT\Dia8.JPG) --- --- The actual event subscribing and handling is not facilitated through the _EVENTDISPATCHER, but it is done through the @{BASE} class, @{UNIT} class and @{GROUP} class. --- The _EVENTDISPATCHER is a component that is quietly working in the background of MOOSE. --- --- ![Objects](..\Presentations\EVENT\Dia9.JPG) --- --- The BASE class provides methods to catch DCS Events. These are events that are triggered from within the DCS simulator, --- and handled through lua scripting. MOOSE provides an encapsulation to handle these events more efficiently. --- --- ### 1.2.1 Subscribe / Unsubscribe to DCS Events --- --- At first, the mission designer will need to **Subscribe** to a specific DCS event for the class. --- So, when the DCS event occurs, the class will be notified of that event. --- There are two functions which you use to subscribe to or unsubscribe from an event. --- --- * @{Base#BASE.HandleEvent}(): Subscribe to a DCS Event. --- * @{Base#BASE.UnHandleEvent}(): Unsubscribe from a DCS Event. --- --- Note that for a UNIT, the event will be handled **for that UNIT only**! --- Note that for a GROUP, the event will be handled **for all the UNITs in that GROUP only**! --- --- For all objects of other classes, the subscribed events will be handled for **all UNITs within the Mission**! --- So if a UNIT within the mission has the subscribed event for that object, --- then the object event handler will receive the event for that UNIT! --- --- ### 1.3.2 Event Handling of DCS Events --- --- Once the class is subscribed to the event, an **Event Handling** method on the object or class needs to be written that will be called --- when the DCS event occurs. The Event Handling method receives an @{Event#EVENTDATA} structure, which contains a lot of information --- about the event that occurred. --- --- Find below an example of the prototype how to write an event handling function for two units: --- --- local Tank1 = UNIT:FindByName( "Tank A" ) --- local Tank2 = UNIT:FindByName( "Tank B" ) --- --- -- Here we subscribe to the Dead events. So, if one of these tanks dies, the Tank1 or Tank2 objects will be notified. --- Tank1:HandleEvent( EVENTS.Dead ) --- Tank2:HandleEvent( EVENTS.Dead ) --- --- --- This function is an Event Handling function that will be called when Tank1 is Dead. --- -- @param Wrapper.Unit#UNIT self --- -- @param Core.Event#EVENTDATA EventData --- function Tank1:OnEventDead( EventData ) --- --- self:SmokeGreen() --- end --- --- --- This function is an Event Handling function that will be called when Tank2 is Dead. --- -- @param Wrapper.Unit#UNIT self --- -- @param Core.Event#EVENTDATA EventData --- function Tank2:OnEventDead( EventData ) --- --- self:SmokeBlue() --- end --- --- ### 1.3.3 Event Handling methods that are automatically called upon subscribed DCS events --- --- ![Objects](..\Presentations\EVENT\Dia10.JPG) --- --- The following list outlines which EVENTS item in the structure corresponds to which Event Handling method. --- Always ensure that your event handling methods align with the events being subscribed to, or nothing will be executed. --- --- # 2) EVENTS type --- --- The EVENTS structure contains names for all the different DCS events that objects can subscribe to using the --- @{Base#BASE.HandleEvent}() method. --- --- # 3) EVENTDATA type --- --- The @{Event#EVENTDATA} structure contains all the fields that are populated with event information before --- an Event Handler method is being called by the event dispatcher. --- The Event Handler received the EVENTDATA object as a parameter, and can be used to investigate further the different events. --- There are basically 4 main categories of information stored in the EVENTDATA structure: --- --- * Initiator Unit data: Several fields documenting the initiator unit related to the event. --- * Target Unit data: Several fields documenting the target unit related to the event. --- * Weapon data: Certain events populate weapon information. --- * Place data: Certain events populate place information. --- --- --- This function is an Event Handling function that will be called when Tank1 is Dead. --- -- EventData is an EVENTDATA structure. --- -- We use the EventData.IniUnit to smoke the tank Green. --- -- @param Wrapper.Unit#UNIT self --- -- @param Core.Event#EVENTDATA EventData --- function Tank1:OnEventDead( EventData ) --- --- EventData.IniUnit:SmokeGreen() --- end --- --- --- Find below an overview which events populate which information categories: --- --- ![Objects](..\Presentations\EVENT\Dia14.JPG) --- --- **IMPORTANT NOTE:** Some events can involve not just UNIT objects, but also STATIC objects!!! --- In that case the initiator or target unit fields will refer to a STATIC object! --- In case a STATIC object is involved, the documentation indicates which fields will and won't not be populated. --- The fields **IniObjectCategory** and **TgtObjectCategory** contain the indicator which **kind of object is involved** in the event. --- You can use the enumerator **Object.Category.UNIT** and **Object.Category.STATIC** to check on IniObjectCategory and TgtObjectCategory. --- Example code snippet: --- --- if Event.IniObjectCategory == Object.Category.UNIT then --- ... --- end --- if Event.IniObjectCategory == Object.Category.STATIC then --- ... --- end --- --- When a static object is involved in the event, the Group and Player fields won't be populated. --- --- ==== --- --- # **API CHANGE HISTORY** --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- YYYY-MM-DD: CLASS:**NewFunction**( Params ) replaces CLASS:_OldFunction_( Params ) --- YYYY-MM-DD: CLASS:**NewFunction( Params )** added --- --- Hereby the change log: --- --- * 2017-03-07: Added the correct event dispatching in case the event is subscribed by a GROUP. --- --- * 2017-02-07: Did a complete revision of the Event Handing API and underlying mechanisms. --- --- === --- --- # **AUTHORS and CONTRIBUTIONS** --- --- ### Contributions: --- --- ### Authors: --- --- * [**FlightControl**](https://forums.eagle.ru/member.php?u=89536): Design & Programming & documentation. --- --- @module Event - - ---- The EVENT structure --- @type EVENT --- @field #EVENT.Events Events --- @extends Core.Base#BASE -EVENT = { - ClassName = "EVENT", - ClassID = 0, -} - ---- The different types of events supported by MOOSE. --- Use this structure to subscribe to events using the @{Base#BASE.HandleEvent}() method. --- @type EVENTS -EVENTS = { - Shot = world.event.S_EVENT_SHOT, - Hit = world.event.S_EVENT_HIT, - Takeoff = world.event.S_EVENT_TAKEOFF, - Land = world.event.S_EVENT_LAND, - Crash = world.event.S_EVENT_CRASH, - Ejection = world.event.S_EVENT_EJECTION, - Refueling = world.event.S_EVENT_REFUELING, - Dead = world.event.S_EVENT_DEAD, - PilotDead = world.event.S_EVENT_PILOT_DEAD, - BaseCaptured = world.event.S_EVENT_BASE_CAPTURED, - MissionStart = world.event.S_EVENT_MISSION_START, - MissionEnd = world.event.S_EVENT_MISSION_END, - TookControl = world.event.S_EVENT_TOOK_CONTROL, - RefuelingStop = world.event.S_EVENT_REFUELING_STOP, - Birth = world.event.S_EVENT_BIRTH, - HumanFailure = world.event.S_EVENT_HUMAN_FAILURE, - EngineStartup = world.event.S_EVENT_ENGINE_STARTUP, - EngineShutdown = world.event.S_EVENT_ENGINE_SHUTDOWN, - PlayerEnterUnit = world.event.S_EVENT_PLAYER_ENTER_UNIT, - PlayerLeaveUnit = world.event.S_EVENT_PLAYER_LEAVE_UNIT, - PlayerComment = world.event.S_EVENT_PLAYER_COMMENT, - ShootingStart = world.event.S_EVENT_SHOOTING_START, - ShootingEnd = world.event.S_EVENT_SHOOTING_END, -} - ---- The Event structure --- Note that at the beginning of each field description, there is an indication which field will be populated depending on the object type involved in the Event: --- --- * A (Object.Category.)UNIT : A UNIT object type is involved in the Event. --- * A (Object.Category.)STATIC : A STATIC object type is involved in the Event.µ --- --- @type EVENTDATA --- @field #number id The identifier of the event. --- --- @field Dcs.DCSUnit#Unit initiator (UNIT/STATIC/SCENERY) The initiating @{Dcs.DCSUnit#Unit} or @{Dcs.DCSStaticObject#StaticObject}. --- @field Dcs.DCSObject#Object.Category IniObjectCategory (UNIT/STATIC/SCENERY) The initiator object category ( Object.Category.UNIT or Object.Category.STATIC ). --- @field Dcs.DCSUnit#Unit IniDCSUnit (UNIT/STATIC) The initiating @{DCSUnit#Unit} or @{DCSStaticObject#StaticObject}. --- @field #string IniDCSUnitName (UNIT/STATIC) The initiating Unit name. --- @field Wrapper.Unit#UNIT IniUnit (UNIT/STATIC) The initiating MOOSE wrapper @{Unit#UNIT} of the initiator Unit object. --- @field #string IniUnitName (UNIT/STATIC) The initiating UNIT name (same as IniDCSUnitName). --- @field Dcs.DCSGroup#Group IniDCSGroup (UNIT) The initiating {DCSGroup#Group}. --- @field #string IniDCSGroupName (UNIT) The initiating Group name. --- @field Wrapper.Group#GROUP IniGroup (UNIT) The initiating MOOSE wrapper @{Group#GROUP} of the initiator Group object. --- @field #string IniGroupName UNIT) The initiating GROUP name (same as IniDCSGroupName). --- @field #string IniPlayerName (UNIT) The name of the initiating player in case the Unit is a client or player slot. --- @field Dcs.DCScoalition#coalition.side IniCoalition (UNIT) The coalition of the initiator. --- @field Dcs.DCSUnit#Unit.Category IniCategory (UNIT) The category of the initiator. --- @field #string IniTypeName (UNIT) The type name of the initiator. --- --- @field Dcs.DCSUnit#Unit target (UNIT/STATIC) The target @{Dcs.DCSUnit#Unit} or @{DCSStaticObject#StaticObject}. --- @field Dcs.DCSObject#Object.Category TgtObjectCategory (UNIT/STATIC) The target object category ( Object.Category.UNIT or Object.Category.STATIC ). --- @field Dcs.DCSUnit#Unit TgtDCSUnit (UNIT/STATIC) The target @{DCSUnit#Unit} or @{DCSStaticObject#StaticObject}. --- @field #string TgtDCSUnitName (UNIT/STATIC) The target Unit name. --- @field Wrapper.Unit#UNIT TgtUnit (UNIT/STATIC) The target MOOSE wrapper @{Unit#UNIT} of the target Unit object. --- @field #string TgtUnitName (UNIT/STATIC) The target UNIT name (same as TgtDCSUnitName). --- @field Dcs.DCSGroup#Group TgtDCSGroup (UNIT) The target {DCSGroup#Group}. --- @field #string TgtDCSGroupName (UNIT) The target Group name. --- @field Wrapper.Group#GROUP TgtGroup (UNIT) The target MOOSE wrapper @{Group#GROUP} of the target Group object. --- @field #string TgtGroupName (UNIT) The target GROUP name (same as TgtDCSGroupName). --- @field #string TgtPlayerName (UNIT) The name of the target player in case the Unit is a client or player slot. --- @field Dcs.DCScoalition#coalition.side TgtCoalition (UNIT) The coalition of the target. --- @field Dcs.DCSUnit#Unit.Category TgtCategory (UNIT) The category of the target. --- @field #string TgtTypeName (UNIT) The type name of the target. --- --- @field weapon The weapon used during the event. --- @field Weapon --- @field WeaponName --- @field WeaponTgtDCSUnit - - -local _EVENTMETA = { - [world.event.S_EVENT_SHOT] = { - Order = 1, - Event = "OnEventShot", - Text = "S_EVENT_SHOT" - }, - [world.event.S_EVENT_HIT] = { - Order = 1, - Event = "OnEventHit", - Text = "S_EVENT_HIT" - }, - [world.event.S_EVENT_TAKEOFF] = { - Order = 1, - Event = "OnEventTakeoff", - Text = "S_EVENT_TAKEOFF" - }, - [world.event.S_EVENT_LAND] = { - Order = 1, - Event = "OnEventLand", - Text = "S_EVENT_LAND" - }, - [world.event.S_EVENT_CRASH] = { - Order = -1, - Event = "OnEventCrash", - Text = "S_EVENT_CRASH" - }, - [world.event.S_EVENT_EJECTION] = { - Order = 1, - Event = "OnEventEjection", - Text = "S_EVENT_EJECTION" - }, - [world.event.S_EVENT_REFUELING] = { - Order = 1, - Event = "OnEventRefueling", - Text = "S_EVENT_REFUELING" - }, - [world.event.S_EVENT_DEAD] = { - Order = -1, - Event = "OnEventDead", - Text = "S_EVENT_DEAD" - }, - [world.event.S_EVENT_PILOT_DEAD] = { - Order = 1, - Event = "OnEventPilotDead", - Text = "S_EVENT_PILOT_DEAD" - }, - [world.event.S_EVENT_BASE_CAPTURED] = { - Order = 1, - Event = "OnEventBaseCaptured", - Text = "S_EVENT_BASE_CAPTURED" - }, - [world.event.S_EVENT_MISSION_START] = { - Order = 1, - Event = "OnEventMissionStart", - Text = "S_EVENT_MISSION_START" - }, - [world.event.S_EVENT_MISSION_END] = { - Order = 1, - Event = "OnEventMissionEnd", - Text = "S_EVENT_MISSION_END" - }, - [world.event.S_EVENT_TOOK_CONTROL] = { - Order = 1, - Event = "OnEventTookControl", - Text = "S_EVENT_TOOK_CONTROL" - }, - [world.event.S_EVENT_REFUELING_STOP] = { - Order = 1, - Event = "OnEventRefuelingStop", - Text = "S_EVENT_REFUELING_STOP" - }, - [world.event.S_EVENT_BIRTH] = { - Order = 1, - Event = "OnEventBirth", - Text = "S_EVENT_BIRTH" - }, - [world.event.S_EVENT_HUMAN_FAILURE] = { - Order = 1, - Event = "OnEventHumanFailure", - Text = "S_EVENT_HUMAN_FAILURE" - }, - [world.event.S_EVENT_ENGINE_STARTUP] = { - Order = 1, - Event = "OnEventEngineStartup", - Text = "S_EVENT_ENGINE_STARTUP" - }, - [world.event.S_EVENT_ENGINE_SHUTDOWN] = { - Order = 1, - Event = "OnEventEngineShutdown", - Text = "S_EVENT_ENGINE_SHUTDOWN" - }, - [world.event.S_EVENT_PLAYER_ENTER_UNIT] = { - Order = 1, - Event = "OnEventPlayerEnterUnit", - Text = "S_EVENT_PLAYER_ENTER_UNIT" - }, - [world.event.S_EVENT_PLAYER_LEAVE_UNIT] = { - Order = -1, - Event = "OnEventPlayerLeaveUnit", - Text = "S_EVENT_PLAYER_LEAVE_UNIT" - }, - [world.event.S_EVENT_PLAYER_COMMENT] = { - Order = 1, - Event = "OnEventPlayerComment", - Text = "S_EVENT_PLAYER_COMMENT" - }, - [world.event.S_EVENT_SHOOTING_START] = { - Order = 1, - Event = "OnEventShootingStart", - Text = "S_EVENT_SHOOTING_START" - }, - [world.event.S_EVENT_SHOOTING_END] = { - Order = 1, - Event = "OnEventShootingEnd", - Text = "S_EVENT_SHOOTING_END" - }, -} - - ---- The Events structure --- @type EVENT.Events --- @field #number IniUnit - -function EVENT:New() - local self = BASE:Inherit( self, BASE:New() ) - self:F2() - self.EventHandler = world.addEventHandler( self ) - return self -end - -function EVENT:EventText( EventID ) - - local EventText = _EVENTMETA[EventID].Text - - return EventText -end - - ---- Initializes the Events structure for the event --- @param #EVENT self --- @param Dcs.DCSWorld#world.event EventID --- @param Core.Base#BASE EventClass --- @return #EVENT.Events -function EVENT:Init( EventID, EventClass ) - self:F3( { _EVENTMETA[EventID].Text, EventClass } ) - - if not self.Events[EventID] then - -- Create a WEAK table to ensure that the garbage collector is cleaning the event links when the object usage is cleaned. - self.Events[EventID] = setmetatable( {}, { __mode = "k" } ) - end - - -- Each event has a subtable of EventClasses, ordered by EventPriority. - local EventPriority = EventClass:GetEventPriority() - if not self.Events[EventID][EventPriority] then - self.Events[EventID][EventPriority] = setmetatable( {}, { __mode = "k" } ) - end - - if not self.Events[EventID][EventPriority][EventClass] then - self.Events[EventID][EventPriority][EventClass] = setmetatable( {}, { __mode = "v" } ) - end - return self.Events[EventID][EventPriority][EventClass] -end - ---- Removes an Events entry --- @param #EVENT self --- @param Core.Base#BASE EventClass The self instance of the class for which the event is. --- @param Dcs.DCSWorld#world.event EventID --- @return #EVENT.Events -function EVENT:Remove( EventClass, EventID ) - self:F3( { EventClass, _EVENTMETA[EventID].Text } ) - - local EventClass = EventClass - local EventPriority = EventClass:GetEventPriority() - self.Events[EventID][EventPriority][EventClass] = nil -end - ---- Removes an Events entry for a UNIT. --- @param #EVENT self --- @param #string UnitName The name of the UNIT. --- @param Core.Base#BASE EventClass The self instance of the class for which the event is. --- @param Dcs.DCSWorld#world.event EventID --- @return #EVENT.Events -function EVENT:RemoveForUnit( UnitName, EventClass, EventID ) - self:F3( { EventClass, _EVENTMETA[EventID].Text } ) - - local EventClass = EventClass - local EventPriority = EventClass:GetEventPriority() - local Event = self.Events[EventID][EventPriority][EventClass] - Event.EventUnit[UnitName] = nil -end - ---- Removes an Events entry for a GROUP. --- @param #EVENT self --- @param #string GroupName The name of the GROUP. --- @param Core.Base#BASE EventClass The self instance of the class for which the event is. --- @param Dcs.DCSWorld#world.event EventID --- @return #EVENT.Events -function EVENT:RemoveForGroup( GroupName, EventClass, EventID ) - self:F3( { EventClass, _EVENTMETA[EventID].Text } ) - - local EventClass = EventClass - local EventPriority = EventClass:GetEventPriority() - local Event = self.Events[EventID][EventPriority][EventClass] - Event.EventGroup[GroupName] = nil -end - ---- Clears all event subscriptions for a @{Base#BASE} derived object. --- @param #EVENT self --- @param Core.Base#BASE EventObject -function EVENT:RemoveAll( EventObject ) - self:F3( { EventObject:GetClassNameAndID() } ) - - local EventClass = EventObject:GetClassNameAndID() - local EventPriority = EventClass:GetEventPriority() - for EventID, EventData in pairs( self.Events ) do - self.Events[EventID][EventPriority][EventClass] = nil - end -end - - - ---- Create an OnDead event handler for a group --- @param #EVENT self --- @param #table EventTemplate --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param EventClass The instance of the class for which the event is. --- @param #function OnEventFunction --- @return #EVENT -function EVENT:OnEventForTemplate( EventTemplate, EventFunction, EventClass, EventID ) - self:F2( EventTemplate.name ) - - for EventUnitID, EventUnit in pairs( EventTemplate.units ) do - self:OnEventForUnit( EventUnit.name, EventFunction, EventClass, EventID ) - 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 EventData = self:Init( EventID, EventClass ) - EventData.EventFunction = EventFunction - EventData.EventClass = EventClass - - return self -end - - ---- Set a new listener for an S_EVENT_X event for a UNIT. --- @param #EVENT self --- @param #string UnitName The name of the UNIT. --- @param #function EventFunction The function to be called when the event occurs for the GROUP. --- @param Core.Base#BASE EventClass The self instance of the class for which the event is. --- @param EventID --- @return #EVENT -function EVENT:OnEventForUnit( UnitName, EventFunction, EventClass, EventID ) - self:F2( UnitName ) - - local EventData = self:Init( EventID, EventClass ) - if not EventData.EventUnit then - EventData.EventUnit = {} - end - EventData.EventUnit[UnitName] = {} - EventData.EventUnit[UnitName].EventFunction = EventFunction - EventData.EventUnit[UnitName].EventClass = EventClass - return self -end - ---- Set a new listener for an S_EVENT_X event for a GROUP. --- @param #EVENT self --- @param #string GroupName The name of the GROUP. --- @param #function EventFunction The function to be called when the event occurs for the GROUP. --- @param Core.Base#BASE EventClass The self instance of the class for which the event is. --- @param EventID --- @return #EVENT -function EVENT:OnEventForGroup( GroupName, EventFunction, EventClass, EventID ) - self:F2( GroupName ) - - local Event = self:Init( EventID, EventClass ) - if not Event.EventGroup then - Event.EventGroup = {} - end - Event.EventGroup[GroupName] = {} - Event.EventGroup[GroupName].EventFunction = EventFunction - Event.EventGroup[GroupName].EventClass = EventClass - return self -end - -do -- OnBirth - - --- Create an OnBirth event handler for a group - -- @param #EVENT self - -- @param Wrapper.Group#GROUP EventGroup - -- @param #function EventFunction The function to be called when the event occurs for the unit. - -- @param EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnBirthForTemplate( EventTemplate, EventFunction, EventClass ) - self:F2( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, EVENTS.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, EVENTS.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, EVENTS.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, EVENTS.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, EVENTS.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, EVENTS.EngineShutdown ) - - 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 - - self:E( _EVENTMETA[Event.id].Text, Event ) - - if self and self.Events and self.Events[Event.id] then - - - if Event.initiator then - - Event.IniObjectCategory = Event.initiator:getCategory() - - if Event.IniObjectCategory == Object.Category.UNIT then - Event.IniDCSUnit = Event.initiator - Event.IniDCSUnitName = Event.IniDCSUnit:getName() - Event.IniUnitName = Event.IniDCSUnitName - Event.IniDCSGroup = Event.IniDCSUnit:getGroup() - Event.IniUnit = UNIT:FindByName( Event.IniDCSUnitName ) - if not Event.IniUnit then - -- Unit can be a CLIENT. Most likely this will be the case ... - Event.IniUnit = CLIENT:FindByName( Event.IniDCSUnitName, '', true ) - end - Event.IniDCSGroupName = "" - if Event.IniDCSGroup and Event.IniDCSGroup:isExist() then - Event.IniDCSGroupName = Event.IniDCSGroup:getName() - Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) - if Event.IniGroup then - Event.IniGroupName = Event.IniDCSGroupName - end - end - Event.IniPlayerName = Event.IniDCSUnit:getPlayerName() - Event.IniCoalition = Event.IniDCSUnit:getCoalition() - Event.IniTypeName = Event.IniDCSUnit:getTypeName() - Event.IniCategory = Event.IniDCSUnit:getDesc().category - end - - if Event.IniObjectCategory == Object.Category.STATIC then - Event.IniDCSUnit = Event.initiator - Event.IniDCSUnitName = Event.IniDCSUnit:getName() - Event.IniUnitName = Event.IniDCSUnitName - Event.IniUnit = STATIC:FindByName( Event.IniDCSUnitName, false ) - Event.IniCoalition = Event.IniDCSUnit:getCoalition() - Event.IniCategory = Event.IniDCSUnit:getDesc().category - Event.IniTypeName = Event.IniDCSUnit:getTypeName() - end - - if Event.IniObjectCategory == Object.Category.SCENERY then - Event.IniDCSUnit = Event.initiator - Event.IniDCSUnitName = Event.IniDCSUnit:getName() - Event.IniUnitName = Event.IniDCSUnitName - Event.IniUnit = SCENERY:Register( Event.IniDCSUnitName, Event.initiator ) - Event.IniCategory = Event.IniDCSUnit:getDesc().category - Event.IniTypeName = Event.IniDCSUnit:getTypeName() - end - end - - if Event.target then - - Event.TgtObjectCategory = Event.target:getCategory() - - if Event.TgtObjectCategory == Object.Category.UNIT then - Event.TgtDCSUnit = Event.target - Event.TgtDCSGroup = Event.TgtDCSUnit:getGroup() - Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() - Event.TgtUnitName = Event.TgtDCSUnitName - Event.TgtUnit = UNIT:FindByName( Event.TgtDCSUnitName ) - Event.TgtDCSGroupName = "" - if Event.TgtDCSGroup and Event.TgtDCSGroup:isExist() then - Event.TgtDCSGroupName = Event.TgtDCSGroup:getName() - Event.TgtGroup = GROUP:FindByName( Event.TgtDCSGroupName ) - if Event.TgtGroup then - Event.TgtGroupName = Event.TgtDCSGroupName - end - end - Event.TgtPlayerName = Event.TgtDCSUnit:getPlayerName() - Event.TgtCoalition = Event.TgtDCSUnit:getCoalition() - Event.TgtCategory = Event.TgtDCSUnit:getDesc().category - Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() - end - - if Event.TgtObjectCategory == Object.Category.STATIC then - Event.TgtDCSUnit = Event.target - Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() - Event.TgtUnitName = Event.TgtDCSUnitName - Event.TgtUnit = STATIC:FindByName( Event.TgtDCSUnitName ) - Event.TgtCoalition = Event.TgtDCSUnit:getCoalition() - Event.TgtCategory = Event.TgtDCSUnit:getDesc().category - Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() - end - - if Event.TgtObjectCategory == Object.Category.SCENERY then - Event.TgtDCSUnit = Event.target - Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() - Event.TgtUnitName = Event.TgtDCSUnitName - Event.TgtUnit = SCENERY:Register( Event.TgtDCSUnitName, Event.target ) - Event.TgtCategory = Event.TgtDCSUnit:getDesc().category - Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() - end - end - - if Event.weapon then - Event.Weapon = Event.weapon - Event.WeaponName = Event.Weapon:getTypeName() - Event.WeaponUNIT = CLIENT:Find( Event.Weapon, '', true ) -- Sometimes, the weapon is a player unit! - Event.WeaponPlayerName = Event.WeaponUNIT and Event.Weapon:getPlayerName() - Event.WeaponCoalition = Event.WeaponUNIT and Event.Weapon:getCoalition() - Event.WeaponCategory = Event.WeaponUNIT and Event.Weapon:getDesc().category - Event.WeaponTypeName = Event.WeaponUNIT and Event.Weapon:getTypeName() - --Event.WeaponTgtDCSUnit = Event.Weapon:getTarget() - end - - local PriorityOrder = _EVENTMETA[Event.id].Order - local PriorityBegin = PriorityOrder == -1 and 5 or 1 - local PriorityEnd = PriorityOrder == -1 and 1 or 5 - - if Event.IniObjectCategory ~= 3 then - self:E( { _EVENTMETA[Event.id].Text, Event, Event.IniDCSUnitName, Event.TgtDCSUnitName, PriorityOrder } ) - end - - for EventPriority = PriorityBegin, PriorityEnd, PriorityOrder do - - if self.Events[Event.id][EventPriority] then - - -- Okay, we got the event from DCS. Now loop the SORTED self.EventSorted[] table for the received Event.id, and for each EventData registered, check if a function needs to be called. - for EventClass, EventData in pairs( self.Events[Event.id][EventPriority] ) do - - Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) - Event.TgtGroup = GROUP:FindByName( Event.TgtDCSGroupName ) - - -- If the EventData is for a UNIT, the call directly the EventClass EventFunction for that UNIT. - if ( Event.IniDCSUnitName and EventData.EventUnit and EventData.EventUnit[Event.IniDCSUnitName] ) or - ( Event.TgtDCSUnitName and EventData.EventUnit and EventData.EventUnit[Event.TgtDCSUnitName] ) then - - if EventData.EventUnit[Event.IniDCSUnitName] then - - -- First test if a EventFunction is Set, otherwise search for the default function - if EventData.EventUnit[Event.IniDCSUnitName].EventFunction then - - if Event.IniObjectCategory ~= 3 then - self:E( { "Calling EventFunction for UNIT ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName, EventPriority } ) - end - - local Result, Value = xpcall( - function() - return EventData.EventUnit[Event.IniDCSUnitName].EventFunction( EventClass, Event ) - end, ErrorHandler ) - - else - - -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. - local EventFunction = EventClass[ _EVENTMETA[Event.id].Event ] - if EventFunction and type( EventFunction ) == "function" then - - -- Now call the default event function. - if Event.IniObjectCategory ~= 3 then - self:E( { "Calling " .. _EVENTMETA[Event.id].Event .. " for Class ", EventClass:GetClassNameAndID(), EventPriority } ) - end - - local Result, Value = xpcall( - function() - return EventFunction( EventClass, Event ) - end, ErrorHandler ) - end - end - end - - if EventData.EventUnit[Event.TgtDCSUnitName] then - - -- First test if a EventFunction is Set, otherwise search for the default function - if EventData.EventUnit[Event.TgtDCSUnitName].EventFunction then - - if Event.IniObjectCategory ~= 3 then - self:E( { "Calling EventFunction for UNIT ", EventClass:GetClassNameAndID(), ", Unit ", Event.TgtUnitName, EventPriority } ) - end - - local Result, Value = xpcall( - function() - return EventData.EventUnit[Event.TgtDCSUnitName].EventFunction( EventClass, Event ) - end, ErrorHandler ) - - else - - -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. - local EventFunction = EventClass[ _EVENTMETA[Event.id].Event ] - if EventFunction and type( EventFunction ) == "function" then - - -- Now call the default event function. - if Event.IniObjectCategory ~= 3 then - self:E( { "Calling " .. _EVENTMETA[Event.id].Event .. " for Class ", EventClass:GetClassNameAndID(), EventPriority } ) - end - - local Result, Value = xpcall( - function() - return EventFunction( EventClass, Event ) - end, ErrorHandler ) - end - end - end - - else - - -- If the EventData is for a GROUP, the call directly the EventClass EventFunction for the UNIT in that GROUP. - if ( Event.IniDCSUnitName and Event.IniDCSGroupName and Event.IniGroupName and EventData.EventGroup and EventData.EventGroup[Event.IniGroupName] ) or - ( Event.TgtDCSUnitName and Event.TgtDCSGroupName and Event.TgtGroupName and EventData.EventGroup and EventData.EventGroup[Event.TgtGroupName] ) then - - if EventData.EventGroup[Event.IniGroupName] then - -- First test if a EventFunction is Set, otherwise search for the default function - if EventData.EventGroup[Event.IniGroupName].EventFunction then - - if Event.IniObjectCategory ~= 3 then - self:E( { "Calling EventFunction for GROUP ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName, EventPriority } ) - end - - local Result, Value = xpcall( - function() - return EventData.EventGroup[Event.IniGroupName].EventFunction( EventClass, Event ) - end, ErrorHandler ) - - else - - -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. - local EventFunction = EventClass[ _EVENTMETA[Event.id].Event ] - if EventFunction and type( EventFunction ) == "function" then - - -- Now call the default event function. - if Event.IniObjectCategory ~= 3 then - self:E( { "Calling " .. _EVENTMETA[Event.id].Event .. " for GROUP ", EventClass:GetClassNameAndID(), EventPriority } ) - end - - local Result, Value = xpcall( - function() - return EventFunction( EventClass, Event ) - end, ErrorHandler ) - end - end - end - - if EventData.EventGroup[Event.TgtGroupName] then - if EventData.EventGroup[Event.TgtGroupName].EventFunction then - - if Event.IniObjectCategory ~= 3 then - self:E( { "Calling EventFunction for GROUP ", EventClass:GetClassNameAndID(), ", Unit ", Event.TgtUnitName, EventPriority } ) - end - - local Result, Value = xpcall( - function() - return EventData.EventGroup[Event.TgtGroupName].EventFunction( EventClass, Event ) - end, ErrorHandler ) - - else - - -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. - local EventFunction = EventClass[ _EVENTMETA[Event.id].Event ] - if EventFunction and type( EventFunction ) == "function" then - - -- Now call the default event function. - if Event.IniObjectCategory ~= 3 then - self:E( { "Calling " .. _EVENTMETA[Event.id].Event .. " for GROUP ", EventClass:GetClassNameAndID(), EventPriority } ) - end - - local Result, Value = xpcall( - function() - return EventFunction( EventClass, Event ) - end, ErrorHandler ) - end - end - end - - else - - -- If the EventData is not bound to a specific unit, then call the EventClass EventFunction. - -- Note that here the EventFunction will need to implement and determine the logic for the relevant source- or target unit, or weapon. - if (Event.IniDCSUnit or Event.WeaponUNIT) and not EventData.EventUnit then - - if EventClass == EventData.EventClass then - - -- First test if a EventFunction is Set, otherwise search for the default function - if EventData.EventFunction then - - -- There is an EventFunction defined, so call the EventFunction. - if Event.IniObjectCategory ~= 3 then - self:E( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID(), EventPriority } ) - end - local Result, Value = xpcall( - function() - return EventData.EventFunction( EventClass, Event ) - end, ErrorHandler ) - else - - -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. - local EventFunction = EventClass[ _EVENTMETA[Event.id].Event ] - if EventFunction and type( EventFunction ) == "function" then - - -- Now call the default event function. - if Event.IniObjectCategory ~= 3 then - self:E( { "Calling " .. _EVENTMETA[Event.id].Event .. " for Class ", EventClass:GetClassNameAndID(), EventPriority } ) - end - - local Result, Value = xpcall( - function() - local Result, Value = EventFunction( EventClass, Event ) - return Result, Value - end, ErrorHandler ) - end - end - end - end - end - end - end - end - end - else - self:E( { _EVENTMETA[Event.id].Text, Event } ) - end - - Event = nil -end - ---- The EVENTHANDLER structure --- @type EVENTHANDLER --- @extends Core.Base#BASE -EVENTHANDLER = { - ClassName = "EVENTHANDLER", - ClassID = 0, -} - ---- The EVENTHANDLER constructor --- @param #EVENTHANDLER self --- @return #EVENTHANDLER -function EVENTHANDLER:New() - self = BASE:Inherit( self, BASE:New() ) -- #EVENTHANDLER - return self -end ---- **Core** -- MENU_ classes model the definition of **hierarchical menu structures** and **commands for players** within a mission. --- --- === --- --- 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 - -- @param #MENU_BASE - -- @return #MENU_BASE - 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 - self.Menus = {} - self.MenuCount = 0 - self.MenuRemoveParent = false - self.MenuTime = timer.getTime() - - return self - end - - --- Gets a @{Menu} from a parent @{Menu} - -- @param #MENU_BASE self - -- @param #string MenuText The text of the child menu. - -- @return #MENU_BASE - function MENU_BASE:GetMenu( MenuText ) - self:F( { self.Menus, MenuText } ) - return self.Menus[MenuText] - end - - --- Sets a @{Menu} to remove automatically the parent menu when the menu removed is the last child menu of that parent @{Menu}. - -- @param #MENU_BASE self - -- @param #boolean RemoveParent If true, the parent menu is automatically removed when this menu is the last child menu of that parent @{Menu}. - -- @return #MENU_BASE - function MENU_BASE:SetRemoveParent( RemoveParent ) - self:F( { RemoveParent } ) - self.MenuRemoveParent = RemoveParent - return self - end - - - --- Sets a time stamp for later prevention of menu removal. - -- @param #MENU_BASE self - -- @param MenuTime - -- @return #MENU_BASE - function MENU_BASE:SetTime( MenuTime ) - self.MenuTime = MenuTime - return self - end - -end - -do -- MENU_COMMAND_BASE - - --- The MENU_COMMAND_BASE class - -- @type MENU_COMMAND_BASE - -- @field #function MenuCallHandler - -- @extends Core.Menu#MENU_BASE - MENU_COMMAND_BASE = { - ClassName = "MENU_COMMAND_BASE", - CommandMenuFunction = nil, - CommandMenuArgument = nil, - MenuCallHandler = nil, - } - - --- Constructor - -- @param #MENU_COMMAND_BASE - -- @return #MENU_COMMAND_BASE - 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 Core.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 - 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 - 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 Core.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 Core.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 - 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 Core.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 - 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 Core.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 Core.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 Core.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.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 self.ParentMenu and self.ParentMenu.Menus then - self.ParentMenu.Menus[MenuText] = self - self:F( { self.ParentMenu.Menus, MenuText } ) - self.ParentMenu.MenuCount = self.ParentMenu.MenuCount + 1 - 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 - -- @param MenuTime - -- @return #MENU_GROUP self - function MENU_GROUP:RemoveSubMenus( MenuTime ) - self:F2( { self.MenuPath, MenuTime, self.MenuTime } ) - - self:T( { "Removing Group SubMenus:", self.MenuGroup:GetName(), self.MenuPath } ) - for MenuText, Menu in pairs( self.Menus ) do - Menu:Remove( MenuTime ) - end - - end - - - --- Removes the main menu and sub menus recursively of this MENU_GROUP. - -- @param #MENU_GROUP self - -- @param MenuTime - -- @return #nil - function MENU_GROUP:Remove( MenuTime ) - self:F( { self.MenuGroupID, self.MenuPath, MenuTime, self.MenuTime } ) - - self:RemoveSubMenus( MenuTime ) - - if not MenuTime or self.MenuTime ~= MenuTime then - 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.MenuText] = nil - self.ParentMenu.MenuCount = self.ParentMenu.MenuCount - 1 - if self.ParentMenu.MenuCount == 0 then - if self.MenuRemoveParent == true then - self:T( "Removing Parent Menu " ) - self.ParentMenu:Remove() - end - end - end - self:T( { "Removing Group Menu:", self.MenuGroup:GetName(), self.MenuGroup._Menus[self.Path].Path } ) - self.MenuGroup._Menus[self.Path] = nil - self = nil - end - end - - return nil - end - - - --- The MENU_GROUP_COMMAND class - -- @type MENU_GROUP_COMMAND - -- @extends Core.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_GROUP_COMMAND - 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] - self:T( { "Re-using Group Command Menu:", MenuGroup:GetName(), MenuText } ) - 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 Group Command Menu:", MenuGroup:GetName(), MenuText, self.MenuParentPath } ) - self.MenuPath = missionCommands.addCommandForGroup( self.MenuGroupID, MenuText, self.MenuParentPath, self.MenuCallHandler, arg ) - - if self.ParentMenu and self.ParentMenu.Menus then - self.ParentMenu.Menus[MenuText] = self - self.ParentMenu.MenuCount = self.ParentMenu.MenuCount + 1 - self:F( { ParentMenu.Menus, MenuText } ) - end - end - - return self - end - - --- Removes a menu structure for a group. - -- @param #MENU_GROUP_COMMAND self - -- @param MenuTime - -- @return #nil - function MENU_GROUP_COMMAND:Remove( MenuTime ) - self:F( { self.MenuGroupID, self.MenuPath, MenuTime, self.MenuTime } ) - - if not MenuTime or self.MenuTime ~= MenuTime then - if self.MenuGroup._Menus[self.Path] then - self = self.MenuGroup._Menus[self.Path] - - missionCommands.removeItemForGroup( self.MenuGroupID, self.MenuPath ) - self:T( { "Removing Group Command Menu:", self.MenuGroup:GetName(), self.MenuText, self.Path, self.MenuGroup._Menus[self.Path].Path } ) - - self.ParentMenu.Menus[self.MenuText] = nil - self.ParentMenu.MenuCount = self.ParentMenu.MenuCount - 1 - if self.ParentMenu.MenuCount == 0 then - if self.MenuRemoveParent == true then - self:T( "Removing Parent Menu " ) - self.ParentMenu:Remove() - end - end - - self.MenuGroup._Menus[self.Path] = nil - self = nil - end - end - - return nil - end - -end - ---- **Core** - ZONE classes define **zones** within your mission of **various forms**, with **various capabilities**. --- --- ![Banner Image](..\Presentations\ZONE\Dia1.JPG) --- --- === --- --- 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_BASE}: The ZONE_BASE class defining the base for all other zone classes. --- * @{#ZONE_RADIUS}: The ZONE_RADIUS class defined by a zone name, a location and a radius. --- * @{#ZONE}: The ZONE class, defined by the zone name as defined within the Mission Editor. --- * @{#ZONE_UNIT}: The ZONE_UNIT class defines by a zone around a @{Unit#UNIT} with a radius. --- * @{#ZONE_GROUP}: The ZONE_GROUP class defines by a zone around a @{Group#GROUP} with a radius. --- * @{#ZONE_POLYGON}: The ZONE_POLYGON class defines by a sequence of @{Group#GROUP} waypoints within the Mission Editor, forming a polygon. --- --- === --- --- # **API CHANGE HISTORY** --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- Hereby the change log: --- --- 2017-02-28: ZONE\_BASE:**IsVec2InZone()** replaces ZONE\_BASE:_IsPointVec2InZone()_. --- 2017-02-28: ZONE\_BASE:**IsVec3InZone()** replaces ZONE\_BASE:_IsPointVec3InZone()_. --- 2017-02-28: ZONE\_RADIUS:**IsVec2InZone()** replaces ZONE\_RADIUS:_IsPointVec2InZone()_. --- 2017-02-28: ZONE\_RADIUS:**IsVec3InZone()** replaces ZONE\_RADIUS:_IsPointVec3InZone()_. --- 2017-02-28: ZONE\_POLYGON:**IsVec2InZone()** replaces ZONE\_POLYGON:_IsPointVec2InZone()_. --- 2017-02-28: ZONE\_POLYGON:**IsVec3InZone()** replaces ZONE\_POLYGON:_IsPointVec3InZone()_. --- --- 2017-02-18: ZONE\_POLYGON_BASE:**GetRandomPointVec2()** added. --- --- 2017-02-18: ZONE\_POLYGON_BASE:**GetRandomPointVec3()** added. --- --- 2017-02-18: ZONE\_RADIUS:**GetRandomPointVec3( inner, outer )** added. --- --- 2017-02-18: ZONE\_RADIUS:**GetRandomPointVec2( inner, outer )** added. --- --- 2016-08-15: ZONE\_BASE:**GetName()** added. --- --- 2016-08-15: ZONE\_BASE:**SetZoneProbability( ZoneProbability )** added. --- --- 2016-08-15: ZONE\_BASE:**GetZoneProbability()** added. --- --- 2016-08-15: ZONE\_BASE:**GetZoneMaybe()** added. --- --- === --- --- @module Zone - - ---- The ZONE_BASE class --- @type ZONE_BASE --- @field #string ZoneName Name of the zone. --- @field #number ZoneProbability A value between 0 and 1. 0 = 0% and 1 = 100% probability. --- @extends Core.Base#BASE - - ---- # 1) ZONE_BASE class, extends @{Base#BASE} --- --- This class is an abstract BASE class for derived classes, and is not meant to be instantiated. --- --- ## 1.1) Each zone has a name: --- --- * @{#ZONE_BASE.GetName}(): Returns the name of the zone. --- --- ## 1.2) Each zone implements two polymorphic functions defined in @{Zone#ZONE_BASE}: --- --- * @{#ZONE_BASE.IsVec2InZone}(): Returns if a Vec2 is within the zone. --- * @{#ZONE_BASE.IsVec3InZone}(): Returns if a Vec3 is within the zone. --- --- ## 1.3) A zone has a probability factor that can be set to randomize a selection between zones: --- --- * @{#ZONE_BASE.SetRandomizeProbability}(): Set the randomization probability of a zone to be selected, taking a value between 0 and 1 ( 0 = 0%, 1 = 100% ) --- * @{#ZONE_BASE.GetRandomizeProbability}(): Get the randomization probability of a zone to be selected, passing a value between 0 and 1 ( 0 = 0%, 1 = 100% ) --- * @{#ZONE_BASE.GetZoneMaybe}(): Get the zone taking into account the randomization probability. nil is returned if this zone is not a candidate. --- --- ## 1.4) A zone manages Vectors: --- --- * @{#ZONE_BASE.GetVec2}(): Returns the @{DCSTypes#Vec2} coordinate of the zone. --- * @{#ZONE_BASE.GetRandomVec2}(): Define a random @{DCSTypes#Vec2} within the zone. --- --- ## 1.5) A zone has a bounding square: --- --- * @{#ZONE_BASE.GetBoundingSquare}(): Get the outer most bounding square of the zone. --- --- ## 1.6) A zone can be marked: --- --- * @{#ZONE_BASE.SmokeZone}(): Smokes the zone boundaries in a color. --- * @{#ZONE_BASE.FlareZone}(): Flares the zone boundaries in a color. --- --- === --- @field #ZONE_BASE ZONE_BASE -ZONE_BASE = { - ClassName = "ZONE_BASE", - ZoneName = "", - ZoneProbability = 1, - } - - ---- The ZONE_BASE.BoundingSquare --- @type ZONE_BASE.BoundingSquare --- @field Dcs.DCSTypes#Distance x1 The lower x coordinate (left down) --- @field Dcs.DCSTypes#Distance y1 The lower y coordinate (left down) --- @field Dcs.DCSTypes#Distance x2 The higher x coordinate (right up) --- @field Dcs.DCSTypes#Distance y2 The higher y coordinate (right up) - - ---- ZONE_BASE constructor --- @param #ZONE_BASE self --- @param #string ZoneName Name of the zone. --- @return #ZONE_BASE self -function ZONE_BASE:New( ZoneName ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( ZoneName ) - - self.ZoneName = ZoneName - - return self -end - ---- Returns the name of the zone. --- @param #ZONE_BASE self --- @return #string The name of the zone. -function ZONE_BASE:GetName() - self:F2() - - return self.ZoneName -end ---- Returns if a location is within the zone. --- @param #ZONE_BASE self --- @param Dcs.DCSTypes#Vec2 Vec2 The location to test. --- @return #boolean true if the location is within the zone. -function ZONE_BASE:IsVec2InZone( Vec2 ) - self:F2( Vec2 ) - - return false -end - ---- Returns if a point is within the zone. --- @param #ZONE_BASE self --- @param Dcs.DCSTypes#Vec3 Vec3 The point to test. --- @return #boolean true if the point is within the zone. -function ZONE_BASE:IsVec3InZone( Vec3 ) - self:F2( Vec3 ) - - local InZone = self:IsVec2InZone( { x = Vec3.x, y = Vec3.z } ) - - return InZone -end - ---- Returns the @{DCSTypes#Vec2} coordinate of the zone. --- @param #ZONE_BASE self --- @return #nil. -function ZONE_BASE:GetVec2() - self:F2( self.ZoneName ) - - return nil -end - ---- Returns a @{Point#POINT_VEC2} of the zone. --- @param #ZONE_BASE self --- @param Dcs.DCSTypes#Distance Height The height to add to the land height where the center of the zone is located. --- @return Core.Point#POINT_VEC2 The PointVec2 of the zone. -function ZONE_BASE:GetPointVec2() - self:F2( self.ZoneName ) - - local Vec2 = self:GetVec2() - - local PointVec2 = POINT_VEC2:NewFromVec2( Vec2 ) - - self:T2( { PointVec2 } ) - - return PointVec2 -end - - ---- Returns the @{DCSTypes#Vec3} of the zone. --- @param #ZONE_BASE self --- @param Dcs.DCSTypes#Distance Height The height to add to the land height where the center of the zone is located. --- @return Dcs.DCSTypes#Vec3 The Vec3 of the zone. -function ZONE_BASE: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 - ---- Returns a @{Point#POINT_VEC3} of the zone. --- @param #ZONE_BASE self --- @param Dcs.DCSTypes#Distance Height The height to add to the land height where the center of the zone is located. --- @return Core.Point#POINT_VEC3 The PointVec3 of the zone. -function ZONE_BASE:GetPointVec3( Height ) - self:F2( self.ZoneName ) - - local Vec3 = self:GetVec3( Height ) - - local PointVec3 = POINT_VEC3:NewFromVec3( Vec3 ) - - self:T2( { PointVec3 } ) - - return PointVec3 -end - - ---- Define a random @{DCSTypes#Vec2} within the zone. --- @param #ZONE_BASE self --- @return Dcs.DCSTypes#Vec2 The Vec2 coordinates. -function ZONE_BASE:GetRandomVec2() - return nil -end - ---- Define a random @{Point#POINT_VEC2} within the zone. --- @param #ZONE_BASE self --- @return Core.Point#POINT_VEC2 The PointVec2 coordinates. -function ZONE_BASE:GetRandomPointVec2() - return nil -end - ---- Define a random @{Point#POINT_VEC3} within the zone. --- @param #ZONE_BASE self --- @return Core.Point#POINT_VEC3 The PointVec3 coordinates. -function ZONE_BASE:GetRandomPointVec3() - return nil -end - ---- Get the bounding square the zone. --- @param #ZONE_BASE self --- @return #nil The bounding square. -function ZONE_BASE:GetBoundingSquare() - --return { x1 = 0, y1 = 0, x2 = 0, y2 = 0 } - return nil -end - ---- Bound the zone boundaries with a tires. --- @param #ZONE_BASE self -function ZONE_BASE:BoundZone() - self:F2() - -end - ---- Smokes the zone boundaries in a color. --- @param #ZONE_BASE self --- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color. -function ZONE_BASE:SmokeZone( SmokeColor ) - self:F2( SmokeColor ) - -end - ---- Set the randomization probability of a zone to be selected. --- @param #ZONE_BASE self --- @param ZoneProbability A value between 0 and 1. 0 = 0% and 1 = 100% probability. -function ZONE_BASE:SetZoneProbability( ZoneProbability ) - self:F2( ZoneProbability ) - - self.ZoneProbability = ZoneProbability or 1 - return self -end - ---- Get the randomization probability of a zone to be selected. --- @param #ZONE_BASE self --- @return #number A value between 0 and 1. 0 = 0% and 1 = 100% probability. -function ZONE_BASE:GetZoneProbability() - self:F2() - - return self.ZoneProbability -end - ---- Get the zone taking into account the randomization probability of a zone to be selected. --- @param #ZONE_BASE self --- @return #ZONE_BASE The zone is selected taking into account the randomization probability factor. --- @return #nil The zone is not selected taking into account the randomization probability factor. -function ZONE_BASE:GetZoneMaybe() - self:F2() - - local Randomization = math.random() - if Randomization <= self.ZoneProbability then - return self - else - return nil - end -end - - ---- The ZONE_RADIUS class, defined by a zone name, a location and a radius. --- @type ZONE_RADIUS --- @field Dcs.DCSTypes#Vec2 Vec2 The current location of the zone. --- @field Dcs.DCSTypes#Distance Radius The radius of the zone. --- @extends Core.Zone#ZONE_BASE - ---- # 2) @{Zone#ZONE_RADIUS} class, extends @{Zone#ZONE_BASE} --- --- The ZONE_RADIUS class defined by a zone name, a location and a radius. --- This class implements the inherited functions from Core.Zone#ZONE_BASE taking into account the own zone format and properties. --- --- ## 2.1) @{Zone#ZONE_RADIUS} constructor --- --- * @{#ZONE_RADIUS.New}(): Constructor. --- --- ## 2.2) Manage the radius of the zone --- --- * @{#ZONE_RADIUS.SetRadius}(): Sets the radius of the zone. --- * @{#ZONE_RADIUS.GetRadius}(): Returns the radius of the zone. --- --- ## 2.3) Manage the location of the zone --- --- * @{#ZONE_RADIUS.SetVec2}(): Sets the @{DCSTypes#Vec2} of the zone. --- * @{#ZONE_RADIUS.GetVec2}(): Returns the @{DCSTypes#Vec2} of the zone. --- * @{#ZONE_RADIUS.GetVec3}(): Returns the @{DCSTypes#Vec3} of the zone, taking an additional height parameter. --- --- ## 2.4) Zone point randomization --- --- Various functions exist to find random points within the zone. --- --- * @{#ZONE_RADIUS.GetRandomVec2}(): Gets a random 2D point in the zone. --- * @{#ZONE_RADIUS.GetRandomPointVec2}(): Gets a @{Point#POINT_VEC2} object representing a random 2D point in the zone. --- * @{#ZONE_RADIUS.GetRandomPointVec3}(): Gets a @{Point#POINT_VEC3} object representing a random 3D point in the zone. Note that the height of the point is at landheight. --- --- === --- --- @field #ZONE_RADIUS ZONE_RADIUS --- -ZONE_RADIUS = { - ClassName="ZONE_RADIUS", - } - ---- Constructor of @{#ZONE_RADIUS}, taking the zone name, the zone location and a radius. --- @param #ZONE_RADIUS self --- @param #string ZoneName Name of the zone. --- @param Dcs.DCSTypes#Vec2 Vec2 The location of the zone. --- @param Dcs.DCSTypes#Distance Radius The radius of the zone. --- @return #ZONE_RADIUS self -function ZONE_RADIUS:New( ZoneName, Vec2, Radius ) - local self = BASE:Inherit( self, ZONE_BASE:New( ZoneName ) ) -- #ZONE_RADIUS - self:F( { ZoneName, Vec2, Radius } ) - - self.Radius = Radius - self.Vec2 = Vec2 - - return self -end - ---- Bounds the zone with tires. --- @param #ZONE_RADIUS self --- @param #number Points (optional) The amount of points in the circle. --- @param #boolean UnBound If true the tyres will be destroyed. --- @return #ZONE_RADIUS self -function ZONE_RADIUS:BoundZone( Points, CountryID, UnBound ) - - local Point = {} - local Vec2 = self:GetVec2() - - Points = Points and Points or 360 - - local Angle - local RadialBase = math.pi*2 - - -- - for Angle = 0, 360, (360 / Points ) do - local Radial = Angle * RadialBase / 360 - Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() - Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() - - local CountryName = _DATABASE.COUNTRY_NAME[CountryID] - - local Tire = { - ["country"] = CountryName, - ["category"] = "Fortifications", - ["canCargo"] = false, - ["shape_name"] = "H-tyre_B_WF", - ["type"] = "Black_Tyre_WF", - --["unitId"] = Angle + 10000, - ["y"] = Point.y, - ["x"] = Point.x, - ["name"] = string.format( "%s-Tire #%0d", self:GetName(), Angle ), - ["heading"] = 0, - } -- end of ["group"] - - local Group = coalition.addStaticObject( CountryID, Tire ) - if UnBound and UnBound == true then - Group:destroy() - end - end - - return self -end - - ---- Smokes the zone boundaries in a color. --- @param #ZONE_RADIUS self --- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color. --- @param #number Points (optional) The amount of points in the circle. --- @return #ZONE_RADIUS self -function ZONE_RADIUS:SmokeZone( SmokeColor, Points ) - self:F2( SmokeColor ) - - local Point = {} - local Vec2 = self:GetVec2() - - Points = Points and Points or 360 - - local Angle - local RadialBase = math.pi*2 - - for Angle = 0, 360, 360 / Points do - local Radial = Angle * RadialBase / 360 - Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() - Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() - POINT_VEC2:New( Point.x, Point.y ):Smoke( SmokeColor ) - end - - return self -end - - ---- Flares the zone boundaries in a color. --- @param #ZONE_RADIUS self --- @param Utilities.Utils#FLARECOLOR FlareColor The flare color. --- @param #number Points (optional) The amount of points in the circle. --- @param Dcs.DCSTypes#Azimuth Azimuth (optional) Azimuth The azimuth of the flare. --- @return #ZONE_RADIUS self -function ZONE_RADIUS:FlareZone( FlareColor, Points, Azimuth ) - self:F2( { FlareColor, Azimuth } ) - - local Point = {} - local Vec2 = self:GetVec2() - - Points = Points and Points or 360 - - local Angle - local RadialBase = math.pi*2 - - for Angle = 0, 360, 360 / Points do - local Radial = Angle * RadialBase / 360 - Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() - Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() - POINT_VEC2:New( Point.x, Point.y ):Flare( FlareColor, Azimuth ) - end - - return self -end - ---- Returns the radius of the zone. --- @param #ZONE_RADIUS self --- @return Dcs.DCSTypes#Distance The radius of the zone. -function ZONE_RADIUS:GetRadius() - self:F2( self.ZoneName ) - - self:T2( { self.Radius } ) - - return self.Radius -end - ---- Sets the radius of the zone. --- @param #ZONE_RADIUS self --- @param Dcs.DCSTypes#Distance Radius The radius of the zone. --- @return Dcs.DCSTypes#Distance The radius of the zone. -function ZONE_RADIUS:SetRadius( Radius ) - self:F2( self.ZoneName ) - - self.Radius = Radius - self:T2( { self.Radius } ) - - return self.Radius -end - ---- Returns the @{DCSTypes#Vec2} of the zone. --- @param #ZONE_RADIUS self --- @return Dcs.DCSTypes#Vec2 The location of the zone. -function ZONE_RADIUS:GetVec2() - self:F2( self.ZoneName ) - - self:T2( { self.Vec2 } ) - - return self.Vec2 -end - ---- Sets the @{DCSTypes#Vec2} of the zone. --- @param #ZONE_RADIUS self --- @param Dcs.DCSTypes#Vec2 Vec2 The new location of the zone. --- @return Dcs.DCSTypes#Vec2 The new location of the zone. -function ZONE_RADIUS:SetVec2( Vec2 ) - self:F2( self.ZoneName ) - - self.Vec2 = Vec2 - - self:T2( { self.Vec2 } ) - - return self.Vec2 -end - ---- Returns the @{DCSTypes#Vec3} of the ZONE_RADIUS. --- @param #ZONE_RADIUS self --- @param Dcs.DCSTypes#Distance Height The height to add to the land height where the center of the zone is located. --- @return Dcs.DCSTypes#Vec3 The point of the zone. -function ZONE_RADIUS:GetVec3( Height ) - self:F2( { self.ZoneName, Height } ) - - Height = Height or 0 - local Vec2 = self:GetVec2() - - local Vec3 = { x = Vec2.x, y = land.getHeight( self:GetVec2() ) + Height, z = Vec2.y } - - self:T2( { Vec3 } ) - - return Vec3 -end - - ---- Returns if a location is within the zone. --- @param #ZONE_RADIUS self --- @param Dcs.DCSTypes#Vec2 Vec2 The location to test. --- @return #boolean true if the location is within the zone. -function ZONE_RADIUS:IsVec2InZone( Vec2 ) - self:F2( Vec2 ) - - local ZoneVec2 = self:GetVec2() - - if ZoneVec2 then - if (( Vec2.x - ZoneVec2.x )^2 + ( Vec2.y - ZoneVec2.y ) ^2 ) ^ 0.5 <= self:GetRadius() then - return true - end - end - - return false -end - ---- Returns if a point is within the zone. --- @param #ZONE_RADIUS self --- @param Dcs.DCSTypes#Vec3 Vec3 The point to test. --- @return #boolean true if the point is within the zone. -function ZONE_RADIUS:IsVec3InZone( Vec3 ) - self:F2( Vec3 ) - - local InZone = self:IsVec2InZone( { x = Vec3.x, y = Vec3.z } ) - - return InZone -end - ---- Returns a random Vec2 location within the zone. --- @param #ZONE_RADIUS self --- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. --- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. --- @return Dcs.DCSTypes#Vec2 The random location within the zone. -function ZONE_RADIUS:GetRandomVec2( inner, outer ) - self:F( self.ZoneName, inner, outer ) - - local Point = {} - local Vec2 = self:GetVec2() - local _inner = inner or 0 - local _outer = outer or self:GetRadius() - - local angle = math.random() * math.pi * 2; - Point.x = Vec2.x + math.cos( angle ) * math.random(_inner, _outer); - Point.y = Vec2.y + math.sin( angle ) * math.random(_inner, _outer); - - self:T( { Point } ) - - return Point -end - ---- Returns a @{Point#POINT_VEC2} object reflecting a random 2D location within the zone. --- @param #ZONE_RADIUS self --- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. --- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. --- @return Core.Point#POINT_VEC2 The @{Point#POINT_VEC2} object reflecting the random 3D location within the zone. -function ZONE_RADIUS:GetRandomPointVec2( inner, outer ) - self:F( self.ZoneName, inner, outer ) - - local PointVec2 = POINT_VEC2:NewFromVec2( self:GetRandomVec2() ) - - self:T3( { PointVec2 } ) - - return PointVec2 -end - ---- Returns a @{Point#POINT_VEC3} object reflecting a random 3D location within the zone. --- @param #ZONE_RADIUS self --- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. --- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. --- @return Core.Point#POINT_VEC3 The @{Point#POINT_VEC3} object reflecting the random 3D location within the zone. -function ZONE_RADIUS:GetRandomPointVec3( inner, outer ) - self:F( self.ZoneName, inner, outer ) - - local PointVec3 = POINT_VEC3:NewFromVec2( self:GetRandomVec2() ) - - self:T3( { PointVec3 } ) - - return PointVec3 -end - - - --- @type ZONE --- @extends Core.Zone#ZONE_RADIUS - - ---- # 3) 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 @{#ZONE_RADIUS} taking into account the own zone format and properties. --- --- === --- --- @field #ZONE ZONE --- -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 - ---- # 4) #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_RADIUS} taking into account the own zone format and properties. --- --- === --- --- @field #ZONE_UNIT ZONE_UNIT --- -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 - ---- @type ZONE_GROUP --- @field Wrapper.Group#GROUP ZoneGROUP --- @extends Core.Zone#ZONE_RADIUS - - ---- # 5) #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. --- --- === --- --- @field #ZONE_GROUP ZONE_GROUP --- -ZONE_GROUP = { - ClassName="ZONE_GROUP", - } - ---- Constructor to create a ZONE_GROUP instance, taking the zone name, a zone @{Group#GROUP} and a radius. --- @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 - - - ---- @type ZONE_POLYGON_BASE --- @field #ZONE_POLYGON_BASE.ListVec2 Polygon The polygon defined by an array of @{DCSTypes#Vec2}. --- @extends Core.Zone#ZONE_BASE - - ---- # 6) ZONE_POLYGON_BASE class, extends @{Zone#ZONE_BASE} --- --- The ZONE_POLYGON_BASE class defined by a sequence of @{Group#GROUP} waypoints within the Mission Editor, forming a polygon. --- This class implements the inherited functions from @{Zone#ZONE_RADIUS} taking into account the own zone format and properties. --- This class is an abstract BASE class for derived classes, and is not meant to be instantiated. --- --- ## 6.1) Zone point randomization --- --- Various functions exist to find random points within the zone. --- --- * @{#ZONE_POLYGON_BASE.GetRandomVec2}(): Gets a random 2D point in the zone. --- * @{#ZONE_POLYGON_BASE.GetRandomPointVec2}(): Return a @{Point#POINT_VEC2} object representing a random 2D point within the zone. --- * @{#ZONE_POLYGON_BASE.GetRandomPointVec3}(): Return a @{Point#POINT_VEC3} object representing a random 3D point at landheight within the zone. --- --- === --- --- @field #ZONE_POLYGON_BASE ZONE_POLYGON_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 #boolean UnBound If true, the tyres will be destroyed. --- @return #ZONE_POLYGON_BASE self -function ZONE_POLYGON_BASE:BoundZone( UnBound ) - - local i - local j - local Segments = 10 - - i = 1 - j = #self.Polygon - - while i <= #self.Polygon do - self:T( { i, j, self.Polygon[i], self.Polygon[j] } ) - - local DeltaX = self.Polygon[j].x - self.Polygon[i].x - local DeltaY = self.Polygon[j].y - self.Polygon[i].y - - for Segment = 0, Segments do -- We divide each line in 5 segments and smoke a point on the line. - local PointX = self.Polygon[i].x + ( Segment * DeltaX / Segments ) - local PointY = self.Polygon[i].y + ( Segment * DeltaY / Segments ) - local Tire = { - ["country"] = "USA", - ["category"] = "Fortifications", - ["canCargo"] = false, - ["shape_name"] = "H-tyre_B_WF", - ["type"] = "Black_Tyre_WF", - ["y"] = PointY, - ["x"] = PointX, - ["name"] = string.format( "%s-Tire #%0d", self:GetName(), ((i - 1) * Segments) + Segment ), - ["heading"] = 0, - } -- end of ["group"] - - local Group = coalition.addStaticObject( country.id.USA, Tire ) - if UnBound and UnBound == true then - Group:destroy() - end - - end - j = i - i = i + 1 - end - - return self -end - - - ---- Smokes the zone boundaries in a color. --- @param #ZONE_POLYGON_BASE self --- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color. --- @return #ZONE_POLYGON_BASE self -function ZONE_POLYGON_BASE:SmokeZone( SmokeColor ) - self:F2( SmokeColor ) - - local i - local j - local Segments = 10 - - i = 1 - j = #self.Polygon - - while i <= #self.Polygon do - self:T( { i, j, self.Polygon[i], self.Polygon[j] } ) - - local DeltaX = self.Polygon[j].x - self.Polygon[i].x - local DeltaY = self.Polygon[j].y - self.Polygon[i].y - - for Segment = 0, Segments do -- We divide each line in 5 segments and smoke a point on the line. - local PointX = self.Polygon[i].x + ( Segment * DeltaX / Segments ) - local PointY = self.Polygon[i].y + ( Segment * DeltaY / Segments ) - POINT_VEC2:New( PointX, PointY ):Smoke( SmokeColor ) - end - j = i - i = i + 1 - end - - return self -end - - - - ---- Returns if a location is within the zone. --- Source learned and taken from: https://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html --- @param #ZONE_POLYGON_BASE self --- @param Dcs.DCSTypes#Vec2 Vec2 The location to test. --- @return #boolean true if the location is within the zone. -function ZONE_POLYGON_BASE:IsVec2InZone( Vec2 ) - self:F2( Vec2 ) - - local Next - local Prev - local InPolygon = false - - Next = 1 - Prev = #self.Polygon - - while Next <= #self.Polygon do - self:T( { Next, Prev, self.Polygon[Next], self.Polygon[Prev] } ) - if ( ( ( self.Polygon[Next].y > Vec2.y ) ~= ( self.Polygon[Prev].y > Vec2.y ) ) and - ( Vec2.x < ( self.Polygon[Prev].x - self.Polygon[Next].x ) * ( Vec2.y - self.Polygon[Next].y ) / ( self.Polygon[Prev].y - self.Polygon[Next].y ) + self.Polygon[Next].x ) - ) then - InPolygon = not InPolygon - end - self:T2( { InPolygon = InPolygon } ) - Prev = Next - Next = Next + 1 - end - - self:T( { InPolygon = InPolygon } ) - return InPolygon -end - ---- Define a random @{DCSTypes#Vec2} within the zone. --- @param #ZONE_POLYGON_BASE self --- @return Dcs.DCSTypes#Vec2 The Vec2 coordinate. -function ZONE_POLYGON_BASE:GetRandomVec2() - self:F2() - - --- It is a bit tricky to find a random point within a polygon. Right now i am doing it the dirty and inefficient way... - local Vec2Found = false - local Vec2 - local BS = self:GetBoundingSquare() - - self:T2( BS ) - - while Vec2Found == false do - Vec2 = { x = math.random( BS.x1, BS.x2 ), y = math.random( BS.y1, BS.y2 ) } - self:T2( Vec2 ) - if self:IsVec2InZone( Vec2 ) then - Vec2Found = true - end - end - - self:T2( Vec2 ) - - return Vec2 -end - ---- Return a @{Point#POINT_VEC2} object representing a random 2D point at landheight within the zone. --- @param #ZONE_POLYGON_BASE self --- @return @{Point#POINT_VEC2} -function ZONE_POLYGON_BASE:GetRandomPointVec2() - self:F2() - - local PointVec2 = POINT_VEC2:NewFromVec2( self:GetRandomVec2() ) - - self:T2( PointVec2 ) - - return PointVec2 -end - ---- Return a @{Point#POINT_VEC3} object representing a random 3D point at landheight within the zone. --- @param #ZONE_POLYGON_BASE self --- @return @{Point#POINT_VEC3} -function ZONE_POLYGON_BASE:GetRandomPointVec3() - self:F2() - - local PointVec3 = POINT_VEC3:NewFromVec2( self:GetRandomVec2() ) - - self:T2( PointVec3 ) - - return PointVec3 -end - - ---- Get the bounding square the zone. --- @param #ZONE_POLYGON_BASE self --- @return #ZONE_POLYGON_BASE.BoundingSquare The bounding square. -function ZONE_POLYGON_BASE:GetBoundingSquare() - - local x1 = self.Polygon[1].x - local y1 = self.Polygon[1].y - local x2 = self.Polygon[1].x - local y2 = self.Polygon[1].y - - for i = 2, #self.Polygon do - self:T2( { self.Polygon[i], x1, y1, x2, y2 } ) - x1 = ( x1 > self.Polygon[i].x ) and self.Polygon[i].x or x1 - x2 = ( x2 < self.Polygon[i].x ) and self.Polygon[i].x or x2 - y1 = ( y1 > self.Polygon[i].y ) and self.Polygon[i].y or y1 - y2 = ( y2 < self.Polygon[i].y ) and self.Polygon[i].y or y2 - - end - - return { x1 = x1, y1 = y1, x2 = x2, y2 = y2 } -end - - ---- @type ZONE_POLYGON --- @extends Core.Zone#ZONE_POLYGON_BASE - - ---- # 7) 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. --- --- === --- --- @field #ZONE_POLYGON ZONE_POLYGON --- -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 = {}, - COUNTRY_ID = {}, - COUNTRY_NAME = {}, - NavPoints = {}, -} - -local _DATABASECoalition = - { - [1] = "Red", - [2] = "Blue", - } - -local _DATABASECategory = - { - ["plane"] = Unit.Category.AIRPLANE, - ["helicopter"] = Unit.Category.HELICOPTER, - ["vehicle"] = Unit.Category.GROUND_UNIT, - ["ship"] = Unit.Category.SHIP, - ["static"] = Unit.Category.STRUCTURE, - } - - ---- Creates a new DATABASE object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. --- @param #DATABASE self --- @return #DATABASE --- @usage --- -- Define a new DATABASE Object. This DBObject will contain a reference to all Group and Unit Templates defined within the ME and the DCSRTE. --- DBObject = DATABASE:New() -function DATABASE:New() - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) - - self:SetEventPriority( 1 ) - - self:HandleEvent( EVENTS.Birth, self._EventOnBirth ) - self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) - self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) - - -- Follow alive players and clients - self:HandleEvent( EVENTS.PlayerEnterUnit, self._EventOnPlayerEnterUnit ) - self:HandleEvent( EVENTS.PlayerLeaveUnit, self._EventOnPlayerLeaveUnit ) - - self:_RegisterTemplates() - self:_RegisterGroupsAndUnits() - self:_RegisterClients() - self:_RegisterStatics() - self:_RegisterPlayers() - self:_RegisterAirbases() - - return self -end - ---- Finds a Unit based on the Unit Name. --- @param #DATABASE self --- @param #string UnitName --- @return Wrapper.Unit#UNIT The found Unit. -function DATABASE:FindUnit( UnitName ) - - local UnitFound = self.UNITS[UnitName] - return UnitFound -end - - ---- Adds a Unit based on the Unit Name in the DATABASE. --- @param #DATABASE self -function DATABASE:AddUnit( DCSUnitName ) - - if not self.UNITS[DCSUnitName] then - local UnitRegister = UNIT:Register( DCSUnitName ) - self.UNITS[DCSUnitName] = UNIT:Register( DCSUnitName ) - end - - return self.UNITS[DCSUnitName] -end - - ---- Deletes a Unit from the DATABASE based on the Unit Name. --- @param #DATABASE self -function DATABASE:DeleteUnit( DCSUnitName ) - - --self.UNITS[DCSUnitName] = nil -end - ---- Adds a Static based on the Static Name in the DATABASE. --- @param #DATABASE self -function DATABASE:AddStatic( DCSStaticName ) - - if not self.STATICS[DCSStaticName] then - self.STATICS[DCSStaticName] = STATIC:Register( DCSStaticName ) - end -end - - ---- Deletes a Static from the DATABASE based on the Static Name. --- @param #DATABASE self -function DATABASE:DeleteStatic( DCSStaticName ) - - --self.STATICS[DCSStaticName] = nil -end - ---- Finds a STATIC based on the StaticName. --- @param #DATABASE self --- @param #string StaticName --- @return Wrapper.Static#STATIC The found STATIC. -function DATABASE:FindStatic( StaticName ) - - local StaticFound = self.STATICS[StaticName] - return StaticFound -end - ---- Adds a Airbase based on the Airbase Name in the DATABASE. --- @param #DATABASE self -function DATABASE:AddAirbase( DCSAirbaseName ) - - if not self.AIRBASES[DCSAirbaseName] then - self.AIRBASES[DCSAirbaseName] = AIRBASE:Register( DCSAirbaseName ) - end -end - - ---- Deletes a Airbase from the DATABASE based on the Airbase Name. --- @param #DATABASE self -function DATABASE:DeleteAirbase( DCSAirbaseName ) - - --self.AIRBASES[DCSAirbaseName] = nil -end - ---- Finds a AIRBASE based on the AirbaseName. --- @param #DATABASE self --- @param #string AirbaseName --- @return Wrapper.Airbase#AIRBASE The found AIRBASE. -function DATABASE:FindAirbase( AirbaseName ) - - local AirbaseFound = self.AIRBASES[AirbaseName] - return AirbaseFound -end - - ---- Finds a CLIENT based on the ClientName. --- @param #DATABASE self --- @param #string ClientName --- @return Wrapper.Client#CLIENT The found CLIENT. -function DATABASE:FindClient( ClientName ) - - local ClientFound = self.CLIENTS[ClientName] - return ClientFound -end - - ---- Adds a CLIENT based on the ClientName in the DATABASE. --- @param #DATABASE self -function DATABASE:AddClient( ClientName ) - - if not self.CLIENTS[ClientName] then - self.CLIENTS[ClientName] = CLIENT:Register( ClientName ) - end - - return self.CLIENTS[ClientName] -end - - ---- Finds a GROUP based on the GroupName. --- @param #DATABASE self --- @param #string GroupName --- @return Wrapper.Group#GROUP The found GROUP. -function DATABASE:FindGroup( GroupName ) - - local GroupFound = self.GROUPS[GroupName] - return GroupFound -end - - ---- Adds a GROUP based on the GroupName in the DATABASE. --- @param #DATABASE self -function DATABASE:AddGroup( GroupName ) - - if not self.GROUPS[GroupName] then - self:E( { "Add GROUP:", GroupName } ) - self.GROUPS[GroupName] = GROUP:Register( GroupName ) - end - - return self.GROUPS[GroupName] -end - ---- Adds a player based on the Player Name in the DATABASE. --- @param #DATABASE self -function DATABASE:AddPlayer( UnitName, PlayerName ) - - if PlayerName then - self:E( { "Add player for unit:", UnitName, PlayerName } ) - self.PLAYERS[PlayerName] = self:FindUnit( UnitName ) - self.PLAYERSJOINED[PlayerName] = PlayerName - end -end - ---- Deletes a player from the DATABASE based on the Player Name. --- @param #DATABASE self -function DATABASE:DeletePlayer( PlayerName ) - - if PlayerName then - self:E( { "Clean player:", PlayerName } ) - self.PLAYERS[PlayerName] = nil - end -end - - ---- Instantiate new Groups within the DCSRTE. --- This method expects EXACTLY the same structure as a structure within the ME, and needs 2 additional fields defined: --- SpawnCountryID, SpawnCategoryID --- This method is used by the SPAWN class. --- @param #DATABASE self --- @param #table SpawnTemplate --- @return #DATABASE self -function DATABASE:Spawn( SpawnTemplate ) - self:F( SpawnTemplate.name ) - - self:T( { SpawnTemplate.SpawnCountryID, SpawnTemplate.SpawnCategoryID } ) - - -- Copy the spawn variables of the template in temporary storage, nullify, and restore the spawn variables. - local SpawnCoalitionID = SpawnTemplate.CoalitionID - local SpawnCountryID = SpawnTemplate.CountryID - local SpawnCategoryID = SpawnTemplate.CategoryID - - -- Nullify - SpawnTemplate.CoalitionID = nil - SpawnTemplate.CountryID = nil - SpawnTemplate.CategoryID = nil - - self:_RegisterTemplate( SpawnTemplate, SpawnCoalitionID, SpawnCategoryID, SpawnCountryID ) - - self:T3( SpawnTemplate ) - coalition.addGroup( SpawnCountryID, SpawnCategoryID, SpawnTemplate ) - - -- Restore - SpawnTemplate.CoalitionID = SpawnCoalitionID - SpawnTemplate.CountryID = SpawnCountryID - SpawnTemplate.CategoryID = SpawnCategoryID - - local SpawnGroup = self:AddGroup( SpawnTemplate.name ) - return SpawnGroup -end - ---- Set a status to a Group within the Database, this to check crossing events for example. -function DATABASE:SetStatusGroup( GroupName, Status ) - self:F2( Status ) - - self.Templates.Groups[GroupName].Status = Status -end - ---- Get a status to a Group within the Database, this to check crossing events for example. -function DATABASE:GetStatusGroup( GroupName ) - self:F2( Status ) - - if self.Templates.Groups[GroupName] then - return self.Templates.Groups[GroupName].Status - else - return "" - end -end - ---- Private method that registers new Group Templates within the DATABASE Object. --- @param #DATABASE self --- @param #table GroupTemplate --- @return #DATABASE self -function DATABASE:_RegisterTemplate( GroupTemplate, CoalitionID, CategoryID, CountryID ) - - local GroupTemplateName = env.getValueDictByKey(GroupTemplate.name) - - local TraceTable = {} - - if not self.Templates.Groups[GroupTemplateName] then - self.Templates.Groups[GroupTemplateName] = {} - self.Templates.Groups[GroupTemplateName].Status = nil - end - - -- Delete the spans from the route, it is not needed and takes memory. - if GroupTemplate.route and GroupTemplate.route.spans then - GroupTemplate.route.spans = nil - end - - GroupTemplate.CategoryID = CategoryID - GroupTemplate.CoalitionID = CoalitionID - GroupTemplate.CountryID = CountryID - - self.Templates.Groups[GroupTemplateName].GroupName = GroupTemplateName - self.Templates.Groups[GroupTemplateName].Template = GroupTemplate - self.Templates.Groups[GroupTemplateName].groupId = GroupTemplate.groupId - self.Templates.Groups[GroupTemplateName].UnitCount = #GroupTemplate.units - self.Templates.Groups[GroupTemplateName].Units = GroupTemplate.units - self.Templates.Groups[GroupTemplateName].CategoryID = CategoryID - self.Templates.Groups[GroupTemplateName].CoalitionID = CoalitionID - self.Templates.Groups[GroupTemplateName].CountryID = CountryID - - - TraceTable[#TraceTable+1] = "Group" - TraceTable[#TraceTable+1] = self.Templates.Groups[GroupTemplateName].GroupName - - TraceTable[#TraceTable+1] = "Coalition" - TraceTable[#TraceTable+1] = self.Templates.Groups[GroupTemplateName].CoalitionID - TraceTable[#TraceTable+1] = "Category" - TraceTable[#TraceTable+1] = self.Templates.Groups[GroupTemplateName].CategoryID - TraceTable[#TraceTable+1] = "Country" - TraceTable[#TraceTable+1] = self.Templates.Groups[GroupTemplateName].CountryID - - TraceTable[#TraceTable+1] = "Units" - - for unit_num, UnitTemplate in pairs( GroupTemplate.units ) do - - UnitTemplate.name = env.getValueDictByKey(UnitTemplate.name) - - self.Templates.Units[UnitTemplate.name] = {} - self.Templates.Units[UnitTemplate.name].UnitName = UnitTemplate.name - self.Templates.Units[UnitTemplate.name].Template = UnitTemplate - self.Templates.Units[UnitTemplate.name].GroupName = GroupTemplateName - self.Templates.Units[UnitTemplate.name].GroupTemplate = GroupTemplate - self.Templates.Units[UnitTemplate.name].GroupId = GroupTemplate.groupId - self.Templates.Units[UnitTemplate.name].CategoryID = CategoryID - self.Templates.Units[UnitTemplate.name].CoalitionID = CoalitionID - self.Templates.Units[UnitTemplate.name].CountryID = CountryID - - if UnitTemplate.skill and (UnitTemplate.skill == "Client" or UnitTemplate.skill == "Player") then - self.Templates.ClientsByName[UnitTemplate.name] = UnitTemplate - self.Templates.ClientsByName[UnitTemplate.name].CategoryID = CategoryID - self.Templates.ClientsByName[UnitTemplate.name].CoalitionID = CoalitionID - self.Templates.ClientsByName[UnitTemplate.name].CountryID = CountryID - self.Templates.ClientsByID[UnitTemplate.unitId] = UnitTemplate - end - - TraceTable[#TraceTable+1] = self.Templates.Units[UnitTemplate.name].UnitName - end - - self:E( TraceTable ) -end - -function DATABASE:GetGroupTemplate( GroupName ) - local GroupTemplate = self.Templates.Groups[GroupName].Template - GroupTemplate.SpawnCoalitionID = self.Templates.Groups[GroupName].CoalitionID - GroupTemplate.SpawnCategoryID = self.Templates.Groups[GroupName].CategoryID - GroupTemplate.SpawnCountryID = self.Templates.Groups[GroupName].CountryID - return GroupTemplate -end - -function DATABASE:GetGroupNameFromUnitName( UnitName ) - return self.Templates.Units[UnitName].GroupName -end - -function DATABASE:GetGroupTemplateFromUnitName( UnitName ) - return self.Templates.Units[UnitName].GroupTemplate -end - -function DATABASE:GetCoalitionFromClientTemplate( ClientName ) - return self.Templates.ClientsByName[ClientName].CoalitionID -end - -function DATABASE:GetCategoryFromClientTemplate( ClientName ) - return self.Templates.ClientsByName[ClientName].CategoryID -end - -function DATABASE:GetCountryFromClientTemplate( ClientName ) - return self.Templates.ClientsByName[ClientName].CountryID -end - ---- Airbase - -function DATABASE:GetCoalitionFromAirbase( AirbaseName ) - return self.AIRBASES[AirbaseName]:GetCoalition() -end - -function DATABASE:GetCategoryFromAirbase( AirbaseName ) - return self.AIRBASES[AirbaseName]:GetCategory() -end - - - ---- Private method that registers all alive players in the mission. --- @param #DATABASE self --- @return #DATABASE self -function DATABASE:_RegisterPlayers() - - local CoalitionsData = { AlivePlayersRed = coalition.getPlayers( coalition.side.RED ), AlivePlayersBlue = coalition.getPlayers( coalition.side.BLUE ) } - for CoalitionId, CoalitionData in pairs( CoalitionsData ) do - for UnitId, UnitData in pairs( CoalitionData ) do - self:T3( { "UnitData:", UnitData } ) - if UnitData and UnitData:isExist() then - local UnitName = UnitData:getName() - local PlayerName = UnitData:getPlayerName() - if not self.PLAYERS[PlayerName] then - self:E( { "Add player for unit:", UnitName, PlayerName } ) - self:AddPlayer( UnitName, PlayerName ) - end - end - end - end - - return self -end - - ---- Private method that registers all Groups and Units within in the mission. --- @param #DATABASE self --- @return #DATABASE self -function DATABASE:_RegisterGroupsAndUnits() - - local CoalitionsData = { GroupsRed = coalition.getGroups( coalition.side.RED ), GroupsBlue = coalition.getGroups( coalition.side.BLUE ) } - for CoalitionId, CoalitionData in pairs( CoalitionsData ) do - for DCSGroupId, DCSGroup in pairs( CoalitionData ) do - - if DCSGroup:isExist() then - local DCSGroupName = DCSGroup:getName() - - self:E( { "Register Group:", DCSGroupName } ) - self:AddGroup( DCSGroupName ) - - for DCSUnitId, DCSUnit in pairs( DCSGroup:getUnits() ) do - - local DCSUnitName = DCSUnit:getName() - self:E( { "Register Unit:", DCSUnitName } ) - self:AddUnit( DCSUnitName ) - end - else - self:E( { "Group does not exist: ", DCSGroup } ) - end - - end - end - - return self -end - ---- Private method that registers all Units of skill Client or Player within in the mission. --- @param #DATABASE self --- @return #DATABASE self -function DATABASE:_RegisterClients() - - for ClientName, ClientTemplate in pairs( self.Templates.ClientsByName ) do - self:E( { "Register Client:", ClientName } ) - self:AddClient( ClientName ) - end - - return self -end - ---- @param #DATABASE self -function DATABASE:_RegisterStatics() - - local CoalitionsData = { GroupsRed = coalition.getStaticObjects( coalition.side.RED ), GroupsBlue = coalition.getStaticObjects( coalition.side.BLUE ) } - for CoalitionId, CoalitionData in pairs( CoalitionsData ) do - for DCSStaticId, DCSStatic in pairs( CoalitionData ) do - - if DCSStatic:isExist() then - local DCSStaticName = DCSStatic:getName() - - self:E( { "Register Static:", DCSStaticName } ) - self:AddStatic( DCSStaticName ) - else - self:E( { "Static does not exist: ", DCSStatic } ) - end - end - end - - return self -end - ---- @param #DATABASE self -function DATABASE:_RegisterAirbases() - - local CoalitionsData = { AirbasesRed = coalition.getAirbases( coalition.side.RED ), AirbasesBlue = coalition.getAirbases( coalition.side.BLUE ), AirbasesNeutral = coalition.getAirbases( coalition.side.NEUTRAL ) } - for CoalitionId, CoalitionData in pairs( CoalitionsData ) do - for DCSAirbaseId, DCSAirbase in pairs( CoalitionData ) do - - local DCSAirbaseName = DCSAirbase:getName() - - self:E( { "Register Airbase:", DCSAirbaseName } ) - self:AddAirbase( DCSAirbaseName ) - end - end - - return self -end - - ---- Events - ---- Handles the OnBirth event for the alive units set. --- @param #DATABASE self --- @param Core.Event#EVENTDATA Event -function DATABASE:_EventOnBirth( Event ) - self:F2( { Event } ) - - if Event.IniDCSUnit then - if Event.IniObjectCategory == 3 then - self:AddStatic( Event.IniDCSUnitName ) - else - if Event.IniObjectCategory == 1 then - self:AddUnit( Event.IniDCSUnitName ) - self:AddGroup( Event.IniDCSGroupName ) - end - end - self:_EventOnPlayerEnterUnit( Event ) - end -end - - ---- Handles the OnDead or OnCrash event for alive units set. --- @param #DATABASE self --- @param Core.Event#EVENTDATA Event -function DATABASE:_EventOnDeadOrCrash( Event ) - self:F2( { Event } ) - - if Event.IniDCSUnit then - if Event.IniObjectCategory == 3 then - if self.STATICS[Event.IniDCSUnitName] then - self:DeleteStatic( Event.IniDCSUnitName ) - end - else - if Event.IniObjectCategory == 1 then - if self.UNITS[Event.IniDCSUnitName] then - self:DeleteUnit( Event.IniDCSUnitName ) - end - end - end - end -end - - ---- Handles the OnPlayerEnterUnit event to fill the active players table (with the unit filter applied). --- @param #DATABASE self --- @param Core.Event#EVENTDATA Event -function DATABASE:_EventOnPlayerEnterUnit( Event ) - self:F2( { Event } ) - - if Event.IniUnit then - if Event.IniObjectCategory == 1 then - self:AddUnit( Event.IniDCSUnitName ) - self:AddGroup( Event.IniDCSGroupName ) - local PlayerName = Event.IniUnit:GetPlayerName() - if not self.PLAYERS[PlayerName] then - self:AddPlayer( Event.IniUnitName, PlayerName ) - end - end - end -end - - ---- Handles the OnPlayerLeaveUnit event to clean the active players table. --- @param #DATABASE self --- @param Core.Event#EVENTDATA Event -function DATABASE:_EventOnPlayerLeaveUnit( Event ) - self:F2( { Event } ) - - if Event.IniUnit then - if Event.IniObjectCategory == 1 then - local PlayerName = Event.IniUnit:GetPlayerName() - if self.PLAYERS[PlayerName] then - self:DeletePlayer( PlayerName ) - end - end - end -end - ---- Iterators - ---- Iterate the DATABASE and call an iterator function for the given set, providing the Object for each element within the set and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called when there is an alive player in the database. --- @return #DATABASE self -function DATABASE:ForEach( IteratorFunction, FinalizeFunction, arg, Set ) - self:F2( arg ) - - local function CoRoutine() - local Count = 0 - for ObjectID, Object in pairs( Set ) do - self:T2( Object ) - IteratorFunction( Object, unpack( arg ) ) - Count = Count + 1 --- if Count % 100 == 0 then --- coroutine.yield( false ) --- end - end - return true - end - --- local co = coroutine.create( CoRoutine ) - local co = CoRoutine - - local function Schedule() - --- local status, res = coroutine.resume( co ) - local status, res = co() - self:T3( { status, res } ) - - if status == false then - error( res ) - end - if res == false then - return true -- resume next time the loop - end - if FinalizeFunction then - FinalizeFunction( unpack( arg ) ) - end - return false - end - - local Scheduler = SCHEDULER:New( self, Schedule, {}, 0.001, 0.001, 0 ) - - return self -end - - ---- Iterate the DATABASE and call an iterator function for each **alive** UNIT, providing the UNIT and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the database. The function needs to accept a UNIT parameter. --- @return #DATABASE self -function DATABASE:ForEachUnit( IteratorFunction, FinalizeFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, FinalizeFunction, arg, self.UNITS ) - - return self -end - ---- Iterate the DATABASE and call an iterator function for each **alive** GROUP, providing the GROUP and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the database. The function needs to accept a GROUP parameter. --- @return #DATABASE self -function DATABASE:ForEachGroup( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.GROUPS ) - - return self -end - - ---- Iterate the DATABASE and call an iterator function for each **ALIVE** player, providing the player name and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called when there is an player in the database. The function needs to accept the player name. --- @return #DATABASE self -function DATABASE:ForEachPlayer( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.PLAYERS ) - - return self -end - - ---- Iterate the DATABASE and call an iterator function for each player who has joined the mission, providing the Unit of the player and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called when there is was a player in the database. The function needs to accept a UNIT parameter. --- @return #DATABASE self -function DATABASE:ForEachPlayerJoined( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.PLAYERSJOINED ) - - return self -end - ---- Iterate the DATABASE and call an iterator function for each CLIENT, providing the CLIENT to the function and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called when there is an alive player in the database. The function needs to accept a CLIENT parameter. --- @return #DATABASE self -function DATABASE:ForEachClient( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.CLIENTS ) - - return self -end - - -function DATABASE:_RegisterTemplates() - self:F2() - - self.Navpoints = {} - self.UNITS = {} - --Build routines.db.units and self.Navpoints - for CoalitionName, coa_data in pairs(env.mission.coalition) do - - if (CoalitionName == 'red' or CoalitionName == 'blue') and type(coa_data) == 'table' then - --self.Units[coa_name] = {} - - local CoalitionSide = coalition.side[string.upper(CoalitionName)] - - ---------------------------------------------- - -- build nav points DB - self.Navpoints[CoalitionName] = {} - if coa_data.nav_points then --navpoints - for nav_ind, nav_data in pairs(coa_data.nav_points) do - - if type(nav_data) == 'table' then - self.Navpoints[CoalitionName][nav_ind] = routines.utils.deepCopy(nav_data) - - self.Navpoints[CoalitionName][nav_ind]['name'] = nav_data.callsignStr -- name is a little bit more self-explanatory. - self.Navpoints[CoalitionName][nav_ind]['point'] = {} -- point is used by SSE, support it. - self.Navpoints[CoalitionName][nav_ind]['point']['x'] = nav_data.x - self.Navpoints[CoalitionName][nav_ind]['point']['y'] = 0 - self.Navpoints[CoalitionName][nav_ind]['point']['z'] = nav_data.y - end - end - end - ------------------------------------------------- - if coa_data.country then --there is a country table - for cntry_id, cntry_data in pairs(coa_data.country) do - - local CountryName = string.upper(cntry_data.name) - local CountryID = cntry_data.id - - self.COUNTRY_ID[CountryName] = CountryID - self.COUNTRY_NAME[CountryID] = CountryName - - --self.Units[coa_name][countryName] = {} - --self.Units[coa_name][countryName]["countryId"] = cntry_data.id - - if type(cntry_data) == 'table' then --just making sure - - for obj_type_name, obj_type_data in pairs(cntry_data) do - - if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then --should be an unncessary check - - local CategoryName = obj_type_name - - if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! - - --self.Units[coa_name][countryName][category] = {} - - for group_num, GroupTemplate in pairs(obj_type_data.group) do - - if GroupTemplate and GroupTemplate.units and type(GroupTemplate.units) == 'table' then --making sure again- this is a valid group - self:_RegisterTemplate( - GroupTemplate, - CoalitionSide, - _DATABASECategory[string.lower(CategoryName)], - CountryID - ) - end --if GroupTemplate and GroupTemplate.units then - end --for group_num, GroupTemplate in pairs(obj_type_data.group) do - end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then - end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then - end --for obj_type_name, obj_type_data in pairs(cntry_data) do - end --if type(cntry_data) == 'table' then - end --for cntry_id, cntry_data in pairs(coa_data.country) do - end --if coa_data.country then --there is a country table - end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then - end --for coa_name, coa_data in pairs(mission.coalition) do - - return self -end - - - - ---- **Core** - SET_ classes define **collections** of objects to perform **bulk actions** and logically **group** objects. --- --- ![Banner Image](..\Presentations\SET\Dia1.JPG) --- --- === --- --- SET_ classes group objects of the same type into a collection, which is either: --- --- * Manually managed using the **:Add...()** or **:Remove...()** methods. The initial SET can be filtered with the **@{#SET_BASE.FilterOnce}()** method --- * Dynamically updated when new objects are created or objects are destroyed using the **@{#SET_BASE.FilterStart}()** method. --- --- Various types of SET_ classes are available: --- --- * @{#SET_UNIT}: Defines a colleciton of @{Unit}s filtered by filter criteria. --- * @{#SET_GROUP}: Defines a collection of @{Group}s filtered by filter criteria. --- * @{#SET_CLIENT}: Defines a collection of @{Client}s filterd by filter criteria. --- * @{#SET_AIRBASE}: Defines a collection of @{Airbase}s filtered by filter criteria. --- --- These classes are derived from @{#SET_BASE}, which contains the main methods to manage SETs. --- --- A multitude of other methods are available in SET_ classes that allow to: --- --- * Validate the presence of objects in the SET. --- * Trigger events when objects in the SET change a zone presence. --- --- ### Authors: --- --- * FlightControl : Design & Programming --- --- ### Contributions: --- --- --- @module Set - - ---- @type SET_BASE --- @field #table Filter --- @field #table Set --- @field #table List --- @field Core.Scheduler#SCHEDULER CallScheduler --- @extends Core.Base#BASE - - ---- # 1) SET_BASE class, extends @{Base#BASE} --- The @{Set#SET_BASE} class defines the core functions that define a collection of objects. --- A SET provides iterators to iterate the SET, but will **temporarily** yield the ForEach interator loop at defined **"intervals"** to the mail simulator loop. --- In this way, large loops can be done while not blocking the simulator main processing loop. --- The default **"yield interval"** is after 10 objects processed. --- The default **"time interval"** is after 0.001 seconds. --- --- ## 1.1) Add or remove objects from the SET --- --- Some key core functions are @{Set#SET_BASE.Add} and @{Set#SET_BASE.Remove} to add or remove objects from the SET in your logic. --- --- ## 1.2) Define the SET iterator **"yield interval"** and the **"time interval"** --- --- Modify the iterator intervals with the @{Set#SET_BASE.SetInteratorIntervals} method. --- You can set the **"yield interval"**, and the **"time interval"**. (See above). --- --- @field #SET_BASE SET_BASE -SET_BASE = { - ClassName = "SET_BASE", - Filter = {}, - Set = {}, - List = {}, - Index = {}, -} - - ---- Creates a new SET_BASE object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. --- @param #SET_BASE self --- @return #SET_BASE --- @usage --- -- Define a new SET_BASE Object. This DBObject will contain a reference to all Group and Unit Templates defined within the ME and the DCSRTE. --- DBObject = SET_BASE:New() -function SET_BASE:New( Database ) - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) -- Core.Set#SET_BASE - - self.Database = Database - - self.YieldInterval = 10 - self.TimeInterval = 0.001 - - self.Set = {} - - self.List = {} - self.List.__index = self.List - self.List = setmetatable( { Count = 0 }, self.List ) - - self.Index = {} - - self.CallScheduler = SCHEDULER:New( self ) - - self:SetEventPriority( 2 ) - - return self -end - ---- Finds an @{Base#BASE} object based on the object Name. --- @param #SET_BASE self --- @param #string ObjectName --- @return Core.Base#BASE The Object found. -function SET_BASE:_Find( ObjectName ) - - local ObjectFound = self.Set[ObjectName] - return ObjectFound -end - - ---- Gets the Set. --- @param #SET_BASE self --- @return #SET_BASE self -function SET_BASE:GetSet() - self:F2() - - return self.Set -end - ---- 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._ - - table.insert( self.Index, ObjectName ) - -end - ---- Adds a @{Base#BASE} object in the @{Set#SET_BASE}, using the Object Name as the index. --- @param #SET_BASE self --- @param Wrapper.Object#OBJECT Object --- @return Core.Base#BASE The added BASE Object. -function SET_BASE:AddObject( Object ) - self:F2( Object.ObjectName ) - - self:T( Object.UnitName ) - self:T( Object.ObjectName ) - self:Add( Object.ObjectName, Object ) - -end - - - ---- 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 - - for Index, Key in ipairs( self.Index ) do - if Key == ObjectName then - table.remove( self.Index, Index ) - break - end - end - - 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 - ---- Gets the first object from the @{Set#SET_BASE} and derived classes. --- @param #SET_BASE self --- @return Core.Base#BASE -function SET_BASE:GetFirst() - self:F() - - local ObjectName = self.Index[1] - local FirstObject = self.Set[ObjectName] - self:T3( { FirstObject } ) - return FirstObject -end - ---- Gets the last object from the @{Set#SET_BASE} and derived classes. --- @param #SET_BASE self --- @return Core.Base#BASE -function SET_BASE:GetLast() - self:F() - - local ObjectName = self.Index[#self.Index] - local LastObject = self.Set[ObjectName] - self:T3( { LastObject } ) - return LastObject -end - ---- Gets a random object from the @{Set#SET_BASE} and derived classes. --- @param #SET_BASE self --- @return Core.Base#BASE -function SET_BASE:GetRandom() - self:F() - - local RandomItem = self.Set[self.Index[math.random(#self.Index)]] - - self:T3( { RandomItem } ) - - return RandomItem -end - - ---- Retrieves the amount of objects in the @{Set#SET_BASE} and derived classes. --- @param #SET_BASE self --- @return #number Count -function SET_BASE:Count() - - return #self.Index or 0 -end - - - ---- Copies the Filter criteria from a given Set (for rebuilding a new Set based on an existing Set). --- @param #SET_BASE self --- @param #SET_BASE BaseSet --- @return #SET_BASE -function SET_BASE:SetDatabase( BaseSet ) - - -- Copy the filter criteria of the BaseSet - local OtherFilter = routines.utils.deepCopy( BaseSet.Filter ) - self.Filter = OtherFilter - - -- Now base the new Set on the BaseSet - self.Database = BaseSet:GetSet() - return self -end - - - ---- Define the SET iterator **"yield interval"** and the **"time interval"**. --- @param #SET_BASE self --- @param #number YieldInterval Sets the frequency when the iterator loop will yield after the number of objects processed. The default frequency is 10 objects processed. --- @param #number TimeInterval Sets the time in seconds when the main logic will resume the iterator loop. The default time is 0.001 seconds. --- @return #SET_BASE self -function SET_BASE:SetIteratorIntervals( YieldInterval, TimeInterval ) - - self.YieldInterval = YieldInterval - self.TimeInterval = TimeInterval - - return self -end - - ---- Filters for the defined collection. --- @param #SET_BASE self --- @return #SET_BASE self -function SET_BASE:FilterOnce() - - for ObjectName, Object in pairs( self.Database ) do - - if self:IsIncludeObject( Object ) then - self:Add( ObjectName, Object ) - end - end - - return self -end - ---- Starts the filtering for the defined collection. --- @param #SET_BASE self --- @return #SET_BASE self -function SET_BASE:_FilterStart() - - for ObjectName, Object in pairs( self.Database ) do - - if self:IsIncludeObject( Object ) then - self:E( { "Adding Object:", ObjectName } ) - self:Add( ObjectName, Object ) - end - end - - self:HandleEvent( EVENTS.Birth, self._EventOnBirth ) - self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) - self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) - - -- Follow alive players and clients - self:HandleEvent( EVENTS.PlayerEnterUnit, self._EventOnPlayerEnterUnit ) - self:HandleEvent( EVENTS.PlayerLeaveUnit, self._EventOnPlayerLeaveUnit ) - - - return self -end - ---- Stops the filtering for the defined collection. --- @param #SET_BASE self --- @return #SET_BASE self -function SET_BASE:FilterStop() - - self:UnHandleEvent( EVENTS.Birth ) - self:UnHandleEvent( EVENTS.Dead ) - self:UnHandleEvent( EVENTS.Crash ) - - return self -end - ---- Iterate the SET_BASE while identifying the nearest object from a @{Point#POINT_VEC2}. --- @param #SET_BASE self --- @param Core.Point#POINT_VEC2 PointVec2 A @{Point#POINT_VEC2} object from where to evaluate the closest object in the set. --- @return Core.Base#BASE The closest object. -function SET_BASE:FindNearestObjectFromPointVec2( PointVec2 ) - self:F2( PointVec2 ) - - local NearestObject = nil - local ClosestDistance = nil - - for ObjectID, ObjectData in pairs( self.Set ) do - if NearestObject == nil then - NearestObject = ObjectData - ClosestDistance = PointVec2:DistanceFromVec2( ObjectData:GetVec2() ) - else - local Distance = PointVec2:DistanceFromVec2( ObjectData:GetVec2() ) - if Distance < ClosestDistance then - NearestObject = ObjectData - ClosestDistance = Distance - end - end - end - - return NearestObject -end - - - ------ Private method that registers all alive players in the mission. ----- @param #SET_BASE self ----- @return #SET_BASE self ---function SET_BASE:_RegisterPlayers() --- --- local CoalitionsData = { AlivePlayersRed = coalition.getPlayers( coalition.side.RED ), AlivePlayersBlue = coalition.getPlayers( coalition.side.BLUE ) } --- for CoalitionId, CoalitionData in pairs( CoalitionsData ) do --- for UnitId, UnitData in pairs( CoalitionData ) do --- self:T3( { "UnitData:", UnitData } ) --- if UnitData and UnitData:isExist() then --- local UnitName = UnitData:getName() --- if not self.PlayersAlive[UnitName] then --- self:E( { "Add player for unit:", UnitName, UnitData:getPlayerName() } ) --- self.PlayersAlive[UnitName] = UnitData:getPlayerName() --- end --- end --- end --- end --- --- return self ---end - ---- Events - ---- Handles the OnBirth event for the Set. --- @param #SET_BASE self --- @param Core.Event#EVENTDATA Event -function SET_BASE:_EventOnBirth( Event ) - self:F3( { Event } ) - - if Event.IniDCSUnit then - local ObjectName, Object = self:AddInDatabase( Event ) - self:T3( ObjectName, Object ) - if Object and self:IsIncludeObject( Object ) then - self:Add( ObjectName, Object ) - --self:_EventOnPlayerEnterUnit( Event ) - end - end -end - ---- Handles the OnDead or OnCrash event for alive units set. --- @param #SET_BASE self --- @param Core.Event#EVENTDATA Event -function SET_BASE:_EventOnDeadOrCrash( Event ) - self:F3( { Event } ) - - if Event.IniDCSUnit then - local ObjectName, Object = self:FindInDatabase( Event ) - if ObjectName and Object ~= nil then - self:Remove( ObjectName ) - end - end -end - ---- Handles the OnPlayerEnterUnit event to fill the active players table (with the unit filter applied). --- @param #SET_BASE self --- @param Core.Event#EVENTDATA Event -function SET_BASE:_EventOnPlayerEnterUnit( Event ) - self:F3( { Event } ) - - if Event.IniDCSUnit then - local ObjectName, Object = self:AddInDatabase( Event ) - self:T3( ObjectName, Object ) - if self:IsIncludeObject( Object ) then - self:Add( ObjectName, Object ) - --self:_EventOnPlayerEnterUnit( Event ) - end - end -end - ---- Handles the OnPlayerLeaveUnit event to clean the active players table. --- @param #SET_BASE self --- @param Core.Event#EVENTDATA Event -function SET_BASE:_EventOnPlayerLeaveUnit( Event ) - self:F3( { Event } ) - - local ObjectName = Event.IniDCSUnit - if Event.IniDCSUnit then - if Event.IniDCSGroup then - local GroupUnits = Event.IniDCSGroup:getUnits() - local PlayerCount = 0 - for _, DCSUnit in pairs( GroupUnits ) do - if DCSUnit ~= Event.IniDCSUnit then - if DCSUnit:getPlayer() ~= nil then - PlayerCount = PlayerCount + 1 - end - end - end - self:E(PlayerCount) - if PlayerCount == 0 then - self:Remove( Event.IniDCSGroupName ) - end - end - end -end - --- Iterators - ---- Iterate the SET_BASE and derived classes and call an iterator function for the given SET_BASE, providing the Object for each element within the set and optional parameters. --- @param #SET_BASE self --- @param #function IteratorFunction The function that will be called. --- @return #SET_BASE self -function SET_BASE:ForEach( IteratorFunction, arg, Set, Function, FunctionArguments ) - self:F3( arg ) - - Set = Set or self:GetSet() - arg = arg or {} - - local function CoRoutine() - local Count = 0 - for ObjectID, ObjectData in pairs( Set ) do - local Object = ObjectData - self:T3( Object ) - if Function then - if Function( unpack( FunctionArguments ), Object ) == true then - IteratorFunction( Object, unpack( arg ) ) - end - else - IteratorFunction( Object, unpack( arg ) ) - end - Count = Count + 1 --- if Count % self.YieldInterval == 0 then --- coroutine.yield( false ) --- end - end - return true - end - --- local co = coroutine.create( CoRoutine ) - local co = CoRoutine - - local function Schedule() - --- local status, res = coroutine.resume( co ) - local status, res = co() - self:T3( { status, res } ) - - if status == false then - error( res ) - end - if res == false then - return true -- resume next time the loop - end - - return false - end - - --self.CallScheduler:Schedule( self, Schedule, {}, self.TimeInterval, self.TimeInterval, 0 ) - Schedule() - - return self -end - - ------ Iterate the SET_BASE and call an interator function for each **alive** unit, providing the Unit and optional parameters. ----- @param #SET_BASE self ----- @param #function IteratorFunction The function that will be called when there is an alive unit in the SET_BASE. The function needs to accept a UNIT parameter. ----- @return #SET_BASE self ---function SET_BASE:ForEachDCSUnitAlive( IteratorFunction, ... ) --- self:F3( arg ) --- --- self:ForEach( IteratorFunction, arg, self.DCSUnitsAlive ) --- --- return self ---end --- ------ Iterate the SET_BASE and call an interator function for each **alive** player, providing the Unit of the player and optional parameters. ----- @param #SET_BASE self ----- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_BASE. The function needs to accept a UNIT parameter. ----- @return #SET_BASE self ---function SET_BASE:ForEachPlayer( IteratorFunction, ... ) --- self:F3( arg ) --- --- self:ForEach( IteratorFunction, arg, self.PlayersAlive ) --- --- return self ---end --- --- ------ Iterate the SET_BASE and call an interator function for each client, providing the Client to the function and optional parameters. ----- @param #SET_BASE self ----- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_BASE. The function needs to accept a CLIENT parameter. ----- @return #SET_BASE self ---function SET_BASE:ForEachClient( IteratorFunction, ... ) --- self:F3( arg ) --- --- self:ForEach( IteratorFunction, arg, self.Clients ) --- --- return self ---end - - ---- Decides whether to include the Object --- @param #SET_BASE self --- @param #table Object --- @return #SET_BASE self -function SET_BASE:IsIncludeObject( Object ) - self:F3( Object ) - - return true -end - ---- 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 - - ---- @type SET_GROUP --- @extends Core.Set#SET_BASE - ---- # 2) 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 constructor --- --- 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. --- --- === --- @field #SET_GROUP SET_GROUP -SET_GROUP = { - ClassName = "SET_GROUP", - Filter = { - Coalitions = nil, - Categories = nil, - Countries = nil, - GroupPrefixes = nil, - }, - FilterMeta = { - Coalitions = { - red = coalition.side.RED, - blue = coalition.side.BLUE, - neutral = coalition.side.NEUTRAL, - }, - Categories = { - plane = Group.Category.AIRPLANE, - helicopter = Group.Category.HELICOPTER, - ground = Group.Category.GROUND_UNIT, - ship = Group.Category.SHIP, - structure = Group.Category.STRUCTURE, - }, - }, -} - - ---- Creates a new SET_GROUP object, building a set of groups belonging to a coalitions, categories, countries, types or with defined prefix names. --- @param #SET_GROUP self --- @return #SET_GROUP --- @usage --- -- Define a new SET_GROUP Object. This DBObject will contain a reference to all alive GROUPS. --- DBObject = SET_GROUP:New() -function SET_GROUP:New() - - -- Inherits from BASE - local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.GROUPS ) ) - - return self -end - ---- Add GROUP(s) to SET_GROUP. --- @param Core.Set#SET_GROUP self --- @param #string AddGroupNames A single name or an array of GROUP names. --- @return self -function SET_GROUP:AddGroupsByName( AddGroupNames ) - - local AddGroupNamesArray = ( type( AddGroupNames ) == "table" ) and AddGroupNames or { AddGroupNames } - - for AddGroupID, AddGroupName in pairs( AddGroupNamesArray ) do - self:Add( AddGroupName, GROUP:FindByName( AddGroupName ) ) - end - - return self -end - ---- Remove GROUP(s) from SET_GROUP. --- @param Core.Set#SET_GROUP self --- @param Wrapper.Group#GROUP RemoveGroupNames A single name or an array of GROUP names. --- @return self -function SET_GROUP:RemoveGroupsByName( RemoveGroupNames ) - - local RemoveGroupNamesArray = ( type( RemoveGroupNames ) == "table" ) and RemoveGroupNames or { RemoveGroupNames } - - for RemoveGroupID, RemoveGroupName in pairs( RemoveGroupNamesArray ) do - self:Remove( RemoveGroupName.GroupName ) - end - - return self -end - - - - ---- Finds a Group based on the Group Name. --- @param #SET_GROUP self --- @param #string GroupName --- @return Wrapper.Group#GROUP The found Group. -function SET_GROUP:FindGroup( GroupName ) - - local GroupFound = self.Set[GroupName] - return GroupFound -end - - - ---- Builds a set of groups of coalitions. --- Possible current coalitions are red, blue and neutral. --- @param #SET_GROUP self --- @param #string Coalitions Can take the following values: "red", "blue", "neutral". --- @return #SET_GROUP self -function SET_GROUP:FilterCoalitions( Coalitions ) - if not self.Filter.Coalitions then - self.Filter.Coalitions = {} - end - if type( Coalitions ) ~= "table" then - Coalitions = { Coalitions } - end - for CoalitionID, Coalition in pairs( Coalitions ) do - self.Filter.Coalitions[Coalition] = Coalition - end - return self -end - - ---- Builds a set of groups out of categories. --- Possible current categories are plane, helicopter, ground, ship. --- @param #SET_GROUP self --- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship". --- @return #SET_GROUP self -function SET_GROUP:FilterCategories( Categories ) - if not self.Filter.Categories then - self.Filter.Categories = {} - end - if type( Categories ) ~= "table" then - Categories = { Categories } - end - for CategoryID, Category in pairs( Categories ) do - self.Filter.Categories[Category] = Category - end - return self -end - ---- Builds a set of groups of defined countries. --- Possible current countries are those known within DCS world. --- @param #SET_GROUP self --- @param #string Countries Can take those country strings known within DCS world. --- @return #SET_GROUP self -function SET_GROUP:FilterCountries( Countries ) - if not self.Filter.Countries then - self.Filter.Countries = {} - end - if type( Countries ) ~= "table" then - Countries = { Countries } - end - for CountryID, Country in pairs( Countries ) do - self.Filter.Countries[Country] = Country - end - return self -end - - ---- Builds a set of groups of defined GROUP prefixes. --- All the groups starting with the given prefixes will be included within the set. --- @param #SET_GROUP self --- @param #string Prefixes The prefix of which the group name starts with. --- @return #SET_GROUP self -function SET_GROUP:FilterPrefixes( Prefixes ) - if not self.Filter.GroupPrefixes then - self.Filter.GroupPrefixes = {} - end - if type( Prefixes ) ~= "table" then - Prefixes = { Prefixes } - end - for PrefixID, Prefix in pairs( Prefixes ) do - self.Filter.GroupPrefixes[Prefix] = Prefix - end - return self -end - - ---- Starts the filtering. --- @param #SET_GROUP self --- @return #SET_GROUP self -function SET_GROUP:FilterStart() - - if _DATABASE then - self:_FilterStart() - end - - - - return self -end - ---- Handles the Database to check on an event (birth) that the Object was added in the Database. --- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! --- @param #SET_GROUP self --- @param Core.Event#EVENTDATA Event --- @return #string The name of the GROUP --- @return #table The GROUP -function SET_GROUP:AddInDatabase( Event ) - self:F3( { Event } ) - - if Event.IniObjectCategory == 1 then - if not self.Database[Event.IniDCSGroupName] then - self.Database[Event.IniDCSGroupName] = GROUP:Register( Event.IniDCSGroupName ) - self:T3( self.Database[Event.IniDCSGroupName] ) - end - end - - return Event.IniDCSGroupName, self.Database[Event.IniDCSGroupName] -end - ---- Handles the Database to check on any event that Object exists in the Database. --- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! --- @param #SET_GROUP self --- @param Core.Event#EVENTDATA Event --- @return #string The name of the GROUP --- @return #table The GROUP -function SET_GROUP:FindInDatabase( Event ) - self:F3( { Event } ) - - return Event.IniDCSGroupName, self.Database[Event.IniDCSGroupName] -end - ---- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP, providing the GROUP and optional parameters. --- @param #SET_GROUP self --- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. --- @return #SET_GROUP self -function SET_GROUP:ForEachGroup( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set ) - - return self -end - ---- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence completely in a @{Zone}, providing the GROUP and optional parameters to the called function. --- @param #SET_GROUP self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. --- @return #SET_GROUP self -function SET_GROUP:ForEachGroupCompletelyInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set, - --- @param Core.Zone#ZONE_BASE ZoneObject - -- @param Wrapper.Group#GROUP GroupObject - function( ZoneObject, GroupObject ) - if GroupObject:IsCompletelyInZone( ZoneObject ) then - return true - else - return false - end - end, { ZoneObject } ) - - return self -end - ---- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence partly in a @{Zone}, providing the GROUP and optional parameters to the called function. --- @param #SET_GROUP self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. --- @return #SET_GROUP self -function SET_GROUP:ForEachGroupPartlyInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set, - --- @param Core.Zone#ZONE_BASE ZoneObject - -- @param Wrapper.Group#GROUP GroupObject - function( ZoneObject, GroupObject ) - if GroupObject:IsPartlyInZone( ZoneObject ) then - return true - else - return false - end - end, { ZoneObject } ) - - return self -end - ---- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence not in a @{Zone}, providing the GROUP and optional parameters to the called function. --- @param #SET_GROUP self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. --- @return #SET_GROUP self -function SET_GROUP:ForEachGroupNotInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set, - --- @param Core.Zone#ZONE_BASE ZoneObject - -- @param Wrapper.Group#GROUP GroupObject - function( ZoneObject, GroupObject ) - if GroupObject:IsNotInZone( ZoneObject ) then - return true - else - return false - end - end, { ZoneObject } ) - - return self -end - - ------ Iterate the SET_GROUP and call an interator function for each **alive** player, providing the Group of the player and optional parameters. ----- @param #SET_GROUP self ----- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_GROUP. The function needs to accept a GROUP parameter. ----- @return #SET_GROUP self ---function SET_GROUP:ForEachPlayer( IteratorFunction, ... ) --- self:F2( arg ) --- --- self:ForEach( IteratorFunction, arg, self.PlayersAlive ) --- --- return self ---end --- --- ------ Iterate the SET_GROUP and call an interator function for each client, providing the Client to the function and optional parameters. ----- @param #SET_GROUP self ----- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_GROUP. The function needs to accept a CLIENT parameter. ----- @return #SET_GROUP self ---function SET_GROUP:ForEachClient( IteratorFunction, ... ) --- self:F2( arg ) --- --- self:ForEach( IteratorFunction, arg, self.Clients ) --- --- return self ---end - - ---- --- @param #SET_GROUP self --- @param Wrapper.Group#GROUP MooseGroup --- @return #SET_GROUP self -function SET_GROUP:IsIncludeObject( MooseGroup ) - self:F2( MooseGroup ) - local MooseGroupInclude = true - - if self.Filter.Coalitions then - local MooseGroupCoalition = false - for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do - self:T3( { "Coalition:", MooseGroup:GetCoalition(), self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) - if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == MooseGroup:GetCoalition() then - MooseGroupCoalition = true - end - end - MooseGroupInclude = MooseGroupInclude and MooseGroupCoalition - end - - if self.Filter.Categories then - local MooseGroupCategory = false - for CategoryID, CategoryName in pairs( self.Filter.Categories ) do - self:T3( { "Category:", MooseGroup:GetCategory(), self.FilterMeta.Categories[CategoryName], CategoryName } ) - if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == MooseGroup:GetCategory() then - MooseGroupCategory = true - end - end - MooseGroupInclude = MooseGroupInclude and MooseGroupCategory - end - - if self.Filter.Countries then - local MooseGroupCountry = false - for CountryID, CountryName in pairs( self.Filter.Countries ) do - self:T3( { "Country:", MooseGroup:GetCountry(), CountryName } ) - if country.id[CountryName] == MooseGroup:GetCountry() then - MooseGroupCountry = true - end - end - MooseGroupInclude = MooseGroupInclude and MooseGroupCountry - end - - if self.Filter.GroupPrefixes then - local MooseGroupPrefix = false - for GroupPrefixId, GroupPrefix in pairs( self.Filter.GroupPrefixes ) do - self:T3( { "Prefix:", string.find( MooseGroup:GetName(), GroupPrefix, 1 ), GroupPrefix } ) - if string.find( MooseGroup:GetName(), GroupPrefix, 1 ) then - MooseGroupPrefix = true - end - end - MooseGroupInclude = MooseGroupInclude and MooseGroupPrefix - end - - self:T2( MooseGroupInclude ) - return MooseGroupInclude -end - ---- @type SET_UNIT --- @extends Core.Set#SET_BASE - ---- # 3) SET_UNIT class, extends @{Set#SET_BASE} --- --- Mission designers can use the SET_UNIT class to build sets of units belonging to certain: --- --- * Coalitions --- * Categories --- * Countries --- * Unit types --- * Starting with certain prefix strings. --- --- ## 3.1) SET_UNIT constructor --- --- Create a new SET_UNIT object with the @{#SET_UNIT.New} method: --- --- * @{#SET_UNIT.New}: Creates a new SET_UNIT object. --- --- ## 3.2) Add or Remove UNIT(s) from SET_UNIT --- --- 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. --- --- ## 3.5 ) SET_UNIT atomic methods --- --- Various methods exist for a SET_UNIT to perform actions or calculations and retrieve results from the SET_UNIT: --- --- * @{#SET_UNIT.GetTypeNames}(): Retrieve the type names of the @{Unit}s in the SET, delimited by a comma. --- --- === --- @field #SET_UNIT SET_UNIT -SET_UNIT = { - ClassName = "SET_UNIT", - Units = {}, - Filter = { - Coalitions = nil, - Categories = nil, - Types = nil, - Countries = nil, - UnitPrefixes = nil, - }, - FilterMeta = { - Coalitions = { - red = coalition.side.RED, - blue = coalition.side.BLUE, - neutral = coalition.side.NEUTRAL, - }, - Categories = { - plane = Unit.Category.AIRPLANE, - helicopter = Unit.Category.HELICOPTER, - ground = Unit.Category.GROUND_UNIT, - ship = Unit.Category.SHIP, - structure = Unit.Category.STRUCTURE, - }, - }, -} - - ---- Creates a new SET_UNIT object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. --- @param #SET_UNIT self --- @return #SET_UNIT --- @usage --- -- Define a new SET_UNIT Object. This DBObject will contain a reference to all alive Units. --- DBObject = SET_UNIT:New() -function SET_UNIT:New() - - -- Inherits from BASE - local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.UNITS ) ) - - return self -end - ---- Add UNIT(s) to SET_UNIT. --- @param #SET_UNIT self --- @param #string AddUnit A single UNIT. --- @return #SET_UNIT self -function SET_UNIT:AddUnit( AddUnit ) - self:F2( AddUnit:GetName() ) - - self:Add( AddUnit:GetName(), AddUnit ) - - return self -end - - ---- Add UNIT(s) to SET_UNIT. --- @param #SET_UNIT self --- @param #string AddUnitNames A single name or an array of UNIT names. --- @return #SET_UNIT self -function SET_UNIT:AddUnitsByName( AddUnitNames ) - - local AddUnitNamesArray = ( type( AddUnitNames ) == "table" ) and AddUnitNames or { AddUnitNames } - - self:T( AddUnitNamesArray ) - for AddUnitID, AddUnitName in pairs( AddUnitNamesArray ) do - self:Add( AddUnitName, UNIT:FindByName( AddUnitName ) ) - end - - return self -end - ---- Remove UNIT(s) from SET_UNIT. --- @param Core.Set#SET_UNIT self --- @param Wrapper.Unit#UNIT RemoveUnitNames A single name or an array of UNIT names. --- @return self -function SET_UNIT:RemoveUnitsByName( RemoveUnitNames ) - - local RemoveUnitNamesArray = ( type( RemoveUnitNames ) == "table" ) and RemoveUnitNames or { RemoveUnitNames } - - for RemoveUnitID, RemoveUnitName in pairs( RemoveUnitNamesArray ) do - self:Remove( RemoveUnitName ) - end - - return self -end - - ---- Finds a Unit based on the Unit Name. --- @param #SET_UNIT self --- @param #string UnitName --- @return Wrapper.Unit#UNIT The found Unit. -function SET_UNIT:FindUnit( UnitName ) - - local UnitFound = self.Set[UnitName] - return UnitFound -end - - - ---- Builds a set of units of coalitions. --- Possible current coalitions are red, blue and neutral. --- @param #SET_UNIT self --- @param #string Coalitions Can take the following values: "red", "blue", "neutral". --- @return #SET_UNIT self -function SET_UNIT:FilterCoalitions( Coalitions ) - if not self.Filter.Coalitions then - self.Filter.Coalitions = {} - end - if type( Coalitions ) ~= "table" then - Coalitions = { Coalitions } - end - for CoalitionID, Coalition in pairs( Coalitions ) do - self.Filter.Coalitions[Coalition] = Coalition - end - return self -end - - ---- Builds a set of units out of categories. --- Possible current categories are plane, helicopter, ground, ship. --- @param #SET_UNIT self --- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship". --- @return #SET_UNIT self -function SET_UNIT:FilterCategories( Categories ) - if not self.Filter.Categories then - self.Filter.Categories = {} - end - if type( Categories ) ~= "table" then - Categories = { Categories } - end - for CategoryID, Category in pairs( Categories ) do - self.Filter.Categories[Category] = Category - end - return self -end - - ---- Builds a set of units of defined unit types. --- Possible current types are those types known within DCS world. --- @param #SET_UNIT self --- @param #string Types Can take those type strings known within DCS world. --- @return #SET_UNIT self -function SET_UNIT:FilterTypes( Types ) - if not self.Filter.Types then - self.Filter.Types = {} - end - if type( Types ) ~= "table" then - Types = { Types } - end - for TypeID, Type in pairs( Types ) do - self.Filter.Types[Type] = Type - end - return self -end - - ---- Builds a set of units of defined countries. --- Possible current countries are those known within DCS world. --- @param #SET_UNIT self --- @param #string Countries Can take those country strings known within DCS world. --- @return #SET_UNIT self -function SET_UNIT:FilterCountries( Countries ) - if not self.Filter.Countries then - self.Filter.Countries = {} - end - if type( Countries ) ~= "table" then - Countries = { Countries } - end - for CountryID, Country in pairs( Countries ) do - self.Filter.Countries[Country] = Country - end - return self -end - - ---- Builds a set of units of defined unit prefixes. --- All the units starting with the given prefixes will be included within the set. --- @param #SET_UNIT self --- @param #string Prefixes The prefix of which the unit name starts with. --- @return #SET_UNIT self -function SET_UNIT:FilterPrefixes( Prefixes ) - if not self.Filter.UnitPrefixes then - self.Filter.UnitPrefixes = {} - end - if type( Prefixes ) ~= "table" then - Prefixes = { Prefixes } - end - for PrefixID, Prefix in pairs( Prefixes ) do - self.Filter.UnitPrefixes[Prefix] = Prefix - end - return self -end - ---- Builds a set of units having a radar of give types. --- All the units having a radar of a given type will be included within the set. --- @param #SET_UNIT self --- @param #table RadarTypes The radar types. --- @return #SET_UNIT self -function SET_UNIT:FilterHasRadar( RadarTypes ) - - self.Filter.RadarTypes = self.Filter.RadarTypes or {} - if type( RadarTypes ) ~= "table" then - RadarTypes = { RadarTypes } - end - for RadarTypeID, RadarType in pairs( RadarTypes ) do - self.Filter.RadarTypes[RadarType] = RadarType - end - return self -end - ---- Builds a set of SEADable units. --- @param #SET_UNIT self --- @return #SET_UNIT self -function SET_UNIT:FilterHasSEAD() - - self.Filter.SEAD = true - return self -end - - - ---- Starts the filtering. --- @param #SET_UNIT self --- @return #SET_UNIT self -function SET_UNIT:FilterStart() - - if _DATABASE then - self:_FilterStart() - end - - return self -end - ---- Handles the Database to check on an event (birth) that the Object was added in the Database. --- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! --- @param #SET_UNIT self --- @param Core.Event#EVENTDATA Event --- @return #string The name of the UNIT --- @return #table The UNIT -function SET_UNIT:AddInDatabase( Event ) - self:F3( { Event } ) - - if Event.IniObjectCategory == 1 then - if not self.Database[Event.IniDCSUnitName] then - self.Database[Event.IniDCSUnitName] = UNIT:Register( Event.IniDCSUnitName ) - self:T3( self.Database[Event.IniDCSUnitName] ) - end - end - - return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] -end - ---- Handles the Database to check on any event that Object exists in the Database. --- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! --- @param #SET_UNIT self --- @param Core.Event#EVENTDATA Event --- @return #string The name of the UNIT --- @return #table The UNIT -function SET_UNIT:FindInDatabase( Event ) - self:E( { Event.IniDCSUnitName, self.Set[Event.IniDCSUnitName], Event } ) - - - return Event.IniDCSUnitName, self.Set[Event.IniDCSUnitName] -end - ---- Iterate the SET_UNIT and call an interator function for each **alive** UNIT, providing the UNIT and optional parameters. --- @param #SET_UNIT self --- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the SET_UNIT. The function needs to accept a UNIT parameter. --- @return #SET_UNIT self -function SET_UNIT:ForEachUnit( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set ) - - return self -end - ---- Iterate the SET_UNIT and call an iterator function for each **alive** UNIT presence completely in a @{Zone}, providing the UNIT and optional parameters to the called function. --- @param #SET_UNIT self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the SET_UNIT. The function needs to accept a UNIT parameter. --- @return #SET_UNIT self -function SET_UNIT:ForEachUnitCompletelyInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set, - --- @param Core.Zone#ZONE_BASE ZoneObject - -- @param Wrapper.Unit#UNIT UnitObject - function( ZoneObject, UnitObject ) - if UnitObject:IsCompletelyInZone( ZoneObject ) then - return true - else - return false - end - end, { ZoneObject } ) - - return self -end - ---- Iterate the SET_UNIT and call an iterator function for each **alive** UNIT presence not in a @{Zone}, providing the UNIT and optional parameters to the called function. --- @param #SET_UNIT self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the SET_UNIT. The function needs to accept a UNIT parameter. --- @return #SET_UNIT self -function SET_UNIT:ForEachUnitNotInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set, - --- @param Core.Zone#ZONE_BASE ZoneObject - -- @param Wrapper.Unit#UNIT UnitObject - function( ZoneObject, UnitObject ) - if UnitObject:IsNotInZone( ZoneObject ) then - return true - else - return false - end - end, { ZoneObject } ) - - return self -end - ---- Returns map of unit types. --- @param #SET_UNIT self --- @return #map<#string,#number> A map of the unit types found. The key is the UnitTypeName and the value is the amount of unit types found. -function SET_UNIT:GetUnitTypes() - self:F2() - - local MT = {} -- Message Text - local UnitTypes = {} - - for UnitID, UnitData in pairs( self:GetSet() ) do - local TextUnit = UnitData -- Wrapper.Unit#UNIT - if TextUnit:IsAlive() then - local UnitType = TextUnit:GetTypeName() - - if not UnitTypes[UnitType] then - UnitTypes[UnitType] = 1 - else - UnitTypes[UnitType] = UnitTypes[UnitType] + 1 - end - end - end - - for UnitTypeID, UnitType in pairs( UnitTypes ) do - MT[#MT+1] = UnitType .. " of " .. UnitTypeID - end - - return UnitTypes -end - - ---- Returns a comma separated string of the unit types with a count in the @{Set}. --- @param #SET_UNIT self --- @return #string The unit types string -function SET_UNIT:GetUnitTypesText() - self:F2() - - local MT = {} -- Message Text - local UnitTypes = self:GetUnitTypes() - - for UnitTypeID, UnitType in pairs( UnitTypes ) do - MT[#MT+1] = UnitType .. " of " .. UnitTypeID - end - - return table.concat( MT, ", " ) -end - ---- Returns map of unit threat levels. --- @param #SET_UNIT self --- @return #table. -function SET_UNIT:GetUnitThreatLevels() - self:F2() - - local UnitThreatLevels = {} - - for UnitID, UnitData in pairs( self:GetSet() ) do - local ThreatUnit = UnitData -- Wrapper.Unit#UNIT - if ThreatUnit:IsAlive() then - local UnitThreatLevel, UnitThreatLevelText = ThreatUnit:GetThreatLevel() - local ThreatUnitName = ThreatUnit:GetName() - - UnitThreatLevels[UnitThreatLevel] = UnitThreatLevels[UnitThreatLevel] or {} - UnitThreatLevels[UnitThreatLevel].UnitThreatLevelText = UnitThreatLevelText - UnitThreatLevels[UnitThreatLevel].Units = UnitThreatLevels[UnitThreatLevel].Units or {} - UnitThreatLevels[UnitThreatLevel].Units[ThreatUnitName] = ThreatUnit - end - end - - return UnitThreatLevels -end - ---- Calculate the maxium A2G threat level of the SET_UNIT. --- @param #SET_UNIT self -function SET_UNIT:CalculateThreatLevelA2G() - - local MaxThreatLevelA2G = 0 - for UnitName, UnitData in pairs( self:GetSet() ) do - local ThreatUnit = UnitData -- Wrapper.Unit#UNIT - local ThreatLevelA2G = ThreatUnit:GetThreatLevel() - if ThreatLevelA2G > MaxThreatLevelA2G then - MaxThreatLevelA2G = ThreatLevelA2G - end - end - - self:T3( MaxThreatLevelA2G ) - return MaxThreatLevelA2G - -end - - ---- Returns if the @{Set} has targets having a radar (of a given type). --- @param #SET_UNIT self --- @param Dcs.DCSWrapper.Unit#Unit.RadarType RadarType --- @return #number The amount of radars in the Set with the given type -function SET_UNIT:HasRadar( RadarType ) - self:F2( RadarType ) - - local RadarCount = 0 - for UnitID, UnitData in pairs( self:GetSet()) do - local UnitSensorTest = UnitData -- Wrapper.Unit#UNIT - local HasSensors - if RadarType then - HasSensors = UnitSensorTest:HasSensors( Unit.SensorType.RADAR, RadarType ) - else - HasSensors = UnitSensorTest:HasSensors( Unit.SensorType.RADAR ) - end - self:T3(HasSensors) - if HasSensors then - RadarCount = RadarCount + 1 - end - end - - return RadarCount -end - ---- Returns if the @{Set} has targets that can be SEADed. --- @param #SET_UNIT self --- @return #number The amount of SEADable units in the Set -function SET_UNIT:HasSEAD() - self:F2() - - local SEADCount = 0 - for UnitID, UnitData in pairs( self:GetSet()) do - local UnitSEAD = UnitData -- Wrapper.Unit#UNIT - if UnitSEAD:IsAlive() then - local UnitSEADAttributes = UnitSEAD:GetDesc().attributes - - local HasSEAD = UnitSEAD:HasSEAD() - - self:T3(HasSEAD) - if HasSEAD then - SEADCount = SEADCount + 1 - end - end - end - - return SEADCount -end - ---- Returns if the @{Set} has ground targets. --- @param #SET_UNIT self --- @return #number The amount of ground targets in the Set. -function SET_UNIT:HasGroundUnits() - self:F2() - - local GroundUnitCount = 0 - for UnitID, UnitData in pairs( self:GetSet()) do - local UnitTest = UnitData -- Wrapper.Unit#UNIT - if UnitTest:IsGround() then - GroundUnitCount = GroundUnitCount + 1 - end - end - - return GroundUnitCount -end - ---- Returns if the @{Set} has friendly ground units. --- @param #SET_UNIT self --- @return #number The amount of ground targets in the Set. -function SET_UNIT:HasFriendlyUnits( FriendlyCoalition ) - self:F2() - - local FriendlyUnitCount = 0 - for UnitID, UnitData in pairs( self:GetSet()) do - local UnitTest = UnitData -- Wrapper.Unit#UNIT - if UnitTest:IsFriendly( FriendlyCoalition ) then - FriendlyUnitCount = FriendlyUnitCount + 1 - end - end - - return FriendlyUnitCount -end - - - ------ Iterate the SET_UNIT and call an interator function for each **alive** player, providing the Unit of the player and optional parameters. ----- @param #SET_UNIT self ----- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_UNIT. The function needs to accept a UNIT parameter. ----- @return #SET_UNIT self ---function SET_UNIT:ForEachPlayer( IteratorFunction, ... ) --- self:F2( arg ) --- --- self:ForEach( IteratorFunction, arg, self.PlayersAlive ) --- --- return self ---end --- --- ------ Iterate the SET_UNIT and call an interator function for each client, providing the Client to the function and optional parameters. ----- @param #SET_UNIT self ----- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_UNIT. The function needs to accept a CLIENT parameter. ----- @return #SET_UNIT self ---function SET_UNIT:ForEachClient( IteratorFunction, ... ) --- self:F2( arg ) --- --- self:ForEach( IteratorFunction, arg, self.Clients ) --- --- return self ---end - - ---- --- @param #SET_UNIT self --- @param Wrapper.Unit#UNIT MUnit --- @return #SET_UNIT self -function SET_UNIT:IsIncludeObject( MUnit ) - self:F2( MUnit ) - local MUnitInclude = true - - if self.Filter.Coalitions then - local MUnitCoalition = false - for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do - self:T3( { "Coalition:", MUnit:GetCoalition(), self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) - if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == MUnit:GetCoalition() then - MUnitCoalition = true - end - end - MUnitInclude = MUnitInclude and MUnitCoalition - end - - if self.Filter.Categories then - local MUnitCategory = false - for CategoryID, CategoryName in pairs( self.Filter.Categories ) do - self:T3( { "Category:", MUnit:GetDesc().category, self.FilterMeta.Categories[CategoryName], CategoryName } ) - if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == MUnit:GetDesc().category then - MUnitCategory = true - end - end - MUnitInclude = MUnitInclude and MUnitCategory - end - - if self.Filter.Types then - local MUnitType = false - for TypeID, TypeName in pairs( self.Filter.Types ) do - self:T3( { "Type:", MUnit:GetTypeName(), TypeName } ) - if TypeName == MUnit:GetTypeName() then - MUnitType = true - end - end - MUnitInclude = MUnitInclude and MUnitType - end - - if self.Filter.Countries then - local MUnitCountry = false - for CountryID, CountryName in pairs( self.Filter.Countries ) do - self:T3( { "Country:", MUnit:GetCountry(), CountryName } ) - if country.id[CountryName] == MUnit:GetCountry() then - MUnitCountry = true - end - end - MUnitInclude = MUnitInclude and MUnitCountry - end - - if self.Filter.UnitPrefixes then - local MUnitPrefix = false - for UnitPrefixId, UnitPrefix in pairs( self.Filter.UnitPrefixes ) do - self:T3( { "Prefix:", string.find( MUnit:GetName(), UnitPrefix, 1 ), UnitPrefix } ) - if string.find( MUnit:GetName(), UnitPrefix, 1 ) then - MUnitPrefix = true - end - end - MUnitInclude = MUnitInclude and MUnitPrefix - end - - if self.Filter.RadarTypes then - local MUnitRadar = false - for RadarTypeID, RadarType in pairs( self.Filter.RadarTypes ) do - self:T3( { "Radar:", RadarType } ) - if MUnit:HasSensors( Unit.SensorType.RADAR, RadarType ) == true then - if MUnit:GetRadar() == true then -- This call is necessary to evaluate the SEAD capability. - self:T3( "RADAR Found" ) - end - MUnitRadar = true - end - end - MUnitInclude = MUnitInclude and MUnitRadar - end - - if self.Filter.SEAD then - local MUnitSEAD = false - if MUnit:HasSEAD() == true then - self:T3( "SEAD Found" ) - MUnitSEAD = true - end - MUnitInclude = MUnitInclude and MUnitSEAD - end - - self:T2( MUnitInclude ) - return MUnitInclude -end - - ---- Retrieve the type names of the @{Unit}s in the SET, delimited by an optional delimiter. --- @param #SET_UNIT self --- @param #string Delimiter (optional) The delimiter, which is default a comma. --- @return #string The types of the @{Unit}s delimited. -function SET_UNIT:GetTypeNames( Delimiter ) - - Delimiter = Delimiter or ", " - local TypeReport = REPORT:New() - local Types = {} - - for UnitName, UnitData in pairs( self:GetSet() ) do - - local Unit = UnitData -- Wrapper.Unit#UNIT - local UnitTypeName = Unit:GetTypeName() - - if not Types[UnitTypeName] then - Types[UnitTypeName] = UnitTypeName - TypeReport:Add( UnitTypeName ) - end - end - - return TypeReport:Text( Delimiter ) -end - - ---- SET_CLIENT - - ---- @type SET_CLIENT --- @extends Core.Set#SET_BASE - - - ---- # 4) SET_CLIENT class, extends @{Set#SET_BASE} --- --- Mission designers can use the @{Set#SET_CLIENT} class to build sets of units belonging to certain: --- --- * Coalitions --- * Categories --- * Countries --- * Client types --- * Starting with certain prefix strings. --- --- ## 4.1) SET_CLIENT constructor --- --- Create a new SET_CLIENT object with the @{#SET_CLIENT.New} method: --- --- * @{#SET_CLIENT.New}: Creates a new SET_CLIENT object. --- --- ## 4.2) Add or Remove CLIENT(s) from SET_CLIENT --- --- CLIENTs can be added and removed using the @{Set#SET_CLIENT.AddClientsByName} and @{Set#SET_CLIENT.RemoveClientsByName} respectively. --- These methods take a single CLIENT name or an array of CLIENT names to be added or removed from SET_CLIENT. --- --- ## 4.3) SET_CLIENT filter criteria --- --- You can set filter criteria to define the set of clients within the SET_CLIENT. --- Filter criteria are defined by: --- --- * @{#SET_CLIENT.FilterCoalitions}: Builds the SET_CLIENT with the clients belonging to the coalition(s). --- * @{#SET_CLIENT.FilterCategories}: Builds the SET_CLIENT with the clients belonging to the category(ies). --- * @{#SET_CLIENT.FilterTypes}: Builds the SET_CLIENT with the clients belonging to the client type(s). --- * @{#SET_CLIENT.FilterCountries}: Builds the SET_CLIENT with the clients belonging to the country(ies). --- * @{#SET_CLIENT.FilterPrefixes}: Builds the SET_CLIENT with the clients starting with the same prefix string(s). --- --- Once the filter criteria have been set for the SET_CLIENT, you can start filtering using: --- --- * @{#SET_CLIENT.FilterStart}: Starts the filtering of the clients within the SET_CLIENT. --- --- Planned filter criteria within development are (so these are not yet available): --- --- * @{#SET_CLIENT.FilterZones}: Builds the SET_CLIENT with the clients within a @{Zone#ZONE}. --- --- ## 4.4) SET_CLIENT iterators --- --- Once the filters have been defined and the SET_CLIENT has been built, you can iterate the SET_CLIENT with the available iterator methods. --- The iterator methods will walk the SET_CLIENT set, and call for each element within the set a function that you provide. --- The following iterator methods are currently available within the SET_CLIENT: --- --- * @{#SET_CLIENT.ForEachClient}: Calls a function for each alive client it finds within the SET_CLIENT. --- --- === --- @field #SET_CLIENT SET_CLIENT -SET_CLIENT = { - ClassName = "SET_CLIENT", - Clients = {}, - Filter = { - Coalitions = nil, - Categories = nil, - Types = nil, - Countries = nil, - ClientPrefixes = nil, - }, - FilterMeta = { - Coalitions = { - red = coalition.side.RED, - blue = coalition.side.BLUE, - neutral = coalition.side.NEUTRAL, - }, - 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 - ---- @type SET_AIRBASE --- @extends Core.Set#SET_BASE - ---- # 5) SET_AIRBASE class, extends @{Set#SET_BASE} --- --- Mission designers can use the @{Set#SET_AIRBASE} class to build sets of airbases optionally belonging to certain: --- --- * Coalitions --- --- ## 5.1) SET_AIRBASE constructor --- --- Create a new SET_AIRBASE object with the @{#SET_AIRBASE.New} method: --- --- * @{#SET_AIRBASE.New}: Creates a new SET_AIRBASE object. --- --- ## 5.2) Add or Remove AIRBASEs from SET_AIRBASE --- --- AIRBASEs can be added and removed using the @{Set#SET_AIRBASE.AddAirbasesByName} and @{Set#SET_AIRBASE.RemoveAirbasesByName} respectively. --- These methods take a single AIRBASE name or an array of AIRBASE names to be added or removed from SET_AIRBASE. --- --- ## 5.3) SET_AIRBASE filter criteria --- --- You can set filter criteria to define the set of clients within the SET_AIRBASE. --- Filter criteria are defined by: --- --- * @{#SET_AIRBASE.FilterCoalitions}: Builds the SET_AIRBASE with the airbases belonging to the coalition(s). --- --- Once the filter criteria have been set for the SET_AIRBASE, you can start filtering using: --- --- * @{#SET_AIRBASE.FilterStart}: Starts the filtering of the airbases within the SET_AIRBASE. --- --- ## 5.4) SET_AIRBASE iterators --- --- Once the filters have been defined and the SET_AIRBASE has been built, you can iterate the SET_AIRBASE with the available iterator methods. --- The iterator methods will walk the SET_AIRBASE set, and call for each airbase within the set a function that you provide. --- The following iterator methods are currently available within the SET_AIRBASE: --- --- * @{#SET_AIRBASE.ForEachAirbase}: Calls a function for each airbase it finds within the SET_AIRBASE. --- --- === --- @field #SET_AIRBASE SET_AIRBASE -SET_AIRBASE = { - ClassName = "SET_AIRBASE", - Airbases = {}, - Filter = { - Coalitions = nil, - }, - FilterMeta = { - Coalitions = { - red = coalition.side.RED, - blue = coalition.side.BLUE, - neutral = coalition.side.NEUTRAL, - }, - 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 ---- **Core** - **POINT\_VEC** classes define an **extensive API** to **manage 3D points** in the simulation space. --- --- 1) @{Point#POINT_VEC3} class, extends @{Base#BASE} --- ================================================== --- The @{Point#POINT_VEC3} class defines a 3D point in the simulator. --- --- **Important Note:** Most of the functions in this section were taken from MIST, and reworked to OO concepts. --- In order to keep the credibility of the the author, I want to emphasize that the of the MIST framework was created by Grimes, who you can find on the Eagle Dynamics Forums. --- --- ## 1.1) POINT_VEC3 constructor --- --- A new POINT_VEC3 instance can be created with: --- --- * @{Point#POINT_VEC3.New}(): a 3D point. --- * @{Point#POINT_VEC3.NewFromVec3}(): a 3D point created from a @{DCSTypes#Vec3}. --- --- ## 1.2) Manupulate the X, Y, Z coordinates of the point --- --- A POINT_VEC3 class works in 3D space. It contains internally an X, Y, Z coordinate. --- Methods exist to manupulate these coordinates. --- --- The current X, Y, Z axis can be retrieved with the methods @{#POINT_VEC3.GetX}(), @{#POINT_VEC3.GetY}(), @{#POINT_VEC3.GetZ}() respectively. --- The methods @{#POINT_VEC3.SetX}(), @{#POINT_VEC3.SetY}(), @{#POINT_VEC3.SetZ}() change the respective axis with a new value. --- The current axis values can be changed by using the methods @{#POINT_VEC3.AddX}(), @{#POINT_VEC3.AddY}(), @{#POINT_VEC3.AddZ}() --- to add or substract a value from the current respective axis value. --- Note that the Set and Add methods return the current POINT_VEC3 object, so these manipulation methods can be chained... For example: --- --- local Vec3 = PointVec3:AddX( 100 ):AddZ( 150 ):GetVec3() --- --- ## 1.3) Create waypoints for routes --- --- A POINT_VEC3 can prepare waypoints for Ground, Air and Naval groups to be embedded into a Route. --- --- --- ## 1.5) Smoke, flare, explode, illuminate --- --- At the point a smoke, flare, explosion and illumination bomb can be triggered. Use the following methods: --- --- ### 1.5.1) Smoke --- --- * @{#POINT_VEC3.Smoke}(): To smoke the point in a certain color. --- * @{#POINT_VEC3.SmokeBlue}(): To smoke the point in blue. --- * @{#POINT_VEC3.SmokeRed}(): To smoke the point in red. --- * @{#POINT_VEC3.SmokeOrange}(): To smoke the point in orange. --- * @{#POINT_VEC3.SmokeWhite}(): To smoke the point in white. --- * @{#POINT_VEC3.SmokeGreen}(): To smoke the point in green. --- --- ### 1.5.2) Flare --- --- * @{#POINT_VEC3.Flare}(): To flare the point in a certain color. --- * @{#POINT_VEC3.FlareRed}(): To flare the point in red. --- * @{#POINT_VEC3.FlareYellow}(): To flare the point in yellow. --- * @{#POINT_VEC3.FlareWhite}(): To flare the point in white. --- * @{#POINT_VEC3.FlareGreen}(): To flare the point in green. --- --- ### 1.5.3) Explode --- --- * @{#POINT_VEC3.Explosion}(): To explode the point with a certain intensity. --- --- ### 1.5.4) Illuminate --- --- * @{#POINT_VEC3.IlluminationBomb}(): To illuminate the point. --- --- --- 2) @{Point#POINT_VEC2} class, extends @{Point#POINT_VEC3} --- ========================================================= --- The @{Point#POINT_VEC2} class defines a 2D point in the simulator. The height coordinate (if needed) will be the land height + an optional added height specified. --- --- 2.1) POINT_VEC2 constructor --- --------------------------- --- A new POINT_VEC2 instance can be created with: --- --- * @{Point#POINT_VEC2.New}(): a 2D point, taking an additional height parameter. --- * @{Point#POINT_VEC2.NewFromVec2}(): a 2D point created from a @{DCSTypes#Vec2}. --- --- ## 1.2) Manupulate the X, Altitude, Y coordinates of the 2D point --- --- A POINT_VEC2 class works in 2D space, with an altitude setting. It contains internally an X, Altitude, Y coordinate. --- Methods exist to manupulate these coordinates. --- --- The current X, Altitude, Y axis can be retrieved with the methods @{#POINT_VEC2.GetX}(), @{#POINT_VEC2.GetAlt}(), @{#POINT_VEC2.GetY}() respectively. --- The methods @{#POINT_VEC2.SetX}(), @{#POINT_VEC2.SetAlt}(), @{#POINT_VEC2.SetY}() change the respective axis with a new value. --- The current Lat(itude), Alt(itude), Lon(gitude) values can also be retrieved with the methods @{#POINT_VEC2.GetLat}(), @{#POINT_VEC2.GetAlt}(), @{#POINT_VEC2.GetLon}() respectively. --- The current axis values can be changed by using the methods @{#POINT_VEC2.AddX}(), @{#POINT_VEC2.AddAlt}(), @{#POINT_VEC2.AddY}() --- to add or substract a value from the current respective axis value. --- Note that the Set and Add methods return the current POINT_VEC2 object, so these manipulation methods can be chained... For example: --- --- local Vec2 = PointVec2:AddX( 100 ):AddY( 2000 ):GetVec2() --- --- === --- --- **API CHANGE HISTORY** --- ====================== --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- Hereby the change log: --- --- 2017-03-03: POINT\_VEC3:**Explosion( ExplosionIntensity )** added. --- 2017-03-03: POINT\_VEC3:**IlluminationBomb()** added. --- --- 2017-02-18: POINT\_VEC3:**NewFromVec2( Vec2, LandHeightAdd )** added. --- --- 2016-08-12: POINT\_VEC3:**Translate( Distance, Angle )** added. --- --- 2016-08-06: Made PointVec3 and Vec3, PointVec2 and Vec2 terminology used in the code consistent. --- --- * Replaced method _Point_Vec3() to **Vec3**() where the code manages a Vec3. Replaced all references to the method. --- * Replaced method _Point_Vec2() to **Vec2**() where the code manages a Vec2. Replaced all references to the method. --- * Replaced method Random_Point_Vec3() to **RandomVec3**() where the code manages a Vec3. Replaced all references to the method. --- . --- === --- --- ### Authors: --- --- * FlightControl : Design & Programming --- --- ### Contributions: --- --- @module Point - ---- The POINT_VEC3 class --- @type POINT_VEC3 --- @field #number x The x coordinate in 3D space. --- @field #number y The y coordinate in 3D space. --- @field #number z The z coordiante in 3D space. --- @field Utilities.Utils#SMOKECOLOR SmokeColor --- @field Utilities.Utils#FLARECOLOR FlareColor --- @field #POINT_VEC3.RoutePointAltType RoutePointAltType --- @field #POINT_VEC3.RoutePointType RoutePointType --- @field #POINT_VEC3.RoutePointAction RoutePointAction --- @extends Core.Base#BASE -POINT_VEC3 = { - ClassName = "POINT_VEC3", - Metric = true, - RoutePointAltType = { - BARO = "BARO", - }, - RoutePointType = { - TakeOffParking = "TakeOffParking", - TurningPoint = "Turning Point", - }, - RoutePointAction = { - FromParkingArea = "From Parking Area", - TurningPoint = "Turning Point", - }, -} - ---- The POINT_VEC2 class --- @type POINT_VEC2 --- @field Dcs.DCSTypes#Distance x The x coordinate in meters. --- @field Dcs.DCSTypes#Distance y the y coordinate in meters. --- @extends Core.Point#POINT_VEC3 -POINT_VEC2 = { - ClassName = "POINT_VEC2", -} - - -do -- POINT_VEC3 - ---- RoutePoint AltTypes --- @type POINT_VEC3.RoutePointAltType --- @field BARO "BARO" - ---- RoutePoint Types --- @type POINT_VEC3.RoutePointType --- @field TakeOffParking "TakeOffParking" --- @field TurningPoint "Turning Point" - ---- RoutePoint Actions --- @type POINT_VEC3.RoutePointAction --- @field FromParkingArea "From Parking Area" --- @field TurningPoint "Turning Point" - --- Constructor. - ---- Create a new POINT_VEC3 object. --- @param #POINT_VEC3 self --- @param Dcs.DCSTypes#Distance x The x coordinate of the Vec3 point, pointing to the North. --- @param Dcs.DCSTypes#Distance y The y coordinate of the Vec3 point, pointing Upwards. --- @param Dcs.DCSTypes#Distance z The z coordinate of the Vec3 point, pointing to the Right. --- @return Core.Point#POINT_VEC3 self -function POINT_VEC3:New( x, y, z ) - - local self = BASE:Inherit( self, BASE:New() ) - self.x = x - self.y = y - self.z = z - - return self -end - ---- Create a new POINT_VEC3 object from Vec2 coordinates. --- @param #POINT_VEC3 self --- @param Dcs.DCSTypes#Vec2 Vec2 The Vec2 point. --- @return Core.Point#POINT_VEC3 self -function POINT_VEC3:NewFromVec2( Vec2, LandHeightAdd ) - - local LandHeight = land.getHeight( Vec2 ) - - LandHeightAdd = LandHeightAdd or 0 - LandHeight = LandHeight + LandHeightAdd - - self = self:New( Vec2.x, LandHeight, Vec2.y ) - - self:F2( self ) - - return self -end - ---- Create a new POINT_VEC3 object from Vec3 coordinates. --- @param #POINT_VEC3 self --- @param Dcs.DCSTypes#Vec3 Vec3 The Vec3 point. --- @return Core.Point#POINT_VEC3 self -function POINT_VEC3:NewFromVec3( Vec3 ) - - self = self:New( Vec3.x, Vec3.y, Vec3.z ) - self:F2( self ) - return self -end - - ---- Return the coordinates of the POINT_VEC3 in Vec3 format. --- @param #POINT_VEC3 self --- @return Dcs.DCSTypes#Vec3 The Vec3 coodinate. -function POINT_VEC3:GetVec3() - return { x = self.x, y = self.y, z = self.z } -end - ---- Return the coordinates of the POINT_VEC3 in Vec2 format. --- @param #POINT_VEC3 self --- @return Dcs.DCSTypes#Vec2 The Vec2 coodinate. -function POINT_VEC3:GetVec2() - return { x = self.x, y = self.z } -end - - ---- Return the x coordinate of the POINT_VEC3. --- @param #POINT_VEC3 self --- @return #number The x coodinate. -function POINT_VEC3:GetX() - return self.x -end - ---- Return the y coordinate of the POINT_VEC3. --- @param #POINT_VEC3 self --- @return #number The y coodinate. -function POINT_VEC3:GetY() - return self.y -end - ---- Return the z coordinate of the POINT_VEC3. --- @param #POINT_VEC3 self --- @return #number The z coodinate. -function POINT_VEC3:GetZ() - return self.z -end - ---- Set the x coordinate of the POINT_VEC3. --- @param #POINT_VEC3 self --- @param #number x The x coordinate. --- @return #POINT_VEC3 -function POINT_VEC3:SetX( x ) - self.x = x - return self -end - ---- Set the y coordinate of the POINT_VEC3. --- @param #POINT_VEC3 self --- @param #number y The y coordinate. --- @return #POINT_VEC3 -function POINT_VEC3:SetY( y ) - self.y = y - return self -end - ---- Set the z coordinate of the POINT_VEC3. --- @param #POINT_VEC3 self --- @param #number z The z coordinate. --- @return #POINT_VEC3 -function POINT_VEC3:SetZ( z ) - self.z = z - return self -end - ---- Add to the x coordinate of the POINT_VEC3. --- @param #POINT_VEC3 self --- @param #number x The x coordinate value to add to the current x coodinate. --- @return #POINT_VEC3 -function POINT_VEC3:AddX( x ) - self.x = self.x + x - return self -end - ---- Add to the y coordinate of the POINT_VEC3. --- @param #POINT_VEC3 self --- @param #number y The y coordinate value to add to the current y coodinate. --- @return #POINT_VEC3 -function POINT_VEC3:AddY( y ) - self.y = self.y + y - return self -end - ---- Add to the z coordinate of the POINT_VEC3. --- @param #POINT_VEC3 self --- @param #number z The z coordinate value to add to the current z coodinate. --- @return #POINT_VEC3 -function POINT_VEC3:AddZ( z ) - self.z = self.z +z - return self -end - ---- Return a random Vec2 within an Outer Radius and optionally NOT within an Inner Radius of the POINT_VEC3. --- @param #POINT_VEC3 self --- @param Dcs.DCSTypes#Distance OuterRadius --- @param Dcs.DCSTypes#Distance InnerRadius --- @return Dcs.DCSTypes#Vec2 Vec2 -function POINT_VEC3:GetRandomVec2InRadius( OuterRadius, InnerRadius ) - self:F2( { OuterRadius, InnerRadius } ) - - local Theta = 2 * math.pi * math.random() - local Radials = math.random() + math.random() - if Radials > 1 then - Radials = 2 - Radials - end - - local RadialMultiplier - if InnerRadius and InnerRadius <= OuterRadius then - RadialMultiplier = ( OuterRadius - InnerRadius ) * Radials + InnerRadius - else - RadialMultiplier = OuterRadius * Radials - end - - local RandomVec2 - if OuterRadius > 0 then - RandomVec2 = { x = math.cos( Theta ) * RadialMultiplier + self:GetX(), y = math.sin( Theta ) * RadialMultiplier + self:GetZ() } - else - RandomVec2 = { x = self:GetX(), y = self:GetZ() } - end - - return RandomVec2 -end - ---- Return a random POINT_VEC2 within an Outer Radius and optionally NOT within an Inner Radius of the POINT_VEC3. --- @param #POINT_VEC3 self --- @param Dcs.DCSTypes#Distance OuterRadius --- @param Dcs.DCSTypes#Distance InnerRadius --- @return #POINT_VEC2 -function POINT_VEC3:GetRandomPointVec2InRadius( OuterRadius, InnerRadius ) - self:F2( { OuterRadius, InnerRadius } ) - - return POINT_VEC2:NewFromVec2( self:GetRandomVec2InRadius( OuterRadius, InnerRadius ) ) -end - ---- Return a random Vec3 within an Outer Radius and optionally NOT within an Inner Radius of the POINT_VEC3. --- @param #POINT_VEC3 self --- @param Dcs.DCSTypes#Distance OuterRadius --- @param Dcs.DCSTypes#Distance InnerRadius --- @return Dcs.DCSTypes#Vec3 Vec3 -function POINT_VEC3:GetRandomVec3InRadius( OuterRadius, InnerRadius ) - - local RandomVec2 = self:GetRandomVec2InRadius( OuterRadius, InnerRadius ) - local y = self:GetY() + math.random( InnerRadius, OuterRadius ) - local RandomVec3 = { x = RandomVec2.x, y = y, z = RandomVec2.y } - - 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.x - RoutePoint.y = self.z - RoutePoint.alt = self.y - RoutePoint.alt_type = AltType - - RoutePoint.type = Type - RoutePoint.action = Action - - RoutePoint.speed = Speed / 3.6 - RoutePoint.speed_locked = true - --- ["task"] = --- { --- ["id"] = "ComboTask", --- ["params"] = --- { --- ["tasks"] = --- { --- }, -- end of ["tasks"] --- }, -- end of ["params"] --- }, -- end of ["task"] - - - RoutePoint.task = {} - RoutePoint.task.id = "ComboTask" - RoutePoint.task.params = {} - RoutePoint.task.params.tasks = {} - - - return RoutePoint -end - ---- 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.x - RoutePoint.y = self.z - - RoutePoint.action = Formation or "" - - - RoutePoint.speed = Speed / 3.6 - RoutePoint.speed_locked = true - --- ["task"] = --- { --- ["id"] = "ComboTask", --- ["params"] = --- { --- ["tasks"] = --- { --- }, -- end of ["tasks"] --- }, -- end of ["params"] --- }, -- end of ["task"] - - - RoutePoint.task = {} - RoutePoint.task.id = "ComboTask" - RoutePoint.task.params = {} - RoutePoint.task.params.tasks = {} - - - return RoutePoint -end - ---- Creates an explosion at the point of a certain intensity. --- @param #POINT_VEC3 self --- @param #number ExplosionIntensity -function POINT_VEC3:Explosion( ExplosionIntensity ) - self:F2( { ExplosionIntensity } ) - trigger.action.explosion( self:GetVec3(), ExplosionIntensity ) -end - ---- Creates an illumination bomb at the point. --- @param #POINT_VEC3 self -function POINT_VEC3:IlluminationBomb() - self:F2() - trigger.action.illuminationBomb( self:GetVec3() ) -end - - ---- Smokes the point in a color. --- @param #POINT_VEC3 self --- @param Utilities.Utils#SMOKECOLOR SmokeColor -function POINT_VEC3:Smoke( SmokeColor ) - self:F2( { SmokeColor } ) - trigger.action.smoke( self:GetVec3(), SmokeColor ) -end - ---- Smoke the POINT_VEC3 Green. --- @param #POINT_VEC3 self -function POINT_VEC3:SmokeGreen() - self:F2() - self:Smoke( SMOKECOLOR.Green ) -end - ---- Smoke the POINT_VEC3 Red. --- @param #POINT_VEC3 self -function POINT_VEC3:SmokeRed() - self:F2() - self:Smoke( SMOKECOLOR.Red ) -end - ---- Smoke the POINT_VEC3 White. --- @param #POINT_VEC3 self -function POINT_VEC3:SmokeWhite() - self:F2() - self:Smoke( SMOKECOLOR.White ) -end - ---- Smoke the POINT_VEC3 Orange. --- @param #POINT_VEC3 self -function POINT_VEC3:SmokeOrange() - self:F2() - self:Smoke( SMOKECOLOR.Orange ) -end - ---- Smoke the POINT_VEC3 Blue. --- @param #POINT_VEC3 self -function POINT_VEC3:SmokeBlue() - self:F2() - self:Smoke( SMOKECOLOR.Blue ) -end - ---- Flares the point in a color. --- @param #POINT_VEC3 self --- @param Utilities.Utils#FLARECOLOR FlareColor --- @param Dcs.DCSTypes#Azimuth (optional) Azimuth The azimuth of the flare direction. The default azimuth is 0. -function POINT_VEC3:Flare( FlareColor, Azimuth ) - self:F2( { FlareColor } ) - trigger.action.signalFlare( self:GetVec3(), FlareColor, Azimuth and Azimuth or 0 ) -end - ---- Flare the POINT_VEC3 White. --- @param #POINT_VEC3 self --- @param Dcs.DCSTypes#Azimuth (optional) Azimuth The azimuth of the flare direction. The default azimuth is 0. -function POINT_VEC3:FlareWhite( Azimuth ) - self:F2( Azimuth ) - self:Flare( FLARECOLOR.White, Azimuth ) -end - ---- Flare the POINT_VEC3 Yellow. --- @param #POINT_VEC3 self --- @param Dcs.DCSTypes#Azimuth (optional) Azimuth The azimuth of the flare direction. The default azimuth is 0. -function POINT_VEC3:FlareYellow( Azimuth ) - self:F2( Azimuth ) - self:Flare( FLARECOLOR.Yellow, Azimuth ) -end - ---- Flare the POINT_VEC3 Green. --- @param #POINT_VEC3 self --- @param Dcs.DCSTypes#Azimuth (optional) Azimuth The azimuth of the flare direction. The default azimuth is 0. -function POINT_VEC3:FlareGreen( Azimuth ) - self:F2( Azimuth ) - self:Flare( FLARECOLOR.Green, Azimuth ) -end - ---- Flare the POINT_VEC3 Red. --- @param #POINT_VEC3 self -function POINT_VEC3:FlareRed( Azimuth ) - self:F2( Azimuth ) - self:Flare( FLARECOLOR.Red, Azimuth ) -end - -end - -do -- POINT_VEC2 - - - ---- POINT_VEC2 constructor. --- @param #POINT_VEC2 self --- @param Dcs.DCSTypes#Distance x The x coordinate of the Vec3 point, pointing to the North. --- @param Dcs.DCSTypes#Distance y The y coordinate of the Vec3 point, pointing to the Right. --- @param Dcs.DCSTypes#Distance LandHeightAdd (optional) The default height if required to be evaluated will be the land height of the x, y coordinate. You can specify an extra height to be added to the land height. --- @return Core.Point#POINT_VEC2 -function POINT_VEC2:New( x, y, LandHeightAdd ) - - local LandHeight = land.getHeight( { ["x"] = x, ["y"] = y } ) - - LandHeightAdd = LandHeightAdd or 0 - LandHeight = LandHeight + LandHeightAdd - - self = BASE:Inherit( self, POINT_VEC3:New( x, LandHeight, y ) ) - self:F2( self ) - - return self -end - ---- Create a new POINT_VEC2 object from Vec2 coordinates. --- @param #POINT_VEC2 self --- @param Dcs.DCSTypes#Vec2 Vec2 The Vec2 point. --- @return Core.Point#POINT_VEC2 self -function POINT_VEC2:NewFromVec2( Vec2, LandHeightAdd ) - - local LandHeight = land.getHeight( Vec2 ) - - LandHeightAdd = LandHeightAdd or 0 - LandHeight = LandHeight + LandHeightAdd - - self = BASE:Inherit( self, POINT_VEC3:New( Vec2.x, LandHeight, Vec2.y ) ) - self:F2( self ) - - return self -end - ---- Create a new POINT_VEC2 object from Vec3 coordinates. --- @param #POINT_VEC2 self --- @param Dcs.DCSTypes#Vec3 Vec3 The Vec3 point. --- @return Core.Point#POINT_VEC2 self -function POINT_VEC2:NewFromVec3( Vec3 ) - - local self = BASE:Inherit( self, BASE:New() ) - local Vec2 = { x = Vec3.x, y = Vec3.z } - - local LandHeight = land.getHeight( Vec2 ) - - self = BASE:Inherit( self, POINT_VEC3:New( Vec2.x, LandHeight, Vec2.y ) ) - self:F2( self ) - - return self -end - ---- Return the x coordinate of the POINT_VEC2. --- @param #POINT_VEC2 self --- @return #number The x coodinate. -function POINT_VEC2:GetX() - return self.x -end - ---- Return the y coordinate of the POINT_VEC2. --- @param #POINT_VEC2 self --- @return #number The y coodinate. -function POINT_VEC2:GetY() - return self.z -end - ---- Return the altitude (height) 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 - ---- Return Return the Lat(itude) coordinate of the POINT_VEC2 (ie: (parent)POINT_VEC3.x). --- @param #POINT_VEC2 self --- @return #number The x coodinate. -function POINT_VEC2:GetLat() - return self.x -end - ---- Return the Lon(gitude) coordinate of the POINT_VEC2 (ie: (parent)POINT_VEC3.z). --- @param #POINT_VEC2 self --- @return #number The y coodinate. -function POINT_VEC2:GetLon() - return self.z -end - ---- Set the x coordinate of the POINT_VEC2. --- @param #POINT_VEC2 self --- @param #number x The x coordinate. --- @return #POINT_VEC2 -function POINT_VEC2:SetX( x ) - self.x = x - return self -end - ---- Set the y coordinate of the POINT_VEC2. --- @param #POINT_VEC2 self --- @param #number y The y coordinate. --- @return #POINT_VEC2 -function POINT_VEC2:SetY( y ) - self.z = y - return self -end - ---- Set the Lat(itude) coordinate of the POINT_VEC2 (ie: POINT_VEC3.x). --- @param #POINT_VEC2 self --- @param #number x The x coordinate. --- @return #POINT_VEC2 -function POINT_VEC2:SetLat( x ) - self.x = x - return self -end - ---- Set the altitude of the POINT_VEC2. --- @param #POINT_VEC2 self --- @param #number Altitude The land altitude. If nothing (nil) is given, then the current land altitude is set. --- @return #POINT_VEC2 -function POINT_VEC2:SetAlt( Altitude ) - self.y = Altitude or land.getHeight( { x = self.x, y = self.z } ) - return self -end - ---- Set the Lon(gitude) coordinate of the POINT_VEC2 (ie: POINT_VEC3.z). --- @param #POINT_VEC2 self --- @param #number y The y coordinate. --- @return #POINT_VEC2 -function POINT_VEC2:SetLon( z ) - self.z = z - return self -end - ---- Add to the x coordinate of the POINT_VEC2. --- @param #POINT_VEC2 self --- @param #number x The x coordinate. --- @return #POINT_VEC2 -function POINT_VEC2:AddX( x ) - self.x = self.x + x - return self -end - ---- Add to the y coordinate of the POINT_VEC2. --- @param #POINT_VEC2 self --- @param #number y The y coordinate. --- @return #POINT_VEC2 -function POINT_VEC2:AddY( y ) - self.z = self.z + y - return self -end - ---- Add to the current land height an altitude. --- @param #POINT_VEC2 self --- @param #number Altitude The Altitude to add. If nothing (nil) is given, then the current land altitude is set. --- @return #POINT_VEC2 -function POINT_VEC2:AddAlt( Altitude ) - self.y = land.getHeight( { x = self.x, y = self.z } ) + Altitude or 0 - return self -end - - - ---- Calculate the distance from a reference @{#POINT_VEC2}. --- @param #POINT_VEC2 self --- @param #POINT_VEC2 PointVec2Reference The reference @{#POINT_VEC2}. --- @return Dcs.DCSTypes#Distance The distance from the reference @{#POINT_VEC2} in meters. -function POINT_VEC2:DistanceFromPointVec2( PointVec2Reference ) - self:F2( PointVec2Reference ) - - local Distance = ( ( PointVec2Reference:GetX() - self:GetX() ) ^ 2 + ( PointVec2Reference:GetY() - self:GetY() ) ^2 ) ^0.5 - - self:T2( Distance ) - return Distance -end - ---- Calculate the distance from a reference @{DCSTypes#Vec2}. --- @param #POINT_VEC2 self --- @param Dcs.DCSTypes#Vec2 Vec2Reference The reference @{DCSTypes#Vec2}. --- @return Dcs.DCSTypes#Distance The distance from the reference @{DCSTypes#Vec2} in meters. -function POINT_VEC2:DistanceFromVec2( Vec2Reference ) - self:F2( Vec2Reference ) - - local Distance = ( ( Vec2Reference.x - self:GetX() ) ^ 2 + ( Vec2Reference.y - self:GetY() ) ^2 ) ^0.5 - - self:T2( Distance ) - return Distance -end - - ---- Return no text for the altitude of the POINT_VEC2. --- @param #POINT_VEC2 self --- @return #string Empty string. -function POINT_VEC2:GetAltitudeText() - return '' -end - ---- Add a Distance in meters from the POINT_VEC2 orthonormal plane, with the given angle, and calculate the new POINT_VEC2. --- @param #POINT_VEC2 self --- @param Dcs.DCSTypes#Distance Distance The Distance to be added in meters. --- @param Dcs.DCSTypes#Angle Angle The Angle in degrees. --- @return #POINT_VEC2 The new calculated POINT_VEC2. -function POINT_VEC2:Translate( Distance, Angle ) - local SX = self:GetX() - local SY = self:GetY() - local Radians = Angle / 180 * math.pi - local TX = Distance * math.cos( Radians ) + SX - local TY = Distance * math.sin( Radians ) + SY - - return POINT_VEC2:New( TX, TY ) -end - -end - - ---- **Core** - MESSAGE class takes are of the **real-time notifications** and **messages to players** during a simulation. --- --- ![Banner Image](..\Presentations\MESSAGE\Dia1.JPG) --- --- === --- --- # 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 --- --- 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 to an audience --- --- Messages are sent: --- --- * To a @{Client} using @{Message#MESSAGE.ToClient}(). --- * To a @{Group} using @{Message#MESSAGE.ToGroup}() --- * To a coalition using @{Message#MESSAGE.ToCoalition}(). --- * To the red coalition using @{Message#MESSAGE.ToRed}(). --- * To the blue coalition using @{Message#MESSAGE.ToBlue}(). --- * To all Players using @{Message#MESSAGE.ToAll}(). --- --- ## 1.3) Send conditionally to an audience --- --- Messages can be sent conditionally to an audience (when a condition is true): --- --- * To all players using @{Message#MESSAGE.ToAllIf}(). --- * To a coalition using @{Message#MESSAGE.ToCoalitionIf}(). --- --- --- @module Message - ---- The MESSAGE class --- @type MESSAGE --- @extends Core.Base#BASE -MESSAGE = { - ClassName = "MESSAGE", - MessageCategory = 0, - MessageID = 0, -} - - ---- Creates a new MESSAGE object. Note that these MESSAGE objects are not yet displayed on the display panel. You must use the functions @{ToClient} or @{ToCoalition} or @{ToAll} to send these Messages to the respective recipients. --- @param self --- @param #string MessageText is the text of the Message. --- @param #number MessageDuration is a number in seconds of how long the MESSAGE should be shown on the display panel. --- @param #string MessageCategory (optional) is a string expressing the "category" of the Message. The category will be shown as the first text in the message followed by a ": ". --- @return #MESSAGE --- @usage --- -- Create a series of new Messages. --- -- MessageAll is meant to be sent to all players, for 25 seconds, and is classified as "Score". --- -- MessageRED is meant to be sent to the RED players only, for 10 seconds, and is classified as "End of Mission", with ID "Win". --- -- MessageClient1 is meant to be sent to a Client, for 25 seconds, and is classified as "Score", with ID "Score". --- -- MessageClient1 is meant to be sent to a Client, for 25 seconds, and is classified as "Score", with ID "Score". --- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", 25, "End of Mission" ) --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", 25, "Penalty" ) --- MessageClient1 = MESSAGE:New( "Congratulations, you've just hit a target", 25, "Score" ) --- MessageClient2 = MESSAGE:New( "Congratulations, you've just killed a target", 25, "Score") -function MESSAGE:New( MessageText, MessageDuration, MessageCategory ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( { MessageText, MessageDuration, MessageCategory } ) - - -- When no MessageCategory is given, we don't show it as a title... - if MessageCategory and MessageCategory ~= "" then - if MessageCategory:sub(-1) ~= "\n" then - self.MessageCategory = MessageCategory .. ": " - else - self.MessageCategory = MessageCategory:sub( 1, -2 ) .. ":\n" - end - else - self.MessageCategory = "" - end - - self.MessageDuration = MessageDuration or 5 - self.MessageTime = timer.getTime() - self.MessageText = MessageText - - self.MessageSent = false - self.MessageGroup = false - self.MessageCoalition = false - - return self -end - ---- Sends a MESSAGE to a Client Group. Note that the Group needs to be defined within the ME with the skillset "Client" or "Player". --- @param #MESSAGE self --- @param Wrapper.Client#CLIENT Client is the Group of the Client. --- @return #MESSAGE --- @usage --- -- Send the 2 messages created with the @{New} method to the Client Group. --- -- Note that the Message of MessageClient2 is overwriting the Message of MessageClient1. --- ClientGroup = Group.getByName( "ClientGroup" ) --- --- MessageClient1 = MESSAGE:New( "Congratulations, you've just hit a target", "Score", 25, "Score" ):ToClient( ClientGroup ) --- MessageClient2 = MESSAGE:New( "Congratulations, you've just killed a target", "Score", 25, "Score" ):ToClient( ClientGroup ) --- or --- MESSAGE:New( "Congratulations, you've just hit a target", "Score", 25, "Score" ):ToClient( ClientGroup ) --- MESSAGE:New( "Congratulations, you've just killed a target", "Score", 25, "Score" ):ToClient( ClientGroup ) --- or --- MessageClient1 = MESSAGE:New( "Congratulations, you've just hit a target", "Score", 25, "Score" ) --- MessageClient2 = MESSAGE:New( "Congratulations, you've just killed a target", "Score", 25, "Score" ) --- MessageClient1:ToClient( ClientGroup ) --- MessageClient2:ToClient( ClientGroup ) -function MESSAGE:ToClient( Client ) - self:F( Client ) - - if Client and Client:GetClientGroupID() then - - local ClientGroupID = Client:GetClientGroupID() - self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration ) - trigger.action.outTextForGroup( ClientGroupID, self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration ) - end - - return self -end - ---- Sends a MESSAGE to a Group. --- @param #MESSAGE self --- @param Wrapper.Group#GROUP Group is the Group. --- @return #MESSAGE -function MESSAGE:ToGroup( Group ) - self:F( Group.GroupName ) - - if Group then - - self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration ) - trigger.action.outTextForGroup( Group:GetID(), self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration ) - end - - return self -end ---- Sends a MESSAGE to the Blue coalition. --- @param #MESSAGE self --- @return #MESSAGE --- @usage --- -- Send a message created with the @{New} method to the BLUE coalition. --- MessageBLUE = MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToBlue() --- or --- MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToBlue() --- or --- MessageBLUE = MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ) --- MessageBLUE:ToBlue() -function MESSAGE:ToBlue() - self:F() - - self:ToCoalition( coalition.side.BLUE ) - - return self -end - ---- Sends a MESSAGE to the Red Coalition. --- @param #MESSAGE self --- @return #MESSAGE --- @usage --- -- Send a message created with the @{New} method to the RED coalition. --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToRed() --- or --- MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToRed() --- or --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ) --- MessageRED:ToRed() -function MESSAGE:ToRed( ) - self:F() - - self:ToCoalition( coalition.side.RED ) - - return self -end - ---- Sends a MESSAGE to a Coalition. --- @param #MESSAGE self --- @param CoalitionSide needs to be filled out by the defined structure of the standard scripting engine @{coalition.side}. --- @return #MESSAGE --- @usage --- -- Send a message created with the @{New} method to the RED coalition. --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToCoalition( coalition.side.RED ) --- or --- MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToCoalition( coalition.side.RED ) --- or --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ) --- MessageRED:ToCoalition( coalition.side.RED ) -function MESSAGE:ToCoalition( CoalitionSide ) - self:F( CoalitionSide ) - - if CoalitionSide then - self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration ) - trigger.action.outTextForCoalition( CoalitionSide, self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration ) - end - - return self -end - ---- Sends a MESSAGE to a Coalition if the given Condition is true. --- @param #MESSAGE self --- @param CoalitionSide needs to be filled out by the defined structure of the standard scripting engine @{coalition.side}. --- @return #MESSAGE -function MESSAGE:ToCoalitionIf( CoalitionSide, Condition ) - self:F( CoalitionSide ) - - if Condition and Condition == true then - self:ToCoalition( CoalitionSide ) - end - - return self -end - ---- Sends a MESSAGE to all players. --- @param #MESSAGE self --- @return #MESSAGE --- @usage --- -- Send a message created to all players. --- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ):ToAll() --- or --- MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ):ToAll() --- or --- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ) --- MessageAll:ToAll() -function MESSAGE:ToAll() - self:F() - - self:ToCoalition( coalition.side.RED ) - self:ToCoalition( coalition.side.BLUE ) - - return self -end - - ---- Sends a MESSAGE to all players if the given Condition is true. --- @param #MESSAGE self --- @return #MESSAGE -function MESSAGE:ToAllIf( Condition ) - - if Condition and Condition == true then - self:ToCoalition( coalition.side.RED ) - self:ToCoalition( coalition.side.BLUE ) - end - - return self -end ---- **Core** - The **FSM** (**F**inite **S**tate **M**achine) class and derived **FSM\_** classes --- 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. --- --- The following derived classes are available in the MOOSE framework, that implement a specialised form of a FSM: --- --- * @{#FSM_TASK}: Models Finite State Machines for @{Task}s. --- * @{#FSM_PROCESS}: Models Finite State Machines for @{Task} actions, which control @{Client}s. --- * @{#FSM_CONTROLLABLE}: Models Finite State Machines for @{Controllable}s, which are @{Group}s, @{Unit}s, @{Client}s. --- * @{#FSM_SET}: Models Finite State Machines for @{Set}s. Note that these FSMs control multiple objects!!! So State concerns here --- for multiple objects or the position of the state machine in the process. --- --- ==== --- --- # **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 - - - --- # 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. - -- - -- === - -- - -- @field #FSM FSM - -- - FSM = { - ClassName = "FSM", - } - - --- Creates a new FSM object. - -- @param #FSM self - -- @return #FSM - function FSM:New( FsmT ) - - -- Inherits from BASE - self = BASE:Inherit( self, BASE:New() ) - - self.options = options or {} - self.options.subs = self.options.subs or {} - self.current = self.options.initial or 'none' - self.Events = {} - self.subs = {} - self.endstates = {} - - self.Scores = {} - - self._StartState = "none" - self._Transitions = {} - self._Processes = {} - self._EndStates = {} - self._Scores = {} - self._EventSchedules = {} - - self.CallScheduler = SCHEDULER:New( self ) - - - return self - end - - - --- Sets the start state of the FSM. - -- @param #FSM self - -- @param #string State A string defining the start state. - function FSM:SetStartState( State ) - - self._StartState = State - self.current = State - end - - - --- Returns the start state of the FSM. - -- @param #FSM self - -- @return #string A string containing the start state. - function FSM:GetStartState() - - return self._StartState or {} - end - - --- Add a new transition rule to the FSM. - -- A transition rule defines when and if the FSM can transition from a state towards another state upon a triggered event. - -- @param #FSM self - -- @param #table From Can contain a string indicating the From state or a table of strings containing multiple From states. - -- @param #string Event The Event name. - -- @param #string To The To state. - function FSM:AddTransition( From, Event, To ) - - local Transition = {} - Transition.From = From - Transition.Event = Event - Transition.To = To - - self:T( Transition ) - - self._Transitions[Transition] = Transition - self:_eventmap( self.Events, Transition ) - end - - - --- Returns a table of the transition rules defined within the FSM. - -- @return #table - function FSM:GetTransitions() - - return self._Transitions or {} - end - - --- Set the default @{Process} template with key ProcessName providing the ProcessClass and the process object when it is assigned to a @{Controllable} by the task. - -- @param #FSM self - -- @param #table From Can contain a string indicating the From state or a table of strings containing multiple From states. - -- @param #string Event The Event name. - -- @param Core.Fsm#FSM_PROCESS Process An sub-process FSM. - -- @param #table ReturnEvents A table indicating for which returned events of the SubFSM which Event must be triggered in the FSM. - -- @return Core.Fsm#FSM_PROCESS The SubFSM. - function FSM:AddProcess( From, Event, Process, ReturnEvents ) - self:T( { From, Event, Process, ReturnEvents } ) - - local Sub = {} - Sub.From = From - Sub.Event = Event - Sub.fsm = Process - Sub.StartEvent = "Start" - Sub.ReturnEvents = ReturnEvents - - self._Processes[Sub] = Sub - - self:_submap( self.subs, Sub, nil ) - - self:AddTransition( From, Event, From ) - - return Process - end - - - --- Returns a table of the SubFSM rules defined within the FSM. - -- @return #table - function FSM:GetProcesses() - - return self._Processes or {} - end - - function FSM:GetProcess( From, Event ) - - for ProcessID, Process in pairs( self:GetProcesses() ) do - if Process.From == From and Process.Event == Event then - 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:F( { 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:F( { From, Event, State, ScoreText, Score } ) - - local Process = self:GetProcess( From, Event ) - - Process._Scores[State] = Process._Scores[State] or {} - Process._Scores[State].ScoreText = ScoreText - Process._Scores[State].Score = Score - - self:T( Process._Scores ) - - return Process - end - - --- Returns a table with the scores defined. - function FSM:GetScores() - - return self._Scores or {} - end - - --- Returns a table with the Subs defined. - function FSM:GetSubs() - - return self.options.subs - end - - - function FSM:LoadCallBacks( CallBackTable ) - - for name, callback in pairs( CallBackTable or {} ) do - self[name] = callback - end - - end - - function FSM:_eventmap( Events, EventStructure ) - - local Event = EventStructure.Event - local __Event = "__" .. EventStructure.Event - self[Event] = self[Event] or self:_create_transition(Event) - self[__Event] = self[__Event] or self:_delayed_transition(Event) - self:T( "Added methods: " .. Event .. ", " .. __Event ) - Events[Event] = self.Events[Event] or { map = {} } - self:_add_to_map( Events[Event].map, EventStructure ) - - end - - function FSM:_submap( subs, sub, name ) - self:F( { sub = sub, name = name } ) - subs[sub.From] = subs[sub.From] or {} - subs[sub.From][sub.Event] = subs[sub.From][sub.Event] or {} - - -- Make the reference table weak. - -- setmetatable( subs[sub.From][sub.Event], { __mode = "k" } ) - - subs[sub.From][sub.Event][sub] = {} - subs[sub.From][sub.Event][sub].fsm = sub.fsm - subs[sub.From][sub.Event][sub].StartEvent = sub.StartEvent - subs[sub.From][sub.Event][sub].ReturnEvents = sub.ReturnEvents or {} -- these events need to be given to find the correct continue event ... if none given, the processing will stop. - subs[sub.From][sub.Event][sub].name = name - subs[sub.From][sub.Event][sub].fsmparent = self - end - - - function FSM:_call_handler( handler, params, EventName ) - - 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:T( "Calling " .. handler ) - self._EventSchedules[EventName] = nil - local Result, Value = xpcall( function() return self[handler]( self, unpack( params ) ) end, ErrorHandler ) - return Value - end - end - - function FSM._handler( self, EventName, ... ) - - local Can, to = self:can( EventName ) - - if to == "*" then - to = self.current - end - - if Can then - local from = self.current - local params = { from, EventName, to, ... } - - if self.Controllable then - self:T( "FSM Transition for " .. self.Controllable.ControllableName .. " :" .. self.current .. " --> " .. EventName .. " --> " .. to ) - else - self:T( "FSM Transition:" .. self.current .. " --> " .. EventName .. " --> " .. to ) - end - - if ( self:_call_handler("onbefore" .. EventName, params, EventName ) == false ) - or ( self:_call_handler("OnBefore" .. EventName, params, EventName ) == false ) - or ( self:_call_handler("onleave" .. from, params, EventName ) == false ) - or ( self:_call_handler("OnLeave" .. from, params, EventName ) == false ) then - self:T( "Cancel Transition" ) - return false - end - - self.current = to - - local execute = true - - local subtable = self:_gosub( from, EventName ) - for _, sub in pairs( subtable ) do - --if sub.nextevent then - -- self:F2( "nextevent = " .. sub.nextevent ) - -- self[sub.nextevent]( self ) - --end - self:T( "calling sub start event: " .. sub.StartEvent ) - sub.fsm.fsmparent = self - sub.fsm.ReturnEvents = sub.ReturnEvents - sub.fsm[sub.StartEvent]( sub.fsm ) - execute = false - end - - local fsmparent, Event = self:_isendstate( to ) - if fsmparent and Event then - self:F2( { "end state: ", fsmparent, Event } ) - self:_call_handler("onenter" .. to, params, EventName ) - self:_call_handler("OnEnter" .. to, params, EventName ) - self:_call_handler("onafter" .. EventName, params, EventName ) - self:_call_handler("OnAfter" .. EventName, params, EventName ) - self:_call_handler("onstatechange", params, EventName ) - fsmparent[Event]( fsmparent ) - execute = false - end - - if execute then - -- only execute the call if the From state is not equal to the To state! Otherwise this function should never execute! - --if from ~= to then - self:_call_handler("onenter" .. to, params, EventName ) - self:_call_handler("OnEnter" .. to, params, EventName ) - --end - - self:_call_handler("onafter" .. EventName, params, EventName ) - self:_call_handler("OnAfter" .. EventName, params, EventName ) - - self:_call_handler("onstatechange", params, EventName ) - end - else - self:T( "Cannot execute transition." ) - self:T( { From = self.current, Event = EventName, To = to, Can = Can } ) - end - - return nil - end - - function FSM:_delayed_transition( EventName ) - return function( self, DelaySeconds, ... ) - self:T2( "Delayed Event: " .. EventName ) - local CallID = 0 - if DelaySeconds ~= nil then - if DelaySeconds < 0 then -- Only call the event ONCE! - DelaySeconds = math.abs( DelaySeconds ) - if not self._EventSchedules[EventName] then - CallID = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1 ) - self._EventSchedules[EventName] = CallID - else - -- reschedule - end - else - CallID = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1 ) - end - else - error( "FSM: An asynchronous event trigger requires a DelaySeconds parameter!!! This can be positive or negative! Sorry, but will not process this." ) - end - self:T2( { CallID = CallID } ) - end - end - - function FSM:_create_transition( EventName ) - return function( self, ... ) return self._handler( self, EventName , ... ) end - end - - function FSM:_gosub( ParentFrom, ParentEvent ) - local fsmtable = {} - if self.subs[ParentFrom] and self.subs[ParentFrom][ParentEvent] then - self:T( { ParentFrom, ParentEvent, self.subs[ParentFrom], self.subs[ParentFrom][ParentEvent] } ) - return self.subs[ParentFrom][ParentEvent] - else - return {} - end - end - - function FSM:_isendstate( Current ) - local FSMParent = self.fsmparent - if FSMParent and self.endstates[Current] then - self:T( { state = Current, endstates = self.endstates, endstate = self.endstates[Current] } ) - FSMParent.current = Current - local ParentFrom = FSMParent.current - self:T( ParentFrom ) - self:T( self.ReturnEvents ) - local Event = self.ReturnEvents[Current] - self:T( { ParentFrom, Event, self.ReturnEvents } ) - if Event then - return FSMParent, Event - else - self:T( { "Could not find parent event name for state ", ParentFrom } ) - end - end - - return nil - end - - function FSM:_add_to_map( Map, Event ) - self:F3( { Map, Event } ) - if type(Event.From) == 'string' then - Map[Event.From] = Event.To - else - for _, From in ipairs(Event.From) do - Map[From] = Event.To - end - end - self:T3( { Map, Event } ) - end - - function FSM:GetState() - return self.current - end - - - function FSM:Is( State ) - return self.current == State - end - - function FSM:is(state) - return self.current == state - end - - function FSM:can(e) - local Event = self.Events[e] - self:F3( { self.current, Event } ) - local To = Event and Event.map[self.current] or Event.map['*'] - return To ~= nil, To - end - - function FSM:cannot(e) - return not self:can(e) - end - -end - -do -- FSM_CONTROLLABLE - - --- @type FSM_CONTROLLABLE - -- @field Wrapper.Controllable#CONTROLLABLE Controllable - -- @extends Core.Fsm#FSM - - --- # FSM_CONTROLLABLE, extends @{#FSM} - -- - -- FSM_CONTROLLABLE class models Finite State Machines for @{Controllable}s, which are @{Group}s, @{Unit}s, @{Client}s. - -- - -- === - -- - -- @field #FSM_CONTROLLABLE FSM_CONTROLLABLE - -- - 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 - - self:AddTransition( "*", "Stop", "Stopped" ) - - --- OnBefore Transition Handler for Event Stop. - -- @function [parent=#FSM_CONTROLLABLE] OnBeforeStop - -- @param #FSM_CONTROLLABLE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Stop. - -- @function [parent=#FSM_CONTROLLABLE] OnAfterStop - -- @param #FSM_CONTROLLABLE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Stop. - -- @function [parent=#FSM_CONTROLLABLE] Stop - -- @param #FSM_CONTROLLABLE self - - --- Asynchronous Event Trigger for Event Stop. - -- @function [parent=#FSM_CONTROLLABLE] __Stop - -- @param #FSM_CONTROLLABLE self - -- @param #number Delay The delay in seconds. - - --- OnLeave Transition Handler for State Stopped. - -- @function [parent=#FSM_CONTROLLABLE] OnLeaveStopped - -- @param #FSM_CONTROLLABLE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnEnter Transition Handler for State Stopped. - -- @function [parent=#FSM_CONTROLLABLE] OnEnterStopped - -- @param #FSM_CONTROLLABLE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object 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 self - end - - --- OnAfter Transition Handler for Event Stop. - -- @function [parent=#FSM_CONTROLLABLE] OnAfterStop - -- @param #FSM_CONTROLLABLE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object 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 FSM_CONTROLLABLE:OnAfterStop(Controllable,From,Event,To) - - -- Clear all pending schedules - self.CallScheduler:Clear() - end - - --- Sets the CONTROLLABLE object that the FSM_CONTROLLABLE governs. - -- @param #FSM_CONTROLLABLE self - -- @param Wrapper.Controllable#CONTROLLABLE FSMControllable - -- @return #FSM_CONTROLLABLE - function FSM_CONTROLLABLE:SetControllable( FSMControllable ) - self:F( FSMControllable ) - self.Controllable = FSMControllable - end - - --- Gets the CONTROLLABLE object that the FSM_CONTROLLABLE governs. - -- @param #FSM_CONTROLLABLE self - -- @return Wrapper.Controllable#CONTROLLABLE - function FSM_CONTROLLABLE:GetControllable() - return self.Controllable - end - - function FSM_CONTROLLABLE:_call_handler( handler, params, EventName ) - - local ErrorHandler = function( errmsg ) - - env.info( "Error in SCHEDULER function:" .. errmsg ) - if debug ~= nil then - env.info( debug.traceback() ) - end - - return errmsg - end - - if self[handler] then - self:F3( "Calling " .. handler ) - self._EventSchedules[EventName] = nil - local Result, Value = xpcall( function() return self[handler]( self, self.Controllable, unpack( params ) ) end, ErrorHandler ) - return Value - --return self[handler]( self, self.Controllable, unpack( params ) ) - end - end - -end - -do -- FSM_PROCESS - - --- @type FSM_PROCESS - -- @field Tasking.Task#TASK Task - -- @extends Core.Fsm#FSM_CONTROLLABLE - - - --- # FSM_PROCESS, extends @{#FSM} - -- - -- FSM_PROCESS class models Finite State Machines for @{Task} actions, which control @{Client}s. - -- - -- === - -- - -- @field #FSM_PROCESS FSM_PROCESS - -- - 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 - - function FSM_PROCESS:_call_handler( handler, params, EventName ) - - local ErrorHandler = function( errmsg ) - - env.info( "Error in FSM_PROCESS call handler:" .. errmsg ) - if debug ~= nil then - env.info( debug.traceback() ) - end - - return errmsg - end - - if self[handler] then - self:F3( "Calling " .. handler ) - self._EventSchedules[EventName] = nil - local Result, Value = xpcall( function() return self[handler]( self, self.Controllable, self.Task, unpack( params ) ) end, ErrorHandler ) - return Value - --return self[handler]( self, self.Controllable, unpack( params ) ) - end - 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:E( { 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 - - --- Removes an FSM_PROCESS object. - -- @param #FSM_PROCESS self - -- @return #FSM_PROCESS - function FSM_PROCESS:Remove() - self:T( { self:GetClassNameAndID() } ) - - -- Copy Processes - for ProcessID, Process in pairs( self:GetProcesses() ) do - self:E( { Process} ) - Process.fsm:Remove() - Process.fsm = nil - end - - return self - 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, Task, 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( { Scores = self._Scores, To = 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, extends @{#FSM} - -- - -- FSM_TASK class models Finite State Machines for @{Task}s. - -- - -- === - -- - -- @field #FSM_TASK FSM_TASK - -- - FSM_TASK = { - ClassName = "FSM_TASK", - } - - --- Creates a new FSM_TASK object. - -- @param #FSM_TASK self - -- @param #table FSMT - -- @param Tasking.Task#TASK Task - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #FSM_TASK - function FSM_TASK:New( FSMT ) - - local self = BASE:Inherit( self, FSM_CONTROLLABLE:New( FSMT ) ) -- Core.Fsm#FSM_TASK - - self["onstatechange"] = self.OnStateChange - - return self - end - - function FSM_TASK:_call_handler( handler, params, EventName ) - if self[handler] then - self:T( "Calling " .. handler ) - self._EventSchedules[EventName] = nil - return self[handler]( self, unpack( params ) ) - end - end - -end -- FSM_TASK - -do -- FSM_SET - - --- FSM_SET class - -- @type FSM_SET - -- @field Core.Set#SET_BASE Set - -- @extends Core.Fsm#FSM - - - --- # FSM_SET, extends @{#FSM} - -- - -- FSM_SET class models Finite State Machines for @{Set}s. Note that these FSMs control multiple objects!!! So State concerns here - -- for multiple objects or the position of the state machine in the process. - -- - -- === - -- - -- @field #FSM_SET FSM_SET - -- - FSM_SET = { - ClassName = "FSM_SET", - } - - --- Creates a new FSM_SET object. - -- @param #FSM_SET self - -- @param #table FSMT Finite State Machine Table - -- @param Set_SET_BASE FSMSet (optional) The Set object that the FSM_SET governs. - -- @return #FSM_SET - function FSM_SET:New( FSMSet ) - - -- Inherits from BASE - self = BASE:Inherit( self, FSM:New() ) -- Core.Fsm#FSM_SET - - if FSMSet then - self:Set( FSMSet ) - end - - return self - end - - --- Sets the SET_BASE object that the FSM_SET governs. - -- @param #FSM_SET self - -- @param Core.Set#SET_BASE FSMSet - -- @return #FSM_SET - function FSM_SET:Set( FSMSet ) - self:F( FSMSet ) - self.Set = FSMSet - end - - --- Gets the SET_BASE object that the FSM_SET governs. - -- @param #FSM_SET self - -- @return Core.Set#SET_BASE - function FSM_SET:Get() - return self.Controllable - end - - function FSM_SET:_call_handler( handler, params, EventName ) - if self[handler] then - self:T( "Calling " .. handler ) - self._EventSchedules[EventName] = nil - return self[handler]( self, self.Set, unpack( params ) ) - end - end - -end -- FSM_SET - ---- **Core** - The RADIO class is responsible for **transmitting radio communications**. --- --- --- bitmap --- --- === --- --- What are radio communications in DCS ? --- --- * Radio transmissions consist of **sound files** that are broadcasted on a specific **frequency** (e.g. 115MHz) and **modulation** (e.g. AM), --- * They can be **subtitled** for a specific **duration**, the **power** in Watts of the transmiter's antenna can be set, and the transmission can be **looped**. --- --- How to supply DCS my own Sound Files ? --- --- * Your sound files need to be encoded in **.ogg** or .wav, --- * Your sound files should be **as tiny as possible**. It is suggested you encode in .ogg with low bitrate and sampling settings, --- * They need to be added in .\l10n\DEFAULT\ in you .miz file (wich can be decompressed like a .zip file), --- * For simplicty sake, you can **let DCS' Mission Editor add the file** itself, by creating a new Trigger with the action "Sound to Country", and choosing your sound file and a country you don't use in your mission. --- --- Due to weird DCS quirks, **radio communications behave differently** if sent by a @{Unit#UNIT} or a @{Group#GROUP} or by any other @{Positionable#POSITIONABLE} --- --- * If the transmitter is a @{Unit#UNIT} or a @{Group#GROUP}, DCS will set the power of the transmission automatically, --- * If the transmitter is any other @{Positionable#POSITIONABLE}, the transmisison can't be subtitled or looped. --- --- Note that obviously, the **frequency** and the **modulation** of the transmission are important only if the players are piloting an **Advanced System Modelling** enabled aircraft, --- like the A10C or the Mirage 2000C. They will **hear the transmission** if they are tuned on the **right frequency and modulation** (and if they are close enough - more on that below). --- If a FC3 airacraft is used, it will **hear every communication, whatever the frequency and the modulation** is set to. --- --- === --- --- ### Authors: Hugues "Grey_Echo" Bousquet --- --- @module Radio - ---- # 1) RADIO class, extends @{Base#BASE} --- --- ## 1.1) RADIO usage --- --- There are 3 steps to a successful radio transmission. --- --- * First, you need to **"add" a @{#RADIO} object** to your @{Positionable#POSITIONABLE}. This is done using the @{Positionable#POSITIONABLE.GetRadio}() function, --- * Then, you will **set the relevant parameters** to the transmission (see below), --- * When done, you can actually **broadcast the transmission** (i.e. play the sound) with the @{Positionable#POSITIONABLE.Broadcast}() function. --- --- Methods to set relevant parameters for both a @{Unit#UNIT} or a @{Group#GROUP} or any other @{Positionable#POSITIONABLE} --- --- * @{#RADIO.SetFileName}() : Sets the file name of your sound file (e.g. "Noise.ogg"), --- * @{#RADIO.SetFrequency}() : Sets the frequency of your transmission, --- * @{#RADIO.SetModulation}() : Sets the modulation of your transmission. --- --- Additional Methods to set relevant parameters if the transmiter is a @{Unit#UNIT} or a @{Group#GROUP} --- --- * @{#RADIO.SetLoop}() : Choose if you want the transmission to be looped, --- * @{#RADIO.SetSubtitle}() : Set both the subtitle and its duration, --- * @{#RADIO.NewUnitTransmission}() : Shortcut to set all the relevant parameters in one method call --- --- Additional Methods to set relevant parameters if the transmiter is any other @{Wrapper.Positionable#POSITIONABLE} --- --- * @{#RADIO.SetPower}() : Sets the power of the antenna in Watts --- * @{#RADIO.NewGenericTransmission}() : Shortcut to set all the relevant parameters in one method call --- --- What is this power thing ? --- --- * If your transmission is sent by a @{Positionable#POSITIONABLE} other than a @{Unit#UNIT} or a @{Group#GROUP}, you can set the power of the antenna, --- * Otherwise, DCS sets it automatically, depending on what's available on your Unit, --- * If the player gets **too far** from the transmiter, or if the antenna is **too weak**, the transmission will **fade** and **become noisyer**, --- * This an automated DCS calculation you have no say on, --- * For reference, a standard VOR station has a 100W antenna, a standard AA TACAN has a 120W antenna, and civilian ATC's antenna usually range between 300 and 500W, --- * Note that if the transmission has a subtitle, it will be readable, regardless of the quality of the transmission. --- --- @type RADIO --- @field Wrapper.Positionable#POSITIONABLE Positionable The transmiter --- @field #string FileName Name of the sound file --- @field #number Frequency Frequency of the transmission in Hz --- @field #number Modulation Modulation of the transmission (either radio.modulation.AM or radio.modulation.FM) --- @field #string Subtitle Subtitle of the transmission --- @field #number SubtitleDuration Duration of the Subtitle in seconds --- @field #number Power Power of the antenna is Watts --- @field #boolean Loop --- @extends Core.Base#BASE -RADIO = { - ClassName = "RADIO", - FileName = "", - Frequency = 0, - Modulation = radio.modulation.AM, - Subtitle = "", - SubtitleDuration = 0, - Power = 100, - Loop = 0, -} - ---- Create a new RADIO Object. This doesn't broadcast a transmission, though, use @{#RADIO.Broadcast} to actually broadcast --- @param #RADIO self --- @param Wrapper.Positionable#POSITIONABLE Positionable The @{Positionable} that will receive radio capabilities. --- @return #RADIO Radio --- @return #nil If Positionable is invalid --- @usage --- -- If you want to create a RADIO, you probably should use @{Positionable#POSITIONABLE.GetRadio}() instead -function RADIO:New(Positionable) - local self = BASE:Inherit( self, BASE:New() ) -- Core.Radio#RADIO - - self:F(Positionable) - - if Positionable:GetPointVec2() then -- It's stupid, but the only way I found to make sure positionable is valid - self.Positionable = Positionable - return self - end - - self:E({"The passed positionable is invalid, no RADIO created", Positionable}) - return nil -end - ---- Check validity of the filename passed and sets RADIO.FileName --- @param #RADIO self --- @param #string FileName File name of the sound file (i.e. "Noise.ogg") --- @return #RADIO self -function RADIO:SetFileName(FileName) - self:F2(FileName) - - if type(FileName) == "string" then - if FileName:find(".ogg") or FileName:find(".wav") then - if not FileName:find("l10n/DEFAULT/") then - FileName = "l10n/DEFAULT/" .. FileName - end - self.FileName = FileName - return self - end - end - - self:E({"File name invalid. Maybe something wrong with the extension ?", self.FileName}) - return self -end - ---- Check validity of the frequency passed and sets RADIO.Frequency --- @param #RADIO self --- @param #number Frequency in MHz (Ranges allowed for radio transmissions in DCS : 30-88 / 108-152 / 225-400MHz) --- @return #RADIO self -function RADIO:SetFrequency(Frequency) - self:F2(Frequency) - if type(Frequency) == "number" then - -- If frequency is in range - if (Frequency >= 30 and Frequency < 88) or (Frequency >= 108 and Frequency < 152) or (Frequency >= 225 and Frequency < 400) then - self.Frequency = Frequency * 1000000 -- Conversion in Hz - -- If the RADIO is attached to a UNIT or a GROUP, we need to send the DCS Command "SetFrequency" to change the UNIT or GROUP frequency - if self.Positionable.ClassName == "UNIT" or self.Positionable.ClassName == "GROUP" then - self.Positionable:SetCommand({ - id = "SetFrequency", - params = { - frequency = self.Frequency, - modulation = self.Modulation, - } - }) - end - return self - end - end - self:E({"Frequency is outside of DCS Frequency ranges (30-80, 108-152, 225-400). Frequency unchanged.", self.Frequency}) - return self -end - ---- Check validity of the frequency passed and sets RADIO.Modulation --- @param #RADIO self --- @param #number Modulation either radio.modulation.AM or radio.modulation.FM --- @return #RADIO self -function RADIO:SetModulation(Modulation) - self:F2(Modulation) - if type(Modulation) == "number" then - if Modulation == radio.modulation.AM or Modulation == radio.modulation.FM then --TODO Maybe make this future proof if ED decides to add an other modulation ? - self.Modulation = Modulation - return self - end - end - self:E({"Modulation is invalid. Use DCS's enum radio.modulation. Modulation unchanged.", self.Modulation}) - return self -end - ---- Check validity of the power passed and sets RADIO.Power --- @param #RADIO self --- @param #number Power in W --- @return #RADIO self -function RADIO:SetPower(Power) - self:F2(Power) - if type(Power) == "number" then - self.Power = math.floor(math.abs(Power)) --TODO Find what is the maximum power allowed by DCS and limit power to that - return self - end - self:E({"Power is invalid. Power unchanged.", self.Power}) - return self -end - ---- Check validity of the loop passed and sets RADIO.Loop --- @param #RADIO self --- @param #boolean Loop --- @return #RADIO self --- @usage -function RADIO:SetLoop(Loop) - self:F2(Loop) - if type(Loop) == "boolean" then - self.Loop = Loop - return self - end - self:E({"Loop is invalid. Loop unchanged.", self.Loop}) - return self -end - ---- Check validity of the subtitle and the subtitleDuration passed and sets RADIO.subtitle and RADIO.subtitleDuration --- @param #RADIO self --- @param #string Subtitle --- @param #number SubtitleDuration in s --- @return #RADIO self --- @usage --- -- Both parameters are mandatory, since it wouldn't make much sense to change the Subtitle and not its duration -function RADIO:SetSubtitle(Subtitle, SubtitleDuration) - self:F2({Subtitle, SubtitleDuration}) - if type(Subtitle) == "string" then - self.Subtitle = Subtitle - else - self.Subtitle = "" - self:E({"Subtitle is invalid. Subtitle reset.", self.Subtitle}) - end - if type(SubtitleDuration) == "number" then - if math.floor(math.abs(SubtitleDuration)) == SubtitleDuration then - self.SubtitleDuration = SubtitleDuration - return self - end - end - self.SubtitleDuration = 0 - self:E({"SubtitleDuration is invalid. SubtitleDuration reset.", self.SubtitleDuration}) -end - ---- Create a new transmission, that is to say, populate the RADIO with relevant data --- @param #RADIO self --- @param #string FileName --- @param #number Frequency in MHz --- @param #number Modulation either radio.modulation.AM or radio.modulation.FM --- @param #number Power in W --- @return #RADIO self --- @usage --- -- In this function the data is especially relevant if the broadcaster is anything but a UNIT or a GROUP, --- but it will work with a UNIT or a GROUP anyway --- -- Only the RADIO and the Filename are mandatory -function RADIO:NewGenericTransmission(FileName, Frequency, Modulation, Power) - self:F({FileName, Frequency, Modulation, Power}) - - self:SetFileName(FileName) - if Frequency then self:SetFrequency(Frequency) end - if Modulation then self:SetModulation(Modulation) end - if Power then self:SetPower(Power) end - - return self -end - - ---- Create a new transmission, that is to say, populate the RADIO with relevant data --- @param #RADIO self --- @param #string FileName --- @param #string Subtitle --- @param #number SubtitleDuration in s --- @param #number Frequency in MHz --- @param #number Modulation either radio.modulation.AM or radio.modulation.FM --- @param #boolean Loop --- @return #RADIO self --- @usage --- -- In this function the data is especially relevant if the broadcaster is a UNIT or a GROUP, --- but it will work for any POSITIONABLE --- -- Only the RADIO and the Filename are mandatory -function RADIO:NewUnitTransmission(FileName, Subtitle, SubtitleDuration, Frequency, Modulation, Loop) - self:F({FileName, Subtitle, SubtitleDuration, Frequency, Modulation, Loop}) - - self:SetFileName(FileName) - if Subtitle then self:SetSubtitle(Subtitle) end - if SubtitleDuration then self:SetSubtitleDuration(SubtitleDuration) end - if Frequency then self:SetFrequency(Frequency) end - if Modulation then self:SetModulation(Modulation) end - if Loop then self:SetLoop(Loop) end - - return self -end - ---- Actually Broadcast the transmission --- @param #RADIO self --- @return #RADIO self --- @usage --- -- The Radio has to be populated with the new transmission before broadcasting. --- -- Please use RADIO setters or either @{Radio#RADIO.NewGenericTransmission} or @{Radio#RADIO.NewUnitTransmission} --- -- This class is in fact pretty smart, it determines the right DCS function to use depending on the type of POSITIONABLE --- -- If the POSITIONABLE is not a UNIT or a GROUP, we use the generic (but limited) trigger.action.radioTransmission() --- -- If the POSITIONABLE is a UNIT or a GROUP, we use the "TransmitMessage" Command --- -- If your POSITIONABLE is a UNIT or a GROUP, the Power is ignored. --- -- If your POSITIONABLE is not a UNIT or a GROUP, the Subtitle, SubtitleDuration and Loop are ignored -function RADIO:Broadcast() - self:F() - -- If the POSITIONABLE is actually a UNIT or a GROUP, use the more complicated DCS command system - if self.Positionable.ClassName == "UNIT" or self.Positionable.ClassName == "GROUP" then - self:T2("Broadcasting from a UNIT or a GROUP") - self.Positionable:SetCommand({ - id = "TransmitMessage", - params = { - file = self.FileName, - duration = self.SubtitleDuration, - subtitle = self.Subtitle, - loop = self.Loop, - } - }) - else - -- If the POSITIONABLE is anything else, we revert to the general singleton function - self:T2("Broadcasting from a POSITIONABLE") - trigger.action.radioTransmission(self.FileName, self.Positionable:GetPositionVec3(), self.Modulation, false, self.Frequency, self.Power) - end - return self -end - ---- Stops a transmission --- @param #RADIO self --- @return #RADIO self --- @usage --- -- Especially usefull to stop the broadcast of looped transmissions --- -- Only works with broadcasts from UNIT or GROUP -function RADIO:StopBroadcast() - self:F() - -- If the POSITIONABLE is a UNIT or a GROUP, stop the transmission with the DCS "StopTransmission" command - if self.Positionable.ClassName == "UNIT" or self.Positionable.ClassName == "GROUP" then - self.Positionable:SetCommand({ - id = "StopTransmission", - params = {} - }) - else - self:E("This broadcast can't be stopped. It's not looped either, so please wait for the end of the sound file playback") - end - return self -end--- This module contains the OBJECT class. --- --- 1) @{Object#OBJECT} class, extends @{Base#BASE} --- =========================================================== --- The @{Object#OBJECT} class is a wrapper class to handle the DCS Object objects: --- --- * Support all DCS Object APIs. --- * Enhance with Object specific APIs not in the DCS Object API set. --- * Manage the "state" of the DCS Object. --- --- 1.1) OBJECT constructor: --- ------------------------------ --- The OBJECT class provides the following functions to construct a OBJECT instance: --- --- * @{Object#OBJECT.New}(): Create a OBJECT instance. --- --- 1.2) OBJECT methods: --- -------------------------- --- The following methods can be used to identify an Object object: --- --- * @{Object#OBJECT.GetID}(): Returns the ID of the Object object. --- --- === --- --- @module Object - ---- The OBJECT class --- @type OBJECT --- @extends Core.Base#BASE --- @field #string ObjectName The name of the Object. -OBJECT = { - ClassName = "OBJECT", - ObjectName = "", -} - ---- A DCSObject --- @type DCSObject --- @field id_ The ID of the controllable in DCS - ---- Create a new OBJECT from a DCSObject --- @param #OBJECT self --- @param Dcs.DCSWrapper.Object#Object ObjectName The Object name --- @return #OBJECT self -function OBJECT:New( ObjectName, Test ) - local self = BASE:Inherit( self, BASE:New() ) - self:F2( ObjectName ) - self.ObjectName = ObjectName - - return self -end - - ---- Returns the unit's unique identifier. --- @param Wrapper.Object#OBJECT self --- @return Dcs.DCSWrapper.Object#Object.ID ObjectID --- @return #nil The DCS Object is not existing or alive. -function OBJECT:GetID() - self:F2( self.ObjectName ) - - local DCSObject = self:GetDCSObject() - - if DCSObject then - local ObjectID = DCSObject:getID() - return ObjectID - end - - return nil -end - ---- Destroys the OBJECT. --- @param #OBJECT self --- @return #nil The DCS Unit is not existing or alive. -function OBJECT:Destroy() - self:F2( self.ObjectName ) - - local DCSObject = self:GetDCSObject() - - if DCSObject then - - DCSObject:destroy() - end - - return nil -end - - - - ---- This module contains the IDENTIFIABLE class. --- --- 1) @{#IDENTIFIABLE} class, extends @{Object#OBJECT} --- =============================================================== --- The @{#IDENTIFIABLE} class is a wrapper class to handle the DCS Identifiable objects: --- --- * Support all DCS Identifiable APIs. --- * Enhance with Identifiable specific APIs not in the DCS Identifiable API set. --- * Manage the "state" of the DCS Identifiable. --- --- 1.1) IDENTIFIABLE constructor: --- ------------------------------ --- The IDENTIFIABLE class provides the following functions to construct a IDENTIFIABLE instance: --- --- * @{#IDENTIFIABLE.New}(): Create a IDENTIFIABLE instance. --- --- 1.2) IDENTIFIABLE methods: --- -------------------------- --- The following methods can be used to identify an identifiable object: --- --- * @{#IDENTIFIABLE.GetName}(): Returns the name of the Identifiable. --- * @{#IDENTIFIABLE.IsAlive}(): Returns if the Identifiable is alive. --- * @{#IDENTIFIABLE.GetTypeName}(): Returns the type name of the Identifiable. --- * @{#IDENTIFIABLE.GetCoalition}(): Returns the coalition of the Identifiable. --- * @{#IDENTIFIABLE.GetCountry}(): Returns the country of the Identifiable. --- * @{#IDENTIFIABLE.GetDesc}(): Returns the descriptor structure of the Identifiable. --- --- --- === --- --- @module Identifiable - ---- The IDENTIFIABLE class --- @type IDENTIFIABLE --- @extends Wrapper.Object#OBJECT --- @field #string IdentifiableName The name of the identifiable. -IDENTIFIABLE = { - ClassName = "IDENTIFIABLE", - IdentifiableName = "", -} - -local _CategoryName = { - [Unit.Category.AIRPLANE] = "Airplane", - [Unit.Category.HELICOPTER] = "Helicoper", - [Unit.Category.GROUND_UNIT] = "Ground Identifiable", - [Unit.Category.SHIP] = "Ship", - [Unit.Category.STRUCTURE] = "Structure", - } - ---- Create a new IDENTIFIABLE from a DCSIdentifiable --- @param #IDENTIFIABLE self --- @param Dcs.DCSWrapper.Identifiable#Identifiable IdentifiableName The DCS Identifiable name --- @return #IDENTIFIABLE self -function IDENTIFIABLE:New( IdentifiableName ) - local self = BASE:Inherit( self, OBJECT:New( IdentifiableName ) ) - self:F2( IdentifiableName ) - self.IdentifiableName = IdentifiableName - return self -end - ---- Returns if the Identifiable is alive. --- @param #IDENTIFIABLE self --- @return #boolean true if Identifiable is alive. --- @return #nil The DCS Identifiable is not existing or alive. -function IDENTIFIABLE:IsAlive() - self:F3( self.IdentifiableName ) - - local DCSIdentifiable = self:GetDCSObject() - - if DCSIdentifiable then - local IdentifiableIsAlive = DCSIdentifiable:isExist() - return IdentifiableIsAlive - end - - return false -end - - - - ---- Returns DCS Identifiable object name. --- The function provides access to non-activated objects too. --- @param #IDENTIFIABLE self --- @return #string The name of the DCS Identifiable. --- @return #nil The DCS Identifiable is not existing or alive. -function IDENTIFIABLE:GetName() - self:F2( self.IdentifiableName ) - - local DCSIdentifiable = self:GetDCSObject() - - if DCSIdentifiable then - local IdentifiableName = self.IdentifiableName - return IdentifiableName - end - - self:E( self.ClassName .. " " .. self.IdentifiableName .. " not found!" ) - return nil -end - - ---- Returns the type name of the DCS Identifiable. --- @param #IDENTIFIABLE self --- @return #string The type name of the DCS Identifiable. --- @return #nil The DCS Identifiable is not existing or alive. -function IDENTIFIABLE:GetTypeName() - self:F2( self.IdentifiableName ) - - local DCSIdentifiable = self:GetDCSObject() - - if DCSIdentifiable then - local IdentifiableTypeName = DCSIdentifiable:getTypeName() - self:T3( IdentifiableTypeName ) - return IdentifiableTypeName - end - - self:E( self.ClassName .. " " .. self.IdentifiableName .. " not found!" ) - return nil -end - - ---- Returns category of the DCS Identifiable. --- @param #IDENTIFIABLE self --- @return Dcs.DCSWrapper.Object#Object.Category The category ID -function IDENTIFIABLE:GetCategory() - self:F2( self.ObjectName ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - local ObjectCategory = DCSObject:getCategory() - self:T3( ObjectCategory ) - return ObjectCategory - end - - return nil -end - - ---- Returns the DCS Identifiable category name as defined within the DCS Identifiable Descriptor. --- @param #IDENTIFIABLE self --- @return #string The DCS Identifiable Category Name -function IDENTIFIABLE:GetCategoryName() - local DCSIdentifiable = self:GetDCSObject() - - if DCSIdentifiable then - local IdentifiableCategoryName = _CategoryName[ self:GetDesc().category ] - return IdentifiableCategoryName - end - - self:E( self.ClassName .. " " .. self.IdentifiableName .. " not found!" ) - return nil -end - ---- Returns coalition of the Identifiable. --- @param #IDENTIFIABLE self --- @return Dcs.DCSCoalitionWrapper.Object#coalition.side The side of the coalition. --- @return #nil The DCS Identifiable is not existing or alive. -function IDENTIFIABLE:GetCoalition() - self:F2( self.IdentifiableName ) - - local DCSIdentifiable = self:GetDCSObject() - - if DCSIdentifiable then - local IdentifiableCoalition = DCSIdentifiable:getCoalition() - self:T3( IdentifiableCoalition ) - return IdentifiableCoalition - end - - self:E( self.ClassName .. " " .. self.IdentifiableName .. " not found!" ) - return nil -end - ---- Returns country of the Identifiable. --- @param #IDENTIFIABLE self --- @return Dcs.DCScountry#country.id The country identifier. --- @return #nil The DCS Identifiable is not existing or alive. -function IDENTIFIABLE:GetCountry() - self:F2( self.IdentifiableName ) - - local DCSIdentifiable = self:GetDCSObject() - - if DCSIdentifiable then - local IdentifiableCountry = DCSIdentifiable:getCountry() - self:T3( IdentifiableCountry ) - return IdentifiableCountry - end - - self:E( self.ClassName .. " " .. self.IdentifiableName .. " not found!" ) - return nil -end - - - ---- Returns Identifiable descriptor. Descriptor type depends on Identifiable category. --- @param #IDENTIFIABLE self --- @return Dcs.DCSWrapper.Identifiable#Identifiable.Desc The Identifiable descriptor. --- @return #nil The DCS Identifiable is not existing or alive. -function IDENTIFIABLE:GetDesc() - self:F2( self.IdentifiableName ) - - local DCSIdentifiable = self:GetDCSObject() - - if DCSIdentifiable then - local IdentifiableDesc = DCSIdentifiable:getDesc() - self:T2( IdentifiableDesc ) - return IdentifiableDesc - end - - self:E( self.ClassName .. " " .. self.IdentifiableName .. " not found!" ) - return nil -end - ---- Gets the CallSign of the IDENTIFIABLE, which is a blank by default. --- @param #IDENTIFIABLE self --- @return #string The CallSign of the IDENTIFIABLE. -function IDENTIFIABLE:GetCallsign() - return '' -end - - -function IDENTIFIABLE:GetThreatLevel() - - return 0, "Scenery" -end ---- This module contains the POSITIONABLE class. --- --- 1) @{Positionable#POSITIONABLE} class, extends @{Identifiable#IDENTIFIABLE} --- =========================================================== --- The @{Positionable#POSITIONABLE} class is a wrapper class to handle the POSITIONABLE objects: --- --- * Support all DCS APIs. --- * Enhance with POSITIONABLE specific APIs not in the DCS API set. --- * Manage the "state" of the POSITIONABLE. --- --- 1.1) POSITIONABLE constructor: --- ------------------------------ --- The POSITIONABLE class provides the following functions to construct a POSITIONABLE instance: --- --- * @{Positionable#POSITIONABLE.New}(): Create a POSITIONABLE instance. --- --- 1.2) POSITIONABLE methods: --- -------------------------- --- The following methods can be used to identify an measurable object: --- --- * @{Positionable#POSITIONABLE.GetID}(): Returns the ID of the measurable object. --- * @{Positionable#POSITIONABLE.GetName}(): Returns the name of the measurable object. --- --- === --- --- @module Positionable - ---- The POSITIONABLE class --- @type POSITIONABLE --- @extends Wrapper.Identifiable#IDENTIFIABLE --- @field #string PositionableName The name of the measurable. -POSITIONABLE = { - ClassName = "POSITIONABLE", - PositionableName = "", -} - ---- A DCSPositionable --- @type DCSPositionable --- @field id_ The ID of the controllable in DCS - ---- Create a new POSITIONABLE from a DCSPositionable --- @param #POSITIONABLE self --- @param Dcs.DCSWrapper.Positionable#Positionable PositionableName The POSITIONABLE name --- @return #POSITIONABLE self -function POSITIONABLE:New( PositionableName ) - local self = BASE:Inherit( self, IDENTIFIABLE:New( PositionableName ) ) - - self.PositionableName = PositionableName - return self -end - ---- Returns the @{DCSTypes#Position3} position vectors indicating the point and direction vectors in 3D of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Position The 3D position vectors of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetPositionVec3() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionablePosition = DCSPositionable:getPosition().p - self:T3( PositionablePosition ) - return PositionablePosition - end - - return nil -end - ---- Returns the @{DCSTypes#Vec2} vector indicating the point in 2D of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Vec2 The 2D point vector of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetVec2() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionableVec3 = DCSPositionable:getPosition().p - - local PositionableVec2 = {} - PositionableVec2.x = PositionableVec3.x - PositionableVec2.y = PositionableVec3.z - - self:T2( PositionableVec2 ) - return PositionableVec2 - end - - return nil -end - ---- Returns a POINT_VEC2 object indicating the point in 2D of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Core.Point#POINT_VEC2 The 2D point vector of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetPointVec2() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionableVec3 = DCSPositionable:getPosition().p - - local PositionablePointVec2 = POINT_VEC2:NewFromVec3( PositionableVec3 ) - - self:T2( PositionablePointVec2 ) - return PositionablePointVec2 - end - - return nil -end - ---- Returns a POINT_VEC3 object indicating the point in 3D of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Core.Point#POINT_VEC3 The 3D point vector of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetPointVec3() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionableVec3 = self:GetPositionVec3() - - local PositionablePointVec3 = POINT_VEC3:NewFromVec3( PositionableVec3 ) - - self:T2( PositionablePointVec3 ) - return PositionablePointVec3 - end - - return nil -end - - ---- Returns a random @{DCSTypes#Vec3} vector within a range, indicating the point in 3D of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Vec3 The 3D point vector of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetRandomVec3( Radius ) - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionablePointVec3 = DCSPositionable:getPosition().p - local PositionableRandomVec3 = {} - local angle = math.random() * math.pi*2; - PositionableRandomVec3.x = PositionablePointVec3.x + math.cos( angle ) * math.random() * Radius; - PositionableRandomVec3.y = PositionablePointVec3.y - PositionableRandomVec3.z = PositionablePointVec3.z + math.sin( angle ) * math.random() * Radius; - - self:T3( PositionableRandomVec3 ) - return PositionableRandomVec3 - end - - return nil -end - ---- Returns the @{DCSTypes#Vec3} vector indicating the 3D vector of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Vec3 The 3D point vector of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetVec3() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionableVec3 = DCSPositionable:getPosition().p - self:T3( PositionableVec3 ) - return PositionableVec3 - end - - return nil -end - ---- Returns the altitude of the POSITIONABLE. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Distance The altitude of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetAltitude() - self:F2() - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionablePointVec3 = DCSPositionable:getPoint() --Dcs.DCSTypes#Vec3 - return PositionablePointVec3.y - end - - return nil -end - ---- Returns if the Positionable is located above a runway. --- @param Wrapper.Positionable#POSITIONABLE self --- @return #boolean true if Positionable is above a runway. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:IsAboveRunway() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - - local Vec2 = self:GetVec2() - local SurfaceType = land.getSurfaceType( Vec2 ) - local IsAboveRunway = SurfaceType == land.SurfaceType.RUNWAY - - self:T2( IsAboveRunway ) - return IsAboveRunway - end - - return nil -end - - - ---- Returns the POSITIONABLE heading in degrees. --- @param Wrapper.Positionable#POSITIONABLE self --- @return #number The POSTIONABLE heading -function POSITIONABLE:GetHeading() - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - - local PositionablePosition = DCSPositionable:getPosition() - if PositionablePosition then - local PositionableHeading = math.atan2( PositionablePosition.x.z, PositionablePosition.x.x ) - if PositionableHeading < 0 then - PositionableHeading = PositionableHeading + 2 * math.pi - end - PositionableHeading = PositionableHeading * 180 / math.pi - self:T2( PositionableHeading ) - return PositionableHeading - end - end - - return nil -end - - ---- Returns true if the POSITIONABLE is in the air. --- Polymorphic, is overridden in GROUP and UNIT. --- @param Wrapper.Positionable#POSITIONABLE self --- @return #boolean true if in the air. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:InAir() - self:F2( self.PositionableName ) - - return nil -end - - ---- Returns the POSITIONABLE velocity vector. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Vec3 The velocity vector --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetVelocity() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionableVelocityVec3 = DCSPositionable:getVelocity() - self:T3( PositionableVelocityVec3 ) - return PositionableVelocityVec3 - end - - return nil -end - ---- Returns the POSITIONABLE velocity in km/h. --- @param Wrapper.Positionable#POSITIONABLE self --- @return #number The velocity in km/h --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetVelocityKMH() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local VelocityVec3 = self:GetVelocity() - local Velocity = ( VelocityVec3.x ^ 2 + VelocityVec3.y ^ 2 + VelocityVec3.z ^ 2 ) ^ 0.5 -- in meters / sec - local Velocity = Velocity * 3.6 -- now it is in km/h. - self:T3( Velocity ) - return Velocity - end - - return nil -end - ---- Returns a message with the callsign embedded (if there is one). --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTypes#Duration Duration The duration of the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. --- @return Core.Message#MESSAGE -function POSITIONABLE:GetMessage( Message, Duration, Name ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - Name = Name or self:GetTypeName() - return MESSAGE:New( Message, Duration, self:GetCallsign() .. " (" .. Name .. ")" ) - end - - return nil -end - ---- Send a message to all coalitions. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTypes#Duration Duration The duration of the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. -function POSITIONABLE:MessageToAll( Message, Duration, Name ) - self:F2( { Message, Duration } ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - self:GetMessage( Message, Duration, Name ):ToAll() - end - - return nil -end - ---- Send a message to a coalition. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTYpes#Duration Duration The duration of the message. --- @param Dcs.DCScoalition#coalition MessageCoalition The Coalition receiving the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. -function POSITIONABLE:MessageToCoalition( Message, Duration, MessageCoalition, Name ) - self:F2( { Message, Duration } ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - self:GetMessage( Message, Duration, Name ):ToCoalition( MessageCoalition ) - end - - return nil -end - - ---- Send a message to the red coalition. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTYpes#Duration Duration The duration of the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. -function POSITIONABLE:MessageToRed( Message, Duration, Name ) - self:F2( { Message, Duration } ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - self:GetMessage( Message, Duration, Name ):ToRed() - end - - return nil -end - ---- Send a message to the blue coalition. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTypes#Duration Duration The duration of the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. -function POSITIONABLE:MessageToBlue( Message, Duration, Name ) - self:F2( { Message, Duration } ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - self:GetMessage( Message, Duration, Name ):ToBlue() - end - - return nil -end - ---- Send a message to a client. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTypes#Duration Duration The duration of the message. --- @param Wrapper.Client#CLIENT Client The client object receiving the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. -function POSITIONABLE:MessageToClient( Message, Duration, Client, Name ) - self:F2( { Message, Duration } ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - self:GetMessage( Message, Duration, Name ):ToClient( Client ) - end - - return nil -end - ---- Send a message to a @{Group}. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTypes#Duration Duration The duration of the message. --- @param Wrapper.Group#GROUP MessageGroup The GROUP object receiving the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. -function POSITIONABLE:MessageToGroup( Message, Duration, MessageGroup, Name ) - self:F2( { Message, Duration } ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - if DCSObject:isExist() then - self:GetMessage( Message, Duration, Name ):ToGroup( MessageGroup ) - end - end - - return nil -end - ---- Send a message to the players in the @{Group}. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTypes#Duration Duration The duration of the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. -function POSITIONABLE:Message( Message, Duration, Name ) - self:F2( { Message, Duration } ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - self:GetMessage( Message, Duration, Name ):ToGroup( self ) - end - - return nil -end - ---- Create a @{Radio#RADIO}, to allow radio transmission for this POSITIONABLE. --- Set parameters with the methods provided, then use RADIO:Broadcast() to actually broadcast the message --- @param #POSITIONABLE self --- @return #RADIO Radio -function POSITIONABLE:GetRadio() - self:F2(self) - return RADIO:New(self) -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.TaskAttackGroup}: (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_AttackGroup}: (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 - ---- Clear all tasks from the controllable. --- @param #CONTROLLABLE self --- @return #CONTROLLABLE -function CONTROLLABLE:ClearTasks() - self:F2() - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - local Controller = self:_GetController() - Controller:resetTask() - return self - end - - return nil -end - - ---- 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 "AttackGroup" and "AttackUnit" tasks. If the flag is false then attackQty is a desired attack quantity for "Bombing" and "BombingRunway" tasks. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:TaskAttackGroup( AttackGroup, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit ) - self:F2( { self.ControllableName, AttackGroup, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit } ) - - -- AttackGroup = { - -- id = 'AttackGroup', - -- 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 = 'AttackGroup', - 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 #boolean GroupAttack (optional) If true, all units in the group will attack the Unit when found. --- @param Dcs.DCSTypes#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. --- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. --- @param Dcs.DCSTypes#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. --- @param #number Altitude (optional) The altitude from where to attack. --- @param #boolean Visible (optional) not a clue. --- @param #number WeaponType (optional) The WeaponType. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:TaskAttackUnit( AttackUnit, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, Visible, WeaponType ) - self:F2( { self.ControllableName, AttackUnit, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, Visible, WeaponType } ) - - local DCSTask - DCSTask = { - id = 'AttackUnit', - params = { - unitId = AttackUnit:GetID(), - groupAttack = GroupAttack or false, - visible = Visible or false, - expend = WeaponExpend or "Auto", - directionEnabled = Direction and true or false, - direction = Direction, - altitudeEnabled = Altitude and true or false, - altitude = Altitude or 30, - attackQtyLimit = AttackQty and true or false, - attackQty = AttackQty, - weaponType = WeaponType - } - } - - 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 AttackGroup 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_AttackGroup = { --- id = 'FAC_AttackGroup', --- params = { --- groupId = Group.ID, --- weaponType = number, --- designation = enum AI.Task.Designation, --- datalink = boolean --- } --- } - - local DCSTask - DCSTask = { id = 'FAC_AttackGroup', - 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 "AttackGroup" and "AttackUnit" tasks. If the flag is false then attackQty is a desired attack quantity for "Bombing" and "BombingRunway" tasks. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -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) Search and attack the Unit. --- @param #CONTROLLABLE self --- @param Wrapper.Unit#UNIT EngageUnit The UNIT. --- @param #number Priority (optional) All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. --- @param #boolean GroupAttack (optional) If true, all units in the group will attack the Unit when found. --- @param Dcs.DCSTypes#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. --- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. --- @param Dcs.DCSTypes#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. --- @param Dcs.DCSTypes#Distance Altitude (optional) Desired altitude to perform the unit engagement. --- @param #boolean Visible (optional) Unit must be visible. --- @param #boolean ControllableAttack (optional) Flag indicates that the target must be engaged by all aircrafts of the controllable. Has effect only if the task is assigned to a controllable, not to a single aircraft. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskEngageUnit( EngageUnit, Priority, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, Visible, ControllableAttack ) - self:F2( { self.ControllableName, EngageUnit, Priority, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, Visible, ControllableAttack } ) - - -- EngageUnit = { - -- id = 'EngageUnit', - -- params = { - -- unitId = Unit.ID, - -- weaponType = number, - -- expend = enum AI.Task.WeaponExpend - -- attackQty = number, - -- direction = Azimuth, - -- attackQtyLimit = boolean, - -- controllableAttack = boolean, - -- priority = number, - -- } - -- } - - local DCSTask - DCSTask = { id = 'EngageUnit', - params = { - unitId = EngageUnit:GetID(), - priority = Priority or 1, - groupAttack = GroupAttack or false, - visible = Visible or false, - expend = WeaponExpend or "Auto", - directionEnabled = Direction and true or false, - direction = Direction, - altitudeEnabled = Altitude and true or false, - altitude = Altitude, - attackQtyLimit = AttackQty and true or false, - attackQty = AttackQty, - controllableAttack = ControllableAttack, - }, - }, - - self:T3( { DCSTask } ) - return DCSTask -end - - - ---- (AIR) Aircraft will act as an AWACS for friendly units (will provide them with information about contacts). No parameters. --- @param #CONTROLLABLE self --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskAWACS( ) - self:F2( { self.ControllableName } ) - --- AWACS = { --- id = 'AWACS', --- params = { --- } --- } - - local DCSTask - DCSTask = { id = 'AWACS', - params = { - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - ---- (AIR) Aircraft will act as a tanker for friendly units. No parameters. --- @param #CONTROLLABLE self --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskTanker( ) - self:F2( { self.ControllableName } ) - --- Tanker = { --- id = 'Tanker', --- params = { --- } --- } - - local DCSTask - DCSTask = { id = 'Tanker', - params = { - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - --- En-route tasks for ground units/controllables - ---- (GROUND) Ground unit (EW-radar) will act as an EWR for friendly units (will provide them with information about contacts). No parameters. --- @param #CONTROLLABLE self --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskEWR( ) - self:F2( { self.ControllableName } ) - --- EWR = { --- id = 'EWR', --- params = { --- } --- } - - local DCSTask - DCSTask = { id = 'EWR', - params = { - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - --- En-route tasks for airborne and ground units/controllables - ---- (AIR + GROUND) The task makes the controllable/unit a FAC and lets the FAC to choose the target (enemy ground controllable) as well as other assigned targets. --- The killer is player-controlled allied CAS-aircraft that is in contact with the FAC. --- If the task is assigned to the controllable lead unit will be a FAC. --- @param #CONTROLLABLE self --- @param Wrapper.Controllable#CONTROLLABLE AttackGroup Target CONTROLLABLE. --- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. --- @param #number WeaponType Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. --- @param Dcs.DCSTypes#AI.Task.Designation Designation (optional) Designation type. --- @param #boolean Datalink (optional) Allows to use datalink to send the target information to attack aircraft. Enabled by default. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskFAC_EngageGroup( AttackGroup, Priority, WeaponType, Designation, Datalink ) - self:F2( { self.ControllableName, AttackGroup, WeaponType, Priority, Designation, Datalink } ) - --- FAC_EngageControllable = { --- id = 'FAC_EngageControllable', --- params = { --- groupId = Group.ID, --- weaponType = number, --- designation = enum AI.Task.Designation, --- datalink = boolean, --- priority = number, --- } --- } - - local DCSTask - DCSTask = { id = 'FAC_EngageControllable', - params = { - groupId = AttackGroup:GetID(), - weaponType = WeaponType, - designation = Designation, - datalink = Datalink, - priority = Priority, - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - ---- (AIR + GROUND) The task makes the controllable/unit a FAC and lets the FAC to choose a targets (enemy ground controllable) around as well as other assigned targets. --- The killer is player-controlled allied CAS-aircraft that is in contact with the FAC. --- If the task is assigned to the controllable lead unit will be a FAC. --- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Distance Radius The maximal distance from the FAC to a target. --- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskFAC( Radius, Priority ) - self:F2( { self.ControllableName, Radius, Priority } ) - --- FAC = { --- id = 'FAC', --- params = { --- radius = Distance, --- priority = number --- } --- } - - local DCSTask - DCSTask = { id = 'FAC', - params = { - radius = Radius, - priority = Priority - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - - - ---- (AIR) Move the controllable to a Vec2 Point, wait for a defined duration and embark a controllable. --- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Vec2 Point The point where to wait. --- @param #number Duration The duration in seconds to wait. --- @param #CONTROLLABLE EmbarkingControllable The controllable to be embarked. --- @return Dcs.DCSTasking.Task#Task The DCS task structure -function CONTROLLABLE:TaskEmbarking( Point, Duration, EmbarkingControllable ) - self:F2( { self.ControllableName, Point, Duration, EmbarkingControllable.DCSControllable } ) - - local DCSTask - DCSTask = { id = 'Embarking', - params = { x = Point.x, - y = Point.y, - duration = Duration, - controllablesForEmbarking = { EmbarkingControllable.ControllableID }, - durationFlag = true, - distributionFlag = false, - distribution = {}, - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - ---- (GROUND) Embark to a Transport landed at a location. - ---- Move to a defined Vec2 Point, and embark to a controllable when arrived within a defined Radius. --- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Vec2 Point The point where to wait. --- @param #number Radius The radius of the embarking zone around the Point. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:TaskEmbarkToTransport( Point, Radius ) - self:F2( { self.ControllableName, Point, Radius } ) - - local DCSTask --Dcs.DCSTasking.Task#Task - DCSTask = { id = 'EmbarkToTransport', - params = { x = Point.x, - y = Point.y, - zoneRadius = Radius, - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - - ---- (AIR + GROUND) Return a mission task from a mission template. --- @param #CONTROLLABLE self --- @param #table TaskMission A table containing the mission task. --- @return Dcs.DCSTasking.Task#Task -function CONTROLLABLE:TaskMission( TaskMission ) - self:F2( Points ) - - local DCSTask - DCSTask = { id = 'Mission', params = { TaskMission, }, } - - self:T3( { DCSTask } ) - return DCSTask -end - ---- Return a Misson task to follow a given route defined by Points. --- @param #CONTROLLABLE self --- @param #table Points A table of route points. --- @return Dcs.DCSTasking.Task#Task -function CONTROLLABLE:TaskRoute( Points ) - self:F2( Points ) - - local DCSTask - DCSTask = { id = 'Mission', params = { route = { points = Points, }, }, } - - self:T3( { DCSTask } ) - return DCSTask -end - ---- (AIR + GROUND) Make the Controllable move to fly to a given point. --- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Vec3 Point The destination point in Vec3 format. --- @param #number Speed The speed to travel. --- @return #CONTROLLABLE self -function CONTROLLABLE:RouteToVec2( Point, Speed ) - self:F2( { Point, Speed } ) - - local ControllablePoint = self:GetUnit( 1 ):GetVec2() - - local PointFrom = {} - PointFrom.x = ControllablePoint.x - PointFrom.y = ControllablePoint.y - PointFrom.type = "Turning Point" - PointFrom.action = "Turning Point" - PointFrom.speed = Speed - PointFrom.speed_locked = true - PointFrom.properties = { - ["vnav"] = 1, - ["scale"] = 0, - ["angle"] = 0, - ["vangle"] = 0, - ["steer"] = 2, - } - - - local PointTo = {} - PointTo.x = Point.x - PointTo.y = Point.y - PointTo.type = "Turning Point" - PointTo.action = "Fly Over Point" - PointTo.speed = Speed - PointTo.speed_locked = true - PointTo.properties = { - ["vnav"] = 1, - ["scale"] = 0, - ["angle"] = 0, - ["vangle"] = 0, - ["steer"] = 2, - } - - - local Points = { PointFrom, PointTo } - - self:T3( Points ) - - self:Route( Points ) - - return self -end - ---- (AIR + GROUND) Make the Controllable move to a given point. --- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Vec3 Point The destination point in Vec3 format. --- @param #number Speed The speed to travel. --- @return #CONTROLLABLE self -function CONTROLLABLE:RouteToVec3( Point, Speed ) - self:F2( { Point, Speed } ) - - local ControllableVec3 = self:GetUnit( 1 ):GetVec3() - - local PointFrom = {} - PointFrom.x = ControllableVec3.x - PointFrom.y = ControllableVec3.z - PointFrom.alt = ControllableVec3.y - PointFrom.alt_type = "BARO" - PointFrom.type = "Turning Point" - PointFrom.action = "Turning Point" - PointFrom.speed = Speed - PointFrom.speed_locked = true - PointFrom.properties = { - ["vnav"] = 1, - ["scale"] = 0, - ["angle"] = 0, - ["vangle"] = 0, - ["steer"] = 2, - } - - - local PointTo = {} - PointTo.x = Point.x - PointTo.y = Point.z - PointTo.alt = Point.y - PointTo.alt_type = "BARO" - PointTo.type = "Turning Point" - PointTo.action = "Fly Over Point" - PointTo.speed = Speed - PointTo.speed_locked = true - PointTo.properties = { - ["vnav"] = 1, - ["scale"] = 0, - ["angle"] = 0, - ["vangle"] = 0, - ["steer"] = 2, - } - - - local Points = { PointFrom, PointTo } - - self:T3( Points ) - - self:Route( Points ) - - return self -end - - - ---- Make the controllable to follow a given route. --- @param #CONTROLLABLE self --- @param #table GoPoints A table of Route Points. --- @return #CONTROLLABLE self -function CONTROLLABLE:Route( GoPoints ) - self:F2( GoPoints ) - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - local Points = routines.utils.deepCopy( GoPoints ) - local MissionTask = { id = 'Mission', params = { route = { points = Points, }, }, } - local Controller = self:_GetController() - --Controller.setTask( Controller, MissionTask ) - self.TaskScheduler:Schedule( Controller, Controller.setTask, { MissionTask }, 1 ) - return self - end - - return nil -end - - - ---- (AIR + GROUND) Route the controllable to a given zone. --- The controllable final destination point can be randomized. --- A speed can be given in km/h. --- A given formation can be given. --- @param #CONTROLLABLE self --- @param Core.Zone#ZONE Zone The zone where to route to. --- @param #boolean Randomize Defines whether to target point gets randomized within the Zone. --- @param #number Speed The speed. --- @param Base#FORMATION Formation The formation string. -function CONTROLLABLE:TaskRouteToZone( Zone, Randomize, Speed, Formation ) - self:F2( Zone ) - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - - local ControllablePoint = self:GetVec2() - - local PointFrom = {} - PointFrom.x = ControllablePoint.x - PointFrom.y = ControllablePoint.y - PointFrom.type = "Turning Point" - PointFrom.action = Formation or "Cone" - PointFrom.speed = 20 / 1.6 - - - local PointTo = {} - local ZonePoint - - if Randomize then - ZonePoint = Zone:GetRandomVec2() - else - ZonePoint = Zone:GetVec2() - end - - PointTo.x = ZonePoint.x - PointTo.y = ZonePoint.y - PointTo.type = "Turning Point" - - if Formation then - PointTo.action = Formation - else - PointTo.action = "Cone" - end - - if Speed then - PointTo.speed = Speed - else - PointTo.speed = 20 / 1.6 - end - - local Points = { PointFrom, PointTo } - - self:T3( Points ) - - self:Route( Points ) - - return self - end - - return nil -end - ---- (AIR) Return the Controllable to an @{Airbase#AIRBASE} --- A speed can be given in km/h. --- A given formation can be given. --- @param #CONTROLLABLE self --- @param Wrapper.Airbase#AIRBASE ReturnAirbase The @{Airbase#AIRBASE} to return to. --- @param #number Speed (optional) The speed. --- @return #string The route -function CONTROLLABLE:RouteReturnToAirbase( ReturnAirbase, Speed ) - self:F2( { ReturnAirbase, Speed } ) - --- Example --- [4] = --- { --- ["alt"] = 45, --- ["type"] = "Land", --- ["action"] = "Landing", --- ["alt_type"] = "BARO", --- ["formation_template"] = "", --- ["properties"] = --- { --- ["vnav"] = 1, --- ["scale"] = 0, --- ["angle"] = 0, --- ["vangle"] = 0, --- ["steer"] = 2, --- }, -- end of ["properties"] --- ["ETA"] = 527.81058817743, --- ["airdromeId"] = 12, --- ["y"] = 243127.2973737, --- ["x"] = -5406.2803440839, --- ["name"] = "DictKey_WptName_53", --- ["speed"] = 138.88888888889, --- ["ETA_locked"] = false, --- ["task"] = --- { --- ["id"] = "ComboTask", --- ["params"] = --- { --- ["tasks"] = --- { --- }, -- end of ["tasks"] --- }, -- end of ["params"] --- }, -- end of ["task"] --- ["speed_locked"] = true, --- }, -- end of [4] - - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - - local ControllablePoint = self:GetVec2() - local ControllableVelocity = self:GetMaxVelocity() - - local PointFrom = {} - PointFrom.x = ControllablePoint.x - PointFrom.y = ControllablePoint.y - PointFrom.type = "Turning Point" - PointFrom.action = "Turning Point" - PointFrom.speed = ControllableVelocity - - - local PointTo = {} - local AirbasePoint = ReturnAirbase:GetVec2() - - PointTo.x = AirbasePoint.x - PointTo.y = AirbasePoint.y - PointTo.type = "Land" - PointTo.action = "Landing" - PointTo.airdromeId = ReturnAirbase:GetID()-- Airdrome ID - self:T(PointTo.airdromeId) - --PointTo.alt = 0 - - local Points = { PointFrom, PointTo } - - self:T3( Points ) - - local Route = { points = Points, } - - return Route - end - - return nil -end - --- Commands - ---- Do Script command --- @param #CONTROLLABLE self --- @param #string DoScript --- @return #DCSCommand -function CONTROLLABLE:CommandDoScript( DoScript ) - - local DCSDoScript = { - id = "Script", - params = { - command = DoScript, - }, - } - - self:T3( DCSDoScript ) - return DCSDoScript -end - - ---- Return the mission template of the controllable. --- @param #CONTROLLABLE self --- @return #table The MissionTemplate --- TODO: Rework the method how to retrieve a template ... -function CONTROLLABLE:GetTaskMission() - self:F2( self.ControllableName ) - - return routines.utils.deepCopy( _DATABASE.Templates.Controllables[self.ControllableName].Template ) -end - ---- Return the mission route of the controllable. --- @param #CONTROLLABLE self --- @return #table The mission route defined by points. -function CONTROLLABLE:GetTaskRoute() - self:F2( self.ControllableName ) - - return routines.utils.deepCopy( _DATABASE.Templates.Controllables[self.ControllableName].Template.route.points ) -end - ---- Return the route of a controllable by using the @{Database#DATABASE} class. --- @param #CONTROLLABLE self --- @param #number Begin The route point from where the copy will start. The base route point is 0. --- @param #number End The route point where the copy will end. The End point is the last point - the End point. The last point has base 0. --- @param #boolean Randomize Randomization of the route, when true. --- @param #number Radius When randomization is on, the randomization is within the radius. -function CONTROLLABLE:CopyRoute( Begin, End, Randomize, Radius ) - self:F2( { Begin, End } ) - - local Points = {} - - -- Could be a Spawned Controllable - local ControllableName = string.match( self:GetName(), ".*#" ) - if ControllableName then - ControllableName = ControllableName:sub( 1, -2 ) - else - ControllableName = self:GetName() - end - - self:T3( { ControllableName } ) - - local Template = _DATABASE.Templates.Controllables[ControllableName].Template - - if Template then - if not Begin then - Begin = 0 - end - if not End then - End = 0 - end - - for TPointID = Begin + 1, #Template.route.points - End do - if Template.route.points[TPointID] then - Points[#Points+1] = routines.utils.deepCopy( Template.route.points[TPointID] ) - if Randomize then - if not Radius then - Radius = 500 - end - Points[#Points].x = Points[#Points].x + math.random( Radius * -1, Radius ) - Points[#Points].y = Points[#Points].y + math.random( Radius * -1, Radius ) - end - end - end - return Points - else - error( "Template not found for Controllable : " .. ControllableName ) - end - - return nil -end - - ---- Return the detected targets of the controllable. --- The optional parametes specify the detection methods that can be applied. --- If no detection method is given, the detection will use all the available methods by default. --- @param Wrapper.Controllable#CONTROLLABLE self --- @param #boolean DetectVisual (optional) --- @param #boolean DetectOptical (optional) --- @param #boolean DetectRadar (optional) --- @param #boolean DetectIRST (optional) --- @param #boolean DetectRWR (optional) --- @param #boolean DetectDLINK (optional) --- @return #table DetectedTargets -function CONTROLLABLE:GetDetectedTargets( DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK ) - self:F2( self.ControllableName ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local DetectionVisual = ( DetectVisual and DetectVisual == true ) and Controller.Detection.VISUAL or nil - local DetectionOptical = ( DetectOptical and DetectOptical == true ) and Controller.Detection.OPTICAL or nil - local DetectionRadar = ( DetectRadar and DetectRadar == true ) and Controller.Detection.RADAR or nil - local DetectionIRST = ( DetectIRST and DetectIRST == true ) and Controller.Detection.IRST or nil - local DetectionRWR = ( DetectRWR and DetectRWR == true ) and Controller.Detection.RWR or nil - local DetectionDLINK = ( DetectDLINK and DetectDLINK == true ) and Controller.Detection.DLINK or nil - - - return self:_GetController():getDetectedTargets( DetectionVisual, DetectionOptical, DetectionRadar, DetectionIRST, DetectionRWR, DetectionDLINK ) - end - - return nil -end - -function CONTROLLABLE:IsTargetDetected( DCSObject ) - self:F2( self.ControllableName ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - - local TargetIsDetected, TargetIsVisible, TargetLastTime, TargetKnowType, TargetKnowDistance, TargetLastPos, TargetLastVelocity - = self:_GetController().isTargetDetected( self:_GetController(), DCSObject, - Controller.Detection.VISUAL, - Controller.Detection.OPTIC, - Controller.Detection.RADAR, - Controller.Detection.IRST, - Controller.Detection.RWR, - Controller.Detection.DLINK - ) - return TargetIsDetected, TargetIsVisible, TargetLastTime, TargetKnowType, TargetKnowDistance, TargetLastPos, TargetLastVelocity - end - - return nil -end - --- Options - ---- Can the CONTROLLABLE hold their weapons? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROEHoldFirePossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() or self:IsGround() or self:IsShip() then - return true - end - - return false - end - - return nil -end - ---- Holding weapons. --- @param Wrapper.Controllable#CONTROLLABLE self --- @return Wrapper.Controllable#CONTROLLABLE self -function CONTROLLABLE:OptionROEHoldFire() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD ) - elseif self:IsGround() then - Controller:setOption( AI.Option.Ground.id.ROE, AI.Option.Ground.val.ROE.WEAPON_HOLD ) - elseif self:IsShip() then - Controller:setOption( AI.Option.Naval.id.ROE, AI.Option.Naval.val.ROE.WEAPON_HOLD ) - end - - return self - end - - return nil -end - ---- Can the CONTROLLABLE attack returning on enemy fire? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROEReturnFirePossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() or self:IsGround() or self:IsShip() then - return true - end - - return false - end - - return nil -end - ---- Return fire. --- @param #CONTROLLABLE self --- @return #CONTROLLABLE self -function CONTROLLABLE:OptionROEReturnFire() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.RETURN_FIRE ) - elseif self:IsGround() then - Controller:setOption( AI.Option.Ground.id.ROE, AI.Option.Ground.val.ROE.RETURN_FIRE ) - elseif self:IsShip() then - Controller:setOption( AI.Option.Naval.id.ROE, AI.Option.Naval.val.ROE.RETURN_FIRE ) - end - - return self - end - - return nil -end - ---- Can the CONTROLLABLE attack designated targets? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROEOpenFirePossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() or self:IsGround() or self:IsShip() then - return true - end - - return false - end - - return nil -end - ---- Openfire. --- @param #CONTROLLABLE self --- @return #CONTROLLABLE self -function CONTROLLABLE:OptionROEOpenFire() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE ) - elseif self:IsGround() then - Controller:setOption( AI.Option.Ground.id.ROE, AI.Option.Ground.val.ROE.OPEN_FIRE ) - elseif self:IsShip() then - Controller:setOption( AI.Option.Naval.id.ROE, AI.Option.Naval.val.ROE.OPEN_FIRE ) - end - - return self - end - - return nil -end - ---- Can the CONTROLLABLE attack targets of opportunity? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROEWeaponFreePossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() then - return true - end - - return false - end - - return nil -end - ---- Weapon free. --- @param #CONTROLLABLE self --- @return #CONTROLLABLE self -function CONTROLLABLE:OptionROEWeaponFree() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_FREE ) - end - - return self - end - - return nil -end - ---- Can the CONTROLLABLE ignore enemy fire? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROTNoReactionPossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() then - return true - end - - return false - end - - return nil -end - - ---- No evasion on enemy threats. --- @param #CONTROLLABLE self --- @return #CONTROLLABLE self -function CONTROLLABLE:OptionROTNoReaction() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.NO_REACTION ) - end - - return self - end - - return nil -end - ---- Can the CONTROLLABLE evade using passive defenses? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROTPassiveDefensePossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() then - return true - end - - return false - end - - return nil -end - ---- Evasion passive defense. --- @param #CONTROLLABLE self --- @return #CONTROLLABLE self -function CONTROLLABLE:OptionROTPassiveDefense() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.PASSIVE_DEFENCE ) - end - - return self - end - - return nil -end - ---- Can the CONTROLLABLE evade on enemy fire? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROTEvadeFirePossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() then - return true - end - - return false - end - - return nil -end - - ---- Evade on fire. --- @param #CONTROLLABLE self --- @return #CONTROLLABLE self -function CONTROLLABLE:OptionROTEvadeFire() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) - end - - return self - end - - return nil -end - ---- Can the CONTROLLABLE evade on fire using vertical manoeuvres? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROTVerticalPossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() then - return true - end - - return false - end - - return nil -end - - ---- Evade on fire using vertical manoeuvres. --- @param #CONTROLLABLE self --- @return #CONTROLLABLE self -function CONTROLLABLE:OptionROTVertical() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.BYPASS_AND_ESCAPE ) - end - - return self - end - - return nil -end - ---- Retrieve the controllable mission and allow to place function hooks within the mission waypoint plan. --- Use the method @{Controllable#CONTROLLABLE:WayPointFunction} to define the hook functions for specific waypoints. --- Use the method @{Controllable@CONTROLLABLE:WayPointExecute) to start the execution of the new mission plan. --- Note that when WayPointInitialize is called, the Mission of the controllable is RESTARTED! --- @param #CONTROLLABLE self --- @param #table WayPoints If WayPoints is given, then use the route. --- @return #CONTROLLABLE -function CONTROLLABLE:WayPointInitialize( WayPoints ) - self:F( { WayPoints } ) - - if WayPoints then - self.WayPoints = WayPoints - else - self.WayPoints = self:GetTaskRoute() - end - - return self -end - ---- Get the current WayPoints set with the WayPoint functions( Note that the WayPoints can be nil, although there ARE waypoints). --- @param #CONTROLLABLE self --- @return #table WayPoints If WayPoints is given, then return the WayPoints structure. -function CONTROLLABLE:GetWayPoints() - self:F( ) - - if self.WayPoints then - return self.WayPoints - end - - return nil -end - ---- Registers a waypoint function that will be executed when the controllable moves over the WayPoint. --- @param #CONTROLLABLE self --- @param #number WayPoint The waypoint number. Note that the start waypoint on the route is WayPoint 1! --- @param #number WayPointIndex When defining multiple WayPoint functions for one WayPoint, use WayPointIndex to set the sequence of actions. --- @param #function WayPointFunction The waypoint function to be called when the controllable moves over the waypoint. The waypoint function takes variable parameters. --- @return #CONTROLLABLE -function CONTROLLABLE:WayPointFunction( WayPoint, WayPointIndex, WayPointFunction, ... ) - self:F2( { WayPoint, WayPointIndex, WayPointFunction } ) - - table.insert( self.WayPoints[WayPoint].task.params.tasks, WayPointIndex ) - self.WayPoints[WayPoint].task.params.tasks[WayPointIndex] = self:TaskFunction( WayPoint, WayPointIndex, WayPointFunction, arg ) - return self -end - - -function CONTROLLABLE:TaskFunction( WayPoint, WayPointIndex, FunctionString, FunctionArguments ) - self:F2( { WayPoint, WayPointIndex, FunctionString, FunctionArguments } ) - - local DCSTask - - local DCSScript = {} - DCSScript[#DCSScript+1] = "local MissionControllable = GROUP:Find( ... ) " - - if FunctionArguments and #FunctionArguments > 0 then - DCSScript[#DCSScript+1] = FunctionString .. "( MissionControllable, " .. table.concat( FunctionArguments, "," ) .. ")" - else - DCSScript[#DCSScript+1] = FunctionString .. "( MissionControllable )" - end - - DCSTask = self:TaskWrappedAction( - self:CommandDoScript( - table.concat( DCSScript ) - ), WayPointIndex - ) - - self:T3( DCSTask ) - - return DCSTask - -end - ---- Executes the WayPoint plan. --- The function gets a WayPoint parameter, that you can use to restart the mission at a specific WayPoint. --- Note that when the WayPoint parameter is used, the new start mission waypoint of the controllable will be 1! --- @param #CONTROLLABLE self --- @param #number WayPoint The WayPoint from where to execute the mission. --- @param #number WaitTime The amount seconds to wait before initiating the mission. --- @return #CONTROLLABLE -function CONTROLLABLE:WayPointExecute( WayPoint, WaitTime ) - self:F( { WayPoint, WaitTime } ) - - if not WayPoint then - WayPoint = 1 - end - - -- When starting the mission from a certain point, the TaskPoints need to be deleted before the given WayPoint. - for TaskPointID = 1, WayPoint - 1 do - table.remove( self.WayPoints, 1 ) - end - - self:T3( self.WayPoints ) - - self:SetTask( self:TaskRoute( self.WayPoints ), WaitTime ) - - return self -end - --- Message APIs--- This module contains the GROUP class. --- --- 1) @{Group#GROUP} class, extends @{Controllable#CONTROLLABLE} --- ============================================================= --- The @{Group#GROUP} class is a wrapper class to handle the DCS Group objects: --- --- * Support all DCS Group APIs. --- * Enhance with Group specific APIs not in the DCS Group API set. --- * Handle local Group Controller. --- * Manage the "state" of the DCS Group. --- --- **IMPORTANT: ONE SHOULD NEVER SANATIZE these GROUP OBJECT REFERENCES! (make the GROUP object references nil).** --- --- 1.1) GROUP reference methods --- ----------------------- --- For each DCS Group object alive within a running mission, a GROUP wrapper object (instance) will be created within the _@{DATABASE} object. --- This is done at the beginning of the mission (when the mission starts), and dynamically when new DCS Group objects are spawned (using the @{SPAWN} class). --- --- The GROUP class does not contain a :New() method, rather it provides :Find() methods to retrieve the object reference --- using the DCS Group or the DCS GroupName. --- --- Another thing to know is that GROUP objects do not "contain" the DCS Group object. --- The GROUP methods will reference the DCS Group object by name when it is needed during API execution. --- If the DCS Group object does not exist or is nil, the GROUP methods will return nil and log an exception in the DCS.log file. --- --- The GROUP class provides the following functions to retrieve quickly the relevant GROUP instance: --- --- * @{#GROUP.Find}(): Find a GROUP instance from the _DATABASE object using a DCS Group object. --- * @{#GROUP.FindByName}(): Find a GROUP instance from the _DATABASE object using a DCS Group name. --- --- ## 1.2) GROUP task methods --- --- A GROUP is a @{Controllable}. See the @{Controllable} task methods section for a description of the task methods. --- --- ### 1.2.4) Obtain the mission from group templates --- --- Group templates contain complete mission descriptions. Sometimes you want to copy a complete mission from a group and assign it to another: --- --- * @{Controllable#CONTROLLABLE.TaskMission}: (AIR + GROUND) Return a mission task from a mission template. --- --- ## 1.3) GROUP Command methods --- --- A GROUP is a @{Controllable}. See the @{Controllable} command methods section for a description of the command methods. --- --- ## 1.4) GROUP option methods --- --- A GROUP is a @{Controllable}. See the @{Controllable} option methods section for a description of the option methods. --- --- ## 1.5) GROUP Zone validation methods --- --- The group can be validated whether it is completely, partly or not within a @{Zone}. --- Use the following Zone validation methods on the group: --- --- * @{#GROUP.IsCompletelyInZone}: Returns true if all units of the group are within a @{Zone}. --- * @{#GROUP.IsPartlyInZone}: Returns true if some units of the group are within a @{Zone}. --- * @{#GROUP.IsNotInZone}: Returns true if none of the group units of the group are within a @{Zone}. --- --- The zone can be of any @{Zone} class derived from @{Zone#ZONE_BASE}. So, these methods are polymorphic to the zones tested on. --- --- ## 1.6) GROUP AI methods --- --- A GROUP has AI methods to control the AI activation. --- --- * @{#GROUP.SetAIOnOff}(): Turns the GROUP AI On or Off. --- * @{#GROUP.SetAIOn}(): Turns the GROUP AI On. --- * @{#GROUP.SetAIOff}(): Turns the GROUP AI Off. --- --- ==== --- --- # **API CHANGE HISTORY** --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- Hereby the change log: --- --- 2017-03-07: GROUP:**HandleEvent( Event, EventFunction )** added. --- 2017-03-07: GROUP:**UnHandleEvent( Event )** added. --- --- 2017-01-24: GROUP:**SetAIOnOff( AIOnOff )** added. --- --- 2017-01-24: GROUP:**SetAIOn()** added. --- --- 2017-01-24: GROUP:**SetAIOff()** added. --- --- === --- --- # **AUTHORS and CONTRIBUTIONS** --- --- ### Contributions: --- --- * [**Entropy**](https://forums.eagle.ru/member.php?u=111471), **Afinegan**: Came up with the requirement for AIOnOff(). --- --- ### Authors: --- --- * **FlightControl**: Design & Programming --- --- @module Group --- @author FlightControl - ---- The GROUP class --- @type GROUP --- @extends Wrapper.Controllable#CONTROLLABLE --- @field #string GroupName The name of the group. -GROUP = { - ClassName = "GROUP", -} - ---- Create a new GROUP from a DCSGroup --- @param #GROUP self --- @param Dcs.DCSWrapper.Group#Group GroupName The DCS Group name --- @return #GROUP self -function GROUP:Register( GroupName ) - self = BASE:Inherit( self, CONTROLLABLE:New( GroupName ) ) - self:F2( GroupName ) - self.GroupName = GroupName - - self:SetEventPriority( 4 ) - return self -end - --- Reference methods. - ---- Find the GROUP wrapper class instance using the DCS Group. --- @param #GROUP self --- @param Dcs.DCSWrapper.Group#Group DCSGroup The DCS Group. --- @return #GROUP The GROUP. -function GROUP:Find( DCSGroup ) - - local GroupName = DCSGroup:getName() -- Wrapper.Group#GROUP - local GroupFound = _DATABASE:FindGroup( GroupName ) - return GroupFound -end - ---- Find the created GROUP using the DCS Group Name. --- @param #GROUP self --- @param #string GroupName The DCS Group Name. --- @return #GROUP The GROUP. -function GROUP:FindByName( GroupName ) - - local GroupFound = _DATABASE:FindGroup( GroupName ) - return GroupFound -end - --- DCS Group methods support. - ---- Returns the DCS Group. --- @param #GROUP self --- @return Dcs.DCSWrapper.Group#Group The DCS Group. -function GROUP:GetDCSObject() - local DCSGroup = Group.getByName( self.GroupName ) - - if DCSGroup then - return DCSGroup - end - - return nil -end - ---- Returns the @{DCSTypes#Position3} position vectors indicating the point and direction vectors in 3D of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Position The 3D position vectors of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function GROUP:GetPositionVec3() -- Overridden from POSITIONABLE:GetPositionVec3() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionablePosition = DCSPositionable:getUnits()[1]:getPosition().p - self:T3( PositionablePosition ) - return PositionablePosition - end - - return nil -end - ---- Returns if the DCS Group is alive. --- When the group exists at run-time, this method will return true, otherwise false. --- @param #GROUP self --- @return #boolean true if the DCS Group is alive. -function GROUP:IsAlive() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupIsAlive = DCSGroup:isExist() and DCSGroup:getUnit(1) ~= nil - self:T3( GroupIsAlive ) - return GroupIsAlive - end - - return nil -end - ---- Destroys the DCS Group and all of its DCS Units. --- Note that this destroy method also raises a destroy event at run-time. --- So all event listeners will catch the destroy event of this DCS Group. --- @param #GROUP self -function GROUP:Destroy() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - for Index, UnitData in pairs( DCSGroup:getUnits() ) do - self:CreateEventCrash( timer.getTime(), UnitData ) - end - DCSGroup:destroy() - DCSGroup = nil - end - - return nil -end - ---- Returns category of the DCS Group. --- @param #GROUP self --- @return Dcs.DCSWrapper.Group#Group.Category The category ID -function GROUP:GetCategory() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - if DCSGroup then - local GroupCategory = DCSGroup:getCategory() - self:T3( GroupCategory ) - return GroupCategory - end - - return nil -end - ---- Returns the category name of the #GROUP. --- @param #GROUP self --- @return #string Category name = Helicopter, Airplane, Ground Unit, Ship -function GROUP:GetCategoryName() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - if DCSGroup then - local CategoryNames = { - [Group.Category.AIRPLANE] = "Airplane", - [Group.Category.HELICOPTER] = "Helicopter", - [Group.Category.GROUND] = "Ground Unit", - [Group.Category.SHIP] = "Ship", - } - local GroupCategory = DCSGroup:getCategory() - self:T3( GroupCategory ) - - return CategoryNames[GroupCategory] - end - - return nil -end - - ---- Returns the coalition of the DCS Group. --- @param #GROUP self --- @return Dcs.DCSCoalitionWrapper.Object#coalition.side The coalition side of the DCS Group. -function GROUP:GetCoalition() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - if DCSGroup then - local GroupCoalition = DCSGroup:getCoalition() - self:T3( GroupCoalition ) - return GroupCoalition - end - - return nil -end - ---- Returns the country of the DCS Group. --- @param #GROUP self --- @return Dcs.DCScountry#country.id The country identifier. --- @return #nil The DCS Group is not existing or alive. -function GROUP:GetCountry() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - if DCSGroup then - local GroupCountry = DCSGroup:getUnit(1):getCountry() - self:T3( GroupCountry ) - return GroupCountry - end - - return nil -end - ---- Returns the UNIT wrapper class with number UnitNumber. --- If the underlying DCS Unit does not exist, the method will return nil. . --- @param #GROUP self --- @param #number UnitNumber The number of the UNIT wrapper class to be returned. --- @return Wrapper.Unit#UNIT The UNIT wrapper class. -function GROUP:GetUnit( UnitNumber ) - self:F2( { self.GroupName, UnitNumber } ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local UnitFound = UNIT:Find( DCSGroup:getUnit( UnitNumber ) ) - self:T2( UnitFound ) - return UnitFound - end - - return nil -end - ---- Returns the DCS Unit with number UnitNumber. --- If the underlying DCS Unit does not exist, the method will return nil. . --- @param #GROUP self --- @param #number UnitNumber The number of the DCS Unit to be returned. --- @return Dcs.DCSWrapper.Unit#Unit The DCS Unit. -function GROUP:GetDCSUnit( UnitNumber ) - self:F2( { self.GroupName, UnitNumber } ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local DCSUnitFound = DCSGroup:getUnit( UnitNumber ) - self:T3( DCSUnitFound ) - return DCSUnitFound - end - - return nil -end - ---- Returns current size of the DCS Group. --- If some of the DCS Units of the DCS Group are destroyed the size of the DCS Group is changed. --- @param #GROUP self --- @return #number The DCS Group size. -function GROUP:GetSize() - self:F2( { self.GroupName } ) - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupSize = DCSGroup:getSize() - self:T3( GroupSize ) - return GroupSize - end - - return nil -end - ---- ---- Returns the initial size of the DCS Group. --- If some of the DCS Units of the DCS Group are destroyed, the initial size of the DCS Group is unchanged. --- @param #GROUP self --- @return #number The DCS Group initial size. -function GROUP:GetInitialSize() - self:F2( { self.GroupName } ) - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupInitialSize = DCSGroup:getInitialSize() - self:T3( GroupInitialSize ) - return GroupInitialSize - end - - return nil -end - - ---- Returns the DCS Units of the DCS Group. --- @param #GROUP self --- @return #table The DCS Units. -function GROUP:GetDCSUnits() - self:F2( { self.GroupName } ) - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local DCSUnits = DCSGroup:getUnits() - self:T3( DCSUnits ) - return DCSUnits - end - - return nil -end - - ---- Activates a GROUP. --- @param #GROUP self -function GROUP:Activate() - self:F2( { self.GroupName } ) - trigger.action.activateGroup( self:GetDCSObject() ) - return self:GetDCSObject() -end - - ---- Gets the type name of the group. --- @param #GROUP self --- @return #string The type name of the group. -function GROUP:GetTypeName() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupTypeName = DCSGroup:getUnit(1):getTypeName() - self:T3( GroupTypeName ) - return( GroupTypeName ) - end - - return nil -end - ---- Gets the CallSign of the first DCS Unit of the DCS Group. --- @param #GROUP self --- @return #string The CallSign of the first DCS Unit of the DCS Group. -function GROUP:GetCallsign() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupCallSign = DCSGroup:getUnit(1):getCallsign() - self:T3( GroupCallSign ) - return GroupCallSign - end - - return nil -end - ---- Returns the current point (Vec2 vector) of the first DCS Unit in the DCS Group. --- @param #GROUP self --- @return Dcs.DCSTypes#Vec2 Current Vec2 point of the first DCS Unit of the DCS Group. -function GROUP:GetVec2() - self:F2( self.GroupName ) - - local UnitPoint = self:GetUnit(1) - UnitPoint:GetVec2() - local GroupPointVec2 = UnitPoint:GetVec2() - self:T3( GroupPointVec2 ) - return GroupPointVec2 -end - ---- Returns the current Vec3 vector of the first DCS Unit in the GROUP. --- @return Dcs.DCSTypes#Vec3 Current Vec3 of the first DCS Unit of the GROUP. -function GROUP:GetVec3() - self:F2( self.GroupName ) - - local GroupVec3 = self:GetUnit(1):GetVec3() - self:T3( GroupVec3 ) - return GroupVec3 -end - - - -do -- Is Zone methods - ---- Returns true if all units of the group are within a @{Zone}. --- @param #GROUP self --- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the Group is completely within the @{Zone#ZONE_BASE} -function GROUP:IsCompletelyInZone( Zone ) - self:F2( { self.GroupName, Zone } ) - - for UnitID, UnitData in pairs( self:GetUnits() ) do - local Unit = UnitData -- Wrapper.Unit#UNIT - if Zone:IsVec3InZone( Unit:GetVec3() ) then - else - return false - end - end - - return true -end - ---- Returns true if some units of the group are within a @{Zone}. --- @param #GROUP self --- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the Group is completely within the @{Zone#ZONE_BASE} -function GROUP:IsPartlyInZone( Zone ) - self:F2( { self.GroupName, Zone } ) - - for UnitID, UnitData in pairs( self:GetUnits() ) do - local Unit = UnitData -- Wrapper.Unit#UNIT - if Zone:IsVec3InZone( Unit:GetVec3() ) then - return true - end - end - - return false -end - ---- Returns true if none of the group units of the group are within a @{Zone}. --- @param #GROUP self --- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the Group is completely within the @{Zone#ZONE_BASE} -function GROUP:IsNotInZone( Zone ) - self:F2( { self.GroupName, Zone } ) - - for UnitID, UnitData in pairs( self:GetUnits() ) do - local Unit = UnitData -- Wrapper.Unit#UNIT - if Zone:IsVec3InZone( Unit:GetVec3() ) then - return false - end - end - - return true -end - ---- Returns if the group is of an air category. --- If the group is a helicopter or a plane, then this method will return true, otherwise false. --- @param #GROUP self --- @return #boolean Air category evaluation result. -function GROUP:IsAir() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local IsAirResult = DCSGroup:getCategory() == Group.Category.AIRPLANE or DCSGroup:getCategory() == Group.Category.HELICOPTER - self:T3( IsAirResult ) - return IsAirResult - end - - return nil -end - ---- Returns if the DCS Group contains Helicopters. --- @param #GROUP self --- @return #boolean true if DCS Group contains Helicopters. -function GROUP:IsHelicopter() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupCategory = DCSGroup:getCategory() - self:T2( GroupCategory ) - return GroupCategory == Group.Category.HELICOPTER - end - - return nil -end - ---- Returns if the DCS Group contains AirPlanes. --- @param #GROUP self --- @return #boolean true if DCS Group contains AirPlanes. -function GROUP:IsAirPlane() - self:F2() - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupCategory = DCSGroup:getCategory() - self:T2( GroupCategory ) - return GroupCategory == Group.Category.AIRPLANE - end - - return nil -end - ---- Returns if the DCS Group contains Ground troops. --- @param #GROUP self --- @return #boolean true if DCS Group contains Ground troops. -function GROUP:IsGround() - self:F2() - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupCategory = DCSGroup:getCategory() - self:T2( GroupCategory ) - return GroupCategory == Group.Category.GROUND - end - - return nil -end - ---- Returns if the DCS Group contains Ships. --- @param #GROUP self --- @return #boolean true if DCS Group contains Ships. -function GROUP:IsShip() - self:F2() - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupCategory = DCSGroup:getCategory() - self:T2( GroupCategory ) - return GroupCategory == Group.Category.SHIP - end - - return nil -end - ---- Returns if all units of the group are on the ground or landed. --- If all units of this group are on the ground, this function will return true, otherwise false. --- @param #GROUP self --- @return #boolean All units on the ground result. -function GROUP:AllOnGround() - self:F2() - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local AllOnGroundResult = true - - for Index, UnitData in pairs( DCSGroup:getUnits() ) do - if UnitData:inAir() then - AllOnGroundResult = false - end - end - - self:T3( AllOnGroundResult ) - return AllOnGroundResult - end - - return nil -end - -end - -do -- AI methods - - --- Turns the AI On or Off for the GROUP. - -- @param #GROUP self - -- @param #boolean AIOnOff The value true turns the AI On, the value false turns the AI Off. - -- @return #GROUP The GROUP. - function GROUP:SetAIOnOff( AIOnOff ) - - local DCSGroup = self:GetDCSObject() -- Dcs.DCSGroup#Group - - if DCSGroup then - local DCSController = DCSGroup:getController() -- Dcs.DCSController#Controller - if DCSController then - DCSController:setOnOff( AIOnOff ) - return self - end - end - - return nil - end - - --- Turns the AI On for the GROUP. - -- @param #GROUP self - -- @return #GROUP The GROUP. - function GROUP:SetAIOn() - - return self:SetAIOnOff( true ) - end - - --- Turns the AI Off for the GROUP. - -- @param #GROUP self - -- @return #GROUP The GROUP. - function GROUP:SetAIOff() - - return self:SetAIOnOff( false ) - end - -end - - - ---- Returns the current maximum velocity of the group. --- Each unit within the group gets evaluated, and the maximum velocity (= the unit which is going the fastest) is returned. --- @param #GROUP self --- @return #number Maximum velocity found. -function GROUP:GetMaxVelocity() - self:F2() - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupVelocityMax = 0 - - for Index, UnitData in pairs( DCSGroup:getUnits() ) do - - local UnitVelocityVec3 = UnitData:getVelocity() - local UnitVelocity = math.abs( UnitVelocityVec3.x ) + math.abs( UnitVelocityVec3.y ) + math.abs( UnitVelocityVec3.z ) - - if UnitVelocity > GroupVelocityMax then - GroupVelocityMax = UnitVelocity - end - end - - return GroupVelocityMax - end - - return nil -end - ---- Returns the current minimum height of the group. --- Each unit within the group gets evaluated, and the minimum height (= the unit which is the lowest elevated) is returned. --- @param #GROUP self --- @return #number Minimum height found. -function GROUP:GetMinHeight() - self:F2() - -end - ---- Returns the current maximum height of the group. --- Each unit within the group gets evaluated, and the maximum height (= the unit which is the highest elevated) is returned. --- @param #GROUP self --- @return #number Maximum height found. -function GROUP:GetMaxHeight() - self:F2() - -end - --- SPAWNING - ---- Respawn the @{GROUP} using a (tweaked) template of the Group. --- The template must be retrieved with the @{Group#GROUP.GetTemplate}() function. --- The template contains all the definitions as declared within the mission file. --- To understand templates, do the following: --- --- * unpack your .miz file into a directory using 7-zip. --- * browse in the directory created to the file **mission**. --- * open the file and search for the country group definitions. --- --- Your group template will contain the fields as described within the mission file. --- --- This function will: --- --- * Get the current position and heading of the group. --- * When the group is alive, it will tweak the template x, y and heading coordinates of the group and the embedded units to the current units positions. --- * Then it will destroy the current alive group. --- * And it will respawn the group using your new template definition. --- @param Wrapper.Group#GROUP self --- @param #table Template The template of the Group retrieved with GROUP:GetTemplate() -function GROUP:Respawn( Template ) - - local Vec3 = self:GetVec3() - Template.x = Vec3.x - Template.y = Vec3.z - --Template.x = nil - --Template.y = nil - - self:E( #Template.units ) - for UnitID, UnitData in pairs( self:GetUnits() ) do - local GroupUnit = UnitData -- Wrapper.Unit#UNIT - self:E( GroupUnit:GetName() ) - if GroupUnit:IsAlive() then - local GroupUnitVec3 = GroupUnit:GetVec3() - local GroupUnitHeading = GroupUnit:GetHeading() - Template.units[UnitID].alt = GroupUnitVec3.y - Template.units[UnitID].x = GroupUnitVec3.x - Template.units[UnitID].y = GroupUnitVec3.z - Template.units[UnitID].heading = GroupUnitHeading - self:E( { UnitID, Template.units[UnitID], Template.units[UnitID] } ) - end - end - - self:Destroy() - _DATABASE:Spawn( Template ) -end - ---- Returns the group template from the @{DATABASE} (_DATABASE object). --- @param #GROUP self --- @return #table -function GROUP:GetTemplate() - local GroupName = self:GetName() - self:E( GroupName ) - return _DATABASE:GetGroupTemplate( GroupName ) -end - ---- Sets the controlled status in a Template. --- @param #GROUP self --- @param #boolean Controlled true is controlled, false is uncontrolled. --- @return #table -function GROUP:SetTemplateControlled( Template, Controlled ) - Template.uncontrolled = not Controlled - return Template -end - ---- Sets the CountryID of the group in a Template. --- @param #GROUP self --- @param Dcs.DCScountry#country.id CountryID The country ID. --- @return #table -function GROUP:SetTemplateCountry( Template, CountryID ) - Template.CountryID = CountryID - return Template -end - ---- Sets the CoalitionID of the group in a Template. --- @param #GROUP self --- @param Dcs.DCSCoalitionWrapper.Object#coalition.side CoalitionID The coalition ID. --- @return #table -function GROUP:SetTemplateCoalition( Template, CoalitionID ) - Template.CoalitionID = CoalitionID - return Template -end - - - - ---- Return the mission template of the group. --- @param #GROUP self --- @return #table The MissionTemplate -function GROUP:GetTaskMission() - self:F2( self.GroupName ) - - return routines.utils.deepCopy( _DATABASE.Templates.Groups[self.GroupName].Template ) -end - ---- Return the mission route of the group. --- @param #GROUP self --- @return #table The mission route defined by points. -function GROUP:GetTaskRoute() - self:F2( self.GroupName ) - - return routines.utils.deepCopy( _DATABASE.Templates.Groups[self.GroupName].Template.route.points ) -end - ---- Return the route of a group by using the @{Database#DATABASE} class. --- @param #GROUP self --- @param #number Begin The route point from where the copy will start. The base route point is 0. --- @param #number End The route point where the copy will end. The End point is the last point - the End point. The last point has base 0. --- @param #boolean Randomize Randomization of the route, when true. --- @param #number Radius When randomization is on, the randomization is within the radius. -function GROUP:CopyRoute( Begin, End, Randomize, Radius ) - self:F2( { Begin, End } ) - - local Points = {} - - -- Could be a Spawned Group - local GroupName = string.match( self:GetName(), ".*#" ) - if GroupName then - GroupName = GroupName:sub( 1, -2 ) - else - GroupName = self:GetName() - end - - self:T3( { GroupName } ) - - local Template = _DATABASE.Templates.Groups[GroupName].Template - - if Template then - if not Begin then - Begin = 0 - end - if not End then - End = 0 - end - - for TPointID = Begin + 1, #Template.route.points - End do - if Template.route.points[TPointID] then - Points[#Points+1] = routines.utils.deepCopy( Template.route.points[TPointID] ) - if Randomize then - if not Radius then - Radius = 500 - end - Points[#Points].x = Points[#Points].x + math.random( Radius * -1, Radius ) - Points[#Points].y = Points[#Points].y + math.random( Radius * -1, Radius ) - end - end - end - return Points - else - error( "Template not found for Group : " .. GroupName ) - end - - return nil -end - ---- Calculate the maxium A2G threat level of the Group. --- @param #GROUP self -function GROUP:CalculateThreatLevelA2G() - - local MaxThreatLevelA2G = 0 - for UnitName, UnitData in pairs( self:GetUnits() ) do - local ThreatUnit = UnitData -- Wrapper.Unit#UNIT - local ThreatLevelA2G = ThreatUnit:GetThreatLevel() - if ThreatLevelA2G > MaxThreatLevelA2G then - MaxThreatLevelA2G = ThreatLevelA2G - end - end - - self:T3( MaxThreatLevelA2G ) - return MaxThreatLevelA2G -end - ---- Returns true if the first unit of the GROUP is in the air. --- @param Wrapper.Group#GROUP self --- @return #boolean true if in the first unit of the group is in the air. --- @return #nil The GROUP is not existing or not alive. -function GROUP:InAir() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local DCSUnit = DCSGroup:getUnit(1) - if DCSUnit then - local GroupInAir = DCSGroup:getUnit(1):inAir() - self:T3( GroupInAir ) - return GroupInAir - end - end - - return nil -end - -function GROUP:OnReSpawn( ReSpawnFunction ) - - self.ReSpawnFunction = ReSpawnFunction -end - -do -- Event Handling - - --- Subscribe to a DCS Event. - -- @param #GROUP self - -- @param Core.Event#EVENTS Event - -- @param #function EventFunction (optional) The function to be called when the event occurs for the GROUP. - -- @return #GROUP - function GROUP:HandleEvent( Event, EventFunction ) - - self:EventDispatcher():OnEventForGroup( self:GetName(), EventFunction, self, Event ) - - return self - end - - --- UnSubscribe to a DCS event. - -- @param #GROUP self - -- @param Core.Event#EVENTS Event - -- @return #GROUP - function GROUP:UnHandleEvent( Event ) - - self:EventDispatcher():RemoveForGroup( self:GetName(), self, Event ) - - return self - end - -end - -do -- Players - - --- Get player names - -- @param #GROUP self - -- @return #table The group has players, an array of player names is returned. - -- @return #nil The group has no players - function GROUP:GetPlayerNames() - - local PlayerNames = nil - - local Units = self:GetUnits() - for UnitID, UnitData in pairs( Units ) do - local Unit = UnitData -- Wrapper.Unit#UNIT - local PlayerName = Unit:GetPlayerName() - if PlayerName and PlayerName ~= "" then - PlayerNames = PlayerNames or {} - table.insert( PlayerNames, PlayerName ) - end - end - - self:F( PlayerNames ) - return PlayerNames - end - -end--- This module contains the UNIT class. --- --- 1) @{#UNIT} class, extends @{Controllable#CONTROLLABLE} --- =========================================================== --- The @{#UNIT} class is a wrapper class to handle the DCS Unit objects: --- --- * Support all DCS Unit APIs. --- * Enhance with Unit specific APIs not in the DCS Unit API set. --- * Handle local Unit Controller. --- * Manage the "state" of the DCS Unit. --- --- --- 1.1) UNIT reference methods --- ---------------------- --- For each DCS Unit object alive within a running mission, a UNIT wrapper object (instance) will be created within the _@{DATABASE} object. --- This is done at the beginning of the mission (when the mission starts), and dynamically when new DCS Unit objects are spawned (using the @{SPAWN} class). --- --- The UNIT class **does not contain a :New()** method, rather it provides **:Find()** methods to retrieve the object reference --- using the DCS Unit or the DCS UnitName. --- --- Another thing to know is that UNIT objects do not "contain" the DCS Unit object. --- The UNIT methods will reference the DCS Unit object by name when it is needed during API execution. --- If the DCS Unit object does not exist or is nil, the UNIT methods will return nil and log an exception in the DCS.log file. --- --- The UNIT class provides the following functions to retrieve quickly the relevant UNIT instance: --- --- * @{#UNIT.Find}(): Find a UNIT instance from the _DATABASE object using a DCS Unit object. --- * @{#UNIT.FindByName}(): Find a UNIT instance from the _DATABASE object using a DCS Unit name. --- --- IMPORTANT: ONE SHOULD NEVER SANATIZE these UNIT OBJECT REFERENCES! (make the UNIT object references nil). --- --- 1.2) DCS UNIT APIs --- ------------------ --- The DCS Unit APIs are used extensively within MOOSE. The UNIT class has for each DCS Unit API a corresponding method. --- To be able to distinguish easily in your code the difference between a UNIT API call and a DCS Unit API call, --- the first letter of the method is also capitalized. So, by example, the DCS Unit method @{DCSWrapper.Unit#Unit.getName}() --- is implemented in the UNIT class as @{#UNIT.GetName}(). --- --- 1.3) Smoke, Flare Units --- ----------------------- --- The UNIT class provides methods to smoke or flare units easily. --- The @{#UNIT.SmokeBlue}(), @{#UNIT.SmokeGreen}(),@{#UNIT.SmokeOrange}(), @{#UNIT.SmokeRed}(), @{#UNIT.SmokeRed}() methods --- will smoke the unit in the corresponding color. Note that smoking a unit is done at the current position of the DCS Unit. --- When the DCS Unit moves for whatever reason, the smoking will still continue! --- The @{#UNIT.FlareGreen}(), @{#UNIT.FlareRed}(), @{#UNIT.FlareWhite}(), @{#UNIT.FlareYellow}() --- methods will fire off a flare in the air with the corresponding color. Note that a flare is a one-off shot and its effect is of very short duration. --- --- 1.4) Location Position, Point --- ----------------------------- --- The UNIT class provides methods to obtain the current point or position of the DCS Unit. --- The @{#UNIT.GetPointVec2}(), @{#UNIT.GetVec3}() will obtain the current **location** of the DCS Unit in a Vec2 (2D) or a **point** in a Vec3 (3D) vector respectively. --- If you want to obtain the complete **3D position** including ori�ntation and direction vectors, consult the @{#UNIT.GetPositionVec3}() method respectively. --- --- 1.5) Test if alive --- ------------------ --- The @{#UNIT.IsAlive}(), @{#UNIT.IsActive}() methods determines if the DCS Unit is alive, meaning, it is existing and active. --- --- 1.6) Test for proximity --- ----------------------- --- The UNIT class contains methods to test the location or proximity against zones or other objects. --- --- ### 1.6.1) Zones --- To test whether the Unit is within a **zone**, use the @{#UNIT.IsInZone}() or the @{#UNIT.IsNotInZone}() methods. Any zone can be tested on, but the zone must be derived from @{Zone#ZONE_BASE}. --- --- ### 1.6.2) Units --- Test if another DCS Unit is within a given radius of the current DCS Unit, use the @{#UNIT.OtherUnitInRadius}() method. --- --- @module Unit --- @author FlightControl - - - - - ---- The UNIT class --- @type UNIT --- @extends Wrapper.Controllable#CONTROLLABLE -UNIT = { - ClassName="UNIT", -} - - ---- Unit.SensorType --- @type Unit.SensorType --- @field OPTIC --- @field RADAR --- @field IRST --- @field RWR - - --- Registration. - ---- Create a new UNIT from DCSUnit. --- @param #UNIT self --- @param #string UnitName The name of the DCS unit. --- @return #UNIT -function UNIT:Register( UnitName ) - local self = BASE:Inherit( self, CONTROLLABLE:New( UnitName ) ) - self.UnitName = UnitName - - self:SetEventPriority( 3 ) - return self -end - --- Reference methods. - ---- Finds a UNIT from the _DATABASE using a DCSUnit object. --- @param #UNIT self --- @param Dcs.DCSWrapper.Unit#Unit DCSUnit An existing DCS Unit object reference. --- @return #UNIT self -function UNIT:Find( DCSUnit ) - - local UnitName = DCSUnit:getName() - local UnitFound = _DATABASE:FindUnit( UnitName ) - return UnitFound -end - ---- Find a UNIT in the _DATABASE using the name of an existing DCS Unit. --- @param #UNIT self --- @param #string UnitName The Unit Name. --- @return #UNIT self -function UNIT:FindByName( UnitName ) - - local UnitFound = _DATABASE:FindUnit( UnitName ) - return UnitFound -end - ---- Return the name of the UNIT. --- @param #UNIT self --- @return #string The UNIT name. -function UNIT:Name() - - return self.UnitName -end - - ---- @param #UNIT self --- @return Dcs.DCSWrapper.Unit#Unit -function UNIT:GetDCSObject() - - local DCSUnit = Unit.getByName( self.UnitName ) - - if DCSUnit then - return DCSUnit - end - - return nil -end - ---- Respawn the @{Unit} using a (tweaked) template of the parent Group. --- --- This function will: --- --- * Get the current position and heading of the group. --- * When the unit is alive, it will tweak the template x, y and heading coordinates of the group and the embedded units to the current units positions. --- * Then it will respawn the re-modelled group. --- --- @param #UNIT self --- @param Dcs.DCSTypes#Vec3 SpawnVec3 The position where to Spawn the new Unit at. --- @param #number Heading The heading of the unit respawn. -function UNIT:ReSpawn( SpawnVec3, Heading ) - - local SpawnGroupTemplate = UTILS.DeepCopy( _DATABASE:GetGroupTemplateFromUnitName( self:Name() ) ) - self:T( SpawnGroupTemplate ) - - local SpawnGroup = self:GetGroup() - - if SpawnGroup then - - local Vec3 = SpawnGroup:GetVec3() - SpawnGroupTemplate.x = SpawnVec3.x - SpawnGroupTemplate.y = SpawnVec3.z - - self:E( #SpawnGroupTemplate.units ) - for UnitID, UnitData in pairs( SpawnGroup:GetUnits() ) do - local GroupUnit = UnitData -- #UNIT - self:E( GroupUnit:GetName() ) - if GroupUnit:IsAlive() then - local GroupUnitVec3 = GroupUnit:GetVec3() - local GroupUnitHeading = GroupUnit:GetHeading() - SpawnGroupTemplate.units[UnitID].alt = GroupUnitVec3.y - SpawnGroupTemplate.units[UnitID].x = GroupUnitVec3.x - SpawnGroupTemplate.units[UnitID].y = GroupUnitVec3.z - SpawnGroupTemplate.units[UnitID].heading = GroupUnitHeading - self:E( { UnitID, SpawnGroupTemplate.units[UnitID], SpawnGroupTemplate.units[UnitID] } ) - end - end - end - - for UnitTemplateID, UnitTemplateData in pairs( SpawnGroupTemplate.units ) do - self:T( UnitTemplateData.name ) - if UnitTemplateData.name == self:Name() then - self:T("Adjusting") - SpawnGroupTemplate.units[UnitTemplateID].alt = SpawnVec3.y - SpawnGroupTemplate.units[UnitTemplateID].x = SpawnVec3.x - SpawnGroupTemplate.units[UnitTemplateID].y = SpawnVec3.z - SpawnGroupTemplate.units[UnitTemplateID].heading = Heading - self:E( { UnitTemplateID, SpawnGroupTemplate.units[UnitTemplateID], SpawnGroupTemplate.units[UnitTemplateID] } ) - else - self:E( SpawnGroupTemplate.units[UnitTemplateID].name ) - local GroupUnit = UNIT:FindByName( SpawnGroupTemplate.units[UnitTemplateID].name ) -- #UNIT - if GroupUnit and GroupUnit:IsAlive() then - local GroupUnitVec3 = GroupUnit:GetVec3() - local GroupUnitHeading = GroupUnit:GetHeading() - UnitTemplateData.alt = GroupUnitVec3.y - UnitTemplateData.x = GroupUnitVec3.x - UnitTemplateData.y = GroupUnitVec3.z - UnitTemplateData.heading = GroupUnitHeading - else - if SpawnGroupTemplate.units[UnitTemplateID].name ~= self:Name() then - self:T("nilling") - SpawnGroupTemplate.units[UnitTemplateID].delete = true - end - end - end - end - - -- Remove obscolete units from the group structure - i = 1 - while i <= #SpawnGroupTemplate.units do - - local UnitTemplateData = SpawnGroupTemplate.units[i] - self:T( UnitTemplateData.name ) - - if UnitTemplateData.delete then - table.remove( SpawnGroupTemplate.units, i ) - else - i = i + 1 - end - end - - _DATABASE:Spawn( SpawnGroupTemplate ) -end - - - ---- Returns if the unit is activated. --- @param #UNIT self --- @return #boolean true if Unit is activated. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:IsActive() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - - local UnitIsActive = DCSUnit:isActive() - return UnitIsActive - end - - return nil -end - - - ---- Returns the Unit's callsign - the localized string. --- @param #UNIT self --- @return #string The Callsign of the Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetCallsign() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitCallSign = DCSUnit:getCallsign() - return UnitCallSign - end - - self:E( self.ClassName .. " " .. self.UnitName .. " not found!" ) - return nil -end - - ---- Returns name of the player that control the unit or nil if the unit is controlled by A.I. --- @param #UNIT self --- @return #string Player Name --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetPlayerName() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - - local PlayerName = DCSUnit:getPlayerName() - if PlayerName == nil then - PlayerName = "" - end - return PlayerName - end - - return nil -end - ---- Returns the unit's number in the group. --- The number is the same number the unit has in ME. --- It may not be changed during the mission. --- If any unit in the group is destroyed, the numbers of another units will not be changed. --- @param #UNIT self --- @return #number The Unit number. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetNumber() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitNumber = DCSUnit:getNumber() - return UnitNumber - end - - return nil -end - ---- Returns the unit's group if it exist and nil otherwise. --- @param Wrapper.Unit#UNIT self --- @return Wrapper.Group#GROUP The Group of the Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetGroup() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitGroup = GROUP:Find( DCSUnit:getGroup() ) - return UnitGroup - end - - return nil -end - - --- Need to add here functions to check if radar is on and which object etc. - ---- Returns the prefix name of the DCS Unit. A prefix name is a part of the name before a '#'-sign. --- DCS Units spawned with the @{SPAWN} class contain a '#'-sign to indicate the end of the (base) DCS Unit name. --- The spawn sequence number and unit number are contained within the name after the '#' sign. --- @param #UNIT self --- @return #string The name of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetPrefix() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitPrefix = string.match( self.UnitName, ".*#" ):sub( 1, -2 ) - self:T3( UnitPrefix ) - return UnitPrefix - end - - return nil -end - ---- Returns the Unit's ammunition. --- @param #UNIT self --- @return Dcs.DCSWrapper.Unit#Unit.Ammo --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetAmmo() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitAmmo = DCSUnit:getAmmo() - return UnitAmmo - end - - return nil -end - ---- Returns the unit sensors. --- @param #UNIT self --- @return Dcs.DCSWrapper.Unit#Unit.Sensors --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetSensors() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitSensors = DCSUnit:getSensors() - return UnitSensors - end - - return nil -end - --- Need to add here a function per sensortype --- unit:hasSensors(Unit.SensorType.RADAR, Unit.RadarType.AS) - ---- Returns if the unit has sensors of a certain type. --- @param #UNIT self --- @return #boolean returns true if the unit has specified types of sensors. This function is more preferable than Unit.getSensors() if you don't want to get information about all the unit's sensors, and just want to check if the unit has specified types of sensors. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:HasSensors( ... ) - self:F2( arg ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local HasSensors = DCSUnit:hasSensors( unpack( arg ) ) - return HasSensors - end - - return nil -end - ---- Returns if the unit is SEADable. --- @param #UNIT self --- @return #boolean returns true if the unit is SEADable. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:HasSEAD() - self:F2() - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitSEADAttributes = DCSUnit:getDesc().attributes - - local HasSEAD = false - if UnitSEADAttributes["RADAR_BAND1_FOR_ARM"] and UnitSEADAttributes["RADAR_BAND1_FOR_ARM"] == true or - UnitSEADAttributes["RADAR_BAND2_FOR_ARM"] and UnitSEADAttributes["RADAR_BAND2_FOR_ARM"] == true then - HasSEAD = true - end - return HasSEAD - end - - return nil -end - ---- Returns two values: --- --- * First value indicates if at least one of the unit's radar(s) is on. --- * Second value is the object of the radar's interest. Not nil only if at least one radar of the unit is tracking a target. --- @param #UNIT self --- @return #boolean Indicates if at least one of the unit's radar(s) is on. --- @return Dcs.DCSWrapper.Object#Object The object of the radar's interest. Not nil only if at least one radar of the unit is tracking a target. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetRadar() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitRadarOn, UnitRadarObject = DCSUnit:getRadar() - return UnitRadarOn, UnitRadarObject - end - - return nil, nil -end - ---- Returns relative amount of fuel (from 0.0 to 1.0) the unit has in its internal tanks. If there are additional fuel tanks the value may be greater than 1.0. --- @param #UNIT self --- @return #number The relative amount of fuel (from 0.0 to 1.0). --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetFuel() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitFuel = DCSUnit:getFuel() - return UnitFuel - end - - return nil -end - ---- Returns the UNIT in a UNIT list of one element. --- @param #UNIT self --- @return #list The UNITs wrappers. -function UNIT:GetUnits() - self:F2( { self.UnitName } ) - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local DCSUnits = DCSUnit:getUnits() - local Units = {} - Units[1] = UNIT:Find( DCSUnit ) - self:T3( Units ) - return Units - end - - return nil -end - - ---- Returns the unit's health. Dead units has health <= 1.0. --- @param #UNIT self --- @return #number The Unit's health value. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetLife() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitLife = DCSUnit:getLife() - return UnitLife - end - - return nil -end - ---- Returns the Unit's initial health. --- @param #UNIT self --- @return #number The Unit's initial health value. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetLife0() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitLife0 = DCSUnit:getLife0() - return UnitLife0 - end - - return nil -end - ---- Returns the category name of the #UNIT. --- @param #UNIT self --- @return #string Category name = Helicopter, Airplane, Ground Unit, Ship -function UNIT:GetCategoryName() - self:F3( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - if DCSUnit then - local CategoryNames = { - [Unit.Category.AIRPLANE] = "Airplane", - [Unit.Category.HELICOPTER] = "Helicopter", - [Unit.Category.GROUND_UNIT] = "Ground Unit", - [Unit.Category.SHIP] = "Ship", - [Unit.Category.STRUCTURE] = "Structure", - } - local UnitCategory = DCSUnit:getDesc().category - self:T3( UnitCategory ) - - return CategoryNames[UnitCategory] - end - - return nil -end - - ---- Returns the Unit's A2G threat level on a scale from 1 to 10 ... --- The following threat levels are foreseen: --- --- * Threat level 0: Unit is unarmed. --- * Threat level 1: Unit is infantry. --- * Threat level 2: Unit is an infantry vehicle. --- * Threat level 3: Unit is ground artillery. --- * Threat level 4: Unit is a tank. --- * Threat level 5: Unit is a modern tank or ifv with ATGM. --- * Threat level 6: Unit is a AAA. --- * Threat level 7: Unit is a SAM or manpad, IR guided. --- * Threat level 8: Unit is a Short Range SAM, radar guided. --- * Threat level 9: Unit is a Medium Range SAM, radar guided. --- * Threat level 10: Unit is a Long Range SAM, radar guided. --- @param #UNIT self -function UNIT:GetThreatLevel() - - local Attributes = self:GetDesc().attributes - self:T( Attributes ) - - local ThreatLevel = 0 - local ThreatText = "" - - if self:IsGround() then - - self:T( "Ground" ) - - local ThreatLevels = { - "Unarmed", - "Infantry", - "Old Tanks & APCs", - "Tanks & IFVs without ATGM", - "Tanks & IFV with ATGM", - "Modern Tanks", - "AAA", - "IR Guided SAMs", - "SR SAMs", - "MR SAMs", - "LR SAMs" - } - - - if Attributes["LR SAM"] then ThreatLevel = 10 - elseif Attributes["MR SAM"] then ThreatLevel = 9 - elseif Attributes["SR SAM"] and - not Attributes["IR Guided SAM"] then ThreatLevel = 8 - elseif ( Attributes["SR SAM"] or Attributes["MANPADS"] ) and - Attributes["IR Guided SAM"] then ThreatLevel = 7 - elseif Attributes["AAA"] then ThreatLevel = 6 - elseif Attributes["Modern Tanks"] then ThreatLevel = 5 - elseif ( Attributes["Tanks"] or Attributes["IFV"] ) and - Attributes["ATGM"] then ThreatLevel = 4 - elseif ( Attributes["Tanks"] or Attributes["IFV"] ) and - not Attributes["ATGM"] then ThreatLevel = 3 - elseif Attributes["Old Tanks"] or Attributes["APC"] or Attributes["Artillery"] then ThreatLevel = 2 - elseif Attributes["Infantry"] then ThreatLevel = 1 - end - - ThreatText = ThreatLevels[ThreatLevel+1] - end - - if self:IsAir() then - - self:T( "Air" ) - - local ThreatLevels = { - "Unarmed", - "Tanker", - "AWACS", - "Transport Helicpter", - "UAV", - "Bomber", - "Strategic Bomber", - "Attack Helicopter", - "Interceptor", - "Multirole Fighter", - "Fighter" - } - - - if Attributes["Fighters"] then ThreatLevel = 10 - elseif Attributes["Multirole fighters"] then ThreatLevel = 9 - elseif Attributes["Battleplanes"] then ThreatLevel = 8 - elseif Attributes["Attack helicopters"] then ThreatLevel = 7 - elseif Attributes["Strategic bombers"] then ThreatLevel = 6 - elseif Attributes["Bombers"] then ThreatLevel = 5 - elseif Attributes["UAVs"] then ThreatLevel = 4 - elseif Attributes["Transport helicopters"] then ThreatLevel = 3 - elseif Attributes["AWACS"] then ThreatLevel = 2 - elseif Attributes["Tankers"] then ThreatLevel = 1 - end - - ThreatText = ThreatLevels[ThreatLevel+1] - end - - if self:IsShip() then - - self:T( "Ship" ) - ---["Aircraft Carriers"] = {"Heavy armed ships",}, ---["Cruisers"] = {"Heavy armed ships",}, ---["Destroyers"] = {"Heavy armed ships",}, ---["Frigates"] = {"Heavy armed ships",}, ---["Corvettes"] = {"Heavy armed ships",}, ---["Heavy armed ships"] = {"Armed ships", "Armed Air Defence", "HeavyArmoredUnits",}, ---["Light armed ships"] = {"Armed ships","NonArmoredUnits"}, ---["Armed ships"] = {"Ships"}, ---["Unarmed ships"] = {"Ships","HeavyArmoredUnits",}, - - local ThreatLevels = { - "Unarmed ship", - "Light armed ships", - "Corvettes", - "", - "Frigates", - "", - "Cruiser", - "", - "Destroyer", - "", - "Aircraft Carrier" - } - - - if Attributes["Aircraft Carriers"] then ThreatLevel = 10 - elseif Attributes["Destroyers"] then ThreatLevel = 8 - elseif Attributes["Cruisers"] then ThreatLevel = 6 - elseif Attributes["Frigates"] then ThreatLevel = 4 - elseif Attributes["Corvettes"] then ThreatLevel = 2 - elseif Attributes["Light armed ships"] then ThreatLevel = 1 - end - - ThreatText = ThreatLevels[ThreatLevel+1] - end - - self:T2( ThreatLevel ) - return ThreatLevel, ThreatText - -end - - --- Is functions - ---- Returns true if the unit is within a @{Zone}. --- @param #UNIT self --- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the unit is within the @{Zone#ZONE_BASE} -function UNIT:IsInZone( Zone ) - self:F2( { self.UnitName, Zone } ) - - if self:IsAlive() then - local IsInZone = Zone:IsVec3InZone( self:GetVec3() ) - - self:T( { IsInZone } ) - return IsInZone - end - - return false -end - ---- Returns true if the unit is not within a @{Zone}. --- @param #UNIT self --- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the unit is not within the @{Zone#ZONE_BASE} -function UNIT:IsNotInZone( Zone ) - self:F2( { self.UnitName, Zone } ) - - if self:IsAlive() then - local IsInZone = not Zone:IsVec3InZone( self:GetVec3() ) - - self:T( { IsInZone } ) - return IsInZone - else - return false - end -end - - ---- Returns true if there is an **other** DCS Unit within a radius of the current 2D point of the DCS Unit. --- @param #UNIT self --- @param #UNIT AwaitUnit The other UNIT wrapper object. --- @param Radius The radius in meters with the DCS Unit in the centre. --- @return true If the other DCS Unit is within the radius of the 2D point of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:OtherUnitInRadius( AwaitUnit, Radius ) - self:F2( { self.UnitName, AwaitUnit.UnitName, Radius } ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitVec3 = self:GetVec3() - local AwaitUnitVec3 = AwaitUnit:GetVec3() - - if (((UnitVec3.x - AwaitUnitVec3.x)^2 + (UnitVec3.z - AwaitUnitVec3.z)^2)^0.5 <= Radius) then - self:T3( "true" ) - return true - else - self:T3( "false" ) - return false - end - end - - return nil -end - - - ---- Signal a flare at the position of the UNIT. --- @param #UNIT self --- @param Utilities.Utils#FLARECOLOR FlareColor -function UNIT:Flare( FlareColor ) - self:F2() - trigger.action.signalFlare( self:GetVec3(), FlareColor , 0 ) -end - ---- Signal a white flare at the position of the UNIT. --- @param #UNIT self -function UNIT:FlareWhite() - self:F2() - trigger.action.signalFlare( self:GetVec3(), trigger.flareColor.White , 0 ) -end - ---- Signal a yellow flare at the position of the UNIT. --- @param #UNIT self -function UNIT:FlareYellow() - self:F2() - trigger.action.signalFlare( self:GetVec3(), trigger.flareColor.Yellow , 0 ) -end - ---- Signal a green flare at the position of the UNIT. --- @param #UNIT self -function UNIT:FlareGreen() - self:F2() - trigger.action.signalFlare( self:GetVec3(), trigger.flareColor.Green , 0 ) -end - ---- Signal a red flare at the position of the UNIT. --- @param #UNIT self -function UNIT:FlareRed() - self:F2() - local Vec3 = self:GetVec3() - if Vec3 then - trigger.action.signalFlare( Vec3, trigger.flareColor.Red, 0 ) - end -end - ---- Smoke the UNIT. --- @param #UNIT self -function UNIT:Smoke( SmokeColor, Range ) - self:F2() - if Range then - trigger.action.smoke( self:GetRandomVec3( Range ), SmokeColor ) - else - trigger.action.smoke( self:GetVec3(), SmokeColor ) - end - -end - ---- Smoke the UNIT Green. --- @param #UNIT self -function UNIT:SmokeGreen() - self:F2() - trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Green ) -end - ---- Smoke the UNIT Red. --- @param #UNIT self -function UNIT:SmokeRed() - self:F2() - trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Red ) -end - ---- Smoke the UNIT White. --- @param #UNIT self -function UNIT:SmokeWhite() - self:F2() - trigger.action.smoke( self:GetVec3(), trigger.smokeColor.White ) -end - ---- Smoke the UNIT Orange. --- @param #UNIT self -function UNIT:SmokeOrange() - self:F2() - trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Orange ) -end - ---- Smoke the UNIT Blue. --- @param #UNIT self -function UNIT:SmokeBlue() - self:F2() - trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Blue ) -end - --- Is methods - ---- Returns if the unit is of an air category. --- If the unit is a helicopter or a plane, then this method will return true, otherwise false. --- @param #UNIT self --- @return #boolean Air category evaluation result. -function UNIT:IsAir() - self:F2() - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitDescriptor = DCSUnit:getDesc() - self:T3( { UnitDescriptor.category, Unit.Category.AIRPLANE, Unit.Category.HELICOPTER } ) - - local IsAirResult = ( UnitDescriptor.category == Unit.Category.AIRPLANE ) or ( UnitDescriptor.category == Unit.Category.HELICOPTER ) - - self:T3( IsAirResult ) - return IsAirResult - end - - return nil -end - ---- Returns if the unit is of an ground category. --- If the unit is a ground vehicle or infantry, this method will return true, otherwise false. --- @param #UNIT self --- @return #boolean Ground category evaluation result. -function UNIT:IsGround() - self:F2() - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitDescriptor = DCSUnit:getDesc() - self:T3( { UnitDescriptor.category, Unit.Category.GROUND_UNIT } ) - - local IsGroundResult = ( UnitDescriptor.category == Unit.Category.GROUND_UNIT ) - - self:T3( IsGroundResult ) - return IsGroundResult - end - - return nil -end - ---- Returns if the unit is a friendly unit. --- @param #UNIT self --- @return #boolean IsFriendly evaluation result. -function UNIT:IsFriendly( FriendlyCoalition ) - self:F2() - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitCoalition = DCSUnit:getCoalition() - self:T3( { UnitCoalition, FriendlyCoalition } ) - - local IsFriendlyResult = ( UnitCoalition == FriendlyCoalition ) - - self:E( IsFriendlyResult ) - return IsFriendlyResult - end - - return nil -end - ---- Returns if the unit is of a ship category. --- If the unit is a ship, this method will return true, otherwise false. --- @param #UNIT self --- @return #boolean Ship category evaluation result. -function UNIT:IsShip() - self:F2() - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitDescriptor = DCSUnit:getDesc() - self:T3( { UnitDescriptor.category, Unit.Category.SHIP } ) - - local IsShipResult = ( UnitDescriptor.category == Unit.Category.SHIP ) - - self:T3( IsShipResult ) - return IsShipResult - end - - return nil -end - ---- Returns true if the UNIT is in the air. --- @param Wrapper.Positionable#UNIT self --- @return #boolean true if in the air. --- @return #nil The UNIT is not existing or alive. -function UNIT:InAir() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitInAir = DCSUnit:inAir() - self:T3( UnitInAir ) - return UnitInAir - end - - return nil -end - -do -- Event Handling - - --- Subscribe to a DCS Event. - -- @param #UNIT self - -- @param Core.Event#EVENTS Event - -- @param #function EventFunction (optional) The function to be called when the event occurs for the unit. - -- @return #UNIT - function UNIT:HandleEvent( Event, EventFunction ) - - self:EventDispatcher():OnEventForUnit( self:GetName(), EventFunction, self, Event ) - - return self - end - - --- UnSubscribe to a DCS event. - -- @param #UNIT self - -- @param Core.Event#EVENTS Event - -- @return #UNIT - function UNIT:UnHandleEvent( Event ) - - self:EventDispatcher():RemoveForUnit( self:GetName(), self, Event ) - - return self - end - -end--- This module contains the CLIENT class. --- --- 1) @{Client#CLIENT} class, extends @{Unit#UNIT} --- =============================================== --- Clients are those **Units** defined within the Mission Editor that have the skillset defined as __Client__ or __Player__. --- Note that clients are NOT the same as Units, they are NOT necessarily alive. --- The @{Client#CLIENT} class is a wrapper class to handle the DCS Unit objects that have the skillset defined as __Client__ or __Player__: --- --- * Wraps the DCS Unit objects with skill level set to Player or Client. --- * Support all DCS Unit APIs. --- * Enhance with Unit specific APIs not in the DCS Group API set. --- * When player joins Unit, execute alive init logic. --- * Handles messages to players. --- * Manage the "state" of the DCS Unit. --- --- Clients are being used by the @{MISSION} class to follow players and register their successes. --- --- 1.1) CLIENT reference methods --- ----------------------------- --- For each DCS Unit having skill level Player or Client, a CLIENT wrapper object (instance) will be created within the _@{DATABASE} object. --- This is done at the beginning of the mission (when the mission starts). --- --- The CLIENT class does not contain a :New() method, rather it provides :Find() methods to retrieve the object reference --- using the DCS Unit or the DCS UnitName. --- --- Another thing to know is that CLIENT objects do not "contain" the DCS Unit object. --- The CLIENT methods will reference the DCS Unit object by name when it is needed during API execution. --- If the DCS Unit object does not exist or is nil, the CLIENT methods will return nil and log an exception in the DCS.log file. --- --- The CLIENT class provides the following functions to retrieve quickly the relevant CLIENT instance: --- --- * @{#CLIENT.Find}(): Find a CLIENT instance from the _DATABASE object using a DCS Unit object. --- * @{#CLIENT.FindByName}(): Find a CLIENT instance from the _DATABASE object using a DCS Unit name. --- --- IMPORTANT: ONE SHOULD NEVER SANATIZE these CLIENT OBJECT REFERENCES! (make the CLIENT object references nil). --- --- @module Client - ---- The CLIENT class --- @type CLIENT --- @extends Wrapper.Unit#UNIT -CLIENT = { - ONBOARDSIDE = { - NONE = 0, - LEFT = 1, - RIGHT = 2, - BACK = 3, - FRONT = 4 - }, - ClassName = "CLIENT", - ClientName = nil, - ClientAlive = false, - ClientTransport = false, - ClientBriefingShown = false, - _Menus = {}, - _Tasks = {}, - Messages = { - } -} - - ---- Finds a CLIENT from the _DATABASE using the relevant DCS Unit. --- @param #CLIENT self --- @param #string ClientName Name of the DCS **Unit** as defined within the Mission Editor. --- @param #string ClientBriefing Text that describes the briefing of the mission when a Player logs into the Client. --- @return #CLIENT --- @usage --- -- Create new Clients. --- local Mission = MISSIONSCHEDULER.AddMission( 'Russia Transport Troops SA-6', 'Operational', 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', 'Russia' ) --- Mission:AddGoal( DeploySA6TroopsGoal ) --- --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 1' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 3' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 2' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 4' ):Transport() ) -function CLIENT:Find( DCSUnit, Error ) - local ClientName = DCSUnit:getName() - local ClientFound = _DATABASE:FindClient( ClientName ) - - if ClientFound then - ClientFound:F( ClientName ) - return ClientFound - end - - if not Error then - error( "CLIENT not found for: " .. ClientName ) - end -end - - ---- Finds a CLIENT from the _DATABASE using the relevant Client Unit Name. --- As an optional parameter, a briefing text can be given also. --- @param #CLIENT self --- @param #string ClientName Name of the DCS **Unit** as defined within the Mission Editor. --- @param #string ClientBriefing Text that describes the briefing of the mission when a Player logs into the Client. --- @param #boolean Error A flag that indicates whether an error should be raised if the CLIENT cannot be found. By default an error will be raised. --- @return #CLIENT --- @usage --- -- Create new Clients. --- local Mission = MISSIONSCHEDULER.AddMission( 'Russia Transport Troops SA-6', 'Operational', 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', 'Russia' ) --- Mission:AddGoal( DeploySA6TroopsGoal ) --- --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 1' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 3' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 2' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 4' ):Transport() ) -function CLIENT:FindByName( ClientName, ClientBriefing, Error ) - local ClientFound = _DATABASE:FindClient( ClientName ) - - if ClientFound then - ClientFound:F( { ClientName, ClientBriefing } ) - ClientFound:AddBriefing( ClientBriefing ) - ClientFound.MessageSwitch = true - - return ClientFound - end - - if not Error then - error( "CLIENT not found for: " .. ClientName ) - end -end - -function CLIENT:Register( ClientName ) - local self = BASE:Inherit( self, UNIT:Register( ClientName ) ) - - self:F( ClientName ) - self.ClientName = ClientName - self.MessageSwitch = true - self.ClientAlive2 = false - - --self.AliveCheckScheduler = routines.scheduleFunction( self._AliveCheckScheduler, { self }, timer.getTime() + 1, 5 ) - self.AliveCheckScheduler = SCHEDULER:New( self, self._AliveCheckScheduler, { "Client Alive " .. ClientName }, 1, 5 ) - - self:E( self ) - return self -end - - ---- Transport defines that the Client is a Transport. Transports show cargo. --- @param #CLIENT self --- @return #CLIENT -function CLIENT:Transport() - self:F() - - self.ClientTransport = true - return self -end - ---- AddBriefing adds a briefing to a CLIENT when a player joins a mission. --- @param #CLIENT self --- @param #string ClientBriefing is the text defining the Mission briefing. --- @return #CLIENT self -function CLIENT:AddBriefing( ClientBriefing ) - self:F( ClientBriefing ) - self.ClientBriefing = ClientBriefing - self.ClientBriefingShown = false - - return self -end - ---- Show the briefing of a CLIENT. --- @param #CLIENT self --- @return #CLIENT self -function CLIENT:ShowBriefing() - self:F( { self.ClientName, self.ClientBriefingShown } ) - - if not self.ClientBriefingShown then - self.ClientBriefingShown = true - local Briefing = "" - if self.ClientBriefing then - Briefing = Briefing .. self.ClientBriefing - end - Briefing = Briefing .. " Press [LEFT ALT]+[B] to view the complete mission briefing." - self:Message( Briefing, 60, "Briefing" ) - end - - return self -end - ---- Show the mission briefing of a MISSION to the CLIENT. --- @param #CLIENT self --- @param #string MissionBriefing --- @return #CLIENT self -function CLIENT:ShowMissionBriefing( MissionBriefing ) - self:F( { self.ClientName } ) - - if MissionBriefing then - self:Message( MissionBriefing, 60, "Mission Briefing" ) - end - - return self -end - - - ---- Resets a CLIENT. --- @param #CLIENT self --- @param #string ClientName Name of the Group as defined within the Mission Editor. The Group must have a Unit with the type Client. -function CLIENT:Reset( ClientName ) - self:F() - self._Menus = {} -end - --- Is Functions - ---- Checks if the CLIENT is a multi-seated UNIT. --- @param #CLIENT self --- @return #boolean true if multi-seated. -function CLIENT:IsMultiSeated() - self:F( self.ClientName ) - - local ClientMultiSeatedTypes = { - ["Mi-8MT"] = "Mi-8MT", - ["UH-1H"] = "UH-1H", - ["P-51B"] = "P-51B" - } - - if self:IsAlive() then - local ClientTypeName = self:GetClientGroupUnit():GetTypeName() - if ClientMultiSeatedTypes[ClientTypeName] then - return true - end - end - - return false -end - ---- Checks for a client alive event and calls a function on a continuous basis. --- @param #CLIENT self --- @param #function CallBackFunction Create a function that will be called when a player joins the slot. --- @return #CLIENT -function CLIENT:Alive( CallBackFunction, ... ) - self:F() - - self.ClientCallBack = CallBackFunction - self.ClientParameters = arg - - return self -end - ---- @param #CLIENT self -function CLIENT:_AliveCheckScheduler( SchedulerName ) - self:F3( { SchedulerName, self.ClientName, self.ClientAlive2, self.ClientBriefingShown, self.ClientCallBack } ) - - if self:IsAlive() then - if self.ClientAlive2 == false then - self:ShowBriefing() - if self.ClientCallBack then - self:T("Calling Callback function") - self.ClientCallBack( self, unpack( self.ClientParameters ) ) - end - self.ClientAlive2 = true - end - else - if self.ClientAlive2 == true then - self.ClientAlive2 = false - end - end - - return true -end - ---- Return the DCSGroup of a Client. --- This function is modified to deal with a couple of bugs in DCS 1.5.3 --- @param #CLIENT self --- @return Dcs.DCSWrapper.Group#Group -function CLIENT:GetDCSGroup() - self:F3() - --- local ClientData = Group.getByName( self.ClientName ) --- if ClientData and ClientData:isExist() then --- self:T( self.ClientName .. " : group found!" ) --- return ClientData --- else --- return nil --- end - - local ClientUnit = Unit.getByName( self.ClientName ) - - local CoalitionsData = { AlivePlayersRed = coalition.getPlayers( coalition.side.RED ), AlivePlayersBlue = coalition.getPlayers( coalition.side.BLUE ) } - for CoalitionId, CoalitionData in pairs( CoalitionsData ) do - self:T3( { "CoalitionData:", CoalitionData } ) - for UnitId, UnitData in pairs( CoalitionData ) do - self:T3( { "UnitData:", UnitData } ) - if UnitData and UnitData:isExist() then - - --self:E(self.ClientName) - if ClientUnit then - local ClientGroup = ClientUnit:getGroup() - if ClientGroup then - self:T3( "ClientGroup = " .. self.ClientName ) - if ClientGroup:isExist() and UnitData:getGroup():isExist() then - if ClientGroup:getID() == UnitData:getGroup():getID() then - self:T3( "Normal logic" ) - self:T3( self.ClientName .. " : group found!" ) - self.ClientGroupID = ClientGroup:getID() - self.ClientGroupName = ClientGroup:getName() - return ClientGroup - end - else - -- Now we need to resolve the bugs in DCS 1.5 ... - -- Consult the database for the units of the Client Group. (ClientGroup:getUnits() returns nil) - self:T3( "Bug 1.5 logic" ) - local ClientGroupTemplate = _DATABASE.Templates.Units[self.ClientName].GroupTemplate - self.ClientGroupID = ClientGroupTemplate.groupId - self.ClientGroupName = _DATABASE.Templates.Units[self.ClientName].GroupName - self:T3( self.ClientName .. " : group found in bug 1.5 resolvement logic!" ) - return ClientGroup - end - -- else - -- error( "Client " .. self.ClientName .. " not found!" ) - end - else - --self:E( { "Client not found!", self.ClientName } ) - end - end - end - end - - -- For non player clients - if ClientUnit then - local ClientGroup = ClientUnit:getGroup() - if ClientGroup then - self:T3( "ClientGroup = " .. self.ClientName ) - if ClientGroup:isExist() then - self:T3( "Normal logic" ) - self:T3( self.ClientName .. " : group found!" ) - return ClientGroup - end - end - end - - self.ClientGroupID = nil - self.ClientGroupUnit = nil - - return nil -end - - --- TODO: Check Dcs.DCSTypes#Group.ID ---- Get the group ID of the client. --- @param #CLIENT self --- @return Dcs.DCSTypes#Group.ID -function CLIENT:GetClientGroupID() - - local ClientGroup = self:GetDCSGroup() - - --self:E( self.ClientGroupID ) -- Determined in GetDCSGroup() - return self.ClientGroupID -end - - ---- Get the name of the group of the client. --- @param #CLIENT self --- @return #string -function CLIENT:GetClientGroupName() - - local ClientGroup = self:GetDCSGroup() - - self:T( self.ClientGroupName ) -- Determined in GetDCSGroup() - return self.ClientGroupName -end - ---- Returns the UNIT of the CLIENT. --- @param #CLIENT self --- @return Wrapper.Unit#UNIT -function CLIENT:GetClientGroupUnit() - self:F2() - - local ClientDCSUnit = Unit.getByName( self.ClientName ) - - self:T( self.ClientDCSUnit ) - if ClientDCSUnit and ClientDCSUnit:isExist() then - local ClientUnit = _DATABASE:FindUnit( self.ClientName ) - self:T2( ClientUnit ) - return ClientUnit - end -end - ---- Returns the DCSUnit of the CLIENT. --- @param #CLIENT self --- @return Dcs.DCSTypes#Unit -function CLIENT:GetClientGroupDCSUnit() - self:F2() - - local ClientDCSUnit = Unit.getByName( self.ClientName ) - - if ClientDCSUnit and ClientDCSUnit:isExist() then - self:T2( ClientDCSUnit ) - return ClientDCSUnit - end -end - - ---- Evaluates if the CLIENT is a transport. --- @param #CLIENT self --- @return #boolean true is a transport. -function CLIENT:IsTransport() - self:F() - return self.ClientTransport -end - ---- Shows the @{AI_Cargo#CARGO} contained within the CLIENT to the player as a message. --- The @{AI_Cargo#CARGO} is shown using the @{Message#MESSAGE} distribution system. --- @param #CLIENT self -function CLIENT:ShowCargo() - self:F() - - local CargoMsg = "" - - for CargoName, Cargo in pairs( CARGOS ) do - if self == Cargo:IsLoadedInClient() then - CargoMsg = CargoMsg .. Cargo.CargoName .. " Type:" .. Cargo.CargoType .. " Weight: " .. Cargo.CargoWeight .. "\n" - end - end - - if CargoMsg == "" then - CargoMsg = "empty" - end - - self:Message( CargoMsg, 15, "Co-Pilot: Cargo Status", 30 ) - -end - --- TODO (1) I urgently need to revise this. ---- A local function called by the DCS World Menu system to switch off messages. -function CLIENT.SwitchMessages( PrmTable ) - PrmTable[1].MessageSwitch = PrmTable[2] -end - ---- The main message driver for the CLIENT. --- This function displays various messages to the Player logged into the CLIENT through the DCS World Messaging system. --- @param #CLIENT self --- @param #string Message is the text describing the message. --- @param #number MessageDuration is the duration in seconds that the Message should be displayed. --- @param #string MessageCategory is the category of the message (the title). --- @param #number MessageInterval is the interval in seconds between the display of the @{Message#MESSAGE} when the CLIENT is in the air. --- @param #string MessageID is the identifier of the message when displayed with intervals. -function CLIENT:Message( Message, MessageDuration, MessageCategory, MessageInterval, MessageID ) - self:F( { Message, MessageDuration, MessageCategory, MessageInterval } ) - - if self.MessageSwitch == true then - if MessageCategory == nil then - MessageCategory = "Messages" - end - if MessageID ~= nil then - if self.Messages[MessageID] == nil then - self.Messages[MessageID] = {} - self.Messages[MessageID].MessageId = MessageID - self.Messages[MessageID].MessageTime = timer.getTime() - self.Messages[MessageID].MessageDuration = MessageDuration - if MessageInterval == nil then - self.Messages[MessageID].MessageInterval = 600 - else - self.Messages[MessageID].MessageInterval = MessageInterval - end - MESSAGE:New( Message, MessageDuration, MessageCategory ):ToClient( self ) - else - if self:GetClientGroupDCSUnit() and not self:GetClientGroupDCSUnit():inAir() then - if timer.getTime() - self.Messages[MessageID].MessageTime >= self.Messages[MessageID].MessageDuration + 10 then - MESSAGE:New( Message, MessageDuration , MessageCategory):ToClient( self ) - self.Messages[MessageID].MessageTime = timer.getTime() - end - else - if timer.getTime() - self.Messages[MessageID].MessageTime >= self.Messages[MessageID].MessageDuration + self.Messages[MessageID].MessageInterval then - MESSAGE:New( Message, MessageDuration, MessageCategory ):ToClient( self ) - self.Messages[MessageID].MessageTime = timer.getTime() - end - end - end - else - MESSAGE:New( Message, MessageDuration, MessageCategory ):ToClient( self ) - end - end -end ---- This module contains the STATIC class. --- --- 1) @{Static#STATIC} class, extends @{Positionable#POSITIONABLE} --- =============================================================== --- Statics are **Static Units** defined within the Mission Editor. --- Note that Statics are almost the same as Units, but they don't have a controller. --- The @{Static#STATIC} class is a wrapper class to handle the DCS Static objects: --- --- * Wraps the DCS Static objects. --- * Support all DCS Static APIs. --- * Enhance with Static specific APIs not in the DCS API set. --- --- 1.1) STATIC reference methods --- ----------------------------- --- For each DCS Static will have a STATIC wrapper object (instance) within the _@{DATABASE} object. --- This is done at the beginning of the mission (when the mission starts). --- --- The STATIC class does not contain a :New() method, rather it provides :Find() methods to retrieve the object reference --- using the Static Name. --- --- Another thing to know is that STATIC objects do not "contain" the DCS Static object. --- The STATIc methods will reference the DCS Static object by name when it is needed during API execution. --- If the DCS Static object does not exist or is nil, the STATIC methods will return nil and log an exception in the DCS.log file. --- --- The STATIc class provides the following functions to retrieve quickly the relevant STATIC instance: --- --- * @{#STATIC.FindByName}(): Find a STATIC instance from the _DATABASE object using a DCS Static name. --- --- IMPORTANT: ONE SHOULD NEVER SANATIZE these STATIC OBJECT REFERENCES! (make the STATIC object references nil). --- --- @module Static --- @author FlightControl - - - - - - ---- The STATIC class --- @type STATIC --- @extends Wrapper.Positionable#POSITIONABLE -STATIC = { - ClassName = "STATIC", -} - - ---- Finds a STATIC from the _DATABASE using the relevant Static Name. --- As an optional parameter, a briefing text can be given also. --- @param #STATIC self --- @param #string StaticName Name of the DCS **Static** as defined within the Mission Editor. --- @param #boolean RaiseError Raise an error if not found. --- @return #STATIC -function STATIC:FindByName( StaticName, RaiseError ) - local StaticFound = _DATABASE:FindStatic( StaticName ) - - self.StaticName = StaticName - - if StaticFound then - StaticFound:F3( { StaticName } ) - - return StaticFound - end - - if RaiseError == nil or RaiseError == true then - error( "STATIC not found for: " .. StaticName ) - end - - return nil -end - -function STATIC:Register( StaticName ) - local self = BASE:Inherit( self, POSITIONABLE:New( StaticName ) ) - self.StaticName = StaticName - return self -end - - -function STATIC:GetDCSObject() - local DCSStatic = StaticObject.getByName( self.StaticName ) - - if DCSStatic then - return DCSStatic - end - - return nil -end - -function STATIC:GetThreatLevel() - - return 1, "Static" -end--- This module contains the AIRBASE classes. --- --- === --- --- 1) @{Airbase#AIRBASE} class, extends @{Positionable#POSITIONABLE} --- ================================================================= --- The @{AIRBASE} class is a wrapper class to handle the DCS Airbase objects: --- --- * Support all DCS Airbase APIs. --- * Enhance with Airbase specific APIs not in the DCS Airbase API set. --- --- --- 1.1) AIRBASE reference methods --- ------------------------------ --- For each DCS Airbase object alive within a running mission, a AIRBASE wrapper object (instance) will be created within the _@{DATABASE} object. --- This is done at the beginning of the mission (when the mission starts). --- --- The AIRBASE class **does not contain a :New()** method, rather it provides **:Find()** methods to retrieve the object reference --- using the DCS Airbase or the DCS AirbaseName. --- --- Another thing to know is that AIRBASE objects do not "contain" the DCS Airbase object. --- The AIRBASE methods will reference the DCS Airbase object by name when it is needed during API execution. --- If the DCS Airbase object does not exist or is nil, the AIRBASE methods will return nil and log an exception in the DCS.log file. --- --- The AIRBASE class provides the following functions to retrieve quickly the relevant AIRBASE instance: --- --- * @{#AIRBASE.Find}(): Find a AIRBASE instance from the _DATABASE object using a DCS Airbase object. --- * @{#AIRBASE.FindByName}(): Find a AIRBASE instance from the _DATABASE object using a DCS Airbase name. --- --- IMPORTANT: ONE SHOULD NEVER SANATIZE these AIRBASE OBJECT REFERENCES! (make the AIRBASE object references nil). --- --- 1.2) DCS AIRBASE APIs --- --------------------- --- The DCS Airbase APIs are used extensively within MOOSE. The AIRBASE class has for each DCS Airbase API a corresponding method. --- To be able to distinguish easily in your code the difference between a AIRBASE API call and a DCS Airbase API call, --- the first letter of the method is also capitalized. So, by example, the DCS Airbase method @{DCSWrapper.Airbase#Airbase.getName}() --- is implemented in the AIRBASE class as @{#AIRBASE.GetName}(). --- --- More functions will be added --- ---------------------------- --- During the MOOSE development, more functions will be added. --- --- @module Airbase --- @author FlightControl - - - - - ---- The AIRBASE class --- @type AIRBASE --- @extends Wrapper.Positionable#POSITIONABLE -AIRBASE = { - ClassName="AIRBASE", - CategoryName = { - [Airbase.Category.AIRDROME] = "Airdrome", - [Airbase.Category.HELIPAD] = "Helipad", - [Airbase.Category.SHIP] = "Ship", - }, - } - --- Registration. - ---- Create a new AIRBASE from DCSAirbase. --- @param #AIRBASE self --- @param #string AirbaseName The name of the airbase. --- @return Wrapper.Airbase#AIRBASE -function AIRBASE:Register( AirbaseName ) - - local self = BASE:Inherit( self, POSITIONABLE:New( AirbaseName ) ) - self.AirbaseName = AirbaseName - return self -end - --- Reference methods. - ---- Finds a AIRBASE from the _DATABASE using a DCSAirbase object. --- @param #AIRBASE self --- @param Dcs.DCSWrapper.Airbase#Airbase DCSAirbase An existing DCS Airbase object reference. --- @return Wrapper.Airbase#AIRBASE self -function AIRBASE:Find( DCSAirbase ) - - local AirbaseName = DCSAirbase:getName() - local AirbaseFound = _DATABASE:FindAirbase( AirbaseName ) - return AirbaseFound -end - ---- Find a AIRBASE in the _DATABASE using the name of an existing DCS Airbase. --- @param #AIRBASE self --- @param #string AirbaseName The Airbase Name. --- @return Wrapper.Airbase#AIRBASE self -function AIRBASE:FindByName( AirbaseName ) - - local AirbaseFound = _DATABASE:FindAirbase( AirbaseName ) - return AirbaseFound -end - -function AIRBASE:GetDCSObject() - local DCSAirbase = Airbase.getByName( self.AirbaseName ) - - if DCSAirbase then - return DCSAirbase - end - - return nil -end - - - ---- This module contains the SCENERY class. --- --- 1) @{Scenery#SCENERY} class, extends @{Positionable#POSITIONABLE} --- =============================================================== --- Scenery objects are defined on the map. --- The @{Scenery#SCENERY} class is a wrapper class to handle the DCS Scenery objects: --- --- * Wraps the DCS Scenery objects. --- * Support all DCS Scenery APIs. --- * Enhance with Scenery specific APIs not in the DCS API set. --- --- @module Scenery --- @author FlightControl - - - ---- The SCENERY class --- @type SCENERY --- @extends Wrapper.Positionable#POSITIONABLE -SCENERY = { - ClassName = "SCENERY", -} - - -function SCENERY:Register( SceneryName, SceneryObject ) - local self = BASE:Inherit( self, POSITIONABLE:New( SceneryName ) ) - self.SceneryName = SceneryName - self.SceneryObject = SceneryObject - return self -end - -function SCENERY:GetDCSObject() - return self.SceneryObject -end - -function SCENERY:GetThreatLevel() - - return 0, "Scenery" -end ---- Single-Player:**Yes** / Multi-Player:**Yes** / Core:**Yes** -- **Administer the scoring of player achievements, --- and create a CSV file logging the scoring events for use at team or squadron websites.** --- --- ![Banner Image](..\Presentations\SCORING\Dia1.JPG) --- --- === --- --- The @{#SCORING} class administers the scoring of player achievements, --- and creates a CSV file logging the scoring events and results for use at team or squadron websites. --- --- SCORING automatically calculates the threat level of the objects hit and destroyed by players, --- which can be @{Unit}, @{Static) and @{Scenery} objects. --- --- Positive score points are granted when enemy or neutral targets are destroyed. --- Negative score points or penalties are given when a friendly target is hit or destroyed. --- This brings a lot of dynamism in the scoring, where players need to take care to inflict damage on the right target. --- By default, penalties weight heavier in the scoring, to ensure that players don't commit fratricide. --- The total score of the player is calculated by **adding the scores minus the penalties**. --- --- ![Banner Image](..\Presentations\SCORING\Dia4.JPG) --- --- The score value is calculated based on the **threat level of the player** and the **threat level of the target**. --- A calculated score takes the threat level of the target divided by a balanced threat level of the player unit. --- As such, if the threat level of the target is high, and the player threat level is low, a higher score will be given than --- if the threat level of the player would be high too. --- --- ![Banner Image](..\Presentations\SCORING\Dia5.JPG) --- --- When multiple players hit the same target, and finally succeed in destroying the target, then each player who contributed to the target --- destruction, will receive a score. This is important for targets that require significant damage before it can be destroyed, like --- ships or heavy planes. --- --- ![Banner Image](..\Presentations\SCORING\Dia13.JPG) --- --- Optionally, the score values can be **scaled** by a **scale**. Specific scales can be set for positive cores or negative penalties. --- The default range of the scores granted is a value between 0 and 10. The default range of penalties given is a value between 0 and 30. --- --- ![Banner Image](..\Presentations\SCORING\Dia7.JPG) --- --- **Additional scores** can be granted to **specific objects**, when the player(s) destroy these objects. --- --- ![Banner Image](..\Presentations\SCORING\Dia9.JPG) --- --- Various @{Zone}s can be defined for which scores are also granted when objects in that @{Zone} are destroyed. --- This is **specifically useful** to designate **scenery targets on the map** that will generate points when destroyed. --- --- With a small change in MissionScripting.lua, the scoring results can also be logged in a **CSV file**. --- These CSV files can be used to: --- --- * Upload scoring to a database or a BI tool to publish the scoring results to the player community. --- * Upload scoring in an (online) Excel like tool, using pivot tables and pivot charts to show mission results. --- * Share scoring amoung players after the mission to discuss mission results. --- --- Scores can be **reported**. **Menu options** are automatically added to **each player group** when a player joins a client slot or a CA unit. --- Use the radio menu F10 to consult the scores while running the mission. --- Scores can be reported for your user, or an overall score can be reported of all players currently active in the mission. --- --- # 1) @{Scoring#SCORING} class, extends @{Base#BASE} --- --- ## 1.1) Set the destroy score or penalty scale --- --- Score scales can be set for scores granted when enemies or friendlies are destroyed. --- Use the method @{#SCORING.SetScaleDestroyScore}() to set the scale of enemy destroys (positive destroys). --- Use the method @{#SCORING.SetScaleDestroyPenalty}() to set the scale of friendly destroys (negative destroys). --- --- local Scoring = SCORING:New( "Scoring File" ) --- Scoring:SetScaleDestroyScore( 10 ) --- Scoring:SetScaleDestroyPenalty( 40 ) --- --- The above sets the scale for valid scores to 10. So scores will be given in a scale from 0 to 10. --- The penalties will be given in a scale from 0 to 40. --- --- ## 1.2) Define special targets that will give extra scores. --- --- Special targets can be set that will give extra scores to the players when these are destroyed. --- Use the methods @{#SCORING.AddUnitScore}() and @{#SCORING.RemoveUnitScore}() to specify a special additional score for a specific @{Unit}s. --- Use the methods @{#SCORING.AddStaticScore}() and @{#SCORING.RemoveStaticScore}() to specify a special additional score for a specific @{Static}s. --- Use the method @{#SCORING.SetGroupGroup}() to specify a special additional score for a specific @{Group}s. --- --- local Scoring = SCORING:New( "Scoring File" ) --- Scoring:AddUnitScore( UNIT:FindByName( "Unit #001" ), 200 ) --- Scoring:AddStaticScore( STATIC:FindByName( "Static #1" ), 100 ) --- --- The above grants an additional score of 200 points for Unit #001 and an additional 100 points of Static #1 if these are destroyed. --- Note that later in the mission, one can remove these scores set, for example, when the a goal achievement time limit is over. --- For example, this can be done as follows: --- --- Scoring:RemoveUnitScore( UNIT:FindByName( "Unit #001" ) ) --- --- ## 1.3) Define destruction zones that will give extra scores. --- --- Define zones of destruction. Any object destroyed within the zone of the given category will give extra points. --- Use the method @{#SCORING.AddZoneScore}() to add a @{Zone} for additional scoring. --- Use the method @{#SCORING.RemoveZoneScore}() to remove a @{Zone} for additional scoring. --- There are interesting variations that can be achieved with this functionality. For example, if the @{Zone} is a @{Zone#ZONE_UNIT}, --- then the zone is a moving zone, and anything destroyed within that @{Zone} will generate points. --- The other implementation could be to designate a scenery target (a building) in the mission editor surrounded by a @{Zone}, --- just large enough around that building. --- --- ## 1.4) Add extra Goal scores upon an event or a condition. --- --- A mission has goals and achievements. The scoring system provides an API to set additional scores when a goal or achievement event happens. --- Use the method @{#SCORING.AddGoalScore}() to add a score for a Player at any time in your mission. --- --- ## 1.5) Configure fratricide level. --- --- When a player commits too much damage to friendlies, his penalty score will reach a certain level. --- Use the method @{#SCORING.SetFratricide}() to define the level when a player gets kicked. --- By default, the fratricide level is the default penalty mutiplier * 2 for the penalty score. --- --- ## 1.6) Penalty score when a player changes the coalition. --- --- When a player changes the coalition, he can receive a penalty score. --- Use the method @{#SCORING.SetCoalitionChangePenalty}() to define the penalty when a player changes coalition. --- By default, the penalty for changing coalition is the default penalty scale. --- --- ## 1.8) Define output CSV files. --- --- The CSV file is given the name of the string given in the @{#SCORING.New}{} constructor, followed by the .csv extension. --- The file is incrementally saved in the **\\Saved Games\\DCS\\Logs** folder, and has a time stamp indicating each mission run. --- See the following example: --- --- local ScoringFirstMission = SCORING:New( "FirstMission" ) --- local ScoringSecondMission = SCORING:New( "SecondMission" ) --- --- The above documents that 2 Scoring objects are created, ScoringFirstMission and ScoringSecondMission. --- --- ## 1.9) Configure messages. --- --- When players hit or destroy targets, messages are sent. --- Various methods exist to configure: --- --- * Which messages are sent upon the event. --- * Which audience receives the message. --- --- ### 1.9.1) Configure the messages sent upon the event. --- --- Use the following methods to configure when to send messages. By default, all messages are sent. --- --- * @{#SCORING.SetMessagesHit}(): Configure to send messages after a target has been hit. --- * @{#SCORING.SetMessagesDestroy}(): Configure to send messages after a target has been destroyed. --- * @{#SCORING.SetMessagesAddon}(): Configure to send messages for additional score, after a target has been destroyed. --- * @{#SCORING.SetMessagesZone}(): Configure to send messages for additional score, after a target has been destroyed within a given zone. --- --- ### 1.9.2) Configure the audience of the messages. --- --- Use the following methods to configure the audience of the messages. By default, the messages are sent to all players in the mission. --- --- * @{#SCORING.SetMessagesToAll}(): Configure to send messages to all players. --- * @{#SCORING.SetMessagesToCoalition}(): Configure to send messages to only those players within the same coalition as the player. --- --- --- ==== --- --- # **API CHANGE HISTORY** --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- Hereby the change log: --- --- 2017-02-26: Initial class and API. --- --- === --- --- # **AUTHORS and CONTRIBUTIONS** --- --- ### Contributions: --- --- * **Wingthor (TAW)**: Testing & Advice. --- * **Dutch-Baron (TAW)**: Testing & Advice. --- * **[Whisper](http://forums.eagle.ru/member.php?u=3829): Testing and Advice. --- --- ### Authors: --- --- * **FlightControl**: Concept, Design & Programming. --- --- @module Scoring - - ---- The Scoring class --- @type SCORING --- @field Players A collection of the current players that have joined the game. --- @extends Core.Base#BASE -SCORING = { - ClassName = "SCORING", - ClassID = 0, - Players = {}, -} - -local _SCORINGCoalition = - { - [1] = "Red", - [2] = "Blue", - } - -local _SCORINGCategory = - { - [Unit.Category.AIRPLANE] = "Plane", - [Unit.Category.HELICOPTER] = "Helicopter", - [Unit.Category.GROUND_UNIT] = "Vehicle", - [Unit.Category.SHIP] = "Ship", - [Unit.Category.STRUCTURE] = "Structure", - } - ---- Creates a new SCORING object to administer the scoring achieved by players. --- @param #SCORING self --- @param #string GameName The name of the game. This name is also logged in the CSV score file. --- @return #SCORING self --- @usage --- -- Define a new scoring object for the mission Gori Valley. --- ScoringObject = SCORING:New( "Gori Valley" ) -function SCORING:New( GameName ) - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) -- #SCORING - - if GameName then - self.GameName = GameName - else - error( "A game name must be given to register the scoring results" ) - end - - - -- Additional Object scores - self.ScoringObjects = {} - - -- Additional Zone scores. - self.ScoringZones = {} - - -- Configure Messages - self:SetMessagesToAll() - self:SetMessagesHit( true ) - self:SetMessagesDestroy( true ) - self:SetMessagesScore( true ) - self:SetMessagesZone( true ) - - -- Scales - self:SetScaleDestroyScore( 10 ) - self:SetScaleDestroyPenalty( 30 ) - - -- Default fratricide penalty level (maximum penalty that can be assigned to a player before he gets kicked). - self:SetFratricide( self.ScaleDestroyPenalty * 3 ) - - -- Default penalty when a player changes coalition. - self:SetCoalitionChangePenalty( self.ScaleDestroyPenalty ) - - -- Event handlers - self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) - self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) - self:HandleEvent( EVENTS.Hit, self._EventOnHit ) - self:HandleEvent( EVENTS.PlayerEnterUnit ) - self:HandleEvent( EVENTS.PlayerLeaveUnit ) - - -- Create the CSV file. - self:OpenCSV( GameName ) - - return self - -end - ---- Set the scale for scoring valid destroys (enemy destroys). --- A default calculated score is a value between 1 and 10. --- The scale magnifies the scores given to the players. --- @param #SCORING self --- @param #number Scale The scale of the score given. -function SCORING:SetScaleDestroyScore( Scale ) - - self.ScaleDestroyScore = Scale - - return self -end - ---- Set the scale for scoring penalty destroys (friendly destroys). --- A default calculated penalty is a value between 1 and 10. --- The scale magnifies the scores given to the players. --- @param #SCORING self --- @param #number Scale The scale of the score given. --- @return #SCORING -function SCORING:SetScaleDestroyPenalty( Scale ) - - self.ScaleDestroyPenalty = Scale - - return self -end - ---- Add a @{Unit} for additional scoring when the @{Unit} is destroyed. --- Note that if there was already a @{Unit} declared within the scoring with the same name, --- then the old @{Unit} will be replaced with the new @{Unit}. --- @param #SCORING self --- @param Wrapper.Unit#UNIT ScoreUnit The @{Unit} for which the Score needs to be given. --- @param #number Score The Score value. --- @return #SCORING -function SCORING:AddUnitScore( ScoreUnit, Score ) - - local UnitName = ScoreUnit:GetName() - - self.ScoringObjects[UnitName] = Score - - return self -end - ---- Removes a @{Unit} for additional scoring when the @{Unit} is destroyed. --- @param #SCORING self --- @param Wrapper.Unit#UNIT ScoreUnit The @{Unit} for which the Score needs to be given. --- @return #SCORING -function SCORING:RemoveUnitScore( ScoreUnit ) - - local UnitName = ScoreUnit:GetName() - - self.ScoringObjects[UnitName] = nil - - return self -end - ---- Add a @{Static} for additional scoring when the @{Static} is destroyed. --- Note that if there was already a @{Static} declared within the scoring with the same name, --- then the old @{Static} will be replaced with the new @{Static}. --- @param #SCORING self --- @param Wrapper.Static#UNIT ScoreStatic The @{Static} for which the Score needs to be given. --- @param #number Score The Score value. --- @return #SCORING -function SCORING:AddStaticScore( ScoreStatic, Score ) - - local StaticName = ScoreStatic:GetName() - - self.ScoringObjects[StaticName] = Score - - return self -end - ---- Removes a @{Static} for additional scoring when the @{Static} is destroyed. --- @param #SCORING self --- @param Wrapper.Static#UNIT ScoreStatic The @{Static} for which the Score needs to be given. --- @return #SCORING -function SCORING:RemoveStaticScore( ScoreStatic ) - - local StaticName = ScoreStatic:GetName() - - self.ScoringObjects[StaticName] = nil - - return self -end - - ---- Specify a special additional score for a @{Group}. --- @param #SCORING self --- @param Wrapper.Group#GROUP ScoreGroup The @{Group} for which each @{Unit} a Score is given. --- @param #number Score The Score value. --- @return #SCORING -function SCORING:AddScoreGroup( ScoreGroup, Score ) - - local ScoreUnits = ScoreGroup:GetUnits() - - for ScoreUnitID, ScoreUnit in pairs( ScoreUnits ) do - local UnitName = ScoreUnit:GetName() - self.ScoringObjects[UnitName] = Score - end - - return self -end - ---- Add a @{Zone} to define additional scoring when any object is destroyed in that zone. --- Note that if a @{Zone} with the same name is already within the scoring added, the @{Zone} (type) and Score will be replaced! --- This allows for a dynamic destruction zone evolution within your mission. --- @param #SCORING self --- @param Core.Zone#ZONE_BASE ScoreZone The @{Zone} which defines the destruction score perimeters. --- Note that a zone can be a polygon or a moving zone. --- @param #number Score The Score value. --- @return #SCORING -function SCORING:AddZoneScore( ScoreZone, Score ) - - local ZoneName = ScoreZone:GetName() - - self.ScoringZones[ZoneName] = {} - self.ScoringZones[ZoneName].ScoreZone = ScoreZone - self.ScoringZones[ZoneName].Score = Score - - return self -end - ---- Remove a @{Zone} for additional scoring. --- The scoring will search if any @{Zone} is added with the given name, and will remove that zone from the scoring. --- This allows for a dynamic destruction zone evolution within your mission. --- @param #SCORING self --- @param Core.Zone#ZONE_BASE ScoreZone The @{Zone} which defines the destruction score perimeters. --- Note that a zone can be a polygon or a moving zone. --- @return #SCORING -function SCORING:RemoveZoneScore( ScoreZone ) - - local ZoneName = ScoreZone:GetName() - - self.ScoringZones[ZoneName] = nil - - return self -end - - ---- Configure to send messages after a target has been hit. --- @param #SCORING self --- @param #boolean OnOff If true is given, the messages are sent. --- @return #SCORING -function SCORING:SetMessagesHit( OnOff ) - - self.MessagesHit = OnOff - return self -end - ---- If to send messages after a target has been hit. --- @param #SCORING self --- @return #boolean -function SCORING:IfMessagesHit() - - return self.MessagesHit -end - ---- Configure to send messages after a target has been destroyed. --- @param #SCORING self --- @param #boolean OnOff If true is given, the messages are sent. --- @return #SCORING -function SCORING:SetMessagesDestroy( OnOff ) - - self.MessagesDestroy = OnOff - return self -end - ---- If to send messages after a target has been destroyed. --- @param #SCORING self --- @return #boolean -function SCORING:IfMessagesDestroy() - - return self.MessagesDestroy -end - ---- Configure to send messages after a target has been destroyed and receives additional scores. --- @param #SCORING self --- @param #boolean OnOff If true is given, the messages are sent. --- @return #SCORING -function SCORING:SetMessagesScore( OnOff ) - - self.MessagesScore = OnOff - return self -end - ---- If to send messages after a target has been destroyed and receives additional scores. --- @param #SCORING self --- @return #boolean -function SCORING:IfMessagesScore() - - return self.MessagesScore -end - ---- Configure to send messages after a target has been hit in a zone, and additional score is received. --- @param #SCORING self --- @param #boolean OnOff If true is given, the messages are sent. --- @return #SCORING -function SCORING:SetMessagesZone( OnOff ) - - self.MessagesZone = OnOff - return self -end - ---- If to send messages after a target has been hit in a zone, and additional score is received. --- @param #SCORING self --- @return #boolean -function SCORING:IfMessagesZone() - - return self.MessagesZone -end - ---- Configure to send messages to all players. --- @param #SCORING self --- @return #SCORING -function SCORING:SetMessagesToAll() - - self.MessagesAudience = 1 - return self -end - ---- If to send messages to all players. --- @param #SCORING self --- @return #boolean -function SCORING:IfMessagesToAll() - - return self.MessagesAudience == 1 -end - ---- Configure to send messages to only those players within the same coalition as the player. --- @param #SCORING self --- @return #SCORING -function SCORING:SetMessagesToCoalition() - - self.MessagesAudience = 2 - return self -end - ---- If to send messages to only those players within the same coalition as the player. --- @param #SCORING self --- @return #boolean -function SCORING:IfMessagesToCoalition() - - return self.MessagesAudience == 2 -end - - ---- When a player commits too much damage to friendlies, his penalty score will reach a certain level. --- Use this method to define the level when a player gets kicked. --- By default, the fratricide level is the default penalty mutiplier * 2 for the penalty score. --- @param #SCORING self --- @param #number Fratricide The amount of maximum penalty that may be inflicted by a friendly player before he gets kicked. --- @return #SCORING -function SCORING:SetFratricide( Fratricide ) - - self.Fratricide = Fratricide - return self -end - - ---- When a player changes the coalition, he can receive a penalty score. --- Use the method @{#SCORING.SetCoalitionChangePenalty}() to define the penalty when a player changes coalition. --- By default, the penalty for changing coalition is the default penalty scale. --- @param #SCORING self --- @param #number CoalitionChangePenalty The amount of penalty that is given. --- @return #SCORING -function SCORING:SetCoalitionChangePenalty( CoalitionChangePenalty ) - - self.CoalitionChangePenalty = CoalitionChangePenalty - return self -end - - ---- Add a new player entering a Unit. --- @param #SCORING self --- @param Wrapper.Unit#UNIT UnitData -function SCORING:_AddPlayerFromUnit( UnitData ) - self:F( UnitData ) - - if UnitData:IsAlive() then - local UnitName = UnitData:GetName() - local PlayerName = UnitData:GetPlayerName() - local UnitDesc = UnitData:GetDesc() - local UnitCategory = UnitDesc.category - local UnitCoalition = UnitData:GetCoalition() - local UnitTypeName = UnitData:GetTypeName() - local UnitThreatLevel, UnitThreatType = UnitData:GetThreatLevel() - - self:T( { PlayerName, UnitName, UnitCategory, UnitCoalition, UnitTypeName } ) - - if self.Players[PlayerName] == nil then -- I believe this is the place where a Player gets a life in a mission when he enters a unit ... - self.Players[PlayerName] = {} - self.Players[PlayerName].Hit = {} - self.Players[PlayerName].Destroy = {} - self.Players[PlayerName].Goals = {} - self.Players[PlayerName].Mission = {} - - -- for CategoryID, CategoryName in pairs( SCORINGCategory ) do - -- self.Players[PlayerName].Hit[CategoryID] = {} - -- self.Players[PlayerName].Destroy[CategoryID] = {} - -- end - self.Players[PlayerName].HitPlayers = {} - self.Players[PlayerName].Score = 0 - self.Players[PlayerName].Penalty = 0 - self.Players[PlayerName].PenaltyCoalition = 0 - self.Players[PlayerName].PenaltyWarning = 0 - end - - if not self.Players[PlayerName].UnitCoalition then - self.Players[PlayerName].UnitCoalition = UnitCoalition - else - if self.Players[PlayerName].UnitCoalition ~= UnitCoalition then - self.Players[PlayerName].Penalty = self.Players[PlayerName].Penalty + 50 - self.Players[PlayerName].PenaltyCoalition = self.Players[PlayerName].PenaltyCoalition + 1 - MESSAGE:New( "Player '" .. PlayerName .. "' changed coalition from " .. _SCORINGCoalition[self.Players[PlayerName].UnitCoalition] .. " to " .. _SCORINGCoalition[UnitCoalition] .. - "(changed " .. self.Players[PlayerName].PenaltyCoalition .. " times the coalition). 50 Penalty points added.", - 2 - ):ToAll() - self:ScoreCSV( PlayerName, "", "COALITION_PENALTY", 1, -50, self.Players[PlayerName].UnitName, _SCORINGCoalition[self.Players[PlayerName].UnitCoalition], _SCORINGCategory[self.Players[PlayerName].UnitCategory], self.Players[PlayerName].UnitType, - UnitName, _SCORINGCoalition[UnitCoalition], _SCORINGCategory[UnitCategory], UnitData:GetTypeName() ) - end - end - self.Players[PlayerName].UnitName = UnitName - self.Players[PlayerName].UnitCoalition = UnitCoalition - self.Players[PlayerName].UnitCategory = UnitCategory - self.Players[PlayerName].UnitType = UnitTypeName - self.Players[PlayerName].UNIT = UnitData - self.Players[PlayerName].ThreatLevel = UnitThreatLevel - self.Players[PlayerName].ThreatType = UnitThreatType - - if self.Players[PlayerName].Penalty > self.Fratricide * 0.50 then - if self.Players[PlayerName].PenaltyWarning < 1 then - MESSAGE:New( "Player '" .. PlayerName .. "': WARNING! If you continue to commit FRATRICIDE and have a PENALTY score higher than " .. self.Fratricide .. ", you will be COURT MARTIALED and DISMISSED from this mission! \nYour total penalty is: " .. self.Players[PlayerName].Penalty, - 30 - ):ToAll() - self.Players[PlayerName].PenaltyWarning = self.Players[PlayerName].PenaltyWarning + 1 - end - end - - if self.Players[PlayerName].Penalty > self.Fratricide then - UnitData:Destroy() - MESSAGE:New( "Player '" .. PlayerName .. "' committed FRATRICIDE, he will be COURT MARTIALED and is DISMISSED from this mission!", - 10 - ):ToAll() - end - - end -end - - ---- Add a goal score for a player. --- The method takes the PlayerUnit for which the Goal score needs to be set. --- The GoalTag is a string or identifier that is taken into the CSV file scoring log to identify the goal. --- A free text can be given that is shown to the players. --- The Score can be both positive and negative. --- @param #SCORING self --- @param Wrapper.Unit#UNIT PlayerUnit The @{Unit} of the Player. Other Properties for the scoring are taken from this PlayerUnit, like coalition, type etc. --- @param #string GoalTag The string or identifier that is used in the CSV file to identify the goal (sort or group later in Excel). --- @param #string Text A free text that is shown to the players. --- @param #number Score The score can be both positive or negative ( Penalty ). -function SCORING:AddGoalScore( PlayerUnit, GoalTag, Text, Score ) - - local PlayerName = PlayerUnit:GetPlayerName() - - self:E( { PlayerUnit.UnitName, PlayerName, GoalTag, Text, Score } ) - - -- PlayerName can be nil, if the Unit with the player crashed or due to another reason. - if PlayerName then - local PlayerData = self.Players[PlayerName] - - PlayerData.Goals[GoalTag] = PlayerData.Goals[GoalTag] or { Score = 0 } - PlayerData.Goals[GoalTag].Score = PlayerData.Goals[GoalTag].Score + Score - PlayerData.Score = PlayerData.Score + Score - - MESSAGE:New( Text, 30 ):ToAll() - - self:ScoreCSV( PlayerName, "", "GOAL_" .. string.upper( GoalTag ), 1, Score, PlayerUnit:GetName() ) - end -end - - ---- Registers Scores the players completing a Mission Task. --- @param #SCORING self --- @param Tasking.Mission#MISSION Mission --- @param Wrapper.Unit#UNIT PlayerUnit --- @param #string Text --- @param #number Score -function SCORING:_AddMissionTaskScore( Mission, PlayerUnit, Text, Score ) - - local PlayerName = PlayerUnit:GetPlayerName() - local MissionName = Mission:GetName() - - self:E( { Mission:GetName(), PlayerUnit.UnitName, PlayerName, Text, Score } ) - - -- PlayerName can be nil, if the Unit with the player crashed or due to another reason. - if PlayerName then - local PlayerData = self.Players[PlayerName] - - if not PlayerData.Mission[MissionName] then - PlayerData.Mission[MissionName] = {} - PlayerData.Mission[MissionName].ScoreTask = 0 - PlayerData.Mission[MissionName].ScoreMission = 0 - end - - self:T( PlayerName ) - self:T( PlayerData.Mission[MissionName] ) - - PlayerData.Score = self.Players[PlayerName].Score + Score - PlayerData.Mission[MissionName].ScoreTask = self.Players[PlayerName].Mission[MissionName].ScoreTask + Score - - MESSAGE:New( "Player '" .. PlayerName .. "' has " .. Text .. " in Mission '" .. MissionName .. "'. " .. - Score .. " task score!", - 30 ):ToAll() - - self:ScoreCSV( PlayerName, "", "TASK_" .. MissionName:gsub( ' ', '_' ), 1, Score, PlayerUnit:GetName() ) - end -end - - ---- Registers Mission Scores for possible multiple players that contributed in the Mission. --- @param #SCORING self --- @param Tasking.Mission#MISSION Mission --- @param Wrapper.Unit#UNIT PlayerUnit --- @param #string Text --- @param #number Score -function SCORING:_AddMissionScore( Mission, Text, Score ) - - local MissionName = Mission:GetName() - - self:E( { Mission, Text, Score } ) - self:E( self.Players ) - - for PlayerName, PlayerData in pairs( self.Players ) do - - self:E( PlayerData ) - if PlayerData.Mission[MissionName] then - - PlayerData.Score = PlayerData.Score + Score - PlayerData.Mission[MissionName].ScoreMission = PlayerData.Mission[MissionName].ScoreMission + Score - - MESSAGE:New( "Player '" .. PlayerName .. "' has " .. Text .. " in Mission '" .. MissionName .. "'. " .. - Score .. " mission score!", - 60 ):ToAll() - - self:ScoreCSV( PlayerName, "", "MISSION_" .. MissionName:gsub( ' ', '_' ), 1, Score ) - end - end -end - - ---- Handles the OnPlayerEnterUnit event for the scoring. --- @param #SCORING self --- @param Core.Event#EVENTDATA Event -function SCORING:OnEventPlayerEnterUnit( Event ) - if Event.IniUnit then - self:_AddPlayerFromUnit( Event.IniUnit ) - local Menu = MENU_GROUP:New( Event.IniGroup, 'Scoring' ) - local ReportGroupSummary = MENU_GROUP_COMMAND:New( Event.IniGroup, 'Summary report players in group', Menu, SCORING.ReportScoreGroupSummary, self, Event.IniGroup ) - local ReportGroupDetailed = MENU_GROUP_COMMAND:New( Event.IniGroup, 'Detailed report players in group', Menu, SCORING.ReportScoreGroupDetailed, self, Event.IniGroup ) - local ReportToAllSummary = MENU_GROUP_COMMAND:New( Event.IniGroup, 'Summary report all players', Menu, SCORING.ReportScoreAllSummary, self, Event.IniGroup ) - self:SetState( Event.IniUnit, "ScoringMenu", Menu ) - end -end - ---- Handles the OnPlayerLeaveUnit event for the scoring. --- @param #SCORING self --- @param Core.Event#EVENTDATA Event -function SCORING:OnEventPlayerLeaveUnit( Event ) - if Event.IniUnit then - local Menu = self:GetState( Event.IniUnit, "ScoringMenu" ) -- Core.Menu#MENU_GROUP - if Menu then - Menu:Remove() - end - end -end - - ---- Handles the OnHit event for the scoring. --- @param #SCORING self --- @param Core.Event#EVENTDATA Event -function SCORING:_EventOnHit( Event ) - self:F( { Event } ) - - local InitUnit = nil - local InitUNIT = nil - local InitUnitName = "" - local InitGroup = nil - local InitGroupName = "" - local InitPlayerName = nil - - local InitCoalition = nil - local InitCategory = nil - local InitType = nil - local InitUnitCoalition = nil - local InitUnitCategory = nil - local InitUnitType = nil - - local TargetUnit = nil - local TargetUNIT = nil - local TargetUnitName = "" - local TargetGroup = nil - local TargetGroupName = "" - local TargetPlayerName = nil - - local TargetCoalition = nil - local TargetCategory = nil - local TargetType = nil - local TargetUnitCoalition = nil - local TargetUnitCategory = nil - local TargetUnitType = nil - - if Event.IniDCSUnit then - - InitUnit = Event.IniDCSUnit - InitUNIT = Event.IniUnit - InitUnitName = Event.IniDCSUnitName - InitGroup = Event.IniDCSGroup - InitGroupName = Event.IniDCSGroupName - InitPlayerName = Event.IniPlayerName - - InitCoalition = Event.IniCoalition - --TODO: Workaround Client DCS Bug - --InitCategory = InitUnit:getCategory() - --InitCategory = InitUnit:getDesc().category - InitCategory = Event.IniCategory - InitType = Event.IniTypeName - - InitUnitCoalition = _SCORINGCoalition[InitCoalition] - InitUnitCategory = _SCORINGCategory[InitCategory] - InitUnitType = InitType - - self:T( { InitUnitName, InitGroupName, InitPlayerName, InitCoalition, InitCategory, InitType , InitUnitCoalition, InitUnitCategory, InitUnitType } ) - end - - - if Event.TgtDCSUnit then - - TargetUnit = Event.TgtDCSUnit - TargetUNIT = Event.TgtUnit - TargetUnitName = Event.TgtDCSUnitName - TargetGroup = Event.TgtDCSGroup - TargetGroupName = Event.TgtDCSGroupName - TargetPlayerName = Event.TgtPlayerName - - TargetCoalition = Event.TgtCoalition - --TODO: Workaround Client DCS Bug - --TargetCategory = TargetUnit:getCategory() - --TargetCategory = TargetUnit:getDesc().category - TargetCategory = Event.TgtCategory - TargetType = Event.TgtTypeName - - TargetUnitCoalition = _SCORINGCoalition[TargetCoalition] - TargetUnitCategory = _SCORINGCategory[TargetCategory] - TargetUnitType = TargetType - - self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType, TargetUnitCoalition, TargetUnitCategory, TargetUnitType } ) - end - - if InitPlayerName ~= nil then -- It is a player that is hitting something - self:_AddPlayerFromUnit( InitUNIT ) - if self.Players[InitPlayerName] then -- This should normally not happen, but i'll test it anyway. - if TargetPlayerName ~= nil then -- It is a player hitting another player ... - self:_AddPlayerFromUnit( TargetUNIT ) - end - - self:T( "Hitting Something" ) - - -- What is he hitting? - if TargetCategory then - - -- A target got hit, score it. - -- Player contains the score data from self.Players[InitPlayerName] - local Player = self.Players[InitPlayerName] - - -- Ensure there is a hit table per TargetCategory and TargetUnitName. - Player.Hit[TargetCategory] = Player.Hit[TargetCategory] or {} - Player.Hit[TargetCategory][TargetUnitName] = Player.Hit[TargetCategory][TargetUnitName] or {} - - -- PlayerHit contains the score counters and data per unit that was hit. - local PlayerHit = Player.Hit[TargetCategory][TargetUnitName] - - PlayerHit.Score = PlayerHit.Score or 0 - PlayerHit.Penalty = PlayerHit.Penalty or 0 - PlayerHit.ScoreHit = PlayerHit.ScoreHit or 0 - PlayerHit.PenaltyHit = PlayerHit.PenaltyHit or 0 - PlayerHit.TimeStamp = PlayerHit.TimeStamp or 0 - PlayerHit.UNIT = PlayerHit.UNIT or TargetUNIT - PlayerHit.ThreatLevel, PlayerHit.ThreatType = PlayerHit.UNIT:GetThreatLevel() - - -- Only grant hit scores if there was more than one second between the last hit. - if timer.getTime() - PlayerHit.TimeStamp > 1 then - PlayerHit.TimeStamp = timer.getTime() - - if TargetPlayerName ~= nil then -- It is a player hitting another player ... - - -- Ensure there is a Player to Player hit reference table. - Player.HitPlayers[TargetPlayerName] = true - end - - local Score = 0 - - if InitCoalition then -- A coalition object was hit. - if InitCoalition == TargetCoalition then - Player.Penalty = Player.Penalty + 10 - PlayerHit.Penalty = PlayerHit.Penalty + 10 - PlayerHit.PenaltyHit = PlayerHit.PenaltyHit + 1 - - if TargetPlayerName ~= nil then -- It is a player hitting another player ... - MESSAGE - :New( "Player '" .. InitPlayerName .. "' hit friendly player '" .. TargetPlayerName .. "' " .. - TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.PenaltyHit .. " times. " .. - "Penalty: -" .. PlayerHit.Penalty .. ". Score Total:" .. Player.Score - Player.Penalty, - 2 - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) - else - MESSAGE - :New( "Player '" .. InitPlayerName .. "' hit a friendly target " .. - TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.PenaltyHit .. " times. " .. - "Penalty: -" .. PlayerHit.Penalty .. ". Score Total:" .. Player.Score - Player.Penalty, - 2 - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) - end - self:ScoreCSV( InitPlayerName, TargetPlayerName, "HIT_PENALTY", 1, -10, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - else - Player.Score = Player.Score + 1 - PlayerHit.Score = PlayerHit.Score + 1 - PlayerHit.ScoreHit = PlayerHit.ScoreHit + 1 - if TargetPlayerName ~= nil then -- It is a player hitting another player ... - MESSAGE - :New( "Player '" .. InitPlayerName .. "' hit enemy player '" .. TargetPlayerName .. "' " .. - TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.ScoreHit .. " times. " .. - "Score: " .. PlayerHit.Score .. ". Score Total:" .. Player.Score - Player.Penalty, - 2 - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) - else - MESSAGE - :New( "Player '" .. InitPlayerName .. "' hit an enemy target " .. - TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.ScoreHit .. " times. " .. - "Score: " .. PlayerHit.Score .. ". Score Total:" .. Player.Score - Player.Penalty, - 2 - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) - end - self:ScoreCSV( InitPlayerName, TargetPlayerName, "HIT_SCORE", 1, 1, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - end - else -- A scenery object was hit. - MESSAGE - :New( "Player '" .. InitPlayerName .. "' hit a scenery object.", - 2 - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) - self:ScoreCSV( InitPlayerName, "", "HIT_SCORE", 1, 0, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, "", "Scenery", TargetUnitType ) - end - end - end - end - elseif InitPlayerName == nil then -- It is an AI hitting a player??? - - end - - -- It is a weapon initiated by a player, that is hitting something - -- This seems to occur only with scenery and static objects. - if Event.WeaponPlayerName ~= nil then - self:_AddPlayerFromUnit( Event.WeaponUNIT ) - if self.Players[Event.WeaponPlayerName] then -- This should normally not happen, but i'll test it anyway. - if TargetPlayerName ~= nil then -- It is a player hitting another player ... - self:_AddPlayerFromUnit( TargetUNIT ) - end - - self:T( "Hitting Scenery" ) - - -- What is he hitting? - if TargetCategory then - - -- A scenery or static got hit, score it. - -- Player contains the score data from self.Players[WeaponPlayerName] - local Player = self.Players[Event.WeaponPlayerName] - - -- Ensure there is a hit table per TargetCategory and TargetUnitName. - Player.Hit[TargetCategory] = Player.Hit[TargetCategory] or {} - Player.Hit[TargetCategory][TargetUnitName] = Player.Hit[TargetCategory][TargetUnitName] or {} - - -- PlayerHit contains the score counters and data per unit that was hit. - local PlayerHit = Player.Hit[TargetCategory][TargetUnitName] - - PlayerHit.Score = PlayerHit.Score or 0 - PlayerHit.Penalty = PlayerHit.Penalty or 0 - PlayerHit.ScoreHit = PlayerHit.ScoreHit or 0 - PlayerHit.PenaltyHit = PlayerHit.PenaltyHit or 0 - PlayerHit.TimeStamp = PlayerHit.TimeStamp or 0 - PlayerHit.UNIT = PlayerHit.UNIT or TargetUNIT - PlayerHit.ThreatLevel, PlayerHit.ThreatType = PlayerHit.UNIT:GetThreatLevel() - - -- Only grant hit scores if there was more than one second between the last hit. - if timer.getTime() - PlayerHit.TimeStamp > 1 then - PlayerHit.TimeStamp = timer.getTime() - - local Score = 0 - - if InitCoalition then -- A coalition object was hit, probably a static. - if InitCoalition == TargetCoalition then - -- TODO: Penalty according scale - Player.Penalty = Player.Penalty + 10 - PlayerHit.Penalty = PlayerHit.Penalty + 10 - PlayerHit.PenaltyHit = PlayerHit.PenaltyHit + 1 - - MESSAGE - :New( "Player '" .. Event.WeaponPlayerName .. "' hit a friendly target " .. - TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.PenaltyHit .. " times. " .. - "Penalty: -" .. PlayerHit.Penalty .. ". Score Total:" .. Player.Score - Player.Penalty, - 2 - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( Event.WeaponCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) - self:ScoreCSV( Event.WeaponPlayerName, TargetPlayerName, "HIT_PENALTY", 1, -10, Event.WeaponName, Event.WeaponCoalition, Event.WeaponCategory, Event.WeaponTypeName, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - else - Player.Score = Player.Score + 1 - PlayerHit.Score = PlayerHit.Score + 1 - PlayerHit.ScoreHit = PlayerHit.ScoreHit + 1 - MESSAGE - :New( "Player '" .. Event.WeaponPlayerName .. "' hit an enemy target " .. - TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.ScoreHit .. " times. " .. - "Score: " .. PlayerHit.Score .. ". Score Total:" .. Player.Score - Player.Penalty, - 2 - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( Event.WeaponCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) - self:ScoreCSV( Event.WeaponPlayerName, TargetPlayerName, "HIT_SCORE", 1, 1, Event.WeaponName, Event.WeaponCoalition, Event.WeaponCategory, Event.WeaponTypeName, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - end - else -- A scenery object was hit. - MESSAGE - :New( "Player '" .. Event.WeaponPlayerName .. "' hit a scenery object.", - 2 - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) - self:ScoreCSV( Event.WeaponPlayerName, "", "HIT_SCORE", 1, 0, Event.WeaponName, Event.WeaponCoalition, Event.WeaponCategory, Event.WeaponTypeName, TargetUnitName, "", "Scenery", TargetUnitType ) - end - end - end - end - end -end - ---- Track DEAD or CRASH events for the scoring. --- @param #SCORING self --- @param Core.Event#EVENTDATA Event -function SCORING:_EventOnDeadOrCrash( Event ) - self:F( { Event } ) - - local TargetUnit = nil - local TargetGroup = nil - local TargetUnitName = "" - local TargetGroupName = "" - local TargetPlayerName = "" - local TargetCoalition = nil - local TargetCategory = nil - local TargetType = nil - local TargetUnitCoalition = nil - local TargetUnitCategory = nil - local TargetUnitType = nil - - if Event.IniDCSUnit then - - TargetUnit = Event.IniUnit - TargetUnitName = Event.IniDCSUnitName - TargetGroup = Event.IniDCSGroup - TargetGroupName = Event.IniDCSGroupName - TargetPlayerName = Event.IniPlayerName - - TargetCoalition = Event.IniCoalition - --TargetCategory = TargetUnit:getCategory() - --TargetCategory = TargetUnit:getDesc().category -- Workaround - TargetCategory = Event.IniCategory - TargetType = Event.IniTypeName - - TargetUnitCoalition = _SCORINGCoalition[TargetCoalition] - TargetUnitCategory = _SCORINGCategory[TargetCategory] - TargetUnitType = TargetType - - self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType } ) - end - - -- Player contains the score and reference data for the player. - for PlayerName, Player in pairs( self.Players ) do - if Player then -- This should normally not happen, but i'll test it anyway. - self:T( "Something got destroyed" ) - - -- Some variables - local InitUnitName = Player.UnitName - local InitUnitType = Player.UnitType - local InitCoalition = Player.UnitCoalition - local InitCategory = Player.UnitCategory - local InitUnitCoalition = _SCORINGCoalition[InitCoalition] - local InitUnitCategory = _SCORINGCategory[InitCategory] - - self:T( { InitUnitName, InitUnitType, InitUnitCoalition, InitCoalition, InitUnitCategory, InitCategory } ) - - local Destroyed = false - - -- What is the player destroying? - if Player and Player.Hit and Player.Hit[TargetCategory] and Player.Hit[TargetCategory][TargetUnitName] and Player.Hit[TargetCategory][TargetUnitName].TimeStamp ~= 0 then -- Was there a hit for this unit for this player before registered??? - - local TargetThreatLevel = Player.Hit[TargetCategory][TargetUnitName].ThreatLevel - local TargetThreatType = Player.Hit[TargetCategory][TargetUnitName].ThreatType - - Player.Destroy[TargetCategory] = Player.Destroy[TargetCategory] or {} - Player.Destroy[TargetCategory][TargetType] = Player.Destroy[TargetCategory][TargetType] or {} - - -- PlayerDestroy contains the destroy score data per category and target type of the player. - local TargetDestroy = Player.Destroy[TargetCategory][TargetType] - TargetDestroy.Score = TargetDestroy.Score or 0 - TargetDestroy.ScoreDestroy = TargetDestroy.ScoreDestroy or 0 - TargetDestroy.Penalty = TargetDestroy.Penalty or 0 - TargetDestroy.PenaltyDestroy = TargetDestroy.PenaltyDestroy or 0 - - if TargetCoalition then - if InitCoalition == TargetCoalition then - local ThreatLevelTarget = TargetThreatLevel - local ThreatTypeTarget = TargetThreatType - local ThreatLevelPlayer = Player.ThreatLevel / 10 + 1 - local ThreatPenalty = math.ceil( ( ThreatLevelTarget / ThreatLevelPlayer ) * self.ScaleDestroyPenalty / 10 ) - self:E( { ThreatLevel = ThreatPenalty, ThreatLevelTarget = ThreatLevelTarget, ThreatTypeTarget = ThreatTypeTarget, ThreatLevelPlayer = ThreatLevelPlayer } ) - - Player.Penalty = Player.Penalty + ThreatPenalty - TargetDestroy.Penalty = TargetDestroy.Penalty + ThreatPenalty - TargetDestroy.PenaltyDestroy = TargetDestroy.PenaltyDestroy + 1 - - if Player.HitPlayers[TargetPlayerName] then -- A player destroyed another player - MESSAGE - :New( "Player '" .. PlayerName .. "' destroyed friendly player '" .. TargetPlayerName .. "' " .. - TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. TargetDestroy.PenaltyDestroy .. " times. " .. - "Penalty: -" .. TargetDestroy.Penalty .. ". Score Total:" .. Player.Score - Player.Penalty, - 15 - ) - :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) - else - MESSAGE - :New( "Player '" .. PlayerName .. "' destroyed a friendly target " .. - TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. TargetDestroy.PenaltyDestroy .. " times. " .. - "Penalty: -" .. TargetDestroy.Penalty .. ". Score Total:" .. Player.Score - Player.Penalty, - 15 - ) - :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) - end - - Destroyed = true - self:ScoreCSV( PlayerName, TargetPlayerName, "DESTROY_PENALTY", 1, ThreatPenalty, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - else - - local ThreatLevelTarget = TargetThreatLevel - local ThreatTypeTarget = TargetThreatType - local ThreatLevelPlayer = Player.ThreatLevel / 10 + 1 - local ThreatScore = math.ceil( ( ThreatLevelTarget / ThreatLevelPlayer ) * self.ScaleDestroyScore / 10 ) - - self:E( { ThreatLevel = ThreatScore, ThreatLevelTarget = ThreatLevelTarget, ThreatTypeTarget = ThreatTypeTarget, ThreatLevelPlayer = ThreatLevelPlayer } ) - - Player.Score = Player.Score + ThreatScore - TargetDestroy.Score = TargetDestroy.Score + ThreatScore - TargetDestroy.ScoreDestroy = TargetDestroy.ScoreDestroy + 1 - if Player.HitPlayers[TargetPlayerName] then -- A player destroyed another player - MESSAGE - :New( "Player '" .. PlayerName .. "' destroyed enemy player '" .. TargetPlayerName .. "' " .. - TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. TargetDestroy.ScoreDestroy .. " times. " .. - "Score: " .. TargetDestroy.Score .. ". Score Total:" .. Player.Score - Player.Penalty, - 15 - ) - :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) - else - MESSAGE - :New( "Player '" .. PlayerName .. "' destroyed an enemy " .. - TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. TargetDestroy.ScoreDestroy .. " times. " .. - "Score: " .. TargetDestroy.Score .. ". Total:" .. Player.Score - Player.Penalty, - 15 - ) - :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) - end - Destroyed = true - self:ScoreCSV( PlayerName, TargetPlayerName, "DESTROY_SCORE", 1, ThreatScore, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - - local UnitName = TargetUnit:GetName() - local Score = self.ScoringObjects[UnitName] - if Score then - Player.Score = Player.Score + Score - TargetDestroy.Score = TargetDestroy.Score + Score - MESSAGE - :New( "Special target '" .. TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. " destroyed! " .. - "Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! Total: " .. Player.Score - Player.Penalty, - 15 - ) - :ToAllIf( self:IfMessagesScore() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesScore() and self:IfMessagesToCoalition() ) - self:ScoreCSV( PlayerName, TargetPlayerName, "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - Destroyed = true - end - - -- Check if there are Zones where the destruction happened. - for ZoneName, ScoreZoneData in pairs( self.ScoringZones ) do - self:E( { ScoringZone = ScoreZoneData } ) - local ScoreZone = ScoreZoneData.ScoreZone -- Core.Zone#ZONE_BASE - local Score = ScoreZoneData.Score - if ScoreZone:IsVec2InZone( TargetUnit:GetVec2() ) then - Player.Score = Player.Score + Score - TargetDestroy.Score = TargetDestroy.Score + Score - MESSAGE - :New( "Target destroyed in zone '" .. ScoreZone:GetName() .. "'." .. - "Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! " .. - "Total: " .. Player.Score - Player.Penalty, - 15 ) - :ToAllIf( self:IfMessagesZone() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesZone() and self:IfMessagesToCoalition() ) - self:ScoreCSV( PlayerName, TargetPlayerName, "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - Destroyed = true - end - end - - end - else - -- Check if there are Zones where the destruction happened. - for ZoneName, ScoreZoneData in pairs( self.ScoringZones ) do - self:E( { ScoringZone = ScoreZoneData } ) - local ScoreZone = ScoreZoneData.ScoreZone -- Core.Zone#ZONE_BASE - local Score = ScoreZoneData.Score - if ScoreZone:IsVec2InZone( TargetUnit:GetVec2() ) then - Player.Score = Player.Score + Score - TargetDestroy.Score = TargetDestroy.Score + Score - MESSAGE - :New( "Scenery destroyed in zone '" .. ScoreZone:GetName() .. "'." .. - "Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! " .. - "Total: " .. Player.Score - Player.Penalty, - 15 - ) - :ToAllIf( self:IfMessagesZone() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesZone() and self:IfMessagesToCoalition() ) - Destroyed = true - self:ScoreCSV( PlayerName, "", "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, "", "Scenery", TargetUnitType ) - end - end - end - - -- Delete now the hit cache if the target was destroyed. - -- Otherwise points will be granted every time a target gets killed by the players that hit that target. - -- This is only relevant for player to player destroys. - if Destroyed then - Player.Hit[TargetCategory][TargetUnitName].TimeStamp = 0 - end - end - end - end -end - - ---- Produce detailed report of player hit scores. --- @param #SCORING self --- @param #string PlayerName The name of the player. --- @return #string The report. -function SCORING:ReportDetailedPlayerHits( PlayerName ) - - local ScoreMessage = "" - local PlayerScore = 0 - local PlayerPenalty = 0 - - local PlayerData = self.Players[PlayerName] - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) - - -- Some variables - local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] - local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local ScoreMessageHits = "" - for CategoryID, CategoryName in pairs( _SCORINGCategory ) do - self:T( CategoryName ) - if PlayerData.Hit[CategoryID] then - self:T( "Hit scores exist for player " .. PlayerName ) - local Score = 0 - local ScoreHit = 0 - local Penalty = 0 - local PenaltyHit = 0 - for UnitName, UnitData in pairs( PlayerData.Hit[CategoryID] ) do - Score = Score + UnitData.Score - ScoreHit = ScoreHit + UnitData.ScoreHit - Penalty = Penalty + UnitData.Penalty - PenaltyHit = UnitData.PenaltyHit - end - local ScoreMessageHit = string.format( "%s:%d ", CategoryName, Score - Penalty ) - self:T( ScoreMessageHit ) - ScoreMessageHits = ScoreMessageHits .. ScoreMessageHit - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageHits ~= "" then - ScoreMessage = "Hits: " .. ScoreMessageHits - end - end - - return ScoreMessage, PlayerScore, PlayerPenalty -end - - ---- Produce detailed report of player destroy scores. --- @param #SCORING self --- @param #string PlayerName The name of the player. --- @return #string The report. -function SCORING:ReportDetailedPlayerDestroys( PlayerName ) - - local ScoreMessage = "" - local PlayerScore = 0 - local PlayerPenalty = 0 - - local PlayerData = self.Players[PlayerName] - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) - - -- Some variables - local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] - local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local ScoreMessageDestroys = "" - for CategoryID, CategoryName in pairs( _SCORINGCategory ) do - if PlayerData.Destroy[CategoryID] then - self:T( "Destroy scores exist for player " .. PlayerName ) - local Score = 0 - local ScoreDestroy = 0 - local Penalty = 0 - local PenaltyDestroy = 0 - - for UnitName, UnitData in pairs( PlayerData.Destroy[CategoryID] ) do - self:E( { UnitData = UnitData } ) - if UnitData ~= {} then - Score = Score + UnitData.Score - ScoreDestroy = ScoreDestroy + UnitData.ScoreDestroy - Penalty = Penalty + UnitData.Penalty - PenaltyDestroy = PenaltyDestroy + UnitData.PenaltyDestroy - end - end - - local ScoreMessageDestroy = string.format( " %s:%d ", CategoryName, Score - Penalty ) - self:T( ScoreMessageDestroy ) - ScoreMessageDestroys = ScoreMessageDestroys .. ScoreMessageDestroy - - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageDestroys = ScoreMessageDestroys .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageDestroys ~= "" then - ScoreMessage = "Destroys: " .. ScoreMessageDestroys - end - end - - return ScoreMessage, PlayerScore, PlayerPenalty -end - ---- Produce detailed report of player penalty scores because of changing the coalition. --- @param #SCORING self --- @param #string PlayerName The name of the player. --- @return #string The report. -function SCORING:ReportDetailedPlayerCoalitionChanges( PlayerName ) - - local ScoreMessage = "" - local PlayerScore = 0 - local PlayerPenalty = 0 - - local PlayerData = self.Players[PlayerName] - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) - - -- Some variables - local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] - local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local ScoreMessageCoalitionChangePenalties = "" - if PlayerData.PenaltyCoalition ~= 0 then - ScoreMessageCoalitionChangePenalties = ScoreMessageCoalitionChangePenalties .. string.format( " -%d (%d changed)", PlayerData.Penalty, PlayerData.PenaltyCoalition ) - PlayerPenalty = PlayerPenalty + PlayerData.Penalty - end - if ScoreMessageCoalitionChangePenalties ~= "" then - ScoreMessage = ScoreMessage .. "Coalition Penalties: " .. ScoreMessageCoalitionChangePenalties - end - end - - return ScoreMessage, PlayerScore, PlayerPenalty -end - ---- Produce detailed report of player goal scores. --- @param #SCORING self --- @param #string PlayerName The name of the player. --- @return #string The report. -function SCORING:ReportDetailedPlayerGoals( PlayerName ) - - local ScoreMessage = "" - local PlayerScore = 0 - local PlayerPenalty = 0 - - local PlayerData = self.Players[PlayerName] - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) - - -- Some variables - local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] - local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local ScoreMessageGoal = "" - local ScoreGoal = 0 - local ScoreTask = 0 - for GoalName, GoalData in pairs( PlayerData.Goals ) do - ScoreGoal = ScoreGoal + GoalData.Score - ScoreMessageGoal = ScoreMessageGoal .. "'" .. GoalName .. "':" .. GoalData.Score .. "; " - end - PlayerScore = PlayerScore + ScoreGoal - - if ScoreMessageGoal ~= "" then - ScoreMessage = "Goals: " .. ScoreMessageGoal - end - end - - return ScoreMessage, PlayerScore, PlayerPenalty -end - ---- Produce detailed report of player penalty scores because of changing the coalition. --- @param #SCORING self --- @param #string PlayerName The name of the player. --- @return #string The report. -function SCORING:ReportDetailedPlayerMissions( PlayerName ) - - local ScoreMessage = "" - local PlayerScore = 0 - local PlayerPenalty = 0 - - local PlayerData = self.Players[PlayerName] - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) - - -- Some variables - local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] - local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local ScoreMessageMission = "" - local ScoreMission = 0 - local ScoreTask = 0 - for MissionName, MissionData in pairs( PlayerData.Mission ) do - ScoreMission = ScoreMission + MissionData.ScoreMission - ScoreTask = ScoreTask + MissionData.ScoreTask - ScoreMessageMission = ScoreMessageMission .. "'" .. MissionName .. "'; " - end - PlayerScore = PlayerScore + ScoreMission + ScoreTask - - if ScoreMessageMission ~= "" then - ScoreMessage = "Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ")" - end - end - - return ScoreMessage, PlayerScore, PlayerPenalty -end - - ---- Report Group Score Summary --- @param #SCORING self --- @param Wrapper.Group#GROUP PlayerGroup The player group. -function SCORING:ReportScoreGroupSummary( PlayerGroup ) - - local PlayerMessage = "" - - self:T( "Report Score Group Summary" ) - - local PlayerUnits = PlayerGroup:GetUnits() - for UnitID, PlayerUnit in pairs( PlayerUnits ) do - local PlayerUnit = PlayerUnit -- Wrapper.Unit#UNIT - local PlayerName = PlayerUnit:GetPlayerName() - - if PlayerName then - - local ReportHits, ScoreHits, PenaltyHits = self:ReportDetailedPlayerHits( PlayerName ) - ReportHits = ReportHits ~= "" and "\n- " .. ReportHits or ReportHits - self:E( { ReportHits, ScoreHits, PenaltyHits } ) - - local ReportDestroys, ScoreDestroys, PenaltyDestroys = self:ReportDetailedPlayerDestroys( PlayerName ) - ReportDestroys = ReportDestroys ~= "" and "\n- " .. ReportDestroys or ReportDestroys - self:E( { ReportDestroys, ScoreDestroys, PenaltyDestroys } ) - - local ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges = self:ReportDetailedPlayerCoalitionChanges( PlayerName ) - ReportCoalitionChanges = ReportCoalitionChanges ~= "" and "\n- " .. ReportCoalitionChanges or ReportCoalitionChanges - self:E( { ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges } ) - - local ReportGoals, ScoreGoals, PenaltyGoals = self:ReportDetailedPlayerGoals( PlayerName ) - ReportGoals = ReportGoals ~= "" and "\n- " .. ReportGoals or ReportGoals - self:E( { ReportGoals, ScoreGoals, PenaltyGoals } ) - - local ReportMissions, ScoreMissions, PenaltyMissions = self:ReportDetailedPlayerMissions( PlayerName ) - ReportMissions = ReportMissions ~= "" and "\n- " .. ReportMissions or ReportMissions - self:E( { ReportMissions, ScoreMissions, PenaltyMissions } ) - - local PlayerScore = ScoreHits + ScoreDestroys + ScoreCoalitionChanges + ScoreGoals + ScoreMissions - local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + ScoreGoals + PenaltyMissions - - PlayerMessage = - string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )", - PlayerName, - PlayerScore - PlayerPenalty, - PlayerScore, - PlayerPenalty - ) - MESSAGE:New( PlayerMessage, 30, "Player '" .. PlayerName .. "'" ):ToGroup( PlayerGroup ) - end - end - -end - ---- Report Group Score Detailed --- @param #SCORING self --- @param Wrapper.Group#GROUP PlayerGroup The player group. -function SCORING:ReportScoreGroupDetailed( PlayerGroup ) - - local PlayerMessage = "" - - self:T( "Report Score Group Detailed" ) - - local PlayerUnits = PlayerGroup:GetUnits() - for UnitID, PlayerUnit in pairs( PlayerUnits ) do - local PlayerUnit = PlayerUnit -- Wrapper.Unit#UNIT - local PlayerName = PlayerUnit:GetPlayerName() - - if PlayerName then - - local ReportHits, ScoreHits, PenaltyHits = self:ReportDetailedPlayerHits( PlayerName ) - ReportHits = ReportHits ~= "" and "\n- " .. ReportHits or ReportHits - self:E( { ReportHits, ScoreHits, PenaltyHits } ) - - local ReportDestroys, ScoreDestroys, PenaltyDestroys = self:ReportDetailedPlayerDestroys( PlayerName ) - ReportDestroys = ReportDestroys ~= "" and "\n- " .. ReportDestroys or ReportDestroys - self:E( { ReportDestroys, ScoreDestroys, PenaltyDestroys } ) - - local ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges = self:ReportDetailedPlayerCoalitionChanges( PlayerName ) - ReportCoalitionChanges = ReportCoalitionChanges ~= "" and "\n- " .. ReportCoalitionChanges or ReportCoalitionChanges - self:E( { ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges } ) - - local ReportGoals, ScoreGoals, PenaltyGoals = self:ReportDetailedPlayerGoals( PlayerName ) - ReportGoals = ReportGoals ~= "" and "\n- " .. ReportGoals or ReportGoals - self:E( { ReportGoals, ScoreGoals, PenaltyGoals } ) - - local ReportMissions, ScoreMissions, PenaltyMissions = self:ReportDetailedPlayerMissions( PlayerName ) - ReportMissions = ReportMissions ~= "" and "\n- " .. ReportMissions or ReportMissions - self:E( { ReportMissions, ScoreMissions, PenaltyMissions } ) - - local PlayerScore = ScoreHits + ScoreDestroys + ScoreCoalitionChanges + ScoreGoals + ScoreMissions - local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + ScoreGoals + PenaltyMissions - - PlayerMessage = - string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )%s%s%s%s%s", - PlayerName, - PlayerScore - PlayerPenalty, - PlayerScore, - PlayerPenalty, - ReportHits, - ReportDestroys, - ReportCoalitionChanges, - ReportGoals, - ReportMissions - ) - MESSAGE:New( PlayerMessage, 30, "Player '" .. PlayerName .. "'" ):ToGroup( PlayerGroup ) - end - end - -end - ---- Report all players score --- @param #SCORING self --- @param Wrapper.Group#GROUP PlayerGroup The player group. -function SCORING:ReportScoreAllSummary( PlayerGroup ) - - local PlayerMessage = "" - - self:T( "Report Score All Players" ) - - for PlayerName, PlayerData in pairs( self.Players ) do - - if PlayerName then - - local ReportHits, ScoreHits, PenaltyHits = self:ReportDetailedPlayerHits( PlayerName ) - ReportHits = ReportHits ~= "" and "\n- " .. ReportHits or ReportHits - self:E( { ReportHits, ScoreHits, PenaltyHits } ) - - local ReportDestroys, ScoreDestroys, PenaltyDestroys = self:ReportDetailedPlayerDestroys( PlayerName ) - ReportDestroys = ReportDestroys ~= "" and "\n- " .. ReportDestroys or ReportDestroys - self:E( { ReportDestroys, ScoreDestroys, PenaltyDestroys } ) - - local ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges = self:ReportDetailedPlayerCoalitionChanges( PlayerName ) - ReportCoalitionChanges = ReportCoalitionChanges ~= "" and "\n- " .. ReportCoalitionChanges or ReportCoalitionChanges - self:E( { ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges } ) - - local ReportGoals, ScoreGoals, PenaltyGoals = self:ReportDetailedPlayerGoals( PlayerName ) - ReportGoals = ReportGoals ~= "" and "\n- " .. ReportGoals or ReportGoals - self:E( { ReportGoals, ScoreGoals, PenaltyGoals } ) - - local ReportMissions, ScoreMissions, PenaltyMissions = self:ReportDetailedPlayerMissions( PlayerName ) - ReportMissions = ReportMissions ~= "" and "\n- " .. ReportMissions or ReportMissions - self:E( { ReportMissions, ScoreMissions, PenaltyMissions } ) - - local PlayerScore = ScoreHits + ScoreDestroys + ScoreCoalitionChanges + ScoreGoals + ScoreMissions - local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + ScoreGoals + PenaltyMissions - - PlayerMessage = - string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )", - PlayerName, - PlayerScore - PlayerPenalty, - PlayerScore, - PlayerPenalty - ) - MESSAGE:New( PlayerMessage, 30, "Player '" .. PlayerName .. "'" ):ToGroup( PlayerGroup ) - end - end - -end - - -function SCORING:SecondsToClock(sSeconds) - local nSeconds = sSeconds - if nSeconds == 0 then - --return nil; - return "00:00:00"; - else - nHours = string.format("%02.f", math.floor(nSeconds/3600)); - nMins = string.format("%02.f", math.floor(nSeconds/60 - (nHours*60))); - nSecs = string.format("%02.f", math.floor(nSeconds - nHours*3600 - nMins *60)); - return nHours..":"..nMins..":"..nSecs - end -end - ---- Opens a score CSV file to log the scores. --- @param #SCORING self --- @param #string ScoringCSV --- @return #SCORING self --- @usage --- -- Open a new CSV file to log the scores of the game Gori Valley. Let the name of the CSV file begin with "Player Scores". --- ScoringObject = SCORING:New( "Gori Valley" ) --- ScoringObject:OpenCSV( "Player Scores" ) -function SCORING:OpenCSV( ScoringCSV ) - self:F( ScoringCSV ) - - if lfs and io and os then - if ScoringCSV then - self.ScoringCSV = ScoringCSV - local fdir = lfs.writedir() .. [[Logs\]] .. self.ScoringCSV .. " " .. os.date( "%Y-%m-%d %H-%M-%S" ) .. ".csv" - - self.CSVFile, self.err = io.open( fdir, "w+" ) - if not self.CSVFile then - error( "Error: Cannot open CSV file in " .. lfs.writedir() ) - end - - self.CSVFile:write( '"GameName","RunTime","Time","PlayerName","TargetPlayerName","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 TargetPlayerName The name of the target 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, TargetPlayerName, 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( '"', '_' ) - - TargetPlayerName = TargetPlayerName or "" - TargetPlayerName = TargetPlayerName:gsub( '"', '_' ) - - if PlayerUnitName and PlayerUnitName ~= '' then - local PlayerUnit = Unit.getByName( PlayerUnitName ) - - if PlayerUnit then - if not PlayerUnitCategory then - --PlayerUnitCategory = SCORINGCategory[PlayerUnit:getCategory()] - PlayerUnitCategory = _SCORINGCategory[PlayerUnit:getDesc().category] - end - - if not PlayerUnitCoalition then - PlayerUnitCoalition = _SCORINGCoalition[PlayerUnit:getCoalition()] - end - - if not PlayerUnitType then - PlayerUnitType = PlayerUnit:getTypeName() - end - else - PlayerUnitName = '' - PlayerUnitCategory = '' - PlayerUnitCoalition = '' - PlayerUnitType = '' - end - else - PlayerUnitName = '' - PlayerUnitCategory = '' - PlayerUnitCoalition = '' - PlayerUnitType = '' - end - - TargetUnitCoalition = TargetUnitCoalition or "" - TargetUnitCategory = TargetUnitCategory or "" - TargetUnitType = TargetUnitType or "" - TargetUnitName = TargetUnitName or "" - - if lfs and io and os then - self.CSVFile:write( - '"' .. self.GameName .. '"' .. ',' .. - '"' .. self.RunTime .. '"' .. ',' .. - '' .. ScoreTime .. '' .. ',' .. - '"' .. PlayerName .. '"' .. ',' .. - '"' .. TargetPlayerName .. '"' .. ',' .. - '"' .. 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() ) -- #CLEANUP - self:F( { ZoneNames, TimeInterval } ) - - if type( ZoneNames ) == 'table' then - self.ZoneNames = ZoneNames - else - self.ZoneNames = { ZoneNames } - end - if TimeInterval then - self.TimeInterval = TimeInterval - end - - self:HandleEvent( EVENTS.Birth ) - - 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 - ---- @param #CLEANUP self --- @param Core.Event#EVENTDATA EventData -function CLEANUP:_OnEventBirth( EventData ) - self:F( { EventData } ) - - self.CleanUpList[EventData.IniDCSUnitName] = {} - self.CleanUpList[EventData.IniDCSUnitName].CleanUpUnit = EventData.IniDCSUnit - self.CleanUpList[EventData.IniDCSUnitName].CleanUpGroup = EventData.IniDCSGroup - self.CleanUpList[EventData.IniDCSUnitName].CleanUpGroupName = EventData.IniDCSGroupName - self.CleanUpList[EventData.IniDCSUnitName].CleanUpUnitName = EventData.IniDCSUnitName - - EventData.IniUnit:HandleEvent( EVENTS.EngineShutdown , self._EventAddForCleanUp ) - EventData.IniUnit:HandleEvent( EVENTS.EngineStartup, self._EventAddForCleanUp ) - EventData.IniUnit:HandleEvent( EVENTS.Hit, self._EventAddForCleanUp ) - EventData.IniUnit:HandleEvent( EVENTS.PilotDead, self._EventCrash ) - EventData.IniUnit:HandleEvent( EVENTS.Dead, self._EventCrash ) - EventData.IniUnit:HandleEvent( EVENTS.Crash, self._EventCrash ) - EventData.IniUnit:HandleEvent( EVENTS.Shot, self._EventShot ) - -end - ---- Detects if a crash event occurs. --- Crashed units go into a CleanUpList for removal. --- @param #CLEANUP self --- @param Dcs.DCSTypes#Event event -function CLEANUP:_EventCrash( Event ) - self:F( { Event } ) - - --TODO: This stuff is not working due to a DCS bug. Burning units cannot be destroyed. - -- self:T("before getGroup") - -- local _grp = Unit.getGroup(event.initiator)-- Identify the group that fired - -- self:T("after getGroup") - -- _grp:destroy() - -- self:T("after deactivateGroup") - -- event.initiator:destroy() - - self.CleanUpList[Event.IniDCSUnitName] = {} - self.CleanUpList[Event.IniDCSUnitName].CleanUpUnit = Event.IniDCSUnit - self.CleanUpList[Event.IniDCSUnitName].CleanUpGroup = Event.IniDCSGroup - self.CleanUpList[Event.IniDCSUnitName].CleanUpGroupName = Event.IniDCSGroupName - self.CleanUpList[Event.IniDCSUnitName].CleanUpUnitName = Event.IniDCSUnitName - -end - ---- Detects if a unit shoots a missile. --- If this occurs within one of the zones, then the weapon used must be destroyed. --- @param #CLEANUP self --- @param Dcs.DCSTypes#Event event -function CLEANUP:_EventShot( Event ) - self:F( { Event } ) - - -- Test if the missile was fired within one of the CLEANUP.ZoneNames. - local CurrentLandingZoneID = 0 - CurrentLandingZoneID = routines.IsUnitInZones( Event.IniDCSUnit, self.ZoneNames ) - if ( CurrentLandingZoneID ) then - -- Okay, the missile was fired within the CLEANUP.ZoneNames, destroy the fired weapon. - --_SEADmissile:destroy() - SCHEDULER:New( self, CLEANUP._DestroyMissile, { Event.Weapon }, 0.1 ) - end -end - - ---- Detects if the Unit has an S_EVENT_HIT within the given ZoneNames. If this is the case, destroy the unit. --- @param #CLEANUP self --- @param Dcs.DCSTypes#Event event -function CLEANUP:_EventHitCleanUp( Event ) - self:F( { Event } ) - - if Event.IniDCSUnit then - if routines.IsUnitInZones( Event.IniDCSUnit, self.ZoneNames ) ~= nil then - self:T( { "Life: ", Event.IniDCSUnitName, ' = ', Event.IniDCSUnit:getLife(), "/", Event.IniDCSUnit:getLife0() } ) - if Event.IniDCSUnit:getLife() < Event.IniDCSUnit:getLife0() then - self:T( "CleanUp: Destroy: " .. Event.IniDCSUnitName ) - SCHEDULER:New( self, CLEANUP._DestroyUnit, { Event.IniDCSUnit }, 0.1 ) - end - end - end - - if Event.TgtDCSUnit then - if routines.IsUnitInZones( Event.TgtDCSUnit, self.ZoneNames ) ~= nil then - self:T( { "Life: ", Event.TgtDCSUnitName, ' = ', Event.TgtDCSUnit:getLife(), "/", Event.TgtDCSUnit:getLife0() } ) - if Event.TgtDCSUnit:getLife() < Event.TgtDCSUnit:getLife0() then - self:T( "CleanUp: Destroy: " .. Event.TgtDCSUnitName ) - SCHEDULER:New( self, CLEANUP._DestroyUnit, { Event.TgtDCSUnit }, 0.1 ) - end - end - end -end - ---- Add the @{DCSWrapper.Unit#Unit} to the CleanUpList for CleanUp. -function CLEANUP:_AddForCleanUp( CleanUpUnit, CleanUpUnitName ) - self:F( { CleanUpUnit, CleanUpUnitName } ) - - self.CleanUpList[CleanUpUnitName] = {} - self.CleanUpList[CleanUpUnitName].CleanUpUnit = CleanUpUnit - self.CleanUpList[CleanUpUnitName].CleanUpUnitName = CleanUpUnitName - self.CleanUpList[CleanUpUnitName].CleanUpGroup = Unit.getGroup(CleanUpUnit) - self.CleanUpList[CleanUpUnitName].CleanUpGroupName = Unit.getGroup(CleanUpUnit):getName() - self.CleanUpList[CleanUpUnitName].CleanUpTime = timer.getTime() - self.CleanUpList[CleanUpUnitName].CleanUpMoved = false - - self:T( { "CleanUp: Add to CleanUpList: ", Unit.getGroup(CleanUpUnit):getName(), CleanUpUnitName } ) - -end - ---- Detects if the Unit has an S_EVENT_ENGINE_SHUTDOWN or an S_EVENT_HIT within the given ZoneNames. If this is the case, add the Group to the CLEANUP List. --- @param #CLEANUP self --- @param Dcs.DCSTypes#Event event -function CLEANUP:_EventAddForCleanUp( Event ) - - if Event.IniDCSUnit then - if self.CleanUpList[Event.IniDCSUnitName] == nil then - if routines.IsUnitInZones( Event.IniDCSUnit, self.ZoneNames ) ~= nil then - self:_AddForCleanUp( Event.IniDCSUnit, Event.IniDCSUnitName ) - end - end - end - - if Event.TgtDCSUnit then - if self.CleanUpList[Event.TgtDCSUnitName] == nil then - if routines.IsUnitInZones( Event.TgtDCSUnit, self.ZoneNames ) ~= nil then - self:_AddForCleanUp( Event.TgtDCSUnit, Event.TgtDCSUnitName ) - end - end - end - -end - -local CleanUpSurfaceTypeText = { - "LAND", - "SHALLOW_WATER", - "WATER", - "ROAD", - "RUNWAY" - } - ---- At the defined time interval, CleanUp the Groups within the CleanUpList. --- @param #CLEANUP self -function CLEANUP:_CleanUpScheduler() - self:F( { "CleanUp Scheduler" } ) - - local CleanUpCount = 0 - for CleanUpUnitName, UnitData in pairs( self.CleanUpList ) do - CleanUpCount = CleanUpCount + 1 - - self:T( { CleanUpUnitName, UnitData } ) - local CleanUpUnit = Unit.getByName(UnitData.CleanUpUnitName) - local CleanUpGroupName = UnitData.CleanUpGroupName - local CleanUpUnitName = UnitData.CleanUpUnitName - if CleanUpUnit then - self:T( { "CleanUp Scheduler", "Checking:", CleanUpUnitName } ) - if _DATABASE:GetStatusGroup( CleanUpGroupName ) ~= "ReSpawn" then - local CleanUpUnitVec3 = CleanUpUnit:getPoint() - --self:T( CleanUpUnitVec3 ) - local CleanUpUnitVec2 = {} - CleanUpUnitVec2.x = CleanUpUnitVec3.x - CleanUpUnitVec2.y = CleanUpUnitVec3.z - --self:T( CleanUpUnitVec2 ) - local CleanUpSurfaceType = land.getSurfaceType(CleanUpUnitVec2) - --self:T( CleanUpSurfaceType ) - - if CleanUpUnit and CleanUpUnit:getLife() <= CleanUpUnit:getLife0() * 0.95 then - if CleanUpSurfaceType == land.SurfaceType.RUNWAY then - if CleanUpUnit:inAir() then - local CleanUpLandHeight = land.getHeight(CleanUpUnitVec2) - local CleanUpUnitHeight = CleanUpUnitVec3.y - CleanUpLandHeight - self:T( { "CleanUp Scheduler", "Height = " .. CleanUpUnitHeight } ) - if CleanUpUnitHeight < 30 then - self:T( { "CleanUp Scheduler", "Destroy " .. CleanUpUnitName .. " because below safe height and damaged." } ) - self:_DestroyUnit(CleanUpUnit, CleanUpUnitName) - end - else - self:T( { "CleanUp Scheduler", "Destroy " .. CleanUpUnitName .. " because on runway and damaged." } ) - self:_DestroyUnit(CleanUpUnit, CleanUpUnitName) - end - end - end - -- Clean Units which are waiting for a very long time in the CleanUpZone. - if CleanUpUnit then - local CleanUpUnitVelocity = CleanUpUnit:getVelocity() - local CleanUpUnitVelocityTotal = math.abs(CleanUpUnitVelocity.x) + math.abs(CleanUpUnitVelocity.y) + math.abs(CleanUpUnitVelocity.z) - if CleanUpUnitVelocityTotal < 1 then - if UnitData.CleanUpMoved then - if UnitData.CleanUpTime + 180 <= timer.getTime() then - self:T( { "CleanUp Scheduler", "Destroy due to not moving anymore " .. CleanUpUnitName } ) - self:_DestroyUnit(CleanUpUnit, CleanUpUnitName) - end - end - else - UnitData.CleanUpTime = timer.getTime() - UnitData.CleanUpMoved = true - end - end - - else - -- Do nothing ... - self.CleanUpList[CleanUpUnitName] = nil -- Not anymore in the DCSRTE - end - else - self:T( "CleanUp: Group " .. CleanUpUnitName .. " cannot be found in DCS RTE, removing ..." ) - self.CleanUpList[CleanUpUnitName] = nil -- Not anymore in the DCSRTE - end - end - self:T(CleanUpCount) - - return true -end - ---- Single-Player:**Yes** / Multi-Player:**Yes** / AI:**Yes** / Human:**No** / Types:**All** -- --- **Spawn groups of units dynamically in your missions.** --- --- ![Banner Image](..\Presentations\SPAWN\SPAWN.JPG) --- --- === --- --- # 1) @{#SPAWN} class, extends @{Base#BASE} --- --- The @{#SPAWN} class allows to spawn dynamically new groups, based on pre-defined initialization settings, modifying the behaviour when groups are spawned. --- For each group to be spawned, within the mission editor, a group has to be created with the "late activation flag" set. We call this group the *"Spawn Template"* of the SPAWN object. --- A reference to this Spawn Template needs to be provided when constructing the SPAWN object, by indicating the name of the group within the mission editor in the constructor methods. --- --- Within the SPAWN object, there is an internal index that keeps track of which group from the internal group list was spawned. --- When new groups get spawned by using the SPAWN methods (see below), it will be validated whether the Limits (@{#SPAWN.Limit}) of the SPAWN object are not reached. --- When all is valid, a new group will be created by the spawning methods, and the internal index will be increased with 1. --- --- Regarding the name of new spawned groups, a _SpawnPrefix_ will be assigned for each new group created. --- If you want to have the Spawn Template name to be used as the _SpawnPrefix_ name, use the @{#SPAWN.New} constructor. --- However, when the @{#SPAWN.NewWithAlias} constructor was used, the Alias name will define the _SpawnPrefix_ name. --- Groups will follow the following naming structure when spawned at run-time: --- --- 1. Spawned groups will have the name _SpawnPrefix_#ggg, where ggg is a counter from 0 to 999. --- 2. Spawned units will have the name _SpawnPrefix_#ggg-uu, where uu is a counter from 0 to 99 for each new spawned unit belonging to the group. --- --- Some additional notes that need to be remembered: --- --- * Templates are actually groups defined within the mission editor, with the flag "Late Activation" set. As such, these groups are never used within the mission, but are used by the @{#SPAWN} module. --- * It is important to defined BEFORE you spawn new groups, a proper initialization of the SPAWN instance is done with the options you want to use. --- * When designing a mission, NEVER name groups using a "#" within the name of the group Spawn Template(s), or the SPAWN module logic won't work anymore. --- --- ## 1.1) SPAWN construction methods --- --- Create a new SPAWN object with the @{#SPAWN.New}() or the @{#SPAWN.NewWithAlias}() methods: --- --- * @{#SPAWN.New}(): Creates a new SPAWN object taking the name of the group that represents the GROUP Template (definition). --- * @{#SPAWN.NewWithAlias}(): Creates a new SPAWN object taking the name of the group that represents the GROUP Template (definition), and gives each spawned @{Group} an different name. --- --- It is important to understand how the SPAWN class works internally. The SPAWN object created will contain internally a list of groups that will be spawned and that are already spawned. --- The initialization methods will modify this list of groups so that when a group gets spawned, ALL information is already prepared when spawning. This is done for performance reasons. --- So in principle, the group list will contain all parameters and configurations after initialization, and when groups get actually spawned, this spawning can be done quickly and efficient. --- --- ## 1.2) SPAWN initialization methods --- --- A spawn object will behave differently based on the usage of **initialization** methods, which all start with the **Init** prefix: --- --- * @{#SPAWN.InitKeepUnitNames}(): Keeps the unit names as defined within the mission editor, but note that anything after a # mark is ignored, and any spaces before and after the resulting name are removed. IMPORTANT! This method MUST be the first used after :New !!! --- * @{#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.InitRandomizePosition}(): Randomizes the position of @{Group}s that are spawned within a **radius band**, given an Outer and Inner radius, from the point that the spawn happens. --- * @{#SPAWN.InitRandomizeUnits}(): Randomizes the @{Unit}s in the @{Group} that is spawned within a **radius band**, given an Outer and Inner radius. --- * @{#SPAWN.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-03-14: SPAWN:**InitKeepUnitNames()** added. --- 2017-03-14: SPAWN:**InitRandomizePosition( RandomizePosition, OuterRadious, InnerRadius )** added. --- --- 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.SpawnInitLimit = false -- By default, no InitLimit - self.SpawnMaxUnitsAlive = 0 -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time. - self.SpawnMaxGroups = 0 -- The maximum amount of groups that can be spawned. - self.SpawnRandomize = false -- Sets the randomization flag of new Spawned units to false. - self.SpawnVisible = false -- Flag that indicates if all the Groups of the SpawnGroup need to be visible when Spawned. - self.AIOnOff = true -- The AI is on by default when spawning a group. - self.SpawnUnControlled = false - self.SpawnInitKeepUnitNames = false -- Overwrite unit names by default with group name. - - self.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 - - self:SetEventPriority( 5 ) - - 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.SpawnInitLimit = false -- By default, no InitLimit - self.SpawnMaxUnitsAlive = 0 -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time. - self.SpawnMaxGroups = 0 -- The maximum amount of groups that can be spawned. - self.SpawnRandomize = false -- Sets the randomization flag of new Spawned units to false. - self.SpawnVisible = false -- Flag that indicates if all the Groups of the SpawnGroup need to be visible when Spawned. - self.AIOnOff = true -- The AI is on by default when spawning a group. - self.SpawnUnControlled = false - self.SpawnInitKeepUnitNames = false -- Overwrite unit names by default with group name. - - self.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 - - self:SetEventPriority( 5 ) - - 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.SpawnInitLimit = true - 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 - ---- Keeps the unit names as defined within the mission editor, --- but note that anything after a # mark is ignored, --- and any spaces before and after the resulting name are removed. --- IMPORTANT! This method MUST be the first used after :New !!! --- @param #SPAWN self --- @return #SPAWN self -function SPAWN:InitKeepUnitNames() - self:F( ) - - self.SpawnInitKeepUnitNames = true - - 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 position of @{Group}s that are spawned within a **radius band**, given an Outer and Inner radius, from the point that the spawn happens. --- @param #SPAWN self --- @param #boolean RandomizePosition If true, SPAWN will perform the randomization of the @{Group}s position between a given outer and inner radius. --- @param Dcs.DCSTypes#Distance OuterRadius (optional) The outer radius in meters where the new group will be spawned. --- @param Dcs.DCSTypes#Distance InnerRadius (optional) The inner radius in meters where the new group will NOT be spawned. --- @return #SPAWN -function SPAWN:InitRandomizePosition( RandomizePosition, OuterRadius, InnerRadius ) - self:F( { self.SpawnTemplatePrefix, RandomizePosition, OuterRadius, InnerRadius } ) - - self.SpawnRandomizePosition = RandomizePosition or false - self.SpawnRandomizePositionOuterRadius = OuterRadius or 0 - self.SpawnRandomizePositionInnerRadius = InnerRadius or 0 - - 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 - - self:HandleEvent( EVENTS.Birth, self._OnBirth ) - self:HandleEvent( EVENTS.Dead, self._OnDeadOrCrash ) - self:HandleEvent( EVENTS.Crash, self._OnDeadOrCrash ) - if self.Repeat then - self:HandleEvent( EVENTS.Takeoff, self._OnTakeOff ) - self:HandleEvent( EVENTS.Land, self._OnLand ) - end - if self.RepeatOnEngineShutDown then - self:HandleEvent( EVENTS.EngineShutdown, self._OnEngineShutDown ) - 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 RandomizePosition, then Randomize the formation in the zone band, keeping the template. - if self.SpawnRandomizePosition then - local RandomVec2 = PointVec3:GetRandomVec2InRadius( self.SpawnRandomizePositionOuterRadius, self.SpawnRandomizePositionInnerRadius ) - local CurrentX = SpawnTemplate.units[1].x - local CurrentY = SpawnTemplate.units[1].y - SpawnTemplate.x = RandomVec2.x - SpawnTemplate.y = RandomVec2.y - for UnitID = 1, #SpawnTemplate.units do - SpawnTemplate.units[UnitID].x = SpawnTemplate.units[UnitID].x + ( RandomVec2.x - CurrentX ) - SpawnTemplate.units[UnitID].y = SpawnTemplate.units[UnitID].y + ( RandomVec2.y - CurrentY ) - self:T( 'SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) - end - end - - -- If RandomizeUnits, then Randomize the formation at the start point. - if self.SpawnRandomizeUnits then - for UnitID = 1, #SpawnTemplate.units do - local RandomVec2 = PointVec3:GetRandomVec2InRadius( self.SpawnOuterRadius, self.SpawnInnerRadius ) - SpawnTemplate.units[UnitID].x = RandomVec2.x - SpawnTemplate.units[UnitID].y = RandomVec2.y - self:T( 'SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) - end - end - - if SpawnTemplate.CategoryID == Group.Category.HELICOPTER or SpawnTemplate.CategoryID == Group.Category.AIRPLANE then - if SpawnTemplate.route.points[1].type == "TakeOffParking" then - SpawnTemplate.uncontrolled = self.SpawnUnControlled - end - end - end - - self:HandleEvent( EVENTS.Birth, self._OnBirth ) - self:HandleEvent( EVENTS.Dead, self._OnDeadOrCrash ) - self:HandleEvent( EVENTS.Crash, self._OnDeadOrCrash ) - if self.Repeat then - self:HandleEvent( EVENTS.Takeoff, self._OnTakeOff ) - self:HandleEvent( EVENTS.Land, self._OnLand ) - end - if self.RepeatOnEngineShutDown then - self:HandleEvent( EVENTS.EngineShutdown, self._OnEngineShutDown ) - end - - 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 - - self:T3( SpawnTemplate.name ) - - -- 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 - - ---- 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:_GetPrefixFromGroup( SpawnGroup ) - self:F3( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnGroup } ) - - local GroupName = SpawnGroup:GetName() - if GroupName then - local SpawnPrefix = string.match( GroupName, ".*#" ) - if SpawnPrefix then - SpawnPrefix = SpawnPrefix:sub( 1, -2 ) - end - return SpawnPrefix - end - - return nil -end - - ---- Get the index from a given group. --- The function will search the name of the group for a #, and will return the number behind the #-mark. -function SPAWN:GetSpawnIndexFromGroup( SpawnGroup ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnGroup } ) - - local IndexString = string.match( SpawnGroup:GetName(), "#(%d*)$" ):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. --- @param #SPAWN self -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 self.SpawnInitKeepUnitNames == false then - for UnitID = 1, #SpawnTemplate.units do - SpawnTemplate.units[UnitID].name = string.format( SpawnTemplate.name .. '-%02d', UnitID ) - SpawnTemplate.units[UnitID].unitId = nil - end - else - for UnitID = 1, #SpawnTemplate.units do - local UnitPrefix, Rest = string.match( SpawnTemplate.units[UnitID].name, "^([^#]+)#?" ):gsub( "^%s*(.-)%s*$", "%1" ) - self:T( { UnitPrefix, Rest } ) - - SpawnTemplate.units[UnitID].name = string.format( '%s#%03d-%02d', UnitPrefix, SpawnIndex, UnitID ) - SpawnTemplate.units[UnitID].unitId = nil - end - end - - self:T3( { "Template:", SpawnTemplate } ) - return SpawnTemplate - -end - ---- Private method randomizing the routes. --- @param #SPAWN self --- @param #number SpawnIndex The index of the group to be spawned. --- @return #SPAWN -function SPAWN:_RandomizeRoute( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeRoute, self.SpawnRandomizeRouteStartPoint, self.SpawnRandomizeRouteEndPoint, self.SpawnRandomizeRouteRadius } ) - - if self.SpawnRandomizeRoute then - local SpawnTemplate = self.SpawnGroups[SpawnIndex].SpawnTemplate - local RouteCount = #SpawnTemplate.route.points - - for t = self.SpawnRandomizeRouteStartPoint + 1, ( RouteCount - self.SpawnRandomizeRouteEndPoint ) do - - SpawnTemplate.route.points[t].x = SpawnTemplate.route.points[t].x + math.random( self.SpawnRandomizeRouteRadius * -1, self.SpawnRandomizeRouteRadius ) - SpawnTemplate.route.points[t].y = SpawnTemplate.route.points[t].y + math.random( self.SpawnRandomizeRouteRadius * -1, self.SpawnRandomizeRouteRadius ) - - -- Manage randomization of altitude for airborne units ... - if SpawnTemplate.CategoryID == Group.Category.AIRPLANE or SpawnTemplate.CategoryID == Group.Category.HELICOPTER then - if SpawnTemplate.route.points[t].alt and self.SpawnRandomizeRouteHeight then - SpawnTemplate.route.points[t].alt = SpawnTemplate.route.points[t].alt + math.random( 1, self.SpawnRandomizeRouteHeight ) - end - else - SpawnTemplate.route.points[t].alt = nil - end - - self:T( 'SpawnTemplate.route.points[' .. t .. '].x = ' .. SpawnTemplate.route.points[t].x .. ', SpawnTemplate.route.points[' .. t .. '].y = ' .. SpawnTemplate.route.points[t].y ) - end - end - - self:_RandomizeZones( SpawnIndex ) - - return self -end - ---- Private method that randomizes the template of the group. --- @param #SPAWN self --- @param #number SpawnIndex --- @return #SPAWN self -function SPAWN:_RandomizeTemplate( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeTemplate } ) - - if self.SpawnRandomizeTemplate then - self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix = self.SpawnTemplatePrefixTable[ math.random( 1, #self.SpawnTemplatePrefixTable ) ] - self.SpawnGroups[SpawnIndex].SpawnTemplate = self:_Prepare( self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix, SpawnIndex ) - self.SpawnGroups[SpawnIndex].SpawnTemplate.route = routines.utils.deepCopy( self.SpawnTemplate.route ) - self.SpawnGroups[SpawnIndex].SpawnTemplate.x = self.SpawnTemplate.x - self.SpawnGroups[SpawnIndex].SpawnTemplate.y = self.SpawnTemplate.y - self.SpawnGroups[SpawnIndex].SpawnTemplate.start_time = self.SpawnTemplate.start_time - local OldX = self.SpawnGroups[SpawnIndex].SpawnTemplate.units[1].x - local OldY = self.SpawnGroups[SpawnIndex].SpawnTemplate.units[1].y - for UnitID = 1, #self.SpawnGroups[SpawnIndex].SpawnTemplate.units do - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].heading = self.SpawnTemplate.units[1].heading - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].x = self.SpawnTemplate.units[1].x + ( self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].x - OldX ) - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].y = self.SpawnTemplate.units[1].y + ( self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].y - OldY ) - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].alt = self.SpawnTemplate.units[1].alt - end - end - - self:_RandomizeRoute( SpawnIndex ) - - return self -end - ---- Private method that randomizes the @{Zone}s where the Group will be spawned. --- @param #SPAWN self --- @param #number SpawnIndex --- @return #SPAWN self -function SPAWN:_RandomizeZones( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeZones } ) - - if self.SpawnRandomizeZones then - local SpawnZone = nil -- Core.Zone#ZONE_BASE - while not SpawnZone do - self:T( { SpawnZoneTableCount = #self.SpawnZoneTable, self.SpawnZoneTable } ) - local ZoneID = math.random( #self.SpawnZoneTable ) - self:T( ZoneID ) - SpawnZone = self.SpawnZoneTable[ ZoneID ]:GetZoneMaybe() - end - - self:T( "Preparing Spawn in Zone", SpawnZone:GetName() ) - - local SpawnVec2 = SpawnZone:GetRandomVec2() - - self:T( { SpawnVec2 = SpawnVec2 } ) - - local SpawnTemplate = self.SpawnGroups[SpawnIndex].SpawnTemplate - - self:T( { Route = SpawnTemplate.route } ) - - for UnitID = 1, #SpawnTemplate.units do - local UnitTemplate = SpawnTemplate.units[UnitID] - self:T( 'Before Translation SpawnTemplate.units['..UnitID..'].x = ' .. UnitTemplate.x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. UnitTemplate.y ) - local SX = UnitTemplate.x - local SY = UnitTemplate.y - local BX = SpawnTemplate.route.points[1].x - local BY = SpawnTemplate.route.points[1].y - local TX = SpawnVec2.x + ( SX - BX ) - local TY = SpawnVec2.y + ( SY - BY ) - UnitTemplate.x = TX - UnitTemplate.y = TY - -- TODO: Manage altitude based on landheight... - --SpawnTemplate.units[UnitID].alt = SpawnVec2: - self:T( 'After Translation SpawnTemplate.units['..UnitID..'].x = ' .. UnitTemplate.x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. UnitTemplate.y ) - end - SpawnTemplate.x = SpawnVec2.x - SpawnTemplate.y = SpawnVec2.y - SpawnTemplate.route.points[1].x = SpawnVec2.x - SpawnTemplate.route.points[1].y = SpawnVec2.y - end - - return self - -end - -function SPAWN:_TranslateRotate( SpawnIndex, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle } ) - - -- Translate - local TranslatedX = SpawnX - local TranslatedY = SpawnY - - -- Rotate - -- From Wikipedia: https://en.wikipedia.org/wiki/Rotation_matrix#Common_rotations - -- x' = x \cos \theta - y \sin \theta\ - -- y' = x \sin \theta + y \cos \theta\ - local RotatedX = - TranslatedX * math.cos( math.rad( SpawnAngle ) ) - + TranslatedY * math.sin( math.rad( SpawnAngle ) ) - local RotatedY = TranslatedX * math.sin( math.rad( SpawnAngle ) ) - + TranslatedY * math.cos( math.rad( SpawnAngle ) ) - - -- Assign - self.SpawnGroups[SpawnIndex].SpawnTemplate.x = SpawnRootX - RotatedX - self.SpawnGroups[SpawnIndex].SpawnTemplate.y = SpawnRootY + RotatedY - - - local SpawnUnitCount = table.getn( self.SpawnGroups[SpawnIndex].SpawnTemplate.units ) - for u = 1, SpawnUnitCount do - - -- Translate - local TranslatedX = SpawnX - local TranslatedY = SpawnY - 10 * ( u - 1 ) - - -- Rotate - local RotatedX = - TranslatedX * math.cos( math.rad( SpawnAngle ) ) - + TranslatedY * math.sin( math.rad( SpawnAngle ) ) - local RotatedY = TranslatedX * math.sin( math.rad( SpawnAngle ) ) - + TranslatedY * math.cos( math.rad( SpawnAngle ) ) - - -- Assign - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].x = SpawnRootX - RotatedX - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].y = SpawnRootY + RotatedY - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].heading = self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].heading + math.rad( SpawnAngle ) - end - - return self -end - ---- Get the next index of the groups to be spawned. This method is complicated, as it is used at several spaces. -function SPAWN:_GetSpawnIndex( SpawnIndex ) - self:F2( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnMaxGroups, self.SpawnMaxUnitsAlive, self.AliveUnits, #self.SpawnTemplate.units } ) - - if ( self.SpawnMaxGroups == 0 ) or ( SpawnIndex <= self.SpawnMaxGroups ) then - if ( self.SpawnMaxUnitsAlive == 0 ) or ( self.AliveUnits + #self.SpawnTemplate.units <= self.SpawnMaxUnitsAlive ) or self.UnControlled == true then - if SpawnIndex and SpawnIndex >= self.SpawnCount + 1 then - self.SpawnCount = self.SpawnCount + 1 - SpawnIndex = self.SpawnCount - end - self.SpawnIndex = SpawnIndex - if not self.SpawnGroups[self.SpawnIndex] then - self:_InitializeSpawnGroups( self.SpawnIndex ) - end - else - return nil - end - else - return nil - end - - return self.SpawnIndex -end - - --- TODO Need to delete this... _DATABASE does this now ... - ---- @param #SPAWN self --- @param Core.Event#EVENTDATA EventData -function SPAWN:_OnBirth( EventData ) - self:F( self.SpawnTemplatePrefix ) - - local SpawnGroup = EventData.IniGroup - - if SpawnGroup then - local EventPrefix = self:_GetPrefixFromGroup( SpawnGroup ) - 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 - ---- Obscolete --- @todo Need to delete this... _DATABASE does this now ... - ---- @param #SPAWN self --- @param Core.Event#EVENTDATA EventData -function SPAWN:_OnDeadOrCrash( EventData ) - self:F( self.SpawnTemplatePrefix ) - - local SpawnGroup = EventData.IniGroup - - if SpawnGroup then - local EventPrefix = self:_GetPrefixFromGroup( SpawnGroup ) - self:T( { "Dead event: " .. EventPrefix } ) - 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. --- @param #SPAWN self --- @param Core.Event#EVENTDATA EventData -function SPAWN:_OnTakeOff( EventData ) - self:F( self.SpawnTemplatePrefix ) - - local SpawnGroup = EventData.IniGroup - if SpawnGroup then - local EventPrefix = self:_GetPrefixFromGroup( SpawnGroup ) - self:T( { "TakeOff event: " .. EventPrefix } ) - if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then - self:T( "self.Landed = false" ) - SpawnGroup:SetState( SpawnGroup, "Spawn_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. --- @param #SPAWN self --- @param Core.Event#EVENTDATA EventData -function SPAWN:_OnLand( EventData ) - self:F( self.SpawnTemplatePrefix ) - - local SpawnGroup = EventData.IniGroup - if SpawnGroup then - local EventPrefix = self:_GetPrefixFromGroup( SpawnGroup ) - self:T( { "Land event: " .. EventPrefix } ) - if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then - -- TODO: Check if this is the last unit of the group that lands. - SpawnGroup:SetState( SpawnGroup, "Spawn_Landed", true ) - if 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 --- @param Core.Event#EVENTDATA EventData -function SPAWN:_OnEngineShutDown( EventData ) - self:F( self.SpawnTemplatePrefix ) - - local SpawnGroup = EventData.IniGroup - if SpawnGroup then - local EventPrefix = self:_GetPrefixFromGroup( SpawnGroup ) - self:T( { "EngineShutdown event: " .. EventPrefix } ) - if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then - -- todo: test if on the runway - local Landed = SpawnGroup:GetState( SpawnGroup, "Spawn_Landed" ) - if 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 --- @extends Core.Base#BASE -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() ) -- #MOVEMENT - 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. - - self:HandleEvent( EVENTS.Birth ) - --- 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. --- @param #MOVEMENT self --- @param Core.Event#EVENTDATA self -function MOVEMENT:OnEventBirth( EventData ) - self:F( { EventData } ) - - 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 EventData.IniDCSUnit then - self:T( "Birth object : " .. EventData.IniDCSUnitName ) - if EventData.IniDCSGroup and EventData.IniDCSGroup:isExist() then - for MovePrefixID, MovePrefix in pairs( self.MovePrefixes ) do - if string.find( EventData.IniDCSUnitName, MovePrefix, 1, true ) then - self.AliveUnits = self.AliveUnits + 1 - self.MoveUnits[EventData.IniDCSUnitName] = EventData.IniDCSGroupName - self:T( self.AliveUnits ) - end - end - end - end - - EventData.IniUnit:HandleEvent( EVENTS.DEAD, self.OnDeadOrCrash ) - 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 - - self:HandleEvent( EVENTS.Shot ) - - 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 --- @param #SEAD --- @param Core.Event#EVENTDATA EventData -function SEAD:OnEventShot( EventData ) - self:F( { EventData } ) - - local SEADUnit = EventData.IniDCSUnit - local SEADUnitName = EventData.IniDCSUnitName - local SEADWeapon = EventData.Weapon -- Identify the weapon fired - local SEADWeaponName = EventData.WeaponName -- return weapon type - -- Start of the 2nd loop - self:T( "Missile Launched = " .. SEADWeaponName ) - if SEADWeaponName == "KH-58" or SEADWeaponName == "KH-25MPU" or SEADWeaponName == "AGM-88" or SEADWeaponName == "KH-31A" or SEADWeaponName == "KH-31P" then -- Check if the missile is a SEAD - local _evade = math.random (1,100) -- random number for chance of evading action - local _targetMim = EventData.Weapon:getTarget() -- Identify target - local _targetMimname = Unit.getName(_targetMim) - 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 (F10) of an active unit hosted by a player: --- --- * @{#ESCORT.MenuFollowAt}: Creates a menu to make the escort follow the client. --- * @{#ESCORT.MenuHoldAtEscortPosition}: Creates a menu to hold the escort at its current position. --- * @{#ESCORT.MenuHoldAtLeaderPosition}: Creates a menu to hold the escort at the client position. --- * @{#ESCORT.MenuScanForTargets}: Creates a menu so that the escort scans targets. --- * @{#ESCORT.MenuFlare}: Creates a menu to disperse flares. --- * @{#ESCORT.MenuSmoke}: Creates a menu to disparse smoke. --- * @{#ESCORT.MenuReportTargets}: Creates a menu so that the escort reports targets. --- * @{#ESCORT.MenuReportPosition}: Creates a menu so that the escort reports its current position from bullseye. --- * @{#ESCORT.MenuAssistedAttack: Creates a menu so that the escort supportes assisted attack from other escorts with the client. --- * @{#ESCORT.MenuROE: Creates a menu structure to set the rules of engagement of the escort. --- * @{#ESCORT.MenuEvasion: Creates a menu structure to set the evasion techniques when the escort is under threat. --- * @{#ESCORT.MenuResumeMission}: Creates a menu structure so that the escort can resume from a waypoint. --- --- --- @usage --- -- Declare a new EscortPlanes object as follows: --- --- -- 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 --- @field Functional.Detection#DETECTION_BASE Detection -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() ) -- #ESCORT - self:F( { EscortClient, EscortGroup, EscortName } ) - - self.EscortClient = EscortClient -- Wrapper.Client#CLIENT - self.EscortGroup = EscortGroup -- Wrapper.Group#GROUP - self.EscortName = EscortName - self.EscortBriefing = EscortBriefing - - self.EscortSetGroup = SET_GROUP:New() - self.EscortSetGroup:AddObject( self.EscortGroup ) - self.EscortSetGroup:Flush() - self.Detection = DETECTION_UNITS:New( self.EscortSetGroup, 15000 ) - - self.EscortGroup.Detection = self.Detection - - -- 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()].Detection = self.EscortGroup.Detection - 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, self.FollowSchedule = SCHEDULER:New( self, self._FollowScheduler, {}, 1, .5, .01 ) - self.FollowScheduler:Stop( self.FollowSchedule ) - - self.EscortMode = ESCORT.MODE.MISSION - - - return self -end - ---- Set a Detection method for the EscortClient to be reported upon. --- Detection methods are based on the derived classes from DETECTION_BASE. --- @param #ESCORT self --- @param Function.Detection#DETECTION_BASE Detection -function ESCORT:SetDetection( Detection ) - - self.Detection = Detection - self.EscortGroup.Detection = self.Detection - self.EscortClient._EscortGroups[self.EscortGroup:GetName()].Detection = self.EscortGroup.Detection - - Detection:__Start( 1 ) - -end - ---- This function is for test, it will put on the frequency of the FollowScheduler a red smoke at the direction vector calculated for the escort to fly to. --- This allows to visualize where the escort is flying to. --- @param #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, self, 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, - self, - self.EscortGroup, - Height, - 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, - self, - 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, self ) - self.EscortMenuFlareGreen = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release green flare", self.EscortMenuFlare, ESCORT._Flare, self, FLARECOLOR.Green, "Released a green flare!" ) - self.EscortMenuFlareRed = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release red flare", self.EscortMenuFlare, ESCORT._Flare, self, FLARECOLOR.Red, "Released a red flare!" ) - self.EscortMenuFlareWhite = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release white flare", self.EscortMenuFlare, ESCORT._Flare, self, FLARECOLOR.White, "Released a white flare!" ) - self.EscortMenuFlareYellow = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release yellow flare", self.EscortMenuFlare, ESCORT._Flare, self, FLARECOLOR.Yellow, "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, self ) - self.EscortMenuSmokeGreen = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release green smoke", self.EscortMenuSmoke, ESCORT._Smoke, self, SMOKECOLOR.Green, "Releasing green smoke!" ) - self.EscortMenuSmokeRed = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release red smoke", self.EscortMenuSmoke, ESCORT._Smoke, self, SMOKECOLOR.Red, "Releasing red smoke!" ) - self.EscortMenuSmokeWhite = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release white smoke", self.EscortMenuSmoke, ESCORT._Smoke, self, SMOKECOLOR.White, "Releasing white smoke!" ) - self.EscortMenuSmokeOrange = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release orange smoke", self.EscortMenuSmoke, ESCORT._Smoke, self, SMOKECOLOR.Orange, "Releasing orange smoke!" ) - self.EscortMenuSmokeBlue = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release blue smoke", self.EscortMenuSmoke, ESCORT._Smoke, self, SMOKECOLOR.Blue, "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, self ) - self.EscortMenuReportNearbyTargetsOn = MENU_CLIENT_COMMAND:New( self.EscortClient, "Report targets on", self.EscortMenuReportNearbyTargets, ESCORT._SwitchReportNearbyTargets, self, true ) - self.EscortMenuReportNearbyTargetsOff = MENU_CLIENT_COMMAND:New( self.EscortClient, "Report targets off", self.EscortMenuReportNearbyTargets, ESCORT._SwitchReportNearbyTargets, self, 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, self, self.EscortGroup:OptionROEHoldFire(), "Holding weapons!" ) - end - if self.EscortGroup:OptionROEReturnFirePossible() then - self.EscortMenuROEReturnFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Return Fire", self.EscortMenuROE, ESCORT._ROE, self, self.EscortGroup:OptionROEReturnFire(), "Returning fire!" ) - end - if self.EscortGroup:OptionROEOpenFirePossible() then - self.EscortMenuROEOpenFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Open Fire", self.EscortMenuROE, ESCORT._ROE, self, self.EscortGroup:OptionROEOpenFire(), "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, self, self.EscortGroup:OptionROEWeaponFree(), "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, self, self.EscortGroup:OptionROTNoReaction(), "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, self, self.EscortGroup:OptionROTPassiveDefense(), "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, self, self.EscortGroup:OptionROTEvadeFire(), "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, self, self.EscortGroup:OptionROTVertical(), "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( OrbitGroup, OrbitHeight, OrbitSeconds ) - - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - local OrbitUnit = OrbitGroup:GetUnit(1) -- Wrapper.Unit#UNIT - - self.FollowScheduler:Stop( self.FollowSchedule ) - - 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( Distance ) - - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - self.Distance = Distance - - 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( self.FollowSchedule ) - - EscortGroup:OptionROEHoldFire() - EscortGroup:OptionROTPassiveDefense() - - self.EscortMode = ESCORT.MODE.FOLLOW - - self.CT1 = 0 - self.GT1 = 0 - self.FollowScheduler:Start( self.FollowSchedule ) - - EscortGroup:MessageToClient( "Rejoining and Following at " .. Distance .. "!", 30, EscortClient ) -end - ---- @param #MENUPARAM MenuParam -function ESCORT:_Flare( Color, Message ) - - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - EscortGroup:GetUnit(1):Flare( Color ) - EscortGroup:MessageToClient( Message, 10, EscortClient ) -end - ---- @param #MENUPARAM MenuParam -function ESCORT:_Smoke( Color, Message ) - - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - EscortGroup:GetUnit(1):Smoke( Color ) - EscortGroup:MessageToClient( Message, 10, EscortClient ) -end - - ---- @param #MENUPARAM MenuParam -function ESCORT:_ReportNearbyTargetsNow() - - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - self:_ReportTargetsScheduler() - -end - -function ESCORT:_SwitchReportNearbyTargets( ReportTargets ) - - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - self.ReportTargets = ReportTargets - - if self.ReportTargets then - if not self.ReportTargetsScheduler then - self.ReportTargetsScheduler:Schedule( self, self._ReportTargetsScheduler, {}, 1, 30 ) - end - else - routines.removeFunction( self.ReportTargetsScheduler ) - self.ReportTargetsScheduler = nil - end -end - ---- @param #MENUPARAM MenuParam -function ESCORT:_ScanTargets( ScanDuration ) - - local EscortGroup = self.EscortGroup -- Wrapper.Group#GROUP - local EscortClient = self.EscortClient - - self.FollowScheduler:Stop( self.FollowSchedule ) - - if EscortGroup:IsHelicopter() then - EscortGroup:PushTask( - EscortGroup:TaskControlled( - EscortGroup:TaskOrbitCircle( 200, 20 ), - EscortGroup:TaskCondition( nil, nil, nil, nil, ScanDuration, nil ) - ), 1 ) - elseif EscortGroup:IsAirPlane() then - EscortGroup:PushTask( - EscortGroup:TaskControlled( - EscortGroup:TaskOrbitCircle( 1000, 500 ), - EscortGroup:TaskCondition( nil, nil, nil, nil, ScanDuration, nil ) - ), 1 ) - end - - EscortGroup:MessageToClient( "Scanning targets for " .. ScanDuration .. " seconds.", ScanDuration, EscortClient ) - - if self.EscortMode == ESCORT.MODE.FOLLOW then - self.FollowScheduler:Start( self.FollowSchedule ) - 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 #ESCORT self --- @param #number DetectedItemID -function ESCORT:_AttackTarget( DetectedItemID ) - - local EscortGroup = self.EscortGroup -- Wrapper.Group#GROUP - self:E( EscortGroup ) - - local EscortClient = self.EscortClient - - self.FollowScheduler:Stop( self.FollowSchedule ) - - if EscortGroup:IsAir() then - EscortGroup:OptionROEOpenFire() - EscortGroup:OptionROTPassiveDefense() - EscortGroup:SetState( EscortGroup, "Escort", self ) - - local DetectedSet = self.Detection:GetDetectedSet( DetectedItemID ) - - local Tasks = {} - - DetectedSet:ForEachUnit( - --- @param Wrapper.Unit#UNIT DetectedUnit - function( DetectedUnit, Tasks ) - if DetectedUnit:IsAlive() then - Tasks[#Tasks+1] = EscortGroup:TaskAttackUnit( DetectedUnit ) - end - end, Tasks - ) - - Tasks[#Tasks+1] = EscortGroup:TaskFunction( 1, 2, "_Resume", { "''" } ) - - EscortGroup:SetTask( - EscortGroup:TaskCombo( - Tasks - ), 1 - ) - - else - - local DetectedSet = self.Detection:GetDetectedSet( DetectedItemID ) - - local Tasks = {} - - DetectedSet:ForEachUnit( - --- @param Wrapper.Unit#UNIT DetectedUnit - function( DetectedUnit, Tasks ) - if DetectedUnit:IsAlive() then - Tasks[#Tasks+1] = EscortGroup:TaskFireAtPoint( DetectedUnit:GetVec2(), 50 ) - end - end, Tasks - ) - - EscortGroup:SetTask( - EscortGroup:TaskCombo( - Tasks - ), 1 - ) - - end - - EscortGroup:MessageToClient( "Engaging Designated Unit!", 10, EscortClient ) - -end - ---- --- @param #number DetectedItemID -function ESCORT:_AssistTarget( EscortGroupAttack, DetectedItemID ) - - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - self.FollowScheduler:Stop( self.FollowSchedule ) - - if EscortGroupAttack:IsAir() then - EscortGroupAttack:OptionROEOpenFire() - EscortGroupAttack:OptionROTVertical() - - local DetectedSet = self.Detection:GetDetectedSet( DetectedItemID ) - - local Tasks = {} - - DetectedSet:ForEachUnit( - --- @param Wrapper.Unit#UNIT DetectedUnit - function( DetectedUnit, Tasks ) - if DetectedUnit:IsAlive() then - Tasks[#Tasks+1] = EscortGroupAttack:TaskAttackUnit( DetectedUnit ) - end - end, Tasks - ) - - Tasks[#Tasks+1] = EscortGroupAttack:TaskOrbitCircle( 500, 350 ) - - EscortGroupAttack:SetTask( - EscortGroupAttack:TaskCombo( - Tasks - ), 1 - ) - - else - local DetectedSet = self.Detection:GetDetectedSet( DetectedItemID ) - - local Tasks = {} - - DetectedSet:ForEachUnit( - --- @param Wrapper.Unit#UNIT DetectedUnit - function( DetectedUnit, Tasks ) - if DetectedUnit:IsAlive() then - Tasks[#Tasks+1] = EscortGroupAttack:TaskFireAtPoint( DetectedUnit:GetVec2(), 50 ) - end - end, Tasks - ) - - EscortGroupAttack:SetTask( - EscortGroupAttack:TaskCombo( - Tasks - ), 1 - ) - - end - - EscortGroupAttack:MessageToClient( "Assisting with the destroying the enemy unit!", 10, EscortClient ) - -end - ---- @param #MENUPARAM MenuParam -function ESCORT:_ROE( EscortROEFunction, EscortROEMessage ) - - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - pcall( function() EscortROEFunction() end ) - EscortGroup:MessageToClient( EscortROEMessage, 10, EscortClient ) -end - ---- @param #MENUPARAM MenuParam -function ESCORT:_ROT( EscortROTFunction, EscortROTMessage ) - - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - pcall( function() EscortROTFunction() end ) - EscortGroup:MessageToClient( EscortROTMessage, 10, EscortClient ) -end - ---- @param #MENUPARAM MenuParam -function ESCORT:_ResumeMission( WayPoint ) - - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - self.FollowScheduler:Stop( self.FollowSchedule ) - - local WayPoints = EscortGroup:GetTaskRoute() - self:T( WayPoint, WayPoints ) - - for WayPointIgnore = 1, WayPoint do - table.remove( WayPoints, 1 ) - end - - SCHEDULER:New( EscortGroup, EscortGroup.SetTask, { EscortGroup:TaskRoute( WayPoints ) }, 1 ) - - EscortGroup:MessageToClient( "Resuming mission from waypoint " .. WayPoint .. ".", 10, EscortClient ) -end - ---- Registers the waypoints --- @param #ESCORT self --- @return #table -function ESCORT:RegisterRoute() - self:F() - - local EscortGroup = self.EscortGroup -- Wrapper.Group#GROUP - - local TaskPoints = EscortGroup:GetTaskRoute() - - self:T( TaskPoints ) - - return TaskPoints -end - ---- @param Functional.Escort#ESCORT self -function ESCORT:_FollowScheduler() - self:F( { self.FollowDistance } ) - - self:T( {self.EscortClient.UnitName, self.EscortGroup.GroupName } ) - if self.EscortGroup:IsAlive() and self.EscortClient:IsAlive() then - - local ClientUnit = self.EscortClient:GetClientGroupUnit() - local GroupUnit = self.EscortGroup:GetUnit( 1 ) - local FollowDistance = self.FollowDistance - - self:T( {ClientUnit.UnitName, GroupUnit.UnitName } ) - - if self.CT1 == 0 and self.GT1 == 0 then - self.CV1 = ClientUnit:GetVec3() - self:T( { "self.CV1", self.CV1 } ) - self.CT1 = timer.getTime() - self.GV1 = GroupUnit:GetVec3() - self.GT1 = timer.getTime() - else - local CT1 = self.CT1 - local CT2 = timer.getTime() - local CV1 = self.CV1 - local CV2 = ClientUnit:GetVec3() - self.CT1 = CT2 - self.CV1 = CV2 - - local CD = ( ( CV2.x - CV1.x )^2 + ( CV2.y - CV1.y )^2 + ( CV2.z - CV1.z )^2 ) ^ 0.5 - local CT = CT2 - CT1 - - local CS = ( 3600 / CT ) * ( CD / 1000 ) - - self:T2( { "Client:", CS, CD, CT, CV2, CV1, CT2, CT1 } ) - - local GT1 = self.GT1 - local GT2 = timer.getTime() - local GV1 = self.GV1 - local GV2 = GroupUnit:GetVec3() - self.GT1 = GT2 - self.GV1 = GV2 - - local GD = ( ( GV2.x - GV1.x )^2 + ( GV2.y - GV1.y )^2 + ( GV2.z - GV1.z )^2 ) ^ 0.5 - local GT = GT2 - GT1 - - local GS = ( 3600 / GT ) * ( GD / 1000 ) - - self:T2( { "Group:", GS, GD, GT, GV2, GV1, GT2, GT1 } ) - - -- Calculate the group direction vector - local GV = { x = GV2.x - CV2.x, y = GV2.y - CV2.y, z = GV2.z - CV2.z } - - -- Calculate GH2, GH2 with the same height as CV2. - local GH2 = { x = GV2.x, y = CV2.y, z = GV2.z } - - -- Calculate the angle of GV to the orthonormal plane - local alpha = math.atan2( GV.z, GV.x ) - - -- Now we calculate the intersecting vector between the circle around CV2 with radius FollowDistance and GH2. - -- From the GeoGebra model: CVI = (x(CV2) + FollowDistance cos(alpha), y(GH2) + FollowDistance sin(alpha), z(CV2)) - local CVI = { x = CV2.x + FollowDistance * math.cos(alpha), - y = GH2.y, - z = CV2.z + FollowDistance * math.sin(alpha), - } - - -- Calculate the direction vector DV of the escort group. We use CVI as the base and CV2 as the direction. - local DV = { x = CV2.x - CVI.x, y = CV2.y - CVI.y, z = CV2.z - CVI.z } - - -- We now calculate the unary direction vector DVu, so that we can multiply DVu with the speed, which is expressed in meters / s. - -- We need to calculate this vector to predict the point the escort group needs to fly to according its speed. - -- The distance of the destination point should be far enough not to have the aircraft starting to swipe left to right... - local DVu = { x = DV.x / FollowDistance, y = DV.y / FollowDistance, z = DV.z / FollowDistance } - - -- Now we can calculate the group destination vector GDV. - local GDV = { x = DVu.x * CS * 8 + CVI.x, y = CVI.y, z = DVu.z * CS * 8 + CVI.z } - - if self.SmokeDirectionVector == true then - trigger.action.smoke( GDV, trigger.smokeColor.Red ) - end - - self:T2( { "CV2:", CV2 } ) - self:T2( { "CVI:", CVI } ) - self:T2( { "GDV:", GDV } ) - - -- Measure distance between client and group - local CatchUpDistance = ( ( GDV.x - GV2.x )^2 + ( GDV.y - GV2.y )^2 + ( GDV.z - GV2.z )^2 ) ^ 0.5 - - -- The calculation of the Speed would simulate that the group would take 30 seconds to overcome - -- the requested Distance). - local Time = 10 - local CatchUpSpeed = ( CatchUpDistance - ( CS * 8.4 ) ) / Time - - local Speed = CS + CatchUpSpeed - if Speed < 0 then - Speed = 0 - end - - self:T( { "Client Speed, Escort Speed, Speed, FollowDistance, Time:", CS, GS, Speed, FollowDistance, Time } ) - - -- Now route the escort to the desired point with the desired speed. - self.EscortGroup:RouteToVec3( GDV, Speed / 3.6 ) -- DCS models speed in Mps (Miles per second) - end - - return true - end - - return false -end - - ---- Report Targets Scheduler. --- @param #ESCORT self -function ESCORT:_ReportTargetsScheduler() - self:F( self.EscortGroup:GetName() ) - - if self.EscortGroup:IsAlive() and self.EscortClient:IsAlive() then - - if true then - - local EscortGroupName = self.EscortGroup:GetName() - - self.EscortMenuAttackNearbyTargets:RemoveSubMenus() - - if self.EscortMenuTargetAssistance then - self.EscortMenuTargetAssistance:RemoveSubMenus() - end - - local DetectedItems = self.Detection:GetDetectedItems() - self:E( DetectedItems ) - - local DetectedTargets = false - - local DetectedMsgs = {} - - for ClientEscortGroupName, EscortGroupData in pairs( self.EscortClient._EscortGroups ) do - - local ClientEscortTargets = EscortGroupData.Detection - - for DetectedItemID, DetectedItem in ipairs( DetectedItems ) do - self:E( { DetectedItemID, DetectedItem } ) - -- Remove the sub menus of the Attack menu of the Escort for the EscortGroup. - - local DetectedItemReportSummary = self.Detection:DetectedItemReportSummary( DetectedItemID ) - - if ClientEscortGroupName == EscortGroupName then - - DetectedMsgs[#DetectedMsgs+1] = DetectedItemReportSummary - - MENU_CLIENT_COMMAND:New( self.EscortClient, - DetectedItemReportSummary, - self.EscortMenuAttackNearbyTargets, - ESCORT._AttackTarget, - self, - DetectedItemID - ) - else - if self.EscortMenuTargetAssistance then - - self:T( DetectedItemReportSummary ) - local MenuTargetAssistance = MENU_CLIENT:New( self.EscortClient, EscortGroupData.EscortName, self.EscortMenuTargetAssistance ) - MENU_CLIENT_COMMAND:New( self.EscortClient, - DetectedItemReportSummary, - MenuTargetAssistance, - ESCORT._AssistTarget, - self, - EscortGroupData.EscortGroup, - DetectedItemID - ) - end - end - - DetectedTargets = true - - end - end - self:E( DetectedMsgs ) - if DetectedTargets then - self.EscortGroup:MessageToClient( "Detected targets:\n" .. table.concat( DetectedMsgs, "\n" ), 20, self.EscortClient ) - else - self.EscortGroup:MessageToClient( "No targets detected.", 10, self.EscortClient ) - end - - return true - else --- 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, --- self, --- EscortGroupData.EscortGroup, --- 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 - 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 - - self:HandleEvent( EVENTS.Shot ) - - 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 EventData -function MISSILETRAINER:OnEventShot( EVentData ) - self:F( { EVentData } ) - - local TrainerSourceDCSUnit = EVentData.IniDCSUnit - local TrainerSourceDCSUnitName = EVentData.IniDCSUnitName - local TrainerWeapon = EVentData.Weapon -- Identify the weapon fired - local TrainerWeaponName = EVentData.WeaponName -- return weapon type - - self:T( "Missile Launched = " .. TrainerWeaponName ) - - local TrainerTargetDCSUnit = TrainerWeapon:getTarget() -- Identify target - if TrainerTargetDCSUnit then - local TrainerTargetDCSUnitName = Unit.getName( TrainerTargetDCSUnit ) - local TrainerTargetSkill = _DATABASE.Templates.Units[TrainerTargetDCSUnitName].Template.skill - - self:T(TrainerTargetDCSUnitName ) - - local Client = self.DBClients:FindClient( TrainerTargetDCSUnitName ) - if Client then - - local TrainerSourceUnit = UNIT:Find( TrainerSourceDCSUnit ) - local TrainerTargetUnit = UNIT:Find( TrainerTargetDCSUnit ) - - if self.MessagesOnOff == true and self.AlertsLaunchesOnOff == true then - - local Message = MESSAGE:New( - string.format( "%s launched a %s", - TrainerSourceUnit:GetTypeName(), - TrainerWeaponName - ) .. self:_AddRange( Client, TrainerWeapon ) .. self:_AddBearing( Client, TrainerWeapon ), 5, "Launch Alert" ) - - if self.AlertsToAll then - Message:ToAll() - else - Message:ToClient( Client ) - end - end - - local ClientID = Client:GetID() - self:T( ClientID ) - local MissileData = {} - MissileData.TrainerSourceUnit = TrainerSourceUnit - MissileData.TrainerWeapon = TrainerWeapon - MissileData.TrainerTargetUnit = TrainerTargetUnit - MissileData.TrainerWeaponTypeName = TrainerWeapon:getTypeName() - MissileData.TrainerWeaponLaunched = true - table.insert( self.TrackingMissiles[ClientID].MissileData, MissileData ) - --self:T( self.TrackingMissiles ) - end - else - -- TODO: some weapons don't know the target unit... Need to develop a workaround for this. - if ( TrainerWeapon:getTypeName() == "9M311" ) then - SCHEDULER:New( TrainerWeapon, TrainerWeapon.destroy, {}, 1 ) - else - end - end -end - -function MISSILETRAINER:_AddRange( Client, TrainerWeapon ) - - local RangeText = "" - - if self.DetailsRangeOnOff then - - local PositionMissile = TrainerWeapon:getPoint() - local TargetVec3 = Client:GetVec3() - - local Range = ( ( PositionMissile.x - TargetVec3.x )^2 + - ( PositionMissile.y - TargetVec3.y )^2 + - ( PositionMissile.z - TargetVec3.z )^2 - ) ^ 0.5 / 1000 - - RangeText = string.format( ", at %4.2fkm", Range ) - end - - return RangeText -end - -function MISSILETRAINER:_AddBearing( Client, TrainerWeapon ) - - local BearingText = "" - - if self.DetailsBearingOnOff then - - local PositionMissile = TrainerWeapon:getPoint() - local TargetVec3 = Client:GetVec3() - - self:T2( { TargetVec3, PositionMissile }) - - local DirectionVector = { x = PositionMissile.x - TargetVec3.x, y = PositionMissile.y - TargetVec3.y, z = PositionMissile.z - TargetVec3.z } - local DirectionRadians = math.atan2( DirectionVector.z, DirectionVector.x ) - --DirectionRadians = DirectionRadians + routines.getNorthCorrection( PositionTarget ) - if DirectionRadians < 0 then - DirectionRadians = DirectionRadians + 2 * math.pi - end - local DirectionDegrees = DirectionRadians * 180 / math.pi - - BearingText = string.format( ", %d degrees", DirectionDegrees ) - end - - return BearingText -end - - -function MISSILETRAINER:_TrackMissiles() - self:F2() - - - local ShowMessages = false - if self.MessagesOnOff and self.MessageLastTime + self.TrackingFrequency <= timer.getTime() then - self.MessageLastTime = timer.getTime() - ShowMessages = true - end - - -- ALERTS PART - - -- Loop for all Player Clients to check the alerts and deletion of missiles. - for ClientDataID, ClientData in pairs( self.TrackingMissiles ) do - - local Client = ClientData.Client - self:T2( { Client:GetName() } ) - - for MissileDataID, MissileData in pairs( ClientData.MissileData ) do - self:T3( MissileDataID ) - - local TrainerSourceUnit = MissileData.TrainerSourceUnit - local TrainerWeapon = MissileData.TrainerWeapon - local TrainerTargetUnit = MissileData.TrainerTargetUnit - local TrainerWeaponTypeName = MissileData.TrainerWeaponTypeName - local TrainerWeaponLaunched = MissileData.TrainerWeaponLaunched - - if Client and Client:IsAlive() and TrainerSourceUnit and TrainerSourceUnit:IsAlive() and TrainerWeapon and TrainerWeapon:isExist() and TrainerTargetUnit and TrainerTargetUnit:IsAlive() then - local PositionMissile = TrainerWeapon:getPosition().p - local TargetVec3 = Client:GetVec3() - - local Distance = ( ( PositionMissile.x - TargetVec3.x )^2 + - ( PositionMissile.y - TargetVec3.y )^2 + - ( PositionMissile.z - TargetVec3.z )^2 - ) ^ 0.5 / 1000 - - if Distance <= self.Distance then - -- Hit alert - TrainerWeapon:destroy() - if self.MessagesOnOff == true and self.AlertsHitsOnOff == true then - - self:T( "killed" ) - - local Message = MESSAGE:New( - string.format( "%s launched by %s killed %s", - TrainerWeapon:getTypeName(), - TrainerSourceUnit:GetTypeName(), - TrainerTargetUnit:GetPlayerName() - ), 15, "Hit Alert" ) - - if self.AlertsToAll == true then - Message:ToAll() - else - Message:ToClient( Client ) - end - - MissileData = nil - table.remove( ClientData.MissileData, MissileDataID ) - self:T(ClientData.MissileData) - end - end - else - if not ( TrainerWeapon and TrainerWeapon:isExist() ) then - if self.MessagesOnOff == true and self.AlertsLaunchesOnOff == true then - -- Weapon does not exist anymore. Delete from Table - local Message = MESSAGE:New( - string.format( "%s launched by %s self destructed!", - TrainerWeaponTypeName, - TrainerSourceUnit:GetTypeName() - ), 5, "Tracking" ) - - if self.AlertsToAll == true then - Message:ToAll() - else - Message:ToClient( Client ) - end - end - MissileData = nil - table.remove( ClientData.MissileData, MissileDataID ) - self:T( ClientData.MissileData ) - end - end - end - end - - if ShowMessages == true and self.MessagesOnOff == true and self.TrackingOnOff == true then -- Only do this when tracking information needs to be displayed. - - -- TRACKING PART - - -- For the current client, the missile range and bearing details are displayed To the Player Client. - -- For the other clients, the missile range and bearing details are displayed To the other Player Clients. - -- To achieve this, a cross loop is done for each Player Client <-> Other Player Client missile information. - - -- Main Player Client loop - for ClientDataID, ClientData in pairs( self.TrackingMissiles ) do - - local Client = ClientData.Client - self:T2( { Client:GetName() } ) - - - ClientData.MessageToClient = "" - ClientData.MessageToAll = "" - - -- Other Players Client loop - for TrackingDataID, TrackingData in pairs( self.TrackingMissiles ) do - - for MissileDataID, MissileData in pairs( TrackingData.MissileData ) do - self:T3( MissileDataID ) - - local TrainerSourceUnit = MissileData.TrainerSourceUnit - local TrainerWeapon = MissileData.TrainerWeapon - local TrainerTargetUnit = MissileData.TrainerTargetUnit - local TrainerWeaponTypeName = MissileData.TrainerWeaponTypeName - local TrainerWeaponLaunched = MissileData.TrainerWeaponLaunched - - if Client and Client:IsAlive() and TrainerSourceUnit and TrainerSourceUnit:IsAlive() and TrainerWeapon and TrainerWeapon:isExist() and TrainerTargetUnit and TrainerTargetUnit:IsAlive() then - - if ShowMessages == true then - local TrackingTo - TrackingTo = string.format( " -> %s", - TrainerWeaponTypeName - ) - - if ClientDataID == TrackingDataID then - if ClientData.MessageToClient == "" then - ClientData.MessageToClient = "Missiles to You:\n" - end - ClientData.MessageToClient = ClientData.MessageToClient .. TrackingTo .. self:_AddRange( ClientData.Client, TrainerWeapon ) .. self:_AddBearing( ClientData.Client, TrainerWeapon ) .. "\n" - else - if self.TrackingToAll == true then - if ClientData.MessageToAll == "" then - ClientData.MessageToAll = "Missiles to other Players:\n" - end - ClientData.MessageToAll = ClientData.MessageToAll .. TrackingTo .. self:_AddRange( ClientData.Client, TrainerWeapon ) .. self:_AddBearing( ClientData.Client, TrainerWeapon ) .. " ( " .. TrainerTargetUnit:GetPlayerName() .. " )\n" - end - end - end - end - end - end - - -- Once the Player Client and the Other Player Client tracking messages are prepared, show them. - if ClientData.MessageToClient ~= "" or ClientData.MessageToAll ~= "" then - local Message = MESSAGE:New( ClientData.MessageToClient .. ClientData.MessageToAll, 1, "Tracking" ):ToClient( Client ) - end - end - end - - return true -end ---- This module contains the AIRBASEPOLICE classes. --- --- === --- --- 1) @{AirbasePolice#AIRBASEPOLICE_BASE} class, extends @{Base#BASE} --- ================================================================== --- The @{AirbasePolice#AIRBASEPOLICE_BASE} class provides the main methods to monitor CLIENT behaviour at airbases. --- CLIENTS should not be allowed to: --- --- * Don't taxi faster than 40 km/h. --- * Don't take-off on taxiways. --- * Avoid to hit other planes on the airbase. --- * Obey ground control orders. --- --- 2) @{AirbasePolice#AIRBASEPOLICE_CAUCASUS} class, extends @{AirbasePolice#AIRBASEPOLICE_BASE} --- ============================================================================================= --- All the airbases on the caucasus map can be monitored using this class. --- If you want to monitor specific airbases, you need to use the @{#AIRBASEPOLICE_BASE.Monitor}() method, which takes a table or airbase names. --- The following names can be given: --- * AnapaVityazevo --- * Batumi --- * Beslan --- * Gelendzhik --- * Gudauta --- * Kobuleti --- * KrasnodarCenter --- * KrasnodarPashkovsky --- * Krymsk --- * Kutaisi --- * MaykopKhanskaya --- * MineralnyeVody --- * Mozdok --- * Nalchik --- * Novorossiysk --- * SenakiKolkhi --- * SochiAdler --- * Soganlug --- * SukhumiBabushara --- * TbilisiLochini --- * Vaziani --- --- 3) @{AirbasePolice#AIRBASEPOLICE_NEVADA} class, extends @{AirbasePolice#AIRBASEPOLICE_BASE} --- ============================================================================================= --- All the airbases on the NEVADA map can be monitored using this class. --- If you want to monitor specific airbases, you need to use the @{#AIRBASEPOLICE_BASE.Monitor}() method, which takes a table or airbase names. --- The following names can be given: --- * Nellis --- * McCarran --- * Creech --- * Groom Lake --- --- ### Contributions: Dutch Baron - Concept & Testing --- ### Author: FlightControl - Framework Design & Programming --- --- @module AirbasePolice - - - - - ---- @type AIRBASEPOLICE_BASE --- @field Core.Set#SET_CLIENT SetClient --- @extends Core.Base#BASE - -AIRBASEPOLICE_BASE = { - ClassName = "AIRBASEPOLICE_BASE", - SetClient = nil, - Airbases = nil, - AirbaseNames = nil, -} - - ---- Creates a new AIRBASEPOLICE_BASE object. --- @param #AIRBASEPOLICE_BASE self --- @param SetClient A SET_CLIENT object that will contain the CLIENT objects to be monitored if they follow the rules of the airbase. --- @param Airbases A table of Airbase Names. --- @return #AIRBASEPOLICE_BASE self -function AIRBASEPOLICE_BASE:New( SetClient, Airbases ) - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) - self:E( { self.ClassName, SetClient, Airbases } ) - - self.SetClient = SetClient - self.Airbases = Airbases - - for AirbaseID, Airbase in pairs( self.Airbases ) do - Airbase.ZoneBoundary = ZONE_POLYGON_BASE:New( "Boundary", Airbase.PointsBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - for PointsRunwayID, PointsRunway in pairs( Airbase.PointsRunways ) do - Airbase.ZoneRunways[PointsRunwayID] = ZONE_POLYGON_BASE:New( "Runway " .. PointsRunwayID, PointsRunway ):SmokeZone(SMOKECOLOR.Red):Flush() - end - end - --- -- Template --- local TemplateBoundary = GROUP:FindByName( "Template Boundary" ) --- self.Airbases.Template.ZoneBoundary = ZONE_POLYGON:New( "Template Boundary", TemplateBoundary ):SmokeZone(SMOKECOLOR.White):Flush() --- --- local TemplateRunway1 = GROUP:FindByName( "Template Runway 1" ) --- self.Airbases.Template.ZoneRunways[1] = ZONE_POLYGON:New( "Template Runway 1", TemplateRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - - self.SetClient:ForEachClient( - --- @param Wrapper.Client#CLIENT Client - function( Client ) - Client:SetState( self, "Speeding", false ) - Client:SetState( self, "Warnings", 0) - Client:SetState( self, "Taxi", false ) - end - ) - - self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, {}, 0, 2, 0.05 ) - - return self -end - ---- @type AIRBASEPOLICE_BASE.AirbaseNames --- @list <#string> - ---- Monitor a table of airbase names. --- @param #AIRBASEPOLICE_BASE self --- @param #AIRBASEPOLICE_BASE.AirbaseNames AirbaseNames A list of AirbaseNames to monitor. If this parameters is nil, then all airbases will be monitored. --- @return #AIRBASEPOLICE_BASE self -function AIRBASEPOLICE_BASE:Monitor( AirbaseNames ) - - if AirbaseNames then - if type( AirbaseNames ) == "table" then - self.AirbaseNames = AirbaseNames - else - self.AirbaseNames = { AirbaseNames } - end - end -end - ---- @param #AIRBASEPOLICE_BASE self -function AIRBASEPOLICE_BASE:_AirbaseMonitor() - - for AirbaseID, Airbase in pairs( self.Airbases ) do - - if not self.AirbaseNames or self.AirbaseNames[AirbaseID] then - - self:E( AirbaseID ) - - self.SetClient:ForEachClientInZone( Airbase.ZoneBoundary, - - --- @param Wrapper.Client#CLIENT Client - function( Client ) - - self:E( Client.UnitName ) - if Client:IsAlive() then - local NotInRunwayZone = true - for ZoneRunwayID, ZoneRunway in pairs( Airbase.ZoneRunways ) do - NotInRunwayZone = ( Client:IsNotInZone( ZoneRunway ) == true ) and NotInRunwayZone or false - end - - if NotInRunwayZone then - local Taxi = self:GetState( self, "Taxi" ) - self:E( Taxi ) - if Taxi == false then - Client:Message( "Welcome at " .. AirbaseID .. ". The maximum taxiing speed is " .. Airbase.MaximumSpeed " km/h.", 20, "ATC" ) - self:SetState( self, "Taxi", true ) - end - - -- TODO: GetVelocityKMH function usage - local VelocityVec3 = Client:GetVelocity() - local Velocity = ( VelocityVec3.x ^ 2 + VelocityVec3.y ^ 2 + VelocityVec3.z ^ 2 ) ^ 0.5 -- in meters / sec - local Velocity = Velocity * 3.6 -- now it is in km/h. - -- MESSAGE:New( "Velocity = " .. Velocity, 1 ):ToAll() - local IsAboveRunway = Client:IsAboveRunway() - local IsOnGround = Client:InAir() == false - self:T( IsAboveRunway, IsOnGround ) - - if IsAboveRunway and IsOnGround then - - if Velocity > Airbase.MaximumSpeed then - local IsSpeeding = Client:GetState( self, "Speeding" ) - - if IsSpeeding == true then - local SpeedingWarnings = Client:GetState( self, "Warnings" ) - self:T( SpeedingWarnings ) - - if SpeedingWarnings <= 3 then - Client:Message( "You are speeding on the taxiway! Slow down or you will be removed from this airbase! Your current velocity is " .. string.format( "%2.0f km/h", Velocity ), 5, "Warning " .. SpeedingWarnings .. " / 3" ) - Client:SetState( self, "Warnings", SpeedingWarnings + 1 ) - else - MESSAGE:New( "Player " .. Client:GetPlayerName() .. " has been removed from the airbase, due to a speeding violation ...", 10, "Airbase Police" ):ToAll() - Client:Destroy() - trigger.action.setUserFlag( "AIRCRAFT_"..Client:GetID(), 100) - Client:SetState( self, "Speeding", false ) - Client:SetState( self, "Warnings", 0 ) - end - - else - Client:Message( "You are speeding on the taxiway, slow down now! Your current velocity is " .. string.format( "%2.0f km/h", Velocity ), 5, "Attention! " ) - Client:SetState( self, "Speeding", true ) - Client:SetState( self, "Warnings", 1 ) - end - - else - Client:SetState( self, "Speeding", false ) - Client:SetState( self, "Warnings", 0 ) - end - end - - else - Client:SetState( self, "Speeding", false ) - Client:SetState( self, "Warnings", 0 ) - local Taxi = self:GetState( self, "Taxi" ) - if Taxi == true then - Client:Message( "You have progressed to the runway ... Await take-off clearance ...", 20, "ATC" ) - self:SetState( self, "Taxi", false ) - end - end - end - end - ) - end - end - - return true -end - - ---- @type AIRBASEPOLICE_CAUCASUS --- @field Core.Set#SET_CLIENT SetClient --- @extends #AIRBASEPOLICE_BASE - -AIRBASEPOLICE_CAUCASUS = { - ClassName = "AIRBASEPOLICE_CAUCASUS", - Airbases = { - AnapaVityazevo = { - PointsBoundary = { - [1]={["y"]=242234.85714287,["x"]=-6616.5714285726,}, - [2]={["y"]=241060.57142858,["x"]=-5585.142857144,}, - [3]={["y"]=243806.2857143,["x"]=-3962.2857142868,}, - [4]={["y"]=245240.57142858,["x"]=-4816.5714285726,}, - [5]={["y"]=244783.42857144,["x"]=-5630.8571428583,}, - [6]={["y"]=243800.57142858,["x"]=-5065.142857144,}, - [7]={["y"]=242232.00000001,["x"]=-6622.2857142868,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=242140.57142858,["x"]=-6478.8571428583,}, - [2]={["y"]=242188.57142858,["x"]=-6522.0000000011,}, - [3]={["y"]=244124.2857143,["x"]=-4344.0000000011,}, - [4]={["y"]=244068.2857143,["x"]=-4296.5714285726,}, - [5]={["y"]=242140.57142858,["x"]=-6480.0000000011,} - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Batumi = { - PointsBoundary = { - [1]={["y"]=617567.14285714,["x"]=-355313.14285715,}, - [2]={["y"]=616181.42857142,["x"]=-354800.28571429,}, - [3]={["y"]=616007.14285714,["x"]=-355128.85714286,}, - [4]={["y"]=618230,["x"]=-356914.57142858,}, - [5]={["y"]=618727.14285714,["x"]=-356166,}, - [6]={["y"]=617572.85714285,["x"]=-355308.85714286,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=616442.28571429,["x"]=-355090.28571429,}, - [2]={["y"]=618450.57142857,["x"]=-356522,}, - [3]={["y"]=618407.71428571,["x"]=-356584.85714286,}, - [4]={["y"]=618361.99999999,["x"]=-356554.85714286,}, - [5]={["y"]=618324.85714285,["x"]=-356599.14285715,}, - [6]={["y"]=618250.57142856,["x"]=-356543.42857143,}, - [7]={["y"]=618257.7142857,["x"]=-356496.28571429,}, - [8]={["y"]=618237.7142857,["x"]=-356459.14285715,}, - [9]={["y"]=616555.71428571,["x"]=-355258.85714286,}, - [10]={["y"]=616486.28571428,["x"]=-355280.57142858,}, - [11]={["y"]=616410.57142856,["x"]=-355227.71428572,}, - [12]={["y"]=616441.99999999,["x"]=-355179.14285715,}, - [13]={["y"]=616401.99999999,["x"]=-355147.71428572,}, - [14]={["y"]=616441.42857142,["x"]=-355092.57142858,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Beslan = { - PointsBoundary = { - [1]={["y"]=842082.57142857,["x"]=-148445.14285715,}, - [2]={["y"]=845237.71428572,["x"]=-148639.71428572,}, - [3]={["y"]=845232,["x"]=-148765.42857143,}, - [4]={["y"]=844220.57142857,["x"]=-149168.28571429,}, - [5]={["y"]=843274.85714286,["x"]=-149125.42857143,}, - [6]={["y"]=842077.71428572,["x"]=-148554,}, - [7]={["y"]=842083.42857143,["x"]=-148445.42857143,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=842104.57142857,["x"]=-148460.57142857,}, - [2]={["y"]=845225.71428572,["x"]=-148656,}, - [3]={["y"]=845220.57142858,["x"]=-148750,}, - [4]={["y"]=842098.85714286,["x"]=-148556.28571429,}, - [5]={["y"]=842104,["x"]=-148460.28571429,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Gelendzhik = { - PointsBoundary = { - [1]={["y"]=297856.00000001,["x"]=-51151.428571429,}, - [2]={["y"]=299044.57142858,["x"]=-49720.000000001,}, - [3]={["y"]=298861.71428572,["x"]=-49580.000000001,}, - [4]={["y"]=298198.85714286,["x"]=-49842.857142858,}, - [5]={["y"]=297990.28571429,["x"]=-50151.428571429,}, - [6]={["y"]=297696.00000001,["x"]=-51054.285714286,}, - [7]={["y"]=297850.28571429,["x"]=-51160.000000001,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=297834.00000001,["x"]=-51107.428571429,}, - [2]={["y"]=297786.57142858,["x"]=-51068.857142858,}, - [3]={["y"]=298946.57142858,["x"]=-49686.000000001,}, - [4]={["y"]=298993.14285715,["x"]=-49725.714285715,}, - [5]={["y"]=297835.14285715,["x"]=-51107.714285715,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Gudauta = { - PointsBoundary = { - [1]={["y"]=517246.57142857,["x"]=-197850.28571429,}, - [2]={["y"]=516749.42857142,["x"]=-198070.28571429,}, - [3]={["y"]=515755.14285714,["x"]=-197598.85714286,}, - [4]={["y"]=515369.42857142,["x"]=-196538.85714286,}, - [5]={["y"]=515623.71428571,["x"]=-195618.85714286,}, - [6]={["y"]=515946.57142857,["x"]=-195510.28571429,}, - [7]={["y"]=517243.71428571,["x"]=-197858.85714286,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=517096.57142857,["x"]=-197804.57142857,}, - [2]={["y"]=515880.85714285,["x"]=-195590.28571429,}, - [3]={["y"]=515812.28571428,["x"]=-195628.85714286,}, - [4]={["y"]=517036.57142857,["x"]=-197834.57142857,}, - [5]={["y"]=517097.99999999,["x"]=-197807.42857143,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Kobuleti = { - PointsBoundary = { - [1]={["y"]=634427.71428571,["x"]=-318290.28571429,}, - [2]={["y"]=635033.42857143,["x"]=-317550.2857143,}, - [3]={["y"]=635864.85714286,["x"]=-317333.14285715,}, - [4]={["y"]=636967.71428571,["x"]=-317261.71428572,}, - [5]={["y"]=637144.85714286,["x"]=-317913.14285715,}, - [6]={["y"]=634630.57142857,["x"]=-318687.42857144,}, - [7]={["y"]=634424.85714286,["x"]=-318290.2857143,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=634509.71428571,["x"]=-318339.42857144,}, - [2]={["y"]=636767.42857143,["x"]=-317516.57142858,}, - [3]={["y"]=636790,["x"]=-317575.71428572,}, - [4]={["y"]=634531.42857143,["x"]=-318398.00000001,}, - [5]={["y"]=634510.28571429,["x"]=-318339.71428572,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - KrasnodarCenter = { - PointsBoundary = { - [1]={["y"]=366680.28571429,["x"]=11699.142857142,}, - [2]={["y"]=366654.28571429,["x"]=11225.142857142,}, - [3]={["y"]=367497.14285715,["x"]=11082.285714285,}, - [4]={["y"]=368025.71428572,["x"]=10396.57142857,}, - [5]={["y"]=369854.28571429,["x"]=11367.999999999,}, - [6]={["y"]=369840.00000001,["x"]=11910.857142856,}, - [7]={["y"]=366682.57142858,["x"]=11697.999999999,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=369205.42857144,["x"]=11789.142857142,}, - [2]={["y"]=369209.71428572,["x"]=11714.857142856,}, - [3]={["y"]=366699.71428572,["x"]=11581.714285713,}, - [4]={["y"]=366698.28571429,["x"]=11659.142857142,}, - [5]={["y"]=369208.85714286,["x"]=11788.57142857,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - KrasnodarPashkovsky = { - PointsBoundary = { - [1]={["y"]=386754,["x"]=6476.5714285703,}, - [2]={["y"]=389182.57142858,["x"]=8722.2857142846,}, - [3]={["y"]=388832.57142858,["x"]=9086.5714285703,}, - [4]={["y"]=386961.14285715,["x"]=7707.9999999989,}, - [5]={["y"]=385404,["x"]=9179.4285714274,}, - [6]={["y"]=383239.71428572,["x"]=7386.5714285703,}, - [7]={["y"]=383954,["x"]=6486.5714285703,}, - [8]={["y"]=385775.42857143,["x"]=8097.9999999989,}, - [9]={["y"]=386804,["x"]=7319.4285714274,}, - [10]={["y"]=386375.42857143,["x"]=6797.9999999989,}, - [11]={["y"]=386746.85714286,["x"]=6472.2857142846,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=385891.14285715,["x"]=8416.5714285703,}, - [2]={["y"]=385842.28571429,["x"]=8467.9999999989,}, - [3]={["y"]=384180.85714286,["x"]=6917.1428571417,}, - [4]={["y"]=384228.57142858,["x"]=6867.7142857132,}, - [5]={["y"]=385891.14285715,["x"]=8416.5714285703,}, - }, - [2] = { - [1]={["y"]=386714.85714286,["x"]=6674.857142856,}, - [2]={["y"]=386757.71428572,["x"]=6627.7142857132,}, - [3]={["y"]=389028.57142858,["x"]=8741.4285714275,}, - [4]={["y"]=388981.71428572,["x"]=8790.5714285703,}, - [5]={["y"]=386714.57142858,["x"]=6674.5714285703,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Krymsk = { - PointsBoundary = { - [1]={["y"]=293338.00000001,["x"]=-7575.4285714297,}, - [2]={["y"]=295199.42857144,["x"]=-5434.0000000011,}, - [3]={["y"]=295595.14285715,["x"]=-6239.7142857154,}, - [4]={["y"]=294152.2857143,["x"]=-8325.4285714297,}, - [5]={["y"]=293345.14285715,["x"]=-7596.8571428582,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=293522.00000001,["x"]=-7567.4285714297,}, - [2]={["y"]=293578.57142858,["x"]=-7616.0000000011,}, - [3]={["y"]=295246.00000001,["x"]=-5591.142857144,}, - [4]={["y"]=295187.71428573,["x"]=-5546.0000000011,}, - [5]={["y"]=293523.14285715,["x"]=-7568.2857142868,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Kutaisi = { - PointsBoundary = { - [1]={["y"]=682087.42857143,["x"]=-284512.85714286,}, - [2]={["y"]=685387.42857143,["x"]=-283662.85714286,}, - [3]={["y"]=685294.57142857,["x"]=-284977.14285715,}, - [4]={["y"]=682744.57142857,["x"]=-286505.71428572,}, - [5]={["y"]=682094.57142857,["x"]=-284527.14285715,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=682638,["x"]=-285202.28571429,}, - [2]={["y"]=685050.28571429,["x"]=-284507.42857144,}, - [3]={["y"]=685068.85714286,["x"]=-284578.85714286,}, - [4]={["y"]=682657.42857143,["x"]=-285264.28571429,}, - [5]={["y"]=682638.28571429,["x"]=-285202.85714286,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - MaykopKhanskaya = { - PointsBoundary = { - [1]={["y"]=456876.28571429,["x"]=-27665.42857143,}, - [2]={["y"]=457800,["x"]=-28392.857142858,}, - [3]={["y"]=459368.57142857,["x"]=-26378.571428573,}, - [4]={["y"]=459425.71428572,["x"]=-25242.857142858,}, - [5]={["y"]=458961.42857143,["x"]=-24964.285714287,}, - [6]={["y"]=456878.57142857,["x"]=-27667.714285715,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=457005.42857143,["x"]=-27668.000000001,}, - [2]={["y"]=459028.85714286,["x"]=-25168.857142858,}, - [3]={["y"]=459082.57142857,["x"]=-25216.857142858,}, - [4]={["y"]=457060,["x"]=-27714.285714287,}, - [5]={["y"]=457004.57142857,["x"]=-27669.714285715,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - MineralnyeVody = { - PointsBoundary = { - [1]={["y"]=703857.14285714,["x"]=-50226.000000002,}, - [2]={["y"]=707385.71428571,["x"]=-51911.714285716,}, - [3]={["y"]=707595.71428571,["x"]=-51434.857142859,}, - [4]={["y"]=707900,["x"]=-51568.857142859,}, - [5]={["y"]=707542.85714286,["x"]=-52326.000000002,}, - [6]={["y"]=706628.57142857,["x"]=-52568.857142859,}, - [7]={["y"]=705142.85714286,["x"]=-51790.285714288,}, - [8]={["y"]=703678.57142857,["x"]=-50611.714285716,}, - [9]={["y"]=703857.42857143,["x"]=-50226.857142859,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=703904,["x"]=-50352.571428573,}, - [2]={["y"]=707596.28571429,["x"]=-52094.571428573,}, - [3]={["y"]=707560.57142858,["x"]=-52161.714285716,}, - [4]={["y"]=703871.71428572,["x"]=-50420.571428573,}, - [5]={["y"]=703902,["x"]=-50352.000000002,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Mozdok = { - PointsBoundary = { - [1]={["y"]=832123.42857143,["x"]=-83608.571428573,}, - [2]={["y"]=835916.28571429,["x"]=-83144.285714288,}, - [3]={["y"]=835474.28571429,["x"]=-84170.571428573,}, - [4]={["y"]=832911.42857143,["x"]=-84470.571428573,}, - [5]={["y"]=832487.71428572,["x"]=-85565.714285716,}, - [6]={["y"]=831573.42857143,["x"]=-85351.42857143,}, - [7]={["y"]=832123.71428572,["x"]=-83610.285714288,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=832201.14285715,["x"]=-83699.428571431,}, - [2]={["y"]=832212.57142857,["x"]=-83780.571428574,}, - [3]={["y"]=835730.28571429,["x"]=-83335.714285717,}, - [4]={["y"]=835718.85714286,["x"]=-83246.571428574,}, - [5]={["y"]=832200.57142857,["x"]=-83700.000000002,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Nalchik = { - PointsBoundary = { - [1]={["y"]=759370,["x"]=-125502.85714286,}, - [2]={["y"]=761384.28571429,["x"]=-124177.14285714,}, - [3]={["y"]=761472.85714286,["x"]=-124325.71428572,}, - [4]={["y"]=761092.85714286,["x"]=-125048.57142857,}, - [5]={["y"]=760295.71428572,["x"]=-125685.71428572,}, - [6]={["y"]=759444.28571429,["x"]=-125734.28571429,}, - [7]={["y"]=759375.71428572,["x"]=-125511.42857143,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=759454.28571429,["x"]=-125551.42857143,}, - [2]={["y"]=759492.85714286,["x"]=-125610.85714286,}, - [3]={["y"]=761406.28571429,["x"]=-124304.28571429,}, - [4]={["y"]=761361.14285714,["x"]=-124239.71428572,}, - [5]={["y"]=759456,["x"]=-125552.57142857,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Novorossiysk = { - PointsBoundary = { - [1]={["y"]=278677.71428573,["x"]=-41656.571428572,}, - [2]={["y"]=278446.2857143,["x"]=-41453.714285715,}, - [3]={["y"]=278989.14285716,["x"]=-40188.000000001,}, - [4]={["y"]=279717.71428573,["x"]=-39968.000000001,}, - [5]={["y"]=280020.57142859,["x"]=-40208.000000001,}, - [6]={["y"]=278674.85714287,["x"]=-41660.857142858,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=278673.14285716,["x"]=-41615.142857144,}, - [2]={["y"]=278625.42857144,["x"]=-41570.571428572,}, - [3]={["y"]=279835.42857144,["x"]=-40226.000000001,}, - [4]={["y"]=279882.2857143,["x"]=-40270.000000001,}, - [5]={["y"]=278672.00000001,["x"]=-41614.857142858,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - SenakiKolkhi = { - PointsBoundary = { - [1]={["y"]=646036.57142857,["x"]=-281778.85714286,}, - [2]={["y"]=646045.14285714,["x"]=-281191.71428571,}, - [3]={["y"]=647032.28571429,["x"]=-280598.85714285,}, - [4]={["y"]=647669.42857143,["x"]=-281273.14285714,}, - [5]={["y"]=648323.71428571,["x"]=-281370.28571428,}, - [6]={["y"]=648520.85714286,["x"]=-281978.85714285,}, - [7]={["y"]=646039.42857143,["x"]=-281783.14285714,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=646060.85714285,["x"]=-281736,}, - [2]={["y"]=646056.57142857,["x"]=-281631.71428571,}, - [3]={["y"]=648442.28571428,["x"]=-281840.28571428,}, - [4]={["y"]=648432.28571428,["x"]=-281918.85714286,}, - [5]={["y"]=646063.71428571,["x"]=-281738.85714286,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - SochiAdler = { - PointsBoundary = { - [1]={["y"]=460642.28571428,["x"]=-164861.71428571,}, - [2]={["y"]=462820.85714285,["x"]=-163368.85714286,}, - [3]={["y"]=463649.42857142,["x"]=-163340.28571429,}, - [4]={["y"]=463835.14285714,["x"]=-164040.28571429,}, - [5]={["y"]=462535.14285714,["x"]=-165654.57142857,}, - [6]={["y"]=460678,["x"]=-165247.42857143,}, - [7]={["y"]=460635.14285714,["x"]=-164876,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=460831.42857143,["x"]=-165180,}, - [2]={["y"]=460878.57142857,["x"]=-165257.14285714,}, - [3]={["y"]=463663.71428571,["x"]=-163793.14285714,}, - [4]={["y"]=463612.28571428,["x"]=-163697.42857143,}, - [5]={["y"]=460831.42857143,["x"]=-165177.14285714,}, - }, - [2] = { - [1]={["y"]=460831.42857143,["x"]=-165180,}, - [2]={["y"]=460878.57142857,["x"]=-165257.14285714,}, - [3]={["y"]=463663.71428571,["x"]=-163793.14285714,}, - [4]={["y"]=463612.28571428,["x"]=-163697.42857143,}, - [5]={["y"]=460831.42857143,["x"]=-165177.14285714,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Soganlug = { - PointsBoundary = { - [1]={["y"]=894530.85714286,["x"]=-316928.28571428,}, - [2]={["y"]=896422.28571428,["x"]=-318622.57142857,}, - [3]={["y"]=896090.85714286,["x"]=-318934,}, - [4]={["y"]=894019.42857143,["x"]=-317119.71428571,}, - [5]={["y"]=894533.71428571,["x"]=-316925.42857143,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=894525.71428571,["x"]=-316964,}, - [2]={["y"]=896363.14285714,["x"]=-318634.28571428,}, - [3]={["y"]=896299.14285714,["x"]=-318702.85714286,}, - [4]={["y"]=894464,["x"]=-317031.71428571,}, - [5]={["y"]=894524.57142857,["x"]=-316963.71428571,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - SukhumiBabushara = { - PointsBoundary = { - [1]={["y"]=562541.14285714,["x"]=-219852.28571429,}, - [2]={["y"]=562691.14285714,["x"]=-219395.14285714,}, - [3]={["y"]=564326.85714286,["x"]=-219523.71428571,}, - [4]={["y"]=566262.57142857,["x"]=-221166.57142857,}, - [5]={["y"]=566069.71428571,["x"]=-221580.85714286,}, - [6]={["y"]=562534,["x"]=-219873.71428571,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=562684,["x"]=-219779.71428571,}, - [2]={["y"]=562717.71428571,["x"]=-219718,}, - [3]={["y"]=566046.85714286,["x"]=-221376.57142857,}, - [4]={["y"]=566012.28571428,["x"]=-221446.57142857,}, - [5]={["y"]=562684.57142857,["x"]=-219782.57142857,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - TbilisiLochini = { - PointsBoundary = { - [1]={["y"]=895172.85714286,["x"]=-314667.42857143,}, - [2]={["y"]=895337.42857143,["x"]=-314143.14285714,}, - [3]={["y"]=895990.28571429,["x"]=-314036,}, - [4]={["y"]=897730.28571429,["x"]=-315284.57142857,}, - [5]={["y"]=897901.71428571,["x"]=-316284.57142857,}, - [6]={["y"]=897684.57142857,["x"]=-316618.85714286,}, - [7]={["y"]=895173.14285714,["x"]=-314667.42857143,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=895261.14285715,["x"]=-314652.28571428,}, - [2]={["y"]=897654.57142857,["x"]=-316523.14285714,}, - [3]={["y"]=897711.71428571,["x"]=-316450.28571429,}, - [4]={["y"]=895327.42857143,["x"]=-314568.85714286,}, - [5]={["y"]=895261.71428572,["x"]=-314656,}, - }, - [2] = { - [1]={["y"]=895605.71428572,["x"]=-314724.57142857,}, - [2]={["y"]=897639.71428572,["x"]=-316148,}, - [3]={["y"]=897683.42857143,["x"]=-316087.14285714,}, - [4]={["y"]=895650,["x"]=-314660,}, - [5]={["y"]=895606,["x"]=-314724.85714286,} - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Vaziani = { - PointsBoundary = { - [1]={["y"]=902122,["x"]=-318163.71428572,}, - [2]={["y"]=902678.57142857,["x"]=-317594,}, - [3]={["y"]=903275.71428571,["x"]=-317405.42857143,}, - [4]={["y"]=903418.57142857,["x"]=-317891.14285714,}, - [5]={["y"]=904292.85714286,["x"]=-318748.28571429,}, - [6]={["y"]=904542,["x"]=-319740.85714286,}, - [7]={["y"]=904042,["x"]=-320166.57142857,}, - [8]={["y"]=902121.42857143,["x"]=-318164.85714286,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=902239.14285714,["x"]=-318190.85714286,}, - [2]={["y"]=904014.28571428,["x"]=-319994.57142857,}, - [3]={["y"]=904064.85714285,["x"]=-319945.14285715,}, - [4]={["y"]=902294.57142857,["x"]=-318146,}, - [5]={["y"]=902247.71428571,["x"]=-318190.85714286,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - }, -} - ---- Creates a new AIRBASEPOLICE_CAUCASUS object. --- @param #AIRBASEPOLICE_CAUCASUS self --- @param SetClient A SET_CLIENT object that will contain the CLIENT objects to be monitored if they follow the rules of the airbase. --- @return #AIRBASEPOLICE_CAUCASUS self -function AIRBASEPOLICE_CAUCASUS:New( SetClient ) - - -- Inherits from BASE - local self = BASE:Inherit( self, AIRBASEPOLICE_BASE:New( SetClient, self.Airbases ) ) - - -- -- AnapaVityazevo - -- local AnapaVityazevoBoundary = GROUP:FindByName( "AnapaVityazevo Boundary" ) - -- self.Airbases.AnapaVityazevo.ZoneBoundary = ZONE_POLYGON:New( "AnapaVityazevo Boundary", AnapaVityazevoBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local AnapaVityazevoRunway1 = GROUP:FindByName( "AnapaVityazevo Runway 1" ) - -- self.Airbases.AnapaVityazevo.ZoneRunways[1] = ZONE_POLYGON:New( "AnapaVityazevo Runway 1", AnapaVityazevoRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Batumi - -- local BatumiBoundary = GROUP:FindByName( "Batumi Boundary" ) - -- self.Airbases.Batumi.ZoneBoundary = ZONE_POLYGON:New( "Batumi Boundary", BatumiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local BatumiRunway1 = GROUP:FindByName( "Batumi Runway 1" ) - -- self.Airbases.Batumi.ZoneRunways[1] = ZONE_POLYGON:New( "Batumi Runway 1", BatumiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Beslan - -- local BeslanBoundary = GROUP:FindByName( "Beslan Boundary" ) - -- self.Airbases.Beslan.ZoneBoundary = ZONE_POLYGON:New( "Beslan Boundary", BeslanBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local BeslanRunway1 = GROUP:FindByName( "Beslan Runway 1" ) - -- self.Airbases.Beslan.ZoneRunways[1] = ZONE_POLYGON:New( "Beslan Runway 1", BeslanRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Gelendzhik - -- local GelendzhikBoundary = GROUP:FindByName( "Gelendzhik Boundary" ) - -- self.Airbases.Gelendzhik.ZoneBoundary = ZONE_POLYGON:New( "Gelendzhik Boundary", GelendzhikBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local GelendzhikRunway1 = GROUP:FindByName( "Gelendzhik Runway 1" ) - -- self.Airbases.Gelendzhik.ZoneRunways[1] = ZONE_POLYGON:New( "Gelendzhik Runway 1", GelendzhikRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Gudauta - -- local GudautaBoundary = GROUP:FindByName( "Gudauta Boundary" ) - -- self.Airbases.Gudauta.ZoneBoundary = ZONE_POLYGON:New( "Gudauta Boundary", GudautaBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local GudautaRunway1 = GROUP:FindByName( "Gudauta Runway 1" ) - -- self.Airbases.Gudauta.ZoneRunways[1] = ZONE_POLYGON:New( "Gudauta Runway 1", GudautaRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Kobuleti - -- local KobuletiBoundary = GROUP:FindByName( "Kobuleti Boundary" ) - -- self.Airbases.Kobuleti.ZoneBoundary = ZONE_POLYGON:New( "Kobuleti Boundary", KobuletiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local KobuletiRunway1 = GROUP:FindByName( "Kobuleti Runway 1" ) - -- self.Airbases.Kobuleti.ZoneRunways[1] = ZONE_POLYGON:New( "Kobuleti Runway 1", KobuletiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- KrasnodarCenter - -- local KrasnodarCenterBoundary = GROUP:FindByName( "KrasnodarCenter Boundary" ) - -- self.Airbases.KrasnodarCenter.ZoneBoundary = ZONE_POLYGON:New( "KrasnodarCenter Boundary", KrasnodarCenterBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local KrasnodarCenterRunway1 = GROUP:FindByName( "KrasnodarCenter Runway 1" ) - -- self.Airbases.KrasnodarCenter.ZoneRunways[1] = ZONE_POLYGON:New( "KrasnodarCenter Runway 1", KrasnodarCenterRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- KrasnodarPashkovsky - -- local KrasnodarPashkovskyBoundary = GROUP:FindByName( "KrasnodarPashkovsky Boundary" ) - -- self.Airbases.KrasnodarPashkovsky.ZoneBoundary = ZONE_POLYGON:New( "KrasnodarPashkovsky Boundary", KrasnodarPashkovskyBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local KrasnodarPashkovskyRunway1 = GROUP:FindByName( "KrasnodarPashkovsky Runway 1" ) - -- self.Airbases.KrasnodarPashkovsky.ZoneRunways[1] = ZONE_POLYGON:New( "KrasnodarPashkovsky Runway 1", KrasnodarPashkovskyRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- local KrasnodarPashkovskyRunway2 = GROUP:FindByName( "KrasnodarPashkovsky Runway 2" ) - -- self.Airbases.KrasnodarPashkovsky.ZoneRunways[2] = ZONE_POLYGON:New( "KrasnodarPashkovsky Runway 2", KrasnodarPashkovskyRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Krymsk - -- local KrymskBoundary = GROUP:FindByName( "Krymsk Boundary" ) - -- self.Airbases.Krymsk.ZoneBoundary = ZONE_POLYGON:New( "Krymsk Boundary", KrymskBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local KrymskRunway1 = GROUP:FindByName( "Krymsk Runway 1" ) - -- self.Airbases.Krymsk.ZoneRunways[1] = ZONE_POLYGON:New( "Krymsk Runway 1", KrymskRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Kutaisi - -- local KutaisiBoundary = GROUP:FindByName( "Kutaisi Boundary" ) - -- self.Airbases.Kutaisi.ZoneBoundary = ZONE_POLYGON:New( "Kutaisi Boundary", KutaisiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local KutaisiRunway1 = GROUP:FindByName( "Kutaisi Runway 1" ) - -- self.Airbases.Kutaisi.ZoneRunways[1] = ZONE_POLYGON:New( "Kutaisi Runway 1", KutaisiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- MaykopKhanskaya - -- local MaykopKhanskayaBoundary = GROUP:FindByName( "MaykopKhanskaya Boundary" ) - -- self.Airbases.MaykopKhanskaya.ZoneBoundary = ZONE_POLYGON:New( "MaykopKhanskaya Boundary", MaykopKhanskayaBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local MaykopKhanskayaRunway1 = GROUP:FindByName( "MaykopKhanskaya Runway 1" ) - -- self.Airbases.MaykopKhanskaya.ZoneRunways[1] = ZONE_POLYGON:New( "MaykopKhanskaya Runway 1", MaykopKhanskayaRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- MineralnyeVody - -- local MineralnyeVodyBoundary = GROUP:FindByName( "MineralnyeVody Boundary" ) - -- self.Airbases.MineralnyeVody.ZoneBoundary = ZONE_POLYGON:New( "MineralnyeVody Boundary", MineralnyeVodyBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local MineralnyeVodyRunway1 = GROUP:FindByName( "MineralnyeVody Runway 1" ) - -- self.Airbases.MineralnyeVody.ZoneRunways[1] = ZONE_POLYGON:New( "MineralnyeVody Runway 1", MineralnyeVodyRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Mozdok - -- local MozdokBoundary = GROUP:FindByName( "Mozdok Boundary" ) - -- self.Airbases.Mozdok.ZoneBoundary = ZONE_POLYGON:New( "Mozdok Boundary", MozdokBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local MozdokRunway1 = GROUP:FindByName( "Mozdok Runway 1" ) - -- self.Airbases.Mozdok.ZoneRunways[1] = ZONE_POLYGON:New( "Mozdok Runway 1", MozdokRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Nalchik - -- local NalchikBoundary = GROUP:FindByName( "Nalchik Boundary" ) - -- self.Airbases.Nalchik.ZoneBoundary = ZONE_POLYGON:New( "Nalchik Boundary", NalchikBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local NalchikRunway1 = GROUP:FindByName( "Nalchik Runway 1" ) - -- self.Airbases.Nalchik.ZoneRunways[1] = ZONE_POLYGON:New( "Nalchik Runway 1", NalchikRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Novorossiysk - -- local NovorossiyskBoundary = GROUP:FindByName( "Novorossiysk Boundary" ) - -- self.Airbases.Novorossiysk.ZoneBoundary = ZONE_POLYGON:New( "Novorossiysk Boundary", NovorossiyskBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local NovorossiyskRunway1 = GROUP:FindByName( "Novorossiysk Runway 1" ) - -- self.Airbases.Novorossiysk.ZoneRunways[1] = ZONE_POLYGON:New( "Novorossiysk Runway 1", NovorossiyskRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- SenakiKolkhi - -- local SenakiKolkhiBoundary = GROUP:FindByName( "SenakiKolkhi Boundary" ) - -- self.Airbases.SenakiKolkhi.ZoneBoundary = ZONE_POLYGON:New( "SenakiKolkhi Boundary", SenakiKolkhiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local SenakiKolkhiRunway1 = GROUP:FindByName( "SenakiKolkhi Runway 1" ) - -- self.Airbases.SenakiKolkhi.ZoneRunways[1] = ZONE_POLYGON:New( "SenakiKolkhi Runway 1", SenakiKolkhiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- SochiAdler - -- local SochiAdlerBoundary = GROUP:FindByName( "SochiAdler Boundary" ) - -- self.Airbases.SochiAdler.ZoneBoundary = ZONE_POLYGON:New( "SochiAdler Boundary", SochiAdlerBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local SochiAdlerRunway1 = GROUP:FindByName( "SochiAdler Runway 1" ) - -- self.Airbases.SochiAdler.ZoneRunways[1] = ZONE_POLYGON:New( "SochiAdler Runway 1", SochiAdlerRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- local SochiAdlerRunway2 = GROUP:FindByName( "SochiAdler Runway 2" ) - -- self.Airbases.SochiAdler.ZoneRunways[2] = ZONE_POLYGON:New( "SochiAdler Runway 2", SochiAdlerRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Soganlug - -- local SoganlugBoundary = GROUP:FindByName( "Soganlug Boundary" ) - -- self.Airbases.Soganlug.ZoneBoundary = ZONE_POLYGON:New( "Soganlug Boundary", SoganlugBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local SoganlugRunway1 = GROUP:FindByName( "Soganlug Runway 1" ) - -- self.Airbases.Soganlug.ZoneRunways[1] = ZONE_POLYGON:New( "Soganlug Runway 1", SoganlugRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- SukhumiBabushara - -- local SukhumiBabusharaBoundary = GROUP:FindByName( "SukhumiBabushara Boundary" ) - -- self.Airbases.SukhumiBabushara.ZoneBoundary = ZONE_POLYGON:New( "SukhumiBabushara Boundary", SukhumiBabusharaBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local SukhumiBabusharaRunway1 = GROUP:FindByName( "SukhumiBabushara Runway 1" ) - -- self.Airbases.SukhumiBabushara.ZoneRunways[1] = ZONE_POLYGON:New( "SukhumiBabushara Runway 1", SukhumiBabusharaRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- TbilisiLochini - -- local TbilisiLochiniBoundary = GROUP:FindByName( "TbilisiLochini Boundary" ) - -- self.Airbases.TbilisiLochini.ZoneBoundary = ZONE_POLYGON:New( "TbilisiLochini Boundary", TbilisiLochiniBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local TbilisiLochiniRunway1 = GROUP:FindByName( "TbilisiLochini Runway 1" ) - -- self.Airbases.TbilisiLochini.ZoneRunways[1] = ZONE_POLYGON:New( "TbilisiLochini Runway 1", TbilisiLochiniRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- local TbilisiLochiniRunway2 = GROUP:FindByName( "TbilisiLochini Runway 2" ) - -- self.Airbases.TbilisiLochini.ZoneRunways[2] = ZONE_POLYGON:New( "TbilisiLochini Runway 2", TbilisiLochiniRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Vaziani - -- local VazianiBoundary = GROUP:FindByName( "Vaziani Boundary" ) - -- self.Airbases.Vaziani.ZoneBoundary = ZONE_POLYGON:New( "Vaziani Boundary", VazianiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local VazianiRunway1 = GROUP:FindByName( "Vaziani Runway 1" ) - -- self.Airbases.Vaziani.ZoneRunways[1] = ZONE_POLYGON:New( "Vaziani Runway 1", VazianiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - - - -- Template - -- local TemplateBoundary = GROUP:FindByName( "Template Boundary" ) - -- self.Airbases.Template.ZoneBoundary = ZONE_POLYGON:New( "Template Boundary", TemplateBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local TemplateRunway1 = GROUP:FindByName( "Template Runway 1" ) - -- self.Airbases.Template.ZoneRunways[1] = ZONE_POLYGON:New( "Template Runway 1", TemplateRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - - return self - -end - - - - ---- @type AIRBASEPOLICE_NEVADA --- @extends Functional.AirbasePolice#AIRBASEPOLICE_BASE -AIRBASEPOLICE_NEVADA = { - ClassName = "AIRBASEPOLICE_NEVADA", - Airbases = { - Nellis = { - PointsBoundary = { - [1]={["y"]=-17814.714285714,["x"]=-399823.14285714,}, - [2]={["y"]=-16875.857142857,["x"]=-398763.14285714,}, - [3]={["y"]=-16251.571428571,["x"]=-398988.85714286,}, - [4]={["y"]=-16163,["x"]=-398693.14285714,}, - [5]={["y"]=-16328.714285714,["x"]=-398034.57142857,}, - [6]={["y"]=-15943,["x"]=-397571.71428571,}, - [7]={["y"]=-15711.571428571,["x"]=-397551.71428571,}, - [8]={["y"]=-15748.714285714,["x"]=-396806,}, - [9]={["y"]=-16288.714285714,["x"]=-396517.42857143,}, - [10]={["y"]=-16751.571428571,["x"]=-396308.85714286,}, - [11]={["y"]=-17263,["x"]=-396234.57142857,}, - [12]={["y"]=-17577.285714286,["x"]=-396640.28571429,}, - [13]={["y"]=-17614.428571429,["x"]=-397400.28571429,}, - [14]={["y"]=-19405.857142857,["x"]=-399428.85714286,}, - [15]={["y"]=-19234.428571429,["x"]=-399683.14285714,}, - [16]={["y"]=-18708.714285714,["x"]=-399408.85714286,}, - [17]={["y"]=-18397.285714286,["x"]=-399657.42857143,}, - [18]={["y"]=-17814.428571429,["x"]=-399823.42857143,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=-18687,["x"]=-399380.28571429,}, - [2]={["y"]=-18620.714285714,["x"]=-399436.85714286,}, - [3]={["y"]=-16217.857142857,["x"]=-396596.85714286,}, - [4]={["y"]=-16300.142857143,["x"]=-396530,}, - [5]={["y"]=-18687,["x"]=-399380.85714286,}, - }, - [2] = { - [1]={["y"]=-18451.571428572,["x"]=-399580.57142857,}, - [2]={["y"]=-18392.142857143,["x"]=-399628.57142857,}, - [3]={["y"]=-16011,["x"]=-396806.85714286,}, - [4]={["y"]=-16074.714285714,["x"]=-396751.71428572,}, - [5]={["y"]=-18451.571428572,["x"]=-399580.85714285,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - McCarran = { - PointsBoundary = { - [1]={["y"]=-29455.285714286,["x"]=-416277.42857142,}, - [2]={["y"]=-28860.142857143,["x"]=-416492,}, - [3]={["y"]=-25044.428571429,["x"]=-416344.85714285,}, - [4]={["y"]=-24580.142857143,["x"]=-415959.14285714,}, - [5]={["y"]=-25073,["x"]=-415630.57142857,}, - [6]={["y"]=-25087.285714286,["x"]=-415130.57142857,}, - [7]={["y"]=-25830.142857143,["x"]=-414866.28571428,}, - [8]={["y"]=-26658.714285715,["x"]=-414880.57142857,}, - [9]={["y"]=-26973,["x"]=-415273.42857142,}, - [10]={["y"]=-27380.142857143,["x"]=-415187.71428571,}, - [11]={["y"]=-27715.857142857,["x"]=-414144.85714285,}, - [12]={["y"]=-27551.571428572,["x"]=-413473.42857142,}, - [13]={["y"]=-28630.142857143,["x"]=-413201.99999999,}, - [14]={["y"]=-29494.428571429,["x"]=-415437.71428571,}, - [15]={["y"]=-29455.571428572,["x"]=-416277.71428571,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=-29408.428571429,["x"]=-416016.28571428,}, - [2]={["y"]=-29408.142857144,["x"]=-416105.42857142,}, - [3]={["y"]=-24680.714285715,["x"]=-416003.14285713,}, - [4]={["y"]=-24681.857142858,["x"]=-415926.57142856,}, - [5]={["y"]=-29408.42857143,["x"]=-416016.57142856,}, - }, - [2] = { - [1]={["y"]=-28575.571428572,["x"]=-416303.14285713,}, - [2]={["y"]=-28575.571428572,["x"]=-416382.57142856,}, - [3]={["y"]=-25111.000000001,["x"]=-416309.7142857,}, - [4]={["y"]=-25111.000000001,["x"]=-416249.14285713,}, - [5]={["y"]=-28575.571428572,["x"]=-416303.7142857,}, - }, - [3] = { - [1]={["y"]=-29331.000000001,["x"]=-416275.42857141,}, - [2]={["y"]=-29259.000000001,["x"]=-416306.85714284,}, - [3]={["y"]=-28005.571428572,["x"]=-413449.7142857,}, - [4]={["y"]=-28068.714285715,["x"]=-413422.85714284,}, - [5]={["y"]=-29331.000000001,["x"]=-416275.7142857,}, - }, - [4] = { - [1]={["y"]=-29073.285714286,["x"]=-416386.57142856,}, - [2]={["y"]=-28997.285714286,["x"]=-416417.42857141,}, - [3]={["y"]=-27697.571428572,["x"]=-413464.57142856,}, - [4]={["y"]=-27767.857142858,["x"]=-413434.28571427,}, - [5]={["y"]=-29073.000000001,["x"]=-416386.85714284,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Creech = { - PointsBoundary = { - [1]={["y"]=-74522.714285715,["x"]=-360887.99999998,}, - [2]={["y"]=-74197,["x"]=-360556.57142855,}, - [3]={["y"]=-74402.714285715,["x"]=-359639.42857141,}, - [4]={["y"]=-74637,["x"]=-359279.42857141,}, - [5]={["y"]=-75759.857142857,["x"]=-359005.14285712,}, - [6]={["y"]=-75834.142857143,["x"]=-359045.14285712,}, - [7]={["y"]=-75902.714285714,["x"]=-359782.28571427,}, - [8]={["y"]=-76099.857142857,["x"]=-360399.42857141,}, - [9]={["y"]=-77314.142857143,["x"]=-360219.42857141,}, - [10]={["y"]=-77728.428571429,["x"]=-360445.14285713,}, - [11]={["y"]=-77585.571428571,["x"]=-360585.14285713,}, - [12]={["y"]=-76471.285714286,["x"]=-360819.42857141,}, - [13]={["y"]=-76325.571428571,["x"]=-360942.28571427,}, - [14]={["y"]=-74671.857142857,["x"]=-360927.7142857,}, - [15]={["y"]=-74522.714285714,["x"]=-360888.85714284,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=-74237.571428571,["x"]=-360591.7142857,}, - [2]={["y"]=-74234.428571429,["x"]=-360493.71428571,}, - [3]={["y"]=-77605.285714286,["x"]=-360399.14285713,}, - [4]={["y"]=-77608.714285715,["x"]=-360498.85714285,}, - [5]={["y"]=-74237.857142857,["x"]=-360591.7142857,}, - }, - [2] = { - [1]={["y"]=-75807.571428572,["x"]=-359073.42857142,}, - [2]={["y"]=-74770.142857144,["x"]=-360581.71428571,}, - [3]={["y"]=-74641.285714287,["x"]=-360585.42857142,}, - [4]={["y"]=-75734.142857144,["x"]=-359023.14285714,}, - [5]={["y"]=-75807.285714287,["x"]=-359073.42857142,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - GroomLake = { - PointsBoundary = { - [1]={["y"]=-88916.714285714,["x"]=-289102.28571425,}, - [2]={["y"]=-87023.571428572,["x"]=-290388.57142857,}, - [3]={["y"]=-85916.428571429,["x"]=-290674.28571428,}, - [4]={["y"]=-87645.000000001,["x"]=-286567.14285714,}, - [5]={["y"]=-88380.714285715,["x"]=-286388.57142857,}, - [6]={["y"]=-89670.714285715,["x"]=-283524.28571428,}, - [7]={["y"]=-89797.857142858,["x"]=-283567.14285714,}, - [8]={["y"]=-88635.000000001,["x"]=-286749.99999999,}, - [9]={["y"]=-89177.857142858,["x"]=-287207.14285714,}, - [10]={["y"]=-89092.142857144,["x"]=-288892.85714285,}, - [11]={["y"]=-88917.000000001,["x"]=-289102.85714285,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=-86039.000000001,["x"]=-290606.28571428,}, - [2]={["y"]=-85965.285714287,["x"]=-290573.99999999,}, - [3]={["y"]=-87692.714285715,["x"]=-286634.85714285,}, - [4]={["y"]=-87756.714285715,["x"]=-286663.99999999,}, - [5]={["y"]=-86038.714285715,["x"]=-290606.85714285,}, - }, - [2] = { - [1]={["y"]=-86808.428571429,["x"]=-290375.7142857,}, - [2]={["y"]=-86732.714285715,["x"]=-290344.28571427,}, - [3]={["y"]=-89672.714285714,["x"]=-283546.57142855,}, - [4]={["y"]=-89772.142857143,["x"]=-283587.71428569,}, - [5]={["y"]=-86808.142857143,["x"]=-290375.7142857,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - }, -} - ---- Creates a new AIRBASEPOLICE_NEVADA object. --- @param #AIRBASEPOLICE_NEVADA self --- @param SetClient A SET_CLIENT object that will contain the CLIENT objects to be monitored if they follow the rules of the airbase. --- @return #AIRBASEPOLICE_NEVADA self -function AIRBASEPOLICE_NEVADA:New( SetClient ) - - -- Inherits from BASE - local self = BASE:Inherit( self, AIRBASEPOLICE_BASE:New( SetClient, self.Airbases ) ) - --- -- Nellis --- local NellisBoundary = GROUP:FindByName( "Nellis Boundary" ) --- self.Airbases.Nellis.ZoneBoundary = ZONE_POLYGON:New( "Nellis Boundary", NellisBoundary ):SmokeZone(SMOKECOLOR.White):Flush() --- --- local NellisRunway1 = GROUP:FindByName( "Nellis Runway 1" ) --- self.Airbases.Nellis.ZoneRunways[1] = ZONE_POLYGON:New( "Nellis Runway 1", NellisRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- local NellisRunway2 = GROUP:FindByName( "Nellis Runway 2" ) --- self.Airbases.Nellis.ZoneRunways[2] = ZONE_POLYGON:New( "Nellis Runway 2", NellisRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- -- McCarran --- local McCarranBoundary = GROUP:FindByName( "McCarran Boundary" ) --- self.Airbases.McCarran.ZoneBoundary = ZONE_POLYGON:New( "McCarran Boundary", McCarranBoundary ):SmokeZone(SMOKECOLOR.White):Flush() --- --- local McCarranRunway1 = GROUP:FindByName( "McCarran Runway 1" ) --- self.Airbases.McCarran.ZoneRunways[1] = ZONE_POLYGON:New( "McCarran Runway 1", McCarranRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- local McCarranRunway2 = GROUP:FindByName( "McCarran Runway 2" ) --- self.Airbases.McCarran.ZoneRunways[2] = ZONE_POLYGON:New( "McCarran Runway 2", McCarranRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- local McCarranRunway3 = GROUP:FindByName( "McCarran Runway 3" ) --- self.Airbases.McCarran.ZoneRunways[3] = ZONE_POLYGON:New( "McCarran Runway 3", McCarranRunway3 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- local McCarranRunway4 = GROUP:FindByName( "McCarran Runway 4" ) --- self.Airbases.McCarran.ZoneRunways[4] = ZONE_POLYGON:New( "McCarran Runway 4", McCarranRunway4 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- -- Creech --- local CreechBoundary = GROUP:FindByName( "Creech Boundary" ) --- self.Airbases.Creech.ZoneBoundary = ZONE_POLYGON:New( "Creech Boundary", CreechBoundary ):SmokeZone(SMOKECOLOR.White):Flush() --- --- local CreechRunway1 = GROUP:FindByName( "Creech Runway 1" ) --- self.Airbases.Creech.ZoneRunways[1] = ZONE_POLYGON:New( "Creech Runway 1", CreechRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- local CreechRunway2 = GROUP:FindByName( "Creech Runway 2" ) --- self.Airbases.Creech.ZoneRunways[2] = ZONE_POLYGON:New( "Creech Runway 2", CreechRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- -- Groom Lake --- local GroomLakeBoundary = GROUP:FindByName( "GroomLake Boundary" ) --- self.Airbases.GroomLake.ZoneBoundary = ZONE_POLYGON:New( "GroomLake Boundary", GroomLakeBoundary ):SmokeZone(SMOKECOLOR.White):Flush() --- --- local GroomLakeRunway1 = GROUP:FindByName( "GroomLake Runway 1" ) --- self.Airbases.GroomLake.ZoneRunways[1] = ZONE_POLYGON:New( "GroomLake Runway 1", GroomLakeRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- local GroomLakeRunway2 = GROUP:FindByName( "GroomLake Runway 2" ) --- self.Airbases.GroomLake.ZoneRunways[2] = ZONE_POLYGON:New( "GroomLake Runway 2", GroomLakeRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - -end - - - - - - --- **Functional** - DETECTION_ classes model the detection of enemy units by FACs or RECCEs and group them according various methods. --- --- ![Banner Image](..\Presentations\DETECTION\Dia1.JPG) --- --- === --- --- DETECTION classes facilitate the detection of enemy units within the battle zone executed by FACs (Forward Air Controllers) or RECCEs (Reconnassance Units). --- DETECTION uses the in-built detection capabilities of DCS World, but adds new functionalities. --- --- Please watch this [youtube video](https://youtu.be/C7p81dUwP-E) that explains the detection concepts. --- --- --- ### Contributions: --- --- * Mechanist : Early concept of DETECTION_AREAS. --- --- ### Authors: --- --- * FlightControl : Analysis, Design, Programming, Testing --- --- @module Detection - - -do -- DETECTION_BASE - - --- # 1) DETECTION_BASE class, extends @{Fsm#FSM} - -- - -- The DETECTION_BASE class defines the core functions to administer detected objects. - -- The DETECTION_BASE class will detect objects within the battle zone for a list of @{Group}s detecting targets following (a) detection method(s). - -- - -- ## 1.1) DETECTION_BASE constructor - -- - -- Construct a new DETECTION_BASE instance using the @{#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_BASE.InitDetectVisual}(): Detected using Visual. - -- * @{#DETECTION_BASE.InitDetectOptical}(): Detected using Optical. - -- * @{#DETECTION_BASE.InitDetectRadar}(): Detected using Radar. - -- * @{#DETECTION_BASE.InitDetectIRST}(): Detected using IRST. - -- * @{#DETECTION_BASE.InitDetectRWR}(): Detected using RWR. - -- * @{#DETECTION_BASE.InitDetectDLINK}(): Detected using DLINK. - -- - -- ## 1.3) DETECTION_BASE derived classes group the detected units into a DetectedItems[] list - -- - -- DETECTION_BASE derived classes build a list called DetectedItems[], which is essentially a first later - -- of grouping of detected units. Each DetectedItem within the DetectedItems[] list contains - -- a SET_UNIT object that contains the detected units that belong to that group. - -- - -- Derived classes will apply different methods to group the detected units. - -- Examples are per area, per quadrant, per distance, per type. - -- See further the derived DETECTION classes on which grouping methods are currently supported. - -- - -- Various methods exist how to retrieve the grouped items from a DETECTION_BASE derived class: - -- - -- * The method @{Detection#DETECTION_BASE.GetDetectedItems}() retrieves the DetectedItems[] list. - -- * A DetectedItem from the DetectedItems[] list can be retrieved using the method @{Detection#DETECTION_BASE.GetDetectedItem}( DetectedItemIndex ). - -- Note that this method returns a DetectedItem element from the list, that contains a Set variable and further information - -- about the DetectedItem that is set by the DETECTION_BASE derived classes, used to group the DetectedItem. - -- * A DetectedSet from the DetectedItems[] list can be retrieved using the method @{Detection#DETECTION_BASE.GetDetectedSet}( DetectedItemIndex ). - -- This method retrieves the Set from a DetectedItem element from the DetectedItem list (DetectedItems[ DetectedItemIndex ].Set ). - -- - -- ## 1.4) Apply additional Filters to fine-tune the detected objects - -- - -- By default, DCS World will return any object that is in LOS and within "visual reach", or detectable through one of the electronic detection means. - -- That being said, the DCS World detection algorithm can sometimes be unrealistic. - -- Especially for a visual detection, DCS World is able to report within 1 second a detailed detection of a group of 20 units (including types of the units) that are 10 kilometers away, using only visual capabilities. - -- Additionally, trees and other obstacles are not accounted during the DCS World detection. - -- - -- Therefore, an additional (optional) filtering has been built into the DETECTION_BASE class, that can be set for visual detected units. - -- For electronic detection, this filtering is not applied, only for visually detected targets. - -- - -- The following additional filtering can be applied for visual filtering: - -- - -- * A probability factor per kilometer distance. - -- * A probability factor based on the alpha angle between the detected object and the unit detecting. - -- A detection from a higher altitude allows for better detection than when on the ground. - -- * Define a probability factor for "cloudy zones", which are zones where forests or villages are located. In these zones, detection will be much more difficult. - -- The mission designer needs to define these cloudy zones within the mission, and needs to register these zones in the DETECTION_ objects additing a probability factor per zone. - -- - -- I advise however, that, when you first use the DETECTION derived classes, that you don't use these filters. - -- Only when you experience unrealistic behaviour in your missions, these filters could be applied. - -- - -- ### 1.4.1 ) Distance visual detection probability - -- - -- Upon a **visual** detection, the further away a detected object is, the less likely it is to be detected properly. - -- Also, the speed of accurate detection plays a role. - -- - -- A distance probability factor between 0 and 1 can be given, that will model a linear extrapolated probability over 10 km distance. - -- - -- For example, if a probability factor of 0.6 (60%) is given, the extrapolated probabilities over 15 kilometers would like like: - -- 1 km: 96%, 2 km: 92%, 3 km: 88%, 4 km: 84%, 5 km: 80%, 6 km: 76%, 7 km: 72%, 8 km: 68%, 9 km: 64%, 10 km: 60%, 11 km: 56%, 12 km: 52%, 13 km: 48%, 14 km: 44%, 15 km: 40%. - -- - -- Note that based on this probability factor, not only the detection but also the **type** of the unit will be applied! - -- - -- Use the method @{Detection#DETECTION_BASE.SetDistanceProbability}() to set the probability factor upon a 10 km distance. - -- - -- ### 1.4.2 ) Alpha Angle visual detection probability - -- - -- Upon a **visual** detection, the higher the unit is during the detecting process, the more likely the detected unit is to be detected properly. - -- A detection at a 90% alpha angle is the most optimal, a detection at 10% is less and a detection at 0% is less likely to be correct. - -- - -- A probability factor between 0 and 1 can be given, that will model a progressive extrapolated probability if the target would be detected at a 0° angle. - -- - -- For example, if a alpha angle probability factor of 0.7 is given, the extrapolated probabilities of the different angles would look like: - -- 0°: 70%, 10°: 75,21%, 20°: 80,26%, 30°: 85%, 40°: 89,28%, 50°: 92,98%, 60°: 95,98%, 70°: 98,19%, 80°: 99,54%, 90°: 100% - -- - -- Use the method @{Detection#DETECTION_BASE.SetAlphaAngleProbability}() to set the probability factor if 0°. - -- - -- ### 1.4.3 ) Cloudy Zones detection probability - -- - -- Upon a **visual** detection, the more a detected unit is within a cloudy zone, the less likely the detected unit is to be detected successfully. - -- The Cloudy Zones work with the ZONE_BASE derived classes. The mission designer can define within the mission - -- zones that reflect cloudy areas where detected units may not be so easily visually detected. - -- - -- Use the method @{Detection#DETECTION_BASE.SetZoneProbability}() to set for a defined number of zones, the probability factors. - -- - -- Note however, that the more zones are defined to be "cloudy" within a detection, the more performance it will take - -- from the DETECTION_BASE to calculate the presence of the detected unit within each zone. - -- Expecially for ZONE_POLYGON, try to limit the amount of nodes of the polygon! - -- - -- Typically, this kind of filter would be applied for very specific areas were a detection needs to be very realisting for - -- AI not to detect so easily targets within a forrest or village rich area. - -- - -- ## 1.5 ) Accept / Reject detected units - -- - -- DETECTION_BASE can accept or reject successful detections based on the location of the detected object, - -- if it is located in range or located inside or outside of specific zones. - -- - -- ### 1.5.1 ) Detection acceptance of within range limit - -- - -- A range can be set that will limit a successful detection for a unit. - -- Use the method @{Detection#DETECTION_BASE.SetAcceptRange}() to apply a range in meters till where detected units will be accepted. - -- - -- local SetGroup = SET_GROUP:New():FilterPrefixes( "FAC" ):FilterStart() -- Build a SetGroup of Forward Air Controllers. - -- - -- -- Build a detect object. - -- local Detection = DETECTION_BASE:New( SetGroup ) - -- - -- -- This will accept detected units if the range is below 5000 meters. - -- Detection:SetAcceptRange( 5000 ) - -- - -- -- Start the Detection. - -- Detection:Start() - -- - -- - -- ### 1.5.2 ) Detection acceptance if within zone(s). - -- - -- Specific ZONE_BASE object(s) can be given as a parameter, which will only accept a detection if the unit is within the specified ZONE_BASE object(s). - -- Use the method @{Detection#DETECTION_BASE.SetAcceptZones}() will accept detected units if they are within the specified zones. - -- - -- local SetGroup = SET_GROUP:New():FilterPrefixes( "FAC" ):FilterStart() -- Build a SetGroup of Forward Air Controllers. - -- - -- -- Search fo the zones where units are to be accepted. - -- local ZoneAccept1 = ZONE:New( "AcceptZone1" ) - -- local ZoneAccept2 = ZONE:New( "AcceptZone2" ) - -- - -- -- Build a detect object. - -- local Detection = DETECTION_BASE:New( SetGroup ) - -- - -- -- This will accept detected units by Detection when the unit is within ZoneAccept1 OR ZoneAccept2. - -- Detection:SetAcceptZones( { ZoneAccept1, ZoneAccept2 } ) - -- - -- -- Start the Detection. - -- Detection:Start() - -- - -- ### 1.5.3 ) Detection rejectance if within zone(s). - -- - -- Specific ZONE_BASE object(s) can be given as a parameter, which will reject detection if the unit is within the specified ZONE_BASE object(s). - -- Use the method @{Detection#DETECTION_BASE.SetRejectZones}() will reject detected units if they are within the specified zones. - -- An example of how to use the method is shown below. - -- - -- local SetGroup = SET_GROUP:New():FilterPrefixes( "FAC" ):FilterStart() -- Build a SetGroup of Forward Air Controllers. - -- - -- -- Search fo the zones where units are to be rejected. - -- local ZoneReject1 = ZONE:New( "RejectZone1" ) - -- local ZoneReject2 = ZONE:New( "RejectZone2" ) - -- - -- -- Build a detect object. - -- local Detection = DETECTION_BASE:New( SetGroup ) - -- - -- -- This will reject detected units by Detection when the unit is within ZoneReject1 OR ZoneReject2. - -- Detection:SetRejectZones( { ZoneReject1, ZoneReject2 } ) - -- - -- -- Start the Detection. - -- Detection:Start() - -- - -- ## 1.6) DETECTION_BASE is a Finite State Machine - -- - -- Various Events and State Transitions can be tailored using DETECTION_BASE. - -- - -- ### 1.6.1) DETECTION_BASE States - -- - -- * **Detecting**: The detection is running. - -- * **Stopped**: The detection is stopped. - -- - -- ### 1.6.2) DETECTION_BASE Events - -- - -- * **Start**: Start the detection process. - -- * **Detect**: Detect new units. - -- * **Detected**: New units have been detected. - -- * **Stop**: Stop the detection process. - -- - -- @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.Fsm#FSM - DETECTION_BASE = { - ClassName = "DETECTION_BASE", - DetectionSetGroup = nil, - DetectionRange = nil, - DetectedObjects = {}, - DetectionRun = 0, - DetectedObjectsIdentified = {}, - DetectedItems = {}, - } - - --- @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 - - --- @type DETECTION_BASE.DetectedItems - -- @list <#DETECTION_BASE.DetectedItem> - - --- @type DETECTION_BASE.DetectedItem - -- @field Core.Set#SET_UNIT Set - -- @field Core.Set#SET_UNIT Set -- The Set of Units in the detected area. - -- @field Core.Zone#ZONE_UNIT Zone -- The Zone of the detected area. - -- @field #boolean Changed Documents if the detected area has changes. - -- @field #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 ItemID -- 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 constructor. - -- @param #DETECTION_BASE self - -- @param Core.Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Forward Air Controller role. - -- @return #DETECTION_BASE self - function DETECTION_BASE:New( DetectionSetGroup ) - - -- Inherits from BASE - local self = BASE:Inherit( self, FSM:New() ) -- #DETECTION_BASE - - self.DetectedItemCount = 0 - self.DetectedItemMax = 0 - self.DetectedItems = {} - - self.DetectionSetGroup = DetectionSetGroup - - self.DetectionInterval = 30 - - self:InitDetectVisual( true ) - self:InitDetectOptical( false ) - self:InitDetectRadar( false ) - self:InitDetectRWR( false ) - self:InitDetectIRST( false ) - self:InitDetectDLINK( false ) - - -- Create FSM transitions. - - self:SetStartState( "Stopped" ) - self.CountryID = DetectionSetGroup:GetFirst():GetCountry() - - self:AddTransition( "Stopped", "Start", "Detecting") - - --- OnLeave Transition Handler for State Stopped. - -- @function [parent=#DETECTION_BASE] OnLeaveStopped - -- @param #DETECTION_BASE self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnEnter Transition Handler for State Stopped. - -- @function [parent=#DETECTION_BASE] OnEnterStopped - -- @param #DETECTION_BASE self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- OnBefore Transition Handler for Event Start. - -- @function [parent=#DETECTION_BASE] OnBeforeStart - -- @param #DETECTION_BASE self - -- @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=#DETECTION_BASE] OnAfterStart - -- @param #DETECTION_BASE self - -- @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=#DETECTION_BASE] Start - -- @param #DETECTION_BASE self - - --- Asynchronous Event Trigger for Event Start. - -- @function [parent=#DETECTION_BASE] __Start - -- @param #DETECTION_BASE self - -- @param #number Delay The delay in seconds. - - --- OnLeave Transition Handler for State Detecting. - -- @function [parent=#DETECTION_BASE] OnLeaveDetecting - -- @param #DETECTION_BASE self - -- @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 Detecting. - -- @function [parent=#DETECTION_BASE] OnEnterDetecting - -- @param #DETECTION_BASE self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - self:AddTransition( "Detecting", "Detect", "Detecting" ) - self:AddTransition( "Detecting", "DetectionGroup", "Detecting" ) - - --- OnBefore Transition Handler for Event Detect. - -- @function [parent=#DETECTION_BASE] OnBeforeDetect - -- @param #DETECTION_BASE self - -- @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=#DETECTION_BASE] OnAfterDetect - -- @param #DETECTION_BASE self - -- @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=#DETECTION_BASE] Detect - -- @param #DETECTION_BASE self - - --- Asynchronous Event Trigger for Event Detect. - -- @function [parent=#DETECTION_BASE] __Detect - -- @param #DETECTION_BASE self - -- @param #number Delay The delay in seconds. - - - self:AddTransition( "Detecting", "Detected", "Detecting" ) - - --- OnBefore Transition Handler for Event Detected. - -- @function [parent=#DETECTION_BASE] OnBeforeDetected - -- @param #DETECTION_BASE self - -- @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=#DETECTION_BASE] OnAfterDetected - -- @param #DETECTION_BASE self - -- @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=#DETECTION_BASE] Detected - -- @param #DETECTION_BASE self - - --- Asynchronous Event Trigger for Event Detected. - -- @function [parent=#DETECTION_BASE] __Detected - -- @param #DETECTION_BASE self - -- @param #number Delay The delay in seconds. - - - self:AddTransition( "*", "Stop", "Stopped" ) - - --- OnBefore Transition Handler for Event Stop. - -- @function [parent=#DETECTION_BASE] OnBeforeStop - -- @param #DETECTION_BASE self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Stop. - -- @function [parent=#DETECTION_BASE] OnAfterStop - -- @param #DETECTION_BASE self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Stop. - -- @function [parent=#DETECTION_BASE] Stop - -- @param #DETECTION_BASE self - - --- Asynchronous Event Trigger for Event Stop. - -- @function [parent=#DETECTION_BASE] __Stop - -- @param #DETECTION_BASE self - -- @param #number Delay The delay in seconds. - - --- OnLeave Transition Handler for State Stopped. - -- @function [parent=#DETECTION_BASE] OnLeaveStopped - -- @param #DETECTION_BASE self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnEnter Transition Handler for State Stopped. - -- @function [parent=#DETECTION_BASE] OnEnterStopped - -- @param #DETECTION_BASE self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - return self - end - - do -- State Transition Handling - - --- @param #DETECTION_BASE self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - function DETECTION_BASE:onafterStart(From,Event,To) - self:__Detect(0.1) - end - - --- @param #DETECTION_BASE self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - function DETECTION_BASE:onafterDetect(From,Event,To) - self:E( {From,Event,To}) - - local DetectDelay = 0.1 - self.DetectionCount = 0 - self.DetectionRun = 0 - self:UnIdentifyAllDetectedObjects() -- Resets the DetectedObjectsIdentified table - - self.DetectionSetGroup:Flush() - - for DetectionGroupID, DetectionGroupData in pairs( self.DetectionSetGroup:GetSet() ) do - self:E( {DetectionGroupData}) - self:__DetectionGroup( DetectDelay, DetectionGroupData ) -- Process each detection asynchronously. - self.DetectionCount = self.DetectionCount + 1 - DetectDelay = DetectDelay + 0.1 - end - end - - --- @param #DETECTION_BASE self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @param Wrapper.Group#GROUP DetectionGroup The Group detecting. - function DETECTION_BASE:onafterDetectionGroup( From, Event, To, DetectionGroup ) - self:E( {From,Event,To}) - - self.DetectionRun = self.DetectionRun + 1 - - local HasDetectedObjects = false - - if DetectionGroup:IsAlive() then - - self:T( { "DetectionGroup is Alive", DetectionGroup:GetName() } ) - - local DetectionGroupName = DetectionGroup:GetName() - - local DetectedUnits = {} - - local DetectedTargets = DetectionGroup:GetDetectedTargets( - self.DetectVisual, - self.DetectOptical, - self.DetectRadar, - self.DetectIRST, - self.DetectRWR, - self.DetectDLINK - ) - - self:T( DetectedTargets ) - - for DetectionObjectID, Detection in pairs( DetectedTargets ) do - local DetectedObject = Detection.object -- Dcs.DCSWrapper.Object#Object - self:T2( DetectedObject ) - - if DetectedObject and DetectedObject:isExist() and DetectedObject.id_ < 50000000 then - - local DetectionAccepted = true - - local DetectedObjectName = DetectedObject:getName() - - local DetectedObjectVec3 = DetectedObject:getPoint() - local DetectedObjectVec2 = { x = DetectedObjectVec3.x, y = DetectedObjectVec3.z } - local DetectionGroupVec3 = DetectionGroup:GetVec3() - local DetectionGroupVec2 = { x = DetectionGroupVec3.x, y = DetectionGroupVec3.z } - - local Distance = ( ( DetectedObjectVec3.x - DetectionGroupVec3.x )^2 + - ( DetectedObjectVec3.y - DetectionGroupVec3.y )^2 + - ( DetectedObjectVec3.z - DetectionGroupVec3.z )^2 - ) ^ 0.5 / 1000 - - self:T( { "Detected Target", DetectionGroupName, DetectedObjectName, Distance } ) - - -- Calculate Acceptance - - if self.AcceptRange and Distance > self.AcceptRange then - DetectionAccepted = false - end - - if self.AcceptZones then - for AcceptZoneID, AcceptZone in pairs( self.AcceptZones ) do - local AcceptZone = AcceptZone -- Core.Zone#ZONE_BASE - if AcceptZone:IsPointVec2InZone( DetectedObjectVec2 ) == false then - DetectionAccepted = false - end - end - end - - if self.RejectZones then - for RejectZoneID, RejectZone in pairs( self.RejectZones ) do - local RejectZone = RejectZone -- Core.Zone#ZONE_BASE - if RejectZone:IsPointVec2InZone( DetectedObjectVec2 ) == true then - DetectionAccepted = false - end - end - end - - -- Calculate additional probabilities - - if not self.DetectedObjects[DetectedObjectName] and Detection.visible and self.DistanceProbability then - local DistanceFactor = Distance / 4 - local DistanceProbabilityReversed = ( 1 - self.DistanceProbability ) * DistanceFactor - local DistanceProbability = 1 - DistanceProbabilityReversed - DistanceProbability = DistanceProbability * 30 / 300 - local Probability = math.random() -- Selects a number between 0 and 1 - self:T( { Probability, DistanceProbability } ) - if Probability > DistanceProbability then - DetectionAccepted = false - end - end - - if not self.DetectedObjects[DetectedObjectName] and Detection.visible and self.AlphaAngleProbability then - local NormalVec2 = { x = DetectedObjectVec2.x - DetectionGroupVec2.x, y = DetectedObjectVec2.y - DetectionGroupVec2.y } - local AlphaAngle = math.atan2( NormalVec2.y, NormalVec2.x ) - local Sinus = math.sin( AlphaAngle ) - local AlphaAngleProbabilityReversed = ( 1 - self.AlphaAngleProbability ) * ( 1 - Sinus ) - local AlphaAngleProbability = 1 - AlphaAngleProbabilityReversed - - AlphaAngleProbability = AlphaAngleProbability * 30 / 300 - - local Probability = math.random() -- Selects a number between 0 and 1 - self:T( { Probability, AlphaAngleProbability } ) - if Probability > AlphaAngleProbability then - DetectionAccepted = false - end - - end - - if not self.DetectedObjects[DetectedObjectName] and Detection.visible and self.ZoneProbability then - - for ZoneDataID, ZoneData in pairs( self.ZoneProbability ) do - self:E({ZoneData}) - local ZoneObject = ZoneData[1] -- Core.Zone#ZONE_BASE - local ZoneProbability = ZoneData[2] -- #number - ZoneProbability = ZoneProbability * 30 / 300 - - if ZoneObject:IsPointVec2InZone( DetectedObjectVec2 ) == true then - local Probability = math.random() -- Selects a number between 0 and 1 - self:T( { Probability, ZoneProbability } ) - if Probability > ZoneProbability then - DetectionAccepted = false - break - end - end - end - end - - if DetectionAccepted then - - HasDetectedObjects = true - - if not self.DetectedObjects[DetectedObjectName] then - self.DetectedObjects[DetectedObjectName] = {} - end - self.DetectedObjects[DetectedObjectName].Name = DetectedObjectName - self.DetectedObjects[DetectedObjectName].Visible = Detection.visible - self.DetectedObjects[DetectedObjectName].Type = Detection.type - self.DetectedObjects[DetectedObjectName].Distance = Distance - - local DetectedUnit = UNIT:FindByName( DetectedObjectName ) - - DetectedUnits[DetectedObjectName] = DetectedUnit - else - -- if beyond the DetectionRange then nullify... - if self.DetectedObjects[DetectedObjectName] then - self.DetectedObjects[DetectedObjectName] = nil - end - end - end - - self:T2( self.DetectedObjects ) - end - - if HasDetectedObjects then - self:__Detected( 0.1, DetectedUnits ) - end - - end - - if self.DetectionCount > 0 and self.DetectionRun == self.DetectionCount then - self:__Detect( self.DetectionInterval ) - - self:T( "--> Create Detection Sets" ) - self:CreateDetectionSets() - end - - end - - - end - - do -- Initialization methods - - --- 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 - - end - - do - - --- Set the detection interval time in seconds. - -- @param #DETECTION_BASE self - -- @param #number DetectionInterval Interval in seconds. - -- @return #DETECTION_BASE self - function DETECTION_BASE:SetDetectionInterval( DetectionInterval ) - self:F2() - - self.DetectionInterval = DetectionInterval - - return self - end - - end - - do -- Accept / Reject detected units - - --- Accept detections if within a range in meters. - -- @param #DETECTION_BASE self - -- @param #number AcceptRange Accept a detection if the unit is within the AcceptRange in meters. - -- @return #DETECTION_BASE self - function DETECTION_BASE:SetAcceptRange( AcceptRange ) - self:F2() - - self.AcceptRange = AcceptRange - - return self - end - - --- Accept detections if within the specified zone(s). - -- @param #DETECTION_BASE self - -- @param AcceptZones Can be a list or ZONE_BASE objects, or a single ZONE_BASE object. - -- @return #DETECTION_BASE self - function DETECTION_BASE:SetAcceptZones( AcceptZones ) - self:F2() - - if type( AcceptZones ) == "table" then - self.AcceptZones = AcceptZones - else - self.AcceptZones = { AcceptZones } - end - - return self - end - - --- Reject detections if within the specified zone(s). - -- @param #DETECTION_BASE self - -- @param RejectZones Can be a list or ZONE_BASE objects, or a single ZONE_BASE object. - -- @return #DETECTION_BASE self - function DETECTION_BASE:SetRejectZones( RejectZones ) - self:F2() - - if type( RejectZones ) == "table" then - self.RejectZones = RejectZones - else - self.RejectZones = { RejectZones } - end - - return self - end - - end - - do -- Probability methods - - --- Upon a **visual** detection, the further away a detected object is, the less likely it is to be detected properly. - -- Also, the speed of accurate detection plays a role. - -- A distance probability factor between 0 and 1 can be given, that will model a linear extrapolated probability over 10 km distance. - -- For example, if a probability factor of 0.6 (60%) is given, the extrapolated probabilities over 15 kilometers would like like: - -- 1 km: 96%, 2 km: 92%, 3 km: 88%, 4 km: 84%, 5 km: 80%, 6 km: 76%, 7 km: 72%, 8 km: 68%, 9 km: 64%, 10 km: 60%, 11 km: 56%, 12 km: 52%, 13 km: 48%, 14 km: 44%, 15 km: 40%. - -- @param #DETECTION_BASE self - -- @param DistanceProbability The probability factor. - -- @return #DETECTION_BASE self - function DETECTION_BASE:SetDistanceProbability( DistanceProbability ) - self:F2() - - self.DistanceProbability = DistanceProbability - - return self - end - - - --- Upon a **visual** detection, the higher the unit is during the detecting process, the more likely the detected unit is to be detected properly. - -- A detection at a 90% alpha angle is the most optimal, a detection at 10% is less and a detection at 0% is less likely to be correct. - -- - -- A probability factor between 0 and 1 can be given, that will model a progressive extrapolated probability if the target would be detected at a 0° angle. - -- - -- For example, if a alpha angle probability factor of 0.7 is given, the extrapolated probabilities of the different angles would look like: - -- 0°: 70%, 10°: 75,21%, 20°: 80,26%, 30°: 85%, 40°: 89,28%, 50°: 92,98%, 60°: 95,98%, 70°: 98,19%, 80°: 99,54%, 90°: 100% - -- @param #DETECTION_BASE self - -- @param AlphaAngleProbability The probability factor. - -- @return #DETECTION_BASE self - function DETECTION_BASE:SetAlphaAngleProbability( AlphaAngleProbability ) - self:F2() - - self.AlphaAngleProbability = AlphaAngleProbability - - return self - end - - --- Upon a **visual** detection, the more a detected unit is within a cloudy zone, the less likely the detected unit is to be detected successfully. - -- The Cloudy Zones work with the ZONE_BASE derived classes. The mission designer can define within the mission - -- zones that reflect cloudy areas where detected units may not be so easily visually detected. - -- @param #DETECTION_BASE self - -- @param ZoneArray Aray of a The ZONE_BASE object and a ZoneProbability pair.. - -- @return #DETECTION_BASE self - function DETECTION_BASE:SetZoneProbability( ZoneArray ) - self:F2() - - self.ZoneProbability = ZoneArray - - return self - end - - - end - - do -- Change processing - - --- Accepts changes from the detected item. - -- @param #DETECTION_BASE self - -- @param #DETECTION_BASE.DetectedItem DetectedItem - -- @return #DETECTION_BASE self - function DETECTION_BASE:AcceptChanges( DetectedItem ) - - DetectedItem.Changed = false - DetectedItem.Changes = {} - - return self - end - - --- Add a change to the detected zone. - -- @param #DETECTION_BASE self - -- @param #DETECTION_BASE.DetectedItem DetectedItem - -- @param #string ChangeCode - -- @return #DETECTION_BASE self - function DETECTION_BASE:AddChangeItem( DetectedItem, ChangeCode, ItemUnitType ) - - DetectedItem.Changed = true - local ItemID = DetectedItem.ItemID - - DetectedItem.Changes = DetectedItem.Changes or {} - DetectedItem.Changes[ChangeCode] = DetectedItem.Changes[ChangeCode] or {} - DetectedItem.Changes[ChangeCode].ItemID = ItemID - DetectedItem.Changes[ChangeCode].ItemUnitType = ItemUnitType - - self:T( { "Change on Detection Item:", DetectedItem.ItemID, ChangeCode, ItemUnitType } ) - - return self - end - - - --- Add a change to the detected zone. - -- @param #DETECTION_BASE self - -- @param #DETECTION_BASE.DetectedItem DetectedItem - -- @param #string ChangeCode - -- @param #string ChangeUnitType - -- @return #DETECTION_BASE self - function DETECTION_BASE:AddChangeUnit( DetectedItem, ChangeCode, ChangeUnitType ) - - DetectedItem.Changed = true - local ItemID = DetectedItem.ItemID - - DetectedItem.Changes = DetectedItem.Changes or {} - DetectedItem.Changes[ChangeCode] = DetectedItem.Changes[ChangeCode] or {} - DetectedItem.Changes[ChangeCode][ChangeUnitType] = DetectedItem.Changes[ChangeCode][ChangeUnitType] or 0 - DetectedItem.Changes[ChangeCode][ChangeUnitType] = DetectedItem.Changes[ChangeCode][ChangeUnitType] + 1 - DetectedItem.Changes[ChangeCode].ItemID = ItemID - - self:T( { "Change on Detection Item:", DetectedItem.ItemID, ChangeCode, ChangeUnitType } ) - - return self - end - - - end - - do -- Threat - - --- Returns if there are friendlies nearby the FAC units ... - -- @param #DETECTION_BASE self - -- @return #boolean trhe if there are friendlies nearby - function DETECTION_BASE:IsFriendliesNearBy( DetectedItem ) - - self:T3( DetectedItem.FriendliesNearBy ) - return DetectedItem.FriendliesNearBy or false - end - - --- Background worker function to determine if there are friendlies nearby ... - -- @param #DETECTION_BASE self - function DETECTION_BASE:ReportFriendliesNearBy( ReportGroupData ) - self:F2() - - local DetectedItem = ReportGroupData.DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem - local DetectedSet = ReportGroupData.DetectedItem.Set - local DetectedUnit = DetectedSet:GetFirst() - - DetectedItem.FriendliesNearBy = false - - if DetectedUnit then - - - local SphereSearch = { - id = world.VolumeType.SPHERE, - params = { - point = DetectedUnit: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 DetectedItem = ReportGroupData.DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem - local DetectedSet = ReportGroupData.DetectedItem.Set - local DetectedUnit = DetectedSet:GetFirst() -- Wrapper.Unit#UNIT - local ReportSetGroup = ReportGroupData.ReportSetGroup - - local EnemyCoalition = DetectedUnit:GetCoalition() - - local FoundUnitCoalition = FoundDCSUnit:getCoalition() - local FoundUnitName = FoundDCSUnit:getName() - local FoundUnitGroupName = FoundDCSUnit:getGroup():getName() - local EnemyUnitName = DetectedUnit:GetName() - local FoundUnitInReportSetGroup = ReportSetGroup:FindGroup( FoundUnitGroupName ) ~= nil - - self:T3( { "Friendlies search:", FoundUnitName, FoundUnitCoalition, EnemyUnitName, EnemyCoalition, FoundUnitInReportSetGroup } ) - - if FoundUnitCoalition ~= EnemyCoalition and FoundUnitInReportSetGroup == false then - DetectedItem.FriendliesNearBy = true - return false - end - - return true - end - - world.searchObjects( Object.Category.UNIT, SphereSearch, FindNearByFriendlies, ReportGroupData ) - end - end - - 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( { "Identified:", 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:F( 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 - - - --- Adds a new DetectedItem to the DetectedItems list. - -- The DetectedItem is a table and contains a SET_UNIT in the field Set. - -- @param #DETECTION_BASE self - -- @param #string DetectedItemIndex The index of the DetectedItem. - -- @param Core.Set#SET_UNIT Set (optional) The Set of Units to be added. - -- @return #DETECTION_BASE.DetectedItem - function DETECTION_BASE:AddDetectedItem( DetectedItemIndex, Set ) - - local DetectedItem = {} - self.DetectedItemCount = self.DetectedItemCount + 1 - self.DetectedItemMax = self.DetectedItemMax + 1 - - if DetectedItemIndex then - self.DetectedItems[DetectedItemIndex] = DetectedItem - else - self.DetectedItems[self.DetectedItemCount] = DetectedItem - end - - DetectedItem.Set = Set or SET_UNIT:New() - DetectedItem.ItemID = self.DetectedItemMax - DetectedItem.Removed = false - - return DetectedItem - end - - --- Adds a new DetectedItem to the DetectedItems list. - -- The DetectedItem is a table and contains a SET_UNIT in the field Set. - -- @param #DETECTION_BASE self - -- @param #string DetectedItemIndex The index of the DetectedItem. - -- @param Core.Set#SET_UNIT Set (optional) The Set of Units to be added. - -- @param Core.Zone#ZONE_UNIT Zone (optional) The Zone to be added where the Units are located. - -- @return #DETECTION_BASE.DetectedItem - function DETECTION_BASE:AddDetectedItemZone( DetectedItemIndex, Set, Zone ) - - local DetectedItem = self:AddDetectedItem( DetectedItemIndex, Set ) - - DetectedItem.Zone = Zone - - return DetectedItem - end - - --- Removes an existing DetectedItem from the DetectedItems list. - -- The DetectedItem is a table and contains a SET_UNIT in the field Set. - -- @param #DETECTION_BASE self - -- @param #number DetectedItemIndex The index or position in the DetectedItems list where the item needs to be removed. - function DETECTION_BASE:RemoveDetectedItem( DetectedItemIndex ) - - self.DetectedItemCount = self.DetectedItemCount - 1 - self.DetectedItems[DetectedItemIndex] = nil - end - - - --- Get the detected @{Set#SET_BASE}s. - -- @param #DETECTION_BASE self - -- @return #DETECTION_BASE.DetectedItems - function DETECTION_BASE:GetDetectedItems() - - return self.DetectedItems - end - - --- Get the amount of SETs with detected objects. - -- @param #DETECTION_BASE self - -- @return #number Count - function DETECTION_BASE:GetDetectedItemsCount() - - local DetectedCount = self.DetectedItemCount - return DetectedCount - end - - --- Get a detected item using a given numeric index. - -- @param #DETECTION_BASE self - -- @param #number Index - -- @return #DETECTION_BASE.DetectedItem - function DETECTION_BASE:GetDetectedItem( Index ) - - local DetectedItem = self.DetectedItems[Index] - if DetectedItem then - return DetectedItem - end - - return nil - end - - --- Get the @{Set#SET_UNIT} of a detecttion area using a given numeric index. - -- @param #DETECTION_BASE self - -- @param #number Index - -- @return Core.Set#SET_UNIT DetectedSet - function DETECTION_BASE:GetDetectedSet( Index ) - - local DetectedItem = self:GetDetectedItem( Index ) - local DetectedSetUnit = DetectedItem.Set - if DetectedSetUnit then - return DetectedSetUnit - end - - return nil - end - - do -- Zones - - --- Get the @{Zone#ZONE_UNIT} of a detection area using a given numeric index. - -- @param #DETECTION_BASE self - -- @param #number Index - -- @return Core.Zone#ZONE_UNIT DetectedZone - function DETECTION_BASE:GetDetectedZone( Index ) - - local DetectedZone = self.DetectedItems[Index].Zone - if DetectedZone then - return DetectedZone - end - - return nil - end - - end - - - --- Report summary of a detected item using a given numeric index. - -- @param #DETECTION_BASE self - -- @param Index - -- @return #string - function DETECTION_BASE:DetectedItemReportSummary( Index ) - self:F( Index ) - return nil - end - - --- Report detailed of a detectedion result. - -- @param #DETECTION_BASE self - -- @return #string - function DETECTION_BASE:DetectedReportDetailed() - self:F() - 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 - -end - -do -- DETECTION_UNITS - - --- # 2) DETECTION_UNITS class, extends @{Detection#DETECTION_BASE} - -- - -- The DETECTION_UNITS class will detect units within the battle zone. - -- It will build a DetectedItems list filled with DetectedItems. Each DetectedItem will contain a field Set, which contains a @{Set#SET_UNIT} containing ONE @{UNIT} object reference. - -- Beware that when the amount of units detected is large, the DetectedItems list will be large also. - -- - -- @type DETECTION_UNITS - -- @field Dcs.DCSTypes#Distance DetectionRange The range till which targets are detected. - -- @extends #DETECTION_BASE - DETECTION_UNITS = { - ClassName = "DETECTION_UNITS", - DetectionRange = nil, - } - - --- DETECTION_UNITS constructor. - -- @param Functional.Detection#DETECTION_UNITS self - -- @param Core.Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Forward Air Controller role. - -- @return Functional.Detection#DETECTION_UNITS self - function DETECTION_UNITS:New( DetectionSetGroup ) - - -- Inherits from DETECTION_BASE - local self = BASE:Inherit( self, DETECTION_BASE:New( DetectionSetGroup ) ) -- #DETECTION_UNITS - - self._SmokeDetectedUnits = false - self._FlareDetectedUnits = false - self._SmokeDetectedZones = false - self._FlareDetectedZones = false - self._BoundDetectedZones = false - - return self - end - - --- Make text documenting the changes of the detected zone. - -- @param #DETECTION_UNITS self - -- @param #DETECTION_UNITS.DetectedItem DetectedItem - -- @return #string The Changes text - function DETECTION_UNITS:GetChangeText( DetectedItem ) - self:F( DetectedItem ) - - local MT = {} - - for ChangeCode, ChangeData in pairs( DetectedItem.Changes ) do - - if ChangeCode == "AU" then - local MTUT = {} - for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do - if ChangeUnitType ~= "ItemID" then - MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType - end - end - MT[#MT+1] = " New target(s) detected: " .. table.concat( MTUT, ", " ) .. "." - end - - if ChangeCode == "RU" then - local MTUT = {} - for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do - if ChangeUnitType ~= "ItemID" then - MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType - end - end - MT[#MT+1] = " Invisible or destroyed target(s): " .. table.concat( MTUT, ", " ) .. "." - end - - end - - return table.concat( MT, "\n" ) - - end - - - --- Create the DetectedItems list from the DetectedObjects table. - -- For each DetectedItem, a one field array is created containing the Unit detected. - -- @param #DETECTION_UNITS self - -- @return #DETECTION_UNITS self - function DETECTION_UNITS:CreateDetectionSets() - self:F2( #self.DetectedObjects ) - - -- Loop the current detected items, and check if each object still exists and is detected. - - for DetectedItemID, DetectedItem in pairs( self.DetectedItems ) do - - local DetectedItemSet = DetectedItem.Set -- Core.Set#SET_UNIT - local DetectedTypeName = DetectedItem.Type - - for DetectedUnitName, DetectedUnitData in pairs( DetectedItemSet:GetSet() ) do - local DetectedUnit = DetectedUnitData -- Wrapper.Unit#UNIT - - local DetectedObject = nil - self:E( DetectedUnit ) - if DetectedUnit:IsAlive() then - --self:E(DetectedUnit:GetName()) - DetectedObject = self:GetDetectedObject( DetectedUnit:GetName() ) - end - if DetectedObject then - - -- Yes, the DetectedUnit is still detected or exists. Flag as identified. - self:IdentifyDetectedObject( DetectedObject ) - else - -- There was no DetectedObject, remove DetectedUnit from the Set. - self:AddChangeUnit( DetectedItem, "RU", DetectedUnitName ) - DetectedItemSet:Remove( DetectedUnitName ) - end - end - end - - - -- Now we need to loop through the unidentified detected units and add these... These are all new items. - for DetectedUnitName, DetectedObjectData in pairs( self.DetectedObjects ) do - - local DetectedObject = self:GetDetectedObject( DetectedUnitName ) - if DetectedObject then - self:T( { "Detected Unit #", DetectedUnitName } ) - - local DetectedUnit = UNIT:FindByName( DetectedUnitName ) -- Wrapper.Unit#UNIT - - if DetectedUnit then - local DetectedTypeName = DetectedUnit:GetTypeName() - local DetectedItem = self:GetDetectedItem( DetectedUnitName ) - if not DetectedItem then - self:T( "Added new DetectedItem" ) - DetectedItem = self:AddDetectedItem( DetectedUnitName ) - DetectedItem.Type = DetectedUnit:GetTypeName() - DetectedItem.Name = DetectedObjectData.Name - DetectedItem.Visible = DetectedObjectData.Visible - DetectedItem.Distance = DetectedObjectData.Distance - end - - DetectedItem.Set:AddUnit( DetectedUnit ) - self:AddChangeUnit( DetectedItem, "AU", DetectedTypeName ) - end - end - end - - for DetectedItemID, DetectedItemData in pairs( self.DetectedItems ) do - - local DetectedItem = DetectedItemData -- #DETECTION_BASE.DetectedItem - local DetectedSet = DetectedItem.Set - - self:ReportFriendliesNearBy( { DetectedItem = DetectedItem, ReportSetGroup = self.DetectionSetGroup } ) -- Fill the Friendlies table - --self:NearestFAC( DetectedItem ) - end - - end - - --- Report summary of a DetectedItem using a given numeric index. - -- @param #DETECTION_UNITS self - -- @param Index - -- @return #string - function DETECTION_UNITS:DetectedItemReportSummary( Index ) - self:F( Index ) - - local DetectedItem = self:GetDetectedItem( Index ) - local DetectedSet = self:GetDetectedSet( Index ) - - self:T( DetectedSet ) - if DetectedSet then - local ReportSummary = "" - local UnitDistanceText = "" - local UnitCategoryText = "" - - local DetectedItemUnit = DetectedSet:GetFirst() -- Wrapper.Unit#UNIT - - if DetectedItemUnit and DetectedItemUnit:IsAlive() then - self:T(DetectedItemUnit) - - local UnitCategoryName = DetectedItemUnit:GetCategoryName() or "" - local UnitCategoryType = DetectedItemUnit:GetTypeName() or "" - - if DetectedItem.Type and UnitCategoryName and UnitCategoryType then - UnitCategoryText = UnitCategoryName .. " (" .. UnitCategoryType .. ") at " - else - UnitCategoryText = "Unknown target at " - end - - if DetectedItem.Visible == false then - UnitDistanceText = string.format( "%.2f", DetectedItem.Distance ) .. " estimated km" - else - UnitDistanceText = string.format( "%.2f", DetectedItem.Distance ) .. " km, visual contact" - end - - local DetectedItemPointVec3 = DetectedItemUnit:GetPointVec3() - local DetectedItemPointLL = DetectedItemPointVec3:ToStringLL( 3, true ) - - local ThreatLevelA2G = DetectedItemUnit:GetThreatLevel( DetectedItem ) - - ReportSummary = string.format( - "%s - Threat [%s] (%2d) - %s%s", - DetectedItemPointLL, - string.rep( "â– ", ThreatLevelA2G ), - ThreatLevelA2G, - UnitCategoryText, - UnitDistanceText - ) - end - - self:T( ReportSummary ) - - return ReportSummary - end - end - - --- Report detailed of a detection result. - -- @param #DETECTION_UNITS self - -- @return #string - function DETECTION_UNITS:DetectedReportDetailed() - self:F() - - local Report = REPORT:New( "Detected units:" ) - for DetectedItemID, DetectedItem in ipairs( self.DetectedItems ) do - local DetectedItem = DetectedItem -- #DETECTION_BASE.DetectedItem - local ReportSummary = self:DetectedItemReportSummary( DetectedItemID ) - Report:Add( ReportSummary ) - end - - local ReportText = Report:Text() - - return ReportText - end - -end - -do -- DETECTION_TYPES - - --- # 3) DETECTION_TYPES class, extends @{Detection#DETECTION_BASE} - -- - -- The DETECTION_TYPES class will detect units within the battle zone. - -- It will build a DetectedItems[] list filled with DetectedItems, grouped by the type of units detected. - -- Each DetectedItem will contain a field Set, which contains a @{Set#SET_UNIT} containing ONE @{UNIT} object reference. - -- Beware that when the amount of different types detected is large, the DetectedItems[] list will be large also. - -- - -- @type DETECTION_TYPES - -- @extends #DETECTION_BASE - DETECTION_TYPES = { - ClassName = "DETECTION_TYPES", - DetectionRange = nil, - } - - --- DETECTION_TYPES constructor. - -- @param Functional.Detection#DETECTION_TYPES self - -- @param Core.Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Recce role. - -- @return Functional.Detection#DETECTION_TYPES self - function DETECTION_TYPES:New( DetectionSetGroup ) - - -- Inherits from DETECTION_BASE - local self = BASE:Inherit( self, DETECTION_BASE:New( DetectionSetGroup ) ) -- #DETECTION_TYPES - - self._SmokeDetectedUnits = false - self._FlareDetectedUnits = false - self._SmokeDetectedZones = false - self._FlareDetectedZones = false - self._BoundDetectedZones = false - - return self - end - - --- Make text documenting the changes of the detected zone. - -- @param #DETECTION_TYPES self - -- @param #DETECTION_TYPES.DetectedItem DetectedItem - -- @return #string The Changes text - function DETECTION_TYPES:GetChangeText( DetectedItem ) - self:F( DetectedItem ) - - local MT = {} - - for ChangeCode, ChangeData in pairs( DetectedItem.Changes ) do - - if ChangeCode == "AU" then - local MTUT = {} - for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do - if ChangeUnitType ~= "ItemID" then - MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType - end - end - MT[#MT+1] = " New target(s) detected: " .. table.concat( MTUT, ", " ) .. "." - end - - if ChangeCode == "RU" then - local MTUT = {} - for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do - if ChangeUnitType ~= "ItemID" then - MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType - end - end - MT[#MT+1] = " Invisible or destroyed target(s): " .. table.concat( MTUT, ", " ) .. "." - end - - end - - return table.concat( MT, "\n" ) - - end - - - --- Create the DetectedItems list from the DetectedObjects table. - -- For each DetectedItem, a one field array is created containing the Unit detected. - -- @param #DETECTION_TYPES self - -- @return #DETECTION_TYPES self - function DETECTION_TYPES:CreateDetectionSets() - self:F2( #self.DetectedObjects ) - - -- Loop the current detected items, and check if each object still exists and is detected. - - for DetectedItemID, DetectedItem in pairs( self.DetectedItems ) do - - local DetectedItemSet = DetectedItem.Set -- Core.Set#SET_UNIT - local DetectedTypeName = DetectedItem.Type - - for DetectedUnitName, DetectedUnitData in pairs( DetectedItemSet: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 - - -- Yes, the DetectedUnit is still detected or exists. Flag as identified. - self:IdentifyDetectedObject( DetectedObject ) - else - -- There was no DetectedObject, remove DetectedUnit from the Set. - self:AddChangeUnit( DetectedItem, "RU", DetectedUnitName ) - DetectedItemSet:Remove( DetectedUnitName ) - end - end - end - - - -- Now we need to loop through the unidentified detected units and add these... These are all new items. - for DetectedUnitName, DetectedObjectData in pairs( self.DetectedObjects ) do - - local DetectedObject = self:GetDetectedObject( DetectedUnitName ) - if DetectedObject then - self:T( { "Detected Unit #", DetectedUnitName } ) - - local DetectedUnit = UNIT:FindByName( DetectedUnitName ) -- Wrapper.Unit#UNIT - - if DetectedUnit then - local DetectedTypeName = DetectedUnit:GetTypeName() - local DetectedItem = self:GetDetectedItem( DetectedTypeName ) - if not DetectedItem then - DetectedItem = self:AddDetectedItem( DetectedTypeName ) - DetectedItem.Type = DetectedUnit:GetTypeName() - end - - DetectedItem.Set:AddUnit( DetectedUnit ) - self:AddChangeUnit( DetectedItem, "AU", DetectedTypeName ) - end - end - end - - for DetectedItemID, DetectedItemData in pairs( self.DetectedItems ) do - - local DetectedItem = DetectedItemData -- #DETECTION_BASE.DetectedItem - local DetectedSet = DetectedItem.Set - - self:ReportFriendliesNearBy( { DetectedItem = DetectedItem, ReportSetGroup = self.DetectionSetGroup } ) -- Fill the Friendlies table - --self:NearestFAC( DetectedItem ) - end - - end - - --- Report summary of a DetectedItem using a given numeric index. - -- @param #DETECTION_TYPES self - -- @param Index - -- @return #string - function DETECTION_TYPES:DetectedItemReportSummary( DetectedTypeName ) - self:F( DetectedTypeName ) - - local DetectedItem = self:GetDetectedItem( DetectedTypeName ) - local DetectedSet = self:GetDetectedSet( DetectedTypeName ) - - self:T( DetectedItem ) - if DetectedItem then - - local ThreatLevelA2G = DetectedSet:CalculateThreatLevelA2G() - local DetectedItemsCount = DetectedSet:Count() - local DetectedItemType = DetectedItem.Type - - local ReportSummary = string.format( - "Threat [%s] (%2d) - %2d of %s", - string.rep( "â– ", ThreatLevelA2G ), - ThreatLevelA2G, - DetectedItemsCount, - DetectedItemType - ) - self:T( ReportSummary ) - - return ReportSummary - end - end - - --- Report detailed of a detection result. - -- @param #DETECTION_TYPES self - -- @return #string - function DETECTION_TYPES:DetectedReportDetailed() - self:F() - - local Report = REPORT:New( "Detected types:" ) - for DetectedItemTypeName, DetectedItem in pairs( self.DetectedItems ) do - local DetectedItem = DetectedItem -- #DETECTION_BASE.DetectedItem - local ReportSummary = self:DetectedItemReportSummary( DetectedItemTypeName ) - Report:Add( ReportSummary ) - end - - local ReportText = Report:Text() - - return ReportText - end - -end - - -do -- DETECTION_AREAS - - --- # 4) DETECTION_AREAS class, extends @{Detection#DETECTION_BASE} - -- - -- The DETECTION_AREAS class will detect units within the battle zone for a list of @{Group}s detecting targets following (a) detection method(s), - -- and will build a list (table) of @{Set#SET_UNIT}s containing the @{Unit#UNIT}s detected. - -- The class is group the detected units within zones given a DetectedZoneRange parameter. - -- A set with multiple detected zones will be created as there are groups of units detected. - -- - -- ## 4.1) Retrieve the Detected Unit Sets and Detected Zones - -- - -- The methods to manage the DetectedItems[].Set(s) are implemented in @{Detection#DECTECTION_BASE} and - -- the methods to manage the DetectedItems[].Zone(s) is implemented in @{Detection#DETECTION_AREAS}. - -- - -- Retrieve the DetectedItems[].Set with the method @{Detection#DETECTION_BASE.GetDetectedSet}(). A @{Set#SET_UNIT} object will be returned. - -- - -- Retrieve the formed @{Zone@ZONE_UNIT}s as a result of the grouping the detected units within the DetectionZoneRange, use the method @{Detection#DETECTION_BASE.GetDetectionZones}(). - -- To understand the amount of zones created, use the method @{Detection#DETECTION_BASE.GetDetectionZoneCount}(). - -- If you want to obtain a specific zone from the DetectedZones, use the method @{Detection#DETECTION_BASE.GetDetectionZone}() with a given index. - -- - -- ## 4.4) Flare or Smoke detected units - -- - -- Use the methods @{Detection#DETECTION_AREAS.FlareDetectedUnits}() or @{Detection#DETECTION_AREAS.SmokeDetectedUnits}() to flare or smoke the detected units when a new detection has taken place. - -- - -- ## 4.5) Flare or Smoke or Bound detected zones - -- - -- Use the methods: - -- - -- * @{Detection#DETECTION_AREAS.FlareDetectedZones}() to flare in a color - -- * @{Detection#DETECTION_AREAS.SmokeDetectedZones}() to smoke in a color - -- * @{Detection#DETECTION_AREAS.SmokeDetectedZones}() to bound with a tire with a white flag - -- - -- the detected zones when a new detection has taken place. - -- - -- @type DETECTION_AREAS - -- @field Dcs.DCSTypes#Distance DetectionZoneRange The range till which targets are grouped upon the first detected target. - -- @field #DETECTION_BASE.DetectedItems DetectedItems A list of areas containing the set of @{Unit}s, @{Zone}s, the center @{Unit} within the zone, and ID of each area that was detected within a DetectionZoneRange. - -- @extends #DETECTION_BASE - DETECTION_AREAS = { - ClassName = "DETECTION_AREAS", - DetectionZoneRange = nil, - } - - - --- DETECTION_AREAS constructor. - -- @param #DETECTION_AREAS self - -- @param Core.Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Forward Air Controller role. - -- @param Dcs.DCSTypes#Distance DetectionZoneRange The range till which targets are grouped upon the first detected target. - -- @return #DETECTION_AREAS - function DETECTION_AREAS:New( DetectionSetGroup, DetectionZoneRange ) - - -- Inherits from DETECTION_BASE - local self = BASE:Inherit( self, DETECTION_BASE:New( DetectionSetGroup ) ) - - self.DetectionZoneRange = DetectionZoneRange - - self._SmokeDetectedUnits = false - self._FlareDetectedUnits = false - self._SmokeDetectedZones = false - self._FlareDetectedZones = false - self._BoundDetectedZones = false - - return self - end - - --- Report summary of a detected item using a given numeric index. - -- @param #DETECTION_AREAS self - -- @param Index - -- @return #string - function DETECTION_AREAS:DetectedItemReportSummary( Index ) - self:F( Index ) - - local DetectedItem = self:GetDetectedItem( Index ) - if DetectedItem then - local DetectedSet = self:GetDetectedSet( Index ) - local ReportSummaryItem - - local DetectedZone = self:GetDetectedZone( Index ) - local DetectedItemPointVec3 = DetectedZone:GetPointVec3() - local DetectedItemPointLL = DetectedItemPointVec3:ToStringLL( 3, true ) - - local ThreatLevelA2G = self:GetTreatLevelA2G( DetectedItem ) - local DetectedItemsCount = DetectedSet:Count() - local DetectedItemsTypes = DetectedSet:GetTypeNames() - - local ReportSummary = string.format( - "%s - Threat [%s] (%2d) - %2d of %s", - DetectedItemPointLL, - string.rep( "â– ", ThreatLevelA2G ), - ThreatLevelA2G, - DetectedItemsCount, - DetectedItemsTypes - ) - - return ReportSummary - end - - return nil - 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( DetectedItem ) - - self:T3( DetectedItem.FriendliesNearBy ) - return DetectedItem.FriendliesNearBy or false - end - - --- Calculate the maxium A2G threat level of the DetectedItem. - -- @param #DETECTION_AREAS self - -- @param #DETECTION_BASE.DetectedItem DetectedItem - function DETECTION_AREAS:CalculateThreatLevelA2G( DetectedItem ) - - local MaxThreatLevelA2G = 0 - for UnitName, UnitData in pairs( DetectedItem.Set:GetSet() ) do - local ThreatUnit = UnitData -- Wrapper.Unit#UNIT - local ThreatLevelA2G = ThreatUnit:GetThreatLevel() - if ThreatLevelA2G > MaxThreatLevelA2G then - MaxThreatLevelA2G = ThreatLevelA2G - end - end - - self:T3( MaxThreatLevelA2G ) - DetectedItem.MaxThreatLevelA2G = MaxThreatLevelA2G - - end - - --- Find the nearest FAC of the DetectedItem. - -- @param #DETECTION_AREAS self - -- @param #DETECTION_BASE.DetectedItem DetectedItem - -- @return Wrapper.Unit#UNIT The nearest FAC unit - function DETECTION_AREAS:NearestFAC( DetectedItem ) - - 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 - - DetectedItem.NearestFAC = NearestFAC - - end - - --- Returns the A2G threat level of the units in the DetectedItem - -- @param #DETECTION_AREAS self - -- @param #DETECTION_BASE.DetectedItem DetectedItem - -- @return #number a scale from 0 to 10. - function DETECTION_AREAS:GetTreatLevelA2G( DetectedItem ) - - self:T3( DetectedItem.MaxThreatLevelA2G ) - return DetectedItem.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 - - --- Bound the detected zones - -- @param #DETECTION_AREAS self - -- @return #DETECTION_AREAS self - function DETECTION_AREAS:BoundDetectedZones() - self:F2() - - self._BoundDetectedZones = true - return self - end - - --- Make text documenting the changes of the detected zone. - -- @param #DETECTION_AREAS self - -- @param #DETECTION_BASE.DetectedItem DetectedItem - -- @return #string The Changes text - function DETECTION_AREAS:GetChangeText( DetectedItem ) - self:F( DetectedItem ) - - local MT = {} - - for ChangeCode, ChangeData in pairs( DetectedItem.Changes ) do - - if ChangeCode == "AA" then - MT[#MT+1] = "Detected new area " .. ChangeData.ItemID .. ". The center target is a " .. ChangeData.ItemUnitType .. "." - end - - if ChangeCode == "RAU" then - MT[#MT+1] = "Changed area " .. ChangeData.ItemID .. ". Removed the center target." - end - - if ChangeCode == "AAU" then - MT[#MT+1] = "Changed area " .. ChangeData.ItemID .. ". The new center target is a " .. ChangeData.ItemUnitType .. "." - end - - if ChangeCode == "RA" then - MT[#MT+1] = "Removed old area " .. ChangeData.ItemID .. ". No more targets in this area." - end - - if ChangeCode == "AU" then - local MTUT = {} - for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do - if ChangeUnitType ~= "ItemID" then - MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType - end - end - MT[#MT+1] = "Detected for area " .. ChangeData.ItemID .. " new target(s) " .. table.concat( MTUT, ", " ) .. "." - end - - if ChangeCode == "RU" then - local MTUT = {} - for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do - if ChangeUnitType ~= "ItemID" then - MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType - end - end - MT[#MT+1] = "Removed for area " .. ChangeData.ItemID .. " invisible or destroyed target(s) " .. table.concat( MTUT, ", " ) .. "." - end - - end - - return table.concat( MT, "\n" ) - - end - - - --- Make a DetectionSet table. This function will be overridden in the derived clsses. - -- @param #DETECTION_AREAS self - -- @return #DETECTION_AREAS self - function DETECTION_AREAS:CreateDetectionSets() - self:F2() - - - self:T( "Checking Detected Items for new Detected Units ..." ) - -- First go through all detected sets, and check if there are new detected units, match all existing detected units and identify undetected units. - -- Regroup when needed, split groups when needed. - for DetectedItemID, DetectedItemData in pairs( self.DetectedItems ) do - - local DetectedItem = DetectedItemData -- #DETECTION_BASE.DetectedItem - if DetectedItem then - - self:T( { "Detected Item ID:", DetectedItemID } ) - - - local DetectedSet = DetectedItem.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( { "Zone Center Unit:", DetectedItem.Zone.ZoneUNIT.UnitName } ) - local DetectedZoneObject = self:GetDetectedObject( DetectedItem.Zone.ZoneUNIT.UnitName ) - self:T3( { "Detected Zone Object:", DetectedItem.Zone:GetName(), 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( DetectedItem.Zone.ZoneUNIT.UnitName ) - - self:AddChangeItem( DetectedItem, '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 - - DetectedItem.Zone:BoundZone( 12, self.CountryID, true) - - -- Assign the Unit as the new center unit of the detected area. - DetectedItem.Zone = ZONE_UNIT:New( DetectedUnit:GetName(), DetectedUnit, self.DetectionZoneRange ) - - self:AddChangeItem( DetectedItem, "AAU", DetectedItem.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 DetectedItem.Zone - if DetectedUnit:IsInZone( DetectedItem.Zone ) then - - -- Yes, the DetectedUnit is within the DetectedItem.Zone, no changes, DetectedUnit can be kept within the Set. - self:IdentifyDetectedObject( DetectedObject ) - - else - -- No, the DetectedUnit is not within the DetectedItem.Zone, remove DetectedUnit from the Set. - DetectedSet:Remove( DetectedUnitName ) - self:AddChangeUnit( DetectedItem, "RU", DetectedUnit:GetTypeName() ) - end - - else - -- There was no DetectedObject, remove DetectedUnit from the Set. - self:AddChangeUnit( DetectedItem, "RU", "destroyed target" ) - DetectedSet:Remove( DetectedUnitName ) - - -- The DetectedObject has been identified, because it does not exist ... - -- self:IdentifyDetectedObject( DetectedObject ) - end - end - else - DetectedItem.Zone:BoundZone( 12, self.CountryID, true) - self:RemoveDetectedItem( DetectedItemID ) - self:AddChangeItem( DetectedItem, "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 DetectedItemID, DetectedItemData in pairs( self.DetectedItems ) do - - local DetectedItem = DetectedItemData -- #DETECTION_BASE.DetectedItem - if DetectedItem then - self:T( "Detection Area #" .. DetectedItem.ItemID ) - local DetectedSet = DetectedItem.Set - if not self:IsDetectedObjectIdentified( DetectedObject ) and DetectedUnit:IsInZone( DetectedItem.Zone ) then - self:IdentifyDetectedObject( DetectedObject ) - DetectedSet:AddUnit( DetectedUnit ) - AddedToDetectionArea = true - self:AddChangeUnit( DetectedItem, "AU", DetectedUnit:GetTypeName() ) - end - end - end - - if AddedToDetectionArea == false then - - -- New detection area - local DetectedItem = self:AddDetectedItemZone( nil, - SET_UNIT:New(), - ZONE_UNIT:New( DetectedUnitName, DetectedUnit, self.DetectionZoneRange ) - ) - --self:E( DetectedItem.Zone.ZoneUNIT.UnitName ) - DetectedItem.Set:AddUnit( DetectedUnit ) - self:AddChangeItem( DetectedItem, "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 DetectedItemID, DetectedItemData in pairs( self.DetectedItems ) do - - local DetectedItem = DetectedItemData -- #DETECTION_BASE.DetectedItem - local DetectedSet = DetectedItem.Set - local DetectedZone = DetectedItem.Zone - - self:ReportFriendliesNearBy( { DetectedItem = DetectedItem, ReportSetGroup = self.DetectionSetGroup } ) -- Fill the Friendlies table - self:CalculateThreatLevelA2G( DetectedItem ) -- Calculate A2G threat level - self:NearestFAC( DetectedItem ) - - 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 #" .. DetectedItem.ItemID .. ":" .. 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 - - if DETECTION_AREAS._BoundDetectedZones or self._BoundDetectedZones then - DetectedZone:BoundZone( 12, self.CountryID ) - end - end - - end - -end ---- Single-Player:**No** / Multi-Player:**Yes** / AI:**Yes** / Human:**No** / Types:**All** -- **AI Balancing will replace in multi player missions --- non-occupied human slots with AI groups, in order to provide an engaging simulation environment, --- even when there are hardly any players in the mission.** --- --- ![Banner Image](..\Presentations\AI_Balancer\Dia1.JPG) --- --- === --- --- # 1) @{AI_Balancer#AI_BALANCER} class, extends @{Fsm#FSM_SET} --- --- The @{AI_Balancer#AI_BALANCER} class monitors and manages as many replacement AI groups as there are --- CLIENTS in a SET_CLIENT collection, which are not occupied by human players. --- In other words, use AI_BALANCER to simulate human behaviour by spawning in replacement AI in multi player missions. --- --- The parent class @{Fsm#FSM_SET} manages the functionality to control the Finite State Machine (FSM). --- The mission designer can tailor the behaviour of the AI_BALANCER, by defining event and state transition methods. --- An explanation about state and event transition methods can be found in the @{FSM} module documentation. --- --- The mission designer can tailor the AI_BALANCER behaviour, by implementing a state or event handling method for the following: --- --- * **@{#AI_BALANCER.OnAfterSpawned}**( AISet, From, Event, To, AIGroup ): Define to add extra logic when an AI is spawned. --- --- ## 1.1) AI_BALANCER construction --- --- Create a new AI_BALANCER object with the @{#AI_BALANCER.New}() method: --- --- ## 1.2) AI_BALANCER is a FSM --- --- ![Process](..\Presentations\AI_Balancer\Dia13.JPG) --- --- ### 1.2.1) AI_BALANCER States --- --- * **Monitoring** ( Set ): Monitoring the Set if all AI is spawned for the Clients. --- * **Spawning** ( Set, ClientName ): There is a new AI group spawned with ClientName as the name of reference. --- * **Spawned** ( Set, AIGroup ): A new AI has been spawned. You can handle this event to customize the AI behaviour with other AI FSMs or own processes. --- * **Destroying** ( Set, AIGroup ): The AI is being destroyed. --- * **Returning** ( Set, AIGroup ): The AI is returning to the airbase specified by the ReturnToAirbase methods. Handle this state to customize the return behaviour of the AI, if any. --- --- ### 1.2.2) AI_BALANCER Events --- --- * **Monitor** ( Set ): Every 10 seconds, the Monitor event is triggered to monitor the Set. --- * **Spawn** ( Set, ClientName ): Triggers when there is a new AI group to be spawned with ClientName as the name of reference. --- * **Spawned** ( Set, AIGroup ): Triggers when a new AI has been spawned. You can handle this event to customize the AI behaviour with other AI FSMs or own processes. --- * **Destroy** ( Set, AIGroup ): The AI is being destroyed. --- * **Return** ( Set, AIGroup ): The AI is returning to the airbase specified by the ReturnToAirbase methods. --- --- ## 1.3) AI_BALANCER spawn interval for replacement AI --- --- Use the method @{#AI_BALANCER.InitSpawnInterval}() to set the earliest and latest interval in seconds that is waited until a new replacement AI is spawned. --- --- ## 1.4) AI_BALANCER returns AI to Airbases --- --- By default, When a human player joins a slot that is AI_BALANCED, the AI group will be destroyed by default. --- However, there are 2 additional options that you can use to customize the destroy behaviour. --- When a human player joins a slot, you can configure to let the AI return to: --- --- * @{#AI_BALANCER.ReturnToHomeAirbase}: Returns the AI to the **home** @{Airbase#AIRBASE}. --- * @{#AI_BALANCER.ReturnToNearestAirbases}: Returns the AI to the **nearest friendly** @{Airbase#AIRBASE}. --- --- Note that when AI returns to an airbase, the AI_BALANCER will trigger the **Return** event and the AI will return, --- otherwise the AI_BALANCER will trigger a **Destroy** event, and the AI will be destroyed. --- --- === --- --- # **API CHANGE HISTORY** --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- Hereby the change log: --- --- 2017-01-17: There is still a problem with AI being destroyed, but not respawned. Need to check further upon that. --- --- 2017-01-08: AI_BALANCER:**InitSpawnInterval( Earliest, Latest )** added. --- --- === --- --- # **AUTHORS and CONTRIBUTIONS** --- --- ### Contributions: --- --- * **[Dutch_Baron](https://forums.eagle.ru/member.php?u=112075)**: Working together with James has resulted in the creation of the AI_BALANCER class. James has shared his ideas on balancing AI with air units, and together we made a first design which you can use now :-) --- * **SNAFU**: Had a couple of mails with the guys to validate, if the same concept in the GCI/CAP script could be reworked within MOOSE. None of the script code has been used however within the new AI_BALANCER moose class. --- --- ### Authors: --- --- * FlightControl: Framework Design & Programming and Documentation. --- --- @module AI_Balancer - ---- AI_BALANCER class --- @type AI_BALANCER --- @field Core.Set#SET_CLIENT SetClient --- @field Functional.Spawn#SPAWN SpawnAI --- @field Wrapper.Group#GROUP Test --- @extends Core.Fsm#FSM_SET -AI_BALANCER = { - ClassName = "AI_BALANCER", - PatrolZones = {}, - AIGroups = {}, - Earliest = 5, -- Earliest a new AI can be spawned is in 5 seconds. - Latest = 60, -- Latest a new AI can be spawned is in 60 seconds. -} - - - ---- Creates a new AI_BALANCER object --- @param #AI_BALANCER self --- @param Core.Set#SET_CLIENT SetClient A SET\_CLIENT object that will contain the CLIENT objects to be monitored if they are alive or not (joined by a player). --- @param Functional.Spawn#SPAWN SpawnAI The default Spawn object to spawn new AI Groups when needed. --- @return #AI_BALANCER -function AI_BALANCER:New( SetClient, SpawnAI ) - - -- Inherits from BASE - local self = BASE:Inherit( self, FSM_SET:New( SET_GROUP:New() ) ) -- AI.AI_Balancer#AI_BALANCER - - -- TODO: Define the OnAfterSpawned event - self:SetStartState( "None" ) - self:AddTransition( "*", "Monitor", "Monitoring" ) - self:AddTransition( "*", "Spawn", "Spawning" ) - self:AddTransition( "Spawning", "Spawned", "Spawned" ) - self:AddTransition( "*", "Destroy", "Destroying" ) - self:AddTransition( "*", "Return", "Returning" ) - - self.SetClient = SetClient - self.SetClient:FilterOnce() - self.SpawnAI = SpawnAI - - self.SpawnQueue = {} - - self.ToNearestAirbase = false - self.ToHomeAirbase = false - - self:__Monitor( 1 ) - - return self -end - ---- Sets the earliest to the latest interval in seconds how long AI_BALANCER will wait to spawn a new AI. --- Provide 2 identical seconds if the interval should be a fixed amount of seconds. --- @param #AI_BALANCER self --- @param #number Earliest The earliest a new AI can be spawned in seconds. --- @param #number Latest The latest a new AI can be spawned in seconds. --- @return self -function AI_BALANCER:InitSpawnInterval( Earliest, Latest ) - - self.Earliest = Earliest - self.Latest = Latest - - return self -end - ---- Returns the AI to the nearest friendly @{Airbase#AIRBASE}. --- @param #AI_BALANCER self --- @param Dcs.DCSTypes#Distance ReturnTresholdRange If there is an enemy @{Client#CLIENT} within the ReturnTresholdRange given in meters, the AI will not return to the nearest @{Airbase#AIRBASE}. --- @param Core.Set#SET_AIRBASE ReturnAirbaseSet The SET of @{Set#SET_AIRBASE}s to evaluate where to return to. -function AI_BALANCER:ReturnToNearestAirbases( ReturnTresholdRange, ReturnAirbaseSet ) - - self.ToNearestAirbase = true - self.ReturnTresholdRange = ReturnTresholdRange - self.ReturnAirbaseSet = ReturnAirbaseSet -end - ---- Returns the AI to the home @{Airbase#AIRBASE}. --- @param #AI_BALANCER self --- @param Dcs.DCSTypes#Distance ReturnTresholdRange If there is an enemy @{Client#CLIENT} within the ReturnTresholdRange given in meters, the AI will not return to the nearest @{Airbase#AIRBASE}. -function AI_BALANCER:ReturnToHomeAirbase( ReturnTresholdRange ) - - self.ToHomeAirbase = true - self.ReturnTresholdRange = ReturnTresholdRange -end - ---- @param #AI_BALANCER self --- @param Core.Set#SET_GROUP SetGroup --- @param #string ClientName --- @param Wrapper.Group#GROUP AIGroup -function AI_BALANCER:onenterSpawning( SetGroup, From, Event, To, ClientName ) - - -- OK, Spawn a new group from the default SpawnAI object provided. - local AIGroup = self.SpawnAI:Spawn() -- Wrapper.Group#GROUP - if AIGroup then - AIGroup:E( "Spawning new AIGroup" ) - --TODO: need to rework UnitName thing ... - - SetGroup:Add( ClientName, AIGroup ) - self.SpawnQueue[ClientName] = nil - - -- Fire the Spawned event. The first parameter is the AIGroup just Spawned. - -- Mission designers can catch this event to bind further actions to the AIGroup. - self:Spawned( AIGroup ) - end -end - ---- @param #AI_BALANCER self --- @param Core.Set#SET_GROUP SetGroup --- @param Wrapper.Group#GROUP AIGroup -function AI_BALANCER:onenterDestroying( SetGroup, From, Event, To, ClientName, AIGroup ) - - AIGroup:Destroy() - SetGroup:Flush() - SetGroup:Remove( ClientName ) - SetGroup:Flush() -end - ---- @param #AI_BALANCER self --- @param Core.Set#SET_GROUP SetGroup --- @param Wrapper.Group#GROUP AIGroup -function AI_BALANCER:onenterReturning( SetGroup, From, Event, To, AIGroup ) - - local AIGroupTemplate = AIGroup:GetTemplate() - if self.ToHomeAirbase == true then - local WayPointCount = #AIGroupTemplate.route.points - local SwitchWayPointCommand = AIGroup:CommandSwitchWayPoint( 1, WayPointCount, 1 ) - AIGroup:SetCommand( SwitchWayPointCommand ) - AIGroup:MessageToRed( "Returning to home base ...", 30 ) - else - -- Okay, we need to send this Group back to the nearest base of the Coalition of the AI. - --TODO: i need to rework the POINT_VEC2 thing. - local PointVec2 = POINT_VEC2:New( AIGroup:GetVec2().x, AIGroup:GetVec2().y ) - local ClosestAirbase = self.ReturnAirbaseSet:FindNearestAirbaseFromPointVec2( PointVec2 ) - self:T( ClosestAirbase.AirbaseName ) - AIGroup:MessageToRed( "Returning to " .. ClosestAirbase:GetName().. " ...", 30 ) - local RTBRoute = AIGroup:RouteReturnToAirbase( ClosestAirbase ) - AIGroupTemplate.route = RTBRoute - AIGroup:Respawn( AIGroupTemplate ) - end - -end - - ---- @param #AI_BALANCER self -function AI_BALANCER:onenterMonitoring( SetGroup ) - - self:T2( { self.SetClient:Count() } ) - --self.SetClient:Flush() - - self.SetClient:ForEachClient( - --- @param Wrapper.Client#CLIENT Client - function( Client ) - self:T3(Client.ClientName) - - local AIGroup = self.Set:Get( Client.UnitName ) -- Wrapper.Group#GROUP - if Client:IsAlive() then - - if AIGroup and AIGroup:IsAlive() == true then - - if self.ToNearestAirbase == false and self.ToHomeAirbase == false then - self:Destroy( Client.UnitName, AIGroup ) - else - -- We test if there is no other CLIENT within the self.ReturnTresholdRange of the first unit of the AI group. - -- If there is a CLIENT, the AI stays engaged and will not return. - -- If there is no CLIENT within the self.ReturnTresholdRange, then the unit will return to the Airbase return method selected. - - local PlayerInRange = { Value = false } - local RangeZone = ZONE_RADIUS:New( 'RangeZone', AIGroup:GetVec2(), self.ReturnTresholdRange ) - - self:T2( RangeZone ) - - _DATABASE:ForEachPlayer( - --- @param Wrapper.Unit#UNIT RangeTestUnit - function( RangeTestUnit, RangeZone, AIGroup, PlayerInRange ) - self:T2( { PlayerInRange, RangeTestUnit.UnitName, RangeZone.ZoneName } ) - if RangeTestUnit:IsInZone( RangeZone ) == true then - self:T2( "in zone" ) - if RangeTestUnit:GetCoalition() ~= AIGroup:GetCoalition() then - self:T2( "in range" ) - PlayerInRange.Value = true - end - end - end, - - --- @param Core.Zone#ZONE_RADIUS RangeZone - -- @param Wrapper.Group#GROUP AIGroup - function( RangeZone, AIGroup, PlayerInRange ) - if PlayerInRange.Value == false then - self:Return( AIGroup ) - end - end - , RangeZone, AIGroup, PlayerInRange - ) - - end - self.Set:Remove( Client.UnitName ) - end - else - if not AIGroup or not AIGroup:IsAlive() == true then - self:T( "Client " .. Client.UnitName .. " not alive." ) - if not self.SpawnQueue[Client.UnitName] then - -- Spawn a new AI taking into account the spawn interval Earliest, Latest - self:__Spawn( math.random( self.Earliest, self.Latest ), Client.UnitName ) - self.SpawnQueue[Client.UnitName] = true - self:E( "New AI Spawned for Client " .. Client.UnitName ) - end - end - end - return true - end - ) - - self:__Monitor( 10 ) -end - - - ---- **AI** -- **Air Patrolling or Staging.** --- --- ![Banner Image](..\Presentations\AI_PATROL\Dia1.JPG) --- --- === --- --- AI PATROL classes makes AI Controllables execute an Patrol. --- --- There are the following types of PATROL classes defined: --- --- * @{#AI_PATROL_ZONE}: Perform a PATROL in a zone. --- --- ==== --- --- # **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 - ---- # 1) @{#AI_PATROL_ZONE} class, extends @{Fsm#FSM_CONTROLLABLE} --- --- The @{#AI_PATROL_ZONE} class implements the core functions to patrol a @{Zone} by an AI @{Controllable} or @{Group}. --- --- ![Process](..\Presentations\AI_PATROL\Dia3.JPG) --- --- The AI_PATROL_ZONE is assigned a @{Group} and this must be done before the AI_PATROL_ZONE process can be started using the **Start** event. --- --- ![Process](..\Presentations\AI_PATROL\Dia4.JPG) --- --- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits. --- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. --- --- ![Process](..\Presentations\AI_PATROL\Dia5.JPG) --- --- This cycle will continue. --- --- ![Process](..\Presentations\AI_PATROL\Dia6.JPG) --- --- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event. --- --- ![Process](..\Presentations\AI_PATROL\Dia9.JPG) --- ----- Note that the enemy is not engaged! To model enemy engagement, either tailor the **Detected** event, or --- use derived AI_ classes to model AI offensive or defensive behaviour. --- --- ![Process](..\Presentations\AI_PATROL\Dia10.JPG) --- --- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. --- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. --- --- ![Process](..\Presentations\AI_PATROL\Dia11.JPG) --- --- ## 1.1) AI_PATROL_ZONE constructor --- --- * @{#AI_PATROL_ZONE.New}(): Creates a new AI_PATROL_ZONE object. --- --- ## 1.2) AI_PATROL_ZONE is a FSM --- --- ![Process](..\Presentations\AI_PATROL\Dia2.JPG) --- --- ### 1.2.1) AI_PATROL_ZONE States --- --- * **None** ( Group ): The process is not started yet. --- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. --- * **Returning** ( Group ): The AI is returning to Base. --- * **Stopped** ( Group ): The process is stopped. --- * **Crashed** ( Group ): The AI has crashed or is dead. --- --- ### 1.2.2) AI_PATROL_ZONE Events --- --- * **Start** ( Group ): Start the process. --- * **Stop** ( Group ): Stop the process. --- * **Route** ( Group ): Route the AI to a new random 3D point within the Patrol Zone. --- * **RTB** ( Group ): Route the AI to the home base. --- * **Detect** ( Group ): The AI is detecting targets. --- * **Detected** ( Group ): The AI has detected new targets. --- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. --- --- ## 1.3) Set or Get the AI controllable --- --- * @{#AI_PATROL_ZONE.SetControllable}(): Set the AIControllable. --- * @{#AI_PATROL_ZONE.GetControllable}(): Get the AIControllable. --- --- ## 1.4) Set the Speed and Altitude boundaries of the AI controllable --- --- * @{#AI_PATROL_ZONE.SetSpeed}(): Set the patrol speed boundaries of the AI, for the next patrol. --- * @{#AI_PATROL_ZONE.SetAltitude}(): Set altitude boundaries of the AI, for the next patrol. --- --- ## 1.5) Manage the detection process of the AI controllable --- --- The detection process of the AI controllable can be manipulated. --- Detection requires an amount of CPU power, which has an impact on your mission performance. --- Only put detection on when absolutely necessary, and the frequency of the detection can also be set. --- --- * @{#AI_PATROL_ZONE.SetDetectionOn}(): Set the detection on. The AI will detect for targets. --- * @{#AI_PATROL_ZONE.SetDetectionOff}(): Set the detection off, the AI will not detect for targets. The existing target list will NOT be erased. --- --- The detection frequency can be set with @{#AI_PATROL_ZONE.SetDetectionInterval}( seconds ), where the amount of seconds specify how much seconds will be waited before the next detection. --- Use the method @{#AI_PATROL_ZONE.GetDetectedUnits}() to obtain a list of the @{Unit}s detected by the AI. --- --- The detection can be filtered to potential targets in a specific zone. --- Use the method @{#AI_PATROL_ZONE.SetDetectionZone}() to set the zone where targets need to be detected. --- Note that when the zone is too far away, or the AI is not heading towards the zone, or the AI is too high, no targets may be detected --- according the weather conditions. --- --- ## 1.6) Manage the "out of fuel" in the AI_PATROL_ZONE --- --- When the AI is out of fuel, it is required that a new AI is started, before the old AI can return to the home base. --- Therefore, with a parameter and a calculation of the distance to the home base, the fuel treshold is calculated. --- When the fuel treshold is reached, the AI will continue for a given time its patrol task in orbit, --- while a new AI is targetted to the AI_PATROL_ZONE. --- Once the time is finished, the old AI will return to the base. --- Use the method @{#AI_PATROL_ZONE.ManageFuel}() to have this proces in place. --- --- ## 1.7) Manage "damage" behaviour of the AI in the AI_PATROL_ZONE --- --- When the AI is damaged, it is required that a new AIControllable is started. However, damage cannon be foreseen early on. --- Therefore, when the damage treshold is reached, the AI will return immediately to the home base (RTB). --- Use the method @{#AI_PATROL_ZONE.ManageDamage}() to have this proces in place. --- --- === --- --- @field #AI_PATROL_ZONE AI_PATROL_ZONE --- -AI_PATROL_ZONE = { - ClassName = "AI_PATROL_ZONE", -} - ---- Creates a new AI_PATROL_ZONE object --- @param #AI_PATROL_ZONE self --- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. --- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. --- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h. --- @param Dcs.DCSTypes#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO --- @return #AI_PATROL_ZONE self --- @usage --- -- Define a new AI_PATROL_ZONE Object. This PatrolArea will patrol an AIControllable within PatrolZone between 3000 and 6000 meters, with a variying speed between 600 and 900 km/h. --- PatrolZone = ZONE:New( 'PatrolZone' ) --- PatrolSpawn = SPAWN:New( 'Patrol Group' ) --- PatrolArea = AI_PATROL_ZONE:New( PatrolZone, 3000, 6000, 600, 900 ) -function AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) - - -- Inherits from BASE - local self = BASE:Inherit( self, FSM_CONTROLLABLE:New() ) -- #AI_PATROL_ZONE - - - self.PatrolZone = PatrolZone - self.PatrolFloorAltitude = PatrolFloorAltitude - self.PatrolCeilingAltitude = PatrolCeilingAltitude - self.PatrolMinSpeed = PatrolMinSpeed - self.PatrolMaxSpeed = PatrolMaxSpeed - - -- defafult PatrolAltType to "RADIO" if not specified - self.PatrolAltType = PatrolAltType or "RADIO" - - self:SetDetectionInterval( 30 ) - - self.CheckStatus = true - - self:ManageFuel( .2, 60 ) - self:ManageDamage( 1 ) - - - self.DetectedUnits = {} -- This table contains the targets detected during patrol. - - self:SetStartState( "None" ) - - self:AddTransition( "*", "Stop", "Stopped" ) - ---- OnLeave Transition Handler for State Stopped. --- @function [parent=#AI_PATROL_ZONE] OnLeaveStopped --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnEnter Transition Handler for State Stopped. --- @function [parent=#AI_PATROL_ZONE] OnEnterStopped --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- OnBefore Transition Handler for Event Stop. --- @function [parent=#AI_PATROL_ZONE] OnBeforeStop --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event Stop. --- @function [parent=#AI_PATROL_ZONE] OnAfterStop --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event Stop. --- @function [parent=#AI_PATROL_ZONE] Stop --- @param #AI_PATROL_ZONE self - ---- Asynchronous Event Trigger for Event Stop. --- @function [parent=#AI_PATROL_ZONE] __Stop --- @param #AI_PATROL_ZONE self --- @param #number Delay The delay in seconds. - - self:AddTransition( "None", "Start", "Patrolling" ) - ---- OnBefore Transition Handler for Event Start. --- @function [parent=#AI_PATROL_ZONE] OnBeforeStart --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event Start. --- @function [parent=#AI_PATROL_ZONE] OnAfterStart --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event Start. --- @function [parent=#AI_PATROL_ZONE] Start --- @param #AI_PATROL_ZONE self - ---- Asynchronous Event Trigger for Event Start. --- @function [parent=#AI_PATROL_ZONE] __Start --- @param #AI_PATROL_ZONE self --- @param #number Delay The delay in seconds. - ---- OnLeave Transition Handler for State Patrolling. --- @function [parent=#AI_PATROL_ZONE] OnLeavePatrolling --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnEnter Transition Handler for State Patrolling. --- @function [parent=#AI_PATROL_ZONE] OnEnterPatrolling --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - - self:AddTransition( "Patrolling", "Route", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. - ---- OnBefore Transition Handler for Event Route. --- @function [parent=#AI_PATROL_ZONE] OnBeforeRoute --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event Route. --- @function [parent=#AI_PATROL_ZONE] OnAfterRoute --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event Route. --- @function [parent=#AI_PATROL_ZONE] Route --- @param #AI_PATROL_ZONE self - ---- Asynchronous Event Trigger for Event Route. --- @function [parent=#AI_PATROL_ZONE] __Route --- @param #AI_PATROL_ZONE self --- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "Status", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. - ---- OnBefore Transition Handler for Event Status. --- @function [parent=#AI_PATROL_ZONE] OnBeforeStatus --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event Status. --- @function [parent=#AI_PATROL_ZONE] OnAfterStatus --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event Status. --- @function [parent=#AI_PATROL_ZONE] Status --- @param #AI_PATROL_ZONE self - ---- Asynchronous Event Trigger for Event Status. --- @function [parent=#AI_PATROL_ZONE] __Status --- @param #AI_PATROL_ZONE self --- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "Detect", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. - ---- OnBefore Transition Handler for Event Detect. --- @function [parent=#AI_PATROL_ZONE] OnBeforeDetect --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event Detect. --- @function [parent=#AI_PATROL_ZONE] OnAfterDetect --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event Detect. --- @function [parent=#AI_PATROL_ZONE] Detect --- @param #AI_PATROL_ZONE self - ---- Asynchronous Event Trigger for Event Detect. --- @function [parent=#AI_PATROL_ZONE] __Detect --- @param #AI_PATROL_ZONE self --- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "Detected", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. - ---- OnBefore Transition Handler for Event Detected. --- @function [parent=#AI_PATROL_ZONE] OnBeforeDetected --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event Detected. --- @function [parent=#AI_PATROL_ZONE] OnAfterDetected --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event Detected. --- @function [parent=#AI_PATROL_ZONE] Detected --- @param #AI_PATROL_ZONE self - ---- Asynchronous Event Trigger for Event Detected. --- @function [parent=#AI_PATROL_ZONE] __Detected --- @param #AI_PATROL_ZONE self --- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "RTB", "Returning" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. - ---- OnBefore Transition Handler for Event RTB. --- @function [parent=#AI_PATROL_ZONE] OnBeforeRTB --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event RTB. --- @function [parent=#AI_PATROL_ZONE] OnAfterRTB --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event RTB. --- @function [parent=#AI_PATROL_ZONE] RTB --- @param #AI_PATROL_ZONE self - ---- Asynchronous Event Trigger for Event RTB. --- @function [parent=#AI_PATROL_ZONE] __RTB --- @param #AI_PATROL_ZONE self --- @param #number Delay The delay in seconds. - ---- OnLeave Transition Handler for State Returning. --- @function [parent=#AI_PATROL_ZONE] OnLeaveReturning --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnEnter Transition Handler for State Returning. --- @function [parent=#AI_PATROL_ZONE] OnEnterReturning --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - - self:AddTransition( "*", "Reset", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. - - self:AddTransition( "*", "Eject", "*" ) - self:AddTransition( "*", "Crash", "Crashed" ) - self:AddTransition( "*", "PilotDead", "*" ) - - return self -end - - - - ---- Sets (modifies) the minimum and maximum speed of the patrol. --- @param #AI_PATROL_ZONE self --- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. --- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h. --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetSpeed( PatrolMinSpeed, PatrolMaxSpeed ) - self:F2( { PatrolMinSpeed, PatrolMaxSpeed } ) - - self.PatrolMinSpeed = PatrolMinSpeed - self.PatrolMaxSpeed = PatrolMaxSpeed -end - - - ---- Sets the floor and ceiling altitude of the patrol. --- @param #AI_PATROL_ZONE self --- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetAltitude( PatrolFloorAltitude, PatrolCeilingAltitude ) - self:F2( { PatrolFloorAltitude, PatrolCeilingAltitude } ) - - self.PatrolFloorAltitude = PatrolFloorAltitude - self.PatrolCeilingAltitude = PatrolCeilingAltitude -end - --- * @{#AI_PATROL_ZONE.SetDetectionOn}(): Set the detection on. The AI will detect for targets. --- * @{#AI_PATROL_ZONE.SetDetectionOff}(): Set the detection off, the AI will not detect for targets. The existing target list will NOT be erased. - ---- Set the detection on. The AI will detect for targets. --- @param #AI_PATROL_ZONE self --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetDetectionOn() - self:F2() - - self.DetectOn = true -end - ---- Set the detection off. The AI will NOT detect for targets. --- However, the list of already detected targets will be kept and can be enquired! --- @param #AI_PATROL_ZONE self --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetDetectionOff() - self:F2() - - self.DetectOn = false -end - ---- Set the status checking off. --- @param #AI_PATROL_ZONE self --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetStatusOff() - self:F2() - - self.CheckStatus = false -end - ---- Activate the detection. The AI will detect for targets if the Detection is switched On. --- @param #AI_PATROL_ZONE self --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetDetectionActivated() - self:F2() - - self:ClearDetectedUnits() - self.DetectActivated = true - self:__Detect( -self.DetectInterval ) -end - ---- Deactivate the detection. The AI will NOT detect for targets. --- @param #AI_PATROL_ZONE self --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetDetectionDeactivated() - self:F2() - - self:ClearDetectedUnits() - self.DetectActivated = false -end - ---- Set the interval in seconds between each detection executed by the AI. --- The list of already detected targets will be kept and updated. --- Newly detected targets will be added, but already detected targets that were --- not detected in this cycle, will NOT be removed! --- The default interval is 30 seconds. --- @param #AI_PATROL_ZONE self --- @param #number Seconds The interval in seconds. --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetDetectionInterval( Seconds ) - self:F2() - - if Seconds then - self.DetectInterval = Seconds - else - self.DetectInterval = 30 - end -end - ---- Set the detection zone where the AI is detecting targets. --- @param #AI_PATROL_ZONE self --- @param Core.Zone#ZONE DetectionZone The zone where to detect targets. --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetDetectionZone( DetectionZone ) - self:F2() - - if DetectionZone then - self.DetectZone = DetectionZone - else - self.DetectZone = nil - end -end - ---- Gets a list of @{Unit#UNIT}s that were detected by the AI. --- No filtering is applied, so, ANY detected UNIT can be in this list. --- It is up to the mission designer to use the @{Unit} class and methods to filter the targets. --- @param #AI_PATROL_ZONE self --- @return #table The list of @{Unit#UNIT}s -function AI_PATROL_ZONE:GetDetectedUnits() - self:F2() - - return self.DetectedUnits -end - ---- Clears the list of @{Unit#UNIT}s that were detected by the AI. --- @param #AI_PATROL_ZONE self -function AI_PATROL_ZONE:ClearDetectedUnits() - self:F2() - self.DetectedUnits = {} -end - ---- When the AI is out of fuel, it is required that a new AI is started, before the old AI can return to the home base. --- Therefore, with a parameter and a calculation of the distance to the home base, the fuel treshold is calculated. --- When the fuel treshold is reached, the AI will continue for a given time its patrol task in orbit, while a new AIControllable is targetted to the AI_PATROL_ZONE. --- Once the time is finished, the old AI will return to the base. --- @param #AI_PATROL_ZONE self --- @param #number PatrolFuelTresholdPercentage The treshold in percentage (between 0 and 1) when the AIControllable is considered to get out of fuel. --- @param #number PatrolOutOfFuelOrbitTime The amount of seconds the out of fuel AIControllable will orbit before returning to the base. --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:ManageFuel( PatrolFuelTresholdPercentage, PatrolOutOfFuelOrbitTime ) - - self.PatrolManageFuel = true - self.PatrolFuelTresholdPercentage = PatrolFuelTresholdPercentage - self.PatrolOutOfFuelOrbitTime = PatrolOutOfFuelOrbitTime - - return self -end - ---- When the AI is damaged beyond a certain treshold, it is required that the AI returns to the home base. --- However, damage cannot be foreseen early on. --- Therefore, when the damage treshold is reached, --- the AI will return immediately to the home base (RTB). --- Note that for groups, the average damage of the complete group will be calculated. --- So, in a group of 4 airplanes, 2 lost and 2 with damage 0.2, the damage treshold will be 0.25. --- @param #AI_PATROL_ZONE self --- @param #number PatrolDamageTreshold The treshold in percentage (between 0 and 1) when the AI is considered to be damaged. --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:ManageDamage( PatrolDamageTreshold ) - - self.PatrolManageDamage = true - self.PatrolDamageTreshold = PatrolDamageTreshold - - return self -end - ---- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. --- @param #AI_PATROL_ZONE self --- @return #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_PATROL_ZONE:onafterStart( Controllable, From, Event, To ) - self:F2() - - self:__Route( 1 ) -- Route to the patrol point. The asynchronous trigger is important, because a spawned group and units takes at least one second to come live. - self:__Status( 60 ) -- Check status status every 30 seconds. - self:SetDetectionActivated() - - self:HandleEvent( EVENTS.PilotDead, self.OnPilotDead ) - self:HandleEvent( EVENTS.Crash, self.OnCrash ) - self:HandleEvent( EVENTS.Ejection, self.OnEjection ) - - Controllable:OptionROEHoldFire() - Controllable:OptionROTVertical() - - self.Controllable:OnReSpawn( - function( PatrolGroup ) - self:E( "ReSpawn" ) - self:__Reset( 1 ) - self:__Route( 5 ) - end - ) - - self:SetDetectionOn() - -end - - ---- @param #AI_PATROL_ZONE self ---- @param Wrapper.Controllable#CONTROLLABLE Controllable -function AI_PATROL_ZONE:onbeforeDetect( Controllable, From, Event, To ) - - return self.DetectOn and self.DetectActivated -end - ---- @param #AI_PATROL_ZONE self ---- @param Wrapper.Controllable#CONTROLLABLE Controllable -function AI_PATROL_ZONE:onafterDetect( Controllable, From, Event, To ) - - local Detected = false - - local DetectedTargets = Controllable:GetDetectedTargets() - for TargetID, Target in pairs( DetectedTargets or {} ) do - local TargetObject = Target.object - - if TargetObject and TargetObject:isExist() and TargetObject.id_ < 50000000 then - - local TargetUnit = UNIT:Find( TargetObject ) - local TargetUnitName = TargetUnit:GetName() - - if self.DetectionZone then - if TargetUnit:IsInZone( self.DetectionZone ) then - self:T( {"Detected ", TargetUnit } ) - if self.DetectedUnits[TargetUnit] == nil then - self.DetectedUnits[TargetUnit] = true - end - Detected = true - end - else - if self.DetectedUnits[TargetUnit] == nil then - self.DetectedUnits[TargetUnit] = true - end - Detected = true - end - end - end - - self:__Detect( -self.DetectInterval ) - - if Detected == true then - self:__Detected( 1.5 ) - end - -end - ---- @param Wrapper.Controllable#CONTROLLABLE AIControllable --- This statis method is called from the route path within the last task at the last waaypoint of the Controllable. --- Note that this method is required, as triggers the next route when patrolling for the Controllable. -function AI_PATROL_ZONE:_NewPatrolRoute( AIControllable ) - - local PatrolZone = AIControllable:GetState( AIControllable, "PatrolZone" ) -- PatrolCore.Zone#AI_PATROL_ZONE - PatrolZone:__Route( 1 ) -end - - ---- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_PATROL_ZONE:onafterRoute( Controllable, From, Event, To ) - - self:F2() - - -- When RTB, don't allow anymore the routing. - if From == "RTB" then - return - end - - - if self.Controllable:IsAlive() then - -- Determine if the AIControllable is within the PatrolZone. - -- If not, make a waypoint within the to that the AIControllable will fly at maximum speed to that point. - - local PatrolRoute = {} - - -- Calculate the current route point of the controllable as the start point of the route. - -- However, when the controllable is not in the air, - -- the controllable current waypoint is probably the airbase... - -- Thus, if we would take the current waypoint as the startpoint, upon take-off, the controllable flies - -- immediately back to the airbase, and this is not correct. - -- Therefore, when on a runway, get as the current route point a random point within the PatrolZone. - -- This will make the plane fly immediately to the patrol zone. - - if self.Controllable:InAir() == false then - self:E( "Not in the air, finding route path within PatrolZone" ) - local CurrentVec2 = self.Controllable:GetVec2() - --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). - local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() - local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) - local ToPatrolZoneSpeed = self.PatrolMaxSpeed - local CurrentRoutePoint = CurrentPointVec3:RoutePointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TakeOffParking, - POINT_VEC3.RoutePointAction.FromParkingArea, - ToPatrolZoneSpeed, - true - ) - PatrolRoute[#PatrolRoute+1] = CurrentRoutePoint - else - self:E( "In the air, finding route path within PatrolZone" ) - local CurrentVec2 = self.Controllable:GetVec2() - --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). - local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() - local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) - local ToPatrolZoneSpeed = self.PatrolMaxSpeed - local CurrentRoutePoint = CurrentPointVec3:RoutePointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToPatrolZoneSpeed, - true - ) - PatrolRoute[#PatrolRoute+1] = CurrentRoutePoint - end - - - --- Define a random point in the @{Zone}. The AI will fly to that point within the zone. - - --- Find a random 2D point in PatrolZone. - local ToTargetVec2 = self.PatrolZone:GetRandomVec2() - self:T2( ToTargetVec2 ) - - --- Define Speed and Altitude. - local ToTargetAltitude = math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ) - local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) - self:T2( { self.PatrolMinSpeed, self.PatrolMaxSpeed, ToTargetSpeed } ) - - --- Obtain a 3D @{Point} from the 2D point + altitude. - local ToTargetPointVec3 = POINT_VEC3:New( ToTargetVec2.x, ToTargetAltitude, ToTargetVec2.y ) - - --- Create a route point of type air. - local ToTargetRoutePoint = ToTargetPointVec3:RoutePointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, - true - ) - - --self.CoordTest:SpawnFromVec3( ToTargetPointVec3:GetVec3() ) - - --ToTargetPointVec3:SmokeRed() - - PatrolRoute[#PatrolRoute+1] = ToTargetRoutePoint - - --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... - self.Controllable:WayPointInitialize( PatrolRoute ) - - --- Do a trick, link the NewPatrolRoute function of the PATROLGROUP object to the AIControllable in a temporary variable ... - self.Controllable:SetState( self.Controllable, "PatrolZone", self ) - self.Controllable:WayPointFunction( #PatrolRoute, 1, "AI_PATROL_ZONE:_NewPatrolRoute" ) - - --- NOW ROUTE THE GROUP! - self.Controllable:WayPointExecute( 1, 2 ) - end - -end - ---- @param #AI_PATROL_ZONE self -function AI_PATROL_ZONE:onbeforeStatus() - - return self.CheckStatus -end - ---- @param #AI_PATROL_ZONE self -function AI_PATROL_ZONE:onafterStatus() - self:F2() - - if self.Controllable and self.Controllable:IsAlive() then - - local RTB = false - - local Fuel = self.Controllable:GetUnit(1):GetFuel() - if Fuel < self.PatrolFuelTresholdPercentage then - self:E( self.Controllable:GetName() .. " is out of fuel:" .. Fuel .. ", RTB!" ) - local OldAIControllable = self.Controllable - local AIControllableTemplate = self.Controllable:GetTemplate() - - local OrbitTask = OldAIControllable:TaskOrbitCircle( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ), self.PatrolMinSpeed ) - local TimedOrbitTask = OldAIControllable:TaskControlled( OrbitTask, OldAIControllable:TaskCondition(nil,nil,nil,nil,self.PatrolOutOfFuelOrbitTime,nil ) ) - OldAIControllable:SetTask( TimedOrbitTask, 10 ) - - RTB = true - else - end - - -- TODO: Check GROUP damage function. - local Damage = self.Controllable:GetLife() - if Damage <= self.PatrolDamageTreshold then - self:E( self.Controllable:GetName() .. " is damaged:" .. Damage .. ", RTB!" ) - RTB = true - end - - if RTB == true then - self:RTB() - else - self:__Status( 60 ) -- Execute the Patrol event after 30 seconds. - end - end -end - ---- @param #AI_PATROL_ZONE self -function AI_PATROL_ZONE:onafterRTB() - self:F2() - - if self.Controllable and self.Controllable:IsAlive() then - - self:SetDetectionOff() - self.CheckStatus = false - - local PatrolRoute = {} - - --- Calculate the current route point. - local CurrentVec2 = self.Controllable:GetVec2() - - --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). - local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() - local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) - local ToPatrolZoneSpeed = self.PatrolMaxSpeed - local CurrentRoutePoint = CurrentPointVec3:RoutePointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToPatrolZoneSpeed, - true - ) - - PatrolRoute[#PatrolRoute+1] = CurrentRoutePoint - - --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... - self.Controllable:WayPointInitialize( PatrolRoute ) - - --- NOW ROUTE THE GROUP! - self.Controllable:WayPointExecute( 1, 1 ) - - end - -end - ---- @param #AI_PATROL_ZONE self -function AI_PATROL_ZONE:onafterDead() - self:SetDetectionOff() - self:SetStatusOff() -end - ---- @param #AI_PATROL_ZONE self --- @param Core.Event#EVENTDATA EventData -function AI_PATROL_ZONE:OnCrash( EventData ) - - if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then - self:E( self.Controllable:GetUnits() ) - if #self.Controllable:GetUnits() == 1 then - self:__Crash( 1, EventData ) - end - end -end - ---- @param #AI_PATROL_ZONE self --- @param Core.Event#EVENTDATA EventData -function AI_PATROL_ZONE:OnEjection( EventData ) - - if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then - self:__Eject( 1, EventData ) - end -end - ---- @param #AI_PATROL_ZONE self --- @param Core.Event#EVENTDATA EventData -function AI_PATROL_ZONE:OnPilotDead( EventData ) - - if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then - self:__PilotDead( 1, EventData ) - end -end ---- **AI** -- **Provide Close Air Support to friendly ground troops.** --- --- ![Banner Image](..\Presentations\AI_CAS\Dia1.JPG) --- --- === --- --- AI CAS classes makes AI Controllables execute a Close Air Support. --- --- There are the following types of CAS classes defined: --- --- * @{#AI_CAS_ZONE}: Perform a CAS in a 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. --- --- ### 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 - ---- # 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 --- --- * **@{AI_Patrol#AI_PATROL_ZONE.Start}**: Start the process. --- * **@{AI_Patrol#AI_PATROL_ZONE.Route}**: Route the AI to a new random 3D point within the Patrol Zone. --- * **@{#AI_CAS_ZONE.Engage}**: Engage the AI to provide CAS in the Engage Zone, destroying any target it finds. --- * **@{#AI_CAS_ZONE.Abort}**: Aborts the engagement and return patrolling in the patrol zone. --- * **@{AI_Patrol#AI_PATROL_ZONE.RTB}**: Route the AI to the home base. --- * **@{AI_Patrol#AI_PATROL_ZONE.Detect}**: The AI is detecting targets. --- * **@{AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets. --- * **@{#AI_CAS_ZONE.Destroy}**: The AI has destroyed a target @{Unit}. --- * **@{#AI_CAS_ZONE.Destroyed}**: The AI has destroyed all target @{Unit}s assigned in the CAS task. --- * **Status**: The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. --- --- === --- --- @field #AI_CAS_ZONE AI_CAS_ZONE --- -AI_CAS_ZONE = { - ClassName = "AI_CAS_ZONE", -} - - - ---- Creates a new AI_CAS_ZONE object --- @param #AI_CAS_ZONE self --- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. --- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. --- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h. --- @param Core.Zone#ZONE_BASE EngageZone The zone where the engage will happen. --- @param Dcs.DCSTypes#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO --- @return #AI_CAS_ZONE self -function AI_CAS_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageZone, PatrolAltType ) - - -- Inherits from BASE - local self = BASE:Inherit( self, AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_CAS_ZONE - - self.EngageZone = EngageZone - self.Accomplished = false - - self:SetDetectionZone( self.EngageZone ) - - self:AddTransition( { "Patrolling", "Engaging" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. - - --- OnBefore Transition Handler for Event Engage. - -- @function [parent=#AI_CAS_ZONE] OnBeforeEngage - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - -- @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 - -- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone. - -- @param Dcs.DCSTypes#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. - -- @param Dcs.DCSTypes#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. - -- If parameter is not defined the unit / controllable will choose expend on its own discretion. - -- Use the structure @{DCSTypes#AI.Task.WeaponExpend} to define the amount of weapons to be release at each attack. - -- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. - -- @param Dcs.DCSTypes#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. - - --- Asynchronous Event Trigger for Event Engage. - -- @function [parent=#AI_CAS_ZONE] __Engage - -- @param #AI_CAS_ZONE self - -- @param #number Delay The delay in seconds. - -- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone. - -- @param Dcs.DCSTypes#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. - -- @param Dcs.DCSTypes#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. - -- If parameter is not defined the unit / controllable will choose expend on its own discretion. - -- Use the structure @{DCSTypes#AI.Task.WeaponExpend} to define the amount of weapons to be release at each attack. - -- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. - -- @param Dcs.DCSTypes#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. - ---- OnLeave Transition Handler for State Engaging. --- @function [parent=#AI_CAS_ZONE] OnLeaveEngaging --- @param #AI_CAS_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnEnter Transition Handler for State Engaging. --- @function [parent=#AI_CAS_ZONE] OnEnterEngaging --- @param #AI_CAS_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - - self:AddTransition( "Engaging", "Target", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. - - self:AddTransition( "Engaging", "Fired", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. - - --- OnBefore Transition Handler for Event Fired. - -- @function [parent=#AI_CAS_ZONE] OnBeforeFired - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Fired. - -- @function [parent=#AI_CAS_ZONE] OnAfterFired - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Fired. - -- @function [parent=#AI_CAS_ZONE] Fired - -- @param #AI_CAS_ZONE self - - --- Asynchronous Event Trigger for Event Fired. - -- @function [parent=#AI_CAS_ZONE] __Fired - -- @param #AI_CAS_ZONE self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. - - --- OnBefore Transition Handler for Event Destroy. - -- @function [parent=#AI_CAS_ZONE] OnBeforeDestroy - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Destroy. - -- @function [parent=#AI_CAS_ZONE] OnAfterDestroy - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Destroy. - -- @function [parent=#AI_CAS_ZONE] Destroy - -- @param #AI_CAS_ZONE self - - --- Asynchronous Event Trigger for Event Destroy. - -- @function [parent=#AI_CAS_ZONE] __Destroy - -- @param #AI_CAS_ZONE self - -- @param #number Delay The delay in seconds. - - - self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. - - --- OnBefore Transition Handler for Event Abort. - -- @function [parent=#AI_CAS_ZONE] OnBeforeAbort - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Abort. - -- @function [parent=#AI_CAS_ZONE] OnAfterAbort - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Abort. - -- @function [parent=#AI_CAS_ZONE] Abort - -- @param #AI_CAS_ZONE self - - --- Asynchronous Event Trigger for Event Abort. - -- @function [parent=#AI_CAS_ZONE] __Abort - -- @param #AI_CAS_ZONE self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. - - --- OnBefore Transition Handler for Event Accomplish. - -- @function [parent=#AI_CAS_ZONE] OnBeforeAccomplish - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Accomplish. - -- @function [parent=#AI_CAS_ZONE] OnAfterAccomplish - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Accomplish. - -- @function [parent=#AI_CAS_ZONE] Accomplish - -- @param #AI_CAS_ZONE self - - --- Asynchronous Event Trigger for Event Accomplish. - -- @function [parent=#AI_CAS_ZONE] __Accomplish - -- @param #AI_CAS_ZONE self - -- @param #number Delay The delay in seconds. - - return self -end - - ---- Set the Engage Zone where the AI is performing CAS. Note that if the EngageZone is changed, the AI needs to re-detect targets. --- @param #AI_CAS_ZONE self --- @param Core.Zone#ZONE EngageZone The zone where the AI is performing CAS. --- @return #AI_CAS_ZONE self -function AI_CAS_ZONE:SetEngageZone( EngageZone ) - self:F2() - - if EngageZone then - self.EngageZone = EngageZone - else - self.EngageZone = nil - end -end - - - ---- onafter State Transition for Event Start. --- @param #AI_CAS_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAS_ZONE:onafterStart( Controllable, From, Event, To ) - - -- Call the parent Start event handler - self:GetParent(self).onafterStart( self, Controllable, From, Event, To ) - self:HandleEvent( EVENTS.Dead ) - - self:SetDetectionDeactivated() -- When not engaging, set the detection off. -end - ---- @param Wrapper.Controllable#CONTROLLABLE AIControllable -function _NewEngageRoute( AIControllable ) - - AIControllable:T( "NewEngageRoute" ) - local EngageZone = AIControllable:GetState( AIControllable, "EngageZone" ) -- AI.AI_Cas#AI_CAS_ZONE - EngageZone:__Engage( 1, EngageZone.EngageSpeed, EngageZone.EngageAltitude, EngageZone.EngageWeaponExpend, EngageZone.EngageAttackQty, EngageZone.EngageDirection ) -end - - ---- @param #AI_CAS_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAS_ZONE:onbeforeEngage( Controllable, From, Event, To ) - - if self.Accomplished == true then - return false - end -end - ---- @param #AI_CAS_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAS_ZONE:onafterTarget( Controllable, From, Event, To ) - self:E("onafterTarget") - - if Controllable:IsAlive() then - - local AttackTasks = {} - - for DetectedUnit, Detected in pairs( self.DetectedUnits ) do - local DetectedUnit = DetectedUnit -- Wrapper.Unit#UNIT - if DetectedUnit:IsAlive() then - if DetectedUnit:IsInZone( self.EngageZone ) then - if Detected == true then - self:E( {"Target: ", DetectedUnit } ) - self.DetectedUnits[DetectedUnit] = false - local AttackTask = Controllable:TaskAttackUnit( DetectedUnit, false, self.EngageWeaponExpend, self.EngageAttackQty, self.EngageDirection, self.EngageAltitude, nil ) - self.Controllable:PushTask( AttackTask, 1 ) - end - end - else - self.DetectedUnits[DetectedUnit] = nil - end - end - - self:__Target( -10 ) - - end -end - - ---- @param #AI_CAS_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAS_ZONE:onafterAbort( Controllable, From, Event, To ) - Controllable:ClearTasks() - self:__Route( 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. --- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone. --- @param Dcs.DCSTypes#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. --- @param Dcs.DCSTypes#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. --- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. --- @param Dcs.DCSTypes#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. -function AI_CAS_ZONE:onafterEngage( Controllable, From, Event, To, - EngageSpeed, - EngageAltitude, - EngageWeaponExpend, - EngageAttackQty, - EngageDirection ) - self:F("onafterEngage") - - self.EngageSpeed = EngageSpeed or 400 - self.EngageAltitude = EngageAltitude or 2000 - self.EngageWeaponExpend = EngageWeaponExpend - self.EngageAttackQty = EngageAttackQty - self.EngageDirection = EngageDirection - - if Controllable:IsAlive() then - - local EngageRoute = {} - - --- Calculate the current route point. - local CurrentVec2 = self.Controllable:GetVec2() - - --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). - local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() - local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) - local ToEngageZoneSpeed = self.PatrolMaxSpeed - local CurrentRoutePoint = CurrentPointVec3:RoutePointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - self.EngageSpeed, - true - ) - - EngageRoute[#EngageRoute+1] = CurrentRoutePoint - - 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, - true, - EngageWeaponExpend, - EngageAttackQty, - EngageDirection - ) - end - else - self.DetectedUnits[DetectedUnit] = nil - end - end - - EngageRoute[1].task = Controllable:TaskCombo( AttackTasks ) - - --- Define a random point in the @{Zone}. The AI will fly to that point within the zone. - - --- Find a random 2D point in EngageZone. - local ToTargetVec2 = self.EngageZone:GetRandomVec2() - self:T2( ToTargetVec2 ) - - --- Obtain a 3D @{Point} from the 2D point + altitude. - local ToTargetPointVec3 = POINT_VEC3:New( ToTargetVec2.x, self.EngageAltitude, ToTargetVec2.y ) - - --- Create a route point of type air. - local ToTargetRoutePoint = ToTargetPointVec3:RoutePointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - self.EngageSpeed, - true - ) - - EngageRoute[#EngageRoute+1] = ToTargetRoutePoint - - --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... - Controllable:WayPointInitialize( EngageRoute ) - - --- Do a trick, link the NewEngageRoute function of the object to the AIControllable in a temporary variable ... - Controllable:SetState( Controllable, "EngageZone", self ) - - Controllable:WayPointFunction( #EngageRoute, 1, "_NewEngageRoute" ) - - --- NOW ROUTE THE GROUP! - Controllable:WayPointExecute( 1 ) - - Controllable:OptionROEOpenFire() - Controllable:OptionROTVertical() - - self:SetDetectionInterval( 2 ) - self:SetDetectionActivated() - self:__Target( -2 ) -- Start Targetting - end -end - - ---- @param #AI_CAS_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAS_ZONE:onafterAccomplish( Controllable, From, Event, To ) - self.Accomplished = true - self:SetDetectionDeactivated() -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 -end - - ---- @param #AI_CAS_ZONE self --- @param Core.Event#EVENTDATA EventData -function AI_CAS_ZONE:OnEventDead( EventData ) - self:F( { "EventDead", EventData } ) - - if EventData.IniDCSUnit then - if self.DetectedUnits and self.DetectedUnits[EventData.IniUnit] then - self:__Destroy( 1, EventData ) - end - end -end - - ---- **AI** - **Execute Combat Air Patrol (CAP).** --- --- ![Banner Image](..\Presentations\AI_CAP\Dia1.JPG) --- --- === --- --- AI CAP classes makes AI Controllables execute a Combat Air Patrol. --- --- There are the following types of CAP classes defined: --- --- * @{#AI_CAP_ZONE}: Perform a CAP in a 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 - - ---- @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 - - ---- # 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 --- --- * **@{AI_Patrol#AI_PATROL_ZONE.Start}**: Start the process. --- * **@{AI_Patrol#AI_PATROL_ZONE.Route}**: Route the AI to a new random 3D point within the Patrol Zone. --- * **@{#AI_CAP_ZONE.Engage}**: Let the AI engage the bogeys. --- * **@{#AI_CAP_ZONE.Abort}**: Aborts the engagement and return patrolling in the patrol zone. --- * **@{AI_Patrol#AI_PATROL_ZONE.RTB}**: Route the AI to the home base. --- * **@{AI_Patrol#AI_PATROL_ZONE.Detect}**: The AI is detecting targets. --- * **@{AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets. --- * **@{#AI_CAP_ZONE.Destroy}**: The AI has destroyed a bogey @{Unit}. --- * **@{#AI_CAP_ZONE.Destroyed}**: The AI has destroyed all bogeys @{Unit}s assigned in the CAS task. --- * **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. --- --- === --- --- @field #AI_CAP_ZONE AI_CAP_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 ) - self:HandleEvent( EVENTS.Dead ) - -end - --- todo: need to fix this global function - ---- @param Wrapper.Controllable#CONTROLLABLE AIControllable -function _NewEngageCapRoute( AIControllable ) - - AIControllable:T( "NewEngageRoute" ) - local EngageZone = AIControllable:GetState( AIControllable, "EngageZone" ) -- AI.AI_Cap#AI_CAP_ZONE - EngageZone:__Engage( 1 ) -end - ---- @param #AI_CAP_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAP_ZONE:onbeforeEngage( Controllable, From, Event, To ) - - if self.Accomplished == true then - return false - end -end - ---- @param #AI_CAP_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAP_ZONE:onafterDetected( Controllable, From, Event, To ) - - if From ~= "Engaging" then - - local Engage = false - - for DetectedUnit, Detected in pairs( self.DetectedUnits ) do - - local DetectedUnit = DetectedUnit -- Wrapper.Unit#UNIT - self:T( DetectedUnit ) - if DetectedUnit:IsAlive() and DetectedUnit:IsAir() then - Engage = true - break - end - end - - if Engage == true then - self:E( 'Detected -> Engaging' ) - self:__Engage( 1 ) - end - end -end - - ---- @param #AI_CAP_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAP_ZONE:onafterAbort( Controllable, From, Event, To ) - Controllable:ClearTasks() - self:__Route( 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:onafterEngage( Controllable, From, Event, To ) - - if Controllable:IsAlive() then - - local EngageRoute = {} - - --- Calculate the current route point. - local CurrentVec2 = self.Controllable:GetVec2() - - --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). - local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() - local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) - local ToEngageZoneSpeed = self.PatrolMaxSpeed - local CurrentRoutePoint = CurrentPointVec3:RoutePointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToEngageZoneSpeed, - true - ) - - EngageRoute[#EngageRoute+1] = CurrentRoutePoint - - - --- Find a random 2D point in PatrolZone. - local ToTargetVec2 = self.PatrolZone:GetRandomVec2() - self:T2( ToTargetVec2 ) - - --- Define Speed and Altitude. - local ToTargetAltitude = math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) - local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) - self:T2( { self.PatrolMinSpeed, self.PatrolMaxSpeed, ToTargetSpeed } ) - - --- Obtain a 3D @{Point} from the 2D point + altitude. - local ToTargetPointVec3 = POINT_VEC3:New( ToTargetVec2.x, ToTargetAltitude, ToTargetVec2.y ) - - --- Create a route point of type air. - local ToPatrolRoutePoint = ToTargetPointVec3:RoutePointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, - true - ) - - EngageRoute[#EngageRoute+1] = ToPatrolRoutePoint - - Controllable:OptionROEOpenFire() - Controllable:OptionROTPassiveDefense() - - local AttackTasks = {} - - for DetectedUnit, Detected in pairs( self.DetectedUnits ) do - local DetectedUnit = DetectedUnit -- Wrapper.Unit#UNIT - self:T( { DetectedUnit, DetectedUnit:IsAlive(), DetectedUnit:IsAir() } ) - if DetectedUnit:IsAlive() and DetectedUnit:IsAir() then - if self.EngageZone then - if DetectedUnit:IsInZone( self.EngageZone ) then - self:E( {"Within Zone and Engaging ", DetectedUnit } ) - AttackTasks[#AttackTasks+1] = Controllable:TaskAttackUnit( DetectedUnit ) - end - else - if self.EngageRange then - if DetectedUnit:GetPointVec3():Get2DDistance(Controllable:GetPointVec3() ) <= self.EngageRange then - self:E( {"Within Range and Engaging", DetectedUnit } ) - AttackTasks[#AttackTasks+1] = Controllable:TaskAttackUnit( DetectedUnit ) - end - else - AttackTasks[#AttackTasks+1] = Controllable:TaskAttackUnit( DetectedUnit ) - end - end - else - self.DetectedUnits[DetectedUnit] = nil - end - end - - --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... - self.Controllable:WayPointInitialize( EngageRoute ) - - - if #AttackTasks == 0 then - self:E("No targets found -> Going back to Patrolling") - self:__Abort( 1 ) - self:__Route( 1 ) - self:SetDetectionActivated() - else - EngageRoute[1].task = Controllable:TaskCombo( AttackTasks ) - - --- Do a trick, link the NewEngageRoute function of the object to the AIControllable in a temporary variable ... - self.Controllable:SetState( self.Controllable, "EngageZone", self ) - - self.Controllable:WayPointFunction( #EngageRoute, 1, "_NewEngageCapRoute" ) - - self:SetDetectionDeactivated() - end - - --- NOW ROUTE THE GROUP! - self.Controllable:WayPointExecute( 1, 2 ) - - end -end - ---- @param #AI_CAP_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAP_ZONE:onafterAccomplish( Controllable, From, Event, To ) - self.Accomplished = true - self:SetDetectionOff() -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 -end - ---- @param #AI_CAP_ZONE self --- @param Core.Event#EVENTDATA EventData -function AI_CAP_ZONE:OnEventDead( EventData ) - self:F( { "EventDead", EventData } ) - - if EventData.IniDCSUnit then - if self.DetectedUnits and self.DetectedUnits[EventData.IniUnit] then - self:__Destroy( 1, EventData ) - end - end -end ----Single-Player:**Yes** / Multi-Player:**Yes** / AI:**Yes** / Human:**No** / Types:**Ground** -- --- **Management of logical cargo objects, that can be transported from and to transportation carriers.** --- --- ![Banner Image](..\Presentations\AI_CARGO\CARGO.JPG) --- --- === --- --- Cargo can be of various forms, always are composed out of ONE object ( one unit or one static or one slingload crate ): --- --- * AI_CARGO_UNIT, represented by a @{Unit} in a @{Group}: Cargo can be represented by a Unit in a Group. Destruction of the Unit will mean that the cargo is lost. --- * CARGO_STATIC, represented by a @{Static}: Cargo can be represented by a Static. Destruction of the Static will mean that the cargo is lost. --- * AI_CARGO_PACKAGE, contained in a @{Unit} of a @{Group}: Cargo can be contained within a Unit of a Group. The cargo can be **delivered** by the @{Unit}. If the Unit is destroyed, the cargo will be destroyed also. --- * AI_CARGO_PACKAGE, Contained in a @{Static}: Cargo can be contained within a Static. The cargo can be **collected** from the @Static. If the @{Static} is destroyed, the cargo will be destroyed. --- * CARGO_SLINGLOAD, represented by a @{Cargo} that is transportable: Cargo can be represented by a Cargo object that is transportable. Destruction of the Cargo will mean that the cargo is lost. --- --- * AI_CARGO_GROUPED, represented by a Group of CARGO_UNITs. --- --- # 1) @{#AI_CARGO} class, extends @{Fsm#FSM_PROCESS} --- --- The @{#AI_CARGO} class defines the core functions that defines a cargo object within MOOSE. --- A cargo is a logical object defined that is available for transport, and has a life status within a simulation. --- --- The AI_CARGO is a state machine: it manages the different events and states of the cargo. --- All derived classes from AI_CARGO follow the same state machine, expose the same cargo event functions, and provide the same cargo states. --- --- ## 1.2.1) AI_CARGO Events: --- --- * @{#AI_CARGO.Board}( ToCarrier ): Boards the cargo to a carrier. --- * @{#AI_CARGO.Load}( ToCarrier ): Loads the cargo into a carrier, regardless of its position. --- * @{#AI_CARGO.UnBoard}( ToPointVec2 ): UnBoard the cargo from a carrier. This will trigger a movement of the cargo to the option ToPointVec2. --- * @{#AI_CARGO.UnLoad}( ToPointVec2 ): UnLoads the cargo from a carrier. --- * @{#AI_CARGO.Dead}( Controllable ): The cargo is dead. The cargo process will be ended. --- --- ## 1.2.2) AI_CARGO States: --- --- * **UnLoaded**: The cargo is unloaded from a carrier. --- * **Boarding**: The cargo is currently boarding (= running) into a carrier. --- * **Loaded**: The cargo is loaded into a carrier. --- * **UnBoarding**: The cargo is currently unboarding (=running) from a carrier. --- * **Dead**: The cargo is dead ... --- * **End**: The process has come to an end. --- --- ## 1.2.3) AI_CARGO state transition methods: --- --- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. --- There are 2 moments when state transition methods will be called by the state machine: --- --- * **Leaving** the state. --- The state transition method needs to start with the name **OnLeave + the name of the state**. --- If the state transition method returns false, then the processing of the state transition will not be done! --- If you want to change the behaviour of the AIControllable at this event, return false, --- but then you'll need to specify your own logic using the AIControllable! --- --- * **Entering** the state. --- The state transition method needs to start with the name **OnEnter + the name of the state**. --- These state transition methods need to provide a return value, which is specified at the function description. --- --- # 2) #AI_CARGO_UNIT class --- --- The AI_CARGO_UNIT class defines a cargo that is represented by a UNIT object within the simulator, and can be transported by a carrier. --- Use the event functions as described above to Load, UnLoad, Board, UnBoard the AI_CARGO_UNIT objects to and from carriers. --- --- # 5) #AI_CARGO_GROUPED class --- --- The AI_CARGO_GROUPED class defines a cargo that is represented by a group of UNIT objects within the simulator, and can be transported by a carrier. --- Use the event functions as described above to Load, UnLoad, Board, UnBoard the AI_CARGO_UNIT objects to and from carriers. --- --- This module is still under construction, but is described above works already, and will keep working ... --- --- @module Cargo - --- Events - --- Board - ---- Boards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo to the Carrier. --- The cargo must be in the **UnLoaded** state. --- @function [parent=#AI_CARGO] Board --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. - ---- Boards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo to the Carrier. --- The cargo must be in the **UnLoaded** state. --- @function [parent=#AI_CARGO] __Board --- @param #AI_CARGO self --- @param #number DelaySeconds The amount of seconds to delay the action. --- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. - - --- UnBoard - ---- UnBoards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo from the Carrier. --- The cargo must be in the **Loaded** state. --- @function [parent=#AI_CARGO] UnBoard --- @param #AI_CARGO self --- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Point#POINT_VEC2) to where the cargo should run after onboarding. If not provided, the cargo will run to 60 meters behind the Carrier location. - ---- UnBoards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo from the Carrier. --- The cargo must be in the **Loaded** state. --- @function [parent=#AI_CARGO] __UnBoard --- @param #AI_CARGO self --- @param #number DelaySeconds The amount of seconds to delay the action. --- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Point#POINT_VEC2) to where the cargo should run after onboarding. If not provided, the cargo will run to 60 meters behind the Carrier location. - - --- Load - ---- Loads the cargo to a Carrier. The event will load the cargo into the Carrier regardless of its position. There will be no movement simulated of the cargo loading. --- The cargo must be in the **UnLoaded** state. --- @function [parent=#AI_CARGO] Load --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. - ---- Loads the cargo to a Carrier. The event will load the cargo into the Carrier regardless of its position. There will be no movement simulated of the cargo loading. --- The cargo must be in the **UnLoaded** state. --- @function [parent=#AI_CARGO] __Load --- @param #AI_CARGO self --- @param #number DelaySeconds The amount of seconds to delay the action. --- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. - - --- UnLoad - ---- UnLoads the cargo to a Carrier. The event will unload the cargo from the Carrier. There will be no movement simulated of the cargo loading. --- The cargo must be in the **Loaded** state. --- @function [parent=#AI_CARGO] UnLoad --- @param #AI_CARGO self --- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Point#POINT_VEC2) to where the cargo will be placed after unloading. If not provided, the cargo will be placed 60 meters behind the Carrier location. - ---- UnLoads the cargo to a Carrier. The event will unload the cargo from the Carrier. There will be no movement simulated of the cargo loading. --- The cargo must be in the **Loaded** state. --- @function [parent=#AI_CARGO] __UnLoad --- @param #AI_CARGO self --- @param #number DelaySeconds The amount of seconds to delay the action. --- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Point#POINT_VEC2) to where the cargo will be placed after unloading. If not provided, the cargo will be placed 60 meters behind the Carrier location. - --- State Transition Functions - --- UnLoaded - ---- @function [parent=#AI_CARGO] OnLeaveUnLoaded --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable --- @return #boolean - ---- @function [parent=#AI_CARGO] OnEnterUnLoaded --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable - --- Loaded - ---- @function [parent=#AI_CARGO] OnLeaveLoaded --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable --- @return #boolean - ---- @function [parent=#AI_CARGO] OnEnterLoaded --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable - --- Boarding - ---- @function [parent=#AI_CARGO] OnLeaveBoarding --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable --- @return #boolean - ---- @function [parent=#AI_CARGO] OnEnterBoarding --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable - --- UnBoarding - ---- @function [parent=#AI_CARGO] OnLeaveUnBoarding --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable --- @return #boolean - ---- @function [parent=#AI_CARGO] OnEnterUnBoarding --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable - - --- TODO: Find all Carrier objects and make the type of the Carriers Wrapper.Unit#UNIT in the documentation. - -CARGOS = {} - -do -- AI_CARGO - - --- @type AI_CARGO - -- @extends Core.Fsm#FSM_PROCESS - -- @field #string Type A string defining the type of the cargo. eg. Engineers, Equipment, Screwdrivers. - -- @field #string Name A string defining the name of the cargo. The name is the unique identifier of the cargo. - -- @field #number Weight A number defining the weight of the cargo. The weight is expressed in kg. - -- @field #number ReportRadius (optional) A number defining the radius in meters when the cargo is signalling or reporting to a Carrier. - -- @field #number NearRadius (optional) A number defining the radius in meters when the cargo is near to a Carrier, so that it can be loaded. - -- @field Wrapper.Controllable#CONTROLLABLE CargoObject The alive DCS object representing the cargo. This value can be nil, meaning, that the cargo is not represented anywhere... - -- @field Wrapper.Controllable#CONTROLLABLE CargoCarrier The alive DCS object carrying the cargo. This value can be nil, meaning, that the cargo is not contained anywhere... - -- @field #boolean Slingloadable This flag defines if the cargo can be slingloaded. - -- @field #boolean Moveable This flag defines if the cargo is moveable. - -- @field #boolean Representable This flag defines if the cargo can be represented by a DCS Unit. - -- @field #boolean Containable This flag defines if the cargo can be contained within a DCS Unit. - AI_CARGO = { - ClassName = "AI_CARGO", - Type = nil, - Name = nil, - Weight = nil, - CargoObject = nil, - CargoCarrier = nil, - Representable = false, - Slingloadable = false, - Moveable = false, - Containable = false, - } - ---- @type AI_CARGO.CargoObjects --- @map < #string, Wrapper.Positionable#POSITIONABLE > The alive POSITIONABLE objects representing the the cargo. - - ---- AI_CARGO Constructor. This class is an abstract class and should not be instantiated. --- @param #AI_CARGO self --- @param #string Type --- @param #string Name --- @param #number Weight --- @param #number ReportRadius (optional) --- @param #number NearRadius (optional) --- @return #AI_CARGO -function AI_CARGO:New( Type, Name, Weight, ReportRadius, NearRadius ) - - local self = BASE:Inherit( self, FSM:New() ) -- Core.Fsm#FSM_CONTROLLABLE - self:F( { Type, Name, Weight, ReportRadius, NearRadius } ) - - self:SetStartState( "UnLoaded" ) - self:AddTransition( "UnLoaded", "Board", "Boarding" ) - self:AddTransition( "Boarding", "Boarding", "Boarding" ) - self:AddTransition( "Boarding", "Load", "Loaded" ) - self:AddTransition( "UnLoaded", "Load", "Loaded" ) - self:AddTransition( "Loaded", "UnBoard", "UnBoarding" ) - self:AddTransition( "UnBoarding", "UnBoarding", "UnBoarding" ) - self:AddTransition( "UnBoarding", "UnLoad", "UnLoaded" ) - self:AddTransition( "Loaded", "UnLoad", "UnLoaded" ) - - - self.Type = Type - self.Name = Name - self.Weight = Weight - self.ReportRadius = ReportRadius - self.NearRadius = NearRadius - self.CargoObject = nil - self.CargoCarrier = nil - self.Representable = false - self.Slingloadable = false - self.Moveable = false - self.Containable = false - - - self.CargoScheduler = SCHEDULER:New() - - CARGOS[self.Name] = self - - return self -end - - ---- Template method to spawn a new representation of the AI_CARGO in the simulator. --- @param #AI_CARGO self --- @return #AI_CARGO -function AI_CARGO:Spawn( PointVec2 ) - self:F() - -end - - ---- Check if CargoCarrier is near the Cargo to be Loaded. --- @param #AI_CARGO self --- @param Core.Point#POINT_VEC2 PointVec2 --- @return #boolean -function AI_CARGO:IsNear( PointVec2 ) - self:F( { PointVec2 } ) - - local Distance = PointVec2:DistanceFromPointVec2( self.CargoObject:GetPointVec2() ) - self:T( Distance ) - - if Distance <= self.NearRadius then - return true - else - return false - end -end - -end - -do -- AI_CARGO_REPRESENTABLE - - --- @type AI_CARGO_REPRESENTABLE - -- @extends #AI_CARGO - AI_CARGO_REPRESENTABLE = { - ClassName = "AI_CARGO_REPRESENTABLE" - } - ---- AI_CARGO_REPRESENTABLE Constructor. --- @param #AI_CARGO_REPRESENTABLE self --- @param Wrapper.Controllable#Controllable CargoObject --- @param #string Type --- @param #string Name --- @param #number Weight --- @param #number ReportRadius (optional) --- @param #number NearRadius (optional) --- @return #AI_CARGO_REPRESENTABLE -function AI_CARGO_REPRESENTABLE:New( CargoObject, Type, Name, Weight, ReportRadius, NearRadius ) - local self = BASE:Inherit( self, AI_CARGO:New( Type, Name, Weight, ReportRadius, NearRadius ) ) -- #AI_CARGO - self:F( { Type, Name, Weight, ReportRadius, NearRadius } ) - - return self -end - ---- Route a cargo unit to a PointVec2. --- @param #AI_CARGO_REPRESENTABLE self --- @param Core.Point#POINT_VEC2 ToPointVec2 --- @param #number Speed --- @return #AI_CARGO_REPRESENTABLE -function AI_CARGO_REPRESENTABLE:RouteTo( ToPointVec2, Speed ) - self:F2( ToPointVec2 ) - - local Points = {} - - local PointStartVec2 = self.CargoObject:GetPointVec2() - - Points[#Points+1] = PointStartVec2:RoutePointGround( Speed ) - Points[#Points+1] = ToPointVec2:RoutePointGround( Speed ) - - local TaskRoute = self.CargoObject:TaskRoute( Points ) - self.CargoObject:SetTask( TaskRoute, 2 ) - return self -end - -end -- AI_CARGO - -do -- AI_CARGO_UNIT - - --- @type AI_CARGO_UNIT - -- @extends #AI_CARGO_REPRESENTABLE - AI_CARGO_UNIT = { - ClassName = "AI_CARGO_UNIT" - } - ---- AI_CARGO_UNIT Constructor. --- @param #AI_CARGO_UNIT self --- @param Wrapper.Unit#UNIT CargoUnit --- @param #string Type --- @param #string Name --- @param #number Weight --- @param #number ReportRadius (optional) --- @param #number NearRadius (optional) --- @return #AI_CARGO_UNIT -function AI_CARGO_UNIT:New( CargoUnit, Type, Name, Weight, ReportRadius, NearRadius ) - local self = BASE:Inherit( self, AI_CARGO_REPRESENTABLE:New( CargoUnit, Type, Name, Weight, ReportRadius, NearRadius ) ) -- #AI_CARGO_UNIT - self:F( { Type, Name, Weight, ReportRadius, NearRadius } ) - - self:T( CargoUnit ) - self.CargoObject = CargoUnit - - self:T( self.ClassName ) - - return self -end - ---- Enter UnBoarding State. --- @param #AI_CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To --- @param Core.Point#POINT_VEC2 ToPointVec2 -function AI_CARGO_UNIT:onenterUnBoarding( From, Event, To, ToPointVec2 ) - self:F() - - local Angle = 180 - local Speed = 10 - local DeployDistance = 5 - local RouteDistance = 60 - - if From == "Loaded" then - - local CargoCarrierPointVec2 = self.CargoCarrier:GetPointVec2() - local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - local CargoDeployPointVec2 = CargoCarrierPointVec2:Translate( DeployDistance, CargoDeployHeading ) - local CargoRoutePointVec2 = CargoCarrierPointVec2:Translate( RouteDistance, CargoDeployHeading ) - - -- if there is no ToPointVec2 given, then use the CargoRoutePointVec2 - ToPointVec2 = ToPointVec2 or CargoRoutePointVec2 - - local FromPointVec2 = CargoCarrierPointVec2 - - -- Respawn the group... - if self.CargoObject then - self.CargoObject:ReSpawn( CargoDeployPointVec2:GetVec3(), CargoDeployHeading ) - self.CargoCarrier = nil - - local Points = {} - Points[#Points+1] = FromPointVec2:RoutePointGround( Speed ) - Points[#Points+1] = ToPointVec2:RoutePointGround( Speed ) - - local TaskRoute = self.CargoObject:TaskRoute( Points ) - self.CargoObject:SetTask( TaskRoute, 1 ) - - self:__UnBoarding( 1, ToPointVec2 ) - end - end - -end - ---- Leave UnBoarding State. --- @param #AI_CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To --- @param Core.Point#POINT_VEC2 ToPointVec2 -function AI_CARGO_UNIT:onleaveUnBoarding( From, Event, To, ToPointVec2 ) - self:F( { ToPointVec2, From, Event, To } ) - - local Angle = 180 - local Speed = 10 - local Distance = 5 - - if From == "UnBoarding" then - if self:IsNear( ToPointVec2 ) then - return true - else - self:__UnBoarding( 1, ToPointVec2 ) - end - return false - end - -end - ---- UnBoard Event. --- @param #AI_CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To --- @param Core.Point#POINT_VEC2 ToPointVec2 -function AI_CARGO_UNIT:onafterUnBoarding( From, Event, To, ToPointVec2 ) - self:F( { ToPointVec2, From, Event, To } ) - - self.CargoInAir = self.CargoObject:InAir() - - self:T( self.CargoInAir ) - - -- Only unboard the cargo when the carrier is not in the air. - -- (eg. cargo can be on a oil derrick, moving the cargo on the oil derrick will drop the cargo on the sea). - if not self.CargoInAir then - - end - - self:__UnLoad( 1, ToPointVec2 ) - -end - - - ---- Enter UnLoaded State. --- @param #AI_CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To --- @param Core.Point#POINT_VEC2 -function AI_CARGO_UNIT:onenterUnLoaded( From, Event, To, ToPointVec2 ) - self:F( { ToPointVec2, From, Event, To } ) - - local Angle = 180 - local Speed = 10 - local Distance = 5 - - if From == "Loaded" then - local StartPointVec2 = self.CargoCarrier:GetPointVec2() - local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - local CargoDeployPointVec2 = StartPointVec2:Translate( Distance, CargoDeployHeading ) - - ToPointVec2 = ToPointVec2 or POINT_VEC2:New( CargoDeployPointVec2:GetX(), CargoDeployPointVec2:GetY() ) - - -- Respawn the group... - if self.CargoObject then - self.CargoObject:ReSpawn( ToPointVec2:GetVec3(), 0 ) - self.CargoCarrier = nil - end - - end - - if self.OnUnLoadedCallBack then - self.OnUnLoadedCallBack( self, unpack( self.OnUnLoadedParameters ) ) - self.OnUnLoadedCallBack = nil - end - -end - - - ---- Enter Boarding State. --- @param #AI_CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT CargoCarrier -function AI_CARGO_UNIT:onenterBoarding( From, Event, To, CargoCarrier ) - self:F( { CargoCarrier.UnitName, From, Event, To } ) - - local Speed = 10 - local Angle = 180 - local Distance = 5 - - if From == "UnLoaded" then - local CargoCarrierPointVec2 = CargoCarrier:GetPointVec2() - local CargoCarrierHeading = CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - local CargoDeployPointVec2 = CargoCarrierPointVec2:Translate( Distance, CargoDeployHeading ) - - local Points = {} - - local PointStartVec2 = self.CargoObject:GetPointVec2() - - Points[#Points+1] = PointStartVec2:RoutePointGround( Speed ) - Points[#Points+1] = CargoDeployPointVec2:RoutePointGround( Speed ) - - local TaskRoute = self.CargoObject:TaskRoute( Points ) - self.CargoObject:SetTask( TaskRoute, 2 ) - end - -end - ---- Leave Boarding State. --- @param #AI_CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT CargoCarrier -function AI_CARGO_UNIT:onleaveBoarding( From, Event, To, CargoCarrier ) - self:F( { CargoCarrier.UnitName, From, Event, To } ) - - if self:IsNear( CargoCarrier:GetPointVec2() ) then - self:__Load( 1, CargoCarrier ) - return true - else - self:__Boarding( 1, CargoCarrier ) - end - return false -end - ---- Loaded State. --- @param #AI_CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT CargoCarrier -function AI_CARGO_UNIT:onenterLoaded( From, Event, To, CargoCarrier ) - self:F() - - self.CargoCarrier = CargoCarrier - - -- Only destroy the CargoObject is if there is a CargoObject (packages don't have CargoObjects). - if self.CargoObject then - self:T("Destroying") - self.CargoObject:Destroy() - end -end - - ---- Board Event. --- @param #AI_CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To -function AI_CARGO_UNIT:onafterBoard( From, Event, To, CargoCarrier ) - self:F() - - self.CargoInAir = self.CargoObject:InAir() - - self:T( self.CargoInAir ) - - -- Only move the group to the carrier when the cargo is not in the air - -- (eg. cargo can be on a oil derrick, moving the cargo on the oil derrick will drop the cargo on the sea). - if not self.CargoInAir then - self:Load( CargoCarrier ) - end - -end - -end - -do -- AI_CARGO_PACKAGE - - --- @type AI_CARGO_PACKAGE - -- @extends #AI_CARGO_REPRESENTABLE - AI_CARGO_PACKAGE = { - ClassName = "AI_CARGO_PACKAGE" - } - ---- AI_CARGO_PACKAGE Constructor. --- @param #AI_CARGO_PACKAGE self --- @param Wrapper.Unit#UNIT CargoCarrier The UNIT carrying the package. --- @param #string Type --- @param #string Name --- @param #number Weight --- @param #number ReportRadius (optional) --- @param #number NearRadius (optional) --- @return #AI_CARGO_PACKAGE -function AI_CARGO_PACKAGE:New( CargoCarrier, Type, Name, Weight, ReportRadius, NearRadius ) - local self = BASE:Inherit( self, AI_CARGO_REPRESENTABLE:New( CargoCarrier, Type, Name, Weight, ReportRadius, NearRadius ) ) -- #AI_CARGO_PACKAGE - self:F( { Type, Name, Weight, ReportRadius, NearRadius } ) - - self:T( CargoCarrier ) - self.CargoCarrier = CargoCarrier - - return self -end - ---- Board Event. --- @param #AI_CARGO_PACKAGE self --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT CargoCarrier --- @param #number Speed --- @param #number BoardDistance --- @param #number Angle -function AI_CARGO_PACKAGE:onafterOnBoard( From, Event, To, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) - self:F() - - self.CargoInAir = self.CargoCarrier:InAir() - - self:T( self.CargoInAir ) - - -- Only move the CargoCarrier to the New CargoCarrier when the New CargoCarrier is not in the air. - if not self.CargoInAir then - - local Points = {} - - local StartPointVec2 = self.CargoCarrier:GetPointVec2() - local CargoCarrierHeading = CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - self:T( { CargoCarrierHeading, CargoDeployHeading } ) - local CargoDeployPointVec2 = CargoCarrier:GetPointVec2():Translate( BoardDistance, CargoDeployHeading ) - - Points[#Points+1] = StartPointVec2:RoutePointGround( Speed ) - Points[#Points+1] = CargoDeployPointVec2:RoutePointGround( Speed ) - - local TaskRoute = self.CargoCarrier:TaskRoute( Points ) - self.CargoCarrier:SetTask( TaskRoute, 1 ) - end - - self:Boarded( CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) - -end - ---- Check if CargoCarrier is near the Cargo to be Loaded. --- @param #AI_CARGO_PACKAGE self --- @param Wrapper.Unit#UNIT CargoCarrier --- @return #boolean -function AI_CARGO_PACKAGE:IsNear( CargoCarrier ) - self:F() - - local CargoCarrierPoint = CargoCarrier:GetPointVec2() - - local Distance = CargoCarrierPoint:DistanceFromPointVec2( self.CargoCarrier:GetPointVec2() ) - self:T( Distance ) - - if Distance <= self.NearRadius then - return true - else - return false - end -end - ---- Boarded Event. --- @param #AI_CARGO_PACKAGE self --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT CargoCarrier -function AI_CARGO_PACKAGE:onafterOnBoarded( From, Event, To, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) - self:F() - - if self:IsNear( CargoCarrier ) then - self:__Load( 1, CargoCarrier, Speed, LoadDistance, Angle ) - else - self:__Boarded( 1, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) - end -end - ---- UnBoard Event. --- @param #AI_CARGO_PACKAGE self --- @param #string Event --- @param #string From --- @param #string To --- @param #number Speed --- @param #number UnLoadDistance --- @param #number UnBoardDistance --- @param #number Radius --- @param #number Angle -function AI_CARGO_PACKAGE:onafterUnBoard( From, Event, To, CargoCarrier, Speed, UnLoadDistance, UnBoardDistance, Radius, Angle ) - self:F() - - self.CargoInAir = self.CargoCarrier:InAir() - - self:T( self.CargoInAir ) - - -- Only unboard the cargo when the carrier is not in the air. - -- (eg. cargo can be on a oil derrick, moving the cargo on the oil derrick will drop the cargo on the sea). - if not self.CargoInAir then - - self:_Next( self.FsmP.UnLoad, UnLoadDistance, Angle ) - - local Points = {} - - local StartPointVec2 = CargoCarrier:GetPointVec2() - local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - self:T( { CargoCarrierHeading, CargoDeployHeading } ) - local CargoDeployPointVec2 = StartPointVec2:Translate( UnBoardDistance, CargoDeployHeading ) - - Points[#Points+1] = StartPointVec2:RoutePointGround( Speed ) - Points[#Points+1] = CargoDeployPointVec2:RoutePointGround( Speed ) - - local TaskRoute = CargoCarrier:TaskRoute( Points ) - CargoCarrier:SetTask( TaskRoute, 1 ) - end - - self:__UnBoarded( 1 , CargoCarrier, Speed ) - -end - ---- UnBoarded Event. --- @param #AI_CARGO_PACKAGE self --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT CargoCarrier -function AI_CARGO_PACKAGE:onafterUnBoarded( From, Event, To, CargoCarrier, Speed ) - self:F() - - if self:IsNear( CargoCarrier ) then - self:__UnLoad( 1, CargoCarrier, Speed ) - else - self:__UnBoarded( 1, CargoCarrier, Speed ) - end -end - ---- Load Event. --- @param #AI_CARGO_PACKAGE self --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT CargoCarrier --- @param #number Speed --- @param #number LoadDistance --- @param #number Angle -function AI_CARGO_PACKAGE:onafterLoad( From, Event, To, CargoCarrier, Speed, LoadDistance, Angle ) - self:F() - - self.CargoCarrier = CargoCarrier - - local StartPointVec2 = self.CargoCarrier:GetPointVec2() - local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - local CargoDeployPointVec2 = StartPointVec2:Translate( LoadDistance, CargoDeployHeading ) - - local Points = {} - Points[#Points+1] = StartPointVec2:RoutePointGround( Speed ) - Points[#Points+1] = CargoDeployPointVec2:RoutePointGround( Speed ) - - local TaskRoute = self.CargoCarrier:TaskRoute( Points ) - self.CargoCarrier:SetTask( TaskRoute, 1 ) - -end - ---- UnLoad Event. --- @param #AI_CARGO_PACKAGE self --- @param #string Event --- @param #string From --- @param #string To --- @param #number Distance --- @param #number Angle -function AI_CARGO_PACKAGE:onafterUnLoad( From, Event, To, CargoCarrier, Speed, Distance, Angle ) - self:F() - - local StartPointVec2 = self.CargoCarrier:GetPointVec2() - local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - local CargoDeployPointVec2 = StartPointVec2:Translate( Distance, CargoDeployHeading ) - - self.CargoCarrier = CargoCarrier - - local Points = {} - Points[#Points+1] = StartPointVec2:RoutePointGround( Speed ) - Points[#Points+1] = CargoDeployPointVec2:RoutePointGround( Speed ) - - local TaskRoute = self.CargoCarrier:TaskRoute( Points ) - self.CargoCarrier:SetTask( TaskRoute, 1 ) - -end - - -end - -do -- AI_CARGO_GROUP - - --- @type AI_CARGO_GROUP - -- @extends AI.AI_Cargo#AI_CARGO - -- @field Set#SET_BASE CargoSet A set of cargo objects. - -- @field #string Name A string defining the name of the cargo group. The name is the unique identifier of the cargo. - AI_CARGO_GROUP = { - ClassName = "AI_CARGO_GROUP", - } - ---- AI_CARGO_GROUP constructor. --- @param #AI_CARGO_GROUP self --- @param Core.Set#Set_BASE CargoSet --- @param #string Type --- @param #string Name --- @param #number Weight --- @param #number ReportRadius (optional) --- @param #number NearRadius (optional) --- @return #AI_CARGO_GROUP -function AI_CARGO_GROUP:New( CargoSet, Type, Name, ReportRadius, NearRadius ) - local self = BASE:Inherit( self, AI_CARGO:New( Type, Name, 0, ReportRadius, NearRadius ) ) -- #AI_CARGO_GROUP - self:F( { Type, Name, ReportRadius, NearRadius } ) - - self.CargoSet = CargoSet - - - return self -end - -end -- AI_CARGO_GROUP - -do -- AI_CARGO_GROUPED - - --- @type AI_CARGO_GROUPED - -- @extends AI.AI_Cargo#AI_CARGO_GROUP - AI_CARGO_GROUPED = { - ClassName = "AI_CARGO_GROUPED", - } - ---- AI_CARGO_GROUPED constructor. --- @param #AI_CARGO_GROUPED self --- @param Core.Set#Set_BASE CargoSet --- @param #string Type --- @param #string Name --- @param #number Weight --- @param #number ReportRadius (optional) --- @param #number NearRadius (optional) --- @return #AI_CARGO_GROUPED -function AI_CARGO_GROUPED:New( CargoSet, Type, Name, ReportRadius, NearRadius ) - local self = BASE:Inherit( self, AI_CARGO_GROUP:New( CargoSet, Type, Name, ReportRadius, NearRadius ) ) -- #AI_CARGO_GROUPED - self:F( { Type, Name, ReportRadius, NearRadius } ) - - return self -end - ---- Enter Boarding State. --- @param #AI_CARGO_GROUPED self --- @param Wrapper.Unit#UNIT CargoCarrier --- @param #string Event --- @param #string From --- @param #string To -function AI_CARGO_GROUPED:onenterBoarding( From, Event, To, CargoCarrier ) - self:F( { CargoCarrier.UnitName, From, Event, To } ) - - if From == "UnLoaded" then - - -- For each Cargo object within the AI_CARGO_GROUPED, route each object to the CargoLoadPointVec2 - self.CargoSet:ForEach( - function( Cargo ) - Cargo:__Board( 1, CargoCarrier ) - end - ) - - self:__Boarding( 1, CargoCarrier ) - end - -end - ---- Enter Loaded State. --- @param #AI_CARGO_GROUPED self --- @param Wrapper.Unit#UNIT CargoCarrier --- @param #string Event --- @param #string From --- @param #string To -function AI_CARGO_GROUPED:onenterLoaded( From, Event, To, CargoCarrier ) - self:F( { CargoCarrier.UnitName, From, Event, To } ) - - if From == "UnLoaded" then - -- For each Cargo object within the AI_CARGO_GROUPED, load each cargo to the CargoCarrier. - for CargoID, Cargo in pairs( self.CargoSet:GetSet() ) do - Cargo:Load( CargoCarrier ) - end - end -end - ---- Leave Boarding State. --- @param #AI_CARGO_GROUPED self --- @param Wrapper.Unit#UNIT CargoCarrier --- @param #string Event --- @param #string From --- @param #string To -function AI_CARGO_GROUPED:onleaveBoarding( From, Event, To, CargoCarrier ) - self:F( { CargoCarrier.UnitName, From, Event, To } ) - - local Boarded = true - - -- For each Cargo object within the AI_CARGO_GROUPED, route each object to the CargoLoadPointVec2 - for CargoID, Cargo in pairs( self.CargoSet:GetSet() ) do - self:T( Cargo.current ) - if not Cargo:is( "Loaded" ) then - Boarded = false - end - end - - if not Boarded then - self:__Boarding( 1, CargoCarrier ) - else - self:__Load( 1, CargoCarrier ) - end - return Boarded -end - ---- Enter UnBoarding State. --- @param #AI_CARGO_GROUPED self --- @param Core.Point#POINT_VEC2 ToPointVec2 --- @param #string Event --- @param #string From --- @param #string To -function AI_CARGO_GROUPED:onenterUnBoarding( From, Event, To, ToPointVec2 ) - self:F() - - local Timer = 1 - - if From == "Loaded" then - - -- For each Cargo object within the AI_CARGO_GROUPED, route each object to the CargoLoadPointVec2 - self.CargoSet:ForEach( - function( Cargo ) - Cargo:__UnBoard( Timer, ToPointVec2 ) - Timer = Timer + 10 - end - ) - - self:__UnBoarding( 1, ToPointVec2 ) - end - -end - ---- Leave UnBoarding State. --- @param #AI_CARGO_GROUPED self --- @param Core.Point#POINT_VEC2 ToPointVec2 --- @param #string Event --- @param #string From --- @param #string To -function AI_CARGO_GROUPED:onleaveUnBoarding( From, Event, To, ToPointVec2 ) - self:F( { ToPointVec2, From, Event, To } ) - - local Angle = 180 - local Speed = 10 - local Distance = 5 - - if From == "UnBoarding" then - local UnBoarded = true - - -- For each Cargo object within the AI_CARGO_GROUPED, route each object to the CargoLoadPointVec2 - for CargoID, Cargo in pairs( self.CargoSet:GetSet() ) do - self:T( Cargo.current ) - if not Cargo:is( "UnLoaded" ) then - UnBoarded = false - end - end - - if UnBoarded then - return true - else - self:__UnBoarding( 1, ToPointVec2 ) - end - - return false - end - -end - ---- UnBoard Event. --- @param #AI_CARGO_GROUPED self --- @param Core.Point#POINT_VEC2 ToPointVec2 --- @param #string Event --- @param #string From --- @param #string To -function AI_CARGO_GROUPED:onafterUnBoarding( From, Event, To, ToPointVec2 ) - self:F( { ToPointVec2, From, Event, To } ) - - self:__UnLoad( 1, ToPointVec2 ) -end - - - ---- Enter UnLoaded State. --- @param #AI_CARGO_GROUPED self --- @param Core.Point#POINT_VEC2 --- @param #string Event --- @param #string From --- @param #string To -function AI_CARGO_GROUPED:onenterUnLoaded( From, Event, To, ToPointVec2 ) - self:F( { ToPointVec2, From, Event, To } ) - - if From == "Loaded" then - - -- For each Cargo object within the AI_CARGO_GROUPED, route each object to the CargoLoadPointVec2 - self.CargoSet:ForEach( - function( Cargo ) - Cargo:UnLoad( ToPointVec2 ) - end - ) - - end - -end - -end -- AI_CARGO_GROUPED - - - ---- (SP) (MP) (FSM) Accept or reject process for player (task) assignments. --- --- === --- --- # @{#ACT_ASSIGN} FSM template class, extends @{Fsm#FSM_PROCESS} --- --- ## ACT_ASSIGN state machine: --- --- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur. --- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below. --- Each derived class follows exactly the same process, using the same events and following the same state transitions, --- but will have **different implementation behaviour** upon each event or state transition. --- --- ### ACT_ASSIGN **Events**: --- --- These are the events defined in this class: --- --- * **Start**: Start the tasking acceptance process. --- * **Assign**: Assign the task. --- * **Reject**: Reject the task.. --- --- ### ACT_ASSIGN **Event methods**: --- --- Event methods are available (dynamically allocated by the state machine), that accomodate for state transitions occurring in the process. --- There are two types of event methods, which you can use to influence the normal mechanisms in the state machine: --- --- * **Immediate**: The event method has exactly the name of the event. --- * **Delayed**: The event method starts with a __ + the name of the event. The first parameter of the event method is a number value, expressing the delay in seconds when the event will be executed. --- --- ### ACT_ASSIGN **States**: --- --- * **UnAssigned**: The player has not accepted the task. --- * **Assigned (*)**: The player has accepted the task. --- * **Rejected (*)**: The player has not accepted the task. --- * **Waiting**: The process is awaiting player feedback. --- * **Failed (*)**: The process has failed. --- --- (*) End states of the process. --- --- ### ACT_ASSIGN state transition methods: --- --- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. --- There are 2 moments when state transition methods will be called by the state machine: --- --- * **Before** the state transition. --- The state transition method needs to start with the name **OnBefore + the name of the state**. --- If the state transition method returns false, then the processing of the state transition will not be done! --- If you want to change the behaviour of the AIControllable at this event, return false, --- but then you'll need to specify your own logic using the AIControllable! --- --- * **After** the state transition. --- The state transition method needs to start with the name **OnAfter + the name of the state**. --- These state transition methods need to provide a return value, which is specified at the function description. --- --- === --- --- # 1) @{#ACT_ASSIGN_ACCEPT} class, extends @{Fsm.Assign#ACT_ASSIGN} --- --- The ACT_ASSIGN_ACCEPT class accepts by default a task for a player. No player intervention is allowed to reject the task. --- --- ## 1.1) ACT_ASSIGN_ACCEPT constructor: --- --- * @{#ACT_ASSIGN_ACCEPT.New}(): Creates a new ACT_ASSIGN_ACCEPT object. --- --- === --- --- # 2) @{#ACT_ASSIGN_MENU_ACCEPT} class, extends @{Fsm.Assign#ACT_ASSIGN} --- --- The ACT_ASSIGN_MENU_ACCEPT class accepts a task when the player accepts the task through an added menu option. --- This assignment type is useful to conditionally allow the player to choose whether or not he would accept the task. --- The assignment type also allows to reject the task. --- --- ## 2.1) ACT_ASSIGN_MENU_ACCEPT constructor: --- ----------------------------------------- --- --- * @{#ACT_ASSIGN_MENU_ACCEPT.New}(): Creates a new ACT_ASSIGN_MENU_ACCEPT object. --- --- === --- --- @module Assign - - -do -- ACT_ASSIGN - - --- ACT_ASSIGN class - -- @type ACT_ASSIGN - -- @field Tasking.Task#TASK Task - -- @field Wrapper.Unit#UNIT ProcessUnit - -- @field Core.Zone#ZONE_BASE TargetZone - -- @extends Core.Fsm#FSM_PROCESS - ACT_ASSIGN = { - ClassName = "ACT_ASSIGN", - } - - - --- Creates a new task assignment state machine. The process will accept the task by default, no player intervention accepted. - -- @param #ACT_ASSIGN self - -- @return #ACT_ASSIGN The task acceptance process. - function ACT_ASSIGN:New() - - -- Inherits from BASE - local self = BASE:Inherit( self, FSM_PROCESS:New( "ACT_ASSIGN" ) ) -- Core.Fsm#FSM_PROCESS - - self:AddTransition( "UnAssigned", "Start", "Waiting" ) - self:AddTransition( "Waiting", "Assign", "Assigned" ) - self:AddTransition( "Waiting", "Reject", "Rejected" ) - self:AddTransition( "*", "Fail", "Failed" ) - - self:AddEndState( "Assigned" ) - self:AddEndState( "Rejected" ) - self:AddEndState( "Failed" ) - - self:SetStartState( "UnAssigned" ) - - return self - end - -end -- ACT_ASSIGN - - - -do -- ACT_ASSIGN_ACCEPT - - --- ACT_ASSIGN_ACCEPT class - -- @type ACT_ASSIGN_ACCEPT - -- @field Tasking.Task#TASK Task - -- @field Wrapper.Unit#UNIT ProcessUnit - -- @field Core.Zone#ZONE_BASE TargetZone - -- @extends #ACT_ASSIGN - ACT_ASSIGN_ACCEPT = { - ClassName = "ACT_ASSIGN_ACCEPT", - } - - - --- Creates a new task assignment state machine. The process will accept the task by default, no player intervention accepted. - -- @param #ACT_ASSIGN_ACCEPT self - -- @param #string TaskBriefing - function ACT_ASSIGN_ACCEPT:New( TaskBriefing ) - - local self = BASE:Inherit( self, ACT_ASSIGN:New() ) -- #ACT_ASSIGN_ACCEPT - - self.TaskBriefing = TaskBriefing - - return self - end - - function ACT_ASSIGN_ACCEPT:Init( FsmAssign ) - - self.TaskBriefing = FsmAssign.TaskBriefing - end - - --- StateMachine callback function - -- @param #ACT_ASSIGN_ACCEPT self - -- @param Wrapper.Unit#UNIT ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ASSIGN_ACCEPT:onafterStart( ProcessUnit, From, Event, To ) - self:E( { ProcessUnit, From, Event, To } ) - - self:__Assign( 1 ) - end - - --- StateMachine callback function - -- @param #ACT_ASSIGN_ACCEPT self - -- @param Wrapper.Unit#UNIT ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ASSIGN_ACCEPT:onenterAssigned( ProcessUnit, From, Event, To ) - env.info( "in here" ) - self:E( { ProcessUnit, From, Event, To } ) - - local ProcessGroup = ProcessUnit:GetGroup() - - self:Message( "You are assigned to the task " .. self.Task:GetName() ) - - self.Task:Assign( ProcessUnit, ProcessUnit:GetPlayerName() ) - 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 Zone - -- @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_POINT - - --- ACT_ROUTE_POINT class - -- @type ACT_ROUTE_POINT - -- @field Tasking.Task#TASK TASK - -- @extends #ACT_ROUTE - ACT_ROUTE_POINT = { - ClassName = "ACT_ROUTE_POINT", - } - - - --- Creates a new routing state machine. - -- The task will route a controllable to a PointVec2 until the controllable is within the Range. - -- @param #ACT_ROUTE_POINT self - -- @param Core.Point#POINT_VEC2 The PointVec2 to Target. - -- @param #number Range The Distance to Target. - -- @param Core.Zone#ZONE_BASE Zone - function ACT_ROUTE_POINT:New( PointVec2, Range ) - local self = BASE:Inherit( self, ACT_ROUTE:New() ) -- #ACT_ROUTE_POINT - - self.PointVec2 = PointVec2 - self.Range = Range or 0 - - self.DisplayInterval = 30 - self.DisplayCount = 30 - self.DisplayMessage = true - self.DisplayTime = 10 -- 10 seconds is the default - - return self - end - - function ACT_ROUTE_POINT:Init( FsmRoute ) - - self.PointVec2 = FsmRoute.PointVec2 - self.Range = FsmRoute.Range or 0 - - self.DisplayInterval = 30 - self.DisplayCount = 30 - self.DisplayMessage = true - self.DisplayTime = 10 -- 10 seconds is the default - end - - --- Set PointVec2 - -- @param #ACT_ROUTE_POINT self - -- @param Core.Point#POINT_VEC2 PointVec2 The PointVec2 to route to. - function ACT_ROUTE_POINT:SetPointVec2( PointVec2 ) - self:F2( { PointVec2 } ) - self.PointVec2 = PointVec2 - end - - --- Get PointVec2 - -- @param #ACT_ROUTE_POINT self - -- @return Core.Point#POINT_VEC2 PointVec2 The PointVec2 to route to. - function ACT_ROUTE_POINT:GetPointVec2() - self:F2( { self.PointVec2 } ) - return self.PointVec2 - end - - --- Set Range around PointVec2 - -- @param #ACT_ROUTE_POINT self - -- @param #number Range The Range to consider the arrival. Default is 10000 meters. - function ACT_ROUTE_POINT:SetRange( Range ) - self:F2( { self.Range } ) - self.Range = Range or 10000 - end - - --- Get Range around PointVec2 - -- @param #ACT_ROUTE_POINT self - -- @return #number The Range to consider the arrival. Default is 10000 meters. - function ACT_ROUTE_POINT:GetRange() - return self.Range - end - - --- Method override to check if the controllable has arrived. - -- @param #ACT_ROUTE_POINT self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @return #boolean - function ACT_ROUTE_POINT:onfuncHasArrived( ProcessUnit ) - - if ProcessUnit:IsAlive() then - local Distance = self.PointVec2:Get2DDistance( ProcessUnit:GetPointVec2() ) - - if Distance <= self.Range then - local RouteText = "You have arrived." - self:Message( RouteText ) - return true - end - end - - return false - end - - --- Task Events - - --- StateMachine callback function - -- @param #ACT_ROUTE_POINT self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ROUTE_POINT:onenterReporting( ProcessUnit, From, Event, To ) - - local TaskUnitPointVec2 = ProcessUnit:GetPointVec2() - local RouteText = "Route to " .. TaskUnitPointVec2:GetBRText( self.PointVec2 ) .. " km." - self:Message( RouteText ) - end - -end -- ACT_ROUTE_POINT - - -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 Zone - -- @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 Zone - function ACT_ROUTE_ZONE:New( Zone ) - local self = BASE:Inherit( self, ACT_ROUTE:New() ) -- #ACT_ROUTE_ZONE - - self.Zone = Zone - - self.DisplayInterval = 30 - self.DisplayCount = 30 - self.DisplayMessage = true - self.DisplayTime = 10 -- 10 seconds is the default - - return self - end - - function ACT_ROUTE_ZONE:Init( FsmRoute ) - - self.Zone = FsmRoute.Zone - - self.DisplayInterval = 30 - self.DisplayCount = 30 - self.DisplayMessage = true - self.DisplayTime = 10 -- 10 seconds is the default - end - - --- Set Zone - -- @param #ACT_ROUTE_ZONE self - -- @param Core.Zone#ZONE_BASE Zone The Zone object where to route to. - function ACT_ROUTE_ZONE:SetZone( Zone ) - self.Zone = Zone - end - - --- Get Zone - -- @param #ACT_ROUTE_ZONE self - -- @return Core.Zone#ZONE_BASE Zone The Zone object where to route to. - function ACT_ROUTE_ZONE:GetZone() - return self.Zone - end - - --- 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.Zone ) then - local RouteText = "You have arrived within the zone." - self:Message( RouteText ) - end - - return ProcessUnit:IsInZone( self.Zone ) - 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.Zone: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." - self:Message( RouteText ) - end - -end -- ACT_ROUTE_ZONE ---- **Actions** - ACT_ACCOUNT_ classes **account for** (detect, count & report) various DCS events occuring on @{Unit}s. --- --- ![Banner Image](..\Presentations\ACT_ACCOUNT\Dia1.JPG) --- --- === --- --- @module Account - - -do -- ACT_ACCOUNT - - --- # @{#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 States - -- - -- * **Asigned**: The player is assigned. - -- * **Waiting**: Waiting for an event. - -- * **Report**: Reporting. - -- * **Account**: Account for an event. - -- * **Accounted**: All events have been accounted for, end of the process. - -- * **Failed**: Failed the process. - -- - -- ### ACT_ACCOUNT Events - -- - -- * **Start**: Start the process. - -- * **Wait**: Wait for an event. - -- * **Report**: Report the status of the accounting. - -- * **Event**: An event happened, process the event. - -- * **More**: More targets. - -- * **NoMore (*)**: No more targets. - -- * **Fail (*)**: The action process has failed. - -- - -- (*) End states of the process. - -- - -- ### ACT_ACCOUNT state transition methods: - -- - -- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. - -- There are 2 moments when state transition methods will be called by the state machine: - -- - -- * **Before** the state transition. - -- The state transition method needs to start with the name **OnBefore + the name of the state**. - -- 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. - -- - -- @type ACT_ACCOUNT - -- @field Set#SET_UNIT TargetSetUnit - -- @extends Core.Fsm#FSM_PROCESS - ACT_ACCOUNT = { - ClassName = "ACT_ACCOUNT", - TargetSetUnit = nil, - } - - --- Creates a new DESTROY process. - -- @param #ACT_ACCOUNT self - -- @return #ACT_ACCOUNT - function ACT_ACCOUNT:New() - - -- Inherits from BASE - local self = BASE:Inherit( self, FSM_PROCESS:New() ) -- Core.Fsm#FSM_PROCESS - - self:AddTransition( "Assigned", "Start", "Waiting") - self:AddTransition( "*", "Wait", "Waiting") - self:AddTransition( "*", "Report", "Report") - self:AddTransition( "*", "Event", "Account") - self:AddTransition( "Account", "More", "Wait") - self:AddTransition( "Account", "NoMore", "Accounted") - self:AddTransition( "*", "Fail", "Failed") - - self:AddEndState( "Accounted" ) - self:AddEndState( "Failed" ) - - self:SetStartState( "Assigned" ) - - return self - end - - --- Process Events - - --- StateMachine callback function - -- @param #ACT_ACCOUNT self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ACCOUNT:onafterStart( ProcessUnit, From, Event, To ) - - self:HandleEvent( EVENTS.Dead, self.onfuncEventDead ) - - self:__Wait( 1 ) - end - - - --- StateMachine callback function - -- @param #ACT_ACCOUNT self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ACCOUNT:onenterWaiting( ProcessUnit, From, Event, To ) - - if self.DisplayCount >= self.DisplayInterval then - self:Report() - self.DisplayCount = 1 - else - self.DisplayCount = self.DisplayCount + 1 - end - - return true -- Process always the event. - end - - --- StateMachine callback function - -- @param #ACT_ACCOUNT self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ACCOUNT:onafterEvent( ProcessUnit, From, Event, To, Event ) - - self:__NoMore( 1 ) - end - -end -- ACT_ACCOUNT - -do -- ACT_ACCOUNT_DEADS - - --- # @{#ACT_ACCOUNT_DEADS} 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. - -- - -- @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 - - --- 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, Task, 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, Task, From, Event, To, EventData ) - self:T( { ProcessUnit, EventData, From, Event, To } ) - - self:T({self.Controllable}) - - self.TargetSetUnit:Flush() - - self:T( { "Before sending Message", EventData.IniUnitName, self.TargetSetUnit:FindUnit( EventData.IniUnitName ) } ) - if self.TargetSetUnit:FindUnit( EventData.IniUnitName ) then - self:T( "Sending Message" ) - local TaskGroup = ProcessUnit:GetGroup() - self.TargetSetUnit:Remove( 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 - self:T( { "After sending Message" } ) - 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, Task, From, Event, To ) - - 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( 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():GetMenu( 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 - - --- StateMachine callback function - -- @param #ACT_ASSIST self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ASSIST:onafterStop( ProcessUnit, From, Event, To ) - - self.Menu:Remove() -- When stopped, remove the menus - end - -end - -do -- ACT_ASSIST_SMOKE_TARGETS_ZONE - - --- 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 = {} - if Title then - self.Report[#self.Report+1] = Title - end - - 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] -end - ---- Produces the text of the report, taking into account an optional delimeter, which is \n by default. --- @param #REPORT self --- @param #string Delimiter (optional) A delimiter text. --- @return #string The report text. -function REPORT:Text( Delimiter ) - Delimiter = Delimiter or "\n" - local ReportText = table.concat( self.Report, Delimiter ) or "" - return ReportText -end - ---- The COMMANDCENTER class --- @type COMMANDCENTER --- @field Wrapper.Group#GROUP HQ --- @field Dcs.DCSCoalitionWrapper.Object#coalition CommandCenterCoalition --- @list Missions --- @extends Core.Base#BASE -COMMANDCENTER = { - ClassName = "COMMANDCENTER", - CommandCenterName = "", - CommandCenterCoalition = nil, - CommandCenterPositionable = nil, - Name = "", -} ---- The constructor takes an IDENTIFIABLE as the HQ command center. --- @param #COMMANDCENTER self --- @param Wrapper.Positionable#POSITIONABLE CommandCenterPositionable --- @param #string CommandCenterName --- @return #COMMANDCENTER -function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName ) - - local self = BASE:Inherit( self, BASE:New() ) - - self.CommandCenterPositionable = CommandCenterPositionable - self.CommandCenterName = CommandCenterName or CommandCenterPositionable:GetName() - self.CommandCenterCoalition = CommandCenterPositionable:GetCoalition() - - self.Missions = {} - - self:HandleEvent( EVENTS.Birth, - --- @param #COMMANDCENTER self - -- @param Core.Event#EVENTDATA EventData - function( self, EventData ) - if EventData.IniObjectCategory == 1 then - 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 - - end - ) - - -- When a player enters a client or a unit, the CommandCenter will check for each Mission and each Task in the Mission if the player has things to do. - -- For these elements, it will= - -- - Set the correct menu. - -- - Assign the PlayerUnit to the Task if required. - -- - Send a message to the other players in the group that this player has joined. - self:HandleEvent( EVENTS.PlayerEnterUnit, - --- @param #COMMANDCENTER self - -- @param Core.Event#EVENTDATA EventData - function( self, EventData ) - local PlayerUnit = EventData.IniUnit - for MissionID, Mission in pairs( self:GetMissions() ) do - local Mission = Mission -- Tasking.Mission#MISSION - local PlayerGroup = EventData.IniGroup -- The GROUP object should be filled! - Mission:JoinUnit( PlayerUnit, PlayerGroup ) - Mission:ReportDetails() - end - end - ) - - -- Handle when a player leaves a slot and goes back to spectators ... - -- The PlayerUnit will be UnAssigned from the Task. - -- When there is no Unit left running the Task, the Task goes into Abort... - self:HandleEvent( EVENTS.PlayerLeaveUnit, - --- @param #TASK self - -- @param Core.Event#EVENTDATA EventData - function( self, EventData ) - local PlayerUnit = EventData.IniUnit - for MissionID, Mission in pairs( self:GetMissions() ) do - local Mission = Mission -- Tasking.Mission#MISSION - Mission:AbortUnit( PlayerUnit ) - end - end - ) - - -- Handle when a player leaves a slot and goes back to spectators ... - -- The PlayerUnit will be UnAssigned from the Task. - -- When there is no Unit left running the Task, the Task goes into Abort... - self:HandleEvent( EVENTS.Crash, - --- @param #TASK self - -- @param Core.Event#EVENTDATA EventData - function( self, EventData ) - local PlayerUnit = EventData.IniUnit - for MissionID, Mission in pairs( self:GetMissions() ) do - Mission:CrashUnit( PlayerUnit ) - end - end - ) - - return self -end - ---- Gets the name of the HQ command center. --- @param #COMMANDCENTER self --- @return #string -function COMMANDCENTER:GetName() - - return self.CommandCenterName -end - ---- Gets the POSITIONABLE of the HQ command center. --- @param #COMMANDCENTER self --- @return Wrapper.Positionable#POSITIONABLE -function COMMANDCENTER:GetPositionable() - return self.CommandCenterPositionable -end - ---- Get the Missions governed by the HQ command center. --- @param #COMMANDCENTER self --- @return #list -function COMMANDCENTER:GetMissions() - - return self.Missions -end - ---- Add a MISSION to be governed by the HQ command center. --- @param #COMMANDCENTER self --- @param Tasking.Mission#MISSION Mission --- @return Tasking.Mission#MISSION -function COMMANDCENTER:AddMission( Mission ) - - self.Missions[Mission] = Mission - - return Mission -end - ---- Removes a MISSION to be governed by the HQ command center. --- The given Mission is not nilified. --- @param #COMMANDCENTER self --- @param Tasking.Mission#MISSION Mission --- @return Tasking.Mission#MISSION -function COMMANDCENTER:RemoveMission( Mission ) - - self.Missions[Mission] = nil - - return Mission -end - ---- Sets the menu structure of the Missions governed by the HQ command center. --- @param #COMMANDCENTER self -function COMMANDCENTER:SetMenu() - self:F() - - self.CommandCenterMenu = self.CommandCenterMenu or MENU_COALITION:New( self.CommandCenterCoalition, "Command Center (" .. self:GetName() .. ")" ) - - local MenuTime = timer.getTime() - for MissionID, Mission in pairs( self:GetMissions() ) do - local Mission = Mission -- Tasking.Mission#MISSION - Mission:SetMenu( MenuTime ) - end - - for MissionID, Mission in pairs( self:GetMissions() ) do - local Mission = Mission -- Tasking.Mission#MISSION - Mission:RemoveMenu( MenuTime ) - end - -end - ---- Gets the commandcenter menu structure governed by the HQ command center. --- @param #COMMANDCENTER self --- @return Core.Menu#MENU_COALITION -function COMMANDCENTER:GetMenu() - self:F() - return self.CommandCenterMenu -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 the coalition of the CC. --- @param #COMMANDCENTER self -function COMMANDCENTER:MessageToAll( Message ) - - self:GetPositionable():MessageToAll( Message, 20, self:GetName() ) - -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 = "@ Group" - Prefix = Prefix .. ( Name and " (" .. 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", -} - ---- 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" ) - - --- OnLeave Transition Handler for State Idle. - -- @function [parent=#MISSION] OnLeaveIdle - -- @param #MISSION self - -- @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 Idle. - -- @function [parent=#MISSION] OnEnterIdle - -- @param #MISSION self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- OnLeave Transition Handler for State Ongoing. - -- @function [parent=#MISSION] OnLeaveOngoing - -- @param #MISSION self - -- @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 Ongoing. - -- @function [parent=#MISSION] OnEnterOngoing - -- @param #MISSION self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- OnBefore Transition Handler for Event Start. - -- @function [parent=#MISSION] OnBeforeStart - -- @param #MISSION self - -- @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=#MISSION] OnAfterStart - -- @param #MISSION self - -- @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=#MISSION] Start - -- @param #MISSION self - - --- Asynchronous Event Trigger for Event Start. - -- @function [parent=#MISSION] __Start - -- @param #MISSION self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "Ongoing", "Stop", "Idle" ) - - --- OnLeave Transition Handler for State Idle. - -- @function [parent=#MISSION] OnLeaveIdle - -- @param #MISSION self - -- @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 Idle. - -- @function [parent=#MISSION] OnEnterIdle - -- @param #MISSION self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- OnBefore Transition Handler for Event Stop. - -- @function [parent=#MISSION] OnBeforeStop - -- @param #MISSION self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Stop. - -- @function [parent=#MISSION] OnAfterStop - -- @param #MISSION self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Stop. - -- @function [parent=#MISSION] Stop - -- @param #MISSION self - - --- Asynchronous Event Trigger for Event Stop. - -- @function [parent=#MISSION] __Stop - -- @param #MISSION self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "Ongoing", "Complete", "Completed" ) - - --- OnLeave Transition Handler for State Completed. - -- @function [parent=#MISSION] OnLeaveCompleted - -- @param #MISSION self - -- @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 Completed. - -- @function [parent=#MISSION] OnEnterCompleted - -- @param #MISSION self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- OnBefore Transition Handler for Event Complete. - -- @function [parent=#MISSION] OnBeforeComplete - -- @param #MISSION self - -- @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 Complete. - -- @function [parent=#MISSION] OnAfterComplete - -- @param #MISSION self - -- @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 Complete. - -- @function [parent=#MISSION] Complete - -- @param #MISSION self - - --- Asynchronous Event Trigger for Event Complete. - -- @function [parent=#MISSION] __Complete - -- @param #MISSION self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "Fail", "Failed" ) - - --- OnLeave Transition Handler for State Failed. - -- @function [parent=#MISSION] OnLeaveFailed - -- @param #MISSION self - -- @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 Failed. - -- @function [parent=#MISSION] OnEnterFailed - -- @param #MISSION self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- OnBefore Transition Handler for Event Fail. - -- @function [parent=#MISSION] OnBeforeFail - -- @param #MISSION self - -- @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 Fail. - -- @function [parent=#MISSION] OnAfterFail - -- @param #MISSION self - -- @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 Fail. - -- @function [parent=#MISSION] Fail - -- @param #MISSION self - - --- Asynchronous Event Trigger for Event Fail. - -- @function [parent=#MISSION] __Fail - -- @param #MISSION self - -- @param #number Delay The delay in seconds. - - 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 = {} - - -- Private implementations - - - - return self -end - --- FSM function for a MISSION --- @param #MISSION self --- @param #string From --- @param #string Event --- @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 From --- @param #string Event --- @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 --- @param #number MenuTime -function MISSION:SetMenu( MenuTime ) - self:F() - - for _, TaskData in pairs( self:GetTasks() ) do - local Task = TaskData -- Tasking.Task#TASK - Task:SetMenu( MenuTime ) - end -end - ---- Removes the Planned Task menu. --- @param #MISSION self --- @param #number MenuTime -function MISSION:RemoveMenu( MenuTime ) - self:F() - - for _, Task in pairs( self:GetTasks() ) do - local Task = Task -- Tasking.Task#TASK - Task:RemoveMenu( MenuTime ) - end -end - - ---- Gets the COMMANDCENTER. --- @param #MISSION self --- @return Tasking.CommandCenter#COMMANDCENTER -function MISSION:GetCommandCenter() - return self.CommandCenter -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:GetMenu( TaskGroup ) - - local CommandCenter = self:GetCommandCenter() - local CommandCenterMenu = CommandCenter:GetMenu() - - local MissionName = self:GetName() - local MissionMenu = CommandCenterMenu:GetMenu( MissionName ) - - return MissionMenu -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 - ---- Is the @{Mission} **Completed**. --- @param #MISSION self --- @return #boolean -function MISSION:IsCompleted() - return self:Is( "Completed" ) -end - ---- Is the @{Mission} **Idle**. --- @param #MISSION self --- @return #boolean -function MISSION:IsIdle() - return self:Is( "Idle" ) -end - ---- Is the @{Mission} **Ongoing**. --- @param #MISSION self --- @return #boolean -function MISSION:IsOngoing() - return self:Is( "Ongoing" ) -end - ---- Is the @{Mission} **Failed**. --- @param #MISSION self --- @return #boolean -function MISSION:IsFailed() - return self:Is( "Failed" ) -end - ---- Is the @{Mission} **Hold**. --- @param #MISSION self --- @return #boolean -function MISSION:IsHold() - return self:Is( "Hold" ) -end - ---- Validates if the Mission has a Group --- @param #MISSION --- @return #boolean true if the Mission has a Group. -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 - ---- 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 - - ---- This module contains the TASK class. --- --- 1) @{#TASK} class, extends @{Base#BASE} --- ============================================ --- 1.1) The @{#TASK} class implements the methods for task orchestration within MOOSE. --- ---------------------------------------------------------------------------------------- --- The class provides a couple of methods to: --- --- * @{#TASK.AssignToGroup}():Assign a task to a group (of players). --- * @{#TASK.AddProcess}():Add a @{Process} to a task. --- * @{#TASK.RemoveProcesses}():Remove a running @{Process} from a running task. --- * @{#TASK.SetStateMachine}():Set a @{Fsm} to a task. --- * @{#TASK.RemoveStateMachine}():Remove @{Fsm} from a task. --- * @{#TASK.HasStateMachine}():Enquire if the task has a @{Fsm} --- * @{#TASK.AssignToUnit}(): Assign a task to a unit. (Needs to be implemented in the derived classes from @{#TASK}. --- * @{#TASK.UnAssignFromUnit}(): Unassign the task from a unit. --- * @{#TASK.SetTimeOut}(): Set timer in seconds before task gets cancelled if not assigned. --- --- 1.2) Set and enquire task status (beyond the task state machine processing). --- ---------------------------------------------------------------------------- --- A task needs to implement as a minimum the following task states: --- --- * **Success**: Expresses the successful execution and finalization of the task. --- * **Failed**: Expresses the failure of a task. --- * **Planned**: Expresses that the task is created, but not yet in execution and is not assigned yet. --- * **Assigned**: Expresses that the task is assigned to a Group of players, and that the task is in execution mode. --- --- A task may also implement the following task states: --- --- * **Rejected**: Expresses that the task is rejected by a player, who was requested to accept the task. --- * **Cancelled**: Expresses that the task is cancelled by HQ or through a logical situation where a cancellation of the task is required. --- --- A task can implement more statusses than the ones outlined above. Please consult the documentation of the specific tasks to understand the different status modelled. --- --- The status of tasks can be set by the methods **State** followed by the task status. An example is `StateAssigned()`. --- The status of tasks can be enquired by the methods **IsState** followed by the task status name. An example is `if IsStateAssigned() then`. --- --- 1.3) Add scoring when reaching a certain task status: --- ----------------------------------------------------- --- Upon reaching a certain task status in a task, additional scoring can be given. If the Mission has a scoring system attached, the scores will be added to the mission scoring. --- Use the method @{#TASK.AddScore}() to add scores when a status is reached. --- --- 1.4) Task briefing: --- ------------------- --- A task briefing can be given that is shown to the player when he is assigned to the task. --- --- === --- --- ### Authors: FlightControl - Design and Programming --- --- @module Task - ---- The TASK class --- @type TASK --- @field Core.Scheduler#SCHEDULER TaskScheduler --- @field Tasking.Mission#MISSION Mission --- @field Core.Set#SET_GROUP SetGroup The Set of Groups assigned to the Task --- @field Core.Fsm#FSM_PROCESS FsmTemplate --- @field Tasking.Mission#MISSION Mission --- @field Tasking.CommandCenter#COMMANDCENTER CommandCenter --- @extends Core.Fsm#FSM_TASK -TASK = { - ClassName = "TASK", - TaskScheduler = nil, - ProcessClasses = {}, -- The container of the Process classes that will be used to create and assign new processes for the task to ProcessUnits. - Processes = {}, -- The container of actual process objects instantiated and assigned to ProcessUnits. - Players = nil, - Scores = {}, - Menu = {}, - SetGroup = nil, - FsmTemplate = nil, - Mission = nil, - CommandCenter = nil, - TimeOut = 0, -} - ---- FSM PlayerAborted event handler prototype for TASK. --- @function [parent=#TASK] OnAfterPlayerAborted --- @param #TASK self --- @param Wrapper.Unit#UNIT PlayerUnit The Unit of the Player when he went back to spectators or left the mission. --- @param #string PlayerName The name of the Player. - ---- FSM PlayerCrashed event handler prototype for TASK. --- @function [parent=#TASK] OnAfterPlayerCrashed --- @param #TASK self --- @param Wrapper.Unit#UNIT PlayerUnit The Unit of the Player when he crashed in the mission. --- @param #string PlayerName The name of the Player. - ---- FSM PlayerDead event handler prototype for TASK. --- @function [parent=#TASK] OnAfterPlayerDead --- @param #TASK self --- @param Wrapper.Unit#UNIT PlayerUnit The Unit of the Player when he died in the mission. --- @param #string PlayerName The name of the Player. - ---- FSM Fail synchronous event function for TASK. --- Use this event to Fail the Task. --- @function [parent=#TASK] Fail --- @param #TASK self - ---- FSM Fail asynchronous event function for TASK. --- Use this event to Fail the Task. --- @function [parent=#TASK] __Fail --- @param #TASK self - ---- FSM Abort synchronous event function for TASK. --- Use this event to Abort the Task. --- @function [parent=#TASK] Abort --- @param #TASK self - ---- FSM Abort asynchronous event function for TASK. --- Use this event to Abort the Task. --- @function [parent=#TASK] __Abort --- @param #TASK self - ---- FSM Success synchronous event function for TASK. --- Use this event to make the Task a Success. --- @function [parent=#TASK] Success --- @param #TASK self - ---- FSM Success asynchronous event function for TASK. --- Use this event to make the Task a Success. --- @function [parent=#TASK] __Success --- @param #TASK self - ---- FSM Cancel synchronous event function for TASK. --- Use this event to Cancel the Task. --- @function [parent=#TASK] Cancel --- @param #TASK self - ---- FSM Cancel asynchronous event function for TASK. --- Use this event to Cancel the Task. --- @function [parent=#TASK] __Cancel --- @param #TASK self - ---- FSM Replan synchronous event function for TASK. --- Use this event to Replan the Task. --- @function [parent=#TASK] Replan --- @param #TASK self - ---- FSM Replan asynchronous event function for TASK. --- Use this event to Replan the Task. --- @function [parent=#TASK] __Replan --- @param #TASK self - - ---- Instantiates a new TASK. Should never be used. Interface Class. --- @param #TASK self --- @param Tasking.Mission#MISSION Mission The mission wherein the Task is registered. --- @param Core.Set#SET_GROUP SetGroupAssign The set of groups for which the Task can be assigned. --- @param #string TaskName The name of the Task --- @param #string TaskType The type of the Task --- @return #TASK self -function TASK:New( Mission, SetGroupAssign, TaskName, TaskType ) - - local self = BASE:Inherit( self, FSM_TASK:New() ) -- Core.Fsm#FSM_TASK - - self:SetStartState( "Planned" ) - self:AddTransition( "Planned", "Assign", "Assigned" ) - self:AddTransition( "Assigned", "AssignUnit", "Assigned" ) - self:AddTransition( "Assigned", "Success", "Success" ) - self:AddTransition( "Assigned", "Fail", "Failed" ) - self:AddTransition( "Assigned", "Abort", "Aborted" ) - self:AddTransition( "Assigned", "Cancel", "Cancelled" ) - self:AddTransition( "*", "PlayerCrashed", "*" ) - self:AddTransition( "*", "PlayerAborted", "*" ) - self:AddTransition( "*", "PlayerDead", "*" ) - self:AddTransition( { "Failed", "Aborted", "Cancelled" }, "Replan", "Planned" ) - self:AddTransition( "*", "TimeOut", "Cancelled" ) - - self:E( "New TASK " .. TaskName ) - - self.Processes = {} - self.Fsm = {} - - self.Mission = Mission - self.CommandCenter = Mission:GetCommandCenter() - - self.SetGroup = SetGroupAssign - - self:SetType( TaskType ) - self:SetName( TaskName ) - self:SetID( Mission:GetNextTaskID( self ) ) -- The Mission orchestrates the task sequences .. - - self.TaskBriefing = "You are invited for the task: " .. self.TaskName .. "." - - self.FsmTemplate = self.FsmTemplate or FSM_PROCESS:New() - - - return self -end - ---- Get the Task FSM Process Template --- @param #TASK self --- @return Core.Fsm#FSM_PROCESS -function TASK:GetUnitProcess( TaskUnit ) - - if TaskUnit then - return self:GetStateMachine( TaskUnit ) - else - return self.FsmTemplate - end -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 - self:UnAssignFromGroup( PlayerGroup ) - PlayerGroup:SetState( PlayerGroup, "Assigned", nil ) - self:RemoveMenuForGroup( PlayerGroup ) - end - self:Abort() - 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 ) - - local Mission = self:GetMission() - local MissionMenu = Mission:GetMenu( TaskGroup ) - MissionMenu:RemoveSubMenus() - - --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:GetName() ) - - self:RemoveStateMachine( TaskUnit ) - - return self -end - ---- Sets the TimeOut for the @{Task}. If @{Task} stayed planned for longer than TimeOut, it gets into Cancelled status. --- @param #TASK self --- @param #integer Timer in seconds --- @return #TASK self -function TASK:SetTimeOut ( Timer ) - self:F( Timer ) - self.TimeOut = Timer - self:__TimeOut( self.TimeOut ) - return self -end - ---- Send a message of the @{Task} to the assigned @{Group}s. --- @param #TASK self -function TASK:MessageToGroups( Message ) - self:F( { Message = Message } ) - - local Mission = self:GetMission() - local CC = Mission:GetCommandCenter() - - for TaskGroupName, TaskGroup in pairs( self.SetGroup:GetSet() ) do - local TaskGroup = TaskGroup -- Wrapper.Group#GROUP - CC:MessageToGroup( Message, TaskGroup, TaskGroup:GetName() ) - end -end - - ---- Send the briefng message of the @{Task} to the assigned @{Group}s. --- @param #TASK self -function TASK:SendBriefingToAssignedGroups() - self:F2() - - for TaskGroupName, TaskGroup in pairs( self.SetGroup:GetSet() ) do - - if self:IsAssignedToGroup( TaskGroup ) then - TaskGroup:Message( self.TaskBriefing, 60 ) - end - end -end - - ---- UnAssign the @{Task} from the @{Group}s. --- @param #TASK self -function TASK:UnAssignFromGroups() - self:F2() - - for TaskGroupName, TaskGroup in pairs( self.SetGroup:GetSet() ) do - self:UnAssignFromGroup( TaskGroup ) - end -end - ---- UnAssign the @{Task} from a @{Group}. --- @param #TASK self -function TASK:UnAssignFromGroup( TaskGroup ) - self:F2( { TaskGroup } ) - - TaskGroup:SetState( TaskGroup, "Assigned", nil ) - - self:RemoveAssignedMenuForGroup( 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 - - - ---- 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 - self:T( { "Task is assigned to:", TaskGroup:GetName() } ) - return true - end - end - - self:T( { "Task is not assigned to:", TaskGroup:GetName() } ) - 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 --- @param #number MenuTime --- @return #TASK -function TASK:SetMenu( MenuTime ) - self:F() - - self.SetGroup:Flush() - for TaskGroupID, TaskGroupData in pairs( self.SetGroup:GetSet() ) do - local TaskGroup = TaskGroupData -- Wrapper.Group#GROUP - if TaskGroup:IsAlive() and TaskGroup:GetPlayerNames() then - if self:IsStatePlanned() or self:IsStateReplanned() then - self:SetMenuForGroup( TaskGroup, MenuTime ) - end - end - end -end - - - ---- Set the Menu for a Group --- @param #TASK self --- @param #number MenuTime --- @return #TASK -function TASK:SetMenuForGroup( TaskGroup, MenuTime ) - - if not TaskGroup:GetState( TaskGroup, "Assigned" ) then - self:SetPlannedMenuForGroup( TaskGroup, self:GetTaskName(), MenuTime ) - else - if not self:IsAssignedToGroup( TaskGroup ) then - self:SetAssignedMenuForGroup( TaskGroup, MenuTime ) - end - end -end - - ---- Set the planned menu option of the @{Task}. --- @param #TASK self --- @param Wrapper.Group#GROUP TaskGroup --- @param #string MenuText The menu text. --- @param #number MenuTime --- @return #TASK self -function TASK:SetPlannedMenuForGroup( TaskGroup, MenuText, MenuTime ) - self:E( TaskGroup:GetName() ) - - local Mission = self:GetMission() - local MissionName = Mission:GetName() - local CommandCenter = Mission:GetCommandCenter() - local CommandCenterMenu = CommandCenter:GetMenu() - - local MissionMenu = MENU_GROUP:New( TaskGroup, MissionName, CommandCenterMenu ):SetTime( MenuTime ) - - - local MissionMenu = Mission:GetMenu( TaskGroup ) - - local TaskType = self:GetType() - local TaskTypeMenu = MENU_GROUP:New( TaskGroup, TaskType, MissionMenu ):SetTime( MenuTime ) - local TaskMenu = MENU_GROUP_COMMAND:New( TaskGroup, MenuText, TaskTypeMenu, self.MenuAssignToGroup, { self = self, TaskGroup = TaskGroup } ):SetTime( MenuTime ):SetRemoveParent( true ) - - return self -end - ---- Set the assigned menu options of the @{Task}. --- @param #TASK self --- @param Wrapper.Group#GROUP TaskGroup --- @param #number MenuTime --- @return #TASK self -function TASK:SetAssignedMenuForGroup( TaskGroup, MenuTime ) - self:E( TaskGroup:GetName() ) - - local Mission = self:GetMission() - local MissionMenu = Mission:GetMenu( TaskGroup ) - - self:E( { MissionMenu = MissionMenu } ) - - local TaskTypeMenu = MENU_GROUP_COMMAND:New( TaskGroup, "Task Status", MissionMenu, self.MenuTaskStatus, self, TaskGroup ):SetTime( MenuTime ) - local TaskMenu = MENU_GROUP_COMMAND:New( TaskGroup, "Abort Task", MissionMenu, self.MenuTaskAbort, self, TaskGroup ):SetTime( MenuTime ) - - return self -end - ---- Remove the menu options of the @{Task} to all the groups in the SetGroup. --- @param #TASK self --- @param #number MenuTime --- @return #TASK -function TASK:RemoveMenu( MenuTime ) - self:F() - - for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do - local TaskGroup = TaskGroup -- Wrapper.Group#GROUP - if TaskGroup:IsAlive() and TaskGroup:GetPlayerNames() then - if not self:IsAssignedToGroup( TaskGroup ) then - self:RemovePlannedMenuForGroup( TaskGroup, MenuTime ) - end - end - end -end - - ---- Remove the menu option of the @{Task} for a @{Group}. --- @param #TASK self --- @param Wrapper.Group#GROUP TaskGroup --- @param #number MenuTime --- @return #TASK self -function TASK:RemovePlannedMenuForGroup( TaskGroup, MenuTime ) - self:F() - - local Mission = self:GetMission() - local MissionName = Mission:GetName() - - local MissionMenu = Mission:GetMenu( TaskGroup ) - - if MissionMenu then - local TaskType = self:GetType() - local TypeMenu = MissionMenu:GetMenu( TaskType ) - - if TypeMenu then - local TaskMenu = TypeMenu:GetMenu( self:GetTaskName() ) - if TaskMenu then - TaskMenu:Remove( MenuTime ) - end - end - end - -end - ---- Remove the assigned menu option of the @{Task} for a @{Group}. --- @param #TASK self --- @param Wrapper.Group#GROUP TaskGroup --- @param #number MenuTime --- @return #TASK self -function TASK:RemoveAssignedMenuForGroup( TaskGroup ) - self:F() - - local Mission = self:GetMission() - local MissionName = Mission:GetName() - - local MissionMenu = Mission:GetMenu( TaskGroup ) - - if MissionMenu then - MissionMenu:RemoveSubMenus() - end - -end - -function TASK.MenuAssignToGroup( MenuParam ) - - local self = MenuParam.self - local TaskGroup = MenuParam.TaskGroup - - self:E( "Assigned menu selected") - - self:AssignToGroup( TaskGroup ) -end - ---- Report the task status. --- @param #TASK self -function TASK:MenuTaskStatus( TaskGroup ) - - local ReportText = self:ReportDetails() - - self:T( ReportText ) - self:GetMission():GetCommandCenter():MessageToGroup( ReportText, TaskGroup ) - -end - ---- Report the task status. --- @param #TASK self -function TASK:MenuTaskAbort( 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 --- @param Core.Fsm#FSM_PROCESS Fsm --- @return #TASK self -function TASK:SetStateMachine( TaskUnit, Fsm ) - self:F2( { TaskUnit, self.Fsm[TaskUnit] ~= nil, Fsm:GetClassNameAndID() } ) - - self.Fsm[TaskUnit] = Fsm - - return Fsm -end - ---- Gets the FiniteStateMachine of @{Task} with key Task@{Unit} --- @param #TASK self --- @param Wrapper.Unit#UNIT TaskUnit --- @return Core.Fsm#FSM_PROCESS -function TASK:GetStateMachine( TaskUnit ) - self:F2( { TaskUnit, self.Fsm[TaskUnit] ~= nil } ) - - return self.Fsm[TaskUnit] -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:E( self.Fsm ) - for TaskUnitT, Fsm in pairs( self.Fsm ) do - self:E( TaskUnitT ) - end - - self.Fsm[TaskUnit] = nil - - collectgarbage() - self:E( "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 **Aborted**. --- @param #TASK self -function TASK:StateAborted() - self:SetState( self, "State", "Aborted" ) - return self -end - ---- Is the @{Task} status **Aborted**. --- @param #TASK self -function TASK:IsStateAborted() - return self:Is( "Aborted" ) -end - ---- Sets a @{Task} to status **Cancelled**. --- @param #TASK self -function TASK:StateCancelled() - self:SetState( self, "State", "Cancelled" ) - return self -end - ---- Is the @{Task} status **Cancelled**. --- @param #TASK self -function TASK:IsStateCancelled() - return self:Is( "Cancelled" ) -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, PlayerUnit, PlayerName ) - - self:E( { "Task Assigned", self.Dispatcher } ) - - self:MessageToGroups( "Task " .. self:GetName() .. " has been assigned to your group." ) - - if self.Dispatcher then - self:E( "Firing Assign event " ) - self.Dispatcher:Assign( self, PlayerUnit, PlayerName ) - end - - self:GetMission():__Start( 1 ) -end - - ---- FSM function for a TASK --- @param #TASK self --- @param #string Event --- @param #string From --- @param #string To -function TASK:onenterSuccess( From, Event, To ) - - self:E( "Task Success" ) - - self:GetMission():GetCommandCenter():MessageToCoalition( "Task " .. self:GetName() .. " is successful! Good job!" ) - self:UnAssignFromGroups() - - self:GetMission():__Complete( 1 ) - -end - - ---- FSM function for a TASK --- @param #TASK self --- @param #string From --- @param #string Event --- @param #string To -function TASK:onenterAborted( From, Event, To ) - - self:E( "Task Aborted" ) - - self:GetMission():GetCommandCenter():MessageToCoalition( "Task " .. self:GetName() .. " has been aborted! Task may be replanned." ) - - self:UnAssignFromGroups() - - self:__Replan( 5 ) -end - ---- FSM function for a TASK --- @param #TASK self --- @param #string From --- @param #string Event --- @param #string To -function TASK:onafterReplan( From, Event, To ) - - self:E( "Task Replanned" ) - - self:GetMission():GetCommandCenter():MessageToCoalition( "Replanning Task " .. self:GetName() .. "." ) - - self:SetMenu() - -end - ---- FSM function for a TASK --- @param #TASK self --- @param #string From --- @param #string Event --- @param #string To -function TASK:onenterFailed( From, Event, To ) - - self:E( "Task Failed" ) - - self:GetMission():GetCommandCenter():MessageToCoalition( "Task " .. self:GetName() .. " has failed!" ) - - self:UnAssignFromGroups() -end - ---- FSM function for a TASK --- @param #TASK self --- @param #string Event --- @param #string From --- @param #string To -function TASK:onstatechange( From, Event, To ) - - if self:IsTrace() then - MESSAGE:New( "@ Task " .. self.TaskName .. " : " .. Event .. " changed to state " .. To, 2 ):ToAll() - end - - if self.Scores[To] then - local Scoring = self:GetScoring() - if Scoring then - self:E( { self.Scores[To].ScoreText, self.Scores[To].Score } ) - Scoring:_AddMissionScore( self.Mission, self.Scores[To].ScoreText, self.Scores[To].Score ) - end - end - -end - ---- FSM function for a TASK --- @param #TASK self --- @param #string Event --- @param #string From --- @param #string To -function TASK:onenterPlanned( From, Event, To) - if not self.TimeOut == 0 then - self.__TimeOut( self.TimeOut ) - end -end - ---- FSM function for a TASK --- @param #TASK self --- @param #string Event --- @param #string From --- @param #string To -function TASK:onbeforeTimeOut( From, Event, To ) - if From == "Planned" then - self:RemoveMenu() - return true - end - return false -end - -do -- Dispatcher - - --- Set dispatcher of a task - -- @param #TASK self - -- @param Tasking.DetectionManager#DETECTION_MANAGER Dispatcher - -- @return #TASK - function TASK:SetDispatcher( Dispatcher ) - self.Dispatcher = Dispatcher - 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 = {} - local PlayerReport = REPORT:New( " - Players:" ) - for PlayerGroupID, PlayerGroupData in pairs( self:GetGroups():GetSet() ) do - local PlayerGroup = PlayerGroupData -- Wrapper.Group#GROUP - PlayerNames = PlayerGroup:GetPlayerNames() - if PlayerNames then - PlayerReport:Add( " -- Group " .. PlayerGroup:GetCallsign() .. ": " .. table.concat( PlayerNames, ", " ) ) - end - end - - -- Loop each Process in the Task, and find Reporting Details. - Report:Add( string.format( " - Task %s\n -- State '%s'\n%s", Name, State, PlayerReport:Text() ) ) - return Report:Text() -end - - -end -- Reporting ---- This module contains the DETECTION_MANAGER class and derived classes. --- --- === --- --- 1) @{DetectionManager#DETECTION_MANAGER} class, extends @{Fsm#FSM} --- ==================================================================== --- The @{DetectionManager#DETECTION_MANAGER} class defines the core functions to report detected objects to groups. --- 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. --- --- --- === --- --- ### 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 Core.Fsm#FSM - 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, FSM:New() ) -- #DETECTION_MANAGER - - self.SetGroup = SetGroup - self.Detection = Detection - - self:SetStartState( "Stopped" ) - self:AddTransition( "Stopped", "Start", "Started" ) - self:AddTransition( "Started", "Stop", "Stopped" ) - self:AddTransition( "Started", "Report", "Started" ) - - self:SetReportInterval( 30 ) - self:SetReportDisplayTime( 25 ) - - Detection:__Start( 1 ) - - return self - end - - function DETECTION_MANAGER:onafterStart( From, Event, To ) - self:Report() - end - - function DETECTION_MANAGER:onafterReport( From, Event, To ) - - self:E( "onafterReport" ) - - self:__Report( -self._ReportInterval ) - - self:ProcessDetected( self.Detection ) - 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:ProcessDetected( Detection ) - self:E() - - 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 - ---- **Tasking** - The TASK_A2G_DISPATCHER creates and manages player TASK_A2G tasks based on detected targets. --- --- === --- --- # 1) @{#TASK_A2G_DISPATCHER} class, extends @{#DETECTION_MANAGER} --- --- The @{#TASK_A2G_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) TASK_A2G_DISPATCHER constructor: --- -------------------------------------- --- The @{#TASK_A2G_DISPATCHER.New}() method creates a new TASK_A2G_DISPATCHER instance. --- --- === --- --- # **API CHANGE HISTORY** --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- Hereby the change log: --- --- 2017-03-09: Initial class and API. --- --- === --- --- # **AUTHORS and CONTRIBUTIONS** --- --- ### Contributions: --- --- ### Authors: --- --- * **FlightControl**: Concept, Design & Programming. --- --- @module Task_A2G_Dispatcher - -do -- TASK_A2G_DISPATCHER - - --- TASK_A2G_DISPATCHER class. - -- @type TASK_A2G_DISPATCHER - -- @field Set#SET_GROUP SetGroup The groups to which the FAC will report to. - -- @field Functional.Detection#DETECTION_BASE Detection The DETECTION_BASE object that is used to report the detected objects. - -- @field Tasking.Mission#MISSION Mission - -- @extends Tasking.DetectionManager#DETECTION_MANAGER - TASK_A2G_DISPATCHER = { - ClassName = "TASK_A2G_DISPATCHER", - Mission = nil, - Detection = nil, - } - - - --- TASK_A2G_DISPATCHER constructor. - -- @param #TASK_A2G_DISPATCHER self - -- @param Tasking.Mission#MISSION Mission The mission for which the task dispatching is done. - -- @param Set#SET_GROUP SetGroup The set of groups that can join the tasks within the mission. - -- @param Functional.Detection#DETECTION_BASE Detection The detection results that are used to dynamically assign new tasks to human players. - -- @return #TASK_A2G_DISPATCHER self - function TASK_A2G_DISPATCHER:New( Mission, SetGroup, Detection ) - - -- Inherits from DETECTION_MANAGER - local self = BASE:Inherit( self, DETECTION_MANAGER:New( SetGroup, Detection ) ) -- #TASK_A2G_DISPATCHER - - self.Detection = Detection - self.Mission = Mission - - self:AddTransition( "Started", "Assign", "Started" ) - - --- OnAfter Transition Handler for Event Assign. - -- @function [parent=#TASK_A2G_DISPATCHER] OnAfterAssign - -- @param #TASK_A2G_DISPATCHER self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @param Tasking.Task_A2G#TASK_A2G Task - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param #string PlayerName - - self:__Start( 5 ) - - return self - end - - - --- Creates a SEAD task when there are targets for it. - -- @param #TASK_A2G_DISPATCHER self - -- @param Functional.Detection#DETECTION_AREAS.DetectedItem DetectedItem - -- @return Set#SET_UNIT TargetSetUnit: The target set of units. - -- @return #nil If there are no targets to be set. - function TASK_A2G_DISPATCHER:EvaluateSEAD( DetectedItem ) - self:F( { DetectedItem.ItemID } ) - - local DetectedSet = DetectedItem.Set - local DetectedZone = DetectedItem.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 #TASK_A2G_DISPATCHER self - -- @param Functional.Detection#DETECTION_AREAS.DetectedItem DetectedItem - -- @return Tasking.Task#TASK - function TASK_A2G_DISPATCHER:EvaluateCAS( DetectedItem ) - self:F( { DetectedItem.ItemID } ) - - local DetectedSet = DetectedItem.Set - local DetectedZone = DetectedItem.Zone - - - -- Determine if the set has radar targets. If it does, construct a SEAD task. - local GroundUnitCount = DetectedSet:HasGroundUnits() - local FriendliesNearBy = self.Detection:IsFriendliesNearBy( DetectedItem ) - - 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 #TASK_A2G_DISPATCHER self - -- @param Functional.Detection#DETECTION_AREAS.DetectedItem DetectedItem - -- @return Tasking.Task#TASK - function TASK_A2G_DISPATCHER:EvaluateBAI( DetectedItem, FriendlyCoalition ) - self:F( { DetectedItem.ItemID } ) - - local DetectedSet = DetectedItem.Set - local DetectedZone = DetectedItem.Zone - - - -- Determine if the set has radar targets. If it does, construct a SEAD task. - local GroundUnitCount = DetectedSet:HasGroundUnits() - local FriendliesNearBy = self.Detection:IsFriendliesNearBy( DetectedItem ) - - 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 DetectedItem is Changed AND the state of the Task is "Planned". - -- @param #TASK_A2G_DISPATCHER self - -- @param Tasking.Mission#MISSION Mission - -- @param Tasking.Task#TASK Task - -- @param Functional.Detection#DETECTION_AREAS.DetectedItem DetectedItem - -- @return Tasking.Task#TASK - function TASK_A2G_DISPATCHER:EvaluateRemoveTask( Mission, Task, DetectedItem ) - - if Task then - if Task:IsStatePlanned() and DetectedItem.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 #TASK_A2G_DISPATCHER self - -- @param Functional.Detection#DETECTION_BASE Detection The detection created by the @{Detection#DETECTION_BASE} derived object. - -- @return #boolean Return true if you want the task assigning to continue... false will cancel the loop. - function TASK_A2G_DISPATCHER:ProcessDetected( Detection ) - self:E() - - local AreaMsg = {} - local TaskMsg = {} - local ChangeMsg = {} - - local Mission = self.Mission - local ReportSEAD = REPORT:New( "- SEAD Tasks:") - local ReportCAS = REPORT:New( "- CAS Tasks:") - local ReportBAI = REPORT:New( "- BAI Tasks:") - local ReportChanges = REPORT:New( " - Changes:" ) - - --- First we need to the detected targets. - for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do - - local DetectedItem = DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem - local DetectedSet = DetectedItem.Set -- Functional.Detection#DETECTION_BASE.DetectedSet - local DetectedZone = DetectedItem.Zone - self:E( { "Targets in DetectedItem", DetectedItem.ItemID, DetectedSet:Count(), tostring( DetectedItem ) } ) - DetectedSet:Flush() - - local ItemID = DetectedItem.ItemID - - -- Evaluate SEAD Tasking - local SEADTask = Mission:GetTask( string.format( "SEAD.%03d", ItemID ) ) - SEADTask = self:EvaluateRemoveTask( Mission, SEADTask, DetectedItem ) - if not SEADTask then - local TargetSetUnit = self:EvaluateSEAD( DetectedItem ) -- Returns a SetUnit if there are targets to be SEADed... - if TargetSetUnit then - local Task = TASK_SEAD:New( Mission, self.SetGroup, string.format( "SEAD.%03d", ItemID ), TargetSetUnit ) - Task:SetTargetZone( DetectedZone ) - Task:SetDispatcher( self ) - SEADTask = Mission:AddTask( Task ) - end - end - if SEADTask and SEADTask:IsStatePlanned() then - ReportSEAD:Add( string.format( " - %s.%02d - %s", "SEAD", ItemID, Detection:DetectedItemReportSummary(DetectedItemID) ) ) - end - - -- Evaluate CAS Tasking - local CASTask = Mission:GetTask( string.format( "CAS.%03d", ItemID ) ) - CASTask = self:EvaluateRemoveTask( Mission, CASTask, DetectedItem ) - if not CASTask then - local TargetSetUnit = self:EvaluateCAS( DetectedItem ) -- Returns a SetUnit if there are targets to be SEADed... - if TargetSetUnit then - local Task = TASK_CAS:New( Mission, self.SetGroup, string.format( "CAS.%03d", ItemID ), TargetSetUnit ) - Task:SetTargetZone( DetectedZone ) - Task:SetDispatcher( self ) - CASTask = Mission:AddTask( Task ) - end - end - if CASTask and CASTask:IsStatePlanned() then - ReportCAS:Add( string.format( " - %s.%02d - %s", "CAS", ItemID, Detection:DetectedItemReportSummary(DetectedItemID) ) ) - end - - -- Evaluate BAI Tasking - local BAITask = Mission:GetTask( string.format( "BAI.%03d", ItemID ) ) - BAITask = self:EvaluateRemoveTask( Mission, BAITask, DetectedItem ) - if not BAITask then - local TargetSetUnit = self:EvaluateBAI( DetectedItem, self.Mission:GetCommandCenter():GetPositionable():GetCoalition() ) -- Returns a SetUnit if there are targets to be SEADed... - if TargetSetUnit then - local Task = TASK_BAI:New( Mission, self.SetGroup, string.format( "BAI.%03d", ItemID ), TargetSetUnit ) - Task:SetTargetZone( DetectedZone ) - Task:SetDispatcher( self ) - BAITask = Mission:AddTask( Task ) - end - end - if BAITask and BAITask:IsStatePlanned() then - ReportBAI:Add( string.format( " - %s.%02d - %s", "BAI", ItemID, Detection:DetectedItemReportSummary(DetectedItemID) ) ) - end - - - -- Loop through the changes ... - local ChangeText = Detection:GetChangeText( DetectedItem ) - ReportChanges:Add( ChangeText ) - - - -- OK, so the tasking has been done, now delete the changes reported for the area. - Detection:AcceptChanges( DetectedItem ) - - end - - -- TODO set menus using the HQ coordinator - Mission:GetCommandCenter():SetMenu() - - for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do - if not TaskGroup:GetState( TaskGroup, "Assigned" ) then - Mission:GetCommandCenter():MessageToGroup( - string.format( "HQ Reporting - Planned tasks for mission '%s':\n%s\n", - self.Mission:GetName(), - string.format( "%s\n\n%s\n\n%s\n\n%s", ReportSEAD:Text(), ReportCAS:Text(), ReportBAI:Text(), ReportChanges:Text() - ) - ), TaskGroup - ) - end - end - - return true - end - -end--- **Tasking** - The TASK_A2G models tasks for players in Air to Ground engagements. --- --- ![Banner Image](..\Presentations\TASK_A2G\Dia1.JPG) --- --- --- # 1) @{Task_A2G#TASK_A2G} class, extends @{Task#TASK} --- --- The @{#TASK_A2G} class defines Air To Ground tasks for a @{Set} of Target Units, --- based on the tasking capabilities defined in @{Task#TASK}. --- The TASK_A2G is implemented using a @{Statemachine#FSM_TASK}, and has the following statuses: --- --- * **None**: Start of the process --- * **Planned**: The A2G task is planned. --- * **Assigned**: The A2G task is assigned to a @{Group#GROUP}. --- * **Success**: The A2G task is successfully completed. --- * **Failed**: The A2G task has failed. This will happen if the player exists the task early, without communicating a possible cancellation to HQ. --- --- # 1.1) Set the scoring of achievements in an A2G attack. --- --- Scoring or penalties can be given in the following circumstances: --- --- * @{#TASK_A2G.SetScoreOnDestroy}(): Set a score when a target in scope of the A2G attack, has been destroyed. --- * @{#TASK_A2G.SetScoreOnSuccess}(): Set a score when all the targets in scope of the A2G attack, have been destroyed. --- * @{#TASK_A2G.SetPenaltyOnFailed}(): Set a penalty when the A2G attack has failed. --- --- # 2) @{Task_A2G#TASK_SEAD} class, extends @{Task_A2G#TASK_A2G} --- --- The @{#TASK_SEAD} class defines a SEAD task for a @{Set} of Target Units. --- --- === --- --- # 3) @{Task_A2G#TASK_CAS} class, extends @{Task_A2G#TASK_A2G} --- --- The @{#TASK_CAS} class defines a CAS task for a @{Set} of Target Units. --- --- === --- --- # 4) @{Task_A2G#TASK_BAI} class, extends @{Task_A2G#TASK_A2G} --- --- The @{#TASK_BAI} class defines a BAI task for a @{Set} of Target Units. --- --- ==== --- --- # **API CHANGE HISTORY** --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- Hereby the change log: --- --- 2017-03-09: Revised version. --- --- === --- --- # **AUTHORS and CONTRIBUTIONS** --- --- ### Contributions: --- --- * **[WingThor]**: Concept, Advice & Testing. --- --- ### Authors: --- --- * **FlightControl**: Concept, Design & Programming. --- --- @module Task_A2G - -do -- TASK_A2G - - --- The TASK_A2G class - -- @type TASK_A2G - -- @field Set#SET_UNIT TargetSetUnit - -- @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 Set#SET_UNIT UnitSetTargets - -- @param #number TargetDistance The distance to Target when the Player is considered to have "arrived" at the engagement range. - -- @param Core.Zone#ZONE_BASE TargetZone The target zone, if known. - -- If the TargetZone parameter is specified, the player will be routed to the center of the zone where all the targets are assumed to be. - -- @return #TASK_A2G self - function TASK_A2G:New( Mission, SetGroup, TaskName, TargetSetUnit, TaskType ) - local self = BASE:Inherit( self, TASK:New( Mission, SetGroup, TaskName, TaskType ) ) -- Tasking.Task#TASK_A2G - self:F() - - self.TargetSetUnit = TargetSetUnit - self.TaskType = TaskType - - Mission:AddTask( self ) - - local Fsm = self:GetUnitProcess() - - - Fsm:AddProcess ( "Planned", "Accept", ACT_ASSIGN_ACCEPT:New( self.TaskBriefing ), { Assigned = "RouteToRendezVous", Rejected = "Reject" } ) - - Fsm:AddTransition( "Assigned", "RouteToRendezVous", "RoutingToRendezVous" ) - Fsm:AddProcess ( "RoutingToRendezVous", "RouteToRendezVousPoint", ACT_ROUTE_POINT:New(), { Arrived = "ArriveAtRendezVous" } ) - Fsm:AddProcess ( "RoutingToRendezVous", "RouteToRendezVousZone", ACT_ROUTE_ZONE:New(), { Arrived = "ArriveAtRendezVous" } ) - - Fsm:AddTransition( { "Arrived", "RoutingToRendezVous" }, "ArriveAtRendezVous", "ArrivedAtRendezVous" ) - - Fsm:AddTransition( { "ArrivedAtRendezVous", "HoldingAtRendezVous" }, "Engage", "Engaging" ) - Fsm:AddTransition( { "ArrivedAtRendezVous", "HoldingAtRendezVous" }, "HoldAtRendezVous", "HoldingAtRendezVous" ) - - Fsm:AddProcess ( "Engaging", "Account", ACT_ACCOUNT_DEADS:New( self.TargetSetUnit, self.TaskType ), { Accounted = "Success" } ) - Fsm:AddTransition( "Engaging", "RouteToTarget", "Engaging" ) - Fsm:AddProcess( "Engaging", "RouteToTargetZone", ACT_ROUTE_ZONE:New(), {} ) - Fsm:AddProcess( "Engaging", "RouteToTargetPoint", ACT_ROUTE_POINT:New(), {} ) - Fsm:AddTransition( "Engaging", "RouteToTargets", "Engaging" ) - - Fsm:AddTransition( "Accounted", "DestroyedAll", "Accounted" ) - Fsm:AddTransition( "Accounted", "Success", "Success" ) - Fsm:AddTransition( "Rejected", "Reject", "Aborted" ) - Fsm:AddTransition( "Failed", "Fail", "Failed" ) - - - --- Test - -- @param #FSM_PROCESS self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_A2G#TASK_A2G Task - function Fsm:onafterRouteToRendezVous( TaskUnit, Task ) - self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - -- Determine the first Unit from the self.RendezVousSetUnit - - if Task:GetRendezVousZone( TaskUnit ) then - self:__RouteToRendezVousZone( 0.1 ) - else - if Task:GetRendezVousPointVec2( TaskUnit ) then - self:__RouteToRendezVousPoint( 0.1 ) - else - self:__ArriveAtRendezVous( 0.1 ) - end - end - end - - --- Test - -- @param #FSM_PROCESS self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task#TASK_A2G Task - function Fsm:OnAfterArriveAtRendezVous( TaskUnit, Task ) - self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - -- Determine the first Unit from the self.TargetSetUnit - - self:__Engage( 0.1 ) - end - - --- Test - -- @param #FSM_PROCESS self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task#TASK_A2G Task - function Fsm:onafterEngage( TaskUnit, Task ) - self:E( { self } ) - self:__Account( 0.1 ) - self:__RouteToTarget(0.1 ) - self:__RouteToTargets( -10 ) - end - - --- Test - -- @param #FSM_PROCESS self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_A2G#TASK_A2G Task - function Fsm:onafterRouteToTarget( TaskUnit, Task ) - self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - -- Determine the first Unit from the self.TargetSetUnit - - if Task:GetTargetZone( TaskUnit ) then - self:__RouteToTargetZone( 0.1 ) - else - local TargetUnit = Task.TargetSetUnit:GetFirst() -- Wrapper.Unit#UNIT - if TargetUnit then - local PointVec2 = TargetUnit:GetPointVec2() - self:T( { TargetPointVec2 = PointVec2, PointVec2:GetX(), PointVec2:GetAlt(), PointVec2:GetZ() } ) - Task:SetTargetPointVec2( TargetUnit:GetPointVec2(), TaskUnit ) - end - self:__RouteToTargetPoint( 0.1 ) - end - end - - --- Test - -- @param #FSM_PROCESS self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_A2G#TASK_A2G Task - function Fsm:onafterRouteToTargets( TaskUnit, Task ) - self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - local TargetUnit = Task.TargetSetUnit:GetFirst() -- Wrapper.Unit#UNIT - if TargetUnit then - Task:SetTargetPointVec2( TargetUnit:GetPointVec2(), TaskUnit ) - end - self:__RouteToTargets( -10 ) - end - - return self - - end - - --- @param #TASK_A2G self - function TASK_A2G:GetPlannedMenuText() - return self:GetStateString() .. " - " .. self:GetTaskName() .. " ( " .. self.TargetSetUnit:GetUnitTypesText() .. " )" - end - - --- @param #TASK_A2G self - -- @param Core.Point#POINT_VEC2 RendezVousPointVec2 The PointVec2 object referencing to the 2D point where the RendezVous point is located on the map. - -- @param #number RendezVousRange The RendezVousRange that defines when the player is considered to have arrived at the RendezVous point. - -- @param Wrapper.Unit#UNIT TaskUnit - function TASK_A2G:SetRendezVousPointVec2( RendezVousPointVec2, RendezVousRange, TaskUnit ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT - ActRouteRendezVous:SetPointVec2( RendezVousPointVec2 ) - ActRouteRendezVous:SetRange( RendezVousRange ) - end - - --- @param #TASK_A2G self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return Core.Point#POINT_VEC2 The PointVec2 object referencing to the 2D point where the RendezVous point is located on the map. - -- @return #number The RendezVousRange that defines when the player is considered to have arrived at the RendezVous point. - function TASK_A2G:GetRendezVousPointVec2( TaskUnit ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT - return ActRouteRendezVous:GetPointVec2(), ActRouteRendezVous:GetRange() - end - - - - --- @param #TASK_A2G self - -- @param Core.Zone#ZONE_BASE RendezVousZone The Zone object where the RendezVous is located on the map. - -- @param Wrapper.Unit#UNIT TaskUnit - function TASK_A2G:SetRendezVousZone( RendezVousZone, TaskUnit ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE - ActRouteRendezVous:SetZone( RendezVousZone ) - end - - --- @param #TASK_A2G self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return Core.Zone#ZONE_BASE The Zone object where the RendezVous is located on the map. - function TASK_A2G:GetRendezVousZone( TaskUnit ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE - return ActRouteRendezVous:GetZone() - end - - --- @param #TASK_A2G self - -- @param Core.Point#POINT_VEC2 TargetPointVec2 The PointVec2 object where the Target is located on the map. - -- @param Wrapper.Unit#UNIT TaskUnit - function TASK_A2G:SetTargetPointVec2( TargetPointVec2, TaskUnit ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT - ActRouteTarget:SetPointVec2( TargetPointVec2 ) - end - - - --- @param #TASK_A2G self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return Core.Point#POINT_VEC2 The PointVec2 object where the Target is located on the map. - function TASK_A2G:GetTargetPointVec2( TaskUnit ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT - return ActRouteTarget:GetPointVec2() - end - - - --- @param #TASK_A2G self - -- @param Core.Zone#ZONE_BASE TargetZone The Zone object where the Target is located on the map. - -- @param Wrapper.Unit#UNIT TaskUnit - function TASK_A2G:SetTargetZone( TargetZone, TaskUnit ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE - ActRouteTarget:SetZone( TargetZone ) - end - - - --- @param #TASK_A2G self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return Core.Zone#ZONE_BASE The Zone object where the Target is located on the map. - function TASK_A2G:GetTargetZone( TaskUnit ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE - return ActRouteTarget:GetZone() - end - - --- Set a score when a target in scope of the A2G attack, has been destroyed . - -- @param #TASK_A2G self - -- @param #string Text The text to display to the player, when the target has been destroyed. - -- @param #number Score The score in points. - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #TASK_A2G - function TASK_A2G:SetScoreOnDestroy( Text, Score, TaskUnit ) - self:F( { Text, Score, TaskUnit } ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - ProcessUnit:AddScoreProcess( "Engaging", "Account", "Account", Text, Score ) - - return self - end - - --- Set a score when all the targets in scope of the A2G attack, have been destroyed. - -- @param #TASK_A2G self - -- @param #string Text The text to display to the player, when all targets hav been destroyed. - -- @param #number Score The score in points. - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #TASK_A2G - function TASK_A2G:SetScoreOnSuccess( Text, Score, TaskUnit ) - self:F( { Text, Score, TaskUnit } ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - ProcessUnit:AddScore( "Success", Text, Score ) - - return self - end - - --- Set a penalty when the A2G attack has failed. - -- @param #TASK_A2G self - -- @param #string Text The text to display to the player, when the A2G attack has failed. - -- @param #number Penalty The penalty in points. - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #TASK_A2G - function TASK_A2G:SetPenaltyOnFailed( Text, Penalty, TaskUnit ) - self:F( { Text, Score, TaskUnit } ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - ProcessUnit:AddScore( "Failed", Text, Penalty ) - - return self - end - - -end - - -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 #number TargetDistance The distance to Target when the Player is considered to have "arrived" at the engagement range. - -- @param Core.Zone#ZONE_BASE TargetZone The target zone, if known. - -- If the TargetZone parameter is specified, the player will be routed to the center of the zone where all the targets are assumed to be. - -- @return #TASK_SEAD self - function TASK_SEAD:New( Mission, SetGroup, TaskName, TargetSetUnit ) - local self = BASE:Inherit( self, TASK_A2G:New( Mission, SetGroup, TaskName, TargetSetUnit, "SEAD" ) ) -- #TASK_SEAD - self:F() - - return self - end - -end - -do -- TASK_BAI - - --- The TASK_BAI class - -- @type TASK_BAI - -- @field Set#SET_UNIT TargetSetUnit - -- @extends Tasking.Task#TASK - TASK_BAI = { - ClassName = "TASK_BAI", - } - - --- Instantiates a new TASK_BAI. - -- @param #TASK_BAI 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 #number TargetDistance The distance to Target when the Player is considered to have "arrived" at the engagement range. - -- @param Core.Zone#ZONE_BASE TargetZone The target zone, if known. - -- If the TargetZone parameter is specified, the player will be routed to the center of the zone where all the targets are assumed to be. - -- @return #TASK_BAI self - function TASK_BAI:New( Mission, SetGroup, TaskName, TargetSetUnit ) - local self = BASE:Inherit( self, TASK_A2G:New( Mission, SetGroup, TaskName, TargetSetUnit, "BAI" ) ) -- #TASK_BAI - self:F() - - return self - end - -end - -do -- TASK_CAS - - --- The TASK_CAS class - -- @type TASK_CAS - -- @field Set#SET_UNIT TargetSetUnit - -- @extends Tasking.Task#TASK - TASK_CAS = { - ClassName = "TASK_CAS", - } - - --- Instantiates a new TASK_CAS. - -- @param #TASK_CAS 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 #number TargetDistance The distance to Target when the Player is considered to have "arrived" at the engagement range. - -- @param Core.Zone#ZONE_BASE TargetZone The target zone, if known. - -- If the TargetZone parameter is specified, the player will be routed to the center of the zone where all the targets are assumed to be. - -- @return #TASK_CAS self - function TASK_CAS:New( Mission, SetGroup, TaskName, TargetSetUnit ) - local self = BASE:Inherit( self, TASK_A2G:New( Mission, SetGroup, TaskName, TargetSetUnit, "CAS" ) ) -- #TASK_CAS - self:F() - - return self - end - -end ---- The main include file for the MOOSE system. --- Test of permissions - ---- 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" ) -Include.File( "Core/Radio" ) - ---- Wrapper Classes -Include.File( "Wrapper/Object" ) -Include.File( "Wrapper/Identifiable" ) -Include.File( "Wrapper/Positionable" ) -Include.File( "Wrapper/Controllable" ) -Include.File( "Wrapper/Group" ) -Include.File( "Wrapper/Unit" ) -Include.File( "Wrapper/Client" ) -Include.File( "Wrapper/Static" ) -Include.File( "Wrapper/Airbase" ) -Include.File( "Wrapper/Scenery" ) - ---- Functional Classes -Include.File( "Functional/Scoring" ) -Include.File( "Functional/CleanUp" ) -Include.File( "Functional/Spawn" ) -Include.File( "Functional/Movement" ) -Include.File( "Functional/Sead" ) -Include.File( "Functional/Escort" ) -Include.File( "Functional/MissileTrainer" ) -Include.File( "Functional/AirbasePolice" ) -Include.File( "Functional/Detection" ) - ---- AI Classes -Include.File( "AI/AI_Balancer" ) -Include.File( "AI/AI_Patrol" ) -Include.File( "AI/AI_Cap" ) -Include.File( "AI/AI_Cas" ) -Include.File( "AI/AI_Cargo" ) - ---- Actions -Include.File( "Actions/Act_Assign" ) -Include.File( "Actions/Act_Route" ) -Include.File( "Actions/Act_Account" ) -Include.File( "Actions/Act_Assist" ) - ---- Task Handling Classes -Include.File( "Tasking/CommandCenter" ) -Include.File( "Tasking/Mission" ) -Include.File( "Tasking/Task" ) -Include.File( "Tasking/DetectionManager" ) -Include.File( "Tasking/Task_A2G_Dispatcher") -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 9aca34abf..4db9338c3 100644 --- a/Moose Mission Setup/Moose.lua +++ b/Moose Mission Setup/Moose.lua @@ -1,35834 +1,31 @@ -env.info( '*** MOOSE STATIC INCLUDE START *** ' ) -env.info( 'Moose Generation Timestamp: 20170325_0623' ) +env.info( '*** MOOSE DYNAMIC INCLUDE START *** ' ) +env.info( 'Moose Generation Timestamp: 20170325_0745' ) + 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 ---- **Core** - BASE forms **the basis of the MOOSE framework**. Each class within the MOOSE framework derives from BASE. --- --- ![Banner Image](..\Presentations\BASE\Dia1.JPG) --- --- === --- --- The @{#BASE} class is the core root class from where every other class in moose is derived. --- --- === --- --- # **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 - ---- @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. - ---- # 1) #BASE class --- --- All classes within the MOOSE framework are derived from the BASE class. --- --- BASE provides facilities for : --- --- * The construction and inheritance of MOOSE classes. --- * The class naming and numbering system. --- * The class hierarchy search system. --- * The tracing of information or objects during mission execution for debuggin purposes. --- * The subscription to DCS events for event handling in MOOSE objects. --- --- Note: The BASE class is an abstract class and is not meant to be used directly. --- --- ## 1.1) BASE constructor --- --- Any class derived from BASE, will use the @{Base#BASE.New} constructor embedded in the @{Base#BASE.Inherit} method. --- See an example at the @{Base#BASE.New} method how this is done. --- --- ## 1.2) Trace information for debugging --- --- The BASE class contains trace methods to trace progress within a mission execution of a certain object. --- These trace methods are inherited by each MOOSE class interiting BASE, soeach object created from derived class from BASE can use the tracing methods to trace its execution. --- --- Any type of information can be passed to these tracing methods. See the following examples: --- --- self:E( "Hello" ) --- --- Result in the word "Hello" in the dcs.log. --- --- local Array = { 1, nil, "h", { "a","b" }, "x" } --- self:E( Array ) --- --- Results with the text [1]=1,[3]="h",[4]={[1]="a",[2]="b"},[5]="x"} in the dcs.log. --- --- local Object1 = "Object1" --- local Object2 = 3 --- local Object3 = { Object 1, Object 2 } --- self:E( { Object1, Object2, Object3 } ) --- --- Results with the text [1]={[1]="Object",[2]=3,[3]={[1]="Object",[2]=3}} in the dcs.log. --- --- local SpawnObject = SPAWN:New( "Plane" ) --- local GroupObject = GROUP:FindByName( "Group" ) --- self:E( { Spawn = SpawnObject, Group = GroupObject } ) --- --- Results with the text [1]={Spawn={....),Group={...}} in the dcs.log. --- --- Below a more detailed explanation of the different method types for tracing. --- --- ### 1.2.1) Tracing methods categories --- --- There are basically 3 types of tracing methods available: --- --- * @{#BASE.F}: Used to trace the entrance of a function and its given parameters. An F is indicated at column 44 in the DCS.log file. --- * @{#BASE.T}: Used to trace further logic within a function giving optional variables or parameters. A T is indicated at column 44 in the DCS.log file. --- * @{#BASE.E}: Used to always trace information giving optional variables or parameters. An E is indicated at column 44 in the DCS.log file. --- --- ### 1.2.2) Tracing levels --- --- There are 3 tracing levels within MOOSE. --- These tracing levels were defined to avoid bulks of tracing to be generated by lots of objects. --- --- As such, the F and T methods have additional variants to trace level 2 and 3 respectively: --- --- * @{#BASE.F2}: Trace the beginning of a function and its given parameters with tracing level 2. --- * @{#BASE.F3}: Trace the beginning of a function and its given parameters with tracing level 3. --- * @{#BASE.T2}: Trace further logic within a function giving optional variables or parameters with tracing level 2. --- * @{#BASE.T3}: Trace further logic within a function giving optional variables or parameters with tracing level 3. --- --- ### 1.2.3) Trace activation. --- --- Tracing can be activated in several ways: --- --- * Switch tracing on or off through the @{#BASE.TraceOnOff}() method. --- * Activate all tracing through the @{#BASE.TraceAll}() method. --- * Activate only the tracing of a certain class (name) through the @{#BASE.TraceClass}() method. --- * Activate only the tracing of a certain method of a certain class through the @{#BASE.TraceClassMethod}() method. --- * Activate only the tracing of a certain level through the @{#BASE.TraceLevel}() method. --- --- ### 1.2.4) Check if tracing is on. --- --- The method @{#BASE.IsTrace}() will validate if tracing is activated or not. --- --- ## 1.3 DCS simulator Event Handling --- --- The BASE class provides methods to catch DCS Events. These are events that are triggered from within the DCS simulator, --- and handled through lua scripting. MOOSE provides an encapsulation to handle these events more efficiently. --- --- ### 1.3.1 Subscribe / Unsubscribe to DCS Events --- --- At first, the mission designer will need to **Subscribe** to a specific DCS event for the class. --- So, when the DCS event occurs, the class will be notified of that event. --- There are two methods which you use to subscribe to or unsubscribe from an event. --- --- * @{#BASE.HandleEvent}(): Subscribe to a DCS Event. --- * @{#BASE.UnHandleEvent}(): Unsubscribe from a DCS Event. --- --- ### 1.3.2 Event Handling of DCS Events --- --- Once the class is subscribed to the event, an **Event Handling** method on the object or class needs to be written that will be called --- when the DCS event occurs. The Event Handling method receives an @{Event#EVENTDATA} structure, which contains a lot of information --- about the event that occurred. --- --- Find below an example of the prototype how to write an event handling function for two units: --- --- local Tank1 = UNIT:FindByName( "Tank A" ) --- local Tank2 = UNIT:FindByName( "Tank B" ) --- --- -- Here we subscribe to the Dead events. So, if one of these tanks dies, the Tank1 or Tank2 objects will be notified. --- Tank1:HandleEvent( EVENTS.Dead ) --- Tank2:HandleEvent( EVENTS.Dead ) --- --- --- This function is an Event Handling function that will be called when Tank1 is Dead. --- -- @param Wrapper.Unit#UNIT self --- -- @param Core.Event#EVENTDATA EventData --- function Tank1:OnEventDead( EventData ) --- --- self:SmokeGreen() --- end --- --- --- This function is an Event Handling function that will be called when Tank2 is Dead. --- -- @param Wrapper.Unit#UNIT self --- -- @param Core.Event#EVENTDATA EventData --- function Tank2:OnEventDead( EventData ) --- --- self:SmokeBlue() --- end --- --- --- --- See the @{Event} module for more information about event handling. --- --- ## 1.4) Class identification methods --- --- BASE provides methods to get more information of each object: --- --- * @{#BASE.GetClassID}(): Gets the ID (number) of the object. Each object created is assigned a number, that is incremented by one. --- * @{#BASE.GetClassName}(): Gets the name of the object, which is the name of the class the object was instantiated from. --- * @{#BASE.GetClassNameAndID}(): Gets the name and ID of the object. --- --- ## 1.5) All objects derived from BASE can have "States" --- --- A mechanism is in place in MOOSE, that allows to let the objects administer **states**. --- States are essentially properties of objects, which are identified by a **Key** and a **Value**. --- --- The method @{#BASE.SetState}() can be used to set a Value with a reference Key to the object. --- To **read or retrieve** a state Value based on a Key, use the @{#BASE.GetState} method. --- --- These two methods provide a very handy way to keep state at long lasting processes. --- Values can be stored within the objects, and later retrieved or changed when needed. --- There is one other important thing to note, the @{#BASE.SetState}() and @{#BASE.GetState} methods --- receive as the **first parameter the object for which the state needs to be set**. --- Thus, if the state is to be set for the same object as the object for which the method is used, then provide the same --- object name to the method. --- --- ## 1.10) Inheritance --- --- The following methods are available to implement 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. --- --- === --- --- @field #BASE BASE --- -BASE = { - ClassName = "BASE", - ClassID = 0, - _Private = {}, - Events = {}, - States = {} -} - ---- The Formation Class --- @type FORMATION --- @field Cone A cone formation. -FORMATION = { - Cone = "Cone" -} - - - ---- BASE constructor. --- --- This is an example how to use the BASE:New() constructor in a new class definition when inheriting from BASE. --- --- function EVENT:New() --- local self = BASE:Inherit( self, BASE:New() ) -- #EVENT --- return self --- end --- --- @param #BASE self --- @return #BASE -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 - - --- THIS IS WHY WE NEED LUA 5.2 ... -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. --- Note that the Parent class must be passed to call the parent class method. --- --- self:GetParent(self):ParentMethod() --- --- --- @param #BASE self --- @param #BASE Child is the Child class from which the Parent class needs to be retrieved. --- @return #BASE -function BASE:GetParent( Child ) - local Parent = getmetatable( Child ) --- env.info('Inherited class of ' .. Child.ClassName .. ' is ' .. Parent.ClassName ) - return Parent -end - ---- Get the ClassName + ClassID of the class instance. --- The ClassName + ClassID is formatted as '%s#%09d'. --- @param #BASE self --- @return #string The ClassName + ClassID of the class instance. -function BASE:GetClassNameAndID() - return string.format( '%s#%09d', self.ClassName, self.ClassID ) -end - ---- Get the ClassName of the class instance. --- @param #BASE self --- @return #string The ClassName of the class instance. -function BASE:GetClassName() - return self.ClassName -end - ---- Get the ClassID of the class instance. --- @param #BASE self --- @return #string The ClassID of the class instance. -function BASE:GetClassID() - return self.ClassID -end - -do -- Event Handling - - --- Returns the event dispatcher - -- @param #BASE self - -- @return Core.Event#EVENT - function BASE:EventDispatcher() - - return _EVENTDISPATCHER - end - - - --- Get the Class @{Event} processing Priority. - -- The Event processing Priority is a number from 1 to 10, - -- reflecting the order of the classes subscribed to the Event to be processed. - -- @param #BASE self - -- @return #number The @{Event} processing Priority. - function BASE:GetEventPriority() - return self._Private.EventPriority or 5 - end - - --- Set the Class @{Event} processing Priority. - -- The Event processing Priority is a number from 1 to 10, - -- reflecting the order of the classes subscribed to the Event to be processed. - -- @param #BASE self - -- @param #number EventPriority The @{Event} processing Priority. - -- @return self - function BASE:SetEventPriority( EventPriority ) - self._Private.EventPriority = EventPriority - end - - --- Remove all subscribed events - -- @param #BASE self - -- @return #BASE - function BASE:EventRemoveAll() - - self:EventDispatcher():RemoveAll( self ) - - return self - end - - --- Subscribe to a DCS Event. - -- @param #BASE self - -- @param Core.Event#EVENTS Event - -- @param #function EventFunction (optional) The function to be called when the event occurs for the unit. - -- @return #BASE - function BASE:HandleEvent( Event, EventFunction ) - - self:EventDispatcher():OnEventGeneric( EventFunction, self, Event ) - - return self - end - - --- UnSubscribe to a DCS event. - -- @param #BASE self - -- @param Core.Event#EVENTS Event - -- @return #BASE - function BASE:UnHandleEvent( Event ) - - self:EventDispatcher():Remove( self, Event ) - - return self - end - - -- Event handling function prototypes - - --- Occurs whenever any unit in a mission fires a weapon. But not any machine gun or autocannon based weapon, those are handled by EVENT.ShootingStart. - -- @function [parent=#BASE] OnEventShot - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs whenever an object is hit by a weapon. - -- initiator : The unit object the fired the weapon - -- weapon: Weapon object that hit the target - -- target: The Object that was hit. - -- @function [parent=#BASE] OnEventHit - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when an aircraft takes off from an airbase, farp, or ship. - -- initiator : The unit that tookoff - -- place: Object from where the AI took-off from. Can be an Airbase Object, FARP, or Ships - -- @function [parent=#BASE] OnEventTakeoff - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when an aircraft lands at an airbase, farp or ship - -- initiator : The unit that has landed - -- place: Object that the unit landed on. Can be an Airbase Object, FARP, or Ships - -- @function [parent=#BASE] OnEventLand - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any aircraft crashes into the ground and is completely destroyed. - -- initiator : The unit that has crashed - -- @function [parent=#BASE] OnEventCrash - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when a pilot ejects from an aircraft - -- initiator : The unit that has ejected - -- @function [parent=#BASE] OnEventEjection - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when an aircraft connects with a tanker and begins taking on fuel. - -- initiator : The unit that is receiving fuel. - -- @function [parent=#BASE] OnEventRefueling - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when an object is completely destroyed. - -- initiator : The unit that is was destroyed. - -- @function [parent=#BASE] OnEvent - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when the pilot of an aircraft is killed. Can occur either if the player is alive and crashes or if a weapon kills the pilot without completely destroying the plane. - -- initiator : The unit that the pilot has died in. - -- @function [parent=#BASE] OnEventPilotDead - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when a ground unit captures either an airbase or a farp. - -- initiator : The unit that captured the base - -- place: The airbase that was captured, can be a FARP or Airbase. When calling place:getCoalition() the faction will already be the new owning faction. - -- @function [parent=#BASE] OnEventBaseCaptured - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when a mission starts - -- @function [parent=#BASE] OnEventMissionStart - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when a mission ends - -- @function [parent=#BASE] OnEventMissionEnd - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when an aircraft is finished taking fuel. - -- initiator : The unit that was receiving fuel. - -- @function [parent=#BASE] OnEventRefuelingStop - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any object is spawned into the mission. - -- initiator : The unit that was spawned - -- @function [parent=#BASE] OnEventBirth - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any system fails on a human controlled aircraft. - -- initiator : The unit that had the failure - -- @function [parent=#BASE] OnEventHumanFailure - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any aircraft starts its engines. - -- initiator : The unit that is starting its engines. - -- @function [parent=#BASE] OnEventEngineStartup - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any aircraft shuts down its engines. - -- initiator : The unit that is stopping its engines. - -- @function [parent=#BASE] OnEventEngineShutdown - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any player assumes direct control of a unit. - -- initiator : The unit that is being taken control of. - -- @function [parent=#BASE] OnEventPlayerEnterUnit - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any player relieves control of a unit to the AI. - -- initiator : The unit that the player left. - -- @function [parent=#BASE] OnEventPlayerLeaveUnit - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any unit begins firing a weapon that has a high rate of fire. Most common with aircraft cannons (GAU-8), autocannons, and machine guns. - -- initiator : The unit that is doing the shooing. - -- target: The unit that is being targeted. - -- @function [parent=#BASE] OnEventShootingStart - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - - --- Occurs when any unit stops firing its weapon. Event will always correspond with a shooting start event. - -- initiator : The unit that was doing the shooing. - -- @function [parent=#BASE] OnEventShootingEnd - -- @param #BASE self - -- @param Core.Event#EVENTDATA EventData The EventData structure. - -end - - ---- Creation of a Birth Event. --- @param #BASE self --- @param Dcs.DCSTypes#Time EventTime The time stamp of the event. --- @param Dcs.DCSWrapper.Object#Object Initiator The initiating object of the event. --- @param #string IniUnitName The initiating unit name. --- @param place --- @param subplace -function BASE:CreateEventBirth( EventTime, Initiator, IniUnitName, place, subplace ) - self:F( { EventTime, Initiator, IniUnitName, place, subplace } ) - - local Event = { - id = world.event.S_EVENT_BIRTH, - time = EventTime, - initiator = Initiator, - IniUnitName = IniUnitName, - place = place, - subplace = subplace - } - - world.onEvent( Event ) -end - ---- Creation of a Crash Event. --- @param #BASE self --- @param Dcs.DCSTypes#Time EventTime The time stamp of the event. --- @param Dcs.DCSWrapper.Object#Object Initiator The initiating object of the event. -function BASE:CreateEventCrash( EventTime, Initiator ) - self:F( { EventTime, Initiator } ) - - local Event = { - id = world.event.S_EVENT_CRASH, - time = EventTime, - initiator = Initiator, - } - - world.onEvent( Event ) -end - --- TODO: Complete Dcs.DCSTypes#Event structure. ---- The main event handling function... This function captures all events generated for the class. --- @param #BASE self --- @param Dcs.DCSTypes#Event event -function BASE:onEvent(event) - --self:F( { BaseEventCodes[event.id], event } ) - - if self then - for EventID, EventObject in pairs( self.Events ) do - if EventObject.EventEnabled then - --env.info( 'onEvent Table EventObject.Self = ' .. tostring(EventObject.Self) ) - --env.info( 'onEvent event.id = ' .. tostring(event.id) ) - --env.info( 'onEvent EventObject.Event = ' .. tostring(EventObject.Event) ) - if event.id == EventObject.Event then - if self == EventObject.Self then - if event.initiator and event.initiator:isExist() then - event.IniUnitName = event.initiator:getName() - end - if event.target and event.target:isExist() then - event.TgtUnitName = event.target:getName() - end - --self:T( { BaseEventCodes[event.id], event } ) - --EventObject.EventFunction( self, event ) - end - end - end - end - end -end - ---- Set a state or property of the Object given a Key and a Value. --- Note that if the Object is destroyed, nillified or garbage collected, then the Values and Keys will also be gone. --- @param #BASE self --- @param Object The object that will hold the Value set by the Key. --- @param Key The key that is used as a reference of the value. Note that the key can be a #string, but it can also be any other type! --- @param Value The value to is stored in the object. --- @return The Value set. --- @return #nil The Key was not found and thus the Value could not be retrieved. -function BASE:SetState( Object, Key, Value ) - - local ClassNameAndID = Object:GetClassNameAndID() - - self.States[ClassNameAndID] = self.States[ClassNameAndID] or {} - self.States[ClassNameAndID][Key] = Value - self:T2( { ClassNameAndID, Key, Value } ) - - return self.States[ClassNameAndID][Key] -end - - ---- Get a Value given a Key from the Object. --- Note that if the Object is destroyed, nillified or garbage collected, then the Values and Keys will also be gone. --- @param #BASE self --- @param Object The object that holds the Value set by the Key. --- @param Key The key that is used to retrieve the value. Note that the key can be a #string, but it can also be any other type! --- @param Value The value to is stored in the Object. --- @return The Value retrieved. -function BASE:GetState( Object, Key ) - - local ClassNameAndID = Object:GetClassNameAndID() - - if self.States[ClassNameAndID] then - local Value = self.States[ClassNameAndID][Key] or false - self:T2( { ClassNameAndID, Key, Value } ) - return Value - end - - return nil -end - -function BASE:ClearState( Object, StateName ) - - local ClassNameAndID = Object:GetClassNameAndID() - if self.States[ClassNameAndID] then - self.States[ClassNameAndID][StateName] = nil - end -end - --- Trace section - --- Log a trace (only shown when trace is on) --- TODO: Make trace function using variable parameters. - ---- Set trace on or off --- Note that when trace is off, no debug statement is performed, increasing performance! --- When Moose is loaded statically, (as one file), tracing is switched off by default. --- So tracing must be switched on manually in your mission if you are using Moose statically. --- When moose is loading dynamically (for moose class development), tracing is switched on by default. --- @param #BASE self --- @param #boolean TraceOnOff Switch the tracing on or off. --- @usage --- -- Switch the tracing On --- BASE:TraceOnOff( true ) --- --- -- Switch the tracing Off --- BASE:TraceOnOff( false ) -function BASE:TraceOnOff( TraceOnOff ) - _TraceOnOff = TraceOnOff -end - - ---- Enquires if tracing is on (for the class). --- @param #BASE self --- @return #boolean -function BASE:IsTrace() - - if debug and ( _TraceAll == true ) or ( _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName] ) then - return true - else - return false - end -end - ---- Set trace level --- @param #BASE self --- @param #number Level -function BASE:TraceLevel( Level ) - _TraceLevel = Level - self:E( "Tracing level " .. Level ) -end - ---- Trace all methods in MOOSE --- @param #BASE self --- @param #boolean TraceAll true = trace all methods in MOOSE. -function BASE:TraceAll( TraceAll ) - - _TraceAll = TraceAll - - if _TraceAll then - self:E( "Tracing all methods in MOOSE " ) - else - self:E( "Switched off tracing all methods in MOOSE" ) - end -end - ---- Set tracing for a class --- @param #BASE self --- @param #string Class -function BASE:TraceClass( Class ) - _TraceClass[Class] = true - _TraceClassMethod[Class] = {} - self:E( "Tracing class " .. Class ) -end - ---- Set tracing for a specific method of class --- @param #BASE self --- @param #string Class --- @param #string Method -function BASE:TraceClassMethod( Class, Method ) - if not _TraceClassMethod[Class] then - _TraceClassMethod[Class] = {} - _TraceClassMethod[Class].Method = {} - end - _TraceClassMethod[Class].Method[Method] = true - self:E( "Tracing method " .. Method .. " of class " .. Class ) -end - ---- Trace a function call. This function is private. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:_F( Arguments, DebugInfoCurrentParam, DebugInfoFromParam ) - - if debug and ( _TraceAll == true ) or ( _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName] ) then - - local DebugInfoCurrent = DebugInfoCurrentParam and DebugInfoCurrentParam or debug.getinfo( 2, "nl" ) - local DebugInfoFrom = DebugInfoFromParam and DebugInfoFromParam or debug.getinfo( 3, "l" ) - - local Function = "function" - if DebugInfoCurrent.name then - Function = DebugInfoCurrent.name - end - - if _TraceAll == true or _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName].Method[Function] then - local LineCurrent = 0 - if DebugInfoCurrent.currentline then - LineCurrent = DebugInfoCurrent.currentline - end - local LineFrom = 0 - if DebugInfoFrom then - LineFrom = DebugInfoFrom.currentline - end - env.info( string.format( "%6d(%6d)/%1s:%20s%05d.%s(%s)" , LineCurrent, LineFrom, "F", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) - end - end -end - ---- Trace a function call. Must be at the beginning of the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:F( Arguments ) - - if debug and _TraceOnOff then - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - if _TraceLevel >= 1 then - self:_F( Arguments, DebugInfoCurrent, DebugInfoFrom ) - end - end -end - - ---- Trace a function call level 2. Must be at the beginning of the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:F2( Arguments ) - - if debug and _TraceOnOff then - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - if _TraceLevel >= 2 then - self:_F( Arguments, DebugInfoCurrent, DebugInfoFrom ) - end - end -end - ---- Trace a function call level 3. Must be at the beginning of the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:F3( Arguments ) - - if debug and _TraceOnOff then - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - if _TraceLevel >= 3 then - self:_F( Arguments, DebugInfoCurrent, DebugInfoFrom ) - end - end -end - ---- Trace a function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:_T( Arguments, DebugInfoCurrentParam, DebugInfoFromParam ) - - if debug and ( _TraceAll == true ) or ( _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName] ) then - - local DebugInfoCurrent = DebugInfoCurrentParam and DebugInfoCurrentParam or debug.getinfo( 2, "nl" ) - local DebugInfoFrom = DebugInfoFromParam and DebugInfoFromParam or debug.getinfo( 3, "l" ) - - local Function = "function" - if DebugInfoCurrent.name then - Function = DebugInfoCurrent.name - end - - if _TraceAll == true or _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName].Method[Function] then - local LineCurrent = 0 - if DebugInfoCurrent.currentline then - LineCurrent = DebugInfoCurrent.currentline - end - local LineFrom = 0 - if DebugInfoFrom then - LineFrom = DebugInfoFrom.currentline - end - env.info( string.format( "%6d(%6d)/%1s:%20s%05d.%s" , LineCurrent, LineFrom, "T", self.ClassName, self.ClassID, routines.utils.oneLineSerialize( Arguments ) ) ) - end - end -end - ---- Trace a function logic level 1. Can be anywhere within the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:T( Arguments ) - - if debug and _TraceOnOff then - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - if _TraceLevel >= 1 then - self:_T( Arguments, DebugInfoCurrent, DebugInfoFrom ) - end - end -end - - ---- Trace a function logic level 2. Can be anywhere within the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:T2( Arguments ) - - if debug and _TraceOnOff then - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - if _TraceLevel >= 2 then - self:_T( Arguments, DebugInfoCurrent, DebugInfoFrom ) - end - end -end - ---- Trace a function logic level 3. Can be anywhere within the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:T3( Arguments ) - - if debug and _TraceOnOff then - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - if _TraceLevel >= 3 then - self:_T( Arguments, DebugInfoCurrent, DebugInfoFrom ) - end - end -end - ---- Log an exception which will be traced always. Can be anywhere within the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:E( Arguments ) - - if debug then - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - local Function = "function" - if DebugInfoCurrent.name then - Function = DebugInfoCurrent.name - end - - local LineCurrent = DebugInfoCurrent.currentline - local LineFrom = -1 - if DebugInfoFrom then - LineFrom = DebugInfoFrom.currentline - end - - env.info( string.format( "%6d(%6d)/%1s:%20s%05d.%s(%s)" , LineCurrent, LineFrom, "E", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) - end - -end - - - ---- **Core** - SCHEDULER prepares and handles the **execution of functions over scheduled time (intervals)**. --- --- ![Banner Image](..\Presentations\SCHEDULER\Dia1.JPG) --- --- === --- --- # 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 - ---- Clears all pending schedules. --- @param #SCHEDULER self -function SCHEDULER:Clear() - self:F3( ) - - _SCHEDULEDISPATCHER:Clear( self ) -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" } ) -- or {} - - 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] - -- Only start when there is no ScheduleID defined! - -- This prevents to "Start" the scheduler twice with the same CallID... - if not Schedule[CallID].ScheduleID then - Schedule[CallID].ScheduleID = timer.scheduleFunction( - Schedule[CallID].CallHandler, - CallID, - timer.getTime() + Schedule[CallID].Start - ) - end - 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] - -- Only stop when there is a ScheduleID defined for the CallID. - -- So, when the scheduler was stopped before, do nothing. - if Schedule[CallID].ScheduleID then - timer.removeFunction( Schedule[CallID].ScheduleID ) - Schedule[CallID].ScheduleID = nil - end - else - for CallID, Schedule in pairs( self.Schedule[Scheduler] ) do - self:Stop( Scheduler, CallID ) -- Recursive - end - end -end - -function SCHEDULEDISPATCHER:Clear( Scheduler ) - self:F2( { Scheduler = Scheduler } ) - - for CallID, Schedule in pairs( self.Schedule[Scheduler] ) do - self:Stop( Scheduler, CallID ) -- Recursive - end -end - - - ---- **Core** - EVENT models DCS **event dispatching** using a **publish-subscribe** model. --- --- ![Banner Image](..\Presentations\EVENT\Dia1.JPG) --- --- === --- --- # 1) Event Handling Overview --- --- ![Objects](..\Presentations\EVENT\Dia2.JPG) --- --- Within a running mission, various DCS events occur. Units are dynamically created, crash, die, shoot stuff, get hit etc. --- This module provides a mechanism to dispatch those events occuring within your running mission, to the different objects orchestrating your mission. --- --- ![Objects](..\Presentations\EVENT\Dia3.JPG) --- --- Objects can subscribe to different events. The Event dispatcher will publish the received DCS events to the subscribed MOOSE objects, in a specified order. --- In this way, the subscribed MOOSE objects are kept in sync with your evolving running mission. --- --- ## 1.1) Event Dispatching --- --- ![Objects](..\Presentations\EVENT\Dia4.JPG) --- --- The _EVENTDISPATCHER object is automatically created within MOOSE, --- and handles the dispatching of DCS Events occurring --- in the simulator to the subscribed objects --- in the correct processing order. --- --- ![Objects](..\Presentations\EVENT\Dia5.JPG) --- --- There are 5 levels of kind of objects that the _EVENTDISPATCHER services: --- --- * _DATABASE object: The core of the MOOSE objects. Any object that is created, deleted or updated, is done in this database. --- * SET_ derived classes: Subsets of the _DATABASE object. These subsets are updated by the _EVENTDISPATCHER as the second priority. --- * UNIT objects: UNIT objects can subscribe to DCS events. Each DCS event will be directly published to teh subscribed UNIT object. --- * GROUP objects: GROUP objects can subscribe to DCS events. Each DCS event will be directly published to the subscribed GROUP object. --- * Any other object: Various other objects can subscribe to DCS events. Each DCS event triggered will be published to each subscribed object. --- --- ![Objects](..\Presentations\EVENT\Dia6.JPG) --- --- For most DCS events, the above order of updating will be followed. --- --- ![Objects](..\Presentations\EVENT\Dia7.JPG) --- --- But for some DCS events, the publishing order is reversed. This is due to the fact that objects need to be **erased** instead of added. --- --- ## 1.2) Event Handling --- --- ![Objects](..\Presentations\EVENT\Dia8.JPG) --- --- The actual event subscribing and handling is not facilitated through the _EVENTDISPATCHER, but it is done through the @{BASE} class, @{UNIT} class and @{GROUP} class. --- The _EVENTDISPATCHER is a component that is quietly working in the background of MOOSE. --- --- ![Objects](..\Presentations\EVENT\Dia9.JPG) --- --- The BASE class provides methods to catch DCS Events. These are events that are triggered from within the DCS simulator, --- and handled through lua scripting. MOOSE provides an encapsulation to handle these events more efficiently. --- --- ### 1.2.1 Subscribe / Unsubscribe to DCS Events --- --- At first, the mission designer will need to **Subscribe** to a specific DCS event for the class. --- So, when the DCS event occurs, the class will be notified of that event. --- There are two functions which you use to subscribe to or unsubscribe from an event. --- --- * @{Base#BASE.HandleEvent}(): Subscribe to a DCS Event. --- * @{Base#BASE.UnHandleEvent}(): Unsubscribe from a DCS Event. --- --- Note that for a UNIT, the event will be handled **for that UNIT only**! --- Note that for a GROUP, the event will be handled **for all the UNITs in that GROUP only**! --- --- For all objects of other classes, the subscribed events will be handled for **all UNITs within the Mission**! --- So if a UNIT within the mission has the subscribed event for that object, --- then the object event handler will receive the event for that UNIT! --- --- ### 1.3.2 Event Handling of DCS Events --- --- Once the class is subscribed to the event, an **Event Handling** method on the object or class needs to be written that will be called --- when the DCS event occurs. The Event Handling method receives an @{Event#EVENTDATA} structure, which contains a lot of information --- about the event that occurred. --- --- Find below an example of the prototype how to write an event handling function for two units: --- --- local Tank1 = UNIT:FindByName( "Tank A" ) --- local Tank2 = UNIT:FindByName( "Tank B" ) --- --- -- Here we subscribe to the Dead events. So, if one of these tanks dies, the Tank1 or Tank2 objects will be notified. --- Tank1:HandleEvent( EVENTS.Dead ) --- Tank2:HandleEvent( EVENTS.Dead ) --- --- --- This function is an Event Handling function that will be called when Tank1 is Dead. --- -- @param Wrapper.Unit#UNIT self --- -- @param Core.Event#EVENTDATA EventData --- function Tank1:OnEventDead( EventData ) --- --- self:SmokeGreen() --- end --- --- --- This function is an Event Handling function that will be called when Tank2 is Dead. --- -- @param Wrapper.Unit#UNIT self --- -- @param Core.Event#EVENTDATA EventData --- function Tank2:OnEventDead( EventData ) --- --- self:SmokeBlue() --- end --- --- ### 1.3.3 Event Handling methods that are automatically called upon subscribed DCS events --- --- ![Objects](..\Presentations\EVENT\Dia10.JPG) --- --- The following list outlines which EVENTS item in the structure corresponds to which Event Handling method. --- Always ensure that your event handling methods align with the events being subscribed to, or nothing will be executed. --- --- # 2) EVENTS type --- --- The EVENTS structure contains names for all the different DCS events that objects can subscribe to using the --- @{Base#BASE.HandleEvent}() method. --- --- # 3) EVENTDATA type --- --- The @{Event#EVENTDATA} structure contains all the fields that are populated with event information before --- an Event Handler method is being called by the event dispatcher. --- The Event Handler received the EVENTDATA object as a parameter, and can be used to investigate further the different events. --- There are basically 4 main categories of information stored in the EVENTDATA structure: --- --- * Initiator Unit data: Several fields documenting the initiator unit related to the event. --- * Target Unit data: Several fields documenting the target unit related to the event. --- * Weapon data: Certain events populate weapon information. --- * Place data: Certain events populate place information. --- --- --- This function is an Event Handling function that will be called when Tank1 is Dead. --- -- EventData is an EVENTDATA structure. --- -- We use the EventData.IniUnit to smoke the tank Green. --- -- @param Wrapper.Unit#UNIT self --- -- @param Core.Event#EVENTDATA EventData --- function Tank1:OnEventDead( EventData ) --- --- EventData.IniUnit:SmokeGreen() --- end --- --- --- Find below an overview which events populate which information categories: --- --- ![Objects](..\Presentations\EVENT\Dia14.JPG) --- --- **IMPORTANT NOTE:** Some events can involve not just UNIT objects, but also STATIC objects!!! --- In that case the initiator or target unit fields will refer to a STATIC object! --- In case a STATIC object is involved, the documentation indicates which fields will and won't not be populated. --- The fields **IniObjectCategory** and **TgtObjectCategory** contain the indicator which **kind of object is involved** in the event. --- You can use the enumerator **Object.Category.UNIT** and **Object.Category.STATIC** to check on IniObjectCategory and TgtObjectCategory. --- Example code snippet: --- --- if Event.IniObjectCategory == Object.Category.UNIT then --- ... --- end --- if Event.IniObjectCategory == Object.Category.STATIC then --- ... --- end --- --- When a static object is involved in the event, the Group and Player fields won't be populated. --- --- ==== --- --- # **API CHANGE HISTORY** --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- YYYY-MM-DD: CLASS:**NewFunction**( Params ) replaces CLASS:_OldFunction_( Params ) --- YYYY-MM-DD: CLASS:**NewFunction( Params )** added --- --- Hereby the change log: --- --- * 2017-03-07: Added the correct event dispatching in case the event is subscribed by a GROUP. --- --- * 2017-02-07: Did a complete revision of the Event Handing API and underlying mechanisms. --- --- === --- --- # **AUTHORS and CONTRIBUTIONS** --- --- ### Contributions: --- --- ### Authors: --- --- * [**FlightControl**](https://forums.eagle.ru/member.php?u=89536): Design & Programming & documentation. --- --- @module Event - - ---- The EVENT structure --- @type EVENT --- @field #EVENT.Events Events --- @extends Core.Base#BASE -EVENT = { - ClassName = "EVENT", - ClassID = 0, -} - ---- The different types of events supported by MOOSE. --- Use this structure to subscribe to events using the @{Base#BASE.HandleEvent}() method. --- @type EVENTS -EVENTS = { - Shot = world.event.S_EVENT_SHOT, - Hit = world.event.S_EVENT_HIT, - Takeoff = world.event.S_EVENT_TAKEOFF, - Land = world.event.S_EVENT_LAND, - Crash = world.event.S_EVENT_CRASH, - Ejection = world.event.S_EVENT_EJECTION, - Refueling = world.event.S_EVENT_REFUELING, - Dead = world.event.S_EVENT_DEAD, - PilotDead = world.event.S_EVENT_PILOT_DEAD, - BaseCaptured = world.event.S_EVENT_BASE_CAPTURED, - MissionStart = world.event.S_EVENT_MISSION_START, - MissionEnd = world.event.S_EVENT_MISSION_END, - TookControl = world.event.S_EVENT_TOOK_CONTROL, - RefuelingStop = world.event.S_EVENT_REFUELING_STOP, - Birth = world.event.S_EVENT_BIRTH, - HumanFailure = world.event.S_EVENT_HUMAN_FAILURE, - EngineStartup = world.event.S_EVENT_ENGINE_STARTUP, - EngineShutdown = world.event.S_EVENT_ENGINE_SHUTDOWN, - PlayerEnterUnit = world.event.S_EVENT_PLAYER_ENTER_UNIT, - PlayerLeaveUnit = world.event.S_EVENT_PLAYER_LEAVE_UNIT, - PlayerComment = world.event.S_EVENT_PLAYER_COMMENT, - ShootingStart = world.event.S_EVENT_SHOOTING_START, - ShootingEnd = world.event.S_EVENT_SHOOTING_END, -} - ---- The Event structure --- Note that at the beginning of each field description, there is an indication which field will be populated depending on the object type involved in the Event: --- --- * A (Object.Category.)UNIT : A UNIT object type is involved in the Event. --- * A (Object.Category.)STATIC : A STATIC object type is involved in the Event.µ --- --- @type EVENTDATA --- @field #number id The identifier of the event. --- --- @field Dcs.DCSUnit#Unit initiator (UNIT/STATIC/SCENERY) The initiating @{Dcs.DCSUnit#Unit} or @{Dcs.DCSStaticObject#StaticObject}. --- @field Dcs.DCSObject#Object.Category IniObjectCategory (UNIT/STATIC/SCENERY) The initiator object category ( Object.Category.UNIT or Object.Category.STATIC ). --- @field Dcs.DCSUnit#Unit IniDCSUnit (UNIT/STATIC) The initiating @{DCSUnit#Unit} or @{DCSStaticObject#StaticObject}. --- @field #string IniDCSUnitName (UNIT/STATIC) The initiating Unit name. --- @field Wrapper.Unit#UNIT IniUnit (UNIT/STATIC) The initiating MOOSE wrapper @{Unit#UNIT} of the initiator Unit object. --- @field #string IniUnitName (UNIT/STATIC) The initiating UNIT name (same as IniDCSUnitName). --- @field Dcs.DCSGroup#Group IniDCSGroup (UNIT) The initiating {DCSGroup#Group}. --- @field #string IniDCSGroupName (UNIT) The initiating Group name. --- @field Wrapper.Group#GROUP IniGroup (UNIT) The initiating MOOSE wrapper @{Group#GROUP} of the initiator Group object. --- @field #string IniGroupName UNIT) The initiating GROUP name (same as IniDCSGroupName). --- @field #string IniPlayerName (UNIT) The name of the initiating player in case the Unit is a client or player slot. --- @field Dcs.DCScoalition#coalition.side IniCoalition (UNIT) The coalition of the initiator. --- @field Dcs.DCSUnit#Unit.Category IniCategory (UNIT) The category of the initiator. --- @field #string IniTypeName (UNIT) The type name of the initiator. --- --- @field Dcs.DCSUnit#Unit target (UNIT/STATIC) The target @{Dcs.DCSUnit#Unit} or @{DCSStaticObject#StaticObject}. --- @field Dcs.DCSObject#Object.Category TgtObjectCategory (UNIT/STATIC) The target object category ( Object.Category.UNIT or Object.Category.STATIC ). --- @field Dcs.DCSUnit#Unit TgtDCSUnit (UNIT/STATIC) The target @{DCSUnit#Unit} or @{DCSStaticObject#StaticObject}. --- @field #string TgtDCSUnitName (UNIT/STATIC) The target Unit name. --- @field Wrapper.Unit#UNIT TgtUnit (UNIT/STATIC) The target MOOSE wrapper @{Unit#UNIT} of the target Unit object. --- @field #string TgtUnitName (UNIT/STATIC) The target UNIT name (same as TgtDCSUnitName). --- @field Dcs.DCSGroup#Group TgtDCSGroup (UNIT) The target {DCSGroup#Group}. --- @field #string TgtDCSGroupName (UNIT) The target Group name. --- @field Wrapper.Group#GROUP TgtGroup (UNIT) The target MOOSE wrapper @{Group#GROUP} of the target Group object. --- @field #string TgtGroupName (UNIT) The target GROUP name (same as TgtDCSGroupName). --- @field #string TgtPlayerName (UNIT) The name of the target player in case the Unit is a client or player slot. --- @field Dcs.DCScoalition#coalition.side TgtCoalition (UNIT) The coalition of the target. --- @field Dcs.DCSUnit#Unit.Category TgtCategory (UNIT) The category of the target. --- @field #string TgtTypeName (UNIT) The type name of the target. --- --- @field weapon The weapon used during the event. --- @field Weapon --- @field WeaponName --- @field WeaponTgtDCSUnit - - -local _EVENTMETA = { - [world.event.S_EVENT_SHOT] = { - Order = 1, - Event = "OnEventShot", - Text = "S_EVENT_SHOT" - }, - [world.event.S_EVENT_HIT] = { - Order = 1, - Event = "OnEventHit", - Text = "S_EVENT_HIT" - }, - [world.event.S_EVENT_TAKEOFF] = { - Order = 1, - Event = "OnEventTakeoff", - Text = "S_EVENT_TAKEOFF" - }, - [world.event.S_EVENT_LAND] = { - Order = 1, - Event = "OnEventLand", - Text = "S_EVENT_LAND" - }, - [world.event.S_EVENT_CRASH] = { - Order = -1, - Event = "OnEventCrash", - Text = "S_EVENT_CRASH" - }, - [world.event.S_EVENT_EJECTION] = { - Order = 1, - Event = "OnEventEjection", - Text = "S_EVENT_EJECTION" - }, - [world.event.S_EVENT_REFUELING] = { - Order = 1, - Event = "OnEventRefueling", - Text = "S_EVENT_REFUELING" - }, - [world.event.S_EVENT_DEAD] = { - Order = -1, - Event = "OnEventDead", - Text = "S_EVENT_DEAD" - }, - [world.event.S_EVENT_PILOT_DEAD] = { - Order = 1, - Event = "OnEventPilotDead", - Text = "S_EVENT_PILOT_DEAD" - }, - [world.event.S_EVENT_BASE_CAPTURED] = { - Order = 1, - Event = "OnEventBaseCaptured", - Text = "S_EVENT_BASE_CAPTURED" - }, - [world.event.S_EVENT_MISSION_START] = { - Order = 1, - Event = "OnEventMissionStart", - Text = "S_EVENT_MISSION_START" - }, - [world.event.S_EVENT_MISSION_END] = { - Order = 1, - Event = "OnEventMissionEnd", - Text = "S_EVENT_MISSION_END" - }, - [world.event.S_EVENT_TOOK_CONTROL] = { - Order = 1, - Event = "OnEventTookControl", - Text = "S_EVENT_TOOK_CONTROL" - }, - [world.event.S_EVENT_REFUELING_STOP] = { - Order = 1, - Event = "OnEventRefuelingStop", - Text = "S_EVENT_REFUELING_STOP" - }, - [world.event.S_EVENT_BIRTH] = { - Order = 1, - Event = "OnEventBirth", - Text = "S_EVENT_BIRTH" - }, - [world.event.S_EVENT_HUMAN_FAILURE] = { - Order = 1, - Event = "OnEventHumanFailure", - Text = "S_EVENT_HUMAN_FAILURE" - }, - [world.event.S_EVENT_ENGINE_STARTUP] = { - Order = 1, - Event = "OnEventEngineStartup", - Text = "S_EVENT_ENGINE_STARTUP" - }, - [world.event.S_EVENT_ENGINE_SHUTDOWN] = { - Order = 1, - Event = "OnEventEngineShutdown", - Text = "S_EVENT_ENGINE_SHUTDOWN" - }, - [world.event.S_EVENT_PLAYER_ENTER_UNIT] = { - Order = 1, - Event = "OnEventPlayerEnterUnit", - Text = "S_EVENT_PLAYER_ENTER_UNIT" - }, - [world.event.S_EVENT_PLAYER_LEAVE_UNIT] = { - Order = -1, - Event = "OnEventPlayerLeaveUnit", - Text = "S_EVENT_PLAYER_LEAVE_UNIT" - }, - [world.event.S_EVENT_PLAYER_COMMENT] = { - Order = 1, - Event = "OnEventPlayerComment", - Text = "S_EVENT_PLAYER_COMMENT" - }, - [world.event.S_EVENT_SHOOTING_START] = { - Order = 1, - Event = "OnEventShootingStart", - Text = "S_EVENT_SHOOTING_START" - }, - [world.event.S_EVENT_SHOOTING_END] = { - Order = 1, - Event = "OnEventShootingEnd", - Text = "S_EVENT_SHOOTING_END" - }, -} - - ---- The Events structure --- @type EVENT.Events --- @field #number IniUnit - -function EVENT:New() - local self = BASE:Inherit( self, BASE:New() ) - self:F2() - self.EventHandler = world.addEventHandler( self ) - return self -end - -function EVENT:EventText( EventID ) - - local EventText = _EVENTMETA[EventID].Text - - return EventText -end - - ---- Initializes the Events structure for the event --- @param #EVENT self --- @param Dcs.DCSWorld#world.event EventID --- @param Core.Base#BASE EventClass --- @return #EVENT.Events -function EVENT:Init( EventID, EventClass ) - self:F3( { _EVENTMETA[EventID].Text, EventClass } ) - - if not self.Events[EventID] then - -- Create a WEAK table to ensure that the garbage collector is cleaning the event links when the object usage is cleaned. - self.Events[EventID] = setmetatable( {}, { __mode = "k" } ) - end - - -- Each event has a subtable of EventClasses, ordered by EventPriority. - local EventPriority = EventClass:GetEventPriority() - if not self.Events[EventID][EventPriority] then - self.Events[EventID][EventPriority] = setmetatable( {}, { __mode = "k" } ) - end - - if not self.Events[EventID][EventPriority][EventClass] then - self.Events[EventID][EventPriority][EventClass] = setmetatable( {}, { __mode = "v" } ) - end - return self.Events[EventID][EventPriority][EventClass] -end - ---- Removes an Events entry --- @param #EVENT self --- @param Core.Base#BASE EventClass The self instance of the class for which the event is. --- @param Dcs.DCSWorld#world.event EventID --- @return #EVENT.Events -function EVENT:Remove( EventClass, EventID ) - self:F3( { EventClass, _EVENTMETA[EventID].Text } ) - - local EventClass = EventClass - local EventPriority = EventClass:GetEventPriority() - self.Events[EventID][EventPriority][EventClass] = nil -end - ---- Removes an Events entry for a UNIT. --- @param #EVENT self --- @param #string UnitName The name of the UNIT. --- @param Core.Base#BASE EventClass The self instance of the class for which the event is. --- @param Dcs.DCSWorld#world.event EventID --- @return #EVENT.Events -function EVENT:RemoveForUnit( UnitName, EventClass, EventID ) - self:F3( { EventClass, _EVENTMETA[EventID].Text } ) - - local EventClass = EventClass - local EventPriority = EventClass:GetEventPriority() - local Event = self.Events[EventID][EventPriority][EventClass] - Event.EventUnit[UnitName] = nil -end - ---- Removes an Events entry for a GROUP. --- @param #EVENT self --- @param #string GroupName The name of the GROUP. --- @param Core.Base#BASE EventClass The self instance of the class for which the event is. --- @param Dcs.DCSWorld#world.event EventID --- @return #EVENT.Events -function EVENT:RemoveForGroup( GroupName, EventClass, EventID ) - self:F3( { EventClass, _EVENTMETA[EventID].Text } ) - - local EventClass = EventClass - local EventPriority = EventClass:GetEventPriority() - local Event = self.Events[EventID][EventPriority][EventClass] - Event.EventGroup[GroupName] = nil -end - ---- Clears all event subscriptions for a @{Base#BASE} derived object. --- @param #EVENT self --- @param Core.Base#BASE EventObject -function EVENT:RemoveAll( EventObject ) - self:F3( { EventObject:GetClassNameAndID() } ) - - local EventClass = EventObject:GetClassNameAndID() - local EventPriority = EventClass:GetEventPriority() - for EventID, EventData in pairs( self.Events ) do - self.Events[EventID][EventPriority][EventClass] = nil - end -end - - - ---- Create an OnDead event handler for a group --- @param #EVENT self --- @param #table EventTemplate --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param EventClass The instance of the class for which the event is. --- @param #function OnEventFunction --- @return #EVENT -function EVENT:OnEventForTemplate( EventTemplate, EventFunction, EventClass, EventID ) - self:F2( EventTemplate.name ) - - for EventUnitID, EventUnit in pairs( EventTemplate.units ) do - self:OnEventForUnit( EventUnit.name, EventFunction, EventClass, EventID ) - 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 EventData = self:Init( EventID, EventClass ) - EventData.EventFunction = EventFunction - EventData.EventClass = EventClass - - return self -end - - ---- Set a new listener for an S_EVENT_X event for a UNIT. --- @param #EVENT self --- @param #string UnitName The name of the UNIT. --- @param #function EventFunction The function to be called when the event occurs for the GROUP. --- @param Core.Base#BASE EventClass The self instance of the class for which the event is. --- @param EventID --- @return #EVENT -function EVENT:OnEventForUnit( UnitName, EventFunction, EventClass, EventID ) - self:F2( UnitName ) - - local EventData = self:Init( EventID, EventClass ) - if not EventData.EventUnit then - EventData.EventUnit = {} - end - EventData.EventUnit[UnitName] = {} - EventData.EventUnit[UnitName].EventFunction = EventFunction - EventData.EventUnit[UnitName].EventClass = EventClass - return self -end - ---- Set a new listener for an S_EVENT_X event for a GROUP. --- @param #EVENT self --- @param #string GroupName The name of the GROUP. --- @param #function EventFunction The function to be called when the event occurs for the GROUP. --- @param Core.Base#BASE EventClass The self instance of the class for which the event is. --- @param EventID --- @return #EVENT -function EVENT:OnEventForGroup( GroupName, EventFunction, EventClass, EventID ) - self:F2( GroupName ) - - local Event = self:Init( EventID, EventClass ) - if not Event.EventGroup then - Event.EventGroup = {} - end - Event.EventGroup[GroupName] = {} - Event.EventGroup[GroupName].EventFunction = EventFunction - Event.EventGroup[GroupName].EventClass = EventClass - return self -end - -do -- OnBirth - - --- Create an OnBirth event handler for a group - -- @param #EVENT self - -- @param Wrapper.Group#GROUP EventGroup - -- @param #function EventFunction The function to be called when the event occurs for the unit. - -- @param EventClass The self instance of the class for which the event is. - -- @return #EVENT - function EVENT:OnBirthForTemplate( EventTemplate, EventFunction, EventClass ) - self:F2( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, EVENTS.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, EVENTS.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, EVENTS.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, EVENTS.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, EVENTS.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, EVENTS.EngineShutdown ) - - 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 - - self:E( _EVENTMETA[Event.id].Text, Event ) - - if self and self.Events and self.Events[Event.id] then - - - if Event.initiator then - - Event.IniObjectCategory = Event.initiator:getCategory() - - if Event.IniObjectCategory == Object.Category.UNIT then - Event.IniDCSUnit = Event.initiator - Event.IniDCSUnitName = Event.IniDCSUnit:getName() - Event.IniUnitName = Event.IniDCSUnitName - Event.IniDCSGroup = Event.IniDCSUnit:getGroup() - Event.IniUnit = UNIT:FindByName( Event.IniDCSUnitName ) - if not Event.IniUnit then - -- Unit can be a CLIENT. Most likely this will be the case ... - Event.IniUnit = CLIENT:FindByName( Event.IniDCSUnitName, '', true ) - end - Event.IniDCSGroupName = "" - if Event.IniDCSGroup and Event.IniDCSGroup:isExist() then - Event.IniDCSGroupName = Event.IniDCSGroup:getName() - Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) - if Event.IniGroup then - Event.IniGroupName = Event.IniDCSGroupName - end - end - Event.IniPlayerName = Event.IniDCSUnit:getPlayerName() - Event.IniCoalition = Event.IniDCSUnit:getCoalition() - Event.IniTypeName = Event.IniDCSUnit:getTypeName() - Event.IniCategory = Event.IniDCSUnit:getDesc().category - end - - if Event.IniObjectCategory == Object.Category.STATIC then - Event.IniDCSUnit = Event.initiator - Event.IniDCSUnitName = Event.IniDCSUnit:getName() - Event.IniUnitName = Event.IniDCSUnitName - Event.IniUnit = STATIC:FindByName( Event.IniDCSUnitName, false ) - Event.IniCoalition = Event.IniDCSUnit:getCoalition() - Event.IniCategory = Event.IniDCSUnit:getDesc().category - Event.IniTypeName = Event.IniDCSUnit:getTypeName() - end - - if Event.IniObjectCategory == Object.Category.SCENERY then - Event.IniDCSUnit = Event.initiator - Event.IniDCSUnitName = Event.IniDCSUnit:getName() - Event.IniUnitName = Event.IniDCSUnitName - Event.IniUnit = SCENERY:Register( Event.IniDCSUnitName, Event.initiator ) - Event.IniCategory = Event.IniDCSUnit:getDesc().category - Event.IniTypeName = Event.IniDCSUnit:getTypeName() - end - end - - if Event.target then - - Event.TgtObjectCategory = Event.target:getCategory() - - if Event.TgtObjectCategory == Object.Category.UNIT then - Event.TgtDCSUnit = Event.target - Event.TgtDCSGroup = Event.TgtDCSUnit:getGroup() - Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() - Event.TgtUnitName = Event.TgtDCSUnitName - Event.TgtUnit = UNIT:FindByName( Event.TgtDCSUnitName ) - Event.TgtDCSGroupName = "" - if Event.TgtDCSGroup and Event.TgtDCSGroup:isExist() then - Event.TgtDCSGroupName = Event.TgtDCSGroup:getName() - Event.TgtGroup = GROUP:FindByName( Event.TgtDCSGroupName ) - if Event.TgtGroup then - Event.TgtGroupName = Event.TgtDCSGroupName - end - end - Event.TgtPlayerName = Event.TgtDCSUnit:getPlayerName() - Event.TgtCoalition = Event.TgtDCSUnit:getCoalition() - Event.TgtCategory = Event.TgtDCSUnit:getDesc().category - Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() - end - - if Event.TgtObjectCategory == Object.Category.STATIC then - Event.TgtDCSUnit = Event.target - Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() - Event.TgtUnitName = Event.TgtDCSUnitName - Event.TgtUnit = STATIC:FindByName( Event.TgtDCSUnitName ) - Event.TgtCoalition = Event.TgtDCSUnit:getCoalition() - Event.TgtCategory = Event.TgtDCSUnit:getDesc().category - Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() - end - - if Event.TgtObjectCategory == Object.Category.SCENERY then - Event.TgtDCSUnit = Event.target - Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() - Event.TgtUnitName = Event.TgtDCSUnitName - Event.TgtUnit = SCENERY:Register( Event.TgtDCSUnitName, Event.target ) - Event.TgtCategory = Event.TgtDCSUnit:getDesc().category - Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() - end - end - - if Event.weapon then - Event.Weapon = Event.weapon - Event.WeaponName = Event.Weapon:getTypeName() - Event.WeaponUNIT = CLIENT:Find( Event.Weapon, '', true ) -- Sometimes, the weapon is a player unit! - Event.WeaponPlayerName = Event.WeaponUNIT and Event.Weapon:getPlayerName() - Event.WeaponCoalition = Event.WeaponUNIT and Event.Weapon:getCoalition() - Event.WeaponCategory = Event.WeaponUNIT and Event.Weapon:getDesc().category - Event.WeaponTypeName = Event.WeaponUNIT and Event.Weapon:getTypeName() - --Event.WeaponTgtDCSUnit = Event.Weapon:getTarget() - end - - local PriorityOrder = _EVENTMETA[Event.id].Order - local PriorityBegin = PriorityOrder == -1 and 5 or 1 - local PriorityEnd = PriorityOrder == -1 and 1 or 5 - - if Event.IniObjectCategory ~= 3 then - self:E( { _EVENTMETA[Event.id].Text, Event, Event.IniDCSUnitName, Event.TgtDCSUnitName, PriorityOrder } ) - end - - for EventPriority = PriorityBegin, PriorityEnd, PriorityOrder do - - if self.Events[Event.id][EventPriority] then - - -- Okay, we got the event from DCS. Now loop the SORTED self.EventSorted[] table for the received Event.id, and for each EventData registered, check if a function needs to be called. - for EventClass, EventData in pairs( self.Events[Event.id][EventPriority] ) do - - Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) - Event.TgtGroup = GROUP:FindByName( Event.TgtDCSGroupName ) - - -- If the EventData is for a UNIT, the call directly the EventClass EventFunction for that UNIT. - if ( Event.IniDCSUnitName and EventData.EventUnit and EventData.EventUnit[Event.IniDCSUnitName] ) or - ( Event.TgtDCSUnitName and EventData.EventUnit and EventData.EventUnit[Event.TgtDCSUnitName] ) then - - if EventData.EventUnit[Event.IniDCSUnitName] then - - -- First test if a EventFunction is Set, otherwise search for the default function - if EventData.EventUnit[Event.IniDCSUnitName].EventFunction then - - if Event.IniObjectCategory ~= 3 then - self:E( { "Calling EventFunction for UNIT ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName, EventPriority } ) - end - - local Result, Value = xpcall( - function() - return EventData.EventUnit[Event.IniDCSUnitName].EventFunction( EventClass, Event ) - end, ErrorHandler ) - - else - - -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. - local EventFunction = EventClass[ _EVENTMETA[Event.id].Event ] - if EventFunction and type( EventFunction ) == "function" then - - -- Now call the default event function. - if Event.IniObjectCategory ~= 3 then - self:E( { "Calling " .. _EVENTMETA[Event.id].Event .. " for Class ", EventClass:GetClassNameAndID(), EventPriority } ) - end - - local Result, Value = xpcall( - function() - return EventFunction( EventClass, Event ) - end, ErrorHandler ) - end - end - end - - if EventData.EventUnit[Event.TgtDCSUnitName] then - - -- First test if a EventFunction is Set, otherwise search for the default function - if EventData.EventUnit[Event.TgtDCSUnitName].EventFunction then - - if Event.IniObjectCategory ~= 3 then - self:E( { "Calling EventFunction for UNIT ", EventClass:GetClassNameAndID(), ", Unit ", Event.TgtUnitName, EventPriority } ) - end - - local Result, Value = xpcall( - function() - return EventData.EventUnit[Event.TgtDCSUnitName].EventFunction( EventClass, Event ) - end, ErrorHandler ) - - else - - -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. - local EventFunction = EventClass[ _EVENTMETA[Event.id].Event ] - if EventFunction and type( EventFunction ) == "function" then - - -- Now call the default event function. - if Event.IniObjectCategory ~= 3 then - self:E( { "Calling " .. _EVENTMETA[Event.id].Event .. " for Class ", EventClass:GetClassNameAndID(), EventPriority } ) - end - - local Result, Value = xpcall( - function() - return EventFunction( EventClass, Event ) - end, ErrorHandler ) - end - end - end - - else - - -- If the EventData is for a GROUP, the call directly the EventClass EventFunction for the UNIT in that GROUP. - if ( Event.IniDCSUnitName and Event.IniDCSGroupName and Event.IniGroupName and EventData.EventGroup and EventData.EventGroup[Event.IniGroupName] ) or - ( Event.TgtDCSUnitName and Event.TgtDCSGroupName and Event.TgtGroupName and EventData.EventGroup and EventData.EventGroup[Event.TgtGroupName] ) then - - if EventData.EventGroup[Event.IniGroupName] then - -- First test if a EventFunction is Set, otherwise search for the default function - if EventData.EventGroup[Event.IniGroupName].EventFunction then - - if Event.IniObjectCategory ~= 3 then - self:E( { "Calling EventFunction for GROUP ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName, EventPriority } ) - end - - local Result, Value = xpcall( - function() - return EventData.EventGroup[Event.IniGroupName].EventFunction( EventClass, Event ) - end, ErrorHandler ) - - else - - -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. - local EventFunction = EventClass[ _EVENTMETA[Event.id].Event ] - if EventFunction and type( EventFunction ) == "function" then - - -- Now call the default event function. - if Event.IniObjectCategory ~= 3 then - self:E( { "Calling " .. _EVENTMETA[Event.id].Event .. " for GROUP ", EventClass:GetClassNameAndID(), EventPriority } ) - end - - local Result, Value = xpcall( - function() - return EventFunction( EventClass, Event ) - end, ErrorHandler ) - end - end - end - - if EventData.EventGroup[Event.TgtGroupName] then - if EventData.EventGroup[Event.TgtGroupName].EventFunction then - - if Event.IniObjectCategory ~= 3 then - self:E( { "Calling EventFunction for GROUP ", EventClass:GetClassNameAndID(), ", Unit ", Event.TgtUnitName, EventPriority } ) - end - - local Result, Value = xpcall( - function() - return EventData.EventGroup[Event.TgtGroupName].EventFunction( EventClass, Event ) - end, ErrorHandler ) - - else - - -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. - local EventFunction = EventClass[ _EVENTMETA[Event.id].Event ] - if EventFunction and type( EventFunction ) == "function" then - - -- Now call the default event function. - if Event.IniObjectCategory ~= 3 then - self:E( { "Calling " .. _EVENTMETA[Event.id].Event .. " for GROUP ", EventClass:GetClassNameAndID(), EventPriority } ) - end - - local Result, Value = xpcall( - function() - return EventFunction( EventClass, Event ) - end, ErrorHandler ) - end - end - end - - else - - -- If the EventData is not bound to a specific unit, then call the EventClass EventFunction. - -- Note that here the EventFunction will need to implement and determine the logic for the relevant source- or target unit, or weapon. - if (Event.IniDCSUnit or Event.WeaponUNIT) and not EventData.EventUnit then - - if EventClass == EventData.EventClass then - - -- First test if a EventFunction is Set, otherwise search for the default function - if EventData.EventFunction then - - -- There is an EventFunction defined, so call the EventFunction. - if Event.IniObjectCategory ~= 3 then - self:E( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID(), EventPriority } ) - end - local Result, Value = xpcall( - function() - return EventData.EventFunction( EventClass, Event ) - end, ErrorHandler ) - else - - -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. - local EventFunction = EventClass[ _EVENTMETA[Event.id].Event ] - if EventFunction and type( EventFunction ) == "function" then - - -- Now call the default event function. - if Event.IniObjectCategory ~= 3 then - self:E( { "Calling " .. _EVENTMETA[Event.id].Event .. " for Class ", EventClass:GetClassNameAndID(), EventPriority } ) - end - - local Result, Value = xpcall( - function() - local Result, Value = EventFunction( EventClass, Event ) - return Result, Value - end, ErrorHandler ) - end - end - end - end - end - end - end - end - end - else - self:E( { _EVENTMETA[Event.id].Text, Event } ) - end - - Event = nil -end - ---- The EVENTHANDLER structure --- @type EVENTHANDLER --- @extends Core.Base#BASE -EVENTHANDLER = { - ClassName = "EVENTHANDLER", - ClassID = 0, -} - ---- The EVENTHANDLER constructor --- @param #EVENTHANDLER self --- @return #EVENTHANDLER -function EVENTHANDLER:New() - self = BASE:Inherit( self, BASE:New() ) -- #EVENTHANDLER - return self -end ---- **Core** -- MENU_ classes model the definition of **hierarchical menu structures** and **commands for players** within a mission. --- --- === --- --- 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 - -- @param #MENU_BASE - -- @return #MENU_BASE - 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 - self.Menus = {} - self.MenuCount = 0 - self.MenuRemoveParent = false - self.MenuTime = timer.getTime() - - return self - end - - --- Gets a @{Menu} from a parent @{Menu} - -- @param #MENU_BASE self - -- @param #string MenuText The text of the child menu. - -- @return #MENU_BASE - function MENU_BASE:GetMenu( MenuText ) - self:F( { self.Menus, MenuText } ) - return self.Menus[MenuText] - end - - --- Sets a @{Menu} to remove automatically the parent menu when the menu removed is the last child menu of that parent @{Menu}. - -- @param #MENU_BASE self - -- @param #boolean RemoveParent If true, the parent menu is automatically removed when this menu is the last child menu of that parent @{Menu}. - -- @return #MENU_BASE - function MENU_BASE:SetRemoveParent( RemoveParent ) - self:F( { RemoveParent } ) - self.MenuRemoveParent = RemoveParent - return self - end - - - --- Sets a time stamp for later prevention of menu removal. - -- @param #MENU_BASE self - -- @param MenuTime - -- @return #MENU_BASE - function MENU_BASE:SetTime( MenuTime ) - self.MenuTime = MenuTime - return self - end - -end - -do -- MENU_COMMAND_BASE - - --- The MENU_COMMAND_BASE class - -- @type MENU_COMMAND_BASE - -- @field #function MenuCallHandler - -- @extends Core.Menu#MENU_BASE - MENU_COMMAND_BASE = { - ClassName = "MENU_COMMAND_BASE", - CommandMenuFunction = nil, - CommandMenuArgument = nil, - MenuCallHandler = nil, - } - - --- Constructor - -- @param #MENU_COMMAND_BASE - -- @return #MENU_COMMAND_BASE - 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 Core.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 - 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 - 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 Core.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 Core.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 - 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 Core.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 - 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 Core.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 Core.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 Core.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.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 self.ParentMenu and self.ParentMenu.Menus then - self.ParentMenu.Menus[MenuText] = self - self:F( { self.ParentMenu.Menus, MenuText } ) - self.ParentMenu.MenuCount = self.ParentMenu.MenuCount + 1 - 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 - -- @param MenuTime - -- @return #MENU_GROUP self - function MENU_GROUP:RemoveSubMenus( MenuTime ) - self:F2( { self.MenuPath, MenuTime, self.MenuTime } ) - - self:T( { "Removing Group SubMenus:", self.MenuGroup:GetName(), self.MenuPath } ) - for MenuText, Menu in pairs( self.Menus ) do - Menu:Remove( MenuTime ) - end - - end - - - --- Removes the main menu and sub menus recursively of this MENU_GROUP. - -- @param #MENU_GROUP self - -- @param MenuTime - -- @return #nil - function MENU_GROUP:Remove( MenuTime ) - self:F( { self.MenuGroupID, self.MenuPath, MenuTime, self.MenuTime } ) - - self:RemoveSubMenus( MenuTime ) - - if not MenuTime or self.MenuTime ~= MenuTime then - 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.MenuText] = nil - self.ParentMenu.MenuCount = self.ParentMenu.MenuCount - 1 - if self.ParentMenu.MenuCount == 0 then - if self.MenuRemoveParent == true then - self:T( "Removing Parent Menu " ) - self.ParentMenu:Remove() - end - end - end - self:T( { "Removing Group Menu:", self.MenuGroup:GetName(), self.MenuGroup._Menus[self.Path].Path } ) - self.MenuGroup._Menus[self.Path] = nil - self = nil - end - end - - return nil - end - - - --- The MENU_GROUP_COMMAND class - -- @type MENU_GROUP_COMMAND - -- @extends Core.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_GROUP_COMMAND - 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] - self:T( { "Re-using Group Command Menu:", MenuGroup:GetName(), MenuText } ) - 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 Group Command Menu:", MenuGroup:GetName(), MenuText, self.MenuParentPath } ) - self.MenuPath = missionCommands.addCommandForGroup( self.MenuGroupID, MenuText, self.MenuParentPath, self.MenuCallHandler, arg ) - - if self.ParentMenu and self.ParentMenu.Menus then - self.ParentMenu.Menus[MenuText] = self - self.ParentMenu.MenuCount = self.ParentMenu.MenuCount + 1 - self:F( { ParentMenu.Menus, MenuText } ) - end - end - - return self - end - - --- Removes a menu structure for a group. - -- @param #MENU_GROUP_COMMAND self - -- @param MenuTime - -- @return #nil - function MENU_GROUP_COMMAND:Remove( MenuTime ) - self:F( { self.MenuGroupID, self.MenuPath, MenuTime, self.MenuTime } ) - - if not MenuTime or self.MenuTime ~= MenuTime then - if self.MenuGroup._Menus[self.Path] then - self = self.MenuGroup._Menus[self.Path] - - missionCommands.removeItemForGroup( self.MenuGroupID, self.MenuPath ) - self:T( { "Removing Group Command Menu:", self.MenuGroup:GetName(), self.MenuText, self.Path, self.MenuGroup._Menus[self.Path].Path } ) - - self.ParentMenu.Menus[self.MenuText] = nil - self.ParentMenu.MenuCount = self.ParentMenu.MenuCount - 1 - if self.ParentMenu.MenuCount == 0 then - if self.MenuRemoveParent == true then - self:T( "Removing Parent Menu " ) - self.ParentMenu:Remove() - end - end - - self.MenuGroup._Menus[self.Path] = nil - self = nil - end - end - - return nil - end - -end - ---- **Core** - ZONE classes define **zones** within your mission of **various forms**, with **various capabilities**. --- --- ![Banner Image](..\Presentations\ZONE\Dia1.JPG) --- --- === --- --- 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_BASE}: The ZONE_BASE class defining the base for all other zone classes. --- * @{#ZONE_RADIUS}: The ZONE_RADIUS class defined by a zone name, a location and a radius. --- * @{#ZONE}: The ZONE class, defined by the zone name as defined within the Mission Editor. --- * @{#ZONE_UNIT}: The ZONE_UNIT class defines by a zone around a @{Unit#UNIT} with a radius. --- * @{#ZONE_GROUP}: The ZONE_GROUP class defines by a zone around a @{Group#GROUP} with a radius. --- * @{#ZONE_POLYGON}: The ZONE_POLYGON class defines by a sequence of @{Group#GROUP} waypoints within the Mission Editor, forming a polygon. --- --- === --- --- # **API CHANGE HISTORY** --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- Hereby the change log: --- --- 2017-02-28: ZONE\_BASE:**IsVec2InZone()** replaces ZONE\_BASE:_IsPointVec2InZone()_. --- 2017-02-28: ZONE\_BASE:**IsVec3InZone()** replaces ZONE\_BASE:_IsPointVec3InZone()_. --- 2017-02-28: ZONE\_RADIUS:**IsVec2InZone()** replaces ZONE\_RADIUS:_IsPointVec2InZone()_. --- 2017-02-28: ZONE\_RADIUS:**IsVec3InZone()** replaces ZONE\_RADIUS:_IsPointVec3InZone()_. --- 2017-02-28: ZONE\_POLYGON:**IsVec2InZone()** replaces ZONE\_POLYGON:_IsPointVec2InZone()_. --- 2017-02-28: ZONE\_POLYGON:**IsVec3InZone()** replaces ZONE\_POLYGON:_IsPointVec3InZone()_. --- --- 2017-02-18: ZONE\_POLYGON_BASE:**GetRandomPointVec2()** added. --- --- 2017-02-18: ZONE\_POLYGON_BASE:**GetRandomPointVec3()** added. --- --- 2017-02-18: ZONE\_RADIUS:**GetRandomPointVec3( inner, outer )** added. --- --- 2017-02-18: ZONE\_RADIUS:**GetRandomPointVec2( inner, outer )** added. --- --- 2016-08-15: ZONE\_BASE:**GetName()** added. --- --- 2016-08-15: ZONE\_BASE:**SetZoneProbability( ZoneProbability )** added. --- --- 2016-08-15: ZONE\_BASE:**GetZoneProbability()** added. --- --- 2016-08-15: ZONE\_BASE:**GetZoneMaybe()** added. --- --- === --- --- @module Zone - - ---- The ZONE_BASE class --- @type ZONE_BASE --- @field #string ZoneName Name of the zone. --- @field #number ZoneProbability A value between 0 and 1. 0 = 0% and 1 = 100% probability. --- @extends Core.Base#BASE - - ---- # 1) ZONE_BASE class, extends @{Base#BASE} --- --- This class is an abstract BASE class for derived classes, and is not meant to be instantiated. --- --- ## 1.1) Each zone has a name: --- --- * @{#ZONE_BASE.GetName}(): Returns the name of the zone. --- --- ## 1.2) Each zone implements two polymorphic functions defined in @{Zone#ZONE_BASE}: --- --- * @{#ZONE_BASE.IsVec2InZone}(): Returns if a Vec2 is within the zone. --- * @{#ZONE_BASE.IsVec3InZone}(): Returns if a Vec3 is within the zone. --- --- ## 1.3) A zone has a probability factor that can be set to randomize a selection between zones: --- --- * @{#ZONE_BASE.SetRandomizeProbability}(): Set the randomization probability of a zone to be selected, taking a value between 0 and 1 ( 0 = 0%, 1 = 100% ) --- * @{#ZONE_BASE.GetRandomizeProbability}(): Get the randomization probability of a zone to be selected, passing a value between 0 and 1 ( 0 = 0%, 1 = 100% ) --- * @{#ZONE_BASE.GetZoneMaybe}(): Get the zone taking into account the randomization probability. nil is returned if this zone is not a candidate. --- --- ## 1.4) A zone manages Vectors: --- --- * @{#ZONE_BASE.GetVec2}(): Returns the @{DCSTypes#Vec2} coordinate of the zone. --- * @{#ZONE_BASE.GetRandomVec2}(): Define a random @{DCSTypes#Vec2} within the zone. --- --- ## 1.5) A zone has a bounding square: --- --- * @{#ZONE_BASE.GetBoundingSquare}(): Get the outer most bounding square of the zone. --- --- ## 1.6) A zone can be marked: --- --- * @{#ZONE_BASE.SmokeZone}(): Smokes the zone boundaries in a color. --- * @{#ZONE_BASE.FlareZone}(): Flares the zone boundaries in a color. --- --- === --- @field #ZONE_BASE ZONE_BASE -ZONE_BASE = { - ClassName = "ZONE_BASE", - ZoneName = "", - ZoneProbability = 1, - } - - ---- The ZONE_BASE.BoundingSquare --- @type ZONE_BASE.BoundingSquare --- @field Dcs.DCSTypes#Distance x1 The lower x coordinate (left down) --- @field Dcs.DCSTypes#Distance y1 The lower y coordinate (left down) --- @field Dcs.DCSTypes#Distance x2 The higher x coordinate (right up) --- @field Dcs.DCSTypes#Distance y2 The higher y coordinate (right up) - - ---- ZONE_BASE constructor --- @param #ZONE_BASE self --- @param #string ZoneName Name of the zone. --- @return #ZONE_BASE self -function ZONE_BASE:New( ZoneName ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( ZoneName ) - - self.ZoneName = ZoneName - - return self -end - ---- Returns the name of the zone. --- @param #ZONE_BASE self --- @return #string The name of the zone. -function ZONE_BASE:GetName() - self:F2() - - return self.ZoneName -end ---- Returns if a location is within the zone. --- @param #ZONE_BASE self --- @param Dcs.DCSTypes#Vec2 Vec2 The location to test. --- @return #boolean true if the location is within the zone. -function ZONE_BASE:IsVec2InZone( Vec2 ) - self:F2( Vec2 ) - - return false -end - ---- Returns if a point is within the zone. --- @param #ZONE_BASE self --- @param Dcs.DCSTypes#Vec3 Vec3 The point to test. --- @return #boolean true if the point is within the zone. -function ZONE_BASE:IsVec3InZone( Vec3 ) - self:F2( Vec3 ) - - local InZone = self:IsVec2InZone( { x = Vec3.x, y = Vec3.z } ) - - return InZone -end - ---- Returns the @{DCSTypes#Vec2} coordinate of the zone. --- @param #ZONE_BASE self --- @return #nil. -function ZONE_BASE:GetVec2() - self:F2( self.ZoneName ) - - return nil -end - ---- Returns a @{Point#POINT_VEC2} of the zone. --- @param #ZONE_BASE self --- @param Dcs.DCSTypes#Distance Height The height to add to the land height where the center of the zone is located. --- @return Core.Point#POINT_VEC2 The PointVec2 of the zone. -function ZONE_BASE:GetPointVec2() - self:F2( self.ZoneName ) - - local Vec2 = self:GetVec2() - - local PointVec2 = POINT_VEC2:NewFromVec2( Vec2 ) - - self:T2( { PointVec2 } ) - - return PointVec2 -end - - ---- Returns the @{DCSTypes#Vec3} of the zone. --- @param #ZONE_BASE self --- @param Dcs.DCSTypes#Distance Height The height to add to the land height where the center of the zone is located. --- @return Dcs.DCSTypes#Vec3 The Vec3 of the zone. -function ZONE_BASE: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 - ---- Returns a @{Point#POINT_VEC3} of the zone. --- @param #ZONE_BASE self --- @param Dcs.DCSTypes#Distance Height The height to add to the land height where the center of the zone is located. --- @return Core.Point#POINT_VEC3 The PointVec3 of the zone. -function ZONE_BASE:GetPointVec3( Height ) - self:F2( self.ZoneName ) - - local Vec3 = self:GetVec3( Height ) - - local PointVec3 = POINT_VEC3:NewFromVec3( Vec3 ) - - self:T2( { PointVec3 } ) - - return PointVec3 -end - - ---- Define a random @{DCSTypes#Vec2} within the zone. --- @param #ZONE_BASE self --- @return Dcs.DCSTypes#Vec2 The Vec2 coordinates. -function ZONE_BASE:GetRandomVec2() - return nil -end - ---- Define a random @{Point#POINT_VEC2} within the zone. --- @param #ZONE_BASE self --- @return Core.Point#POINT_VEC2 The PointVec2 coordinates. -function ZONE_BASE:GetRandomPointVec2() - return nil -end - ---- Define a random @{Point#POINT_VEC3} within the zone. --- @param #ZONE_BASE self --- @return Core.Point#POINT_VEC3 The PointVec3 coordinates. -function ZONE_BASE:GetRandomPointVec3() - return nil -end - ---- Get the bounding square the zone. --- @param #ZONE_BASE self --- @return #nil The bounding square. -function ZONE_BASE:GetBoundingSquare() - --return { x1 = 0, y1 = 0, x2 = 0, y2 = 0 } - return nil -end - ---- Bound the zone boundaries with a tires. --- @param #ZONE_BASE self -function ZONE_BASE:BoundZone() - self:F2() - -end - ---- Smokes the zone boundaries in a color. --- @param #ZONE_BASE self --- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color. -function ZONE_BASE:SmokeZone( SmokeColor ) - self:F2( SmokeColor ) - -end - ---- Set the randomization probability of a zone to be selected. --- @param #ZONE_BASE self --- @param ZoneProbability A value between 0 and 1. 0 = 0% and 1 = 100% probability. -function ZONE_BASE:SetZoneProbability( ZoneProbability ) - self:F2( ZoneProbability ) - - self.ZoneProbability = ZoneProbability or 1 - return self -end - ---- Get the randomization probability of a zone to be selected. --- @param #ZONE_BASE self --- @return #number A value between 0 and 1. 0 = 0% and 1 = 100% probability. -function ZONE_BASE:GetZoneProbability() - self:F2() - - return self.ZoneProbability -end - ---- Get the zone taking into account the randomization probability of a zone to be selected. --- @param #ZONE_BASE self --- @return #ZONE_BASE The zone is selected taking into account the randomization probability factor. --- @return #nil The zone is not selected taking into account the randomization probability factor. -function ZONE_BASE:GetZoneMaybe() - self:F2() - - local Randomization = math.random() - if Randomization <= self.ZoneProbability then - return self - else - return nil - end -end - - ---- The ZONE_RADIUS class, defined by a zone name, a location and a radius. --- @type ZONE_RADIUS --- @field Dcs.DCSTypes#Vec2 Vec2 The current location of the zone. --- @field Dcs.DCSTypes#Distance Radius The radius of the zone. --- @extends Core.Zone#ZONE_BASE - ---- # 2) @{Zone#ZONE_RADIUS} class, extends @{Zone#ZONE_BASE} --- --- The ZONE_RADIUS class defined by a zone name, a location and a radius. --- This class implements the inherited functions from Core.Zone#ZONE_BASE taking into account the own zone format and properties. --- --- ## 2.1) @{Zone#ZONE_RADIUS} constructor --- --- * @{#ZONE_RADIUS.New}(): Constructor. --- --- ## 2.2) Manage the radius of the zone --- --- * @{#ZONE_RADIUS.SetRadius}(): Sets the radius of the zone. --- * @{#ZONE_RADIUS.GetRadius}(): Returns the radius of the zone. --- --- ## 2.3) Manage the location of the zone --- --- * @{#ZONE_RADIUS.SetVec2}(): Sets the @{DCSTypes#Vec2} of the zone. --- * @{#ZONE_RADIUS.GetVec2}(): Returns the @{DCSTypes#Vec2} of the zone. --- * @{#ZONE_RADIUS.GetVec3}(): Returns the @{DCSTypes#Vec3} of the zone, taking an additional height parameter. --- --- ## 2.4) Zone point randomization --- --- Various functions exist to find random points within the zone. --- --- * @{#ZONE_RADIUS.GetRandomVec2}(): Gets a random 2D point in the zone. --- * @{#ZONE_RADIUS.GetRandomPointVec2}(): Gets a @{Point#POINT_VEC2} object representing a random 2D point in the zone. --- * @{#ZONE_RADIUS.GetRandomPointVec3}(): Gets a @{Point#POINT_VEC3} object representing a random 3D point in the zone. Note that the height of the point is at landheight. --- --- === --- --- @field #ZONE_RADIUS ZONE_RADIUS --- -ZONE_RADIUS = { - ClassName="ZONE_RADIUS", - } - ---- Constructor of @{#ZONE_RADIUS}, taking the zone name, the zone location and a radius. --- @param #ZONE_RADIUS self --- @param #string ZoneName Name of the zone. --- @param Dcs.DCSTypes#Vec2 Vec2 The location of the zone. --- @param Dcs.DCSTypes#Distance Radius The radius of the zone. --- @return #ZONE_RADIUS self -function ZONE_RADIUS:New( ZoneName, Vec2, Radius ) - local self = BASE:Inherit( self, ZONE_BASE:New( ZoneName ) ) -- #ZONE_RADIUS - self:F( { ZoneName, Vec2, Radius } ) - - self.Radius = Radius - self.Vec2 = Vec2 - - return self -end - ---- Bounds the zone with tires. --- @param #ZONE_RADIUS self --- @param #number Points (optional) The amount of points in the circle. --- @param #boolean UnBound If true the tyres will be destroyed. --- @return #ZONE_RADIUS self -function ZONE_RADIUS:BoundZone( Points, CountryID, UnBound ) - - local Point = {} - local Vec2 = self:GetVec2() - - Points = Points and Points or 360 - - local Angle - local RadialBase = math.pi*2 - - -- - for Angle = 0, 360, (360 / Points ) do - local Radial = Angle * RadialBase / 360 - Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() - Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() - - local CountryName = _DATABASE.COUNTRY_NAME[CountryID] - - local Tire = { - ["country"] = CountryName, - ["category"] = "Fortifications", - ["canCargo"] = false, - ["shape_name"] = "H-tyre_B_WF", - ["type"] = "Black_Tyre_WF", - --["unitId"] = Angle + 10000, - ["y"] = Point.y, - ["x"] = Point.x, - ["name"] = string.format( "%s-Tire #%0d", self:GetName(), Angle ), - ["heading"] = 0, - } -- end of ["group"] - - local Group = coalition.addStaticObject( CountryID, Tire ) - if UnBound and UnBound == true then - Group:destroy() - end - end - - return self -end - - ---- Smokes the zone boundaries in a color. --- @param #ZONE_RADIUS self --- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color. --- @param #number Points (optional) The amount of points in the circle. --- @return #ZONE_RADIUS self -function ZONE_RADIUS:SmokeZone( SmokeColor, Points ) - self:F2( SmokeColor ) - - local Point = {} - local Vec2 = self:GetVec2() - - Points = Points and Points or 360 - - local Angle - local RadialBase = math.pi*2 - - for Angle = 0, 360, 360 / Points do - local Radial = Angle * RadialBase / 360 - Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() - Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() - POINT_VEC2:New( Point.x, Point.y ):Smoke( SmokeColor ) - end - - return self -end - - ---- Flares the zone boundaries in a color. --- @param #ZONE_RADIUS self --- @param Utilities.Utils#FLARECOLOR FlareColor The flare color. --- @param #number Points (optional) The amount of points in the circle. --- @param Dcs.DCSTypes#Azimuth Azimuth (optional) Azimuth The azimuth of the flare. --- @return #ZONE_RADIUS self -function ZONE_RADIUS:FlareZone( FlareColor, Points, Azimuth ) - self:F2( { FlareColor, Azimuth } ) - - local Point = {} - local Vec2 = self:GetVec2() - - Points = Points and Points or 360 - - local Angle - local RadialBase = math.pi*2 - - for Angle = 0, 360, 360 / Points do - local Radial = Angle * RadialBase / 360 - Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() - Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() - POINT_VEC2:New( Point.x, Point.y ):Flare( FlareColor, Azimuth ) - end - - return self -end - ---- Returns the radius of the zone. --- @param #ZONE_RADIUS self --- @return Dcs.DCSTypes#Distance The radius of the zone. -function ZONE_RADIUS:GetRadius() - self:F2( self.ZoneName ) - - self:T2( { self.Radius } ) - - return self.Radius -end - ---- Sets the radius of the zone. --- @param #ZONE_RADIUS self --- @param Dcs.DCSTypes#Distance Radius The radius of the zone. --- @return Dcs.DCSTypes#Distance The radius of the zone. -function ZONE_RADIUS:SetRadius( Radius ) - self:F2( self.ZoneName ) - - self.Radius = Radius - self:T2( { self.Radius } ) - - return self.Radius -end - ---- Returns the @{DCSTypes#Vec2} of the zone. --- @param #ZONE_RADIUS self --- @return Dcs.DCSTypes#Vec2 The location of the zone. -function ZONE_RADIUS:GetVec2() - self:F2( self.ZoneName ) - - self:T2( { self.Vec2 } ) - - return self.Vec2 -end - ---- Sets the @{DCSTypes#Vec2} of the zone. --- @param #ZONE_RADIUS self --- @param Dcs.DCSTypes#Vec2 Vec2 The new location of the zone. --- @return Dcs.DCSTypes#Vec2 The new location of the zone. -function ZONE_RADIUS:SetVec2( Vec2 ) - self:F2( self.ZoneName ) - - self.Vec2 = Vec2 - - self:T2( { self.Vec2 } ) - - return self.Vec2 -end - ---- Returns the @{DCSTypes#Vec3} of the ZONE_RADIUS. --- @param #ZONE_RADIUS self --- @param Dcs.DCSTypes#Distance Height The height to add to the land height where the center of the zone is located. --- @return Dcs.DCSTypes#Vec3 The point of the zone. -function ZONE_RADIUS:GetVec3( Height ) - self:F2( { self.ZoneName, Height } ) - - Height = Height or 0 - local Vec2 = self:GetVec2() - - local Vec3 = { x = Vec2.x, y = land.getHeight( self:GetVec2() ) + Height, z = Vec2.y } - - self:T2( { Vec3 } ) - - return Vec3 -end - - ---- Returns if a location is within the zone. --- @param #ZONE_RADIUS self --- @param Dcs.DCSTypes#Vec2 Vec2 The location to test. --- @return #boolean true if the location is within the zone. -function ZONE_RADIUS:IsVec2InZone( Vec2 ) - self:F2( Vec2 ) - - local ZoneVec2 = self:GetVec2() - - if ZoneVec2 then - if (( Vec2.x - ZoneVec2.x )^2 + ( Vec2.y - ZoneVec2.y ) ^2 ) ^ 0.5 <= self:GetRadius() then - return true - end - end - - return false -end - ---- Returns if a point is within the zone. --- @param #ZONE_RADIUS self --- @param Dcs.DCSTypes#Vec3 Vec3 The point to test. --- @return #boolean true if the point is within the zone. -function ZONE_RADIUS:IsVec3InZone( Vec3 ) - self:F2( Vec3 ) - - local InZone = self:IsVec2InZone( { x = Vec3.x, y = Vec3.z } ) - - return InZone -end - ---- Returns a random Vec2 location within the zone. --- @param #ZONE_RADIUS self --- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. --- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. --- @return Dcs.DCSTypes#Vec2 The random location within the zone. -function ZONE_RADIUS:GetRandomVec2( inner, outer ) - self:F( self.ZoneName, inner, outer ) - - local Point = {} - local Vec2 = self:GetVec2() - local _inner = inner or 0 - local _outer = outer or self:GetRadius() - - local angle = math.random() * math.pi * 2; - Point.x = Vec2.x + math.cos( angle ) * math.random(_inner, _outer); - Point.y = Vec2.y + math.sin( angle ) * math.random(_inner, _outer); - - self:T( { Point } ) - - return Point -end - ---- Returns a @{Point#POINT_VEC2} object reflecting a random 2D location within the zone. --- @param #ZONE_RADIUS self --- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. --- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. --- @return Core.Point#POINT_VEC2 The @{Point#POINT_VEC2} object reflecting the random 3D location within the zone. -function ZONE_RADIUS:GetRandomPointVec2( inner, outer ) - self:F( self.ZoneName, inner, outer ) - - local PointVec2 = POINT_VEC2:NewFromVec2( self:GetRandomVec2() ) - - self:T3( { PointVec2 } ) - - return PointVec2 -end - ---- Returns a @{Point#POINT_VEC3} object reflecting a random 3D location within the zone. --- @param #ZONE_RADIUS self --- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. --- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. --- @return Core.Point#POINT_VEC3 The @{Point#POINT_VEC3} object reflecting the random 3D location within the zone. -function ZONE_RADIUS:GetRandomPointVec3( inner, outer ) - self:F( self.ZoneName, inner, outer ) - - local PointVec3 = POINT_VEC3:NewFromVec2( self:GetRandomVec2() ) - - self:T3( { PointVec3 } ) - - return PointVec3 -end - - - --- @type ZONE --- @extends Core.Zone#ZONE_RADIUS - - ---- # 3) 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 @{#ZONE_RADIUS} taking into account the own zone format and properties. --- --- === --- --- @field #ZONE ZONE --- -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 - ---- # 4) #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_RADIUS} taking into account the own zone format and properties. --- --- === --- --- @field #ZONE_UNIT ZONE_UNIT --- -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 - ---- @type ZONE_GROUP --- @field Wrapper.Group#GROUP ZoneGROUP --- @extends Core.Zone#ZONE_RADIUS - - ---- # 5) #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. --- --- === --- --- @field #ZONE_GROUP ZONE_GROUP --- -ZONE_GROUP = { - ClassName="ZONE_GROUP", - } - ---- Constructor to create a ZONE_GROUP instance, taking the zone name, a zone @{Group#GROUP} and a radius. --- @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 - - - ---- @type ZONE_POLYGON_BASE --- @field #ZONE_POLYGON_BASE.ListVec2 Polygon The polygon defined by an array of @{DCSTypes#Vec2}. --- @extends Core.Zone#ZONE_BASE - - ---- # 6) ZONE_POLYGON_BASE class, extends @{Zone#ZONE_BASE} --- --- The ZONE_POLYGON_BASE class defined by a sequence of @{Group#GROUP} waypoints within the Mission Editor, forming a polygon. --- This class implements the inherited functions from @{Zone#ZONE_RADIUS} taking into account the own zone format and properties. --- This class is an abstract BASE class for derived classes, and is not meant to be instantiated. --- --- ## 6.1) Zone point randomization --- --- Various functions exist to find random points within the zone. --- --- * @{#ZONE_POLYGON_BASE.GetRandomVec2}(): Gets a random 2D point in the zone. --- * @{#ZONE_POLYGON_BASE.GetRandomPointVec2}(): Return a @{Point#POINT_VEC2} object representing a random 2D point within the zone. --- * @{#ZONE_POLYGON_BASE.GetRandomPointVec3}(): Return a @{Point#POINT_VEC3} object representing a random 3D point at landheight within the zone. --- --- === --- --- @field #ZONE_POLYGON_BASE ZONE_POLYGON_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 #boolean UnBound If true, the tyres will be destroyed. --- @return #ZONE_POLYGON_BASE self -function ZONE_POLYGON_BASE:BoundZone( UnBound ) - - local i - local j - local Segments = 10 - - i = 1 - j = #self.Polygon - - while i <= #self.Polygon do - self:T( { i, j, self.Polygon[i], self.Polygon[j] } ) - - local DeltaX = self.Polygon[j].x - self.Polygon[i].x - local DeltaY = self.Polygon[j].y - self.Polygon[i].y - - for Segment = 0, Segments do -- We divide each line in 5 segments and smoke a point on the line. - local PointX = self.Polygon[i].x + ( Segment * DeltaX / Segments ) - local PointY = self.Polygon[i].y + ( Segment * DeltaY / Segments ) - local Tire = { - ["country"] = "USA", - ["category"] = "Fortifications", - ["canCargo"] = false, - ["shape_name"] = "H-tyre_B_WF", - ["type"] = "Black_Tyre_WF", - ["y"] = PointY, - ["x"] = PointX, - ["name"] = string.format( "%s-Tire #%0d", self:GetName(), ((i - 1) * Segments) + Segment ), - ["heading"] = 0, - } -- end of ["group"] - - local Group = coalition.addStaticObject( country.id.USA, Tire ) - if UnBound and UnBound == true then - Group:destroy() - end - - end - j = i - i = i + 1 - end - - return self -end - - - ---- Smokes the zone boundaries in a color. --- @param #ZONE_POLYGON_BASE self --- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color. --- @return #ZONE_POLYGON_BASE self -function ZONE_POLYGON_BASE:SmokeZone( SmokeColor ) - self:F2( SmokeColor ) - - local i - local j - local Segments = 10 - - i = 1 - j = #self.Polygon - - while i <= #self.Polygon do - self:T( { i, j, self.Polygon[i], self.Polygon[j] } ) - - local DeltaX = self.Polygon[j].x - self.Polygon[i].x - local DeltaY = self.Polygon[j].y - self.Polygon[i].y - - for Segment = 0, Segments do -- We divide each line in 5 segments and smoke a point on the line. - local PointX = self.Polygon[i].x + ( Segment * DeltaX / Segments ) - local PointY = self.Polygon[i].y + ( Segment * DeltaY / Segments ) - POINT_VEC2:New( PointX, PointY ):Smoke( SmokeColor ) - end - j = i - i = i + 1 - end - - return self -end - - - - ---- Returns if a location is within the zone. --- Source learned and taken from: https://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html --- @param #ZONE_POLYGON_BASE self --- @param Dcs.DCSTypes#Vec2 Vec2 The location to test. --- @return #boolean true if the location is within the zone. -function ZONE_POLYGON_BASE:IsVec2InZone( Vec2 ) - self:F2( Vec2 ) - - local Next - local Prev - local InPolygon = false - - Next = 1 - Prev = #self.Polygon - - while Next <= #self.Polygon do - self:T( { Next, Prev, self.Polygon[Next], self.Polygon[Prev] } ) - if ( ( ( self.Polygon[Next].y > Vec2.y ) ~= ( self.Polygon[Prev].y > Vec2.y ) ) and - ( Vec2.x < ( self.Polygon[Prev].x - self.Polygon[Next].x ) * ( Vec2.y - self.Polygon[Next].y ) / ( self.Polygon[Prev].y - self.Polygon[Next].y ) + self.Polygon[Next].x ) - ) then - InPolygon = not InPolygon - end - self:T2( { InPolygon = InPolygon } ) - Prev = Next - Next = Next + 1 - end - - self:T( { InPolygon = InPolygon } ) - return InPolygon -end - ---- Define a random @{DCSTypes#Vec2} within the zone. --- @param #ZONE_POLYGON_BASE self --- @return Dcs.DCSTypes#Vec2 The Vec2 coordinate. -function ZONE_POLYGON_BASE:GetRandomVec2() - self:F2() - - --- It is a bit tricky to find a random point within a polygon. Right now i am doing it the dirty and inefficient way... - local Vec2Found = false - local Vec2 - local BS = self:GetBoundingSquare() - - self:T2( BS ) - - while Vec2Found == false do - Vec2 = { x = math.random( BS.x1, BS.x2 ), y = math.random( BS.y1, BS.y2 ) } - self:T2( Vec2 ) - if self:IsVec2InZone( Vec2 ) then - Vec2Found = true - end - end - - self:T2( Vec2 ) - - return Vec2 -end - ---- Return a @{Point#POINT_VEC2} object representing a random 2D point at landheight within the zone. --- @param #ZONE_POLYGON_BASE self --- @return @{Point#POINT_VEC2} -function ZONE_POLYGON_BASE:GetRandomPointVec2() - self:F2() - - local PointVec2 = POINT_VEC2:NewFromVec2( self:GetRandomVec2() ) - - self:T2( PointVec2 ) - - return PointVec2 -end - ---- Return a @{Point#POINT_VEC3} object representing a random 3D point at landheight within the zone. --- @param #ZONE_POLYGON_BASE self --- @return @{Point#POINT_VEC3} -function ZONE_POLYGON_BASE:GetRandomPointVec3() - self:F2() - - local PointVec3 = POINT_VEC3:NewFromVec2( self:GetRandomVec2() ) - - self:T2( PointVec3 ) - - return PointVec3 -end - - ---- Get the bounding square the zone. --- @param #ZONE_POLYGON_BASE self --- @return #ZONE_POLYGON_BASE.BoundingSquare The bounding square. -function ZONE_POLYGON_BASE:GetBoundingSquare() - - local x1 = self.Polygon[1].x - local y1 = self.Polygon[1].y - local x2 = self.Polygon[1].x - local y2 = self.Polygon[1].y - - for i = 2, #self.Polygon do - self:T2( { self.Polygon[i], x1, y1, x2, y2 } ) - x1 = ( x1 > self.Polygon[i].x ) and self.Polygon[i].x or x1 - x2 = ( x2 < self.Polygon[i].x ) and self.Polygon[i].x or x2 - y1 = ( y1 > self.Polygon[i].y ) and self.Polygon[i].y or y1 - y2 = ( y2 < self.Polygon[i].y ) and self.Polygon[i].y or y2 - - end - - return { x1 = x1, y1 = y1, x2 = x2, y2 = y2 } -end - - ---- @type ZONE_POLYGON --- @extends Core.Zone#ZONE_POLYGON_BASE - - ---- # 7) 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. --- --- === --- --- @field #ZONE_POLYGON ZONE_POLYGON --- -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 = {}, - COUNTRY_ID = {}, - COUNTRY_NAME = {}, - NavPoints = {}, -} - -local _DATABASECoalition = - { - [1] = "Red", - [2] = "Blue", - } - -local _DATABASECategory = - { - ["plane"] = Unit.Category.AIRPLANE, - ["helicopter"] = Unit.Category.HELICOPTER, - ["vehicle"] = Unit.Category.GROUND_UNIT, - ["ship"] = Unit.Category.SHIP, - ["static"] = Unit.Category.STRUCTURE, - } - - ---- Creates a new DATABASE object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. --- @param #DATABASE self --- @return #DATABASE --- @usage --- -- Define a new DATABASE Object. This DBObject will contain a reference to all Group and Unit Templates defined within the ME and the DCSRTE. --- DBObject = DATABASE:New() -function DATABASE:New() - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) - - self:SetEventPriority( 1 ) - - self:HandleEvent( EVENTS.Birth, self._EventOnBirth ) - self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) - self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) - - -- Follow alive players and clients - self:HandleEvent( EVENTS.PlayerEnterUnit, self._EventOnPlayerEnterUnit ) - self:HandleEvent( EVENTS.PlayerLeaveUnit, self._EventOnPlayerLeaveUnit ) - - self:_RegisterTemplates() - self:_RegisterGroupsAndUnits() - self:_RegisterClients() - self:_RegisterStatics() - self:_RegisterPlayers() - self:_RegisterAirbases() - - return self -end - ---- Finds a Unit based on the Unit Name. --- @param #DATABASE self --- @param #string UnitName --- @return Wrapper.Unit#UNIT The found Unit. -function DATABASE:FindUnit( UnitName ) - - local UnitFound = self.UNITS[UnitName] - return UnitFound -end - - ---- Adds a Unit based on the Unit Name in the DATABASE. --- @param #DATABASE self -function DATABASE:AddUnit( DCSUnitName ) - - if not self.UNITS[DCSUnitName] then - local UnitRegister = UNIT:Register( DCSUnitName ) - self.UNITS[DCSUnitName] = UNIT:Register( DCSUnitName ) - end - - return self.UNITS[DCSUnitName] -end - - ---- Deletes a Unit from the DATABASE based on the Unit Name. --- @param #DATABASE self -function DATABASE:DeleteUnit( DCSUnitName ) - - --self.UNITS[DCSUnitName] = nil -end - ---- Adds a Static based on the Static Name in the DATABASE. --- @param #DATABASE self -function DATABASE:AddStatic( DCSStaticName ) - - if not self.STATICS[DCSStaticName] then - self.STATICS[DCSStaticName] = STATIC:Register( DCSStaticName ) - end -end - - ---- Deletes a Static from the DATABASE based on the Static Name. --- @param #DATABASE self -function DATABASE:DeleteStatic( DCSStaticName ) - - --self.STATICS[DCSStaticName] = nil -end - ---- Finds a STATIC based on the StaticName. --- @param #DATABASE self --- @param #string StaticName --- @return Wrapper.Static#STATIC The found STATIC. -function DATABASE:FindStatic( StaticName ) - - local StaticFound = self.STATICS[StaticName] - return StaticFound -end - ---- Adds a Airbase based on the Airbase Name in the DATABASE. --- @param #DATABASE self -function DATABASE:AddAirbase( DCSAirbaseName ) - - if not self.AIRBASES[DCSAirbaseName] then - self.AIRBASES[DCSAirbaseName] = AIRBASE:Register( DCSAirbaseName ) - end -end - - ---- Deletes a Airbase from the DATABASE based on the Airbase Name. --- @param #DATABASE self -function DATABASE:DeleteAirbase( DCSAirbaseName ) - - --self.AIRBASES[DCSAirbaseName] = nil -end - ---- Finds a AIRBASE based on the AirbaseName. --- @param #DATABASE self --- @param #string AirbaseName --- @return Wrapper.Airbase#AIRBASE The found AIRBASE. -function DATABASE:FindAirbase( AirbaseName ) - - local AirbaseFound = self.AIRBASES[AirbaseName] - return AirbaseFound -end - - ---- Finds a CLIENT based on the ClientName. --- @param #DATABASE self --- @param #string ClientName --- @return Wrapper.Client#CLIENT The found CLIENT. -function DATABASE:FindClient( ClientName ) - - local ClientFound = self.CLIENTS[ClientName] - return ClientFound -end - - ---- Adds a CLIENT based on the ClientName in the DATABASE. --- @param #DATABASE self -function DATABASE:AddClient( ClientName ) - - if not self.CLIENTS[ClientName] then - self.CLIENTS[ClientName] = CLIENT:Register( ClientName ) - end - - return self.CLIENTS[ClientName] -end - - ---- Finds a GROUP based on the GroupName. --- @param #DATABASE self --- @param #string GroupName --- @return Wrapper.Group#GROUP The found GROUP. -function DATABASE:FindGroup( GroupName ) - - local GroupFound = self.GROUPS[GroupName] - return GroupFound -end - - ---- Adds a GROUP based on the GroupName in the DATABASE. --- @param #DATABASE self -function DATABASE:AddGroup( GroupName ) - - if not self.GROUPS[GroupName] then - self:E( { "Add GROUP:", GroupName } ) - self.GROUPS[GroupName] = GROUP:Register( GroupName ) - end - - return self.GROUPS[GroupName] -end - ---- Adds a player based on the Player Name in the DATABASE. --- @param #DATABASE self -function DATABASE:AddPlayer( UnitName, PlayerName ) - - if PlayerName then - self:E( { "Add player for unit:", UnitName, PlayerName } ) - self.PLAYERS[PlayerName] = self:FindUnit( UnitName ) - self.PLAYERSJOINED[PlayerName] = PlayerName - end -end - ---- Deletes a player from the DATABASE based on the Player Name. --- @param #DATABASE self -function DATABASE:DeletePlayer( PlayerName ) - - if PlayerName then - self:E( { "Clean player:", PlayerName } ) - self.PLAYERS[PlayerName] = nil - end -end - - ---- Instantiate new Groups within the DCSRTE. --- This method expects EXACTLY the same structure as a structure within the ME, and needs 2 additional fields defined: --- SpawnCountryID, SpawnCategoryID --- This method is used by the SPAWN class. --- @param #DATABASE self --- @param #table SpawnTemplate --- @return #DATABASE self -function DATABASE:Spawn( SpawnTemplate ) - self:F( SpawnTemplate.name ) - - self:T( { SpawnTemplate.SpawnCountryID, SpawnTemplate.SpawnCategoryID } ) - - -- Copy the spawn variables of the template in temporary storage, nullify, and restore the spawn variables. - local SpawnCoalitionID = SpawnTemplate.CoalitionID - local SpawnCountryID = SpawnTemplate.CountryID - local SpawnCategoryID = SpawnTemplate.CategoryID - - -- Nullify - SpawnTemplate.CoalitionID = nil - SpawnTemplate.CountryID = nil - SpawnTemplate.CategoryID = nil - - self:_RegisterTemplate( SpawnTemplate, SpawnCoalitionID, SpawnCategoryID, SpawnCountryID ) - - self:T3( SpawnTemplate ) - coalition.addGroup( SpawnCountryID, SpawnCategoryID, SpawnTemplate ) - - -- Restore - SpawnTemplate.CoalitionID = SpawnCoalitionID - SpawnTemplate.CountryID = SpawnCountryID - SpawnTemplate.CategoryID = SpawnCategoryID - - local SpawnGroup = self:AddGroup( SpawnTemplate.name ) - return SpawnGroup -end - ---- Set a status to a Group within the Database, this to check crossing events for example. -function DATABASE:SetStatusGroup( GroupName, Status ) - self:F2( Status ) - - self.Templates.Groups[GroupName].Status = Status -end - ---- Get a status to a Group within the Database, this to check crossing events for example. -function DATABASE:GetStatusGroup( GroupName ) - self:F2( Status ) - - if self.Templates.Groups[GroupName] then - return self.Templates.Groups[GroupName].Status - else - return "" - end -end - ---- Private method that registers new Group Templates within the DATABASE Object. --- @param #DATABASE self --- @param #table GroupTemplate --- @return #DATABASE self -function DATABASE:_RegisterTemplate( GroupTemplate, CoalitionID, CategoryID, CountryID ) - - local GroupTemplateName = env.getValueDictByKey(GroupTemplate.name) - - local TraceTable = {} - - if not self.Templates.Groups[GroupTemplateName] then - self.Templates.Groups[GroupTemplateName] = {} - self.Templates.Groups[GroupTemplateName].Status = nil - end - - -- Delete the spans from the route, it is not needed and takes memory. - if GroupTemplate.route and GroupTemplate.route.spans then - GroupTemplate.route.spans = nil - end - - GroupTemplate.CategoryID = CategoryID - GroupTemplate.CoalitionID = CoalitionID - GroupTemplate.CountryID = CountryID - - self.Templates.Groups[GroupTemplateName].GroupName = GroupTemplateName - self.Templates.Groups[GroupTemplateName].Template = GroupTemplate - self.Templates.Groups[GroupTemplateName].groupId = GroupTemplate.groupId - self.Templates.Groups[GroupTemplateName].UnitCount = #GroupTemplate.units - self.Templates.Groups[GroupTemplateName].Units = GroupTemplate.units - self.Templates.Groups[GroupTemplateName].CategoryID = CategoryID - self.Templates.Groups[GroupTemplateName].CoalitionID = CoalitionID - self.Templates.Groups[GroupTemplateName].CountryID = CountryID - - - TraceTable[#TraceTable+1] = "Group" - TraceTable[#TraceTable+1] = self.Templates.Groups[GroupTemplateName].GroupName - - TraceTable[#TraceTable+1] = "Coalition" - TraceTable[#TraceTable+1] = self.Templates.Groups[GroupTemplateName].CoalitionID - TraceTable[#TraceTable+1] = "Category" - TraceTable[#TraceTable+1] = self.Templates.Groups[GroupTemplateName].CategoryID - TraceTable[#TraceTable+1] = "Country" - TraceTable[#TraceTable+1] = self.Templates.Groups[GroupTemplateName].CountryID - - TraceTable[#TraceTable+1] = "Units" - - for unit_num, UnitTemplate in pairs( GroupTemplate.units ) do - - UnitTemplate.name = env.getValueDictByKey(UnitTemplate.name) - - self.Templates.Units[UnitTemplate.name] = {} - self.Templates.Units[UnitTemplate.name].UnitName = UnitTemplate.name - self.Templates.Units[UnitTemplate.name].Template = UnitTemplate - self.Templates.Units[UnitTemplate.name].GroupName = GroupTemplateName - self.Templates.Units[UnitTemplate.name].GroupTemplate = GroupTemplate - self.Templates.Units[UnitTemplate.name].GroupId = GroupTemplate.groupId - self.Templates.Units[UnitTemplate.name].CategoryID = CategoryID - self.Templates.Units[UnitTemplate.name].CoalitionID = CoalitionID - self.Templates.Units[UnitTemplate.name].CountryID = CountryID - - if UnitTemplate.skill and (UnitTemplate.skill == "Client" or UnitTemplate.skill == "Player") then - self.Templates.ClientsByName[UnitTemplate.name] = UnitTemplate - self.Templates.ClientsByName[UnitTemplate.name].CategoryID = CategoryID - self.Templates.ClientsByName[UnitTemplate.name].CoalitionID = CoalitionID - self.Templates.ClientsByName[UnitTemplate.name].CountryID = CountryID - self.Templates.ClientsByID[UnitTemplate.unitId] = UnitTemplate - end - - TraceTable[#TraceTable+1] = self.Templates.Units[UnitTemplate.name].UnitName - end - - self:E( TraceTable ) -end - -function DATABASE:GetGroupTemplate( GroupName ) - local GroupTemplate = self.Templates.Groups[GroupName].Template - GroupTemplate.SpawnCoalitionID = self.Templates.Groups[GroupName].CoalitionID - GroupTemplate.SpawnCategoryID = self.Templates.Groups[GroupName].CategoryID - GroupTemplate.SpawnCountryID = self.Templates.Groups[GroupName].CountryID - return GroupTemplate -end - -function DATABASE:GetGroupNameFromUnitName( UnitName ) - return self.Templates.Units[UnitName].GroupName -end - -function DATABASE:GetGroupTemplateFromUnitName( UnitName ) - return self.Templates.Units[UnitName].GroupTemplate -end - -function DATABASE:GetCoalitionFromClientTemplate( ClientName ) - return self.Templates.ClientsByName[ClientName].CoalitionID -end - -function DATABASE:GetCategoryFromClientTemplate( ClientName ) - return self.Templates.ClientsByName[ClientName].CategoryID -end - -function DATABASE:GetCountryFromClientTemplate( ClientName ) - return self.Templates.ClientsByName[ClientName].CountryID -end - ---- Airbase - -function DATABASE:GetCoalitionFromAirbase( AirbaseName ) - return self.AIRBASES[AirbaseName]:GetCoalition() -end - -function DATABASE:GetCategoryFromAirbase( AirbaseName ) - return self.AIRBASES[AirbaseName]:GetCategory() -end - - - ---- Private method that registers all alive players in the mission. --- @param #DATABASE self --- @return #DATABASE self -function DATABASE:_RegisterPlayers() - - local CoalitionsData = { AlivePlayersRed = coalition.getPlayers( coalition.side.RED ), AlivePlayersBlue = coalition.getPlayers( coalition.side.BLUE ) } - for CoalitionId, CoalitionData in pairs( CoalitionsData ) do - for UnitId, UnitData in pairs( CoalitionData ) do - self:T3( { "UnitData:", UnitData } ) - if UnitData and UnitData:isExist() then - local UnitName = UnitData:getName() - local PlayerName = UnitData:getPlayerName() - if not self.PLAYERS[PlayerName] then - self:E( { "Add player for unit:", UnitName, PlayerName } ) - self:AddPlayer( UnitName, PlayerName ) - end - end - end - end - - return self -end - - ---- Private method that registers all Groups and Units within in the mission. --- @param #DATABASE self --- @return #DATABASE self -function DATABASE:_RegisterGroupsAndUnits() - - local CoalitionsData = { GroupsRed = coalition.getGroups( coalition.side.RED ), GroupsBlue = coalition.getGroups( coalition.side.BLUE ) } - for CoalitionId, CoalitionData in pairs( CoalitionsData ) do - for DCSGroupId, DCSGroup in pairs( CoalitionData ) do - - if DCSGroup:isExist() then - local DCSGroupName = DCSGroup:getName() - - self:E( { "Register Group:", DCSGroupName } ) - self:AddGroup( DCSGroupName ) - - for DCSUnitId, DCSUnit in pairs( DCSGroup:getUnits() ) do - - local DCSUnitName = DCSUnit:getName() - self:E( { "Register Unit:", DCSUnitName } ) - self:AddUnit( DCSUnitName ) - end - else - self:E( { "Group does not exist: ", DCSGroup } ) - end - - end - end - - return self -end - ---- Private method that registers all Units of skill Client or Player within in the mission. --- @param #DATABASE self --- @return #DATABASE self -function DATABASE:_RegisterClients() - - for ClientName, ClientTemplate in pairs( self.Templates.ClientsByName ) do - self:E( { "Register Client:", ClientName } ) - self:AddClient( ClientName ) - end - - return self -end - ---- @param #DATABASE self -function DATABASE:_RegisterStatics() - - local CoalitionsData = { GroupsRed = coalition.getStaticObjects( coalition.side.RED ), GroupsBlue = coalition.getStaticObjects( coalition.side.BLUE ) } - for CoalitionId, CoalitionData in pairs( CoalitionsData ) do - for DCSStaticId, DCSStatic in pairs( CoalitionData ) do - - if DCSStatic:isExist() then - local DCSStaticName = DCSStatic:getName() - - self:E( { "Register Static:", DCSStaticName } ) - self:AddStatic( DCSStaticName ) - else - self:E( { "Static does not exist: ", DCSStatic } ) - end - end - end - - return self -end - ---- @param #DATABASE self -function DATABASE:_RegisterAirbases() - - local CoalitionsData = { AirbasesRed = coalition.getAirbases( coalition.side.RED ), AirbasesBlue = coalition.getAirbases( coalition.side.BLUE ), AirbasesNeutral = coalition.getAirbases( coalition.side.NEUTRAL ) } - for CoalitionId, CoalitionData in pairs( CoalitionsData ) do - for DCSAirbaseId, DCSAirbase in pairs( CoalitionData ) do - - local DCSAirbaseName = DCSAirbase:getName() - - self:E( { "Register Airbase:", DCSAirbaseName } ) - self:AddAirbase( DCSAirbaseName ) - end - end - - return self -end - - ---- Events - ---- Handles the OnBirth event for the alive units set. --- @param #DATABASE self --- @param Core.Event#EVENTDATA Event -function DATABASE:_EventOnBirth( Event ) - self:F2( { Event } ) - - if Event.IniDCSUnit then - if Event.IniObjectCategory == 3 then - self:AddStatic( Event.IniDCSUnitName ) - else - if Event.IniObjectCategory == 1 then - self:AddUnit( Event.IniDCSUnitName ) - self:AddGroup( Event.IniDCSGroupName ) - end - end - self:_EventOnPlayerEnterUnit( Event ) - end -end - - ---- Handles the OnDead or OnCrash event for alive units set. --- @param #DATABASE self --- @param Core.Event#EVENTDATA Event -function DATABASE:_EventOnDeadOrCrash( Event ) - self:F2( { Event } ) - - if Event.IniDCSUnit then - if Event.IniObjectCategory == 3 then - if self.STATICS[Event.IniDCSUnitName] then - self:DeleteStatic( Event.IniDCSUnitName ) - end - else - if Event.IniObjectCategory == 1 then - if self.UNITS[Event.IniDCSUnitName] then - self:DeleteUnit( Event.IniDCSUnitName ) - end - end - end - end -end - - ---- Handles the OnPlayerEnterUnit event to fill the active players table (with the unit filter applied). --- @param #DATABASE self --- @param Core.Event#EVENTDATA Event -function DATABASE:_EventOnPlayerEnterUnit( Event ) - self:F2( { Event } ) - - if Event.IniUnit then - if Event.IniObjectCategory == 1 then - self:AddUnit( Event.IniDCSUnitName ) - self:AddGroup( Event.IniDCSGroupName ) - local PlayerName = Event.IniUnit:GetPlayerName() - if not self.PLAYERS[PlayerName] then - self:AddPlayer( Event.IniUnitName, PlayerName ) - end - end - end -end - - ---- Handles the OnPlayerLeaveUnit event to clean the active players table. --- @param #DATABASE self --- @param Core.Event#EVENTDATA Event -function DATABASE:_EventOnPlayerLeaveUnit( Event ) - self:F2( { Event } ) - - if Event.IniUnit then - if Event.IniObjectCategory == 1 then - local PlayerName = Event.IniUnit:GetPlayerName() - if self.PLAYERS[PlayerName] then - self:DeletePlayer( PlayerName ) - end - end - end -end - ---- Iterators - ---- Iterate the DATABASE and call an iterator function for the given set, providing the Object for each element within the set and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called when there is an alive player in the database. --- @return #DATABASE self -function DATABASE:ForEach( IteratorFunction, FinalizeFunction, arg, Set ) - self:F2( arg ) - - local function CoRoutine() - local Count = 0 - for ObjectID, Object in pairs( Set ) do - self:T2( Object ) - IteratorFunction( Object, unpack( arg ) ) - Count = Count + 1 --- if Count % 100 == 0 then --- coroutine.yield( false ) --- end - end - return true - end - --- local co = coroutine.create( CoRoutine ) - local co = CoRoutine - - local function Schedule() - --- local status, res = coroutine.resume( co ) - local status, res = co() - self:T3( { status, res } ) - - if status == false then - error( res ) - end - if res == false then - return true -- resume next time the loop - end - if FinalizeFunction then - FinalizeFunction( unpack( arg ) ) - end - return false - end - - local Scheduler = SCHEDULER:New( self, Schedule, {}, 0.001, 0.001, 0 ) - - return self -end - - ---- Iterate the DATABASE and call an iterator function for each **alive** UNIT, providing the UNIT and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the database. The function needs to accept a UNIT parameter. --- @return #DATABASE self -function DATABASE:ForEachUnit( IteratorFunction, FinalizeFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, FinalizeFunction, arg, self.UNITS ) - - return self -end - ---- Iterate the DATABASE and call an iterator function for each **alive** GROUP, providing the GROUP and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the database. The function needs to accept a GROUP parameter. --- @return #DATABASE self -function DATABASE:ForEachGroup( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.GROUPS ) - - return self -end - - ---- Iterate the DATABASE and call an iterator function for each **ALIVE** player, providing the player name and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called when there is an player in the database. The function needs to accept the player name. --- @return #DATABASE self -function DATABASE:ForEachPlayer( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.PLAYERS ) - - return self -end - - ---- Iterate the DATABASE and call an iterator function for each player who has joined the mission, providing the Unit of the player and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called when there is was a player in the database. The function needs to accept a UNIT parameter. --- @return #DATABASE self -function DATABASE:ForEachPlayerJoined( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.PLAYERSJOINED ) - - return self -end - ---- Iterate the DATABASE and call an iterator function for each CLIENT, providing the CLIENT to the function and optional parameters. --- @param #DATABASE self --- @param #function IteratorFunction The function that will be called when there is an alive player in the database. The function needs to accept a CLIENT parameter. --- @return #DATABASE self -function DATABASE:ForEachClient( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.CLIENTS ) - - return self -end - - -function DATABASE:_RegisterTemplates() - self:F2() - - self.Navpoints = {} - self.UNITS = {} - --Build routines.db.units and self.Navpoints - for CoalitionName, coa_data in pairs(env.mission.coalition) do - - if (CoalitionName == 'red' or CoalitionName == 'blue') and type(coa_data) == 'table' then - --self.Units[coa_name] = {} - - local CoalitionSide = coalition.side[string.upper(CoalitionName)] - - ---------------------------------------------- - -- build nav points DB - self.Navpoints[CoalitionName] = {} - if coa_data.nav_points then --navpoints - for nav_ind, nav_data in pairs(coa_data.nav_points) do - - if type(nav_data) == 'table' then - self.Navpoints[CoalitionName][nav_ind] = routines.utils.deepCopy(nav_data) - - self.Navpoints[CoalitionName][nav_ind]['name'] = nav_data.callsignStr -- name is a little bit more self-explanatory. - self.Navpoints[CoalitionName][nav_ind]['point'] = {} -- point is used by SSE, support it. - self.Navpoints[CoalitionName][nav_ind]['point']['x'] = nav_data.x - self.Navpoints[CoalitionName][nav_ind]['point']['y'] = 0 - self.Navpoints[CoalitionName][nav_ind]['point']['z'] = nav_data.y - end - end - end - ------------------------------------------------- - if coa_data.country then --there is a country table - for cntry_id, cntry_data in pairs(coa_data.country) do - - local CountryName = string.upper(cntry_data.name) - local CountryID = cntry_data.id - - self.COUNTRY_ID[CountryName] = CountryID - self.COUNTRY_NAME[CountryID] = CountryName - - --self.Units[coa_name][countryName] = {} - --self.Units[coa_name][countryName]["countryId"] = cntry_data.id - - if type(cntry_data) == 'table' then --just making sure - - for obj_type_name, obj_type_data in pairs(cntry_data) do - - if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then --should be an unncessary check - - local CategoryName = obj_type_name - - if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! - - --self.Units[coa_name][countryName][category] = {} - - for group_num, GroupTemplate in pairs(obj_type_data.group) do - - if GroupTemplate and GroupTemplate.units and type(GroupTemplate.units) == 'table' then --making sure again- this is a valid group - self:_RegisterTemplate( - GroupTemplate, - CoalitionSide, - _DATABASECategory[string.lower(CategoryName)], - CountryID - ) - end --if GroupTemplate and GroupTemplate.units then - end --for group_num, GroupTemplate in pairs(obj_type_data.group) do - end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then - end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then - end --for obj_type_name, obj_type_data in pairs(cntry_data) do - end --if type(cntry_data) == 'table' then - end --for cntry_id, cntry_data in pairs(coa_data.country) do - end --if coa_data.country then --there is a country table - end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then - end --for coa_name, coa_data in pairs(mission.coalition) do - - return self -end - - - - ---- **Core** - SET_ classes define **collections** of objects to perform **bulk actions** and logically **group** objects. --- --- ![Banner Image](..\Presentations\SET\Dia1.JPG) --- --- === --- --- SET_ classes group objects of the same type into a collection, which is either: --- --- * Manually managed using the **:Add...()** or **:Remove...()** methods. The initial SET can be filtered with the **@{#SET_BASE.FilterOnce}()** method --- * Dynamically updated when new objects are created or objects are destroyed using the **@{#SET_BASE.FilterStart}()** method. --- --- Various types of SET_ classes are available: --- --- * @{#SET_UNIT}: Defines a colleciton of @{Unit}s filtered by filter criteria. --- * @{#SET_GROUP}: Defines a collection of @{Group}s filtered by filter criteria. --- * @{#SET_CLIENT}: Defines a collection of @{Client}s filterd by filter criteria. --- * @{#SET_AIRBASE}: Defines a collection of @{Airbase}s filtered by filter criteria. --- --- These classes are derived from @{#SET_BASE}, which contains the main methods to manage SETs. --- --- A multitude of other methods are available in SET_ classes that allow to: --- --- * Validate the presence of objects in the SET. --- * Trigger events when objects in the SET change a zone presence. --- --- ### Authors: --- --- * FlightControl : Design & Programming --- --- ### Contributions: --- --- --- @module Set - - ---- @type SET_BASE --- @field #table Filter --- @field #table Set --- @field #table List --- @field Core.Scheduler#SCHEDULER CallScheduler --- @extends Core.Base#BASE - - ---- # 1) SET_BASE class, extends @{Base#BASE} --- The @{Set#SET_BASE} class defines the core functions that define a collection of objects. --- A SET provides iterators to iterate the SET, but will **temporarily** yield the ForEach interator loop at defined **"intervals"** to the mail simulator loop. --- In this way, large loops can be done while not blocking the simulator main processing loop. --- The default **"yield interval"** is after 10 objects processed. --- The default **"time interval"** is after 0.001 seconds. --- --- ## 1.1) Add or remove objects from the SET --- --- Some key core functions are @{Set#SET_BASE.Add} and @{Set#SET_BASE.Remove} to add or remove objects from the SET in your logic. --- --- ## 1.2) Define the SET iterator **"yield interval"** and the **"time interval"** --- --- Modify the iterator intervals with the @{Set#SET_BASE.SetInteratorIntervals} method. --- You can set the **"yield interval"**, and the **"time interval"**. (See above). --- --- @field #SET_BASE SET_BASE -SET_BASE = { - ClassName = "SET_BASE", - Filter = {}, - Set = {}, - List = {}, - Index = {}, -} - - ---- Creates a new SET_BASE object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. --- @param #SET_BASE self --- @return #SET_BASE --- @usage --- -- Define a new SET_BASE Object. This DBObject will contain a reference to all Group and Unit Templates defined within the ME and the DCSRTE. --- DBObject = SET_BASE:New() -function SET_BASE:New( Database ) - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) -- Core.Set#SET_BASE - - self.Database = Database - - self.YieldInterval = 10 - self.TimeInterval = 0.001 - - self.Set = {} - - self.List = {} - self.List.__index = self.List - self.List = setmetatable( { Count = 0 }, self.List ) - - self.Index = {} - - self.CallScheduler = SCHEDULER:New( self ) - - self:SetEventPriority( 2 ) - - return self -end - ---- Finds an @{Base#BASE} object based on the object Name. --- @param #SET_BASE self --- @param #string ObjectName --- @return Core.Base#BASE The Object found. -function SET_BASE:_Find( ObjectName ) - - local ObjectFound = self.Set[ObjectName] - return ObjectFound -end - - ---- Gets the Set. --- @param #SET_BASE self --- @return #SET_BASE self -function SET_BASE:GetSet() - self:F2() - - return self.Set -end - ---- 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._ - - table.insert( self.Index, ObjectName ) - -end - ---- Adds a @{Base#BASE} object in the @{Set#SET_BASE}, using the Object Name as the index. --- @param #SET_BASE self --- @param Wrapper.Object#OBJECT Object --- @return Core.Base#BASE The added BASE Object. -function SET_BASE:AddObject( Object ) - self:F2( Object.ObjectName ) - - self:T( Object.UnitName ) - self:T( Object.ObjectName ) - self:Add( Object.ObjectName, Object ) - -end - - - ---- 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 - - for Index, Key in ipairs( self.Index ) do - if Key == ObjectName then - table.remove( self.Index, Index ) - break - end - end - - 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 - ---- Gets the first object from the @{Set#SET_BASE} and derived classes. --- @param #SET_BASE self --- @return Core.Base#BASE -function SET_BASE:GetFirst() - self:F() - - local ObjectName = self.Index[1] - local FirstObject = self.Set[ObjectName] - self:T3( { FirstObject } ) - return FirstObject -end - ---- Gets the last object from the @{Set#SET_BASE} and derived classes. --- @param #SET_BASE self --- @return Core.Base#BASE -function SET_BASE:GetLast() - self:F() - - local ObjectName = self.Index[#self.Index] - local LastObject = self.Set[ObjectName] - self:T3( { LastObject } ) - return LastObject -end - ---- Gets a random object from the @{Set#SET_BASE} and derived classes. --- @param #SET_BASE self --- @return Core.Base#BASE -function SET_BASE:GetRandom() - self:F() - - local RandomItem = self.Set[self.Index[math.random(#self.Index)]] - - self:T3( { RandomItem } ) - - return RandomItem -end - - ---- Retrieves the amount of objects in the @{Set#SET_BASE} and derived classes. --- @param #SET_BASE self --- @return #number Count -function SET_BASE:Count() - - return #self.Index or 0 -end - - - ---- Copies the Filter criteria from a given Set (for rebuilding a new Set based on an existing Set). --- @param #SET_BASE self --- @param #SET_BASE BaseSet --- @return #SET_BASE -function SET_BASE:SetDatabase( BaseSet ) - - -- Copy the filter criteria of the BaseSet - local OtherFilter = routines.utils.deepCopy( BaseSet.Filter ) - self.Filter = OtherFilter - - -- Now base the new Set on the BaseSet - self.Database = BaseSet:GetSet() - return self -end - - - ---- Define the SET iterator **"yield interval"** and the **"time interval"**. --- @param #SET_BASE self --- @param #number YieldInterval Sets the frequency when the iterator loop will yield after the number of objects processed. The default frequency is 10 objects processed. --- @param #number TimeInterval Sets the time in seconds when the main logic will resume the iterator loop. The default time is 0.001 seconds. --- @return #SET_BASE self -function SET_BASE:SetIteratorIntervals( YieldInterval, TimeInterval ) - - self.YieldInterval = YieldInterval - self.TimeInterval = TimeInterval - - return self -end - - ---- Filters for the defined collection. --- @param #SET_BASE self --- @return #SET_BASE self -function SET_BASE:FilterOnce() - - for ObjectName, Object in pairs( self.Database ) do - - if self:IsIncludeObject( Object ) then - self:Add( ObjectName, Object ) - end - end - - return self -end - ---- Starts the filtering for the defined collection. --- @param #SET_BASE self --- @return #SET_BASE self -function SET_BASE:_FilterStart() - - for ObjectName, Object in pairs( self.Database ) do - - if self:IsIncludeObject( Object ) then - self:E( { "Adding Object:", ObjectName } ) - self:Add( ObjectName, Object ) - end - end - - self:HandleEvent( EVENTS.Birth, self._EventOnBirth ) - self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) - self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) - - -- Follow alive players and clients - self:HandleEvent( EVENTS.PlayerEnterUnit, self._EventOnPlayerEnterUnit ) - self:HandleEvent( EVENTS.PlayerLeaveUnit, self._EventOnPlayerLeaveUnit ) - - - return self -end - ---- Stops the filtering for the defined collection. --- @param #SET_BASE self --- @return #SET_BASE self -function SET_BASE:FilterStop() - - self:UnHandleEvent( EVENTS.Birth ) - self:UnHandleEvent( EVENTS.Dead ) - self:UnHandleEvent( EVENTS.Crash ) - - return self -end - ---- Iterate the SET_BASE while identifying the nearest object from a @{Point#POINT_VEC2}. --- @param #SET_BASE self --- @param Core.Point#POINT_VEC2 PointVec2 A @{Point#POINT_VEC2} object from where to evaluate the closest object in the set. --- @return Core.Base#BASE The closest object. -function SET_BASE:FindNearestObjectFromPointVec2( PointVec2 ) - self:F2( PointVec2 ) - - local NearestObject = nil - local ClosestDistance = nil - - for ObjectID, ObjectData in pairs( self.Set ) do - if NearestObject == nil then - NearestObject = ObjectData - ClosestDistance = PointVec2:DistanceFromVec2( ObjectData:GetVec2() ) - else - local Distance = PointVec2:DistanceFromVec2( ObjectData:GetVec2() ) - if Distance < ClosestDistance then - NearestObject = ObjectData - ClosestDistance = Distance - end - end - end - - return NearestObject -end - - - ------ Private method that registers all alive players in the mission. ----- @param #SET_BASE self ----- @return #SET_BASE self ---function SET_BASE:_RegisterPlayers() --- --- local CoalitionsData = { AlivePlayersRed = coalition.getPlayers( coalition.side.RED ), AlivePlayersBlue = coalition.getPlayers( coalition.side.BLUE ) } --- for CoalitionId, CoalitionData in pairs( CoalitionsData ) do --- for UnitId, UnitData in pairs( CoalitionData ) do --- self:T3( { "UnitData:", UnitData } ) --- if UnitData and UnitData:isExist() then --- local UnitName = UnitData:getName() --- if not self.PlayersAlive[UnitName] then --- self:E( { "Add player for unit:", UnitName, UnitData:getPlayerName() } ) --- self.PlayersAlive[UnitName] = UnitData:getPlayerName() --- end --- end --- end --- end --- --- return self ---end - ---- Events - ---- Handles the OnBirth event for the Set. --- @param #SET_BASE self --- @param Core.Event#EVENTDATA Event -function SET_BASE:_EventOnBirth( Event ) - self:F3( { Event } ) - - if Event.IniDCSUnit then - local ObjectName, Object = self:AddInDatabase( Event ) - self:T3( ObjectName, Object ) - if Object and self:IsIncludeObject( Object ) then - self:Add( ObjectName, Object ) - --self:_EventOnPlayerEnterUnit( Event ) - end - end -end - ---- Handles the OnDead or OnCrash event for alive units set. --- @param #SET_BASE self --- @param Core.Event#EVENTDATA Event -function SET_BASE:_EventOnDeadOrCrash( Event ) - self:F3( { Event } ) - - if Event.IniDCSUnit then - local ObjectName, Object = self:FindInDatabase( Event ) - if ObjectName and Object ~= nil then - self:Remove( ObjectName ) - end - end -end - ---- Handles the OnPlayerEnterUnit event to fill the active players table (with the unit filter applied). --- @param #SET_BASE self --- @param Core.Event#EVENTDATA Event -function SET_BASE:_EventOnPlayerEnterUnit( Event ) - self:F3( { Event } ) - - if Event.IniDCSUnit then - local ObjectName, Object = self:AddInDatabase( Event ) - self:T3( ObjectName, Object ) - if self:IsIncludeObject( Object ) then - self:Add( ObjectName, Object ) - --self:_EventOnPlayerEnterUnit( Event ) - end - end -end - ---- Handles the OnPlayerLeaveUnit event to clean the active players table. --- @param #SET_BASE self --- @param Core.Event#EVENTDATA Event -function SET_BASE:_EventOnPlayerLeaveUnit( Event ) - self:F3( { Event } ) - - local ObjectName = Event.IniDCSUnit - if Event.IniDCSUnit then - if Event.IniDCSGroup then - local GroupUnits = Event.IniDCSGroup:getUnits() - local PlayerCount = 0 - for _, DCSUnit in pairs( GroupUnits ) do - if DCSUnit ~= Event.IniDCSUnit then - if DCSUnit:getPlayer() ~= nil then - PlayerCount = PlayerCount + 1 - end - end - end - self:E(PlayerCount) - if PlayerCount == 0 then - self:Remove( Event.IniDCSGroupName ) - end - end - end -end - --- Iterators - ---- Iterate the SET_BASE and derived classes and call an iterator function for the given SET_BASE, providing the Object for each element within the set and optional parameters. --- @param #SET_BASE self --- @param #function IteratorFunction The function that will be called. --- @return #SET_BASE self -function SET_BASE:ForEach( IteratorFunction, arg, Set, Function, FunctionArguments ) - self:F3( arg ) - - Set = Set or self:GetSet() - arg = arg or {} - - local function CoRoutine() - local Count = 0 - for ObjectID, ObjectData in pairs( Set ) do - local Object = ObjectData - self:T3( Object ) - if Function then - if Function( unpack( FunctionArguments ), Object ) == true then - IteratorFunction( Object, unpack( arg ) ) - end - else - IteratorFunction( Object, unpack( arg ) ) - end - Count = Count + 1 --- if Count % self.YieldInterval == 0 then --- coroutine.yield( false ) --- end - end - return true - end - --- local co = coroutine.create( CoRoutine ) - local co = CoRoutine - - local function Schedule() - --- local status, res = coroutine.resume( co ) - local status, res = co() - self:T3( { status, res } ) - - if status == false then - error( res ) - end - if res == false then - return true -- resume next time the loop - end - - return false - end - - --self.CallScheduler:Schedule( self, Schedule, {}, self.TimeInterval, self.TimeInterval, 0 ) - Schedule() - - return self -end - - ------ Iterate the SET_BASE and call an interator function for each **alive** unit, providing the Unit and optional parameters. ----- @param #SET_BASE self ----- @param #function IteratorFunction The function that will be called when there is an alive unit in the SET_BASE. The function needs to accept a UNIT parameter. ----- @return #SET_BASE self ---function SET_BASE:ForEachDCSUnitAlive( IteratorFunction, ... ) --- self:F3( arg ) --- --- self:ForEach( IteratorFunction, arg, self.DCSUnitsAlive ) --- --- return self ---end --- ------ Iterate the SET_BASE and call an interator function for each **alive** player, providing the Unit of the player and optional parameters. ----- @param #SET_BASE self ----- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_BASE. The function needs to accept a UNIT parameter. ----- @return #SET_BASE self ---function SET_BASE:ForEachPlayer( IteratorFunction, ... ) --- self:F3( arg ) --- --- self:ForEach( IteratorFunction, arg, self.PlayersAlive ) --- --- return self ---end --- --- ------ Iterate the SET_BASE and call an interator function for each client, providing the Client to the function and optional parameters. ----- @param #SET_BASE self ----- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_BASE. The function needs to accept a CLIENT parameter. ----- @return #SET_BASE self ---function SET_BASE:ForEachClient( IteratorFunction, ... ) --- self:F3( arg ) --- --- self:ForEach( IteratorFunction, arg, self.Clients ) --- --- return self ---end - - ---- Decides whether to include the Object --- @param #SET_BASE self --- @param #table Object --- @return #SET_BASE self -function SET_BASE:IsIncludeObject( Object ) - self:F3( Object ) - - return true -end - ---- 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 - - ---- @type SET_GROUP --- @extends Core.Set#SET_BASE - ---- # 2) 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 constructor --- --- 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. --- --- === --- @field #SET_GROUP SET_GROUP -SET_GROUP = { - ClassName = "SET_GROUP", - Filter = { - Coalitions = nil, - Categories = nil, - Countries = nil, - GroupPrefixes = nil, - }, - FilterMeta = { - Coalitions = { - red = coalition.side.RED, - blue = coalition.side.BLUE, - neutral = coalition.side.NEUTRAL, - }, - Categories = { - plane = Group.Category.AIRPLANE, - helicopter = Group.Category.HELICOPTER, - ground = Group.Category.GROUND_UNIT, - ship = Group.Category.SHIP, - structure = Group.Category.STRUCTURE, - }, - }, -} - - ---- Creates a new SET_GROUP object, building a set of groups belonging to a coalitions, categories, countries, types or with defined prefix names. --- @param #SET_GROUP self --- @return #SET_GROUP --- @usage --- -- Define a new SET_GROUP Object. This DBObject will contain a reference to all alive GROUPS. --- DBObject = SET_GROUP:New() -function SET_GROUP:New() - - -- Inherits from BASE - local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.GROUPS ) ) - - return self -end - ---- Add GROUP(s) to SET_GROUP. --- @param Core.Set#SET_GROUP self --- @param #string AddGroupNames A single name or an array of GROUP names. --- @return self -function SET_GROUP:AddGroupsByName( AddGroupNames ) - - local AddGroupNamesArray = ( type( AddGroupNames ) == "table" ) and AddGroupNames or { AddGroupNames } - - for AddGroupID, AddGroupName in pairs( AddGroupNamesArray ) do - self:Add( AddGroupName, GROUP:FindByName( AddGroupName ) ) - end - - return self -end - ---- Remove GROUP(s) from SET_GROUP. --- @param Core.Set#SET_GROUP self --- @param Wrapper.Group#GROUP RemoveGroupNames A single name or an array of GROUP names. --- @return self -function SET_GROUP:RemoveGroupsByName( RemoveGroupNames ) - - local RemoveGroupNamesArray = ( type( RemoveGroupNames ) == "table" ) and RemoveGroupNames or { RemoveGroupNames } - - for RemoveGroupID, RemoveGroupName in pairs( RemoveGroupNamesArray ) do - self:Remove( RemoveGroupName.GroupName ) - end - - return self -end - - - - ---- Finds a Group based on the Group Name. --- @param #SET_GROUP self --- @param #string GroupName --- @return Wrapper.Group#GROUP The found Group. -function SET_GROUP:FindGroup( GroupName ) - - local GroupFound = self.Set[GroupName] - return GroupFound -end - - - ---- Builds a set of groups of coalitions. --- Possible current coalitions are red, blue and neutral. --- @param #SET_GROUP self --- @param #string Coalitions Can take the following values: "red", "blue", "neutral". --- @return #SET_GROUP self -function SET_GROUP:FilterCoalitions( Coalitions ) - if not self.Filter.Coalitions then - self.Filter.Coalitions = {} - end - if type( Coalitions ) ~= "table" then - Coalitions = { Coalitions } - end - for CoalitionID, Coalition in pairs( Coalitions ) do - self.Filter.Coalitions[Coalition] = Coalition - end - return self -end - - ---- Builds a set of groups out of categories. --- Possible current categories are plane, helicopter, ground, ship. --- @param #SET_GROUP self --- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship". --- @return #SET_GROUP self -function SET_GROUP:FilterCategories( Categories ) - if not self.Filter.Categories then - self.Filter.Categories = {} - end - if type( Categories ) ~= "table" then - Categories = { Categories } - end - for CategoryID, Category in pairs( Categories ) do - self.Filter.Categories[Category] = Category - end - return self -end - ---- Builds a set of groups of defined countries. --- Possible current countries are those known within DCS world. --- @param #SET_GROUP self --- @param #string Countries Can take those country strings known within DCS world. --- @return #SET_GROUP self -function SET_GROUP:FilterCountries( Countries ) - if not self.Filter.Countries then - self.Filter.Countries = {} - end - if type( Countries ) ~= "table" then - Countries = { Countries } - end - for CountryID, Country in pairs( Countries ) do - self.Filter.Countries[Country] = Country - end - return self -end - - ---- Builds a set of groups of defined GROUP prefixes. --- All the groups starting with the given prefixes will be included within the set. --- @param #SET_GROUP self --- @param #string Prefixes The prefix of which the group name starts with. --- @return #SET_GROUP self -function SET_GROUP:FilterPrefixes( Prefixes ) - if not self.Filter.GroupPrefixes then - self.Filter.GroupPrefixes = {} - end - if type( Prefixes ) ~= "table" then - Prefixes = { Prefixes } - end - for PrefixID, Prefix in pairs( Prefixes ) do - self.Filter.GroupPrefixes[Prefix] = Prefix - end - return self -end - - ---- Starts the filtering. --- @param #SET_GROUP self --- @return #SET_GROUP self -function SET_GROUP:FilterStart() - - if _DATABASE then - self:_FilterStart() - end - - - - return self -end - ---- Handles the Database to check on an event (birth) that the Object was added in the Database. --- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! --- @param #SET_GROUP self --- @param Core.Event#EVENTDATA Event --- @return #string The name of the GROUP --- @return #table The GROUP -function SET_GROUP:AddInDatabase( Event ) - self:F3( { Event } ) - - if Event.IniObjectCategory == 1 then - if not self.Database[Event.IniDCSGroupName] then - self.Database[Event.IniDCSGroupName] = GROUP:Register( Event.IniDCSGroupName ) - self:T3( self.Database[Event.IniDCSGroupName] ) - end - end - - return Event.IniDCSGroupName, self.Database[Event.IniDCSGroupName] -end - ---- Handles the Database to check on any event that Object exists in the Database. --- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! --- @param #SET_GROUP self --- @param Core.Event#EVENTDATA Event --- @return #string The name of the GROUP --- @return #table The GROUP -function SET_GROUP:FindInDatabase( Event ) - self:F3( { Event } ) - - return Event.IniDCSGroupName, self.Database[Event.IniDCSGroupName] -end - ---- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP, providing the GROUP and optional parameters. --- @param #SET_GROUP self --- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. --- @return #SET_GROUP self -function SET_GROUP:ForEachGroup( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set ) - - return self -end - ---- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence completely in a @{Zone}, providing the GROUP and optional parameters to the called function. --- @param #SET_GROUP self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. --- @return #SET_GROUP self -function SET_GROUP:ForEachGroupCompletelyInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set, - --- @param Core.Zone#ZONE_BASE ZoneObject - -- @param Wrapper.Group#GROUP GroupObject - function( ZoneObject, GroupObject ) - if GroupObject:IsCompletelyInZone( ZoneObject ) then - return true - else - return false - end - end, { ZoneObject } ) - - return self -end - ---- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence partly in a @{Zone}, providing the GROUP and optional parameters to the called function. --- @param #SET_GROUP self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. --- @return #SET_GROUP self -function SET_GROUP:ForEachGroupPartlyInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set, - --- @param Core.Zone#ZONE_BASE ZoneObject - -- @param Wrapper.Group#GROUP GroupObject - function( ZoneObject, GroupObject ) - if GroupObject:IsPartlyInZone( ZoneObject ) then - return true - else - return false - end - end, { ZoneObject } ) - - return self -end - ---- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence not in a @{Zone}, providing the GROUP and optional parameters to the called function. --- @param #SET_GROUP self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. --- @return #SET_GROUP self -function SET_GROUP:ForEachGroupNotInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set, - --- @param Core.Zone#ZONE_BASE ZoneObject - -- @param Wrapper.Group#GROUP GroupObject - function( ZoneObject, GroupObject ) - if GroupObject:IsNotInZone( ZoneObject ) then - return true - else - return false - end - end, { ZoneObject } ) - - return self -end - - ------ Iterate the SET_GROUP and call an interator function for each **alive** player, providing the Group of the player and optional parameters. ----- @param #SET_GROUP self ----- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_GROUP. The function needs to accept a GROUP parameter. ----- @return #SET_GROUP self ---function SET_GROUP:ForEachPlayer( IteratorFunction, ... ) --- self:F2( arg ) --- --- self:ForEach( IteratorFunction, arg, self.PlayersAlive ) --- --- return self ---end --- --- ------ Iterate the SET_GROUP and call an interator function for each client, providing the Client to the function and optional parameters. ----- @param #SET_GROUP self ----- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_GROUP. The function needs to accept a CLIENT parameter. ----- @return #SET_GROUP self ---function SET_GROUP:ForEachClient( IteratorFunction, ... ) --- self:F2( arg ) --- --- self:ForEach( IteratorFunction, arg, self.Clients ) --- --- return self ---end - - ---- --- @param #SET_GROUP self --- @param Wrapper.Group#GROUP MooseGroup --- @return #SET_GROUP self -function SET_GROUP:IsIncludeObject( MooseGroup ) - self:F2( MooseGroup ) - local MooseGroupInclude = true - - if self.Filter.Coalitions then - local MooseGroupCoalition = false - for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do - self:T3( { "Coalition:", MooseGroup:GetCoalition(), self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) - if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == MooseGroup:GetCoalition() then - MooseGroupCoalition = true - end - end - MooseGroupInclude = MooseGroupInclude and MooseGroupCoalition - end - - if self.Filter.Categories then - local MooseGroupCategory = false - for CategoryID, CategoryName in pairs( self.Filter.Categories ) do - self:T3( { "Category:", MooseGroup:GetCategory(), self.FilterMeta.Categories[CategoryName], CategoryName } ) - if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == MooseGroup:GetCategory() then - MooseGroupCategory = true - end - end - MooseGroupInclude = MooseGroupInclude and MooseGroupCategory - end - - if self.Filter.Countries then - local MooseGroupCountry = false - for CountryID, CountryName in pairs( self.Filter.Countries ) do - self:T3( { "Country:", MooseGroup:GetCountry(), CountryName } ) - if country.id[CountryName] == MooseGroup:GetCountry() then - MooseGroupCountry = true - end - end - MooseGroupInclude = MooseGroupInclude and MooseGroupCountry - end - - if self.Filter.GroupPrefixes then - local MooseGroupPrefix = false - for GroupPrefixId, GroupPrefix in pairs( self.Filter.GroupPrefixes ) do - self:T3( { "Prefix:", string.find( MooseGroup:GetName(), GroupPrefix, 1 ), GroupPrefix } ) - if string.find( MooseGroup:GetName(), GroupPrefix, 1 ) then - MooseGroupPrefix = true - end - end - MooseGroupInclude = MooseGroupInclude and MooseGroupPrefix - end - - self:T2( MooseGroupInclude ) - return MooseGroupInclude -end - ---- @type SET_UNIT --- @extends Core.Set#SET_BASE - ---- # 3) SET_UNIT class, extends @{Set#SET_BASE} --- --- Mission designers can use the SET_UNIT class to build sets of units belonging to certain: --- --- * Coalitions --- * Categories --- * Countries --- * Unit types --- * Starting with certain prefix strings. --- --- ## 3.1) SET_UNIT constructor --- --- Create a new SET_UNIT object with the @{#SET_UNIT.New} method: --- --- * @{#SET_UNIT.New}: Creates a new SET_UNIT object. --- --- ## 3.2) Add or Remove UNIT(s) from SET_UNIT --- --- 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. --- --- ## 3.5 ) SET_UNIT atomic methods --- --- Various methods exist for a SET_UNIT to perform actions or calculations and retrieve results from the SET_UNIT: --- --- * @{#SET_UNIT.GetTypeNames}(): Retrieve the type names of the @{Unit}s in the SET, delimited by a comma. --- --- === --- @field #SET_UNIT SET_UNIT -SET_UNIT = { - ClassName = "SET_UNIT", - Units = {}, - Filter = { - Coalitions = nil, - Categories = nil, - Types = nil, - Countries = nil, - UnitPrefixes = nil, - }, - FilterMeta = { - Coalitions = { - red = coalition.side.RED, - blue = coalition.side.BLUE, - neutral = coalition.side.NEUTRAL, - }, - Categories = { - plane = Unit.Category.AIRPLANE, - helicopter = Unit.Category.HELICOPTER, - ground = Unit.Category.GROUND_UNIT, - ship = Unit.Category.SHIP, - structure = Unit.Category.STRUCTURE, - }, - }, -} - - ---- Creates a new SET_UNIT object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. --- @param #SET_UNIT self --- @return #SET_UNIT --- @usage --- -- Define a new SET_UNIT Object. This DBObject will contain a reference to all alive Units. --- DBObject = SET_UNIT:New() -function SET_UNIT:New() - - -- Inherits from BASE - local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.UNITS ) ) - - return self -end - ---- Add UNIT(s) to SET_UNIT. --- @param #SET_UNIT self --- @param #string AddUnit A single UNIT. --- @return #SET_UNIT self -function SET_UNIT:AddUnit( AddUnit ) - self:F2( AddUnit:GetName() ) - - self:Add( AddUnit:GetName(), AddUnit ) - - return self -end - - ---- Add UNIT(s) to SET_UNIT. --- @param #SET_UNIT self --- @param #string AddUnitNames A single name or an array of UNIT names. --- @return #SET_UNIT self -function SET_UNIT:AddUnitsByName( AddUnitNames ) - - local AddUnitNamesArray = ( type( AddUnitNames ) == "table" ) and AddUnitNames or { AddUnitNames } - - self:T( AddUnitNamesArray ) - for AddUnitID, AddUnitName in pairs( AddUnitNamesArray ) do - self:Add( AddUnitName, UNIT:FindByName( AddUnitName ) ) - end - - return self -end - ---- Remove UNIT(s) from SET_UNIT. --- @param Core.Set#SET_UNIT self --- @param Wrapper.Unit#UNIT RemoveUnitNames A single name or an array of UNIT names. --- @return self -function SET_UNIT:RemoveUnitsByName( RemoveUnitNames ) - - local RemoveUnitNamesArray = ( type( RemoveUnitNames ) == "table" ) and RemoveUnitNames or { RemoveUnitNames } - - for RemoveUnitID, RemoveUnitName in pairs( RemoveUnitNamesArray ) do - self:Remove( RemoveUnitName ) - end - - return self -end - - ---- Finds a Unit based on the Unit Name. --- @param #SET_UNIT self --- @param #string UnitName --- @return Wrapper.Unit#UNIT The found Unit. -function SET_UNIT:FindUnit( UnitName ) - - local UnitFound = self.Set[UnitName] - return UnitFound -end - - - ---- Builds a set of units of coalitions. --- Possible current coalitions are red, blue and neutral. --- @param #SET_UNIT self --- @param #string Coalitions Can take the following values: "red", "blue", "neutral". --- @return #SET_UNIT self -function SET_UNIT:FilterCoalitions( Coalitions ) - if not self.Filter.Coalitions then - self.Filter.Coalitions = {} - end - if type( Coalitions ) ~= "table" then - Coalitions = { Coalitions } - end - for CoalitionID, Coalition in pairs( Coalitions ) do - self.Filter.Coalitions[Coalition] = Coalition - end - return self -end - - ---- Builds a set of units out of categories. --- Possible current categories are plane, helicopter, ground, ship. --- @param #SET_UNIT self --- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship". --- @return #SET_UNIT self -function SET_UNIT:FilterCategories( Categories ) - if not self.Filter.Categories then - self.Filter.Categories = {} - end - if type( Categories ) ~= "table" then - Categories = { Categories } - end - for CategoryID, Category in pairs( Categories ) do - self.Filter.Categories[Category] = Category - end - return self -end - - ---- Builds a set of units of defined unit types. --- Possible current types are those types known within DCS world. --- @param #SET_UNIT self --- @param #string Types Can take those type strings known within DCS world. --- @return #SET_UNIT self -function SET_UNIT:FilterTypes( Types ) - if not self.Filter.Types then - self.Filter.Types = {} - end - if type( Types ) ~= "table" then - Types = { Types } - end - for TypeID, Type in pairs( Types ) do - self.Filter.Types[Type] = Type - end - return self -end - - ---- Builds a set of units of defined countries. --- Possible current countries are those known within DCS world. --- @param #SET_UNIT self --- @param #string Countries Can take those country strings known within DCS world. --- @return #SET_UNIT self -function SET_UNIT:FilterCountries( Countries ) - if not self.Filter.Countries then - self.Filter.Countries = {} - end - if type( Countries ) ~= "table" then - Countries = { Countries } - end - for CountryID, Country in pairs( Countries ) do - self.Filter.Countries[Country] = Country - end - return self -end - - ---- Builds a set of units of defined unit prefixes. --- All the units starting with the given prefixes will be included within the set. --- @param #SET_UNIT self --- @param #string Prefixes The prefix of which the unit name starts with. --- @return #SET_UNIT self -function SET_UNIT:FilterPrefixes( Prefixes ) - if not self.Filter.UnitPrefixes then - self.Filter.UnitPrefixes = {} - end - if type( Prefixes ) ~= "table" then - Prefixes = { Prefixes } - end - for PrefixID, Prefix in pairs( Prefixes ) do - self.Filter.UnitPrefixes[Prefix] = Prefix - end - return self -end - ---- Builds a set of units having a radar of give types. --- All the units having a radar of a given type will be included within the set. --- @param #SET_UNIT self --- @param #table RadarTypes The radar types. --- @return #SET_UNIT self -function SET_UNIT:FilterHasRadar( RadarTypes ) - - self.Filter.RadarTypes = self.Filter.RadarTypes or {} - if type( RadarTypes ) ~= "table" then - RadarTypes = { RadarTypes } - end - for RadarTypeID, RadarType in pairs( RadarTypes ) do - self.Filter.RadarTypes[RadarType] = RadarType - end - return self -end - ---- Builds a set of SEADable units. --- @param #SET_UNIT self --- @return #SET_UNIT self -function SET_UNIT:FilterHasSEAD() - - self.Filter.SEAD = true - return self -end - - - ---- Starts the filtering. --- @param #SET_UNIT self --- @return #SET_UNIT self -function SET_UNIT:FilterStart() - - if _DATABASE then - self:_FilterStart() - end - - return self -end - ---- Handles the Database to check on an event (birth) that the Object was added in the Database. --- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! --- @param #SET_UNIT self --- @param Core.Event#EVENTDATA Event --- @return #string The name of the UNIT --- @return #table The UNIT -function SET_UNIT:AddInDatabase( Event ) - self:F3( { Event } ) - - if Event.IniObjectCategory == 1 then - if not self.Database[Event.IniDCSUnitName] then - self.Database[Event.IniDCSUnitName] = UNIT:Register( Event.IniDCSUnitName ) - self:T3( self.Database[Event.IniDCSUnitName] ) - end - end - - return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] -end - ---- Handles the Database to check on any event that Object exists in the Database. --- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! --- @param #SET_UNIT self --- @param Core.Event#EVENTDATA Event --- @return #string The name of the UNIT --- @return #table The UNIT -function SET_UNIT:FindInDatabase( Event ) - self:E( { Event.IniDCSUnitName, self.Set[Event.IniDCSUnitName], Event } ) - - - return Event.IniDCSUnitName, self.Set[Event.IniDCSUnitName] -end - ---- Iterate the SET_UNIT and call an interator function for each **alive** UNIT, providing the UNIT and optional parameters. --- @param #SET_UNIT self --- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the SET_UNIT. The function needs to accept a UNIT parameter. --- @return #SET_UNIT self -function SET_UNIT:ForEachUnit( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set ) - - return self -end - ---- Iterate the SET_UNIT and call an iterator function for each **alive** UNIT presence completely in a @{Zone}, providing the UNIT and optional parameters to the called function. --- @param #SET_UNIT self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the SET_UNIT. The function needs to accept a UNIT parameter. --- @return #SET_UNIT self -function SET_UNIT:ForEachUnitCompletelyInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set, - --- @param Core.Zone#ZONE_BASE ZoneObject - -- @param Wrapper.Unit#UNIT UnitObject - function( ZoneObject, UnitObject ) - if UnitObject:IsCompletelyInZone( ZoneObject ) then - return true - else - return false - end - end, { ZoneObject } ) - - return self -end - ---- Iterate the SET_UNIT and call an iterator function for each **alive** UNIT presence not in a @{Zone}, providing the UNIT and optional parameters to the called function. --- @param #SET_UNIT self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the SET_UNIT. The function needs to accept a UNIT parameter. --- @return #SET_UNIT self -function SET_UNIT:ForEachUnitNotInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self.Set, - --- @param Core.Zone#ZONE_BASE ZoneObject - -- @param Wrapper.Unit#UNIT UnitObject - function( ZoneObject, UnitObject ) - if UnitObject:IsNotInZone( ZoneObject ) then - return true - else - return false - end - end, { ZoneObject } ) - - return self -end - ---- Returns map of unit types. --- @param #SET_UNIT self --- @return #map<#string,#number> A map of the unit types found. The key is the UnitTypeName and the value is the amount of unit types found. -function SET_UNIT:GetUnitTypes() - self:F2() - - local MT = {} -- Message Text - local UnitTypes = {} - - for UnitID, UnitData in pairs( self:GetSet() ) do - local TextUnit = UnitData -- Wrapper.Unit#UNIT - if TextUnit:IsAlive() then - local UnitType = TextUnit:GetTypeName() - - if not UnitTypes[UnitType] then - UnitTypes[UnitType] = 1 - else - UnitTypes[UnitType] = UnitTypes[UnitType] + 1 - end - end - end - - for UnitTypeID, UnitType in pairs( UnitTypes ) do - MT[#MT+1] = UnitType .. " of " .. UnitTypeID - end - - return UnitTypes -end - - ---- Returns a comma separated string of the unit types with a count in the @{Set}. --- @param #SET_UNIT self --- @return #string The unit types string -function SET_UNIT:GetUnitTypesText() - self:F2() - - local MT = {} -- Message Text - local UnitTypes = self:GetUnitTypes() - - for UnitTypeID, UnitType in pairs( UnitTypes ) do - MT[#MT+1] = UnitType .. " of " .. UnitTypeID - end - - return table.concat( MT, ", " ) -end - ---- Returns map of unit threat levels. --- @param #SET_UNIT self --- @return #table. -function SET_UNIT:GetUnitThreatLevels() - self:F2() - - local UnitThreatLevels = {} - - for UnitID, UnitData in pairs( self:GetSet() ) do - local ThreatUnit = UnitData -- Wrapper.Unit#UNIT - if ThreatUnit:IsAlive() then - local UnitThreatLevel, UnitThreatLevelText = ThreatUnit:GetThreatLevel() - local ThreatUnitName = ThreatUnit:GetName() - - UnitThreatLevels[UnitThreatLevel] = UnitThreatLevels[UnitThreatLevel] or {} - UnitThreatLevels[UnitThreatLevel].UnitThreatLevelText = UnitThreatLevelText - UnitThreatLevels[UnitThreatLevel].Units = UnitThreatLevels[UnitThreatLevel].Units or {} - UnitThreatLevels[UnitThreatLevel].Units[ThreatUnitName] = ThreatUnit - end - end - - return UnitThreatLevels -end - ---- Calculate the maxium A2G threat level of the SET_UNIT. --- @param #SET_UNIT self -function SET_UNIT:CalculateThreatLevelA2G() - - local MaxThreatLevelA2G = 0 - for UnitName, UnitData in pairs( self:GetSet() ) do - local ThreatUnit = UnitData -- Wrapper.Unit#UNIT - local ThreatLevelA2G = ThreatUnit:GetThreatLevel() - if ThreatLevelA2G > MaxThreatLevelA2G then - MaxThreatLevelA2G = ThreatLevelA2G - end - end - - self:T3( MaxThreatLevelA2G ) - return MaxThreatLevelA2G - -end - - ---- Returns if the @{Set} has targets having a radar (of a given type). --- @param #SET_UNIT self --- @param Dcs.DCSWrapper.Unit#Unit.RadarType RadarType --- @return #number The amount of radars in the Set with the given type -function SET_UNIT:HasRadar( RadarType ) - self:F2( RadarType ) - - local RadarCount = 0 - for UnitID, UnitData in pairs( self:GetSet()) do - local UnitSensorTest = UnitData -- Wrapper.Unit#UNIT - local HasSensors - if RadarType then - HasSensors = UnitSensorTest:HasSensors( Unit.SensorType.RADAR, RadarType ) - else - HasSensors = UnitSensorTest:HasSensors( Unit.SensorType.RADAR ) - end - self:T3(HasSensors) - if HasSensors then - RadarCount = RadarCount + 1 - end - end - - return RadarCount -end - ---- Returns if the @{Set} has targets that can be SEADed. --- @param #SET_UNIT self --- @return #number The amount of SEADable units in the Set -function SET_UNIT:HasSEAD() - self:F2() - - local SEADCount = 0 - for UnitID, UnitData in pairs( self:GetSet()) do - local UnitSEAD = UnitData -- Wrapper.Unit#UNIT - if UnitSEAD:IsAlive() then - local UnitSEADAttributes = UnitSEAD:GetDesc().attributes - - local HasSEAD = UnitSEAD:HasSEAD() - - self:T3(HasSEAD) - if HasSEAD then - SEADCount = SEADCount + 1 - end - end - end - - return SEADCount -end - ---- Returns if the @{Set} has ground targets. --- @param #SET_UNIT self --- @return #number The amount of ground targets in the Set. -function SET_UNIT:HasGroundUnits() - self:F2() - - local GroundUnitCount = 0 - for UnitID, UnitData in pairs( self:GetSet()) do - local UnitTest = UnitData -- Wrapper.Unit#UNIT - if UnitTest:IsGround() then - GroundUnitCount = GroundUnitCount + 1 - end - end - - return GroundUnitCount -end - ---- Returns if the @{Set} has friendly ground units. --- @param #SET_UNIT self --- @return #number The amount of ground targets in the Set. -function SET_UNIT:HasFriendlyUnits( FriendlyCoalition ) - self:F2() - - local FriendlyUnitCount = 0 - for UnitID, UnitData in pairs( self:GetSet()) do - local UnitTest = UnitData -- Wrapper.Unit#UNIT - if UnitTest:IsFriendly( FriendlyCoalition ) then - FriendlyUnitCount = FriendlyUnitCount + 1 - end - end - - return FriendlyUnitCount -end - - - ------ Iterate the SET_UNIT and call an interator function for each **alive** player, providing the Unit of the player and optional parameters. ----- @param #SET_UNIT self ----- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_UNIT. The function needs to accept a UNIT parameter. ----- @return #SET_UNIT self ---function SET_UNIT:ForEachPlayer( IteratorFunction, ... ) --- self:F2( arg ) --- --- self:ForEach( IteratorFunction, arg, self.PlayersAlive ) --- --- return self ---end --- --- ------ Iterate the SET_UNIT and call an interator function for each client, providing the Client to the function and optional parameters. ----- @param #SET_UNIT self ----- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_UNIT. The function needs to accept a CLIENT parameter. ----- @return #SET_UNIT self ---function SET_UNIT:ForEachClient( IteratorFunction, ... ) --- self:F2( arg ) --- --- self:ForEach( IteratorFunction, arg, self.Clients ) --- --- return self ---end - - ---- --- @param #SET_UNIT self --- @param Wrapper.Unit#UNIT MUnit --- @return #SET_UNIT self -function SET_UNIT:IsIncludeObject( MUnit ) - self:F2( MUnit ) - local MUnitInclude = true - - if self.Filter.Coalitions then - local MUnitCoalition = false - for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do - self:T3( { "Coalition:", MUnit:GetCoalition(), self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) - if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == MUnit:GetCoalition() then - MUnitCoalition = true - end - end - MUnitInclude = MUnitInclude and MUnitCoalition - end - - if self.Filter.Categories then - local MUnitCategory = false - for CategoryID, CategoryName in pairs( self.Filter.Categories ) do - self:T3( { "Category:", MUnit:GetDesc().category, self.FilterMeta.Categories[CategoryName], CategoryName } ) - if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == MUnit:GetDesc().category then - MUnitCategory = true - end - end - MUnitInclude = MUnitInclude and MUnitCategory - end - - if self.Filter.Types then - local MUnitType = false - for TypeID, TypeName in pairs( self.Filter.Types ) do - self:T3( { "Type:", MUnit:GetTypeName(), TypeName } ) - if TypeName == MUnit:GetTypeName() then - MUnitType = true - end - end - MUnitInclude = MUnitInclude and MUnitType - end - - if self.Filter.Countries then - local MUnitCountry = false - for CountryID, CountryName in pairs( self.Filter.Countries ) do - self:T3( { "Country:", MUnit:GetCountry(), CountryName } ) - if country.id[CountryName] == MUnit:GetCountry() then - MUnitCountry = true - end - end - MUnitInclude = MUnitInclude and MUnitCountry - end - - if self.Filter.UnitPrefixes then - local MUnitPrefix = false - for UnitPrefixId, UnitPrefix in pairs( self.Filter.UnitPrefixes ) do - self:T3( { "Prefix:", string.find( MUnit:GetName(), UnitPrefix, 1 ), UnitPrefix } ) - if string.find( MUnit:GetName(), UnitPrefix, 1 ) then - MUnitPrefix = true - end - end - MUnitInclude = MUnitInclude and MUnitPrefix - end - - if self.Filter.RadarTypes then - local MUnitRadar = false - for RadarTypeID, RadarType in pairs( self.Filter.RadarTypes ) do - self:T3( { "Radar:", RadarType } ) - if MUnit:HasSensors( Unit.SensorType.RADAR, RadarType ) == true then - if MUnit:GetRadar() == true then -- This call is necessary to evaluate the SEAD capability. - self:T3( "RADAR Found" ) - end - MUnitRadar = true - end - end - MUnitInclude = MUnitInclude and MUnitRadar - end - - if self.Filter.SEAD then - local MUnitSEAD = false - if MUnit:HasSEAD() == true then - self:T3( "SEAD Found" ) - MUnitSEAD = true - end - MUnitInclude = MUnitInclude and MUnitSEAD - end - - self:T2( MUnitInclude ) - return MUnitInclude -end - - ---- Retrieve the type names of the @{Unit}s in the SET, delimited by an optional delimiter. --- @param #SET_UNIT self --- @param #string Delimiter (optional) The delimiter, which is default a comma. --- @return #string The types of the @{Unit}s delimited. -function SET_UNIT:GetTypeNames( Delimiter ) - - Delimiter = Delimiter or ", " - local TypeReport = REPORT:New() - local Types = {} - - for UnitName, UnitData in pairs( self:GetSet() ) do - - local Unit = UnitData -- Wrapper.Unit#UNIT - local UnitTypeName = Unit:GetTypeName() - - if not Types[UnitTypeName] then - Types[UnitTypeName] = UnitTypeName - TypeReport:Add( UnitTypeName ) - end - end - - return TypeReport:Text( Delimiter ) -end - - ---- SET_CLIENT - - ---- @type SET_CLIENT --- @extends Core.Set#SET_BASE - - - ---- # 4) SET_CLIENT class, extends @{Set#SET_BASE} --- --- Mission designers can use the @{Set#SET_CLIENT} class to build sets of units belonging to certain: --- --- * Coalitions --- * Categories --- * Countries --- * Client types --- * Starting with certain prefix strings. --- --- ## 4.1) SET_CLIENT constructor --- --- Create a new SET_CLIENT object with the @{#SET_CLIENT.New} method: --- --- * @{#SET_CLIENT.New}: Creates a new SET_CLIENT object. --- --- ## 4.2) Add or Remove CLIENT(s) from SET_CLIENT --- --- CLIENTs can be added and removed using the @{Set#SET_CLIENT.AddClientsByName} and @{Set#SET_CLIENT.RemoveClientsByName} respectively. --- These methods take a single CLIENT name or an array of CLIENT names to be added or removed from SET_CLIENT. --- --- ## 4.3) SET_CLIENT filter criteria --- --- You can set filter criteria to define the set of clients within the SET_CLIENT. --- Filter criteria are defined by: --- --- * @{#SET_CLIENT.FilterCoalitions}: Builds the SET_CLIENT with the clients belonging to the coalition(s). --- * @{#SET_CLIENT.FilterCategories}: Builds the SET_CLIENT with the clients belonging to the category(ies). --- * @{#SET_CLIENT.FilterTypes}: Builds the SET_CLIENT with the clients belonging to the client type(s). --- * @{#SET_CLIENT.FilterCountries}: Builds the SET_CLIENT with the clients belonging to the country(ies). --- * @{#SET_CLIENT.FilterPrefixes}: Builds the SET_CLIENT with the clients starting with the same prefix string(s). --- --- Once the filter criteria have been set for the SET_CLIENT, you can start filtering using: --- --- * @{#SET_CLIENT.FilterStart}: Starts the filtering of the clients within the SET_CLIENT. --- --- Planned filter criteria within development are (so these are not yet available): --- --- * @{#SET_CLIENT.FilterZones}: Builds the SET_CLIENT with the clients within a @{Zone#ZONE}. --- --- ## 4.4) SET_CLIENT iterators --- --- Once the filters have been defined and the SET_CLIENT has been built, you can iterate the SET_CLIENT with the available iterator methods. --- The iterator methods will walk the SET_CLIENT set, and call for each element within the set a function that you provide. --- The following iterator methods are currently available within the SET_CLIENT: --- --- * @{#SET_CLIENT.ForEachClient}: Calls a function for each alive client it finds within the SET_CLIENT. --- --- === --- @field #SET_CLIENT SET_CLIENT -SET_CLIENT = { - ClassName = "SET_CLIENT", - Clients = {}, - Filter = { - Coalitions = nil, - Categories = nil, - Types = nil, - Countries = nil, - ClientPrefixes = nil, - }, - FilterMeta = { - Coalitions = { - red = coalition.side.RED, - blue = coalition.side.BLUE, - neutral = coalition.side.NEUTRAL, - }, - 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 - ---- @type SET_AIRBASE --- @extends Core.Set#SET_BASE - ---- # 5) SET_AIRBASE class, extends @{Set#SET_BASE} --- --- Mission designers can use the @{Set#SET_AIRBASE} class to build sets of airbases optionally belonging to certain: --- --- * Coalitions --- --- ## 5.1) SET_AIRBASE constructor --- --- Create a new SET_AIRBASE object with the @{#SET_AIRBASE.New} method: --- --- * @{#SET_AIRBASE.New}: Creates a new SET_AIRBASE object. --- --- ## 5.2) Add or Remove AIRBASEs from SET_AIRBASE --- --- AIRBASEs can be added and removed using the @{Set#SET_AIRBASE.AddAirbasesByName} and @{Set#SET_AIRBASE.RemoveAirbasesByName} respectively. --- These methods take a single AIRBASE name or an array of AIRBASE names to be added or removed from SET_AIRBASE. --- --- ## 5.3) SET_AIRBASE filter criteria --- --- You can set filter criteria to define the set of clients within the SET_AIRBASE. --- Filter criteria are defined by: --- --- * @{#SET_AIRBASE.FilterCoalitions}: Builds the SET_AIRBASE with the airbases belonging to the coalition(s). --- --- Once the filter criteria have been set for the SET_AIRBASE, you can start filtering using: --- --- * @{#SET_AIRBASE.FilterStart}: Starts the filtering of the airbases within the SET_AIRBASE. --- --- ## 5.4) SET_AIRBASE iterators --- --- Once the filters have been defined and the SET_AIRBASE has been built, you can iterate the SET_AIRBASE with the available iterator methods. --- The iterator methods will walk the SET_AIRBASE set, and call for each airbase within the set a function that you provide. --- The following iterator methods are currently available within the SET_AIRBASE: --- --- * @{#SET_AIRBASE.ForEachAirbase}: Calls a function for each airbase it finds within the SET_AIRBASE. --- --- === --- @field #SET_AIRBASE SET_AIRBASE -SET_AIRBASE = { - ClassName = "SET_AIRBASE", - Airbases = {}, - Filter = { - Coalitions = nil, - }, - FilterMeta = { - Coalitions = { - red = coalition.side.RED, - blue = coalition.side.BLUE, - neutral = coalition.side.NEUTRAL, - }, - 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 ---- **Core** - **POINT\_VEC** classes define an **extensive API** to **manage 3D points** in the simulation space. --- --- 1) @{Point#POINT_VEC3} class, extends @{Base#BASE} --- ================================================== --- The @{Point#POINT_VEC3} class defines a 3D point in the simulator. --- --- **Important Note:** Most of the functions in this section were taken from MIST, and reworked to OO concepts. --- In order to keep the credibility of the the author, I want to emphasize that the of the MIST framework was created by Grimes, who you can find on the Eagle Dynamics Forums. --- --- ## 1.1) POINT_VEC3 constructor --- --- A new POINT_VEC3 instance can be created with: --- --- * @{Point#POINT_VEC3.New}(): a 3D point. --- * @{Point#POINT_VEC3.NewFromVec3}(): a 3D point created from a @{DCSTypes#Vec3}. --- --- ## 1.2) Manupulate the X, Y, Z coordinates of the point --- --- A POINT_VEC3 class works in 3D space. It contains internally an X, Y, Z coordinate. --- Methods exist to manupulate these coordinates. --- --- The current X, Y, Z axis can be retrieved with the methods @{#POINT_VEC3.GetX}(), @{#POINT_VEC3.GetY}(), @{#POINT_VEC3.GetZ}() respectively. --- The methods @{#POINT_VEC3.SetX}(), @{#POINT_VEC3.SetY}(), @{#POINT_VEC3.SetZ}() change the respective axis with a new value. --- The current axis values can be changed by using the methods @{#POINT_VEC3.AddX}(), @{#POINT_VEC3.AddY}(), @{#POINT_VEC3.AddZ}() --- to add or substract a value from the current respective axis value. --- Note that the Set and Add methods return the current POINT_VEC3 object, so these manipulation methods can be chained... For example: --- --- local Vec3 = PointVec3:AddX( 100 ):AddZ( 150 ):GetVec3() --- --- ## 1.3) Create waypoints for routes --- --- A POINT_VEC3 can prepare waypoints for Ground, Air and Naval groups to be embedded into a Route. --- --- --- ## 1.5) Smoke, flare, explode, illuminate --- --- At the point a smoke, flare, explosion and illumination bomb can be triggered. Use the following methods: --- --- ### 1.5.1) Smoke --- --- * @{#POINT_VEC3.Smoke}(): To smoke the point in a certain color. --- * @{#POINT_VEC3.SmokeBlue}(): To smoke the point in blue. --- * @{#POINT_VEC3.SmokeRed}(): To smoke the point in red. --- * @{#POINT_VEC3.SmokeOrange}(): To smoke the point in orange. --- * @{#POINT_VEC3.SmokeWhite}(): To smoke the point in white. --- * @{#POINT_VEC3.SmokeGreen}(): To smoke the point in green. --- --- ### 1.5.2) Flare --- --- * @{#POINT_VEC3.Flare}(): To flare the point in a certain color. --- * @{#POINT_VEC3.FlareRed}(): To flare the point in red. --- * @{#POINT_VEC3.FlareYellow}(): To flare the point in yellow. --- * @{#POINT_VEC3.FlareWhite}(): To flare the point in white. --- * @{#POINT_VEC3.FlareGreen}(): To flare the point in green. --- --- ### 1.5.3) Explode --- --- * @{#POINT_VEC3.Explosion}(): To explode the point with a certain intensity. --- --- ### 1.5.4) Illuminate --- --- * @{#POINT_VEC3.IlluminationBomb}(): To illuminate the point. --- --- --- 2) @{Point#POINT_VEC2} class, extends @{Point#POINT_VEC3} --- ========================================================= --- The @{Point#POINT_VEC2} class defines a 2D point in the simulator. The height coordinate (if needed) will be the land height + an optional added height specified. --- --- 2.1) POINT_VEC2 constructor --- --------------------------- --- A new POINT_VEC2 instance can be created with: --- --- * @{Point#POINT_VEC2.New}(): a 2D point, taking an additional height parameter. --- * @{Point#POINT_VEC2.NewFromVec2}(): a 2D point created from a @{DCSTypes#Vec2}. --- --- ## 1.2) Manupulate the X, Altitude, Y coordinates of the 2D point --- --- A POINT_VEC2 class works in 2D space, with an altitude setting. It contains internally an X, Altitude, Y coordinate. --- Methods exist to manupulate these coordinates. --- --- The current X, Altitude, Y axis can be retrieved with the methods @{#POINT_VEC2.GetX}(), @{#POINT_VEC2.GetAlt}(), @{#POINT_VEC2.GetY}() respectively. --- The methods @{#POINT_VEC2.SetX}(), @{#POINT_VEC2.SetAlt}(), @{#POINT_VEC2.SetY}() change the respective axis with a new value. --- The current Lat(itude), Alt(itude), Lon(gitude) values can also be retrieved with the methods @{#POINT_VEC2.GetLat}(), @{#POINT_VEC2.GetAlt}(), @{#POINT_VEC2.GetLon}() respectively. --- The current axis values can be changed by using the methods @{#POINT_VEC2.AddX}(), @{#POINT_VEC2.AddAlt}(), @{#POINT_VEC2.AddY}() --- to add or substract a value from the current respective axis value. --- Note that the Set and Add methods return the current POINT_VEC2 object, so these manipulation methods can be chained... For example: --- --- local Vec2 = PointVec2:AddX( 100 ):AddY( 2000 ):GetVec2() --- --- === --- --- **API CHANGE HISTORY** --- ====================== --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- Hereby the change log: --- --- 2017-03-03: POINT\_VEC3:**Explosion( ExplosionIntensity )** added. --- 2017-03-03: POINT\_VEC3:**IlluminationBomb()** added. --- --- 2017-02-18: POINT\_VEC3:**NewFromVec2( Vec2, LandHeightAdd )** added. --- --- 2016-08-12: POINT\_VEC3:**Translate( Distance, Angle )** added. --- --- 2016-08-06: Made PointVec3 and Vec3, PointVec2 and Vec2 terminology used in the code consistent. --- --- * Replaced method _Point_Vec3() to **Vec3**() where the code manages a Vec3. Replaced all references to the method. --- * Replaced method _Point_Vec2() to **Vec2**() where the code manages a Vec2. Replaced all references to the method. --- * Replaced method Random_Point_Vec3() to **RandomVec3**() where the code manages a Vec3. Replaced all references to the method. --- . --- === --- --- ### Authors: --- --- * FlightControl : Design & Programming --- --- ### Contributions: --- --- @module Point - ---- The POINT_VEC3 class --- @type POINT_VEC3 --- @field #number x The x coordinate in 3D space. --- @field #number y The y coordinate in 3D space. --- @field #number z The z coordiante in 3D space. --- @field Utilities.Utils#SMOKECOLOR SmokeColor --- @field Utilities.Utils#FLARECOLOR FlareColor --- @field #POINT_VEC3.RoutePointAltType RoutePointAltType --- @field #POINT_VEC3.RoutePointType RoutePointType --- @field #POINT_VEC3.RoutePointAction RoutePointAction --- @extends Core.Base#BASE -POINT_VEC3 = { - ClassName = "POINT_VEC3", - Metric = true, - RoutePointAltType = { - BARO = "BARO", - }, - RoutePointType = { - TakeOffParking = "TakeOffParking", - TurningPoint = "Turning Point", - }, - RoutePointAction = { - FromParkingArea = "From Parking Area", - TurningPoint = "Turning Point", - }, -} - ---- The POINT_VEC2 class --- @type POINT_VEC2 --- @field Dcs.DCSTypes#Distance x The x coordinate in meters. --- @field Dcs.DCSTypes#Distance y the y coordinate in meters. --- @extends Core.Point#POINT_VEC3 -POINT_VEC2 = { - ClassName = "POINT_VEC2", -} - - -do -- POINT_VEC3 - ---- RoutePoint AltTypes --- @type POINT_VEC3.RoutePointAltType --- @field BARO "BARO" - ---- RoutePoint Types --- @type POINT_VEC3.RoutePointType --- @field TakeOffParking "TakeOffParking" --- @field TurningPoint "Turning Point" - ---- RoutePoint Actions --- @type POINT_VEC3.RoutePointAction --- @field FromParkingArea "From Parking Area" --- @field TurningPoint "Turning Point" - --- Constructor. - ---- Create a new POINT_VEC3 object. --- @param #POINT_VEC3 self --- @param Dcs.DCSTypes#Distance x The x coordinate of the Vec3 point, pointing to the North. --- @param Dcs.DCSTypes#Distance y The y coordinate of the Vec3 point, pointing Upwards. --- @param Dcs.DCSTypes#Distance z The z coordinate of the Vec3 point, pointing to the Right. --- @return Core.Point#POINT_VEC3 self -function POINT_VEC3:New( x, y, z ) - - local self = BASE:Inherit( self, BASE:New() ) - self.x = x - self.y = y - self.z = z - - return self -end - ---- Create a new POINT_VEC3 object from Vec2 coordinates. --- @param #POINT_VEC3 self --- @param Dcs.DCSTypes#Vec2 Vec2 The Vec2 point. --- @return Core.Point#POINT_VEC3 self -function POINT_VEC3:NewFromVec2( Vec2, LandHeightAdd ) - - local LandHeight = land.getHeight( Vec2 ) - - LandHeightAdd = LandHeightAdd or 0 - LandHeight = LandHeight + LandHeightAdd - - self = self:New( Vec2.x, LandHeight, Vec2.y ) - - self:F2( self ) - - return self -end - ---- Create a new POINT_VEC3 object from Vec3 coordinates. --- @param #POINT_VEC3 self --- @param Dcs.DCSTypes#Vec3 Vec3 The Vec3 point. --- @return Core.Point#POINT_VEC3 self -function POINT_VEC3:NewFromVec3( Vec3 ) - - self = self:New( Vec3.x, Vec3.y, Vec3.z ) - self:F2( self ) - return self -end - - ---- Return the coordinates of the POINT_VEC3 in Vec3 format. --- @param #POINT_VEC3 self --- @return Dcs.DCSTypes#Vec3 The Vec3 coodinate. -function POINT_VEC3:GetVec3() - return { x = self.x, y = self.y, z = self.z } -end - ---- Return the coordinates of the POINT_VEC3 in Vec2 format. --- @param #POINT_VEC3 self --- @return Dcs.DCSTypes#Vec2 The Vec2 coodinate. -function POINT_VEC3:GetVec2() - return { x = self.x, y = self.z } -end - - ---- Return the x coordinate of the POINT_VEC3. --- @param #POINT_VEC3 self --- @return #number The x coodinate. -function POINT_VEC3:GetX() - return self.x -end - ---- Return the y coordinate of the POINT_VEC3. --- @param #POINT_VEC3 self --- @return #number The y coodinate. -function POINT_VEC3:GetY() - return self.y -end - ---- Return the z coordinate of the POINT_VEC3. --- @param #POINT_VEC3 self --- @return #number The z coodinate. -function POINT_VEC3:GetZ() - return self.z -end - ---- Set the x coordinate of the POINT_VEC3. --- @param #POINT_VEC3 self --- @param #number x The x coordinate. --- @return #POINT_VEC3 -function POINT_VEC3:SetX( x ) - self.x = x - return self -end - ---- Set the y coordinate of the POINT_VEC3. --- @param #POINT_VEC3 self --- @param #number y The y coordinate. --- @return #POINT_VEC3 -function POINT_VEC3:SetY( y ) - self.y = y - return self -end - ---- Set the z coordinate of the POINT_VEC3. --- @param #POINT_VEC3 self --- @param #number z The z coordinate. --- @return #POINT_VEC3 -function POINT_VEC3:SetZ( z ) - self.z = z - return self -end - ---- Add to the x coordinate of the POINT_VEC3. --- @param #POINT_VEC3 self --- @param #number x The x coordinate value to add to the current x coodinate. --- @return #POINT_VEC3 -function POINT_VEC3:AddX( x ) - self.x = self.x + x - return self -end - ---- Add to the y coordinate of the POINT_VEC3. --- @param #POINT_VEC3 self --- @param #number y The y coordinate value to add to the current y coodinate. --- @return #POINT_VEC3 -function POINT_VEC3:AddY( y ) - self.y = self.y + y - return self -end - ---- Add to the z coordinate of the POINT_VEC3. --- @param #POINT_VEC3 self --- @param #number z The z coordinate value to add to the current z coodinate. --- @return #POINT_VEC3 -function POINT_VEC3:AddZ( z ) - self.z = self.z +z - return self -end - ---- Return a random Vec2 within an Outer Radius and optionally NOT within an Inner Radius of the POINT_VEC3. --- @param #POINT_VEC3 self --- @param Dcs.DCSTypes#Distance OuterRadius --- @param Dcs.DCSTypes#Distance InnerRadius --- @return Dcs.DCSTypes#Vec2 Vec2 -function POINT_VEC3:GetRandomVec2InRadius( OuterRadius, InnerRadius ) - self:F2( { OuterRadius, InnerRadius } ) - - local Theta = 2 * math.pi * math.random() - local Radials = math.random() + math.random() - if Radials > 1 then - Radials = 2 - Radials - end - - local RadialMultiplier - if InnerRadius and InnerRadius <= OuterRadius then - RadialMultiplier = ( OuterRadius - InnerRadius ) * Radials + InnerRadius - else - RadialMultiplier = OuterRadius * Radials - end - - local RandomVec2 - if OuterRadius > 0 then - RandomVec2 = { x = math.cos( Theta ) * RadialMultiplier + self:GetX(), y = math.sin( Theta ) * RadialMultiplier + self:GetZ() } - else - RandomVec2 = { x = self:GetX(), y = self:GetZ() } - end - - return RandomVec2 -end - ---- Return a random POINT_VEC2 within an Outer Radius and optionally NOT within an Inner Radius of the POINT_VEC3. --- @param #POINT_VEC3 self --- @param Dcs.DCSTypes#Distance OuterRadius --- @param Dcs.DCSTypes#Distance InnerRadius --- @return #POINT_VEC2 -function POINT_VEC3:GetRandomPointVec2InRadius( OuterRadius, InnerRadius ) - self:F2( { OuterRadius, InnerRadius } ) - - return POINT_VEC2:NewFromVec2( self:GetRandomVec2InRadius( OuterRadius, InnerRadius ) ) -end - ---- Return a random Vec3 within an Outer Radius and optionally NOT within an Inner Radius of the POINT_VEC3. --- @param #POINT_VEC3 self --- @param Dcs.DCSTypes#Distance OuterRadius --- @param Dcs.DCSTypes#Distance InnerRadius --- @return Dcs.DCSTypes#Vec3 Vec3 -function POINT_VEC3:GetRandomVec3InRadius( OuterRadius, InnerRadius ) - - local RandomVec2 = self:GetRandomVec2InRadius( OuterRadius, InnerRadius ) - local y = self:GetY() + math.random( InnerRadius, OuterRadius ) - local RandomVec3 = { x = RandomVec2.x, y = y, z = RandomVec2.y } - - 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.x - RoutePoint.y = self.z - RoutePoint.alt = self.y - RoutePoint.alt_type = AltType - - RoutePoint.type = Type - RoutePoint.action = Action - - RoutePoint.speed = Speed / 3.6 - RoutePoint.speed_locked = true - --- ["task"] = --- { --- ["id"] = "ComboTask", --- ["params"] = --- { --- ["tasks"] = --- { --- }, -- end of ["tasks"] --- }, -- end of ["params"] --- }, -- end of ["task"] - - - RoutePoint.task = {} - RoutePoint.task.id = "ComboTask" - RoutePoint.task.params = {} - RoutePoint.task.params.tasks = {} - - - return RoutePoint -end - ---- 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.x - RoutePoint.y = self.z - - RoutePoint.action = Formation or "" - - - RoutePoint.speed = Speed / 3.6 - RoutePoint.speed_locked = true - --- ["task"] = --- { --- ["id"] = "ComboTask", --- ["params"] = --- { --- ["tasks"] = --- { --- }, -- end of ["tasks"] --- }, -- end of ["params"] --- }, -- end of ["task"] - - - RoutePoint.task = {} - RoutePoint.task.id = "ComboTask" - RoutePoint.task.params = {} - RoutePoint.task.params.tasks = {} - - - return RoutePoint -end - ---- Creates an explosion at the point of a certain intensity. --- @param #POINT_VEC3 self --- @param #number ExplosionIntensity -function POINT_VEC3:Explosion( ExplosionIntensity ) - self:F2( { ExplosionIntensity } ) - trigger.action.explosion( self:GetVec3(), ExplosionIntensity ) -end - ---- Creates an illumination bomb at the point. --- @param #POINT_VEC3 self -function POINT_VEC3:IlluminationBomb() - self:F2() - trigger.action.illuminationBomb( self:GetVec3() ) -end - - ---- Smokes the point in a color. --- @param #POINT_VEC3 self --- @param Utilities.Utils#SMOKECOLOR SmokeColor -function POINT_VEC3:Smoke( SmokeColor ) - self:F2( { SmokeColor } ) - trigger.action.smoke( self:GetVec3(), SmokeColor ) -end - ---- Smoke the POINT_VEC3 Green. --- @param #POINT_VEC3 self -function POINT_VEC3:SmokeGreen() - self:F2() - self:Smoke( SMOKECOLOR.Green ) -end - ---- Smoke the POINT_VEC3 Red. --- @param #POINT_VEC3 self -function POINT_VEC3:SmokeRed() - self:F2() - self:Smoke( SMOKECOLOR.Red ) -end - ---- Smoke the POINT_VEC3 White. --- @param #POINT_VEC3 self -function POINT_VEC3:SmokeWhite() - self:F2() - self:Smoke( SMOKECOLOR.White ) -end - ---- Smoke the POINT_VEC3 Orange. --- @param #POINT_VEC3 self -function POINT_VEC3:SmokeOrange() - self:F2() - self:Smoke( SMOKECOLOR.Orange ) -end - ---- Smoke the POINT_VEC3 Blue. --- @param #POINT_VEC3 self -function POINT_VEC3:SmokeBlue() - self:F2() - self:Smoke( SMOKECOLOR.Blue ) -end - ---- Flares the point in a color. --- @param #POINT_VEC3 self --- @param Utilities.Utils#FLARECOLOR FlareColor --- @param Dcs.DCSTypes#Azimuth (optional) Azimuth The azimuth of the flare direction. The default azimuth is 0. -function POINT_VEC3:Flare( FlareColor, Azimuth ) - self:F2( { FlareColor } ) - trigger.action.signalFlare( self:GetVec3(), FlareColor, Azimuth and Azimuth or 0 ) -end - ---- Flare the POINT_VEC3 White. --- @param #POINT_VEC3 self --- @param Dcs.DCSTypes#Azimuth (optional) Azimuth The azimuth of the flare direction. The default azimuth is 0. -function POINT_VEC3:FlareWhite( Azimuth ) - self:F2( Azimuth ) - self:Flare( FLARECOLOR.White, Azimuth ) -end - ---- Flare the POINT_VEC3 Yellow. --- @param #POINT_VEC3 self --- @param Dcs.DCSTypes#Azimuth (optional) Azimuth The azimuth of the flare direction. The default azimuth is 0. -function POINT_VEC3:FlareYellow( Azimuth ) - self:F2( Azimuth ) - self:Flare( FLARECOLOR.Yellow, Azimuth ) -end - ---- Flare the POINT_VEC3 Green. --- @param #POINT_VEC3 self --- @param Dcs.DCSTypes#Azimuth (optional) Azimuth The azimuth of the flare direction. The default azimuth is 0. -function POINT_VEC3:FlareGreen( Azimuth ) - self:F2( Azimuth ) - self:Flare( FLARECOLOR.Green, Azimuth ) -end - ---- Flare the POINT_VEC3 Red. --- @param #POINT_VEC3 self -function POINT_VEC3:FlareRed( Azimuth ) - self:F2( Azimuth ) - self:Flare( FLARECOLOR.Red, Azimuth ) -end - -end - -do -- POINT_VEC2 - - - ---- POINT_VEC2 constructor. --- @param #POINT_VEC2 self --- @param Dcs.DCSTypes#Distance x The x coordinate of the Vec3 point, pointing to the North. --- @param Dcs.DCSTypes#Distance y The y coordinate of the Vec3 point, pointing to the Right. --- @param Dcs.DCSTypes#Distance LandHeightAdd (optional) The default height if required to be evaluated will be the land height of the x, y coordinate. You can specify an extra height to be added to the land height. --- @return Core.Point#POINT_VEC2 -function POINT_VEC2:New( x, y, LandHeightAdd ) - - local LandHeight = land.getHeight( { ["x"] = x, ["y"] = y } ) - - LandHeightAdd = LandHeightAdd or 0 - LandHeight = LandHeight + LandHeightAdd - - self = BASE:Inherit( self, POINT_VEC3:New( x, LandHeight, y ) ) - self:F2( self ) - - return self -end - ---- Create a new POINT_VEC2 object from Vec2 coordinates. --- @param #POINT_VEC2 self --- @param Dcs.DCSTypes#Vec2 Vec2 The Vec2 point. --- @return Core.Point#POINT_VEC2 self -function POINT_VEC2:NewFromVec2( Vec2, LandHeightAdd ) - - local LandHeight = land.getHeight( Vec2 ) - - LandHeightAdd = LandHeightAdd or 0 - LandHeight = LandHeight + LandHeightAdd - - self = BASE:Inherit( self, POINT_VEC3:New( Vec2.x, LandHeight, Vec2.y ) ) - self:F2( self ) - - return self -end - ---- Create a new POINT_VEC2 object from Vec3 coordinates. --- @param #POINT_VEC2 self --- @param Dcs.DCSTypes#Vec3 Vec3 The Vec3 point. --- @return Core.Point#POINT_VEC2 self -function POINT_VEC2:NewFromVec3( Vec3 ) - - local self = BASE:Inherit( self, BASE:New() ) - local Vec2 = { x = Vec3.x, y = Vec3.z } - - local LandHeight = land.getHeight( Vec2 ) - - self = BASE:Inherit( self, POINT_VEC3:New( Vec2.x, LandHeight, Vec2.y ) ) - self:F2( self ) - - return self -end - ---- Return the x coordinate of the POINT_VEC2. --- @param #POINT_VEC2 self --- @return #number The x coodinate. -function POINT_VEC2:GetX() - return self.x -end - ---- Return the y coordinate of the POINT_VEC2. --- @param #POINT_VEC2 self --- @return #number The y coodinate. -function POINT_VEC2:GetY() - return self.z -end - ---- Return the altitude (height) 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 - ---- Return Return the Lat(itude) coordinate of the POINT_VEC2 (ie: (parent)POINT_VEC3.x). --- @param #POINT_VEC2 self --- @return #number The x coodinate. -function POINT_VEC2:GetLat() - return self.x -end - ---- Return the Lon(gitude) coordinate of the POINT_VEC2 (ie: (parent)POINT_VEC3.z). --- @param #POINT_VEC2 self --- @return #number The y coodinate. -function POINT_VEC2:GetLon() - return self.z -end - ---- Set the x coordinate of the POINT_VEC2. --- @param #POINT_VEC2 self --- @param #number x The x coordinate. --- @return #POINT_VEC2 -function POINT_VEC2:SetX( x ) - self.x = x - return self -end - ---- Set the y coordinate of the POINT_VEC2. --- @param #POINT_VEC2 self --- @param #number y The y coordinate. --- @return #POINT_VEC2 -function POINT_VEC2:SetY( y ) - self.z = y - return self -end - ---- Set the Lat(itude) coordinate of the POINT_VEC2 (ie: POINT_VEC3.x). --- @param #POINT_VEC2 self --- @param #number x The x coordinate. --- @return #POINT_VEC2 -function POINT_VEC2:SetLat( x ) - self.x = x - return self -end - ---- Set the altitude of the POINT_VEC2. --- @param #POINT_VEC2 self --- @param #number Altitude The land altitude. If nothing (nil) is given, then the current land altitude is set. --- @return #POINT_VEC2 -function POINT_VEC2:SetAlt( Altitude ) - self.y = Altitude or land.getHeight( { x = self.x, y = self.z } ) - return self -end - ---- Set the Lon(gitude) coordinate of the POINT_VEC2 (ie: POINT_VEC3.z). --- @param #POINT_VEC2 self --- @param #number y The y coordinate. --- @return #POINT_VEC2 -function POINT_VEC2:SetLon( z ) - self.z = z - return self -end - ---- Add to the x coordinate of the POINT_VEC2. --- @param #POINT_VEC2 self --- @param #number x The x coordinate. --- @return #POINT_VEC2 -function POINT_VEC2:AddX( x ) - self.x = self.x + x - return self -end - ---- Add to the y coordinate of the POINT_VEC2. --- @param #POINT_VEC2 self --- @param #number y The y coordinate. --- @return #POINT_VEC2 -function POINT_VEC2:AddY( y ) - self.z = self.z + y - return self -end - ---- Add to the current land height an altitude. --- @param #POINT_VEC2 self --- @param #number Altitude The Altitude to add. If nothing (nil) is given, then the current land altitude is set. --- @return #POINT_VEC2 -function POINT_VEC2:AddAlt( Altitude ) - self.y = land.getHeight( { x = self.x, y = self.z } ) + Altitude or 0 - return self -end - - - ---- Calculate the distance from a reference @{#POINT_VEC2}. --- @param #POINT_VEC2 self --- @param #POINT_VEC2 PointVec2Reference The reference @{#POINT_VEC2}. --- @return Dcs.DCSTypes#Distance The distance from the reference @{#POINT_VEC2} in meters. -function POINT_VEC2:DistanceFromPointVec2( PointVec2Reference ) - self:F2( PointVec2Reference ) - - local Distance = ( ( PointVec2Reference:GetX() - self:GetX() ) ^ 2 + ( PointVec2Reference:GetY() - self:GetY() ) ^2 ) ^0.5 - - self:T2( Distance ) - return Distance -end - ---- Calculate the distance from a reference @{DCSTypes#Vec2}. --- @param #POINT_VEC2 self --- @param Dcs.DCSTypes#Vec2 Vec2Reference The reference @{DCSTypes#Vec2}. --- @return Dcs.DCSTypes#Distance The distance from the reference @{DCSTypes#Vec2} in meters. -function POINT_VEC2:DistanceFromVec2( Vec2Reference ) - self:F2( Vec2Reference ) - - local Distance = ( ( Vec2Reference.x - self:GetX() ) ^ 2 + ( Vec2Reference.y - self:GetY() ) ^2 ) ^0.5 - - self:T2( Distance ) - return Distance -end - - ---- Return no text for the altitude of the POINT_VEC2. --- @param #POINT_VEC2 self --- @return #string Empty string. -function POINT_VEC2:GetAltitudeText() - return '' -end - ---- Add a Distance in meters from the POINT_VEC2 orthonormal plane, with the given angle, and calculate the new POINT_VEC2. --- @param #POINT_VEC2 self --- @param Dcs.DCSTypes#Distance Distance The Distance to be added in meters. --- @param Dcs.DCSTypes#Angle Angle The Angle in degrees. --- @return #POINT_VEC2 The new calculated POINT_VEC2. -function POINT_VEC2:Translate( Distance, Angle ) - local SX = self:GetX() - local SY = self:GetY() - local Radians = Angle / 180 * math.pi - local TX = Distance * math.cos( Radians ) + SX - local TY = Distance * math.sin( Radians ) + SY - - return POINT_VEC2:New( TX, TY ) -end - -end - - ---- **Core** - MESSAGE class takes are of the **real-time notifications** and **messages to players** during a simulation. --- --- ![Banner Image](..\Presentations\MESSAGE\Dia1.JPG) --- --- === --- --- # 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 --- --- 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 to an audience --- --- Messages are sent: --- --- * To a @{Client} using @{Message#MESSAGE.ToClient}(). --- * To a @{Group} using @{Message#MESSAGE.ToGroup}() --- * To a coalition using @{Message#MESSAGE.ToCoalition}(). --- * To the red coalition using @{Message#MESSAGE.ToRed}(). --- * To the blue coalition using @{Message#MESSAGE.ToBlue}(). --- * To all Players using @{Message#MESSAGE.ToAll}(). --- --- ## 1.3) Send conditionally to an audience --- --- Messages can be sent conditionally to an audience (when a condition is true): --- --- * To all players using @{Message#MESSAGE.ToAllIf}(). --- * To a coalition using @{Message#MESSAGE.ToCoalitionIf}(). --- --- --- @module Message - ---- The MESSAGE class --- @type MESSAGE --- @extends Core.Base#BASE -MESSAGE = { - ClassName = "MESSAGE", - MessageCategory = 0, - MessageID = 0, -} - - ---- Creates a new MESSAGE object. Note that these MESSAGE objects are not yet displayed on the display panel. You must use the functions @{ToClient} or @{ToCoalition} or @{ToAll} to send these Messages to the respective recipients. --- @param self --- @param #string MessageText is the text of the Message. --- @param #number MessageDuration is a number in seconds of how long the MESSAGE should be shown on the display panel. --- @param #string MessageCategory (optional) is a string expressing the "category" of the Message. The category will be shown as the first text in the message followed by a ": ". --- @return #MESSAGE --- @usage --- -- Create a series of new Messages. --- -- MessageAll is meant to be sent to all players, for 25 seconds, and is classified as "Score". --- -- MessageRED is meant to be sent to the RED players only, for 10 seconds, and is classified as "End of Mission", with ID "Win". --- -- MessageClient1 is meant to be sent to a Client, for 25 seconds, and is classified as "Score", with ID "Score". --- -- MessageClient1 is meant to be sent to a Client, for 25 seconds, and is classified as "Score", with ID "Score". --- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", 25, "End of Mission" ) --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", 25, "Penalty" ) --- MessageClient1 = MESSAGE:New( "Congratulations, you've just hit a target", 25, "Score" ) --- MessageClient2 = MESSAGE:New( "Congratulations, you've just killed a target", 25, "Score") -function MESSAGE:New( MessageText, MessageDuration, MessageCategory ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( { MessageText, MessageDuration, MessageCategory } ) - - -- When no MessageCategory is given, we don't show it as a title... - if MessageCategory and MessageCategory ~= "" then - if MessageCategory:sub(-1) ~= "\n" then - self.MessageCategory = MessageCategory .. ": " - else - self.MessageCategory = MessageCategory:sub( 1, -2 ) .. ":\n" - end - else - self.MessageCategory = "" - end - - self.MessageDuration = MessageDuration or 5 - self.MessageTime = timer.getTime() - self.MessageText = MessageText - - self.MessageSent = false - self.MessageGroup = false - self.MessageCoalition = false - - return self -end - ---- Sends a MESSAGE to a Client Group. Note that the Group needs to be defined within the ME with the skillset "Client" or "Player". --- @param #MESSAGE self --- @param Wrapper.Client#CLIENT Client is the Group of the Client. --- @return #MESSAGE --- @usage --- -- Send the 2 messages created with the @{New} method to the Client Group. --- -- Note that the Message of MessageClient2 is overwriting the Message of MessageClient1. --- ClientGroup = Group.getByName( "ClientGroup" ) --- --- MessageClient1 = MESSAGE:New( "Congratulations, you've just hit a target", "Score", 25, "Score" ):ToClient( ClientGroup ) --- MessageClient2 = MESSAGE:New( "Congratulations, you've just killed a target", "Score", 25, "Score" ):ToClient( ClientGroup ) --- or --- MESSAGE:New( "Congratulations, you've just hit a target", "Score", 25, "Score" ):ToClient( ClientGroup ) --- MESSAGE:New( "Congratulations, you've just killed a target", "Score", 25, "Score" ):ToClient( ClientGroup ) --- or --- MessageClient1 = MESSAGE:New( "Congratulations, you've just hit a target", "Score", 25, "Score" ) --- MessageClient2 = MESSAGE:New( "Congratulations, you've just killed a target", "Score", 25, "Score" ) --- MessageClient1:ToClient( ClientGroup ) --- MessageClient2:ToClient( ClientGroup ) -function MESSAGE:ToClient( Client ) - self:F( Client ) - - if Client and Client:GetClientGroupID() then - - local ClientGroupID = Client:GetClientGroupID() - self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration ) - trigger.action.outTextForGroup( ClientGroupID, self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration ) - end - - return self -end - ---- Sends a MESSAGE to a Group. --- @param #MESSAGE self --- @param Wrapper.Group#GROUP Group is the Group. --- @return #MESSAGE -function MESSAGE:ToGroup( Group ) - self:F( Group.GroupName ) - - if Group then - - self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration ) - trigger.action.outTextForGroup( Group:GetID(), self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration ) - end - - return self -end ---- Sends a MESSAGE to the Blue coalition. --- @param #MESSAGE self --- @return #MESSAGE --- @usage --- -- Send a message created with the @{New} method to the BLUE coalition. --- MessageBLUE = MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToBlue() --- or --- MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToBlue() --- or --- MessageBLUE = MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ) --- MessageBLUE:ToBlue() -function MESSAGE:ToBlue() - self:F() - - self:ToCoalition( coalition.side.BLUE ) - - return self -end - ---- Sends a MESSAGE to the Red Coalition. --- @param #MESSAGE self --- @return #MESSAGE --- @usage --- -- Send a message created with the @{New} method to the RED coalition. --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToRed() --- or --- MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToRed() --- or --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ) --- MessageRED:ToRed() -function MESSAGE:ToRed( ) - self:F() - - self:ToCoalition( coalition.side.RED ) - - return self -end - ---- Sends a MESSAGE to a Coalition. --- @param #MESSAGE self --- @param CoalitionSide needs to be filled out by the defined structure of the standard scripting engine @{coalition.side}. --- @return #MESSAGE --- @usage --- -- Send a message created with the @{New} method to the RED coalition. --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToCoalition( coalition.side.RED ) --- or --- MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToCoalition( coalition.side.RED ) --- or --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ) --- MessageRED:ToCoalition( coalition.side.RED ) -function MESSAGE:ToCoalition( CoalitionSide ) - self:F( CoalitionSide ) - - if CoalitionSide then - self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration ) - trigger.action.outTextForCoalition( CoalitionSide, self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration ) - end - - return self -end - ---- Sends a MESSAGE to a Coalition if the given Condition is true. --- @param #MESSAGE self --- @param CoalitionSide needs to be filled out by the defined structure of the standard scripting engine @{coalition.side}. --- @return #MESSAGE -function MESSAGE:ToCoalitionIf( CoalitionSide, Condition ) - self:F( CoalitionSide ) - - if Condition and Condition == true then - self:ToCoalition( CoalitionSide ) - end - - return self -end - ---- Sends a MESSAGE to all players. --- @param #MESSAGE self --- @return #MESSAGE --- @usage --- -- Send a message created to all players. --- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ):ToAll() --- or --- MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ):ToAll() --- or --- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ) --- MessageAll:ToAll() -function MESSAGE:ToAll() - self:F() - - self:ToCoalition( coalition.side.RED ) - self:ToCoalition( coalition.side.BLUE ) - - return self -end - - ---- Sends a MESSAGE to all players if the given Condition is true. --- @param #MESSAGE self --- @return #MESSAGE -function MESSAGE:ToAllIf( Condition ) - - if Condition and Condition == true then - self:ToCoalition( coalition.side.RED ) - self:ToCoalition( coalition.side.BLUE ) - end - - return self -end ---- **Core** - The **FSM** (**F**inite **S**tate **M**achine) class and derived **FSM\_** classes --- 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. --- --- The following derived classes are available in the MOOSE framework, that implement a specialised form of a FSM: --- --- * @{#FSM_TASK}: Models Finite State Machines for @{Task}s. --- * @{#FSM_PROCESS}: Models Finite State Machines for @{Task} actions, which control @{Client}s. --- * @{#FSM_CONTROLLABLE}: Models Finite State Machines for @{Controllable}s, which are @{Group}s, @{Unit}s, @{Client}s. --- * @{#FSM_SET}: Models Finite State Machines for @{Set}s. Note that these FSMs control multiple objects!!! So State concerns here --- for multiple objects or the position of the state machine in the process. --- --- ==== --- --- # **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 - - - --- # 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. - -- - -- === - -- - -- @field #FSM FSM - -- - FSM = { - ClassName = "FSM", - } - - --- Creates a new FSM object. - -- @param #FSM self - -- @return #FSM - function FSM:New( FsmT ) - - -- Inherits from BASE - self = BASE:Inherit( self, BASE:New() ) - - self.options = options or {} - self.options.subs = self.options.subs or {} - self.current = self.options.initial or 'none' - self.Events = {} - self.subs = {} - self.endstates = {} - - self.Scores = {} - - self._StartState = "none" - self._Transitions = {} - self._Processes = {} - self._EndStates = {} - self._Scores = {} - self._EventSchedules = {} - - self.CallScheduler = SCHEDULER:New( self ) - - - return self - end - - - --- Sets the start state of the FSM. - -- @param #FSM self - -- @param #string State A string defining the start state. - function FSM:SetStartState( State ) - - self._StartState = State - self.current = State - end - - - --- Returns the start state of the FSM. - -- @param #FSM self - -- @return #string A string containing the start state. - function FSM:GetStartState() - - return self._StartState or {} - end - - --- Add a new transition rule to the FSM. - -- A transition rule defines when and if the FSM can transition from a state towards another state upon a triggered event. - -- @param #FSM self - -- @param #table From Can contain a string indicating the From state or a table of strings containing multiple From states. - -- @param #string Event The Event name. - -- @param #string To The To state. - function FSM:AddTransition( From, Event, To ) - - local Transition = {} - Transition.From = From - Transition.Event = Event - Transition.To = To - - self:T( Transition ) - - self._Transitions[Transition] = Transition - self:_eventmap( self.Events, Transition ) - end - - - --- Returns a table of the transition rules defined within the FSM. - -- @return #table - function FSM:GetTransitions() - - return self._Transitions or {} - end - - --- Set the default @{Process} template with key ProcessName providing the ProcessClass and the process object when it is assigned to a @{Controllable} by the task. - -- @param #FSM self - -- @param #table From Can contain a string indicating the From state or a table of strings containing multiple From states. - -- @param #string Event The Event name. - -- @param Core.Fsm#FSM_PROCESS Process An sub-process FSM. - -- @param #table ReturnEvents A table indicating for which returned events of the SubFSM which Event must be triggered in the FSM. - -- @return Core.Fsm#FSM_PROCESS The SubFSM. - function FSM:AddProcess( From, Event, Process, ReturnEvents ) - self:T( { From, Event, Process, ReturnEvents } ) - - local Sub = {} - Sub.From = From - Sub.Event = Event - Sub.fsm = Process - Sub.StartEvent = "Start" - Sub.ReturnEvents = ReturnEvents - - self._Processes[Sub] = Sub - - self:_submap( self.subs, Sub, nil ) - - self:AddTransition( From, Event, From ) - - return Process - end - - - --- Returns a table of the SubFSM rules defined within the FSM. - -- @return #table - function FSM:GetProcesses() - - return self._Processes or {} - end - - function FSM:GetProcess( From, Event ) - - for ProcessID, Process in pairs( self:GetProcesses() ) do - if Process.From == From and Process.Event == Event then - 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:F( { 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:F( { From, Event, State, ScoreText, Score } ) - - local Process = self:GetProcess( From, Event ) - - Process._Scores[State] = Process._Scores[State] or {} - Process._Scores[State].ScoreText = ScoreText - Process._Scores[State].Score = Score - - self:T( Process._Scores ) - - return Process - end - - --- Returns a table with the scores defined. - function FSM:GetScores() - - return self._Scores or {} - end - - --- Returns a table with the Subs defined. - function FSM:GetSubs() - - return self.options.subs - end - - - function FSM:LoadCallBacks( CallBackTable ) - - for name, callback in pairs( CallBackTable or {} ) do - self[name] = callback - end - - end - - function FSM:_eventmap( Events, EventStructure ) - - local Event = EventStructure.Event - local __Event = "__" .. EventStructure.Event - self[Event] = self[Event] or self:_create_transition(Event) - self[__Event] = self[__Event] or self:_delayed_transition(Event) - self:T( "Added methods: " .. Event .. ", " .. __Event ) - Events[Event] = self.Events[Event] or { map = {} } - self:_add_to_map( Events[Event].map, EventStructure ) - - end - - function FSM:_submap( subs, sub, name ) - self:F( { sub = sub, name = name } ) - subs[sub.From] = subs[sub.From] or {} - subs[sub.From][sub.Event] = subs[sub.From][sub.Event] or {} - - -- Make the reference table weak. - -- setmetatable( subs[sub.From][sub.Event], { __mode = "k" } ) - - subs[sub.From][sub.Event][sub] = {} - subs[sub.From][sub.Event][sub].fsm = sub.fsm - subs[sub.From][sub.Event][sub].StartEvent = sub.StartEvent - subs[sub.From][sub.Event][sub].ReturnEvents = sub.ReturnEvents or {} -- these events need to be given to find the correct continue event ... if none given, the processing will stop. - subs[sub.From][sub.Event][sub].name = name - subs[sub.From][sub.Event][sub].fsmparent = self - end - - - function FSM:_call_handler( handler, params, EventName ) - - 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:T( "Calling " .. handler ) - self._EventSchedules[EventName] = nil - local Result, Value = xpcall( function() return self[handler]( self, unpack( params ) ) end, ErrorHandler ) - return Value - end - end - - function FSM._handler( self, EventName, ... ) - - local Can, to = self:can( EventName ) - - if to == "*" then - to = self.current - end - - if Can then - local from = self.current - local params = { from, EventName, to, ... } - - if self.Controllable then - self:T( "FSM Transition for " .. self.Controllable.ControllableName .. " :" .. self.current .. " --> " .. EventName .. " --> " .. to ) - else - self:T( "FSM Transition:" .. self.current .. " --> " .. EventName .. " --> " .. to ) - end - - if ( self:_call_handler("onbefore" .. EventName, params, EventName ) == false ) - or ( self:_call_handler("OnBefore" .. EventName, params, EventName ) == false ) - or ( self:_call_handler("onleave" .. from, params, EventName ) == false ) - or ( self:_call_handler("OnLeave" .. from, params, EventName ) == false ) then - self:T( "Cancel Transition" ) - return false - end - - self.current = to - - local execute = true - - local subtable = self:_gosub( from, EventName ) - for _, sub in pairs( subtable ) do - --if sub.nextevent then - -- self:F2( "nextevent = " .. sub.nextevent ) - -- self[sub.nextevent]( self ) - --end - self:T( "calling sub start event: " .. sub.StartEvent ) - sub.fsm.fsmparent = self - sub.fsm.ReturnEvents = sub.ReturnEvents - sub.fsm[sub.StartEvent]( sub.fsm ) - execute = false - end - - local fsmparent, Event = self:_isendstate( to ) - if fsmparent and Event then - self:F2( { "end state: ", fsmparent, Event } ) - self:_call_handler("onenter" .. to, params, EventName ) - self:_call_handler("OnEnter" .. to, params, EventName ) - self:_call_handler("onafter" .. EventName, params, EventName ) - self:_call_handler("OnAfter" .. EventName, params, EventName ) - self:_call_handler("onstatechange", params, EventName ) - fsmparent[Event]( fsmparent ) - execute = false - end - - if execute then - -- only execute the call if the From state is not equal to the To state! Otherwise this function should never execute! - --if from ~= to then - self:_call_handler("onenter" .. to, params, EventName ) - self:_call_handler("OnEnter" .. to, params, EventName ) - --end - - self:_call_handler("onafter" .. EventName, params, EventName ) - self:_call_handler("OnAfter" .. EventName, params, EventName ) - - self:_call_handler("onstatechange", params, EventName ) - end - else - self:T( "Cannot execute transition." ) - self:T( { From = self.current, Event = EventName, To = to, Can = Can } ) - end - - return nil - end - - function FSM:_delayed_transition( EventName ) - return function( self, DelaySeconds, ... ) - self:T2( "Delayed Event: " .. EventName ) - local CallID = 0 - if DelaySeconds ~= nil then - if DelaySeconds < 0 then -- Only call the event ONCE! - DelaySeconds = math.abs( DelaySeconds ) - if not self._EventSchedules[EventName] then - CallID = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1 ) - self._EventSchedules[EventName] = CallID - else - -- reschedule - end - else - CallID = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1 ) - end - else - error( "FSM: An asynchronous event trigger requires a DelaySeconds parameter!!! This can be positive or negative! Sorry, but will not process this." ) - end - self:T2( { CallID = CallID } ) - end - end - - function FSM:_create_transition( EventName ) - return function( self, ... ) return self._handler( self, EventName , ... ) end - end - - function FSM:_gosub( ParentFrom, ParentEvent ) - local fsmtable = {} - if self.subs[ParentFrom] and self.subs[ParentFrom][ParentEvent] then - self:T( { ParentFrom, ParentEvent, self.subs[ParentFrom], self.subs[ParentFrom][ParentEvent] } ) - return self.subs[ParentFrom][ParentEvent] - else - return {} - end - end - - function FSM:_isendstate( Current ) - local FSMParent = self.fsmparent - if FSMParent and self.endstates[Current] then - self:T( { state = Current, endstates = self.endstates, endstate = self.endstates[Current] } ) - FSMParent.current = Current - local ParentFrom = FSMParent.current - self:T( ParentFrom ) - self:T( self.ReturnEvents ) - local Event = self.ReturnEvents[Current] - self:T( { ParentFrom, Event, self.ReturnEvents } ) - if Event then - return FSMParent, Event - else - self:T( { "Could not find parent event name for state ", ParentFrom } ) - end - end - - return nil - end - - function FSM:_add_to_map( Map, Event ) - self:F3( { Map, Event } ) - if type(Event.From) == 'string' then - Map[Event.From] = Event.To - else - for _, From in ipairs(Event.From) do - Map[From] = Event.To - end - end - self:T3( { Map, Event } ) - end - - function FSM:GetState() - return self.current - end - - - function FSM:Is( State ) - return self.current == State - end - - function FSM:is(state) - return self.current == state - end - - function FSM:can(e) - local Event = self.Events[e] - self:F3( { self.current, Event } ) - local To = Event and Event.map[self.current] or Event.map['*'] - return To ~= nil, To - end - - function FSM:cannot(e) - return not self:can(e) - end - -end - -do -- FSM_CONTROLLABLE - - --- @type FSM_CONTROLLABLE - -- @field Wrapper.Controllable#CONTROLLABLE Controllable - -- @extends Core.Fsm#FSM - - --- # FSM_CONTROLLABLE, extends @{#FSM} - -- - -- FSM_CONTROLLABLE class models Finite State Machines for @{Controllable}s, which are @{Group}s, @{Unit}s, @{Client}s. - -- - -- === - -- - -- @field #FSM_CONTROLLABLE FSM_CONTROLLABLE - -- - 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 - - self:AddTransition( "*", "Stop", "Stopped" ) - - --- OnBefore Transition Handler for Event Stop. - -- @function [parent=#FSM_CONTROLLABLE] OnBeforeStop - -- @param #FSM_CONTROLLABLE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Stop. - -- @function [parent=#FSM_CONTROLLABLE] OnAfterStop - -- @param #FSM_CONTROLLABLE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Stop. - -- @function [parent=#FSM_CONTROLLABLE] Stop - -- @param #FSM_CONTROLLABLE self - - --- Asynchronous Event Trigger for Event Stop. - -- @function [parent=#FSM_CONTROLLABLE] __Stop - -- @param #FSM_CONTROLLABLE self - -- @param #number Delay The delay in seconds. - - --- OnLeave Transition Handler for State Stopped. - -- @function [parent=#FSM_CONTROLLABLE] OnLeaveStopped - -- @param #FSM_CONTROLLABLE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnEnter Transition Handler for State Stopped. - -- @function [parent=#FSM_CONTROLLABLE] OnEnterStopped - -- @param #FSM_CONTROLLABLE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object 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 self - end - - --- OnAfter Transition Handler for Event Stop. - -- @function [parent=#FSM_CONTROLLABLE] OnAfterStop - -- @param #FSM_CONTROLLABLE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object 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 FSM_CONTROLLABLE:OnAfterStop(Controllable,From,Event,To) - - -- Clear all pending schedules - self.CallScheduler:Clear() - end - - --- Sets the CONTROLLABLE object that the FSM_CONTROLLABLE governs. - -- @param #FSM_CONTROLLABLE self - -- @param Wrapper.Controllable#CONTROLLABLE FSMControllable - -- @return #FSM_CONTROLLABLE - function FSM_CONTROLLABLE:SetControllable( FSMControllable ) - self:F( FSMControllable ) - self.Controllable = FSMControllable - end - - --- Gets the CONTROLLABLE object that the FSM_CONTROLLABLE governs. - -- @param #FSM_CONTROLLABLE self - -- @return Wrapper.Controllable#CONTROLLABLE - function FSM_CONTROLLABLE:GetControllable() - return self.Controllable - end - - function FSM_CONTROLLABLE:_call_handler( handler, params, EventName ) - - local ErrorHandler = function( errmsg ) - - env.info( "Error in SCHEDULER function:" .. errmsg ) - if debug ~= nil then - env.info( debug.traceback() ) - end - - return errmsg - end - - if self[handler] then - self:F3( "Calling " .. handler ) - self._EventSchedules[EventName] = nil - local Result, Value = xpcall( function() return self[handler]( self, self.Controllable, unpack( params ) ) end, ErrorHandler ) - return Value - --return self[handler]( self, self.Controllable, unpack( params ) ) - end - end - -end - -do -- FSM_PROCESS - - --- @type FSM_PROCESS - -- @field Tasking.Task#TASK Task - -- @extends Core.Fsm#FSM_CONTROLLABLE - - - --- # FSM_PROCESS, extends @{#FSM} - -- - -- FSM_PROCESS class models Finite State Machines for @{Task} actions, which control @{Client}s. - -- - -- === - -- - -- @field #FSM_PROCESS FSM_PROCESS - -- - 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 - - function FSM_PROCESS:_call_handler( handler, params, EventName ) - - local ErrorHandler = function( errmsg ) - - env.info( "Error in FSM_PROCESS call handler:" .. errmsg ) - if debug ~= nil then - env.info( debug.traceback() ) - end - - return errmsg - end - - if self[handler] then - self:F3( "Calling " .. handler ) - self._EventSchedules[EventName] = nil - local Result, Value = xpcall( function() return self[handler]( self, self.Controllable, self.Task, unpack( params ) ) end, ErrorHandler ) - return Value - --return self[handler]( self, self.Controllable, unpack( params ) ) - end - 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:E( { 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 - - --- Removes an FSM_PROCESS object. - -- @param #FSM_PROCESS self - -- @return #FSM_PROCESS - function FSM_PROCESS:Remove() - self:T( { self:GetClassNameAndID() } ) - - -- Copy Processes - for ProcessID, Process in pairs( self:GetProcesses() ) do - self:E( { Process} ) - Process.fsm:Remove() - Process.fsm = nil - end - - return self - 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, Task, 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( { Scores = self._Scores, To = 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, extends @{#FSM} - -- - -- FSM_TASK class models Finite State Machines for @{Task}s. - -- - -- === - -- - -- @field #FSM_TASK FSM_TASK - -- - FSM_TASK = { - ClassName = "FSM_TASK", - } - - --- Creates a new FSM_TASK object. - -- @param #FSM_TASK self - -- @param #table FSMT - -- @param Tasking.Task#TASK Task - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #FSM_TASK - function FSM_TASK:New( FSMT ) - - local self = BASE:Inherit( self, FSM_CONTROLLABLE:New( FSMT ) ) -- Core.Fsm#FSM_TASK - - self["onstatechange"] = self.OnStateChange - - return self - end - - function FSM_TASK:_call_handler( handler, params, EventName ) - if self[handler] then - self:T( "Calling " .. handler ) - self._EventSchedules[EventName] = nil - return self[handler]( self, unpack( params ) ) - end - end - -end -- FSM_TASK - -do -- FSM_SET - - --- FSM_SET class - -- @type FSM_SET - -- @field Core.Set#SET_BASE Set - -- @extends Core.Fsm#FSM - - - --- # FSM_SET, extends @{#FSM} - -- - -- FSM_SET class models Finite State Machines for @{Set}s. Note that these FSMs control multiple objects!!! So State concerns here - -- for multiple objects or the position of the state machine in the process. - -- - -- === - -- - -- @field #FSM_SET FSM_SET - -- - FSM_SET = { - ClassName = "FSM_SET", - } - - --- Creates a new FSM_SET object. - -- @param #FSM_SET self - -- @param #table FSMT Finite State Machine Table - -- @param Set_SET_BASE FSMSet (optional) The Set object that the FSM_SET governs. - -- @return #FSM_SET - function FSM_SET:New( FSMSet ) - - -- Inherits from BASE - self = BASE:Inherit( self, FSM:New() ) -- Core.Fsm#FSM_SET - - if FSMSet then - self:Set( FSMSet ) - end - - return self - end - - --- Sets the SET_BASE object that the FSM_SET governs. - -- @param #FSM_SET self - -- @param Core.Set#SET_BASE FSMSet - -- @return #FSM_SET - function FSM_SET:Set( FSMSet ) - self:F( FSMSet ) - self.Set = FSMSet - end - - --- Gets the SET_BASE object that the FSM_SET governs. - -- @param #FSM_SET self - -- @return Core.Set#SET_BASE - function FSM_SET:Get() - return self.Controllable - end - - function FSM_SET:_call_handler( handler, params, EventName ) - if self[handler] then - self:T( "Calling " .. handler ) - self._EventSchedules[EventName] = nil - return self[handler]( self, self.Set, unpack( params ) ) - end - end - -end -- FSM_SET - ---- **Core** - The RADIO class is responsible for **transmitting radio communications**. --- --- --- bitmap --- --- === --- --- What are radio communications in DCS ? --- --- * Radio transmissions consist of **sound files** that are broadcasted on a specific **frequency** (e.g. 115MHz) and **modulation** (e.g. AM), --- * They can be **subtitled** for a specific **duration**, the **power** in Watts of the transmiter's antenna can be set, and the transmission can be **looped**. --- --- How to supply DCS my own Sound Files ? --- --- * Your sound files need to be encoded in **.ogg** or .wav, --- * Your sound files should be **as tiny as possible**. It is suggested you encode in .ogg with low bitrate and sampling settings, --- * They need to be added in .\l10n\DEFAULT\ in you .miz file (wich can be decompressed like a .zip file), --- * For simplicty sake, you can **let DCS' Mission Editor add the file** itself, by creating a new Trigger with the action "Sound to Country", and choosing your sound file and a country you don't use in your mission. --- --- Due to weird DCS quirks, **radio communications behave differently** if sent by a @{Unit#UNIT} or a @{Group#GROUP} or by any other @{Positionable#POSITIONABLE} --- --- * If the transmitter is a @{Unit#UNIT} or a @{Group#GROUP}, DCS will set the power of the transmission automatically, --- * If the transmitter is any other @{Positionable#POSITIONABLE}, the transmisison can't be subtitled or looped. --- --- Note that obviously, the **frequency** and the **modulation** of the transmission are important only if the players are piloting an **Advanced System Modelling** enabled aircraft, --- like the A10C or the Mirage 2000C. They will **hear the transmission** if they are tuned on the **right frequency and modulation** (and if they are close enough - more on that below). --- If a FC3 airacraft is used, it will **hear every communication, whatever the frequency and the modulation** is set to. --- --- === --- --- ### Authors: Hugues "Grey_Echo" Bousquet --- --- @module Radio - ---- # 1) RADIO class, extends @{Base#BASE} --- --- ## 1.1) RADIO usage --- --- There are 3 steps to a successful radio transmission. --- --- * First, you need to **"add" a @{#RADIO} object** to your @{Positionable#POSITIONABLE}. This is done using the @{Positionable#POSITIONABLE.GetRadio}() function, --- * Then, you will **set the relevant parameters** to the transmission (see below), --- * When done, you can actually **broadcast the transmission** (i.e. play the sound) with the @{Positionable#POSITIONABLE.Broadcast}() function. --- --- Methods to set relevant parameters for both a @{Unit#UNIT} or a @{Group#GROUP} or any other @{Positionable#POSITIONABLE} --- --- * @{#RADIO.SetFileName}() : Sets the file name of your sound file (e.g. "Noise.ogg"), --- * @{#RADIO.SetFrequency}() : Sets the frequency of your transmission, --- * @{#RADIO.SetModulation}() : Sets the modulation of your transmission. --- --- Additional Methods to set relevant parameters if the transmiter is a @{Unit#UNIT} or a @{Group#GROUP} --- --- * @{#RADIO.SetLoop}() : Choose if you want the transmission to be looped, --- * @{#RADIO.SetSubtitle}() : Set both the subtitle and its duration, --- * @{#RADIO.NewUnitTransmission}() : Shortcut to set all the relevant parameters in one method call --- --- Additional Methods to set relevant parameters if the transmiter is any other @{Wrapper.Positionable#POSITIONABLE} --- --- * @{#RADIO.SetPower}() : Sets the power of the antenna in Watts --- * @{#RADIO.NewGenericTransmission}() : Shortcut to set all the relevant parameters in one method call --- --- What is this power thing ? --- --- * If your transmission is sent by a @{Positionable#POSITIONABLE} other than a @{Unit#UNIT} or a @{Group#GROUP}, you can set the power of the antenna, --- * Otherwise, DCS sets it automatically, depending on what's available on your Unit, --- * If the player gets **too far** from the transmiter, or if the antenna is **too weak**, the transmission will **fade** and **become noisyer**, --- * This an automated DCS calculation you have no say on, --- * For reference, a standard VOR station has a 100W antenna, a standard AA TACAN has a 120W antenna, and civilian ATC's antenna usually range between 300 and 500W, --- * Note that if the transmission has a subtitle, it will be readable, regardless of the quality of the transmission. --- --- @type RADIO --- @field Wrapper.Positionable#POSITIONABLE Positionable The transmiter --- @field #string FileName Name of the sound file --- @field #number Frequency Frequency of the transmission in Hz --- @field #number Modulation Modulation of the transmission (either radio.modulation.AM or radio.modulation.FM) --- @field #string Subtitle Subtitle of the transmission --- @field #number SubtitleDuration Duration of the Subtitle in seconds --- @field #number Power Power of the antenna is Watts --- @field #boolean Loop --- @extends Core.Base#BASE -RADIO = { - ClassName = "RADIO", - FileName = "", - Frequency = 0, - Modulation = radio.modulation.AM, - Subtitle = "", - SubtitleDuration = 0, - Power = 100, - Loop = 0, -} - ---- Create a new RADIO Object. This doesn't broadcast a transmission, though, use @{#RADIO.Broadcast} to actually broadcast --- @param #RADIO self --- @param Wrapper.Positionable#POSITIONABLE Positionable The @{Positionable} that will receive radio capabilities. --- @return #RADIO Radio --- @return #nil If Positionable is invalid --- @usage --- -- If you want to create a RADIO, you probably should use @{Positionable#POSITIONABLE.GetRadio}() instead -function RADIO:New(Positionable) - local self = BASE:Inherit( self, BASE:New() ) -- Core.Radio#RADIO - - self:F(Positionable) - - if Positionable:GetPointVec2() then -- It's stupid, but the only way I found to make sure positionable is valid - self.Positionable = Positionable - return self - end - - self:E({"The passed positionable is invalid, no RADIO created", Positionable}) - return nil -end - ---- Check validity of the filename passed and sets RADIO.FileName --- @param #RADIO self --- @param #string FileName File name of the sound file (i.e. "Noise.ogg") --- @return #RADIO self -function RADIO:SetFileName(FileName) - self:F2(FileName) - - if type(FileName) == "string" then - if FileName:find(".ogg") or FileName:find(".wav") then - if not FileName:find("l10n/DEFAULT/") then - FileName = "l10n/DEFAULT/" .. FileName - end - self.FileName = FileName - return self - end - end - - self:E({"File name invalid. Maybe something wrong with the extension ?", self.FileName}) - return self -end - ---- Check validity of the frequency passed and sets RADIO.Frequency --- @param #RADIO self --- @param #number Frequency in MHz (Ranges allowed for radio transmissions in DCS : 30-88 / 108-152 / 225-400MHz) --- @return #RADIO self -function RADIO:SetFrequency(Frequency) - self:F2(Frequency) - if type(Frequency) == "number" then - -- If frequency is in range - if (Frequency >= 30 and Frequency < 88) or (Frequency >= 108 and Frequency < 152) or (Frequency >= 225 and Frequency < 400) then - self.Frequency = Frequency * 1000000 -- Conversion in Hz - -- If the RADIO is attached to a UNIT or a GROUP, we need to send the DCS Command "SetFrequency" to change the UNIT or GROUP frequency - if self.Positionable.ClassName == "UNIT" or self.Positionable.ClassName == "GROUP" then - self.Positionable:SetCommand({ - id = "SetFrequency", - params = { - frequency = self.Frequency, - modulation = self.Modulation, - } - }) - end - return self - end - end - self:E({"Frequency is outside of DCS Frequency ranges (30-80, 108-152, 225-400). Frequency unchanged.", self.Frequency}) - return self -end - ---- Check validity of the frequency passed and sets RADIO.Modulation --- @param #RADIO self --- @param #number Modulation either radio.modulation.AM or radio.modulation.FM --- @return #RADIO self -function RADIO:SetModulation(Modulation) - self:F2(Modulation) - if type(Modulation) == "number" then - if Modulation == radio.modulation.AM or Modulation == radio.modulation.FM then --TODO Maybe make this future proof if ED decides to add an other modulation ? - self.Modulation = Modulation - return self - end - end - self:E({"Modulation is invalid. Use DCS's enum radio.modulation. Modulation unchanged.", self.Modulation}) - return self -end - ---- Check validity of the power passed and sets RADIO.Power --- @param #RADIO self --- @param #number Power in W --- @return #RADIO self -function RADIO:SetPower(Power) - self:F2(Power) - if type(Power) == "number" then - self.Power = math.floor(math.abs(Power)) --TODO Find what is the maximum power allowed by DCS and limit power to that - return self - end - self:E({"Power is invalid. Power unchanged.", self.Power}) - return self -end - ---- Check validity of the loop passed and sets RADIO.Loop --- @param #RADIO self --- @param #boolean Loop --- @return #RADIO self --- @usage -function RADIO:SetLoop(Loop) - self:F2(Loop) - if type(Loop) == "boolean" then - self.Loop = Loop - return self - end - self:E({"Loop is invalid. Loop unchanged.", self.Loop}) - return self -end - ---- Check validity of the subtitle and the subtitleDuration passed and sets RADIO.subtitle and RADIO.subtitleDuration --- @param #RADIO self --- @param #string Subtitle --- @param #number SubtitleDuration in s --- @return #RADIO self --- @usage --- -- Both parameters are mandatory, since it wouldn't make much sense to change the Subtitle and not its duration -function RADIO:SetSubtitle(Subtitle, SubtitleDuration) - self:F2({Subtitle, SubtitleDuration}) - if type(Subtitle) == "string" then - self.Subtitle = Subtitle - else - self.Subtitle = "" - self:E({"Subtitle is invalid. Subtitle reset.", self.Subtitle}) - end - if type(SubtitleDuration) == "number" then - if math.floor(math.abs(SubtitleDuration)) == SubtitleDuration then - self.SubtitleDuration = SubtitleDuration - return self - end - end - self.SubtitleDuration = 0 - self:E({"SubtitleDuration is invalid. SubtitleDuration reset.", self.SubtitleDuration}) -end - ---- Create a new transmission, that is to say, populate the RADIO with relevant data --- @param #RADIO self --- @param #string FileName --- @param #number Frequency in MHz --- @param #number Modulation either radio.modulation.AM or radio.modulation.FM --- @param #number Power in W --- @return #RADIO self --- @usage --- -- In this function the data is especially relevant if the broadcaster is anything but a UNIT or a GROUP, --- but it will work with a UNIT or a GROUP anyway --- -- Only the RADIO and the Filename are mandatory -function RADIO:NewGenericTransmission(FileName, Frequency, Modulation, Power) - self:F({FileName, Frequency, Modulation, Power}) - - self:SetFileName(FileName) - if Frequency then self:SetFrequency(Frequency) end - if Modulation then self:SetModulation(Modulation) end - if Power then self:SetPower(Power) end - - return self -end - - ---- Create a new transmission, that is to say, populate the RADIO with relevant data --- @param #RADIO self --- @param #string FileName --- @param #string Subtitle --- @param #number SubtitleDuration in s --- @param #number Frequency in MHz --- @param #number Modulation either radio.modulation.AM or radio.modulation.FM --- @param #boolean Loop --- @return #RADIO self --- @usage --- -- In this function the data is especially relevant if the broadcaster is a UNIT or a GROUP, --- but it will work for any POSITIONABLE --- -- Only the RADIO and the Filename are mandatory -function RADIO:NewUnitTransmission(FileName, Subtitle, SubtitleDuration, Frequency, Modulation, Loop) - self:F({FileName, Subtitle, SubtitleDuration, Frequency, Modulation, Loop}) - - self:SetFileName(FileName) - if Subtitle then self:SetSubtitle(Subtitle) end - if SubtitleDuration then self:SetSubtitleDuration(SubtitleDuration) end - if Frequency then self:SetFrequency(Frequency) end - if Modulation then self:SetModulation(Modulation) end - if Loop then self:SetLoop(Loop) end - - return self -end - ---- Actually Broadcast the transmission --- @param #RADIO self --- @return #RADIO self --- @usage --- -- The Radio has to be populated with the new transmission before broadcasting. --- -- Please use RADIO setters or either @{Radio#RADIO.NewGenericTransmission} or @{Radio#RADIO.NewUnitTransmission} --- -- This class is in fact pretty smart, it determines the right DCS function to use depending on the type of POSITIONABLE --- -- If the POSITIONABLE is not a UNIT or a GROUP, we use the generic (but limited) trigger.action.radioTransmission() --- -- If the POSITIONABLE is a UNIT or a GROUP, we use the "TransmitMessage" Command --- -- If your POSITIONABLE is a UNIT or a GROUP, the Power is ignored. --- -- If your POSITIONABLE is not a UNIT or a GROUP, the Subtitle, SubtitleDuration and Loop are ignored -function RADIO:Broadcast() - self:F() - -- If the POSITIONABLE is actually a UNIT or a GROUP, use the more complicated DCS command system - if self.Positionable.ClassName == "UNIT" or self.Positionable.ClassName == "GROUP" then - self:T2("Broadcasting from a UNIT or a GROUP") - self.Positionable:SetCommand({ - id = "TransmitMessage", - params = { - file = self.FileName, - duration = self.SubtitleDuration, - subtitle = self.Subtitle, - loop = self.Loop, - } - }) - else - -- If the POSITIONABLE is anything else, we revert to the general singleton function - self:T2("Broadcasting from a POSITIONABLE") - trigger.action.radioTransmission(self.FileName, self.Positionable:GetPositionVec3(), self.Modulation, false, self.Frequency, self.Power) - end - return self -end - ---- Stops a transmission --- @param #RADIO self --- @return #RADIO self --- @usage --- -- Especially usefull to stop the broadcast of looped transmissions --- -- Only works with broadcasts from UNIT or GROUP -function RADIO:StopBroadcast() - self:F() - -- If the POSITIONABLE is a UNIT or a GROUP, stop the transmission with the DCS "StopTransmission" command - if self.Positionable.ClassName == "UNIT" or self.Positionable.ClassName == "GROUP" then - self.Positionable:SetCommand({ - id = "StopTransmission", - params = {} - }) - else - self:E("This broadcast can't be stopped. It's not looped either, so please wait for the end of the sound file playback") - end - return self -end--- This module contains the OBJECT class. --- --- 1) @{Object#OBJECT} class, extends @{Base#BASE} --- =========================================================== --- The @{Object#OBJECT} class is a wrapper class to handle the DCS Object objects: --- --- * Support all DCS Object APIs. --- * Enhance with Object specific APIs not in the DCS Object API set. --- * Manage the "state" of the DCS Object. --- --- 1.1) OBJECT constructor: --- ------------------------------ --- The OBJECT class provides the following functions to construct a OBJECT instance: --- --- * @{Object#OBJECT.New}(): Create a OBJECT instance. --- --- 1.2) OBJECT methods: --- -------------------------- --- The following methods can be used to identify an Object object: --- --- * @{Object#OBJECT.GetID}(): Returns the ID of the Object object. --- --- === --- --- @module Object - ---- The OBJECT class --- @type OBJECT --- @extends Core.Base#BASE --- @field #string ObjectName The name of the Object. -OBJECT = { - ClassName = "OBJECT", - ObjectName = "", -} - ---- A DCSObject --- @type DCSObject --- @field id_ The ID of the controllable in DCS - ---- Create a new OBJECT from a DCSObject --- @param #OBJECT self --- @param Dcs.DCSWrapper.Object#Object ObjectName The Object name --- @return #OBJECT self -function OBJECT:New( ObjectName, Test ) - local self = BASE:Inherit( self, BASE:New() ) - self:F2( ObjectName ) - self.ObjectName = ObjectName - - return self -end - - ---- Returns the unit's unique identifier. --- @param Wrapper.Object#OBJECT self --- @return Dcs.DCSWrapper.Object#Object.ID ObjectID --- @return #nil The DCS Object is not existing or alive. -function OBJECT:GetID() - self:F2( self.ObjectName ) - - local DCSObject = self:GetDCSObject() - - if DCSObject then - local ObjectID = DCSObject:getID() - return ObjectID - end - - return nil -end - ---- Destroys the OBJECT. --- @param #OBJECT self --- @return #nil The DCS Unit is not existing or alive. -function OBJECT:Destroy() - self:F2( self.ObjectName ) - - local DCSObject = self:GetDCSObject() - - if DCSObject then - - DCSObject:destroy() - end - - return nil -end - - - - ---- This module contains the IDENTIFIABLE class. --- --- 1) @{#IDENTIFIABLE} class, extends @{Object#OBJECT} --- =============================================================== --- The @{#IDENTIFIABLE} class is a wrapper class to handle the DCS Identifiable objects: --- --- * Support all DCS Identifiable APIs. --- * Enhance with Identifiable specific APIs not in the DCS Identifiable API set. --- * Manage the "state" of the DCS Identifiable. --- --- 1.1) IDENTIFIABLE constructor: --- ------------------------------ --- The IDENTIFIABLE class provides the following functions to construct a IDENTIFIABLE instance: --- --- * @{#IDENTIFIABLE.New}(): Create a IDENTIFIABLE instance. --- --- 1.2) IDENTIFIABLE methods: --- -------------------------- --- The following methods can be used to identify an identifiable object: --- --- * @{#IDENTIFIABLE.GetName}(): Returns the name of the Identifiable. --- * @{#IDENTIFIABLE.IsAlive}(): Returns if the Identifiable is alive. --- * @{#IDENTIFIABLE.GetTypeName}(): Returns the type name of the Identifiable. --- * @{#IDENTIFIABLE.GetCoalition}(): Returns the coalition of the Identifiable. --- * @{#IDENTIFIABLE.GetCountry}(): Returns the country of the Identifiable. --- * @{#IDENTIFIABLE.GetDesc}(): Returns the descriptor structure of the Identifiable. --- --- --- === --- --- @module Identifiable - ---- The IDENTIFIABLE class --- @type IDENTIFIABLE --- @extends Wrapper.Object#OBJECT --- @field #string IdentifiableName The name of the identifiable. -IDENTIFIABLE = { - ClassName = "IDENTIFIABLE", - IdentifiableName = "", -} - -local _CategoryName = { - [Unit.Category.AIRPLANE] = "Airplane", - [Unit.Category.HELICOPTER] = "Helicoper", - [Unit.Category.GROUND_UNIT] = "Ground Identifiable", - [Unit.Category.SHIP] = "Ship", - [Unit.Category.STRUCTURE] = "Structure", - } - ---- Create a new IDENTIFIABLE from a DCSIdentifiable --- @param #IDENTIFIABLE self --- @param Dcs.DCSWrapper.Identifiable#Identifiable IdentifiableName The DCS Identifiable name --- @return #IDENTIFIABLE self -function IDENTIFIABLE:New( IdentifiableName ) - local self = BASE:Inherit( self, OBJECT:New( IdentifiableName ) ) - self:F2( IdentifiableName ) - self.IdentifiableName = IdentifiableName - return self -end - ---- Returns if the Identifiable is alive. --- @param #IDENTIFIABLE self --- @return #boolean true if Identifiable is alive. --- @return #nil The DCS Identifiable is not existing or alive. -function IDENTIFIABLE:IsAlive() - self:F3( self.IdentifiableName ) - - local DCSIdentifiable = self:GetDCSObject() - - if DCSIdentifiable then - local IdentifiableIsAlive = DCSIdentifiable:isExist() - return IdentifiableIsAlive - end - - return false -end - - - - ---- Returns DCS Identifiable object name. --- The function provides access to non-activated objects too. --- @param #IDENTIFIABLE self --- @return #string The name of the DCS Identifiable. --- @return #nil The DCS Identifiable is not existing or alive. -function IDENTIFIABLE:GetName() - self:F2( self.IdentifiableName ) - - local DCSIdentifiable = self:GetDCSObject() - - if DCSIdentifiable then - local IdentifiableName = self.IdentifiableName - return IdentifiableName - end - - self:E( self.ClassName .. " " .. self.IdentifiableName .. " not found!" ) - return nil -end - - ---- Returns the type name of the DCS Identifiable. --- @param #IDENTIFIABLE self --- @return #string The type name of the DCS Identifiable. --- @return #nil The DCS Identifiable is not existing or alive. -function IDENTIFIABLE:GetTypeName() - self:F2( self.IdentifiableName ) - - local DCSIdentifiable = self:GetDCSObject() - - if DCSIdentifiable then - local IdentifiableTypeName = DCSIdentifiable:getTypeName() - self:T3( IdentifiableTypeName ) - return IdentifiableTypeName - end - - self:E( self.ClassName .. " " .. self.IdentifiableName .. " not found!" ) - return nil -end - - ---- Returns category of the DCS Identifiable. --- @param #IDENTIFIABLE self --- @return Dcs.DCSWrapper.Object#Object.Category The category ID -function IDENTIFIABLE:GetCategory() - self:F2( self.ObjectName ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - local ObjectCategory = DCSObject:getCategory() - self:T3( ObjectCategory ) - return ObjectCategory - end - - return nil -end - - ---- Returns the DCS Identifiable category name as defined within the DCS Identifiable Descriptor. --- @param #IDENTIFIABLE self --- @return #string The DCS Identifiable Category Name -function IDENTIFIABLE:GetCategoryName() - local DCSIdentifiable = self:GetDCSObject() - - if DCSIdentifiable then - local IdentifiableCategoryName = _CategoryName[ self:GetDesc().category ] - return IdentifiableCategoryName - end - - self:E( self.ClassName .. " " .. self.IdentifiableName .. " not found!" ) - return nil -end - ---- Returns coalition of the Identifiable. --- @param #IDENTIFIABLE self --- @return Dcs.DCSCoalitionWrapper.Object#coalition.side The side of the coalition. --- @return #nil The DCS Identifiable is not existing or alive. -function IDENTIFIABLE:GetCoalition() - self:F2( self.IdentifiableName ) - - local DCSIdentifiable = self:GetDCSObject() - - if DCSIdentifiable then - local IdentifiableCoalition = DCSIdentifiable:getCoalition() - self:T3( IdentifiableCoalition ) - return IdentifiableCoalition - end - - self:E( self.ClassName .. " " .. self.IdentifiableName .. " not found!" ) - return nil -end - ---- Returns country of the Identifiable. --- @param #IDENTIFIABLE self --- @return Dcs.DCScountry#country.id The country identifier. --- @return #nil The DCS Identifiable is not existing or alive. -function IDENTIFIABLE:GetCountry() - self:F2( self.IdentifiableName ) - - local DCSIdentifiable = self:GetDCSObject() - - if DCSIdentifiable then - local IdentifiableCountry = DCSIdentifiable:getCountry() - self:T3( IdentifiableCountry ) - return IdentifiableCountry - end - - self:E( self.ClassName .. " " .. self.IdentifiableName .. " not found!" ) - return nil -end - - - ---- Returns Identifiable descriptor. Descriptor type depends on Identifiable category. --- @param #IDENTIFIABLE self --- @return Dcs.DCSWrapper.Identifiable#Identifiable.Desc The Identifiable descriptor. --- @return #nil The DCS Identifiable is not existing or alive. -function IDENTIFIABLE:GetDesc() - self:F2( self.IdentifiableName ) - - local DCSIdentifiable = self:GetDCSObject() - - if DCSIdentifiable then - local IdentifiableDesc = DCSIdentifiable:getDesc() - self:T2( IdentifiableDesc ) - return IdentifiableDesc - end - - self:E( self.ClassName .. " " .. self.IdentifiableName .. " not found!" ) - return nil -end - ---- Gets the CallSign of the IDENTIFIABLE, which is a blank by default. --- @param #IDENTIFIABLE self --- @return #string The CallSign of the IDENTIFIABLE. -function IDENTIFIABLE:GetCallsign() - return '' -end - - -function IDENTIFIABLE:GetThreatLevel() - - return 0, "Scenery" -end ---- This module contains the POSITIONABLE class. --- --- 1) @{Positionable#POSITIONABLE} class, extends @{Identifiable#IDENTIFIABLE} --- =========================================================== --- The @{Positionable#POSITIONABLE} class is a wrapper class to handle the POSITIONABLE objects: --- --- * Support all DCS APIs. --- * Enhance with POSITIONABLE specific APIs not in the DCS API set. --- * Manage the "state" of the POSITIONABLE. --- --- 1.1) POSITIONABLE constructor: --- ------------------------------ --- The POSITIONABLE class provides the following functions to construct a POSITIONABLE instance: --- --- * @{Positionable#POSITIONABLE.New}(): Create a POSITIONABLE instance. --- --- 1.2) POSITIONABLE methods: --- -------------------------- --- The following methods can be used to identify an measurable object: --- --- * @{Positionable#POSITIONABLE.GetID}(): Returns the ID of the measurable object. --- * @{Positionable#POSITIONABLE.GetName}(): Returns the name of the measurable object. --- --- === --- --- @module Positionable - ---- The POSITIONABLE class --- @type POSITIONABLE --- @extends Wrapper.Identifiable#IDENTIFIABLE --- @field #string PositionableName The name of the measurable. -POSITIONABLE = { - ClassName = "POSITIONABLE", - PositionableName = "", -} - ---- A DCSPositionable --- @type DCSPositionable --- @field id_ The ID of the controllable in DCS - ---- Create a new POSITIONABLE from a DCSPositionable --- @param #POSITIONABLE self --- @param Dcs.DCSWrapper.Positionable#Positionable PositionableName The POSITIONABLE name --- @return #POSITIONABLE self -function POSITIONABLE:New( PositionableName ) - local self = BASE:Inherit( self, IDENTIFIABLE:New( PositionableName ) ) - - self.PositionableName = PositionableName - return self -end - ---- Returns the @{DCSTypes#Position3} position vectors indicating the point and direction vectors in 3D of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Position The 3D position vectors of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetPositionVec3() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionablePosition = DCSPositionable:getPosition().p - self:T3( PositionablePosition ) - return PositionablePosition - end - - return nil -end - ---- Returns the @{DCSTypes#Vec2} vector indicating the point in 2D of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Vec2 The 2D point vector of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetVec2() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionableVec3 = DCSPositionable:getPosition().p - - local PositionableVec2 = {} - PositionableVec2.x = PositionableVec3.x - PositionableVec2.y = PositionableVec3.z - - self:T2( PositionableVec2 ) - return PositionableVec2 - end - - return nil -end - ---- Returns a POINT_VEC2 object indicating the point in 2D of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Core.Point#POINT_VEC2 The 2D point vector of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetPointVec2() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionableVec3 = DCSPositionable:getPosition().p - - local PositionablePointVec2 = POINT_VEC2:NewFromVec3( PositionableVec3 ) - - self:T2( PositionablePointVec2 ) - return PositionablePointVec2 - end - - return nil -end - ---- Returns a POINT_VEC3 object indicating the point in 3D of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Core.Point#POINT_VEC3 The 3D point vector of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetPointVec3() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionableVec3 = self:GetPositionVec3() - - local PositionablePointVec3 = POINT_VEC3:NewFromVec3( PositionableVec3 ) - - self:T2( PositionablePointVec3 ) - return PositionablePointVec3 - end - - return nil -end - - ---- Returns a random @{DCSTypes#Vec3} vector within a range, indicating the point in 3D of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Vec3 The 3D point vector of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetRandomVec3( Radius ) - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionablePointVec3 = DCSPositionable:getPosition().p - local PositionableRandomVec3 = {} - local angle = math.random() * math.pi*2; - PositionableRandomVec3.x = PositionablePointVec3.x + math.cos( angle ) * math.random() * Radius; - PositionableRandomVec3.y = PositionablePointVec3.y - PositionableRandomVec3.z = PositionablePointVec3.z + math.sin( angle ) * math.random() * Radius; - - self:T3( PositionableRandomVec3 ) - return PositionableRandomVec3 - end - - return nil -end - ---- Returns the @{DCSTypes#Vec3} vector indicating the 3D vector of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Vec3 The 3D point vector of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetVec3() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionableVec3 = DCSPositionable:getPosition().p - self:T3( PositionableVec3 ) - return PositionableVec3 - end - - return nil -end - ---- Returns the altitude of the POSITIONABLE. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Distance The altitude of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetAltitude() - self:F2() - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionablePointVec3 = DCSPositionable:getPoint() --Dcs.DCSTypes#Vec3 - return PositionablePointVec3.y - end - - return nil -end - ---- Returns if the Positionable is located above a runway. --- @param Wrapper.Positionable#POSITIONABLE self --- @return #boolean true if Positionable is above a runway. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:IsAboveRunway() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - - local Vec2 = self:GetVec2() - local SurfaceType = land.getSurfaceType( Vec2 ) - local IsAboveRunway = SurfaceType == land.SurfaceType.RUNWAY - - self:T2( IsAboveRunway ) - return IsAboveRunway - end - - return nil -end - - - ---- Returns the POSITIONABLE heading in degrees. --- @param Wrapper.Positionable#POSITIONABLE self --- @return #number The POSTIONABLE heading -function POSITIONABLE:GetHeading() - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - - local PositionablePosition = DCSPositionable:getPosition() - if PositionablePosition then - local PositionableHeading = math.atan2( PositionablePosition.x.z, PositionablePosition.x.x ) - if PositionableHeading < 0 then - PositionableHeading = PositionableHeading + 2 * math.pi - end - PositionableHeading = PositionableHeading * 180 / math.pi - self:T2( PositionableHeading ) - return PositionableHeading - end - end - - return nil -end - - ---- Returns true if the POSITIONABLE is in the air. --- Polymorphic, is overridden in GROUP and UNIT. --- @param Wrapper.Positionable#POSITIONABLE self --- @return #boolean true if in the air. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:InAir() - self:F2( self.PositionableName ) - - return nil -end - - ---- Returns the POSITIONABLE velocity vector. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Vec3 The velocity vector --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetVelocity() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionableVelocityVec3 = DCSPositionable:getVelocity() - self:T3( PositionableVelocityVec3 ) - return PositionableVelocityVec3 - end - - return nil -end - ---- Returns the POSITIONABLE velocity in km/h. --- @param Wrapper.Positionable#POSITIONABLE self --- @return #number The velocity in km/h --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetVelocityKMH() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local VelocityVec3 = self:GetVelocity() - local Velocity = ( VelocityVec3.x ^ 2 + VelocityVec3.y ^ 2 + VelocityVec3.z ^ 2 ) ^ 0.5 -- in meters / sec - local Velocity = Velocity * 3.6 -- now it is in km/h. - self:T3( Velocity ) - return Velocity - end - - return nil -end - ---- Returns a message with the callsign embedded (if there is one). --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTypes#Duration Duration The duration of the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. --- @return Core.Message#MESSAGE -function POSITIONABLE:GetMessage( Message, Duration, Name ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - Name = Name or self:GetTypeName() - return MESSAGE:New( Message, Duration, self:GetCallsign() .. " (" .. Name .. ")" ) - end - - return nil -end - ---- Send a message to all coalitions. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTypes#Duration Duration The duration of the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. -function POSITIONABLE:MessageToAll( Message, Duration, Name ) - self:F2( { Message, Duration } ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - self:GetMessage( Message, Duration, Name ):ToAll() - end - - return nil -end - ---- Send a message to a coalition. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTYpes#Duration Duration The duration of the message. --- @param Dcs.DCScoalition#coalition MessageCoalition The Coalition receiving the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. -function POSITIONABLE:MessageToCoalition( Message, Duration, MessageCoalition, Name ) - self:F2( { Message, Duration } ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - self:GetMessage( Message, Duration, Name ):ToCoalition( MessageCoalition ) - end - - return nil -end - - ---- Send a message to the red coalition. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTYpes#Duration Duration The duration of the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. -function POSITIONABLE:MessageToRed( Message, Duration, Name ) - self:F2( { Message, Duration } ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - self:GetMessage( Message, Duration, Name ):ToRed() - end - - return nil -end - ---- Send a message to the blue coalition. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTypes#Duration Duration The duration of the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. -function POSITIONABLE:MessageToBlue( Message, Duration, Name ) - self:F2( { Message, Duration } ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - self:GetMessage( Message, Duration, Name ):ToBlue() - end - - return nil -end - ---- Send a message to a client. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTypes#Duration Duration The duration of the message. --- @param Wrapper.Client#CLIENT Client The client object receiving the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. -function POSITIONABLE:MessageToClient( Message, Duration, Client, Name ) - self:F2( { Message, Duration } ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - self:GetMessage( Message, Duration, Name ):ToClient( Client ) - end - - return nil -end - ---- Send a message to a @{Group}. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTypes#Duration Duration The duration of the message. --- @param Wrapper.Group#GROUP MessageGroup The GROUP object receiving the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. -function POSITIONABLE:MessageToGroup( Message, Duration, MessageGroup, Name ) - self:F2( { Message, Duration } ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - if DCSObject:isExist() then - self:GetMessage( Message, Duration, Name ):ToGroup( MessageGroup ) - end - end - - return nil -end - ---- Send a message to the players in the @{Group}. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #POSITIONABLE self --- @param #string Message The message text --- @param Dcs.DCSTypes#Duration Duration The duration of the message. --- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. -function POSITIONABLE:Message( Message, Duration, Name ) - self:F2( { Message, Duration } ) - - local DCSObject = self:GetDCSObject() - if DCSObject then - self:GetMessage( Message, Duration, Name ):ToGroup( self ) - end - - return nil -end - ---- Create a @{Radio#RADIO}, to allow radio transmission for this POSITIONABLE. --- Set parameters with the methods provided, then use RADIO:Broadcast() to actually broadcast the message --- @param #POSITIONABLE self --- @return #RADIO Radio -function POSITIONABLE:GetRadio() - self:F2(self) - return RADIO:New(self) -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.TaskAttackGroup}: (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_AttackGroup}: (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 - ---- Clear all tasks from the controllable. --- @param #CONTROLLABLE self --- @return #CONTROLLABLE -function CONTROLLABLE:ClearTasks() - self:F2() - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - local Controller = self:_GetController() - Controller:resetTask() - return self - end - - return nil -end - - ---- 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 "AttackGroup" and "AttackUnit" tasks. If the flag is false then attackQty is a desired attack quantity for "Bombing" and "BombingRunway" tasks. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:TaskAttackGroup( AttackGroup, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit ) - self:F2( { self.ControllableName, AttackGroup, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit } ) - - -- AttackGroup = { - -- id = 'AttackGroup', - -- 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 = 'AttackGroup', - 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 #boolean GroupAttack (optional) If true, all units in the group will attack the Unit when found. --- @param Dcs.DCSTypes#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. --- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. --- @param Dcs.DCSTypes#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. --- @param #number Altitude (optional) The altitude from where to attack. --- @param #boolean Visible (optional) not a clue. --- @param #number WeaponType (optional) The WeaponType. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:TaskAttackUnit( AttackUnit, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, Visible, WeaponType ) - self:F2( { self.ControllableName, AttackUnit, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, Visible, WeaponType } ) - - local DCSTask - DCSTask = { - id = 'AttackUnit', - params = { - unitId = AttackUnit:GetID(), - groupAttack = GroupAttack or false, - visible = Visible or false, - expend = WeaponExpend or "Auto", - directionEnabled = Direction and true or false, - direction = Direction, - altitudeEnabled = Altitude and true or false, - altitude = Altitude or 30, - attackQtyLimit = AttackQty and true or false, - attackQty = AttackQty, - weaponType = WeaponType - } - } - - 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 AttackGroup 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_AttackGroup = { --- id = 'FAC_AttackGroup', --- params = { --- groupId = Group.ID, --- weaponType = number, --- designation = enum AI.Task.Designation, --- datalink = boolean --- } --- } - - local DCSTask - DCSTask = { id = 'FAC_AttackGroup', - 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 "AttackGroup" and "AttackUnit" tasks. If the flag is false then attackQty is a desired attack quantity for "Bombing" and "BombingRunway" tasks. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -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) Search and attack the Unit. --- @param #CONTROLLABLE self --- @param Wrapper.Unit#UNIT EngageUnit The UNIT. --- @param #number Priority (optional) All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. --- @param #boolean GroupAttack (optional) If true, all units in the group will attack the Unit when found. --- @param Dcs.DCSTypes#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. --- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. --- @param Dcs.DCSTypes#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. --- @param Dcs.DCSTypes#Distance Altitude (optional) Desired altitude to perform the unit engagement. --- @param #boolean Visible (optional) Unit must be visible. --- @param #boolean ControllableAttack (optional) Flag indicates that the target must be engaged by all aircrafts of the controllable. Has effect only if the task is assigned to a controllable, not to a single aircraft. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskEngageUnit( EngageUnit, Priority, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, Visible, ControllableAttack ) - self:F2( { self.ControllableName, EngageUnit, Priority, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, Visible, ControllableAttack } ) - - -- EngageUnit = { - -- id = 'EngageUnit', - -- params = { - -- unitId = Unit.ID, - -- weaponType = number, - -- expend = enum AI.Task.WeaponExpend - -- attackQty = number, - -- direction = Azimuth, - -- attackQtyLimit = boolean, - -- controllableAttack = boolean, - -- priority = number, - -- } - -- } - - local DCSTask - DCSTask = { id = 'EngageUnit', - params = { - unitId = EngageUnit:GetID(), - priority = Priority or 1, - groupAttack = GroupAttack or false, - visible = Visible or false, - expend = WeaponExpend or "Auto", - directionEnabled = Direction and true or false, - direction = Direction, - altitudeEnabled = Altitude and true or false, - altitude = Altitude, - attackQtyLimit = AttackQty and true or false, - attackQty = AttackQty, - controllableAttack = ControllableAttack, - }, - }, - - self:T3( { DCSTask } ) - return DCSTask -end - - - ---- (AIR) Aircraft will act as an AWACS for friendly units (will provide them with information about contacts). No parameters. --- @param #CONTROLLABLE self --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskAWACS( ) - self:F2( { self.ControllableName } ) - --- AWACS = { --- id = 'AWACS', --- params = { --- } --- } - - local DCSTask - DCSTask = { id = 'AWACS', - params = { - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - ---- (AIR) Aircraft will act as a tanker for friendly units. No parameters. --- @param #CONTROLLABLE self --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskTanker( ) - self:F2( { self.ControllableName } ) - --- Tanker = { --- id = 'Tanker', --- params = { --- } --- } - - local DCSTask - DCSTask = { id = 'Tanker', - params = { - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - --- En-route tasks for ground units/controllables - ---- (GROUND) Ground unit (EW-radar) will act as an EWR for friendly units (will provide them with information about contacts). No parameters. --- @param #CONTROLLABLE self --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskEWR( ) - self:F2( { self.ControllableName } ) - --- EWR = { --- id = 'EWR', --- params = { --- } --- } - - local DCSTask - DCSTask = { id = 'EWR', - params = { - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - --- En-route tasks for airborne and ground units/controllables - ---- (AIR + GROUND) The task makes the controllable/unit a FAC and lets the FAC to choose the target (enemy ground controllable) as well as other assigned targets. --- The killer is player-controlled allied CAS-aircraft that is in contact with the FAC. --- If the task is assigned to the controllable lead unit will be a FAC. --- @param #CONTROLLABLE self --- @param Wrapper.Controllable#CONTROLLABLE AttackGroup Target CONTROLLABLE. --- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. --- @param #number WeaponType Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. --- @param Dcs.DCSTypes#AI.Task.Designation Designation (optional) Designation type. --- @param #boolean Datalink (optional) Allows to use datalink to send the target information to attack aircraft. Enabled by default. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskFAC_EngageGroup( AttackGroup, Priority, WeaponType, Designation, Datalink ) - self:F2( { self.ControllableName, AttackGroup, WeaponType, Priority, Designation, Datalink } ) - --- FAC_EngageControllable = { --- id = 'FAC_EngageControllable', --- params = { --- groupId = Group.ID, --- weaponType = number, --- designation = enum AI.Task.Designation, --- datalink = boolean, --- priority = number, --- } --- } - - local DCSTask - DCSTask = { id = 'FAC_EngageControllable', - params = { - groupId = AttackGroup:GetID(), - weaponType = WeaponType, - designation = Designation, - datalink = Datalink, - priority = Priority, - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - ---- (AIR + GROUND) The task makes the controllable/unit a FAC and lets the FAC to choose a targets (enemy ground controllable) around as well as other assigned targets. --- The killer is player-controlled allied CAS-aircraft that is in contact with the FAC. --- If the task is assigned to the controllable lead unit will be a FAC. --- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Distance Radius The maximal distance from the FAC to a target. --- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:EnRouteTaskFAC( Radius, Priority ) - self:F2( { self.ControllableName, Radius, Priority } ) - --- FAC = { --- id = 'FAC', --- params = { --- radius = Distance, --- priority = number --- } --- } - - local DCSTask - DCSTask = { id = 'FAC', - params = { - radius = Radius, - priority = Priority - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - - - ---- (AIR) Move the controllable to a Vec2 Point, wait for a defined duration and embark a controllable. --- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Vec2 Point The point where to wait. --- @param #number Duration The duration in seconds to wait. --- @param #CONTROLLABLE EmbarkingControllable The controllable to be embarked. --- @return Dcs.DCSTasking.Task#Task The DCS task structure -function CONTROLLABLE:TaskEmbarking( Point, Duration, EmbarkingControllable ) - self:F2( { self.ControllableName, Point, Duration, EmbarkingControllable.DCSControllable } ) - - local DCSTask - DCSTask = { id = 'Embarking', - params = { x = Point.x, - y = Point.y, - duration = Duration, - controllablesForEmbarking = { EmbarkingControllable.ControllableID }, - durationFlag = true, - distributionFlag = false, - distribution = {}, - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - ---- (GROUND) Embark to a Transport landed at a location. - ---- Move to a defined Vec2 Point, and embark to a controllable when arrived within a defined Radius. --- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Vec2 Point The point where to wait. --- @param #number Radius The radius of the embarking zone around the Point. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:TaskEmbarkToTransport( Point, Radius ) - self:F2( { self.ControllableName, Point, Radius } ) - - local DCSTask --Dcs.DCSTasking.Task#Task - DCSTask = { id = 'EmbarkToTransport', - params = { x = Point.x, - y = Point.y, - zoneRadius = Radius, - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - - ---- (AIR + GROUND) Return a mission task from a mission template. --- @param #CONTROLLABLE self --- @param #table TaskMission A table containing the mission task. --- @return Dcs.DCSTasking.Task#Task -function CONTROLLABLE:TaskMission( TaskMission ) - self:F2( Points ) - - local DCSTask - DCSTask = { id = 'Mission', params = { TaskMission, }, } - - self:T3( { DCSTask } ) - return DCSTask -end - ---- Return a Misson task to follow a given route defined by Points. --- @param #CONTROLLABLE self --- @param #table Points A table of route points. --- @return Dcs.DCSTasking.Task#Task -function CONTROLLABLE:TaskRoute( Points ) - self:F2( Points ) - - local DCSTask - DCSTask = { id = 'Mission', params = { route = { points = Points, }, }, } - - self:T3( { DCSTask } ) - return DCSTask -end - ---- (AIR + GROUND) Make the Controllable move to fly to a given point. --- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Vec3 Point The destination point in Vec3 format. --- @param #number Speed The speed to travel. --- @return #CONTROLLABLE self -function CONTROLLABLE:RouteToVec2( Point, Speed ) - self:F2( { Point, Speed } ) - - local ControllablePoint = self:GetUnit( 1 ):GetVec2() - - local PointFrom = {} - PointFrom.x = ControllablePoint.x - PointFrom.y = ControllablePoint.y - PointFrom.type = "Turning Point" - PointFrom.action = "Turning Point" - PointFrom.speed = Speed - PointFrom.speed_locked = true - PointFrom.properties = { - ["vnav"] = 1, - ["scale"] = 0, - ["angle"] = 0, - ["vangle"] = 0, - ["steer"] = 2, - } - - - local PointTo = {} - PointTo.x = Point.x - PointTo.y = Point.y - PointTo.type = "Turning Point" - PointTo.action = "Fly Over Point" - PointTo.speed = Speed - PointTo.speed_locked = true - PointTo.properties = { - ["vnav"] = 1, - ["scale"] = 0, - ["angle"] = 0, - ["vangle"] = 0, - ["steer"] = 2, - } - - - local Points = { PointFrom, PointTo } - - self:T3( Points ) - - self:Route( Points ) - - return self -end - ---- (AIR + GROUND) Make the Controllable move to a given point. --- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Vec3 Point The destination point in Vec3 format. --- @param #number Speed The speed to travel. --- @return #CONTROLLABLE self -function CONTROLLABLE:RouteToVec3( Point, Speed ) - self:F2( { Point, Speed } ) - - local ControllableVec3 = self:GetUnit( 1 ):GetVec3() - - local PointFrom = {} - PointFrom.x = ControllableVec3.x - PointFrom.y = ControllableVec3.z - PointFrom.alt = ControllableVec3.y - PointFrom.alt_type = "BARO" - PointFrom.type = "Turning Point" - PointFrom.action = "Turning Point" - PointFrom.speed = Speed - PointFrom.speed_locked = true - PointFrom.properties = { - ["vnav"] = 1, - ["scale"] = 0, - ["angle"] = 0, - ["vangle"] = 0, - ["steer"] = 2, - } - - - local PointTo = {} - PointTo.x = Point.x - PointTo.y = Point.z - PointTo.alt = Point.y - PointTo.alt_type = "BARO" - PointTo.type = "Turning Point" - PointTo.action = "Fly Over Point" - PointTo.speed = Speed - PointTo.speed_locked = true - PointTo.properties = { - ["vnav"] = 1, - ["scale"] = 0, - ["angle"] = 0, - ["vangle"] = 0, - ["steer"] = 2, - } - - - local Points = { PointFrom, PointTo } - - self:T3( Points ) - - self:Route( Points ) - - return self -end - - - ---- Make the controllable to follow a given route. --- @param #CONTROLLABLE self --- @param #table GoPoints A table of Route Points. --- @return #CONTROLLABLE self -function CONTROLLABLE:Route( GoPoints ) - self:F2( GoPoints ) - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - local Points = routines.utils.deepCopy( GoPoints ) - local MissionTask = { id = 'Mission', params = { route = { points = Points, }, }, } - local Controller = self:_GetController() - --Controller.setTask( Controller, MissionTask ) - self.TaskScheduler:Schedule( Controller, Controller.setTask, { MissionTask }, 1 ) - return self - end - - return nil -end - - - ---- (AIR + GROUND) Route the controllable to a given zone. --- The controllable final destination point can be randomized. --- A speed can be given in km/h. --- A given formation can be given. --- @param #CONTROLLABLE self --- @param Core.Zone#ZONE Zone The zone where to route to. --- @param #boolean Randomize Defines whether to target point gets randomized within the Zone. --- @param #number Speed The speed. --- @param Base#FORMATION Formation The formation string. -function CONTROLLABLE:TaskRouteToZone( Zone, Randomize, Speed, Formation ) - self:F2( Zone ) - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - - local ControllablePoint = self:GetVec2() - - local PointFrom = {} - PointFrom.x = ControllablePoint.x - PointFrom.y = ControllablePoint.y - PointFrom.type = "Turning Point" - PointFrom.action = Formation or "Cone" - PointFrom.speed = 20 / 1.6 - - - local PointTo = {} - local ZonePoint - - if Randomize then - ZonePoint = Zone:GetRandomVec2() - else - ZonePoint = Zone:GetVec2() - end - - PointTo.x = ZonePoint.x - PointTo.y = ZonePoint.y - PointTo.type = "Turning Point" - - if Formation then - PointTo.action = Formation - else - PointTo.action = "Cone" - end - - if Speed then - PointTo.speed = Speed - else - PointTo.speed = 20 / 1.6 - end - - local Points = { PointFrom, PointTo } - - self:T3( Points ) - - self:Route( Points ) - - return self - end - - return nil -end - ---- (AIR) Return the Controllable to an @{Airbase#AIRBASE} --- A speed can be given in km/h. --- A given formation can be given. --- @param #CONTROLLABLE self --- @param Wrapper.Airbase#AIRBASE ReturnAirbase The @{Airbase#AIRBASE} to return to. --- @param #number Speed (optional) The speed. --- @return #string The route -function CONTROLLABLE:RouteReturnToAirbase( ReturnAirbase, Speed ) - self:F2( { ReturnAirbase, Speed } ) - --- Example --- [4] = --- { --- ["alt"] = 45, --- ["type"] = "Land", --- ["action"] = "Landing", --- ["alt_type"] = "BARO", --- ["formation_template"] = "", --- ["properties"] = --- { --- ["vnav"] = 1, --- ["scale"] = 0, --- ["angle"] = 0, --- ["vangle"] = 0, --- ["steer"] = 2, --- }, -- end of ["properties"] --- ["ETA"] = 527.81058817743, --- ["airdromeId"] = 12, --- ["y"] = 243127.2973737, --- ["x"] = -5406.2803440839, --- ["name"] = "DictKey_WptName_53", --- ["speed"] = 138.88888888889, --- ["ETA_locked"] = false, --- ["task"] = --- { --- ["id"] = "ComboTask", --- ["params"] = --- { --- ["tasks"] = --- { --- }, -- end of ["tasks"] --- }, -- end of ["params"] --- }, -- end of ["task"] --- ["speed_locked"] = true, --- }, -- end of [4] - - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - - local ControllablePoint = self:GetVec2() - local ControllableVelocity = self:GetMaxVelocity() - - local PointFrom = {} - PointFrom.x = ControllablePoint.x - PointFrom.y = ControllablePoint.y - PointFrom.type = "Turning Point" - PointFrom.action = "Turning Point" - PointFrom.speed = ControllableVelocity - - - local PointTo = {} - local AirbasePoint = ReturnAirbase:GetVec2() - - PointTo.x = AirbasePoint.x - PointTo.y = AirbasePoint.y - PointTo.type = "Land" - PointTo.action = "Landing" - PointTo.airdromeId = ReturnAirbase:GetID()-- Airdrome ID - self:T(PointTo.airdromeId) - --PointTo.alt = 0 - - local Points = { PointFrom, PointTo } - - self:T3( Points ) - - local Route = { points = Points, } - - return Route - end - - return nil -end - --- Commands - ---- Do Script command --- @param #CONTROLLABLE self --- @param #string DoScript --- @return #DCSCommand -function CONTROLLABLE:CommandDoScript( DoScript ) - - local DCSDoScript = { - id = "Script", - params = { - command = DoScript, - }, - } - - self:T3( DCSDoScript ) - return DCSDoScript -end - - ---- Return the mission template of the controllable. --- @param #CONTROLLABLE self --- @return #table The MissionTemplate --- TODO: Rework the method how to retrieve a template ... -function CONTROLLABLE:GetTaskMission() - self:F2( self.ControllableName ) - - return routines.utils.deepCopy( _DATABASE.Templates.Controllables[self.ControllableName].Template ) -end - ---- Return the mission route of the controllable. --- @param #CONTROLLABLE self --- @return #table The mission route defined by points. -function CONTROLLABLE:GetTaskRoute() - self:F2( self.ControllableName ) - - return routines.utils.deepCopy( _DATABASE.Templates.Controllables[self.ControllableName].Template.route.points ) -end - ---- Return the route of a controllable by using the @{Database#DATABASE} class. --- @param #CONTROLLABLE self --- @param #number Begin The route point from where the copy will start. The base route point is 0. --- @param #number End The route point where the copy will end. The End point is the last point - the End point. The last point has base 0. --- @param #boolean Randomize Randomization of the route, when true. --- @param #number Radius When randomization is on, the randomization is within the radius. -function CONTROLLABLE:CopyRoute( Begin, End, Randomize, Radius ) - self:F2( { Begin, End } ) - - local Points = {} - - -- Could be a Spawned Controllable - local ControllableName = string.match( self:GetName(), ".*#" ) - if ControllableName then - ControllableName = ControllableName:sub( 1, -2 ) - else - ControllableName = self:GetName() - end - - self:T3( { ControllableName } ) - - local Template = _DATABASE.Templates.Controllables[ControllableName].Template - - if Template then - if not Begin then - Begin = 0 - end - if not End then - End = 0 - end - - for TPointID = Begin + 1, #Template.route.points - End do - if Template.route.points[TPointID] then - Points[#Points+1] = routines.utils.deepCopy( Template.route.points[TPointID] ) - if Randomize then - if not Radius then - Radius = 500 - end - Points[#Points].x = Points[#Points].x + math.random( Radius * -1, Radius ) - Points[#Points].y = Points[#Points].y + math.random( Radius * -1, Radius ) - end - end - end - return Points - else - error( "Template not found for Controllable : " .. ControllableName ) - end - - return nil -end - - ---- Return the detected targets of the controllable. --- The optional parametes specify the detection methods that can be applied. --- If no detection method is given, the detection will use all the available methods by default. --- @param Wrapper.Controllable#CONTROLLABLE self --- @param #boolean DetectVisual (optional) --- @param #boolean DetectOptical (optional) --- @param #boolean DetectRadar (optional) --- @param #boolean DetectIRST (optional) --- @param #boolean DetectRWR (optional) --- @param #boolean DetectDLINK (optional) --- @return #table DetectedTargets -function CONTROLLABLE:GetDetectedTargets( DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK ) - self:F2( self.ControllableName ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local DetectionVisual = ( DetectVisual and DetectVisual == true ) and Controller.Detection.VISUAL or nil - local DetectionOptical = ( DetectOptical and DetectOptical == true ) and Controller.Detection.OPTICAL or nil - local DetectionRadar = ( DetectRadar and DetectRadar == true ) and Controller.Detection.RADAR or nil - local DetectionIRST = ( DetectIRST and DetectIRST == true ) and Controller.Detection.IRST or nil - local DetectionRWR = ( DetectRWR and DetectRWR == true ) and Controller.Detection.RWR or nil - local DetectionDLINK = ( DetectDLINK and DetectDLINK == true ) and Controller.Detection.DLINK or nil - - - return self:_GetController():getDetectedTargets( DetectionVisual, DetectionOptical, DetectionRadar, DetectionIRST, DetectionRWR, DetectionDLINK ) - end - - return nil -end - -function CONTROLLABLE:IsTargetDetected( DCSObject ) - self:F2( self.ControllableName ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - - local TargetIsDetected, TargetIsVisible, TargetLastTime, TargetKnowType, TargetKnowDistance, TargetLastPos, TargetLastVelocity - = self:_GetController().isTargetDetected( self:_GetController(), DCSObject, - Controller.Detection.VISUAL, - Controller.Detection.OPTIC, - Controller.Detection.RADAR, - Controller.Detection.IRST, - Controller.Detection.RWR, - Controller.Detection.DLINK - ) - return TargetIsDetected, TargetIsVisible, TargetLastTime, TargetKnowType, TargetKnowDistance, TargetLastPos, TargetLastVelocity - end - - return nil -end - --- Options - ---- Can the CONTROLLABLE hold their weapons? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROEHoldFirePossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() or self:IsGround() or self:IsShip() then - return true - end - - return false - end - - return nil -end - ---- Holding weapons. --- @param Wrapper.Controllable#CONTROLLABLE self --- @return Wrapper.Controllable#CONTROLLABLE self -function CONTROLLABLE:OptionROEHoldFire() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD ) - elseif self:IsGround() then - Controller:setOption( AI.Option.Ground.id.ROE, AI.Option.Ground.val.ROE.WEAPON_HOLD ) - elseif self:IsShip() then - Controller:setOption( AI.Option.Naval.id.ROE, AI.Option.Naval.val.ROE.WEAPON_HOLD ) - end - - return self - end - - return nil -end - ---- Can the CONTROLLABLE attack returning on enemy fire? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROEReturnFirePossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() or self:IsGround() or self:IsShip() then - return true - end - - return false - end - - return nil -end - ---- Return fire. --- @param #CONTROLLABLE self --- @return #CONTROLLABLE self -function CONTROLLABLE:OptionROEReturnFire() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.RETURN_FIRE ) - elseif self:IsGround() then - Controller:setOption( AI.Option.Ground.id.ROE, AI.Option.Ground.val.ROE.RETURN_FIRE ) - elseif self:IsShip() then - Controller:setOption( AI.Option.Naval.id.ROE, AI.Option.Naval.val.ROE.RETURN_FIRE ) - end - - return self - end - - return nil -end - ---- Can the CONTROLLABLE attack designated targets? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROEOpenFirePossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() or self:IsGround() or self:IsShip() then - return true - end - - return false - end - - return nil -end - ---- Openfire. --- @param #CONTROLLABLE self --- @return #CONTROLLABLE self -function CONTROLLABLE:OptionROEOpenFire() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE ) - elseif self:IsGround() then - Controller:setOption( AI.Option.Ground.id.ROE, AI.Option.Ground.val.ROE.OPEN_FIRE ) - elseif self:IsShip() then - Controller:setOption( AI.Option.Naval.id.ROE, AI.Option.Naval.val.ROE.OPEN_FIRE ) - end - - return self - end - - return nil -end - ---- Can the CONTROLLABLE attack targets of opportunity? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROEWeaponFreePossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() then - return true - end - - return false - end - - return nil -end - ---- Weapon free. --- @param #CONTROLLABLE self --- @return #CONTROLLABLE self -function CONTROLLABLE:OptionROEWeaponFree() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_FREE ) - end - - return self - end - - return nil -end - ---- Can the CONTROLLABLE ignore enemy fire? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROTNoReactionPossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() then - return true - end - - return false - end - - return nil -end - - ---- No evasion on enemy threats. --- @param #CONTROLLABLE self --- @return #CONTROLLABLE self -function CONTROLLABLE:OptionROTNoReaction() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.NO_REACTION ) - end - - return self - end - - return nil -end - ---- Can the CONTROLLABLE evade using passive defenses? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROTPassiveDefensePossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() then - return true - end - - return false - end - - return nil -end - ---- Evasion passive defense. --- @param #CONTROLLABLE self --- @return #CONTROLLABLE self -function CONTROLLABLE:OptionROTPassiveDefense() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.PASSIVE_DEFENCE ) - end - - return self - end - - return nil -end - ---- Can the CONTROLLABLE evade on enemy fire? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROTEvadeFirePossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() then - return true - end - - return false - end - - return nil -end - - ---- Evade on fire. --- @param #CONTROLLABLE self --- @return #CONTROLLABLE self -function CONTROLLABLE:OptionROTEvadeFire() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE ) - end - - return self - end - - return nil -end - ---- Can the CONTROLLABLE evade on fire using vertical manoeuvres? --- @param #CONTROLLABLE self --- @return #boolean -function CONTROLLABLE:OptionROTVerticalPossible() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - if self:IsAir() then - return true - end - - return false - end - - return nil -end - - ---- Evade on fire using vertical manoeuvres. --- @param #CONTROLLABLE self --- @return #CONTROLLABLE self -function CONTROLLABLE:OptionROTVertical() - self:F2( { self.ControllableName } ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local Controller = self:_GetController() - - if self:IsAir() then - Controller:setOption( AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.BYPASS_AND_ESCAPE ) - end - - return self - end - - return nil -end - ---- Retrieve the controllable mission and allow to place function hooks within the mission waypoint plan. --- Use the method @{Controllable#CONTROLLABLE:WayPointFunction} to define the hook functions for specific waypoints. --- Use the method @{Controllable@CONTROLLABLE:WayPointExecute) to start the execution of the new mission plan. --- Note that when WayPointInitialize is called, the Mission of the controllable is RESTARTED! --- @param #CONTROLLABLE self --- @param #table WayPoints If WayPoints is given, then use the route. --- @return #CONTROLLABLE -function CONTROLLABLE:WayPointInitialize( WayPoints ) - self:F( { WayPoints } ) - - if WayPoints then - self.WayPoints = WayPoints - else - self.WayPoints = self:GetTaskRoute() - end - - return self -end - ---- Get the current WayPoints set with the WayPoint functions( Note that the WayPoints can be nil, although there ARE waypoints). --- @param #CONTROLLABLE self --- @return #table WayPoints If WayPoints is given, then return the WayPoints structure. -function CONTROLLABLE:GetWayPoints() - self:F( ) - - if self.WayPoints then - return self.WayPoints - end - - return nil -end - ---- Registers a waypoint function that will be executed when the controllable moves over the WayPoint. --- @param #CONTROLLABLE self --- @param #number WayPoint The waypoint number. Note that the start waypoint on the route is WayPoint 1! --- @param #number WayPointIndex When defining multiple WayPoint functions for one WayPoint, use WayPointIndex to set the sequence of actions. --- @param #function WayPointFunction The waypoint function to be called when the controllable moves over the waypoint. The waypoint function takes variable parameters. --- @return #CONTROLLABLE -function CONTROLLABLE:WayPointFunction( WayPoint, WayPointIndex, WayPointFunction, ... ) - self:F2( { WayPoint, WayPointIndex, WayPointFunction } ) - - table.insert( self.WayPoints[WayPoint].task.params.tasks, WayPointIndex ) - self.WayPoints[WayPoint].task.params.tasks[WayPointIndex] = self:TaskFunction( WayPoint, WayPointIndex, WayPointFunction, arg ) - return self -end - - -function CONTROLLABLE:TaskFunction( WayPoint, WayPointIndex, FunctionString, FunctionArguments ) - self:F2( { WayPoint, WayPointIndex, FunctionString, FunctionArguments } ) - - local DCSTask - - local DCSScript = {} - DCSScript[#DCSScript+1] = "local MissionControllable = GROUP:Find( ... ) " - - if FunctionArguments and #FunctionArguments > 0 then - DCSScript[#DCSScript+1] = FunctionString .. "( MissionControllable, " .. table.concat( FunctionArguments, "," ) .. ")" - else - DCSScript[#DCSScript+1] = FunctionString .. "( MissionControllable )" - end - - DCSTask = self:TaskWrappedAction( - self:CommandDoScript( - table.concat( DCSScript ) - ), WayPointIndex - ) - - self:T3( DCSTask ) - - return DCSTask - -end - ---- Executes the WayPoint plan. --- The function gets a WayPoint parameter, that you can use to restart the mission at a specific WayPoint. --- Note that when the WayPoint parameter is used, the new start mission waypoint of the controllable will be 1! --- @param #CONTROLLABLE self --- @param #number WayPoint The WayPoint from where to execute the mission. --- @param #number WaitTime The amount seconds to wait before initiating the mission. --- @return #CONTROLLABLE -function CONTROLLABLE:WayPointExecute( WayPoint, WaitTime ) - self:F( { WayPoint, WaitTime } ) - - if not WayPoint then - WayPoint = 1 - end - - -- When starting the mission from a certain point, the TaskPoints need to be deleted before the given WayPoint. - for TaskPointID = 1, WayPoint - 1 do - table.remove( self.WayPoints, 1 ) - end - - self:T3( self.WayPoints ) - - self:SetTask( self:TaskRoute( self.WayPoints ), WaitTime ) - - return self -end - --- Message APIs--- This module contains the GROUP class. --- --- 1) @{Group#GROUP} class, extends @{Controllable#CONTROLLABLE} --- ============================================================= --- The @{Group#GROUP} class is a wrapper class to handle the DCS Group objects: --- --- * Support all DCS Group APIs. --- * Enhance with Group specific APIs not in the DCS Group API set. --- * Handle local Group Controller. --- * Manage the "state" of the DCS Group. --- --- **IMPORTANT: ONE SHOULD NEVER SANATIZE these GROUP OBJECT REFERENCES! (make the GROUP object references nil).** --- --- 1.1) GROUP reference methods --- ----------------------- --- For each DCS Group object alive within a running mission, a GROUP wrapper object (instance) will be created within the _@{DATABASE} object. --- This is done at the beginning of the mission (when the mission starts), and dynamically when new DCS Group objects are spawned (using the @{SPAWN} class). --- --- The GROUP class does not contain a :New() method, rather it provides :Find() methods to retrieve the object reference --- using the DCS Group or the DCS GroupName. --- --- Another thing to know is that GROUP objects do not "contain" the DCS Group object. --- The GROUP methods will reference the DCS Group object by name when it is needed during API execution. --- If the DCS Group object does not exist or is nil, the GROUP methods will return nil and log an exception in the DCS.log file. --- --- The GROUP class provides the following functions to retrieve quickly the relevant GROUP instance: --- --- * @{#GROUP.Find}(): Find a GROUP instance from the _DATABASE object using a DCS Group object. --- * @{#GROUP.FindByName}(): Find a GROUP instance from the _DATABASE object using a DCS Group name. --- --- ## 1.2) GROUP task methods --- --- A GROUP is a @{Controllable}. See the @{Controllable} task methods section for a description of the task methods. --- --- ### 1.2.4) Obtain the mission from group templates --- --- Group templates contain complete mission descriptions. Sometimes you want to copy a complete mission from a group and assign it to another: --- --- * @{Controllable#CONTROLLABLE.TaskMission}: (AIR + GROUND) Return a mission task from a mission template. --- --- ## 1.3) GROUP Command methods --- --- A GROUP is a @{Controllable}. See the @{Controllable} command methods section for a description of the command methods. --- --- ## 1.4) GROUP option methods --- --- A GROUP is a @{Controllable}. See the @{Controllable} option methods section for a description of the option methods. --- --- ## 1.5) GROUP Zone validation methods --- --- The group can be validated whether it is completely, partly or not within a @{Zone}. --- Use the following Zone validation methods on the group: --- --- * @{#GROUP.IsCompletelyInZone}: Returns true if all units of the group are within a @{Zone}. --- * @{#GROUP.IsPartlyInZone}: Returns true if some units of the group are within a @{Zone}. --- * @{#GROUP.IsNotInZone}: Returns true if none of the group units of the group are within a @{Zone}. --- --- The zone can be of any @{Zone} class derived from @{Zone#ZONE_BASE}. So, these methods are polymorphic to the zones tested on. --- --- ## 1.6) GROUP AI methods --- --- A GROUP has AI methods to control the AI activation. --- --- * @{#GROUP.SetAIOnOff}(): Turns the GROUP AI On or Off. --- * @{#GROUP.SetAIOn}(): Turns the GROUP AI On. --- * @{#GROUP.SetAIOff}(): Turns the GROUP AI Off. --- --- ==== --- --- # **API CHANGE HISTORY** --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- Hereby the change log: --- --- 2017-03-07: GROUP:**HandleEvent( Event, EventFunction )** added. --- 2017-03-07: GROUP:**UnHandleEvent( Event )** added. --- --- 2017-01-24: GROUP:**SetAIOnOff( AIOnOff )** added. --- --- 2017-01-24: GROUP:**SetAIOn()** added. --- --- 2017-01-24: GROUP:**SetAIOff()** added. --- --- === --- --- # **AUTHORS and CONTRIBUTIONS** --- --- ### Contributions: --- --- * [**Entropy**](https://forums.eagle.ru/member.php?u=111471), **Afinegan**: Came up with the requirement for AIOnOff(). --- --- ### Authors: --- --- * **FlightControl**: Design & Programming --- --- @module Group --- @author FlightControl - ---- The GROUP class --- @type GROUP --- @extends Wrapper.Controllable#CONTROLLABLE --- @field #string GroupName The name of the group. -GROUP = { - ClassName = "GROUP", -} - ---- Create a new GROUP from a DCSGroup --- @param #GROUP self --- @param Dcs.DCSWrapper.Group#Group GroupName The DCS Group name --- @return #GROUP self -function GROUP:Register( GroupName ) - self = BASE:Inherit( self, CONTROLLABLE:New( GroupName ) ) - self:F2( GroupName ) - self.GroupName = GroupName - - self:SetEventPriority( 4 ) - return self -end - --- Reference methods. - ---- Find the GROUP wrapper class instance using the DCS Group. --- @param #GROUP self --- @param Dcs.DCSWrapper.Group#Group DCSGroup The DCS Group. --- @return #GROUP The GROUP. -function GROUP:Find( DCSGroup ) - - local GroupName = DCSGroup:getName() -- Wrapper.Group#GROUP - local GroupFound = _DATABASE:FindGroup( GroupName ) - return GroupFound -end - ---- Find the created GROUP using the DCS Group Name. --- @param #GROUP self --- @param #string GroupName The DCS Group Name. --- @return #GROUP The GROUP. -function GROUP:FindByName( GroupName ) - - local GroupFound = _DATABASE:FindGroup( GroupName ) - return GroupFound -end - --- DCS Group methods support. - ---- Returns the DCS Group. --- @param #GROUP self --- @return Dcs.DCSWrapper.Group#Group The DCS Group. -function GROUP:GetDCSObject() - local DCSGroup = Group.getByName( self.GroupName ) - - if DCSGroup then - return DCSGroup - end - - return nil -end - ---- Returns the @{DCSTypes#Position3} position vectors indicating the point and direction vectors in 3D of the POSITIONABLE within the mission. --- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Position The 3D position vectors of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. -function GROUP:GetPositionVec3() -- Overridden from POSITIONABLE:GetPositionVec3() - self:F2( self.PositionableName ) - - local DCSPositionable = self:GetDCSObject() - - if DCSPositionable then - local PositionablePosition = DCSPositionable:getUnits()[1]:getPosition().p - self:T3( PositionablePosition ) - return PositionablePosition - end - - return nil -end - ---- Returns if the DCS Group is alive. --- When the group exists at run-time, this method will return true, otherwise false. --- @param #GROUP self --- @return #boolean true if the DCS Group is alive. -function GROUP:IsAlive() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupIsAlive = DCSGroup:isExist() and DCSGroup:getUnit(1) ~= nil - self:T3( GroupIsAlive ) - return GroupIsAlive - end - - return nil -end - ---- Destroys the DCS Group and all of its DCS Units. --- Note that this destroy method also raises a destroy event at run-time. --- So all event listeners will catch the destroy event of this DCS Group. --- @param #GROUP self -function GROUP:Destroy() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - for Index, UnitData in pairs( DCSGroup:getUnits() ) do - self:CreateEventCrash( timer.getTime(), UnitData ) - end - DCSGroup:destroy() - DCSGroup = nil - end - - return nil -end - ---- Returns category of the DCS Group. --- @param #GROUP self --- @return Dcs.DCSWrapper.Group#Group.Category The category ID -function GROUP:GetCategory() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - if DCSGroup then - local GroupCategory = DCSGroup:getCategory() - self:T3( GroupCategory ) - return GroupCategory - end - - return nil -end - ---- Returns the category name of the #GROUP. --- @param #GROUP self --- @return #string Category name = Helicopter, Airplane, Ground Unit, Ship -function GROUP:GetCategoryName() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - if DCSGroup then - local CategoryNames = { - [Group.Category.AIRPLANE] = "Airplane", - [Group.Category.HELICOPTER] = "Helicopter", - [Group.Category.GROUND] = "Ground Unit", - [Group.Category.SHIP] = "Ship", - } - local GroupCategory = DCSGroup:getCategory() - self:T3( GroupCategory ) - - return CategoryNames[GroupCategory] - end - - return nil -end - - ---- Returns the coalition of the DCS Group. --- @param #GROUP self --- @return Dcs.DCSCoalitionWrapper.Object#coalition.side The coalition side of the DCS Group. -function GROUP:GetCoalition() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - if DCSGroup then - local GroupCoalition = DCSGroup:getCoalition() - self:T3( GroupCoalition ) - return GroupCoalition - end - - return nil -end - ---- Returns the country of the DCS Group. --- @param #GROUP self --- @return Dcs.DCScountry#country.id The country identifier. --- @return #nil The DCS Group is not existing or alive. -function GROUP:GetCountry() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - if DCSGroup then - local GroupCountry = DCSGroup:getUnit(1):getCountry() - self:T3( GroupCountry ) - return GroupCountry - end - - return nil -end - ---- Returns the UNIT wrapper class with number UnitNumber. --- If the underlying DCS Unit does not exist, the method will return nil. . --- @param #GROUP self --- @param #number UnitNumber The number of the UNIT wrapper class to be returned. --- @return Wrapper.Unit#UNIT The UNIT wrapper class. -function GROUP:GetUnit( UnitNumber ) - self:F2( { self.GroupName, UnitNumber } ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local UnitFound = UNIT:Find( DCSGroup:getUnit( UnitNumber ) ) - self:T2( UnitFound ) - return UnitFound - end - - return nil -end - ---- Returns the DCS Unit with number UnitNumber. --- If the underlying DCS Unit does not exist, the method will return nil. . --- @param #GROUP self --- @param #number UnitNumber The number of the DCS Unit to be returned. --- @return Dcs.DCSWrapper.Unit#Unit The DCS Unit. -function GROUP:GetDCSUnit( UnitNumber ) - self:F2( { self.GroupName, UnitNumber } ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local DCSUnitFound = DCSGroup:getUnit( UnitNumber ) - self:T3( DCSUnitFound ) - return DCSUnitFound - end - - return nil -end - ---- Returns current size of the DCS Group. --- If some of the DCS Units of the DCS Group are destroyed the size of the DCS Group is changed. --- @param #GROUP self --- @return #number The DCS Group size. -function GROUP:GetSize() - self:F2( { self.GroupName } ) - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupSize = DCSGroup:getSize() - self:T3( GroupSize ) - return GroupSize - end - - return nil -end - ---- ---- Returns the initial size of the DCS Group. --- If some of the DCS Units of the DCS Group are destroyed, the initial size of the DCS Group is unchanged. --- @param #GROUP self --- @return #number The DCS Group initial size. -function GROUP:GetInitialSize() - self:F2( { self.GroupName } ) - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupInitialSize = DCSGroup:getInitialSize() - self:T3( GroupInitialSize ) - return GroupInitialSize - end - - return nil -end - - ---- Returns the DCS Units of the DCS Group. --- @param #GROUP self --- @return #table The DCS Units. -function GROUP:GetDCSUnits() - self:F2( { self.GroupName } ) - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local DCSUnits = DCSGroup:getUnits() - self:T3( DCSUnits ) - return DCSUnits - end - - return nil -end - - ---- Activates a GROUP. --- @param #GROUP self -function GROUP:Activate() - self:F2( { self.GroupName } ) - trigger.action.activateGroup( self:GetDCSObject() ) - return self:GetDCSObject() -end - - ---- Gets the type name of the group. --- @param #GROUP self --- @return #string The type name of the group. -function GROUP:GetTypeName() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupTypeName = DCSGroup:getUnit(1):getTypeName() - self:T3( GroupTypeName ) - return( GroupTypeName ) - end - - return nil -end - ---- Gets the CallSign of the first DCS Unit of the DCS Group. --- @param #GROUP self --- @return #string The CallSign of the first DCS Unit of the DCS Group. -function GROUP:GetCallsign() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupCallSign = DCSGroup:getUnit(1):getCallsign() - self:T3( GroupCallSign ) - return GroupCallSign - end - - return nil -end - ---- Returns the current point (Vec2 vector) of the first DCS Unit in the DCS Group. --- @param #GROUP self --- @return Dcs.DCSTypes#Vec2 Current Vec2 point of the first DCS Unit of the DCS Group. -function GROUP:GetVec2() - self:F2( self.GroupName ) - - local UnitPoint = self:GetUnit(1) - UnitPoint:GetVec2() - local GroupPointVec2 = UnitPoint:GetVec2() - self:T3( GroupPointVec2 ) - return GroupPointVec2 -end - ---- Returns the current Vec3 vector of the first DCS Unit in the GROUP. --- @return Dcs.DCSTypes#Vec3 Current Vec3 of the first DCS Unit of the GROUP. -function GROUP:GetVec3() - self:F2( self.GroupName ) - - local GroupVec3 = self:GetUnit(1):GetVec3() - self:T3( GroupVec3 ) - return GroupVec3 -end - - - -do -- Is Zone methods - ---- Returns true if all units of the group are within a @{Zone}. --- @param #GROUP self --- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the Group is completely within the @{Zone#ZONE_BASE} -function GROUP:IsCompletelyInZone( Zone ) - self:F2( { self.GroupName, Zone } ) - - for UnitID, UnitData in pairs( self:GetUnits() ) do - local Unit = UnitData -- Wrapper.Unit#UNIT - if Zone:IsVec3InZone( Unit:GetVec3() ) then - else - return false - end - end - - return true -end - ---- Returns true if some units of the group are within a @{Zone}. --- @param #GROUP self --- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the Group is completely within the @{Zone#ZONE_BASE} -function GROUP:IsPartlyInZone( Zone ) - self:F2( { self.GroupName, Zone } ) - - for UnitID, UnitData in pairs( self:GetUnits() ) do - local Unit = UnitData -- Wrapper.Unit#UNIT - if Zone:IsVec3InZone( Unit:GetVec3() ) then - return true - end - end - - return false -end - ---- Returns true if none of the group units of the group are within a @{Zone}. --- @param #GROUP self --- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the Group is completely within the @{Zone#ZONE_BASE} -function GROUP:IsNotInZone( Zone ) - self:F2( { self.GroupName, Zone } ) - - for UnitID, UnitData in pairs( self:GetUnits() ) do - local Unit = UnitData -- Wrapper.Unit#UNIT - if Zone:IsVec3InZone( Unit:GetVec3() ) then - return false - end - end - - return true -end - ---- Returns if the group is of an air category. --- If the group is a helicopter or a plane, then this method will return true, otherwise false. --- @param #GROUP self --- @return #boolean Air category evaluation result. -function GROUP:IsAir() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local IsAirResult = DCSGroup:getCategory() == Group.Category.AIRPLANE or DCSGroup:getCategory() == Group.Category.HELICOPTER - self:T3( IsAirResult ) - return IsAirResult - end - - return nil -end - ---- Returns if the DCS Group contains Helicopters. --- @param #GROUP self --- @return #boolean true if DCS Group contains Helicopters. -function GROUP:IsHelicopter() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupCategory = DCSGroup:getCategory() - self:T2( GroupCategory ) - return GroupCategory == Group.Category.HELICOPTER - end - - return nil -end - ---- Returns if the DCS Group contains AirPlanes. --- @param #GROUP self --- @return #boolean true if DCS Group contains AirPlanes. -function GROUP:IsAirPlane() - self:F2() - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupCategory = DCSGroup:getCategory() - self:T2( GroupCategory ) - return GroupCategory == Group.Category.AIRPLANE - end - - return nil -end - ---- Returns if the DCS Group contains Ground troops. --- @param #GROUP self --- @return #boolean true if DCS Group contains Ground troops. -function GROUP:IsGround() - self:F2() - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupCategory = DCSGroup:getCategory() - self:T2( GroupCategory ) - return GroupCategory == Group.Category.GROUND - end - - return nil -end - ---- Returns if the DCS Group contains Ships. --- @param #GROUP self --- @return #boolean true if DCS Group contains Ships. -function GROUP:IsShip() - self:F2() - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupCategory = DCSGroup:getCategory() - self:T2( GroupCategory ) - return GroupCategory == Group.Category.SHIP - end - - return nil -end - ---- Returns if all units of the group are on the ground or landed. --- If all units of this group are on the ground, this function will return true, otherwise false. --- @param #GROUP self --- @return #boolean All units on the ground result. -function GROUP:AllOnGround() - self:F2() - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local AllOnGroundResult = true - - for Index, UnitData in pairs( DCSGroup:getUnits() ) do - if UnitData:inAir() then - AllOnGroundResult = false - end - end - - self:T3( AllOnGroundResult ) - return AllOnGroundResult - end - - return nil -end - -end - -do -- AI methods - - --- Turns the AI On or Off for the GROUP. - -- @param #GROUP self - -- @param #boolean AIOnOff The value true turns the AI On, the value false turns the AI Off. - -- @return #GROUP The GROUP. - function GROUP:SetAIOnOff( AIOnOff ) - - local DCSGroup = self:GetDCSObject() -- Dcs.DCSGroup#Group - - if DCSGroup then - local DCSController = DCSGroup:getController() -- Dcs.DCSController#Controller - if DCSController then - DCSController:setOnOff( AIOnOff ) - return self - end - end - - return nil - end - - --- Turns the AI On for the GROUP. - -- @param #GROUP self - -- @return #GROUP The GROUP. - function GROUP:SetAIOn() - - return self:SetAIOnOff( true ) - end - - --- Turns the AI Off for the GROUP. - -- @param #GROUP self - -- @return #GROUP The GROUP. - function GROUP:SetAIOff() - - return self:SetAIOnOff( false ) - end - -end - - - ---- Returns the current maximum velocity of the group. --- Each unit within the group gets evaluated, and the maximum velocity (= the unit which is going the fastest) is returned. --- @param #GROUP self --- @return #number Maximum velocity found. -function GROUP:GetMaxVelocity() - self:F2() - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local GroupVelocityMax = 0 - - for Index, UnitData in pairs( DCSGroup:getUnits() ) do - - local UnitVelocityVec3 = UnitData:getVelocity() - local UnitVelocity = math.abs( UnitVelocityVec3.x ) + math.abs( UnitVelocityVec3.y ) + math.abs( UnitVelocityVec3.z ) - - if UnitVelocity > GroupVelocityMax then - GroupVelocityMax = UnitVelocity - end - end - - return GroupVelocityMax - end - - return nil -end - ---- Returns the current minimum height of the group. --- Each unit within the group gets evaluated, and the minimum height (= the unit which is the lowest elevated) is returned. --- @param #GROUP self --- @return #number Minimum height found. -function GROUP:GetMinHeight() - self:F2() - -end - ---- Returns the current maximum height of the group. --- Each unit within the group gets evaluated, and the maximum height (= the unit which is the highest elevated) is returned. --- @param #GROUP self --- @return #number Maximum height found. -function GROUP:GetMaxHeight() - self:F2() - -end - --- SPAWNING - ---- Respawn the @{GROUP} using a (tweaked) template of the Group. --- The template must be retrieved with the @{Group#GROUP.GetTemplate}() function. --- The template contains all the definitions as declared within the mission file. --- To understand templates, do the following: --- --- * unpack your .miz file into a directory using 7-zip. --- * browse in the directory created to the file **mission**. --- * open the file and search for the country group definitions. --- --- Your group template will contain the fields as described within the mission file. --- --- This function will: --- --- * Get the current position and heading of the group. --- * When the group is alive, it will tweak the template x, y and heading coordinates of the group and the embedded units to the current units positions. --- * Then it will destroy the current alive group. --- * And it will respawn the group using your new template definition. --- @param Wrapper.Group#GROUP self --- @param #table Template The template of the Group retrieved with GROUP:GetTemplate() -function GROUP:Respawn( Template ) - - local Vec3 = self:GetVec3() - Template.x = Vec3.x - Template.y = Vec3.z - --Template.x = nil - --Template.y = nil - - self:E( #Template.units ) - for UnitID, UnitData in pairs( self:GetUnits() ) do - local GroupUnit = UnitData -- Wrapper.Unit#UNIT - self:E( GroupUnit:GetName() ) - if GroupUnit:IsAlive() then - local GroupUnitVec3 = GroupUnit:GetVec3() - local GroupUnitHeading = GroupUnit:GetHeading() - Template.units[UnitID].alt = GroupUnitVec3.y - Template.units[UnitID].x = GroupUnitVec3.x - Template.units[UnitID].y = GroupUnitVec3.z - Template.units[UnitID].heading = GroupUnitHeading - self:E( { UnitID, Template.units[UnitID], Template.units[UnitID] } ) - end - end - - self:Destroy() - _DATABASE:Spawn( Template ) -end - ---- Returns the group template from the @{DATABASE} (_DATABASE object). --- @param #GROUP self --- @return #table -function GROUP:GetTemplate() - local GroupName = self:GetName() - self:E( GroupName ) - return _DATABASE:GetGroupTemplate( GroupName ) -end - ---- Sets the controlled status in a Template. --- @param #GROUP self --- @param #boolean Controlled true is controlled, false is uncontrolled. --- @return #table -function GROUP:SetTemplateControlled( Template, Controlled ) - Template.uncontrolled = not Controlled - return Template -end - ---- Sets the CountryID of the group in a Template. --- @param #GROUP self --- @param Dcs.DCScountry#country.id CountryID The country ID. --- @return #table -function GROUP:SetTemplateCountry( Template, CountryID ) - Template.CountryID = CountryID - return Template -end - ---- Sets the CoalitionID of the group in a Template. --- @param #GROUP self --- @param Dcs.DCSCoalitionWrapper.Object#coalition.side CoalitionID The coalition ID. --- @return #table -function GROUP:SetTemplateCoalition( Template, CoalitionID ) - Template.CoalitionID = CoalitionID - return Template -end - - - - ---- Return the mission template of the group. --- @param #GROUP self --- @return #table The MissionTemplate -function GROUP:GetTaskMission() - self:F2( self.GroupName ) - - return routines.utils.deepCopy( _DATABASE.Templates.Groups[self.GroupName].Template ) -end - ---- Return the mission route of the group. --- @param #GROUP self --- @return #table The mission route defined by points. -function GROUP:GetTaskRoute() - self:F2( self.GroupName ) - - return routines.utils.deepCopy( _DATABASE.Templates.Groups[self.GroupName].Template.route.points ) -end - ---- Return the route of a group by using the @{Database#DATABASE} class. --- @param #GROUP self --- @param #number Begin The route point from where the copy will start. The base route point is 0. --- @param #number End The route point where the copy will end. The End point is the last point - the End point. The last point has base 0. --- @param #boolean Randomize Randomization of the route, when true. --- @param #number Radius When randomization is on, the randomization is within the radius. -function GROUP:CopyRoute( Begin, End, Randomize, Radius ) - self:F2( { Begin, End } ) - - local Points = {} - - -- Could be a Spawned Group - local GroupName = string.match( self:GetName(), ".*#" ) - if GroupName then - GroupName = GroupName:sub( 1, -2 ) - else - GroupName = self:GetName() - end - - self:T3( { GroupName } ) - - local Template = _DATABASE.Templates.Groups[GroupName].Template - - if Template then - if not Begin then - Begin = 0 - end - if not End then - End = 0 - end - - for TPointID = Begin + 1, #Template.route.points - End do - if Template.route.points[TPointID] then - Points[#Points+1] = routines.utils.deepCopy( Template.route.points[TPointID] ) - if Randomize then - if not Radius then - Radius = 500 - end - Points[#Points].x = Points[#Points].x + math.random( Radius * -1, Radius ) - Points[#Points].y = Points[#Points].y + math.random( Radius * -1, Radius ) - end - end - end - return Points - else - error( "Template not found for Group : " .. GroupName ) - end - - return nil -end - ---- Calculate the maxium A2G threat level of the Group. --- @param #GROUP self -function GROUP:CalculateThreatLevelA2G() - - local MaxThreatLevelA2G = 0 - for UnitName, UnitData in pairs( self:GetUnits() ) do - local ThreatUnit = UnitData -- Wrapper.Unit#UNIT - local ThreatLevelA2G = ThreatUnit:GetThreatLevel() - if ThreatLevelA2G > MaxThreatLevelA2G then - MaxThreatLevelA2G = ThreatLevelA2G - end - end - - self:T3( MaxThreatLevelA2G ) - return MaxThreatLevelA2G -end - ---- Returns true if the first unit of the GROUP is in the air. --- @param Wrapper.Group#GROUP self --- @return #boolean true if in the first unit of the group is in the air. --- @return #nil The GROUP is not existing or not alive. -function GROUP:InAir() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - local DCSUnit = DCSGroup:getUnit(1) - if DCSUnit then - local GroupInAir = DCSGroup:getUnit(1):inAir() - self:T3( GroupInAir ) - return GroupInAir - end - end - - return nil -end - -function GROUP:OnReSpawn( ReSpawnFunction ) - - self.ReSpawnFunction = ReSpawnFunction -end - -do -- Event Handling - - --- Subscribe to a DCS Event. - -- @param #GROUP self - -- @param Core.Event#EVENTS Event - -- @param #function EventFunction (optional) The function to be called when the event occurs for the GROUP. - -- @return #GROUP - function GROUP:HandleEvent( Event, EventFunction ) - - self:EventDispatcher():OnEventForGroup( self:GetName(), EventFunction, self, Event ) - - return self - end - - --- UnSubscribe to a DCS event. - -- @param #GROUP self - -- @param Core.Event#EVENTS Event - -- @return #GROUP - function GROUP:UnHandleEvent( Event ) - - self:EventDispatcher():RemoveForGroup( self:GetName(), self, Event ) - - return self - end - -end - -do -- Players - - --- Get player names - -- @param #GROUP self - -- @return #table The group has players, an array of player names is returned. - -- @return #nil The group has no players - function GROUP:GetPlayerNames() - - local PlayerNames = nil - - local Units = self:GetUnits() - for UnitID, UnitData in pairs( Units ) do - local Unit = UnitData -- Wrapper.Unit#UNIT - local PlayerName = Unit:GetPlayerName() - if PlayerName and PlayerName ~= "" then - PlayerNames = PlayerNames or {} - table.insert( PlayerNames, PlayerName ) - end - end - - self:F( PlayerNames ) - return PlayerNames - end - -end--- This module contains the UNIT class. --- --- 1) @{#UNIT} class, extends @{Controllable#CONTROLLABLE} --- =========================================================== --- The @{#UNIT} class is a wrapper class to handle the DCS Unit objects: --- --- * Support all DCS Unit APIs. --- * Enhance with Unit specific APIs not in the DCS Unit API set. --- * Handle local Unit Controller. --- * Manage the "state" of the DCS Unit. --- --- --- 1.1) UNIT reference methods --- ---------------------- --- For each DCS Unit object alive within a running mission, a UNIT wrapper object (instance) will be created within the _@{DATABASE} object. --- This is done at the beginning of the mission (when the mission starts), and dynamically when new DCS Unit objects are spawned (using the @{SPAWN} class). --- --- The UNIT class **does not contain a :New()** method, rather it provides **:Find()** methods to retrieve the object reference --- using the DCS Unit or the DCS UnitName. --- --- Another thing to know is that UNIT objects do not "contain" the DCS Unit object. --- The UNIT methods will reference the DCS Unit object by name when it is needed during API execution. --- If the DCS Unit object does not exist or is nil, the UNIT methods will return nil and log an exception in the DCS.log file. --- --- The UNIT class provides the following functions to retrieve quickly the relevant UNIT instance: --- --- * @{#UNIT.Find}(): Find a UNIT instance from the _DATABASE object using a DCS Unit object. --- * @{#UNIT.FindByName}(): Find a UNIT instance from the _DATABASE object using a DCS Unit name. --- --- IMPORTANT: ONE SHOULD NEVER SANATIZE these UNIT OBJECT REFERENCES! (make the UNIT object references nil). --- --- 1.2) DCS UNIT APIs --- ------------------ --- The DCS Unit APIs are used extensively within MOOSE. The UNIT class has for each DCS Unit API a corresponding method. --- To be able to distinguish easily in your code the difference between a UNIT API call and a DCS Unit API call, --- the first letter of the method is also capitalized. So, by example, the DCS Unit method @{DCSWrapper.Unit#Unit.getName}() --- is implemented in the UNIT class as @{#UNIT.GetName}(). --- --- 1.3) Smoke, Flare Units --- ----------------------- --- The UNIT class provides methods to smoke or flare units easily. --- The @{#UNIT.SmokeBlue}(), @{#UNIT.SmokeGreen}(),@{#UNIT.SmokeOrange}(), @{#UNIT.SmokeRed}(), @{#UNIT.SmokeRed}() methods --- will smoke the unit in the corresponding color. Note that smoking a unit is done at the current position of the DCS Unit. --- When the DCS Unit moves for whatever reason, the smoking will still continue! --- The @{#UNIT.FlareGreen}(), @{#UNIT.FlareRed}(), @{#UNIT.FlareWhite}(), @{#UNIT.FlareYellow}() --- methods will fire off a flare in the air with the corresponding color. Note that a flare is a one-off shot and its effect is of very short duration. --- --- 1.4) Location Position, Point --- ----------------------------- --- The UNIT class provides methods to obtain the current point or position of the DCS Unit. --- The @{#UNIT.GetPointVec2}(), @{#UNIT.GetVec3}() will obtain the current **location** of the DCS Unit in a Vec2 (2D) or a **point** in a Vec3 (3D) vector respectively. --- If you want to obtain the complete **3D position** including ori�ntation and direction vectors, consult the @{#UNIT.GetPositionVec3}() method respectively. --- --- 1.5) Test if alive --- ------------------ --- The @{#UNIT.IsAlive}(), @{#UNIT.IsActive}() methods determines if the DCS Unit is alive, meaning, it is existing and active. --- --- 1.6) Test for proximity --- ----------------------- --- The UNIT class contains methods to test the location or proximity against zones or other objects. --- --- ### 1.6.1) Zones --- To test whether the Unit is within a **zone**, use the @{#UNIT.IsInZone}() or the @{#UNIT.IsNotInZone}() methods. Any zone can be tested on, but the zone must be derived from @{Zone#ZONE_BASE}. --- --- ### 1.6.2) Units --- Test if another DCS Unit is within a given radius of the current DCS Unit, use the @{#UNIT.OtherUnitInRadius}() method. --- --- @module Unit --- @author FlightControl - - - - - ---- The UNIT class --- @type UNIT --- @extends Wrapper.Controllable#CONTROLLABLE -UNIT = { - ClassName="UNIT", -} - - ---- Unit.SensorType --- @type Unit.SensorType --- @field OPTIC --- @field RADAR --- @field IRST --- @field RWR - - --- Registration. - ---- Create a new UNIT from DCSUnit. --- @param #UNIT self --- @param #string UnitName The name of the DCS unit. --- @return #UNIT -function UNIT:Register( UnitName ) - local self = BASE:Inherit( self, CONTROLLABLE:New( UnitName ) ) - self.UnitName = UnitName - - self:SetEventPriority( 3 ) - return self -end - --- Reference methods. - ---- Finds a UNIT from the _DATABASE using a DCSUnit object. --- @param #UNIT self --- @param Dcs.DCSWrapper.Unit#Unit DCSUnit An existing DCS Unit object reference. --- @return #UNIT self -function UNIT:Find( DCSUnit ) - - local UnitName = DCSUnit:getName() - local UnitFound = _DATABASE:FindUnit( UnitName ) - return UnitFound -end - ---- Find a UNIT in the _DATABASE using the name of an existing DCS Unit. --- @param #UNIT self --- @param #string UnitName The Unit Name. --- @return #UNIT self -function UNIT:FindByName( UnitName ) - - local UnitFound = _DATABASE:FindUnit( UnitName ) - return UnitFound -end - ---- Return the name of the UNIT. --- @param #UNIT self --- @return #string The UNIT name. -function UNIT:Name() - - return self.UnitName -end - - ---- @param #UNIT self --- @return Dcs.DCSWrapper.Unit#Unit -function UNIT:GetDCSObject() - - local DCSUnit = Unit.getByName( self.UnitName ) - - if DCSUnit then - return DCSUnit - end - - return nil -end - ---- Respawn the @{Unit} using a (tweaked) template of the parent Group. --- --- This function will: --- --- * Get the current position and heading of the group. --- * When the unit is alive, it will tweak the template x, y and heading coordinates of the group and the embedded units to the current units positions. --- * Then it will respawn the re-modelled group. --- --- @param #UNIT self --- @param Dcs.DCSTypes#Vec3 SpawnVec3 The position where to Spawn the new Unit at. --- @param #number Heading The heading of the unit respawn. -function UNIT:ReSpawn( SpawnVec3, Heading ) - - local SpawnGroupTemplate = UTILS.DeepCopy( _DATABASE:GetGroupTemplateFromUnitName( self:Name() ) ) - self:T( SpawnGroupTemplate ) - - local SpawnGroup = self:GetGroup() - - if SpawnGroup then - - local Vec3 = SpawnGroup:GetVec3() - SpawnGroupTemplate.x = SpawnVec3.x - SpawnGroupTemplate.y = SpawnVec3.z - - self:E( #SpawnGroupTemplate.units ) - for UnitID, UnitData in pairs( SpawnGroup:GetUnits() ) do - local GroupUnit = UnitData -- #UNIT - self:E( GroupUnit:GetName() ) - if GroupUnit:IsAlive() then - local GroupUnitVec3 = GroupUnit:GetVec3() - local GroupUnitHeading = GroupUnit:GetHeading() - SpawnGroupTemplate.units[UnitID].alt = GroupUnitVec3.y - SpawnGroupTemplate.units[UnitID].x = GroupUnitVec3.x - SpawnGroupTemplate.units[UnitID].y = GroupUnitVec3.z - SpawnGroupTemplate.units[UnitID].heading = GroupUnitHeading - self:E( { UnitID, SpawnGroupTemplate.units[UnitID], SpawnGroupTemplate.units[UnitID] } ) - end - end - end - - for UnitTemplateID, UnitTemplateData in pairs( SpawnGroupTemplate.units ) do - self:T( UnitTemplateData.name ) - if UnitTemplateData.name == self:Name() then - self:T("Adjusting") - SpawnGroupTemplate.units[UnitTemplateID].alt = SpawnVec3.y - SpawnGroupTemplate.units[UnitTemplateID].x = SpawnVec3.x - SpawnGroupTemplate.units[UnitTemplateID].y = SpawnVec3.z - SpawnGroupTemplate.units[UnitTemplateID].heading = Heading - self:E( { UnitTemplateID, SpawnGroupTemplate.units[UnitTemplateID], SpawnGroupTemplate.units[UnitTemplateID] } ) - else - self:E( SpawnGroupTemplate.units[UnitTemplateID].name ) - local GroupUnit = UNIT:FindByName( SpawnGroupTemplate.units[UnitTemplateID].name ) -- #UNIT - if GroupUnit and GroupUnit:IsAlive() then - local GroupUnitVec3 = GroupUnit:GetVec3() - local GroupUnitHeading = GroupUnit:GetHeading() - UnitTemplateData.alt = GroupUnitVec3.y - UnitTemplateData.x = GroupUnitVec3.x - UnitTemplateData.y = GroupUnitVec3.z - UnitTemplateData.heading = GroupUnitHeading - else - if SpawnGroupTemplate.units[UnitTemplateID].name ~= self:Name() then - self:T("nilling") - SpawnGroupTemplate.units[UnitTemplateID].delete = true - end - end - end - end - - -- Remove obscolete units from the group structure - i = 1 - while i <= #SpawnGroupTemplate.units do - - local UnitTemplateData = SpawnGroupTemplate.units[i] - self:T( UnitTemplateData.name ) - - if UnitTemplateData.delete then - table.remove( SpawnGroupTemplate.units, i ) - else - i = i + 1 - end - end - - _DATABASE:Spawn( SpawnGroupTemplate ) -end - - - ---- Returns if the unit is activated. --- @param #UNIT self --- @return #boolean true if Unit is activated. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:IsActive() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - - local UnitIsActive = DCSUnit:isActive() - return UnitIsActive - end - - return nil -end - - - ---- Returns the Unit's callsign - the localized string. --- @param #UNIT self --- @return #string The Callsign of the Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetCallsign() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitCallSign = DCSUnit:getCallsign() - return UnitCallSign - end - - self:E( self.ClassName .. " " .. self.UnitName .. " not found!" ) - return nil -end - - ---- Returns name of the player that control the unit or nil if the unit is controlled by A.I. --- @param #UNIT self --- @return #string Player Name --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetPlayerName() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - - local PlayerName = DCSUnit:getPlayerName() - if PlayerName == nil then - PlayerName = "" - end - return PlayerName - end - - return nil -end - ---- Returns the unit's number in the group. --- The number is the same number the unit has in ME. --- It may not be changed during the mission. --- If any unit in the group is destroyed, the numbers of another units will not be changed. --- @param #UNIT self --- @return #number The Unit number. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetNumber() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitNumber = DCSUnit:getNumber() - return UnitNumber - end - - return nil -end - ---- Returns the unit's group if it exist and nil otherwise. --- @param Wrapper.Unit#UNIT self --- @return Wrapper.Group#GROUP The Group of the Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetGroup() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitGroup = GROUP:Find( DCSUnit:getGroup() ) - return UnitGroup - end - - return nil -end - - --- Need to add here functions to check if radar is on and which object etc. - ---- Returns the prefix name of the DCS Unit. A prefix name is a part of the name before a '#'-sign. --- DCS Units spawned with the @{SPAWN} class contain a '#'-sign to indicate the end of the (base) DCS Unit name. --- The spawn sequence number and unit number are contained within the name after the '#' sign. --- @param #UNIT self --- @return #string The name of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetPrefix() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitPrefix = string.match( self.UnitName, ".*#" ):sub( 1, -2 ) - self:T3( UnitPrefix ) - return UnitPrefix - end - - return nil -end - ---- Returns the Unit's ammunition. --- @param #UNIT self --- @return Dcs.DCSWrapper.Unit#Unit.Ammo --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetAmmo() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitAmmo = DCSUnit:getAmmo() - return UnitAmmo - end - - return nil -end - ---- Returns the unit sensors. --- @param #UNIT self --- @return Dcs.DCSWrapper.Unit#Unit.Sensors --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetSensors() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitSensors = DCSUnit:getSensors() - return UnitSensors - end - - return nil -end - --- Need to add here a function per sensortype --- unit:hasSensors(Unit.SensorType.RADAR, Unit.RadarType.AS) - ---- Returns if the unit has sensors of a certain type. --- @param #UNIT self --- @return #boolean returns true if the unit has specified types of sensors. This function is more preferable than Unit.getSensors() if you don't want to get information about all the unit's sensors, and just want to check if the unit has specified types of sensors. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:HasSensors( ... ) - self:F2( arg ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local HasSensors = DCSUnit:hasSensors( unpack( arg ) ) - return HasSensors - end - - return nil -end - ---- Returns if the unit is SEADable. --- @param #UNIT self --- @return #boolean returns true if the unit is SEADable. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:HasSEAD() - self:F2() - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitSEADAttributes = DCSUnit:getDesc().attributes - - local HasSEAD = false - if UnitSEADAttributes["RADAR_BAND1_FOR_ARM"] and UnitSEADAttributes["RADAR_BAND1_FOR_ARM"] == true or - UnitSEADAttributes["RADAR_BAND2_FOR_ARM"] and UnitSEADAttributes["RADAR_BAND2_FOR_ARM"] == true then - HasSEAD = true - end - return HasSEAD - end - - return nil -end - ---- Returns two values: --- --- * First value indicates if at least one of the unit's radar(s) is on. --- * Second value is the object of the radar's interest. Not nil only if at least one radar of the unit is tracking a target. --- @param #UNIT self --- @return #boolean Indicates if at least one of the unit's radar(s) is on. --- @return Dcs.DCSWrapper.Object#Object The object of the radar's interest. Not nil only if at least one radar of the unit is tracking a target. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetRadar() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitRadarOn, UnitRadarObject = DCSUnit:getRadar() - return UnitRadarOn, UnitRadarObject - end - - return nil, nil -end - ---- Returns relative amount of fuel (from 0.0 to 1.0) the unit has in its internal tanks. If there are additional fuel tanks the value may be greater than 1.0. --- @param #UNIT self --- @return #number The relative amount of fuel (from 0.0 to 1.0). --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetFuel() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitFuel = DCSUnit:getFuel() - return UnitFuel - end - - return nil -end - ---- Returns the UNIT in a UNIT list of one element. --- @param #UNIT self --- @return #list The UNITs wrappers. -function UNIT:GetUnits() - self:F2( { self.UnitName } ) - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local DCSUnits = DCSUnit:getUnits() - local Units = {} - Units[1] = UNIT:Find( DCSUnit ) - self:T3( Units ) - return Units - end - - return nil -end - - ---- Returns the unit's health. Dead units has health <= 1.0. --- @param #UNIT self --- @return #number The Unit's health value. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetLife() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitLife = DCSUnit:getLife() - return UnitLife - end - - return nil -end - ---- Returns the Unit's initial health. --- @param #UNIT self --- @return #number The Unit's initial health value. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetLife0() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitLife0 = DCSUnit:getLife0() - return UnitLife0 - end - - return nil -end - ---- Returns the category name of the #UNIT. --- @param #UNIT self --- @return #string Category name = Helicopter, Airplane, Ground Unit, Ship -function UNIT:GetCategoryName() - self:F3( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - if DCSUnit then - local CategoryNames = { - [Unit.Category.AIRPLANE] = "Airplane", - [Unit.Category.HELICOPTER] = "Helicopter", - [Unit.Category.GROUND_UNIT] = "Ground Unit", - [Unit.Category.SHIP] = "Ship", - [Unit.Category.STRUCTURE] = "Structure", - } - local UnitCategory = DCSUnit:getDesc().category - self:T3( UnitCategory ) - - return CategoryNames[UnitCategory] - end - - return nil -end - - ---- Returns the Unit's A2G threat level on a scale from 1 to 10 ... --- The following threat levels are foreseen: --- --- * Threat level 0: Unit is unarmed. --- * Threat level 1: Unit is infantry. --- * Threat level 2: Unit is an infantry vehicle. --- * Threat level 3: Unit is ground artillery. --- * Threat level 4: Unit is a tank. --- * Threat level 5: Unit is a modern tank or ifv with ATGM. --- * Threat level 6: Unit is a AAA. --- * Threat level 7: Unit is a SAM or manpad, IR guided. --- * Threat level 8: Unit is a Short Range SAM, radar guided. --- * Threat level 9: Unit is a Medium Range SAM, radar guided. --- * Threat level 10: Unit is a Long Range SAM, radar guided. --- @param #UNIT self -function UNIT:GetThreatLevel() - - local Attributes = self:GetDesc().attributes - self:T( Attributes ) - - local ThreatLevel = 0 - local ThreatText = "" - - if self:IsGround() then - - self:T( "Ground" ) - - local ThreatLevels = { - "Unarmed", - "Infantry", - "Old Tanks & APCs", - "Tanks & IFVs without ATGM", - "Tanks & IFV with ATGM", - "Modern Tanks", - "AAA", - "IR Guided SAMs", - "SR SAMs", - "MR SAMs", - "LR SAMs" - } - - - if Attributes["LR SAM"] then ThreatLevel = 10 - elseif Attributes["MR SAM"] then ThreatLevel = 9 - elseif Attributes["SR SAM"] and - not Attributes["IR Guided SAM"] then ThreatLevel = 8 - elseif ( Attributes["SR SAM"] or Attributes["MANPADS"] ) and - Attributes["IR Guided SAM"] then ThreatLevel = 7 - elseif Attributes["AAA"] then ThreatLevel = 6 - elseif Attributes["Modern Tanks"] then ThreatLevel = 5 - elseif ( Attributes["Tanks"] or Attributes["IFV"] ) and - Attributes["ATGM"] then ThreatLevel = 4 - elseif ( Attributes["Tanks"] or Attributes["IFV"] ) and - not Attributes["ATGM"] then ThreatLevel = 3 - elseif Attributes["Old Tanks"] or Attributes["APC"] or Attributes["Artillery"] then ThreatLevel = 2 - elseif Attributes["Infantry"] then ThreatLevel = 1 - end - - ThreatText = ThreatLevels[ThreatLevel+1] - end - - if self:IsAir() then - - self:T( "Air" ) - - local ThreatLevels = { - "Unarmed", - "Tanker", - "AWACS", - "Transport Helicpter", - "UAV", - "Bomber", - "Strategic Bomber", - "Attack Helicopter", - "Interceptor", - "Multirole Fighter", - "Fighter" - } - - - if Attributes["Fighters"] then ThreatLevel = 10 - elseif Attributes["Multirole fighters"] then ThreatLevel = 9 - elseif Attributes["Battleplanes"] then ThreatLevel = 8 - elseif Attributes["Attack helicopters"] then ThreatLevel = 7 - elseif Attributes["Strategic bombers"] then ThreatLevel = 6 - elseif Attributes["Bombers"] then ThreatLevel = 5 - elseif Attributes["UAVs"] then ThreatLevel = 4 - elseif Attributes["Transport helicopters"] then ThreatLevel = 3 - elseif Attributes["AWACS"] then ThreatLevel = 2 - elseif Attributes["Tankers"] then ThreatLevel = 1 - end - - ThreatText = ThreatLevels[ThreatLevel+1] - end - - if self:IsShip() then - - self:T( "Ship" ) - ---["Aircraft Carriers"] = {"Heavy armed ships",}, ---["Cruisers"] = {"Heavy armed ships",}, ---["Destroyers"] = {"Heavy armed ships",}, ---["Frigates"] = {"Heavy armed ships",}, ---["Corvettes"] = {"Heavy armed ships",}, ---["Heavy armed ships"] = {"Armed ships", "Armed Air Defence", "HeavyArmoredUnits",}, ---["Light armed ships"] = {"Armed ships","NonArmoredUnits"}, ---["Armed ships"] = {"Ships"}, ---["Unarmed ships"] = {"Ships","HeavyArmoredUnits",}, - - local ThreatLevels = { - "Unarmed ship", - "Light armed ships", - "Corvettes", - "", - "Frigates", - "", - "Cruiser", - "", - "Destroyer", - "", - "Aircraft Carrier" - } - - - if Attributes["Aircraft Carriers"] then ThreatLevel = 10 - elseif Attributes["Destroyers"] then ThreatLevel = 8 - elseif Attributes["Cruisers"] then ThreatLevel = 6 - elseif Attributes["Frigates"] then ThreatLevel = 4 - elseif Attributes["Corvettes"] then ThreatLevel = 2 - elseif Attributes["Light armed ships"] then ThreatLevel = 1 - end - - ThreatText = ThreatLevels[ThreatLevel+1] - end - - self:T2( ThreatLevel ) - return ThreatLevel, ThreatText - -end - - --- Is functions - ---- Returns true if the unit is within a @{Zone}. --- @param #UNIT self --- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the unit is within the @{Zone#ZONE_BASE} -function UNIT:IsInZone( Zone ) - self:F2( { self.UnitName, Zone } ) - - if self:IsAlive() then - local IsInZone = Zone:IsVec3InZone( self:GetVec3() ) - - self:T( { IsInZone } ) - return IsInZone - end - - return false -end - ---- Returns true if the unit is not within a @{Zone}. --- @param #UNIT self --- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the unit is not within the @{Zone#ZONE_BASE} -function UNIT:IsNotInZone( Zone ) - self:F2( { self.UnitName, Zone } ) - - if self:IsAlive() then - local IsInZone = not Zone:IsVec3InZone( self:GetVec3() ) - - self:T( { IsInZone } ) - return IsInZone - else - return false - end -end - - ---- Returns true if there is an **other** DCS Unit within a radius of the current 2D point of the DCS Unit. --- @param #UNIT self --- @param #UNIT AwaitUnit The other UNIT wrapper object. --- @param Radius The radius in meters with the DCS Unit in the centre. --- @return true If the other DCS Unit is within the radius of the 2D point of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:OtherUnitInRadius( AwaitUnit, Radius ) - self:F2( { self.UnitName, AwaitUnit.UnitName, Radius } ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitVec3 = self:GetVec3() - local AwaitUnitVec3 = AwaitUnit:GetVec3() - - if (((UnitVec3.x - AwaitUnitVec3.x)^2 + (UnitVec3.z - AwaitUnitVec3.z)^2)^0.5 <= Radius) then - self:T3( "true" ) - return true - else - self:T3( "false" ) - return false - end - end - - return nil -end - - - ---- Signal a flare at the position of the UNIT. --- @param #UNIT self --- @param Utilities.Utils#FLARECOLOR FlareColor -function UNIT:Flare( FlareColor ) - self:F2() - trigger.action.signalFlare( self:GetVec3(), FlareColor , 0 ) -end - ---- Signal a white flare at the position of the UNIT. --- @param #UNIT self -function UNIT:FlareWhite() - self:F2() - trigger.action.signalFlare( self:GetVec3(), trigger.flareColor.White , 0 ) -end - ---- Signal a yellow flare at the position of the UNIT. --- @param #UNIT self -function UNIT:FlareYellow() - self:F2() - trigger.action.signalFlare( self:GetVec3(), trigger.flareColor.Yellow , 0 ) -end - ---- Signal a green flare at the position of the UNIT. --- @param #UNIT self -function UNIT:FlareGreen() - self:F2() - trigger.action.signalFlare( self:GetVec3(), trigger.flareColor.Green , 0 ) -end - ---- Signal a red flare at the position of the UNIT. --- @param #UNIT self -function UNIT:FlareRed() - self:F2() - local Vec3 = self:GetVec3() - if Vec3 then - trigger.action.signalFlare( Vec3, trigger.flareColor.Red, 0 ) - end -end - ---- Smoke the UNIT. --- @param #UNIT self -function UNIT:Smoke( SmokeColor, Range ) - self:F2() - if Range then - trigger.action.smoke( self:GetRandomVec3( Range ), SmokeColor ) - else - trigger.action.smoke( self:GetVec3(), SmokeColor ) - end - -end - ---- Smoke the UNIT Green. --- @param #UNIT self -function UNIT:SmokeGreen() - self:F2() - trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Green ) -end - ---- Smoke the UNIT Red. --- @param #UNIT self -function UNIT:SmokeRed() - self:F2() - trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Red ) -end - ---- Smoke the UNIT White. --- @param #UNIT self -function UNIT:SmokeWhite() - self:F2() - trigger.action.smoke( self:GetVec3(), trigger.smokeColor.White ) -end - ---- Smoke the UNIT Orange. --- @param #UNIT self -function UNIT:SmokeOrange() - self:F2() - trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Orange ) -end - ---- Smoke the UNIT Blue. --- @param #UNIT self -function UNIT:SmokeBlue() - self:F2() - trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Blue ) -end - --- Is methods - ---- Returns if the unit is of an air category. --- If the unit is a helicopter or a plane, then this method will return true, otherwise false. --- @param #UNIT self --- @return #boolean Air category evaluation result. -function UNIT:IsAir() - self:F2() - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitDescriptor = DCSUnit:getDesc() - self:T3( { UnitDescriptor.category, Unit.Category.AIRPLANE, Unit.Category.HELICOPTER } ) - - local IsAirResult = ( UnitDescriptor.category == Unit.Category.AIRPLANE ) or ( UnitDescriptor.category == Unit.Category.HELICOPTER ) - - self:T3( IsAirResult ) - return IsAirResult - end - - return nil -end - ---- Returns if the unit is of an ground category. --- If the unit is a ground vehicle or infantry, this method will return true, otherwise false. --- @param #UNIT self --- @return #boolean Ground category evaluation result. -function UNIT:IsGround() - self:F2() - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitDescriptor = DCSUnit:getDesc() - self:T3( { UnitDescriptor.category, Unit.Category.GROUND_UNIT } ) - - local IsGroundResult = ( UnitDescriptor.category == Unit.Category.GROUND_UNIT ) - - self:T3( IsGroundResult ) - return IsGroundResult - end - - return nil -end - ---- Returns if the unit is a friendly unit. --- @param #UNIT self --- @return #boolean IsFriendly evaluation result. -function UNIT:IsFriendly( FriendlyCoalition ) - self:F2() - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitCoalition = DCSUnit:getCoalition() - self:T3( { UnitCoalition, FriendlyCoalition } ) - - local IsFriendlyResult = ( UnitCoalition == FriendlyCoalition ) - - self:E( IsFriendlyResult ) - return IsFriendlyResult - end - - return nil -end - ---- Returns if the unit is of a ship category. --- If the unit is a ship, this method will return true, otherwise false. --- @param #UNIT self --- @return #boolean Ship category evaluation result. -function UNIT:IsShip() - self:F2() - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitDescriptor = DCSUnit:getDesc() - self:T3( { UnitDescriptor.category, Unit.Category.SHIP } ) - - local IsShipResult = ( UnitDescriptor.category == Unit.Category.SHIP ) - - self:T3( IsShipResult ) - return IsShipResult - end - - return nil -end - ---- Returns true if the UNIT is in the air. --- @param Wrapper.Positionable#UNIT self --- @return #boolean true if in the air. --- @return #nil The UNIT is not existing or alive. -function UNIT:InAir() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitInAir = DCSUnit:inAir() - self:T3( UnitInAir ) - return UnitInAir - end - - return nil -end - -do -- Event Handling - - --- Subscribe to a DCS Event. - -- @param #UNIT self - -- @param Core.Event#EVENTS Event - -- @param #function EventFunction (optional) The function to be called when the event occurs for the unit. - -- @return #UNIT - function UNIT:HandleEvent( Event, EventFunction ) - - self:EventDispatcher():OnEventForUnit( self:GetName(), EventFunction, self, Event ) - - return self - end - - --- UnSubscribe to a DCS event. - -- @param #UNIT self - -- @param Core.Event#EVENTS Event - -- @return #UNIT - function UNIT:UnHandleEvent( Event ) - - self:EventDispatcher():RemoveForUnit( self:GetName(), self, Event ) - - return self - end - -end--- This module contains the CLIENT class. --- --- 1) @{Client#CLIENT} class, extends @{Unit#UNIT} --- =============================================== --- Clients are those **Units** defined within the Mission Editor that have the skillset defined as __Client__ or __Player__. --- Note that clients are NOT the same as Units, they are NOT necessarily alive. --- The @{Client#CLIENT} class is a wrapper class to handle the DCS Unit objects that have the skillset defined as __Client__ or __Player__: --- --- * Wraps the DCS Unit objects with skill level set to Player or Client. --- * Support all DCS Unit APIs. --- * Enhance with Unit specific APIs not in the DCS Group API set. --- * When player joins Unit, execute alive init logic. --- * Handles messages to players. --- * Manage the "state" of the DCS Unit. --- --- Clients are being used by the @{MISSION} class to follow players and register their successes. --- --- 1.1) CLIENT reference methods --- ----------------------------- --- For each DCS Unit having skill level Player or Client, a CLIENT wrapper object (instance) will be created within the _@{DATABASE} object. --- This is done at the beginning of the mission (when the mission starts). --- --- The CLIENT class does not contain a :New() method, rather it provides :Find() methods to retrieve the object reference --- using the DCS Unit or the DCS UnitName. --- --- Another thing to know is that CLIENT objects do not "contain" the DCS Unit object. --- The CLIENT methods will reference the DCS Unit object by name when it is needed during API execution. --- If the DCS Unit object does not exist or is nil, the CLIENT methods will return nil and log an exception in the DCS.log file. --- --- The CLIENT class provides the following functions to retrieve quickly the relevant CLIENT instance: --- --- * @{#CLIENT.Find}(): Find a CLIENT instance from the _DATABASE object using a DCS Unit object. --- * @{#CLIENT.FindByName}(): Find a CLIENT instance from the _DATABASE object using a DCS Unit name. --- --- IMPORTANT: ONE SHOULD NEVER SANATIZE these CLIENT OBJECT REFERENCES! (make the CLIENT object references nil). --- --- @module Client - ---- The CLIENT class --- @type CLIENT --- @extends Wrapper.Unit#UNIT -CLIENT = { - ONBOARDSIDE = { - NONE = 0, - LEFT = 1, - RIGHT = 2, - BACK = 3, - FRONT = 4 - }, - ClassName = "CLIENT", - ClientName = nil, - ClientAlive = false, - ClientTransport = false, - ClientBriefingShown = false, - _Menus = {}, - _Tasks = {}, - Messages = { - } -} - - ---- Finds a CLIENT from the _DATABASE using the relevant DCS Unit. --- @param #CLIENT self --- @param #string ClientName Name of the DCS **Unit** as defined within the Mission Editor. --- @param #string ClientBriefing Text that describes the briefing of the mission when a Player logs into the Client. --- @return #CLIENT --- @usage --- -- Create new Clients. --- local Mission = MISSIONSCHEDULER.AddMission( 'Russia Transport Troops SA-6', 'Operational', 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', 'Russia' ) --- Mission:AddGoal( DeploySA6TroopsGoal ) --- --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 1' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 3' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 2' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 4' ):Transport() ) -function CLIENT:Find( DCSUnit, Error ) - local ClientName = DCSUnit:getName() - local ClientFound = _DATABASE:FindClient( ClientName ) - - if ClientFound then - ClientFound:F( ClientName ) - return ClientFound - end - - if not Error then - error( "CLIENT not found for: " .. ClientName ) - end -end - - ---- Finds a CLIENT from the _DATABASE using the relevant Client Unit Name. --- As an optional parameter, a briefing text can be given also. --- @param #CLIENT self --- @param #string ClientName Name of the DCS **Unit** as defined within the Mission Editor. --- @param #string ClientBriefing Text that describes the briefing of the mission when a Player logs into the Client. --- @param #boolean Error A flag that indicates whether an error should be raised if the CLIENT cannot be found. By default an error will be raised. --- @return #CLIENT --- @usage --- -- Create new Clients. --- local Mission = MISSIONSCHEDULER.AddMission( 'Russia Transport Troops SA-6', 'Operational', 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', 'Russia' ) --- Mission:AddGoal( DeploySA6TroopsGoal ) --- --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 1' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 3' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 2' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 4' ):Transport() ) -function CLIENT:FindByName( ClientName, ClientBriefing, Error ) - local ClientFound = _DATABASE:FindClient( ClientName ) - - if ClientFound then - ClientFound:F( { ClientName, ClientBriefing } ) - ClientFound:AddBriefing( ClientBriefing ) - ClientFound.MessageSwitch = true - - return ClientFound - end - - if not Error then - error( "CLIENT not found for: " .. ClientName ) - end -end - -function CLIENT:Register( ClientName ) - local self = BASE:Inherit( self, UNIT:Register( ClientName ) ) - - self:F( ClientName ) - self.ClientName = ClientName - self.MessageSwitch = true - self.ClientAlive2 = false - - --self.AliveCheckScheduler = routines.scheduleFunction( self._AliveCheckScheduler, { self }, timer.getTime() + 1, 5 ) - self.AliveCheckScheduler = SCHEDULER:New( self, self._AliveCheckScheduler, { "Client Alive " .. ClientName }, 1, 5 ) - - self:E( self ) - return self -end - - ---- Transport defines that the Client is a Transport. Transports show cargo. --- @param #CLIENT self --- @return #CLIENT -function CLIENT:Transport() - self:F() - - self.ClientTransport = true - return self -end - ---- AddBriefing adds a briefing to a CLIENT when a player joins a mission. --- @param #CLIENT self --- @param #string ClientBriefing is the text defining the Mission briefing. --- @return #CLIENT self -function CLIENT:AddBriefing( ClientBriefing ) - self:F( ClientBriefing ) - self.ClientBriefing = ClientBriefing - self.ClientBriefingShown = false - - return self -end - ---- Show the briefing of a CLIENT. --- @param #CLIENT self --- @return #CLIENT self -function CLIENT:ShowBriefing() - self:F( { self.ClientName, self.ClientBriefingShown } ) - - if not self.ClientBriefingShown then - self.ClientBriefingShown = true - local Briefing = "" - if self.ClientBriefing then - Briefing = Briefing .. self.ClientBriefing - end - Briefing = Briefing .. " Press [LEFT ALT]+[B] to view the complete mission briefing." - self:Message( Briefing, 60, "Briefing" ) - end - - return self -end - ---- Show the mission briefing of a MISSION to the CLIENT. --- @param #CLIENT self --- @param #string MissionBriefing --- @return #CLIENT self -function CLIENT:ShowMissionBriefing( MissionBriefing ) - self:F( { self.ClientName } ) - - if MissionBriefing then - self:Message( MissionBriefing, 60, "Mission Briefing" ) - end - - return self -end - - - ---- Resets a CLIENT. --- @param #CLIENT self --- @param #string ClientName Name of the Group as defined within the Mission Editor. The Group must have a Unit with the type Client. -function CLIENT:Reset( ClientName ) - self:F() - self._Menus = {} -end - --- Is Functions - ---- Checks if the CLIENT is a multi-seated UNIT. --- @param #CLIENT self --- @return #boolean true if multi-seated. -function CLIENT:IsMultiSeated() - self:F( self.ClientName ) - - local ClientMultiSeatedTypes = { - ["Mi-8MT"] = "Mi-8MT", - ["UH-1H"] = "UH-1H", - ["P-51B"] = "P-51B" - } - - if self:IsAlive() then - local ClientTypeName = self:GetClientGroupUnit():GetTypeName() - if ClientMultiSeatedTypes[ClientTypeName] then - return true - end - end - - return false -end - ---- Checks for a client alive event and calls a function on a continuous basis. --- @param #CLIENT self --- @param #function CallBackFunction Create a function that will be called when a player joins the slot. --- @return #CLIENT -function CLIENT:Alive( CallBackFunction, ... ) - self:F() - - self.ClientCallBack = CallBackFunction - self.ClientParameters = arg - - return self -end - ---- @param #CLIENT self -function CLIENT:_AliveCheckScheduler( SchedulerName ) - self:F3( { SchedulerName, self.ClientName, self.ClientAlive2, self.ClientBriefingShown, self.ClientCallBack } ) - - if self:IsAlive() then - if self.ClientAlive2 == false then - self:ShowBriefing() - if self.ClientCallBack then - self:T("Calling Callback function") - self.ClientCallBack( self, unpack( self.ClientParameters ) ) - end - self.ClientAlive2 = true - end - else - if self.ClientAlive2 == true then - self.ClientAlive2 = false - end - end - - return true -end - ---- Return the DCSGroup of a Client. --- This function is modified to deal with a couple of bugs in DCS 1.5.3 --- @param #CLIENT self --- @return Dcs.DCSWrapper.Group#Group -function CLIENT:GetDCSGroup() - self:F3() - --- local ClientData = Group.getByName( self.ClientName ) --- if ClientData and ClientData:isExist() then --- self:T( self.ClientName .. " : group found!" ) --- return ClientData --- else --- return nil --- end - - local ClientUnit = Unit.getByName( self.ClientName ) - - local CoalitionsData = { AlivePlayersRed = coalition.getPlayers( coalition.side.RED ), AlivePlayersBlue = coalition.getPlayers( coalition.side.BLUE ) } - for CoalitionId, CoalitionData in pairs( CoalitionsData ) do - self:T3( { "CoalitionData:", CoalitionData } ) - for UnitId, UnitData in pairs( CoalitionData ) do - self:T3( { "UnitData:", UnitData } ) - if UnitData and UnitData:isExist() then - - --self:E(self.ClientName) - if ClientUnit then - local ClientGroup = ClientUnit:getGroup() - if ClientGroup then - self:T3( "ClientGroup = " .. self.ClientName ) - if ClientGroup:isExist() and UnitData:getGroup():isExist() then - if ClientGroup:getID() == UnitData:getGroup():getID() then - self:T3( "Normal logic" ) - self:T3( self.ClientName .. " : group found!" ) - self.ClientGroupID = ClientGroup:getID() - self.ClientGroupName = ClientGroup:getName() - return ClientGroup - end - else - -- Now we need to resolve the bugs in DCS 1.5 ... - -- Consult the database for the units of the Client Group. (ClientGroup:getUnits() returns nil) - self:T3( "Bug 1.5 logic" ) - local ClientGroupTemplate = _DATABASE.Templates.Units[self.ClientName].GroupTemplate - self.ClientGroupID = ClientGroupTemplate.groupId - self.ClientGroupName = _DATABASE.Templates.Units[self.ClientName].GroupName - self:T3( self.ClientName .. " : group found in bug 1.5 resolvement logic!" ) - return ClientGroup - end - -- else - -- error( "Client " .. self.ClientName .. " not found!" ) - end - else - --self:E( { "Client not found!", self.ClientName } ) - end - end - end - end - - -- For non player clients - if ClientUnit then - local ClientGroup = ClientUnit:getGroup() - if ClientGroup then - self:T3( "ClientGroup = " .. self.ClientName ) - if ClientGroup:isExist() then - self:T3( "Normal logic" ) - self:T3( self.ClientName .. " : group found!" ) - return ClientGroup - end - end - end - - self.ClientGroupID = nil - self.ClientGroupUnit = nil - - return nil -end - - --- TODO: Check Dcs.DCSTypes#Group.ID ---- Get the group ID of the client. --- @param #CLIENT self --- @return Dcs.DCSTypes#Group.ID -function CLIENT:GetClientGroupID() - - local ClientGroup = self:GetDCSGroup() - - --self:E( self.ClientGroupID ) -- Determined in GetDCSGroup() - return self.ClientGroupID -end - - ---- Get the name of the group of the client. --- @param #CLIENT self --- @return #string -function CLIENT:GetClientGroupName() - - local ClientGroup = self:GetDCSGroup() - - self:T( self.ClientGroupName ) -- Determined in GetDCSGroup() - return self.ClientGroupName -end - ---- Returns the UNIT of the CLIENT. --- @param #CLIENT self --- @return Wrapper.Unit#UNIT -function CLIENT:GetClientGroupUnit() - self:F2() - - local ClientDCSUnit = Unit.getByName( self.ClientName ) - - self:T( self.ClientDCSUnit ) - if ClientDCSUnit and ClientDCSUnit:isExist() then - local ClientUnit = _DATABASE:FindUnit( self.ClientName ) - self:T2( ClientUnit ) - return ClientUnit - end -end - ---- Returns the DCSUnit of the CLIENT. --- @param #CLIENT self --- @return Dcs.DCSTypes#Unit -function CLIENT:GetClientGroupDCSUnit() - self:F2() - - local ClientDCSUnit = Unit.getByName( self.ClientName ) - - if ClientDCSUnit and ClientDCSUnit:isExist() then - self:T2( ClientDCSUnit ) - return ClientDCSUnit - end -end - - ---- Evaluates if the CLIENT is a transport. --- @param #CLIENT self --- @return #boolean true is a transport. -function CLIENT:IsTransport() - self:F() - return self.ClientTransport -end - ---- Shows the @{AI_Cargo#CARGO} contained within the CLIENT to the player as a message. --- The @{AI_Cargo#CARGO} is shown using the @{Message#MESSAGE} distribution system. --- @param #CLIENT self -function CLIENT:ShowCargo() - self:F() - - local CargoMsg = "" - - for CargoName, Cargo in pairs( CARGOS ) do - if self == Cargo:IsLoadedInClient() then - CargoMsg = CargoMsg .. Cargo.CargoName .. " Type:" .. Cargo.CargoType .. " Weight: " .. Cargo.CargoWeight .. "\n" - end - end - - if CargoMsg == "" then - CargoMsg = "empty" - end - - self:Message( CargoMsg, 15, "Co-Pilot: Cargo Status", 30 ) - -end - --- TODO (1) I urgently need to revise this. ---- A local function called by the DCS World Menu system to switch off messages. -function CLIENT.SwitchMessages( PrmTable ) - PrmTable[1].MessageSwitch = PrmTable[2] -end - ---- The main message driver for the CLIENT. --- This function displays various messages to the Player logged into the CLIENT through the DCS World Messaging system. --- @param #CLIENT self --- @param #string Message is the text describing the message. --- @param #number MessageDuration is the duration in seconds that the Message should be displayed. --- @param #string MessageCategory is the category of the message (the title). --- @param #number MessageInterval is the interval in seconds between the display of the @{Message#MESSAGE} when the CLIENT is in the air. --- @param #string MessageID is the identifier of the message when displayed with intervals. -function CLIENT:Message( Message, MessageDuration, MessageCategory, MessageInterval, MessageID ) - self:F( { Message, MessageDuration, MessageCategory, MessageInterval } ) - - if self.MessageSwitch == true then - if MessageCategory == nil then - MessageCategory = "Messages" - end - if MessageID ~= nil then - if self.Messages[MessageID] == nil then - self.Messages[MessageID] = {} - self.Messages[MessageID].MessageId = MessageID - self.Messages[MessageID].MessageTime = timer.getTime() - self.Messages[MessageID].MessageDuration = MessageDuration - if MessageInterval == nil then - self.Messages[MessageID].MessageInterval = 600 - else - self.Messages[MessageID].MessageInterval = MessageInterval - end - MESSAGE:New( Message, MessageDuration, MessageCategory ):ToClient( self ) - else - if self:GetClientGroupDCSUnit() and not self:GetClientGroupDCSUnit():inAir() then - if timer.getTime() - self.Messages[MessageID].MessageTime >= self.Messages[MessageID].MessageDuration + 10 then - MESSAGE:New( Message, MessageDuration , MessageCategory):ToClient( self ) - self.Messages[MessageID].MessageTime = timer.getTime() - end - else - if timer.getTime() - self.Messages[MessageID].MessageTime >= self.Messages[MessageID].MessageDuration + self.Messages[MessageID].MessageInterval then - MESSAGE:New( Message, MessageDuration, MessageCategory ):ToClient( self ) - self.Messages[MessageID].MessageTime = timer.getTime() - end - end - end - else - MESSAGE:New( Message, MessageDuration, MessageCategory ):ToClient( self ) - end - end -end ---- This module contains the STATIC class. --- --- 1) @{Static#STATIC} class, extends @{Positionable#POSITIONABLE} --- =============================================================== --- Statics are **Static Units** defined within the Mission Editor. --- Note that Statics are almost the same as Units, but they don't have a controller. --- The @{Static#STATIC} class is a wrapper class to handle the DCS Static objects: --- --- * Wraps the DCS Static objects. --- * Support all DCS Static APIs. --- * Enhance with Static specific APIs not in the DCS API set. --- --- 1.1) STATIC reference methods --- ----------------------------- --- For each DCS Static will have a STATIC wrapper object (instance) within the _@{DATABASE} object. --- This is done at the beginning of the mission (when the mission starts). --- --- The STATIC class does not contain a :New() method, rather it provides :Find() methods to retrieve the object reference --- using the Static Name. --- --- Another thing to know is that STATIC objects do not "contain" the DCS Static object. --- The STATIc methods will reference the DCS Static object by name when it is needed during API execution. --- If the DCS Static object does not exist or is nil, the STATIC methods will return nil and log an exception in the DCS.log file. --- --- The STATIc class provides the following functions to retrieve quickly the relevant STATIC instance: --- --- * @{#STATIC.FindByName}(): Find a STATIC instance from the _DATABASE object using a DCS Static name. --- --- IMPORTANT: ONE SHOULD NEVER SANATIZE these STATIC OBJECT REFERENCES! (make the STATIC object references nil). --- --- @module Static --- @author FlightControl - - - - - - ---- The STATIC class --- @type STATIC --- @extends Wrapper.Positionable#POSITIONABLE -STATIC = { - ClassName = "STATIC", -} - - ---- Finds a STATIC from the _DATABASE using the relevant Static Name. --- As an optional parameter, a briefing text can be given also. --- @param #STATIC self --- @param #string StaticName Name of the DCS **Static** as defined within the Mission Editor. --- @param #boolean RaiseError Raise an error if not found. --- @return #STATIC -function STATIC:FindByName( StaticName, RaiseError ) - local StaticFound = _DATABASE:FindStatic( StaticName ) - - self.StaticName = StaticName - - if StaticFound then - StaticFound:F3( { StaticName } ) - - return StaticFound - end - - if RaiseError == nil or RaiseError == true then - error( "STATIC not found for: " .. StaticName ) - end - - return nil -end - -function STATIC:Register( StaticName ) - local self = BASE:Inherit( self, POSITIONABLE:New( StaticName ) ) - self.StaticName = StaticName - return self -end - - -function STATIC:GetDCSObject() - local DCSStatic = StaticObject.getByName( self.StaticName ) - - if DCSStatic then - return DCSStatic - end - - return nil -end - -function STATIC:GetThreatLevel() - - return 1, "Static" -end--- This module contains the AIRBASE classes. --- --- === --- --- 1) @{Airbase#AIRBASE} class, extends @{Positionable#POSITIONABLE} --- ================================================================= --- The @{AIRBASE} class is a wrapper class to handle the DCS Airbase objects: --- --- * Support all DCS Airbase APIs. --- * Enhance with Airbase specific APIs not in the DCS Airbase API set. --- --- --- 1.1) AIRBASE reference methods --- ------------------------------ --- For each DCS Airbase object alive within a running mission, a AIRBASE wrapper object (instance) will be created within the _@{DATABASE} object. --- This is done at the beginning of the mission (when the mission starts). --- --- The AIRBASE class **does not contain a :New()** method, rather it provides **:Find()** methods to retrieve the object reference --- using the DCS Airbase or the DCS AirbaseName. --- --- Another thing to know is that AIRBASE objects do not "contain" the DCS Airbase object. --- The AIRBASE methods will reference the DCS Airbase object by name when it is needed during API execution. --- If the DCS Airbase object does not exist or is nil, the AIRBASE methods will return nil and log an exception in the DCS.log file. --- --- The AIRBASE class provides the following functions to retrieve quickly the relevant AIRBASE instance: --- --- * @{#AIRBASE.Find}(): Find a AIRBASE instance from the _DATABASE object using a DCS Airbase object. --- * @{#AIRBASE.FindByName}(): Find a AIRBASE instance from the _DATABASE object using a DCS Airbase name. --- --- IMPORTANT: ONE SHOULD NEVER SANATIZE these AIRBASE OBJECT REFERENCES! (make the AIRBASE object references nil). --- --- 1.2) DCS AIRBASE APIs --- --------------------- --- The DCS Airbase APIs are used extensively within MOOSE. The AIRBASE class has for each DCS Airbase API a corresponding method. --- To be able to distinguish easily in your code the difference between a AIRBASE API call and a DCS Airbase API call, --- the first letter of the method is also capitalized. So, by example, the DCS Airbase method @{DCSWrapper.Airbase#Airbase.getName}() --- is implemented in the AIRBASE class as @{#AIRBASE.GetName}(). --- --- More functions will be added --- ---------------------------- --- During the MOOSE development, more functions will be added. --- --- @module Airbase --- @author FlightControl - - - - - ---- The AIRBASE class --- @type AIRBASE --- @extends Wrapper.Positionable#POSITIONABLE -AIRBASE = { - ClassName="AIRBASE", - CategoryName = { - [Airbase.Category.AIRDROME] = "Airdrome", - [Airbase.Category.HELIPAD] = "Helipad", - [Airbase.Category.SHIP] = "Ship", - }, - } - --- Registration. - ---- Create a new AIRBASE from DCSAirbase. --- @param #AIRBASE self --- @param #string AirbaseName The name of the airbase. --- @return Wrapper.Airbase#AIRBASE -function AIRBASE:Register( AirbaseName ) - - local self = BASE:Inherit( self, POSITIONABLE:New( AirbaseName ) ) - self.AirbaseName = AirbaseName - return self -end - --- Reference methods. - ---- Finds a AIRBASE from the _DATABASE using a DCSAirbase object. --- @param #AIRBASE self --- @param Dcs.DCSWrapper.Airbase#Airbase DCSAirbase An existing DCS Airbase object reference. --- @return Wrapper.Airbase#AIRBASE self -function AIRBASE:Find( DCSAirbase ) - - local AirbaseName = DCSAirbase:getName() - local AirbaseFound = _DATABASE:FindAirbase( AirbaseName ) - return AirbaseFound -end - ---- Find a AIRBASE in the _DATABASE using the name of an existing DCS Airbase. --- @param #AIRBASE self --- @param #string AirbaseName The Airbase Name. --- @return Wrapper.Airbase#AIRBASE self -function AIRBASE:FindByName( AirbaseName ) - - local AirbaseFound = _DATABASE:FindAirbase( AirbaseName ) - return AirbaseFound -end - -function AIRBASE:GetDCSObject() - local DCSAirbase = Airbase.getByName( self.AirbaseName ) - - if DCSAirbase then - return DCSAirbase - end - - return nil -end - - - ---- This module contains the SCENERY class. --- --- 1) @{Scenery#SCENERY} class, extends @{Positionable#POSITIONABLE} --- =============================================================== --- Scenery objects are defined on the map. --- The @{Scenery#SCENERY} class is a wrapper class to handle the DCS Scenery objects: --- --- * Wraps the DCS Scenery objects. --- * Support all DCS Scenery APIs. --- * Enhance with Scenery specific APIs not in the DCS API set. --- --- @module Scenery --- @author FlightControl - - - ---- The SCENERY class --- @type SCENERY --- @extends Wrapper.Positionable#POSITIONABLE -SCENERY = { - ClassName = "SCENERY", -} - - -function SCENERY:Register( SceneryName, SceneryObject ) - local self = BASE:Inherit( self, POSITIONABLE:New( SceneryName ) ) - self.SceneryName = SceneryName - self.SceneryObject = SceneryObject - return self -end - -function SCENERY:GetDCSObject() - return self.SceneryObject -end - -function SCENERY:GetThreatLevel() - - return 0, "Scenery" -end ---- Single-Player:**Yes** / Multi-Player:**Yes** / Core:**Yes** -- **Administer the scoring of player achievements, --- and create a CSV file logging the scoring events for use at team or squadron websites.** --- --- ![Banner Image](..\Presentations\SCORING\Dia1.JPG) --- --- === --- --- The @{#SCORING} class administers the scoring of player achievements, --- and creates a CSV file logging the scoring events and results for use at team or squadron websites. --- --- SCORING automatically calculates the threat level of the objects hit and destroyed by players, --- which can be @{Unit}, @{Static) and @{Scenery} objects. --- --- Positive score points are granted when enemy or neutral targets are destroyed. --- Negative score points or penalties are given when a friendly target is hit or destroyed. --- This brings a lot of dynamism in the scoring, where players need to take care to inflict damage on the right target. --- By default, penalties weight heavier in the scoring, to ensure that players don't commit fratricide. --- The total score of the player is calculated by **adding the scores minus the penalties**. --- --- ![Banner Image](..\Presentations\SCORING\Dia4.JPG) --- --- The score value is calculated based on the **threat level of the player** and the **threat level of the target**. --- A calculated score takes the threat level of the target divided by a balanced threat level of the player unit. --- As such, if the threat level of the target is high, and the player threat level is low, a higher score will be given than --- if the threat level of the player would be high too. --- --- ![Banner Image](..\Presentations\SCORING\Dia5.JPG) --- --- When multiple players hit the same target, and finally succeed in destroying the target, then each player who contributed to the target --- destruction, will receive a score. This is important for targets that require significant damage before it can be destroyed, like --- ships or heavy planes. --- --- ![Banner Image](..\Presentations\SCORING\Dia13.JPG) --- --- Optionally, the score values can be **scaled** by a **scale**. Specific scales can be set for positive cores or negative penalties. --- The default range of the scores granted is a value between 0 and 10. The default range of penalties given is a value between 0 and 30. --- --- ![Banner Image](..\Presentations\SCORING\Dia7.JPG) --- --- **Additional scores** can be granted to **specific objects**, when the player(s) destroy these objects. --- --- ![Banner Image](..\Presentations\SCORING\Dia9.JPG) --- --- Various @{Zone}s can be defined for which scores are also granted when objects in that @{Zone} are destroyed. --- This is **specifically useful** to designate **scenery targets on the map** that will generate points when destroyed. --- --- With a small change in MissionScripting.lua, the scoring results can also be logged in a **CSV file**. --- These CSV files can be used to: --- --- * Upload scoring to a database or a BI tool to publish the scoring results to the player community. --- * Upload scoring in an (online) Excel like tool, using pivot tables and pivot charts to show mission results. --- * Share scoring amoung players after the mission to discuss mission results. --- --- Scores can be **reported**. **Menu options** are automatically added to **each player group** when a player joins a client slot or a CA unit. --- Use the radio menu F10 to consult the scores while running the mission. --- Scores can be reported for your user, or an overall score can be reported of all players currently active in the mission. --- --- # 1) @{Scoring#SCORING} class, extends @{Base#BASE} --- --- ## 1.1) Set the destroy score or penalty scale --- --- Score scales can be set for scores granted when enemies or friendlies are destroyed. --- Use the method @{#SCORING.SetScaleDestroyScore}() to set the scale of enemy destroys (positive destroys). --- Use the method @{#SCORING.SetScaleDestroyPenalty}() to set the scale of friendly destroys (negative destroys). --- --- local Scoring = SCORING:New( "Scoring File" ) --- Scoring:SetScaleDestroyScore( 10 ) --- Scoring:SetScaleDestroyPenalty( 40 ) --- --- The above sets the scale for valid scores to 10. So scores will be given in a scale from 0 to 10. --- The penalties will be given in a scale from 0 to 40. --- --- ## 1.2) Define special targets that will give extra scores. --- --- Special targets can be set that will give extra scores to the players when these are destroyed. --- Use the methods @{#SCORING.AddUnitScore}() and @{#SCORING.RemoveUnitScore}() to specify a special additional score for a specific @{Unit}s. --- Use the methods @{#SCORING.AddStaticScore}() and @{#SCORING.RemoveStaticScore}() to specify a special additional score for a specific @{Static}s. --- Use the method @{#SCORING.SetGroupGroup}() to specify a special additional score for a specific @{Group}s. --- --- local Scoring = SCORING:New( "Scoring File" ) --- Scoring:AddUnitScore( UNIT:FindByName( "Unit #001" ), 200 ) --- Scoring:AddStaticScore( STATIC:FindByName( "Static #1" ), 100 ) --- --- The above grants an additional score of 200 points for Unit #001 and an additional 100 points of Static #1 if these are destroyed. --- Note that later in the mission, one can remove these scores set, for example, when the a goal achievement time limit is over. --- For example, this can be done as follows: --- --- Scoring:RemoveUnitScore( UNIT:FindByName( "Unit #001" ) ) --- --- ## 1.3) Define destruction zones that will give extra scores. --- --- Define zones of destruction. Any object destroyed within the zone of the given category will give extra points. --- Use the method @{#SCORING.AddZoneScore}() to add a @{Zone} for additional scoring. --- Use the method @{#SCORING.RemoveZoneScore}() to remove a @{Zone} for additional scoring. --- There are interesting variations that can be achieved with this functionality. For example, if the @{Zone} is a @{Zone#ZONE_UNIT}, --- then the zone is a moving zone, and anything destroyed within that @{Zone} will generate points. --- The other implementation could be to designate a scenery target (a building) in the mission editor surrounded by a @{Zone}, --- just large enough around that building. --- --- ## 1.4) Add extra Goal scores upon an event or a condition. --- --- A mission has goals and achievements. The scoring system provides an API to set additional scores when a goal or achievement event happens. --- Use the method @{#SCORING.AddGoalScore}() to add a score for a Player at any time in your mission. --- --- ## 1.5) Configure fratricide level. --- --- When a player commits too much damage to friendlies, his penalty score will reach a certain level. --- Use the method @{#SCORING.SetFratricide}() to define the level when a player gets kicked. --- By default, the fratricide level is the default penalty mutiplier * 2 for the penalty score. --- --- ## 1.6) Penalty score when a player changes the coalition. --- --- When a player changes the coalition, he can receive a penalty score. --- Use the method @{#SCORING.SetCoalitionChangePenalty}() to define the penalty when a player changes coalition. --- By default, the penalty for changing coalition is the default penalty scale. --- --- ## 1.8) Define output CSV files. --- --- The CSV file is given the name of the string given in the @{#SCORING.New}{} constructor, followed by the .csv extension. --- The file is incrementally saved in the **\\Saved Games\\DCS\\Logs** folder, and has a time stamp indicating each mission run. --- See the following example: --- --- local ScoringFirstMission = SCORING:New( "FirstMission" ) --- local ScoringSecondMission = SCORING:New( "SecondMission" ) --- --- The above documents that 2 Scoring objects are created, ScoringFirstMission and ScoringSecondMission. --- --- ## 1.9) Configure messages. --- --- When players hit or destroy targets, messages are sent. --- Various methods exist to configure: --- --- * Which messages are sent upon the event. --- * Which audience receives the message. --- --- ### 1.9.1) Configure the messages sent upon the event. --- --- Use the following methods to configure when to send messages. By default, all messages are sent. --- --- * @{#SCORING.SetMessagesHit}(): Configure to send messages after a target has been hit. --- * @{#SCORING.SetMessagesDestroy}(): Configure to send messages after a target has been destroyed. --- * @{#SCORING.SetMessagesAddon}(): Configure to send messages for additional score, after a target has been destroyed. --- * @{#SCORING.SetMessagesZone}(): Configure to send messages for additional score, after a target has been destroyed within a given zone. --- --- ### 1.9.2) Configure the audience of the messages. --- --- Use the following methods to configure the audience of the messages. By default, the messages are sent to all players in the mission. --- --- * @{#SCORING.SetMessagesToAll}(): Configure to send messages to all players. --- * @{#SCORING.SetMessagesToCoalition}(): Configure to send messages to only those players within the same coalition as the player. --- --- --- ==== --- --- # **API CHANGE HISTORY** --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- Hereby the change log: --- --- 2017-02-26: Initial class and API. --- --- === --- --- # **AUTHORS and CONTRIBUTIONS** --- --- ### Contributions: --- --- * **Wingthor (TAW)**: Testing & Advice. --- * **Dutch-Baron (TAW)**: Testing & Advice. --- * **[Whisper](http://forums.eagle.ru/member.php?u=3829): Testing and Advice. --- --- ### Authors: --- --- * **FlightControl**: Concept, Design & Programming. --- --- @module Scoring - - ---- The Scoring class --- @type SCORING --- @field Players A collection of the current players that have joined the game. --- @extends Core.Base#BASE -SCORING = { - ClassName = "SCORING", - ClassID = 0, - Players = {}, -} - -local _SCORINGCoalition = - { - [1] = "Red", - [2] = "Blue", - } - -local _SCORINGCategory = - { - [Unit.Category.AIRPLANE] = "Plane", - [Unit.Category.HELICOPTER] = "Helicopter", - [Unit.Category.GROUND_UNIT] = "Vehicle", - [Unit.Category.SHIP] = "Ship", - [Unit.Category.STRUCTURE] = "Structure", - } - ---- Creates a new SCORING object to administer the scoring achieved by players. --- @param #SCORING self --- @param #string GameName The name of the game. This name is also logged in the CSV score file. --- @return #SCORING self --- @usage --- -- Define a new scoring object for the mission Gori Valley. --- ScoringObject = SCORING:New( "Gori Valley" ) -function SCORING:New( GameName ) - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) -- #SCORING - - if GameName then - self.GameName = GameName - else - error( "A game name must be given to register the scoring results" ) - end - - - -- Additional Object scores - self.ScoringObjects = {} - - -- Additional Zone scores. - self.ScoringZones = {} - - -- Configure Messages - self:SetMessagesToAll() - self:SetMessagesHit( true ) - self:SetMessagesDestroy( true ) - self:SetMessagesScore( true ) - self:SetMessagesZone( true ) - - -- Scales - self:SetScaleDestroyScore( 10 ) - self:SetScaleDestroyPenalty( 30 ) - - -- Default fratricide penalty level (maximum penalty that can be assigned to a player before he gets kicked). - self:SetFratricide( self.ScaleDestroyPenalty * 3 ) - - -- Default penalty when a player changes coalition. - self:SetCoalitionChangePenalty( self.ScaleDestroyPenalty ) - - -- Event handlers - self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) - self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) - self:HandleEvent( EVENTS.Hit, self._EventOnHit ) - self:HandleEvent( EVENTS.PlayerEnterUnit ) - self:HandleEvent( EVENTS.PlayerLeaveUnit ) - - -- Create the CSV file. - self:OpenCSV( GameName ) - - return self - -end - ---- Set the scale for scoring valid destroys (enemy destroys). --- A default calculated score is a value between 1 and 10. --- The scale magnifies the scores given to the players. --- @param #SCORING self --- @param #number Scale The scale of the score given. -function SCORING:SetScaleDestroyScore( Scale ) - - self.ScaleDestroyScore = Scale - - return self -end - ---- Set the scale for scoring penalty destroys (friendly destroys). --- A default calculated penalty is a value between 1 and 10. --- The scale magnifies the scores given to the players. --- @param #SCORING self --- @param #number Scale The scale of the score given. --- @return #SCORING -function SCORING:SetScaleDestroyPenalty( Scale ) - - self.ScaleDestroyPenalty = Scale - - return self -end - ---- Add a @{Unit} for additional scoring when the @{Unit} is destroyed. --- Note that if there was already a @{Unit} declared within the scoring with the same name, --- then the old @{Unit} will be replaced with the new @{Unit}. --- @param #SCORING self --- @param Wrapper.Unit#UNIT ScoreUnit The @{Unit} for which the Score needs to be given. --- @param #number Score The Score value. --- @return #SCORING -function SCORING:AddUnitScore( ScoreUnit, Score ) - - local UnitName = ScoreUnit:GetName() - - self.ScoringObjects[UnitName] = Score - - return self -end - ---- Removes a @{Unit} for additional scoring when the @{Unit} is destroyed. --- @param #SCORING self --- @param Wrapper.Unit#UNIT ScoreUnit The @{Unit} for which the Score needs to be given. --- @return #SCORING -function SCORING:RemoveUnitScore( ScoreUnit ) - - local UnitName = ScoreUnit:GetName() - - self.ScoringObjects[UnitName] = nil - - return self -end - ---- Add a @{Static} for additional scoring when the @{Static} is destroyed. --- Note that if there was already a @{Static} declared within the scoring with the same name, --- then the old @{Static} will be replaced with the new @{Static}. --- @param #SCORING self --- @param Wrapper.Static#UNIT ScoreStatic The @{Static} for which the Score needs to be given. --- @param #number Score The Score value. --- @return #SCORING -function SCORING:AddStaticScore( ScoreStatic, Score ) - - local StaticName = ScoreStatic:GetName() - - self.ScoringObjects[StaticName] = Score - - return self -end - ---- Removes a @{Static} for additional scoring when the @{Static} is destroyed. --- @param #SCORING self --- @param Wrapper.Static#UNIT ScoreStatic The @{Static} for which the Score needs to be given. --- @return #SCORING -function SCORING:RemoveStaticScore( ScoreStatic ) - - local StaticName = ScoreStatic:GetName() - - self.ScoringObjects[StaticName] = nil - - return self -end - - ---- Specify a special additional score for a @{Group}. --- @param #SCORING self --- @param Wrapper.Group#GROUP ScoreGroup The @{Group} for which each @{Unit} a Score is given. --- @param #number Score The Score value. --- @return #SCORING -function SCORING:AddScoreGroup( ScoreGroup, Score ) - - local ScoreUnits = ScoreGroup:GetUnits() - - for ScoreUnitID, ScoreUnit in pairs( ScoreUnits ) do - local UnitName = ScoreUnit:GetName() - self.ScoringObjects[UnitName] = Score - end - - return self -end - ---- Add a @{Zone} to define additional scoring when any object is destroyed in that zone. --- Note that if a @{Zone} with the same name is already within the scoring added, the @{Zone} (type) and Score will be replaced! --- This allows for a dynamic destruction zone evolution within your mission. --- @param #SCORING self --- @param Core.Zone#ZONE_BASE ScoreZone The @{Zone} which defines the destruction score perimeters. --- Note that a zone can be a polygon or a moving zone. --- @param #number Score The Score value. --- @return #SCORING -function SCORING:AddZoneScore( ScoreZone, Score ) - - local ZoneName = ScoreZone:GetName() - - self.ScoringZones[ZoneName] = {} - self.ScoringZones[ZoneName].ScoreZone = ScoreZone - self.ScoringZones[ZoneName].Score = Score - - return self -end - ---- Remove a @{Zone} for additional scoring. --- The scoring will search if any @{Zone} is added with the given name, and will remove that zone from the scoring. --- This allows for a dynamic destruction zone evolution within your mission. --- @param #SCORING self --- @param Core.Zone#ZONE_BASE ScoreZone The @{Zone} which defines the destruction score perimeters. --- Note that a zone can be a polygon or a moving zone. --- @return #SCORING -function SCORING:RemoveZoneScore( ScoreZone ) - - local ZoneName = ScoreZone:GetName() - - self.ScoringZones[ZoneName] = nil - - return self -end - - ---- Configure to send messages after a target has been hit. --- @param #SCORING self --- @param #boolean OnOff If true is given, the messages are sent. --- @return #SCORING -function SCORING:SetMessagesHit( OnOff ) - - self.MessagesHit = OnOff - return self -end - ---- If to send messages after a target has been hit. --- @param #SCORING self --- @return #boolean -function SCORING:IfMessagesHit() - - return self.MessagesHit -end - ---- Configure to send messages after a target has been destroyed. --- @param #SCORING self --- @param #boolean OnOff If true is given, the messages are sent. --- @return #SCORING -function SCORING:SetMessagesDestroy( OnOff ) - - self.MessagesDestroy = OnOff - return self -end - ---- If to send messages after a target has been destroyed. --- @param #SCORING self --- @return #boolean -function SCORING:IfMessagesDestroy() - - return self.MessagesDestroy -end - ---- Configure to send messages after a target has been destroyed and receives additional scores. --- @param #SCORING self --- @param #boolean OnOff If true is given, the messages are sent. --- @return #SCORING -function SCORING:SetMessagesScore( OnOff ) - - self.MessagesScore = OnOff - return self -end - ---- If to send messages after a target has been destroyed and receives additional scores. --- @param #SCORING self --- @return #boolean -function SCORING:IfMessagesScore() - - return self.MessagesScore -end - ---- Configure to send messages after a target has been hit in a zone, and additional score is received. --- @param #SCORING self --- @param #boolean OnOff If true is given, the messages are sent. --- @return #SCORING -function SCORING:SetMessagesZone( OnOff ) - - self.MessagesZone = OnOff - return self -end - ---- If to send messages after a target has been hit in a zone, and additional score is received. --- @param #SCORING self --- @return #boolean -function SCORING:IfMessagesZone() - - return self.MessagesZone -end - ---- Configure to send messages to all players. --- @param #SCORING self --- @return #SCORING -function SCORING:SetMessagesToAll() - - self.MessagesAudience = 1 - return self -end - ---- If to send messages to all players. --- @param #SCORING self --- @return #boolean -function SCORING:IfMessagesToAll() - - return self.MessagesAudience == 1 -end - ---- Configure to send messages to only those players within the same coalition as the player. --- @param #SCORING self --- @return #SCORING -function SCORING:SetMessagesToCoalition() - - self.MessagesAudience = 2 - return self -end - ---- If to send messages to only those players within the same coalition as the player. --- @param #SCORING self --- @return #boolean -function SCORING:IfMessagesToCoalition() - - return self.MessagesAudience == 2 -end - - ---- When a player commits too much damage to friendlies, his penalty score will reach a certain level. --- Use this method to define the level when a player gets kicked. --- By default, the fratricide level is the default penalty mutiplier * 2 for the penalty score. --- @param #SCORING self --- @param #number Fratricide The amount of maximum penalty that may be inflicted by a friendly player before he gets kicked. --- @return #SCORING -function SCORING:SetFratricide( Fratricide ) - - self.Fratricide = Fratricide - return self -end - - ---- When a player changes the coalition, he can receive a penalty score. --- Use the method @{#SCORING.SetCoalitionChangePenalty}() to define the penalty when a player changes coalition. --- By default, the penalty for changing coalition is the default penalty scale. --- @param #SCORING self --- @param #number CoalitionChangePenalty The amount of penalty that is given. --- @return #SCORING -function SCORING:SetCoalitionChangePenalty( CoalitionChangePenalty ) - - self.CoalitionChangePenalty = CoalitionChangePenalty - return self -end - - ---- Add a new player entering a Unit. --- @param #SCORING self --- @param Wrapper.Unit#UNIT UnitData -function SCORING:_AddPlayerFromUnit( UnitData ) - self:F( UnitData ) - - if UnitData:IsAlive() then - local UnitName = UnitData:GetName() - local PlayerName = UnitData:GetPlayerName() - local UnitDesc = UnitData:GetDesc() - local UnitCategory = UnitDesc.category - local UnitCoalition = UnitData:GetCoalition() - local UnitTypeName = UnitData:GetTypeName() - local UnitThreatLevel, UnitThreatType = UnitData:GetThreatLevel() - - self:T( { PlayerName, UnitName, UnitCategory, UnitCoalition, UnitTypeName } ) - - if self.Players[PlayerName] == nil then -- I believe this is the place where a Player gets a life in a mission when he enters a unit ... - self.Players[PlayerName] = {} - self.Players[PlayerName].Hit = {} - self.Players[PlayerName].Destroy = {} - self.Players[PlayerName].Goals = {} - self.Players[PlayerName].Mission = {} - - -- for CategoryID, CategoryName in pairs( SCORINGCategory ) do - -- self.Players[PlayerName].Hit[CategoryID] = {} - -- self.Players[PlayerName].Destroy[CategoryID] = {} - -- end - self.Players[PlayerName].HitPlayers = {} - self.Players[PlayerName].Score = 0 - self.Players[PlayerName].Penalty = 0 - self.Players[PlayerName].PenaltyCoalition = 0 - self.Players[PlayerName].PenaltyWarning = 0 - end - - if not self.Players[PlayerName].UnitCoalition then - self.Players[PlayerName].UnitCoalition = UnitCoalition - else - if self.Players[PlayerName].UnitCoalition ~= UnitCoalition then - self.Players[PlayerName].Penalty = self.Players[PlayerName].Penalty + 50 - self.Players[PlayerName].PenaltyCoalition = self.Players[PlayerName].PenaltyCoalition + 1 - MESSAGE:New( "Player '" .. PlayerName .. "' changed coalition from " .. _SCORINGCoalition[self.Players[PlayerName].UnitCoalition] .. " to " .. _SCORINGCoalition[UnitCoalition] .. - "(changed " .. self.Players[PlayerName].PenaltyCoalition .. " times the coalition). 50 Penalty points added.", - 2 - ):ToAll() - self:ScoreCSV( PlayerName, "", "COALITION_PENALTY", 1, -50, self.Players[PlayerName].UnitName, _SCORINGCoalition[self.Players[PlayerName].UnitCoalition], _SCORINGCategory[self.Players[PlayerName].UnitCategory], self.Players[PlayerName].UnitType, - UnitName, _SCORINGCoalition[UnitCoalition], _SCORINGCategory[UnitCategory], UnitData:GetTypeName() ) - end - end - self.Players[PlayerName].UnitName = UnitName - self.Players[PlayerName].UnitCoalition = UnitCoalition - self.Players[PlayerName].UnitCategory = UnitCategory - self.Players[PlayerName].UnitType = UnitTypeName - self.Players[PlayerName].UNIT = UnitData - self.Players[PlayerName].ThreatLevel = UnitThreatLevel - self.Players[PlayerName].ThreatType = UnitThreatType - - if self.Players[PlayerName].Penalty > self.Fratricide * 0.50 then - if self.Players[PlayerName].PenaltyWarning < 1 then - MESSAGE:New( "Player '" .. PlayerName .. "': WARNING! If you continue to commit FRATRICIDE and have a PENALTY score higher than " .. self.Fratricide .. ", you will be COURT MARTIALED and DISMISSED from this mission! \nYour total penalty is: " .. self.Players[PlayerName].Penalty, - 30 - ):ToAll() - self.Players[PlayerName].PenaltyWarning = self.Players[PlayerName].PenaltyWarning + 1 - end - end - - if self.Players[PlayerName].Penalty > self.Fratricide then - UnitData:Destroy() - MESSAGE:New( "Player '" .. PlayerName .. "' committed FRATRICIDE, he will be COURT MARTIALED and is DISMISSED from this mission!", - 10 - ):ToAll() - end - - end -end - - ---- Add a goal score for a player. --- The method takes the PlayerUnit for which the Goal score needs to be set. --- The GoalTag is a string or identifier that is taken into the CSV file scoring log to identify the goal. --- A free text can be given that is shown to the players. --- The Score can be both positive and negative. --- @param #SCORING self --- @param Wrapper.Unit#UNIT PlayerUnit The @{Unit} of the Player. Other Properties for the scoring are taken from this PlayerUnit, like coalition, type etc. --- @param #string GoalTag The string or identifier that is used in the CSV file to identify the goal (sort or group later in Excel). --- @param #string Text A free text that is shown to the players. --- @param #number Score The score can be both positive or negative ( Penalty ). -function SCORING:AddGoalScore( PlayerUnit, GoalTag, Text, Score ) - - local PlayerName = PlayerUnit:GetPlayerName() - - self:E( { PlayerUnit.UnitName, PlayerName, GoalTag, Text, Score } ) - - -- PlayerName can be nil, if the Unit with the player crashed or due to another reason. - if PlayerName then - local PlayerData = self.Players[PlayerName] - - PlayerData.Goals[GoalTag] = PlayerData.Goals[GoalTag] or { Score = 0 } - PlayerData.Goals[GoalTag].Score = PlayerData.Goals[GoalTag].Score + Score - PlayerData.Score = PlayerData.Score + Score - - MESSAGE:New( Text, 30 ):ToAll() - - self:ScoreCSV( PlayerName, "", "GOAL_" .. string.upper( GoalTag ), 1, Score, PlayerUnit:GetName() ) - end -end - - ---- Registers Scores the players completing a Mission Task. --- @param #SCORING self --- @param Tasking.Mission#MISSION Mission --- @param Wrapper.Unit#UNIT PlayerUnit --- @param #string Text --- @param #number Score -function SCORING:_AddMissionTaskScore( Mission, PlayerUnit, Text, Score ) - - local PlayerName = PlayerUnit:GetPlayerName() - local MissionName = Mission:GetName() - - self:E( { Mission:GetName(), PlayerUnit.UnitName, PlayerName, Text, Score } ) - - -- PlayerName can be nil, if the Unit with the player crashed or due to another reason. - if PlayerName then - local PlayerData = self.Players[PlayerName] - - if not PlayerData.Mission[MissionName] then - PlayerData.Mission[MissionName] = {} - PlayerData.Mission[MissionName].ScoreTask = 0 - PlayerData.Mission[MissionName].ScoreMission = 0 - end - - self:T( PlayerName ) - self:T( PlayerData.Mission[MissionName] ) - - PlayerData.Score = self.Players[PlayerName].Score + Score - PlayerData.Mission[MissionName].ScoreTask = self.Players[PlayerName].Mission[MissionName].ScoreTask + Score - - MESSAGE:New( "Player '" .. PlayerName .. "' has " .. Text .. " in Mission '" .. MissionName .. "'. " .. - Score .. " task score!", - 30 ):ToAll() - - self:ScoreCSV( PlayerName, "", "TASK_" .. MissionName:gsub( ' ', '_' ), 1, Score, PlayerUnit:GetName() ) - end -end - - ---- Registers Mission Scores for possible multiple players that contributed in the Mission. --- @param #SCORING self --- @param Tasking.Mission#MISSION Mission --- @param Wrapper.Unit#UNIT PlayerUnit --- @param #string Text --- @param #number Score -function SCORING:_AddMissionScore( Mission, Text, Score ) - - local MissionName = Mission:GetName() - - self:E( { Mission, Text, Score } ) - self:E( self.Players ) - - for PlayerName, PlayerData in pairs( self.Players ) do - - self:E( PlayerData ) - if PlayerData.Mission[MissionName] then - - PlayerData.Score = PlayerData.Score + Score - PlayerData.Mission[MissionName].ScoreMission = PlayerData.Mission[MissionName].ScoreMission + Score - - MESSAGE:New( "Player '" .. PlayerName .. "' has " .. Text .. " in Mission '" .. MissionName .. "'. " .. - Score .. " mission score!", - 60 ):ToAll() - - self:ScoreCSV( PlayerName, "", "MISSION_" .. MissionName:gsub( ' ', '_' ), 1, Score ) - end - end -end - - ---- Handles the OnPlayerEnterUnit event for the scoring. --- @param #SCORING self --- @param Core.Event#EVENTDATA Event -function SCORING:OnEventPlayerEnterUnit( Event ) - if Event.IniUnit then - self:_AddPlayerFromUnit( Event.IniUnit ) - local Menu = MENU_GROUP:New( Event.IniGroup, 'Scoring' ) - local ReportGroupSummary = MENU_GROUP_COMMAND:New( Event.IniGroup, 'Summary report players in group', Menu, SCORING.ReportScoreGroupSummary, self, Event.IniGroup ) - local ReportGroupDetailed = MENU_GROUP_COMMAND:New( Event.IniGroup, 'Detailed report players in group', Menu, SCORING.ReportScoreGroupDetailed, self, Event.IniGroup ) - local ReportToAllSummary = MENU_GROUP_COMMAND:New( Event.IniGroup, 'Summary report all players', Menu, SCORING.ReportScoreAllSummary, self, Event.IniGroup ) - self:SetState( Event.IniUnit, "ScoringMenu", Menu ) - end -end - ---- Handles the OnPlayerLeaveUnit event for the scoring. --- @param #SCORING self --- @param Core.Event#EVENTDATA Event -function SCORING:OnEventPlayerLeaveUnit( Event ) - if Event.IniUnit then - local Menu = self:GetState( Event.IniUnit, "ScoringMenu" ) -- Core.Menu#MENU_GROUP - if Menu then - Menu:Remove() - end - end -end - - ---- Handles the OnHit event for the scoring. --- @param #SCORING self --- @param Core.Event#EVENTDATA Event -function SCORING:_EventOnHit( Event ) - self:F( { Event } ) - - local InitUnit = nil - local InitUNIT = nil - local InitUnitName = "" - local InitGroup = nil - local InitGroupName = "" - local InitPlayerName = nil - - local InitCoalition = nil - local InitCategory = nil - local InitType = nil - local InitUnitCoalition = nil - local InitUnitCategory = nil - local InitUnitType = nil - - local TargetUnit = nil - local TargetUNIT = nil - local TargetUnitName = "" - local TargetGroup = nil - local TargetGroupName = "" - local TargetPlayerName = nil - - local TargetCoalition = nil - local TargetCategory = nil - local TargetType = nil - local TargetUnitCoalition = nil - local TargetUnitCategory = nil - local TargetUnitType = nil - - if Event.IniDCSUnit then - - InitUnit = Event.IniDCSUnit - InitUNIT = Event.IniUnit - InitUnitName = Event.IniDCSUnitName - InitGroup = Event.IniDCSGroup - InitGroupName = Event.IniDCSGroupName - InitPlayerName = Event.IniPlayerName - - InitCoalition = Event.IniCoalition - --TODO: Workaround Client DCS Bug - --InitCategory = InitUnit:getCategory() - --InitCategory = InitUnit:getDesc().category - InitCategory = Event.IniCategory - InitType = Event.IniTypeName - - InitUnitCoalition = _SCORINGCoalition[InitCoalition] - InitUnitCategory = _SCORINGCategory[InitCategory] - InitUnitType = InitType - - self:T( { InitUnitName, InitGroupName, InitPlayerName, InitCoalition, InitCategory, InitType , InitUnitCoalition, InitUnitCategory, InitUnitType } ) - end - - - if Event.TgtDCSUnit then - - TargetUnit = Event.TgtDCSUnit - TargetUNIT = Event.TgtUnit - TargetUnitName = Event.TgtDCSUnitName - TargetGroup = Event.TgtDCSGroup - TargetGroupName = Event.TgtDCSGroupName - TargetPlayerName = Event.TgtPlayerName - - TargetCoalition = Event.TgtCoalition - --TODO: Workaround Client DCS Bug - --TargetCategory = TargetUnit:getCategory() - --TargetCategory = TargetUnit:getDesc().category - TargetCategory = Event.TgtCategory - TargetType = Event.TgtTypeName - - TargetUnitCoalition = _SCORINGCoalition[TargetCoalition] - TargetUnitCategory = _SCORINGCategory[TargetCategory] - TargetUnitType = TargetType - - self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType, TargetUnitCoalition, TargetUnitCategory, TargetUnitType } ) - end - - if InitPlayerName ~= nil then -- It is a player that is hitting something - self:_AddPlayerFromUnit( InitUNIT ) - if self.Players[InitPlayerName] then -- This should normally not happen, but i'll test it anyway. - if TargetPlayerName ~= nil then -- It is a player hitting another player ... - self:_AddPlayerFromUnit( TargetUNIT ) - end - - self:T( "Hitting Something" ) - - -- What is he hitting? - if TargetCategory then - - -- A target got hit, score it. - -- Player contains the score data from self.Players[InitPlayerName] - local Player = self.Players[InitPlayerName] - - -- Ensure there is a hit table per TargetCategory and TargetUnitName. - Player.Hit[TargetCategory] = Player.Hit[TargetCategory] or {} - Player.Hit[TargetCategory][TargetUnitName] = Player.Hit[TargetCategory][TargetUnitName] or {} - - -- PlayerHit contains the score counters and data per unit that was hit. - local PlayerHit = Player.Hit[TargetCategory][TargetUnitName] - - PlayerHit.Score = PlayerHit.Score or 0 - PlayerHit.Penalty = PlayerHit.Penalty or 0 - PlayerHit.ScoreHit = PlayerHit.ScoreHit or 0 - PlayerHit.PenaltyHit = PlayerHit.PenaltyHit or 0 - PlayerHit.TimeStamp = PlayerHit.TimeStamp or 0 - PlayerHit.UNIT = PlayerHit.UNIT or TargetUNIT - PlayerHit.ThreatLevel, PlayerHit.ThreatType = PlayerHit.UNIT:GetThreatLevel() - - -- Only grant hit scores if there was more than one second between the last hit. - if timer.getTime() - PlayerHit.TimeStamp > 1 then - PlayerHit.TimeStamp = timer.getTime() - - if TargetPlayerName ~= nil then -- It is a player hitting another player ... - - -- Ensure there is a Player to Player hit reference table. - Player.HitPlayers[TargetPlayerName] = true - end - - local Score = 0 - - if InitCoalition then -- A coalition object was hit. - if InitCoalition == TargetCoalition then - Player.Penalty = Player.Penalty + 10 - PlayerHit.Penalty = PlayerHit.Penalty + 10 - PlayerHit.PenaltyHit = PlayerHit.PenaltyHit + 1 - - if TargetPlayerName ~= nil then -- It is a player hitting another player ... - MESSAGE - :New( "Player '" .. InitPlayerName .. "' hit friendly player '" .. TargetPlayerName .. "' " .. - TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.PenaltyHit .. " times. " .. - "Penalty: -" .. PlayerHit.Penalty .. ". Score Total:" .. Player.Score - Player.Penalty, - 2 - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) - else - MESSAGE - :New( "Player '" .. InitPlayerName .. "' hit a friendly target " .. - TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.PenaltyHit .. " times. " .. - "Penalty: -" .. PlayerHit.Penalty .. ". Score Total:" .. Player.Score - Player.Penalty, - 2 - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) - end - self:ScoreCSV( InitPlayerName, TargetPlayerName, "HIT_PENALTY", 1, -10, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - else - Player.Score = Player.Score + 1 - PlayerHit.Score = PlayerHit.Score + 1 - PlayerHit.ScoreHit = PlayerHit.ScoreHit + 1 - if TargetPlayerName ~= nil then -- It is a player hitting another player ... - MESSAGE - :New( "Player '" .. InitPlayerName .. "' hit enemy player '" .. TargetPlayerName .. "' " .. - TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.ScoreHit .. " times. " .. - "Score: " .. PlayerHit.Score .. ". Score Total:" .. Player.Score - Player.Penalty, - 2 - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) - else - MESSAGE - :New( "Player '" .. InitPlayerName .. "' hit an enemy target " .. - TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.ScoreHit .. " times. " .. - "Score: " .. PlayerHit.Score .. ". Score Total:" .. Player.Score - Player.Penalty, - 2 - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) - end - self:ScoreCSV( InitPlayerName, TargetPlayerName, "HIT_SCORE", 1, 1, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - end - else -- A scenery object was hit. - MESSAGE - :New( "Player '" .. InitPlayerName .. "' hit a scenery object.", - 2 - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) - self:ScoreCSV( InitPlayerName, "", "HIT_SCORE", 1, 0, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, "", "Scenery", TargetUnitType ) - end - end - end - end - elseif InitPlayerName == nil then -- It is an AI hitting a player??? - - end - - -- It is a weapon initiated by a player, that is hitting something - -- This seems to occur only with scenery and static objects. - if Event.WeaponPlayerName ~= nil then - self:_AddPlayerFromUnit( Event.WeaponUNIT ) - if self.Players[Event.WeaponPlayerName] then -- This should normally not happen, but i'll test it anyway. - if TargetPlayerName ~= nil then -- It is a player hitting another player ... - self:_AddPlayerFromUnit( TargetUNIT ) - end - - self:T( "Hitting Scenery" ) - - -- What is he hitting? - if TargetCategory then - - -- A scenery or static got hit, score it. - -- Player contains the score data from self.Players[WeaponPlayerName] - local Player = self.Players[Event.WeaponPlayerName] - - -- Ensure there is a hit table per TargetCategory and TargetUnitName. - Player.Hit[TargetCategory] = Player.Hit[TargetCategory] or {} - Player.Hit[TargetCategory][TargetUnitName] = Player.Hit[TargetCategory][TargetUnitName] or {} - - -- PlayerHit contains the score counters and data per unit that was hit. - local PlayerHit = Player.Hit[TargetCategory][TargetUnitName] - - PlayerHit.Score = PlayerHit.Score or 0 - PlayerHit.Penalty = PlayerHit.Penalty or 0 - PlayerHit.ScoreHit = PlayerHit.ScoreHit or 0 - PlayerHit.PenaltyHit = PlayerHit.PenaltyHit or 0 - PlayerHit.TimeStamp = PlayerHit.TimeStamp or 0 - PlayerHit.UNIT = PlayerHit.UNIT or TargetUNIT - PlayerHit.ThreatLevel, PlayerHit.ThreatType = PlayerHit.UNIT:GetThreatLevel() - - -- Only grant hit scores if there was more than one second between the last hit. - if timer.getTime() - PlayerHit.TimeStamp > 1 then - PlayerHit.TimeStamp = timer.getTime() - - local Score = 0 - - if InitCoalition then -- A coalition object was hit, probably a static. - if InitCoalition == TargetCoalition then - -- TODO: Penalty according scale - Player.Penalty = Player.Penalty + 10 - PlayerHit.Penalty = PlayerHit.Penalty + 10 - PlayerHit.PenaltyHit = PlayerHit.PenaltyHit + 1 - - MESSAGE - :New( "Player '" .. Event.WeaponPlayerName .. "' hit a friendly target " .. - TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.PenaltyHit .. " times. " .. - "Penalty: -" .. PlayerHit.Penalty .. ". Score Total:" .. Player.Score - Player.Penalty, - 2 - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( Event.WeaponCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) - self:ScoreCSV( Event.WeaponPlayerName, TargetPlayerName, "HIT_PENALTY", 1, -10, Event.WeaponName, Event.WeaponCoalition, Event.WeaponCategory, Event.WeaponTypeName, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - else - Player.Score = Player.Score + 1 - PlayerHit.Score = PlayerHit.Score + 1 - PlayerHit.ScoreHit = PlayerHit.ScoreHit + 1 - MESSAGE - :New( "Player '" .. Event.WeaponPlayerName .. "' hit an enemy target " .. - TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.ScoreHit .. " times. " .. - "Score: " .. PlayerHit.Score .. ". Score Total:" .. Player.Score - Player.Penalty, - 2 - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( Event.WeaponCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) - self:ScoreCSV( Event.WeaponPlayerName, TargetPlayerName, "HIT_SCORE", 1, 1, Event.WeaponName, Event.WeaponCoalition, Event.WeaponCategory, Event.WeaponTypeName, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - end - else -- A scenery object was hit. - MESSAGE - :New( "Player '" .. Event.WeaponPlayerName .. "' hit a scenery object.", - 2 - ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) - self:ScoreCSV( Event.WeaponPlayerName, "", "HIT_SCORE", 1, 0, Event.WeaponName, Event.WeaponCoalition, Event.WeaponCategory, Event.WeaponTypeName, TargetUnitName, "", "Scenery", TargetUnitType ) - end - end - end - end - end -end - ---- Track DEAD or CRASH events for the scoring. --- @param #SCORING self --- @param Core.Event#EVENTDATA Event -function SCORING:_EventOnDeadOrCrash( Event ) - self:F( { Event } ) - - local TargetUnit = nil - local TargetGroup = nil - local TargetUnitName = "" - local TargetGroupName = "" - local TargetPlayerName = "" - local TargetCoalition = nil - local TargetCategory = nil - local TargetType = nil - local TargetUnitCoalition = nil - local TargetUnitCategory = nil - local TargetUnitType = nil - - if Event.IniDCSUnit then - - TargetUnit = Event.IniUnit - TargetUnitName = Event.IniDCSUnitName - TargetGroup = Event.IniDCSGroup - TargetGroupName = Event.IniDCSGroupName - TargetPlayerName = Event.IniPlayerName - - TargetCoalition = Event.IniCoalition - --TargetCategory = TargetUnit:getCategory() - --TargetCategory = TargetUnit:getDesc().category -- Workaround - TargetCategory = Event.IniCategory - TargetType = Event.IniTypeName - - TargetUnitCoalition = _SCORINGCoalition[TargetCoalition] - TargetUnitCategory = _SCORINGCategory[TargetCategory] - TargetUnitType = TargetType - - self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType } ) - end - - -- Player contains the score and reference data for the player. - for PlayerName, Player in pairs( self.Players ) do - if Player then -- This should normally not happen, but i'll test it anyway. - self:T( "Something got destroyed" ) - - -- Some variables - local InitUnitName = Player.UnitName - local InitUnitType = Player.UnitType - local InitCoalition = Player.UnitCoalition - local InitCategory = Player.UnitCategory - local InitUnitCoalition = _SCORINGCoalition[InitCoalition] - local InitUnitCategory = _SCORINGCategory[InitCategory] - - self:T( { InitUnitName, InitUnitType, InitUnitCoalition, InitCoalition, InitUnitCategory, InitCategory } ) - - local Destroyed = false - - -- What is the player destroying? - if Player and Player.Hit and Player.Hit[TargetCategory] and Player.Hit[TargetCategory][TargetUnitName] and Player.Hit[TargetCategory][TargetUnitName].TimeStamp ~= 0 then -- Was there a hit for this unit for this player before registered??? - - local TargetThreatLevel = Player.Hit[TargetCategory][TargetUnitName].ThreatLevel - local TargetThreatType = Player.Hit[TargetCategory][TargetUnitName].ThreatType - - Player.Destroy[TargetCategory] = Player.Destroy[TargetCategory] or {} - Player.Destroy[TargetCategory][TargetType] = Player.Destroy[TargetCategory][TargetType] or {} - - -- PlayerDestroy contains the destroy score data per category and target type of the player. - local TargetDestroy = Player.Destroy[TargetCategory][TargetType] - TargetDestroy.Score = TargetDestroy.Score or 0 - TargetDestroy.ScoreDestroy = TargetDestroy.ScoreDestroy or 0 - TargetDestroy.Penalty = TargetDestroy.Penalty or 0 - TargetDestroy.PenaltyDestroy = TargetDestroy.PenaltyDestroy or 0 - - if TargetCoalition then - if InitCoalition == TargetCoalition then - local ThreatLevelTarget = TargetThreatLevel - local ThreatTypeTarget = TargetThreatType - local ThreatLevelPlayer = Player.ThreatLevel / 10 + 1 - local ThreatPenalty = math.ceil( ( ThreatLevelTarget / ThreatLevelPlayer ) * self.ScaleDestroyPenalty / 10 ) - self:E( { ThreatLevel = ThreatPenalty, ThreatLevelTarget = ThreatLevelTarget, ThreatTypeTarget = ThreatTypeTarget, ThreatLevelPlayer = ThreatLevelPlayer } ) - - Player.Penalty = Player.Penalty + ThreatPenalty - TargetDestroy.Penalty = TargetDestroy.Penalty + ThreatPenalty - TargetDestroy.PenaltyDestroy = TargetDestroy.PenaltyDestroy + 1 - - if Player.HitPlayers[TargetPlayerName] then -- A player destroyed another player - MESSAGE - :New( "Player '" .. PlayerName .. "' destroyed friendly player '" .. TargetPlayerName .. "' " .. - TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. TargetDestroy.PenaltyDestroy .. " times. " .. - "Penalty: -" .. TargetDestroy.Penalty .. ". Score Total:" .. Player.Score - Player.Penalty, - 15 - ) - :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) - else - MESSAGE - :New( "Player '" .. PlayerName .. "' destroyed a friendly target " .. - TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. TargetDestroy.PenaltyDestroy .. " times. " .. - "Penalty: -" .. TargetDestroy.Penalty .. ". Score Total:" .. Player.Score - Player.Penalty, - 15 - ) - :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) - end - - Destroyed = true - self:ScoreCSV( PlayerName, TargetPlayerName, "DESTROY_PENALTY", 1, ThreatPenalty, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - else - - local ThreatLevelTarget = TargetThreatLevel - local ThreatTypeTarget = TargetThreatType - local ThreatLevelPlayer = Player.ThreatLevel / 10 + 1 - local ThreatScore = math.ceil( ( ThreatLevelTarget / ThreatLevelPlayer ) * self.ScaleDestroyScore / 10 ) - - self:E( { ThreatLevel = ThreatScore, ThreatLevelTarget = ThreatLevelTarget, ThreatTypeTarget = ThreatTypeTarget, ThreatLevelPlayer = ThreatLevelPlayer } ) - - Player.Score = Player.Score + ThreatScore - TargetDestroy.Score = TargetDestroy.Score + ThreatScore - TargetDestroy.ScoreDestroy = TargetDestroy.ScoreDestroy + 1 - if Player.HitPlayers[TargetPlayerName] then -- A player destroyed another player - MESSAGE - :New( "Player '" .. PlayerName .. "' destroyed enemy player '" .. TargetPlayerName .. "' " .. - TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. TargetDestroy.ScoreDestroy .. " times. " .. - "Score: " .. TargetDestroy.Score .. ". Score Total:" .. Player.Score - Player.Penalty, - 15 - ) - :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) - else - MESSAGE - :New( "Player '" .. PlayerName .. "' destroyed an enemy " .. - TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. TargetDestroy.ScoreDestroy .. " times. " .. - "Score: " .. TargetDestroy.Score .. ". Total:" .. Player.Score - Player.Penalty, - 15 - ) - :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() ) - end - Destroyed = true - self:ScoreCSV( PlayerName, TargetPlayerName, "DESTROY_SCORE", 1, ThreatScore, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - - local UnitName = TargetUnit:GetName() - local Score = self.ScoringObjects[UnitName] - if Score then - Player.Score = Player.Score + Score - TargetDestroy.Score = TargetDestroy.Score + Score - MESSAGE - :New( "Special target '" .. TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. " destroyed! " .. - "Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! Total: " .. Player.Score - Player.Penalty, - 15 - ) - :ToAllIf( self:IfMessagesScore() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesScore() and self:IfMessagesToCoalition() ) - self:ScoreCSV( PlayerName, TargetPlayerName, "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - Destroyed = true - end - - -- Check if there are Zones where the destruction happened. - for ZoneName, ScoreZoneData in pairs( self.ScoringZones ) do - self:E( { ScoringZone = ScoreZoneData } ) - local ScoreZone = ScoreZoneData.ScoreZone -- Core.Zone#ZONE_BASE - local Score = ScoreZoneData.Score - if ScoreZone:IsVec2InZone( TargetUnit:GetVec2() ) then - Player.Score = Player.Score + Score - TargetDestroy.Score = TargetDestroy.Score + Score - MESSAGE - :New( "Target destroyed in zone '" .. ScoreZone:GetName() .. "'." .. - "Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! " .. - "Total: " .. Player.Score - Player.Penalty, - 15 ) - :ToAllIf( self:IfMessagesZone() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesZone() and self:IfMessagesToCoalition() ) - self:ScoreCSV( PlayerName, TargetPlayerName, "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - Destroyed = true - end - end - - end - else - -- Check if there are Zones where the destruction happened. - for ZoneName, ScoreZoneData in pairs( self.ScoringZones ) do - self:E( { ScoringZone = ScoreZoneData } ) - local ScoreZone = ScoreZoneData.ScoreZone -- Core.Zone#ZONE_BASE - local Score = ScoreZoneData.Score - if ScoreZone:IsVec2InZone( TargetUnit:GetVec2() ) then - Player.Score = Player.Score + Score - TargetDestroy.Score = TargetDestroy.Score + Score - MESSAGE - :New( "Scenery destroyed in zone '" .. ScoreZone:GetName() .. "'." .. - "Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! " .. - "Total: " .. Player.Score - Player.Penalty, - 15 - ) - :ToAllIf( self:IfMessagesZone() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesZone() and self:IfMessagesToCoalition() ) - Destroyed = true - self:ScoreCSV( PlayerName, "", "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, "", "Scenery", TargetUnitType ) - end - end - end - - -- Delete now the hit cache if the target was destroyed. - -- Otherwise points will be granted every time a target gets killed by the players that hit that target. - -- This is only relevant for player to player destroys. - if Destroyed then - Player.Hit[TargetCategory][TargetUnitName].TimeStamp = 0 - end - end - end - end -end - - ---- Produce detailed report of player hit scores. --- @param #SCORING self --- @param #string PlayerName The name of the player. --- @return #string The report. -function SCORING:ReportDetailedPlayerHits( PlayerName ) - - local ScoreMessage = "" - local PlayerScore = 0 - local PlayerPenalty = 0 - - local PlayerData = self.Players[PlayerName] - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) - - -- Some variables - local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] - local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local ScoreMessageHits = "" - for CategoryID, CategoryName in pairs( _SCORINGCategory ) do - self:T( CategoryName ) - if PlayerData.Hit[CategoryID] then - self:T( "Hit scores exist for player " .. PlayerName ) - local Score = 0 - local ScoreHit = 0 - local Penalty = 0 - local PenaltyHit = 0 - for UnitName, UnitData in pairs( PlayerData.Hit[CategoryID] ) do - Score = Score + UnitData.Score - ScoreHit = ScoreHit + UnitData.ScoreHit - Penalty = Penalty + UnitData.Penalty - PenaltyHit = UnitData.PenaltyHit - end - local ScoreMessageHit = string.format( "%s:%d ", CategoryName, Score - Penalty ) - self:T( ScoreMessageHit ) - ScoreMessageHits = ScoreMessageHits .. ScoreMessageHit - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageHits ~= "" then - ScoreMessage = "Hits: " .. ScoreMessageHits - end - end - - return ScoreMessage, PlayerScore, PlayerPenalty -end - - ---- Produce detailed report of player destroy scores. --- @param #SCORING self --- @param #string PlayerName The name of the player. --- @return #string The report. -function SCORING:ReportDetailedPlayerDestroys( PlayerName ) - - local ScoreMessage = "" - local PlayerScore = 0 - local PlayerPenalty = 0 - - local PlayerData = self.Players[PlayerName] - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) - - -- Some variables - local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] - local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local ScoreMessageDestroys = "" - for CategoryID, CategoryName in pairs( _SCORINGCategory ) do - if PlayerData.Destroy[CategoryID] then - self:T( "Destroy scores exist for player " .. PlayerName ) - local Score = 0 - local ScoreDestroy = 0 - local Penalty = 0 - local PenaltyDestroy = 0 - - for UnitName, UnitData in pairs( PlayerData.Destroy[CategoryID] ) do - self:E( { UnitData = UnitData } ) - if UnitData ~= {} then - Score = Score + UnitData.Score - ScoreDestroy = ScoreDestroy + UnitData.ScoreDestroy - Penalty = Penalty + UnitData.Penalty - PenaltyDestroy = PenaltyDestroy + UnitData.PenaltyDestroy - end - end - - local ScoreMessageDestroy = string.format( " %s:%d ", CategoryName, Score - Penalty ) - self:T( ScoreMessageDestroy ) - ScoreMessageDestroys = ScoreMessageDestroys .. ScoreMessageDestroy - - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageDestroys = ScoreMessageDestroys .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageDestroys ~= "" then - ScoreMessage = "Destroys: " .. ScoreMessageDestroys - end - end - - return ScoreMessage, PlayerScore, PlayerPenalty -end - ---- Produce detailed report of player penalty scores because of changing the coalition. --- @param #SCORING self --- @param #string PlayerName The name of the player. --- @return #string The report. -function SCORING:ReportDetailedPlayerCoalitionChanges( PlayerName ) - - local ScoreMessage = "" - local PlayerScore = 0 - local PlayerPenalty = 0 - - local PlayerData = self.Players[PlayerName] - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) - - -- Some variables - local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] - local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local ScoreMessageCoalitionChangePenalties = "" - if PlayerData.PenaltyCoalition ~= 0 then - ScoreMessageCoalitionChangePenalties = ScoreMessageCoalitionChangePenalties .. string.format( " -%d (%d changed)", PlayerData.Penalty, PlayerData.PenaltyCoalition ) - PlayerPenalty = PlayerPenalty + PlayerData.Penalty - end - if ScoreMessageCoalitionChangePenalties ~= "" then - ScoreMessage = ScoreMessage .. "Coalition Penalties: " .. ScoreMessageCoalitionChangePenalties - end - end - - return ScoreMessage, PlayerScore, PlayerPenalty -end - ---- Produce detailed report of player goal scores. --- @param #SCORING self --- @param #string PlayerName The name of the player. --- @return #string The report. -function SCORING:ReportDetailedPlayerGoals( PlayerName ) - - local ScoreMessage = "" - local PlayerScore = 0 - local PlayerPenalty = 0 - - local PlayerData = self.Players[PlayerName] - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) - - -- Some variables - local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] - local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local ScoreMessageGoal = "" - local ScoreGoal = 0 - local ScoreTask = 0 - for GoalName, GoalData in pairs( PlayerData.Goals ) do - ScoreGoal = ScoreGoal + GoalData.Score - ScoreMessageGoal = ScoreMessageGoal .. "'" .. GoalName .. "':" .. GoalData.Score .. "; " - end - PlayerScore = PlayerScore + ScoreGoal - - if ScoreMessageGoal ~= "" then - ScoreMessage = "Goals: " .. ScoreMessageGoal - end - end - - return ScoreMessage, PlayerScore, PlayerPenalty -end - ---- Produce detailed report of player penalty scores because of changing the coalition. --- @param #SCORING self --- @param #string PlayerName The name of the player. --- @return #string The report. -function SCORING:ReportDetailedPlayerMissions( PlayerName ) - - local ScoreMessage = "" - local PlayerScore = 0 - local PlayerPenalty = 0 - - local PlayerData = self.Players[PlayerName] - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) - - -- Some variables - local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] - local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local ScoreMessageMission = "" - local ScoreMission = 0 - local ScoreTask = 0 - for MissionName, MissionData in pairs( PlayerData.Mission ) do - ScoreMission = ScoreMission + MissionData.ScoreMission - ScoreTask = ScoreTask + MissionData.ScoreTask - ScoreMessageMission = ScoreMessageMission .. "'" .. MissionName .. "'; " - end - PlayerScore = PlayerScore + ScoreMission + ScoreTask - - if ScoreMessageMission ~= "" then - ScoreMessage = "Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ")" - end - end - - return ScoreMessage, PlayerScore, PlayerPenalty -end - - ---- Report Group Score Summary --- @param #SCORING self --- @param Wrapper.Group#GROUP PlayerGroup The player group. -function SCORING:ReportScoreGroupSummary( PlayerGroup ) - - local PlayerMessage = "" - - self:T( "Report Score Group Summary" ) - - local PlayerUnits = PlayerGroup:GetUnits() - for UnitID, PlayerUnit in pairs( PlayerUnits ) do - local PlayerUnit = PlayerUnit -- Wrapper.Unit#UNIT - local PlayerName = PlayerUnit:GetPlayerName() - - if PlayerName then - - local ReportHits, ScoreHits, PenaltyHits = self:ReportDetailedPlayerHits( PlayerName ) - ReportHits = ReportHits ~= "" and "\n- " .. ReportHits or ReportHits - self:E( { ReportHits, ScoreHits, PenaltyHits } ) - - local ReportDestroys, ScoreDestroys, PenaltyDestroys = self:ReportDetailedPlayerDestroys( PlayerName ) - ReportDestroys = ReportDestroys ~= "" and "\n- " .. ReportDestroys or ReportDestroys - self:E( { ReportDestroys, ScoreDestroys, PenaltyDestroys } ) - - local ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges = self:ReportDetailedPlayerCoalitionChanges( PlayerName ) - ReportCoalitionChanges = ReportCoalitionChanges ~= "" and "\n- " .. ReportCoalitionChanges or ReportCoalitionChanges - self:E( { ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges } ) - - local ReportGoals, ScoreGoals, PenaltyGoals = self:ReportDetailedPlayerGoals( PlayerName ) - ReportGoals = ReportGoals ~= "" and "\n- " .. ReportGoals or ReportGoals - self:E( { ReportGoals, ScoreGoals, PenaltyGoals } ) - - local ReportMissions, ScoreMissions, PenaltyMissions = self:ReportDetailedPlayerMissions( PlayerName ) - ReportMissions = ReportMissions ~= "" and "\n- " .. ReportMissions or ReportMissions - self:E( { ReportMissions, ScoreMissions, PenaltyMissions } ) - - local PlayerScore = ScoreHits + ScoreDestroys + ScoreCoalitionChanges + ScoreGoals + ScoreMissions - local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + ScoreGoals + PenaltyMissions - - PlayerMessage = - string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )", - PlayerName, - PlayerScore - PlayerPenalty, - PlayerScore, - PlayerPenalty - ) - MESSAGE:New( PlayerMessage, 30, "Player '" .. PlayerName .. "'" ):ToGroup( PlayerGroup ) - end - end - -end - ---- Report Group Score Detailed --- @param #SCORING self --- @param Wrapper.Group#GROUP PlayerGroup The player group. -function SCORING:ReportScoreGroupDetailed( PlayerGroup ) - - local PlayerMessage = "" - - self:T( "Report Score Group Detailed" ) - - local PlayerUnits = PlayerGroup:GetUnits() - for UnitID, PlayerUnit in pairs( PlayerUnits ) do - local PlayerUnit = PlayerUnit -- Wrapper.Unit#UNIT - local PlayerName = PlayerUnit:GetPlayerName() - - if PlayerName then - - local ReportHits, ScoreHits, PenaltyHits = self:ReportDetailedPlayerHits( PlayerName ) - ReportHits = ReportHits ~= "" and "\n- " .. ReportHits or ReportHits - self:E( { ReportHits, ScoreHits, PenaltyHits } ) - - local ReportDestroys, ScoreDestroys, PenaltyDestroys = self:ReportDetailedPlayerDestroys( PlayerName ) - ReportDestroys = ReportDestroys ~= "" and "\n- " .. ReportDestroys or ReportDestroys - self:E( { ReportDestroys, ScoreDestroys, PenaltyDestroys } ) - - local ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges = self:ReportDetailedPlayerCoalitionChanges( PlayerName ) - ReportCoalitionChanges = ReportCoalitionChanges ~= "" and "\n- " .. ReportCoalitionChanges or ReportCoalitionChanges - self:E( { ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges } ) - - local ReportGoals, ScoreGoals, PenaltyGoals = self:ReportDetailedPlayerGoals( PlayerName ) - ReportGoals = ReportGoals ~= "" and "\n- " .. ReportGoals or ReportGoals - self:E( { ReportGoals, ScoreGoals, PenaltyGoals } ) - - local ReportMissions, ScoreMissions, PenaltyMissions = self:ReportDetailedPlayerMissions( PlayerName ) - ReportMissions = ReportMissions ~= "" and "\n- " .. ReportMissions or ReportMissions - self:E( { ReportMissions, ScoreMissions, PenaltyMissions } ) - - local PlayerScore = ScoreHits + ScoreDestroys + ScoreCoalitionChanges + ScoreGoals + ScoreMissions - local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + ScoreGoals + PenaltyMissions - - PlayerMessage = - string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )%s%s%s%s%s", - PlayerName, - PlayerScore - PlayerPenalty, - PlayerScore, - PlayerPenalty, - ReportHits, - ReportDestroys, - ReportCoalitionChanges, - ReportGoals, - ReportMissions - ) - MESSAGE:New( PlayerMessage, 30, "Player '" .. PlayerName .. "'" ):ToGroup( PlayerGroup ) - end - end - -end - ---- Report all players score --- @param #SCORING self --- @param Wrapper.Group#GROUP PlayerGroup The player group. -function SCORING:ReportScoreAllSummary( PlayerGroup ) - - local PlayerMessage = "" - - self:T( "Report Score All Players" ) - - for PlayerName, PlayerData in pairs( self.Players ) do - - if PlayerName then - - local ReportHits, ScoreHits, PenaltyHits = self:ReportDetailedPlayerHits( PlayerName ) - ReportHits = ReportHits ~= "" and "\n- " .. ReportHits or ReportHits - self:E( { ReportHits, ScoreHits, PenaltyHits } ) - - local ReportDestroys, ScoreDestroys, PenaltyDestroys = self:ReportDetailedPlayerDestroys( PlayerName ) - ReportDestroys = ReportDestroys ~= "" and "\n- " .. ReportDestroys or ReportDestroys - self:E( { ReportDestroys, ScoreDestroys, PenaltyDestroys } ) - - local ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges = self:ReportDetailedPlayerCoalitionChanges( PlayerName ) - ReportCoalitionChanges = ReportCoalitionChanges ~= "" and "\n- " .. ReportCoalitionChanges or ReportCoalitionChanges - self:E( { ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges } ) - - local ReportGoals, ScoreGoals, PenaltyGoals = self:ReportDetailedPlayerGoals( PlayerName ) - ReportGoals = ReportGoals ~= "" and "\n- " .. ReportGoals or ReportGoals - self:E( { ReportGoals, ScoreGoals, PenaltyGoals } ) - - local ReportMissions, ScoreMissions, PenaltyMissions = self:ReportDetailedPlayerMissions( PlayerName ) - ReportMissions = ReportMissions ~= "" and "\n- " .. ReportMissions or ReportMissions - self:E( { ReportMissions, ScoreMissions, PenaltyMissions } ) - - local PlayerScore = ScoreHits + ScoreDestroys + ScoreCoalitionChanges + ScoreGoals + ScoreMissions - local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + ScoreGoals + PenaltyMissions - - PlayerMessage = - string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )", - PlayerName, - PlayerScore - PlayerPenalty, - PlayerScore, - PlayerPenalty - ) - MESSAGE:New( PlayerMessage, 30, "Player '" .. PlayerName .. "'" ):ToGroup( PlayerGroup ) - end - end - -end - - -function SCORING:SecondsToClock(sSeconds) - local nSeconds = sSeconds - if nSeconds == 0 then - --return nil; - return "00:00:00"; - else - nHours = string.format("%02.f", math.floor(nSeconds/3600)); - nMins = string.format("%02.f", math.floor(nSeconds/60 - (nHours*60))); - nSecs = string.format("%02.f", math.floor(nSeconds - nHours*3600 - nMins *60)); - return nHours..":"..nMins..":"..nSecs - end -end - ---- Opens a score CSV file to log the scores. --- @param #SCORING self --- @param #string ScoringCSV --- @return #SCORING self --- @usage --- -- Open a new CSV file to log the scores of the game Gori Valley. Let the name of the CSV file begin with "Player Scores". --- ScoringObject = SCORING:New( "Gori Valley" ) --- ScoringObject:OpenCSV( "Player Scores" ) -function SCORING:OpenCSV( ScoringCSV ) - self:F( ScoringCSV ) - - if lfs and io and os then - if ScoringCSV then - self.ScoringCSV = ScoringCSV - local fdir = lfs.writedir() .. [[Logs\]] .. self.ScoringCSV .. " " .. os.date( "%Y-%m-%d %H-%M-%S" ) .. ".csv" - - self.CSVFile, self.err = io.open( fdir, "w+" ) - if not self.CSVFile then - error( "Error: Cannot open CSV file in " .. lfs.writedir() ) - end - - self.CSVFile:write( '"GameName","RunTime","Time","PlayerName","TargetPlayerName","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 TargetPlayerName The name of the target 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, TargetPlayerName, 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( '"', '_' ) - - TargetPlayerName = TargetPlayerName or "" - TargetPlayerName = TargetPlayerName:gsub( '"', '_' ) - - if PlayerUnitName and PlayerUnitName ~= '' then - local PlayerUnit = Unit.getByName( PlayerUnitName ) - - if PlayerUnit then - if not PlayerUnitCategory then - --PlayerUnitCategory = SCORINGCategory[PlayerUnit:getCategory()] - PlayerUnitCategory = _SCORINGCategory[PlayerUnit:getDesc().category] - end - - if not PlayerUnitCoalition then - PlayerUnitCoalition = _SCORINGCoalition[PlayerUnit:getCoalition()] - end - - if not PlayerUnitType then - PlayerUnitType = PlayerUnit:getTypeName() - end - else - PlayerUnitName = '' - PlayerUnitCategory = '' - PlayerUnitCoalition = '' - PlayerUnitType = '' - end - else - PlayerUnitName = '' - PlayerUnitCategory = '' - PlayerUnitCoalition = '' - PlayerUnitType = '' - end - - TargetUnitCoalition = TargetUnitCoalition or "" - TargetUnitCategory = TargetUnitCategory or "" - TargetUnitType = TargetUnitType or "" - TargetUnitName = TargetUnitName or "" - - if lfs and io and os then - self.CSVFile:write( - '"' .. self.GameName .. '"' .. ',' .. - '"' .. self.RunTime .. '"' .. ',' .. - '' .. ScoreTime .. '' .. ',' .. - '"' .. PlayerName .. '"' .. ',' .. - '"' .. TargetPlayerName .. '"' .. ',' .. - '"' .. 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() ) -- #CLEANUP - self:F( { ZoneNames, TimeInterval } ) - - if type( ZoneNames ) == 'table' then - self.ZoneNames = ZoneNames - else - self.ZoneNames = { ZoneNames } - end - if TimeInterval then - self.TimeInterval = TimeInterval - end - - self:HandleEvent( EVENTS.Birth ) - - 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 - ---- @param #CLEANUP self --- @param Core.Event#EVENTDATA EventData -function CLEANUP:_OnEventBirth( EventData ) - self:F( { EventData } ) - - self.CleanUpList[EventData.IniDCSUnitName] = {} - self.CleanUpList[EventData.IniDCSUnitName].CleanUpUnit = EventData.IniDCSUnit - self.CleanUpList[EventData.IniDCSUnitName].CleanUpGroup = EventData.IniDCSGroup - self.CleanUpList[EventData.IniDCSUnitName].CleanUpGroupName = EventData.IniDCSGroupName - self.CleanUpList[EventData.IniDCSUnitName].CleanUpUnitName = EventData.IniDCSUnitName - - EventData.IniUnit:HandleEvent( EVENTS.EngineShutdown , self._EventAddForCleanUp ) - EventData.IniUnit:HandleEvent( EVENTS.EngineStartup, self._EventAddForCleanUp ) - EventData.IniUnit:HandleEvent( EVENTS.Hit, self._EventAddForCleanUp ) - EventData.IniUnit:HandleEvent( EVENTS.PilotDead, self._EventCrash ) - EventData.IniUnit:HandleEvent( EVENTS.Dead, self._EventCrash ) - EventData.IniUnit:HandleEvent( EVENTS.Crash, self._EventCrash ) - EventData.IniUnit:HandleEvent( EVENTS.Shot, self._EventShot ) - -end - ---- Detects if a crash event occurs. --- Crashed units go into a CleanUpList for removal. --- @param #CLEANUP self --- @param Dcs.DCSTypes#Event event -function CLEANUP:_EventCrash( Event ) - self:F( { Event } ) - - --TODO: This stuff is not working due to a DCS bug. Burning units cannot be destroyed. - -- self:T("before getGroup") - -- local _grp = Unit.getGroup(event.initiator)-- Identify the group that fired - -- self:T("after getGroup") - -- _grp:destroy() - -- self:T("after deactivateGroup") - -- event.initiator:destroy() - - self.CleanUpList[Event.IniDCSUnitName] = {} - self.CleanUpList[Event.IniDCSUnitName].CleanUpUnit = Event.IniDCSUnit - self.CleanUpList[Event.IniDCSUnitName].CleanUpGroup = Event.IniDCSGroup - self.CleanUpList[Event.IniDCSUnitName].CleanUpGroupName = Event.IniDCSGroupName - self.CleanUpList[Event.IniDCSUnitName].CleanUpUnitName = Event.IniDCSUnitName - -end - ---- Detects if a unit shoots a missile. --- If this occurs within one of the zones, then the weapon used must be destroyed. --- @param #CLEANUP self --- @param Dcs.DCSTypes#Event event -function CLEANUP:_EventShot( Event ) - self:F( { Event } ) - - -- Test if the missile was fired within one of the CLEANUP.ZoneNames. - local CurrentLandingZoneID = 0 - CurrentLandingZoneID = routines.IsUnitInZones( Event.IniDCSUnit, self.ZoneNames ) - if ( CurrentLandingZoneID ) then - -- Okay, the missile was fired within the CLEANUP.ZoneNames, destroy the fired weapon. - --_SEADmissile:destroy() - SCHEDULER:New( self, CLEANUP._DestroyMissile, { Event.Weapon }, 0.1 ) - end -end - - ---- Detects if the Unit has an S_EVENT_HIT within the given ZoneNames. If this is the case, destroy the unit. --- @param #CLEANUP self --- @param Dcs.DCSTypes#Event event -function CLEANUP:_EventHitCleanUp( Event ) - self:F( { Event } ) - - if Event.IniDCSUnit then - if routines.IsUnitInZones( Event.IniDCSUnit, self.ZoneNames ) ~= nil then - self:T( { "Life: ", Event.IniDCSUnitName, ' = ', Event.IniDCSUnit:getLife(), "/", Event.IniDCSUnit:getLife0() } ) - if Event.IniDCSUnit:getLife() < Event.IniDCSUnit:getLife0() then - self:T( "CleanUp: Destroy: " .. Event.IniDCSUnitName ) - SCHEDULER:New( self, CLEANUP._DestroyUnit, { Event.IniDCSUnit }, 0.1 ) - end - end - end - - if Event.TgtDCSUnit then - if routines.IsUnitInZones( Event.TgtDCSUnit, self.ZoneNames ) ~= nil then - self:T( { "Life: ", Event.TgtDCSUnitName, ' = ', Event.TgtDCSUnit:getLife(), "/", Event.TgtDCSUnit:getLife0() } ) - if Event.TgtDCSUnit:getLife() < Event.TgtDCSUnit:getLife0() then - self:T( "CleanUp: Destroy: " .. Event.TgtDCSUnitName ) - SCHEDULER:New( self, CLEANUP._DestroyUnit, { Event.TgtDCSUnit }, 0.1 ) - end - end - end -end - ---- Add the @{DCSWrapper.Unit#Unit} to the CleanUpList for CleanUp. -function CLEANUP:_AddForCleanUp( CleanUpUnit, CleanUpUnitName ) - self:F( { CleanUpUnit, CleanUpUnitName } ) - - self.CleanUpList[CleanUpUnitName] = {} - self.CleanUpList[CleanUpUnitName].CleanUpUnit = CleanUpUnit - self.CleanUpList[CleanUpUnitName].CleanUpUnitName = CleanUpUnitName - self.CleanUpList[CleanUpUnitName].CleanUpGroup = Unit.getGroup(CleanUpUnit) - self.CleanUpList[CleanUpUnitName].CleanUpGroupName = Unit.getGroup(CleanUpUnit):getName() - self.CleanUpList[CleanUpUnitName].CleanUpTime = timer.getTime() - self.CleanUpList[CleanUpUnitName].CleanUpMoved = false - - self:T( { "CleanUp: Add to CleanUpList: ", Unit.getGroup(CleanUpUnit):getName(), CleanUpUnitName } ) - -end - ---- Detects if the Unit has an S_EVENT_ENGINE_SHUTDOWN or an S_EVENT_HIT within the given ZoneNames. If this is the case, add the Group to the CLEANUP List. --- @param #CLEANUP self --- @param Dcs.DCSTypes#Event event -function CLEANUP:_EventAddForCleanUp( Event ) - - if Event.IniDCSUnit then - if self.CleanUpList[Event.IniDCSUnitName] == nil then - if routines.IsUnitInZones( Event.IniDCSUnit, self.ZoneNames ) ~= nil then - self:_AddForCleanUp( Event.IniDCSUnit, Event.IniDCSUnitName ) - end - end - end - - if Event.TgtDCSUnit then - if self.CleanUpList[Event.TgtDCSUnitName] == nil then - if routines.IsUnitInZones( Event.TgtDCSUnit, self.ZoneNames ) ~= nil then - self:_AddForCleanUp( Event.TgtDCSUnit, Event.TgtDCSUnitName ) - end - end - end - -end - -local CleanUpSurfaceTypeText = { - "LAND", - "SHALLOW_WATER", - "WATER", - "ROAD", - "RUNWAY" - } - ---- At the defined time interval, CleanUp the Groups within the CleanUpList. --- @param #CLEANUP self -function CLEANUP:_CleanUpScheduler() - self:F( { "CleanUp Scheduler" } ) - - local CleanUpCount = 0 - for CleanUpUnitName, UnitData in pairs( self.CleanUpList ) do - CleanUpCount = CleanUpCount + 1 - - self:T( { CleanUpUnitName, UnitData } ) - local CleanUpUnit = Unit.getByName(UnitData.CleanUpUnitName) - local CleanUpGroupName = UnitData.CleanUpGroupName - local CleanUpUnitName = UnitData.CleanUpUnitName - if CleanUpUnit then - self:T( { "CleanUp Scheduler", "Checking:", CleanUpUnitName } ) - if _DATABASE:GetStatusGroup( CleanUpGroupName ) ~= "ReSpawn" then - local CleanUpUnitVec3 = CleanUpUnit:getPoint() - --self:T( CleanUpUnitVec3 ) - local CleanUpUnitVec2 = {} - CleanUpUnitVec2.x = CleanUpUnitVec3.x - CleanUpUnitVec2.y = CleanUpUnitVec3.z - --self:T( CleanUpUnitVec2 ) - local CleanUpSurfaceType = land.getSurfaceType(CleanUpUnitVec2) - --self:T( CleanUpSurfaceType ) - - if CleanUpUnit and CleanUpUnit:getLife() <= CleanUpUnit:getLife0() * 0.95 then - if CleanUpSurfaceType == land.SurfaceType.RUNWAY then - if CleanUpUnit:inAir() then - local CleanUpLandHeight = land.getHeight(CleanUpUnitVec2) - local CleanUpUnitHeight = CleanUpUnitVec3.y - CleanUpLandHeight - self:T( { "CleanUp Scheduler", "Height = " .. CleanUpUnitHeight } ) - if CleanUpUnitHeight < 30 then - self:T( { "CleanUp Scheduler", "Destroy " .. CleanUpUnitName .. " because below safe height and damaged." } ) - self:_DestroyUnit(CleanUpUnit, CleanUpUnitName) - end - else - self:T( { "CleanUp Scheduler", "Destroy " .. CleanUpUnitName .. " because on runway and damaged." } ) - self:_DestroyUnit(CleanUpUnit, CleanUpUnitName) - end - end - end - -- Clean Units which are waiting for a very long time in the CleanUpZone. - if CleanUpUnit then - local CleanUpUnitVelocity = CleanUpUnit:getVelocity() - local CleanUpUnitVelocityTotal = math.abs(CleanUpUnitVelocity.x) + math.abs(CleanUpUnitVelocity.y) + math.abs(CleanUpUnitVelocity.z) - if CleanUpUnitVelocityTotal < 1 then - if UnitData.CleanUpMoved then - if UnitData.CleanUpTime + 180 <= timer.getTime() then - self:T( { "CleanUp Scheduler", "Destroy due to not moving anymore " .. CleanUpUnitName } ) - self:_DestroyUnit(CleanUpUnit, CleanUpUnitName) - end - end - else - UnitData.CleanUpTime = timer.getTime() - UnitData.CleanUpMoved = true - end - end - - else - -- Do nothing ... - self.CleanUpList[CleanUpUnitName] = nil -- Not anymore in the DCSRTE - end - else - self:T( "CleanUp: Group " .. CleanUpUnitName .. " cannot be found in DCS RTE, removing ..." ) - self.CleanUpList[CleanUpUnitName] = nil -- Not anymore in the DCSRTE - end - end - self:T(CleanUpCount) - - return true -end - ---- Single-Player:**Yes** / Multi-Player:**Yes** / AI:**Yes** / Human:**No** / Types:**All** -- --- **Spawn groups of units dynamically in your missions.** --- --- ![Banner Image](..\Presentations\SPAWN\SPAWN.JPG) --- --- === --- --- # 1) @{#SPAWN} class, extends @{Base#BASE} --- --- The @{#SPAWN} class allows to spawn dynamically new groups, based on pre-defined initialization settings, modifying the behaviour when groups are spawned. --- For each group to be spawned, within the mission editor, a group has to be created with the "late activation flag" set. We call this group the *"Spawn Template"* of the SPAWN object. --- A reference to this Spawn Template needs to be provided when constructing the SPAWN object, by indicating the name of the group within the mission editor in the constructor methods. --- --- Within the SPAWN object, there is an internal index that keeps track of which group from the internal group list was spawned. --- When new groups get spawned by using the SPAWN methods (see below), it will be validated whether the Limits (@{#SPAWN.Limit}) of the SPAWN object are not reached. --- When all is valid, a new group will be created by the spawning methods, and the internal index will be increased with 1. --- --- Regarding the name of new spawned groups, a _SpawnPrefix_ will be assigned for each new group created. --- If you want to have the Spawn Template name to be used as the _SpawnPrefix_ name, use the @{#SPAWN.New} constructor. --- However, when the @{#SPAWN.NewWithAlias} constructor was used, the Alias name will define the _SpawnPrefix_ name. --- Groups will follow the following naming structure when spawned at run-time: --- --- 1. Spawned groups will have the name _SpawnPrefix_#ggg, where ggg is a counter from 0 to 999. --- 2. Spawned units will have the name _SpawnPrefix_#ggg-uu, where uu is a counter from 0 to 99 for each new spawned unit belonging to the group. --- --- Some additional notes that need to be remembered: --- --- * Templates are actually groups defined within the mission editor, with the flag "Late Activation" set. As such, these groups are never used within the mission, but are used by the @{#SPAWN} module. --- * It is important to defined BEFORE you spawn new groups, a proper initialization of the SPAWN instance is done with the options you want to use. --- * When designing a mission, NEVER name groups using a "#" within the name of the group Spawn Template(s), or the SPAWN module logic won't work anymore. --- --- ## 1.1) SPAWN construction methods --- --- Create a new SPAWN object with the @{#SPAWN.New}() or the @{#SPAWN.NewWithAlias}() methods: --- --- * @{#SPAWN.New}(): Creates a new SPAWN object taking the name of the group that represents the GROUP Template (definition). --- * @{#SPAWN.NewWithAlias}(): Creates a new SPAWN object taking the name of the group that represents the GROUP Template (definition), and gives each spawned @{Group} an different name. --- --- It is important to understand how the SPAWN class works internally. The SPAWN object created will contain internally a list of groups that will be spawned and that are already spawned. --- The initialization methods will modify this list of groups so that when a group gets spawned, ALL information is already prepared when spawning. This is done for performance reasons. --- So in principle, the group list will contain all parameters and configurations after initialization, and when groups get actually spawned, this spawning can be done quickly and efficient. --- --- ## 1.2) SPAWN initialization methods --- --- A spawn object will behave differently based on the usage of **initialization** methods, which all start with the **Init** prefix: --- --- * @{#SPAWN.InitKeepUnitNames}(): Keeps the unit names as defined within the mission editor, but note that anything after a # mark is ignored, and any spaces before and after the resulting name are removed. IMPORTANT! This method MUST be the first used after :New !!! --- * @{#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.InitRandomizePosition}(): Randomizes the position of @{Group}s that are spawned within a **radius band**, given an Outer and Inner radius, from the point that the spawn happens. --- * @{#SPAWN.InitRandomizeUnits}(): Randomizes the @{Unit}s in the @{Group} that is spawned within a **radius band**, given an Outer and Inner radius. --- * @{#SPAWN.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-03-14: SPAWN:**InitKeepUnitNames()** added. --- 2017-03-14: SPAWN:**InitRandomizePosition( RandomizePosition, OuterRadious, InnerRadius )** added. --- --- 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.SpawnInitLimit = false -- By default, no InitLimit - self.SpawnMaxUnitsAlive = 0 -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time. - self.SpawnMaxGroups = 0 -- The maximum amount of groups that can be spawned. - self.SpawnRandomize = false -- Sets the randomization flag of new Spawned units to false. - self.SpawnVisible = false -- Flag that indicates if all the Groups of the SpawnGroup need to be visible when Spawned. - self.AIOnOff = true -- The AI is on by default when spawning a group. - self.SpawnUnControlled = false - self.SpawnInitKeepUnitNames = false -- Overwrite unit names by default with group name. - - self.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 - - self:SetEventPriority( 5 ) - - 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.SpawnInitLimit = false -- By default, no InitLimit - self.SpawnMaxUnitsAlive = 0 -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time. - self.SpawnMaxGroups = 0 -- The maximum amount of groups that can be spawned. - self.SpawnRandomize = false -- Sets the randomization flag of new Spawned units to false. - self.SpawnVisible = false -- Flag that indicates if all the Groups of the SpawnGroup need to be visible when Spawned. - self.AIOnOff = true -- The AI is on by default when spawning a group. - self.SpawnUnControlled = false - self.SpawnInitKeepUnitNames = false -- Overwrite unit names by default with group name. - - self.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 - - self:SetEventPriority( 5 ) - - 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.SpawnInitLimit = true - 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 - ---- Keeps the unit names as defined within the mission editor, --- but note that anything after a # mark is ignored, --- and any spaces before and after the resulting name are removed. --- IMPORTANT! This method MUST be the first used after :New !!! --- @param #SPAWN self --- @return #SPAWN self -function SPAWN:InitKeepUnitNames() - self:F( ) - - self.SpawnInitKeepUnitNames = true - - 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 position of @{Group}s that are spawned within a **radius band**, given an Outer and Inner radius, from the point that the spawn happens. --- @param #SPAWN self --- @param #boolean RandomizePosition If true, SPAWN will perform the randomization of the @{Group}s position between a given outer and inner radius. --- @param Dcs.DCSTypes#Distance OuterRadius (optional) The outer radius in meters where the new group will be spawned. --- @param Dcs.DCSTypes#Distance InnerRadius (optional) The inner radius in meters where the new group will NOT be spawned. --- @return #SPAWN -function SPAWN:InitRandomizePosition( RandomizePosition, OuterRadius, InnerRadius ) - self:F( { self.SpawnTemplatePrefix, RandomizePosition, OuterRadius, InnerRadius } ) - - self.SpawnRandomizePosition = RandomizePosition or false - self.SpawnRandomizePositionOuterRadius = OuterRadius or 0 - self.SpawnRandomizePositionInnerRadius = InnerRadius or 0 - - 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 - - self:HandleEvent( EVENTS.Birth, self._OnBirth ) - self:HandleEvent( EVENTS.Dead, self._OnDeadOrCrash ) - self:HandleEvent( EVENTS.Crash, self._OnDeadOrCrash ) - if self.Repeat then - self:HandleEvent( EVENTS.Takeoff, self._OnTakeOff ) - self:HandleEvent( EVENTS.Land, self._OnLand ) - end - if self.RepeatOnEngineShutDown then - self:HandleEvent( EVENTS.EngineShutdown, self._OnEngineShutDown ) - 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 RandomizePosition, then Randomize the formation in the zone band, keeping the template. - if self.SpawnRandomizePosition then - local RandomVec2 = PointVec3:GetRandomVec2InRadius( self.SpawnRandomizePositionOuterRadius, self.SpawnRandomizePositionInnerRadius ) - local CurrentX = SpawnTemplate.units[1].x - local CurrentY = SpawnTemplate.units[1].y - SpawnTemplate.x = RandomVec2.x - SpawnTemplate.y = RandomVec2.y - for UnitID = 1, #SpawnTemplate.units do - SpawnTemplate.units[UnitID].x = SpawnTemplate.units[UnitID].x + ( RandomVec2.x - CurrentX ) - SpawnTemplate.units[UnitID].y = SpawnTemplate.units[UnitID].y + ( RandomVec2.y - CurrentY ) - self:T( 'SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) - end - end - - -- If RandomizeUnits, then Randomize the formation at the start point. - if self.SpawnRandomizeUnits then - for UnitID = 1, #SpawnTemplate.units do - local RandomVec2 = PointVec3:GetRandomVec2InRadius( self.SpawnOuterRadius, self.SpawnInnerRadius ) - SpawnTemplate.units[UnitID].x = RandomVec2.x - SpawnTemplate.units[UnitID].y = RandomVec2.y - self:T( 'SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) - end - end - - if SpawnTemplate.CategoryID == Group.Category.HELICOPTER or SpawnTemplate.CategoryID == Group.Category.AIRPLANE then - if SpawnTemplate.route.points[1].type == "TakeOffParking" then - SpawnTemplate.uncontrolled = self.SpawnUnControlled - end - end - end - - self:HandleEvent( EVENTS.Birth, self._OnBirth ) - self:HandleEvent( EVENTS.Dead, self._OnDeadOrCrash ) - self:HandleEvent( EVENTS.Crash, self._OnDeadOrCrash ) - if self.Repeat then - self:HandleEvent( EVENTS.Takeoff, self._OnTakeOff ) - self:HandleEvent( EVENTS.Land, self._OnLand ) - end - if self.RepeatOnEngineShutDown then - self:HandleEvent( EVENTS.EngineShutdown, self._OnEngineShutDown ) - end - - 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 - - self:T3( SpawnTemplate.name ) - - -- 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 - - ---- 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:_GetPrefixFromGroup( SpawnGroup ) - self:F3( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnGroup } ) - - local GroupName = SpawnGroup:GetName() - if GroupName then - local SpawnPrefix = string.match( GroupName, ".*#" ) - if SpawnPrefix then - SpawnPrefix = SpawnPrefix:sub( 1, -2 ) - end - return SpawnPrefix - end - - return nil -end - - ---- Get the index from a given group. --- The function will search the name of the group for a #, and will return the number behind the #-mark. -function SPAWN:GetSpawnIndexFromGroup( SpawnGroup ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnGroup } ) - - local IndexString = string.match( SpawnGroup:GetName(), "#(%d*)$" ):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. --- @param #SPAWN self -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 self.SpawnInitKeepUnitNames == false then - for UnitID = 1, #SpawnTemplate.units do - SpawnTemplate.units[UnitID].name = string.format( SpawnTemplate.name .. '-%02d', UnitID ) - SpawnTemplate.units[UnitID].unitId = nil - end - else - for UnitID = 1, #SpawnTemplate.units do - local UnitPrefix, Rest = string.match( SpawnTemplate.units[UnitID].name, "^([^#]+)#?" ):gsub( "^%s*(.-)%s*$", "%1" ) - self:T( { UnitPrefix, Rest } ) - - SpawnTemplate.units[UnitID].name = string.format( '%s#%03d-%02d', UnitPrefix, SpawnIndex, UnitID ) - SpawnTemplate.units[UnitID].unitId = nil - end - end - - self:T3( { "Template:", SpawnTemplate } ) - return SpawnTemplate - -end - ---- Private method randomizing the routes. --- @param #SPAWN self --- @param #number SpawnIndex The index of the group to be spawned. --- @return #SPAWN -function SPAWN:_RandomizeRoute( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeRoute, self.SpawnRandomizeRouteStartPoint, self.SpawnRandomizeRouteEndPoint, self.SpawnRandomizeRouteRadius } ) - - if self.SpawnRandomizeRoute then - local SpawnTemplate = self.SpawnGroups[SpawnIndex].SpawnTemplate - local RouteCount = #SpawnTemplate.route.points - - for t = self.SpawnRandomizeRouteStartPoint + 1, ( RouteCount - self.SpawnRandomizeRouteEndPoint ) do - - SpawnTemplate.route.points[t].x = SpawnTemplate.route.points[t].x + math.random( self.SpawnRandomizeRouteRadius * -1, self.SpawnRandomizeRouteRadius ) - SpawnTemplate.route.points[t].y = SpawnTemplate.route.points[t].y + math.random( self.SpawnRandomizeRouteRadius * -1, self.SpawnRandomizeRouteRadius ) - - -- Manage randomization of altitude for airborne units ... - if SpawnTemplate.CategoryID == Group.Category.AIRPLANE or SpawnTemplate.CategoryID == Group.Category.HELICOPTER then - if SpawnTemplate.route.points[t].alt and self.SpawnRandomizeRouteHeight then - SpawnTemplate.route.points[t].alt = SpawnTemplate.route.points[t].alt + math.random( 1, self.SpawnRandomizeRouteHeight ) - end - else - SpawnTemplate.route.points[t].alt = nil - end - - self:T( 'SpawnTemplate.route.points[' .. t .. '].x = ' .. SpawnTemplate.route.points[t].x .. ', SpawnTemplate.route.points[' .. t .. '].y = ' .. SpawnTemplate.route.points[t].y ) - end - end - - self:_RandomizeZones( SpawnIndex ) - - return self -end - ---- Private method that randomizes the template of the group. --- @param #SPAWN self --- @param #number SpawnIndex --- @return #SPAWN self -function SPAWN:_RandomizeTemplate( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeTemplate } ) - - if self.SpawnRandomizeTemplate then - self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix = self.SpawnTemplatePrefixTable[ math.random( 1, #self.SpawnTemplatePrefixTable ) ] - self.SpawnGroups[SpawnIndex].SpawnTemplate = self:_Prepare( self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix, SpawnIndex ) - self.SpawnGroups[SpawnIndex].SpawnTemplate.route = routines.utils.deepCopy( self.SpawnTemplate.route ) - self.SpawnGroups[SpawnIndex].SpawnTemplate.x = self.SpawnTemplate.x - self.SpawnGroups[SpawnIndex].SpawnTemplate.y = self.SpawnTemplate.y - self.SpawnGroups[SpawnIndex].SpawnTemplate.start_time = self.SpawnTemplate.start_time - local OldX = self.SpawnGroups[SpawnIndex].SpawnTemplate.units[1].x - local OldY = self.SpawnGroups[SpawnIndex].SpawnTemplate.units[1].y - for UnitID = 1, #self.SpawnGroups[SpawnIndex].SpawnTemplate.units do - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].heading = self.SpawnTemplate.units[1].heading - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].x = self.SpawnTemplate.units[1].x + ( self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].x - OldX ) - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].y = self.SpawnTemplate.units[1].y + ( self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].y - OldY ) - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].alt = self.SpawnTemplate.units[1].alt - end - end - - self:_RandomizeRoute( SpawnIndex ) - - return self -end - ---- Private method that randomizes the @{Zone}s where the Group will be spawned. --- @param #SPAWN self --- @param #number SpawnIndex --- @return #SPAWN self -function SPAWN:_RandomizeZones( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeZones } ) - - if self.SpawnRandomizeZones then - local SpawnZone = nil -- Core.Zone#ZONE_BASE - while not SpawnZone do - self:T( { SpawnZoneTableCount = #self.SpawnZoneTable, self.SpawnZoneTable } ) - local ZoneID = math.random( #self.SpawnZoneTable ) - self:T( ZoneID ) - SpawnZone = self.SpawnZoneTable[ ZoneID ]:GetZoneMaybe() - end - - self:T( "Preparing Spawn in Zone", SpawnZone:GetName() ) - - local SpawnVec2 = SpawnZone:GetRandomVec2() - - self:T( { SpawnVec2 = SpawnVec2 } ) - - local SpawnTemplate = self.SpawnGroups[SpawnIndex].SpawnTemplate - - self:T( { Route = SpawnTemplate.route } ) - - for UnitID = 1, #SpawnTemplate.units do - local UnitTemplate = SpawnTemplate.units[UnitID] - self:T( 'Before Translation SpawnTemplate.units['..UnitID..'].x = ' .. UnitTemplate.x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. UnitTemplate.y ) - local SX = UnitTemplate.x - local SY = UnitTemplate.y - local BX = SpawnTemplate.route.points[1].x - local BY = SpawnTemplate.route.points[1].y - local TX = SpawnVec2.x + ( SX - BX ) - local TY = SpawnVec2.y + ( SY - BY ) - UnitTemplate.x = TX - UnitTemplate.y = TY - -- TODO: Manage altitude based on landheight... - --SpawnTemplate.units[UnitID].alt = SpawnVec2: - self:T( 'After Translation SpawnTemplate.units['..UnitID..'].x = ' .. UnitTemplate.x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. UnitTemplate.y ) - end - SpawnTemplate.x = SpawnVec2.x - SpawnTemplate.y = SpawnVec2.y - SpawnTemplate.route.points[1].x = SpawnVec2.x - SpawnTemplate.route.points[1].y = SpawnVec2.y - end - - return self - -end - -function SPAWN:_TranslateRotate( SpawnIndex, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle } ) - - -- Translate - local TranslatedX = SpawnX - local TranslatedY = SpawnY - - -- Rotate - -- From Wikipedia: https://en.wikipedia.org/wiki/Rotation_matrix#Common_rotations - -- x' = x \cos \theta - y \sin \theta\ - -- y' = x \sin \theta + y \cos \theta\ - local RotatedX = - TranslatedX * math.cos( math.rad( SpawnAngle ) ) - + TranslatedY * math.sin( math.rad( SpawnAngle ) ) - local RotatedY = TranslatedX * math.sin( math.rad( SpawnAngle ) ) - + TranslatedY * math.cos( math.rad( SpawnAngle ) ) - - -- Assign - self.SpawnGroups[SpawnIndex].SpawnTemplate.x = SpawnRootX - RotatedX - self.SpawnGroups[SpawnIndex].SpawnTemplate.y = SpawnRootY + RotatedY - - - local SpawnUnitCount = table.getn( self.SpawnGroups[SpawnIndex].SpawnTemplate.units ) - for u = 1, SpawnUnitCount do - - -- Translate - local TranslatedX = SpawnX - local TranslatedY = SpawnY - 10 * ( u - 1 ) - - -- Rotate - local RotatedX = - TranslatedX * math.cos( math.rad( SpawnAngle ) ) - + TranslatedY * math.sin( math.rad( SpawnAngle ) ) - local RotatedY = TranslatedX * math.sin( math.rad( SpawnAngle ) ) - + TranslatedY * math.cos( math.rad( SpawnAngle ) ) - - -- Assign - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].x = SpawnRootX - RotatedX - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].y = SpawnRootY + RotatedY - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].heading = self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].heading + math.rad( SpawnAngle ) - end - - return self -end - ---- Get the next index of the groups to be spawned. This method is complicated, as it is used at several spaces. -function SPAWN:_GetSpawnIndex( SpawnIndex ) - self:F2( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnMaxGroups, self.SpawnMaxUnitsAlive, self.AliveUnits, #self.SpawnTemplate.units } ) - - if ( self.SpawnMaxGroups == 0 ) or ( SpawnIndex <= self.SpawnMaxGroups ) then - if ( self.SpawnMaxUnitsAlive == 0 ) or ( self.AliveUnits + #self.SpawnTemplate.units <= self.SpawnMaxUnitsAlive ) or self.UnControlled == true then - if SpawnIndex and SpawnIndex >= self.SpawnCount + 1 then - self.SpawnCount = self.SpawnCount + 1 - SpawnIndex = self.SpawnCount - end - self.SpawnIndex = SpawnIndex - if not self.SpawnGroups[self.SpawnIndex] then - self:_InitializeSpawnGroups( self.SpawnIndex ) - end - else - return nil - end - else - return nil - end - - return self.SpawnIndex -end - - --- TODO Need to delete this... _DATABASE does this now ... - ---- @param #SPAWN self --- @param Core.Event#EVENTDATA EventData -function SPAWN:_OnBirth( EventData ) - self:F( self.SpawnTemplatePrefix ) - - local SpawnGroup = EventData.IniGroup - - if SpawnGroup then - local EventPrefix = self:_GetPrefixFromGroup( SpawnGroup ) - 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 - ---- Obscolete --- @todo Need to delete this... _DATABASE does this now ... - ---- @param #SPAWN self --- @param Core.Event#EVENTDATA EventData -function SPAWN:_OnDeadOrCrash( EventData ) - self:F( self.SpawnTemplatePrefix ) - - local SpawnGroup = EventData.IniGroup - - if SpawnGroup then - local EventPrefix = self:_GetPrefixFromGroup( SpawnGroup ) - self:T( { "Dead event: " .. EventPrefix } ) - 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. --- @param #SPAWN self --- @param Core.Event#EVENTDATA EventData -function SPAWN:_OnTakeOff( EventData ) - self:F( self.SpawnTemplatePrefix ) - - local SpawnGroup = EventData.IniGroup - if SpawnGroup then - local EventPrefix = self:_GetPrefixFromGroup( SpawnGroup ) - self:T( { "TakeOff event: " .. EventPrefix } ) - if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then - self:T( "self.Landed = false" ) - SpawnGroup:SetState( SpawnGroup, "Spawn_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. --- @param #SPAWN self --- @param Core.Event#EVENTDATA EventData -function SPAWN:_OnLand( EventData ) - self:F( self.SpawnTemplatePrefix ) - - local SpawnGroup = EventData.IniGroup - if SpawnGroup then - local EventPrefix = self:_GetPrefixFromGroup( SpawnGroup ) - self:T( { "Land event: " .. EventPrefix } ) - if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then - -- TODO: Check if this is the last unit of the group that lands. - SpawnGroup:SetState( SpawnGroup, "Spawn_Landed", true ) - if 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 --- @param Core.Event#EVENTDATA EventData -function SPAWN:_OnEngineShutDown( EventData ) - self:F( self.SpawnTemplatePrefix ) - - local SpawnGroup = EventData.IniGroup - if SpawnGroup then - local EventPrefix = self:_GetPrefixFromGroup( SpawnGroup ) - self:T( { "EngineShutdown event: " .. EventPrefix } ) - if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then - -- todo: test if on the runway - local Landed = SpawnGroup:GetState( SpawnGroup, "Spawn_Landed" ) - if 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 --- @extends Core.Base#BASE -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() ) -- #MOVEMENT - 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. - - self:HandleEvent( EVENTS.Birth ) - --- 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. --- @param #MOVEMENT self --- @param Core.Event#EVENTDATA self -function MOVEMENT:OnEventBirth( EventData ) - self:F( { EventData } ) - - 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 EventData.IniDCSUnit then - self:T( "Birth object : " .. EventData.IniDCSUnitName ) - if EventData.IniDCSGroup and EventData.IniDCSGroup:isExist() then - for MovePrefixID, MovePrefix in pairs( self.MovePrefixes ) do - if string.find( EventData.IniDCSUnitName, MovePrefix, 1, true ) then - self.AliveUnits = self.AliveUnits + 1 - self.MoveUnits[EventData.IniDCSUnitName] = EventData.IniDCSGroupName - self:T( self.AliveUnits ) - end - end - end - end - - EventData.IniUnit:HandleEvent( EVENTS.DEAD, self.OnDeadOrCrash ) - 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 - - self:HandleEvent( EVENTS.Shot ) - - 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 --- @param #SEAD --- @param Core.Event#EVENTDATA EventData -function SEAD:OnEventShot( EventData ) - self:F( { EventData } ) - - local SEADUnit = EventData.IniDCSUnit - local SEADUnitName = EventData.IniDCSUnitName - local SEADWeapon = EventData.Weapon -- Identify the weapon fired - local SEADWeaponName = EventData.WeaponName -- return weapon type - -- Start of the 2nd loop - self:T( "Missile Launched = " .. SEADWeaponName ) - if SEADWeaponName == "KH-58" or SEADWeaponName == "KH-25MPU" or SEADWeaponName == "AGM-88" or SEADWeaponName == "KH-31A" or SEADWeaponName == "KH-31P" then -- Check if the missile is a SEAD - local _evade = math.random (1,100) -- random number for chance of evading action - local _targetMim = EventData.Weapon:getTarget() -- Identify target - local _targetMimname = Unit.getName(_targetMim) - 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 (F10) of an active unit hosted by a player: --- --- * @{#ESCORT.MenuFollowAt}: Creates a menu to make the escort follow the client. --- * @{#ESCORT.MenuHoldAtEscortPosition}: Creates a menu to hold the escort at its current position. --- * @{#ESCORT.MenuHoldAtLeaderPosition}: Creates a menu to hold the escort at the client position. --- * @{#ESCORT.MenuScanForTargets}: Creates a menu so that the escort scans targets. --- * @{#ESCORT.MenuFlare}: Creates a menu to disperse flares. --- * @{#ESCORT.MenuSmoke}: Creates a menu to disparse smoke. --- * @{#ESCORT.MenuReportTargets}: Creates a menu so that the escort reports targets. --- * @{#ESCORT.MenuReportPosition}: Creates a menu so that the escort reports its current position from bullseye. --- * @{#ESCORT.MenuAssistedAttack: Creates a menu so that the escort supportes assisted attack from other escorts with the client. --- * @{#ESCORT.MenuROE: Creates a menu structure to set the rules of engagement of the escort. --- * @{#ESCORT.MenuEvasion: Creates a menu structure to set the evasion techniques when the escort is under threat. --- * @{#ESCORT.MenuResumeMission}: Creates a menu structure so that the escort can resume from a waypoint. --- --- --- @usage --- -- Declare a new EscortPlanes object as follows: --- --- -- 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 --- @field Functional.Detection#DETECTION_BASE Detection -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() ) -- #ESCORT - self:F( { EscortClient, EscortGroup, EscortName } ) - - self.EscortClient = EscortClient -- Wrapper.Client#CLIENT - self.EscortGroup = EscortGroup -- Wrapper.Group#GROUP - self.EscortName = EscortName - self.EscortBriefing = EscortBriefing - - self.EscortSetGroup = SET_GROUP:New() - self.EscortSetGroup:AddObject( self.EscortGroup ) - self.EscortSetGroup:Flush() - self.Detection = DETECTION_UNITS:New( self.EscortSetGroup, 15000 ) - - self.EscortGroup.Detection = self.Detection - - -- 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()].Detection = self.EscortGroup.Detection - 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, self.FollowSchedule = SCHEDULER:New( self, self._FollowScheduler, {}, 1, .5, .01 ) - self.FollowScheduler:Stop( self.FollowSchedule ) - - self.EscortMode = ESCORT.MODE.MISSION - - - return self -end - ---- Set a Detection method for the EscortClient to be reported upon. --- Detection methods are based on the derived classes from DETECTION_BASE. --- @param #ESCORT self --- @param Function.Detection#DETECTION_BASE Detection -function ESCORT:SetDetection( Detection ) - - self.Detection = Detection - self.EscortGroup.Detection = self.Detection - self.EscortClient._EscortGroups[self.EscortGroup:GetName()].Detection = self.EscortGroup.Detection - - Detection:__Start( 1 ) - -end - ---- This function is for test, it will put on the frequency of the FollowScheduler a red smoke at the direction vector calculated for the escort to fly to. --- This allows to visualize where the escort is flying to. --- @param #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, self, 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, - self, - self.EscortGroup, - Height, - 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, - self, - 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, self ) - self.EscortMenuFlareGreen = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release green flare", self.EscortMenuFlare, ESCORT._Flare, self, FLARECOLOR.Green, "Released a green flare!" ) - self.EscortMenuFlareRed = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release red flare", self.EscortMenuFlare, ESCORT._Flare, self, FLARECOLOR.Red, "Released a red flare!" ) - self.EscortMenuFlareWhite = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release white flare", self.EscortMenuFlare, ESCORT._Flare, self, FLARECOLOR.White, "Released a white flare!" ) - self.EscortMenuFlareYellow = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release yellow flare", self.EscortMenuFlare, ESCORT._Flare, self, FLARECOLOR.Yellow, "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, self ) - self.EscortMenuSmokeGreen = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release green smoke", self.EscortMenuSmoke, ESCORT._Smoke, self, SMOKECOLOR.Green, "Releasing green smoke!" ) - self.EscortMenuSmokeRed = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release red smoke", self.EscortMenuSmoke, ESCORT._Smoke, self, SMOKECOLOR.Red, "Releasing red smoke!" ) - self.EscortMenuSmokeWhite = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release white smoke", self.EscortMenuSmoke, ESCORT._Smoke, self, SMOKECOLOR.White, "Releasing white smoke!" ) - self.EscortMenuSmokeOrange = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release orange smoke", self.EscortMenuSmoke, ESCORT._Smoke, self, SMOKECOLOR.Orange, "Releasing orange smoke!" ) - self.EscortMenuSmokeBlue = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release blue smoke", self.EscortMenuSmoke, ESCORT._Smoke, self, SMOKECOLOR.Blue, "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, self ) - self.EscortMenuReportNearbyTargetsOn = MENU_CLIENT_COMMAND:New( self.EscortClient, "Report targets on", self.EscortMenuReportNearbyTargets, ESCORT._SwitchReportNearbyTargets, self, true ) - self.EscortMenuReportNearbyTargetsOff = MENU_CLIENT_COMMAND:New( self.EscortClient, "Report targets off", self.EscortMenuReportNearbyTargets, ESCORT._SwitchReportNearbyTargets, self, 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, self, self.EscortGroup:OptionROEHoldFire(), "Holding weapons!" ) - end - if self.EscortGroup:OptionROEReturnFirePossible() then - self.EscortMenuROEReturnFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Return Fire", self.EscortMenuROE, ESCORT._ROE, self, self.EscortGroup:OptionROEReturnFire(), "Returning fire!" ) - end - if self.EscortGroup:OptionROEOpenFirePossible() then - self.EscortMenuROEOpenFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Open Fire", self.EscortMenuROE, ESCORT._ROE, self, self.EscortGroup:OptionROEOpenFire(), "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, self, self.EscortGroup:OptionROEWeaponFree(), "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, self, self.EscortGroup:OptionROTNoReaction(), "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, self, self.EscortGroup:OptionROTPassiveDefense(), "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, self, self.EscortGroup:OptionROTEvadeFire(), "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, self, self.EscortGroup:OptionROTVertical(), "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( OrbitGroup, OrbitHeight, OrbitSeconds ) - - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - local OrbitUnit = OrbitGroup:GetUnit(1) -- Wrapper.Unit#UNIT - - self.FollowScheduler:Stop( self.FollowSchedule ) - - 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( Distance ) - - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - self.Distance = Distance - - 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( self.FollowSchedule ) - - EscortGroup:OptionROEHoldFire() - EscortGroup:OptionROTPassiveDefense() - - self.EscortMode = ESCORT.MODE.FOLLOW - - self.CT1 = 0 - self.GT1 = 0 - self.FollowScheduler:Start( self.FollowSchedule ) - - EscortGroup:MessageToClient( "Rejoining and Following at " .. Distance .. "!", 30, EscortClient ) -end - ---- @param #MENUPARAM MenuParam -function ESCORT:_Flare( Color, Message ) - - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - EscortGroup:GetUnit(1):Flare( Color ) - EscortGroup:MessageToClient( Message, 10, EscortClient ) -end - ---- @param #MENUPARAM MenuParam -function ESCORT:_Smoke( Color, Message ) - - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - EscortGroup:GetUnit(1):Smoke( Color ) - EscortGroup:MessageToClient( Message, 10, EscortClient ) -end - - ---- @param #MENUPARAM MenuParam -function ESCORT:_ReportNearbyTargetsNow() - - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - self:_ReportTargetsScheduler() - -end - -function ESCORT:_SwitchReportNearbyTargets( ReportTargets ) - - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - self.ReportTargets = ReportTargets - - if self.ReportTargets then - if not self.ReportTargetsScheduler then - self.ReportTargetsScheduler:Schedule( self, self._ReportTargetsScheduler, {}, 1, 30 ) - end - else - routines.removeFunction( self.ReportTargetsScheduler ) - self.ReportTargetsScheduler = nil - end -end - ---- @param #MENUPARAM MenuParam -function ESCORT:_ScanTargets( ScanDuration ) - - local EscortGroup = self.EscortGroup -- Wrapper.Group#GROUP - local EscortClient = self.EscortClient - - self.FollowScheduler:Stop( self.FollowSchedule ) - - if EscortGroup:IsHelicopter() then - EscortGroup:PushTask( - EscortGroup:TaskControlled( - EscortGroup:TaskOrbitCircle( 200, 20 ), - EscortGroup:TaskCondition( nil, nil, nil, nil, ScanDuration, nil ) - ), 1 ) - elseif EscortGroup:IsAirPlane() then - EscortGroup:PushTask( - EscortGroup:TaskControlled( - EscortGroup:TaskOrbitCircle( 1000, 500 ), - EscortGroup:TaskCondition( nil, nil, nil, nil, ScanDuration, nil ) - ), 1 ) - end - - EscortGroup:MessageToClient( "Scanning targets for " .. ScanDuration .. " seconds.", ScanDuration, EscortClient ) - - if self.EscortMode == ESCORT.MODE.FOLLOW then - self.FollowScheduler:Start( self.FollowSchedule ) - 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 #ESCORT self --- @param #number DetectedItemID -function ESCORT:_AttackTarget( DetectedItemID ) - - local EscortGroup = self.EscortGroup -- Wrapper.Group#GROUP - self:E( EscortGroup ) - - local EscortClient = self.EscortClient - - self.FollowScheduler:Stop( self.FollowSchedule ) - - if EscortGroup:IsAir() then - EscortGroup:OptionROEOpenFire() - EscortGroup:OptionROTPassiveDefense() - EscortGroup:SetState( EscortGroup, "Escort", self ) - - local DetectedSet = self.Detection:GetDetectedSet( DetectedItemID ) - - local Tasks = {} - - DetectedSet:ForEachUnit( - --- @param Wrapper.Unit#UNIT DetectedUnit - function( DetectedUnit, Tasks ) - if DetectedUnit:IsAlive() then - Tasks[#Tasks+1] = EscortGroup:TaskAttackUnit( DetectedUnit ) - end - end, Tasks - ) - - Tasks[#Tasks+1] = EscortGroup:TaskFunction( 1, 2, "_Resume", { "''" } ) - - EscortGroup:SetTask( - EscortGroup:TaskCombo( - Tasks - ), 1 - ) - - else - - local DetectedSet = self.Detection:GetDetectedSet( DetectedItemID ) - - local Tasks = {} - - DetectedSet:ForEachUnit( - --- @param Wrapper.Unit#UNIT DetectedUnit - function( DetectedUnit, Tasks ) - if DetectedUnit:IsAlive() then - Tasks[#Tasks+1] = EscortGroup:TaskFireAtPoint( DetectedUnit:GetVec2(), 50 ) - end - end, Tasks - ) - - EscortGroup:SetTask( - EscortGroup:TaskCombo( - Tasks - ), 1 - ) - - end - - EscortGroup:MessageToClient( "Engaging Designated Unit!", 10, EscortClient ) - -end - ---- --- @param #number DetectedItemID -function ESCORT:_AssistTarget( EscortGroupAttack, DetectedItemID ) - - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - self.FollowScheduler:Stop( self.FollowSchedule ) - - if EscortGroupAttack:IsAir() then - EscortGroupAttack:OptionROEOpenFire() - EscortGroupAttack:OptionROTVertical() - - local DetectedSet = self.Detection:GetDetectedSet( DetectedItemID ) - - local Tasks = {} - - DetectedSet:ForEachUnit( - --- @param Wrapper.Unit#UNIT DetectedUnit - function( DetectedUnit, Tasks ) - if DetectedUnit:IsAlive() then - Tasks[#Tasks+1] = EscortGroupAttack:TaskAttackUnit( DetectedUnit ) - end - end, Tasks - ) - - Tasks[#Tasks+1] = EscortGroupAttack:TaskOrbitCircle( 500, 350 ) - - EscortGroupAttack:SetTask( - EscortGroupAttack:TaskCombo( - Tasks - ), 1 - ) - - else - local DetectedSet = self.Detection:GetDetectedSet( DetectedItemID ) - - local Tasks = {} - - DetectedSet:ForEachUnit( - --- @param Wrapper.Unit#UNIT DetectedUnit - function( DetectedUnit, Tasks ) - if DetectedUnit:IsAlive() then - Tasks[#Tasks+1] = EscortGroupAttack:TaskFireAtPoint( DetectedUnit:GetVec2(), 50 ) - end - end, Tasks - ) - - EscortGroupAttack:SetTask( - EscortGroupAttack:TaskCombo( - Tasks - ), 1 - ) - - end - - EscortGroupAttack:MessageToClient( "Assisting with the destroying the enemy unit!", 10, EscortClient ) - -end - ---- @param #MENUPARAM MenuParam -function ESCORT:_ROE( EscortROEFunction, EscortROEMessage ) - - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - pcall( function() EscortROEFunction() end ) - EscortGroup:MessageToClient( EscortROEMessage, 10, EscortClient ) -end - ---- @param #MENUPARAM MenuParam -function ESCORT:_ROT( EscortROTFunction, EscortROTMessage ) - - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - pcall( function() EscortROTFunction() end ) - EscortGroup:MessageToClient( EscortROTMessage, 10, EscortClient ) -end - ---- @param #MENUPARAM MenuParam -function ESCORT:_ResumeMission( WayPoint ) - - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - self.FollowScheduler:Stop( self.FollowSchedule ) - - local WayPoints = EscortGroup:GetTaskRoute() - self:T( WayPoint, WayPoints ) - - for WayPointIgnore = 1, WayPoint do - table.remove( WayPoints, 1 ) - end - - SCHEDULER:New( EscortGroup, EscortGroup.SetTask, { EscortGroup:TaskRoute( WayPoints ) }, 1 ) - - EscortGroup:MessageToClient( "Resuming mission from waypoint " .. WayPoint .. ".", 10, EscortClient ) -end - ---- Registers the waypoints --- @param #ESCORT self --- @return #table -function ESCORT:RegisterRoute() - self:F() - - local EscortGroup = self.EscortGroup -- Wrapper.Group#GROUP - - local TaskPoints = EscortGroup:GetTaskRoute() - - self:T( TaskPoints ) - - return TaskPoints -end - ---- @param Functional.Escort#ESCORT self -function ESCORT:_FollowScheduler() - self:F( { self.FollowDistance } ) - - self:T( {self.EscortClient.UnitName, self.EscortGroup.GroupName } ) - if self.EscortGroup:IsAlive() and self.EscortClient:IsAlive() then - - local ClientUnit = self.EscortClient:GetClientGroupUnit() - local GroupUnit = self.EscortGroup:GetUnit( 1 ) - local FollowDistance = self.FollowDistance - - self:T( {ClientUnit.UnitName, GroupUnit.UnitName } ) - - if self.CT1 == 0 and self.GT1 == 0 then - self.CV1 = ClientUnit:GetVec3() - self:T( { "self.CV1", self.CV1 } ) - self.CT1 = timer.getTime() - self.GV1 = GroupUnit:GetVec3() - self.GT1 = timer.getTime() - else - local CT1 = self.CT1 - local CT2 = timer.getTime() - local CV1 = self.CV1 - local CV2 = ClientUnit:GetVec3() - self.CT1 = CT2 - self.CV1 = CV2 - - local CD = ( ( CV2.x - CV1.x )^2 + ( CV2.y - CV1.y )^2 + ( CV2.z - CV1.z )^2 ) ^ 0.5 - local CT = CT2 - CT1 - - local CS = ( 3600 / CT ) * ( CD / 1000 ) - - self:T2( { "Client:", CS, CD, CT, CV2, CV1, CT2, CT1 } ) - - local GT1 = self.GT1 - local GT2 = timer.getTime() - local GV1 = self.GV1 - local GV2 = GroupUnit:GetVec3() - self.GT1 = GT2 - self.GV1 = GV2 - - local GD = ( ( GV2.x - GV1.x )^2 + ( GV2.y - GV1.y )^2 + ( GV2.z - GV1.z )^2 ) ^ 0.5 - local GT = GT2 - GT1 - - local GS = ( 3600 / GT ) * ( GD / 1000 ) - - self:T2( { "Group:", GS, GD, GT, GV2, GV1, GT2, GT1 } ) - - -- Calculate the group direction vector - local GV = { x = GV2.x - CV2.x, y = GV2.y - CV2.y, z = GV2.z - CV2.z } - - -- Calculate GH2, GH2 with the same height as CV2. - local GH2 = { x = GV2.x, y = CV2.y, z = GV2.z } - - -- Calculate the angle of GV to the orthonormal plane - local alpha = math.atan2( GV.z, GV.x ) - - -- Now we calculate the intersecting vector between the circle around CV2 with radius FollowDistance and GH2. - -- From the GeoGebra model: CVI = (x(CV2) + FollowDistance cos(alpha), y(GH2) + FollowDistance sin(alpha), z(CV2)) - local CVI = { x = CV2.x + FollowDistance * math.cos(alpha), - y = GH2.y, - z = CV2.z + FollowDistance * math.sin(alpha), - } - - -- Calculate the direction vector DV of the escort group. We use CVI as the base and CV2 as the direction. - local DV = { x = CV2.x - CVI.x, y = CV2.y - CVI.y, z = CV2.z - CVI.z } - - -- We now calculate the unary direction vector DVu, so that we can multiply DVu with the speed, which is expressed in meters / s. - -- We need to calculate this vector to predict the point the escort group needs to fly to according its speed. - -- The distance of the destination point should be far enough not to have the aircraft starting to swipe left to right... - local DVu = { x = DV.x / FollowDistance, y = DV.y / FollowDistance, z = DV.z / FollowDistance } - - -- Now we can calculate the group destination vector GDV. - local GDV = { x = DVu.x * CS * 8 + CVI.x, y = CVI.y, z = DVu.z * CS * 8 + CVI.z } - - if self.SmokeDirectionVector == true then - trigger.action.smoke( GDV, trigger.smokeColor.Red ) - end - - self:T2( { "CV2:", CV2 } ) - self:T2( { "CVI:", CVI } ) - self:T2( { "GDV:", GDV } ) - - -- Measure distance between client and group - local CatchUpDistance = ( ( GDV.x - GV2.x )^2 + ( GDV.y - GV2.y )^2 + ( GDV.z - GV2.z )^2 ) ^ 0.5 - - -- The calculation of the Speed would simulate that the group would take 30 seconds to overcome - -- the requested Distance). - local Time = 10 - local CatchUpSpeed = ( CatchUpDistance - ( CS * 8.4 ) ) / Time - - local Speed = CS + CatchUpSpeed - if Speed < 0 then - Speed = 0 - end - - self:T( { "Client Speed, Escort Speed, Speed, FollowDistance, Time:", CS, GS, Speed, FollowDistance, Time } ) - - -- Now route the escort to the desired point with the desired speed. - self.EscortGroup:RouteToVec3( GDV, Speed / 3.6 ) -- DCS models speed in Mps (Miles per second) - end - - return true - end - - return false -end - - ---- Report Targets Scheduler. --- @param #ESCORT self -function ESCORT:_ReportTargetsScheduler() - self:F( self.EscortGroup:GetName() ) - - if self.EscortGroup:IsAlive() and self.EscortClient:IsAlive() then - - if true then - - local EscortGroupName = self.EscortGroup:GetName() - - self.EscortMenuAttackNearbyTargets:RemoveSubMenus() - - if self.EscortMenuTargetAssistance then - self.EscortMenuTargetAssistance:RemoveSubMenus() - end - - local DetectedItems = self.Detection:GetDetectedItems() - self:E( DetectedItems ) - - local DetectedTargets = false - - local DetectedMsgs = {} - - for ClientEscortGroupName, EscortGroupData in pairs( self.EscortClient._EscortGroups ) do - - local ClientEscortTargets = EscortGroupData.Detection - - for DetectedItemID, DetectedItem in ipairs( DetectedItems ) do - self:E( { DetectedItemID, DetectedItem } ) - -- Remove the sub menus of the Attack menu of the Escort for the EscortGroup. - - local DetectedItemReportSummary = self.Detection:DetectedItemReportSummary( DetectedItemID ) - - if ClientEscortGroupName == EscortGroupName then - - DetectedMsgs[#DetectedMsgs+1] = DetectedItemReportSummary - - MENU_CLIENT_COMMAND:New( self.EscortClient, - DetectedItemReportSummary, - self.EscortMenuAttackNearbyTargets, - ESCORT._AttackTarget, - self, - DetectedItemID - ) - else - if self.EscortMenuTargetAssistance then - - self:T( DetectedItemReportSummary ) - local MenuTargetAssistance = MENU_CLIENT:New( self.EscortClient, EscortGroupData.EscortName, self.EscortMenuTargetAssistance ) - MENU_CLIENT_COMMAND:New( self.EscortClient, - DetectedItemReportSummary, - MenuTargetAssistance, - ESCORT._AssistTarget, - self, - EscortGroupData.EscortGroup, - DetectedItemID - ) - end - end - - DetectedTargets = true - - end - end - self:E( DetectedMsgs ) - if DetectedTargets then - self.EscortGroup:MessageToClient( "Detected targets:\n" .. table.concat( DetectedMsgs, "\n" ), 20, self.EscortClient ) - else - self.EscortGroup:MessageToClient( "No targets detected.", 10, self.EscortClient ) - end - - return true - else --- 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, --- self, --- EscortGroupData.EscortGroup, --- 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 - 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 - - self:HandleEvent( EVENTS.Shot ) - - 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 EventData -function MISSILETRAINER:OnEventShot( EVentData ) - self:F( { EVentData } ) - - local TrainerSourceDCSUnit = EVentData.IniDCSUnit - local TrainerSourceDCSUnitName = EVentData.IniDCSUnitName - local TrainerWeapon = EVentData.Weapon -- Identify the weapon fired - local TrainerWeaponName = EVentData.WeaponName -- return weapon type - - self:T( "Missile Launched = " .. TrainerWeaponName ) - - local TrainerTargetDCSUnit = TrainerWeapon:getTarget() -- Identify target - if TrainerTargetDCSUnit then - local TrainerTargetDCSUnitName = Unit.getName( TrainerTargetDCSUnit ) - local TrainerTargetSkill = _DATABASE.Templates.Units[TrainerTargetDCSUnitName].Template.skill - - self:T(TrainerTargetDCSUnitName ) - - local Client = self.DBClients:FindClient( TrainerTargetDCSUnitName ) - if Client then - - local TrainerSourceUnit = UNIT:Find( TrainerSourceDCSUnit ) - local TrainerTargetUnit = UNIT:Find( TrainerTargetDCSUnit ) - - if self.MessagesOnOff == true and self.AlertsLaunchesOnOff == true then - - local Message = MESSAGE:New( - string.format( "%s launched a %s", - TrainerSourceUnit:GetTypeName(), - TrainerWeaponName - ) .. self:_AddRange( Client, TrainerWeapon ) .. self:_AddBearing( Client, TrainerWeapon ), 5, "Launch Alert" ) - - if self.AlertsToAll then - Message:ToAll() - else - Message:ToClient( Client ) - end - end - - local ClientID = Client:GetID() - self:T( ClientID ) - local MissileData = {} - MissileData.TrainerSourceUnit = TrainerSourceUnit - MissileData.TrainerWeapon = TrainerWeapon - MissileData.TrainerTargetUnit = TrainerTargetUnit - MissileData.TrainerWeaponTypeName = TrainerWeapon:getTypeName() - MissileData.TrainerWeaponLaunched = true - table.insert( self.TrackingMissiles[ClientID].MissileData, MissileData ) - --self:T( self.TrackingMissiles ) - end - else - -- TODO: some weapons don't know the target unit... Need to develop a workaround for this. - if ( TrainerWeapon:getTypeName() == "9M311" ) then - SCHEDULER:New( TrainerWeapon, TrainerWeapon.destroy, {}, 1 ) - else - end - end -end - -function MISSILETRAINER:_AddRange( Client, TrainerWeapon ) - - local RangeText = "" - - if self.DetailsRangeOnOff then - - local PositionMissile = TrainerWeapon:getPoint() - local TargetVec3 = Client:GetVec3() - - local Range = ( ( PositionMissile.x - TargetVec3.x )^2 + - ( PositionMissile.y - TargetVec3.y )^2 + - ( PositionMissile.z - TargetVec3.z )^2 - ) ^ 0.5 / 1000 - - RangeText = string.format( ", at %4.2fkm", Range ) - end - - return RangeText -end - -function MISSILETRAINER:_AddBearing( Client, TrainerWeapon ) - - local BearingText = "" - - if self.DetailsBearingOnOff then - - local PositionMissile = TrainerWeapon:getPoint() - local TargetVec3 = Client:GetVec3() - - self:T2( { TargetVec3, PositionMissile }) - - local DirectionVector = { x = PositionMissile.x - TargetVec3.x, y = PositionMissile.y - TargetVec3.y, z = PositionMissile.z - TargetVec3.z } - local DirectionRadians = math.atan2( DirectionVector.z, DirectionVector.x ) - --DirectionRadians = DirectionRadians + routines.getNorthCorrection( PositionTarget ) - if DirectionRadians < 0 then - DirectionRadians = DirectionRadians + 2 * math.pi - end - local DirectionDegrees = DirectionRadians * 180 / math.pi - - BearingText = string.format( ", %d degrees", DirectionDegrees ) - end - - return BearingText -end - - -function MISSILETRAINER:_TrackMissiles() - self:F2() - - - local ShowMessages = false - if self.MessagesOnOff and self.MessageLastTime + self.TrackingFrequency <= timer.getTime() then - self.MessageLastTime = timer.getTime() - ShowMessages = true - end - - -- ALERTS PART - - -- Loop for all Player Clients to check the alerts and deletion of missiles. - for ClientDataID, ClientData in pairs( self.TrackingMissiles ) do - - local Client = ClientData.Client - self:T2( { Client:GetName() } ) - - for MissileDataID, MissileData in pairs( ClientData.MissileData ) do - self:T3( MissileDataID ) - - local TrainerSourceUnit = MissileData.TrainerSourceUnit - local TrainerWeapon = MissileData.TrainerWeapon - local TrainerTargetUnit = MissileData.TrainerTargetUnit - local TrainerWeaponTypeName = MissileData.TrainerWeaponTypeName - local TrainerWeaponLaunched = MissileData.TrainerWeaponLaunched - - if Client and Client:IsAlive() and TrainerSourceUnit and TrainerSourceUnit:IsAlive() and TrainerWeapon and TrainerWeapon:isExist() and TrainerTargetUnit and TrainerTargetUnit:IsAlive() then - local PositionMissile = TrainerWeapon:getPosition().p - local TargetVec3 = Client:GetVec3() - - local Distance = ( ( PositionMissile.x - TargetVec3.x )^2 + - ( PositionMissile.y - TargetVec3.y )^2 + - ( PositionMissile.z - TargetVec3.z )^2 - ) ^ 0.5 / 1000 - - if Distance <= self.Distance then - -- Hit alert - TrainerWeapon:destroy() - if self.MessagesOnOff == true and self.AlertsHitsOnOff == true then - - self:T( "killed" ) - - local Message = MESSAGE:New( - string.format( "%s launched by %s killed %s", - TrainerWeapon:getTypeName(), - TrainerSourceUnit:GetTypeName(), - TrainerTargetUnit:GetPlayerName() - ), 15, "Hit Alert" ) - - if self.AlertsToAll == true then - Message:ToAll() - else - Message:ToClient( Client ) - end - - MissileData = nil - table.remove( ClientData.MissileData, MissileDataID ) - self:T(ClientData.MissileData) - end - end - else - if not ( TrainerWeapon and TrainerWeapon:isExist() ) then - if self.MessagesOnOff == true and self.AlertsLaunchesOnOff == true then - -- Weapon does not exist anymore. Delete from Table - local Message = MESSAGE:New( - string.format( "%s launched by %s self destructed!", - TrainerWeaponTypeName, - TrainerSourceUnit:GetTypeName() - ), 5, "Tracking" ) - - if self.AlertsToAll == true then - Message:ToAll() - else - Message:ToClient( Client ) - end - end - MissileData = nil - table.remove( ClientData.MissileData, MissileDataID ) - self:T( ClientData.MissileData ) - end - end - end - end - - if ShowMessages == true and self.MessagesOnOff == true and self.TrackingOnOff == true then -- Only do this when tracking information needs to be displayed. - - -- TRACKING PART - - -- For the current client, the missile range and bearing details are displayed To the Player Client. - -- For the other clients, the missile range and bearing details are displayed To the other Player Clients. - -- To achieve this, a cross loop is done for each Player Client <-> Other Player Client missile information. - - -- Main Player Client loop - for ClientDataID, ClientData in pairs( self.TrackingMissiles ) do - - local Client = ClientData.Client - self:T2( { Client:GetName() } ) - - - ClientData.MessageToClient = "" - ClientData.MessageToAll = "" - - -- Other Players Client loop - for TrackingDataID, TrackingData in pairs( self.TrackingMissiles ) do - - for MissileDataID, MissileData in pairs( TrackingData.MissileData ) do - self:T3( MissileDataID ) - - local TrainerSourceUnit = MissileData.TrainerSourceUnit - local TrainerWeapon = MissileData.TrainerWeapon - local TrainerTargetUnit = MissileData.TrainerTargetUnit - local TrainerWeaponTypeName = MissileData.TrainerWeaponTypeName - local TrainerWeaponLaunched = MissileData.TrainerWeaponLaunched - - if Client and Client:IsAlive() and TrainerSourceUnit and TrainerSourceUnit:IsAlive() and TrainerWeapon and TrainerWeapon:isExist() and TrainerTargetUnit and TrainerTargetUnit:IsAlive() then - - if ShowMessages == true then - local TrackingTo - TrackingTo = string.format( " -> %s", - TrainerWeaponTypeName - ) - - if ClientDataID == TrackingDataID then - if ClientData.MessageToClient == "" then - ClientData.MessageToClient = "Missiles to You:\n" - end - ClientData.MessageToClient = ClientData.MessageToClient .. TrackingTo .. self:_AddRange( ClientData.Client, TrainerWeapon ) .. self:_AddBearing( ClientData.Client, TrainerWeapon ) .. "\n" - else - if self.TrackingToAll == true then - if ClientData.MessageToAll == "" then - ClientData.MessageToAll = "Missiles to other Players:\n" - end - ClientData.MessageToAll = ClientData.MessageToAll .. TrackingTo .. self:_AddRange( ClientData.Client, TrainerWeapon ) .. self:_AddBearing( ClientData.Client, TrainerWeapon ) .. " ( " .. TrainerTargetUnit:GetPlayerName() .. " )\n" - end - end - end - end - end - end - - -- Once the Player Client and the Other Player Client tracking messages are prepared, show them. - if ClientData.MessageToClient ~= "" or ClientData.MessageToAll ~= "" then - local Message = MESSAGE:New( ClientData.MessageToClient .. ClientData.MessageToAll, 1, "Tracking" ):ToClient( Client ) - end - end - end - - return true -end ---- This module contains the AIRBASEPOLICE classes. --- --- === --- --- 1) @{AirbasePolice#AIRBASEPOLICE_BASE} class, extends @{Base#BASE} --- ================================================================== --- The @{AirbasePolice#AIRBASEPOLICE_BASE} class provides the main methods to monitor CLIENT behaviour at airbases. --- CLIENTS should not be allowed to: --- --- * Don't taxi faster than 40 km/h. --- * Don't take-off on taxiways. --- * Avoid to hit other planes on the airbase. --- * Obey ground control orders. --- --- 2) @{AirbasePolice#AIRBASEPOLICE_CAUCASUS} class, extends @{AirbasePolice#AIRBASEPOLICE_BASE} --- ============================================================================================= --- All the airbases on the caucasus map can be monitored using this class. --- If you want to monitor specific airbases, you need to use the @{#AIRBASEPOLICE_BASE.Monitor}() method, which takes a table or airbase names. --- The following names can be given: --- * AnapaVityazevo --- * Batumi --- * Beslan --- * Gelendzhik --- * Gudauta --- * Kobuleti --- * KrasnodarCenter --- * KrasnodarPashkovsky --- * Krymsk --- * Kutaisi --- * MaykopKhanskaya --- * MineralnyeVody --- * Mozdok --- * Nalchik --- * Novorossiysk --- * SenakiKolkhi --- * SochiAdler --- * Soganlug --- * SukhumiBabushara --- * TbilisiLochini --- * Vaziani --- --- 3) @{AirbasePolice#AIRBASEPOLICE_NEVADA} class, extends @{AirbasePolice#AIRBASEPOLICE_BASE} --- ============================================================================================= --- All the airbases on the NEVADA map can be monitored using this class. --- If you want to monitor specific airbases, you need to use the @{#AIRBASEPOLICE_BASE.Monitor}() method, which takes a table or airbase names. --- The following names can be given: --- * Nellis --- * McCarran --- * Creech --- * Groom Lake --- --- ### Contributions: Dutch Baron - Concept & Testing --- ### Author: FlightControl - Framework Design & Programming --- --- @module AirbasePolice - - - - - ---- @type AIRBASEPOLICE_BASE --- @field Core.Set#SET_CLIENT SetClient --- @extends Core.Base#BASE - -AIRBASEPOLICE_BASE = { - ClassName = "AIRBASEPOLICE_BASE", - SetClient = nil, - Airbases = nil, - AirbaseNames = nil, -} - - ---- Creates a new AIRBASEPOLICE_BASE object. --- @param #AIRBASEPOLICE_BASE self --- @param SetClient A SET_CLIENT object that will contain the CLIENT objects to be monitored if they follow the rules of the airbase. --- @param Airbases A table of Airbase Names. --- @return #AIRBASEPOLICE_BASE self -function AIRBASEPOLICE_BASE:New( SetClient, Airbases ) - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) - self:E( { self.ClassName, SetClient, Airbases } ) - - self.SetClient = SetClient - self.Airbases = Airbases - - for AirbaseID, Airbase in pairs( self.Airbases ) do - Airbase.ZoneBoundary = ZONE_POLYGON_BASE:New( "Boundary", Airbase.PointsBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - for PointsRunwayID, PointsRunway in pairs( Airbase.PointsRunways ) do - Airbase.ZoneRunways[PointsRunwayID] = ZONE_POLYGON_BASE:New( "Runway " .. PointsRunwayID, PointsRunway ):SmokeZone(SMOKECOLOR.Red):Flush() - end - end - --- -- Template --- local TemplateBoundary = GROUP:FindByName( "Template Boundary" ) --- self.Airbases.Template.ZoneBoundary = ZONE_POLYGON:New( "Template Boundary", TemplateBoundary ):SmokeZone(SMOKECOLOR.White):Flush() --- --- local TemplateRunway1 = GROUP:FindByName( "Template Runway 1" ) --- self.Airbases.Template.ZoneRunways[1] = ZONE_POLYGON:New( "Template Runway 1", TemplateRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - - self.SetClient:ForEachClient( - --- @param Wrapper.Client#CLIENT Client - function( Client ) - Client:SetState( self, "Speeding", false ) - Client:SetState( self, "Warnings", 0) - Client:SetState( self, "Taxi", false ) - end - ) - - self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, {}, 0, 2, 0.05 ) - - return self -end - ---- @type AIRBASEPOLICE_BASE.AirbaseNames --- @list <#string> - ---- Monitor a table of airbase names. --- @param #AIRBASEPOLICE_BASE self --- @param #AIRBASEPOLICE_BASE.AirbaseNames AirbaseNames A list of AirbaseNames to monitor. If this parameters is nil, then all airbases will be monitored. --- @return #AIRBASEPOLICE_BASE self -function AIRBASEPOLICE_BASE:Monitor( AirbaseNames ) - - if AirbaseNames then - if type( AirbaseNames ) == "table" then - self.AirbaseNames = AirbaseNames - else - self.AirbaseNames = { AirbaseNames } - end - end -end - ---- @param #AIRBASEPOLICE_BASE self -function AIRBASEPOLICE_BASE:_AirbaseMonitor() - - for AirbaseID, Airbase in pairs( self.Airbases ) do - - if not self.AirbaseNames or self.AirbaseNames[AirbaseID] then - - self:E( AirbaseID ) - - self.SetClient:ForEachClientInZone( Airbase.ZoneBoundary, - - --- @param Wrapper.Client#CLIENT Client - function( Client ) - - self:E( Client.UnitName ) - if Client:IsAlive() then - local NotInRunwayZone = true - for ZoneRunwayID, ZoneRunway in pairs( Airbase.ZoneRunways ) do - NotInRunwayZone = ( Client:IsNotInZone( ZoneRunway ) == true ) and NotInRunwayZone or false - end - - if NotInRunwayZone then - local Taxi = self:GetState( self, "Taxi" ) - self:E( Taxi ) - if Taxi == false then - Client:Message( "Welcome at " .. AirbaseID .. ". The maximum taxiing speed is " .. Airbase.MaximumSpeed " km/h.", 20, "ATC" ) - self:SetState( self, "Taxi", true ) - end - - -- TODO: GetVelocityKMH function usage - local VelocityVec3 = Client:GetVelocity() - local Velocity = ( VelocityVec3.x ^ 2 + VelocityVec3.y ^ 2 + VelocityVec3.z ^ 2 ) ^ 0.5 -- in meters / sec - local Velocity = Velocity * 3.6 -- now it is in km/h. - -- MESSAGE:New( "Velocity = " .. Velocity, 1 ):ToAll() - local IsAboveRunway = Client:IsAboveRunway() - local IsOnGround = Client:InAir() == false - self:T( IsAboveRunway, IsOnGround ) - - if IsAboveRunway and IsOnGround then - - if Velocity > Airbase.MaximumSpeed then - local IsSpeeding = Client:GetState( self, "Speeding" ) - - if IsSpeeding == true then - local SpeedingWarnings = Client:GetState( self, "Warnings" ) - self:T( SpeedingWarnings ) - - if SpeedingWarnings <= 3 then - Client:Message( "You are speeding on the taxiway! Slow down or you will be removed from this airbase! Your current velocity is " .. string.format( "%2.0f km/h", Velocity ), 5, "Warning " .. SpeedingWarnings .. " / 3" ) - Client:SetState( self, "Warnings", SpeedingWarnings + 1 ) - else - MESSAGE:New( "Player " .. Client:GetPlayerName() .. " has been removed from the airbase, due to a speeding violation ...", 10, "Airbase Police" ):ToAll() - Client:Destroy() - trigger.action.setUserFlag( "AIRCRAFT_"..Client:GetID(), 100) - Client:SetState( self, "Speeding", false ) - Client:SetState( self, "Warnings", 0 ) - end - - else - Client:Message( "You are speeding on the taxiway, slow down now! Your current velocity is " .. string.format( "%2.0f km/h", Velocity ), 5, "Attention! " ) - Client:SetState( self, "Speeding", true ) - Client:SetState( self, "Warnings", 1 ) - end - - else - Client:SetState( self, "Speeding", false ) - Client:SetState( self, "Warnings", 0 ) - end - end - - else - Client:SetState( self, "Speeding", false ) - Client:SetState( self, "Warnings", 0 ) - local Taxi = self:GetState( self, "Taxi" ) - if Taxi == true then - Client:Message( "You have progressed to the runway ... Await take-off clearance ...", 20, "ATC" ) - self:SetState( self, "Taxi", false ) - end - end - end - end - ) - end - end - - return true -end - - ---- @type AIRBASEPOLICE_CAUCASUS --- @field Core.Set#SET_CLIENT SetClient --- @extends #AIRBASEPOLICE_BASE - -AIRBASEPOLICE_CAUCASUS = { - ClassName = "AIRBASEPOLICE_CAUCASUS", - Airbases = { - AnapaVityazevo = { - PointsBoundary = { - [1]={["y"]=242234.85714287,["x"]=-6616.5714285726,}, - [2]={["y"]=241060.57142858,["x"]=-5585.142857144,}, - [3]={["y"]=243806.2857143,["x"]=-3962.2857142868,}, - [4]={["y"]=245240.57142858,["x"]=-4816.5714285726,}, - [5]={["y"]=244783.42857144,["x"]=-5630.8571428583,}, - [6]={["y"]=243800.57142858,["x"]=-5065.142857144,}, - [7]={["y"]=242232.00000001,["x"]=-6622.2857142868,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=242140.57142858,["x"]=-6478.8571428583,}, - [2]={["y"]=242188.57142858,["x"]=-6522.0000000011,}, - [3]={["y"]=244124.2857143,["x"]=-4344.0000000011,}, - [4]={["y"]=244068.2857143,["x"]=-4296.5714285726,}, - [5]={["y"]=242140.57142858,["x"]=-6480.0000000011,} - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Batumi = { - PointsBoundary = { - [1]={["y"]=617567.14285714,["x"]=-355313.14285715,}, - [2]={["y"]=616181.42857142,["x"]=-354800.28571429,}, - [3]={["y"]=616007.14285714,["x"]=-355128.85714286,}, - [4]={["y"]=618230,["x"]=-356914.57142858,}, - [5]={["y"]=618727.14285714,["x"]=-356166,}, - [6]={["y"]=617572.85714285,["x"]=-355308.85714286,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=616442.28571429,["x"]=-355090.28571429,}, - [2]={["y"]=618450.57142857,["x"]=-356522,}, - [3]={["y"]=618407.71428571,["x"]=-356584.85714286,}, - [4]={["y"]=618361.99999999,["x"]=-356554.85714286,}, - [5]={["y"]=618324.85714285,["x"]=-356599.14285715,}, - [6]={["y"]=618250.57142856,["x"]=-356543.42857143,}, - [7]={["y"]=618257.7142857,["x"]=-356496.28571429,}, - [8]={["y"]=618237.7142857,["x"]=-356459.14285715,}, - [9]={["y"]=616555.71428571,["x"]=-355258.85714286,}, - [10]={["y"]=616486.28571428,["x"]=-355280.57142858,}, - [11]={["y"]=616410.57142856,["x"]=-355227.71428572,}, - [12]={["y"]=616441.99999999,["x"]=-355179.14285715,}, - [13]={["y"]=616401.99999999,["x"]=-355147.71428572,}, - [14]={["y"]=616441.42857142,["x"]=-355092.57142858,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Beslan = { - PointsBoundary = { - [1]={["y"]=842082.57142857,["x"]=-148445.14285715,}, - [2]={["y"]=845237.71428572,["x"]=-148639.71428572,}, - [3]={["y"]=845232,["x"]=-148765.42857143,}, - [4]={["y"]=844220.57142857,["x"]=-149168.28571429,}, - [5]={["y"]=843274.85714286,["x"]=-149125.42857143,}, - [6]={["y"]=842077.71428572,["x"]=-148554,}, - [7]={["y"]=842083.42857143,["x"]=-148445.42857143,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=842104.57142857,["x"]=-148460.57142857,}, - [2]={["y"]=845225.71428572,["x"]=-148656,}, - [3]={["y"]=845220.57142858,["x"]=-148750,}, - [4]={["y"]=842098.85714286,["x"]=-148556.28571429,}, - [5]={["y"]=842104,["x"]=-148460.28571429,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Gelendzhik = { - PointsBoundary = { - [1]={["y"]=297856.00000001,["x"]=-51151.428571429,}, - [2]={["y"]=299044.57142858,["x"]=-49720.000000001,}, - [3]={["y"]=298861.71428572,["x"]=-49580.000000001,}, - [4]={["y"]=298198.85714286,["x"]=-49842.857142858,}, - [5]={["y"]=297990.28571429,["x"]=-50151.428571429,}, - [6]={["y"]=297696.00000001,["x"]=-51054.285714286,}, - [7]={["y"]=297850.28571429,["x"]=-51160.000000001,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=297834.00000001,["x"]=-51107.428571429,}, - [2]={["y"]=297786.57142858,["x"]=-51068.857142858,}, - [3]={["y"]=298946.57142858,["x"]=-49686.000000001,}, - [4]={["y"]=298993.14285715,["x"]=-49725.714285715,}, - [5]={["y"]=297835.14285715,["x"]=-51107.714285715,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Gudauta = { - PointsBoundary = { - [1]={["y"]=517246.57142857,["x"]=-197850.28571429,}, - [2]={["y"]=516749.42857142,["x"]=-198070.28571429,}, - [3]={["y"]=515755.14285714,["x"]=-197598.85714286,}, - [4]={["y"]=515369.42857142,["x"]=-196538.85714286,}, - [5]={["y"]=515623.71428571,["x"]=-195618.85714286,}, - [6]={["y"]=515946.57142857,["x"]=-195510.28571429,}, - [7]={["y"]=517243.71428571,["x"]=-197858.85714286,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=517096.57142857,["x"]=-197804.57142857,}, - [2]={["y"]=515880.85714285,["x"]=-195590.28571429,}, - [3]={["y"]=515812.28571428,["x"]=-195628.85714286,}, - [4]={["y"]=517036.57142857,["x"]=-197834.57142857,}, - [5]={["y"]=517097.99999999,["x"]=-197807.42857143,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Kobuleti = { - PointsBoundary = { - [1]={["y"]=634427.71428571,["x"]=-318290.28571429,}, - [2]={["y"]=635033.42857143,["x"]=-317550.2857143,}, - [3]={["y"]=635864.85714286,["x"]=-317333.14285715,}, - [4]={["y"]=636967.71428571,["x"]=-317261.71428572,}, - [5]={["y"]=637144.85714286,["x"]=-317913.14285715,}, - [6]={["y"]=634630.57142857,["x"]=-318687.42857144,}, - [7]={["y"]=634424.85714286,["x"]=-318290.2857143,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=634509.71428571,["x"]=-318339.42857144,}, - [2]={["y"]=636767.42857143,["x"]=-317516.57142858,}, - [3]={["y"]=636790,["x"]=-317575.71428572,}, - [4]={["y"]=634531.42857143,["x"]=-318398.00000001,}, - [5]={["y"]=634510.28571429,["x"]=-318339.71428572,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - KrasnodarCenter = { - PointsBoundary = { - [1]={["y"]=366680.28571429,["x"]=11699.142857142,}, - [2]={["y"]=366654.28571429,["x"]=11225.142857142,}, - [3]={["y"]=367497.14285715,["x"]=11082.285714285,}, - [4]={["y"]=368025.71428572,["x"]=10396.57142857,}, - [5]={["y"]=369854.28571429,["x"]=11367.999999999,}, - [6]={["y"]=369840.00000001,["x"]=11910.857142856,}, - [7]={["y"]=366682.57142858,["x"]=11697.999999999,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=369205.42857144,["x"]=11789.142857142,}, - [2]={["y"]=369209.71428572,["x"]=11714.857142856,}, - [3]={["y"]=366699.71428572,["x"]=11581.714285713,}, - [4]={["y"]=366698.28571429,["x"]=11659.142857142,}, - [5]={["y"]=369208.85714286,["x"]=11788.57142857,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - KrasnodarPashkovsky = { - PointsBoundary = { - [1]={["y"]=386754,["x"]=6476.5714285703,}, - [2]={["y"]=389182.57142858,["x"]=8722.2857142846,}, - [3]={["y"]=388832.57142858,["x"]=9086.5714285703,}, - [4]={["y"]=386961.14285715,["x"]=7707.9999999989,}, - [5]={["y"]=385404,["x"]=9179.4285714274,}, - [6]={["y"]=383239.71428572,["x"]=7386.5714285703,}, - [7]={["y"]=383954,["x"]=6486.5714285703,}, - [8]={["y"]=385775.42857143,["x"]=8097.9999999989,}, - [9]={["y"]=386804,["x"]=7319.4285714274,}, - [10]={["y"]=386375.42857143,["x"]=6797.9999999989,}, - [11]={["y"]=386746.85714286,["x"]=6472.2857142846,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=385891.14285715,["x"]=8416.5714285703,}, - [2]={["y"]=385842.28571429,["x"]=8467.9999999989,}, - [3]={["y"]=384180.85714286,["x"]=6917.1428571417,}, - [4]={["y"]=384228.57142858,["x"]=6867.7142857132,}, - [5]={["y"]=385891.14285715,["x"]=8416.5714285703,}, - }, - [2] = { - [1]={["y"]=386714.85714286,["x"]=6674.857142856,}, - [2]={["y"]=386757.71428572,["x"]=6627.7142857132,}, - [3]={["y"]=389028.57142858,["x"]=8741.4285714275,}, - [4]={["y"]=388981.71428572,["x"]=8790.5714285703,}, - [5]={["y"]=386714.57142858,["x"]=6674.5714285703,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Krymsk = { - PointsBoundary = { - [1]={["y"]=293338.00000001,["x"]=-7575.4285714297,}, - [2]={["y"]=295199.42857144,["x"]=-5434.0000000011,}, - [3]={["y"]=295595.14285715,["x"]=-6239.7142857154,}, - [4]={["y"]=294152.2857143,["x"]=-8325.4285714297,}, - [5]={["y"]=293345.14285715,["x"]=-7596.8571428582,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=293522.00000001,["x"]=-7567.4285714297,}, - [2]={["y"]=293578.57142858,["x"]=-7616.0000000011,}, - [3]={["y"]=295246.00000001,["x"]=-5591.142857144,}, - [4]={["y"]=295187.71428573,["x"]=-5546.0000000011,}, - [5]={["y"]=293523.14285715,["x"]=-7568.2857142868,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Kutaisi = { - PointsBoundary = { - [1]={["y"]=682087.42857143,["x"]=-284512.85714286,}, - [2]={["y"]=685387.42857143,["x"]=-283662.85714286,}, - [3]={["y"]=685294.57142857,["x"]=-284977.14285715,}, - [4]={["y"]=682744.57142857,["x"]=-286505.71428572,}, - [5]={["y"]=682094.57142857,["x"]=-284527.14285715,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=682638,["x"]=-285202.28571429,}, - [2]={["y"]=685050.28571429,["x"]=-284507.42857144,}, - [3]={["y"]=685068.85714286,["x"]=-284578.85714286,}, - [4]={["y"]=682657.42857143,["x"]=-285264.28571429,}, - [5]={["y"]=682638.28571429,["x"]=-285202.85714286,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - MaykopKhanskaya = { - PointsBoundary = { - [1]={["y"]=456876.28571429,["x"]=-27665.42857143,}, - [2]={["y"]=457800,["x"]=-28392.857142858,}, - [3]={["y"]=459368.57142857,["x"]=-26378.571428573,}, - [4]={["y"]=459425.71428572,["x"]=-25242.857142858,}, - [5]={["y"]=458961.42857143,["x"]=-24964.285714287,}, - [6]={["y"]=456878.57142857,["x"]=-27667.714285715,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=457005.42857143,["x"]=-27668.000000001,}, - [2]={["y"]=459028.85714286,["x"]=-25168.857142858,}, - [3]={["y"]=459082.57142857,["x"]=-25216.857142858,}, - [4]={["y"]=457060,["x"]=-27714.285714287,}, - [5]={["y"]=457004.57142857,["x"]=-27669.714285715,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - MineralnyeVody = { - PointsBoundary = { - [1]={["y"]=703857.14285714,["x"]=-50226.000000002,}, - [2]={["y"]=707385.71428571,["x"]=-51911.714285716,}, - [3]={["y"]=707595.71428571,["x"]=-51434.857142859,}, - [4]={["y"]=707900,["x"]=-51568.857142859,}, - [5]={["y"]=707542.85714286,["x"]=-52326.000000002,}, - [6]={["y"]=706628.57142857,["x"]=-52568.857142859,}, - [7]={["y"]=705142.85714286,["x"]=-51790.285714288,}, - [8]={["y"]=703678.57142857,["x"]=-50611.714285716,}, - [9]={["y"]=703857.42857143,["x"]=-50226.857142859,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=703904,["x"]=-50352.571428573,}, - [2]={["y"]=707596.28571429,["x"]=-52094.571428573,}, - [3]={["y"]=707560.57142858,["x"]=-52161.714285716,}, - [4]={["y"]=703871.71428572,["x"]=-50420.571428573,}, - [5]={["y"]=703902,["x"]=-50352.000000002,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Mozdok = { - PointsBoundary = { - [1]={["y"]=832123.42857143,["x"]=-83608.571428573,}, - [2]={["y"]=835916.28571429,["x"]=-83144.285714288,}, - [3]={["y"]=835474.28571429,["x"]=-84170.571428573,}, - [4]={["y"]=832911.42857143,["x"]=-84470.571428573,}, - [5]={["y"]=832487.71428572,["x"]=-85565.714285716,}, - [6]={["y"]=831573.42857143,["x"]=-85351.42857143,}, - [7]={["y"]=832123.71428572,["x"]=-83610.285714288,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=832201.14285715,["x"]=-83699.428571431,}, - [2]={["y"]=832212.57142857,["x"]=-83780.571428574,}, - [3]={["y"]=835730.28571429,["x"]=-83335.714285717,}, - [4]={["y"]=835718.85714286,["x"]=-83246.571428574,}, - [5]={["y"]=832200.57142857,["x"]=-83700.000000002,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Nalchik = { - PointsBoundary = { - [1]={["y"]=759370,["x"]=-125502.85714286,}, - [2]={["y"]=761384.28571429,["x"]=-124177.14285714,}, - [3]={["y"]=761472.85714286,["x"]=-124325.71428572,}, - [4]={["y"]=761092.85714286,["x"]=-125048.57142857,}, - [5]={["y"]=760295.71428572,["x"]=-125685.71428572,}, - [6]={["y"]=759444.28571429,["x"]=-125734.28571429,}, - [7]={["y"]=759375.71428572,["x"]=-125511.42857143,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=759454.28571429,["x"]=-125551.42857143,}, - [2]={["y"]=759492.85714286,["x"]=-125610.85714286,}, - [3]={["y"]=761406.28571429,["x"]=-124304.28571429,}, - [4]={["y"]=761361.14285714,["x"]=-124239.71428572,}, - [5]={["y"]=759456,["x"]=-125552.57142857,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Novorossiysk = { - PointsBoundary = { - [1]={["y"]=278677.71428573,["x"]=-41656.571428572,}, - [2]={["y"]=278446.2857143,["x"]=-41453.714285715,}, - [3]={["y"]=278989.14285716,["x"]=-40188.000000001,}, - [4]={["y"]=279717.71428573,["x"]=-39968.000000001,}, - [5]={["y"]=280020.57142859,["x"]=-40208.000000001,}, - [6]={["y"]=278674.85714287,["x"]=-41660.857142858,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=278673.14285716,["x"]=-41615.142857144,}, - [2]={["y"]=278625.42857144,["x"]=-41570.571428572,}, - [3]={["y"]=279835.42857144,["x"]=-40226.000000001,}, - [4]={["y"]=279882.2857143,["x"]=-40270.000000001,}, - [5]={["y"]=278672.00000001,["x"]=-41614.857142858,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - SenakiKolkhi = { - PointsBoundary = { - [1]={["y"]=646036.57142857,["x"]=-281778.85714286,}, - [2]={["y"]=646045.14285714,["x"]=-281191.71428571,}, - [3]={["y"]=647032.28571429,["x"]=-280598.85714285,}, - [4]={["y"]=647669.42857143,["x"]=-281273.14285714,}, - [5]={["y"]=648323.71428571,["x"]=-281370.28571428,}, - [6]={["y"]=648520.85714286,["x"]=-281978.85714285,}, - [7]={["y"]=646039.42857143,["x"]=-281783.14285714,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=646060.85714285,["x"]=-281736,}, - [2]={["y"]=646056.57142857,["x"]=-281631.71428571,}, - [3]={["y"]=648442.28571428,["x"]=-281840.28571428,}, - [4]={["y"]=648432.28571428,["x"]=-281918.85714286,}, - [5]={["y"]=646063.71428571,["x"]=-281738.85714286,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - SochiAdler = { - PointsBoundary = { - [1]={["y"]=460642.28571428,["x"]=-164861.71428571,}, - [2]={["y"]=462820.85714285,["x"]=-163368.85714286,}, - [3]={["y"]=463649.42857142,["x"]=-163340.28571429,}, - [4]={["y"]=463835.14285714,["x"]=-164040.28571429,}, - [5]={["y"]=462535.14285714,["x"]=-165654.57142857,}, - [6]={["y"]=460678,["x"]=-165247.42857143,}, - [7]={["y"]=460635.14285714,["x"]=-164876,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=460831.42857143,["x"]=-165180,}, - [2]={["y"]=460878.57142857,["x"]=-165257.14285714,}, - [3]={["y"]=463663.71428571,["x"]=-163793.14285714,}, - [4]={["y"]=463612.28571428,["x"]=-163697.42857143,}, - [5]={["y"]=460831.42857143,["x"]=-165177.14285714,}, - }, - [2] = { - [1]={["y"]=460831.42857143,["x"]=-165180,}, - [2]={["y"]=460878.57142857,["x"]=-165257.14285714,}, - [3]={["y"]=463663.71428571,["x"]=-163793.14285714,}, - [4]={["y"]=463612.28571428,["x"]=-163697.42857143,}, - [5]={["y"]=460831.42857143,["x"]=-165177.14285714,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Soganlug = { - PointsBoundary = { - [1]={["y"]=894530.85714286,["x"]=-316928.28571428,}, - [2]={["y"]=896422.28571428,["x"]=-318622.57142857,}, - [3]={["y"]=896090.85714286,["x"]=-318934,}, - [4]={["y"]=894019.42857143,["x"]=-317119.71428571,}, - [5]={["y"]=894533.71428571,["x"]=-316925.42857143,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=894525.71428571,["x"]=-316964,}, - [2]={["y"]=896363.14285714,["x"]=-318634.28571428,}, - [3]={["y"]=896299.14285714,["x"]=-318702.85714286,}, - [4]={["y"]=894464,["x"]=-317031.71428571,}, - [5]={["y"]=894524.57142857,["x"]=-316963.71428571,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - SukhumiBabushara = { - PointsBoundary = { - [1]={["y"]=562541.14285714,["x"]=-219852.28571429,}, - [2]={["y"]=562691.14285714,["x"]=-219395.14285714,}, - [3]={["y"]=564326.85714286,["x"]=-219523.71428571,}, - [4]={["y"]=566262.57142857,["x"]=-221166.57142857,}, - [5]={["y"]=566069.71428571,["x"]=-221580.85714286,}, - [6]={["y"]=562534,["x"]=-219873.71428571,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=562684,["x"]=-219779.71428571,}, - [2]={["y"]=562717.71428571,["x"]=-219718,}, - [3]={["y"]=566046.85714286,["x"]=-221376.57142857,}, - [4]={["y"]=566012.28571428,["x"]=-221446.57142857,}, - [5]={["y"]=562684.57142857,["x"]=-219782.57142857,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - TbilisiLochini = { - PointsBoundary = { - [1]={["y"]=895172.85714286,["x"]=-314667.42857143,}, - [2]={["y"]=895337.42857143,["x"]=-314143.14285714,}, - [3]={["y"]=895990.28571429,["x"]=-314036,}, - [4]={["y"]=897730.28571429,["x"]=-315284.57142857,}, - [5]={["y"]=897901.71428571,["x"]=-316284.57142857,}, - [6]={["y"]=897684.57142857,["x"]=-316618.85714286,}, - [7]={["y"]=895173.14285714,["x"]=-314667.42857143,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=895261.14285715,["x"]=-314652.28571428,}, - [2]={["y"]=897654.57142857,["x"]=-316523.14285714,}, - [3]={["y"]=897711.71428571,["x"]=-316450.28571429,}, - [4]={["y"]=895327.42857143,["x"]=-314568.85714286,}, - [5]={["y"]=895261.71428572,["x"]=-314656,}, - }, - [2] = { - [1]={["y"]=895605.71428572,["x"]=-314724.57142857,}, - [2]={["y"]=897639.71428572,["x"]=-316148,}, - [3]={["y"]=897683.42857143,["x"]=-316087.14285714,}, - [4]={["y"]=895650,["x"]=-314660,}, - [5]={["y"]=895606,["x"]=-314724.85714286,} - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Vaziani = { - PointsBoundary = { - [1]={["y"]=902122,["x"]=-318163.71428572,}, - [2]={["y"]=902678.57142857,["x"]=-317594,}, - [3]={["y"]=903275.71428571,["x"]=-317405.42857143,}, - [4]={["y"]=903418.57142857,["x"]=-317891.14285714,}, - [5]={["y"]=904292.85714286,["x"]=-318748.28571429,}, - [6]={["y"]=904542,["x"]=-319740.85714286,}, - [7]={["y"]=904042,["x"]=-320166.57142857,}, - [8]={["y"]=902121.42857143,["x"]=-318164.85714286,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=902239.14285714,["x"]=-318190.85714286,}, - [2]={["y"]=904014.28571428,["x"]=-319994.57142857,}, - [3]={["y"]=904064.85714285,["x"]=-319945.14285715,}, - [4]={["y"]=902294.57142857,["x"]=-318146,}, - [5]={["y"]=902247.71428571,["x"]=-318190.85714286,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - }, -} - ---- Creates a new AIRBASEPOLICE_CAUCASUS object. --- @param #AIRBASEPOLICE_CAUCASUS self --- @param SetClient A SET_CLIENT object that will contain the CLIENT objects to be monitored if they follow the rules of the airbase. --- @return #AIRBASEPOLICE_CAUCASUS self -function AIRBASEPOLICE_CAUCASUS:New( SetClient ) - - -- Inherits from BASE - local self = BASE:Inherit( self, AIRBASEPOLICE_BASE:New( SetClient, self.Airbases ) ) - - -- -- AnapaVityazevo - -- local AnapaVityazevoBoundary = GROUP:FindByName( "AnapaVityazevo Boundary" ) - -- self.Airbases.AnapaVityazevo.ZoneBoundary = ZONE_POLYGON:New( "AnapaVityazevo Boundary", AnapaVityazevoBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local AnapaVityazevoRunway1 = GROUP:FindByName( "AnapaVityazevo Runway 1" ) - -- self.Airbases.AnapaVityazevo.ZoneRunways[1] = ZONE_POLYGON:New( "AnapaVityazevo Runway 1", AnapaVityazevoRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Batumi - -- local BatumiBoundary = GROUP:FindByName( "Batumi Boundary" ) - -- self.Airbases.Batumi.ZoneBoundary = ZONE_POLYGON:New( "Batumi Boundary", BatumiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local BatumiRunway1 = GROUP:FindByName( "Batumi Runway 1" ) - -- self.Airbases.Batumi.ZoneRunways[1] = ZONE_POLYGON:New( "Batumi Runway 1", BatumiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Beslan - -- local BeslanBoundary = GROUP:FindByName( "Beslan Boundary" ) - -- self.Airbases.Beslan.ZoneBoundary = ZONE_POLYGON:New( "Beslan Boundary", BeslanBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local BeslanRunway1 = GROUP:FindByName( "Beslan Runway 1" ) - -- self.Airbases.Beslan.ZoneRunways[1] = ZONE_POLYGON:New( "Beslan Runway 1", BeslanRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Gelendzhik - -- local GelendzhikBoundary = GROUP:FindByName( "Gelendzhik Boundary" ) - -- self.Airbases.Gelendzhik.ZoneBoundary = ZONE_POLYGON:New( "Gelendzhik Boundary", GelendzhikBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local GelendzhikRunway1 = GROUP:FindByName( "Gelendzhik Runway 1" ) - -- self.Airbases.Gelendzhik.ZoneRunways[1] = ZONE_POLYGON:New( "Gelendzhik Runway 1", GelendzhikRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Gudauta - -- local GudautaBoundary = GROUP:FindByName( "Gudauta Boundary" ) - -- self.Airbases.Gudauta.ZoneBoundary = ZONE_POLYGON:New( "Gudauta Boundary", GudautaBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local GudautaRunway1 = GROUP:FindByName( "Gudauta Runway 1" ) - -- self.Airbases.Gudauta.ZoneRunways[1] = ZONE_POLYGON:New( "Gudauta Runway 1", GudautaRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Kobuleti - -- local KobuletiBoundary = GROUP:FindByName( "Kobuleti Boundary" ) - -- self.Airbases.Kobuleti.ZoneBoundary = ZONE_POLYGON:New( "Kobuleti Boundary", KobuletiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local KobuletiRunway1 = GROUP:FindByName( "Kobuleti Runway 1" ) - -- self.Airbases.Kobuleti.ZoneRunways[1] = ZONE_POLYGON:New( "Kobuleti Runway 1", KobuletiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- KrasnodarCenter - -- local KrasnodarCenterBoundary = GROUP:FindByName( "KrasnodarCenter Boundary" ) - -- self.Airbases.KrasnodarCenter.ZoneBoundary = ZONE_POLYGON:New( "KrasnodarCenter Boundary", KrasnodarCenterBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local KrasnodarCenterRunway1 = GROUP:FindByName( "KrasnodarCenter Runway 1" ) - -- self.Airbases.KrasnodarCenter.ZoneRunways[1] = ZONE_POLYGON:New( "KrasnodarCenter Runway 1", KrasnodarCenterRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- KrasnodarPashkovsky - -- local KrasnodarPashkovskyBoundary = GROUP:FindByName( "KrasnodarPashkovsky Boundary" ) - -- self.Airbases.KrasnodarPashkovsky.ZoneBoundary = ZONE_POLYGON:New( "KrasnodarPashkovsky Boundary", KrasnodarPashkovskyBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local KrasnodarPashkovskyRunway1 = GROUP:FindByName( "KrasnodarPashkovsky Runway 1" ) - -- self.Airbases.KrasnodarPashkovsky.ZoneRunways[1] = ZONE_POLYGON:New( "KrasnodarPashkovsky Runway 1", KrasnodarPashkovskyRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- local KrasnodarPashkovskyRunway2 = GROUP:FindByName( "KrasnodarPashkovsky Runway 2" ) - -- self.Airbases.KrasnodarPashkovsky.ZoneRunways[2] = ZONE_POLYGON:New( "KrasnodarPashkovsky Runway 2", KrasnodarPashkovskyRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Krymsk - -- local KrymskBoundary = GROUP:FindByName( "Krymsk Boundary" ) - -- self.Airbases.Krymsk.ZoneBoundary = ZONE_POLYGON:New( "Krymsk Boundary", KrymskBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local KrymskRunway1 = GROUP:FindByName( "Krymsk Runway 1" ) - -- self.Airbases.Krymsk.ZoneRunways[1] = ZONE_POLYGON:New( "Krymsk Runway 1", KrymskRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Kutaisi - -- local KutaisiBoundary = GROUP:FindByName( "Kutaisi Boundary" ) - -- self.Airbases.Kutaisi.ZoneBoundary = ZONE_POLYGON:New( "Kutaisi Boundary", KutaisiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local KutaisiRunway1 = GROUP:FindByName( "Kutaisi Runway 1" ) - -- self.Airbases.Kutaisi.ZoneRunways[1] = ZONE_POLYGON:New( "Kutaisi Runway 1", KutaisiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- MaykopKhanskaya - -- local MaykopKhanskayaBoundary = GROUP:FindByName( "MaykopKhanskaya Boundary" ) - -- self.Airbases.MaykopKhanskaya.ZoneBoundary = ZONE_POLYGON:New( "MaykopKhanskaya Boundary", MaykopKhanskayaBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local MaykopKhanskayaRunway1 = GROUP:FindByName( "MaykopKhanskaya Runway 1" ) - -- self.Airbases.MaykopKhanskaya.ZoneRunways[1] = ZONE_POLYGON:New( "MaykopKhanskaya Runway 1", MaykopKhanskayaRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- MineralnyeVody - -- local MineralnyeVodyBoundary = GROUP:FindByName( "MineralnyeVody Boundary" ) - -- self.Airbases.MineralnyeVody.ZoneBoundary = ZONE_POLYGON:New( "MineralnyeVody Boundary", MineralnyeVodyBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local MineralnyeVodyRunway1 = GROUP:FindByName( "MineralnyeVody Runway 1" ) - -- self.Airbases.MineralnyeVody.ZoneRunways[1] = ZONE_POLYGON:New( "MineralnyeVody Runway 1", MineralnyeVodyRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Mozdok - -- local MozdokBoundary = GROUP:FindByName( "Mozdok Boundary" ) - -- self.Airbases.Mozdok.ZoneBoundary = ZONE_POLYGON:New( "Mozdok Boundary", MozdokBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local MozdokRunway1 = GROUP:FindByName( "Mozdok Runway 1" ) - -- self.Airbases.Mozdok.ZoneRunways[1] = ZONE_POLYGON:New( "Mozdok Runway 1", MozdokRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Nalchik - -- local NalchikBoundary = GROUP:FindByName( "Nalchik Boundary" ) - -- self.Airbases.Nalchik.ZoneBoundary = ZONE_POLYGON:New( "Nalchik Boundary", NalchikBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local NalchikRunway1 = GROUP:FindByName( "Nalchik Runway 1" ) - -- self.Airbases.Nalchik.ZoneRunways[1] = ZONE_POLYGON:New( "Nalchik Runway 1", NalchikRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Novorossiysk - -- local NovorossiyskBoundary = GROUP:FindByName( "Novorossiysk Boundary" ) - -- self.Airbases.Novorossiysk.ZoneBoundary = ZONE_POLYGON:New( "Novorossiysk Boundary", NovorossiyskBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local NovorossiyskRunway1 = GROUP:FindByName( "Novorossiysk Runway 1" ) - -- self.Airbases.Novorossiysk.ZoneRunways[1] = ZONE_POLYGON:New( "Novorossiysk Runway 1", NovorossiyskRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- SenakiKolkhi - -- local SenakiKolkhiBoundary = GROUP:FindByName( "SenakiKolkhi Boundary" ) - -- self.Airbases.SenakiKolkhi.ZoneBoundary = ZONE_POLYGON:New( "SenakiKolkhi Boundary", SenakiKolkhiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local SenakiKolkhiRunway1 = GROUP:FindByName( "SenakiKolkhi Runway 1" ) - -- self.Airbases.SenakiKolkhi.ZoneRunways[1] = ZONE_POLYGON:New( "SenakiKolkhi Runway 1", SenakiKolkhiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- SochiAdler - -- local SochiAdlerBoundary = GROUP:FindByName( "SochiAdler Boundary" ) - -- self.Airbases.SochiAdler.ZoneBoundary = ZONE_POLYGON:New( "SochiAdler Boundary", SochiAdlerBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local SochiAdlerRunway1 = GROUP:FindByName( "SochiAdler Runway 1" ) - -- self.Airbases.SochiAdler.ZoneRunways[1] = ZONE_POLYGON:New( "SochiAdler Runway 1", SochiAdlerRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- local SochiAdlerRunway2 = GROUP:FindByName( "SochiAdler Runway 2" ) - -- self.Airbases.SochiAdler.ZoneRunways[2] = ZONE_POLYGON:New( "SochiAdler Runway 2", SochiAdlerRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Soganlug - -- local SoganlugBoundary = GROUP:FindByName( "Soganlug Boundary" ) - -- self.Airbases.Soganlug.ZoneBoundary = ZONE_POLYGON:New( "Soganlug Boundary", SoganlugBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local SoganlugRunway1 = GROUP:FindByName( "Soganlug Runway 1" ) - -- self.Airbases.Soganlug.ZoneRunways[1] = ZONE_POLYGON:New( "Soganlug Runway 1", SoganlugRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- SukhumiBabushara - -- local SukhumiBabusharaBoundary = GROUP:FindByName( "SukhumiBabushara Boundary" ) - -- self.Airbases.SukhumiBabushara.ZoneBoundary = ZONE_POLYGON:New( "SukhumiBabushara Boundary", SukhumiBabusharaBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local SukhumiBabusharaRunway1 = GROUP:FindByName( "SukhumiBabushara Runway 1" ) - -- self.Airbases.SukhumiBabushara.ZoneRunways[1] = ZONE_POLYGON:New( "SukhumiBabushara Runway 1", SukhumiBabusharaRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- TbilisiLochini - -- local TbilisiLochiniBoundary = GROUP:FindByName( "TbilisiLochini Boundary" ) - -- self.Airbases.TbilisiLochini.ZoneBoundary = ZONE_POLYGON:New( "TbilisiLochini Boundary", TbilisiLochiniBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local TbilisiLochiniRunway1 = GROUP:FindByName( "TbilisiLochini Runway 1" ) - -- self.Airbases.TbilisiLochini.ZoneRunways[1] = ZONE_POLYGON:New( "TbilisiLochini Runway 1", TbilisiLochiniRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- local TbilisiLochiniRunway2 = GROUP:FindByName( "TbilisiLochini Runway 2" ) - -- self.Airbases.TbilisiLochini.ZoneRunways[2] = ZONE_POLYGON:New( "TbilisiLochini Runway 2", TbilisiLochiniRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - -- -- Vaziani - -- local VazianiBoundary = GROUP:FindByName( "Vaziani Boundary" ) - -- self.Airbases.Vaziani.ZoneBoundary = ZONE_POLYGON:New( "Vaziani Boundary", VazianiBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local VazianiRunway1 = GROUP:FindByName( "Vaziani Runway 1" ) - -- self.Airbases.Vaziani.ZoneRunways[1] = ZONE_POLYGON:New( "Vaziani Runway 1", VazianiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - -- - -- - -- - - - -- Template - -- local TemplateBoundary = GROUP:FindByName( "Template Boundary" ) - -- self.Airbases.Template.ZoneBoundary = ZONE_POLYGON:New( "Template Boundary", TemplateBoundary ):SmokeZone(SMOKECOLOR.White):Flush() - -- - -- local TemplateRunway1 = GROUP:FindByName( "Template Runway 1" ) - -- self.Airbases.Template.ZoneRunways[1] = ZONE_POLYGON:New( "Template Runway 1", TemplateRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() - - return self - -end - - - - ---- @type AIRBASEPOLICE_NEVADA --- @extends Functional.AirbasePolice#AIRBASEPOLICE_BASE -AIRBASEPOLICE_NEVADA = { - ClassName = "AIRBASEPOLICE_NEVADA", - Airbases = { - Nellis = { - PointsBoundary = { - [1]={["y"]=-17814.714285714,["x"]=-399823.14285714,}, - [2]={["y"]=-16875.857142857,["x"]=-398763.14285714,}, - [3]={["y"]=-16251.571428571,["x"]=-398988.85714286,}, - [4]={["y"]=-16163,["x"]=-398693.14285714,}, - [5]={["y"]=-16328.714285714,["x"]=-398034.57142857,}, - [6]={["y"]=-15943,["x"]=-397571.71428571,}, - [7]={["y"]=-15711.571428571,["x"]=-397551.71428571,}, - [8]={["y"]=-15748.714285714,["x"]=-396806,}, - [9]={["y"]=-16288.714285714,["x"]=-396517.42857143,}, - [10]={["y"]=-16751.571428571,["x"]=-396308.85714286,}, - [11]={["y"]=-17263,["x"]=-396234.57142857,}, - [12]={["y"]=-17577.285714286,["x"]=-396640.28571429,}, - [13]={["y"]=-17614.428571429,["x"]=-397400.28571429,}, - [14]={["y"]=-19405.857142857,["x"]=-399428.85714286,}, - [15]={["y"]=-19234.428571429,["x"]=-399683.14285714,}, - [16]={["y"]=-18708.714285714,["x"]=-399408.85714286,}, - [17]={["y"]=-18397.285714286,["x"]=-399657.42857143,}, - [18]={["y"]=-17814.428571429,["x"]=-399823.42857143,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=-18687,["x"]=-399380.28571429,}, - [2]={["y"]=-18620.714285714,["x"]=-399436.85714286,}, - [3]={["y"]=-16217.857142857,["x"]=-396596.85714286,}, - [4]={["y"]=-16300.142857143,["x"]=-396530,}, - [5]={["y"]=-18687,["x"]=-399380.85714286,}, - }, - [2] = { - [1]={["y"]=-18451.571428572,["x"]=-399580.57142857,}, - [2]={["y"]=-18392.142857143,["x"]=-399628.57142857,}, - [3]={["y"]=-16011,["x"]=-396806.85714286,}, - [4]={["y"]=-16074.714285714,["x"]=-396751.71428572,}, - [5]={["y"]=-18451.571428572,["x"]=-399580.85714285,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - McCarran = { - PointsBoundary = { - [1]={["y"]=-29455.285714286,["x"]=-416277.42857142,}, - [2]={["y"]=-28860.142857143,["x"]=-416492,}, - [3]={["y"]=-25044.428571429,["x"]=-416344.85714285,}, - [4]={["y"]=-24580.142857143,["x"]=-415959.14285714,}, - [5]={["y"]=-25073,["x"]=-415630.57142857,}, - [6]={["y"]=-25087.285714286,["x"]=-415130.57142857,}, - [7]={["y"]=-25830.142857143,["x"]=-414866.28571428,}, - [8]={["y"]=-26658.714285715,["x"]=-414880.57142857,}, - [9]={["y"]=-26973,["x"]=-415273.42857142,}, - [10]={["y"]=-27380.142857143,["x"]=-415187.71428571,}, - [11]={["y"]=-27715.857142857,["x"]=-414144.85714285,}, - [12]={["y"]=-27551.571428572,["x"]=-413473.42857142,}, - [13]={["y"]=-28630.142857143,["x"]=-413201.99999999,}, - [14]={["y"]=-29494.428571429,["x"]=-415437.71428571,}, - [15]={["y"]=-29455.571428572,["x"]=-416277.71428571,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=-29408.428571429,["x"]=-416016.28571428,}, - [2]={["y"]=-29408.142857144,["x"]=-416105.42857142,}, - [3]={["y"]=-24680.714285715,["x"]=-416003.14285713,}, - [4]={["y"]=-24681.857142858,["x"]=-415926.57142856,}, - [5]={["y"]=-29408.42857143,["x"]=-416016.57142856,}, - }, - [2] = { - [1]={["y"]=-28575.571428572,["x"]=-416303.14285713,}, - [2]={["y"]=-28575.571428572,["x"]=-416382.57142856,}, - [3]={["y"]=-25111.000000001,["x"]=-416309.7142857,}, - [4]={["y"]=-25111.000000001,["x"]=-416249.14285713,}, - [5]={["y"]=-28575.571428572,["x"]=-416303.7142857,}, - }, - [3] = { - [1]={["y"]=-29331.000000001,["x"]=-416275.42857141,}, - [2]={["y"]=-29259.000000001,["x"]=-416306.85714284,}, - [3]={["y"]=-28005.571428572,["x"]=-413449.7142857,}, - [4]={["y"]=-28068.714285715,["x"]=-413422.85714284,}, - [5]={["y"]=-29331.000000001,["x"]=-416275.7142857,}, - }, - [4] = { - [1]={["y"]=-29073.285714286,["x"]=-416386.57142856,}, - [2]={["y"]=-28997.285714286,["x"]=-416417.42857141,}, - [3]={["y"]=-27697.571428572,["x"]=-413464.57142856,}, - [4]={["y"]=-27767.857142858,["x"]=-413434.28571427,}, - [5]={["y"]=-29073.000000001,["x"]=-416386.85714284,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - Creech = { - PointsBoundary = { - [1]={["y"]=-74522.714285715,["x"]=-360887.99999998,}, - [2]={["y"]=-74197,["x"]=-360556.57142855,}, - [3]={["y"]=-74402.714285715,["x"]=-359639.42857141,}, - [4]={["y"]=-74637,["x"]=-359279.42857141,}, - [5]={["y"]=-75759.857142857,["x"]=-359005.14285712,}, - [6]={["y"]=-75834.142857143,["x"]=-359045.14285712,}, - [7]={["y"]=-75902.714285714,["x"]=-359782.28571427,}, - [8]={["y"]=-76099.857142857,["x"]=-360399.42857141,}, - [9]={["y"]=-77314.142857143,["x"]=-360219.42857141,}, - [10]={["y"]=-77728.428571429,["x"]=-360445.14285713,}, - [11]={["y"]=-77585.571428571,["x"]=-360585.14285713,}, - [12]={["y"]=-76471.285714286,["x"]=-360819.42857141,}, - [13]={["y"]=-76325.571428571,["x"]=-360942.28571427,}, - [14]={["y"]=-74671.857142857,["x"]=-360927.7142857,}, - [15]={["y"]=-74522.714285714,["x"]=-360888.85714284,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=-74237.571428571,["x"]=-360591.7142857,}, - [2]={["y"]=-74234.428571429,["x"]=-360493.71428571,}, - [3]={["y"]=-77605.285714286,["x"]=-360399.14285713,}, - [4]={["y"]=-77608.714285715,["x"]=-360498.85714285,}, - [5]={["y"]=-74237.857142857,["x"]=-360591.7142857,}, - }, - [2] = { - [1]={["y"]=-75807.571428572,["x"]=-359073.42857142,}, - [2]={["y"]=-74770.142857144,["x"]=-360581.71428571,}, - [3]={["y"]=-74641.285714287,["x"]=-360585.42857142,}, - [4]={["y"]=-75734.142857144,["x"]=-359023.14285714,}, - [5]={["y"]=-75807.285714287,["x"]=-359073.42857142,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - GroomLake = { - PointsBoundary = { - [1]={["y"]=-88916.714285714,["x"]=-289102.28571425,}, - [2]={["y"]=-87023.571428572,["x"]=-290388.57142857,}, - [3]={["y"]=-85916.428571429,["x"]=-290674.28571428,}, - [4]={["y"]=-87645.000000001,["x"]=-286567.14285714,}, - [5]={["y"]=-88380.714285715,["x"]=-286388.57142857,}, - [6]={["y"]=-89670.714285715,["x"]=-283524.28571428,}, - [7]={["y"]=-89797.857142858,["x"]=-283567.14285714,}, - [8]={["y"]=-88635.000000001,["x"]=-286749.99999999,}, - [9]={["y"]=-89177.857142858,["x"]=-287207.14285714,}, - [10]={["y"]=-89092.142857144,["x"]=-288892.85714285,}, - [11]={["y"]=-88917.000000001,["x"]=-289102.85714285,}, - }, - PointsRunways = { - [1] = { - [1]={["y"]=-86039.000000001,["x"]=-290606.28571428,}, - [2]={["y"]=-85965.285714287,["x"]=-290573.99999999,}, - [3]={["y"]=-87692.714285715,["x"]=-286634.85714285,}, - [4]={["y"]=-87756.714285715,["x"]=-286663.99999999,}, - [5]={["y"]=-86038.714285715,["x"]=-290606.85714285,}, - }, - [2] = { - [1]={["y"]=-86808.428571429,["x"]=-290375.7142857,}, - [2]={["y"]=-86732.714285715,["x"]=-290344.28571427,}, - [3]={["y"]=-89672.714285714,["x"]=-283546.57142855,}, - [4]={["y"]=-89772.142857143,["x"]=-283587.71428569,}, - [5]={["y"]=-86808.142857143,["x"]=-290375.7142857,}, - }, - }, - ZoneBoundary = {}, - ZoneRunways = {}, - MaximumSpeed = 50, - }, - }, -} - ---- Creates a new AIRBASEPOLICE_NEVADA object. --- @param #AIRBASEPOLICE_NEVADA self --- @param SetClient A SET_CLIENT object that will contain the CLIENT objects to be monitored if they follow the rules of the airbase. --- @return #AIRBASEPOLICE_NEVADA self -function AIRBASEPOLICE_NEVADA:New( SetClient ) - - -- Inherits from BASE - local self = BASE:Inherit( self, AIRBASEPOLICE_BASE:New( SetClient, self.Airbases ) ) - --- -- Nellis --- local NellisBoundary = GROUP:FindByName( "Nellis Boundary" ) --- self.Airbases.Nellis.ZoneBoundary = ZONE_POLYGON:New( "Nellis Boundary", NellisBoundary ):SmokeZone(SMOKECOLOR.White):Flush() --- --- local NellisRunway1 = GROUP:FindByName( "Nellis Runway 1" ) --- self.Airbases.Nellis.ZoneRunways[1] = ZONE_POLYGON:New( "Nellis Runway 1", NellisRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- local NellisRunway2 = GROUP:FindByName( "Nellis Runway 2" ) --- self.Airbases.Nellis.ZoneRunways[2] = ZONE_POLYGON:New( "Nellis Runway 2", NellisRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- -- McCarran --- local McCarranBoundary = GROUP:FindByName( "McCarran Boundary" ) --- self.Airbases.McCarran.ZoneBoundary = ZONE_POLYGON:New( "McCarran Boundary", McCarranBoundary ):SmokeZone(SMOKECOLOR.White):Flush() --- --- local McCarranRunway1 = GROUP:FindByName( "McCarran Runway 1" ) --- self.Airbases.McCarran.ZoneRunways[1] = ZONE_POLYGON:New( "McCarran Runway 1", McCarranRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- local McCarranRunway2 = GROUP:FindByName( "McCarran Runway 2" ) --- self.Airbases.McCarran.ZoneRunways[2] = ZONE_POLYGON:New( "McCarran Runway 2", McCarranRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- local McCarranRunway3 = GROUP:FindByName( "McCarran Runway 3" ) --- self.Airbases.McCarran.ZoneRunways[3] = ZONE_POLYGON:New( "McCarran Runway 3", McCarranRunway3 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- local McCarranRunway4 = GROUP:FindByName( "McCarran Runway 4" ) --- self.Airbases.McCarran.ZoneRunways[4] = ZONE_POLYGON:New( "McCarran Runway 4", McCarranRunway4 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- -- Creech --- local CreechBoundary = GROUP:FindByName( "Creech Boundary" ) --- self.Airbases.Creech.ZoneBoundary = ZONE_POLYGON:New( "Creech Boundary", CreechBoundary ):SmokeZone(SMOKECOLOR.White):Flush() --- --- local CreechRunway1 = GROUP:FindByName( "Creech Runway 1" ) --- self.Airbases.Creech.ZoneRunways[1] = ZONE_POLYGON:New( "Creech Runway 1", CreechRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- local CreechRunway2 = GROUP:FindByName( "Creech Runway 2" ) --- self.Airbases.Creech.ZoneRunways[2] = ZONE_POLYGON:New( "Creech Runway 2", CreechRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- -- Groom Lake --- local GroomLakeBoundary = GROUP:FindByName( "GroomLake Boundary" ) --- self.Airbases.GroomLake.ZoneBoundary = ZONE_POLYGON:New( "GroomLake Boundary", GroomLakeBoundary ):SmokeZone(SMOKECOLOR.White):Flush() --- --- local GroomLakeRunway1 = GROUP:FindByName( "GroomLake Runway 1" ) --- self.Airbases.GroomLake.ZoneRunways[1] = ZONE_POLYGON:New( "GroomLake Runway 1", GroomLakeRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() --- --- local GroomLakeRunway2 = GROUP:FindByName( "GroomLake Runway 2" ) --- self.Airbases.GroomLake.ZoneRunways[2] = ZONE_POLYGON:New( "GroomLake Runway 2", GroomLakeRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() - -end - - - - - - --- **Functional** - DETECTION_ classes model the detection of enemy units by FACs or RECCEs and group them according various methods. --- --- ![Banner Image](..\Presentations\DETECTION\Dia1.JPG) --- --- === --- --- DETECTION classes facilitate the detection of enemy units within the battle zone executed by FACs (Forward Air Controllers) or RECCEs (Reconnassance Units). --- DETECTION uses the in-built detection capabilities of DCS World, but adds new functionalities. --- --- Please watch this [youtube video](https://youtu.be/C7p81dUwP-E) that explains the detection concepts. --- --- --- ### Contributions: --- --- * Mechanist : Early concept of DETECTION_AREAS. --- --- ### Authors: --- --- * FlightControl : Analysis, Design, Programming, Testing --- --- @module Detection - - -do -- DETECTION_BASE - - --- # 1) DETECTION_BASE class, extends @{Fsm#FSM} - -- - -- The DETECTION_BASE class defines the core functions to administer detected objects. - -- The DETECTION_BASE class will detect objects within the battle zone for a list of @{Group}s detecting targets following (a) detection method(s). - -- - -- ## 1.1) DETECTION_BASE constructor - -- - -- Construct a new DETECTION_BASE instance using the @{#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_BASE.InitDetectVisual}(): Detected using Visual. - -- * @{#DETECTION_BASE.InitDetectOptical}(): Detected using Optical. - -- * @{#DETECTION_BASE.InitDetectRadar}(): Detected using Radar. - -- * @{#DETECTION_BASE.InitDetectIRST}(): Detected using IRST. - -- * @{#DETECTION_BASE.InitDetectRWR}(): Detected using RWR. - -- * @{#DETECTION_BASE.InitDetectDLINK}(): Detected using DLINK. - -- - -- ## 1.3) DETECTION_BASE derived classes group the detected units into a DetectedItems[] list - -- - -- DETECTION_BASE derived classes build a list called DetectedItems[], which is essentially a first later - -- of grouping of detected units. Each DetectedItem within the DetectedItems[] list contains - -- a SET_UNIT object that contains the detected units that belong to that group. - -- - -- Derived classes will apply different methods to group the detected units. - -- Examples are per area, per quadrant, per distance, per type. - -- See further the derived DETECTION classes on which grouping methods are currently supported. - -- - -- Various methods exist how to retrieve the grouped items from a DETECTION_BASE derived class: - -- - -- * The method @{Detection#DETECTION_BASE.GetDetectedItems}() retrieves the DetectedItems[] list. - -- * A DetectedItem from the DetectedItems[] list can be retrieved using the method @{Detection#DETECTION_BASE.GetDetectedItem}( DetectedItemIndex ). - -- Note that this method returns a DetectedItem element from the list, that contains a Set variable and further information - -- about the DetectedItem that is set by the DETECTION_BASE derived classes, used to group the DetectedItem. - -- * A DetectedSet from the DetectedItems[] list can be retrieved using the method @{Detection#DETECTION_BASE.GetDetectedSet}( DetectedItemIndex ). - -- This method retrieves the Set from a DetectedItem element from the DetectedItem list (DetectedItems[ DetectedItemIndex ].Set ). - -- - -- ## 1.4) Apply additional Filters to fine-tune the detected objects - -- - -- By default, DCS World will return any object that is in LOS and within "visual reach", or detectable through one of the electronic detection means. - -- That being said, the DCS World detection algorithm can sometimes be unrealistic. - -- Especially for a visual detection, DCS World is able to report within 1 second a detailed detection of a group of 20 units (including types of the units) that are 10 kilometers away, using only visual capabilities. - -- Additionally, trees and other obstacles are not accounted during the DCS World detection. - -- - -- Therefore, an additional (optional) filtering has been built into the DETECTION_BASE class, that can be set for visual detected units. - -- For electronic detection, this filtering is not applied, only for visually detected targets. - -- - -- The following additional filtering can be applied for visual filtering: - -- - -- * A probability factor per kilometer distance. - -- * A probability factor based on the alpha angle between the detected object and the unit detecting. - -- A detection from a higher altitude allows for better detection than when on the ground. - -- * Define a probability factor for "cloudy zones", which are zones where forests or villages are located. In these zones, detection will be much more difficult. - -- The mission designer needs to define these cloudy zones within the mission, and needs to register these zones in the DETECTION_ objects additing a probability factor per zone. - -- - -- I advise however, that, when you first use the DETECTION derived classes, that you don't use these filters. - -- Only when you experience unrealistic behaviour in your missions, these filters could be applied. - -- - -- ### 1.4.1 ) Distance visual detection probability - -- - -- Upon a **visual** detection, the further away a detected object is, the less likely it is to be detected properly. - -- Also, the speed of accurate detection plays a role. - -- - -- A distance probability factor between 0 and 1 can be given, that will model a linear extrapolated probability over 10 km distance. - -- - -- For example, if a probability factor of 0.6 (60%) is given, the extrapolated probabilities over 15 kilometers would like like: - -- 1 km: 96%, 2 km: 92%, 3 km: 88%, 4 km: 84%, 5 km: 80%, 6 km: 76%, 7 km: 72%, 8 km: 68%, 9 km: 64%, 10 km: 60%, 11 km: 56%, 12 km: 52%, 13 km: 48%, 14 km: 44%, 15 km: 40%. - -- - -- Note that based on this probability factor, not only the detection but also the **type** of the unit will be applied! - -- - -- Use the method @{Detection#DETECTION_BASE.SetDistanceProbability}() to set the probability factor upon a 10 km distance. - -- - -- ### 1.4.2 ) Alpha Angle visual detection probability - -- - -- Upon a **visual** detection, the higher the unit is during the detecting process, the more likely the detected unit is to be detected properly. - -- A detection at a 90% alpha angle is the most optimal, a detection at 10% is less and a detection at 0% is less likely to be correct. - -- - -- A probability factor between 0 and 1 can be given, that will model a progressive extrapolated probability if the target would be detected at a 0° angle. - -- - -- For example, if a alpha angle probability factor of 0.7 is given, the extrapolated probabilities of the different angles would look like: - -- 0°: 70%, 10°: 75,21%, 20°: 80,26%, 30°: 85%, 40°: 89,28%, 50°: 92,98%, 60°: 95,98%, 70°: 98,19%, 80°: 99,54%, 90°: 100% - -- - -- Use the method @{Detection#DETECTION_BASE.SetAlphaAngleProbability}() to set the probability factor if 0°. - -- - -- ### 1.4.3 ) Cloudy Zones detection probability - -- - -- Upon a **visual** detection, the more a detected unit is within a cloudy zone, the less likely the detected unit is to be detected successfully. - -- The Cloudy Zones work with the ZONE_BASE derived classes. The mission designer can define within the mission - -- zones that reflect cloudy areas where detected units may not be so easily visually detected. - -- - -- Use the method @{Detection#DETECTION_BASE.SetZoneProbability}() to set for a defined number of zones, the probability factors. - -- - -- Note however, that the more zones are defined to be "cloudy" within a detection, the more performance it will take - -- from the DETECTION_BASE to calculate the presence of the detected unit within each zone. - -- Expecially for ZONE_POLYGON, try to limit the amount of nodes of the polygon! - -- - -- Typically, this kind of filter would be applied for very specific areas were a detection needs to be very realisting for - -- AI not to detect so easily targets within a forrest or village rich area. - -- - -- ## 1.5 ) Accept / Reject detected units - -- - -- DETECTION_BASE can accept or reject successful detections based on the location of the detected object, - -- if it is located in range or located inside or outside of specific zones. - -- - -- ### 1.5.1 ) Detection acceptance of within range limit - -- - -- A range can be set that will limit a successful detection for a unit. - -- Use the method @{Detection#DETECTION_BASE.SetAcceptRange}() to apply a range in meters till where detected units will be accepted. - -- - -- local SetGroup = SET_GROUP:New():FilterPrefixes( "FAC" ):FilterStart() -- Build a SetGroup of Forward Air Controllers. - -- - -- -- Build a detect object. - -- local Detection = DETECTION_BASE:New( SetGroup ) - -- - -- -- This will accept detected units if the range is below 5000 meters. - -- Detection:SetAcceptRange( 5000 ) - -- - -- -- Start the Detection. - -- Detection:Start() - -- - -- - -- ### 1.5.2 ) Detection acceptance if within zone(s). - -- - -- Specific ZONE_BASE object(s) can be given as a parameter, which will only accept a detection if the unit is within the specified ZONE_BASE object(s). - -- Use the method @{Detection#DETECTION_BASE.SetAcceptZones}() will accept detected units if they are within the specified zones. - -- - -- local SetGroup = SET_GROUP:New():FilterPrefixes( "FAC" ):FilterStart() -- Build a SetGroup of Forward Air Controllers. - -- - -- -- Search fo the zones where units are to be accepted. - -- local ZoneAccept1 = ZONE:New( "AcceptZone1" ) - -- local ZoneAccept2 = ZONE:New( "AcceptZone2" ) - -- - -- -- Build a detect object. - -- local Detection = DETECTION_BASE:New( SetGroup ) - -- - -- -- This will accept detected units by Detection when the unit is within ZoneAccept1 OR ZoneAccept2. - -- Detection:SetAcceptZones( { ZoneAccept1, ZoneAccept2 } ) - -- - -- -- Start the Detection. - -- Detection:Start() - -- - -- ### 1.5.3 ) Detection rejectance if within zone(s). - -- - -- Specific ZONE_BASE object(s) can be given as a parameter, which will reject detection if the unit is within the specified ZONE_BASE object(s). - -- Use the method @{Detection#DETECTION_BASE.SetRejectZones}() will reject detected units if they are within the specified zones. - -- An example of how to use the method is shown below. - -- - -- local SetGroup = SET_GROUP:New():FilterPrefixes( "FAC" ):FilterStart() -- Build a SetGroup of Forward Air Controllers. - -- - -- -- Search fo the zones where units are to be rejected. - -- local ZoneReject1 = ZONE:New( "RejectZone1" ) - -- local ZoneReject2 = ZONE:New( "RejectZone2" ) - -- - -- -- Build a detect object. - -- local Detection = DETECTION_BASE:New( SetGroup ) - -- - -- -- This will reject detected units by Detection when the unit is within ZoneReject1 OR ZoneReject2. - -- Detection:SetRejectZones( { ZoneReject1, ZoneReject2 } ) - -- - -- -- Start the Detection. - -- Detection:Start() - -- - -- ## 1.6) DETECTION_BASE is a Finite State Machine - -- - -- Various Events and State Transitions can be tailored using DETECTION_BASE. - -- - -- ### 1.6.1) DETECTION_BASE States - -- - -- * **Detecting**: The detection is running. - -- * **Stopped**: The detection is stopped. - -- - -- ### 1.6.2) DETECTION_BASE Events - -- - -- * **Start**: Start the detection process. - -- * **Detect**: Detect new units. - -- * **Detected**: New units have been detected. - -- * **Stop**: Stop the detection process. - -- - -- @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.Fsm#FSM - DETECTION_BASE = { - ClassName = "DETECTION_BASE", - DetectionSetGroup = nil, - DetectionRange = nil, - DetectedObjects = {}, - DetectionRun = 0, - DetectedObjectsIdentified = {}, - DetectedItems = {}, - } - - --- @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 - - --- @type DETECTION_BASE.DetectedItems - -- @list <#DETECTION_BASE.DetectedItem> - - --- @type DETECTION_BASE.DetectedItem - -- @field Core.Set#SET_UNIT Set - -- @field Core.Set#SET_UNIT Set -- The Set of Units in the detected area. - -- @field Core.Zone#ZONE_UNIT Zone -- The Zone of the detected area. - -- @field #boolean Changed Documents if the detected area has changes. - -- @field #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 ItemID -- 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 constructor. - -- @param #DETECTION_BASE self - -- @param Core.Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Forward Air Controller role. - -- @return #DETECTION_BASE self - function DETECTION_BASE:New( DetectionSetGroup ) - - -- Inherits from BASE - local self = BASE:Inherit( self, FSM:New() ) -- #DETECTION_BASE - - self.DetectedItemCount = 0 - self.DetectedItemMax = 0 - self.DetectedItems = {} - - self.DetectionSetGroup = DetectionSetGroup - - self.DetectionInterval = 30 - - self:InitDetectVisual( true ) - self:InitDetectOptical( false ) - self:InitDetectRadar( false ) - self:InitDetectRWR( false ) - self:InitDetectIRST( false ) - self:InitDetectDLINK( false ) - - -- Create FSM transitions. - - self:SetStartState( "Stopped" ) - self.CountryID = DetectionSetGroup:GetFirst():GetCountry() - - self:AddTransition( "Stopped", "Start", "Detecting") - - --- OnLeave Transition Handler for State Stopped. - -- @function [parent=#DETECTION_BASE] OnLeaveStopped - -- @param #DETECTION_BASE self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnEnter Transition Handler for State Stopped. - -- @function [parent=#DETECTION_BASE] OnEnterStopped - -- @param #DETECTION_BASE self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- OnBefore Transition Handler for Event Start. - -- @function [parent=#DETECTION_BASE] OnBeforeStart - -- @param #DETECTION_BASE self - -- @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=#DETECTION_BASE] OnAfterStart - -- @param #DETECTION_BASE self - -- @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=#DETECTION_BASE] Start - -- @param #DETECTION_BASE self - - --- Asynchronous Event Trigger for Event Start. - -- @function [parent=#DETECTION_BASE] __Start - -- @param #DETECTION_BASE self - -- @param #number Delay The delay in seconds. - - --- OnLeave Transition Handler for State Detecting. - -- @function [parent=#DETECTION_BASE] OnLeaveDetecting - -- @param #DETECTION_BASE self - -- @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 Detecting. - -- @function [parent=#DETECTION_BASE] OnEnterDetecting - -- @param #DETECTION_BASE self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - self:AddTransition( "Detecting", "Detect", "Detecting" ) - self:AddTransition( "Detecting", "DetectionGroup", "Detecting" ) - - --- OnBefore Transition Handler for Event Detect. - -- @function [parent=#DETECTION_BASE] OnBeforeDetect - -- @param #DETECTION_BASE self - -- @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=#DETECTION_BASE] OnAfterDetect - -- @param #DETECTION_BASE self - -- @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=#DETECTION_BASE] Detect - -- @param #DETECTION_BASE self - - --- Asynchronous Event Trigger for Event Detect. - -- @function [parent=#DETECTION_BASE] __Detect - -- @param #DETECTION_BASE self - -- @param #number Delay The delay in seconds. - - - self:AddTransition( "Detecting", "Detected", "Detecting" ) - - --- OnBefore Transition Handler for Event Detected. - -- @function [parent=#DETECTION_BASE] OnBeforeDetected - -- @param #DETECTION_BASE self - -- @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=#DETECTION_BASE] OnAfterDetected - -- @param #DETECTION_BASE self - -- @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=#DETECTION_BASE] Detected - -- @param #DETECTION_BASE self - - --- Asynchronous Event Trigger for Event Detected. - -- @function [parent=#DETECTION_BASE] __Detected - -- @param #DETECTION_BASE self - -- @param #number Delay The delay in seconds. - - - self:AddTransition( "*", "Stop", "Stopped" ) - - --- OnBefore Transition Handler for Event Stop. - -- @function [parent=#DETECTION_BASE] OnBeforeStop - -- @param #DETECTION_BASE self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Stop. - -- @function [parent=#DETECTION_BASE] OnAfterStop - -- @param #DETECTION_BASE self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Stop. - -- @function [parent=#DETECTION_BASE] Stop - -- @param #DETECTION_BASE self - - --- Asynchronous Event Trigger for Event Stop. - -- @function [parent=#DETECTION_BASE] __Stop - -- @param #DETECTION_BASE self - -- @param #number Delay The delay in seconds. - - --- OnLeave Transition Handler for State Stopped. - -- @function [parent=#DETECTION_BASE] OnLeaveStopped - -- @param #DETECTION_BASE self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnEnter Transition Handler for State Stopped. - -- @function [parent=#DETECTION_BASE] OnEnterStopped - -- @param #DETECTION_BASE self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - return self - end - - do -- State Transition Handling - - --- @param #DETECTION_BASE self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - function DETECTION_BASE:onafterStart(From,Event,To) - self:__Detect(0.1) - end - - --- @param #DETECTION_BASE self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - function DETECTION_BASE:onafterDetect(From,Event,To) - self:E( {From,Event,To}) - - local DetectDelay = 0.1 - self.DetectionCount = 0 - self.DetectionRun = 0 - self:UnIdentifyAllDetectedObjects() -- Resets the DetectedObjectsIdentified table - - self.DetectionSetGroup:Flush() - - for DetectionGroupID, DetectionGroupData in pairs( self.DetectionSetGroup:GetSet() ) do - self:E( {DetectionGroupData}) - self:__DetectionGroup( DetectDelay, DetectionGroupData ) -- Process each detection asynchronously. - self.DetectionCount = self.DetectionCount + 1 - DetectDelay = DetectDelay + 0.1 - end - end - - --- @param #DETECTION_BASE self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @param Wrapper.Group#GROUP DetectionGroup The Group detecting. - function DETECTION_BASE:onafterDetectionGroup( From, Event, To, DetectionGroup ) - self:E( {From,Event,To}) - - self.DetectionRun = self.DetectionRun + 1 - - local HasDetectedObjects = false - - if DetectionGroup:IsAlive() then - - self:T( { "DetectionGroup is Alive", DetectionGroup:GetName() } ) - - local DetectionGroupName = DetectionGroup:GetName() - - local DetectedUnits = {} - - local DetectedTargets = DetectionGroup:GetDetectedTargets( - self.DetectVisual, - self.DetectOptical, - self.DetectRadar, - self.DetectIRST, - self.DetectRWR, - self.DetectDLINK - ) - - self:T( DetectedTargets ) - - for DetectionObjectID, Detection in pairs( DetectedTargets ) do - local DetectedObject = Detection.object -- Dcs.DCSWrapper.Object#Object - self:T2( DetectedObject ) - - if DetectedObject and DetectedObject:isExist() and DetectedObject.id_ < 50000000 then - - local DetectionAccepted = true - - local DetectedObjectName = DetectedObject:getName() - - local DetectedObjectVec3 = DetectedObject:getPoint() - local DetectedObjectVec2 = { x = DetectedObjectVec3.x, y = DetectedObjectVec3.z } - local DetectionGroupVec3 = DetectionGroup:GetVec3() - local DetectionGroupVec2 = { x = DetectionGroupVec3.x, y = DetectionGroupVec3.z } - - local Distance = ( ( DetectedObjectVec3.x - DetectionGroupVec3.x )^2 + - ( DetectedObjectVec3.y - DetectionGroupVec3.y )^2 + - ( DetectedObjectVec3.z - DetectionGroupVec3.z )^2 - ) ^ 0.5 / 1000 - - self:T( { "Detected Target", DetectionGroupName, DetectedObjectName, Distance } ) - - -- Calculate Acceptance - - if self.AcceptRange and Distance > self.AcceptRange then - DetectionAccepted = false - end - - if self.AcceptZones then - for AcceptZoneID, AcceptZone in pairs( self.AcceptZones ) do - local AcceptZone = AcceptZone -- Core.Zone#ZONE_BASE - if AcceptZone:IsPointVec2InZone( DetectedObjectVec2 ) == false then - DetectionAccepted = false - end - end - end - - if self.RejectZones then - for RejectZoneID, RejectZone in pairs( self.RejectZones ) do - local RejectZone = RejectZone -- Core.Zone#ZONE_BASE - if RejectZone:IsPointVec2InZone( DetectedObjectVec2 ) == true then - DetectionAccepted = false - end - end - end - - -- Calculate additional probabilities - - if not self.DetectedObjects[DetectedObjectName] and Detection.visible and self.DistanceProbability then - local DistanceFactor = Distance / 4 - local DistanceProbabilityReversed = ( 1 - self.DistanceProbability ) * DistanceFactor - local DistanceProbability = 1 - DistanceProbabilityReversed - DistanceProbability = DistanceProbability * 30 / 300 - local Probability = math.random() -- Selects a number between 0 and 1 - self:T( { Probability, DistanceProbability } ) - if Probability > DistanceProbability then - DetectionAccepted = false - end - end - - if not self.DetectedObjects[DetectedObjectName] and Detection.visible and self.AlphaAngleProbability then - local NormalVec2 = { x = DetectedObjectVec2.x - DetectionGroupVec2.x, y = DetectedObjectVec2.y - DetectionGroupVec2.y } - local AlphaAngle = math.atan2( NormalVec2.y, NormalVec2.x ) - local Sinus = math.sin( AlphaAngle ) - local AlphaAngleProbabilityReversed = ( 1 - self.AlphaAngleProbability ) * ( 1 - Sinus ) - local AlphaAngleProbability = 1 - AlphaAngleProbabilityReversed - - AlphaAngleProbability = AlphaAngleProbability * 30 / 300 - - local Probability = math.random() -- Selects a number between 0 and 1 - self:T( { Probability, AlphaAngleProbability } ) - if Probability > AlphaAngleProbability then - DetectionAccepted = false - end - - end - - if not self.DetectedObjects[DetectedObjectName] and Detection.visible and self.ZoneProbability then - - for ZoneDataID, ZoneData in pairs( self.ZoneProbability ) do - self:E({ZoneData}) - local ZoneObject = ZoneData[1] -- Core.Zone#ZONE_BASE - local ZoneProbability = ZoneData[2] -- #number - ZoneProbability = ZoneProbability * 30 / 300 - - if ZoneObject:IsPointVec2InZone( DetectedObjectVec2 ) == true then - local Probability = math.random() -- Selects a number between 0 and 1 - self:T( { Probability, ZoneProbability } ) - if Probability > ZoneProbability then - DetectionAccepted = false - break - end - end - end - end - - if DetectionAccepted then - - HasDetectedObjects = true - - if not self.DetectedObjects[DetectedObjectName] then - self.DetectedObjects[DetectedObjectName] = {} - end - self.DetectedObjects[DetectedObjectName].Name = DetectedObjectName - self.DetectedObjects[DetectedObjectName].Visible = Detection.visible - self.DetectedObjects[DetectedObjectName].Type = Detection.type - self.DetectedObjects[DetectedObjectName].Distance = Distance - - local DetectedUnit = UNIT:FindByName( DetectedObjectName ) - - DetectedUnits[DetectedObjectName] = DetectedUnit - else - -- if beyond the DetectionRange then nullify... - if self.DetectedObjects[DetectedObjectName] then - self.DetectedObjects[DetectedObjectName] = nil - end - end - end - - self:T2( self.DetectedObjects ) - end - - if HasDetectedObjects then - self:__Detected( 0.1, DetectedUnits ) - end - - end - - if self.DetectionCount > 0 and self.DetectionRun == self.DetectionCount then - self:__Detect( self.DetectionInterval ) - - self:T( "--> Create Detection Sets" ) - self:CreateDetectionSets() - end - - end - - - end - - do -- Initialization methods - - --- 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 - - end - - do - - --- Set the detection interval time in seconds. - -- @param #DETECTION_BASE self - -- @param #number DetectionInterval Interval in seconds. - -- @return #DETECTION_BASE self - function DETECTION_BASE:SetDetectionInterval( DetectionInterval ) - self:F2() - - self.DetectionInterval = DetectionInterval - - return self - end - - end - - do -- Accept / Reject detected units - - --- Accept detections if within a range in meters. - -- @param #DETECTION_BASE self - -- @param #number AcceptRange Accept a detection if the unit is within the AcceptRange in meters. - -- @return #DETECTION_BASE self - function DETECTION_BASE:SetAcceptRange( AcceptRange ) - self:F2() - - self.AcceptRange = AcceptRange - - return self - end - - --- Accept detections if within the specified zone(s). - -- @param #DETECTION_BASE self - -- @param AcceptZones Can be a list or ZONE_BASE objects, or a single ZONE_BASE object. - -- @return #DETECTION_BASE self - function DETECTION_BASE:SetAcceptZones( AcceptZones ) - self:F2() - - if type( AcceptZones ) == "table" then - self.AcceptZones = AcceptZones - else - self.AcceptZones = { AcceptZones } - end - - return self - end - - --- Reject detections if within the specified zone(s). - -- @param #DETECTION_BASE self - -- @param RejectZones Can be a list or ZONE_BASE objects, or a single ZONE_BASE object. - -- @return #DETECTION_BASE self - function DETECTION_BASE:SetRejectZones( RejectZones ) - self:F2() - - if type( RejectZones ) == "table" then - self.RejectZones = RejectZones - else - self.RejectZones = { RejectZones } - end - - return self - end - - end - - do -- Probability methods - - --- Upon a **visual** detection, the further away a detected object is, the less likely it is to be detected properly. - -- Also, the speed of accurate detection plays a role. - -- A distance probability factor between 0 and 1 can be given, that will model a linear extrapolated probability over 10 km distance. - -- For example, if a probability factor of 0.6 (60%) is given, the extrapolated probabilities over 15 kilometers would like like: - -- 1 km: 96%, 2 km: 92%, 3 km: 88%, 4 km: 84%, 5 km: 80%, 6 km: 76%, 7 km: 72%, 8 km: 68%, 9 km: 64%, 10 km: 60%, 11 km: 56%, 12 km: 52%, 13 km: 48%, 14 km: 44%, 15 km: 40%. - -- @param #DETECTION_BASE self - -- @param DistanceProbability The probability factor. - -- @return #DETECTION_BASE self - function DETECTION_BASE:SetDistanceProbability( DistanceProbability ) - self:F2() - - self.DistanceProbability = DistanceProbability - - return self - end - - - --- Upon a **visual** detection, the higher the unit is during the detecting process, the more likely the detected unit is to be detected properly. - -- A detection at a 90% alpha angle is the most optimal, a detection at 10% is less and a detection at 0% is less likely to be correct. - -- - -- A probability factor between 0 and 1 can be given, that will model a progressive extrapolated probability if the target would be detected at a 0° angle. - -- - -- For example, if a alpha angle probability factor of 0.7 is given, the extrapolated probabilities of the different angles would look like: - -- 0°: 70%, 10°: 75,21%, 20°: 80,26%, 30°: 85%, 40°: 89,28%, 50°: 92,98%, 60°: 95,98%, 70°: 98,19%, 80°: 99,54%, 90°: 100% - -- @param #DETECTION_BASE self - -- @param AlphaAngleProbability The probability factor. - -- @return #DETECTION_BASE self - function DETECTION_BASE:SetAlphaAngleProbability( AlphaAngleProbability ) - self:F2() - - self.AlphaAngleProbability = AlphaAngleProbability - - return self - end - - --- Upon a **visual** detection, the more a detected unit is within a cloudy zone, the less likely the detected unit is to be detected successfully. - -- The Cloudy Zones work with the ZONE_BASE derived classes. The mission designer can define within the mission - -- zones that reflect cloudy areas where detected units may not be so easily visually detected. - -- @param #DETECTION_BASE self - -- @param ZoneArray Aray of a The ZONE_BASE object and a ZoneProbability pair.. - -- @return #DETECTION_BASE self - function DETECTION_BASE:SetZoneProbability( ZoneArray ) - self:F2() - - self.ZoneProbability = ZoneArray - - return self - end - - - end - - do -- Change processing - - --- Accepts changes from the detected item. - -- @param #DETECTION_BASE self - -- @param #DETECTION_BASE.DetectedItem DetectedItem - -- @return #DETECTION_BASE self - function DETECTION_BASE:AcceptChanges( DetectedItem ) - - DetectedItem.Changed = false - DetectedItem.Changes = {} - - return self - end - - --- Add a change to the detected zone. - -- @param #DETECTION_BASE self - -- @param #DETECTION_BASE.DetectedItem DetectedItem - -- @param #string ChangeCode - -- @return #DETECTION_BASE self - function DETECTION_BASE:AddChangeItem( DetectedItem, ChangeCode, ItemUnitType ) - - DetectedItem.Changed = true - local ItemID = DetectedItem.ItemID - - DetectedItem.Changes = DetectedItem.Changes or {} - DetectedItem.Changes[ChangeCode] = DetectedItem.Changes[ChangeCode] or {} - DetectedItem.Changes[ChangeCode].ItemID = ItemID - DetectedItem.Changes[ChangeCode].ItemUnitType = ItemUnitType - - self:T( { "Change on Detection Item:", DetectedItem.ItemID, ChangeCode, ItemUnitType } ) - - return self - end - - - --- Add a change to the detected zone. - -- @param #DETECTION_BASE self - -- @param #DETECTION_BASE.DetectedItem DetectedItem - -- @param #string ChangeCode - -- @param #string ChangeUnitType - -- @return #DETECTION_BASE self - function DETECTION_BASE:AddChangeUnit( DetectedItem, ChangeCode, ChangeUnitType ) - - DetectedItem.Changed = true - local ItemID = DetectedItem.ItemID - - DetectedItem.Changes = DetectedItem.Changes or {} - DetectedItem.Changes[ChangeCode] = DetectedItem.Changes[ChangeCode] or {} - DetectedItem.Changes[ChangeCode][ChangeUnitType] = DetectedItem.Changes[ChangeCode][ChangeUnitType] or 0 - DetectedItem.Changes[ChangeCode][ChangeUnitType] = DetectedItem.Changes[ChangeCode][ChangeUnitType] + 1 - DetectedItem.Changes[ChangeCode].ItemID = ItemID - - self:T( { "Change on Detection Item:", DetectedItem.ItemID, ChangeCode, ChangeUnitType } ) - - return self - end - - - end - - do -- Threat - - --- Returns if there are friendlies nearby the FAC units ... - -- @param #DETECTION_BASE self - -- @return #boolean trhe if there are friendlies nearby - function DETECTION_BASE:IsFriendliesNearBy( DetectedItem ) - - self:T3( DetectedItem.FriendliesNearBy ) - return DetectedItem.FriendliesNearBy or false - end - - --- Background worker function to determine if there are friendlies nearby ... - -- @param #DETECTION_BASE self - function DETECTION_BASE:ReportFriendliesNearBy( ReportGroupData ) - self:F2() - - local DetectedItem = ReportGroupData.DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem - local DetectedSet = ReportGroupData.DetectedItem.Set - local DetectedUnit = DetectedSet:GetFirst() - - DetectedItem.FriendliesNearBy = false - - if DetectedUnit then - - - local SphereSearch = { - id = world.VolumeType.SPHERE, - params = { - point = DetectedUnit: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 DetectedItem = ReportGroupData.DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem - local DetectedSet = ReportGroupData.DetectedItem.Set - local DetectedUnit = DetectedSet:GetFirst() -- Wrapper.Unit#UNIT - local ReportSetGroup = ReportGroupData.ReportSetGroup - - local EnemyCoalition = DetectedUnit:GetCoalition() - - local FoundUnitCoalition = FoundDCSUnit:getCoalition() - local FoundUnitName = FoundDCSUnit:getName() - local FoundUnitGroupName = FoundDCSUnit:getGroup():getName() - local EnemyUnitName = DetectedUnit:GetName() - local FoundUnitInReportSetGroup = ReportSetGroup:FindGroup( FoundUnitGroupName ) ~= nil - - self:T3( { "Friendlies search:", FoundUnitName, FoundUnitCoalition, EnemyUnitName, EnemyCoalition, FoundUnitInReportSetGroup } ) - - if FoundUnitCoalition ~= EnemyCoalition and FoundUnitInReportSetGroup == false then - DetectedItem.FriendliesNearBy = true - return false - end - - return true - end - - world.searchObjects( Object.Category.UNIT, SphereSearch, FindNearByFriendlies, ReportGroupData ) - end - end - - 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( { "Identified:", 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:F( 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 - - - --- Adds a new DetectedItem to the DetectedItems list. - -- The DetectedItem is a table and contains a SET_UNIT in the field Set. - -- @param #DETECTION_BASE self - -- @param #string DetectedItemIndex The index of the DetectedItem. - -- @param Core.Set#SET_UNIT Set (optional) The Set of Units to be added. - -- @return #DETECTION_BASE.DetectedItem - function DETECTION_BASE:AddDetectedItem( DetectedItemIndex, Set ) - - local DetectedItem = {} - self.DetectedItemCount = self.DetectedItemCount + 1 - self.DetectedItemMax = self.DetectedItemMax + 1 - - if DetectedItemIndex then - self.DetectedItems[DetectedItemIndex] = DetectedItem - else - self.DetectedItems[self.DetectedItemCount] = DetectedItem - end - - DetectedItem.Set = Set or SET_UNIT:New() - DetectedItem.ItemID = self.DetectedItemMax - DetectedItem.Removed = false - - return DetectedItem - end - - --- Adds a new DetectedItem to the DetectedItems list. - -- The DetectedItem is a table and contains a SET_UNIT in the field Set. - -- @param #DETECTION_BASE self - -- @param #string DetectedItemIndex The index of the DetectedItem. - -- @param Core.Set#SET_UNIT Set (optional) The Set of Units to be added. - -- @param Core.Zone#ZONE_UNIT Zone (optional) The Zone to be added where the Units are located. - -- @return #DETECTION_BASE.DetectedItem - function DETECTION_BASE:AddDetectedItemZone( DetectedItemIndex, Set, Zone ) - - local DetectedItem = self:AddDetectedItem( DetectedItemIndex, Set ) - - DetectedItem.Zone = Zone - - return DetectedItem - end - - --- Removes an existing DetectedItem from the DetectedItems list. - -- The DetectedItem is a table and contains a SET_UNIT in the field Set. - -- @param #DETECTION_BASE self - -- @param #number DetectedItemIndex The index or position in the DetectedItems list where the item needs to be removed. - function DETECTION_BASE:RemoveDetectedItem( DetectedItemIndex ) - - self.DetectedItemCount = self.DetectedItemCount - 1 - self.DetectedItems[DetectedItemIndex] = nil - end - - - --- Get the detected @{Set#SET_BASE}s. - -- @param #DETECTION_BASE self - -- @return #DETECTION_BASE.DetectedItems - function DETECTION_BASE:GetDetectedItems() - - return self.DetectedItems - end - - --- Get the amount of SETs with detected objects. - -- @param #DETECTION_BASE self - -- @return #number Count - function DETECTION_BASE:GetDetectedItemsCount() - - local DetectedCount = self.DetectedItemCount - return DetectedCount - end - - --- Get a detected item using a given numeric index. - -- @param #DETECTION_BASE self - -- @param #number Index - -- @return #DETECTION_BASE.DetectedItem - function DETECTION_BASE:GetDetectedItem( Index ) - - local DetectedItem = self.DetectedItems[Index] - if DetectedItem then - return DetectedItem - end - - return nil - end - - --- Get the @{Set#SET_UNIT} of a detecttion area using a given numeric index. - -- @param #DETECTION_BASE self - -- @param #number Index - -- @return Core.Set#SET_UNIT DetectedSet - function DETECTION_BASE:GetDetectedSet( Index ) - - local DetectedItem = self:GetDetectedItem( Index ) - local DetectedSetUnit = DetectedItem.Set - if DetectedSetUnit then - return DetectedSetUnit - end - - return nil - end - - do -- Zones - - --- Get the @{Zone#ZONE_UNIT} of a detection area using a given numeric index. - -- @param #DETECTION_BASE self - -- @param #number Index - -- @return Core.Zone#ZONE_UNIT DetectedZone - function DETECTION_BASE:GetDetectedZone( Index ) - - local DetectedZone = self.DetectedItems[Index].Zone - if DetectedZone then - return DetectedZone - end - - return nil - end - - end - - - --- Report summary of a detected item using a given numeric index. - -- @param #DETECTION_BASE self - -- @param Index - -- @return #string - function DETECTION_BASE:DetectedItemReportSummary( Index ) - self:F( Index ) - return nil - end - - --- Report detailed of a detectedion result. - -- @param #DETECTION_BASE self - -- @return #string - function DETECTION_BASE:DetectedReportDetailed() - self:F() - 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 - -end - -do -- DETECTION_UNITS - - --- # 2) DETECTION_UNITS class, extends @{Detection#DETECTION_BASE} - -- - -- The DETECTION_UNITS class will detect units within the battle zone. - -- It will build a DetectedItems list filled with DetectedItems. Each DetectedItem will contain a field Set, which contains a @{Set#SET_UNIT} containing ONE @{UNIT} object reference. - -- Beware that when the amount of units detected is large, the DetectedItems list will be large also. - -- - -- @type DETECTION_UNITS - -- @field Dcs.DCSTypes#Distance DetectionRange The range till which targets are detected. - -- @extends #DETECTION_BASE - DETECTION_UNITS = { - ClassName = "DETECTION_UNITS", - DetectionRange = nil, - } - - --- DETECTION_UNITS constructor. - -- @param Functional.Detection#DETECTION_UNITS self - -- @param Core.Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Forward Air Controller role. - -- @return Functional.Detection#DETECTION_UNITS self - function DETECTION_UNITS:New( DetectionSetGroup ) - - -- Inherits from DETECTION_BASE - local self = BASE:Inherit( self, DETECTION_BASE:New( DetectionSetGroup ) ) -- #DETECTION_UNITS - - self._SmokeDetectedUnits = false - self._FlareDetectedUnits = false - self._SmokeDetectedZones = false - self._FlareDetectedZones = false - self._BoundDetectedZones = false - - return self - end - - --- Make text documenting the changes of the detected zone. - -- @param #DETECTION_UNITS self - -- @param #DETECTION_UNITS.DetectedItem DetectedItem - -- @return #string The Changes text - function DETECTION_UNITS:GetChangeText( DetectedItem ) - self:F( DetectedItem ) - - local MT = {} - - for ChangeCode, ChangeData in pairs( DetectedItem.Changes ) do - - if ChangeCode == "AU" then - local MTUT = {} - for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do - if ChangeUnitType ~= "ItemID" then - MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType - end - end - MT[#MT+1] = " New target(s) detected: " .. table.concat( MTUT, ", " ) .. "." - end - - if ChangeCode == "RU" then - local MTUT = {} - for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do - if ChangeUnitType ~= "ItemID" then - MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType - end - end - MT[#MT+1] = " Invisible or destroyed target(s): " .. table.concat( MTUT, ", " ) .. "." - end - - end - - return table.concat( MT, "\n" ) - - end - - - --- Create the DetectedItems list from the DetectedObjects table. - -- For each DetectedItem, a one field array is created containing the Unit detected. - -- @param #DETECTION_UNITS self - -- @return #DETECTION_UNITS self - function DETECTION_UNITS:CreateDetectionSets() - self:F2( #self.DetectedObjects ) - - -- Loop the current detected items, and check if each object still exists and is detected. - - for DetectedItemID, DetectedItem in pairs( self.DetectedItems ) do - - local DetectedItemSet = DetectedItem.Set -- Core.Set#SET_UNIT - local DetectedTypeName = DetectedItem.Type - - for DetectedUnitName, DetectedUnitData in pairs( DetectedItemSet:GetSet() ) do - local DetectedUnit = DetectedUnitData -- Wrapper.Unit#UNIT - - local DetectedObject = nil - self:E( DetectedUnit ) - if DetectedUnit:IsAlive() then - --self:E(DetectedUnit:GetName()) - DetectedObject = self:GetDetectedObject( DetectedUnit:GetName() ) - end - if DetectedObject then - - -- Yes, the DetectedUnit is still detected or exists. Flag as identified. - self:IdentifyDetectedObject( DetectedObject ) - else - -- There was no DetectedObject, remove DetectedUnit from the Set. - self:AddChangeUnit( DetectedItem, "RU", DetectedUnitName ) - DetectedItemSet:Remove( DetectedUnitName ) - end - end - end - - - -- Now we need to loop through the unidentified detected units and add these... These are all new items. - for DetectedUnitName, DetectedObjectData in pairs( self.DetectedObjects ) do - - local DetectedObject = self:GetDetectedObject( DetectedUnitName ) - if DetectedObject then - self:T( { "Detected Unit #", DetectedUnitName } ) - - local DetectedUnit = UNIT:FindByName( DetectedUnitName ) -- Wrapper.Unit#UNIT - - if DetectedUnit then - local DetectedTypeName = DetectedUnit:GetTypeName() - local DetectedItem = self:GetDetectedItem( DetectedUnitName ) - if not DetectedItem then - self:T( "Added new DetectedItem" ) - DetectedItem = self:AddDetectedItem( DetectedUnitName ) - DetectedItem.Type = DetectedUnit:GetTypeName() - DetectedItem.Name = DetectedObjectData.Name - DetectedItem.Visible = DetectedObjectData.Visible - DetectedItem.Distance = DetectedObjectData.Distance - end - - DetectedItem.Set:AddUnit( DetectedUnit ) - self:AddChangeUnit( DetectedItem, "AU", DetectedTypeName ) - end - end - end - - for DetectedItemID, DetectedItemData in pairs( self.DetectedItems ) do - - local DetectedItem = DetectedItemData -- #DETECTION_BASE.DetectedItem - local DetectedSet = DetectedItem.Set - - self:ReportFriendliesNearBy( { DetectedItem = DetectedItem, ReportSetGroup = self.DetectionSetGroup } ) -- Fill the Friendlies table - --self:NearestFAC( DetectedItem ) - end - - end - - --- Report summary of a DetectedItem using a given numeric index. - -- @param #DETECTION_UNITS self - -- @param Index - -- @return #string - function DETECTION_UNITS:DetectedItemReportSummary( Index ) - self:F( Index ) - - local DetectedItem = self:GetDetectedItem( Index ) - local DetectedSet = self:GetDetectedSet( Index ) - - self:T( DetectedSet ) - if DetectedSet then - local ReportSummary = "" - local UnitDistanceText = "" - local UnitCategoryText = "" - - local DetectedItemUnit = DetectedSet:GetFirst() -- Wrapper.Unit#UNIT - - if DetectedItemUnit and DetectedItemUnit:IsAlive() then - self:T(DetectedItemUnit) - - local UnitCategoryName = DetectedItemUnit:GetCategoryName() or "" - local UnitCategoryType = DetectedItemUnit:GetTypeName() or "" - - if DetectedItem.Type and UnitCategoryName and UnitCategoryType then - UnitCategoryText = UnitCategoryName .. " (" .. UnitCategoryType .. ") at " - else - UnitCategoryText = "Unknown target at " - end - - if DetectedItem.Visible == false then - UnitDistanceText = string.format( "%.2f", DetectedItem.Distance ) .. " estimated km" - else - UnitDistanceText = string.format( "%.2f", DetectedItem.Distance ) .. " km, visual contact" - end - - local DetectedItemPointVec3 = DetectedItemUnit:GetPointVec3() - local DetectedItemPointLL = DetectedItemPointVec3:ToStringLL( 3, true ) - - local ThreatLevelA2G = DetectedItemUnit:GetThreatLevel( DetectedItem ) - - ReportSummary = string.format( - "%s - Threat [%s] (%2d) - %s%s", - DetectedItemPointLL, - string.rep( "■", ThreatLevelA2G ), - ThreatLevelA2G, - UnitCategoryText, - UnitDistanceText - ) - end - - self:T( ReportSummary ) - - return ReportSummary - end - end - - --- Report detailed of a detection result. - -- @param #DETECTION_UNITS self - -- @return #string - function DETECTION_UNITS:DetectedReportDetailed() - self:F() - - local Report = REPORT:New( "Detected units:" ) - for DetectedItemID, DetectedItem in ipairs( self.DetectedItems ) do - local DetectedItem = DetectedItem -- #DETECTION_BASE.DetectedItem - local ReportSummary = self:DetectedItemReportSummary( DetectedItemID ) - Report:Add( ReportSummary ) - end - - local ReportText = Report:Text() - - return ReportText - end - -end - -do -- DETECTION_TYPES - - --- # 3) DETECTION_TYPES class, extends @{Detection#DETECTION_BASE} - -- - -- The DETECTION_TYPES class will detect units within the battle zone. - -- It will build a DetectedItems[] list filled with DetectedItems, grouped by the type of units detected. - -- Each DetectedItem will contain a field Set, which contains a @{Set#SET_UNIT} containing ONE @{UNIT} object reference. - -- Beware that when the amount of different types detected is large, the DetectedItems[] list will be large also. - -- - -- @type DETECTION_TYPES - -- @extends #DETECTION_BASE - DETECTION_TYPES = { - ClassName = "DETECTION_TYPES", - DetectionRange = nil, - } - - --- DETECTION_TYPES constructor. - -- @param Functional.Detection#DETECTION_TYPES self - -- @param Core.Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Recce role. - -- @return Functional.Detection#DETECTION_TYPES self - function DETECTION_TYPES:New( DetectionSetGroup ) - - -- Inherits from DETECTION_BASE - local self = BASE:Inherit( self, DETECTION_BASE:New( DetectionSetGroup ) ) -- #DETECTION_TYPES - - self._SmokeDetectedUnits = false - self._FlareDetectedUnits = false - self._SmokeDetectedZones = false - self._FlareDetectedZones = false - self._BoundDetectedZones = false - - return self - end - - --- Make text documenting the changes of the detected zone. - -- @param #DETECTION_TYPES self - -- @param #DETECTION_TYPES.DetectedItem DetectedItem - -- @return #string The Changes text - function DETECTION_TYPES:GetChangeText( DetectedItem ) - self:F( DetectedItem ) - - local MT = {} - - for ChangeCode, ChangeData in pairs( DetectedItem.Changes ) do - - if ChangeCode == "AU" then - local MTUT = {} - for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do - if ChangeUnitType ~= "ItemID" then - MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType - end - end - MT[#MT+1] = " New target(s) detected: " .. table.concat( MTUT, ", " ) .. "." - end - - if ChangeCode == "RU" then - local MTUT = {} - for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do - if ChangeUnitType ~= "ItemID" then - MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType - end - end - MT[#MT+1] = " Invisible or destroyed target(s): " .. table.concat( MTUT, ", " ) .. "." - end - - end - - return table.concat( MT, "\n" ) - - end - - - --- Create the DetectedItems list from the DetectedObjects table. - -- For each DetectedItem, a one field array is created containing the Unit detected. - -- @param #DETECTION_TYPES self - -- @return #DETECTION_TYPES self - function DETECTION_TYPES:CreateDetectionSets() - self:F2( #self.DetectedObjects ) - - -- Loop the current detected items, and check if each object still exists and is detected. - - for DetectedItemID, DetectedItem in pairs( self.DetectedItems ) do - - local DetectedItemSet = DetectedItem.Set -- Core.Set#SET_UNIT - local DetectedTypeName = DetectedItem.Type - - for DetectedUnitName, DetectedUnitData in pairs( DetectedItemSet: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 - - -- Yes, the DetectedUnit is still detected or exists. Flag as identified. - self:IdentifyDetectedObject( DetectedObject ) - else - -- There was no DetectedObject, remove DetectedUnit from the Set. - self:AddChangeUnit( DetectedItem, "RU", DetectedUnitName ) - DetectedItemSet:Remove( DetectedUnitName ) - end - end - end - - - -- Now we need to loop through the unidentified detected units and add these... These are all new items. - for DetectedUnitName, DetectedObjectData in pairs( self.DetectedObjects ) do - - local DetectedObject = self:GetDetectedObject( DetectedUnitName ) - if DetectedObject then - self:T( { "Detected Unit #", DetectedUnitName } ) - - local DetectedUnit = UNIT:FindByName( DetectedUnitName ) -- Wrapper.Unit#UNIT - - if DetectedUnit then - local DetectedTypeName = DetectedUnit:GetTypeName() - local DetectedItem = self:GetDetectedItem( DetectedTypeName ) - if not DetectedItem then - DetectedItem = self:AddDetectedItem( DetectedTypeName ) - DetectedItem.Type = DetectedUnit:GetTypeName() - end - - DetectedItem.Set:AddUnit( DetectedUnit ) - self:AddChangeUnit( DetectedItem, "AU", DetectedTypeName ) - end - end - end - - for DetectedItemID, DetectedItemData in pairs( self.DetectedItems ) do - - local DetectedItem = DetectedItemData -- #DETECTION_BASE.DetectedItem - local DetectedSet = DetectedItem.Set - - self:ReportFriendliesNearBy( { DetectedItem = DetectedItem, ReportSetGroup = self.DetectionSetGroup } ) -- Fill the Friendlies table - --self:NearestFAC( DetectedItem ) - end - - end - - --- Report summary of a DetectedItem using a given numeric index. - -- @param #DETECTION_TYPES self - -- @param Index - -- @return #string - function DETECTION_TYPES:DetectedItemReportSummary( DetectedTypeName ) - self:F( DetectedTypeName ) - - local DetectedItem = self:GetDetectedItem( DetectedTypeName ) - local DetectedSet = self:GetDetectedSet( DetectedTypeName ) - - self:T( DetectedItem ) - if DetectedItem then - - local ThreatLevelA2G = DetectedSet:CalculateThreatLevelA2G() - local DetectedItemsCount = DetectedSet:Count() - local DetectedItemType = DetectedItem.Type - - local ReportSummary = string.format( - "Threat [%s] (%2d) - %2d of %s", - string.rep( "■", ThreatLevelA2G ), - ThreatLevelA2G, - DetectedItemsCount, - DetectedItemType - ) - self:T( ReportSummary ) - - return ReportSummary - end - end - - --- Report detailed of a detection result. - -- @param #DETECTION_TYPES self - -- @return #string - function DETECTION_TYPES:DetectedReportDetailed() - self:F() - - local Report = REPORT:New( "Detected types:" ) - for DetectedItemTypeName, DetectedItem in pairs( self.DetectedItems ) do - local DetectedItem = DetectedItem -- #DETECTION_BASE.DetectedItem - local ReportSummary = self:DetectedItemReportSummary( DetectedItemTypeName ) - Report:Add( ReportSummary ) - end - - local ReportText = Report:Text() - - return ReportText - end - -end - - -do -- DETECTION_AREAS - - --- # 4) DETECTION_AREAS class, extends @{Detection#DETECTION_BASE} - -- - -- The DETECTION_AREAS class will detect units within the battle zone for a list of @{Group}s detecting targets following (a) detection method(s), - -- and will build a list (table) of @{Set#SET_UNIT}s containing the @{Unit#UNIT}s detected. - -- The class is group the detected units within zones given a DetectedZoneRange parameter. - -- A set with multiple detected zones will be created as there are groups of units detected. - -- - -- ## 4.1) Retrieve the Detected Unit Sets and Detected Zones - -- - -- The methods to manage the DetectedItems[].Set(s) are implemented in @{Detection#DECTECTION_BASE} and - -- the methods to manage the DetectedItems[].Zone(s) is implemented in @{Detection#DETECTION_AREAS}. - -- - -- Retrieve the DetectedItems[].Set with the method @{Detection#DETECTION_BASE.GetDetectedSet}(). A @{Set#SET_UNIT} object will be returned. - -- - -- Retrieve the formed @{Zone@ZONE_UNIT}s as a result of the grouping the detected units within the DetectionZoneRange, use the method @{Detection#DETECTION_BASE.GetDetectionZones}(). - -- To understand the amount of zones created, use the method @{Detection#DETECTION_BASE.GetDetectionZoneCount}(). - -- If you want to obtain a specific zone from the DetectedZones, use the method @{Detection#DETECTION_BASE.GetDetectionZone}() with a given index. - -- - -- ## 4.4) Flare or Smoke detected units - -- - -- Use the methods @{Detection#DETECTION_AREAS.FlareDetectedUnits}() or @{Detection#DETECTION_AREAS.SmokeDetectedUnits}() to flare or smoke the detected units when a new detection has taken place. - -- - -- ## 4.5) Flare or Smoke or Bound detected zones - -- - -- Use the methods: - -- - -- * @{Detection#DETECTION_AREAS.FlareDetectedZones}() to flare in a color - -- * @{Detection#DETECTION_AREAS.SmokeDetectedZones}() to smoke in a color - -- * @{Detection#DETECTION_AREAS.SmokeDetectedZones}() to bound with a tire with a white flag - -- - -- the detected zones when a new detection has taken place. - -- - -- @type DETECTION_AREAS - -- @field Dcs.DCSTypes#Distance DetectionZoneRange The range till which targets are grouped upon the first detected target. - -- @field #DETECTION_BASE.DetectedItems DetectedItems A list of areas containing the set of @{Unit}s, @{Zone}s, the center @{Unit} within the zone, and ID of each area that was detected within a DetectionZoneRange. - -- @extends #DETECTION_BASE - DETECTION_AREAS = { - ClassName = "DETECTION_AREAS", - DetectionZoneRange = nil, - } - - - --- DETECTION_AREAS constructor. - -- @param #DETECTION_AREAS self - -- @param Core.Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Forward Air Controller role. - -- @param Dcs.DCSTypes#Distance DetectionZoneRange The range till which targets are grouped upon the first detected target. - -- @return #DETECTION_AREAS - function DETECTION_AREAS:New( DetectionSetGroup, DetectionZoneRange ) - - -- Inherits from DETECTION_BASE - local self = BASE:Inherit( self, DETECTION_BASE:New( DetectionSetGroup ) ) - - self.DetectionZoneRange = DetectionZoneRange - - self._SmokeDetectedUnits = false - self._FlareDetectedUnits = false - self._SmokeDetectedZones = false - self._FlareDetectedZones = false - self._BoundDetectedZones = false - - return self - end - - --- Report summary of a detected item using a given numeric index. - -- @param #DETECTION_AREAS self - -- @param Index - -- @return #string - function DETECTION_AREAS:DetectedItemReportSummary( Index ) - self:F( Index ) - - local DetectedItem = self:GetDetectedItem( Index ) - if DetectedItem then - local DetectedSet = self:GetDetectedSet( Index ) - local ReportSummaryItem - - local DetectedZone = self:GetDetectedZone( Index ) - local DetectedItemPointVec3 = DetectedZone:GetPointVec3() - local DetectedItemPointLL = DetectedItemPointVec3:ToStringLL( 3, true ) - - local ThreatLevelA2G = self:GetTreatLevelA2G( DetectedItem ) - local DetectedItemsCount = DetectedSet:Count() - local DetectedItemsTypes = DetectedSet:GetTypeNames() - - local ReportSummary = string.format( - "%s - Threat [%s] (%2d) - %2d of %s", - DetectedItemPointLL, - string.rep( "■", ThreatLevelA2G ), - ThreatLevelA2G, - DetectedItemsCount, - DetectedItemsTypes - ) - - return ReportSummary - end - - return nil - 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( DetectedItem ) - - self:T3( DetectedItem.FriendliesNearBy ) - return DetectedItem.FriendliesNearBy or false - end - - --- Calculate the maxium A2G threat level of the DetectedItem. - -- @param #DETECTION_AREAS self - -- @param #DETECTION_BASE.DetectedItem DetectedItem - function DETECTION_AREAS:CalculateThreatLevelA2G( DetectedItem ) - - local MaxThreatLevelA2G = 0 - for UnitName, UnitData in pairs( DetectedItem.Set:GetSet() ) do - local ThreatUnit = UnitData -- Wrapper.Unit#UNIT - local ThreatLevelA2G = ThreatUnit:GetThreatLevel() - if ThreatLevelA2G > MaxThreatLevelA2G then - MaxThreatLevelA2G = ThreatLevelA2G - end - end - - self:T3( MaxThreatLevelA2G ) - DetectedItem.MaxThreatLevelA2G = MaxThreatLevelA2G - - end - - --- Find the nearest FAC of the DetectedItem. - -- @param #DETECTION_AREAS self - -- @param #DETECTION_BASE.DetectedItem DetectedItem - -- @return Wrapper.Unit#UNIT The nearest FAC unit - function DETECTION_AREAS:NearestFAC( DetectedItem ) - - 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 - - DetectedItem.NearestFAC = NearestFAC - - end - - --- Returns the A2G threat level of the units in the DetectedItem - -- @param #DETECTION_AREAS self - -- @param #DETECTION_BASE.DetectedItem DetectedItem - -- @return #number a scale from 0 to 10. - function DETECTION_AREAS:GetTreatLevelA2G( DetectedItem ) - - self:T3( DetectedItem.MaxThreatLevelA2G ) - return DetectedItem.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 - - --- Bound the detected zones - -- @param #DETECTION_AREAS self - -- @return #DETECTION_AREAS self - function DETECTION_AREAS:BoundDetectedZones() - self:F2() - - self._BoundDetectedZones = true - return self - end - - --- Make text documenting the changes of the detected zone. - -- @param #DETECTION_AREAS self - -- @param #DETECTION_BASE.DetectedItem DetectedItem - -- @return #string The Changes text - function DETECTION_AREAS:GetChangeText( DetectedItem ) - self:F( DetectedItem ) - - local MT = {} - - for ChangeCode, ChangeData in pairs( DetectedItem.Changes ) do - - if ChangeCode == "AA" then - MT[#MT+1] = "Detected new area " .. ChangeData.ItemID .. ". The center target is a " .. ChangeData.ItemUnitType .. "." - end - - if ChangeCode == "RAU" then - MT[#MT+1] = "Changed area " .. ChangeData.ItemID .. ". Removed the center target." - end - - if ChangeCode == "AAU" then - MT[#MT+1] = "Changed area " .. ChangeData.ItemID .. ". The new center target is a " .. ChangeData.ItemUnitType .. "." - end - - if ChangeCode == "RA" then - MT[#MT+1] = "Removed old area " .. ChangeData.ItemID .. ". No more targets in this area." - end - - if ChangeCode == "AU" then - local MTUT = {} - for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do - if ChangeUnitType ~= "ItemID" then - MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType - end - end - MT[#MT+1] = "Detected for area " .. ChangeData.ItemID .. " new target(s) " .. table.concat( MTUT, ", " ) .. "." - end - - if ChangeCode == "RU" then - local MTUT = {} - for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do - if ChangeUnitType ~= "ItemID" then - MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType - end - end - MT[#MT+1] = "Removed for area " .. ChangeData.ItemID .. " invisible or destroyed target(s) " .. table.concat( MTUT, ", " ) .. "." - end - - end - - return table.concat( MT, "\n" ) - - end - - - --- Make a DetectionSet table. This function will be overridden in the derived clsses. - -- @param #DETECTION_AREAS self - -- @return #DETECTION_AREAS self - function DETECTION_AREAS:CreateDetectionSets() - self:F2() - - - self:T( "Checking Detected Items for new Detected Units ..." ) - -- First go through all detected sets, and check if there are new detected units, match all existing detected units and identify undetected units. - -- Regroup when needed, split groups when needed. - for DetectedItemID, DetectedItemData in pairs( self.DetectedItems ) do - - local DetectedItem = DetectedItemData -- #DETECTION_BASE.DetectedItem - if DetectedItem then - - self:T( { "Detected Item ID:", DetectedItemID } ) - - - local DetectedSet = DetectedItem.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( { "Zone Center Unit:", DetectedItem.Zone.ZoneUNIT.UnitName } ) - local DetectedZoneObject = self:GetDetectedObject( DetectedItem.Zone.ZoneUNIT.UnitName ) - self:T3( { "Detected Zone Object:", DetectedItem.Zone:GetName(), 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( DetectedItem.Zone.ZoneUNIT.UnitName ) - - self:AddChangeItem( DetectedItem, '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 - - DetectedItem.Zone:BoundZone( 12, self.CountryID, true) - - -- Assign the Unit as the new center unit of the detected area. - DetectedItem.Zone = ZONE_UNIT:New( DetectedUnit:GetName(), DetectedUnit, self.DetectionZoneRange ) - - self:AddChangeItem( DetectedItem, "AAU", DetectedItem.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 DetectedItem.Zone - if DetectedUnit:IsInZone( DetectedItem.Zone ) then - - -- Yes, the DetectedUnit is within the DetectedItem.Zone, no changes, DetectedUnit can be kept within the Set. - self:IdentifyDetectedObject( DetectedObject ) - - else - -- No, the DetectedUnit is not within the DetectedItem.Zone, remove DetectedUnit from the Set. - DetectedSet:Remove( DetectedUnitName ) - self:AddChangeUnit( DetectedItem, "RU", DetectedUnit:GetTypeName() ) - end - - else - -- There was no DetectedObject, remove DetectedUnit from the Set. - self:AddChangeUnit( DetectedItem, "RU", "destroyed target" ) - DetectedSet:Remove( DetectedUnitName ) - - -- The DetectedObject has been identified, because it does not exist ... - -- self:IdentifyDetectedObject( DetectedObject ) - end - end - else - DetectedItem.Zone:BoundZone( 12, self.CountryID, true) - self:RemoveDetectedItem( DetectedItemID ) - self:AddChangeItem( DetectedItem, "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 DetectedItemID, DetectedItemData in pairs( self.DetectedItems ) do - - local DetectedItem = DetectedItemData -- #DETECTION_BASE.DetectedItem - if DetectedItem then - self:T( "Detection Area #" .. DetectedItem.ItemID ) - local DetectedSet = DetectedItem.Set - if not self:IsDetectedObjectIdentified( DetectedObject ) and DetectedUnit:IsInZone( DetectedItem.Zone ) then - self:IdentifyDetectedObject( DetectedObject ) - DetectedSet:AddUnit( DetectedUnit ) - AddedToDetectionArea = true - self:AddChangeUnit( DetectedItem, "AU", DetectedUnit:GetTypeName() ) - end - end - end - - if AddedToDetectionArea == false then - - -- New detection area - local DetectedItem = self:AddDetectedItemZone( nil, - SET_UNIT:New(), - ZONE_UNIT:New( DetectedUnitName, DetectedUnit, self.DetectionZoneRange ) - ) - --self:E( DetectedItem.Zone.ZoneUNIT.UnitName ) - DetectedItem.Set:AddUnit( DetectedUnit ) - self:AddChangeItem( DetectedItem, "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 DetectedItemID, DetectedItemData in pairs( self.DetectedItems ) do - - local DetectedItem = DetectedItemData -- #DETECTION_BASE.DetectedItem - local DetectedSet = DetectedItem.Set - local DetectedZone = DetectedItem.Zone - - self:ReportFriendliesNearBy( { DetectedItem = DetectedItem, ReportSetGroup = self.DetectionSetGroup } ) -- Fill the Friendlies table - self:CalculateThreatLevelA2G( DetectedItem ) -- Calculate A2G threat level - self:NearestFAC( DetectedItem ) - - 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 #" .. DetectedItem.ItemID .. ":" .. 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 - - if DETECTION_AREAS._BoundDetectedZones or self._BoundDetectedZones then - DetectedZone:BoundZone( 12, self.CountryID ) - end - end - - end - -end ---- Single-Player:**No** / Multi-Player:**Yes** / AI:**Yes** / Human:**No** / Types:**All** -- **AI Balancing will replace in multi player missions --- non-occupied human slots with AI groups, in order to provide an engaging simulation environment, --- even when there are hardly any players in the mission.** --- --- ![Banner Image](..\Presentations\AI_Balancer\Dia1.JPG) --- --- === --- --- # 1) @{AI_Balancer#AI_BALANCER} class, extends @{Fsm#FSM_SET} --- --- The @{AI_Balancer#AI_BALANCER} class monitors and manages as many replacement AI groups as there are --- CLIENTS in a SET_CLIENT collection, which are not occupied by human players. --- In other words, use AI_BALANCER to simulate human behaviour by spawning in replacement AI in multi player missions. --- --- The parent class @{Fsm#FSM_SET} manages the functionality to control the Finite State Machine (FSM). --- The mission designer can tailor the behaviour of the AI_BALANCER, by defining event and state transition methods. --- An explanation about state and event transition methods can be found in the @{FSM} module documentation. --- --- The mission designer can tailor the AI_BALANCER behaviour, by implementing a state or event handling method for the following: --- --- * **@{#AI_BALANCER.OnAfterSpawned}**( AISet, From, Event, To, AIGroup ): Define to add extra logic when an AI is spawned. --- --- ## 1.1) AI_BALANCER construction --- --- Create a new AI_BALANCER object with the @{#AI_BALANCER.New}() method: --- --- ## 1.2) AI_BALANCER is a FSM --- --- ![Process](..\Presentations\AI_Balancer\Dia13.JPG) --- --- ### 1.2.1) AI_BALANCER States --- --- * **Monitoring** ( Set ): Monitoring the Set if all AI is spawned for the Clients. --- * **Spawning** ( Set, ClientName ): There is a new AI group spawned with ClientName as the name of reference. --- * **Spawned** ( Set, AIGroup ): A new AI has been spawned. You can handle this event to customize the AI behaviour with other AI FSMs or own processes. --- * **Destroying** ( Set, AIGroup ): The AI is being destroyed. --- * **Returning** ( Set, AIGroup ): The AI is returning to the airbase specified by the ReturnToAirbase methods. Handle this state to customize the return behaviour of the AI, if any. --- --- ### 1.2.2) AI_BALANCER Events --- --- * **Monitor** ( Set ): Every 10 seconds, the Monitor event is triggered to monitor the Set. --- * **Spawn** ( Set, ClientName ): Triggers when there is a new AI group to be spawned with ClientName as the name of reference. --- * **Spawned** ( Set, AIGroup ): Triggers when a new AI has been spawned. You can handle this event to customize the AI behaviour with other AI FSMs or own processes. --- * **Destroy** ( Set, AIGroup ): The AI is being destroyed. --- * **Return** ( Set, AIGroup ): The AI is returning to the airbase specified by the ReturnToAirbase methods. --- --- ## 1.3) AI_BALANCER spawn interval for replacement AI --- --- Use the method @{#AI_BALANCER.InitSpawnInterval}() to set the earliest and latest interval in seconds that is waited until a new replacement AI is spawned. --- --- ## 1.4) AI_BALANCER returns AI to Airbases --- --- By default, When a human player joins a slot that is AI_BALANCED, the AI group will be destroyed by default. --- However, there are 2 additional options that you can use to customize the destroy behaviour. --- When a human player joins a slot, you can configure to let the AI return to: --- --- * @{#AI_BALANCER.ReturnToHomeAirbase}: Returns the AI to the **home** @{Airbase#AIRBASE}. --- * @{#AI_BALANCER.ReturnToNearestAirbases}: Returns the AI to the **nearest friendly** @{Airbase#AIRBASE}. --- --- Note that when AI returns to an airbase, the AI_BALANCER will trigger the **Return** event and the AI will return, --- otherwise the AI_BALANCER will trigger a **Destroy** event, and the AI will be destroyed. --- --- === --- --- # **API CHANGE HISTORY** --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- Hereby the change log: --- --- 2017-01-17: There is still a problem with AI being destroyed, but not respawned. Need to check further upon that. --- --- 2017-01-08: AI_BALANCER:**InitSpawnInterval( Earliest, Latest )** added. --- --- === --- --- # **AUTHORS and CONTRIBUTIONS** --- --- ### Contributions: --- --- * **[Dutch_Baron](https://forums.eagle.ru/member.php?u=112075)**: Working together with James has resulted in the creation of the AI_BALANCER class. James has shared his ideas on balancing AI with air units, and together we made a first design which you can use now :-) --- * **SNAFU**: Had a couple of mails with the guys to validate, if the same concept in the GCI/CAP script could be reworked within MOOSE. None of the script code has been used however within the new AI_BALANCER moose class. --- --- ### Authors: --- --- * FlightControl: Framework Design & Programming and Documentation. --- --- @module AI_Balancer - ---- AI_BALANCER class --- @type AI_BALANCER --- @field Core.Set#SET_CLIENT SetClient --- @field Functional.Spawn#SPAWN SpawnAI --- @field Wrapper.Group#GROUP Test --- @extends Core.Fsm#FSM_SET -AI_BALANCER = { - ClassName = "AI_BALANCER", - PatrolZones = {}, - AIGroups = {}, - Earliest = 5, -- Earliest a new AI can be spawned is in 5 seconds. - Latest = 60, -- Latest a new AI can be spawned is in 60 seconds. -} - - - ---- Creates a new AI_BALANCER object --- @param #AI_BALANCER self --- @param Core.Set#SET_CLIENT SetClient A SET\_CLIENT object that will contain the CLIENT objects to be monitored if they are alive or not (joined by a player). --- @param Functional.Spawn#SPAWN SpawnAI The default Spawn object to spawn new AI Groups when needed. --- @return #AI_BALANCER -function AI_BALANCER:New( SetClient, SpawnAI ) - - -- Inherits from BASE - local self = BASE:Inherit( self, FSM_SET:New( SET_GROUP:New() ) ) -- AI.AI_Balancer#AI_BALANCER - - -- TODO: Define the OnAfterSpawned event - self:SetStartState( "None" ) - self:AddTransition( "*", "Monitor", "Monitoring" ) - self:AddTransition( "*", "Spawn", "Spawning" ) - self:AddTransition( "Spawning", "Spawned", "Spawned" ) - self:AddTransition( "*", "Destroy", "Destroying" ) - self:AddTransition( "*", "Return", "Returning" ) - - self.SetClient = SetClient - self.SetClient:FilterOnce() - self.SpawnAI = SpawnAI - - self.SpawnQueue = {} - - self.ToNearestAirbase = false - self.ToHomeAirbase = false - - self:__Monitor( 1 ) - - return self -end - ---- Sets the earliest to the latest interval in seconds how long AI_BALANCER will wait to spawn a new AI. --- Provide 2 identical seconds if the interval should be a fixed amount of seconds. --- @param #AI_BALANCER self --- @param #number Earliest The earliest a new AI can be spawned in seconds. --- @param #number Latest The latest a new AI can be spawned in seconds. --- @return self -function AI_BALANCER:InitSpawnInterval( Earliest, Latest ) - - self.Earliest = Earliest - self.Latest = Latest - - return self -end - ---- Returns the AI to the nearest friendly @{Airbase#AIRBASE}. --- @param #AI_BALANCER self --- @param Dcs.DCSTypes#Distance ReturnTresholdRange If there is an enemy @{Client#CLIENT} within the ReturnTresholdRange given in meters, the AI will not return to the nearest @{Airbase#AIRBASE}. --- @param Core.Set#SET_AIRBASE ReturnAirbaseSet The SET of @{Set#SET_AIRBASE}s to evaluate where to return to. -function AI_BALANCER:ReturnToNearestAirbases( ReturnTresholdRange, ReturnAirbaseSet ) - - self.ToNearestAirbase = true - self.ReturnTresholdRange = ReturnTresholdRange - self.ReturnAirbaseSet = ReturnAirbaseSet -end - ---- Returns the AI to the home @{Airbase#AIRBASE}. --- @param #AI_BALANCER self --- @param Dcs.DCSTypes#Distance ReturnTresholdRange If there is an enemy @{Client#CLIENT} within the ReturnTresholdRange given in meters, the AI will not return to the nearest @{Airbase#AIRBASE}. -function AI_BALANCER:ReturnToHomeAirbase( ReturnTresholdRange ) - - self.ToHomeAirbase = true - self.ReturnTresholdRange = ReturnTresholdRange -end - ---- @param #AI_BALANCER self --- @param Core.Set#SET_GROUP SetGroup --- @param #string ClientName --- @param Wrapper.Group#GROUP AIGroup -function AI_BALANCER:onenterSpawning( SetGroup, From, Event, To, ClientName ) - - -- OK, Spawn a new group from the default SpawnAI object provided. - local AIGroup = self.SpawnAI:Spawn() -- Wrapper.Group#GROUP - if AIGroup then - AIGroup:E( "Spawning new AIGroup" ) - --TODO: need to rework UnitName thing ... - - SetGroup:Add( ClientName, AIGroup ) - self.SpawnQueue[ClientName] = nil - - -- Fire the Spawned event. The first parameter is the AIGroup just Spawned. - -- Mission designers can catch this event to bind further actions to the AIGroup. - self:Spawned( AIGroup ) - end -end - ---- @param #AI_BALANCER self --- @param Core.Set#SET_GROUP SetGroup --- @param Wrapper.Group#GROUP AIGroup -function AI_BALANCER:onenterDestroying( SetGroup, From, Event, To, ClientName, AIGroup ) - - AIGroup:Destroy() - SetGroup:Flush() - SetGroup:Remove( ClientName ) - SetGroup:Flush() -end - ---- @param #AI_BALANCER self --- @param Core.Set#SET_GROUP SetGroup --- @param Wrapper.Group#GROUP AIGroup -function AI_BALANCER:onenterReturning( SetGroup, From, Event, To, AIGroup ) - - local AIGroupTemplate = AIGroup:GetTemplate() - if self.ToHomeAirbase == true then - local WayPointCount = #AIGroupTemplate.route.points - local SwitchWayPointCommand = AIGroup:CommandSwitchWayPoint( 1, WayPointCount, 1 ) - AIGroup:SetCommand( SwitchWayPointCommand ) - AIGroup:MessageToRed( "Returning to home base ...", 30 ) - else - -- Okay, we need to send this Group back to the nearest base of the Coalition of the AI. - --TODO: i need to rework the POINT_VEC2 thing. - local PointVec2 = POINT_VEC2:New( AIGroup:GetVec2().x, AIGroup:GetVec2().y ) - local ClosestAirbase = self.ReturnAirbaseSet:FindNearestAirbaseFromPointVec2( PointVec2 ) - self:T( ClosestAirbase.AirbaseName ) - AIGroup:MessageToRed( "Returning to " .. ClosestAirbase:GetName().. " ...", 30 ) - local RTBRoute = AIGroup:RouteReturnToAirbase( ClosestAirbase ) - AIGroupTemplate.route = RTBRoute - AIGroup:Respawn( AIGroupTemplate ) - end - -end - - ---- @param #AI_BALANCER self -function AI_BALANCER:onenterMonitoring( SetGroup ) - - self:T2( { self.SetClient:Count() } ) - --self.SetClient:Flush() - - self.SetClient:ForEachClient( - --- @param Wrapper.Client#CLIENT Client - function( Client ) - self:T3(Client.ClientName) - - local AIGroup = self.Set:Get( Client.UnitName ) -- Wrapper.Group#GROUP - if Client:IsAlive() then - - if AIGroup and AIGroup:IsAlive() == true then - - if self.ToNearestAirbase == false and self.ToHomeAirbase == false then - self:Destroy( Client.UnitName, AIGroup ) - else - -- We test if there is no other CLIENT within the self.ReturnTresholdRange of the first unit of the AI group. - -- If there is a CLIENT, the AI stays engaged and will not return. - -- If there is no CLIENT within the self.ReturnTresholdRange, then the unit will return to the Airbase return method selected. - - local PlayerInRange = { Value = false } - local RangeZone = ZONE_RADIUS:New( 'RangeZone', AIGroup:GetVec2(), self.ReturnTresholdRange ) - - self:T2( RangeZone ) - - _DATABASE:ForEachPlayer( - --- @param Wrapper.Unit#UNIT RangeTestUnit - function( RangeTestUnit, RangeZone, AIGroup, PlayerInRange ) - self:T2( { PlayerInRange, RangeTestUnit.UnitName, RangeZone.ZoneName } ) - if RangeTestUnit:IsInZone( RangeZone ) == true then - self:T2( "in zone" ) - if RangeTestUnit:GetCoalition() ~= AIGroup:GetCoalition() then - self:T2( "in range" ) - PlayerInRange.Value = true - end - end - end, - - --- @param Core.Zone#ZONE_RADIUS RangeZone - -- @param Wrapper.Group#GROUP AIGroup - function( RangeZone, AIGroup, PlayerInRange ) - if PlayerInRange.Value == false then - self:Return( AIGroup ) - end - end - , RangeZone, AIGroup, PlayerInRange - ) - - end - self.Set:Remove( Client.UnitName ) - end - else - if not AIGroup or not AIGroup:IsAlive() == true then - self:T( "Client " .. Client.UnitName .. " not alive." ) - if not self.SpawnQueue[Client.UnitName] then - -- Spawn a new AI taking into account the spawn interval Earliest, Latest - self:__Spawn( math.random( self.Earliest, self.Latest ), Client.UnitName ) - self.SpawnQueue[Client.UnitName] = true - self:E( "New AI Spawned for Client " .. Client.UnitName ) - end - end - end - return true - end - ) - - self:__Monitor( 10 ) -end - - - ---- **AI** -- **Air Patrolling or Staging.** --- --- ![Banner Image](..\Presentations\AI_PATROL\Dia1.JPG) --- --- === --- --- AI PATROL classes makes AI Controllables execute an Patrol. --- --- There are the following types of PATROL classes defined: --- --- * @{#AI_PATROL_ZONE}: Perform a PATROL in a zone. --- --- ==== --- --- # **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 - ---- # 1) @{#AI_PATROL_ZONE} class, extends @{Fsm#FSM_CONTROLLABLE} --- --- The @{#AI_PATROL_ZONE} class implements the core functions to patrol a @{Zone} by an AI @{Controllable} or @{Group}. --- --- ![Process](..\Presentations\AI_PATROL\Dia3.JPG) --- --- The AI_PATROL_ZONE is assigned a @{Group} and this must be done before the AI_PATROL_ZONE process can be started using the **Start** event. --- --- ![Process](..\Presentations\AI_PATROL\Dia4.JPG) --- --- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits. --- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. --- --- ![Process](..\Presentations\AI_PATROL\Dia5.JPG) --- --- This cycle will continue. --- --- ![Process](..\Presentations\AI_PATROL\Dia6.JPG) --- --- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event. --- --- ![Process](..\Presentations\AI_PATROL\Dia9.JPG) --- ----- Note that the enemy is not engaged! To model enemy engagement, either tailor the **Detected** event, or --- use derived AI_ classes to model AI offensive or defensive behaviour. --- --- ![Process](..\Presentations\AI_PATROL\Dia10.JPG) --- --- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. --- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. --- --- ![Process](..\Presentations\AI_PATROL\Dia11.JPG) --- --- ## 1.1) AI_PATROL_ZONE constructor --- --- * @{#AI_PATROL_ZONE.New}(): Creates a new AI_PATROL_ZONE object. --- --- ## 1.2) AI_PATROL_ZONE is a FSM --- --- ![Process](..\Presentations\AI_PATROL\Dia2.JPG) --- --- ### 1.2.1) AI_PATROL_ZONE States --- --- * **None** ( Group ): The process is not started yet. --- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. --- * **Returning** ( Group ): The AI is returning to Base. --- * **Stopped** ( Group ): The process is stopped. --- * **Crashed** ( Group ): The AI has crashed or is dead. --- --- ### 1.2.2) AI_PATROL_ZONE Events --- --- * **Start** ( Group ): Start the process. --- * **Stop** ( Group ): Stop the process. --- * **Route** ( Group ): Route the AI to a new random 3D point within the Patrol Zone. --- * **RTB** ( Group ): Route the AI to the home base. --- * **Detect** ( Group ): The AI is detecting targets. --- * **Detected** ( Group ): The AI has detected new targets. --- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. --- --- ## 1.3) Set or Get the AI controllable --- --- * @{#AI_PATROL_ZONE.SetControllable}(): Set the AIControllable. --- * @{#AI_PATROL_ZONE.GetControllable}(): Get the AIControllable. --- --- ## 1.4) Set the Speed and Altitude boundaries of the AI controllable --- --- * @{#AI_PATROL_ZONE.SetSpeed}(): Set the patrol speed boundaries of the AI, for the next patrol. --- * @{#AI_PATROL_ZONE.SetAltitude}(): Set altitude boundaries of the AI, for the next patrol. --- --- ## 1.5) Manage the detection process of the AI controllable --- --- The detection process of the AI controllable can be manipulated. --- Detection requires an amount of CPU power, which has an impact on your mission performance. --- Only put detection on when absolutely necessary, and the frequency of the detection can also be set. --- --- * @{#AI_PATROL_ZONE.SetDetectionOn}(): Set the detection on. The AI will detect for targets. --- * @{#AI_PATROL_ZONE.SetDetectionOff}(): Set the detection off, the AI will not detect for targets. The existing target list will NOT be erased. --- --- The detection frequency can be set with @{#AI_PATROL_ZONE.SetDetectionInterval}( seconds ), where the amount of seconds specify how much seconds will be waited before the next detection. --- Use the method @{#AI_PATROL_ZONE.GetDetectedUnits}() to obtain a list of the @{Unit}s detected by the AI. --- --- The detection can be filtered to potential targets in a specific zone. --- Use the method @{#AI_PATROL_ZONE.SetDetectionZone}() to set the zone where targets need to be detected. --- Note that when the zone is too far away, or the AI is not heading towards the zone, or the AI is too high, no targets may be detected --- according the weather conditions. --- --- ## 1.6) Manage the "out of fuel" in the AI_PATROL_ZONE --- --- When the AI is out of fuel, it is required that a new AI is started, before the old AI can return to the home base. --- Therefore, with a parameter and a calculation of the distance to the home base, the fuel treshold is calculated. --- When the fuel treshold is reached, the AI will continue for a given time its patrol task in orbit, --- while a new AI is targetted to the AI_PATROL_ZONE. --- Once the time is finished, the old AI will return to the base. --- Use the method @{#AI_PATROL_ZONE.ManageFuel}() to have this proces in place. --- --- ## 1.7) Manage "damage" behaviour of the AI in the AI_PATROL_ZONE --- --- When the AI is damaged, it is required that a new AIControllable is started. However, damage cannon be foreseen early on. --- Therefore, when the damage treshold is reached, the AI will return immediately to the home base (RTB). --- Use the method @{#AI_PATROL_ZONE.ManageDamage}() to have this proces in place. --- --- === --- --- @field #AI_PATROL_ZONE AI_PATROL_ZONE --- -AI_PATROL_ZONE = { - ClassName = "AI_PATROL_ZONE", -} - ---- Creates a new AI_PATROL_ZONE object --- @param #AI_PATROL_ZONE self --- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. --- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. --- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h. --- @param Dcs.DCSTypes#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO --- @return #AI_PATROL_ZONE self --- @usage --- -- Define a new AI_PATROL_ZONE Object. This PatrolArea will patrol an AIControllable within PatrolZone between 3000 and 6000 meters, with a variying speed between 600 and 900 km/h. --- PatrolZone = ZONE:New( 'PatrolZone' ) --- PatrolSpawn = SPAWN:New( 'Patrol Group' ) --- PatrolArea = AI_PATROL_ZONE:New( PatrolZone, 3000, 6000, 600, 900 ) -function AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) - - -- Inherits from BASE - local self = BASE:Inherit( self, FSM_CONTROLLABLE:New() ) -- #AI_PATROL_ZONE - - - self.PatrolZone = PatrolZone - self.PatrolFloorAltitude = PatrolFloorAltitude - self.PatrolCeilingAltitude = PatrolCeilingAltitude - self.PatrolMinSpeed = PatrolMinSpeed - self.PatrolMaxSpeed = PatrolMaxSpeed - - -- defafult PatrolAltType to "RADIO" if not specified - self.PatrolAltType = PatrolAltType or "RADIO" - - self:SetDetectionInterval( 30 ) - - self.CheckStatus = true - - self:ManageFuel( .2, 60 ) - self:ManageDamage( 1 ) - - - self.DetectedUnits = {} -- This table contains the targets detected during patrol. - - self:SetStartState( "None" ) - - self:AddTransition( "*", "Stop", "Stopped" ) - ---- OnLeave Transition Handler for State Stopped. --- @function [parent=#AI_PATROL_ZONE] OnLeaveStopped --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnEnter Transition Handler for State Stopped. --- @function [parent=#AI_PATROL_ZONE] OnEnterStopped --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- OnBefore Transition Handler for Event Stop. --- @function [parent=#AI_PATROL_ZONE] OnBeforeStop --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event Stop. --- @function [parent=#AI_PATROL_ZONE] OnAfterStop --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event Stop. --- @function [parent=#AI_PATROL_ZONE] Stop --- @param #AI_PATROL_ZONE self - ---- Asynchronous Event Trigger for Event Stop. --- @function [parent=#AI_PATROL_ZONE] __Stop --- @param #AI_PATROL_ZONE self --- @param #number Delay The delay in seconds. - - self:AddTransition( "None", "Start", "Patrolling" ) - ---- OnBefore Transition Handler for Event Start. --- @function [parent=#AI_PATROL_ZONE] OnBeforeStart --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event Start. --- @function [parent=#AI_PATROL_ZONE] OnAfterStart --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event Start. --- @function [parent=#AI_PATROL_ZONE] Start --- @param #AI_PATROL_ZONE self - ---- Asynchronous Event Trigger for Event Start. --- @function [parent=#AI_PATROL_ZONE] __Start --- @param #AI_PATROL_ZONE self --- @param #number Delay The delay in seconds. - ---- OnLeave Transition Handler for State Patrolling. --- @function [parent=#AI_PATROL_ZONE] OnLeavePatrolling --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnEnter Transition Handler for State Patrolling. --- @function [parent=#AI_PATROL_ZONE] OnEnterPatrolling --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - - self:AddTransition( "Patrolling", "Route", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. - ---- OnBefore Transition Handler for Event Route. --- @function [parent=#AI_PATROL_ZONE] OnBeforeRoute --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event Route. --- @function [parent=#AI_PATROL_ZONE] OnAfterRoute --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event Route. --- @function [parent=#AI_PATROL_ZONE] Route --- @param #AI_PATROL_ZONE self - ---- Asynchronous Event Trigger for Event Route. --- @function [parent=#AI_PATROL_ZONE] __Route --- @param #AI_PATROL_ZONE self --- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "Status", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. - ---- OnBefore Transition Handler for Event Status. --- @function [parent=#AI_PATROL_ZONE] OnBeforeStatus --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event Status. --- @function [parent=#AI_PATROL_ZONE] OnAfterStatus --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event Status. --- @function [parent=#AI_PATROL_ZONE] Status --- @param #AI_PATROL_ZONE self - ---- Asynchronous Event Trigger for Event Status. --- @function [parent=#AI_PATROL_ZONE] __Status --- @param #AI_PATROL_ZONE self --- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "Detect", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. - ---- OnBefore Transition Handler for Event Detect. --- @function [parent=#AI_PATROL_ZONE] OnBeforeDetect --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event Detect. --- @function [parent=#AI_PATROL_ZONE] OnAfterDetect --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event Detect. --- @function [parent=#AI_PATROL_ZONE] Detect --- @param #AI_PATROL_ZONE self - ---- Asynchronous Event Trigger for Event Detect. --- @function [parent=#AI_PATROL_ZONE] __Detect --- @param #AI_PATROL_ZONE self --- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "Detected", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. - ---- OnBefore Transition Handler for Event Detected. --- @function [parent=#AI_PATROL_ZONE] OnBeforeDetected --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event Detected. --- @function [parent=#AI_PATROL_ZONE] OnAfterDetected --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event Detected. --- @function [parent=#AI_PATROL_ZONE] Detected --- @param #AI_PATROL_ZONE self - ---- Asynchronous Event Trigger for Event Detected. --- @function [parent=#AI_PATROL_ZONE] __Detected --- @param #AI_PATROL_ZONE self --- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "RTB", "Returning" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. - ---- OnBefore Transition Handler for Event RTB. --- @function [parent=#AI_PATROL_ZONE] OnBeforeRTB --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event RTB. --- @function [parent=#AI_PATROL_ZONE] OnAfterRTB --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event RTB. --- @function [parent=#AI_PATROL_ZONE] RTB --- @param #AI_PATROL_ZONE self - ---- Asynchronous Event Trigger for Event RTB. --- @function [parent=#AI_PATROL_ZONE] __RTB --- @param #AI_PATROL_ZONE self --- @param #number Delay The delay in seconds. - ---- OnLeave Transition Handler for State Returning. --- @function [parent=#AI_PATROL_ZONE] OnLeaveReturning --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnEnter Transition Handler for State Returning. --- @function [parent=#AI_PATROL_ZONE] OnEnterReturning --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - - self:AddTransition( "*", "Reset", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE. - - self:AddTransition( "*", "Eject", "*" ) - self:AddTransition( "*", "Crash", "Crashed" ) - self:AddTransition( "*", "PilotDead", "*" ) - - return self -end - - - - ---- Sets (modifies) the minimum and maximum speed of the patrol. --- @param #AI_PATROL_ZONE self --- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. --- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h. --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetSpeed( PatrolMinSpeed, PatrolMaxSpeed ) - self:F2( { PatrolMinSpeed, PatrolMaxSpeed } ) - - self.PatrolMinSpeed = PatrolMinSpeed - self.PatrolMaxSpeed = PatrolMaxSpeed -end - - - ---- Sets the floor and ceiling altitude of the patrol. --- @param #AI_PATROL_ZONE self --- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetAltitude( PatrolFloorAltitude, PatrolCeilingAltitude ) - self:F2( { PatrolFloorAltitude, PatrolCeilingAltitude } ) - - self.PatrolFloorAltitude = PatrolFloorAltitude - self.PatrolCeilingAltitude = PatrolCeilingAltitude -end - --- * @{#AI_PATROL_ZONE.SetDetectionOn}(): Set the detection on. The AI will detect for targets. --- * @{#AI_PATROL_ZONE.SetDetectionOff}(): Set the detection off, the AI will not detect for targets. The existing target list will NOT be erased. - ---- Set the detection on. The AI will detect for targets. --- @param #AI_PATROL_ZONE self --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetDetectionOn() - self:F2() - - self.DetectOn = true -end - ---- Set the detection off. The AI will NOT detect for targets. --- However, the list of already detected targets will be kept and can be enquired! --- @param #AI_PATROL_ZONE self --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetDetectionOff() - self:F2() - - self.DetectOn = false -end - ---- Set the status checking off. --- @param #AI_PATROL_ZONE self --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetStatusOff() - self:F2() - - self.CheckStatus = false -end - ---- Activate the detection. The AI will detect for targets if the Detection is switched On. --- @param #AI_PATROL_ZONE self --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetDetectionActivated() - self:F2() - - self:ClearDetectedUnits() - self.DetectActivated = true - self:__Detect( -self.DetectInterval ) -end - ---- Deactivate the detection. The AI will NOT detect for targets. --- @param #AI_PATROL_ZONE self --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetDetectionDeactivated() - self:F2() - - self:ClearDetectedUnits() - self.DetectActivated = false -end - ---- Set the interval in seconds between each detection executed by the AI. --- The list of already detected targets will be kept and updated. --- Newly detected targets will be added, but already detected targets that were --- not detected in this cycle, will NOT be removed! --- The default interval is 30 seconds. --- @param #AI_PATROL_ZONE self --- @param #number Seconds The interval in seconds. --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetDetectionInterval( Seconds ) - self:F2() - - if Seconds then - self.DetectInterval = Seconds - else - self.DetectInterval = 30 - end -end - ---- Set the detection zone where the AI is detecting targets. --- @param #AI_PATROL_ZONE self --- @param Core.Zone#ZONE DetectionZone The zone where to detect targets. --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:SetDetectionZone( DetectionZone ) - self:F2() - - if DetectionZone then - self.DetectZone = DetectionZone - else - self.DetectZone = nil - end -end - ---- Gets a list of @{Unit#UNIT}s that were detected by the AI. --- No filtering is applied, so, ANY detected UNIT can be in this list. --- It is up to the mission designer to use the @{Unit} class and methods to filter the targets. --- @param #AI_PATROL_ZONE self --- @return #table The list of @{Unit#UNIT}s -function AI_PATROL_ZONE:GetDetectedUnits() - self:F2() - - return self.DetectedUnits -end - ---- Clears the list of @{Unit#UNIT}s that were detected by the AI. --- @param #AI_PATROL_ZONE self -function AI_PATROL_ZONE:ClearDetectedUnits() - self:F2() - self.DetectedUnits = {} -end - ---- When the AI is out of fuel, it is required that a new AI is started, before the old AI can return to the home base. --- Therefore, with a parameter and a calculation of the distance to the home base, the fuel treshold is calculated. --- When the fuel treshold is reached, the AI will continue for a given time its patrol task in orbit, while a new AIControllable is targetted to the AI_PATROL_ZONE. --- Once the time is finished, the old AI will return to the base. --- @param #AI_PATROL_ZONE self --- @param #number PatrolFuelTresholdPercentage The treshold in percentage (between 0 and 1) when the AIControllable is considered to get out of fuel. --- @param #number PatrolOutOfFuelOrbitTime The amount of seconds the out of fuel AIControllable will orbit before returning to the base. --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:ManageFuel( PatrolFuelTresholdPercentage, PatrolOutOfFuelOrbitTime ) - - self.PatrolManageFuel = true - self.PatrolFuelTresholdPercentage = PatrolFuelTresholdPercentage - self.PatrolOutOfFuelOrbitTime = PatrolOutOfFuelOrbitTime - - return self -end - ---- When the AI is damaged beyond a certain treshold, it is required that the AI returns to the home base. --- However, damage cannot be foreseen early on. --- Therefore, when the damage treshold is reached, --- the AI will return immediately to the home base (RTB). --- Note that for groups, the average damage of the complete group will be calculated. --- So, in a group of 4 airplanes, 2 lost and 2 with damage 0.2, the damage treshold will be 0.25. --- @param #AI_PATROL_ZONE self --- @param #number PatrolDamageTreshold The treshold in percentage (between 0 and 1) when the AI is considered to be damaged. --- @return #AI_PATROL_ZONE self -function AI_PATROL_ZONE:ManageDamage( PatrolDamageTreshold ) - - self.PatrolManageDamage = true - self.PatrolDamageTreshold = PatrolDamageTreshold - - return self -end - ---- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. --- @param #AI_PATROL_ZONE self --- @return #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_PATROL_ZONE:onafterStart( Controllable, From, Event, To ) - self:F2() - - self:__Route( 1 ) -- Route to the patrol point. The asynchronous trigger is important, because a spawned group and units takes at least one second to come live. - self:__Status( 60 ) -- Check status status every 30 seconds. - self:SetDetectionActivated() - - self:HandleEvent( EVENTS.PilotDead, self.OnPilotDead ) - self:HandleEvent( EVENTS.Crash, self.OnCrash ) - self:HandleEvent( EVENTS.Ejection, self.OnEjection ) - - Controllable:OptionROEHoldFire() - Controllable:OptionROTVertical() - - self.Controllable:OnReSpawn( - function( PatrolGroup ) - self:E( "ReSpawn" ) - self:__Reset( 1 ) - self:__Route( 5 ) - end - ) - - self:SetDetectionOn() - -end - - ---- @param #AI_PATROL_ZONE self ---- @param Wrapper.Controllable#CONTROLLABLE Controllable -function AI_PATROL_ZONE:onbeforeDetect( Controllable, From, Event, To ) - - return self.DetectOn and self.DetectActivated -end - ---- @param #AI_PATROL_ZONE self ---- @param Wrapper.Controllable#CONTROLLABLE Controllable -function AI_PATROL_ZONE:onafterDetect( Controllable, From, Event, To ) - - local Detected = false - - local DetectedTargets = Controllable:GetDetectedTargets() - for TargetID, Target in pairs( DetectedTargets or {} ) do - local TargetObject = Target.object - - if TargetObject and TargetObject:isExist() and TargetObject.id_ < 50000000 then - - local TargetUnit = UNIT:Find( TargetObject ) - local TargetUnitName = TargetUnit:GetName() - - if self.DetectionZone then - if TargetUnit:IsInZone( self.DetectionZone ) then - self:T( {"Detected ", TargetUnit } ) - if self.DetectedUnits[TargetUnit] == nil then - self.DetectedUnits[TargetUnit] = true - end - Detected = true - end - else - if self.DetectedUnits[TargetUnit] == nil then - self.DetectedUnits[TargetUnit] = true - end - Detected = true - end - end - end - - self:__Detect( -self.DetectInterval ) - - if Detected == true then - self:__Detected( 1.5 ) - end - -end - ---- @param Wrapper.Controllable#CONTROLLABLE AIControllable --- This statis method is called from the route path within the last task at the last waaypoint of the Controllable. --- Note that this method is required, as triggers the next route when patrolling for the Controllable. -function AI_PATROL_ZONE:_NewPatrolRoute( AIControllable ) - - local PatrolZone = AIControllable:GetState( AIControllable, "PatrolZone" ) -- PatrolCore.Zone#AI_PATROL_ZONE - PatrolZone:__Route( 1 ) -end - - ---- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. --- @param #AI_PATROL_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_PATROL_ZONE:onafterRoute( Controllable, From, Event, To ) - - self:F2() - - -- When RTB, don't allow anymore the routing. - if From == "RTB" then - return - end - - - if self.Controllable:IsAlive() then - -- Determine if the AIControllable is within the PatrolZone. - -- If not, make a waypoint within the to that the AIControllable will fly at maximum speed to that point. - - local PatrolRoute = {} - - -- Calculate the current route point of the controllable as the start point of the route. - -- However, when the controllable is not in the air, - -- the controllable current waypoint is probably the airbase... - -- Thus, if we would take the current waypoint as the startpoint, upon take-off, the controllable flies - -- immediately back to the airbase, and this is not correct. - -- Therefore, when on a runway, get as the current route point a random point within the PatrolZone. - -- This will make the plane fly immediately to the patrol zone. - - if self.Controllable:InAir() == false then - self:E( "Not in the air, finding route path within PatrolZone" ) - local CurrentVec2 = self.Controllable:GetVec2() - --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). - local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() - local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) - local ToPatrolZoneSpeed = self.PatrolMaxSpeed - local CurrentRoutePoint = CurrentPointVec3:RoutePointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TakeOffParking, - POINT_VEC3.RoutePointAction.FromParkingArea, - ToPatrolZoneSpeed, - true - ) - PatrolRoute[#PatrolRoute+1] = CurrentRoutePoint - else - self:E( "In the air, finding route path within PatrolZone" ) - local CurrentVec2 = self.Controllable:GetVec2() - --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). - local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() - local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) - local ToPatrolZoneSpeed = self.PatrolMaxSpeed - local CurrentRoutePoint = CurrentPointVec3:RoutePointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToPatrolZoneSpeed, - true - ) - PatrolRoute[#PatrolRoute+1] = CurrentRoutePoint - end - - - --- Define a random point in the @{Zone}. The AI will fly to that point within the zone. - - --- Find a random 2D point in PatrolZone. - local ToTargetVec2 = self.PatrolZone:GetRandomVec2() - self:T2( ToTargetVec2 ) - - --- Define Speed and Altitude. - local ToTargetAltitude = math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ) - local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) - self:T2( { self.PatrolMinSpeed, self.PatrolMaxSpeed, ToTargetSpeed } ) - - --- Obtain a 3D @{Point} from the 2D point + altitude. - local ToTargetPointVec3 = POINT_VEC3:New( ToTargetVec2.x, ToTargetAltitude, ToTargetVec2.y ) - - --- Create a route point of type air. - local ToTargetRoutePoint = ToTargetPointVec3:RoutePointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, - true - ) - - --self.CoordTest:SpawnFromVec3( ToTargetPointVec3:GetVec3() ) - - --ToTargetPointVec3:SmokeRed() - - PatrolRoute[#PatrolRoute+1] = ToTargetRoutePoint - - --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... - self.Controllable:WayPointInitialize( PatrolRoute ) - - --- Do a trick, link the NewPatrolRoute function of the PATROLGROUP object to the AIControllable in a temporary variable ... - self.Controllable:SetState( self.Controllable, "PatrolZone", self ) - self.Controllable:WayPointFunction( #PatrolRoute, 1, "AI_PATROL_ZONE:_NewPatrolRoute" ) - - --- NOW ROUTE THE GROUP! - self.Controllable:WayPointExecute( 1, 2 ) - end - -end - ---- @param #AI_PATROL_ZONE self -function AI_PATROL_ZONE:onbeforeStatus() - - return self.CheckStatus -end - ---- @param #AI_PATROL_ZONE self -function AI_PATROL_ZONE:onafterStatus() - self:F2() - - if self.Controllable and self.Controllable:IsAlive() then - - local RTB = false - - local Fuel = self.Controllable:GetUnit(1):GetFuel() - if Fuel < self.PatrolFuelTresholdPercentage then - self:E( self.Controllable:GetName() .. " is out of fuel:" .. Fuel .. ", RTB!" ) - local OldAIControllable = self.Controllable - local AIControllableTemplate = self.Controllable:GetTemplate() - - local OrbitTask = OldAIControllable:TaskOrbitCircle( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ), self.PatrolMinSpeed ) - local TimedOrbitTask = OldAIControllable:TaskControlled( OrbitTask, OldAIControllable:TaskCondition(nil,nil,nil,nil,self.PatrolOutOfFuelOrbitTime,nil ) ) - OldAIControllable:SetTask( TimedOrbitTask, 10 ) - - RTB = true - else - end - - -- TODO: Check GROUP damage function. - local Damage = self.Controllable:GetLife() - if Damage <= self.PatrolDamageTreshold then - self:E( self.Controllable:GetName() .. " is damaged:" .. Damage .. ", RTB!" ) - RTB = true - end - - if RTB == true then - self:RTB() - else - self:__Status( 60 ) -- Execute the Patrol event after 30 seconds. - end - end -end - ---- @param #AI_PATROL_ZONE self -function AI_PATROL_ZONE:onafterRTB() - self:F2() - - if self.Controllable and self.Controllable:IsAlive() then - - self:SetDetectionOff() - self.CheckStatus = false - - local PatrolRoute = {} - - --- Calculate the current route point. - local CurrentVec2 = self.Controllable:GetVec2() - - --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). - local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() - local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) - local ToPatrolZoneSpeed = self.PatrolMaxSpeed - local CurrentRoutePoint = CurrentPointVec3:RoutePointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToPatrolZoneSpeed, - true - ) - - PatrolRoute[#PatrolRoute+1] = CurrentRoutePoint - - --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... - self.Controllable:WayPointInitialize( PatrolRoute ) - - --- NOW ROUTE THE GROUP! - self.Controllable:WayPointExecute( 1, 1 ) - - end - -end - ---- @param #AI_PATROL_ZONE self -function AI_PATROL_ZONE:onafterDead() - self:SetDetectionOff() - self:SetStatusOff() -end - ---- @param #AI_PATROL_ZONE self --- @param Core.Event#EVENTDATA EventData -function AI_PATROL_ZONE:OnCrash( EventData ) - - if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then - self:E( self.Controllable:GetUnits() ) - if #self.Controllable:GetUnits() == 1 then - self:__Crash( 1, EventData ) - end - end -end - ---- @param #AI_PATROL_ZONE self --- @param Core.Event#EVENTDATA EventData -function AI_PATROL_ZONE:OnEjection( EventData ) - - if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then - self:__Eject( 1, EventData ) - end -end - ---- @param #AI_PATROL_ZONE self --- @param Core.Event#EVENTDATA EventData -function AI_PATROL_ZONE:OnPilotDead( EventData ) - - if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then - self:__PilotDead( 1, EventData ) - end -end ---- **AI** -- **Provide Close Air Support to friendly ground troops.** --- --- ![Banner Image](..\Presentations\AI_CAS\Dia1.JPG) --- --- === --- --- AI CAS classes makes AI Controllables execute a Close Air Support. --- --- There are the following types of CAS classes defined: --- --- * @{#AI_CAS_ZONE}: Perform a CAS in a 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. --- --- ### 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 - ---- # 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 --- --- * **@{AI_Patrol#AI_PATROL_ZONE.Start}**: Start the process. --- * **@{AI_Patrol#AI_PATROL_ZONE.Route}**: Route the AI to a new random 3D point within the Patrol Zone. --- * **@{#AI_CAS_ZONE.Engage}**: Engage the AI to provide CAS in the Engage Zone, destroying any target it finds. --- * **@{#AI_CAS_ZONE.Abort}**: Aborts the engagement and return patrolling in the patrol zone. --- * **@{AI_Patrol#AI_PATROL_ZONE.RTB}**: Route the AI to the home base. --- * **@{AI_Patrol#AI_PATROL_ZONE.Detect}**: The AI is detecting targets. --- * **@{AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets. --- * **@{#AI_CAS_ZONE.Destroy}**: The AI has destroyed a target @{Unit}. --- * **@{#AI_CAS_ZONE.Destroyed}**: The AI has destroyed all target @{Unit}s assigned in the CAS task. --- * **Status**: The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. --- --- === --- --- @field #AI_CAS_ZONE AI_CAS_ZONE --- -AI_CAS_ZONE = { - ClassName = "AI_CAS_ZONE", -} - - - ---- Creates a new AI_CAS_ZONE object --- @param #AI_CAS_ZONE self --- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. --- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. --- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h. --- @param Core.Zone#ZONE_BASE EngageZone The zone where the engage will happen. --- @param Dcs.DCSTypes#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO --- @return #AI_CAS_ZONE self -function AI_CAS_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageZone, PatrolAltType ) - - -- Inherits from BASE - local self = BASE:Inherit( self, AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_CAS_ZONE - - self.EngageZone = EngageZone - self.Accomplished = false - - self:SetDetectionZone( self.EngageZone ) - - self:AddTransition( { "Patrolling", "Engaging" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. - - --- OnBefore Transition Handler for Event Engage. - -- @function [parent=#AI_CAS_ZONE] OnBeforeEngage - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - -- @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 - -- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone. - -- @param Dcs.DCSTypes#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. - -- @param Dcs.DCSTypes#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. - -- If parameter is not defined the unit / controllable will choose expend on its own discretion. - -- Use the structure @{DCSTypes#AI.Task.WeaponExpend} to define the amount of weapons to be release at each attack. - -- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. - -- @param Dcs.DCSTypes#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. - - --- Asynchronous Event Trigger for Event Engage. - -- @function [parent=#AI_CAS_ZONE] __Engage - -- @param #AI_CAS_ZONE self - -- @param #number Delay The delay in seconds. - -- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone. - -- @param Dcs.DCSTypes#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. - -- @param Dcs.DCSTypes#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. - -- If parameter is not defined the unit / controllable will choose expend on its own discretion. - -- Use the structure @{DCSTypes#AI.Task.WeaponExpend} to define the amount of weapons to be release at each attack. - -- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. - -- @param Dcs.DCSTypes#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. - ---- OnLeave Transition Handler for State Engaging. --- @function [parent=#AI_CAS_ZONE] OnLeaveEngaging --- @param #AI_CAS_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnEnter Transition Handler for State Engaging. --- @function [parent=#AI_CAS_ZONE] OnEnterEngaging --- @param #AI_CAS_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - - self:AddTransition( "Engaging", "Target", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. - - self:AddTransition( "Engaging", "Fired", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. - - --- OnBefore Transition Handler for Event Fired. - -- @function [parent=#AI_CAS_ZONE] OnBeforeFired - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Fired. - -- @function [parent=#AI_CAS_ZONE] OnAfterFired - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Fired. - -- @function [parent=#AI_CAS_ZONE] Fired - -- @param #AI_CAS_ZONE self - - --- Asynchronous Event Trigger for Event Fired. - -- @function [parent=#AI_CAS_ZONE] __Fired - -- @param #AI_CAS_ZONE self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. - - --- OnBefore Transition Handler for Event Destroy. - -- @function [parent=#AI_CAS_ZONE] OnBeforeDestroy - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Destroy. - -- @function [parent=#AI_CAS_ZONE] OnAfterDestroy - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Destroy. - -- @function [parent=#AI_CAS_ZONE] Destroy - -- @param #AI_CAS_ZONE self - - --- Asynchronous Event Trigger for Event Destroy. - -- @function [parent=#AI_CAS_ZONE] __Destroy - -- @param #AI_CAS_ZONE self - -- @param #number Delay The delay in seconds. - - - self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. - - --- OnBefore Transition Handler for Event Abort. - -- @function [parent=#AI_CAS_ZONE] OnBeforeAbort - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Abort. - -- @function [parent=#AI_CAS_ZONE] OnAfterAbort - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Abort. - -- @function [parent=#AI_CAS_ZONE] Abort - -- @param #AI_CAS_ZONE self - - --- Asynchronous Event Trigger for Event Abort. - -- @function [parent=#AI_CAS_ZONE] __Abort - -- @param #AI_CAS_ZONE self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE. - - --- OnBefore Transition Handler for Event Accomplish. - -- @function [parent=#AI_CAS_ZONE] OnBeforeAccomplish - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Accomplish. - -- @function [parent=#AI_CAS_ZONE] OnAfterAccomplish - -- @param #AI_CAS_ZONE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Accomplish. - -- @function [parent=#AI_CAS_ZONE] Accomplish - -- @param #AI_CAS_ZONE self - - --- Asynchronous Event Trigger for Event Accomplish. - -- @function [parent=#AI_CAS_ZONE] __Accomplish - -- @param #AI_CAS_ZONE self - -- @param #number Delay The delay in seconds. - - return self -end - - ---- Set the Engage Zone where the AI is performing CAS. Note that if the EngageZone is changed, the AI needs to re-detect targets. --- @param #AI_CAS_ZONE self --- @param Core.Zone#ZONE EngageZone The zone where the AI is performing CAS. --- @return #AI_CAS_ZONE self -function AI_CAS_ZONE:SetEngageZone( EngageZone ) - self:F2() - - if EngageZone then - self.EngageZone = EngageZone - else - self.EngageZone = nil - end -end - - - ---- onafter State Transition for Event Start. --- @param #AI_CAS_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAS_ZONE:onafterStart( Controllable, From, Event, To ) - - -- Call the parent Start event handler - self:GetParent(self).onafterStart( self, Controllable, From, Event, To ) - self:HandleEvent( EVENTS.Dead ) - - self:SetDetectionDeactivated() -- When not engaging, set the detection off. -end - ---- @param Wrapper.Controllable#CONTROLLABLE AIControllable -function _NewEngageRoute( AIControllable ) - - AIControllable:T( "NewEngageRoute" ) - local EngageZone = AIControllable:GetState( AIControllable, "EngageZone" ) -- AI.AI_Cas#AI_CAS_ZONE - EngageZone:__Engage( 1, EngageZone.EngageSpeed, EngageZone.EngageAltitude, EngageZone.EngageWeaponExpend, EngageZone.EngageAttackQty, EngageZone.EngageDirection ) -end - - ---- @param #AI_CAS_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAS_ZONE:onbeforeEngage( Controllable, From, Event, To ) - - if self.Accomplished == true then - return false - end -end - ---- @param #AI_CAS_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAS_ZONE:onafterTarget( Controllable, From, Event, To ) - self:E("onafterTarget") - - if Controllable:IsAlive() then - - local AttackTasks = {} - - for DetectedUnit, Detected in pairs( self.DetectedUnits ) do - local DetectedUnit = DetectedUnit -- Wrapper.Unit#UNIT - if DetectedUnit:IsAlive() then - if DetectedUnit:IsInZone( self.EngageZone ) then - if Detected == true then - self:E( {"Target: ", DetectedUnit } ) - self.DetectedUnits[DetectedUnit] = false - local AttackTask = Controllable:TaskAttackUnit( DetectedUnit, false, self.EngageWeaponExpend, self.EngageAttackQty, self.EngageDirection, self.EngageAltitude, nil ) - self.Controllable:PushTask( AttackTask, 1 ) - end - end - else - self.DetectedUnits[DetectedUnit] = nil - end - end - - self:__Target( -10 ) - - end -end - - ---- @param #AI_CAS_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAS_ZONE:onafterAbort( Controllable, From, Event, To ) - Controllable:ClearTasks() - self:__Route( 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. --- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone. --- @param Dcs.DCSTypes#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. --- @param Dcs.DCSTypes#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. --- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. --- @param Dcs.DCSTypes#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. -function AI_CAS_ZONE:onafterEngage( Controllable, From, Event, To, - EngageSpeed, - EngageAltitude, - EngageWeaponExpend, - EngageAttackQty, - EngageDirection ) - self:F("onafterEngage") - - self.EngageSpeed = EngageSpeed or 400 - self.EngageAltitude = EngageAltitude or 2000 - self.EngageWeaponExpend = EngageWeaponExpend - self.EngageAttackQty = EngageAttackQty - self.EngageDirection = EngageDirection - - if Controllable:IsAlive() then - - local EngageRoute = {} - - --- Calculate the current route point. - local CurrentVec2 = self.Controllable:GetVec2() - - --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). - local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() - local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) - local ToEngageZoneSpeed = self.PatrolMaxSpeed - local CurrentRoutePoint = CurrentPointVec3:RoutePointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - self.EngageSpeed, - true - ) - - EngageRoute[#EngageRoute+1] = CurrentRoutePoint - - 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, - true, - EngageWeaponExpend, - EngageAttackQty, - EngageDirection - ) - end - else - self.DetectedUnits[DetectedUnit] = nil - end - end - - EngageRoute[1].task = Controllable:TaskCombo( AttackTasks ) - - --- Define a random point in the @{Zone}. The AI will fly to that point within the zone. - - --- Find a random 2D point in EngageZone. - local ToTargetVec2 = self.EngageZone:GetRandomVec2() - self:T2( ToTargetVec2 ) - - --- Obtain a 3D @{Point} from the 2D point + altitude. - local ToTargetPointVec3 = POINT_VEC3:New( ToTargetVec2.x, self.EngageAltitude, ToTargetVec2.y ) - - --- Create a route point of type air. - local ToTargetRoutePoint = ToTargetPointVec3:RoutePointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - self.EngageSpeed, - true - ) - - EngageRoute[#EngageRoute+1] = ToTargetRoutePoint - - --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... - Controllable:WayPointInitialize( EngageRoute ) - - --- Do a trick, link the NewEngageRoute function of the object to the AIControllable in a temporary variable ... - Controllable:SetState( Controllable, "EngageZone", self ) - - Controllable:WayPointFunction( #EngageRoute, 1, "_NewEngageRoute" ) - - --- NOW ROUTE THE GROUP! - Controllable:WayPointExecute( 1 ) - - Controllable:OptionROEOpenFire() - Controllable:OptionROTVertical() - - self:SetDetectionInterval( 2 ) - self:SetDetectionActivated() - self:__Target( -2 ) -- Start Targetting - end -end - - ---- @param #AI_CAS_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAS_ZONE:onafterAccomplish( Controllable, From, Event, To ) - self.Accomplished = true - self:SetDetectionDeactivated() -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 -end - - ---- @param #AI_CAS_ZONE self --- @param Core.Event#EVENTDATA EventData -function AI_CAS_ZONE:OnEventDead( EventData ) - self:F( { "EventDead", EventData } ) - - if EventData.IniDCSUnit then - if self.DetectedUnits and self.DetectedUnits[EventData.IniUnit] then - self:__Destroy( 1, EventData ) - end - end -end - - ---- **AI** - **Execute Combat Air Patrol (CAP).** --- --- ![Banner Image](..\Presentations\AI_CAP\Dia1.JPG) --- --- === --- --- AI CAP classes makes AI Controllables execute a Combat Air Patrol. --- --- There are the following types of CAP classes defined: --- --- * @{#AI_CAP_ZONE}: Perform a CAP in a 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 - - ---- @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 - - ---- # 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 --- --- * **@{AI_Patrol#AI_PATROL_ZONE.Start}**: Start the process. --- * **@{AI_Patrol#AI_PATROL_ZONE.Route}**: Route the AI to a new random 3D point within the Patrol Zone. --- * **@{#AI_CAP_ZONE.Engage}**: Let the AI engage the bogeys. --- * **@{#AI_CAP_ZONE.Abort}**: Aborts the engagement and return patrolling in the patrol zone. --- * **@{AI_Patrol#AI_PATROL_ZONE.RTB}**: Route the AI to the home base. --- * **@{AI_Patrol#AI_PATROL_ZONE.Detect}**: The AI is detecting targets. --- * **@{AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets. --- * **@{#AI_CAP_ZONE.Destroy}**: The AI has destroyed a bogey @{Unit}. --- * **@{#AI_CAP_ZONE.Destroyed}**: The AI has destroyed all bogeys @{Unit}s assigned in the CAS task. --- * **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. --- --- === --- --- @field #AI_CAP_ZONE AI_CAP_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 ) - self:HandleEvent( EVENTS.Dead ) - -end - --- todo: need to fix this global function - ---- @param Wrapper.Controllable#CONTROLLABLE AIControllable -function _NewEngageCapRoute( AIControllable ) - - AIControllable:T( "NewEngageRoute" ) - local EngageZone = AIControllable:GetState( AIControllable, "EngageZone" ) -- AI.AI_Cap#AI_CAP_ZONE - EngageZone:__Engage( 1 ) -end - ---- @param #AI_CAP_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAP_ZONE:onbeforeEngage( Controllable, From, Event, To ) - - if self.Accomplished == true then - return false - end -end - ---- @param #AI_CAP_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAP_ZONE:onafterDetected( Controllable, From, Event, To ) - - if From ~= "Engaging" then - - local Engage = false - - for DetectedUnit, Detected in pairs( self.DetectedUnits ) do - - local DetectedUnit = DetectedUnit -- Wrapper.Unit#UNIT - self:T( DetectedUnit ) - if DetectedUnit:IsAlive() and DetectedUnit:IsAir() then - Engage = true - break - end - end - - if Engage == true then - self:E( 'Detected -> Engaging' ) - self:__Engage( 1 ) - end - end -end - - ---- @param #AI_CAP_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAP_ZONE:onafterAbort( Controllable, From, Event, To ) - Controllable:ClearTasks() - self:__Route( 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:onafterEngage( Controllable, From, Event, To ) - - if Controllable:IsAlive() then - - local EngageRoute = {} - - --- Calculate the current route point. - local CurrentVec2 = self.Controllable:GetVec2() - - --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). - local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() - local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) - local ToEngageZoneSpeed = self.PatrolMaxSpeed - local CurrentRoutePoint = CurrentPointVec3:RoutePointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToEngageZoneSpeed, - true - ) - - EngageRoute[#EngageRoute+1] = CurrentRoutePoint - - - --- Find a random 2D point in PatrolZone. - local ToTargetVec2 = self.PatrolZone:GetRandomVec2() - self:T2( ToTargetVec2 ) - - --- Define Speed and Altitude. - local ToTargetAltitude = math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) - local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) - self:T2( { self.PatrolMinSpeed, self.PatrolMaxSpeed, ToTargetSpeed } ) - - --- Obtain a 3D @{Point} from the 2D point + altitude. - local ToTargetPointVec3 = POINT_VEC3:New( ToTargetVec2.x, ToTargetAltitude, ToTargetVec2.y ) - - --- Create a route point of type air. - local ToPatrolRoutePoint = ToTargetPointVec3:RoutePointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, - true - ) - - EngageRoute[#EngageRoute+1] = ToPatrolRoutePoint - - Controllable:OptionROEOpenFire() - Controllable:OptionROTPassiveDefense() - - local AttackTasks = {} - - for DetectedUnit, Detected in pairs( self.DetectedUnits ) do - local DetectedUnit = DetectedUnit -- Wrapper.Unit#UNIT - self:T( { DetectedUnit, DetectedUnit:IsAlive(), DetectedUnit:IsAir() } ) - if DetectedUnit:IsAlive() and DetectedUnit:IsAir() then - if self.EngageZone then - if DetectedUnit:IsInZone( self.EngageZone ) then - self:E( {"Within Zone and Engaging ", DetectedUnit } ) - AttackTasks[#AttackTasks+1] = Controllable:TaskAttackUnit( DetectedUnit ) - end - else - if self.EngageRange then - if DetectedUnit:GetPointVec3():Get2DDistance(Controllable:GetPointVec3() ) <= self.EngageRange then - self:E( {"Within Range and Engaging", DetectedUnit } ) - AttackTasks[#AttackTasks+1] = Controllable:TaskAttackUnit( DetectedUnit ) - end - else - AttackTasks[#AttackTasks+1] = Controllable:TaskAttackUnit( DetectedUnit ) - end - end - else - self.DetectedUnits[DetectedUnit] = nil - end - end - - --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... - self.Controllable:WayPointInitialize( EngageRoute ) - - - if #AttackTasks == 0 then - self:E("No targets found -> Going back to Patrolling") - self:__Abort( 1 ) - self:__Route( 1 ) - self:SetDetectionActivated() - else - EngageRoute[1].task = Controllable:TaskCombo( AttackTasks ) - - --- Do a trick, link the NewEngageRoute function of the object to the AIControllable in a temporary variable ... - self.Controllable:SetState( self.Controllable, "EngageZone", self ) - - self.Controllable:WayPointFunction( #EngageRoute, 1, "_NewEngageCapRoute" ) - - self:SetDetectionDeactivated() - end - - --- NOW ROUTE THE GROUP! - self.Controllable:WayPointExecute( 1, 2 ) - - end -end - ---- @param #AI_CAP_ZONE self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_CAP_ZONE:onafterAccomplish( Controllable, From, Event, To ) - self.Accomplished = true - self:SetDetectionOff() -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 -end - ---- @param #AI_CAP_ZONE self --- @param Core.Event#EVENTDATA EventData -function AI_CAP_ZONE:OnEventDead( EventData ) - self:F( { "EventDead", EventData } ) - - if EventData.IniDCSUnit then - if self.DetectedUnits and self.DetectedUnits[EventData.IniUnit] then - self:__Destroy( 1, EventData ) - end - end -end ----Single-Player:**Yes** / Multi-Player:**Yes** / AI:**Yes** / Human:**No** / Types:**Ground** -- --- **Management of logical cargo objects, that can be transported from and to transportation carriers.** --- --- ![Banner Image](..\Presentations\AI_CARGO\CARGO.JPG) --- --- === --- --- Cargo can be of various forms, always are composed out of ONE object ( one unit or one static or one slingload crate ): --- --- * AI_CARGO_UNIT, represented by a @{Unit} in a @{Group}: Cargo can be represented by a Unit in a Group. Destruction of the Unit will mean that the cargo is lost. --- * CARGO_STATIC, represented by a @{Static}: Cargo can be represented by a Static. Destruction of the Static will mean that the cargo is lost. --- * AI_CARGO_PACKAGE, contained in a @{Unit} of a @{Group}: Cargo can be contained within a Unit of a Group. The cargo can be **delivered** by the @{Unit}. If the Unit is destroyed, the cargo will be destroyed also. --- * AI_CARGO_PACKAGE, Contained in a @{Static}: Cargo can be contained within a Static. The cargo can be **collected** from the @Static. If the @{Static} is destroyed, the cargo will be destroyed. --- * CARGO_SLINGLOAD, represented by a @{Cargo} that is transportable: Cargo can be represented by a Cargo object that is transportable. Destruction of the Cargo will mean that the cargo is lost. --- --- * AI_CARGO_GROUPED, represented by a Group of CARGO_UNITs. --- --- # 1) @{#AI_CARGO} class, extends @{Fsm#FSM_PROCESS} --- --- The @{#AI_CARGO} class defines the core functions that defines a cargo object within MOOSE. --- A cargo is a logical object defined that is available for transport, and has a life status within a simulation. --- --- The AI_CARGO is a state machine: it manages the different events and states of the cargo. --- All derived classes from AI_CARGO follow the same state machine, expose the same cargo event functions, and provide the same cargo states. --- --- ## 1.2.1) AI_CARGO Events: --- --- * @{#AI_CARGO.Board}( ToCarrier ): Boards the cargo to a carrier. --- * @{#AI_CARGO.Load}( ToCarrier ): Loads the cargo into a carrier, regardless of its position. --- * @{#AI_CARGO.UnBoard}( ToPointVec2 ): UnBoard the cargo from a carrier. This will trigger a movement of the cargo to the option ToPointVec2. --- * @{#AI_CARGO.UnLoad}( ToPointVec2 ): UnLoads the cargo from a carrier. --- * @{#AI_CARGO.Dead}( Controllable ): The cargo is dead. The cargo process will be ended. --- --- ## 1.2.2) AI_CARGO States: --- --- * **UnLoaded**: The cargo is unloaded from a carrier. --- * **Boarding**: The cargo is currently boarding (= running) into a carrier. --- * **Loaded**: The cargo is loaded into a carrier. --- * **UnBoarding**: The cargo is currently unboarding (=running) from a carrier. --- * **Dead**: The cargo is dead ... --- * **End**: The process has come to an end. --- --- ## 1.2.3) AI_CARGO state transition methods: --- --- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. --- There are 2 moments when state transition methods will be called by the state machine: --- --- * **Leaving** the state. --- The state transition method needs to start with the name **OnLeave + the name of the state**. --- If the state transition method returns false, then the processing of the state transition will not be done! --- If you want to change the behaviour of the AIControllable at this event, return false, --- but then you'll need to specify your own logic using the AIControllable! --- --- * **Entering** the state. --- The state transition method needs to start with the name **OnEnter + the name of the state**. --- These state transition methods need to provide a return value, which is specified at the function description. --- --- # 2) #AI_CARGO_UNIT class --- --- The AI_CARGO_UNIT class defines a cargo that is represented by a UNIT object within the simulator, and can be transported by a carrier. --- Use the event functions as described above to Load, UnLoad, Board, UnBoard the AI_CARGO_UNIT objects to and from carriers. --- --- # 5) #AI_CARGO_GROUPED class --- --- The AI_CARGO_GROUPED class defines a cargo that is represented by a group of UNIT objects within the simulator, and can be transported by a carrier. --- Use the event functions as described above to Load, UnLoad, Board, UnBoard the AI_CARGO_UNIT objects to and from carriers. --- --- This module is still under construction, but is described above works already, and will keep working ... --- --- @module Cargo - --- Events - --- Board - ---- Boards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo to the Carrier. --- The cargo must be in the **UnLoaded** state. --- @function [parent=#AI_CARGO] Board --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. - ---- Boards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo to the Carrier. --- The cargo must be in the **UnLoaded** state. --- @function [parent=#AI_CARGO] __Board --- @param #AI_CARGO self --- @param #number DelaySeconds The amount of seconds to delay the action. --- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. - - --- UnBoard - ---- UnBoards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo from the Carrier. --- The cargo must be in the **Loaded** state. --- @function [parent=#AI_CARGO] UnBoard --- @param #AI_CARGO self --- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Point#POINT_VEC2) to where the cargo should run after onboarding. If not provided, the cargo will run to 60 meters behind the Carrier location. - ---- UnBoards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo from the Carrier. --- The cargo must be in the **Loaded** state. --- @function [parent=#AI_CARGO] __UnBoard --- @param #AI_CARGO self --- @param #number DelaySeconds The amount of seconds to delay the action. --- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Point#POINT_VEC2) to where the cargo should run after onboarding. If not provided, the cargo will run to 60 meters behind the Carrier location. - - --- Load - ---- Loads the cargo to a Carrier. The event will load the cargo into the Carrier regardless of its position. There will be no movement simulated of the cargo loading. --- The cargo must be in the **UnLoaded** state. --- @function [parent=#AI_CARGO] Load --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. - ---- Loads the cargo to a Carrier. The event will load the cargo into the Carrier regardless of its position. There will be no movement simulated of the cargo loading. --- The cargo must be in the **UnLoaded** state. --- @function [parent=#AI_CARGO] __Load --- @param #AI_CARGO self --- @param #number DelaySeconds The amount of seconds to delay the action. --- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. - - --- UnLoad - ---- UnLoads the cargo to a Carrier. The event will unload the cargo from the Carrier. There will be no movement simulated of the cargo loading. --- The cargo must be in the **Loaded** state. --- @function [parent=#AI_CARGO] UnLoad --- @param #AI_CARGO self --- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Point#POINT_VEC2) to where the cargo will be placed after unloading. If not provided, the cargo will be placed 60 meters behind the Carrier location. - ---- UnLoads the cargo to a Carrier. The event will unload the cargo from the Carrier. There will be no movement simulated of the cargo loading. --- The cargo must be in the **Loaded** state. --- @function [parent=#AI_CARGO] __UnLoad --- @param #AI_CARGO self --- @param #number DelaySeconds The amount of seconds to delay the action. --- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Point#POINT_VEC2) to where the cargo will be placed after unloading. If not provided, the cargo will be placed 60 meters behind the Carrier location. - --- State Transition Functions - --- UnLoaded - ---- @function [parent=#AI_CARGO] OnLeaveUnLoaded --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable --- @return #boolean - ---- @function [parent=#AI_CARGO] OnEnterUnLoaded --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable - --- Loaded - ---- @function [parent=#AI_CARGO] OnLeaveLoaded --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable --- @return #boolean - ---- @function [parent=#AI_CARGO] OnEnterLoaded --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable - --- Boarding - ---- @function [parent=#AI_CARGO] OnLeaveBoarding --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable --- @return #boolean - ---- @function [parent=#AI_CARGO] OnEnterBoarding --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable - --- UnBoarding - ---- @function [parent=#AI_CARGO] OnLeaveUnBoarding --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable --- @return #boolean - ---- @function [parent=#AI_CARGO] OnEnterUnBoarding --- @param #AI_CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable - - --- TODO: Find all Carrier objects and make the type of the Carriers Wrapper.Unit#UNIT in the documentation. - -CARGOS = {} - -do -- AI_CARGO - - --- @type AI_CARGO - -- @extends Core.Fsm#FSM_PROCESS - -- @field #string Type A string defining the type of the cargo. eg. Engineers, Equipment, Screwdrivers. - -- @field #string Name A string defining the name of the cargo. The name is the unique identifier of the cargo. - -- @field #number Weight A number defining the weight of the cargo. The weight is expressed in kg. - -- @field #number ReportRadius (optional) A number defining the radius in meters when the cargo is signalling or reporting to a Carrier. - -- @field #number NearRadius (optional) A number defining the radius in meters when the cargo is near to a Carrier, so that it can be loaded. - -- @field Wrapper.Controllable#CONTROLLABLE CargoObject The alive DCS object representing the cargo. This value can be nil, meaning, that the cargo is not represented anywhere... - -- @field Wrapper.Controllable#CONTROLLABLE CargoCarrier The alive DCS object carrying the cargo. This value can be nil, meaning, that the cargo is not contained anywhere... - -- @field #boolean Slingloadable This flag defines if the cargo can be slingloaded. - -- @field #boolean Moveable This flag defines if the cargo is moveable. - -- @field #boolean Representable This flag defines if the cargo can be represented by a DCS Unit. - -- @field #boolean Containable This flag defines if the cargo can be contained within a DCS Unit. - AI_CARGO = { - ClassName = "AI_CARGO", - Type = nil, - Name = nil, - Weight = nil, - CargoObject = nil, - CargoCarrier = nil, - Representable = false, - Slingloadable = false, - Moveable = false, - Containable = false, - } - ---- @type AI_CARGO.CargoObjects --- @map < #string, Wrapper.Positionable#POSITIONABLE > The alive POSITIONABLE objects representing the the cargo. - - ---- AI_CARGO Constructor. This class is an abstract class and should not be instantiated. --- @param #AI_CARGO self --- @param #string Type --- @param #string Name --- @param #number Weight --- @param #number ReportRadius (optional) --- @param #number NearRadius (optional) --- @return #AI_CARGO -function AI_CARGO:New( Type, Name, Weight, ReportRadius, NearRadius ) - - local self = BASE:Inherit( self, FSM:New() ) -- Core.Fsm#FSM_CONTROLLABLE - self:F( { Type, Name, Weight, ReportRadius, NearRadius } ) - - self:SetStartState( "UnLoaded" ) - self:AddTransition( "UnLoaded", "Board", "Boarding" ) - self:AddTransition( "Boarding", "Boarding", "Boarding" ) - self:AddTransition( "Boarding", "Load", "Loaded" ) - self:AddTransition( "UnLoaded", "Load", "Loaded" ) - self:AddTransition( "Loaded", "UnBoard", "UnBoarding" ) - self:AddTransition( "UnBoarding", "UnBoarding", "UnBoarding" ) - self:AddTransition( "UnBoarding", "UnLoad", "UnLoaded" ) - self:AddTransition( "Loaded", "UnLoad", "UnLoaded" ) - - - self.Type = Type - self.Name = Name - self.Weight = Weight - self.ReportRadius = ReportRadius - self.NearRadius = NearRadius - self.CargoObject = nil - self.CargoCarrier = nil - self.Representable = false - self.Slingloadable = false - self.Moveable = false - self.Containable = false - - - self.CargoScheduler = SCHEDULER:New() - - CARGOS[self.Name] = self - - return self -end - - ---- Template method to spawn a new representation of the AI_CARGO in the simulator. --- @param #AI_CARGO self --- @return #AI_CARGO -function AI_CARGO:Spawn( PointVec2 ) - self:F() - -end - - ---- Check if CargoCarrier is near the Cargo to be Loaded. --- @param #AI_CARGO self --- @param Core.Point#POINT_VEC2 PointVec2 --- @return #boolean -function AI_CARGO:IsNear( PointVec2 ) - self:F( { PointVec2 } ) - - local Distance = PointVec2:DistanceFromPointVec2( self.CargoObject:GetPointVec2() ) - self:T( Distance ) - - if Distance <= self.NearRadius then - return true - else - return false - end -end - -end - -do -- AI_CARGO_REPRESENTABLE - - --- @type AI_CARGO_REPRESENTABLE - -- @extends #AI_CARGO - AI_CARGO_REPRESENTABLE = { - ClassName = "AI_CARGO_REPRESENTABLE" - } - ---- AI_CARGO_REPRESENTABLE Constructor. --- @param #AI_CARGO_REPRESENTABLE self --- @param Wrapper.Controllable#Controllable CargoObject --- @param #string Type --- @param #string Name --- @param #number Weight --- @param #number ReportRadius (optional) --- @param #number NearRadius (optional) --- @return #AI_CARGO_REPRESENTABLE -function AI_CARGO_REPRESENTABLE:New( CargoObject, Type, Name, Weight, ReportRadius, NearRadius ) - local self = BASE:Inherit( self, AI_CARGO:New( Type, Name, Weight, ReportRadius, NearRadius ) ) -- #AI_CARGO - self:F( { Type, Name, Weight, ReportRadius, NearRadius } ) - - return self -end - ---- Route a cargo unit to a PointVec2. --- @param #AI_CARGO_REPRESENTABLE self --- @param Core.Point#POINT_VEC2 ToPointVec2 --- @param #number Speed --- @return #AI_CARGO_REPRESENTABLE -function AI_CARGO_REPRESENTABLE:RouteTo( ToPointVec2, Speed ) - self:F2( ToPointVec2 ) - - local Points = {} - - local PointStartVec2 = self.CargoObject:GetPointVec2() - - Points[#Points+1] = PointStartVec2:RoutePointGround( Speed ) - Points[#Points+1] = ToPointVec2:RoutePointGround( Speed ) - - local TaskRoute = self.CargoObject:TaskRoute( Points ) - self.CargoObject:SetTask( TaskRoute, 2 ) - return self -end - -end -- AI_CARGO - -do -- AI_CARGO_UNIT - - --- @type AI_CARGO_UNIT - -- @extends #AI_CARGO_REPRESENTABLE - AI_CARGO_UNIT = { - ClassName = "AI_CARGO_UNIT" - } - ---- AI_CARGO_UNIT Constructor. --- @param #AI_CARGO_UNIT self --- @param Wrapper.Unit#UNIT CargoUnit --- @param #string Type --- @param #string Name --- @param #number Weight --- @param #number ReportRadius (optional) --- @param #number NearRadius (optional) --- @return #AI_CARGO_UNIT -function AI_CARGO_UNIT:New( CargoUnit, Type, Name, Weight, ReportRadius, NearRadius ) - local self = BASE:Inherit( self, AI_CARGO_REPRESENTABLE:New( CargoUnit, Type, Name, Weight, ReportRadius, NearRadius ) ) -- #AI_CARGO_UNIT - self:F( { Type, Name, Weight, ReportRadius, NearRadius } ) - - self:T( CargoUnit ) - self.CargoObject = CargoUnit - - self:T( self.ClassName ) - - return self -end - ---- Enter UnBoarding State. --- @param #AI_CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To --- @param Core.Point#POINT_VEC2 ToPointVec2 -function AI_CARGO_UNIT:onenterUnBoarding( From, Event, To, ToPointVec2 ) - self:F() - - local Angle = 180 - local Speed = 10 - local DeployDistance = 5 - local RouteDistance = 60 - - if From == "Loaded" then - - local CargoCarrierPointVec2 = self.CargoCarrier:GetPointVec2() - local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - local CargoDeployPointVec2 = CargoCarrierPointVec2:Translate( DeployDistance, CargoDeployHeading ) - local CargoRoutePointVec2 = CargoCarrierPointVec2:Translate( RouteDistance, CargoDeployHeading ) - - -- if there is no ToPointVec2 given, then use the CargoRoutePointVec2 - ToPointVec2 = ToPointVec2 or CargoRoutePointVec2 - - local FromPointVec2 = CargoCarrierPointVec2 - - -- Respawn the group... - if self.CargoObject then - self.CargoObject:ReSpawn( CargoDeployPointVec2:GetVec3(), CargoDeployHeading ) - self.CargoCarrier = nil - - local Points = {} - Points[#Points+1] = FromPointVec2:RoutePointGround( Speed ) - Points[#Points+1] = ToPointVec2:RoutePointGround( Speed ) - - local TaskRoute = self.CargoObject:TaskRoute( Points ) - self.CargoObject:SetTask( TaskRoute, 1 ) - - self:__UnBoarding( 1, ToPointVec2 ) - end - end - -end - ---- Leave UnBoarding State. --- @param #AI_CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To --- @param Core.Point#POINT_VEC2 ToPointVec2 -function AI_CARGO_UNIT:onleaveUnBoarding( From, Event, To, ToPointVec2 ) - self:F( { ToPointVec2, From, Event, To } ) - - local Angle = 180 - local Speed = 10 - local Distance = 5 - - if From == "UnBoarding" then - if self:IsNear( ToPointVec2 ) then - return true - else - self:__UnBoarding( 1, ToPointVec2 ) - end - return false - end - -end - ---- UnBoard Event. --- @param #AI_CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To --- @param Core.Point#POINT_VEC2 ToPointVec2 -function AI_CARGO_UNIT:onafterUnBoarding( From, Event, To, ToPointVec2 ) - self:F( { ToPointVec2, From, Event, To } ) - - self.CargoInAir = self.CargoObject:InAir() - - self:T( self.CargoInAir ) - - -- Only unboard the cargo when the carrier is not in the air. - -- (eg. cargo can be on a oil derrick, moving the cargo on the oil derrick will drop the cargo on the sea). - if not self.CargoInAir then - - end - - self:__UnLoad( 1, ToPointVec2 ) - -end - - - ---- Enter UnLoaded State. --- @param #AI_CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To --- @param Core.Point#POINT_VEC2 -function AI_CARGO_UNIT:onenterUnLoaded( From, Event, To, ToPointVec2 ) - self:F( { ToPointVec2, From, Event, To } ) - - local Angle = 180 - local Speed = 10 - local Distance = 5 - - if From == "Loaded" then - local StartPointVec2 = self.CargoCarrier:GetPointVec2() - local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - local CargoDeployPointVec2 = StartPointVec2:Translate( Distance, CargoDeployHeading ) - - ToPointVec2 = ToPointVec2 or POINT_VEC2:New( CargoDeployPointVec2:GetX(), CargoDeployPointVec2:GetY() ) - - -- Respawn the group... - if self.CargoObject then - self.CargoObject:ReSpawn( ToPointVec2:GetVec3(), 0 ) - self.CargoCarrier = nil - end - - end - - if self.OnUnLoadedCallBack then - self.OnUnLoadedCallBack( self, unpack( self.OnUnLoadedParameters ) ) - self.OnUnLoadedCallBack = nil - end - -end - - - ---- Enter Boarding State. --- @param #AI_CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT CargoCarrier -function AI_CARGO_UNIT:onenterBoarding( From, Event, To, CargoCarrier ) - self:F( { CargoCarrier.UnitName, From, Event, To } ) - - local Speed = 10 - local Angle = 180 - local Distance = 5 - - if From == "UnLoaded" then - local CargoCarrierPointVec2 = CargoCarrier:GetPointVec2() - local CargoCarrierHeading = CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - local CargoDeployPointVec2 = CargoCarrierPointVec2:Translate( Distance, CargoDeployHeading ) - - local Points = {} - - local PointStartVec2 = self.CargoObject:GetPointVec2() - - Points[#Points+1] = PointStartVec2:RoutePointGround( Speed ) - Points[#Points+1] = CargoDeployPointVec2:RoutePointGround( Speed ) - - local TaskRoute = self.CargoObject:TaskRoute( Points ) - self.CargoObject:SetTask( TaskRoute, 2 ) - end - -end - ---- Leave Boarding State. --- @param #AI_CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT CargoCarrier -function AI_CARGO_UNIT:onleaveBoarding( From, Event, To, CargoCarrier ) - self:F( { CargoCarrier.UnitName, From, Event, To } ) - - if self:IsNear( CargoCarrier:GetPointVec2() ) then - self:__Load( 1, CargoCarrier ) - return true - else - self:__Boarding( 1, CargoCarrier ) - end - return false -end - ---- Loaded State. --- @param #AI_CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT CargoCarrier -function AI_CARGO_UNIT:onenterLoaded( From, Event, To, CargoCarrier ) - self:F() - - self.CargoCarrier = CargoCarrier - - -- Only destroy the CargoObject is if there is a CargoObject (packages don't have CargoObjects). - if self.CargoObject then - self:T("Destroying") - self.CargoObject:Destroy() - end -end - - ---- Board Event. --- @param #AI_CARGO_UNIT self --- @param #string Event --- @param #string From --- @param #string To -function AI_CARGO_UNIT:onafterBoard( From, Event, To, CargoCarrier ) - self:F() - - self.CargoInAir = self.CargoObject:InAir() - - self:T( self.CargoInAir ) - - -- Only move the group to the carrier when the cargo is not in the air - -- (eg. cargo can be on a oil derrick, moving the cargo on the oil derrick will drop the cargo on the sea). - if not self.CargoInAir then - self:Load( CargoCarrier ) - end - -end - -end - -do -- AI_CARGO_PACKAGE - - --- @type AI_CARGO_PACKAGE - -- @extends #AI_CARGO_REPRESENTABLE - AI_CARGO_PACKAGE = { - ClassName = "AI_CARGO_PACKAGE" - } - ---- AI_CARGO_PACKAGE Constructor. --- @param #AI_CARGO_PACKAGE self --- @param Wrapper.Unit#UNIT CargoCarrier The UNIT carrying the package. --- @param #string Type --- @param #string Name --- @param #number Weight --- @param #number ReportRadius (optional) --- @param #number NearRadius (optional) --- @return #AI_CARGO_PACKAGE -function AI_CARGO_PACKAGE:New( CargoCarrier, Type, Name, Weight, ReportRadius, NearRadius ) - local self = BASE:Inherit( self, AI_CARGO_REPRESENTABLE:New( CargoCarrier, Type, Name, Weight, ReportRadius, NearRadius ) ) -- #AI_CARGO_PACKAGE - self:F( { Type, Name, Weight, ReportRadius, NearRadius } ) - - self:T( CargoCarrier ) - self.CargoCarrier = CargoCarrier - - return self -end - ---- Board Event. --- @param #AI_CARGO_PACKAGE self --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT CargoCarrier --- @param #number Speed --- @param #number BoardDistance --- @param #number Angle -function AI_CARGO_PACKAGE:onafterOnBoard( From, Event, To, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) - self:F() - - self.CargoInAir = self.CargoCarrier:InAir() - - self:T( self.CargoInAir ) - - -- Only move the CargoCarrier to the New CargoCarrier when the New CargoCarrier is not in the air. - if not self.CargoInAir then - - local Points = {} - - local StartPointVec2 = self.CargoCarrier:GetPointVec2() - local CargoCarrierHeading = CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - self:T( { CargoCarrierHeading, CargoDeployHeading } ) - local CargoDeployPointVec2 = CargoCarrier:GetPointVec2():Translate( BoardDistance, CargoDeployHeading ) - - Points[#Points+1] = StartPointVec2:RoutePointGround( Speed ) - Points[#Points+1] = CargoDeployPointVec2:RoutePointGround( Speed ) - - local TaskRoute = self.CargoCarrier:TaskRoute( Points ) - self.CargoCarrier:SetTask( TaskRoute, 1 ) - end - - self:Boarded( CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) - -end - ---- Check if CargoCarrier is near the Cargo to be Loaded. --- @param #AI_CARGO_PACKAGE self --- @param Wrapper.Unit#UNIT CargoCarrier --- @return #boolean -function AI_CARGO_PACKAGE:IsNear( CargoCarrier ) - self:F() - - local CargoCarrierPoint = CargoCarrier:GetPointVec2() - - local Distance = CargoCarrierPoint:DistanceFromPointVec2( self.CargoCarrier:GetPointVec2() ) - self:T( Distance ) - - if Distance <= self.NearRadius then - return true - else - return false - end -end - ---- Boarded Event. --- @param #AI_CARGO_PACKAGE self --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT CargoCarrier -function AI_CARGO_PACKAGE:onafterOnBoarded( From, Event, To, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) - self:F() - - if self:IsNear( CargoCarrier ) then - self:__Load( 1, CargoCarrier, Speed, LoadDistance, Angle ) - else - self:__Boarded( 1, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) - end -end - ---- UnBoard Event. --- @param #AI_CARGO_PACKAGE self --- @param #string Event --- @param #string From --- @param #string To --- @param #number Speed --- @param #number UnLoadDistance --- @param #number UnBoardDistance --- @param #number Radius --- @param #number Angle -function AI_CARGO_PACKAGE:onafterUnBoard( From, Event, To, CargoCarrier, Speed, UnLoadDistance, UnBoardDistance, Radius, Angle ) - self:F() - - self.CargoInAir = self.CargoCarrier:InAir() - - self:T( self.CargoInAir ) - - -- Only unboard the cargo when the carrier is not in the air. - -- (eg. cargo can be on a oil derrick, moving the cargo on the oil derrick will drop the cargo on the sea). - if not self.CargoInAir then - - self:_Next( self.FsmP.UnLoad, UnLoadDistance, Angle ) - - local Points = {} - - local StartPointVec2 = CargoCarrier:GetPointVec2() - local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - self:T( { CargoCarrierHeading, CargoDeployHeading } ) - local CargoDeployPointVec2 = StartPointVec2:Translate( UnBoardDistance, CargoDeployHeading ) - - Points[#Points+1] = StartPointVec2:RoutePointGround( Speed ) - Points[#Points+1] = CargoDeployPointVec2:RoutePointGround( Speed ) - - local TaskRoute = CargoCarrier:TaskRoute( Points ) - CargoCarrier:SetTask( TaskRoute, 1 ) - end - - self:__UnBoarded( 1 , CargoCarrier, Speed ) - -end - ---- UnBoarded Event. --- @param #AI_CARGO_PACKAGE self --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT CargoCarrier -function AI_CARGO_PACKAGE:onafterUnBoarded( From, Event, To, CargoCarrier, Speed ) - self:F() - - if self:IsNear( CargoCarrier ) then - self:__UnLoad( 1, CargoCarrier, Speed ) - else - self:__UnBoarded( 1, CargoCarrier, Speed ) - end -end - ---- Load Event. --- @param #AI_CARGO_PACKAGE self --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT CargoCarrier --- @param #number Speed --- @param #number LoadDistance --- @param #number Angle -function AI_CARGO_PACKAGE:onafterLoad( From, Event, To, CargoCarrier, Speed, LoadDistance, Angle ) - self:F() - - self.CargoCarrier = CargoCarrier - - local StartPointVec2 = self.CargoCarrier:GetPointVec2() - local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - local CargoDeployPointVec2 = StartPointVec2:Translate( LoadDistance, CargoDeployHeading ) - - local Points = {} - Points[#Points+1] = StartPointVec2:RoutePointGround( Speed ) - Points[#Points+1] = CargoDeployPointVec2:RoutePointGround( Speed ) - - local TaskRoute = self.CargoCarrier:TaskRoute( Points ) - self.CargoCarrier:SetTask( TaskRoute, 1 ) - -end - ---- UnLoad Event. --- @param #AI_CARGO_PACKAGE self --- @param #string Event --- @param #string From --- @param #string To --- @param #number Distance --- @param #number Angle -function AI_CARGO_PACKAGE:onafterUnLoad( From, Event, To, CargoCarrier, Speed, Distance, Angle ) - self:F() - - local StartPointVec2 = self.CargoCarrier:GetPointVec2() - local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - local CargoDeployPointVec2 = StartPointVec2:Translate( Distance, CargoDeployHeading ) - - self.CargoCarrier = CargoCarrier - - local Points = {} - Points[#Points+1] = StartPointVec2:RoutePointGround( Speed ) - Points[#Points+1] = CargoDeployPointVec2:RoutePointGround( Speed ) - - local TaskRoute = self.CargoCarrier:TaskRoute( Points ) - self.CargoCarrier:SetTask( TaskRoute, 1 ) - -end - - -end - -do -- AI_CARGO_GROUP - - --- @type AI_CARGO_GROUP - -- @extends AI.AI_Cargo#AI_CARGO - -- @field Set#SET_BASE CargoSet A set of cargo objects. - -- @field #string Name A string defining the name of the cargo group. The name is the unique identifier of the cargo. - AI_CARGO_GROUP = { - ClassName = "AI_CARGO_GROUP", - } - ---- AI_CARGO_GROUP constructor. --- @param #AI_CARGO_GROUP self --- @param Core.Set#Set_BASE CargoSet --- @param #string Type --- @param #string Name --- @param #number Weight --- @param #number ReportRadius (optional) --- @param #number NearRadius (optional) --- @return #AI_CARGO_GROUP -function AI_CARGO_GROUP:New( CargoSet, Type, Name, ReportRadius, NearRadius ) - local self = BASE:Inherit( self, AI_CARGO:New( Type, Name, 0, ReportRadius, NearRadius ) ) -- #AI_CARGO_GROUP - self:F( { Type, Name, ReportRadius, NearRadius } ) - - self.CargoSet = CargoSet - - - return self -end - -end -- AI_CARGO_GROUP - -do -- AI_CARGO_GROUPED - - --- @type AI_CARGO_GROUPED - -- @extends AI.AI_Cargo#AI_CARGO_GROUP - AI_CARGO_GROUPED = { - ClassName = "AI_CARGO_GROUPED", - } - ---- AI_CARGO_GROUPED constructor. --- @param #AI_CARGO_GROUPED self --- @param Core.Set#Set_BASE CargoSet --- @param #string Type --- @param #string Name --- @param #number Weight --- @param #number ReportRadius (optional) --- @param #number NearRadius (optional) --- @return #AI_CARGO_GROUPED -function AI_CARGO_GROUPED:New( CargoSet, Type, Name, ReportRadius, NearRadius ) - local self = BASE:Inherit( self, AI_CARGO_GROUP:New( CargoSet, Type, Name, ReportRadius, NearRadius ) ) -- #AI_CARGO_GROUPED - self:F( { Type, Name, ReportRadius, NearRadius } ) - - return self -end - ---- Enter Boarding State. --- @param #AI_CARGO_GROUPED self --- @param Wrapper.Unit#UNIT CargoCarrier --- @param #string Event --- @param #string From --- @param #string To -function AI_CARGO_GROUPED:onenterBoarding( From, Event, To, CargoCarrier ) - self:F( { CargoCarrier.UnitName, From, Event, To } ) - - if From == "UnLoaded" then - - -- For each Cargo object within the AI_CARGO_GROUPED, route each object to the CargoLoadPointVec2 - self.CargoSet:ForEach( - function( Cargo ) - Cargo:__Board( 1, CargoCarrier ) - end - ) - - self:__Boarding( 1, CargoCarrier ) - end - -end - ---- Enter Loaded State. --- @param #AI_CARGO_GROUPED self --- @param Wrapper.Unit#UNIT CargoCarrier --- @param #string Event --- @param #string From --- @param #string To -function AI_CARGO_GROUPED:onenterLoaded( From, Event, To, CargoCarrier ) - self:F( { CargoCarrier.UnitName, From, Event, To } ) - - if From == "UnLoaded" then - -- For each Cargo object within the AI_CARGO_GROUPED, load each cargo to the CargoCarrier. - for CargoID, Cargo in pairs( self.CargoSet:GetSet() ) do - Cargo:Load( CargoCarrier ) - end - end -end - ---- Leave Boarding State. --- @param #AI_CARGO_GROUPED self --- @param Wrapper.Unit#UNIT CargoCarrier --- @param #string Event --- @param #string From --- @param #string To -function AI_CARGO_GROUPED:onleaveBoarding( From, Event, To, CargoCarrier ) - self:F( { CargoCarrier.UnitName, From, Event, To } ) - - local Boarded = true - - -- For each Cargo object within the AI_CARGO_GROUPED, route each object to the CargoLoadPointVec2 - for CargoID, Cargo in pairs( self.CargoSet:GetSet() ) do - self:T( Cargo.current ) - if not Cargo:is( "Loaded" ) then - Boarded = false - end - end - - if not Boarded then - self:__Boarding( 1, CargoCarrier ) - else - self:__Load( 1, CargoCarrier ) - end - return Boarded -end - ---- Enter UnBoarding State. --- @param #AI_CARGO_GROUPED self --- @param Core.Point#POINT_VEC2 ToPointVec2 --- @param #string Event --- @param #string From --- @param #string To -function AI_CARGO_GROUPED:onenterUnBoarding( From, Event, To, ToPointVec2 ) - self:F() - - local Timer = 1 - - if From == "Loaded" then - - -- For each Cargo object within the AI_CARGO_GROUPED, route each object to the CargoLoadPointVec2 - self.CargoSet:ForEach( - function( Cargo ) - Cargo:__UnBoard( Timer, ToPointVec2 ) - Timer = Timer + 10 - end - ) - - self:__UnBoarding( 1, ToPointVec2 ) - end - -end - ---- Leave UnBoarding State. --- @param #AI_CARGO_GROUPED self --- @param Core.Point#POINT_VEC2 ToPointVec2 --- @param #string Event --- @param #string From --- @param #string To -function AI_CARGO_GROUPED:onleaveUnBoarding( From, Event, To, ToPointVec2 ) - self:F( { ToPointVec2, From, Event, To } ) - - local Angle = 180 - local Speed = 10 - local Distance = 5 - - if From == "UnBoarding" then - local UnBoarded = true - - -- For each Cargo object within the AI_CARGO_GROUPED, route each object to the CargoLoadPointVec2 - for CargoID, Cargo in pairs( self.CargoSet:GetSet() ) do - self:T( Cargo.current ) - if not Cargo:is( "UnLoaded" ) then - UnBoarded = false - end - end - - if UnBoarded then - return true - else - self:__UnBoarding( 1, ToPointVec2 ) - end - - return false - end - -end - ---- UnBoard Event. --- @param #AI_CARGO_GROUPED self --- @param Core.Point#POINT_VEC2 ToPointVec2 --- @param #string Event --- @param #string From --- @param #string To -function AI_CARGO_GROUPED:onafterUnBoarding( From, Event, To, ToPointVec2 ) - self:F( { ToPointVec2, From, Event, To } ) - - self:__UnLoad( 1, ToPointVec2 ) -end - - - ---- Enter UnLoaded State. --- @param #AI_CARGO_GROUPED self --- @param Core.Point#POINT_VEC2 --- @param #string Event --- @param #string From --- @param #string To -function AI_CARGO_GROUPED:onenterUnLoaded( From, Event, To, ToPointVec2 ) - self:F( { ToPointVec2, From, Event, To } ) - - if From == "Loaded" then - - -- For each Cargo object within the AI_CARGO_GROUPED, route each object to the CargoLoadPointVec2 - self.CargoSet:ForEach( - function( Cargo ) - Cargo:UnLoad( ToPointVec2 ) - end - ) - - end - -end - -end -- AI_CARGO_GROUPED - - - ---- (SP) (MP) (FSM) Accept or reject process for player (task) assignments. --- --- === --- --- # @{#ACT_ASSIGN} FSM template class, extends @{Fsm#FSM_PROCESS} --- --- ## ACT_ASSIGN state machine: --- --- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur. --- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below. --- Each derived class follows exactly the same process, using the same events and following the same state transitions, --- but will have **different implementation behaviour** upon each event or state transition. --- --- ### ACT_ASSIGN **Events**: --- --- These are the events defined in this class: --- --- * **Start**: Start the tasking acceptance process. --- * **Assign**: Assign the task. --- * **Reject**: Reject the task.. --- --- ### ACT_ASSIGN **Event methods**: --- --- Event methods are available (dynamically allocated by the state machine), that accomodate for state transitions occurring in the process. --- There are two types of event methods, which you can use to influence the normal mechanisms in the state machine: --- --- * **Immediate**: The event method has exactly the name of the event. --- * **Delayed**: The event method starts with a __ + the name of the event. The first parameter of the event method is a number value, expressing the delay in seconds when the event will be executed. --- --- ### ACT_ASSIGN **States**: --- --- * **UnAssigned**: The player has not accepted the task. --- * **Assigned (*)**: The player has accepted the task. --- * **Rejected (*)**: The player has not accepted the task. --- * **Waiting**: The process is awaiting player feedback. --- * **Failed (*)**: The process has failed. --- --- (*) End states of the process. --- --- ### ACT_ASSIGN state transition methods: --- --- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. --- There are 2 moments when state transition methods will be called by the state machine: --- --- * **Before** the state transition. --- The state transition method needs to start with the name **OnBefore + the name of the state**. --- If the state transition method returns false, then the processing of the state transition will not be done! --- If you want to change the behaviour of the AIControllable at this event, return false, --- but then you'll need to specify your own logic using the AIControllable! --- --- * **After** the state transition. --- The state transition method needs to start with the name **OnAfter + the name of the state**. --- These state transition methods need to provide a return value, which is specified at the function description. --- --- === --- --- # 1) @{#ACT_ASSIGN_ACCEPT} class, extends @{Fsm.Assign#ACT_ASSIGN} --- --- The ACT_ASSIGN_ACCEPT class accepts by default a task for a player. No player intervention is allowed to reject the task. --- --- ## 1.1) ACT_ASSIGN_ACCEPT constructor: --- --- * @{#ACT_ASSIGN_ACCEPT.New}(): Creates a new ACT_ASSIGN_ACCEPT object. --- --- === --- --- # 2) @{#ACT_ASSIGN_MENU_ACCEPT} class, extends @{Fsm.Assign#ACT_ASSIGN} --- --- The ACT_ASSIGN_MENU_ACCEPT class accepts a task when the player accepts the task through an added menu option. --- This assignment type is useful to conditionally allow the player to choose whether or not he would accept the task. --- The assignment type also allows to reject the task. --- --- ## 2.1) ACT_ASSIGN_MENU_ACCEPT constructor: --- ----------------------------------------- --- --- * @{#ACT_ASSIGN_MENU_ACCEPT.New}(): Creates a new ACT_ASSIGN_MENU_ACCEPT object. --- --- === --- --- @module Assign - - -do -- ACT_ASSIGN - - --- ACT_ASSIGN class - -- @type ACT_ASSIGN - -- @field Tasking.Task#TASK Task - -- @field Wrapper.Unit#UNIT ProcessUnit - -- @field Core.Zone#ZONE_BASE TargetZone - -- @extends Core.Fsm#FSM_PROCESS - ACT_ASSIGN = { - ClassName = "ACT_ASSIGN", - } - - - --- Creates a new task assignment state machine. The process will accept the task by default, no player intervention accepted. - -- @param #ACT_ASSIGN self - -- @return #ACT_ASSIGN The task acceptance process. - function ACT_ASSIGN:New() - - -- Inherits from BASE - local self = BASE:Inherit( self, FSM_PROCESS:New( "ACT_ASSIGN" ) ) -- Core.Fsm#FSM_PROCESS - - self:AddTransition( "UnAssigned", "Start", "Waiting" ) - self:AddTransition( "Waiting", "Assign", "Assigned" ) - self:AddTransition( "Waiting", "Reject", "Rejected" ) - self:AddTransition( "*", "Fail", "Failed" ) - - self:AddEndState( "Assigned" ) - self:AddEndState( "Rejected" ) - self:AddEndState( "Failed" ) - - self:SetStartState( "UnAssigned" ) - - return self - end - -end -- ACT_ASSIGN - - - -do -- ACT_ASSIGN_ACCEPT - - --- ACT_ASSIGN_ACCEPT class - -- @type ACT_ASSIGN_ACCEPT - -- @field Tasking.Task#TASK Task - -- @field Wrapper.Unit#UNIT ProcessUnit - -- @field Core.Zone#ZONE_BASE TargetZone - -- @extends #ACT_ASSIGN - ACT_ASSIGN_ACCEPT = { - ClassName = "ACT_ASSIGN_ACCEPT", - } - - - --- Creates a new task assignment state machine. The process will accept the task by default, no player intervention accepted. - -- @param #ACT_ASSIGN_ACCEPT self - -- @param #string TaskBriefing - function ACT_ASSIGN_ACCEPT:New( TaskBriefing ) - - local self = BASE:Inherit( self, ACT_ASSIGN:New() ) -- #ACT_ASSIGN_ACCEPT - - self.TaskBriefing = TaskBriefing - - return self - end - - function ACT_ASSIGN_ACCEPT:Init( FsmAssign ) - - self.TaskBriefing = FsmAssign.TaskBriefing - end - - --- StateMachine callback function - -- @param #ACT_ASSIGN_ACCEPT self - -- @param Wrapper.Unit#UNIT ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ASSIGN_ACCEPT:onafterStart( ProcessUnit, From, Event, To ) - self:E( { ProcessUnit, From, Event, To } ) - - self:__Assign( 1 ) - end - - --- StateMachine callback function - -- @param #ACT_ASSIGN_ACCEPT self - -- @param Wrapper.Unit#UNIT ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ASSIGN_ACCEPT:onenterAssigned( ProcessUnit, From, Event, To ) - env.info( "in here" ) - self:E( { ProcessUnit, From, Event, To } ) - - local ProcessGroup = ProcessUnit:GetGroup() - - self:Message( "You are assigned to the task " .. self.Task:GetName() ) - - self.Task:Assign( ProcessUnit, ProcessUnit:GetPlayerName() ) - 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 Zone - -- @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_POINT - - --- ACT_ROUTE_POINT class - -- @type ACT_ROUTE_POINT - -- @field Tasking.Task#TASK TASK - -- @extends #ACT_ROUTE - ACT_ROUTE_POINT = { - ClassName = "ACT_ROUTE_POINT", - } - - - --- Creates a new routing state machine. - -- The task will route a controllable to a PointVec2 until the controllable is within the Range. - -- @param #ACT_ROUTE_POINT self - -- @param Core.Point#POINT_VEC2 The PointVec2 to Target. - -- @param #number Range The Distance to Target. - -- @param Core.Zone#ZONE_BASE Zone - function ACT_ROUTE_POINT:New( PointVec2, Range ) - local self = BASE:Inherit( self, ACT_ROUTE:New() ) -- #ACT_ROUTE_POINT - - self.PointVec2 = PointVec2 - self.Range = Range or 0 - - self.DisplayInterval = 30 - self.DisplayCount = 30 - self.DisplayMessage = true - self.DisplayTime = 10 -- 10 seconds is the default - - return self - end - - function ACT_ROUTE_POINT:Init( FsmRoute ) - - self.PointVec2 = FsmRoute.PointVec2 - self.Range = FsmRoute.Range or 0 - - self.DisplayInterval = 30 - self.DisplayCount = 30 - self.DisplayMessage = true - self.DisplayTime = 10 -- 10 seconds is the default - end - - --- Set PointVec2 - -- @param #ACT_ROUTE_POINT self - -- @param Core.Point#POINT_VEC2 PointVec2 The PointVec2 to route to. - function ACT_ROUTE_POINT:SetPointVec2( PointVec2 ) - self:F2( { PointVec2 } ) - self.PointVec2 = PointVec2 - end - - --- Get PointVec2 - -- @param #ACT_ROUTE_POINT self - -- @return Core.Point#POINT_VEC2 PointVec2 The PointVec2 to route to. - function ACT_ROUTE_POINT:GetPointVec2() - self:F2( { self.PointVec2 } ) - return self.PointVec2 - end - - --- Set Range around PointVec2 - -- @param #ACT_ROUTE_POINT self - -- @param #number Range The Range to consider the arrival. Default is 10000 meters. - function ACT_ROUTE_POINT:SetRange( Range ) - self:F2( { self.Range } ) - self.Range = Range or 10000 - end - - --- Get Range around PointVec2 - -- @param #ACT_ROUTE_POINT self - -- @return #number The Range to consider the arrival. Default is 10000 meters. - function ACT_ROUTE_POINT:GetRange() - return self.Range - end - - --- Method override to check if the controllable has arrived. - -- @param #ACT_ROUTE_POINT self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @return #boolean - function ACT_ROUTE_POINT:onfuncHasArrived( ProcessUnit ) - - if ProcessUnit:IsAlive() then - local Distance = self.PointVec2:Get2DDistance( ProcessUnit:GetPointVec2() ) - - if Distance <= self.Range then - local RouteText = "You have arrived." - self:Message( RouteText ) - return true - end - end - - return false - end - - --- Task Events - - --- StateMachine callback function - -- @param #ACT_ROUTE_POINT self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ROUTE_POINT:onenterReporting( ProcessUnit, From, Event, To ) - - local TaskUnitPointVec2 = ProcessUnit:GetPointVec2() - local RouteText = "Route to " .. TaskUnitPointVec2:GetBRText( self.PointVec2 ) .. " km." - self:Message( RouteText ) - end - -end -- ACT_ROUTE_POINT - - -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 Zone - -- @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 Zone - function ACT_ROUTE_ZONE:New( Zone ) - local self = BASE:Inherit( self, ACT_ROUTE:New() ) -- #ACT_ROUTE_ZONE - - self.Zone = Zone - - self.DisplayInterval = 30 - self.DisplayCount = 30 - self.DisplayMessage = true - self.DisplayTime = 10 -- 10 seconds is the default - - return self - end - - function ACT_ROUTE_ZONE:Init( FsmRoute ) - - self.Zone = FsmRoute.Zone - - self.DisplayInterval = 30 - self.DisplayCount = 30 - self.DisplayMessage = true - self.DisplayTime = 10 -- 10 seconds is the default - end - - --- Set Zone - -- @param #ACT_ROUTE_ZONE self - -- @param Core.Zone#ZONE_BASE Zone The Zone object where to route to. - function ACT_ROUTE_ZONE:SetZone( Zone ) - self.Zone = Zone - end - - --- Get Zone - -- @param #ACT_ROUTE_ZONE self - -- @return Core.Zone#ZONE_BASE Zone The Zone object where to route to. - function ACT_ROUTE_ZONE:GetZone() - return self.Zone - end - - --- 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.Zone ) then - local RouteText = "You have arrived within the zone." - self:Message( RouteText ) - end - - return ProcessUnit:IsInZone( self.Zone ) - 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.Zone: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." - self:Message( RouteText ) - end - -end -- ACT_ROUTE_ZONE ---- **Actions** - ACT_ACCOUNT_ classes **account for** (detect, count & report) various DCS events occuring on @{Unit}s. --- --- ![Banner Image](..\Presentations\ACT_ACCOUNT\Dia1.JPG) --- --- === --- --- @module Account - - -do -- ACT_ACCOUNT - - --- # @{#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 States - -- - -- * **Asigned**: The player is assigned. - -- * **Waiting**: Waiting for an event. - -- * **Report**: Reporting. - -- * **Account**: Account for an event. - -- * **Accounted**: All events have been accounted for, end of the process. - -- * **Failed**: Failed the process. - -- - -- ### ACT_ACCOUNT Events - -- - -- * **Start**: Start the process. - -- * **Wait**: Wait for an event. - -- * **Report**: Report the status of the accounting. - -- * **Event**: An event happened, process the event. - -- * **More**: More targets. - -- * **NoMore (*)**: No more targets. - -- * **Fail (*)**: The action process has failed. - -- - -- (*) End states of the process. - -- - -- ### ACT_ACCOUNT state transition methods: - -- - -- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. - -- There are 2 moments when state transition methods will be called by the state machine: - -- - -- * **Before** the state transition. - -- The state transition method needs to start with the name **OnBefore + the name of the state**. - -- 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. - -- - -- @type ACT_ACCOUNT - -- @field Set#SET_UNIT TargetSetUnit - -- @extends Core.Fsm#FSM_PROCESS - ACT_ACCOUNT = { - ClassName = "ACT_ACCOUNT", - TargetSetUnit = nil, - } - - --- Creates a new DESTROY process. - -- @param #ACT_ACCOUNT self - -- @return #ACT_ACCOUNT - function ACT_ACCOUNT:New() - - -- Inherits from BASE - local self = BASE:Inherit( self, FSM_PROCESS:New() ) -- Core.Fsm#FSM_PROCESS - - self:AddTransition( "Assigned", "Start", "Waiting") - self:AddTransition( "*", "Wait", "Waiting") - self:AddTransition( "*", "Report", "Report") - self:AddTransition( "*", "Event", "Account") - self:AddTransition( "Account", "More", "Wait") - self:AddTransition( "Account", "NoMore", "Accounted") - self:AddTransition( "*", "Fail", "Failed") - - self:AddEndState( "Accounted" ) - self:AddEndState( "Failed" ) - - self:SetStartState( "Assigned" ) - - return self - end - - --- Process Events - - --- StateMachine callback function - -- @param #ACT_ACCOUNT self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ACCOUNT:onafterStart( ProcessUnit, From, Event, To ) - - self:HandleEvent( EVENTS.Dead, self.onfuncEventDead ) - - self:__Wait( 1 ) - end - - - --- StateMachine callback function - -- @param #ACT_ACCOUNT self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ACCOUNT:onenterWaiting( ProcessUnit, From, Event, To ) - - if self.DisplayCount >= self.DisplayInterval then - self:Report() - self.DisplayCount = 1 - else - self.DisplayCount = self.DisplayCount + 1 - end - - return true -- Process always the event. - end - - --- StateMachine callback function - -- @param #ACT_ACCOUNT self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ACCOUNT:onafterEvent( ProcessUnit, From, Event, To, Event ) - - self:__NoMore( 1 ) - end - -end -- ACT_ACCOUNT - -do -- ACT_ACCOUNT_DEADS - - --- # @{#ACT_ACCOUNT_DEADS} 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. - -- - -- @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 - - --- 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, Task, 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, Task, From, Event, To, EventData ) - self:T( { ProcessUnit, EventData, From, Event, To } ) - - self:T({self.Controllable}) - - self.TargetSetUnit:Flush() - - self:T( { "Before sending Message", EventData.IniUnitName, self.TargetSetUnit:FindUnit( EventData.IniUnitName ) } ) - if self.TargetSetUnit:FindUnit( EventData.IniUnitName ) then - self:T( "Sending Message" ) - local TaskGroup = ProcessUnit:GetGroup() - self.TargetSetUnit:Remove( 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 - self:T( { "After sending Message" } ) - 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, Task, From, Event, To ) - - 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( 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():GetMenu( 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 - - --- StateMachine callback function - -- @param #ACT_ASSIST self - -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit - -- @param #string Event - -- @param #string From - -- @param #string To - function ACT_ASSIST:onafterStop( ProcessUnit, From, Event, To ) - - self.Menu:Remove() -- When stopped, remove the menus - end - -end - -do -- ACT_ASSIST_SMOKE_TARGETS_ZONE - - --- 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 = {} - if Title then - self.Report[#self.Report+1] = Title - end - - 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] -end - ---- Produces the text of the report, taking into account an optional delimeter, which is \n by default. --- @param #REPORT self --- @param #string Delimiter (optional) A delimiter text. --- @return #string The report text. -function REPORT:Text( Delimiter ) - Delimiter = Delimiter or "\n" - local ReportText = table.concat( self.Report, Delimiter ) or "" - return ReportText -end - ---- The COMMANDCENTER class --- @type COMMANDCENTER --- @field Wrapper.Group#GROUP HQ --- @field Dcs.DCSCoalitionWrapper.Object#coalition CommandCenterCoalition --- @list Missions --- @extends Core.Base#BASE -COMMANDCENTER = { - ClassName = "COMMANDCENTER", - CommandCenterName = "", - CommandCenterCoalition = nil, - CommandCenterPositionable = nil, - Name = "", -} ---- The constructor takes an IDENTIFIABLE as the HQ command center. --- @param #COMMANDCENTER self --- @param Wrapper.Positionable#POSITIONABLE CommandCenterPositionable --- @param #string CommandCenterName --- @return #COMMANDCENTER -function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName ) - - local self = BASE:Inherit( self, BASE:New() ) - - self.CommandCenterPositionable = CommandCenterPositionable - self.CommandCenterName = CommandCenterName or CommandCenterPositionable:GetName() - self.CommandCenterCoalition = CommandCenterPositionable:GetCoalition() - - self.Missions = {} - - self:HandleEvent( EVENTS.Birth, - --- @param #COMMANDCENTER self - -- @param Core.Event#EVENTDATA EventData - function( self, EventData ) - if EventData.IniObjectCategory == 1 then - 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 - - end - ) - - -- When a player enters a client or a unit, the CommandCenter will check for each Mission and each Task in the Mission if the player has things to do. - -- For these elements, it will= - -- - Set the correct menu. - -- - Assign the PlayerUnit to the Task if required. - -- - Send a message to the other players in the group that this player has joined. - self:HandleEvent( EVENTS.PlayerEnterUnit, - --- @param #COMMANDCENTER self - -- @param Core.Event#EVENTDATA EventData - function( self, EventData ) - local PlayerUnit = EventData.IniUnit - for MissionID, Mission in pairs( self:GetMissions() ) do - local Mission = Mission -- Tasking.Mission#MISSION - local PlayerGroup = EventData.IniGroup -- The GROUP object should be filled! - Mission:JoinUnit( PlayerUnit, PlayerGroup ) - Mission:ReportDetails() - end - end - ) - - -- Handle when a player leaves a slot and goes back to spectators ... - -- The PlayerUnit will be UnAssigned from the Task. - -- When there is no Unit left running the Task, the Task goes into Abort... - self:HandleEvent( EVENTS.PlayerLeaveUnit, - --- @param #TASK self - -- @param Core.Event#EVENTDATA EventData - function( self, EventData ) - local PlayerUnit = EventData.IniUnit - for MissionID, Mission in pairs( self:GetMissions() ) do - local Mission = Mission -- Tasking.Mission#MISSION - Mission:AbortUnit( PlayerUnit ) - end - end - ) - - -- Handle when a player leaves a slot and goes back to spectators ... - -- The PlayerUnit will be UnAssigned from the Task. - -- When there is no Unit left running the Task, the Task goes into Abort... - self:HandleEvent( EVENTS.Crash, - --- @param #TASK self - -- @param Core.Event#EVENTDATA EventData - function( self, EventData ) - local PlayerUnit = EventData.IniUnit - for MissionID, Mission in pairs( self:GetMissions() ) do - Mission:CrashUnit( PlayerUnit ) - end - end - ) - - return self -end - ---- Gets the name of the HQ command center. --- @param #COMMANDCENTER self --- @return #string -function COMMANDCENTER:GetName() - - return self.CommandCenterName -end - ---- Gets the POSITIONABLE of the HQ command center. --- @param #COMMANDCENTER self --- @return Wrapper.Positionable#POSITIONABLE -function COMMANDCENTER:GetPositionable() - return self.CommandCenterPositionable -end - ---- Get the Missions governed by the HQ command center. --- @param #COMMANDCENTER self --- @return #list -function COMMANDCENTER:GetMissions() - - return self.Missions -end - ---- Add a MISSION to be governed by the HQ command center. --- @param #COMMANDCENTER self --- @param Tasking.Mission#MISSION Mission --- @return Tasking.Mission#MISSION -function COMMANDCENTER:AddMission( Mission ) - - self.Missions[Mission] = Mission - - return Mission -end - ---- Removes a MISSION to be governed by the HQ command center. --- The given Mission is not nilified. --- @param #COMMANDCENTER self --- @param Tasking.Mission#MISSION Mission --- @return Tasking.Mission#MISSION -function COMMANDCENTER:RemoveMission( Mission ) - - self.Missions[Mission] = nil - - return Mission -end - ---- Sets the menu structure of the Missions governed by the HQ command center. --- @param #COMMANDCENTER self -function COMMANDCENTER:SetMenu() - self:F() - - self.CommandCenterMenu = self.CommandCenterMenu or MENU_COALITION:New( self.CommandCenterCoalition, "Command Center (" .. self:GetName() .. ")" ) - - local MenuTime = timer.getTime() - for MissionID, Mission in pairs( self:GetMissions() ) do - local Mission = Mission -- Tasking.Mission#MISSION - Mission:SetMenu( MenuTime ) - end - - for MissionID, Mission in pairs( self:GetMissions() ) do - local Mission = Mission -- Tasking.Mission#MISSION - Mission:RemoveMenu( MenuTime ) - end - -end - ---- Gets the commandcenter menu structure governed by the HQ command center. --- @param #COMMANDCENTER self --- @return Core.Menu#MENU_COALITION -function COMMANDCENTER:GetMenu() - self:F() - return self.CommandCenterMenu -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 the coalition of the CC. --- @param #COMMANDCENTER self -function COMMANDCENTER:MessageToAll( Message ) - - self:GetPositionable():MessageToAll( Message, 20, self:GetName() ) - -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 = "@ Group" - Prefix = Prefix .. ( Name and " (" .. 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", -} - ---- 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" ) - - --- OnLeave Transition Handler for State Idle. - -- @function [parent=#MISSION] OnLeaveIdle - -- @param #MISSION self - -- @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 Idle. - -- @function [parent=#MISSION] OnEnterIdle - -- @param #MISSION self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- OnLeave Transition Handler for State Ongoing. - -- @function [parent=#MISSION] OnLeaveOngoing - -- @param #MISSION self - -- @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 Ongoing. - -- @function [parent=#MISSION] OnEnterOngoing - -- @param #MISSION self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- OnBefore Transition Handler for Event Start. - -- @function [parent=#MISSION] OnBeforeStart - -- @param #MISSION self - -- @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=#MISSION] OnAfterStart - -- @param #MISSION self - -- @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=#MISSION] Start - -- @param #MISSION self - - --- Asynchronous Event Trigger for Event Start. - -- @function [parent=#MISSION] __Start - -- @param #MISSION self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "Ongoing", "Stop", "Idle" ) - - --- OnLeave Transition Handler for State Idle. - -- @function [parent=#MISSION] OnLeaveIdle - -- @param #MISSION self - -- @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 Idle. - -- @function [parent=#MISSION] OnEnterIdle - -- @param #MISSION self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- OnBefore Transition Handler for Event Stop. - -- @function [parent=#MISSION] OnBeforeStop - -- @param #MISSION self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Stop. - -- @function [parent=#MISSION] OnAfterStop - -- @param #MISSION self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Stop. - -- @function [parent=#MISSION] Stop - -- @param #MISSION self - - --- Asynchronous Event Trigger for Event Stop. - -- @function [parent=#MISSION] __Stop - -- @param #MISSION self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "Ongoing", "Complete", "Completed" ) - - --- OnLeave Transition Handler for State Completed. - -- @function [parent=#MISSION] OnLeaveCompleted - -- @param #MISSION self - -- @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 Completed. - -- @function [parent=#MISSION] OnEnterCompleted - -- @param #MISSION self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- OnBefore Transition Handler for Event Complete. - -- @function [parent=#MISSION] OnBeforeComplete - -- @param #MISSION self - -- @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 Complete. - -- @function [parent=#MISSION] OnAfterComplete - -- @param #MISSION self - -- @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 Complete. - -- @function [parent=#MISSION] Complete - -- @param #MISSION self - - --- Asynchronous Event Trigger for Event Complete. - -- @function [parent=#MISSION] __Complete - -- @param #MISSION self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "Fail", "Failed" ) - - --- OnLeave Transition Handler for State Failed. - -- @function [parent=#MISSION] OnLeaveFailed - -- @param #MISSION self - -- @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 Failed. - -- @function [parent=#MISSION] OnEnterFailed - -- @param #MISSION self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- OnBefore Transition Handler for Event Fail. - -- @function [parent=#MISSION] OnBeforeFail - -- @param #MISSION self - -- @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 Fail. - -- @function [parent=#MISSION] OnAfterFail - -- @param #MISSION self - -- @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 Fail. - -- @function [parent=#MISSION] Fail - -- @param #MISSION self - - --- Asynchronous Event Trigger for Event Fail. - -- @function [parent=#MISSION] __Fail - -- @param #MISSION self - -- @param #number Delay The delay in seconds. - - 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 = {} - - -- Private implementations - - - - return self -end - --- FSM function for a MISSION --- @param #MISSION self --- @param #string From --- @param #string Event --- @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 From --- @param #string Event --- @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 --- @param #number MenuTime -function MISSION:SetMenu( MenuTime ) - self:F() - - for _, TaskData in pairs( self:GetTasks() ) do - local Task = TaskData -- Tasking.Task#TASK - Task:SetMenu( MenuTime ) - end -end - ---- Removes the Planned Task menu. --- @param #MISSION self --- @param #number MenuTime -function MISSION:RemoveMenu( MenuTime ) - self:F() - - for _, Task in pairs( self:GetTasks() ) do - local Task = Task -- Tasking.Task#TASK - Task:RemoveMenu( MenuTime ) - end -end - - ---- Gets the COMMANDCENTER. --- @param #MISSION self --- @return Tasking.CommandCenter#COMMANDCENTER -function MISSION:GetCommandCenter() - return self.CommandCenter -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:GetMenu( TaskGroup ) - - local CommandCenter = self:GetCommandCenter() - local CommandCenterMenu = CommandCenter:GetMenu() - - local MissionName = self:GetName() - local MissionMenu = CommandCenterMenu:GetMenu( MissionName ) - - return MissionMenu -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 - ---- Is the @{Mission} **Completed**. --- @param #MISSION self --- @return #boolean -function MISSION:IsCompleted() - return self:Is( "Completed" ) -end - ---- Is the @{Mission} **Idle**. --- @param #MISSION self --- @return #boolean -function MISSION:IsIdle() - return self:Is( "Idle" ) -end - ---- Is the @{Mission} **Ongoing**. --- @param #MISSION self --- @return #boolean -function MISSION:IsOngoing() - return self:Is( "Ongoing" ) -end - ---- Is the @{Mission} **Failed**. --- @param #MISSION self --- @return #boolean -function MISSION:IsFailed() - return self:Is( "Failed" ) -end - ---- Is the @{Mission} **Hold**. --- @param #MISSION self --- @return #boolean -function MISSION:IsHold() - return self:Is( "Hold" ) -end - ---- Validates if the Mission has a Group --- @param #MISSION --- @return #boolean true if the Mission has a Group. -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 - ---- 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 - - ---- This module contains the TASK class. --- --- 1) @{#TASK} class, extends @{Base#BASE} --- ============================================ --- 1.1) The @{#TASK} class implements the methods for task orchestration within MOOSE. --- ---------------------------------------------------------------------------------------- --- The class provides a couple of methods to: --- --- * @{#TASK.AssignToGroup}():Assign a task to a group (of players). --- * @{#TASK.AddProcess}():Add a @{Process} to a task. --- * @{#TASK.RemoveProcesses}():Remove a running @{Process} from a running task. --- * @{#TASK.SetStateMachine}():Set a @{Fsm} to a task. --- * @{#TASK.RemoveStateMachine}():Remove @{Fsm} from a task. --- * @{#TASK.HasStateMachine}():Enquire if the task has a @{Fsm} --- * @{#TASK.AssignToUnit}(): Assign a task to a unit. (Needs to be implemented in the derived classes from @{#TASK}. --- * @{#TASK.UnAssignFromUnit}(): Unassign the task from a unit. --- * @{#TASK.SetTimeOut}(): Set timer in seconds before task gets cancelled if not assigned. --- --- 1.2) Set and enquire task status (beyond the task state machine processing). --- ---------------------------------------------------------------------------- --- A task needs to implement as a minimum the following task states: --- --- * **Success**: Expresses the successful execution and finalization of the task. --- * **Failed**: Expresses the failure of a task. --- * **Planned**: Expresses that the task is created, but not yet in execution and is not assigned yet. --- * **Assigned**: Expresses that the task is assigned to a Group of players, and that the task is in execution mode. --- --- A task may also implement the following task states: --- --- * **Rejected**: Expresses that the task is rejected by a player, who was requested to accept the task. --- * **Cancelled**: Expresses that the task is cancelled by HQ or through a logical situation where a cancellation of the task is required. --- --- A task can implement more statusses than the ones outlined above. Please consult the documentation of the specific tasks to understand the different status modelled. --- --- The status of tasks can be set by the methods **State** followed by the task status. An example is `StateAssigned()`. --- The status of tasks can be enquired by the methods **IsState** followed by the task status name. An example is `if IsStateAssigned() then`. --- --- 1.3) Add scoring when reaching a certain task status: --- ----------------------------------------------------- --- Upon reaching a certain task status in a task, additional scoring can be given. If the Mission has a scoring system attached, the scores will be added to the mission scoring. --- Use the method @{#TASK.AddScore}() to add scores when a status is reached. --- --- 1.4) Task briefing: --- ------------------- --- A task briefing can be given that is shown to the player when he is assigned to the task. --- --- === --- --- ### Authors: FlightControl - Design and Programming --- --- @module Task - ---- The TASK class --- @type TASK --- @field Core.Scheduler#SCHEDULER TaskScheduler --- @field Tasking.Mission#MISSION Mission --- @field Core.Set#SET_GROUP SetGroup The Set of Groups assigned to the Task --- @field Core.Fsm#FSM_PROCESS FsmTemplate --- @field Tasking.Mission#MISSION Mission --- @field Tasking.CommandCenter#COMMANDCENTER CommandCenter --- @extends Core.Fsm#FSM_TASK -TASK = { - ClassName = "TASK", - TaskScheduler = nil, - ProcessClasses = {}, -- The container of the Process classes that will be used to create and assign new processes for the task to ProcessUnits. - Processes = {}, -- The container of actual process objects instantiated and assigned to ProcessUnits. - Players = nil, - Scores = {}, - Menu = {}, - SetGroup = nil, - FsmTemplate = nil, - Mission = nil, - CommandCenter = nil, - TimeOut = 0, -} - ---- FSM PlayerAborted event handler prototype for TASK. --- @function [parent=#TASK] OnAfterPlayerAborted --- @param #TASK self --- @param Wrapper.Unit#UNIT PlayerUnit The Unit of the Player when he went back to spectators or left the mission. --- @param #string PlayerName The name of the Player. - ---- FSM PlayerCrashed event handler prototype for TASK. --- @function [parent=#TASK] OnAfterPlayerCrashed --- @param #TASK self --- @param Wrapper.Unit#UNIT PlayerUnit The Unit of the Player when he crashed in the mission. --- @param #string PlayerName The name of the Player. - ---- FSM PlayerDead event handler prototype for TASK. --- @function [parent=#TASK] OnAfterPlayerDead --- @param #TASK self --- @param Wrapper.Unit#UNIT PlayerUnit The Unit of the Player when he died in the mission. --- @param #string PlayerName The name of the Player. - ---- FSM Fail synchronous event function for TASK. --- Use this event to Fail the Task. --- @function [parent=#TASK] Fail --- @param #TASK self - ---- FSM Fail asynchronous event function for TASK. --- Use this event to Fail the Task. --- @function [parent=#TASK] __Fail --- @param #TASK self - ---- FSM Abort synchronous event function for TASK. --- Use this event to Abort the Task. --- @function [parent=#TASK] Abort --- @param #TASK self - ---- FSM Abort asynchronous event function for TASK. --- Use this event to Abort the Task. --- @function [parent=#TASK] __Abort --- @param #TASK self - ---- FSM Success synchronous event function for TASK. --- Use this event to make the Task a Success. --- @function [parent=#TASK] Success --- @param #TASK self - ---- FSM Success asynchronous event function for TASK. --- Use this event to make the Task a Success. --- @function [parent=#TASK] __Success --- @param #TASK self - ---- FSM Cancel synchronous event function for TASK. --- Use this event to Cancel the Task. --- @function [parent=#TASK] Cancel --- @param #TASK self - ---- FSM Cancel asynchronous event function for TASK. --- Use this event to Cancel the Task. --- @function [parent=#TASK] __Cancel --- @param #TASK self - ---- FSM Replan synchronous event function for TASK. --- Use this event to Replan the Task. --- @function [parent=#TASK] Replan --- @param #TASK self - ---- FSM Replan asynchronous event function for TASK. --- Use this event to Replan the Task. --- @function [parent=#TASK] __Replan --- @param #TASK self - - ---- Instantiates a new TASK. Should never be used. Interface Class. --- @param #TASK self --- @param Tasking.Mission#MISSION Mission The mission wherein the Task is registered. --- @param Core.Set#SET_GROUP SetGroupAssign The set of groups for which the Task can be assigned. --- @param #string TaskName The name of the Task --- @param #string TaskType The type of the Task --- @return #TASK self -function TASK:New( Mission, SetGroupAssign, TaskName, TaskType ) - - local self = BASE:Inherit( self, FSM_TASK:New() ) -- Core.Fsm#FSM_TASK - - self:SetStartState( "Planned" ) - self:AddTransition( "Planned", "Assign", "Assigned" ) - self:AddTransition( "Assigned", "AssignUnit", "Assigned" ) - self:AddTransition( "Assigned", "Success", "Success" ) - self:AddTransition( "Assigned", "Fail", "Failed" ) - self:AddTransition( "Assigned", "Abort", "Aborted" ) - self:AddTransition( "Assigned", "Cancel", "Cancelled" ) - self:AddTransition( "*", "PlayerCrashed", "*" ) - self:AddTransition( "*", "PlayerAborted", "*" ) - self:AddTransition( "*", "PlayerDead", "*" ) - self:AddTransition( { "Failed", "Aborted", "Cancelled" }, "Replan", "Planned" ) - self:AddTransition( "*", "TimeOut", "Cancelled" ) - - self:E( "New TASK " .. TaskName ) - - self.Processes = {} - self.Fsm = {} - - self.Mission = Mission - self.CommandCenter = Mission:GetCommandCenter() - - self.SetGroup = SetGroupAssign - - self:SetType( TaskType ) - self:SetName( TaskName ) - self:SetID( Mission:GetNextTaskID( self ) ) -- The Mission orchestrates the task sequences .. - - self.TaskBriefing = "You are invited for the task: " .. self.TaskName .. "." - - self.FsmTemplate = self.FsmTemplate or FSM_PROCESS:New() - - - return self -end - ---- Get the Task FSM Process Template --- @param #TASK self --- @return Core.Fsm#FSM_PROCESS -function TASK:GetUnitProcess( TaskUnit ) - - if TaskUnit then - return self:GetStateMachine( TaskUnit ) - else - return self.FsmTemplate - end -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 - self:UnAssignFromGroup( PlayerGroup ) - PlayerGroup:SetState( PlayerGroup, "Assigned", nil ) - self:RemoveMenuForGroup( PlayerGroup ) - end - self:Abort() - 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 ) - - local Mission = self:GetMission() - local MissionMenu = Mission:GetMenu( TaskGroup ) - MissionMenu:RemoveSubMenus() - - --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:GetName() ) - - self:RemoveStateMachine( TaskUnit ) - - return self -end - ---- Sets the TimeOut for the @{Task}. If @{Task} stayed planned for longer than TimeOut, it gets into Cancelled status. --- @param #TASK self --- @param #integer Timer in seconds --- @return #TASK self -function TASK:SetTimeOut ( Timer ) - self:F( Timer ) - self.TimeOut = Timer - self:__TimeOut( self.TimeOut ) - return self -end - ---- Send a message of the @{Task} to the assigned @{Group}s. --- @param #TASK self -function TASK:MessageToGroups( Message ) - self:F( { Message = Message } ) - - local Mission = self:GetMission() - local CC = Mission:GetCommandCenter() - - for TaskGroupName, TaskGroup in pairs( self.SetGroup:GetSet() ) do - local TaskGroup = TaskGroup -- Wrapper.Group#GROUP - CC:MessageToGroup( Message, TaskGroup, TaskGroup:GetName() ) - end -end - - ---- Send the briefng message of the @{Task} to the assigned @{Group}s. --- @param #TASK self -function TASK:SendBriefingToAssignedGroups() - self:F2() - - for TaskGroupName, TaskGroup in pairs( self.SetGroup:GetSet() ) do - - if self:IsAssignedToGroup( TaskGroup ) then - TaskGroup:Message( self.TaskBriefing, 60 ) - end - end -end - - ---- UnAssign the @{Task} from the @{Group}s. --- @param #TASK self -function TASK:UnAssignFromGroups() - self:F2() - - for TaskGroupName, TaskGroup in pairs( self.SetGroup:GetSet() ) do - self:UnAssignFromGroup( TaskGroup ) - end -end - ---- UnAssign the @{Task} from a @{Group}. --- @param #TASK self -function TASK:UnAssignFromGroup( TaskGroup ) - self:F2( { TaskGroup } ) - - TaskGroup:SetState( TaskGroup, "Assigned", nil ) - - self:RemoveAssignedMenuForGroup( 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 - - - ---- 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 - self:T( { "Task is assigned to:", TaskGroup:GetName() } ) - return true - end - end - - self:T( { "Task is not assigned to:", TaskGroup:GetName() } ) - 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 --- @param #number MenuTime --- @return #TASK -function TASK:SetMenu( MenuTime ) - self:F() - - self.SetGroup:Flush() - for TaskGroupID, TaskGroupData in pairs( self.SetGroup:GetSet() ) do - local TaskGroup = TaskGroupData -- Wrapper.Group#GROUP - if TaskGroup:IsAlive() and TaskGroup:GetPlayerNames() then - if self:IsStatePlanned() or self:IsStateReplanned() then - self:SetMenuForGroup( TaskGroup, MenuTime ) - end - end - end -end - - - ---- Set the Menu for a Group --- @param #TASK self --- @param #number MenuTime --- @return #TASK -function TASK:SetMenuForGroup( TaskGroup, MenuTime ) - - if not TaskGroup:GetState( TaskGroup, "Assigned" ) then - self:SetPlannedMenuForGroup( TaskGroup, self:GetTaskName(), MenuTime ) - else - if not self:IsAssignedToGroup( TaskGroup ) then - self:SetAssignedMenuForGroup( TaskGroup, MenuTime ) - end - end -end - - ---- Set the planned menu option of the @{Task}. --- @param #TASK self --- @param Wrapper.Group#GROUP TaskGroup --- @param #string MenuText The menu text. --- @param #number MenuTime --- @return #TASK self -function TASK:SetPlannedMenuForGroup( TaskGroup, MenuText, MenuTime ) - self:E( TaskGroup:GetName() ) - - local Mission = self:GetMission() - local MissionName = Mission:GetName() - local CommandCenter = Mission:GetCommandCenter() - local CommandCenterMenu = CommandCenter:GetMenu() - - local MissionMenu = MENU_GROUP:New( TaskGroup, MissionName, CommandCenterMenu ):SetTime( MenuTime ) - - - local MissionMenu = Mission:GetMenu( TaskGroup ) - - local TaskType = self:GetType() - local TaskTypeMenu = MENU_GROUP:New( TaskGroup, TaskType, MissionMenu ):SetTime( MenuTime ) - local TaskMenu = MENU_GROUP_COMMAND:New( TaskGroup, MenuText, TaskTypeMenu, self.MenuAssignToGroup, { self = self, TaskGroup = TaskGroup } ):SetTime( MenuTime ):SetRemoveParent( true ) - - return self -end - ---- Set the assigned menu options of the @{Task}. --- @param #TASK self --- @param Wrapper.Group#GROUP TaskGroup --- @param #number MenuTime --- @return #TASK self -function TASK:SetAssignedMenuForGroup( TaskGroup, MenuTime ) - self:E( TaskGroup:GetName() ) - - local Mission = self:GetMission() - local MissionMenu = Mission:GetMenu( TaskGroup ) - - self:E( { MissionMenu = MissionMenu } ) - - local TaskTypeMenu = MENU_GROUP_COMMAND:New( TaskGroup, "Task Status", MissionMenu, self.MenuTaskStatus, self, TaskGroup ):SetTime( MenuTime ) - local TaskMenu = MENU_GROUP_COMMAND:New( TaskGroup, "Abort Task", MissionMenu, self.MenuTaskAbort, self, TaskGroup ):SetTime( MenuTime ) - - return self -end - ---- Remove the menu options of the @{Task} to all the groups in the SetGroup. --- @param #TASK self --- @param #number MenuTime --- @return #TASK -function TASK:RemoveMenu( MenuTime ) - self:F() - - for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do - local TaskGroup = TaskGroup -- Wrapper.Group#GROUP - if TaskGroup:IsAlive() and TaskGroup:GetPlayerNames() then - if not self:IsAssignedToGroup( TaskGroup ) then - self:RemovePlannedMenuForGroup( TaskGroup, MenuTime ) - end - end - end -end - - ---- Remove the menu option of the @{Task} for a @{Group}. --- @param #TASK self --- @param Wrapper.Group#GROUP TaskGroup --- @param #number MenuTime --- @return #TASK self -function TASK:RemovePlannedMenuForGroup( TaskGroup, MenuTime ) - self:F() - - local Mission = self:GetMission() - local MissionName = Mission:GetName() - - local MissionMenu = Mission:GetMenu( TaskGroup ) - - if MissionMenu then - local TaskType = self:GetType() - local TypeMenu = MissionMenu:GetMenu( TaskType ) - - if TypeMenu then - local TaskMenu = TypeMenu:GetMenu( self:GetTaskName() ) - if TaskMenu then - TaskMenu:Remove( MenuTime ) - end - end - end - -end - ---- Remove the assigned menu option of the @{Task} for a @{Group}. --- @param #TASK self --- @param Wrapper.Group#GROUP TaskGroup --- @param #number MenuTime --- @return #TASK self -function TASK:RemoveAssignedMenuForGroup( TaskGroup ) - self:F() - - local Mission = self:GetMission() - local MissionName = Mission:GetName() - - local MissionMenu = Mission:GetMenu( TaskGroup ) - - if MissionMenu then - MissionMenu:RemoveSubMenus() - end - -end - -function TASK.MenuAssignToGroup( MenuParam ) - - local self = MenuParam.self - local TaskGroup = MenuParam.TaskGroup - - self:E( "Assigned menu selected") - - self:AssignToGroup( TaskGroup ) -end - ---- Report the task status. --- @param #TASK self -function TASK:MenuTaskStatus( TaskGroup ) - - local ReportText = self:ReportDetails() - - self:T( ReportText ) - self:GetMission():GetCommandCenter():MessageToGroup( ReportText, TaskGroup ) - -end - ---- Report the task status. --- @param #TASK self -function TASK:MenuTaskAbort( 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 --- @param Core.Fsm#FSM_PROCESS Fsm --- @return #TASK self -function TASK:SetStateMachine( TaskUnit, Fsm ) - self:F2( { TaskUnit, self.Fsm[TaskUnit] ~= nil, Fsm:GetClassNameAndID() } ) - - self.Fsm[TaskUnit] = Fsm - - return Fsm -end - ---- Gets the FiniteStateMachine of @{Task} with key Task@{Unit} --- @param #TASK self --- @param Wrapper.Unit#UNIT TaskUnit --- @return Core.Fsm#FSM_PROCESS -function TASK:GetStateMachine( TaskUnit ) - self:F2( { TaskUnit, self.Fsm[TaskUnit] ~= nil } ) - - return self.Fsm[TaskUnit] -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:E( self.Fsm ) - for TaskUnitT, Fsm in pairs( self.Fsm ) do - self:E( TaskUnitT ) - end - - self.Fsm[TaskUnit] = nil - - collectgarbage() - self:E( "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 **Aborted**. --- @param #TASK self -function TASK:StateAborted() - self:SetState( self, "State", "Aborted" ) - return self -end - ---- Is the @{Task} status **Aborted**. --- @param #TASK self -function TASK:IsStateAborted() - return self:Is( "Aborted" ) -end - ---- Sets a @{Task} to status **Cancelled**. --- @param #TASK self -function TASK:StateCancelled() - self:SetState( self, "State", "Cancelled" ) - return self -end - ---- Is the @{Task} status **Cancelled**. --- @param #TASK self -function TASK:IsStateCancelled() - return self:Is( "Cancelled" ) -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, PlayerUnit, PlayerName ) - - self:E( { "Task Assigned", self.Dispatcher } ) - - self:MessageToGroups( "Task " .. self:GetName() .. " has been assigned to your group." ) - - if self.Dispatcher then - self:E( "Firing Assign event " ) - self.Dispatcher:Assign( self, PlayerUnit, PlayerName ) - end - - self:GetMission():__Start( 1 ) -end - - ---- FSM function for a TASK --- @param #TASK self --- @param #string Event --- @param #string From --- @param #string To -function TASK:onenterSuccess( From, Event, To ) - - self:E( "Task Success" ) - - self:GetMission():GetCommandCenter():MessageToCoalition( "Task " .. self:GetName() .. " is successful! Good job!" ) - self:UnAssignFromGroups() - - self:GetMission():__Complete( 1 ) - -end - - ---- FSM function for a TASK --- @param #TASK self --- @param #string From --- @param #string Event --- @param #string To -function TASK:onenterAborted( From, Event, To ) - - self:E( "Task Aborted" ) - - self:GetMission():GetCommandCenter():MessageToCoalition( "Task " .. self:GetName() .. " has been aborted! Task may be replanned." ) - - self:UnAssignFromGroups() - - self:__Replan( 5 ) -end - ---- FSM function for a TASK --- @param #TASK self --- @param #string From --- @param #string Event --- @param #string To -function TASK:onafterReplan( From, Event, To ) - - self:E( "Task Replanned" ) - - self:GetMission():GetCommandCenter():MessageToCoalition( "Replanning Task " .. self:GetName() .. "." ) - - self:SetMenu() - -end - ---- FSM function for a TASK --- @param #TASK self --- @param #string From --- @param #string Event --- @param #string To -function TASK:onenterFailed( From, Event, To ) - - self:E( "Task Failed" ) - - self:GetMission():GetCommandCenter():MessageToCoalition( "Task " .. self:GetName() .. " has failed!" ) - - self:UnAssignFromGroups() -end - ---- FSM function for a TASK --- @param #TASK self --- @param #string Event --- @param #string From --- @param #string To -function TASK:onstatechange( From, Event, To ) - - if self:IsTrace() then - MESSAGE:New( "@ Task " .. self.TaskName .. " : " .. Event .. " changed to state " .. To, 2 ):ToAll() - end - - if self.Scores[To] then - local Scoring = self:GetScoring() - if Scoring then - self:E( { self.Scores[To].ScoreText, self.Scores[To].Score } ) - Scoring:_AddMissionScore( self.Mission, self.Scores[To].ScoreText, self.Scores[To].Score ) - end - end - -end - ---- FSM function for a TASK --- @param #TASK self --- @param #string Event --- @param #string From --- @param #string To -function TASK:onenterPlanned( From, Event, To) - if not self.TimeOut == 0 then - self.__TimeOut( self.TimeOut ) - end -end - ---- FSM function for a TASK --- @param #TASK self --- @param #string Event --- @param #string From --- @param #string To -function TASK:onbeforeTimeOut( From, Event, To ) - if From == "Planned" then - self:RemoveMenu() - return true - end - return false -end - -do -- Dispatcher - - --- Set dispatcher of a task - -- @param #TASK self - -- @param Tasking.DetectionManager#DETECTION_MANAGER Dispatcher - -- @return #TASK - function TASK:SetDispatcher( Dispatcher ) - self.Dispatcher = Dispatcher - 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 = {} - local PlayerReport = REPORT:New( " - Players:" ) - for PlayerGroupID, PlayerGroupData in pairs( self:GetGroups():GetSet() ) do - local PlayerGroup = PlayerGroupData -- Wrapper.Group#GROUP - PlayerNames = PlayerGroup:GetPlayerNames() - if PlayerNames then - PlayerReport:Add( " -- Group " .. PlayerGroup:GetCallsign() .. ": " .. table.concat( PlayerNames, ", " ) ) - end - end - - -- Loop each Process in the Task, and find Reporting Details. - Report:Add( string.format( " - Task %s\n -- State '%s'\n%s", Name, State, PlayerReport:Text() ) ) - return Report:Text() -end - - -end -- Reporting ---- This module contains the DETECTION_MANAGER class and derived classes. --- --- === --- --- 1) @{DetectionManager#DETECTION_MANAGER} class, extends @{Fsm#FSM} --- ==================================================================== --- The @{DetectionManager#DETECTION_MANAGER} class defines the core functions to report detected objects to groups. --- 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. --- --- --- === --- --- ### 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 Core.Fsm#FSM - 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, FSM:New() ) -- #DETECTION_MANAGER - - self.SetGroup = SetGroup - self.Detection = Detection - - self:SetStartState( "Stopped" ) - self:AddTransition( "Stopped", "Start", "Started" ) - self:AddTransition( "Started", "Stop", "Stopped" ) - self:AddTransition( "Started", "Report", "Started" ) - - self:SetReportInterval( 30 ) - self:SetReportDisplayTime( 25 ) - - Detection:__Start( 1 ) - - return self - end - - function DETECTION_MANAGER:onafterStart( From, Event, To ) - self:Report() - end - - function DETECTION_MANAGER:onafterReport( From, Event, To ) - - self:E( "onafterReport" ) - - self:__Report( -self._ReportInterval ) - - self:ProcessDetected( self.Detection ) - 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:ProcessDetected( Detection ) - self:E() - - 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 - ---- **Tasking** - The TASK_A2G_DISPATCHER creates and manages player TASK_A2G tasks based on detected targets. --- --- === --- --- # 1) @{#TASK_A2G_DISPATCHER} class, extends @{#DETECTION_MANAGER} --- --- The @{#TASK_A2G_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) TASK_A2G_DISPATCHER constructor: --- -------------------------------------- --- The @{#TASK_A2G_DISPATCHER.New}() method creates a new TASK_A2G_DISPATCHER instance. --- --- === --- --- # **API CHANGE HISTORY** --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- Hereby the change log: --- --- 2017-03-09: Initial class and API. --- --- === --- --- # **AUTHORS and CONTRIBUTIONS** --- --- ### Contributions: --- --- ### Authors: --- --- * **FlightControl**: Concept, Design & Programming. --- --- @module Task_A2G_Dispatcher - -do -- TASK_A2G_DISPATCHER - - --- TASK_A2G_DISPATCHER class. - -- @type TASK_A2G_DISPATCHER - -- @field Set#SET_GROUP SetGroup The groups to which the FAC will report to. - -- @field Functional.Detection#DETECTION_BASE Detection The DETECTION_BASE object that is used to report the detected objects. - -- @field Tasking.Mission#MISSION Mission - -- @extends Tasking.DetectionManager#DETECTION_MANAGER - TASK_A2G_DISPATCHER = { - ClassName = "TASK_A2G_DISPATCHER", - Mission = nil, - Detection = nil, - } - - - --- TASK_A2G_DISPATCHER constructor. - -- @param #TASK_A2G_DISPATCHER self - -- @param Tasking.Mission#MISSION Mission The mission for which the task dispatching is done. - -- @param Set#SET_GROUP SetGroup The set of groups that can join the tasks within the mission. - -- @param Functional.Detection#DETECTION_BASE Detection The detection results that are used to dynamically assign new tasks to human players. - -- @return #TASK_A2G_DISPATCHER self - function TASK_A2G_DISPATCHER:New( Mission, SetGroup, Detection ) - - -- Inherits from DETECTION_MANAGER - local self = BASE:Inherit( self, DETECTION_MANAGER:New( SetGroup, Detection ) ) -- #TASK_A2G_DISPATCHER - - self.Detection = Detection - self.Mission = Mission - - self:AddTransition( "Started", "Assign", "Started" ) - - --- OnAfter Transition Handler for Event Assign. - -- @function [parent=#TASK_A2G_DISPATCHER] OnAfterAssign - -- @param #TASK_A2G_DISPATCHER self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @param Tasking.Task_A2G#TASK_A2G Task - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param #string PlayerName - - self:__Start( 5 ) - - return self - end - - - --- Creates a SEAD task when there are targets for it. - -- @param #TASK_A2G_DISPATCHER self - -- @param Functional.Detection#DETECTION_AREAS.DetectedItem DetectedItem - -- @return Set#SET_UNIT TargetSetUnit: The target set of units. - -- @return #nil If there are no targets to be set. - function TASK_A2G_DISPATCHER:EvaluateSEAD( DetectedItem ) - self:F( { DetectedItem.ItemID } ) - - local DetectedSet = DetectedItem.Set - local DetectedZone = DetectedItem.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 #TASK_A2G_DISPATCHER self - -- @param Functional.Detection#DETECTION_AREAS.DetectedItem DetectedItem - -- @return Tasking.Task#TASK - function TASK_A2G_DISPATCHER:EvaluateCAS( DetectedItem ) - self:F( { DetectedItem.ItemID } ) - - local DetectedSet = DetectedItem.Set - local DetectedZone = DetectedItem.Zone - - - -- Determine if the set has radar targets. If it does, construct a SEAD task. - local GroundUnitCount = DetectedSet:HasGroundUnits() - local FriendliesNearBy = self.Detection:IsFriendliesNearBy( DetectedItem ) - - 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 #TASK_A2G_DISPATCHER self - -- @param Functional.Detection#DETECTION_AREAS.DetectedItem DetectedItem - -- @return Tasking.Task#TASK - function TASK_A2G_DISPATCHER:EvaluateBAI( DetectedItem, FriendlyCoalition ) - self:F( { DetectedItem.ItemID } ) - - local DetectedSet = DetectedItem.Set - local DetectedZone = DetectedItem.Zone - - - -- Determine if the set has radar targets. If it does, construct a SEAD task. - local GroundUnitCount = DetectedSet:HasGroundUnits() - local FriendliesNearBy = self.Detection:IsFriendliesNearBy( DetectedItem ) - - 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 DetectedItem is Changed AND the state of the Task is "Planned". - -- @param #TASK_A2G_DISPATCHER self - -- @param Tasking.Mission#MISSION Mission - -- @param Tasking.Task#TASK Task - -- @param Functional.Detection#DETECTION_AREAS.DetectedItem DetectedItem - -- @return Tasking.Task#TASK - function TASK_A2G_DISPATCHER:EvaluateRemoveTask( Mission, Task, DetectedItem ) - - if Task then - if Task:IsStatePlanned() and DetectedItem.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 #TASK_A2G_DISPATCHER self - -- @param Functional.Detection#DETECTION_BASE Detection The detection created by the @{Detection#DETECTION_BASE} derived object. - -- @return #boolean Return true if you want the task assigning to continue... false will cancel the loop. - function TASK_A2G_DISPATCHER:ProcessDetected( Detection ) - self:E() - - local AreaMsg = {} - local TaskMsg = {} - local ChangeMsg = {} - - local Mission = self.Mission - local ReportSEAD = REPORT:New( "- SEAD Tasks:") - local ReportCAS = REPORT:New( "- CAS Tasks:") - local ReportBAI = REPORT:New( "- BAI Tasks:") - local ReportChanges = REPORT:New( " - Changes:" ) - - --- First we need to the detected targets. - for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do - - local DetectedItem = DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem - local DetectedSet = DetectedItem.Set -- Functional.Detection#DETECTION_BASE.DetectedSet - local DetectedZone = DetectedItem.Zone - self:E( { "Targets in DetectedItem", DetectedItem.ItemID, DetectedSet:Count(), tostring( DetectedItem ) } ) - DetectedSet:Flush() - - local ItemID = DetectedItem.ItemID - - -- Evaluate SEAD Tasking - local SEADTask = Mission:GetTask( string.format( "SEAD.%03d", ItemID ) ) - SEADTask = self:EvaluateRemoveTask( Mission, SEADTask, DetectedItem ) - if not SEADTask then - local TargetSetUnit = self:EvaluateSEAD( DetectedItem ) -- Returns a SetUnit if there are targets to be SEADed... - if TargetSetUnit then - local Task = TASK_SEAD:New( Mission, self.SetGroup, string.format( "SEAD.%03d", ItemID ), TargetSetUnit ) - Task:SetTargetZone( DetectedZone ) - Task:SetDispatcher( self ) - SEADTask = Mission:AddTask( Task ) - end - end - if SEADTask and SEADTask:IsStatePlanned() then - ReportSEAD:Add( string.format( " - %s.%02d - %s", "SEAD", ItemID, Detection:DetectedItemReportSummary(DetectedItemID) ) ) - end - - -- Evaluate CAS Tasking - local CASTask = Mission:GetTask( string.format( "CAS.%03d", ItemID ) ) - CASTask = self:EvaluateRemoveTask( Mission, CASTask, DetectedItem ) - if not CASTask then - local TargetSetUnit = self:EvaluateCAS( DetectedItem ) -- Returns a SetUnit if there are targets to be SEADed... - if TargetSetUnit then - local Task = TASK_CAS:New( Mission, self.SetGroup, string.format( "CAS.%03d", ItemID ), TargetSetUnit ) - Task:SetTargetZone( DetectedZone ) - Task:SetDispatcher( self ) - CASTask = Mission:AddTask( Task ) - end - end - if CASTask and CASTask:IsStatePlanned() then - ReportCAS:Add( string.format( " - %s.%02d - %s", "CAS", ItemID, Detection:DetectedItemReportSummary(DetectedItemID) ) ) - end - - -- Evaluate BAI Tasking - local BAITask = Mission:GetTask( string.format( "BAI.%03d", ItemID ) ) - BAITask = self:EvaluateRemoveTask( Mission, BAITask, DetectedItem ) - if not BAITask then - local TargetSetUnit = self:EvaluateBAI( DetectedItem, self.Mission:GetCommandCenter():GetPositionable():GetCoalition() ) -- Returns a SetUnit if there are targets to be SEADed... - if TargetSetUnit then - local Task = TASK_BAI:New( Mission, self.SetGroup, string.format( "BAI.%03d", ItemID ), TargetSetUnit ) - Task:SetTargetZone( DetectedZone ) - Task:SetDispatcher( self ) - BAITask = Mission:AddTask( Task ) - end - end - if BAITask and BAITask:IsStatePlanned() then - ReportBAI:Add( string.format( " - %s.%02d - %s", "BAI", ItemID, Detection:DetectedItemReportSummary(DetectedItemID) ) ) - end - - - -- Loop through the changes ... - local ChangeText = Detection:GetChangeText( DetectedItem ) - ReportChanges:Add( ChangeText ) - - - -- OK, so the tasking has been done, now delete the changes reported for the area. - Detection:AcceptChanges( DetectedItem ) - - end - - -- TODO set menus using the HQ coordinator - Mission:GetCommandCenter():SetMenu() - - for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do - if not TaskGroup:GetState( TaskGroup, "Assigned" ) then - Mission:GetCommandCenter():MessageToGroup( - string.format( "HQ Reporting - Planned tasks for mission '%s':\n%s\n", - self.Mission:GetName(), - string.format( "%s\n\n%s\n\n%s\n\n%s", ReportSEAD:Text(), ReportCAS:Text(), ReportBAI:Text(), ReportChanges:Text() - ) - ), TaskGroup - ) - end - end - - return true - end - -end--- **Tasking** - The TASK_A2G models tasks for players in Air to Ground engagements. --- --- ![Banner Image](..\Presentations\TASK_A2G\Dia1.JPG) --- --- --- # 1) @{Task_A2G#TASK_A2G} class, extends @{Task#TASK} --- --- The @{#TASK_A2G} class defines Air To Ground tasks for a @{Set} of Target Units, --- based on the tasking capabilities defined in @{Task#TASK}. --- The TASK_A2G is implemented using a @{Statemachine#FSM_TASK}, and has the following statuses: --- --- * **None**: Start of the process --- * **Planned**: The A2G task is planned. --- * **Assigned**: The A2G task is assigned to a @{Group#GROUP}. --- * **Success**: The A2G task is successfully completed. --- * **Failed**: The A2G task has failed. This will happen if the player exists the task early, without communicating a possible cancellation to HQ. --- --- # 1.1) Set the scoring of achievements in an A2G attack. --- --- Scoring or penalties can be given in the following circumstances: --- --- * @{#TASK_A2G.SetScoreOnDestroy}(): Set a score when a target in scope of the A2G attack, has been destroyed. --- * @{#TASK_A2G.SetScoreOnSuccess}(): Set a score when all the targets in scope of the A2G attack, have been destroyed. --- * @{#TASK_A2G.SetPenaltyOnFailed}(): Set a penalty when the A2G attack has failed. --- --- # 2) @{Task_A2G#TASK_SEAD} class, extends @{Task_A2G#TASK_A2G} --- --- The @{#TASK_SEAD} class defines a SEAD task for a @{Set} of Target Units. --- --- === --- --- # 3) @{Task_A2G#TASK_CAS} class, extends @{Task_A2G#TASK_A2G} --- --- The @{#TASK_CAS} class defines a CAS task for a @{Set} of Target Units. --- --- === --- --- # 4) @{Task_A2G#TASK_BAI} class, extends @{Task_A2G#TASK_A2G} --- --- The @{#TASK_BAI} class defines a BAI task for a @{Set} of Target Units. --- --- ==== --- --- # **API CHANGE HISTORY** --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- Hereby the change log: --- --- 2017-03-09: Revised version. --- --- === --- --- # **AUTHORS and CONTRIBUTIONS** --- --- ### Contributions: --- --- * **[WingThor]**: Concept, Advice & Testing. --- --- ### Authors: --- --- * **FlightControl**: Concept, Design & Programming. --- --- @module Task_A2G - -do -- TASK_A2G - - --- The TASK_A2G class - -- @type TASK_A2G - -- @field Set#SET_UNIT TargetSetUnit - -- @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 Set#SET_UNIT UnitSetTargets - -- @param #number TargetDistance The distance to Target when the Player is considered to have "arrived" at the engagement range. - -- @param Core.Zone#ZONE_BASE TargetZone The target zone, if known. - -- If the TargetZone parameter is specified, the player will be routed to the center of the zone where all the targets are assumed to be. - -- @return #TASK_A2G self - function TASK_A2G:New( Mission, SetGroup, TaskName, TargetSetUnit, TaskType ) - local self = BASE:Inherit( self, TASK:New( Mission, SetGroup, TaskName, TaskType ) ) -- Tasking.Task#TASK_A2G - self:F() - - self.TargetSetUnit = TargetSetUnit - self.TaskType = TaskType - - Mission:AddTask( self ) - - local Fsm = self:GetUnitProcess() - - - Fsm:AddProcess ( "Planned", "Accept", ACT_ASSIGN_ACCEPT:New( self.TaskBriefing ), { Assigned = "RouteToRendezVous", Rejected = "Reject" } ) - - Fsm:AddTransition( "Assigned", "RouteToRendezVous", "RoutingToRendezVous" ) - Fsm:AddProcess ( "RoutingToRendezVous", "RouteToRendezVousPoint", ACT_ROUTE_POINT:New(), { Arrived = "ArriveAtRendezVous" } ) - Fsm:AddProcess ( "RoutingToRendezVous", "RouteToRendezVousZone", ACT_ROUTE_ZONE:New(), { Arrived = "ArriveAtRendezVous" } ) - - Fsm:AddTransition( { "Arrived", "RoutingToRendezVous" }, "ArriveAtRendezVous", "ArrivedAtRendezVous" ) - - Fsm:AddTransition( { "ArrivedAtRendezVous", "HoldingAtRendezVous" }, "Engage", "Engaging" ) - Fsm:AddTransition( { "ArrivedAtRendezVous", "HoldingAtRendezVous" }, "HoldAtRendezVous", "HoldingAtRendezVous" ) - - Fsm:AddProcess ( "Engaging", "Account", ACT_ACCOUNT_DEADS:New( self.TargetSetUnit, self.TaskType ), { Accounted = "Success" } ) - Fsm:AddTransition( "Engaging", "RouteToTarget", "Engaging" ) - Fsm:AddProcess( "Engaging", "RouteToTargetZone", ACT_ROUTE_ZONE:New(), {} ) - Fsm:AddProcess( "Engaging", "RouteToTargetPoint", ACT_ROUTE_POINT:New(), {} ) - Fsm:AddTransition( "Engaging", "RouteToTargets", "Engaging" ) - - Fsm:AddTransition( "Accounted", "DestroyedAll", "Accounted" ) - Fsm:AddTransition( "Accounted", "Success", "Success" ) - Fsm:AddTransition( "Rejected", "Reject", "Aborted" ) - Fsm:AddTransition( "Failed", "Fail", "Failed" ) - - - --- Test - -- @param #FSM_PROCESS self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_A2G#TASK_A2G Task - function Fsm:onafterRouteToRendezVous( TaskUnit, Task ) - self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - -- Determine the first Unit from the self.RendezVousSetUnit - - if Task:GetRendezVousZone( TaskUnit ) then - self:__RouteToRendezVousZone( 0.1 ) - else - if Task:GetRendezVousPointVec2( TaskUnit ) then - self:__RouteToRendezVousPoint( 0.1 ) - else - self:__ArriveAtRendezVous( 0.1 ) - end - end - end - - --- Test - -- @param #FSM_PROCESS self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task#TASK_A2G Task - function Fsm:OnAfterArriveAtRendezVous( TaskUnit, Task ) - self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - -- Determine the first Unit from the self.TargetSetUnit - - self:__Engage( 0.1 ) - end - - --- Test - -- @param #FSM_PROCESS self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task#TASK_A2G Task - function Fsm:onafterEngage( TaskUnit, Task ) - self:E( { self } ) - self:__Account( 0.1 ) - self:__RouteToTarget(0.1 ) - self:__RouteToTargets( -10 ) - end - - --- Test - -- @param #FSM_PROCESS self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_A2G#TASK_A2G Task - function Fsm:onafterRouteToTarget( TaskUnit, Task ) - self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - -- Determine the first Unit from the self.TargetSetUnit - - if Task:GetTargetZone( TaskUnit ) then - self:__RouteToTargetZone( 0.1 ) - else - local TargetUnit = Task.TargetSetUnit:GetFirst() -- Wrapper.Unit#UNIT - if TargetUnit then - local PointVec2 = TargetUnit:GetPointVec2() - self:T( { TargetPointVec2 = PointVec2, PointVec2:GetX(), PointVec2:GetAlt(), PointVec2:GetZ() } ) - Task:SetTargetPointVec2( TargetUnit:GetPointVec2(), TaskUnit ) - end - self:__RouteToTargetPoint( 0.1 ) - end - end - - --- Test - -- @param #FSM_PROCESS self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_A2G#TASK_A2G Task - function Fsm:onafterRouteToTargets( TaskUnit, Task ) - self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - local TargetUnit = Task.TargetSetUnit:GetFirst() -- Wrapper.Unit#UNIT - if TargetUnit then - Task:SetTargetPointVec2( TargetUnit:GetPointVec2(), TaskUnit ) - end - self:__RouteToTargets( -10 ) - end - - return self - - end - - --- @param #TASK_A2G self - function TASK_A2G:GetPlannedMenuText() - return self:GetStateString() .. " - " .. self:GetTaskName() .. " ( " .. self.TargetSetUnit:GetUnitTypesText() .. " )" - end - - --- @param #TASK_A2G self - -- @param Core.Point#POINT_VEC2 RendezVousPointVec2 The PointVec2 object referencing to the 2D point where the RendezVous point is located on the map. - -- @param #number RendezVousRange The RendezVousRange that defines when the player is considered to have arrived at the RendezVous point. - -- @param Wrapper.Unit#UNIT TaskUnit - function TASK_A2G:SetRendezVousPointVec2( RendezVousPointVec2, RendezVousRange, TaskUnit ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT - ActRouteRendezVous:SetPointVec2( RendezVousPointVec2 ) - ActRouteRendezVous:SetRange( RendezVousRange ) - end - - --- @param #TASK_A2G self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return Core.Point#POINT_VEC2 The PointVec2 object referencing to the 2D point where the RendezVous point is located on the map. - -- @return #number The RendezVousRange that defines when the player is considered to have arrived at the RendezVous point. - function TASK_A2G:GetRendezVousPointVec2( TaskUnit ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT - return ActRouteRendezVous:GetPointVec2(), ActRouteRendezVous:GetRange() - end - - - - --- @param #TASK_A2G self - -- @param Core.Zone#ZONE_BASE RendezVousZone The Zone object where the RendezVous is located on the map. - -- @param Wrapper.Unit#UNIT TaskUnit - function TASK_A2G:SetRendezVousZone( RendezVousZone, TaskUnit ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE - ActRouteRendezVous:SetZone( RendezVousZone ) - end - - --- @param #TASK_A2G self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return Core.Zone#ZONE_BASE The Zone object where the RendezVous is located on the map. - function TASK_A2G:GetRendezVousZone( TaskUnit ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE - return ActRouteRendezVous:GetZone() - end - - --- @param #TASK_A2G self - -- @param Core.Point#POINT_VEC2 TargetPointVec2 The PointVec2 object where the Target is located on the map. - -- @param Wrapper.Unit#UNIT TaskUnit - function TASK_A2G:SetTargetPointVec2( TargetPointVec2, TaskUnit ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT - ActRouteTarget:SetPointVec2( TargetPointVec2 ) - end - - - --- @param #TASK_A2G self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return Core.Point#POINT_VEC2 The PointVec2 object where the Target is located on the map. - function TASK_A2G:GetTargetPointVec2( TaskUnit ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT - return ActRouteTarget:GetPointVec2() - end - - - --- @param #TASK_A2G self - -- @param Core.Zone#ZONE_BASE TargetZone The Zone object where the Target is located on the map. - -- @param Wrapper.Unit#UNIT TaskUnit - function TASK_A2G:SetTargetZone( TargetZone, TaskUnit ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE - ActRouteTarget:SetZone( TargetZone ) - end - - - --- @param #TASK_A2G self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return Core.Zone#ZONE_BASE The Zone object where the Target is located on the map. - function TASK_A2G:GetTargetZone( TaskUnit ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE - return ActRouteTarget:GetZone() - end - - --- Set a score when a target in scope of the A2G attack, has been destroyed . - -- @param #TASK_A2G self - -- @param #string Text The text to display to the player, when the target has been destroyed. - -- @param #number Score The score in points. - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #TASK_A2G - function TASK_A2G:SetScoreOnDestroy( Text, Score, TaskUnit ) - self:F( { Text, Score, TaskUnit } ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - ProcessUnit:AddScoreProcess( "Engaging", "Account", "Account", Text, Score ) - - return self - end - - --- Set a score when all the targets in scope of the A2G attack, have been destroyed. - -- @param #TASK_A2G self - -- @param #string Text The text to display to the player, when all targets hav been destroyed. - -- @param #number Score The score in points. - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #TASK_A2G - function TASK_A2G:SetScoreOnSuccess( Text, Score, TaskUnit ) - self:F( { Text, Score, TaskUnit } ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - ProcessUnit:AddScore( "Success", Text, Score ) - - return self - end - - --- Set a penalty when the A2G attack has failed. - -- @param #TASK_A2G self - -- @param #string Text The text to display to the player, when the A2G attack has failed. - -- @param #number Penalty The penalty in points. - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #TASK_A2G - function TASK_A2G:SetPenaltyOnFailed( Text, Penalty, TaskUnit ) - self:F( { Text, Score, TaskUnit } ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - ProcessUnit:AddScore( "Failed", Text, Penalty ) - - return self - end - - -end - - -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 #number TargetDistance The distance to Target when the Player is considered to have "arrived" at the engagement range. - -- @param Core.Zone#ZONE_BASE TargetZone The target zone, if known. - -- If the TargetZone parameter is specified, the player will be routed to the center of the zone where all the targets are assumed to be. - -- @return #TASK_SEAD self - function TASK_SEAD:New( Mission, SetGroup, TaskName, TargetSetUnit ) - local self = BASE:Inherit( self, TASK_A2G:New( Mission, SetGroup, TaskName, TargetSetUnit, "SEAD" ) ) -- #TASK_SEAD - self:F() - - return self - end - -end - -do -- TASK_BAI - - --- The TASK_BAI class - -- @type TASK_BAI - -- @field Set#SET_UNIT TargetSetUnit - -- @extends Tasking.Task#TASK - TASK_BAI = { - ClassName = "TASK_BAI", - } - - --- Instantiates a new TASK_BAI. - -- @param #TASK_BAI 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 #number TargetDistance The distance to Target when the Player is considered to have "arrived" at the engagement range. - -- @param Core.Zone#ZONE_BASE TargetZone The target zone, if known. - -- If the TargetZone parameter is specified, the player will be routed to the center of the zone where all the targets are assumed to be. - -- @return #TASK_BAI self - function TASK_BAI:New( Mission, SetGroup, TaskName, TargetSetUnit ) - local self = BASE:Inherit( self, TASK_A2G:New( Mission, SetGroup, TaskName, TargetSetUnit, "BAI" ) ) -- #TASK_BAI - self:F() - - return self - end - -end - -do -- TASK_CAS - - --- The TASK_CAS class - -- @type TASK_CAS - -- @field Set#SET_UNIT TargetSetUnit - -- @extends Tasking.Task#TASK - TASK_CAS = { - ClassName = "TASK_CAS", - } - - --- Instantiates a new TASK_CAS. - -- @param #TASK_CAS 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 #number TargetDistance The distance to Target when the Player is considered to have "arrived" at the engagement range. - -- @param Core.Zone#ZONE_BASE TargetZone The target zone, if known. - -- If the TargetZone parameter is specified, the player will be routed to the center of the zone where all the targets are assumed to be. - -- @return #TASK_CAS self - function TASK_CAS:New( Mission, SetGroup, TaskName, TargetSetUnit ) - local self = BASE:Inherit( self, TASK_A2G:New( Mission, SetGroup, TaskName, TargetSetUnit, "CAS" ) ) -- #TASK_CAS - self:F() - - return self - end - -end ---- The main include file for the MOOSE system. --- Test of permissions - ---- 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" ) -Include.File( "Core/Radio" ) - ---- Wrapper Classes -Include.File( "Wrapper/Object" ) -Include.File( "Wrapper/Identifiable" ) -Include.File( "Wrapper/Positionable" ) -Include.File( "Wrapper/Controllable" ) -Include.File( "Wrapper/Group" ) -Include.File( "Wrapper/Unit" ) -Include.File( "Wrapper/Client" ) -Include.File( "Wrapper/Static" ) -Include.File( "Wrapper/Airbase" ) -Include.File( "Wrapper/Scenery" ) - ---- Functional Classes -Include.File( "Functional/Scoring" ) -Include.File( "Functional/CleanUp" ) -Include.File( "Functional/Spawn" ) -Include.File( "Functional/Movement" ) -Include.File( "Functional/Sead" ) -Include.File( "Functional/Escort" ) -Include.File( "Functional/MissileTrainer" ) -Include.File( "Functional/AirbasePolice" ) -Include.File( "Functional/Detection" ) - ---- AI Classes -Include.File( "AI/AI_Balancer" ) -Include.File( "AI/AI_Patrol" ) -Include.File( "AI/AI_Cap" ) -Include.File( "AI/AI_Cas" ) -Include.File( "AI/AI_Cargo" ) - ---- Actions -Include.File( "Actions/Act_Assign" ) -Include.File( "Actions/Act_Route" ) -Include.File( "Actions/Act_Account" ) -Include.File( "Actions/Act_Assist" ) - ---- Task Handling Classes -Include.File( "Tasking/CommandCenter" ) -Include.File( "Tasking/Mission" ) -Include.File( "Tasking/Task" ) -Include.File( "Tasking/DetectionManager" ) -Include.File( "Tasking/Task_A2G_Dispatcher") -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/TAD - Task Dispatching/TAD-100 - A2G Task Dispatching DETECTION_AREAS/TAD-100 - A2G Task Dispatching DETECTION_AREAS.lua b/Moose Test Missions/TAD - Task Dispatching/TAD-100 - A2G Task Dispatching DETECTION_AREAS/TAD-100 - A2G Task Dispatching DETECTION_AREAS.lua index 23f6dc52d..afcfee050 100644 --- a/Moose Test Missions/TAD - Task Dispatching/TAD-100 - A2G Task Dispatching DETECTION_AREAS/TAD-100 - A2G Task Dispatching DETECTION_AREAS.lua +++ b/Moose Test Missions/TAD - Task Dispatching/TAD-100 - A2G Task Dispatching DETECTION_AREAS/TAD-100 - A2G Task Dispatching DETECTION_AREAS.lua @@ -1,4 +1,4 @@ - --- +--- -- Name: TAD-100 - A2G Task Dispatching DETECTION_AREAS -- Author: FlightControl -- Date Created: 06 Mar 2017 diff --git a/Moose Test Missions/TSK - Task Modelling/TSK-020 - Task Modelling - Pickup/TSK-020 - Task Modelling - Pickup.lua b/Moose Test Missions/TSK - Task Modelling/TSK-020 - Task Modelling - Pickup/TSK-020 - Task Modelling - Pickup.lua deleted file mode 100644 index e9a776183..000000000 --- a/Moose Test Missions/TSK - Task Modelling/TSK-020 - Task Modelling - Pickup/TSK-020 - Task Modelling - Pickup.lua +++ /dev/null @@ -1,125 +0,0 @@ - - - - - - - - - - - - - -do - local Mission = MISSION:New( 'Pickup', 'Operational', 'Pickup Troops', 'NATO' ) - - Mission:AddClient( CLIENT:FindByName( 'DE Pickup Test 1' ):Transport() ) - Mission:AddClient( CLIENT:FindByName( 'DE Pickup Test 2' ):Transport() ) - - local CargoTable = {} - - local EngineerNames = { "Alpha", "Beta", "Gamma", "Delta", "Theta" } - - Cargo_Pickup_Zone_1 = CARGO_ZONE:New( 'Pickup Zone 1', 'DE Communication Center 1' ):BlueSmoke() - Cargo_Pickup_Zone_2 = CARGO_ZONE:New( 'Pickup Zone 2', 'DE Communication Center 2' ):RedSmoke() - - for CargoItem = 1, 2 do - CargoTable[CargoItem] = AI_CARGO_GROUP:New( 'Engineers', 'Team ' .. EngineerNames[CargoItem], math.random( 70, 100 ) * 3, 'DE Infantry', Cargo_Pickup_Zone_1 ) - end - - for CargoItem = 3, 5 do - CargoTable[CargoItem] = AI_CARGO_GROUP:New( 'Engineers', 'Team ' .. EngineerNames[CargoItem], math.random( 70, 100 ) * 3, 'DE Infantry', Cargo_Pickup_Zone_2 ) - end - - --Cargo_Package = CARGO_INVISIBLE:New( 'Letter', 0.1, 'DE Secret Agent', 'Pickup Zone Package' ) - --Cargo_Goods = CARGO_STATIC:New( 'Goods', 20, 'Goods', 'Pickup Zone Goods', 'DE Collection Point' ) - --Cargo_SlingLoad = CARGO_SLING:New( 'Basket', 40, 'Basket', 'Pickup Zone Sling Load', 'DE Cargo Guard' ) - - - -- Assign the Pickup Task - local PickupTask = PICKUPTASK:New( 'Engineers', CLIENT.ONBOARDSIDE.LEFT ) - PickupTask:FromZone( Cargo_Pickup_Zone_1 ) - PickupTask:FromZone( Cargo_Pickup_Zone_2 ) - PickupTask:InitCargo( CargoTable ) - PickupTask:SetGoalTotal( 3 ) - Mission:AddTask( PickupTask, 1 ) - - - Cargo_Deploy_Zone_1 = CARGO_ZONE:New( 'Deploy Zone 1', 'DE Communication Center 3' ):RedFlare() - Cargo_Deploy_Zone_2 = CARGO_ZONE:New( 'Deploy Zone 2', 'DE Communication Center 4' ):WhiteFlare() - - -- Assign the Pickup Task - local DeployTask = DEPLOYTASK:New( 'Engineers' ) - DeployTask:ToZone( Cargo_Deploy_Zone_1 ) - DeployTask:ToZone( Cargo_Deploy_Zone_2 ) - DeployTask:SetGoalTotal( 3 ) - Mission:AddTask( DeployTask, 2 ) - - MISSIONSCHEDULER.AddMission( Mission ) -end - -do - local Mission = MISSION:New( 'Deliver secret letter', 'Operational', 'Pickup letter to the commander.', 'NATO' ) - - Client_Package_1 = CLIENT:FindByName( 'BE Package Test 1' ):Transport() - - Mission:AddClient( Client_Package_1 ) - - Package_Pickup_Zone = CARGO_ZONE:New( 'Package Pickup Zone', 'DE Guard' ):GreenSmoke() - - Cargo_Package = AI_CARGO_PACKAGE:New( 'Letter', 'Letter to Command', 0.1, Client_Package_1 ) - --Cargo_Goods = CARGO_STATIC:New( 'Goods', 20, 'Goods', 'Pickup Zone Goods', 'DE Collection Point' ) - --Cargo_SlingLoad = CARGO_SLING:New( 'Basket', 40, 'Basket', 'Pickup Zone Sling Load', 'DE Cargo Guard' ) - - - -- Assign the Pickup Task - local PickupTask = PICKUPTASK:New( 'Letter', CLIENT.ONBOARDSIDE.FRONT ) - PickupTask:FromZone( Package_Pickup_Zone ) - PickupTask:InitCargo( { Cargo_Package } ) - PickupTask:SetGoalTotal( 1 ) - Mission:AddTask( PickupTask, 1 ) - - - Package_Deploy_Zone = CARGO_ZONE:New( 'Package Deploy Zone', 'DE Secret Car' ):GreenFlare() - - -- Assign the Pickup Task - local DeployTask = DEPLOYTASK:New( 'Letter' ) - DeployTask:ToZone( Package_Deploy_Zone ) - DeployTask:SetGoalTotal( 1 ) - Mission:AddTask( DeployTask, 2 ) - - MISSIONSCHEDULER.AddMission( Mission ) -end - -do - local Mission = MISSION:New( 'Sling load Cargo', 'Operational', 'Sling Load Cargo to Deploy Zone.', 'NATO' ) - - Mission:AddClient( CLIENT:FindByName( 'Sling Load Test Client 1' ):Transport() ) - Mission:AddClient( CLIENT:FindByName( 'Sling Load Test Client 2' ):Transport() ) - - Sling_Load_Pickup_Zone = CARGO_ZONE:New( 'Sling Load Pickup Zone', 'Sling Load Guard' ):RedSmoke() - - Cargo_Sling_Load = CARGO_SLINGLOAD:New( 'Sling', 'Food Boxes', 200, 'Sling Load Pickup Zone', 'Sling Load Guard', country.id.USA ) - --Cargo_Goods = CARGO_STATIC:New( 'Goods', 20, 'Goods', 'Pickup Zone Goods', 'DE Collection Point' ) - --Cargo_SlingLoad = CARGO_SLING:New( 'Basket', 40, 'Basket', 'Pickup Zone Sling Load', 'DE Cargo Guard' ) - - - -- Assign the Pickup Task - local PickupTask = PICKUPTASK:New( 'Sling', CLIENT.ONBOARDSIDE.FRONT ) - PickupTask:FromZone( Sling_Load_Pickup_Zone ) - PickupTask:InitCargo( { Cargo_Sling_Load } ) - PickupTask:SetGoalTotal( 1 ) - Mission:AddTask( PickupTask, 1 ) - - MISSIONSCHEDULER.AddMission( Mission ) -end - - - --- MISSION SCHEDULER STARTUP -MISSIONSCHEDULER.Start() -MISSIONSCHEDULER.ReportMenu() -MISSIONSCHEDULER.ReportMissionsHide() - -env.info( "Test Mission loaded" ) 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 deleted file mode 100644 index 4748939dcd591db9fde8646e05db69766f8f4d19..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 255645 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

-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

w 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 _nU$ODK5cbn$j~W8) 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

l(acx~&&}+TjY+CTK)&T8511ZInG`4+mb zZ=JPlNRK-UmDDJ(N=Z$_N|YATUc=0hij$0Lw3dnAa97(x9tNHb0+~q;$tT1`WC;IPKoqK4NY!`^DNCkvF>?K(!02aH`fp z#Z4FL-j=cBlr#m~MyZ`rh8TU*I%EMEHO#PfQy+6825 zfuDDIGPerzV{RMd#ci~`EknogV8+$qzuYLyd%10(@3s+(bVoF7YvW2w2iE%zLz}IG zst_%hkTQgyOs~!GQ3?@loE!BA>LA21Oqj4*gqM)5g(FrY%-t7lO_^_ZJB!gaD8(@# zLXsRGiJr;mL5WTj?G}Ec@*vp*T*QC~OL#m)+S2cz^bmJp;-~wFpB^TDx|jH=CI0a~ zu<(L?t5fjfj^5BT3hM?<(;)x1koK6F!#v(8SBIlFZNfafX&UI-ZL~_T1Dx6Xll~>v zRJ5yaZl~3%Zi}v-@3yIG6=GD?Ho&Sj+TNC-L+lzwKJj*%Ry7K-t!f)&TpO{-x0iW| z8(xdvutN{(*_e&27URFMy1T7gi^D&9_{I5K=e(A^{$&N2W%TxDxq3BKQhhR?PA=-! zB6nVn?n0+FYdn}%%O$B04^CVSPkPi$sZREId{j>nwGP+~^b)qyU@yne+6;nwK-@dp zWGe`tLV&w<`)I>01so@8bnkcsIF3#3E9gYPah_NArI7%~<|P5b{W#sH1<}C^Y3Y)< zjI-`Nk1x?>ks3|hDd9V%WvBt+r6nhW7e4UP0MDH)gxb!&t+$O%0D6)XD(Y3%FPJDk90xu1W{%U~wA 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 C`QG6G diff --git a/Moose Test Missions/TSK - Task Modelling/TSK-100 - Cargo Pickup/TSK-100 - Cargo Pickup.lua b/Moose Test Missions/TSK - Task Modelling/TSK-100 - Cargo Pickup/TSK-100 - Cargo Pickup.lua new file mode 100644 index 000000000..644e01867 --- /dev/null +++ b/Moose Test Missions/TSK - Task Modelling/TSK-100 - Cargo Pickup/TSK-100 - Cargo Pickup.lua @@ -0,0 +1,33 @@ +--- +-- Name: TSK-100 - Cargo Pickup +-- Author: FlightControl +-- Date Created: 25 Mar 2017 +-- +-- # Situation: +-- +-- This mission demonstrates the pickup of cargo. +-- +-- # Test cases: +-- +-- + +do + HQ = GROUP:FindByName( "HQ", "Bravo HQ" ) + + CommandCenter = COMMANDCENTER:New( HQ, "Lima" ) + + Scoring = SCORING:New( "Pickup Demo" ) + + Mission = MISSION + :New( CommandCenter, "Transport", "High", "Pickup the team", coalition.side.BLUE ) + :AddScoring( Scoring ) + + TransportHelicopters = SET_GROUP:New():FilterPrefixes( "Transport" ):FilterStart() + + CargoEngineer = UNIT:FindByName( "Engineer" ) + InfantryCargo = AI_CARGO_UNIT:New( CargoEngineer, "Engineer", "Engineer Sven", "81", 2000, 25 ) + + Task_Cargo_Pickup = TASK_CARGO_TRANSPORT:New( Mission, TransportHelicopters, "Transport.001", InfantryCargo ) + +end + diff --git a/Moose Test Missions/TSK - Task Modelling/TSK-100 - Cargo Pickup/TSK-100 - Cargo Pickup.miz b/Moose Test Missions/TSK - Task Modelling/TSK-100 - Cargo Pickup/TSK-100 - Cargo Pickup.miz new file mode 100644 index 0000000000000000000000000000000000000000..9880f81feb5a56f459928bb160ac3af28bba8926 GIT binary patch literal 27419 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 Aj{pDw literal 0 HcmV?d00001 From d1a7e5864d2f0e75bf8c32580f026be9289545e8 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sat, 25 Mar 2017 11:59:20 +0100 Subject: [PATCH 03/14] Progress --- Moose Development/Moose/AI/AI_Cargo.lua | 136 +++++++++++------- .../Moose/Tasking/Task_CARGO.lua | 35 ++++- .../TSK-100 - Cargo Pickup.lua | 2 +- .../TSK-100 - Cargo Pickup.miz | Bin 27419 -> 28410 bytes 4 files changed, 118 insertions(+), 55 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Cargo.lua b/Moose Development/Moose/AI/AI_Cargo.lua index 6d44c87fa..d978621f2 100644 --- a/Moose Development/Moose/AI/AI_Cargo.lua +++ b/Moose Development/Moose/AI/AI_Cargo.lua @@ -15,55 +15,7 @@ -- -- * 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 ... -- @@ -200,6 +152,49 @@ do -- AI_CARGO -- @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 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. + -- + -- ## 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. + -- + -- ## 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. + -- + -- ## 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. + -- + -- @field #AI_CARGO AI_CARGO + -- AI_CARGO = { ClassName = "AI_CARGO", Type = nil, @@ -270,6 +265,23 @@ function AI_CARGO:Spawn( PointVec2 ) end +--- Check if CargoCarrier is in the radius for the Cargo to be Loaded. +-- @param #AI_CARGO self +-- @param Core.Point#POINT_VEC2 PointVec2 +-- @return #boolean +function AI_CARGO:IsInRadius( PointVec2 ) + self:F( { PointVec2 } ) + + local Distance = PointVec2:DistanceFromPointVec2( self.CargoObject:GetPointVec2() ) + self:T( Distance ) + + if Distance <= self.ReportRadius then + return true + else + return false + end +end + --- Check if CargoCarrier is near the Cargo to be Loaded. -- @param #AI_CARGO self @@ -299,7 +311,7 @@ end -- @param #AI_CARGO self -- @return #number The range till cargo will board. function AI_CARGO:GetBoardingRange() - return self.NearRadius + return self.ReportRadius end end @@ -354,6 +366,16 @@ do -- AI_CARGO_UNIT --- @type AI_CARGO_UNIT -- @extends #AI_CARGO_REPRESENTABLE + + --- # AI\_CARGO\_UNIT class, extends @{#AI_CARGO_REPRESENTABLE} + -- + -- 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. + -- + -- === + -- + -- @field #AI_CARGO_UNIT AI_CARGO_UNIT + -- AI_CARGO_UNIT = { ClassName = "AI_CARGO_UNIT" } @@ -823,6 +845,14 @@ do -- 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 class + -- + -- The AI\_CARGO\_GROUP 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\_GROUP to and from carrier. + -- + -- @field #AI_CARGO_GROUP AI_CARGO_GROUP + -- AI_CARGO_GROUP = { ClassName = "AI_CARGO_GROUP", } @@ -852,6 +882,14 @@ do -- AI_CARGO_GROUPED --- @type AI_CARGO_GROUPED -- @extends AI.AI_Cargo#AI_CARGO_GROUP + + --- # 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. + -- + -- @field #AI_CARGO_GROUPED AI_CARGO_GROUPED + -- AI_CARGO_GROUPED = { ClassName = "AI_CARGO_GROUPED", } diff --git a/Moose Development/Moose/Tasking/Task_CARGO.lua b/Moose Development/Moose/Tasking/Task_CARGO.lua index 0033a1cfd..b2ba8bce6 100644 --- a/Moose Development/Moose/Tasking/Task_CARGO.lua +++ b/Moose Development/Moose/Tasking/Task_CARGO.lua @@ -29,12 +29,11 @@ -- -- * **FlightControl**: Concept, Design & Programming. -- --- @module Task_A2G +-- @module Task_CARGO do -- TASK_CARGO --- @type TASK_CARGO - -- @field Set#SET_UNIT TargetSetUnit -- @extends Tasking.Task#TASK --- @@ -71,8 +70,8 @@ do -- TASK_CARGO -- @param #string TaskType The type of Cargo task. -- @return #TASK_CARGO self function TASK_CARGO:New( Mission, SetGroup, TaskName, Cargo, TaskType ) - local self = BASE:Inherit( self, TASK:New( Mission, SetGroup, TaskName, TaskType ) ) -- Tasking.Task#TASK_CARGO - self:F() + local self = BASE:Inherit( self, TASK:New( Mission, SetGroup, TaskName, TaskType ) ) -- #TASK_CARGO + self:F( {Mission, SetGroup, TaskName, Cargo, TaskType}) self.Cargo = Cargo self.TaskType = TaskType @@ -92,6 +91,7 @@ do -- TASK_CARGO Fsm:AddTransition( { "ArrivedAtCargo", "LandAtCargo" }, "Land", "Landing" ) Fsm:AddTransition( "Landing", "Landed", "Landed" ) Fsm:AddTransition( "OnGround", "PrepareBoarding", "AwaitBoarding" ) + Fsm:AddTransition( "AwaitBoarding", "Board", "Boarding" ) Fsm:AddTransition( "Boarding", "Boarded", "Boarded" ) @@ -120,7 +120,31 @@ do -- TASK_CARGO function Fsm:OnAfterArriveAtCargo( TaskUnit, Task ) self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - self:__Land( 0.1 ) + if Task.Cargo:IsInRadius( TaskUnit:GetPointVec2() ) then + self:__Land( -0.1 ) + else + self:__ArriveAtCargo( -10 ) + end + end + + --- + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_CARGO#TASK_CARGO Task + function Fsm:OnAfterLand( TaskUnit, Task ) + self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + + if Task.Cargo:IsInRadius( TaskUnit:GetPointVec2() ) then + if TaskUnit:InAir() then + Task:GetMission():GetCommandCenter():MessageToGroup( "Land", TaskUnit:GetGroup(), "Land" ) + self:__Land( -10 ) + else + Task:GetMission():GetCommandCenter():MessageToGroup( "Landed ...", TaskUnit:GetGroup(), "Land" ) + self:__Landed( -0.1 ) + end + else + self:__ArriveAtCargo( -0.1 ) + end end return self @@ -138,6 +162,7 @@ do -- TASK_CARGO -- @return #TASK_CARGO function TASK_CARGO:SetCargoPickup( Cargo, TaskUnit ) + self:F({Cargo, TaskUnit}) local ProcessUnit = self:GetUnitProcess( TaskUnit ) local ActRouteCargo = ProcessUnit:GetProcess( "RoutingToCargo", "RouteToCargoPickup" ) -- Actions.Act_Route#ACT_ROUTE_POINT diff --git a/Moose Test Missions/TSK - Task Modelling/TSK-100 - Cargo Pickup/TSK-100 - Cargo Pickup.lua b/Moose Test Missions/TSK - Task Modelling/TSK-100 - Cargo Pickup/TSK-100 - Cargo Pickup.lua index 644e01867..6ec266dd7 100644 --- a/Moose Test Missions/TSK - Task Modelling/TSK-100 - Cargo Pickup/TSK-100 - Cargo Pickup.lua +++ b/Moose Test Missions/TSK - Task Modelling/TSK-100 - Cargo Pickup/TSK-100 - Cargo Pickup.lua @@ -25,7 +25,7 @@ do TransportHelicopters = SET_GROUP:New():FilterPrefixes( "Transport" ):FilterStart() CargoEngineer = UNIT:FindByName( "Engineer" ) - InfantryCargo = AI_CARGO_UNIT:New( CargoEngineer, "Engineer", "Engineer Sven", "81", 2000, 25 ) + InfantryCargo = AI_CARGO_UNIT:New( CargoEngineer, "Engineer", "Engineer Sven", "81", 500, 25 ) Task_Cargo_Pickup = TASK_CARGO_TRANSPORT:New( Mission, TransportHelicopters, "Transport.001", InfantryCargo ) diff --git a/Moose Test Missions/TSK - Task Modelling/TSK-100 - Cargo Pickup/TSK-100 - Cargo Pickup.miz b/Moose Test Missions/TSK - Task Modelling/TSK-100 - Cargo Pickup/TSK-100 - Cargo Pickup.miz index 9880f81feb5a56f459928bb160ac3af28bba8926..834f0ccfd46154264b8b32b12d2ac93b6edef4d7 100644 GIT binary patch delta 25504 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 delta 24338 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 From a00311ed135e559d008fb5422d309d816fb1a7de Mon Sep 17 00:00:00 2001 From: FlightControl Date: Tue, 28 Mar 2017 12:20:15 +0200 Subject: [PATCH 04/14] Progress on Transport -- Created SET_CARGO -- DATABASE handles CARGO -- Event handles CARGO -- Event triggers CARGO events -- Menu system to pickup and deploy cargo -- Menu system to board and unboard cargo --- Moose Development/Moose/AI/AI_Cargo.lua | 56 ++- Moose Development/Moose/Core/Database.lua | 98 ++++- Moose Development/Moose/Core/Event.lua | 57 ++- Moose Development/Moose/Core/Set.lua | 343 ++++++++++++++++++ Moose Development/Moose/Moose.lua | 2 +- .../Moose/Tasking/Task_CARGO.lua | 221 +++++++++-- .../l10n/DEFAULT/Moose.lua | 2 +- Moose Mission Setup/Moose.lua | 2 +- .../TSK-100 - Cargo Pickup.lua | 4 +- .../TSK-100 - Cargo Pickup.miz | Bin 28410 -> 28460 bytes 10 files changed, 728 insertions(+), 57 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Cargo.lua b/Moose Development/Moose/AI/AI_Cargo.lua index d978621f2..2e6f0d6ab 100644 --- a/Moose Development/Moose/AI/AI_Cargo.lua +++ b/Moose Development/Moose/AI/AI_Cargo.lua @@ -253,9 +253,39 @@ function AI_CARGO:New( Type, Name, Weight, ReportRadius, NearRadius ) CARGOS[self.Name] = self + self:SetEventPriority( 5 ) + return self end +--- Get the name of the Cargo. +-- @param #AI_CARGO self +-- @return #string The name of the Cargo. +function AI_CARGO:GetName() + return self.Name +end + +--- Get the type of the Cargo. +-- @param #AI_CARGO self +-- @return #string The type of the Cargo. +function AI_CARGO:GetType() + return self.Type +end + +--- Check if cargo is loaded. +-- @param #AI_CARGO self +-- @return #boolean true if loaded +function AI_CARGO:IsLoaded() + return self:Is( "Loaded" ) +end + +--- Check if cargo is unloaded. +-- @param #AI_CARGO self +-- @return #boolean true if unloaded +function AI_CARGO:IsUnLoaded() + return self:Is( "UnLoaded" ) +end + --- Template method to spawn a new representation of the AI_CARGO in the simulator. -- @param #AI_CARGO self @@ -398,6 +428,20 @@ function AI_CARGO_UNIT:New( CargoUnit, Type, Name, Weight, ReportRadius, NearRad self:T( self.ClassName ) + -- Cargo objects are added to the _DATABASE and SET_CARGO objects. + _EVENTDISPATCHER:CreateEventNewCargo( self ) + + return self +end + +--- AI_CARGO_UNIT Destructor. +-- @param #AI_CARGO_UNIT self +-- @return #AI_CARGO_UNIT +function AI_CARGO_UNIT:Destroy() + + -- Cargo objects are deleted from the _DATABASE and SET_CARGO objects. + _EVENTDISPATCHER:CreateEventDeleteCargo( self ) + return self end @@ -539,7 +583,7 @@ end -- @param #string From -- @param #string To -- @param Wrapper.Unit#UNIT CargoCarrier -function AI_CARGO_UNIT:onenterBoarding( From, Event, To, CargoCarrier ) +function AI_CARGO_UNIT:onenterBoarding( From, Event, To, CargoCarrier, ... ) self:F( { CargoCarrier.UnitName, From, Event, To } ) local Speed = 10 @@ -571,14 +615,14 @@ end -- @param #string From -- @param #string To -- @param Wrapper.Unit#UNIT CargoCarrier -function AI_CARGO_UNIT:onleaveBoarding( From, Event, To, 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 ) + self:__Load( 1, CargoCarrier, ... ) return true else - self:__Boarding( 1, CargoCarrier ) + self:__Boarding( 1, CargoCarrier, ... ) end return false end @@ -607,7 +651,7 @@ end -- @param #string Event -- @param #string From -- @param #string To -function AI_CARGO_UNIT:onafterBoard( From, Event, To, CargoCarrier ) +function AI_CARGO_UNIT:onafterBoard( From, Event, To, CargoCarrier, ... ) self:F() self.CargoInAir = self.CargoObject:InAir() @@ -617,7 +661,7 @@ function AI_CARGO_UNIT:onafterBoard( From, Event, To, CargoCarrier ) -- 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 ) + self:Load( CargoCarrier, ... ) end end diff --git a/Moose Development/Moose/Core/Database.lua b/Moose Development/Moose/Core/Database.lua index 3e9eea077..04f162690 100644 --- a/Moose Development/Moose/Core/Database.lua +++ b/Moose Development/Moose/Core/Database.lua @@ -6,12 +6,14 @@ -- =================================================== -- Mission designers can use the DATABASE class to refer to: -- +-- * STATICS -- * UNITS -- * GROUPS -- * CLIENTS --- * AIRPORTS +-- * AIRBASES -- * PLAYERSJOINED -- * PLAYERS +-- * CARGOS -- -- On top, for internal MOOSE administration purposes, the DATBASE administers the Unit and Group TEMPLATES as defined within the Mission Editor. -- @@ -53,6 +55,7 @@ DATABASE = { PLAYERS = {}, PLAYERSJOINED = {}, CLIENTS = {}, + CARGOS = {}, AIRBASES = {}, COUNTRY_ID = {}, COUNTRY_NAME = {}, @@ -84,13 +87,15 @@ local _DATABASECategory = function DATABASE:New() -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) + local self = BASE:Inherit( self, BASE:New() ) -- #DATABASE self:SetEventPriority( 1 ) self:HandleEvent( EVENTS.Birth, self._EventOnBirth ) self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) + self:HandleEvent( EVENTS.NewCargo ) + self:HandleEvent( EVENTS.DeleteCargo ) -- Follow alive players and clients self:HandleEvent( EVENTS.PlayerEnterUnit, self._EventOnPlayerEnterUnit ) @@ -166,22 +171,24 @@ end --- Adds a Airbase based on the Airbase Name in the DATABASE. -- @param #DATABASE self -function DATABASE:AddAirbase( DCSAirbaseName ) +-- @param #string AirbaseName The name of the airbase +function DATABASE:AddAirbase( AirbaseName ) - if not self.AIRBASES[DCSAirbaseName] then - self.AIRBASES[DCSAirbaseName] = AIRBASE:Register( DCSAirbaseName ) + if not self.AIRBASES[AirbaseName] then + self.AIRBASES[AirbaseName] = AIRBASE:Register( AirbaseName ) end end --- Deletes a Airbase from the DATABASE based on the Airbase Name. -- @param #DATABASE self -function DATABASE:DeleteAirbase( DCSAirbaseName ) +-- @param #string AirbaseName The name of the airbase +function DATABASE:DeleteAirbase( AirbaseName ) - --self.AIRBASES[DCSAirbaseName] = nil + self.AIRBASES[AirbaseName] = nil end ---- Finds a AIRBASE based on the AirbaseName. +--- Finds an AIRBASE based on the AirbaseName. -- @param #DATABASE self -- @param #string AirbaseName -- @return Wrapper.Airbase#AIRBASE The found AIRBASE. @@ -191,6 +198,35 @@ function DATABASE:FindAirbase( AirbaseName ) return AirbaseFound end +--- Adds a Cargo based on the Cargo Name in the DATABASE. +-- @param #DATABASE self +-- @param #string CargoName The name of the airbase +function DATABASE:AddCargo( Cargo ) + + if not self.CARGOS[Cargo.Name] then + self.CARGOS[Cargo.Name] = Cargo + end +end + + +--- Deletes a Cargo from the DATABASE based on the Cargo Name. +-- @param #DATABASE self +-- @param #string CargoName The name of the airbase +function DATABASE:DeleteCargo( CargoName ) + + self.CARGOS[CargoName] = nil +end + +--- Finds an CARGO based on the CargoName. +-- @param #DATABASE self +-- @param #string CargoName +-- @return Wrapper.Cargo#CARGO The found CARGO. +function DATABASE:FindCargo( CargoName ) + + local CargoFound = self.CARGOS[CargoName] + return CargoFound +end + --- Finds a CLIENT based on the ClientName. -- @param #DATABASE self @@ -665,7 +701,7 @@ 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. +-- @param #function IteratorFunction The function that will be called for each object in the database. The function needs to accept a UNIT parameter. -- @return #DATABASE self function DATABASE:ForEachUnit( IteratorFunction, FinalizeFunction, ... ) self:F2( arg ) @@ -677,7 +713,7 @@ 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. +-- @param #function IteratorFunction The function that will be called for each object in the database. The function needs to accept a GROUP parameter. -- @return #DATABASE self function DATABASE:ForEachGroup( IteratorFunction, ... ) self:F2( arg ) @@ -690,7 +726,7 @@ 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. +-- @param #function IteratorFunction The function that will be called for each object in the database. The function needs to accept the player name. -- @return #DATABASE self function DATABASE:ForEachPlayer( IteratorFunction, ... ) self:F2( arg ) @@ -703,7 +739,7 @@ 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. +-- @param #function IteratorFunction The function that will be called for each object in the database. The function needs to accept a UNIT parameter. -- @return #DATABASE self function DATABASE:ForEachPlayerJoined( IteratorFunction, ... ) self:F2( arg ) @@ -715,7 +751,7 @@ 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. +-- @param #function IteratorFunction The function that will be called object in the database. The function needs to accept a CLIENT parameter. -- @return #DATABASE self function DATABASE:ForEachClient( IteratorFunction, ... ) self:F2( arg ) @@ -725,6 +761,42 @@ function DATABASE:ForEachClient( IteratorFunction, ... ) return self end +--- Iterate the DATABASE and call an iterator function for each CARGO, providing the CARGO object to the function and optional parameters. +-- @param #DATABASE self +-- @param #function IteratorFunction The function that will be called for each object in the database. The function needs to accept a CLIENT parameter. +-- @return #DATABASE self +function DATABASE:ForEachCargo( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self.CARGOS ) + + return self +end + + +--- Handles the OnEventNewCargo event. +-- @param #DATABASE self +-- @param Core.Event#EVENTDATA EventData +function DATABASE:OnEventNewCargo( EventData ) + self:F2( { EventData } ) + + if EventData.Cargo then + self:AddCargo( EventData.Cargo ) + end +end + + +--- Handles the OnEventDeleteCargo. +-- @param #DATABASE self +-- @param Core.Event#EVENTDATA EventData +function DATABASE:OnEventDeleteCargo( EventData ) + self:F2( { EventData } ) + + if EventData.Cargo then + self:DeleteCargo( EventData.Cargo.Name ) + end +end + function DATABASE:_RegisterTemplates() self:F2() diff --git a/Moose Development/Moose/Core/Event.lua b/Moose Development/Moose/Core/Event.lua index 39b0738af..7234188a9 100644 --- a/Moose Development/Moose/Core/Event.lua +++ b/Moose Development/Moose/Core/Event.lua @@ -197,6 +197,9 @@ EVENT = { ClassID = 0, } +world.event.S_EVENT_NEW_CARGO = world.event.S_EVENT_MAX + 1000 +world.event.S_EVENT_DELETE_CARGO = world.event.S_EVENT_MAX + 1001 + --- The different types of events supported by MOOSE. -- Use this structure to subscribe to events using the @{Base#BASE.HandleEvent}() method. -- @type EVENTS @@ -224,6 +227,8 @@ EVENTS = { PlayerComment = world.event.S_EVENT_PLAYER_COMMENT, ShootingStart = world.event.S_EVENT_SHOOTING_START, ShootingEnd = world.event.S_EVENT_SHOOTING_END, + NewCargo = world.event.S_EVENT_NEW_CARGO, + DeleteCargo = world.event.S_EVENT_DELETE_CARGO, } --- The Event structure @@ -271,6 +276,7 @@ EVENTS = { -- @field WeaponTgtDCSUnit + local _EVENTMETA = { [world.event.S_EVENT_SHOT] = { Order = 1, @@ -387,6 +393,16 @@ local _EVENTMETA = { Event = "OnEventShootingEnd", Text = "S_EVENT_SHOOTING_END" }, + [EVENTS.NewCargo] = { + Order = 1, + Event = "OnEventNewCargo", + Text = "S_EVENT_NEW_CARGO" + }, + [EVENTS.DeleteCargo] = { + Order = 1, + Event = "OnEventDeleteCargo", + Text = "S_EVENT_DELETE_CARGO" + }, } @@ -672,6 +688,39 @@ do -- OnEngineShutDown end +do -- Event Creation + + --- Creation of a New Cargo Event. + -- @param #EVENT self + -- @param AI.AI_Cargo#AI_CARGO Cargo The Cargo created. + function EVENT:CreateEventNewCargo( Cargo ) + self:F( { Cargo } ) + + local Event = { + id = EVENTS.NewCargo, + time = timer.getTime(), + cargo = Cargo, + } + + world.onEvent( Event ) + end + + --- Creation of a Cargo Deletion Event. + -- @param #EVENT self + -- @param AI.AI_Cargo#AI_CARGO Cargo The Cargo created. + function EVENT:CreateEventDeleteCargo( Cargo ) + self:F( { Cargo } ) + + local Event = { + id = EVENTS.DeleteCargo, + time = timer.getTime(), + cargo = Cargo, + } + + world.onEvent( Event ) + end + +end --- @param #EVENT self -- @param #EVENTDATA Event @@ -795,6 +844,11 @@ function EVENT:onEvent( Event ) --Event.WeaponTgtDCSUnit = Event.Weapon:getTarget() end + if Event.cargo then + Event.Cargo = Event.cargo + Event.CargoName = Event.cargo.Name + end + local PriorityOrder = _EVENTMETA[Event.id].Order local PriorityBegin = PriorityOrder == -1 and 5 or 1 local PriorityEnd = PriorityOrder == -1 and 1 or 5 @@ -956,7 +1010,8 @@ function EVENT:onEvent( Event ) -- 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 or Event.WeaponUNIT) and not EventData.EventUnit then + if ( ( Event.IniDCSUnit or Event.WeaponUNIT) and not EventData.EventUnit ) or + Event.Cargo then if EventClass == EventData.EventClass then diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index bfb6ac69e..52e526061 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -2391,3 +2391,346 @@ function SET_AIRBASE:IsIncludeObject( MAirbase ) self:T2( MAirbaseInclude ) return MAirbaseInclude end + +--- @type SET_CARGO +-- @extends Core.Set#SET_BASE + +--- # SET_CARGO class, extends @{Set#SET_BASE} +-- +-- Mission designers can use the @{Set#SET_CARGO} class to build sets of cargos optionally belonging to certain: +-- +-- * Coalitions +-- * Types +-- * Name or Prefix +-- +-- ## SET_CARGO constructor +-- +-- Create a new SET_CARGO object with the @{#SET_CARGO.New} method: +-- +-- * @{#SET_CARGO.New}: Creates a new SET_CARGO object. +-- +-- ## Add or Remove CARGOs from SET_CARGO +-- +-- CARGOs can be added and removed using the @{Set#SET_CARGO.AddCargosByName} and @{Set#SET_CARGO.RemoveCargosByName} respectively. +-- These methods take a single CARGO name or an array of CARGO names to be added or removed from SET_CARGO. +-- +-- ## SET_CARGO filter criteria +-- +-- You can set filter criteria to automatically maintain the SET_CARGO contents. +-- Filter criteria are defined by: +-- +-- * @{#SET_CARGO.FilterCoalitions}: Builds the SET_CARGO with the cargos belonging to the coalition(s). +-- * @{#SET_CARGO.FilterPrefixes}: Builds the SET_CARGO with the cargos containing the prefix string(s). +-- * @{#SET_CARGO.FilterTypes}: Builds the SET_CARGO with the cargos belonging to the cargo type(s). +-- * @{#SET_CARGO.FilterCountries}: Builds the SET_CARGO with the cargos belonging to the country(ies). +-- +-- Once the filter criteria have been set for the SET_CARGO, you can start filtering using: +-- +-- * @{#SET_CARGO.FilterStart}: Starts the filtering of the cargos within the SET_CARGO. +-- +-- ## SET_CARGO iterators +-- +-- Once the filters have been defined and the SET_CARGO has been built, you can iterate the SET_CARGO with the available iterator methods. +-- The iterator methods will walk the SET_CARGO set, and call for each cargo within the set a function that you provide. +-- The following iterator methods are currently available within the SET_CARGO: +-- +-- * @{#SET_CARGO.ForEachCargo}: Calls a function for each cargo it finds within the SET_CARGO. +-- +-- @field #SET_CARGO SET_CARGO +-- +SET_CARGO = { + ClassName = "SET_CARGO", + Cargos = {}, + Filter = { + Coalitions = nil, + Types = nil, + Countries = nil, + ClientPrefixes = nil, + }, + FilterMeta = { + Coalitions = { + red = coalition.side.RED, + blue = coalition.side.BLUE, + neutral = coalition.side.NEUTRAL, + }, + }, +} + + +--- Creates a new SET_CARGO object, building a set of cargos belonging to a coalitions and categories. +-- @param #SET_CARGO self +-- @return #SET_CARGO self +-- @usage +-- -- Define a new SET_CARGO Object. The DatabaseSet will contain a reference to all Cargos. +-- DatabaseSet = SET_CARGO:New() +function SET_CARGO:New() + -- Inherits from BASE + local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.CARGOS ) ) + + self:HandleEvent( EVENTS.NewCargo ) + self:HandleEvent( EVENTS.DeleteCargo ) + + return self +end + +--- Add CARGOs to SET_CARGO. +-- @param Core.Set#SET_CARGO self +-- @param #string AddCargoNames A single name or an array of CARGO names. +-- @return self +function SET_CARGO:AddCargosByName( AddCargoNames ) + + local AddCargoNamesArray = ( type( AddCargoNames ) == "table" ) and AddCargoNames or { AddCargoNames } + + for AddCargoID, AddCargoName in pairs( AddCargoNamesArray ) do + self:Add( AddCargoName, CARGO:FindByName( AddCargoName ) ) + end + + return self +end + +--- Remove CARGOs from SET_CARGO. +-- @param Core.Set#SET_CARGO self +-- @param Wrapper.Cargo#CARGO RemoveCargoNames A single name or an array of CARGO names. +-- @return self +function SET_CARGO:RemoveCargosByName( RemoveCargoNames ) + + local RemoveCargoNamesArray = ( type( RemoveCargoNames ) == "table" ) and RemoveCargoNames or { RemoveCargoNames } + + for RemoveCargoID, RemoveCargoName in pairs( RemoveCargoNamesArray ) do + self:Remove( RemoveCargoName.CargoName ) + end + + return self +end + + +--- Finds a Cargo based on the Cargo Name. +-- @param #SET_CARGO self +-- @param #string CargoName +-- @return Wrapper.Cargo#CARGO The found Cargo. +function SET_CARGO:FindCargo( CargoName ) + + local CargoFound = self.Set[CargoName] + return CargoFound +end + + + +--- Builds a set of cargos of coalitions. +-- Possible current coalitions are red, blue and neutral. +-- @param #SET_CARGO self +-- @param #string Coalitions Can take the following values: "red", "blue", "neutral". +-- @return #SET_CARGO self +function SET_CARGO:FilterCoalitions( Coalitions ) + 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 cargos of defined cargo types. +-- Possible current types are those types known within DCS world. +-- @param #SET_CARGO self +-- @param #string Types Can take those type strings known within DCS world. +-- @return #SET_CARGO self +function SET_CARGO:FilterTypes( Types ) + 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 cargos of defined countries. +-- Possible current countries are those known within DCS world. +-- @param #SET_CARGO self +-- @param #string Countries Can take those country strings known within DCS world. +-- @return #SET_CARGO self +function SET_CARGO:FilterCountries( Countries ) + 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 cargos of defined cargo prefixes. +-- All the cargos starting with the given prefixes will be included within the set. +-- @param #SET_CARGO self +-- @param #string Prefixes The prefix of which the cargo name starts with. +-- @return #SET_CARGO self +function SET_CARGO:FilterPrefixes( Prefixes ) + if not self.Filter.CargoPrefixes then + self.Filter.CargoPrefixes = {} + end + if type( Prefixes ) ~= "table" then + Prefixes = { Prefixes } + end + for PrefixID, Prefix in pairs( Prefixes ) do + self.Filter.CargoPrefixes[Prefix] = Prefix + end + return self +end + + + +--- Starts the filtering. +-- @param #SET_CARGO self +-- @return #SET_CARGO self +function SET_CARGO: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_CARGO self +-- @param Core.Event#EVENTDATA Event +-- @return #string The name of the CARGO +-- @return #table The CARGO +function SET_CARGO: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_CARGO self +-- @param Core.Event#EVENTDATA Event +-- @return #string The name of the CARGO +-- @return #table The CARGO +function SET_CARGO:FindInDatabase( Event ) + self:F3( { Event } ) + + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] +end + +--- Iterate the SET_CARGO and call an interator function for each CARGO, providing the CARGO and optional parameters. +-- @param #SET_CARGO self +-- @param #function IteratorFunction The function that will be called when there is an alive CARGO in the SET_CARGO. The function needs to accept a CARGO parameter. +-- @return #SET_CARGO self +function SET_CARGO:ForEachCargo( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self.Set ) + + return self +end + +--- Iterate the SET_CARGO while identifying the nearest @{Cargo#CARGO} from a @{Point#POINT_VEC2}. +-- @param #SET_CARGO self +-- @param Core.Point#POINT_VEC2 PointVec2 A @{Point#POINT_VEC2} object from where to evaluate the closest @{Cargo#CARGO}. +-- @return Wrapper.Cargo#CARGO The closest @{Cargo#CARGO}. +function SET_CARGO:FindNearestCargoFromPointVec2( PointVec2 ) + self:F2( PointVec2 ) + + local NearestCargo = self:FindNearestObjectFromPointVec2( PointVec2 ) + return NearestCargo +end + + + +--- +-- @param #SET_CARGO self +-- @param AI.AI_Cargo#AI_CARGO MCargo +-- @return #SET_CARGO self +function SET_CARGO:IsIncludeObject( MCargo ) + self:F2( MCargo ) + + local MCargoInclude = true + + if MCargo then + local MCargoName = MCargo:GetName() + + if self.Filter.Coalitions then + local MCargoCoalition = false + for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do + local CargoCoalitionID = MCargo:GetCoalition() + self:T3( { "Coalition:", CargoCoalitionID, self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) + if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == CargoCoalitionID then + MCargoCoalition = true + end + end + self:T( { "Evaluated Coalition", MCargoCoalition } ) + MCargoInclude = MCargoInclude and MCargoCoalition + end + + if self.Filter.Types then + local MCargoType = false + for TypeID, TypeName in pairs( self.Filter.Types ) do + self:T3( { "Type:", MCargo:GetType(), TypeName } ) + if TypeName == MCargo:GetType() then + MCargoType = true + end + end + self:T( { "Evaluated Type", MCargoType } ) + MCargoInclude = MCargoInclude and MCargoType + end + + if self.Filter.CargoPrefixes then + local MCargoPrefix = false + for CargoPrefixId, CargoPrefix in pairs( self.Filter.CargoPrefixes ) do + self:T3( { "Prefix:", string.find( MCargo.Name, CargoPrefix, 1 ), CargoPrefix } ) + if string.find( MCargo.Name, CargoPrefix, 1 ) then + MCargoPrefix = true + end + end + self:T( { "Evaluated Prefix", MCargoPrefix } ) + MCargoInclude = MCargoInclude and MCargoPrefix + end + end + + self:T2( MCargoInclude ) + return MCargoInclude +end + +--- Handles the OnEventNewCargo event for the Set. +-- @param #SET_CARGO self +-- @param Core.Event#EVENTDATA EventData +function SET_CARGO:OnEventNewCargo( EventData ) + + if EventData.Cargo then + if EventData.Cargo and self:IsIncludeObject( EventData.Cargo ) then + self:Add( EventData.Cargo.Name , EventData.Cargo ) + end + end +end + +--- Handles the OnDead or OnCrash event for alive units set. +-- @param #SET_CARGO self +-- @param Core.Event#EVENTDATA EventData +function SET_CARGO:OnEventDeleteCargo( EventData ) + self:F3( { EventData } ) + + if EventData.Cargo then + local Cargo = _DATABASE:FindCargo( EventData.Cargo.Name ) + if Cargo and Cargo.Name then + self:Remove( Cargo.Name ) + end + end +end + diff --git a/Moose Development/Moose/Moose.lua b/Moose Development/Moose/Moose.lua index 0312878a0..5195ff310 100644 --- a/Moose Development/Moose/Moose.lua +++ b/Moose Development/Moose/Moose.lua @@ -74,7 +74,7 @@ _EVENTDISPATCHER = EVENT:New() -- Core.Event#EVENT _SCHEDULEDISPATCHER = SCHEDULEDISPATCHER:New() -- Core.Timer#SCHEDULEDISPATCHER --- Declare the main database object, which is used internally by the MOOSE classes. -_DATABASE = DATABASE:New() -- Database#DATABASE +_DATABASE = DATABASE:New() -- Core.Database#DATABASE diff --git a/Moose Development/Moose/Tasking/Task_CARGO.lua b/Moose Development/Moose/Tasking/Task_CARGO.lua index b2ba8bce6..d44a8a920 100644 --- a/Moose Development/Moose/Tasking/Task_CARGO.lua +++ b/Moose Development/Moose/Tasking/Task_CARGO.lua @@ -66,14 +66,14 @@ do -- TASK_CARGO -- @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 AI.AI_Cargo#AI_CARGO Cargo The cargo. + -- @param Core.Set#SET_CARGO SetCargo The scope of the cargo to be transported. -- @param #string TaskType The type of Cargo task. -- @return #TASK_CARGO self - function TASK_CARGO:New( Mission, SetGroup, TaskName, Cargo, TaskType ) + function TASK_CARGO:New( Mission, SetGroup, TaskName, SetCargo, TaskType ) local self = BASE:Inherit( self, TASK:New( Mission, SetGroup, TaskName, TaskType ) ) -- #TASK_CARGO - self:F( {Mission, SetGroup, TaskName, Cargo, TaskType}) + self:F( {Mission, SetGroup, TaskName, SetCargo, TaskType}) - self.Cargo = Cargo + self.SetCargo = SetCargo self.TaskType = TaskType Mission:AddTask( self ) @@ -81,46 +81,125 @@ do -- TASK_CARGO local Fsm = self:GetUnitProcess() - Fsm:AddProcess ( "Planned", "Accept", ACT_ASSIGN_ACCEPT:New( self.TaskBriefing ), { Assigned = "RouteToCargo", Rejected = "Reject" } ) + Fsm:AddProcess ( "Planned", "Accept", ACT_ASSIGN_ACCEPT:New( self.TaskBriefing ), { Assigned = "SelectAction", Rejected = "Reject" } ) - Fsm:AddTransition( "Assigned", "RouteToCargo", "RoutingToCargo" ) - Fsm:AddProcess ( "RoutingToCargo", "RouteToCargoPickup", ACT_ROUTE_POINT:New(), { Arrived = "ArriveAtCargo" } ) + Fsm:AddTransition( { "Assigned", "Landed", "Boarded", "Deployed" } , "SelectAction", "WaitingForCommand" ) + + Fsm:AddTransition( "WaitingForCommand", "RouteToPickup", "RoutingToPickup" ) + Fsm:AddProcess ( "RoutingToPickup", "RouteToPickupPoint", ACT_ROUTE_POINT:New(), { Arrived = "ArriveAtPickup" } ) + Fsm:AddTransition( "Arrived", "ArriveAtPickup", "ArrivedAtPickup" ) + + Fsm:AddTransition( "WaitingForCommand", "RouteToDeploy", "RoutingToDeploy" ) + Fsm:AddProcess ( "RoutingToDeploy", "RouteToDeployPoint", ACT_ROUTE_POINT:New(), { Arrived = "ArriveAtDeploy" } ) + Fsm:AddTransition( "ArrivedAtDeploy", "ArriveAtDeploy", "ArrivedAtDeploy" ) - Fsm:AddTransition( { "Arrived", "RoutingToCargo" }, "ArriveAtCargo", "ArrivedAtCargo" ) - - Fsm:AddTransition( { "ArrivedAtCargo", "LandAtCargo" }, "Land", "Landing" ) + Fsm:AddTransition( { "ArrivedAtPickup", "ArrivedAtDeploy" }, "Land", "Landing" ) Fsm:AddTransition( "Landing", "Landed", "Landed" ) - Fsm:AddTransition( "OnGround", "PrepareBoarding", "AwaitBoarding" ) + Fsm:AddTransition( "WaitingForCommand", "PrepareBoarding", "AwaitBoarding" ) Fsm:AddTransition( "AwaitBoarding", "Board", "Boarding" ) Fsm:AddTransition( "Boarding", "Boarded", "Boarded" ) + + Fsm:AddTransition( "WaitingForCommand", "PrepareUnBoarding", "AwaitUnBoarding" ) + Fsm:AddTransition( "AwaitUnBoarding", "UnBoard", "UnBoarding" ) + Fsm:AddTransition( "UnBoarding", "UnBoarded", "UnBoarded" ) - Fsm:AddTransition( "Accounted", "DestroyedAll", "Accounted" ) - Fsm:AddTransition( "Accounted", "Success", "Success" ) + + Fsm:AddTransition( "Deployed", "Success", "Success" ) Fsm:AddTransition( "Rejected", "Reject", "Aborted" ) Fsm:AddTransition( "Failed", "Fail", "Failed" ) - - --- Route to Cargo - -- @param #FSM_PROCESS self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_CARGO#TASK_CARGO Task - function Fsm:onafterRouteToCargo( TaskUnit, Task ) - self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - - - Task:SetCargoPickup( Task.Cargo, TaskUnit ) - self:__RouteToCargoPickup( 0.1 ) - end --- -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit -- @param Tasking.Task_CARGO#TASK_CARGO Task - function Fsm:OnAfterArriveAtCargo( TaskUnit, Task ) + function Fsm:OnEnterWaitingForCommand( TaskUnit, Task ) self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - if Task.Cargo:IsInRadius( TaskUnit:GetPointVec2() ) then + TaskUnit.Menu = MENU_GROUP:New( TaskUnit:GetGroup(), Task:GetName() .. " @ " .. TaskUnit:GetName() ) + + Task.SetCargo:ForEachCargo( + + --- @param AI.AI_Cargo#AI_CARGO Cargo + function( Cargo ) + if Cargo:IsUnLoaded() then + if Cargo:IsInRadius( TaskUnit:GetPointVec2() ) then + MENU_GROUP_COMMAND:New( + TaskUnit:GetGroup(), + "Pickup cargo " .. Cargo.Name, + TaskUnit.Menu, + self.MenuBoardCargo, + self, + Cargo + ) + else + MENU_GROUP_COMMAND:New( + TaskUnit:GetGroup(), + "Route to cargo " .. Cargo.Name, + TaskUnit.Menu, + self.MenuRouteToPickup, + self, + Cargo + ) + end + end + + if Cargo:IsLoaded() then + MENU_GROUP_COMMAND:New( + TaskUnit:GetGroup(), + "Deploy cargo " .. Cargo.Name, + TaskUnit.Menu, + self.MenuBoardCargo, + self, + Cargo + ) + end + + end + ) + end + + --- + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_CARGO#TASK_CARGO Task + function Fsm:OnLeaveWaitingForCommand( TaskUnit, Task ) + self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + + TaskUnit.Menu:Remove() + end + + function Fsm:MenuBoardCargo( Cargo ) + self:__PrepareBoarding( 1.0, Cargo ) + end + + function Fsm:MenuRouteToPickup( Cargo ) + self:__RouteToPickup( 1.0, Cargo ) + end + + --- Route to Cargo + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_CARGO#TASK_CARGO Task + function Fsm:onafterRouteToPickup( TaskUnit, Task, From, Event, To, Cargo ) + self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + + + self.Cargo = Cargo + Task:SetCargoPickup( self.Cargo, TaskUnit ) + self:__RouteToPickupPoint( 0.1 ) + end + + + --- + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_CARGO#TASK_CARGO Task + function Fsm:OnAfterArriveAtPickup( TaskUnit, Task ) + self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + + if self.Cargo:IsInRadius( TaskUnit:GetPointVec2() ) then self:__Land( -0.1 ) else self:__ArriveAtCargo( -10 ) @@ -131,10 +210,10 @@ do -- TASK_CARGO -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit -- @param Tasking.Task_CARGO#TASK_CARGO Task - function Fsm:OnAfterLand( TaskUnit, Task ) + function Fsm:OnAfterLand( TaskUnit, Task, From, Event, To ) self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - if Task.Cargo:IsInRadius( TaskUnit:GetPointVec2() ) then + if self.Cargo:IsInRadius( TaskUnit:GetPointVec2() ) then if TaskUnit:InAir() then Task:GetMission():GetCommandCenter():MessageToGroup( "Land", TaskUnit:GetGroup(), "Land" ) self:__Land( -10 ) @@ -146,6 +225,82 @@ do -- TASK_CARGO self:__ArriveAtCargo( -0.1 ) end end + + --- + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_CARGO#TASK_CARGO Task + function Fsm:OnAfterLanded( TaskUnit, Task ) + self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + + if self.Cargo:IsInRadius( TaskUnit:GetPointVec2() ) then + if TaskUnit:InAir() then + self:__Land( -0.1 ) + else + Task:GetMission():GetCommandCenter():MessageToGroup( "Preparing to board in 10 seconds ...", TaskUnit:GetGroup(), "Boarding" ) + self:__PrepareBoarding( -10 ) + end + else + self:__ArriveAtCargo( -0.1 ) + end + end + + --- + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_CARGO#TASK_CARGO Task + function Fsm:OnAfterPrepareBoarding( TaskUnit, Task, From, Event, To, Cargo ) + self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + + self.Cargo = Cargo + if self.Cargo:IsInRadius( TaskUnit:GetPointVec2() ) then + if TaskUnit:InAir() then + self:__Land( -0.1 ) + else + self:__Board( -0.1 ) + end + else + self:__ArriveAtCargo( -0.1 ) + end + end + + --- + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_CARGO#TASK_CARGO Task + function Fsm:OnAfterBoard( TaskUnit, Task ) + self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + + function self.Cargo:OnEnterLoaded( From, Event, To, TaskUnit, TaskProcess ) + + self:E({From, Event, To, TaskUnit, TaskProcess }) + + TaskProcess:__Boarded( 0.1 ) + + end + + + if self.Cargo:IsInRadius( TaskUnit:GetPointVec2() ) then + if TaskUnit:InAir() then + self:__Land( -0.1 ) + else + Task:GetMission():GetCommandCenter():MessageToGroup( "Boarding ...", TaskUnit:GetGroup(), "Boarding" ) + self.Cargo:Board( TaskUnit, self ) + end + else + self:__ArriveAtCargo( -0.1 ) + end + end + + --- + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_CARGO#TASK_CARGO Task + function Fsm:OnAfterBoarded( TaskUnit, Task ) + self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + + Task:GetMission():GetCommandCenter():MessageToGroup( "Boarded ...", TaskUnit:GetGroup(), "Boarding" ) + end return self @@ -165,7 +320,7 @@ do -- TASK_CARGO self:F({Cargo, TaskUnit}) local ProcessUnit = self:GetUnitProcess( TaskUnit ) - local ActRouteCargo = ProcessUnit:GetProcess( "RoutingToCargo", "RouteToCargoPickup" ) -- Actions.Act_Route#ACT_ROUTE_POINT + local ActRouteCargo = ProcessUnit:GetProcess( "RoutingToPickup", "RouteToPickupPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT ActRouteCargo:SetPointVec2( Cargo:GetPointVec2() ) ActRouteCargo:SetRange( Cargo:GetBoardingRange() ) return self @@ -284,10 +439,10 @@ do -- TASK_CARGO_TRANSPORT -- @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 AI_Cargo#AI_CARGO Cargo The cargo. + -- @param Core.Set#SET_CARGO SetCargo The scope of the cargo to be transported. -- @return #TASK_CARGO_TRANSPORT self - function TASK_CARGO_TRANSPORT:New( Mission, SetGroup, TaskName, Cargo ) - local self = BASE:Inherit( self, TASK_CARGO:New( Mission, SetGroup, TaskName, Cargo, "TRANSPORT" ) ) -- #TASK_CARGO_TRANSPORT + function TASK_CARGO_TRANSPORT:New( Mission, SetGroup, TaskName, SetCargo ) + local self = BASE:Inherit( self, TASK_CARGO:New( Mission, SetGroup, TaskName, SetCargo, "Transport" ) ) -- #TASK_CARGO_TRANSPORT self:F() return self 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 4db9338c3..3b2b611ed 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 DYNAMIC INCLUDE START *** ' ) -env.info( 'Moose Generation Timestamp: 20170325_0745' ) +env.info( 'Moose Generation Timestamp: 20170328_0728' ) local base = _G diff --git a/Moose Mission Setup/Moose.lua b/Moose Mission Setup/Moose.lua index 4db9338c3..3b2b611ed 100644 --- a/Moose Mission Setup/Moose.lua +++ b/Moose Mission Setup/Moose.lua @@ -1,5 +1,5 @@ env.info( '*** MOOSE DYNAMIC INCLUDE START *** ' ) -env.info( 'Moose Generation Timestamp: 20170325_0745' ) +env.info( 'Moose Generation Timestamp: 20170328_0728' ) local base = _G diff --git a/Moose Test Missions/TSK - Task Modelling/TSK-100 - Cargo Pickup/TSK-100 - Cargo Pickup.lua b/Moose Test Missions/TSK - Task Modelling/TSK-100 - Cargo Pickup/TSK-100 - Cargo Pickup.lua index 6ec266dd7..363e7fcdc 100644 --- a/Moose Test Missions/TSK - Task Modelling/TSK-100 - Cargo Pickup/TSK-100 - Cargo Pickup.lua +++ b/Moose Test Missions/TSK - Task Modelling/TSK-100 - Cargo Pickup/TSK-100 - Cargo Pickup.lua @@ -26,8 +26,10 @@ do CargoEngineer = UNIT:FindByName( "Engineer" ) InfantryCargo = AI_CARGO_UNIT:New( CargoEngineer, "Engineer", "Engineer Sven", "81", 500, 25 ) + + SetCargo = SET_CARGO:New():FilterTypes( "Engineer" ):FilterStart() - Task_Cargo_Pickup = TASK_CARGO_TRANSPORT:New( Mission, TransportHelicopters, "Transport.001", InfantryCargo ) + Task_Cargo_Pickup = TASK_CARGO_TRANSPORT:New( Mission, TransportHelicopters, "Transport.001", SetCargo ) end diff --git a/Moose Test Missions/TSK - Task Modelling/TSK-100 - Cargo Pickup/TSK-100 - Cargo Pickup.miz b/Moose Test Missions/TSK - Task Modelling/TSK-100 - Cargo Pickup/TSK-100 - Cargo Pickup.miz index 834f0ccfd46154264b8b32b12d2ac93b6edef4d7..a06b1f4d141b36bc1a151176192ca167d3a8229d 100644 GIT binary patch delta 11784 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 W#WX3|Q5?J^X~~foM&I^7RsRPG`?Jyj delta 11749 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 Date: Sun, 2 Apr 2017 08:04:43 +0200 Subject: [PATCH 05/14] Progress --- Moose Development/Moose/AI/AI_Cargo.lua | 25 ++- Moose Development/Moose/Core/Zone.lua | 112 ++++++---- .../Moose/Tasking/Task_CARGO.lua | 202 ++++++++++++++---- ...- A2G Task Dispatching DETECTION_AREAS.lua | 35 --- .../TSK-100 - Cargo Pickup.lua | 35 --- .../TSK-100 - Cargo Pickup.miz | Bin 28460 -> 0 bytes 6 files changed, 255 insertions(+), 154 deletions(-) delete mode 100644 Moose Test Missions/TAD - Task Dispatching/TAD-100 - A2G Task Dispatching DETECTION_AREAS/TAD-100 - A2G Task Dispatching DETECTION_AREAS.lua delete mode 100644 Moose Test Missions/TSK - Task Modelling/TSK-100 - Cargo Pickup/TSK-100 - Cargo Pickup.lua delete mode 100644 Moose Test Missions/TSK - Task Modelling/TSK-100 - Cargo Pickup/TSK-100 - Cargo Pickup.miz diff --git a/Moose Development/Moose/AI/AI_Cargo.lua b/Moose Development/Moose/AI/AI_Cargo.lua index 2e6f0d6ab..9b05de6c0 100644 --- a/Moose Development/Moose/AI/AI_Cargo.lua +++ b/Moose Development/Moose/AI/AI_Cargo.lua @@ -302,7 +302,12 @@ end function AI_CARGO:IsInRadius( PointVec2 ) self:F( { PointVec2 } ) - local Distance = PointVec2:DistanceFromPointVec2( self.CargoObject:GetPointVec2() ) + local Distance = 0 + if self:IsLoaded() then + Distance = PointVec2:DistanceFromPointVec2( self.CargoCarrier:GetPointVec2() ) + else + Distance = PointVec2:DistanceFromPointVec2( self.CargoObject:GetPointVec2() ) + end self:T( Distance ) if Distance <= self.ReportRadius then @@ -313,6 +318,24 @@ function AI_CARGO:IsInRadius( PointVec2 ) end +--- Check if Cargo is the given @{Zone}. +-- @param #AI_CARGO self +-- @param Core.Zone#ZONE_BASE Zone +-- @return #boolean **true** if cargo is in the Zone, **false** if cargo is not in the Zone. +function AI_CARGO:IsInZone( Zone ) + self:F( { Zone } ) + + if self:IsLoaded() then + return Zone:IsPointVec2InZone( self.CargoCarrier:GetPointVec2() ) + else + return Zone:IsPointVec2InZone( self.CargoObject:GetPointVec2() ) + end + + return nil + +end + + --- Check if CargoCarrier is near the Cargo to be Loaded. -- @param #AI_CARGO self -- @param Core.Point#POINT_VEC2 PointVec2 diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index 432a360a7..d7c00ae39 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -73,41 +73,41 @@ -- @extends Core.Base#BASE ---- # 1) ZONE_BASE class, extends @{Base#BASE} +--- # ZONE_BASE class, extends @{Base#BASE} -- -- This class is an abstract BASE class for derived classes, and is not meant to be instantiated. -- --- ## 1.1) Each zone has a name: +-- ## 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}: +-- ## Each zone implements two polymorphic functions defined in @{Zone#ZONE_BASE}: -- -- * @{#ZONE_BASE.IsVec2InZone}(): Returns if a Vec2 is within the zone. -- * @{#ZONE_BASE.IsVec3InZone}(): Returns if a Vec3 is within the zone. -- --- ## 1.3) A zone has a probability factor that can be set to randomize a selection between zones: +-- ## 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: +-- ## 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: +-- ## 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: +-- ## 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. -- --- === -- @field #ZONE_BASE ZONE_BASE +-- ZONE_BASE = { ClassName = "ZONE_BASE", ZoneName = "", @@ -144,20 +144,21 @@ function ZONE_BASE:GetName() return self.ZoneName end ---- Returns if a location is within the zone. + +--- Returns if a Vec2 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. +-- @param Dcs.DCSTypes#Vec2 Vec2 The Vec2 to test. +-- @return #boolean true if the Vec2 is within the zone. function ZONE_BASE:IsVec2InZone( Vec2 ) self:F2( Vec2 ) return false end ---- Returns if a point is within the zone. +--- Returns if a Vec3 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. +-- @return #boolean true if the Vec3 is within the zone. function ZONE_BASE:IsVec3InZone( Vec3 ) self:F2( Vec3 ) @@ -166,6 +167,31 @@ function ZONE_BASE:IsVec3InZone( Vec3 ) return InZone end +--- Returns if a PointVec2 is within the zone. +-- @param #ZONE_BASE self +-- @param Core.Point#POINT_VEC2 PointVec2 The PointVec2 to test. +-- @return #boolean true if the PointVec2 is within the zone. +function ZONE_BASE:IsPointVec2InZone( PointVec2 ) + self:F2( PointVec2 ) + + local InZone = self:IsVec2InZone( PointVec2:GetVec2() ) + + return InZone +end + +--- Returns if a PointVec3 is within the zone. +-- @param #ZONE_BASE self +-- @param Core.Point#POINT_VEC3 PointVec3 The PointVec3 to test. +-- @return #boolean true if the PointVec3 is within the zone. +function ZONE_BASE:IsPointVec3InZone( PointVec3 ) + self:F2( PointVec3 ) + + local InZone = self:IsPointVec2InZone( PointVec3 ) + + return InZone +end + + --- Returns the @{DCSTypes#Vec2} coordinate of the zone. -- @param #ZONE_BASE self -- @return #nil. @@ -310,29 +336,29 @@ end -- @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 +-- @extends #ZONE_BASE ---- # 2) @{Zone#ZONE_RADIUS} class, extends @{Zone#ZONE_BASE} +--- # ZONE_RADIUS class, extends @{Zone#ZONE_BASE} -- -- The ZONE_RADIUS class defined by a zone name, a location and a radius. -- This class implements the inherited functions from Core.Zone#ZONE_BASE taking into account the own zone format and properties. -- --- ## 2.1) @{Zone#ZONE_RADIUS} constructor +-- ## ZONE_RADIUS constructor -- -- * @{#ZONE_RADIUS.New}(): Constructor. -- --- ## 2.2) Manage the radius of the zone +-- ## Manage the radius of the zone -- -- * @{#ZONE_RADIUS.SetRadius}(): Sets the radius of the zone. -- * @{#ZONE_RADIUS.GetRadius}(): Returns the radius of the zone. -- --- ## 2.3) Manage the location of the zone +-- ## Manage the location of the zone -- -- * @{#ZONE_RADIUS.SetVec2}(): Sets the @{DCSTypes#Vec2} of the zone. -- * @{#ZONE_RADIUS.GetVec2}(): Returns the @{DCSTypes#Vec2} of the zone. -- * @{#ZONE_RADIUS.GetVec3}(): Returns the @{DCSTypes#Vec3} of the zone, taking an additional height parameter. -- --- ## 2.4) Zone point randomization +-- ## Zone point randomization -- -- Various functions exist to find random points within the zone. -- @@ -340,8 +366,6 @@ end -- * @{#ZONE_RADIUS.GetRandomPointVec2}(): Gets a @{Point#POINT_VEC2} object representing a random 2D point in the zone. -- * @{#ZONE_RADIUS.GetRandomPointVec3}(): Gets a @{Point#POINT_VEC3} object representing a random 3D point in the zone. Note that the height of the point is at landheight. -- --- === --- -- @field #ZONE_RADIUS ZONE_RADIUS -- ZONE_RADIUS = { @@ -615,17 +639,15 @@ end --- @type ZONE --- @extends Core.Zone#ZONE_RADIUS +--- @type ZONE +-- @extends #ZONE_RADIUS ---- # 3) ZONE class, extends @{Zone#ZONE_RADIUS} +--- # 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 @{#ZONE_RADIUS} taking into account the own zone format and properties. -- --- === --- -- @field #ZONE ZONE -- ZONE = { @@ -655,18 +677,15 @@ function ZONE:New( ZoneName ) end ---- The ZONE_UNIT class defined by a zone around a @{Unit#UNIT} with a radius. --- @type ZONE_UNIT +--- @type ZONE_UNIT -- @field Wrapper.Unit#UNIT ZoneUNIT -- @extends Core.Zone#ZONE_RADIUS ---- # 4) #ZONE_UNIT class, extends @{Zone#ZONE_RADIUS} +--- # ZONE_UNIT class, extends @{Zone#ZONE_RADIUS} -- -- The ZONE_UNIT class defined by a zone around a @{Unit#UNIT} with a radius. -- This class implements the inherited functions from @{#ZONE_RADIUS} taking into account the own zone format and properties. -- --- === --- -- @field #ZONE_UNIT ZONE_UNIT -- ZONE_UNIT = { @@ -751,16 +770,14 @@ end --- @type ZONE_GROUP -- @field Wrapper.Group#GROUP ZoneGROUP --- @extends Core.Zone#ZONE_RADIUS +-- @extends #ZONE_RADIUS ---- # 5) #ZONE_GROUP class, extends @{Zone#ZONE_RADIUS} +--- # ZONE_GROUP class, extends @{Zone#ZONE_RADIUS} -- -- The ZONE_GROUP class defines by a zone around a @{Group#GROUP} with a radius. The current leader of the group defines the center of the zone. -- This class implements the inherited functions from @{Zone#ZONE_RADIUS} taking into account the own zone format and properties. -- --- === --- -- @field #ZONE_GROUP ZONE_GROUP -- ZONE_GROUP = { @@ -818,16 +835,16 @@ end --- @type ZONE_POLYGON_BASE -- @field #ZONE_POLYGON_BASE.ListVec2 Polygon The polygon defined by an array of @{DCSTypes#Vec2}. --- @extends Core.Zone#ZONE_BASE +-- @extends #ZONE_BASE ---- # 6) ZONE_POLYGON_BASE class, extends @{Zone#ZONE_BASE} +--- # ZONE_POLYGON_BASE class, extends @{Zone#ZONE_BASE} -- -- The ZONE_POLYGON_BASE class defined by a sequence of @{Group#GROUP} waypoints within the Mission Editor, forming a polygon. -- This class implements the inherited functions from @{Zone#ZONE_RADIUS} taking into account the own zone format and properties. -- This class is an abstract BASE class for derived classes, and is not meant to be instantiated. -- --- ## 6.1) Zone point randomization +-- ## Zone point randomization -- -- Various functions exist to find random points within the zone. -- @@ -835,8 +852,6 @@ end -- * @{#ZONE_POLYGON_BASE.GetRandomPointVec2}(): Return a @{Point#POINT_VEC2} object representing a random 2D point within the zone. -- * @{#ZONE_POLYGON_BASE.GetRandomPointVec3}(): Return a @{Point#POINT_VEC3} object representing a random 3D point at landheight within the zone. -- --- === --- -- @field #ZONE_POLYGON_BASE ZONE_POLYGON_BASE -- ZONE_POLYGON_BASE = { @@ -870,6 +885,17 @@ function ZONE_POLYGON_BASE:New( ZoneName, PointsArray ) return self end +--- Returns the center location of the polygon. +-- @param #ZONE_GROUP self +-- @return Dcs.DCSTypes#Vec2 The location of the zone based on the @{Group} location. +function ZONE_POLYGON_BASE:GetVec2() + self:F( self.ZoneName ) + + local Bounds = self:GetBoundingSquare() + + return { x = ( Bounds.x2 + Bounds.x1 ) / 2, y = ( Bounds.y2 + Bounds.y1 ) / 2 } +end + --- Flush polygon coordinates as a table in DCS.log. -- @param #ZONE_POLYGON_BASE self -- @return #ZONE_POLYGON_BASE self @@ -1073,16 +1099,14 @@ end --- @type ZONE_POLYGON --- @extends Core.Zone#ZONE_POLYGON_BASE +-- @extends #ZONE_POLYGON_BASE ---- # 7) ZONE_POLYGON class, extends @{Zone#ZONE_POLYGON_BASE} +--- # 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. -- --- === --- -- @field #ZONE_POLYGON ZONE_POLYGON -- ZONE_POLYGON = { diff --git a/Moose Development/Moose/Tasking/Task_CARGO.lua b/Moose Development/Moose/Tasking/Task_CARGO.lua index d44a8a920..e234438c2 100644 --- a/Moose Development/Moose/Tasking/Task_CARGO.lua +++ b/Moose Development/Moose/Tasking/Task_CARGO.lua @@ -75,6 +75,8 @@ do -- TASK_CARGO self.SetCargo = SetCargo self.TaskType = TaskType + + self.DeployZones = {} -- setmetatable( {}, { __mode = "v" } ) -- weak table on value Mission:AddTask( self ) @@ -83,14 +85,14 @@ do -- TASK_CARGO Fsm:AddProcess ( "Planned", "Accept", ACT_ASSIGN_ACCEPT:New( self.TaskBriefing ), { Assigned = "SelectAction", Rejected = "Reject" } ) - Fsm:AddTransition( { "Assigned", "Landed", "Boarded", "Deployed" } , "SelectAction", "WaitingForCommand" ) + Fsm:AddTransition( "*", "SelectAction", "WaitingForCommand" ) Fsm:AddTransition( "WaitingForCommand", "RouteToPickup", "RoutingToPickup" ) Fsm:AddProcess ( "RoutingToPickup", "RouteToPickupPoint", ACT_ROUTE_POINT:New(), { Arrived = "ArriveAtPickup" } ) Fsm:AddTransition( "Arrived", "ArriveAtPickup", "ArrivedAtPickup" ) Fsm:AddTransition( "WaitingForCommand", "RouteToDeploy", "RoutingToDeploy" ) - Fsm:AddProcess ( "RoutingToDeploy", "RouteToDeployPoint", ACT_ROUTE_POINT:New(), { Arrived = "ArriveAtDeploy" } ) + Fsm:AddProcess ( "RoutingToDeploy", "RouteToDeployZone", ACT_ROUTE_ZONE:New(), { Arrived = "ArriveAtDeploy" } ) Fsm:AddTransition( "ArrivedAtDeploy", "ArriveAtDeploy", "ArrivedAtDeploy" ) Fsm:AddTransition( { "ArrivedAtPickup", "ArrivedAtDeploy" }, "Land", "Landing" ) @@ -146,14 +148,27 @@ do -- TASK_CARGO end if Cargo:IsLoaded() then - MENU_GROUP_COMMAND:New( - TaskUnit:GetGroup(), - "Deploy cargo " .. Cargo.Name, - TaskUnit.Menu, - self.MenuBoardCargo, - self, - Cargo - ) + for DeployZoneName, DeployZone in pairs( Task.DeployZones ) do + if Cargo:IsInZone( DeployZone ) then + MENU_GROUP_COMMAND:New( + TaskUnit:GetGroup(), + "Deploy cargo " .. Cargo.Name, + TaskUnit.Menu, + self.MenuUnBoardCargo, + self, + Cargo + ) + else + MENU_GROUP_COMMAND:New( + TaskUnit:GetGroup(), + "Route to deploy zone " .. DeployZoneName, + TaskUnit.Menu, + self.MenuRouteToDeploy, + self, + DeployZone + ) + end + end end end @@ -174,10 +189,18 @@ do -- TASK_CARGO self:__PrepareBoarding( 1.0, Cargo ) end + function Fsm:MenuUnBoardCargo( Cargo ) + self:__PrepareUnBoarding( 1.0, Cargo ) + end + function Fsm:MenuRouteToPickup( Cargo ) self:__RouteToPickup( 1.0, Cargo ) end + function Fsm:MenuRouteToDeploy( DeployZone ) + self:__RouteToDeploy( 1.0, DeployZone ) + end + --- Route to Cargo -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit @@ -196,16 +219,54 @@ do -- TASK_CARGO -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit -- @param Tasking.Task_CARGO#TASK_CARGO Task - function Fsm:OnAfterArriveAtPickup( TaskUnit, Task ) + function Fsm:onafterArriveAtPickup( TaskUnit, Task ) self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) if self.Cargo:IsInRadius( TaskUnit:GetPointVec2() ) then - self:__Land( -0.1 ) + if TaskUnit:IsAir() then + self:__Land( -0.1 ) + else + self:__SelectAction( -0.1 ) + end else - self:__ArriveAtCargo( -10 ) + self:__ArriveAtPickup( -10 ) end end + + --- Route to DeployZone + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + function Fsm:onafterRouteToDeploy( TaskUnit, Task, From, Event, To, DeployZone ) + self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + + + self.DeployZone = DeployZone + Task:SetDeployZone( self.DeployZone, TaskUnit ) + self:__RouteToDeployZone( 0.1 ) + end + + + --- + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_CARGO#TASK_CARGO Task + function Fsm:onafterArriveAtDeploy( TaskUnit, Task ) + self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + + if TaskUnit:IsInZone( self.DeployZone ) then + if TaskUnit:IsAir() then + self:__Land( -0.1 ) + else + self:__SelectAction( -0.1 ) + end + else + self:__ArriveAtDeploy( -10 ) + end + end + + + --- -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit @@ -291,7 +352,8 @@ do -- TASK_CARGO self:__ArriveAtCargo( -0.1 ) end end - + + --- -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit @@ -300,8 +362,53 @@ do -- TASK_CARGO self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) Task:GetMission():GetCommandCenter():MessageToGroup( "Boarded ...", TaskUnit:GetGroup(), "Boarding" ) + self:__SelectAction( 1 ) end + + --- + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_CARGO#TASK_CARGO Task + function Fsm:OnAfterPrepareUnBoarding( TaskUnit, Task, From, Event, To, Cargo, DeployZone ) + self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + + self.DeployZone = DeployZone + self.Cargo:__UnBoard( -0.1, DeployZone ) + end + + --- + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_CARGO#TASK_CARGO Task + function Fsm:OnAfterUnBoard( TaskUnit, Task ) + self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + + function self.Cargo:OnEnterUnLoaded( From, Event, To, TaskUnit, TaskProcess ) + + self:E({From, Event, To, TaskUnit, TaskProcess }) + + TaskProcess:__UnBoarded( 0.1 ) + + end + + Task:GetMission():GetCommandCenter():MessageToGroup( "UnBoarding ...", TaskUnit:GetGroup(), "UnBoarding" ) + self.Cargo:__UnBoard( -0.1, self.DeployZone ) + end + + + --- + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_CARGO#TASK_CARGO Task + function Fsm:OnAfterUnBoarded( TaskUnit, Task ) + self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + + Task:GetMission():GetCommandCenter():MessageToGroup( "UnBoarded ...", TaskUnit:GetGroup(), "UnBoarding" ) + self:__SelectAction( 1 ) + end + + return self end @@ -311,6 +418,7 @@ do -- TASK_CARGO return self:GetStateString() .. " - " .. self:GetTaskName() .. " ( " .. self.TargetSetUnit:GetUnitTypesText() .. " )" end + --- @param #TASK_CARGO self -- @param AI.AI_Cargo#AI_CARGO Cargo The cargo. -- @param Wrapper.Unit#UNIT TaskUnit @@ -326,41 +434,57 @@ do -- TASK_CARGO return self end + --- @param #TASK_CARGO self - -- @param Core.Point#POINT_VEC2 TargetPointVec2 The PointVec2 object where the Target is located on the map. + -- @param Core.Zone#ZONE DeployZone -- @param Wrapper.Unit#UNIT TaskUnit - function TASK_CARGO:SetTargetPointVec2( TargetPointVec2, TaskUnit ) + -- @return #TASK_CARGO + function TASK_CARGO:SetDeployZone( DeployZone, TaskUnit ) local ProcessUnit = self:GetUnitProcess( TaskUnit ) - local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT - ActRouteTarget:SetPointVec2( TargetPointVec2 ) + local ActRouteDeployZone = ProcessUnit:GetProcess( "RoutingToDeploy", "RouteToDeployZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE + ActRouteDeployZone:SetZone( DeployZone ) + return self end - - --- @param #TASK_CARGO self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return Core.Point#POINT_VEC2 The PointVec2 object where the Target is located on the map. - function TASK_CARGO:GetTargetPointVec2( TaskUnit ) - - local ProcessUnit = self:GetUnitProcess( TaskUnit ) - - local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT - return ActRouteTarget:GetPointVec2() - end - - - --- @param #TASK_CARGO self - -- @param Core.Zone#ZONE_BASE TargetZone The Zone object where the Target is located on the map. - -- @param Wrapper.Unit#UNIT TaskUnit - function TASK_CARGO:SetTargetZone( TargetZone, TaskUnit ) - local ProcessUnit = self:GetUnitProcess( TaskUnit ) + --- @param #TASK_CARGO self + -- @param Core.Zone#ZONE DeployZone + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return #TASK_CARGO + function TASK_CARGO:AddDeployZone( DeployZone, TaskUnit ) + + self.DeployZones[DeployZone:GetName()] = DeployZone - local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE - ActRouteTarget:SetZone( TargetZone ) + return self end - + + --- @param #TASK_CARGO self + -- @param Core.Zone#ZONE DeployZone + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return #TASK_CARGO + function TASK_CARGO:RemoveDeployZone( DeployZone, TaskUnit ) + + self.DeployZones[DeployZone:GetName()] = nil + + return self + end + + --- @param #TASK_CARGO self + -- @param @list DeployZones + -- @param Wrapper.Unit#UNIT TaskUnit + -- @return #TASK_CARGO + function TASK_CARGO:SetDeployZones( DeployZones, TaskUnit ) + + for DeployZoneID, DeployZone in pairs( DeployZones ) do + self.DeployZones[DeployZone:GetName()] = DeployZone + end + + return self + end + + --- @param #TASK_CARGO self -- @param Wrapper.Unit#UNIT TaskUnit diff --git a/Moose Test Missions/TAD - Task Dispatching/TAD-100 - A2G Task Dispatching DETECTION_AREAS/TAD-100 - A2G Task Dispatching DETECTION_AREAS.lua b/Moose Test Missions/TAD - Task Dispatching/TAD-100 - A2G Task Dispatching DETECTION_AREAS/TAD-100 - A2G Task Dispatching DETECTION_AREAS.lua deleted file mode 100644 index afcfee050..000000000 --- a/Moose Test Missions/TAD - Task Dispatching/TAD-100 - A2G Task Dispatching DETECTION_AREAS/TAD-100 - A2G Task Dispatching DETECTION_AREAS.lua +++ /dev/null @@ -1,35 +0,0 @@ ---- --- Name: TAD-100 - A2G Task Dispatching DETECTION_AREAS --- Author: FlightControl --- Date Created: 06 Mar 2017 --- --- # Situation: --- --- This mission demonstrates the dynamic task dispatching for Air to Ground operations. --- FACA's and FAC's are patrolling around the battle zone, while detecting targets. --- The detection method used is the DETECTION_AREAS method, which groups detected targets into zones. --- --- # Test cases: --- --- 1. Observe the FAC(A)'s detecting targets and grouping them. --- For test, each zone will have a circle of tyres, that are visible on the map too. --- 2. Check that the HQ provides menus to engage on a task set by the FACs. --- -HQ = GROUP:FindByName( "HQ", "Bravo HQ" ) - -CommandCenter = COMMANDCENTER:New( HQ, "Lima" ) - -Scoring = SCORING:New( "Detect Demo" ) - -Mission = MISSION - :New( CommandCenter, "Overlord", "High", "Attack Detect Mission Briefing", coalition.side.RED ) - :AddScoring( Scoring ) - -FACSet = SET_GROUP:New():FilterPrefixes( "FAC" ):FilterCoalitions("red"):FilterStart() - -FACAreas = DETECTION_AREAS:New( FACSet, 500 ) -FACAreas:BoundDetectedZones() - -AttackGroups = SET_GROUP:New():FilterCoalitions( "red" ):FilterPrefixes( "Attack" ):FilterStart() -TaskDispatcher = TASK_A2G_DISPATCHER:New( Mission, HQ, AttackGroups, FACAreas ) - diff --git a/Moose Test Missions/TSK - Task Modelling/TSK-100 - Cargo Pickup/TSK-100 - Cargo Pickup.lua b/Moose Test Missions/TSK - Task Modelling/TSK-100 - Cargo Pickup/TSK-100 - Cargo Pickup.lua deleted file mode 100644 index 363e7fcdc..000000000 --- a/Moose Test Missions/TSK - Task Modelling/TSK-100 - Cargo Pickup/TSK-100 - Cargo Pickup.lua +++ /dev/null @@ -1,35 +0,0 @@ ---- --- Name: TSK-100 - Cargo Pickup --- Author: FlightControl --- Date Created: 25 Mar 2017 --- --- # Situation: --- --- This mission demonstrates the pickup of cargo. --- --- # Test cases: --- --- - -do - HQ = GROUP:FindByName( "HQ", "Bravo HQ" ) - - CommandCenter = COMMANDCENTER:New( HQ, "Lima" ) - - Scoring = SCORING:New( "Pickup Demo" ) - - Mission = MISSION - :New( CommandCenter, "Transport", "High", "Pickup the team", coalition.side.BLUE ) - :AddScoring( Scoring ) - - TransportHelicopters = SET_GROUP:New():FilterPrefixes( "Transport" ):FilterStart() - - CargoEngineer = UNIT:FindByName( "Engineer" ) - InfantryCargo = AI_CARGO_UNIT:New( CargoEngineer, "Engineer", "Engineer Sven", "81", 500, 25 ) - - SetCargo = SET_CARGO:New():FilterTypes( "Engineer" ):FilterStart() - - Task_Cargo_Pickup = TASK_CARGO_TRANSPORT:New( Mission, TransportHelicopters, "Transport.001", SetCargo ) - -end - diff --git a/Moose Test Missions/TSK - Task Modelling/TSK-100 - Cargo Pickup/TSK-100 - Cargo Pickup.miz b/Moose Test Missions/TSK - Task Modelling/TSK-100 - Cargo Pickup/TSK-100 - Cargo Pickup.miz deleted file mode 100644 index a06b1f4d141b36bc1a151176192ca167d3a8229d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28460 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 Xfd;K6|1jMB0M-U_ZP&H_$GiUmfm786 From f8f68ea6957911975bd390cc4cc879fd43eb3f2c Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sun, 2 Apr 2017 09:16:34 +0200 Subject: [PATCH 06/14] Progress --- .../Moose/Tasking/Task_CARGO.lua | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/Moose Development/Moose/Tasking/Task_CARGO.lua b/Moose Development/Moose/Tasking/Task_CARGO.lua index e234438c2..b61eb6b86 100644 --- a/Moose Development/Moose/Tasking/Task_CARGO.lua +++ b/Moose Development/Moose/Tasking/Task_CARGO.lua @@ -93,7 +93,7 @@ do -- TASK_CARGO Fsm:AddTransition( "WaitingForCommand", "RouteToDeploy", "RoutingToDeploy" ) Fsm:AddProcess ( "RoutingToDeploy", "RouteToDeployZone", ACT_ROUTE_ZONE:New(), { Arrived = "ArriveAtDeploy" } ) - Fsm:AddTransition( "ArrivedAtDeploy", "ArriveAtDeploy", "ArrivedAtDeploy" ) + Fsm:AddTransition( "Arrived", "ArriveAtDeploy", "ArrivedAtDeploy" ) Fsm:AddTransition( { "ArrivedAtPickup", "ArrivedAtDeploy" }, "Land", "Landing" ) Fsm:AddTransition( "Landing", "Landed", "Landed" ) @@ -222,14 +222,10 @@ do -- TASK_CARGO function Fsm:onafterArriveAtPickup( TaskUnit, Task ) self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - if self.Cargo:IsInRadius( TaskUnit:GetPointVec2() ) then - if TaskUnit:IsAir() then - self:__Land( -0.1 ) - else - self:__SelectAction( -0.1 ) - end + if TaskUnit:IsAir() then + self:__Land( -0.1 ) else - self:__ArriveAtPickup( -10 ) + self:__SelectAction( -0.1 ) end end @@ -254,14 +250,10 @@ do -- TASK_CARGO function Fsm:onafterArriveAtDeploy( TaskUnit, Task ) self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - if TaskUnit:IsInZone( self.DeployZone ) then - if TaskUnit:IsAir() then - self:__Land( -0.1 ) - else - self:__SelectAction( -0.1 ) - end + if TaskUnit:IsAir() then + self:__Land( -0.1 ) else - self:__ArriveAtDeploy( -10 ) + self:__SelectAction( -0.1 ) end end From 7e9b97dda0687c96f03f9867f9f7c4c36bd1d1e1 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sun, 2 Apr 2017 09:39:00 +0200 Subject: [PATCH 07/14] Documentation --- .../Moose/Tasking/Task_CARGO.lua | 19 +- docs/Documentation/AI_Balancer.html | 1 + docs/Documentation/AI_Cap.html | 1 + docs/Documentation/AI_Cas.html | 1 + docs/Documentation/AI_Patrol.html | 1 + docs/Documentation/Account.html | 1 + docs/Documentation/Airbase.html | 1 + docs/Documentation/AirbasePolice.html | 1 + docs/Documentation/Assign.html | 1 + docs/Documentation/Base.html | 1 + docs/Documentation/Cargo.html | 438 +++++++--- docs/Documentation/CleanUp.html | 1 + docs/Documentation/Client.html | 1 + docs/Documentation/CommandCenter.html | 1 + docs/Documentation/Controllable.html | 1 + docs/Documentation/Database.html | 238 ++++- docs/Documentation/Detection.html | 3 +- docs/Documentation/DetectionManager.html | 1 + docs/Documentation/Escort.html | 1 + docs/Documentation/Event.html | 137 +++ docs/Documentation/Fsm.html | 1 + docs/Documentation/Group.html | 5 +- docs/Documentation/Identifiable.html | 1 + docs/Documentation/MOVEMENT.html | 5 + docs/Documentation/Menu.html | 1 + docs/Documentation/Message.html | 1 + docs/Documentation/MissileTrainer.html | 1 + docs/Documentation/Mission.html | 1 + docs/Documentation/Object.html | 1 + docs/Documentation/Point.html | 1 + docs/Documentation/Positionable.html | 1 + docs/Documentation/Process_JTAC.html | 1 + docs/Documentation/Process_Pickup.html | 1 + docs/Documentation/Radio.html | 1 + docs/Documentation/Route.html | 1 + docs/Documentation/Scenery.html | 1 + docs/Documentation/ScheduleDispatcher.html | 1 + docs/Documentation/Scheduler.html | 1 + docs/Documentation/Scoring.html | 1 + docs/Documentation/Sead.html | 1 + docs/Documentation/Set.html | 637 ++++++++++++++ docs/Documentation/Smoke.html | 1 + docs/Documentation/Spawn.html | 35 +- docs/Documentation/Static.html | 1 + docs/Documentation/Task.html | 224 ++--- docs/Documentation/Task_A2G.html | 25 +- docs/Documentation/Task_A2G_Dispatcher.html | 1 + docs/Documentation/Task_CARGO.html | 826 ++++++++++++++++++ docs/Documentation/Task_PICKUP.html | 1 + docs/Documentation/Unit.html | 1 + docs/Documentation/Utils.html | 1 + docs/Documentation/Zone.html | 179 ++-- docs/Documentation/index.html | 9 +- docs/Documentation/routines.html | 1 + docs/Presentations/TASK_CARGO/Dia1.JPG | Bin 0 -> 223630 bytes 55 files changed, 2422 insertions(+), 398 deletions(-) create mode 100644 docs/Documentation/Task_CARGO.html create mode 100644 docs/Presentations/TASK_CARGO/Dia1.JPG diff --git a/Moose Development/Moose/Tasking/Task_CARGO.lua b/Moose Development/Moose/Tasking/Task_CARGO.lua index b61eb6b86..44d3523d9 100644 --- a/Moose Development/Moose/Tasking/Task_CARGO.lua +++ b/Moose Development/Moose/Tasking/Task_CARGO.lua @@ -1,11 +1,20 @@ ---- **Tasking** - The TASK_CARGO models tasks for players to transport @{Cargo}. --- --- ![Banner Image]() --- +--- **Tasking (Release 2.1)** -- The TASK_CARGO models tasks for players to transport @{Cargo}. -- +-- ![Banner Image](..\Presentations\TASK_CARGO\Dia1.JPG) -- -- ==== -- +-- Cargo are units or cargo objects within DCS world that allow to be transported or sling loaded by other units. +-- The CARGO class, as part of the moose core, is able to Board, Load, UnBoard and UnLoad from Carrier units. +-- This collection of classes in this module define tasks for human players to handle cargo objects. +-- Cargo can be transported, picked-up, deployed and sling-loaded from and to other places. +-- +-- The following classes are important to consider: +-- +-- * @{#TASK_CARGO_TRANSPORT}: Defines a task for a human player to transport a set of cargo between various zones. +-- +-- == +-- -- # **API CHANGE HISTORY** -- -- The underlying change log documents the API changes. Please read this carefully. The following notation is used: @@ -23,8 +32,6 @@ -- -- ### Contributions: -- --- * **[WingThor]**: Concept, Advice & Testing. --- -- ### Authors: -- -- * **FlightControl**: Concept, Design & Programming. diff --git a/docs/Documentation/AI_Balancer.html b/docs/Documentation/AI_Balancer.html index 2b5f48f01..5e65668f3 100644 --- a/docs/Documentation/AI_Balancer.html +++ b/docs/Documentation/AI_Balancer.html @@ -63,6 +63,7 @@

  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/AI_Cap.html b/docs/Documentation/AI_Cap.html index 0657d3fa0..425bec51b 100644 --- a/docs/Documentation/AI_Cap.html +++ b/docs/Documentation/AI_Cap.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/AI_Cas.html b/docs/Documentation/AI_Cas.html index 10992a925..255b3646d 100644 --- a/docs/Documentation/AI_Cas.html +++ b/docs/Documentation/AI_Cas.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/AI_Patrol.html b/docs/Documentation/AI_Patrol.html index f4e911447..0f68f6d16 100644 --- a/docs/Documentation/AI_Patrol.html +++ b/docs/Documentation/AI_Patrol.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Account.html b/docs/Documentation/Account.html index ad23ec3ca..69239c4d4 100644 --- a/docs/Documentation/Account.html +++ b/docs/Documentation/Account.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Airbase.html b/docs/Documentation/Airbase.html index 328b41564..40b70c5df 100644 --- a/docs/Documentation/Airbase.html +++ b/docs/Documentation/Airbase.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/AirbasePolice.html b/docs/Documentation/AirbasePolice.html index 93fdd0ae2..3bfe030d8 100644 --- a/docs/Documentation/AirbasePolice.html +++ b/docs/Documentation/AirbasePolice.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Assign.html b/docs/Documentation/Assign.html index e352ed51e..039ba460e 100644 --- a/docs/Documentation/Assign.html +++ b/docs/Documentation/Assign.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Base.html b/docs/Documentation/Base.html index 6a32f5c84..2a833045d 100644 --- a/docs/Documentation/Base.html +++ b/docs/Documentation/Base.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Cargo.html b/docs/Documentation/Cargo.html index d0b6aea75..e811fa80c 100644 --- a/docs/Documentation/Cargo.html +++ b/docs/Documentation/Cargo.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • @@ -97,60 +98,7 @@
  • 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.

    - -

    The AICARGO is a state machine: it manages the different events and states of the cargo. -All derived classes from AICARGO 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) #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.

    This module is still under construction, but is described above works already, and will keep working ...

    @@ -160,19 +108,25 @@ Use the event functions as described above to Load, UnLoad, Board, UnBoard the A AI_CARGO +

    AI_CARGO class, extends Fsm#FSM_PROCESS

    +

    The AI_CARGO class defines the core functions that defines a cargo object within MOOSE.

    AI_CARGO_GROUP +

    AI_CARGO_GROUP class

    +

    The AI_CARGO_GROUP class defines a cargo that is represented by a group of Unit objects within the simulator, and can be transported by a carrier.

    AI_CARGO_GROUPED +

    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.

    @@ -190,7 +144,9 @@ Use the event functions as described above to Load, UnLoad, Board, UnBoard the A AI_CARGO_UNIT +

    AI_CARGO_UNIT class, extends #AICARGOREPRESENTABLE

    +

    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.

    @@ -218,24 +174,66 @@ Use the event functions as described above to Load, UnLoad, Board, UnBoard the A AI_CARGO.CargoObject

    The alive DCS object representing the cargo. This value can be nil, meaning, that the cargo is not represented anywhere...

    - - - - AI_CARGO.ClassName - - AI_CARGO.Containable

    This flag defines if the cargo can be contained within a DCS Unit.

    + + + + AI_CARGO:GetBoardingRange() + +

    Get the range till cargo will board.

    + + + + AI_CARGO:GetName() + +

    Get the name of the Cargo.

    + + + + AI_CARGO:GetPointVec2() + +

    Get the current PointVec2 of the cargo.

    + + + + AI_CARGO:GetType() + +

    Get the type of the Cargo.

    + + + + AI_CARGO:IsInRadius(PointVec2) + +

    Check if CargoCarrier is in the radius for the Cargo to be Loaded.

    + + + + AI_CARGO:IsInZone(Zone) + +

    Check if Cargo is the given Zone.

    + + + + AI_CARGO:IsLoaded() + +

    Check if cargo is loaded.

    AI_CARGO:IsNear(PointVec2)

    Check if CargoCarrier is near the Cargo to be Loaded.

    + + + + AI_CARGO:IsUnLoaded() + +

    Check if cargo is unloaded.

    @@ -396,12 +394,6 @@ Use the event functions as described above to Load, UnLoad, Board, UnBoard the A AI_CARGO_GROUP.CargoSet

    A set of cargo objects.

    - - - - AI_CARGO_GROUP.ClassName - - @@ -421,12 +413,6 @@ Use the event functions as described above to Load, UnLoad, Board, UnBoard the A

    Type AI_CARGO_GROUPED

    - - - - - + @@ -607,7 +593,7 @@ Use the event functions as described above to Load, UnLoad, Board, UnBoard the A - + @@ -619,7 +605,7 @@ Use the event functions as described above to Load, UnLoad, Board, UnBoard the A - + @@ -643,7 +629,7 @@ Use the event functions as described above to Load, UnLoad, Board, UnBoard the A - + @@ -667,6 +653,52 @@ Use the event functions as described above to Load, UnLoad, Board, UnBoard the A
    +

    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.

    + +

    AI_CARGO Events:

    + +
      +
    • #AI( ToCarrier ): Boards the cargo to a carrier.
    • +
    • #AI( ToCarrier ): Loads the cargo into a carrier, regardless of its position.
    • +
    • #AI( ToPointVec2 ): UnBoard the cargo from a carrier. This will trigger a movement of the cargo to the option ToPointVec2.
    • +
    • #AI( ToPointVec2 ): UnLoads the cargo from a carrier.
    • +
    • #AI( Controllable ): The cargo is dead. The cargo process will be ended.
    • +
    + +

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

    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.

    • +
    @@ -681,6 +713,12 @@ Use the event functions as described above to Load, UnLoad, Board, UnBoard the A
    +

    AI_CARGO_GROUP class

    + +

    The AI_CARGO_GROUP 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_GROUP to and from carrier.

    @@ -695,6 +733,12 @@ Use the event functions as described above to Load, UnLoad, Board, UnBoard the A
    +

    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.

    @@ -737,6 +781,14 @@ Use the event functions as described above to Load, UnLoad, Board, UnBoard the A
    +

    AI_CARGO_UNIT class, extends #AICARGOREPRESENTABLE

    + +

    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.

    + +
    @@ -811,20 +863,6 @@ The Carrier that will hold the cargo.

    The alive DCS object representing the cargo. This value can be nil, meaning, that the cargo is not represented anywhere...

    - - -
    -
    - - #string - -AI_CARGO.ClassName - -
    -
    - - -
    @@ -844,6 +882,148 @@ The Carrier that will hold the cargo.

    + +AI_CARGO:GetBoardingRange() + +
    +
    + +

    Get the range till cargo will board.

    + +

    Return value

    + +

    #number: +The range till cargo will board.

    + +
    +
    +
    +
    + + +AI_CARGO:GetName() + +
    +
    + +

    Get the name of the Cargo.

    + +

    Return value

    + +

    #string: +The name of the Cargo.

    + +
    +
    +
    +
    + + +AI_CARGO:GetPointVec2() + +
    +
    + +

    Get the current PointVec2 of the cargo.

    + +

    Return value

    + +

    Core.Point#POINT_VEC2:

    + + +
    +
    +
    +
    + + +AI_CARGO:GetType() + +
    +
    + +

    Get the type of the Cargo.

    + +

    Return value

    + +

    #string: +The type of the Cargo.

    + +
    +
    +
    +
    + + +AI_CARGO:IsInRadius(PointVec2) + +
    +
    + +

    Check if CargoCarrier is in the radius for the Cargo to be Loaded.

    + +

    Parameter

    + +

    Return value

    + +

    #boolean:

    + + +
    +
    +
    +
    + + +AI_CARGO:IsInZone(Zone) + +
    +
    + +

    Check if Cargo is the given Zone.

    + +

    Parameter

    + +

    Return value

    + +

    #boolean: +true if cargo is in the Zone, false if cargo is not in the Zone.

    + +
    +
    +
    +
    + + +AI_CARGO:IsLoaded() + +
    +
    + +

    Check if cargo is loaded.

    + +

    Return value

    + +

    #boolean: +true if loaded

    + +
    +
    +
    +
    + AI_CARGO:IsNear(PointVec2) @@ -865,6 +1045,24 @@ The Carrier that will hold the cargo.

    #boolean:

    + +
    +
    +
    + + +AI_CARGO:IsUnLoaded() + +
    +
    + +

    Check if cargo is unloaded.

    + +

    Return value

    + +

    #boolean: +true if unloaded

    +
    @@ -1467,20 +1665,6 @@ The amount of seconds to delay the action.

    A set of cargo objects.

    - -
    -
    -
    - - #string - -AI_CARGO_GROUP.ClassName - -
    -
    - - -
    @@ -1556,20 +1740,6 @@ The amount of seconds to delay the action.

    - #string - -AI_CARGO_GROUPED.ClassName - -
    -
    - - - -
    -
    -
    -
    - AI_CARGO_GROUPED:New(CargoSet, Type, Name, Weight, ReportRadius, NearRadius) @@ -2426,6 +2596,7 @@ The UNIT carrying the package.

    + AI_CARGO_UNIT.CargoCarrier @@ -2467,13 +2638,17 @@ The UNIT carrying the package.

    - #string - -AI_CARGO_UNIT.ClassName + +AI_CARGO_UNIT:Destroy()
    +

    AICARGOUNIT Destructor.

    + +

    Return value

    + +

    #AICARGOUNIT:

    @@ -2548,7 +2723,7 @@ The UNIT carrying the package.

    -AI_CARGO_UNIT:onafterBoard(Event, From, To, CargoCarrier) +AI_CARGO_UNIT:onafterBoard(Event, From, To, CargoCarrier, ...)
    @@ -2576,6 +2751,11 @@ The UNIT carrying the package.

    CargoCarrier :

    + +
  • + +

    ... :

    +
  • @@ -2620,7 +2800,7 @@ The UNIT carrying the package.

    -AI_CARGO_UNIT:onenterBoarding(Event, From, To, CargoCarrier) +AI_CARGO_UNIT:onenterBoarding(Event, From, To, CargoCarrier, ...)
    @@ -2648,6 +2828,11 @@ The UNIT carrying the package.

    Wrapper.Unit#UNIT CargoCarrier :

    + +
  • + +

    ... :

    +
  • @@ -2770,7 +2955,7 @@ Point#POINT_VEC2

    -AI_CARGO_UNIT:onleaveBoarding(Event, From, To, CargoCarrier) +AI_CARGO_UNIT:onleaveBoarding(Event, From, To, CargoCarrier, ...)
    @@ -2798,6 +2983,11 @@ Point#POINT_VEC2

    Wrapper.Unit#UNIT CargoCarrier :

    + +
  • + +

    ... :

    +
  • diff --git a/docs/Documentation/CleanUp.html b/docs/Documentation/CleanUp.html index 60829d2bc..7cdcb71be 100644 --- a/docs/Documentation/CleanUp.html +++ b/docs/Documentation/CleanUp.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Client.html b/docs/Documentation/Client.html index 565f34b9b..748b7c484 100644 --- a/docs/Documentation/Client.html +++ b/docs/Documentation/Client.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/CommandCenter.html b/docs/Documentation/CommandCenter.html index 3384b0fa4..ae6d16afd 100644 --- a/docs/Documentation/CommandCenter.html +++ b/docs/Documentation/CommandCenter.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Controllable.html b/docs/Documentation/Controllable.html index 22bfaf97f..0e34ab8f8 100644 --- a/docs/Documentation/Controllable.html +++ b/docs/Documentation/Controllable.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Database.html b/docs/Documentation/Database.html index 0309494e8..31388c613 100644 --- a/docs/Documentation/Database.html +++ b/docs/Documentation/Database.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • @@ -83,12 +84,14 @@

    Mission designers can use the DATABASE class to refer to:

      +
    • STATICS
    • UNITS
    • GROUPS
    • CLIENTS
    • -
    • AIRPORTS
    • +
    • AIRBASES
    • PLAYERSJOINED
    • PLAYERS
    • +
    • CARGOS

    On top, for internal MOOSE administration purposes, the DATBASE administers the Unit and Group TEMPLATES as defined within the Mission Editor.

    @@ -131,9 +134,15 @@ The following iterator methods are currently available within the DATABASE:

    - + + + + + @@ -164,6 +173,12 @@ The following iterator methods are currently available within the DATABASE:

    + + + + @@ -191,9 +206,15 @@ The following iterator methods are currently available within the DATABASE:

    - + + + + + @@ -217,7 +238,13 @@ The following iterator methods are currently available within the DATABASE:

    + + + + @@ -248,6 +275,12 @@ The following iterator methods are currently available within the DATABASE:

    + + + + @@ -350,6 +383,18 @@ The following iterator methods are currently available within the DATABASE:

    + + + + + + + + @@ -502,7 +547,7 @@ The following iterator methods are currently available within the DATABASE:

    -DATABASE:AddAirbase(DCSAirbaseName) +DATABASE:AddAirbase(AirbaseName)
    @@ -513,7 +558,35 @@ The following iterator methods are currently available within the DATABASE:

    • -

      DCSAirbaseName :

      +

      #string AirbaseName : +The name of the airbase

      + +
    • +
    +
    + +
    +
    + + +DATABASE:AddCargo(CargoName, Cargo) + +
    +
    + +

    Adds a Cargo based on the Cargo Name in the DATABASE.

    + +

    Parameters

    +
      +
    • + +

      #string CargoName : +The name of the airbase

      + +
    • +
    • + +

      Cargo :

    @@ -627,6 +700,20 @@ The following iterator methods are currently available within the DATABASE:

    +
    +
    +
    +
    + + + +DATABASE.CARGOS + +
    +
    + + +
    @@ -689,7 +776,7 @@ The following iterator methods are currently available within the DATABASE:

    -DATABASE:DeleteAirbase(DCSAirbaseName) +DATABASE:DeleteAirbase(AirbaseName)
    @@ -700,7 +787,30 @@ The following iterator methods are currently available within the DATABASE:

    • -

      DCSAirbaseName :

      +

      #string AirbaseName : +The name of the airbase

      + +
    • +
    +
    +
    +
    +
    + + +DATABASE:DeleteCargo(CargoName) + +
    +
    + +

    Deletes a Cargo from the DATABASE based on the Cargo Name.

    + +

    Parameter

    +
      +
    • + +

      #string CargoName : +The name of the airbase

    @@ -778,7 +888,7 @@ The following iterator methods are currently available within the DATABASE:

    -

    Finds a AIRBASE based on the AirbaseName.

    +

    Finds an AIRBASE based on the AirbaseName.

    Parameter

      @@ -798,6 +908,32 @@ The found AIRBASE.

      + +DATABASE:FindCargo(CargoName) + +
      +
      + +

      Finds an CARGO based on the CargoName.

      + +

      Parameter

      +
        +
      • + +

        #string CargoName :

        + +
      • +
      +

      Return value

      + +

      Wrapper.Cargo#CARGO: +The found CARGO.

      + +
      +
      +
      +
      + DATABASE:FindClient(ClientName) @@ -944,6 +1080,38 @@ self

      + +DATABASE:ForEachCargo(IteratorFunction, ...) + +
      +
      + +

      Iterate the DATABASE and call an iterator function for each CARGO, providing the CARGO object to the function and optional parameters.

      + +

      Parameters

      +
        +
      • + +

        #function IteratorFunction : +The function that will be called for each object in the database. The function needs to accept a CLIENT parameter.

        + +
      • +
      • + +

        ... :

        + +
      • +
      +

      Return value

      + +

      #DATABASE: +self

      + +
      +
      +
      +
      + DATABASE:ForEachClient(IteratorFunction, ...) @@ -957,7 +1125,7 @@ self

    • #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.

      +The function that will be called object in the database. The function needs to accept a CLIENT parameter.

    • @@ -989,7 +1157,7 @@ self

    • #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.

      +The function that will be called for each object in the database. The function needs to accept a GROUP parameter.

    • @@ -1021,7 +1189,7 @@ self

    • #function IteratorFunction : -The function that will be called when there is an player in the database. The function needs to accept the player name.

      +The function that will be called for each object in the database. The function needs to accept the player name.

    • @@ -1053,7 +1221,7 @@ self

    • #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.

      +The function that will be called for each object in the database. The function needs to accept a UNIT parameter.

    • @@ -1085,7 +1253,7 @@ self

    • #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.

      +The function that will be called for each object in the database. The function needs to accept a UNIT parameter.

    • @@ -1343,6 +1511,48 @@ self

      -- 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()
      +
    +
    +
    +
    + + +DATABASE:OnEventDeleteCargo(EventData) + +
    +
    + +

    Handles the OnEventDeleteCargo.

    + +

    Parameter

    + +
    +
    +
    +
    + + +DATABASE:OnEventNewCargo(EventData) + +
    +
    + +

    Handles the OnEventNewCargo event.

    + +

    Parameter

    +
    diff --git a/docs/Documentation/Detection.html b/docs/Documentation/Detection.html index 64ac45ee0..c1db43d89 100644 --- a/docs/Documentation/Detection.html +++ b/docs/Documentation/Detection.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • @@ -2238,7 +2239,7 @@ self

    - + #number DETECTION_BASE.DetectionInterval diff --git a/docs/Documentation/DetectionManager.html b/docs/Documentation/DetectionManager.html index e31707159..542edf755 100644 --- a/docs/Documentation/DetectionManager.html +++ b/docs/Documentation/DetectionManager.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Escort.html b/docs/Documentation/Escort.html index d2890d3e0..bc481cf03 100644 --- a/docs/Documentation/Escort.html +++ b/docs/Documentation/Escort.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Event.html b/docs/Documentation/Event.html index da27fb360..6c74344c9 100644 --- a/docs/Documentation/Event.html +++ b/docs/Documentation/Event.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • @@ -309,6 +310,18 @@ YYYY-MM-DD: CLASS:NewFunction( Params ) added

    + + + + + + + + @@ -440,6 +453,18 @@ YYYY-MM-DD: CLASS:NewFunction( Params ) added

    Type EVENTDATA

    AI_CARGO_GROUPED.ClassName - -
    AI_CARGO_GROUPED:New(CargoSet, Type, Name, Weight, ReportRadius, NearRadius)

    AICARGOGROUPED constructor.

    @@ -589,9 +575,9 @@ Use the event functions as described above to Load, UnLoad, Board, UnBoard the A
    AI_CARGO_UNIT.ClassNameAI_CARGO_UNIT:Destroy() - +

    AICARGOUNIT Destructor.

    AI_CARGO_UNIT:onafterBoard(Event, From, To, CargoCarrier)AI_CARGO_UNIT:onafterBoard(Event, From, To, CargoCarrier, ...)

    Board Event.

    AI_CARGO_UNIT:onenterBoarding(Event, From, To, CargoCarrier)AI_CARGO_UNIT:onenterBoarding(Event, From, To, CargoCarrier, ...)

    Enter Boarding State.

    AI_CARGO_UNIT:onleaveBoarding(Event, From, To, CargoCarrier)AI_CARGO_UNIT:onleaveBoarding(Event, From, To, CargoCarrier, ...)

    Leave Boarding State.

    DATABASE:AddAirbase(DCSAirbaseName)DATABASE:AddAirbase(AirbaseName)

    Adds a Airbase based on the Airbase Name in the DATABASE.

    +
    DATABASE:AddCargo(CargoName, Cargo) +

    Adds a Cargo based on the Cargo Name in the DATABASE.

    DATABASE:AddUnit(DCSUnitName)

    Adds a Unit based on the Unit Name in the DATABASE.

    +
    DATABASE.CARGOS +
    DATABASE:DeleteAirbase(DCSAirbaseName)DATABASE:DeleteAirbase(AirbaseName)

    Deletes a Airbase from the DATABASE based on the Airbase Name.

    +
    DATABASE:DeleteCargo(CargoName) +

    Deletes a Cargo from the DATABASE based on the Cargo Name.

    DATABASE:FindAirbase(AirbaseName) -

    Finds a AIRBASE based on the AirbaseName.

    +

    Finds an AIRBASE based on the AirbaseName.

    +
    DATABASE:FindCargo(CargoName) +

    Finds an CARGO based on the CargoName.

    DATABASE:ForEach(IteratorFunction, FinalizeFunction, arg, Set)

    Iterate the DATABASE and call an iterator function for the given set, providing the Object for each element within the set and optional parameters.

    +
    DATABASE:ForEachCargo(IteratorFunction, ...) +

    Iterate the DATABASE and call an iterator function for each CARGO, providing the CARGO object to the function and optional parameters.

    DATABASE:New()

    Creates a new DATABASE object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names.

    +
    DATABASE:OnEventDeleteCargo(EventData) +

    Handles the OnEventDeleteCargo.

    +
    DATABASE:OnEventNewCargo(EventData) +

    Handles the OnEventNewCargo event.

    EVENT.ClassName +
    EVENT:CreateEventDeleteCargo(Cargo) +

    Creation of a Cargo Deletion Event.

    +
    EVENT:CreateEventNewCargo(Cargo) +

    Creation of a New Cargo Event.

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

    + + + + @@ -912,6 +949,50 @@ YYYY-MM-DD: CLASS:NewFunction( Params ) added

    + + +
    +
    + + +EVENT:CreateEventDeleteCargo(Cargo) + +
    +
    + +

    Creation of a Cargo Deletion Event.

    + +

    Parameter

    + +
    +
    +
    +
    + + +EVENT:CreateEventNewCargo(Cargo) + +
    +
    + +

    Creation of a New Cargo Event.

    + +

    Parameter

    +
    @@ -1586,6 +1667,34 @@ Note that at the beginning of each field description, there is an indication whi
    + + +EVENTDATA.Cargo + +
    +
    + + + +
    +
    +
    +
    + + + +EVENTDATA.CargoName + +
    +
    + + + +
    +
    +
    +
    + Dcs.DCSUnit#Unit.Category EVENTDATA.IniCategory @@ -2228,6 +2337,20 @@ Note that at the beginning of each field description, there is an indication whi + +
    +
    +
    + + + +EVENTS.DeleteCargo + +
    +
    + + +
    @@ -2340,6 +2463,20 @@ Note that at the beginning of each field description, there is an indication whi + +
    +
    +
    + + + +EVENTS.NewCargo + +
    +
    + + +
    diff --git a/docs/Documentation/Fsm.html b/docs/Documentation/Fsm.html index 5286a9c1e..371f24214 100644 --- a/docs/Documentation/Fsm.html +++ b/docs/Documentation/Fsm.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Group.html b/docs/Documentation/Group.html index c3933dd8c..2d7620f40 100644 --- a/docs/Documentation/Group.html +++ b/docs/Documentation/Group.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • @@ -1528,7 +1529,7 @@ The zone to test.

    Return value

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

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

    @@ -1555,7 +1556,7 @@ The zone to test.

    Return value

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

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

    diff --git a/docs/Documentation/Identifiable.html b/docs/Documentation/Identifiable.html index 26302176c..79739cbf9 100644 --- a/docs/Documentation/Identifiable.html +++ b/docs/Documentation/Identifiable.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/MOVEMENT.html b/docs/Documentation/MOVEMENT.html index 2ec03bdbd..2c3773f5f 100644 --- a/docs/Documentation/MOVEMENT.html +++ b/docs/Documentation/MOVEMENT.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • @@ -191,6 +192,7 @@ on defined intervals (currently every minute).

    + #number MOVEMENT.AliveUnits @@ -199,6 +201,9 @@ on defined intervals (currently every minute).

    + +

    Contains the counter how many units are currently alive

    +
    diff --git a/docs/Documentation/Menu.html b/docs/Documentation/Menu.html index 898011d4b..c09255510 100644 --- a/docs/Documentation/Menu.html +++ b/docs/Documentation/Menu.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Message.html b/docs/Documentation/Message.html index db044781f..e2a7642c8 100644 --- a/docs/Documentation/Message.html +++ b/docs/Documentation/Message.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/MissileTrainer.html b/docs/Documentation/MissileTrainer.html index 028f28dff..da77a2948 100644 --- a/docs/Documentation/MissileTrainer.html +++ b/docs/Documentation/MissileTrainer.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Mission.html b/docs/Documentation/Mission.html index bb21a844f..dfc09bc46 100644 --- a/docs/Documentation/Mission.html +++ b/docs/Documentation/Mission.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Object.html b/docs/Documentation/Object.html index cb8608218..3f7299c86 100644 --- a/docs/Documentation/Object.html +++ b/docs/Documentation/Object.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Point.html b/docs/Documentation/Point.html index 2f03970b6..add1d6996 100644 --- a/docs/Documentation/Point.html +++ b/docs/Documentation/Point.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Positionable.html b/docs/Documentation/Positionable.html index 94f700b92..178f980de 100644 --- a/docs/Documentation/Positionable.html +++ b/docs/Documentation/Positionable.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Process_JTAC.html b/docs/Documentation/Process_JTAC.html index 92c909e8f..11d6329e6 100644 --- a/docs/Documentation/Process_JTAC.html +++ b/docs/Documentation/Process_JTAC.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Process_Pickup.html b/docs/Documentation/Process_Pickup.html index a031bed16..36f9db817 100644 --- a/docs/Documentation/Process_Pickup.html +++ b/docs/Documentation/Process_Pickup.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Radio.html b/docs/Documentation/Radio.html index 3b47dc1f1..0b4642d0f 100644 --- a/docs/Documentation/Radio.html +++ b/docs/Documentation/Radio.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Route.html b/docs/Documentation/Route.html index 8d2ec52ea..1ef383573 100644 --- a/docs/Documentation/Route.html +++ b/docs/Documentation/Route.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Scenery.html b/docs/Documentation/Scenery.html index 84704e530..40fabb2c9 100644 --- a/docs/Documentation/Scenery.html +++ b/docs/Documentation/Scenery.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/ScheduleDispatcher.html b/docs/Documentation/ScheduleDispatcher.html index 887ea6292..b6b78ec9d 100644 --- a/docs/Documentation/ScheduleDispatcher.html +++ b/docs/Documentation/ScheduleDispatcher.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Scheduler.html b/docs/Documentation/Scheduler.html index b8e42d2bb..c1908a711 100644 --- a/docs/Documentation/Scheduler.html +++ b/docs/Documentation/Scheduler.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Scoring.html b/docs/Documentation/Scoring.html index 0d635d4ef..14fa802da 100644 --- a/docs/Documentation/Scoring.html +++ b/docs/Documentation/Scoring.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Sead.html b/docs/Documentation/Sead.html index 12a877b80..08f2cf461 100644 --- a/docs/Documentation/Sead.html +++ b/docs/Documentation/Sead.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Set.html b/docs/Documentation/Set.html index 4c542f663..7a31dc2e8 100644 --- a/docs/Documentation/Set.html +++ b/docs/Documentation/Set.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • @@ -143,6 +144,28 @@
    + + + + @@ -455,6 +478,106 @@ + +
    EVENTDATA.Cargo + +
    EVENTDATA.CargoName + +
    EVENTDATA.IniCategory

    (UNIT) The category of the initiator.

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

    EVENTS.Dead +
    EVENTS.DeleteCargo +
    EVENTS.MissionStart +
    EVENTS.NewCargo +

    1) SET_BASE class, extends Base#BASE

    The Set#SET_BASE class defines the core functions that define a collection of objects.

    +
    SET_CARGO +

    SET_CARGO class, extends Set#SET_BASE

    + +

    Mission designers can use the Set#SET_CARGO class to build sets of cargos optionally belonging to certain:

    + +
      +
    • Coalitions
    • +
    • Types
    • +
    • Name or Prefix
    • +
    + +

    SET_CARGO constructor

    + +

    Create a new SET_CARGO object with the SET_CARGO.New method:

    + +
    SET_BASE:_Find(ObjectName)

    Finds an Base#BASE object based on the object Name.

    +
    + +

    Type SET_CARGO

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SET_CARGO:AddCargosByName(AddCargoNames) +

    Add CARGOs to SET_CARGO.

    +
    SET_CARGO:AddInDatabase(Event) +

    Handles the Database to check on an event (birth) that the Object was added in the Database.

    +
    SET_CARGO:FilterCoalitions(Coalitions) +

    Builds a set of cargos of coalitions.

    +
    SET_CARGO:FilterCountries(Countries) +

    Builds a set of cargos of defined countries.

    +
    SET_CARGO:FilterPrefixes(Prefixes) +

    Builds a set of cargos of defined cargo prefixes.

    +
    SET_CARGO:FilterStart() +

    Starts the filtering.

    +
    SET_CARGO:FilterTypes(Types) +

    Builds a set of cargos of defined cargo types.

    +
    SET_CARGO:FindCargo(CargoName) +

    Finds a Cargo based on the Cargo Name.

    +
    SET_CARGO:FindInDatabase(Event) +

    Handles the Database to check on any event that Object exists in the Database.

    +
    SET_CARGO:FindNearestCargoFromPointVec2(PointVec2) +

    Iterate the SET_CARGO while identifying the nearest Cargo#CARGO from a Point#POINT_VEC2.

    +
    SET_CARGO:ForEachCargo(IteratorFunction, ...) +

    Iterate the SET_CARGO and call an interator function for each CARGO, providing the CARGO and optional parameters.

    +
    SET_CARGO:IsIncludeObject(MCargo) + +
    SET_CARGO:New() +

    Creates a new SET_CARGO object, building a set of cargos belonging to a coalitions and categories.

    +
    SET_CARGO:OnEventDeleteCargo(EventData) +

    Handles the OnDead or OnCrash event for alive units set.

    +
    SET_CARGO:OnEventNewCargo(EventData) +

    Handles the OnEventNewCargo event for the Set.

    +
    SET_CARGO:RemoveCargosByName(RemoveCargoNames) +

    Remove CARGOs from SET_CARGO.

    @@ -923,6 +1046,72 @@ The default "time interval" is after 0.001 seconds.

    You can set the "yield interval", and the "time interval". (See above).

    + + +
    +
    + + #SET_CARGO + +SET_CARGO + +
    +
    + +

    SET_CARGO class, extends Set#SET_BASE

    + +

    Mission designers can use the Set#SET_CARGO class to build sets of cargos optionally belonging to certain:

    + +
      +
    • Coalitions
    • +
    • Types
    • +
    • Name or Prefix
    • +
    + +

    SET_CARGO constructor

    + +

    Create a new SET_CARGO object with the SET_CARGO.New method:

    + + + + +

    +

    Add or Remove CARGOs from SET_CARGO

    + +

    CARGOs can be added and removed using the Set#SET_CARGO.AddCargosByName and Set#SET_CARGO.RemoveCargosByName respectively. +These methods take a single CARGO name or an array of CARGO names to be added or removed from SET_CARGO.

    + +

    SET_CARGO filter criteria

    + +

    You can set filter criteria to automatically maintain the SET_CARGO contents. +Filter criteria are defined by:

    + + + +

    Once the filter criteria have been set for the SET_CARGO, you can start filtering using:

    + + + +

    SET_CARGO iterators

    + +

    Once the filters have been defined and the SETCARGO has been built, you can iterate the SETCARGO with the available iterator methods. +The iterator methods will walk the SETCARGO set, and call for each cargo within the set a function that you provide. +The following iterator methods are currently available within the SETCARGO:

    + + + +
    @@ -2189,6 +2378,454 @@ self

    Core.Base#BASE: The Object found.

    + +
    + +

    Type SET_CARGO

    +

    Field(s)

    +
    +
    + + +SET_CARGO:AddCargosByName(AddCargoNames) + +
    +
    + +

    Add CARGOs to SET_CARGO.

    + +

    Parameter

    +
      +
    • + +

      #string AddCargoNames : +A single name or an array of CARGO names.

      + +
    • +
    +

    Return value

    + + +

    self

    + +
    +
    +
    +
    + + +SET_CARGO:AddInDatabase(Event) + +
    +
    + +

    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 SETBASE birth event!

    + +

    Parameter

    + +

    Return values

    +
      +
    1. + +

      #string: +The name of the CARGO

      + +
    2. +
    3. + +

      #table: +The CARGO

      + +
    4. +
    +
    +
    +
    +
    + + +SET_CARGO:FilterCoalitions(Coalitions) + +
    +
    + +

    Builds a set of cargos of coalitions.

    + + +

    Possible current coalitions are red, blue and neutral.

    + +

    Parameter

    +
      +
    • + +

      #string Coalitions : +Can take the following values: "red", "blue", "neutral".

      + +
    • +
    +

    Return value

    + +

    #SET_CARGO: +self

    + +
    +
    +
    +
    + + +SET_CARGO:FilterCountries(Countries) + +
    +
    + +

    Builds a set of cargos of defined countries.

    + + +

    Possible current countries are those known within DCS world.

    + +

    Parameter

    +
      +
    • + +

      #string Countries : +Can take those country strings known within DCS world.

      + +
    • +
    +

    Return value

    + +

    #SET_CARGO: +self

    + +
    +
    +
    +
    + + +SET_CARGO:FilterPrefixes(Prefixes) + +
    +
    + +

    Builds a set of cargos of defined cargo prefixes.

    + + +

    All the cargos starting with the given prefixes will be included within the set.

    + +

    Parameter

    +
      +
    • + +

      #string Prefixes : +The prefix of which the cargo name starts with.

      + +
    • +
    +

    Return value

    + +

    #SET_CARGO: +self

    + +
    +
    +
    +
    + + +SET_CARGO:FilterStart() + +
    +
    + +

    Starts the filtering.

    + +

    Return value

    + +

    #SET_CARGO: +self

    + +
    +
    +
    +
    + + +SET_CARGO:FilterTypes(Types) + +
    +
    + +

    Builds a set of cargos of defined cargo types.

    + + +

    Possible current types are those types known within DCS world.

    + +

    Parameter

    +
      +
    • + +

      #string Types : +Can take those type strings known within DCS world.

      + +
    • +
    +

    Return value

    + +

    #SET_CARGO: +self

    + +
    +
    +
    +
    + + +SET_CARGO:FindCargo(CargoName) + +
    +
    + +

    Finds a Cargo based on the Cargo Name.

    + +

    Parameter

    +
      +
    • + +

      #string CargoName :

      + +
    • +
    +

    Return value

    + +

    Wrapper.Cargo#CARGO: +The found Cargo.

    + +
    +
    +
    +
    + + +SET_CARGO:FindInDatabase(Event) + +
    +
    + +

    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 SETBASE event or vise versa!

    + +

    Parameter

    + +

    Return values

    +
      +
    1. + +

      #string: +The name of the CARGO

      + +
    2. +
    3. + +

      #table: +The CARGO

      + +
    4. +
    +
    +
    +
    +
    + + +SET_CARGO:FindNearestCargoFromPointVec2(PointVec2) + +
    +
    + +

    Iterate the SET_CARGO while identifying the nearest Cargo#CARGO from a Point#POINT_VEC2.

    + +

    Parameter

    + +

    Return value

    + +

    Wrapper.Cargo#CARGO: +The closest Cargo#CARGO.

    + +
    +
    +
    +
    + + +SET_CARGO:ForEachCargo(IteratorFunction, ...) + +
    +
    + +

    Iterate the SET_CARGO and call an interator function for each CARGO, providing the CARGO and optional parameters.

    + +

    Parameters

    +
      +
    • + +

      #function IteratorFunction : +The function that will be called when there is an alive CARGO in the SET_CARGO. The function needs to accept a CARGO parameter.

      + +
    • +
    • + +

      ... :

      + +
    • +
    +

    Return value

    + +

    #SET_CARGO: +self

    + +
    +
    +
    +
    + + +SET_CARGO:IsIncludeObject(MCargo) + +
    +
    + + + +

    Parameter

    + +

    Return value

    + +

    #SET_CARGO: +self

    + +
    +
    +
    +
    + + +SET_CARGO:New() + +
    +
    + +

    Creates a new SET_CARGO object, building a set of cargos belonging to a coalitions and categories.

    + +

    Return value

    + +

    #SET_CARGO: +self

    + +

    Usage:

    +
    -- Define a new SET_CARGO Object. The DatabaseSet will contain a reference to all Cargos.
    +DatabaseSet = SET_CARGO:New()
    + +
    +
    +
    +
    + + +SET_CARGO:OnEventDeleteCargo(EventData) + +
    +
    + +

    Handles the OnDead or OnCrash event for alive units set.

    + +

    Parameter

    + +
    +
    +
    +
    + + +SET_CARGO:OnEventNewCargo(EventData) + +
    +
    + +

    Handles the OnEventNewCargo event for the Set.

    + +

    Parameter

    + +
    +
    +
    +
    + + +SET_CARGO:RemoveCargosByName(RemoveCargoNames) + +
    +
    + +

    Remove CARGOs from SET_CARGO.

    + +

    Parameter

    + +

    Return value

    + + +

    self

    +
    diff --git a/docs/Documentation/Smoke.html b/docs/Documentation/Smoke.html index f91a5703f..db3f97156 100644 --- a/docs/Documentation/Smoke.html +++ b/docs/Documentation/Smoke.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Spawn.html b/docs/Documentation/Spawn.html index 4a25605f0..d0b095a1d 100644 --- a/docs/Documentation/Spawn.html +++ b/docs/Documentation/Spawn.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • @@ -872,6 +873,12 @@ and any spaces before and after the resulting name are removed.

    SPAWN:_TranslateRotate(SpawnIndex, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle) + + + + SPAWN.uncontrolled + + @@ -1859,9 +1866,6 @@ The group that was spawned. You can use this group for further actions.

    - -

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

    -
    @@ -2332,9 +2336,6 @@ when nothing was spawned.

    - -

    By default, no InitLimit

    -
    @@ -2370,7 +2371,7 @@ when nothing was spawned.

    - #number + SPAWN.SpawnMaxGroups @@ -2387,7 +2388,7 @@ when nothing was spawned.

    - #number + SPAWN.SpawnMaxUnitsAlive @@ -2705,7 +2706,7 @@ Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):Schedule( 600, 0.5 )
    - #boolean + SPAWN.SpawnUnControlled @@ -2729,7 +2730,7 @@ Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):Schedule( 600, 0.5 ) -

    Flag that indicates if all the Groups of the SpawnGroup need to be visible when Spawned.

    +

    When the first Spawn executes, all the Groups need to be made visible before start.

    @@ -3295,6 +3296,20 @@ True = Continue Scheduler

    + +
    +
    +
    + + + +SPAWN.uncontrolled + +
    +
    + + +
    diff --git a/docs/Documentation/Static.html b/docs/Documentation/Static.html index ff176e054..2aeeae3c8 100644 --- a/docs/Documentation/Static.html +++ b/docs/Documentation/Static.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Task.html b/docs/Documentation/Task.html index f2a503a07..0cc3c69d0 100644 --- a/docs/Documentation/Task.html +++ b/docs/Documentation/Task.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • @@ -73,54 +74,12 @@

    Module Task

    -

    This module contains the TASK class.

    +

    Tasking -- 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:

    +
    - - -

    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.


    @@ -132,7 +91,9 @@ Use the method TASK.AddScore() to add scores whe TASK +

    TASK class, extends Base#BASE

    +

    The TASK class implements the methods for task orchestration within MOOSE.

    @@ -166,12 +127,6 @@ Use the method TASK.AddScore() to add scores whe TASK:Cancel()

    FSM Cancel synchronous event function for TASK.

    - - - - TASK.ClassName - - @@ -358,12 +313,6 @@ Use the method TASK.AddScore() to add scores whe TASK:JoinUnit(PlayerUnit, PlayerGroup)

    Add a PlayerUnit to join the Task.

    - - - - TASK.Menu - - @@ -418,24 +367,6 @@ Use the method TASK.AddScore() to add scores whe TASK:OnAfterPlayerDead(PlayerUnit, PlayerName)

    FSM PlayerDead event handler prototype for TASK.

    - - - - TASK.Players - - - - - - TASK.ProcessClasses - - - - - - TASK.Processes - - @@ -478,12 +409,6 @@ Use the method TASK.AddScore() to add scores whe TASK:ReportSummary()

    Create a summary report of the Task.

    - - - - TASK.Scores - - @@ -769,6 +694,57 @@ Use the method TASK.AddScore() to add scores whe
    +

    TASK class, extends Base#BASE

    + +

    The TASK class implements the methods for task orchestration within MOOSE.

    + + + +

    The class provides a couple of methods to:

    + + + +

    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.

    @@ -776,10 +752,7 @@ Use the method TASK.AddScore() to add scores whe

    Type Task

    Type TASK

    - -

    The TASK class

    - -

    Field(s)

    +

    Field(s)

    @@ -893,20 +866,6 @@ self

    Use this event to Cancel the Task.

    - -
    -
    -
    - - #string - -TASK.ClassName - -
    -
    - - -
    @@ -1498,20 +1457,6 @@ The GROUP of the player joining the Mission.

    #boolean: true if Unit is part of the Task.

    - -
    -
    -
    - - - -TASK.Menu - -
    -
    - - -
    @@ -1742,47 +1687,6 @@ The name of the Player.

    - -
    -
    -
    - - -TASK.Players - -
    -
    - - - -
    -
    -
    -
    - - - -TASK.ProcessClasses - -
    -
    - - - -
    -
    -
    -
    - - - -TASK.Processes - -
    -
    - - -
    @@ -1955,20 +1859,6 @@ self

    #string:

    - -
    -
    -
    - - - -TASK.Scores - -
    -
    - - -
    diff --git a/docs/Documentation/Task_A2G.html b/docs/Documentation/Task_A2G.html index 46c91e395..c4f9f3077 100644 --- a/docs/Documentation/Task_A2G.html +++ b/docs/Documentation/Task_A2G.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • @@ -326,7 +327,7 @@ The TASK_A2G is implemented using a Stat - TASK_SEAD:New(Mission, SetGroup, TaskName, UnitSetTargets, TargetDistance, TargetZone, TargetSetUnit) + TASK_SEAD:New(Mission, SetGroup, TaskName, TargetSetUnit)

    Instantiates a new TASK_SEAD.

    @@ -1068,7 +1069,7 @@ self

    -TASK_SEAD:New(Mission, SetGroup, TaskName, UnitSetTargets, TargetDistance, TargetZone, TargetSetUnit) +TASK_SEAD:New(Mission, SetGroup, TaskName, TargetSetUnit)
    @@ -1096,25 +1097,7 @@ The name of the Task.

  • -

    Set#SET_UNIT UnitSetTargets :

    - -
  • -
  • - -

    #number TargetDistance : -The distance to Target when the Player is considered to have "arrived" at the engagement range.

    - -
  • -
  • - -

    Core.Zone#ZONE_BASE TargetZone : -The target zone, if known. -If the TargetZone parameter is specified, the player will be routed to the center of the zone where all the targets are assumed to be.

    - -
  • -
  • - -

    TargetSetUnit :

    +

    Set#SET_UNIT TargetSetUnit :

  • diff --git a/docs/Documentation/Task_A2G_Dispatcher.html b/docs/Documentation/Task_A2G_Dispatcher.html index d3f800748..92d0cee18 100644 --- a/docs/Documentation/Task_A2G_Dispatcher.html +++ b/docs/Documentation/Task_A2G_Dispatcher.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Task_CARGO.html b/docs/Documentation/Task_CARGO.html new file mode 100644 index 000000000..621b65667 --- /dev/null +++ b/docs/Documentation/Task_CARGO.html @@ -0,0 +1,826 @@ + + + + + + +
    +
    + +
    +
    +
    +
    + +
    +

    Module Task_CARGO

    + +

    Tasking (Release 2.1) -- The TASK_CARGO models tasks for players to transport Cargo.

    + + + +

    Banner Image

    + +
    + +

    Cargo are units or cargo objects within DCS world that allow to be transported or sling loaded by other units. +The CARGO class, as part of the moose core, is able to Board, Load, UnBoard and UnLoad from Carrier units. +This collection of classes in this module define tasks for human players to handle cargo objects. +Cargo can be transported, picked-up, deployed and sling-loaded from and to other places.

    + +

    The following classes are important to consider:

    + +
      +
    • #TASKCARGOTRANSPORT: Defines a task for a human player to transport a set of cargo between various zones.
    • +
    + +

    ==

    + +

    API CHANGE HISTORY

    + +

    The underlying change log documents the API changes. Please read this carefully. The following notation is used:

    + +
      +
    • Added parts are expressed in bold type face.
    • +
    • Removed parts are expressed in italic type face.
    • +
    + +

    Hereby the change log:

    + +

    2017-03-09: Revised version.

    + +
    + +

    AUTHORS and CONTRIBUTIONS

    + +

    Contributions:

    + +

    Authors:

    + +
      +
    • FlightControl: Concept, Design & Programming. +
    • +
    + +

    Global(s)

    + + + + + + + + + +
    TASK_CARGO +

    TASK_CARGO class, extends Task#TASK

    + +

    The TASK_CARGO class defines Cargo transport tasks, +based on the tasking capabilities defined in Task#TASK.

    +
    TASK_CARGO_TRANSPORT + +
    +

    Type FSM_PROCESS

    + + + + + + + + + +
    FSM_PROCESS.Cargo + +
    FSM_PROCESS.DeployZone + +
    + +

    Type TASK_CARGO

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    TASK_CARGO:AddDeployZone(DeployZone, TaskUnit) + +
    TASK_CARGO.DeployZones + +
    TASK_CARGO:GetPlannedMenuText() + +
    TASK_CARGO:GetTargetZone(TaskUnit) + +
    TASK_CARGO:New(Mission, SetGroup, TaskName, SetCargo, TaskType) +

    Instantiates a new TASK_CARGO.

    +
    TASK_CARGO:RemoveDeployZone(DeployZone, TaskUnit) + +
    TASK_CARGO.SetCargo + +
    TASK_CARGO:SetCargoPickup(Cargo, TaskUnit) + +
    TASK_CARGO:SetDeployZone(DeployZone, TaskUnit) + +
    TASK_CARGO:SetDeployZones(@, TaskUnit, DeployZones) + +
    TASK_CARGO:SetPenaltyOnFailed(Text, Penalty, TaskUnit) +

    Set a penalty when the A2G attack has failed.

    +
    TASK_CARGO:SetScoreOnDestroy(Text, Score, TaskUnit) +

    Set a score when a target in scope of the A2G attack, has been destroyed .

    +
    TASK_CARGO:SetScoreOnSuccess(Text, Score, TaskUnit) +

    Set a score when all the targets in scope of the A2G attack, have been destroyed.

    +
    TASK_CARGO.TaskType + +
    + +

    Type TASK_CARGO_TRANSPORT

    + + + + + + + + + +
    TASK_CARGO_TRANSPORT.ClassName + +
    TASK_CARGO_TRANSPORT:New(Mission, SetGroup, TaskName, SetCargo) +

    Instantiates a new TASKCARGOTRANSPORT.

    +
    + +

    Global(s)

    +
    +
    + + #TASK_CARGO + +TASK_CARGO + +
    +
    + +

    TASK_CARGO class, extends Task#TASK

    + +

    The TASK_CARGO class defines Cargo transport tasks, +based on the tasking capabilities defined in Task#TASK.

    + + +

    The TASK_CARGO is implemented using a Statemachine#FSM_TASK, and has the following statuses:

    + +
      +
    • None: Start of the process.
    • +
    • Planned: The cargo task is planned.
    • +
    • Assigned: The cargo task is assigned to a Group#GROUP.
    • +
    • Success: The cargo task is successfully completed.
    • +
    • Failed: The cargo task has failed. This will happen if the player exists the task early, without communicating a possible cancellation to HQ.
    • +
    + +

    1.1) Set the scoring of achievements in a cargo task.

    + +

    Scoring or penalties can be given in the following circumstances:

    + +
    + + +
    +
    +
    +
    + + #TASK_CARGO_TRANSPORT + +TASK_CARGO_TRANSPORT + +
    +
    + + + +
    +
    +

    Type Task_CARGO

    + +

    Type FSM_PROCESS

    +

    Field(s)

    +
    +
    + + + +FSM_PROCESS.Cargo + +
    +
    + + + +
    +
    +
    +
    + + + +FSM_PROCESS.DeployZone + +
    +
    + + + +
    +
    + +

    Type TASK_CARGO

    +

    Field(s)

    +
    +
    + + +TASK_CARGO:AddDeployZone(DeployZone, TaskUnit) + +
    +
    + + + +

    Parameters

    + +

    Return value

    + +

    #TASK_CARGO:

    + + +
    +
    +
    +
    + + + +TASK_CARGO.DeployZones + +
    +
    + + + + +

    setmetatable( {}, { __mode = "v" } ) -- weak table on value

    + +
    +
    +
    +
    + + +TASK_CARGO:GetPlannedMenuText() + +
    +
    + + + +
    +
    +
    +
    + + +TASK_CARGO:GetTargetZone(TaskUnit) + +
    +
    + + + +

    Parameter

    + +

    Return value

    + +

    Core.Zone#ZONE_BASE: +The Zone object where the Target is located on the map.

    + +
    +
    +
    +
    + + +TASK_CARGO:New(Mission, SetGroup, TaskName, SetCargo, TaskType) + +
    +
    + +

    Instantiates a new TASK_CARGO.

    + +

    Parameters

    +
      +
    • + +

      Tasking.Mission#MISSION Mission :

      + +
    • +
    • + +

      Set#SET_GROUP SetGroup : +The set of groups for which the Task can be assigned.

      + +
    • +
    • + +

      #string TaskName : +The name of the Task.

      + +
    • +
    • + +

      Core.Set#SET_CARGO SetCargo : +The scope of the cargo to be transported.

      + +
    • +
    • + +

      #string TaskType : +The type of Cargo task.

      + +
    • +
    +

    Return value

    + +

    #TASK_CARGO: +self

    + +
    +
    +
    +
    + + +TASK_CARGO:RemoveDeployZone(DeployZone, TaskUnit) + +
    +
    + + + +

    Parameters

    + +

    Return value

    + +

    #TASK_CARGO:

    + + +
    +
    +
    +
    + + + +TASK_CARGO.SetCargo + +
    +
    + + + +
    +
    +
    +
    + + +TASK_CARGO:SetCargoPickup(Cargo, TaskUnit) + +
    +
    + + + +

    Parameters

    + +

    Return value

    + +

    #TASK_CARGO:

    + + +
    +
    +
    +
    + + +TASK_CARGO:SetDeployZone(DeployZone, TaskUnit) + +
    +
    + + + +

    Parameters

    + +

    Return value

    + +

    #TASK_CARGO:

    + + +
    +
    +
    +
    + + +TASK_CARGO:SetDeployZones(@, TaskUnit, DeployZones) + +
    +
    + + + +

    Parameters

    +
      +
    • + +

      @ : +ist DeployZones

      + +
    • +
    • + +

      Wrapper.Unit#UNIT TaskUnit :

      + +
    • +
    • + +

      DeployZones :

      + +
    • +
    +

    Return value

    + +

    #TASK_CARGO:

    + + +
    +
    +
    +
    + + +TASK_CARGO:SetPenaltyOnFailed(Text, Penalty, TaskUnit) + +
    +
    + +

    Set a penalty when the A2G attack has failed.

    + +

    Parameters

    +
      +
    • + +

      #string Text : +The text to display to the player, when the A2G attack has failed.

      + +
    • +
    • + +

      #number Penalty : +The penalty in points.

      + +
    • +
    • + +

      Wrapper.Unit#UNIT TaskUnit :

      + +
    • +
    +

    Return value

    + +

    #TASK_CARGO:

    + + +
    +
    +
    +
    + + +TASK_CARGO:SetScoreOnDestroy(Text, Score, TaskUnit) + +
    +
    + +

    Set a score when a target in scope of the A2G attack, has been destroyed .

    + +

    Parameters

    +
      +
    • + +

      #string Text : +The text to display to the player, when the target has been destroyed.

      + +
    • +
    • + +

      #number Score : +The score in points.

      + +
    • +
    • + +

      Wrapper.Unit#UNIT TaskUnit :

      + +
    • +
    +

    Return value

    + +

    #TASK_CARGO:

    + + +
    +
    +
    +
    + + +TASK_CARGO:SetScoreOnSuccess(Text, Score, TaskUnit) + +
    +
    + +

    Set a score when all the targets in scope of the A2G attack, have been destroyed.

    + +

    Parameters

    +
      +
    • + +

      #string Text : +The text to display to the player, when all targets hav been destroyed.

      + +
    • +
    • + +

      #number Score : +The score in points.

      + +
    • +
    • + +

      Wrapper.Unit#UNIT TaskUnit :

      + +
    • +
    +

    Return value

    + +

    #TASK_CARGO:

    + + +
    +
    +
    +
    + + + +TASK_CARGO.TaskType + +
    +
    + + + +
    +
    + +

    Type TASK_CARGO_TRANSPORT

    + +

    The TASKCARGOTRANSPORT class

    + +

    Field(s)

    +
    +
    + + #string + +TASK_CARGO_TRANSPORT.ClassName + +
    +
    + + + +
    +
    +
    +
    + + +TASK_CARGO_TRANSPORT:New(Mission, SetGroup, TaskName, SetCargo) + +
    +
    + +

    Instantiates a new TASKCARGOTRANSPORT.

    + +

    Parameters

    + +

    Return value

    + +

    #TASKCARGOTRANSPORT: +self

    + +
    +
    + +
    + +
    + + diff --git a/docs/Documentation/Task_PICKUP.html b/docs/Documentation/Task_PICKUP.html index fe83f60db..08b188774 100644 --- a/docs/Documentation/Task_PICKUP.html +++ b/docs/Documentation/Task_PICKUP.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Unit.html b/docs/Documentation/Unit.html index 097aa6cf3..b3d712e3b 100644 --- a/docs/Documentation/Unit.html +++ b/docs/Documentation/Unit.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Utils.html b/docs/Documentation/Utils.html index 59585ad25..b9a22a874 100644 --- a/docs/Documentation/Utils.html +++ b/docs/Documentation/Utils.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Zone.html b/docs/Documentation/Zone.html index 86c4fee62..9fbe8dd88 100644 --- a/docs/Documentation/Zone.html +++ b/docs/Documentation/Zone.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • @@ -154,7 +155,7 @@ ZONE -

    3) ZONE class, extends Zone#ZONE_RADIUS

    +

    ZONE class, extends Zone#ZONE_RADIUS

    The ZONE class, defined by the zone name as defined within the Mission Editor.

    @@ -162,7 +163,7 @@ ZONE_BASE -

    1) ZONE_BASE class, extends Base#BASE

    +

    ZONE_BASE class, extends Base#BASE

    This class is an abstract BASE class for derived classes, and is not meant to be instantiated.

    @@ -170,7 +171,7 @@ ZONE_GROUP -

    5) #ZONE_GROUP class, extends Zone#ZONE_RADIUS

    +

    ZONE_GROUP class, extends Zone#ZONE_RADIUS

    The ZONE_GROUP class defines by a zone around a Group#GROUP with a radius.

    @@ -178,7 +179,7 @@ ZONE_POLYGON -

    7) ZONE_POLYGON class, extends Zone#ZONEPOLYGONBASE

    +

    ZONE_POLYGON class, extends Zone#ZONEPOLYGONBASE

    The ZONE_POLYGON class defined by a sequence of Group#GROUP waypoints within the Mission Editor, forming a polygon.

    @@ -186,7 +187,7 @@ ZONE_POLYGON_BASE -

    6) ZONEPOLYGONBASE class, extends Zone#ZONE_BASE

    +

    ZONEPOLYGONBASE class, extends Zone#ZONE_BASE

    The ZONEPOLYGONBASE class defined by a sequence of Group#GROUP waypoints within the Mission Editor, forming a polygon.

    @@ -194,7 +195,7 @@ ZONE_RADIUS -

    2) Zone#ZONE_RADIUS class, extends Zone#ZONE_BASE

    +

    ZONE_RADIUS class, extends Zone#ZONE_BASE

    The ZONE_RADIUS class defined by a zone name, a location and a radius.

    @@ -202,7 +203,7 @@ ZONE_UNIT -

    4) #ZONE_UNIT class, extends Zone#ZONE_RADIUS

    +

    ZONE_UNIT class, extends Zone#ZONE_RADIUS

    The ZONE_UNIT class defined by a zone around a Unit#UNIT with a radius.

    @@ -290,18 +291,30 @@ ZONE_BASE:GetZoneProbability()

    Get the randomization probability of a zone to be selected.

    + + + + ZONE_BASE:IsPointVec2InZone(PointVec2) + +

    Returns if a PointVec2 is within the zone.

    + + + + ZONE_BASE:IsPointVec3InZone(PointVec3) + +

    Returns if a PointVec3 is within the zone.

    ZONE_BASE:IsVec2InZone(Vec2) -

    Returns if a location is within the zone.

    +

    Returns if a Vec2 is within the zone.

    ZONE_BASE:IsVec3InZone(Vec3) -

    Returns if a point is within the zone.

    +

    Returns if a Vec3 is within the zone.

    @@ -438,6 +451,12 @@ ZONE_POLYGON_BASE:GetRandomVec2()

    Define a random DCSTypes#Vec2 within the zone.

    + + + + ZONE_POLYGON_BASE:GetVec2() + +

    Returns the center location of the polygon.

    @@ -617,15 +636,13 @@
    -

    3) ZONE class, extends Zone#ZONE_RADIUS

    +

    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 #ZONE_RADIUS taking into account the own zone format and properties.

    -
    -
    @@ -639,26 +656,26 @@
    -

    1) ZONE_BASE class, extends Base#BASE

    +

    ZONE_BASE class, extends Base#BASE

    This class is an abstract BASE class for derived classes, and is not meant to be instantiated.

    -

    1.1) Each zone has a name:

    +

    Each zone has a name:

    -

    1.2) Each zone implements two polymorphic functions defined in Zone#ZONE_BASE:

    +

    Each zone implements two polymorphic functions defined in Zone#ZONE_BASE:

    -

    1.3) A zone has a probability factor that can be set to randomize a selection between zones:

    +

    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% )
    • @@ -666,27 +683,26 @@
    • 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:

    +

    A zone manages Vectors:

    -

    1.5) A zone has a bounding square:

    +

    A zone has a bounding square:

    -

    1.6) A zone can be marked:

    +

    A zone can be marked:

    -
    @@ -700,7 +716,7 @@
    -

    5) #ZONE_GROUP class, extends Zone#ZONE_RADIUS

    +

    ZONE_GROUP class, extends Zone#ZONE_RADIUS

    The ZONE_GROUP class defines by a zone around a Group#GROUP with a radius.

    @@ -708,8 +724,6 @@

    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.

    -
    -
    @@ -723,15 +737,13 @@ This class implements the inherited functions from Zone#ZONEPOLYGONBASE +

    ZONE_POLYGON class, extends Zone#ZONEPOLYGONBASE

    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.

    -
    - @@ -745,7 +757,7 @@ This class implements the inherited functions from Zone#ZONE_BASE +

    ZONEPOLYGONBASE class, extends Zone#ZONE_BASE

    The ZONEPOLYGONBASE class defined by a sequence of Group#GROUP waypoints within the Mission Editor, forming a polygon.

    @@ -753,7 +765,7 @@ This class implements the inherited functions from Zone#ZONE_RADIUS taking into account the own zone format and properties. This class is an abstract BASE class for derived classes, and is not meant to be instantiated.

    -

    6.1) Zone point randomization

    +

    Zone point randomization

    Various functions exist to find random points within the zone.

    @@ -763,8 +775,6 @@ This class is an abstract BASE class for derived classes, and is not meant to be
  • ZONEPOLYGONBASE.GetRandomPointVec3(): Return a Point#POINT_VEC3 object representing a random 3D point at landheight within the zone.
  • -
    - @@ -778,27 +788,27 @@ This class is an abstract BASE class for derived classes, and is not meant to be
    -

    2) Zone#ZONE_RADIUS class, extends Zone#ZONE_BASE

    +

    ZONE_RADIUS class, extends Zone#ZONE_BASE

    The ZONE_RADIUS class defined by a zone name, a location and a radius.

    This class implements the inherited functions from Core.Zone#ZONE_BASE taking into account the own zone format and properties.

    -

    2.1) Zone#ZONE_RADIUS constructor

    +

    ZONE_RADIUS constructor

    -

    2.2) Manage the radius of the zone

    +

    Manage the radius of the zone

    -

    2.3) Manage the location of the zone

    +

    Manage the location of the zone

    -

    2.4) Zone point randomization

    +

    Zone point randomization

    Various functions exist to find random points within the zone.

    @@ -816,8 +826,6 @@ This class is an abstract BASE class for derived classes, and is not meant to be
  • ZONE_RADIUS.GetRandomPointVec3(): Gets a Point#POINT_VEC3 object representing a random 3D point in the zone. Note that the height of the point is at landheight.
  • -
    -
    @@ -831,15 +839,13 @@ This class is an abstract BASE class for derived classes, and is not meant to be
    -

    4) #ZONE_UNIT class, extends Zone#ZONE_RADIUS

    +

    ZONE_UNIT class, extends Zone#ZONE_RADIUS

    The ZONE_UNIT class defined by a zone around a Unit#UNIT with a radius.

    This class implements the inherited functions from #ZONE_RADIUS taking into account the own zone format and properties.

    -
    -
    @@ -1131,27 +1137,81 @@ A value between 0 and 1. 0 = 0% and 1 = 100% probability.

    - -ZONE_BASE:IsVec2InZone(Vec2) + +ZONE_BASE:IsPointVec2InZone(PointVec2)
    -

    Returns if a location is within the zone.

    +

    Returns if a PointVec2 is within the zone.

    Parameter

    Return value

    #boolean: -true if the location is within the zone.

    +true if the PointVec2 is within the zone.

    + +
    +
    +
    +
    + + +ZONE_BASE:IsPointVec3InZone(PointVec3) + +
    +
    + +

    Returns if a PointVec3 is within the zone.

    + +

    Parameter

    + +

    Return value

    + +

    #boolean: +true if the PointVec3 is within the zone.

    + +
    +
    +
    +
    + + +ZONE_BASE:IsVec2InZone(Vec2) + +
    +
    + +

    Returns if a Vec2 is within the zone.

    + +

    Parameter

    + +

    Return value

    + +

    #boolean: +true if the Vec2 is within the zone.

    @@ -1164,7 +1224,7 @@ true if the location is within the zone.

    -

    Returns if a point is within the zone.

    +

    Returns if a Vec3 is within the zone.

    Parameter

      @@ -1178,7 +1238,7 @@ The point to test.

      Return value

      #boolean: -true if the point is within the zone.

      +true if the Vec3 is within the zone.

    @@ -1597,6 +1657,24 @@ The Vec2 coordinate.

    + +ZONE_POLYGON_BASE:GetVec2() + +
    +
    + +

    Returns the center location of the polygon.

    + +

    Return value

    + +

    Dcs.DCSTypes#Vec2: +The location of the zone based on the Group location.

    + +
    +
    +
    +
    + ZONE_POLYGON_BASE:IsVec2InZone(Vec2) @@ -2163,10 +2241,7 @@ self

    Type ZONE_UNIT

    - -

    The ZONE_UNIT class defined by a zone around a Unit#UNIT with a radius.

    - -

    Field(s)

    +

    Field(s)

    diff --git a/docs/Documentation/index.html b/docs/Documentation/index.html index 2c422f83e..85574f96d 100644 --- a/docs/Documentation/index.html +++ b/docs/Documentation/index.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • @@ -392,7 +393,7 @@ and creates a CSV file logging the scoring events and results for use at team or Task -

    This module contains the TASK class.

    +

    Tasking -- This module contains the TASK class.

    @@ -405,6 +406,12 @@ and creates a CSV file logging the scoring events and results for use at team or Task_A2G_Dispatcher

    Tasking - The TASKA2GDISPATCHER creates and manages player TASK_A2G tasks based on detected targets.

    + + + + Task_CARGO + +

    Tasking (Release 2.1) -- The TASK_CARGO models tasks for players to transport Cargo.

    diff --git a/docs/Documentation/routines.html b/docs/Documentation/routines.html index c2949d324..25f4c1a33 100644 --- a/docs/Documentation/routines.html +++ b/docs/Documentation/routines.html @@ -63,6 +63,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Presentations/TASK_CARGO/Dia1.JPG b/docs/Presentations/TASK_CARGO/Dia1.JPG new file mode 100644 index 0000000000000000000000000000000000000000..e4cab902fbc2c7577f8099fb8c58e47dc9f3d887 GIT binary patch literal 223630 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 z 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

      h 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 Date: Wed, 12 Apr 2017 18:02:17 +0200 Subject: [PATCH 08/14] Documentation update --- Moose Mission Setup/Moose.lua | 75 + docs/Documentation/Cargo.html | 1 - docs/Documentation/DCSAirbase.html | 1 + docs/Documentation/DCSCoalitionObject.html | 1 + docs/Documentation/DCSCommand.html | 1 + docs/Documentation/DCSController.html | 1 + docs/Documentation/DCSGroup.html | 1 + docs/Documentation/DCSObject.html | 1 + docs/Documentation/DCSTask.html | 1 + docs/Documentation/DCSTypes.html | 1 + docs/Documentation/DCSUnit.html | 1 + docs/Documentation/DCSVec3.html | 1 + docs/Documentation/DCSWorld.html | 1 + docs/Documentation/DCSZone.html | 1 + docs/Documentation/DCScountry.html | 1 + docs/Documentation/DCStimer.html | 1 + docs/Documentation/DCStrigger.html | 1 + docs/Documentation/Detection.html | 2 + docs/Documentation/Event.html | 2607 ++++++++++++++++++++ docs/Documentation/Fsm.html | 3 +- docs/Documentation/Point.html | 1 - docs/Documentation/Spawn.html | 15 +- docs/Documentation/SpawnStatic.html | 2 +- docs/Documentation/StaticObject.html | 1 + docs/Documentation/Task_CARGO.html | 19 + docs/Documentation/env.html | 1 + docs/Documentation/land.html | 1 + 27 files changed, 2732 insertions(+), 11 deletions(-) diff --git a/Moose Mission Setup/Moose.lua b/Moose Mission Setup/Moose.lua index e69de29bb..7cce9cff2 100644 --- a/Moose Mission Setup/Moose.lua +++ b/Moose Mission Setup/Moose.lua @@ -0,0 +1,75 @@ +env.info( '*** MOOSE DYNAMIC INCLUDE START *** ' ) +env.info( 'Moose Generation Timestamp: 20170412_1738' ) + +local base = _G + +__Moose = {} + +__Moose.Include = function( IncludeFile ) + if not __Moose.Includes[ IncludeFile ] then + __Moose.Includes[IncludeFile] = IncludeFile + local f = assert( base.loadfile( __Moose.ProgramPath .. IncludeFile ) ) + if f == nil then + error ("Moose: Could not load Moose file " .. IncludeFile ) + else + env.info( "Moose: " .. IncludeFile .. " dynamically loaded from " .. __Moose.ProgramPath ) + return f() + end + end +end + +__Moose.ProgramPath = "Scripts/Moose/" + +__Moose.Includes = {} +__Moose.Include( 'Utilities/Routines.lua' ) +__Moose.Include( 'Utilities/Utils.lua' ) +__Moose.Include( 'Core/Base.lua' ) +__Moose.Include( 'Core/Scheduler.lua' ) +__Moose.Include( 'Core/ScheduleDispatcher.lua' ) +__Moose.Include( 'Core/Event.lua' ) +__Moose.Include( 'Core/Menu.lua' ) +__Moose.Include( 'Core/Zone.lua' ) +__Moose.Include( 'Core/Database.lua' ) +__Moose.Include( 'Core/Set.lua' ) +__Moose.Include( 'Core/Point.lua' ) +__Moose.Include( 'Core/Message.lua' ) +__Moose.Include( 'Core/Fsm.lua' ) +__Moose.Include( 'Core/Radio.lua' ) +__Moose.Include( 'Core/SpawnStatic.lua' ) +__Moose.Include( 'Wrapper/Object.lua' ) +__Moose.Include( 'Wrapper/Identifiable.lua' ) +__Moose.Include( 'Wrapper/Positionable.lua' ) +__Moose.Include( 'Wrapper/Controllable.lua' ) +__Moose.Include( 'Wrapper/Group.lua' ) +__Moose.Include( 'Wrapper/Unit.lua' ) +__Moose.Include( 'Wrapper/Client.lua' ) +__Moose.Include( 'Wrapper/Static.lua' ) +__Moose.Include( 'Wrapper/Airbase.lua' ) +__Moose.Include( 'Wrapper/Scenery.lua' ) +__Moose.Include( 'Functional/Scoring.lua' ) +__Moose.Include( 'Functional/CleanUp.lua' ) +__Moose.Include( 'Functional/Spawn.lua' ) +__Moose.Include( 'Functional/Movement.lua' ) +__Moose.Include( 'Functional/Sead.lua' ) +__Moose.Include( 'Functional/Escort.lua' ) +__Moose.Include( 'Functional/MissileTrainer.lua' ) +__Moose.Include( 'Functional/AirbasePolice.lua' ) +__Moose.Include( 'Functional/Detection.lua' ) +__Moose.Include( 'AI/AI_Balancer.lua' ) +__Moose.Include( 'AI/AI_Patrol.lua' ) +__Moose.Include( 'AI/AI_Cap.lua' ) +__Moose.Include( 'AI/AI_Cas.lua' ) +__Moose.Include( 'AI/AI_Cargo.lua' ) +__Moose.Include( 'Actions/Act_Assign.lua' ) +__Moose.Include( 'Actions/Act_Route.lua' ) +__Moose.Include( 'Actions/Act_Account.lua' ) +__Moose.Include( 'Actions/Act_Assist.lua' ) +__Moose.Include( 'Tasking/CommandCenter.lua' ) +__Moose.Include( 'Tasking/Mission.lua' ) +__Moose.Include( 'Tasking/Task.lua' ) +__Moose.Include( 'Tasking/DetectionManager.lua' ) +__Moose.Include( 'Tasking/Task_A2G_Dispatcher.lua' ) +__Moose.Include( 'Tasking/Task_A2G.lua' ) +__Moose.Include( 'Moose.lua' ) +BASE:TraceOnOff( true ) +env.info( '*** MOOSE INCLUDE END *** ' ) diff --git a/docs/Documentation/Cargo.html b/docs/Documentation/Cargo.html index d3e6dcf81..07f90f7bb 100644 --- a/docs/Documentation/Cargo.html +++ b/docs/Documentation/Cargo.html @@ -2615,7 +2615,6 @@ The UNIT carrying the package.

      - AI_CARGO_UNIT.CargoCarrier diff --git a/docs/Documentation/DCSAirbase.html b/docs/Documentation/DCSAirbase.html index 860fab349..5e7df20f9 100644 --- a/docs/Documentation/DCSAirbase.html +++ b/docs/Documentation/DCSAirbase.html @@ -80,6 +80,7 @@
    • Task
    • Task_A2G
    • Task_A2G_Dispatcher
    • +
    • Task_CARGO
    • Task_PICKUP
    • Unit
    • Utils
    • diff --git a/docs/Documentation/DCSCoalitionObject.html b/docs/Documentation/DCSCoalitionObject.html index 003f27213..9adc3b0fb 100644 --- a/docs/Documentation/DCSCoalitionObject.html +++ b/docs/Documentation/DCSCoalitionObject.html @@ -80,6 +80,7 @@
    • Task
    • Task_A2G
    • Task_A2G_Dispatcher
    • +
    • Task_CARGO
    • Task_PICKUP
    • Unit
    • Utils
    • diff --git a/docs/Documentation/DCSCommand.html b/docs/Documentation/DCSCommand.html index 72b05130d..375f7891f 100644 --- a/docs/Documentation/DCSCommand.html +++ b/docs/Documentation/DCSCommand.html @@ -80,6 +80,7 @@
    • Task
    • Task_A2G
    • Task_A2G_Dispatcher
    • +
    • Task_CARGO
    • Task_PICKUP
    • Unit
    • Utils
    • diff --git a/docs/Documentation/DCSController.html b/docs/Documentation/DCSController.html index f1229fcb4..1a1714fcc 100644 --- a/docs/Documentation/DCSController.html +++ b/docs/Documentation/DCSController.html @@ -80,6 +80,7 @@
    • Task
    • Task_A2G
    • Task_A2G_Dispatcher
    • +
    • Task_CARGO
    • Task_PICKUP
    • Unit
    • Utils
    • diff --git a/docs/Documentation/DCSGroup.html b/docs/Documentation/DCSGroup.html index 45543df9b..4236a724c 100644 --- a/docs/Documentation/DCSGroup.html +++ b/docs/Documentation/DCSGroup.html @@ -80,6 +80,7 @@
    • Task
    • Task_A2G
    • Task_A2G_Dispatcher
    • +
    • Task_CARGO
    • Task_PICKUP
    • Unit
    • Utils
    • diff --git a/docs/Documentation/DCSObject.html b/docs/Documentation/DCSObject.html index 0ba8eec6b..b70eab2b0 100644 --- a/docs/Documentation/DCSObject.html +++ b/docs/Documentation/DCSObject.html @@ -80,6 +80,7 @@
    • Task
    • Task_A2G
    • Task_A2G_Dispatcher
    • +
    • Task_CARGO
    • Task_PICKUP
    • Unit
    • Utils
    • diff --git a/docs/Documentation/DCSTask.html b/docs/Documentation/DCSTask.html index 3300796fe..d68a0619b 100644 --- a/docs/Documentation/DCSTask.html +++ b/docs/Documentation/DCSTask.html @@ -80,6 +80,7 @@
    • Task
    • Task_A2G
    • Task_A2G_Dispatcher
    • +
    • Task_CARGO
    • Task_PICKUP
    • Unit
    • Utils
    • diff --git a/docs/Documentation/DCSTypes.html b/docs/Documentation/DCSTypes.html index 771115adc..6d8ff9743 100644 --- a/docs/Documentation/DCSTypes.html +++ b/docs/Documentation/DCSTypes.html @@ -80,6 +80,7 @@
    • Task
    • Task_A2G
    • Task_A2G_Dispatcher
    • +
    • Task_CARGO
    • Task_PICKUP
    • Unit
    • Utils
    • diff --git a/docs/Documentation/DCSUnit.html b/docs/Documentation/DCSUnit.html index 2966a3c3f..cbc0b5504 100644 --- a/docs/Documentation/DCSUnit.html +++ b/docs/Documentation/DCSUnit.html @@ -80,6 +80,7 @@
    • Task
    • Task_A2G
    • Task_A2G_Dispatcher
    • +
    • Task_CARGO
    • Task_PICKUP
    • Unit
    • Utils
    • diff --git a/docs/Documentation/DCSVec3.html b/docs/Documentation/DCSVec3.html index 932c2b123..b7d80f43a 100644 --- a/docs/Documentation/DCSVec3.html +++ b/docs/Documentation/DCSVec3.html @@ -80,6 +80,7 @@
    • Task
    • Task_A2G
    • Task_A2G_Dispatcher
    • +
    • Task_CARGO
    • Task_PICKUP
    • Unit
    • Utils
    • diff --git a/docs/Documentation/DCSWorld.html b/docs/Documentation/DCSWorld.html index 0cc029800..20843d631 100644 --- a/docs/Documentation/DCSWorld.html +++ b/docs/Documentation/DCSWorld.html @@ -80,6 +80,7 @@
    • Task
    • Task_A2G
    • Task_A2G_Dispatcher
    • +
    • Task_CARGO
    • Task_PICKUP
    • Unit
    • Utils
    • diff --git a/docs/Documentation/DCSZone.html b/docs/Documentation/DCSZone.html index 6f730e275..8133929e9 100644 --- a/docs/Documentation/DCSZone.html +++ b/docs/Documentation/DCSZone.html @@ -80,6 +80,7 @@
    • Task
    • Task_A2G
    • Task_A2G_Dispatcher
    • +
    • Task_CARGO
    • Task_PICKUP
    • Unit
    • Utils
    • diff --git a/docs/Documentation/DCScountry.html b/docs/Documentation/DCScountry.html index 2540119a7..6a611ad74 100644 --- a/docs/Documentation/DCScountry.html +++ b/docs/Documentation/DCScountry.html @@ -80,6 +80,7 @@
    • Task
    • Task_A2G
    • Task_A2G_Dispatcher
    • +
    • Task_CARGO
    • Task_PICKUP
    • Unit
    • Utils
    • diff --git a/docs/Documentation/DCStimer.html b/docs/Documentation/DCStimer.html index 6d02f30d2..9a1b8b7ca 100644 --- a/docs/Documentation/DCStimer.html +++ b/docs/Documentation/DCStimer.html @@ -80,6 +80,7 @@
    • Task
    • Task_A2G
    • Task_A2G_Dispatcher
    • +
    • Task_CARGO
    • Task_PICKUP
    • Unit
    • Utils
    • diff --git a/docs/Documentation/DCStrigger.html b/docs/Documentation/DCStrigger.html index 2b9776637..5a5901245 100644 --- a/docs/Documentation/DCStrigger.html +++ b/docs/Documentation/DCStrigger.html @@ -80,6 +80,7 @@
    • Task
    • Task_A2G
    • Task_A2G_Dispatcher
    • +
    • Task_CARGO
    • Task_PICKUP
    • Unit
    • Utils
    • diff --git a/docs/Documentation/Detection.html b/docs/Documentation/Detection.html index f4ef292c5..8c2feee50 100644 --- a/docs/Documentation/Detection.html +++ b/docs/Documentation/Detection.html @@ -2172,6 +2172,7 @@ self

      + #number DETECTION_BASE.DetectedItemCount @@ -2185,6 +2186,7 @@ self

      + #number DETECTION_BASE.DetectedItemMax diff --git a/docs/Documentation/Event.html b/docs/Documentation/Event.html index e69de29bb..ba837766d 100644 --- a/docs/Documentation/Event.html +++ b/docs/Documentation/Event.html @@ -0,0 +1,2607 @@ + + + + + + +
      +
      + +
      +
      +
      +
      + +
      +

      Module Event

      + +

      Core - EVENT models DCS event dispatching using a publish-subscribe model.

      + + + +

      Banner Image

      + +
      + +

      1) Event Handling Overview

      + +

      Objects

      + +

      Within a running mission, various DCS events occur. Units are dynamically created, crash, die, shoot stuff, get hit etc. +This module provides a mechanism to dispatch those events occuring within your running mission, to the different objects orchestrating your mission.

      + +

      Objects

      + +

      Objects can subscribe to different events. The Event dispatcher will publish the received DCS events to the subscribed MOOSE objects, in a specified order. +In this way, the subscribed MOOSE objects are kept in sync with your evolving running mission.

      + +

      1.1) Event Dispatching

      + +

      Objects

      + +

      The _EVENTDISPATCHER object is automatically created within MOOSE, +and handles the dispatching of DCS Events occurring +in the simulator to the subscribed objects +in the correct processing order.

      + +

      Objects

      + +

      There are 5 levels of kind of objects that the _EVENTDISPATCHER services:

      + +
        +
      • _DATABASE object: The core of the MOOSE objects. Any object that is created, deleted or updated, is done in this database.
      • +
      • SET_ derived classes: Subsets of the _DATABASE object. These subsets are updated by the _EVENTDISPATCHER as the second priority.
      • +
      • UNIT objects: UNIT objects can subscribe to DCS events. Each DCS event will be directly published to teh subscribed UNIT object.
      • +
      • GROUP objects: GROUP objects can subscribe to DCS events. Each DCS event will be directly published to the subscribed GROUP object.
      • +
      • Any other object: Various other objects can subscribe to DCS events. Each DCS event triggered will be published to each subscribed object.
      • +
      + +

      Objects

      + +

      For most DCS events, the above order of updating will be followed.

      + +

      Objects

      + +

      But for some DCS events, the publishing order is reversed. This is due to the fact that objects need to be erased instead of added.

      + +

      1.2) Event Handling

      + +

      Objects

      + +

      The actual event subscribing and handling is not facilitated through the _EVENTDISPATCHER, but it is done through the BASE class, UNIT class and GROUP class. +The _EVENTDISPATCHER is a component that is quietly working in the background of MOOSE.

      + +

      Objects

      + +

      The BASE class provides methods to catch DCS Events. These are events that are triggered from within the DCS simulator, +and handled through lua scripting. MOOSE provides an encapsulation to handle these events more efficiently.

      + +

      1.2.1 Subscribe / Unsubscribe to DCS Events

      + +

      At first, the mission designer will need to Subscribe to a specific DCS event for the class. +So, when the DCS event occurs, the class will be notified of that event. +There are two functions which you use to subscribe to or unsubscribe from an event.

      + + + +

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

      + +

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

      + +

      1.3.2 Event Handling of DCS Events

      + +

      Once the class is subscribed to the event, an Event Handling method on the object or class needs to be written that will be called +when the DCS event occurs. The Event Handling method receives an Event#EVENTDATA structure, which contains a lot of information +about the event that occurred.

      + +

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

      + +
       local Tank1 = UNIT:FindByName( "Tank A" )
      + local Tank2 = UNIT:FindByName( "Tank B" )
      +
      + -- Here we subscribe to the Dead events. So, if one of these tanks dies, the Tank1 or Tank2 objects will be notified.
      + Tank1:HandleEvent( EVENTS.Dead )
      + Tank2:HandleEvent( EVENTS.Dead )
      +
      + --- This function is an Event Handling function that will be called when Tank1 is Dead.
      + -- @param Wrapper.Unit#UNIT self 
      + -- @param Core.Event#EVENTDATA EventData
      + function Tank1:OnEventDead( EventData )
      +
      +   self:SmokeGreen()
      + end
      +
      + --- This function is an Event Handling function that will be called when Tank2 is Dead.
      + -- @param Wrapper.Unit#UNIT self 
      + -- @param Core.Event#EVENTDATA EventData
      + function Tank2:OnEventDead( EventData )
      +
      +   self:SmokeBlue()
      + end
      +
      + +

      1.3.3 Event Handling methods that are automatically called upon subscribed DCS events

      + +

      Objects

      + +

      The following list outlines which EVENTS item in the structure corresponds to which Event Handling method. +Always ensure that your event handling methods align with the events being subscribed to, or nothing will be executed.

      + +

      2) EVENTS type

      + +

      The EVENTS structure contains names for all the different DCS events that objects can subscribe to using the +Base#BASE.HandleEvent() method.

      + +

      3) EVENTDATA type

      + +

      The Event#EVENTDATA structure contains all the fields that are populated with event information before +an Event Handler method is being called by the event dispatcher. +The Event Handler received the EVENTDATA object as a parameter, and can be used to investigate further the different events. +There are basically 4 main categories of information stored in the EVENTDATA structure:

      + +
        +
      • Initiator Unit data: Several fields documenting the initiator unit related to the event.

      • +
      • Target Unit data: Several fields documenting the target unit related to the event.

      • +
      • Weapon data: Certain events populate weapon information.

      • +
      • Place data: Certain events populate place information.

        + +

        --- This function is an Event Handling function that will be called when Tank1 is Dead. + -- EventData is an EVENTDATA structure. + -- We use the EventData.IniUnit to smoke the tank Green. + -- @param Wrapper.Unit#UNIT self + -- @param Core.Event#EVENTDATA EventData + function Tank1:OnEventDead( EventData )

        + +

        EventData.IniUnit:SmokeGreen() + end

      • +
      + + +

      Find below an overview which events populate which information categories:

      + +

      Objects

      + +

      IMPORTANT NOTE: Some events can involve not just UNIT objects, but also STATIC objects!!! +In that case the initiator or target unit fields will refer to a STATIC object! +In case a STATIC object is involved, the documentation indicates which fields will and won't not be populated. +The fields IniObjectCategory and TgtObjectCategory contain the indicator which kind of object is involved in the event. +You can use the enumerator Object.Category.UNIT and Object.Category.STATIC to check on IniObjectCategory and TgtObjectCategory. +Example code snippet:

      + +
       if Event.IniObjectCategory == Object.Category.UNIT then
      +  ...
      + end
      + if Event.IniObjectCategory == Object.Category.STATIC then
      +  ...
      + end 
      +
      + +

      When a static object is involved in the event, the Group and Player fields won't be populated.

      + +
      + +

      API CHANGE HISTORY

      + +

      The underlying change log documents the API changes. Please read this carefully. The following notation is used:

      + +
        +
      • Added parts are expressed in bold type face.
      • +
      • Removed parts are expressed in italic type face.
      • +
      + +

      YYYY-MM-DD: CLASS:NewFunction( Params ) replaces CLASS:OldFunction( Params ) +YYYY-MM-DD: CLASS:NewFunction( Params ) added

      + +

      Hereby the change log:

      + +
        +
      • 2017-03-07: Added the correct event dispatching in case the event is subscribed by a GROUP.

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

      • +
      + +
      + +

      AUTHORS and CONTRIBUTIONS

      + +

      Contributions:

      + +

      Authors:

      + + + + +

      Global(s)

      + + + + + + + + + + + + + +
      EVENT + +
      EVENTHANDLER + +
      EVENTS + +
      +

      Type EVENT

      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      EVENT.ClassID + +
      EVENT.ClassName + +
      EVENT:CreateEventDeleteCargo(Cargo) +

      Creation of a Cargo Deletion Event.

      +
      EVENT:CreateEventNewCargo(Cargo) +

      Creation of a New Cargo Event.

      +
      EVENT.Events + +
      EVENT.EventsDead + +
      EVENT:Init(EventID, EventClass) +

      Initializes the Events structure for the event

      +
      EVENT:New() + +
      EVENT:OnBirthForTemplate(EventGroup, EventFunction, EventClass, EventTemplate) +

      Create an OnBirth event handler for a group

      +
      EVENT:OnCrashForTemplate(EventGroup, EventFunction, EventClass, EventTemplate) +

      Create an OnCrash event handler for a group

      +
      EVENT:OnDeadForTemplate(EventGroup, EventFunction, EventClass, EventTemplate) +

      Create an OnDead event handler for a group

      +
      EVENT:OnEngineShutDownForTemplate(EventTemplate, EventFunction, EventClass) +

      Create an OnDead event handler for a group

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

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

      +
      EVENT:OnEventForTemplate(EventTemplate, EventFunction, EventClass, OnEventFunction, EventID) +

      Create an OnDead event handler for a group

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

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

      +
      EVENT:OnEventGeneric(EventFunction, EventClass, EventID) +

      Set a new listener for an SEVENTX event independent from a unit or a weapon.

      +
      EVENT:OnLandForTemplate(EventTemplate, EventFunction, EventClass) + +
      EVENT:OnTakeOffForTemplate(EventTemplate, EventFunction, EventClass) + +
      EVENT:Remove(EventClass, EventID) +

      Removes a subscription

      +
      EVENT:RemoveAll(EventObject) +

      Clears all event subscriptions for a Base#BASE derived object.

      +
      EVENT:Reset(EventClass, EventID, EventObject) +

      Resets subscriptions

      +
      EVENT:onEvent(Event) + +
      + +

      Type EVENT.Events

      + + + + + +
      EVENT.Events.IniUnit + +
      + +

      Type EVENTDATA

      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      EVENTDATA.Cargo + +
      EVENTDATA.CargoName + +
      EVENTDATA.IniCategory +

      (UNIT) The category of the initiator.

      +
      EVENTDATA.IniCoalition +

      (UNIT) The coalition of the initiator.

      +
      EVENTDATA.IniDCSGroup +

      (UNIT) The initiating {DCSGroup#Group}.

      +
      EVENTDATA.IniDCSGroupName +

      (UNIT) The initiating Group name.

      +
      EVENTDATA.IniDCSUnit +

      (UNIT/STATIC) The initiating DCSUnit#Unit or DCSStaticObject#StaticObject.

      +
      EVENTDATA.IniDCSUnitName +

      (UNIT/STATIC) The initiating Unit name.

      +
      EVENTDATA.IniGroup +

      (UNIT) The initiating MOOSE wrapper Group#GROUP of the initiator Group object.

      +
      EVENTDATA.IniGroupName +

      UNIT) The initiating GROUP name (same as IniDCSGroupName).

      +
      EVENTDATA.IniObjectCategory +

      (UNIT/STATIC/SCENERY) The initiator object category ( Object.Category.UNIT or Object.Category.STATIC ).

      +
      EVENTDATA.IniPlayerName +

      (UNIT) The name of the initiating player in case the Unit is a client or player slot.

      +
      EVENTDATA.IniTypeName +

      (UNIT) The type name of the initiator.

      + +
      EVENTDATA.IniUnit +

      (UNIT/STATIC) The initiating MOOSE wrapper Unit#UNIT of the initiator Unit object.

      +
      EVENTDATA.IniUnitName +

      (UNIT/STATIC) The initiating UNIT name (same as IniDCSUnitName).

      +
      EVENTDATA.TgtCategory +

      (UNIT) The category of the target.

      +
      EVENTDATA.TgtCoalition +

      (UNIT) The coalition of the target.

      +
      EVENTDATA.TgtDCSGroup +

      (UNIT) The target {DCSGroup#Group}.

      +
      EVENTDATA.TgtDCSGroupName +

      (UNIT) The target Group name.

      +
      EVENTDATA.TgtDCSUnit +

      (UNIT/STATIC) The target DCSUnit#Unit or DCSStaticObject#StaticObject.

      +
      EVENTDATA.TgtDCSUnitName +

      (UNIT/STATIC) The target Unit name.

      +
      EVENTDATA.TgtGroup +

      (UNIT) The target MOOSE wrapper Group#GROUP of the target Group object.

      +
      EVENTDATA.TgtGroupName +

      (UNIT) The target GROUP name (same as TgtDCSGroupName).

      +
      EVENTDATA.TgtObjectCategory +

      (UNIT/STATIC) The target object category ( Object.Category.UNIT or Object.Category.STATIC ).

      +
      EVENTDATA.TgtPlayerName +

      (UNIT) The name of the target player in case the Unit is a client or player slot.

      +
      EVENTDATA.TgtTypeName +

      (UNIT) The type name of the target.

      + +
      EVENTDATA.TgtUnit +

      (UNIT/STATIC) The target MOOSE wrapper Unit#UNIT of the target Unit object.

      +
      EVENTDATA.TgtUnitName +

      (UNIT/STATIC) The target UNIT name (same as TgtDCSUnitName).

      +
      EVENTDATA.Weapon + +
      EVENTDATA.WeaponCategory + +
      EVENTDATA.WeaponCoalition + +
      EVENTDATA.WeaponName + +
      EVENTDATA.WeaponPlayerName + +
      EVENTDATA.WeaponTgtDCSUnit + +
      EVENTDATA.WeaponTypeName + +
      EVENTDATA.WeaponUNIT + +
      EVENTDATA.id +

      The identifier of the event.

      + +
      EVENTDATA.initiator +

      (UNIT/STATIC/SCENERY) The initiating Dcs.DCSUnit#Unit or Dcs.DCSStaticObject#StaticObject.

      +
      EVENTDATA.target +

      (UNIT/STATIC) The target Dcs.DCSUnit#Unit or DCSStaticObject#StaticObject.

      +
      EVENTDATA.weapon +

      The weapon used during the event.

      +
      + +

      Type EVENTHANDLER

      + + + + + + + + + + + + + +
      EVENTHANDLER.ClassID + +
      EVENTHANDLER.ClassName + +
      EVENTHANDLER:New() +

      The EVENTHANDLER constructor

      +
      + +

      Type EVENTS

      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      EVENTS.BaseCaptured + +
      EVENTS.Birth + +
      EVENTS.Crash + +
      EVENTS.Dead + +
      EVENTS.DeleteCargo + +
      EVENTS.Ejection + +
      EVENTS.EngineShutdown + +
      EVENTS.EngineStartup + +
      EVENTS.Hit + +
      EVENTS.HumanFailure + +
      EVENTS.Land + +
      EVENTS.MissionEnd + +
      EVENTS.MissionStart + +
      EVENTS.NewCargo + +
      EVENTS.PilotDead + +
      EVENTS.PlayerComment + +
      EVENTS.PlayerEnterUnit + +
      EVENTS.PlayerLeaveUnit + +
      EVENTS.Refueling + +
      EVENTS.RefuelingStop + +
      EVENTS.ShootingEnd + +
      EVENTS.ShootingStart + +
      EVENTS.Shot + +
      EVENTS.Takeoff + +
      EVENTS.TookControl + +
      + +

      Global(s)

      +
      +
      + + #EVENT + +EVENT + +
      +
      + + + +
      +
      +
      +
      + + #EVENTHANDLER + +EVENTHANDLER + +
      +
      + + + +
      +
      +
      +
      + + #EVENTS + +EVENTS + +
      +
      + + + +
      +
      +

      Type Event

      + +

      Type EVENT

      + +

      The EVENT structure

      + +

      Field(s)

      +
      +
      + + #number + +EVENT.ClassID + +
      +
      + + + +
      +
      +
      +
      + + #string + +EVENT.ClassName + +
      +
      + + + +
      +
      +
      +
      + + +EVENT:CreateEventDeleteCargo(Cargo) + +
      +
      + +

      Creation of a Cargo Deletion Event.

      + +

      Parameter

      + +
      +
      +
      +
      + + +EVENT:CreateEventNewCargo(Cargo) + +
      +
      + +

      Creation of a New Cargo Event.

      + +

      Parameter

      + +
      +
      +
      +
      + + #EVENT.Events + +EVENT.Events + +
      +
      + + + +
      +
      +
      +
      + + +EVENT.EventsDead + +
      +
      + + + +
      +
      +
      +
      + + +EVENT:Init(EventID, EventClass) + +
      +
      + +

      Initializes the Events structure for the event

      + +

      Parameters

      + +

      Return value

      + +

      #EVENT.Events:

      + + +
      +
      +
      +
      + + +EVENT:New() + +
      +
      + + + +
      +
      +
      +
      + + +EVENT:OnBirthForTemplate(EventGroup, EventFunction, EventClass, EventTemplate) + +
      +
      + +

      Create an OnBirth event handler for a group

      + +

      Parameters

      +
        +
      • + +

        Wrapper.Group#GROUP EventGroup :

        + +
      • +
      • + +

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

        + +
      • +
      • + +

        EventClass : +The self instance of the class for which the event is.

        + +
      • +
      • + +

        EventTemplate :

        + +
      • +
      +

      Return value

      + +

      #EVENT:

      + + +
      +
      +
      +
      + + +EVENT:OnCrashForTemplate(EventGroup, EventFunction, EventClass, EventTemplate) + +
      +
      + +

      Create an OnCrash event handler for a group

      + +

      Parameters

      +
        +
      • + +

        Wrapper.Group#GROUP EventGroup :

        + +
      • +
      • + +

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

        + +
      • +
      • + +

        EventClass : +The self instance of the class for which the event is.

        + +
      • +
      • + +

        EventTemplate :

        + +
      • +
      +

      Return value

      + +

      #EVENT:

      + + +
      +
      +
      +
      + + +EVENT:OnDeadForTemplate(EventGroup, EventFunction, EventClass, EventTemplate) + +
      +
      + +

      Create an OnDead event handler for a group

      + +

      Parameters

      +
        +
      • + +

        Wrapper.Group#GROUP EventGroup :

        + +
      • +
      • + +

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

        + +
      • +
      • + +

        EventClass : +The self instance of the class for which the event is.

        + +
      • +
      • + +

        EventTemplate :

        + +
      • +
      +

      Return value

      + +

      #EVENT:

      + + +
      +
      +
      +
      + + +EVENT:OnEngineShutDownForTemplate(EventTemplate, EventFunction, EventClass) + +
      +
      + +

      Create an OnDead event handler for a group

      + +

      Parameters

      +
        +
      • + +

        #table EventTemplate :

        + +
      • +
      • + +

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

        + +
      • +
      • + +

        EventClass : +The self instance of the class for which the event is.

        + +
      • +
      +

      Return value

      + +

      #EVENT:

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

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

      + +

      Parameters

      +
        +
      • + +

        #string GroupName : +The name of the GROUP.

        + +
      • +
      • + +

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

        + +
      • +
      • + +

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

        + +
      • +
      • + +

        EventID :

        + +
      • +
      +

      Return value

      + +

      #EVENT:

      + + +
      +
      +
      +
      + + +EVENT:OnEventForTemplate(EventTemplate, EventFunction, EventClass, OnEventFunction, EventID) + +
      +
      + +

      Create an OnDead event handler for a group

      + +

      Parameters

      +
        +
      • + +

        #table EventTemplate :

        + +
      • +
      • + +

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

        + +
      • +
      • + +

        EventClass : +The instance of the class for which the event is.

        + +
      • +
      • + +

        #function OnEventFunction :

        + +
      • +
      • + +

        EventID :

        + +
      • +
      +

      Return value

      + +

      #EVENT:

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

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

      + +

      Parameters

      +
        +
      • + +

        #string UnitName : +The name of the UNIT.

        + +
      • +
      • + +

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

        + +
      • +
      • + +

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

        + +
      • +
      • + +

        EventID :

        + +
      • +
      +

      Return value

      + +

      #EVENT:

      + + +
      +
      +
      +
      + + +EVENT:OnEventGeneric(EventFunction, EventClass, EventID) + +
      +
      + +

      Set a new listener for an SEVENTX event independent from a unit or a weapon.

      + +

      Parameters

      +
        +
      • + +

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

        + +
      • +
      • + +

        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.

        + +
      • +
      • + +

        EventID :

        + +
      • +
      +

      Return value

      + +

      #EVENT:

      + + +
      +
      +
      +
      + + +EVENT:OnLandForTemplate(EventTemplate, EventFunction, EventClass) + +
      +
      + + + +

      Parameters

      +
        +
      • + +

        EventTemplate :

        + +
      • +
      • + +

        EventFunction :

        + +
      • +
      • + +

        EventClass :

        + +
      • +
      +
      +
      +
      +
      + + +EVENT:OnTakeOffForTemplate(EventTemplate, EventFunction, EventClass) + +
      +
      + + + +

      Parameters

      +
        +
      • + +

        EventTemplate :

        + +
      • +
      • + +

        EventFunction :

        + +
      • +
      • + +

        EventClass :

        + +
      • +
      +
      +
      +
      +
      + + +EVENT:Remove(EventClass, EventID) + +
      +
      + +

      Removes a subscription

      + +

      Parameters

      + +

      Return value

      + +

      #EVENT.Events:

      + + +
      +
      +
      +
      + + +EVENT:RemoveAll(EventObject) + +
      +
      + +

      Clears all event subscriptions for a Base#BASE derived object.

      + +

      Parameter

      + +
      +
      +
      +
      + + +EVENT:Reset(EventClass, EventID, EventObject) + +
      +
      + +

      Resets subscriptions

      + +

      Parameters

      + +

      Return value

      + +

      #EVENT.Events:

      + + +
      +
      +
      +
      + + +EVENT:onEvent(Event) + +
      +
      + + + +

      Parameter

      + +
      +
      + +

      Type EVENT.Events

      + +

      The Events structure

      + +

      Field(s)

      +
      +
      + + #number + +EVENT.Events.IniUnit + +
      +
      + + + +
      +
      + +

      Type EVENTDATA

      + +

      The Event structure +Note that at the beginning of each field description, there is an indication which field will be populated depending on the object type involved in the Event:

      + +
        +
      • A (Object.Category.)UNIT : A UNIT object type is involved in the Event.
      • +
      + + +
        +
      • A (Object.Category.)STATIC : A STATIC object type is involved in the Event.µ +
      • +
      + +

      Field(s)

      +
      +
      + + + +EVENTDATA.Cargo + +
      +
      + + + +
      +
      +
      +
      + + + +EVENTDATA.CargoName + +
      +
      + + + +
      +
      +
      +
      + + Dcs.DCSUnit#Unit.Category + +EVENTDATA.IniCategory + +
      +
      + +

      (UNIT) The category of the initiator.

      + +
      +
      +
      +
      + + Dcs.DCScoalition#coalition.side + +EVENTDATA.IniCoalition + +
      +
      + +

      (UNIT) The coalition of the initiator.

      + +
      +
      +
      +
      + + Dcs.DCSGroup#Group + +EVENTDATA.IniDCSGroup + +
      +
      + +

      (UNIT) The initiating {DCSGroup#Group}.

      + +
      +
      +
      +
      + + #string + +EVENTDATA.IniDCSGroupName + +
      +
      + +

      (UNIT) The initiating Group name.

      + +
      +
      +
      +
      + + Dcs.DCSUnit#Unit + +EVENTDATA.IniDCSUnit + +
      +
      + +

      (UNIT/STATIC) The initiating DCSUnit#Unit or DCSStaticObject#StaticObject.

      + +
      +
      +
      +
      + + #string + +EVENTDATA.IniDCSUnitName + +
      +
      + +

      (UNIT/STATIC) The initiating Unit name.

      + +
      +
      +
      +
      + + Wrapper.Group#GROUP + +EVENTDATA.IniGroup + +
      +
      + +

      (UNIT) The initiating MOOSE wrapper Group#GROUP of the initiator Group object.

      + +
      +
      +
      +
      + + #string + +EVENTDATA.IniGroupName + +
      +
      + +

      UNIT) The initiating GROUP name (same as IniDCSGroupName).

      + +
      +
      +
      +
      + + Dcs.DCSObject#Object.Category + +EVENTDATA.IniObjectCategory + +
      +
      + +

      (UNIT/STATIC/SCENERY) The initiator object category ( Object.Category.UNIT or Object.Category.STATIC ).

      + +
      +
      +
      +
      + + #string + +EVENTDATA.IniPlayerName + +
      +
      + +

      (UNIT) The name of the initiating player in case the Unit is a client or player slot.

      + +
      +
      +
      +
      + + #string + +EVENTDATA.IniTypeName + +
      +
      + +

      (UNIT) The type name of the initiator.

      + + +
      +
      +
      +
      + + Wrapper.Unit#UNIT + +EVENTDATA.IniUnit + +
      +
      + +

      (UNIT/STATIC) The initiating MOOSE wrapper Unit#UNIT of the initiator Unit object.

      + +
      +
      +
      +
      + + #string + +EVENTDATA.IniUnitName + +
      +
      + +

      (UNIT/STATIC) The initiating UNIT name (same as IniDCSUnitName).

      + +
      +
      +
      +
      + + Dcs.DCSUnit#Unit.Category + +EVENTDATA.TgtCategory + +
      +
      + +

      (UNIT) The category of the target.

      + +
      +
      +
      +
      + + Dcs.DCScoalition#coalition.side + +EVENTDATA.TgtCoalition + +
      +
      + +

      (UNIT) The coalition of the target.

      + +
      +
      +
      +
      + + Dcs.DCSGroup#Group + +EVENTDATA.TgtDCSGroup + +
      +
      + +

      (UNIT) The target {DCSGroup#Group}.

      + +
      +
      +
      +
      + + #string + +EVENTDATA.TgtDCSGroupName + +
      +
      + +

      (UNIT) The target Group name.

      + +
      +
      +
      +
      + + Dcs.DCSUnit#Unit + +EVENTDATA.TgtDCSUnit + +
      +
      + +

      (UNIT/STATIC) The target DCSUnit#Unit or DCSStaticObject#StaticObject.

      + +
      +
      +
      +
      + + #string + +EVENTDATA.TgtDCSUnitName + +
      +
      + +

      (UNIT/STATIC) The target Unit name.

      + +
      +
      +
      +
      + + Wrapper.Group#GROUP + +EVENTDATA.TgtGroup + +
      +
      + +

      (UNIT) The target MOOSE wrapper Group#GROUP of the target Group object.

      + +
      +
      +
      +
      + + #string + +EVENTDATA.TgtGroupName + +
      +
      + +

      (UNIT) The target GROUP name (same as TgtDCSGroupName).

      + +
      +
      +
      +
      + + Dcs.DCSObject#Object.Category + +EVENTDATA.TgtObjectCategory + +
      +
      + +

      (UNIT/STATIC) The target object category ( Object.Category.UNIT or Object.Category.STATIC ).

      + +
      +
      +
      +
      + + #string + +EVENTDATA.TgtPlayerName + +
      +
      + +

      (UNIT) The name of the target player in case the Unit is a client or player slot.

      + +
      +
      +
      +
      + + #string + +EVENTDATA.TgtTypeName + +
      +
      + +

      (UNIT) The type name of the target.

      + + +
      +
      +
      +
      + + Wrapper.Unit#UNIT + +EVENTDATA.TgtUnit + +
      +
      + +

      (UNIT/STATIC) The target MOOSE wrapper Unit#UNIT of the target Unit object.

      + +
      +
      +
      +
      + + #string + +EVENTDATA.TgtUnitName + +
      +
      + +

      (UNIT/STATIC) The target UNIT name (same as TgtDCSUnitName).

      + +
      +
      +
      +
      + + +EVENTDATA.Weapon + +
      +
      + + + +
      +
      +
      +
      + + +EVENTDATA.WeaponCategory + +
      +
      + + + +
      +
      +
      +
      + + +EVENTDATA.WeaponCoalition + +
      +
      + + + +
      +
      +
      +
      + + +EVENTDATA.WeaponName + +
      +
      + + + +
      +
      +
      +
      + + +EVENTDATA.WeaponPlayerName + +
      +
      + + + +
      +
      +
      +
      + + +EVENTDATA.WeaponTgtDCSUnit + +
      +
      + + + +
      +
      +
      +
      + + +EVENTDATA.WeaponTypeName + +
      +
      + + + +
      +
      +
      +
      + + + +EVENTDATA.WeaponUNIT + +
      +
      + + + + +

      Sometimes, the weapon is a player unit!

      + +
      +
      +
      +
      + + #number + +EVENTDATA.id + +
      +
      + +

      The identifier of the event.

      + + +
      +
      +
      +
      + + Dcs.DCSUnit#Unit + +EVENTDATA.initiator + +
      +
      + +

      (UNIT/STATIC/SCENERY) The initiating Dcs.DCSUnit#Unit or Dcs.DCSStaticObject#StaticObject.

      + +
      +
      +
      +
      + + Dcs.DCSUnit#Unit + +EVENTDATA.target + +
      +
      + +

      (UNIT/STATIC) The target Dcs.DCSUnit#Unit or DCSStaticObject#StaticObject.

      + +
      +
      +
      +
      + + +EVENTDATA.weapon + +
      +
      + +

      The weapon used during the event.

      + +
      +
      + +

      Type EVENTHANDLER

      + +

      The EVENTHANDLER structure

      + +

      Field(s)

      +
      +
      + + #number + +EVENTHANDLER.ClassID + +
      +
      + + + +
      +
      +
      +
      + + #string + +EVENTHANDLER.ClassName + +
      +
      + + + +
      +
      +
      +
      + + +EVENTHANDLER:New() + +
      +
      + +

      The EVENTHANDLER constructor

      + +

      Return value

      + +

      #EVENTHANDLER:

      + + +
      +
      + +

      Type EVENTS

      + +

      The different types of events supported by MOOSE.

      + + +

      Use this structure to subscribe to events using the Base#BASE.HandleEvent() method.

      + +

      Field(s)

      +
      +
      + + + +EVENTS.BaseCaptured + +
      +
      + + + +
      +
      +
      +
      + + + +EVENTS.Birth + +
      +
      + + + +
      +
      +
      +
      + + + +EVENTS.Crash + +
      +
      + + + +
      +
      +
      +
      + + + +EVENTS.Dead + +
      +
      + + + +
      +
      +
      +
      + + + +EVENTS.DeleteCargo + +
      +
      + + + +
      +
      +
      +
      + + + +EVENTS.Ejection + +
      +
      + + + +
      +
      +
      +
      + + + +EVENTS.EngineShutdown + +
      +
      + + + +
      +
      +
      +
      + + + +EVENTS.EngineStartup + +
      +
      + + + +
      +
      +
      +
      + + + +EVENTS.Hit + +
      +
      + + + +
      +
      +
      +
      + + + +EVENTS.HumanFailure + +
      +
      + + + +
      +
      +
      +
      + + + +EVENTS.Land + +
      +
      + + + +
      +
      +
      +
      + + + +EVENTS.MissionEnd + +
      +
      + + + +
      +
      +
      +
      + + + +EVENTS.MissionStart + +
      +
      + + + +
      +
      +
      +
      + + + +EVENTS.NewCargo + +
      +
      + + + +
      +
      +
      +
      + + + +EVENTS.PilotDead + +
      +
      + + + +
      +
      +
      +
      + + + +EVENTS.PlayerComment + +
      +
      + + + +
      +
      +
      +
      + + + +EVENTS.PlayerEnterUnit + +
      +
      + + + +
      +
      +
      +
      + + + +EVENTS.PlayerLeaveUnit + +
      +
      + + + +
      +
      +
      +
      + + + +EVENTS.Refueling + +
      +
      + + + +
      +
      +
      +
      + + + +EVENTS.RefuelingStop + +
      +
      + + + +
      +
      +
      +
      + + + +EVENTS.ShootingEnd + +
      +
      + + + +
      +
      +
      +
      + + + +EVENTS.ShootingStart + +
      +
      + + + +
      +
      +
      +
      + + + +EVENTS.Shot + +
      +
      + + + +
      +
      +
      +
      + + + +EVENTS.Takeoff + +
      +
      + + + +
      +
      +
      +
      + + + +EVENTS.TookControl + +
      +
      + + + +
      +
      + +
      + +
      + + diff --git a/docs/Documentation/Fsm.html b/docs/Documentation/Fsm.html index 9c7864cf4..baf5bc616 100644 --- a/docs/Documentation/Fsm.html +++ b/docs/Documentation/Fsm.html @@ -1582,7 +1582,7 @@ A string defining the start state.

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

      - FSM.current diff --git a/docs/Documentation/Point.html b/docs/Documentation/Point.html index d01447df5..a1fb3c45e 100644 --- a/docs/Documentation/Point.html +++ b/docs/Documentation/Point.html @@ -1367,7 +1367,6 @@ The new calculated POINT_VEC2.

      - POINT_VEC2.z diff --git a/docs/Documentation/Spawn.html b/docs/Documentation/Spawn.html index f963faca2..934853fcd 100644 --- a/docs/Documentation/Spawn.html +++ b/docs/Documentation/Spawn.html @@ -2117,6 +2117,9 @@ The group that was spawned. You can use this group for further actions.

      + +

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

      +
      @@ -2570,9 +2573,6 @@ when nothing was spawned.

      - -

      Overwrite unit names by default with group name.

      -
      @@ -2587,6 +2587,9 @@ when nothing was spawned.

      + +

      By default, no InitLimit

      +
      @@ -2622,7 +2625,7 @@ when nothing was spawned.

      - + #number SPAWN.SpawnMaxGroups @@ -2639,7 +2642,7 @@ when nothing was spawned.

      - + #number SPAWN.SpawnMaxUnitsAlive @@ -2967,7 +2970,7 @@ Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):Schedule( 600, 0.5 )
      - + #boolean SPAWN.SpawnUnControlled diff --git a/docs/Documentation/SpawnStatic.html b/docs/Documentation/SpawnStatic.html index 964497670..6d4d01653 100644 --- a/docs/Documentation/SpawnStatic.html +++ b/docs/Documentation/SpawnStatic.html @@ -80,6 +80,7 @@
    • Task
    • Task_A2G
    • Task_A2G_Dispatcher
    • +
    • Task_CARGO
    • Task_PICKUP
    • Unit
    • Utils
    • @@ -441,7 +442,6 @@ ptional) The name of the new static.

      - #number SPAWNSTATIC.SpawnIndex diff --git a/docs/Documentation/StaticObject.html b/docs/Documentation/StaticObject.html index d2314e1c1..05ee02014 100644 --- a/docs/Documentation/StaticObject.html +++ b/docs/Documentation/StaticObject.html @@ -80,6 +80,7 @@
    • Task
    • Task_A2G
    • Task_A2G_Dispatcher
    • +
    • Task_CARGO
    • Task_PICKUP
    • Unit
    • Utils
    • diff --git a/docs/Documentation/Task_CARGO.html b/docs/Documentation/Task_CARGO.html index 621b65667..321ecaa1a 100644 --- a/docs/Documentation/Task_CARGO.html +++ b/docs/Documentation/Task_CARGO.html @@ -31,6 +31,21 @@
    • Client
    • CommandCenter
    • Controllable
    • +
    • DCSAirbase
    • +
    • DCSCoalitionObject
    • +
    • DCSCommand
    • +
    • DCSController
    • +
    • DCSGroup
    • +
    • DCSObject
    • +
    • DCSTask
    • +
    • DCSTypes
    • +
    • DCSUnit
    • +
    • DCSVec3
    • +
    • DCSWorld
    • +
    • DCSZone
    • +
    • DCScountry
    • +
    • DCStimer
    • +
    • DCStrigger
    • Database
    • Detection
    • DetectionManager
    • @@ -59,7 +74,9 @@
    • Set
    • Smoke
    • Spawn
    • +
    • SpawnStatic
    • Static
    • +
    • StaticObject
    • Task
    • Task_A2G
    • Task_A2G_Dispatcher
    • @@ -68,6 +85,8 @@
    • Unit
    • Utils
    • Zone
    • +
    • env
    • +
    • land
    • routines
    diff --git a/docs/Documentation/env.html b/docs/Documentation/env.html index 1e26ec179..2701111a8 100644 --- a/docs/Documentation/env.html +++ b/docs/Documentation/env.html @@ -80,6 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/land.html b/docs/Documentation/land.html index 8d34bf296..012941041 100644 --- a/docs/Documentation/land.html +++ b/docs/Documentation/land.html @@ -80,6 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • +
  • Task_CARGO
  • Task_PICKUP
  • Unit
  • Utils
  • From c20f13f0f8c765de7f4fc97fe839ad9aa3c7ec19 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Thu, 13 Apr 2017 08:45:28 +0200 Subject: [PATCH 09/14] Progress --- .../Moose/{AI/AI_Cargo.lua => Core/Cargo.lua} | 626 +++++++++--------- Moose Mission Setup/Moose.files | 2 +- Moose Mission Setup/Moose.lua | 4 +- 3 files changed, 319 insertions(+), 313 deletions(-) rename Moose Development/Moose/{AI/AI_Cargo.lua => Core/Cargo.lua} (69%) diff --git a/Moose Development/Moose/AI/AI_Cargo.lua b/Moose Development/Moose/Core/Cargo.lua similarity index 69% rename from Moose Development/Moose/AI/AI_Cargo.lua rename to Moose Development/Moose/Core/Cargo.lua index 9b05de6c0..59b77675c 100644 --- a/Moose Development/Moose/AI/AI_Cargo.lua +++ b/Moose Development/Moose/Core/Cargo.lua @@ -1,21 +1,18 @@ ----Single-Player:**Yes** / Multi-Player:**Yes** / AI:**Yes** / Human:**No** / Types:**Ground** -- --- **Management of logical cargo objects, that can be transported from and to transportation carriers.** +--- **Core** -- Management of CARGO logistics, that can be transported from and to transportation carriers. -- --- ![Banner Image](..\Presentations\AI_CARGO\CARGO.JPG) +-- ![Banner Image](..\Presentations\CARGO\Dia1.JPG) -- -- === -- -- Cargo can be of various forms, always are composed out of ONE object ( one unit or one static or one slingload crate ): -- --- * 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_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_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. +-- * 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. --- --- +-- * CARGO_GROUP, represented by a Group of CARGO_UNITs. -- -- This module is still under construction, but is described above works already, and will keep working ... -- @@ -27,14 +24,14 @@ --- 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 +-- @function [parent=#CARGO] Board +-- @param #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 +-- @function [parent=#CARGO] __Board +-- @param #CARGO self -- @param #number DelaySeconds The amount of seconds to delay the action. -- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. @@ -43,14 +40,14 @@ --- 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 +-- @function [parent=#CARGO] UnBoard +-- @param #CARGO self -- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Point#POINT_VEC2) to where the cargo should run after onboarding. If not provided, the cargo will run to 60 meters behind the Carrier location. --- UnBoards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo from the Carrier. -- The cargo must be in the **Loaded** state. --- @function [parent=#AI_CARGO] __UnBoard --- @param #AI_CARGO self +-- @function [parent=#CARGO] __UnBoard +-- @param #CARGO self -- @param #number DelaySeconds The amount of seconds to delay the action. -- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Point#POINT_VEC2) to where the cargo should run after onboarding. If not provided, the cargo will run to 60 meters behind the Carrier location. @@ -59,14 +56,14 @@ --- 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 +-- @function [parent=#CARGO] Load +-- @param #CARGO self -- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. --- Loads the cargo to a Carrier. The event will load the cargo into the Carrier regardless of its position. There will be no movement simulated of the cargo loading. -- The cargo must be in the **UnLoaded** state. --- @function [parent=#AI_CARGO] __Load --- @param #AI_CARGO self +-- @function [parent=#CARGO] __Load +-- @param #CARGO self -- @param #number DelaySeconds The amount of seconds to delay the action. -- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. @@ -75,14 +72,14 @@ --- 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 +-- @function [parent=#CARGO] UnLoad +-- @param #CARGO self -- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Point#POINT_VEC2) to where the cargo will be placed after unloading. If not provided, the cargo will be placed 60 meters behind the Carrier location. --- UnLoads the cargo to a Carrier. The event will unload the cargo from the Carrier. There will be no movement simulated of the cargo loading. -- The cargo must be in the **Loaded** state. --- @function [parent=#AI_CARGO] __UnLoad --- @param #AI_CARGO self +-- @function [parent=#CARGO] __UnLoad +-- @param #CARGO self -- @param #number DelaySeconds The amount of seconds to delay the action. -- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Point#POINT_VEC2) to where the cargo will be placed after unloading. If not provided, the cargo will be placed 60 meters behind the Carrier location. @@ -90,46 +87,46 @@ -- UnLoaded ---- @function [parent=#AI_CARGO] OnLeaveUnLoaded --- @param #AI_CARGO self +--- @function [parent=#CARGO] OnLeaveUnLoaded +-- @param #CARGO self -- @param Wrapper.Controllable#CONTROLLABLE Controllable -- @return #boolean ---- @function [parent=#AI_CARGO] OnEnterUnLoaded --- @param #AI_CARGO self +--- @function [parent=#CARGO] OnEnterUnLoaded +-- @param #CARGO self -- @param Wrapper.Controllable#CONTROLLABLE Controllable -- Loaded ---- @function [parent=#AI_CARGO] OnLeaveLoaded --- @param #AI_CARGO self +--- @function [parent=#CARGO] OnLeaveLoaded +-- @param #CARGO self -- @param Wrapper.Controllable#CONTROLLABLE Controllable -- @return #boolean ---- @function [parent=#AI_CARGO] OnEnterLoaded --- @param #AI_CARGO self +--- @function [parent=#CARGO] OnEnterLoaded +-- @param #CARGO self -- @param Wrapper.Controllable#CONTROLLABLE Controllable -- Boarding ---- @function [parent=#AI_CARGO] OnLeaveBoarding --- @param #AI_CARGO self +--- @function [parent=#CARGO] OnLeaveBoarding +-- @param #CARGO self -- @param Wrapper.Controllable#CONTROLLABLE Controllable -- @return #boolean ---- @function [parent=#AI_CARGO] OnEnterBoarding --- @param #AI_CARGO self +--- @function [parent=#CARGO] OnEnterBoarding +-- @param #CARGO self -- @param Wrapper.Controllable#CONTROLLABLE Controllable -- UnBoarding ---- @function [parent=#AI_CARGO] OnLeaveUnBoarding --- @param #AI_CARGO self +--- @function [parent=#CARGO] OnLeaveUnBoarding +-- @param #CARGO self -- @param Wrapper.Controllable#CONTROLLABLE Controllable -- @return #boolean ---- @function [parent=#AI_CARGO] OnEnterUnBoarding --- @param #AI_CARGO self +--- @function [parent=#CARGO] OnEnterUnBoarding +-- @param #CARGO self -- @param Wrapper.Controllable#CONTROLLABLE Controllable @@ -137,14 +134,13 @@ CARGOS = {} -do -- AI_CARGO +do -- CARGO - --- @type AI_CARGO + --- @type CARGO -- @extends Core.Fsm#FSM_PROCESS -- @field #string Type A string defining the type of the cargo. eg. Engineers, Equipment, Screwdrivers. -- @field #string Name A string defining the name of the cargo. The name is the unique identifier of the cargo. -- @field #number Weight A number defining the weight of the cargo. The weight is expressed in kg. - -- @field #number 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... @@ -153,23 +149,23 @@ do -- AI_CARGO -- @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 class, extends @{Fsm#FSM_PROCESS} + --- # CARGO class, extends @{Fsm#FSM_PROCESS} -- - -- The AI\_CARGO class defines the core functions that defines a cargo object within MOOSE. + -- The CARGO class defines the core functions that defines a cargo object within MOOSE. -- A cargo is a logical object defined that is available for transport, and has a life status within a simulation. -- - -- The 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. + -- The CARGO is a state machine: it manages the different events and states of the cargo. + -- All derived classes from CARGO follow the same state machine, expose the same cargo event functions, and provide the same cargo states. -- - -- ## AI\_CARGO Events: + -- ## 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. + -- * @{#CARGO.Board}( ToCarrier ): Boards the cargo to a carrier. + -- * @{#CARGO.Load}( ToCarrier ): Loads the cargo into a carrier, regardless of its position. + -- * @{#CARGO.UnBoard}( ToPointVec2 ): UnBoard the cargo from a carrier. This will trigger a movement of the cargo to the option ToPointVec2. + -- * @{#CARGO.UnLoad}( ToPointVec2 ): UnLoads the cargo from a carrier. + -- * @{#CARGO.Dead}( Controllable ): The cargo is dead. The cargo process will be ended. -- - -- ## AI\_CARGO States: + -- ## CARGO States: -- -- * **UnLoaded**: The cargo is unloaded from a carrier. -- * **Boarding**: The cargo is currently boarding (= running) into a carrier. @@ -178,7 +174,7 @@ do -- AI_CARGO -- * **Dead**: The cargo is dead ... -- * **End**: The process has come to an end. -- - -- ## AI\_CARGO state transition methods: + -- ## 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: @@ -193,10 +189,10 @@ do -- AI_CARGO -- The state transition method needs to start with the name **OnEnter + the name of the state**. -- These state transition methods need to provide a return value, which is specified at the function description. -- - -- @field #AI_CARGO AI_CARGO + -- @field #CARGO CARGO -- - AI_CARGO = { - ClassName = "AI_CARGO", + CARGO = { + ClassName = "CARGO", Type = nil, Name = nil, Weight = nil, @@ -208,22 +204,21 @@ do -- AI_CARGO Containable = false, } ---- @type AI_CARGO.CargoObjects +--- @type 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 +--- CARGO Constructor. This class is an abstract class and should not be instantiated. +-- @param #CARGO self -- @param #string Type -- @param #string Name -- @param #number Weight --- @param #number ReportRadius (optional) -- @param #number NearRadius (optional) --- @return #AI_CARGO -function AI_CARGO:New( Type, Name, Weight, ReportRadius, NearRadius ) +-- @return #CARGO +function CARGO:New( Type, Name, Weight, NearRadius ) - local self = BASE:Inherit( self, FSM:New() ) -- Core.Fsm#FSM_CONTROLLABLE - self:F( { Type, Name, Weight, ReportRadius, NearRadius } ) + local self = BASE:Inherit( self, FSM:New() ) -- Core.Fsm#FSM + self:F( { Type, Name, Weight, NearRadius } ) self:SetStartState( "UnLoaded" ) self:AddTransition( "UnLoaded", "Board", "Boarding" ) @@ -239,7 +234,6 @@ function AI_CARGO:New( Type, Name, Weight, ReportRadius, NearRadius ) self.Type = Type self.Name = Name self.Weight = Weight - self.ReportRadius = ReportRadius or 1000 self.NearRadius = NearRadius or 200 self.CargoObject = nil self.CargoCarrier = nil @@ -248,7 +242,6 @@ function AI_CARGO:New( Type, Name, Weight, ReportRadius, NearRadius ) self.Moveable = false self.Containable = false - self.CargoScheduler = SCHEDULER:New() CARGOS[self.Name] = self @@ -259,47 +252,175 @@ function AI_CARGO:New( Type, Name, Weight, ReportRadius, NearRadius ) end --- Get the name of the Cargo. --- @param #AI_CARGO self +-- @param #CARGO self -- @return #string The name of the Cargo. -function AI_CARGO:GetName() +function CARGO:GetName() return self.Name end --- Get the type of the Cargo. --- @param #AI_CARGO self +-- @param #CARGO self -- @return #string The type of the Cargo. -function AI_CARGO:GetType() +function CARGO:GetType() return self.Type end --- Check if cargo is loaded. --- @param #AI_CARGO self +-- @param #CARGO self -- @return #boolean true if loaded -function AI_CARGO:IsLoaded() +function CARGO:IsLoaded() return self:Is( "Loaded" ) end --- Check if cargo is unloaded. --- @param #AI_CARGO self +-- @param #CARGO self -- @return #boolean true if unloaded -function AI_CARGO:IsUnLoaded() +function CARGO:IsUnLoaded() return self:Is( "UnLoaded" ) 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 ) +--- Template method to spawn a new representation of the CARGO in the simulator. +-- @param #CARGO self +-- @return #CARGO +function CARGO:Spawn( PointVec2 ) self:F() end ---- Check if CargoCarrier is in the radius for the Cargo to be Loaded. --- @param #AI_CARGO self + + +--- Check if Cargo is the given @{Zone}. +-- @param #CARGO self +-- @param Core.Zone#ZONE_BASE Zone +-- @return #boolean **true** if cargo is in the Zone, **false** if cargo is not in the Zone. +function CARGO:IsInZone( Zone ) + self:F( { Zone } ) + + if self:IsLoaded() then + return Zone:IsPointVec2InZone( self.CargoCarrier:GetPointVec2() ) + else + return Zone:IsPointVec2InZone( self.CargoObject:GetPointVec2() ) + end + + return nil + +end + + +--- Check if CargoCarrier is near the Cargo to be Loaded. +-- @param #CARGO self -- @param Core.Point#POINT_VEC2 PointVec2 -- @return #boolean -function AI_CARGO:IsInRadius( PointVec2 ) +function 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 + +--- Get the current PointVec2 of the cargo. +-- @param #CARGO self +-- @return Core.Point#POINT_VEC2 +function CARGO:GetPointVec2() + return self.CargoObject:GetPointVec2() +end + +--- Set the weight of the cargo. +-- @param #CARGO self +-- @param #number Weight The weight in kg. +-- @return #CARGO +function CARGO:SetWeight( Weight ) + self.Weight = Weight + return self +end + +end + + +do -- CARGO_REPRESENTABLE + + --- @type CARGO_REPRESENTABLE + -- @extends #CARGO + CARGO_REPRESENTABLE = { + ClassName = "CARGO_REPRESENTABLE" + } + +--- CARGO_REPRESENTABLE Constructor. +-- @param #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 #CARGO_REPRESENTABLE +function CARGO_REPRESENTABLE:New( CargoObject, Type, Name, Weight, ReportRadius, NearRadius ) + local self = BASE:Inherit( self, CARGO:New( Type, Name, Weight, ReportRadius, NearRadius ) ) -- #CARGO + self:F( { Type, Name, Weight, ReportRadius, NearRadius } ) + + return self +end + +--- Route a cargo unit to a PointVec2. +-- @param #CARGO_REPRESENTABLE self +-- @param Core.Point#POINT_VEC2 ToPointVec2 +-- @param #number Speed +-- @return #CARGO_REPRESENTABLE +function CARGO_REPRESENTABLE:RouteTo( ToPointVec2, Speed ) + self:F2( ToPointVec2 ) + + local Points = {} + + local PointStartVec2 = self.CargoObject:GetPointVec2() + + Points[#Points+1] = PointStartVec2:RoutePointGround( Speed ) + Points[#Points+1] = ToPointVec2:RoutePointGround( Speed ) + + local TaskRoute = self.CargoObject:TaskRoute( Points ) + self.CargoObject:SetTask( TaskRoute, 2 ) + return self +end + +end -- CARGO_REPRESENTABLE + +do -- CARGO_REPORTABLE + + --- @type CARGO_REPORTABLE + -- @extends #CARGO + CARGO_REPORTABLE = { + ClassName = "CARGO_REPORTABLE" + } + +--- CARGO_REPORTABLE Constructor. +-- @param #CARGO_REPORTABLE self +-- @param Wrapper.Controllable#Controllable CargoObject +-- @param #string Type +-- @param #string Name +-- @param #number Weight +-- @param #number ReportRadius (optional) +-- @param #number NearRadius (optional) +-- @return #CARGO_REPORTABLE +function CARGO_REPORTABLE:New( Type, Name, Weight, ReportRadius, NearRadius ) + local self = BASE:Inherit( self, CARGO:New( Type, Name, Weight, NearRadius ) ) -- #CARGO_REPORTABLE + self:F( { Type, Name, Weight, ReportRadius, NearRadius } ) + + self.ReportRadius = ReportRadius or 1000 + + return self +end + +--- Check if CargoCarrier is in the ReportRadius for the Cargo to be Loaded. +-- @param #CARGO_REPORTABLE self +-- @param Core.Point#POINT_VEC2 PointVec2 +-- @return #boolean +function CARGO_REPORTABLE:IsInRadius( PointVec2 ) self:F( { PointVec2 } ) local Distance = 0 @@ -317,134 +438,45 @@ function AI_CARGO:IsInRadius( PointVec2 ) end end - ---- Check if Cargo is the given @{Zone}. --- @param #AI_CARGO self --- @param Core.Zone#ZONE_BASE Zone --- @return #boolean **true** if cargo is in the Zone, **false** if cargo is not in the Zone. -function AI_CARGO:IsInZone( Zone ) - self:F( { Zone } ) - - if self:IsLoaded() then - return Zone:IsPointVec2InZone( self.CargoCarrier:GetPointVec2() ) - else - return Zone:IsPointVec2InZone( self.CargoObject:GetPointVec2() ) - end - - return nil - -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 - ---- Get the current PointVec2 of the cargo. --- @param #AI_CARGO self --- @return Core.Point#POINT_VEC2 -function AI_CARGO:GetPointVec2() - return self.CargoObject:GetPointVec2() -end - --- Get the range till cargo will board. --- @param #AI_CARGO self +-- @param #CARGO self -- @return #number The range till cargo will board. -function AI_CARGO:GetBoardingRange() +function CARGO:GetBoardingRange() return self.ReportRadius end end -do -- AI_CARGO_REPRESENTABLE +do -- CARGO_UNIT - --- @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 + --- @type CARGO_UNIT + -- @extends #CARGO_REPRESENTABLE - --- # AI\_CARGO\_UNIT class, extends @{#AI_CARGO_REPRESENTABLE} + --- # CARGO\_UNIT class, extends @{#CARGO_REPRESENTABLE} -- - -- 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. + -- The CARGO\_UNIT class defines a cargo that is represented by a UNIT object within the simulator, and can be transported by a carrier. + -- Use the event functions as described above to Load, UnLoad, Board, UnBoard the CARGO\_UNIT objects to and from carriers. -- -- === -- - -- @field #AI_CARGO_UNIT AI_CARGO_UNIT + -- @field #CARGO_UNIT CARGO_UNIT -- - AI_CARGO_UNIT = { - ClassName = "AI_CARGO_UNIT" + CARGO_UNIT = { + ClassName = "CARGO_UNIT" } ---- AI_CARGO_UNIT Constructor. --- @param #AI_CARGO_UNIT self +--- CARGO_UNIT Constructor. +-- @param #CARGO_UNIT self -- @param Wrapper.Unit#UNIT CargoUnit -- @param #string Type -- @param #string Name -- @param #number Weight -- @param #number ReportRadius (optional) -- @param #number NearRadius (optional) --- @return #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 } ) +-- @return #CARGO_UNIT +function CARGO_UNIT:New( CargoUnit, Type, Name, Weight, NearRadius ) + local self = BASE:Inherit( self, CARGO_REPRESENTABLE:New( CargoUnit, Type, Name, Weight, NearRadius ) ) -- #CARGO_UNIT + self:F( { Type, Name, Weight, NearRadius } ) self:T( CargoUnit ) self.CargoObject = CargoUnit @@ -457,10 +489,10 @@ function AI_CARGO_UNIT:New( CargoUnit, Type, Name, Weight, ReportRadius, NearRad return self end ---- AI_CARGO_UNIT Destructor. --- @param #AI_CARGO_UNIT self --- @return #AI_CARGO_UNIT -function AI_CARGO_UNIT:Destroy() +--- CARGO_UNIT Destructor. +-- @param #CARGO_UNIT self +-- @return #CARGO_UNIT +function CARGO_UNIT:Destroy() -- Cargo objects are deleted from the _DATABASE and SET_CARGO objects. _EVENTDISPATCHER:CreateEventDeleteCargo( self ) @@ -469,12 +501,12 @@ function AI_CARGO_UNIT:Destroy() end --- Enter UnBoarding State. --- @param #AI_CARGO_UNIT self +-- @param #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 ) +function CARGO_UNIT:onenterUnBoarding( From, Event, To, ToPointVec2 ) self:F() local Angle = 180 @@ -514,12 +546,12 @@ function AI_CARGO_UNIT:onenterUnBoarding( From, Event, To, ToPointVec2 ) end --- Leave UnBoarding State. --- @param #AI_CARGO_UNIT self +-- @param #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 ) +function CARGO_UNIT:onleaveUnBoarding( From, Event, To, ToPointVec2 ) self:F( { ToPointVec2, From, Event, To } ) local Angle = 180 @@ -538,12 +570,12 @@ function AI_CARGO_UNIT:onleaveUnBoarding( From, Event, To, ToPointVec2 ) end --- UnBoard Event. --- @param #AI_CARGO_UNIT self +-- @param #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 ) +function CARGO_UNIT:onafterUnBoarding( From, Event, To, ToPointVec2 ) self:F( { ToPointVec2, From, Event, To } ) self.CargoInAir = self.CargoObject:InAir() @@ -563,12 +595,12 @@ end --- Enter UnLoaded State. --- @param #AI_CARGO_UNIT self +-- @param #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 ) +function CARGO_UNIT:onenterUnLoaded( From, Event, To, ToPointVec2 ) self:F( { ToPointVec2, From, Event, To } ) local Angle = 180 @@ -601,12 +633,12 @@ end --- Enter Boarding State. --- @param #AI_CARGO_UNIT self +-- @param #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, ... ) +function CARGO_UNIT:onenterBoarding( From, Event, To, CargoCarrier, ... ) self:F( { CargoCarrier.UnitName, From, Event, To } ) local Speed = 10 @@ -633,12 +665,12 @@ function AI_CARGO_UNIT:onenterBoarding( From, Event, To, CargoCarrier, ... ) end --- Leave Boarding State. --- @param #AI_CARGO_UNIT self +-- @param #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, ... ) +function CARGO_UNIT:onleaveBoarding( From, Event, To, CargoCarrier, ... ) self:F( { CargoCarrier.UnitName, From, Event, To } ) if self:IsNear( CargoCarrier:GetPointVec2() ) then @@ -651,12 +683,12 @@ function AI_CARGO_UNIT:onleaveBoarding( From, Event, To, CargoCarrier, ... ) end --- Loaded State. --- @param #AI_CARGO_UNIT self +-- @param #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 ) +function CARGO_UNIT:onenterLoaded( From, Event, To, CargoCarrier ) self:F() self.CargoCarrier = CargoCarrier @@ -670,11 +702,11 @@ end --- Board Event. --- @param #AI_CARGO_UNIT self +-- @param #CARGO_UNIT self -- @param #string Event -- @param #string From -- @param #string To -function AI_CARGO_UNIT:onafterBoard( From, Event, To, CargoCarrier, ... ) +function CARGO_UNIT:onafterBoard( From, Event, To, CargoCarrier, ... ) self:F() self.CargoInAir = self.CargoObject:InAir() @@ -691,25 +723,25 @@ end end -do -- AI_CARGO_PACKAGE +do -- CARGO_PACKAGE - --- @type AI_CARGO_PACKAGE - -- @extends #AI_CARGO_REPRESENTABLE - AI_CARGO_PACKAGE = { - ClassName = "AI_CARGO_PACKAGE" + --- @type CARGO_PACKAGE + -- @extends #CARGO_REPRESENTABLE + CARGO_PACKAGE = { + ClassName = "CARGO_PACKAGE" } ---- AI_CARGO_PACKAGE Constructor. --- @param #AI_CARGO_PACKAGE self +--- CARGO_PACKAGE Constructor. +-- @param #CARGO_PACKAGE self -- @param Wrapper.Unit#UNIT CargoCarrier The UNIT carrying the package. -- @param #string Type -- @param #string Name -- @param #number Weight -- @param #number ReportRadius (optional) -- @param #number NearRadius (optional) --- @return #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 +-- @return #CARGO_PACKAGE +function CARGO_PACKAGE:New( CargoCarrier, Type, Name, Weight, ReportRadius, NearRadius ) + local self = BASE:Inherit( self, CARGO_REPRESENTABLE:New( CargoCarrier, Type, Name, Weight, ReportRadius, NearRadius ) ) -- #CARGO_PACKAGE self:F( { Type, Name, Weight, ReportRadius, NearRadius } ) self:T( CargoCarrier ) @@ -719,7 +751,7 @@ function AI_CARGO_PACKAGE:New( CargoCarrier, Type, Name, Weight, ReportRadius, N end --- Board Event. --- @param #AI_CARGO_PACKAGE self +-- @param #CARGO_PACKAGE self -- @param #string Event -- @param #string From -- @param #string To @@ -727,7 +759,7 @@ end -- @param #number Speed -- @param #number BoardDistance -- @param #number Angle -function AI_CARGO_PACKAGE:onafterOnBoard( From, Event, To, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) +function CARGO_PACKAGE:onafterOnBoard( From, Event, To, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) self:F() self.CargoInAir = self.CargoCarrier:InAir() @@ -757,10 +789,10 @@ function AI_CARGO_PACKAGE:onafterOnBoard( From, Event, To, CargoCarrier, Speed, end --- Check if CargoCarrier is near the Cargo to be Loaded. --- @param #AI_CARGO_PACKAGE self +-- @param #CARGO_PACKAGE self -- @param Wrapper.Unit#UNIT CargoCarrier -- @return #boolean -function AI_CARGO_PACKAGE:IsNear( CargoCarrier ) +function CARGO_PACKAGE:IsNear( CargoCarrier ) self:F() local CargoCarrierPoint = CargoCarrier:GetPointVec2() @@ -776,12 +808,12 @@ function AI_CARGO_PACKAGE:IsNear( CargoCarrier ) end --- Boarded Event. --- @param #AI_CARGO_PACKAGE self +-- @param #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 ) +function CARGO_PACKAGE:onafterOnBoarded( From, Event, To, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) self:F() if self:IsNear( CargoCarrier ) then @@ -792,7 +824,7 @@ function AI_CARGO_PACKAGE:onafterOnBoarded( From, Event, To, CargoCarrier, Speed end --- UnBoard Event. --- @param #AI_CARGO_PACKAGE self +-- @param #CARGO_PACKAGE self -- @param #string Event -- @param #string From -- @param #string To @@ -801,7 +833,7 @@ end -- @param #number UnBoardDistance -- @param #number Radius -- @param #number Angle -function AI_CARGO_PACKAGE:onafterUnBoard( From, Event, To, CargoCarrier, Speed, UnLoadDistance, UnBoardDistance, Radius, Angle ) +function CARGO_PACKAGE:onafterUnBoard( From, Event, To, CargoCarrier, Speed, UnLoadDistance, UnBoardDistance, Radius, Angle ) self:F() self.CargoInAir = self.CargoCarrier:InAir() @@ -834,12 +866,12 @@ function AI_CARGO_PACKAGE:onafterUnBoard( From, Event, To, CargoCarrier, Speed, end --- UnBoarded Event. --- @param #AI_CARGO_PACKAGE self +-- @param #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 ) +function CARGO_PACKAGE:onafterUnBoarded( From, Event, To, CargoCarrier, Speed ) self:F() if self:IsNear( CargoCarrier ) then @@ -850,7 +882,7 @@ function AI_CARGO_PACKAGE:onafterUnBoarded( From, Event, To, CargoCarrier, Speed end --- Load Event. --- @param #AI_CARGO_PACKAGE self +-- @param #CARGO_PACKAGE self -- @param #string Event -- @param #string From -- @param #string To @@ -858,7 +890,7 @@ end -- @param #number Speed -- @param #number LoadDistance -- @param #number Angle -function AI_CARGO_PACKAGE:onafterLoad( From, Event, To, CargoCarrier, Speed, LoadDistance, Angle ) +function CARGO_PACKAGE:onafterLoad( From, Event, To, CargoCarrier, Speed, LoadDistance, Angle ) self:F() self.CargoCarrier = CargoCarrier @@ -878,13 +910,13 @@ function AI_CARGO_PACKAGE:onafterLoad( From, Event, To, CargoCarrier, Speed, Loa end --- UnLoad Event. --- @param #AI_CARGO_PACKAGE self +-- @param #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 ) +function CARGO_PACKAGE:onafterUnLoad( From, Event, To, CargoCarrier, Speed, Distance, Angle ) self:F() local StartPointVec2 = self.CargoCarrier:GetPointVec2() @@ -906,89 +938,65 @@ end end -do -- AI_CARGO_GROUP +do -- 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. + --- @type CARGO_GROUP + -- @extends #CARGO_REPORTABLE - --- # AI\_CARGO\_GROUP class + --- # CARGO\_GROUP class -- - -- The AI\_CARGO\_GROUP 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\_GROUP to and from carrier. + -- The CARGO\_GROUP class defines a cargo that is represented by a @{Group} object within the simulator, and can be transported by a carrier. + -- Use the event functions as described above to Load, UnLoad, Board, UnBoard the CARGO\_GROUP to and from carrier. -- - -- @field #AI_CARGO_GROUP AI_CARGO_GROUP + -- @field #CARGO_GROUP CARGO_GROUP -- - AI_CARGO_GROUP = { - ClassName = "AI_CARGO_GROUP", + CARGO_GROUP = { + ClassName = "CARGO_GROUP", } ---- AI_CARGO_GROUP constructor. --- @param #AI_CARGO_GROUP self --- @param Core.Set#Set_BASE CargoSet +--- CARGO_GROUP constructor. +-- @param #CARGO_GROUP self +-- @param Wrapper.Group#GROUP CargoGroup -- @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 +-- @return #CARGO_GROUP +function CARGO_GROUP:New( CargoGroup, Type, Name, ReportRadius, NearRadius ) + local self = BASE:Inherit( self, CARGO_REPORTABLE:New( Type, Name, 0, ReportRadius, NearRadius ) ) -- #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 + self.CargoSet = SET_CARGO:New() - --- # 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. - -- - -- @field #AI_CARGO_GROUPED AI_CARGO_GROUPED - -- - 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 } ) + local WeightGroup = 0 + + for UnitID, UnitData in pairs( CargoGroup:GetUnits() ) do + local Unit = UnitData -- Wrapper.Unit#UNIT + local WeightUnit = Unit:GetDesc().massEmpty + WeightGroup = WeightGroup + WeightUnit + local CargoUnit = CARGO_UNIT:New( Unit, Type, Unit:GetName(), WeightUnit ) + self.CargoSet:Add( CargoUnit:GetName(), CargoUnit ) + end + self:SetWeight( WeightGroup ) + + self:T( { "Weight Cargo", WeightGroup } ) + return self end --- Enter Boarding State. --- @param #AI_CARGO_GROUPED self +-- @param #CARGO_GROUP self -- @param Wrapper.Unit#UNIT CargoCarrier -- @param #string Event -- @param #string From -- @param #string To -function AI_CARGO_GROUPED:onenterBoarding( From, Event, To, CargoCarrier ) +function CARGO_GROUP: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 + -- For each Cargo object within the CARGO_GROUPED, route each object to the CargoLoadPointVec2 self.CargoSet:ForEach( function( Cargo ) Cargo:__Board( 1, CargoCarrier ) @@ -1001,16 +1009,16 @@ function AI_CARGO_GROUPED:onenterBoarding( From, Event, To, CargoCarrier ) end --- Enter Loaded State. --- @param #AI_CARGO_GROUPED self +-- @param #CARGO_GROUP self -- @param Wrapper.Unit#UNIT CargoCarrier -- @param #string Event -- @param #string From -- @param #string To -function AI_CARGO_GROUPED:onenterLoaded( From, Event, To, CargoCarrier ) +function CARGO_GROUP: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 each Cargo object within the CARGO_GROUP, load each cargo to the CargoCarrier. for CargoID, Cargo in pairs( self.CargoSet:GetSet() ) do Cargo:Load( CargoCarrier ) end @@ -1018,17 +1026,17 @@ function AI_CARGO_GROUPED:onenterLoaded( From, Event, To, CargoCarrier ) end --- Leave Boarding State. --- @param #AI_CARGO_GROUPED self +-- @param #CARGO_GROUP self -- @param Wrapper.Unit#UNIT CargoCarrier -- @param #string Event -- @param #string From -- @param #string To -function AI_CARGO_GROUPED:onleaveBoarding( From, Event, To, CargoCarrier ) +function CARGO_GROUP: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 each Cargo object within the CARGO_GROUP, route each object to the CargoLoadPointVec2 for CargoID, Cargo in pairs( self.CargoSet:GetSet() ) do self:T( Cargo.current ) if not Cargo:is( "Loaded" ) then @@ -1045,19 +1053,19 @@ function AI_CARGO_GROUPED:onleaveBoarding( From, Event, To, CargoCarrier ) end --- Enter UnBoarding State. --- @param #AI_CARGO_GROUPED self +-- @param #CARGO_GROUP 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 ) +function CARGO_GROUP: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 + -- For each Cargo object within the CARGO_GROUP, route each object to the CargoLoadPointVec2 self.CargoSet:ForEach( function( Cargo ) Cargo:__UnBoard( Timer, ToPointVec2 ) @@ -1071,12 +1079,12 @@ function AI_CARGO_GROUPED:onenterUnBoarding( From, Event, To, ToPointVec2 ) end --- Leave UnBoarding State. --- @param #AI_CARGO_GROUPED self +-- @param #CARGO_GROUP 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 ) +function CARGO_GROUP:onleaveUnBoarding( From, Event, To, ToPointVec2 ) self:F( { ToPointVec2, From, Event, To } ) local Angle = 180 @@ -1086,7 +1094,7 @@ function AI_CARGO_GROUPED:onleaveUnBoarding( From, Event, To, ToPointVec2 ) if From == "UnBoarding" then local UnBoarded = true - -- For each Cargo object within the AI_CARGO_GROUPED, route each object to the CargoLoadPointVec2 + -- For each Cargo object within the CARGO_GROUP, route each object to the CargoLoadPointVec2 for CargoID, Cargo in pairs( self.CargoSet:GetSet() ) do self:T( Cargo.current ) if not Cargo:is( "UnLoaded" ) then @@ -1106,12 +1114,12 @@ function AI_CARGO_GROUPED:onleaveUnBoarding( From, Event, To, ToPointVec2 ) end --- UnBoard Event. --- @param #AI_CARGO_GROUPED self +-- @param #CARGO_GROUP 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 ) +function CARGO_GROUP:onafterUnBoarding( From, Event, To, ToPointVec2 ) self:F( { ToPointVec2, From, Event, To } ) self:__UnLoad( 1, ToPointVec2 ) @@ -1120,17 +1128,17 @@ end --- Enter UnLoaded State. --- @param #AI_CARGO_GROUPED self +-- @param #CARGO_GROUP self -- @param Core.Point#POINT_VEC2 -- @param #string Event -- @param #string From -- @param #string To -function AI_CARGO_GROUPED:onenterUnLoaded( From, Event, To, ToPointVec2 ) +function CARGO_GROUP: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 + -- For each Cargo object within the CARGO_GROUP, route each object to the CargoLoadPointVec2 self.CargoSet:ForEach( function( Cargo ) Cargo:UnLoad( ToPointVec2 ) @@ -1141,7 +1149,5 @@ function AI_CARGO_GROUPED:onenterUnLoaded( From, Event, To, ToPointVec2 ) end -end -- AI_CARGO_GROUPED - - +end -- CARGO_GROUP diff --git a/Moose Mission Setup/Moose.files b/Moose Mission Setup/Moose.files index b8084a749..e477fca81 100644 --- a/Moose Mission Setup/Moose.files +++ b/Moose Mission Setup/Moose.files @@ -14,6 +14,7 @@ Core/Message.lua Core/Fsm.lua Core/Radio.lua Core/SpawnStatic.lua +Core/Cargo.lua Wrapper/Object.lua Wrapper/Identifiable.lua @@ -40,7 +41,6 @@ AI/AI_Balancer.lua AI/AI_Patrol.lua AI/AI_Cap.lua AI/AI_Cas.lua -AI/AI_Cargo.lua Actions/Act_Assign.lua Actions/Act_Route.lua diff --git a/Moose Mission Setup/Moose.lua b/Moose Mission Setup/Moose.lua index 7cce9cff2..e922bc157 100644 --- a/Moose Mission Setup/Moose.lua +++ b/Moose Mission Setup/Moose.lua @@ -1,5 +1,5 @@ env.info( '*** MOOSE DYNAMIC INCLUDE START *** ' ) -env.info( 'Moose Generation Timestamp: 20170412_1738' ) +env.info( 'Moose Generation Timestamp: 20170413_0842' ) local base = _G @@ -36,6 +36,7 @@ __Moose.Include( 'Core/Message.lua' ) __Moose.Include( 'Core/Fsm.lua' ) __Moose.Include( 'Core/Radio.lua' ) __Moose.Include( 'Core/SpawnStatic.lua' ) +__Moose.Include( 'Core/Cargo.lua' ) __Moose.Include( 'Wrapper/Object.lua' ) __Moose.Include( 'Wrapper/Identifiable.lua' ) __Moose.Include( 'Wrapper/Positionable.lua' ) @@ -59,7 +60,6 @@ __Moose.Include( 'AI/AI_Balancer.lua' ) __Moose.Include( 'AI/AI_Patrol.lua' ) __Moose.Include( 'AI/AI_Cap.lua' ) __Moose.Include( 'AI/AI_Cas.lua' ) -__Moose.Include( 'AI/AI_Cargo.lua' ) __Moose.Include( 'Actions/Act_Assign.lua' ) __Moose.Include( 'Actions/Act_Route.lua' ) __Moose.Include( 'Actions/Act_Account.lua' ) From 1a5a74120bc6eab708786a1cdcfec8f80ad4f0f7 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Thu, 13 Apr 2017 09:42:22 +0200 Subject: [PATCH 10/14] Progress --- Moose Development/Moose/Core/Cargo.lua | 45 +- Moose Development/Moose/Moose.lua | 69 +- .../Moose/Tasking/Task_CARGO.lua | 2 +- Moose Mission Setup/Moose.files | 1 + Moose Mission Setup/Moose.lua | 3 +- docs/Documentation/Cargo.html | 1225 +++++++++-------- docs/Documentation/Fsm.html | 3 +- docs/Documentation/MOVEMENT.html | 4 - docs/Documentation/Point.html | 1 + docs/Documentation/Spawn.html | 13 +- docs/Documentation/index.html | 13 +- 11 files changed, 677 insertions(+), 702 deletions(-) diff --git a/Moose Development/Moose/Core/Cargo.lua b/Moose Development/Moose/Core/Cargo.lua index 59b77675c..c00740e68 100644 --- a/Moose Development/Moose/Core/Cargo.lua +++ b/Moose Development/Moose/Core/Cargo.lua @@ -27,6 +27,7 @@ -- @function [parent=#CARGO] Board -- @param #CARGO self -- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. +-- @param #number NearRadius The radius when the cargo will board the Carrier (to avoid collision). --- Boards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo to the Carrier. -- The cargo must be in the **UnLoaded** state. @@ -34,6 +35,7 @@ -- @param #CARGO self -- @param #number DelaySeconds The amount of seconds to delay the action. -- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. +-- @param #number NearRadius The radius when the cargo will board the Carrier (to avoid collision). -- UnBoard @@ -117,6 +119,7 @@ --- @function [parent=#CARGO] OnEnterBoarding -- @param #CARGO self -- @param Wrapper.Controllable#CONTROLLABLE Controllable +-- @param #number NearRadius The radius when the cargo will board the Carrier (to avoid collision). -- UnBoarding @@ -215,10 +218,10 @@ do -- CARGO -- @param #number Weight -- @param #number NearRadius (optional) -- @return #CARGO -function CARGO:New( Type, Name, Weight, NearRadius ) +function CARGO:New( Type, Name, Weight ) local self = BASE:Inherit( self, FSM:New() ) -- Core.Fsm#FSM - self:F( { Type, Name, Weight, NearRadius } ) + self:F( { Type, Name, Weight } ) self:SetStartState( "UnLoaded" ) self:AddTransition( "UnLoaded", "Board", "Boarding" ) @@ -234,7 +237,6 @@ function CARGO:New( Type, Name, Weight, NearRadius ) self.Type = Type self.Name = Name self.Weight = Weight - self.NearRadius = NearRadius or 200 self.CargoObject = nil self.CargoCarrier = nil self.Representable = false @@ -311,14 +313,15 @@ end --- Check if CargoCarrier is near the Cargo to be Loaded. -- @param #CARGO self -- @param Core.Point#POINT_VEC2 PointVec2 +-- @param #number NearRadius The radius when the cargo will board the Carrier (to avoid collision). -- @return #boolean -function CARGO:IsNear( PointVec2 ) +function CARGO:IsNear( PointVec2, NearRadius ) self:F( { PointVec2 } ) local Distance = PointVec2:DistanceFromPointVec2( self.CargoObject:GetPointVec2() ) self:T( Distance ) - if Distance <= self.NearRadius then + if Distance <= NearRadius then return true else return false @@ -407,11 +410,12 @@ do -- CARGO_REPORTABLE -- @param #number ReportRadius (optional) -- @param #number NearRadius (optional) -- @return #CARGO_REPORTABLE -function CARGO_REPORTABLE:New( Type, Name, Weight, ReportRadius, NearRadius ) - local self = BASE:Inherit( self, CARGO:New( Type, Name, Weight, NearRadius ) ) -- #CARGO_REPORTABLE - self:F( { Type, Name, Weight, ReportRadius, NearRadius } ) +function CARGO_REPORTABLE:New( CargoObject, Type, Name, Weight, ReportRadius ) + local self = BASE:Inherit( self, CARGO:New( Type, Name, Weight ) ) -- #CARGO_REPORTABLE + self:F( { Type, Name, Weight, ReportRadius } ) self.ReportRadius = ReportRadius or 1000 + self.CargoObject = CargoObject return self end @@ -483,8 +487,6 @@ function CARGO_UNIT:New( CargoUnit, Type, Name, Weight, NearRadius ) self:T( self.ClassName ) - -- Cargo objects are added to the _DATABASE and SET_CARGO objects. - _EVENTDISPATCHER:CreateEventNewCargo( self ) return self end @@ -638,12 +640,14 @@ end -- @param #string From -- @param #string To -- @param Wrapper.Unit#UNIT CargoCarrier -function CARGO_UNIT:onenterBoarding( From, Event, To, CargoCarrier, ... ) - self:F( { CargoCarrier.UnitName, From, Event, To } ) +function CARGO_UNIT:onenterBoarding( From, Event, To, CargoCarrier, NearRadius, ... ) + self:F( { From, Event, To, CargoCarrier.UnitName, NearRadius } ) local Speed = 10 local Angle = 180 local Distance = 5 + + NearRadius = NearRadius or 25 if From == "UnLoaded" then local CargoCarrierPointVec2 = CargoCarrier:GetPointVec2() @@ -670,10 +674,12 @@ end -- @param #string From -- @param #string To -- @param Wrapper.Unit#UNIT CargoCarrier -function CARGO_UNIT:onleaveBoarding( From, Event, To, CargoCarrier, ... ) - self:F( { CargoCarrier.UnitName, From, Event, To } ) +function CARGO_UNIT:onleaveBoarding( From, Event, To, CargoCarrier, NearRadius, ... ) + self:F( { From, Event, To, CargoCarrier.UnitName, NearRadius } ) - if self:IsNear( CargoCarrier:GetPointVec2() ) then + NearRadius = NearRadius or 25 + + if self:IsNear( CargoCarrier:GetPointVec2(), NearRadius ) then self:__Load( 1, CargoCarrier, ... ) return true else @@ -706,9 +712,11 @@ end -- @param #string Event -- @param #string From -- @param #string To -function CARGO_UNIT:onafterBoard( From, Event, To, CargoCarrier, ... ) +function CARGO_UNIT:onafterBoard( From, Event, To, CargoCarrier, NearRadius, ... ) self:F() + NearRadius = NearRadius or 25 + self.CargoInAir = self.CargoObject:InAir() self:T( self.CargoInAir ) @@ -963,7 +971,7 @@ do -- CARGO_GROUP -- @param #number NearRadius (optional) -- @return #CARGO_GROUP function CARGO_GROUP:New( CargoGroup, Type, Name, ReportRadius, NearRadius ) - local self = BASE:Inherit( self, CARGO_REPORTABLE:New( Type, Name, 0, ReportRadius, NearRadius ) ) -- #CARGO_GROUP + local self = BASE:Inherit( self, CARGO_REPORTABLE:New( CargoGroup, Type, Name, 0, ReportRadius, NearRadius ) ) -- #CARGO_GROUP self:F( { Type, Name, ReportRadius, NearRadius } ) self.CargoSet = SET_CARGO:New() @@ -981,6 +989,9 @@ function CARGO_GROUP:New( CargoGroup, Type, Name, ReportRadius, NearRadius ) self:SetWeight( WeightGroup ) self:T( { "Weight Cargo", WeightGroup } ) + + -- Cargo objects are added to the _DATABASE and SET_CARGO objects. + _EVENTDISPATCHER:CreateEventNewCargo( self ) return self end diff --git a/Moose Development/Moose/Moose.lua b/Moose Development/Moose/Moose.lua index 5195ff310..cca24bef8 100644 --- a/Moose Development/Moose/Moose.lua +++ b/Moose Development/Moose/Moose.lua @@ -1,70 +1,3 @@ ---- The main include file for the MOOSE system. --- Test of permissions - ---- 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" ) -Include.File( "Core/Radio" ) - ---- Wrapper Classes -Include.File( "Wrapper/Object" ) -Include.File( "Wrapper/Identifiable" ) -Include.File( "Wrapper/Positionable" ) -Include.File( "Wrapper/Controllable" ) -Include.File( "Wrapper/Group" ) -Include.File( "Wrapper/Unit" ) -Include.File( "Wrapper/Client" ) -Include.File( "Wrapper/Static" ) -Include.File( "Wrapper/Airbase" ) -Include.File( "Wrapper/Scenery" ) - ---- Functional Classes -Include.File( "Functional/Scoring" ) -Include.File( "Functional/CleanUp" ) -Include.File( "Functional/Spawn" ) -Include.File( "Functional/Movement" ) -Include.File( "Functional/Sead" ) -Include.File( "Functional/Escort" ) -Include.File( "Functional/MissileTrainer" ) -Include.File( "Functional/AirbasePolice" ) -Include.File( "Functional/Detection" ) - ---- AI Classes -Include.File( "AI/AI_Balancer" ) -Include.File( "AI/AI_Patrol" ) -Include.File( "AI/AI_Cap" ) -Include.File( "AI/AI_Cas" ) -Include.File( "AI/AI_Cargo" ) - ---- Actions -Include.File( "Actions/Act_Assign" ) -Include.File( "Actions/Act_Route" ) -Include.File( "Actions/Act_Account" ) -Include.File( "Actions/Act_Assist" ) - ---- Task Handling Classes -Include.File( "Tasking/CommandCenter" ) -Include.File( "Tasking/Mission" ) -Include.File( "Tasking/Task" ) -Include.File( "Tasking/DetectionManager" ) -Include.File( "Tasking/Task_A2G_Dispatcher") -Include.File( "Tasking/Task_A2G" ) -Include.File( "Tasking/Task_CARGO" ) - - -- The order of the declarations is important here. Don't touch it. --- Declare the event dispatcher based on the EVENT class @@ -74,7 +7,7 @@ _EVENTDISPATCHER = EVENT:New() -- Core.Event#EVENT _SCHEDULEDISPATCHER = SCHEDULEDISPATCHER:New() -- Core.Timer#SCHEDULEDISPATCHER --- Declare the main database object, which is used internally by the MOOSE classes. -_DATABASE = DATABASE:New() -- Core.Database#DATABASE +_DATABASE = DATABASE:New() -- Database#DATABASE diff --git a/Moose Development/Moose/Tasking/Task_CARGO.lua b/Moose Development/Moose/Tasking/Task_CARGO.lua index 44d3523d9..f6361a777 100644 --- a/Moose Development/Moose/Tasking/Task_CARGO.lua +++ b/Moose Development/Moose/Tasking/Task_CARGO.lua @@ -130,7 +130,7 @@ do -- TASK_CARGO Task.SetCargo:ForEachCargo( - --- @param AI.AI_Cargo#AI_CARGO Cargo + --- @param Core.Cargo#CARGO Cargo function( Cargo ) if Cargo:IsUnLoaded() then if Cargo:IsInRadius( TaskUnit:GetPointVec2() ) then diff --git a/Moose Mission Setup/Moose.files b/Moose Mission Setup/Moose.files index e477fca81..57f9bca5a 100644 --- a/Moose Mission Setup/Moose.files +++ b/Moose Mission Setup/Moose.files @@ -53,5 +53,6 @@ Tasking/Task.lua Tasking/DetectionManager.lua Tasking/Task_A2G_Dispatcher.lua Tasking/Task_A2G.lua +Tasking/Task_Cargo.lua Moose.lua diff --git a/Moose Mission Setup/Moose.lua b/Moose Mission Setup/Moose.lua index e922bc157..688d42a41 100644 --- a/Moose Mission Setup/Moose.lua +++ b/Moose Mission Setup/Moose.lua @@ -1,5 +1,5 @@ env.info( '*** MOOSE DYNAMIC INCLUDE START *** ' ) -env.info( 'Moose Generation Timestamp: 20170413_0842' ) +env.info( 'Moose Generation Timestamp: 20170413_0920' ) local base = _G @@ -70,6 +70,7 @@ __Moose.Include( 'Tasking/Task.lua' ) __Moose.Include( 'Tasking/DetectionManager.lua' ) __Moose.Include( 'Tasking/Task_A2G_Dispatcher.lua' ) __Moose.Include( 'Tasking/Task_A2G.lua' ) +__Moose.Include( 'Tasking/Task_Cargo.lua' ) __Moose.Include( 'Moose.lua' ) BASE:TraceOnOff( true ) env.info( '*** MOOSE INCLUDE END *** ' ) diff --git a/docs/Documentation/Cargo.html b/docs/Documentation/Cargo.html index 07f90f7bb..8feac7d7c 100644 --- a/docs/Documentation/Cargo.html +++ b/docs/Documentation/Cargo.html @@ -93,568 +93,565 @@

    Module Cargo

    -

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

    +

    Core -- Management of CARGO logistics, that can be transported from and to transportation carriers.

    -

    Banner Image

    + + +

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

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

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

    • +
    • CARGOGROUP, represented by a Group of CARGOUNITs.

    - -

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

    This module is still under construction, but is described above works already, and will keep working ...

    Global(s)

    - + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + +
    AI_CARGOCARGO -

    AI_CARGO class, extends Fsm#FSM_PROCESS

    +

    CARGO class, extends Fsm#FSM_PROCESS

    -

    The AI_CARGO class defines the core functions that defines a cargo object within MOOSE.

    -
    AI_CARGO_GROUP -

    AI_CARGO_GROUP class

    - -

    The AI_CARGO_GROUP class defines a cargo that is represented by a group of Unit objects within the simulator, and can be transported by a carrier.

    -
    AI_CARGO_GROUPED -

    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.

    -
    AI_CARGO_PACKAGE - -
    AI_CARGO_REPRESENTABLE - -
    AI_CARGO_UNIT -

    AI_CARGO_UNIT class, extends #AICARGOREPRESENTABLE

    - -

    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.

    +

    The CARGO class defines the core functions that defines a cargo object within MOOSE.

    CARGOS +
    CARGO_GROUP +

    CARGO_GROUP class

    + +

    The CARGO_GROUP class defines a cargo that is represented by a Group object within the simulator, and can be transported by a carrier.

    +
    CARGO_PACKAGE + +
    CARGO_REPORTABLE + +
    CARGO_REPRESENTABLE + +
    CARGO_UNIT +

    CARGO_UNIT class, extends #CARGO_REPRESENTABLE

    + +

    The CARGO_UNIT class defines a cargo that is represented by a UNIT object within the simulator, and can be transported by a carrier.

    -

    Type AI_CARGO

    +

    Type CARGO

    - + - + - + - + - + - + - + - + - - - - - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - - + - + + + + + - + - + - + - + - + - + - + - + - +
    AI_CARGO:Board(ToCarrier)CARGO:Board(ToCarrier, NearRadius)

    Boards the cargo to a Carrier.

    AI_CARGO.CargoCarrierCARGO.CargoCarrier

    The alive DCS object carrying the cargo. This value can be nil, meaning, that the cargo is not contained anywhere...

    AI_CARGO.CargoObjectCARGO.CargoObject

    The alive DCS object representing the cargo. This value can be nil, meaning, that the cargo is not represented anywhere...

    AI_CARGO.ContainableCARGO.Containable

    This flag defines if the cargo can be contained within a DCS Unit.

    AI_CARGO:GetBoardingRange()CARGO:GetBoardingRange()

    Get the range till cargo will board.

    AI_CARGO:GetName()CARGO:GetName()

    Get the name of the Cargo.

    AI_CARGO:GetPointVec2()CARGO:GetPointVec2()

    Get the current PointVec2 of the cargo.

    AI_CARGO:GetType()CARGO:GetType()

    Get the type of the Cargo.

    AI_CARGO:IsInRadius(PointVec2) -

    Check if CargoCarrier is in the radius for the Cargo to be Loaded.

    -
    AI_CARGO:IsInZone(Zone)CARGO:IsInZone(Zone)

    Check if Cargo is the given Zone.

    AI_CARGO:IsLoaded()CARGO:IsLoaded()

    Check if cargo is loaded.

    AI_CARGO:IsNear(PointVec2)CARGO:IsNear(PointVec2, NearRadius)

    Check if CargoCarrier is near the Cargo to be Loaded.

    AI_CARGO:IsUnLoaded()CARGO:IsUnLoaded()

    Check if cargo is unloaded.

    AI_CARGO:Load(ToCarrier)CARGO:Load(ToCarrier)

    Loads the cargo to a Carrier.

    AI_CARGO.MoveableCARGO.Moveable

    This flag defines if the cargo is moveable.

    AI_CARGO.NameCARGO.Name

    A string defining the name of the cargo. The name is the unique identifier of the cargo.

    AI_CARGO.NearRadiusCARGO.NearRadius

    (optional) A number defining the radius in meters when the cargo is near to a Carrier, so that it can be loaded.

    AI_CARGO:New(Type, Name, Weight, ReportRadius, NearRadius)CARGO:New(Type, Name, Weight, NearRadius) -

    AI_CARGO Constructor.

    +

    CARGO Constructor.

    AI_CARGO:OnEnterBoarding(Controllable)CARGO:OnEnterBoarding(Controllable, NearRadius)
    AI_CARGO:OnEnterLoaded(Controllable)CARGO:OnEnterLoaded(Controllable)
    AI_CARGO:OnEnterUnBoarding(Controllable)CARGO:OnEnterUnBoarding(Controllable)
    AI_CARGO:OnEnterUnLoaded(Controllable)CARGO:OnEnterUnLoaded(Controllable)
    AI_CARGO:OnLeaveBoarding(Controllable)CARGO:OnLeaveBoarding(Controllable)
    AI_CARGO:OnLeaveLoaded(Controllable)CARGO:OnLeaveLoaded(Controllable)
    AI_CARGO:OnLeaveUnBoarding(Controllable)CARGO:OnLeaveUnBoarding(Controllable)
    AI_CARGO:OnLeaveUnLoaded(Controllable)CARGO:OnLeaveUnLoaded(Controllable)
    AI_CARGO.ReportRadius -

    (optional) A number defining the radius in meters when the cargo is signalling or reporting to a Carrier.

    -
    AI_CARGO.RepresentableCARGO.Representable

    This flag defines if the cargo can be represented by a DCS Unit.

    AI_CARGO.SlingloadableCARGO:SetWeight(Weight) +

    Set the weight of the cargo.

    +
    CARGO.Slingloadable

    This flag defines if the cargo can be slingloaded.

    AI_CARGO:Spawn(PointVec2)CARGO:Spawn(PointVec2) -

    Template method to spawn a new representation of the AI_CARGO in the simulator.

    +

    Template method to spawn a new representation of the CARGO in the simulator.

    AI_CARGO.TypeCARGO.Type

    A string defining the type of the cargo. eg. Engineers, Equipment, Screwdrivers.

    AI_CARGO:UnBoard(ToPointVec2)CARGO:UnBoard(ToPointVec2)

    UnBoards the cargo to a Carrier.

    AI_CARGO:UnLoad(ToPointVec2)CARGO:UnLoad(ToPointVec2)

    UnLoads the cargo to a Carrier.

    AI_CARGO.WeightCARGO.Weight

    A number defining the weight of the cargo. The weight is expressed in kg.

    AI_CARGO:__Board(DelaySeconds, ToCarrier)CARGO:__Board(DelaySeconds, ToCarrier, NearRadius)

    Boards the cargo to a Carrier.

    AI_CARGO:__Load(DelaySeconds, ToCarrier)CARGO:__Load(DelaySeconds, ToCarrier)

    Loads the cargo to a Carrier.

    AI_CARGO:__UnBoard(DelaySeconds, ToPointVec2)CARGO:__UnBoard(DelaySeconds, ToPointVec2)

    UnBoards the cargo to a Carrier.

    AI_CARGO:__UnLoad(DelaySeconds, ToPointVec2)CARGO:__UnLoad(DelaySeconds, ToPointVec2)

    UnLoads the cargo to a Carrier.

    -

    Type AI_CARGO_GROUP

    +

    Type CARGO_GROUP

    - + - - - - - - - - - -
    AI_CARGO_GROUP.CargoSetCARGO_GROUP.CargoSet -

    A set of cargo objects.

    -
    AI_CARGO_GROUP.Name -

    A string defining the name of the cargo group. The name is the unique identifier of the cargo.

    -
    AI_CARGO_GROUP:New(CargoSet, Type, Name, Weight, ReportRadius, NearRadius) -

    AICARGOGROUP constructor.

    -
    -

    Type AI_CARGO_GROUPED

    - - - - - + + + + + - + - + - + - + - + - +
    AI_CARGO_GROUPED:New(CargoSet, Type, Name, Weight, ReportRadius, NearRadius) -

    AICARGOGROUPED constructor.

    AI_CARGO_GROUPED:onafterUnBoarding(ToPointVec2, Event, From, To)CARGO_GROUP:New(CargoGroup, Type, Name, ReportRadius, NearRadius) +

    CARGO_GROUP constructor.

    +
    CARGO_GROUP:onafterUnBoarding(ToPointVec2, Event, From, To)

    UnBoard Event.

    AI_CARGO_GROUPED:onenterBoarding(CargoCarrier, Event, From, To)CARGO_GROUP:onenterBoarding(CargoCarrier, Event, From, To)

    Enter Boarding State.

    AI_CARGO_GROUPED:onenterLoaded(CargoCarrier, Event, From, To)CARGO_GROUP:onenterLoaded(CargoCarrier, Event, From, To)

    Enter Loaded State.

    AI_CARGO_GROUPED:onenterUnBoarding(ToPointVec2, Event, From, To)CARGO_GROUP:onenterUnBoarding(ToPointVec2, Event, From, To)

    Enter UnBoarding State.

    AI_CARGO_GROUPED:onenterUnLoaded(Core, Event, From, To, ToPointVec2)CARGO_GROUP:onenterUnLoaded(Core, Event, From, To, ToPointVec2)

    Enter UnLoaded State.

    AI_CARGO_GROUPED:onleaveBoarding(CargoCarrier, Event, From, To)CARGO_GROUP:onleaveBoarding(CargoCarrier, Event, From, To)

    Leave Boarding State.

    AI_CARGO_GROUPED:onleaveUnBoarding(ToPointVec2, Event, From, To)CARGO_GROUP:onleaveUnBoarding(ToPointVec2, Event, From, To)

    Leave UnBoarding State.

    -

    Type AI_CARGO_PACKAGE

    +

    Type CARGO_PACKAGE

    - + - + - + - + - + - + - + - + - + - + - +
    AI_CARGO_PACKAGE.CargoCarrierCARGO_PACKAGE.CargoCarrier
    AI_CARGO_PACKAGE.CargoInAirCARGO_PACKAGE.CargoInAir
    AI_CARGO_PACKAGE.ClassNameCARGO_PACKAGE.ClassName
    AI_CARGO_PACKAGE:IsNear(CargoCarrier)CARGO_PACKAGE:IsNear(CargoCarrier)

    Check if CargoCarrier is near the Cargo to be Loaded.

    AI_CARGO_PACKAGE:New(CargoCarrier, Type, Name, Weight, ReportRadius, NearRadius)CARGO_PACKAGE:New(CargoCarrier, Type, Name, Weight, ReportRadius, NearRadius) -

    AICARGOPACKAGE Constructor.

    +

    CARGO_PACKAGE Constructor.

    AI_CARGO_PACKAGE:onafterLoad(Event, From, To, CargoCarrier, Speed, LoadDistance, Angle)CARGO_PACKAGE:onafterLoad(Event, From, To, CargoCarrier, Speed, LoadDistance, Angle)

    Load Event.

    AI_CARGO_PACKAGE:onafterOnBoard(Event, From, To, CargoCarrier, Speed, BoardDistance, Angle, LoadDistance)CARGO_PACKAGE:onafterOnBoard(Event, From, To, CargoCarrier, Speed, BoardDistance, Angle, LoadDistance)

    Board Event.

    AI_CARGO_PACKAGE:onafterOnBoarded(Event, From, To, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle)CARGO_PACKAGE:onafterOnBoarded(Event, From, To, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle)

    Boarded Event.

    AI_CARGO_PACKAGE:onafterUnBoard(Event, From, To, Speed, UnLoadDistance, UnBoardDistance, Radius, Angle, CargoCarrier)CARGO_PACKAGE:onafterUnBoard(Event, From, To, Speed, UnLoadDistance, UnBoardDistance, Radius, Angle, CargoCarrier)

    UnBoard Event.

    AI_CARGO_PACKAGE:onafterUnBoarded(Event, From, To, CargoCarrier, Speed)CARGO_PACKAGE:onafterUnBoarded(Event, From, To, CargoCarrier, Speed)

    UnBoarded Event.

    AI_CARGO_PACKAGE:onafterUnLoad(Event, From, To, Distance, Angle, CargoCarrier, Speed)CARGO_PACKAGE:onafterUnLoad(Event, From, To, Distance, Angle, CargoCarrier, Speed)

    UnLoad Event.

    -

    Type AI_CARGO_REPRESENTABLE

    +

    Type CARGO_REPORTABLE

    - + - + - + + + + + + + +
    AI_CARGO_REPRESENTABLE.ClassNameCARGO_REPORTABLE.ClassName
    AI_CARGO_REPRESENTABLE:New(CargoObject, Type, Name, Weight, ReportRadius, NearRadius)CARGO_REPORTABLE:IsInRadius(PointVec2) -

    AICARGOREPRESENTABLE Constructor.

    +

    Check if CargoCarrier is in the ReportRadius for the Cargo to be Loaded.

    AI_CARGO_REPRESENTABLE:RouteTo(ToPointVec2, Speed)CARGO_REPORTABLE:New(CargoObject, Type, Name, Weight, ReportRadius, NearRadius) +

    CARGO_REPORTABLE Constructor.

    +
    CARGO_REPORTABLE.ReportRadius + +
    + +

    Type CARGO_REPRESENTABLE

    + + + + + + + + + + +
    CARGO_REPRESENTABLE.ClassName + +
    CARGO_REPRESENTABLE:New(CargoObject, Type, Name, Weight, ReportRadius, NearRadius) +

    CARGO_REPRESENTABLE Constructor.

    +
    CARGO_REPRESENTABLE:RouteTo(ToPointVec2, Speed)

    Route a cargo unit to a PointVec2.

    -

    Type AI_CARGO_UNIT

    +

    Type CARGO_UNIT

    - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -665,34 +662,34 @@
    - #AI_CARGO - -AI_CARGO + #CARGO + +CARGO
    -

    AI_CARGO class, extends Fsm#FSM_PROCESS

    +

    CARGO class, extends Fsm#FSM_PROCESS

    -

    The AI_CARGO class defines the core functions that defines a cargo object within MOOSE.

    +

    The CARGO class defines the core functions that defines a cargo object within MOOSE.

    A cargo is a logical object defined that is available for transport, and has a life status within a simulation.

    -

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

    +

    The CARGO is a state machine: it manages the different events and states of the cargo. +All derived classes from CARGO follow the same state machine, expose the same cargo event functions, and provide the same cargo states.

    -

    AI_CARGO Events:

    +

    CARGO Events:

      -
    • #AI( ToCarrier ): Boards the cargo to a carrier.
    • -
    • #AI( ToCarrier ): Loads the cargo into a carrier, regardless of its position.
    • -
    • #AI( ToPointVec2 ): UnBoard the cargo from a carrier. This will trigger a movement of the cargo to the option ToPointVec2.
    • -
    • #AI( ToPointVec2 ): UnLoads the cargo from a carrier.
    • -
    • #AI( Controllable ): The cargo is dead. The cargo process will be ended.
    • +
    • CARGO.Board( ToCarrier ): Boards the cargo to a carrier.
    • +
    • CARGO.Load( ToCarrier ): Loads the cargo into a carrier, regardless of its position.
    • +
    • CARGO.UnBoard( ToPointVec2 ): UnBoard the cargo from a carrier. This will trigger a movement of the cargo to the option ToPointVec2.
    • +
    • CARGO.UnLoad( ToPointVec2 ): UnLoads the cargo from a carrier.
    • +
    • CARGO.Dead( Controllable ): The cargo is dead. The cargo process will be ended.
    -

    AI_CARGO States:

    +

    CARGO States:

    • UnLoaded: The cargo is unloaded from a carrier.
    • @@ -703,7 +700,7 @@ All derived classes from AI_CARGO follow the same state machine, expose the same
    • End: The process has come to an end.
    -

    AI_CARGO state transition methods:

    +

    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:

    @@ -720,96 +717,6 @@ There are 2 moments when state transition methods will be called by the state ma -
    -
    -
    -
    - - #AI_CARGO_GROUP - -AI_CARGO_GROUP - -
    -
    - -

    AI_CARGO_GROUP class

    - -

    The AI_CARGO_GROUP 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_GROUP to and from carrier.

    - - -
    -
    -
    -
    - - #AI_CARGO_GROUPED - -AI_CARGO_GROUPED - -
    -
    - -

    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.

    - - -
    -
    -
    -
    - - #AI_CARGO_PACKAGE - -AI_CARGO_PACKAGE - -
    -
    - - - -
    -
    -
    -
    - - #AI_CARGO_REPRESENTABLE - -AI_CARGO_REPRESENTABLE - -
    -
    - - - -
    -
    -
    -
    - - #AI_CARGO_UNIT - -AI_CARGO_UNIT - -
    -
    - -

    AI_CARGO_UNIT class, extends #AICARGOREPRESENTABLE

    - -

    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.

    - -
    - -
    @@ -824,17 +731,101 @@ There are 2 moments when state transition methods will be called by the state ma + +
    +
    +
    + + #CARGO_GROUP + +CARGO_GROUP + +
    +
    + +

    CARGO_GROUP class

    + +

    The CARGO_GROUP class defines a cargo that is represented by a Group object within the simulator, and can be transported by a carrier.

    + + +

    Use the event functions as described above to Load, UnLoad, Board, UnBoard the CARGO_GROUP to and from carrier.

    + + +
    +
    +
    +
    + + #CARGO_PACKAGE + +CARGO_PACKAGE + +
    +
    + + + +
    +
    +
    +
    + + #CARGO_REPORTABLE + +CARGO_REPORTABLE + +
    +
    + + + +
    +
    +
    +
    + + #CARGO_REPRESENTABLE + +CARGO_REPRESENTABLE + +
    +
    + + + +
    +
    +
    +
    + + #CARGO_UNIT + +CARGO_UNIT + +
    +
    + +

    CARGO_UNIT class, extends #CARGO_REPRESENTABLE

    + +

    The CARGO_UNIT class defines a cargo that is represented by a UNIT object within the simulator, and can be transported by a carrier.

    + + +

    Use the event functions as described above to Load, UnLoad, Board, UnBoard the CARGO_UNIT objects to and from carriers.

    + +
    + +

    Type Cargo

    -

    Type AI_CARGO

    +

    Type CARGO

    Field(s)

    - -AI_CARGO:Board(ToCarrier) + +CARGO:Board(ToCarrier, NearRadius)
    @@ -845,13 +836,19 @@ There are 2 moments when state transition methods will be called by the state ma

    The event will create a movement (= running or driving) of the cargo to the Carrier. The cargo must be in the UnLoaded state.

    -

    Parameter

    +

    Parameters

    • Wrapper.Controllable#CONTROLLABLE ToCarrier : The Carrier that will hold the cargo.

      +
    • +
    • + +

      #number NearRadius : +The radius when the cargo will board the Carrier (to avoid collision).

      +
    @@ -860,8 +857,8 @@ The Carrier that will hold the cargo.

    Wrapper.Controllable#CONTROLLABLE - -AI_CARGO.CargoCarrier + +CARGO.CargoCarrier
    @@ -874,8 +871,8 @@ The Carrier that will hold the cargo.

    Wrapper.Controllable#CONTROLLABLE - -AI_CARGO.CargoObject + +CARGO.CargoObject
    @@ -888,8 +885,8 @@ The Carrier that will hold the cargo.

    #boolean - -AI_CARGO.Containable + +CARGO.Containable
    @@ -901,8 +898,8 @@ The Carrier that will hold the cargo.

    - -AI_CARGO:GetBoardingRange() + +CARGO:GetBoardingRange()
    @@ -919,8 +916,8 @@ The range till cargo will board.

    - -AI_CARGO:GetName() + +CARGO:GetName()
    @@ -937,8 +934,8 @@ The name of the Cargo.

    - -AI_CARGO:GetPointVec2() + +CARGO:GetPointVec2()
    @@ -955,8 +952,8 @@ The name of the Cargo.

    - -AI_CARGO:GetType() + +CARGO:GetType()
    @@ -973,34 +970,8 @@ The type of the Cargo.

    - -AI_CARGO:IsInRadius(PointVec2) - -
    -
    - -

    Check if CargoCarrier is in the radius for the Cargo to be Loaded.

    - -

    Parameter

    - -

    Return value

    - -

    #boolean:

    - - -
    -
    -
    -
    - - -AI_CARGO:IsInZone(Zone) + +CARGO:IsInZone(Zone)
    @@ -1025,8 +996,8 @@ The type of the Cargo.

    - -AI_CARGO:IsLoaded() + +CARGO:IsLoaded()
    @@ -1043,20 +1014,26 @@ true if loaded

    - -AI_CARGO:IsNear(PointVec2) + +CARGO:IsNear(PointVec2, NearRadius)

    Check if CargoCarrier is near the Cargo to be Loaded.

    -

    Parameter

    +

    Parameters

    • Core.Point#POINT_VEC2 PointVec2 :

      +
    • +
    • + +

      #number NearRadius : +The radius when the cargo will board the Carrier (to avoid collision).

      +

    Return value

    @@ -1069,8 +1046,8 @@ true if loaded

    - -AI_CARGO:IsUnLoaded() + +CARGO:IsUnLoaded()
    @@ -1087,8 +1064,8 @@ true if unloaded

    - -AI_CARGO:Load(ToCarrier) + +CARGO:Load(ToCarrier)
    @@ -1114,8 +1091,8 @@ The Carrier that will hold the cargo.

    #boolean - -AI_CARGO.Moveable + +CARGO.Moveable
    @@ -1128,8 +1105,8 @@ The Carrier that will hold the cargo.

    #string - -AI_CARGO.Name + +CARGO.Name
    @@ -1142,8 +1119,8 @@ The Carrier that will hold the cargo.

    #number - -AI_CARGO.NearRadius + +CARGO.NearRadius
    @@ -1155,13 +1132,13 @@ The Carrier that will hold the cargo.

    - -AI_CARGO:New(Type, Name, Weight, ReportRadius, NearRadius) + +CARGO:New(Type, Name, Weight, NearRadius)
    -

    AI_CARGO Constructor.

    +

    CARGO Constructor.

    This class is an abstract class and should not be instantiated.

    @@ -1185,12 +1162,6 @@ The Carrier that will hold the cargo.

  • -

    #number ReportRadius : -(optional)

    - -
  • -
  • -

    #number NearRadius : (optional)

    @@ -1198,7 +1169,7 @@ The Carrier that will hold the cargo.

    Return value

    -

    #AI_CARGO:

    +

    #CARGO:

  • @@ -1206,8 +1177,35 @@ The Carrier that will hold the cargo.

    - -AI_CARGO:OnEnterBoarding(Controllable) + +CARGO:OnEnterBoarding(Controllable, NearRadius) + +
    +
    + + + +

    Parameters

    + +
    +
    +
    +
    + + +CARGO:OnEnterLoaded(Controllable)
    @@ -1227,8 +1225,8 @@ The Carrier that will hold the cargo.

    - -AI_CARGO:OnEnterLoaded(Controllable) + +CARGO:OnEnterUnBoarding(Controllable)
    @@ -1248,8 +1246,8 @@ The Carrier that will hold the cargo.

    - -AI_CARGO:OnEnterUnBoarding(Controllable) + +CARGO:OnEnterUnLoaded(Controllable)
    @@ -1269,29 +1267,8 @@ The Carrier that will hold the cargo.

    - -AI_CARGO:OnEnterUnLoaded(Controllable) - -
    -
    - - - -

    Parameter

    - -
    -
    -
    -
    - - -AI_CARGO:OnLeaveBoarding(Controllable) + +CARGO:OnLeaveBoarding(Controllable)
    @@ -1316,8 +1293,8 @@ The Carrier that will hold the cargo.

    - -AI_CARGO:OnLeaveLoaded(Controllable) + +CARGO:OnLeaveLoaded(Controllable)
    @@ -1342,8 +1319,8 @@ The Carrier that will hold the cargo.

    - -AI_CARGO:OnLeaveUnBoarding(Controllable) + +CARGO:OnLeaveUnBoarding(Controllable)
    @@ -1368,8 +1345,8 @@ The Carrier that will hold the cargo.

    - -AI_CARGO:OnLeaveUnLoaded(Controllable) + +CARGO:OnLeaveUnLoaded(Controllable)
    @@ -1389,42 +1366,55 @@ The Carrier that will hold the cargo.

    #boolean:

    -
    -
    -
    -
    - - #number - -AI_CARGO.ReportRadius - -
    -
    - -

    (optional) A number defining the radius in meters when the cargo is signalling or reporting to a Carrier.

    -
    #boolean - -AI_CARGO.Representable + +CARGO.Representable

    This flag defines if the cargo can be represented by a DCS Unit.

    +
    +
    +
    +
    + + +CARGO:SetWeight(Weight) + +
    +
    + +

    Set the weight of the cargo.

    + +

    Parameter

    +
      +
    • + +

      #number Weight : +The weight in kg.

      + +
    • +
    +

    Return value

    + +

    #CARGO:

    + +
    #boolean - -AI_CARGO.Slingloadable + +CARGO.Slingloadable
    @@ -1436,13 +1426,13 @@ The Carrier that will hold the cargo.

    - -AI_CARGO:Spawn(PointVec2) + +CARGO:Spawn(PointVec2)
    -

    Template method to spawn a new representation of the AI_CARGO in the simulator.

    +

    Template method to spawn a new representation of the CARGO in the simulator.

    Parameter

      @@ -1454,7 +1444,7 @@ The Carrier that will hold the cargo.

    Return value

    -

    #AI_CARGO:

    +

    #CARGO:

    @@ -1463,8 +1453,8 @@ The Carrier that will hold the cargo.

    #string - -AI_CARGO.Type + +CARGO.Type
    @@ -1476,8 +1466,8 @@ The Carrier that will hold the cargo.

    - -AI_CARGO:UnBoard(ToPointVec2) + +CARGO:UnBoard(ToPointVec2)
    @@ -1502,8 +1492,8 @@ The cargo must be in the Loaded state.

    - -AI_CARGO:UnLoad(ToPointVec2) + +CARGO:UnLoad(ToPointVec2)
    @@ -1529,8 +1519,8 @@ The cargo must be in the Loaded state.

    #number - -AI_CARGO.Weight + +CARGO.Weight
    @@ -1542,8 +1532,8 @@ The cargo must be in the Loaded state.

    - -AI_CARGO:__Board(DelaySeconds, ToCarrier) + +CARGO:__Board(DelaySeconds, ToCarrier, NearRadius)
    @@ -1567,6 +1557,12 @@ The amount of seconds to delay the action.

    Wrapper.Controllable#CONTROLLABLE ToCarrier : The Carrier that will hold the cargo.

    + +
  • + +

    #number NearRadius : +The radius when the cargo will board the Carrier (to avoid collision).

    +
  • @@ -1574,8 +1570,8 @@ The Carrier that will hold the cargo.

    - -AI_CARGO:__Load(DelaySeconds, ToCarrier) + +CARGO:__Load(DelaySeconds, ToCarrier)
    @@ -1606,8 +1602,8 @@ The Carrier that will hold the cargo.

    - -AI_CARGO:__UnBoard(DelaySeconds, ToPointVec2) + +CARGO:__UnBoard(DelaySeconds, ToPointVec2)
    @@ -1638,8 +1634,8 @@ The amount of seconds to delay the action.

    - -AI_CARGO:__UnLoad(DelaySeconds, ToPointVec2) + +CARGO:__UnLoad(DelaySeconds, ToPointVec2)
    @@ -1668,54 +1664,40 @@ The amount of seconds to delay the action.

    -

    Type AI_CARGO.CargoObjects

    +

    Type CARGO.CargoObjects

    -

    Type AI_CARGO_GROUP

    +

    Type CARGO_GROUP

    Field(s)

    - Set#SET_BASE - -AI_CARGO_GROUP.CargoSet + + +CARGO_GROUP.CargoSet
    -

    A set of cargo objects.

    +
    - #string - -AI_CARGO_GROUP.Name + +CARGO_GROUP:New(CargoGroup, Type, Name, ReportRadius, NearRadius)
    -

    A string defining the name of the cargo group. The name is the unique identifier of the cargo.

    - -
    -
    -
    -
    - - -AI_CARGO_GROUP:New(CargoSet, Type, Name, Weight, ReportRadius, NearRadius) - -
    -
    - -

    AICARGOGROUP constructor.

    +

    CARGO_GROUP constructor.

    Parameters

    • -

      Core.Set#Set_BASE CargoSet :

      +

      Wrapper.Group#GROUP CargoGroup :

    • @@ -1730,11 +1712,6 @@ The amount of seconds to delay the action.

    • -

      #number Weight :

      - -
    • -
    • -

      #number ReportRadius : (optional)

      @@ -1748,63 +1725,7 @@ The amount of seconds to delay the action.

    Return value

    -

    #AICARGOGROUP:

    - - -
    -
    - -

    Type AI_CARGO_GROUPED

    -

    Field(s)

    -
    -
    - - -AI_CARGO_GROUPED:New(CargoSet, Type, Name, Weight, ReportRadius, NearRadius) - -
    -
    - -

    AICARGOGROUPED constructor.

    - -

    Parameters

    -
      -
    • - -

      Core.Set#Set_BASE CargoSet :

      - -
    • -
    • - -

      #string Type :

      - -
    • -
    • - -

      #string Name :

      - -
    • -
    • - -

      #number Weight :

      - -
    • -
    • - -

      #number ReportRadius : -(optional)

      - -
    • -
    • - -

      #number NearRadius : -(optional)

      - -
    • -
    -

    Return value

    - -

    #AICARGOGROUPED:

    +

    #CARGO_GROUP:

    @@ -1812,8 +1733,8 @@ The amount of seconds to delay the action.

    - -AI_CARGO_GROUPED:onafterUnBoarding(ToPointVec2, Event, From, To) + +CARGO_GROUP:onafterUnBoarding(ToPointVec2, Event, From, To)
    @@ -1848,8 +1769,8 @@ The amount of seconds to delay the action.

    - -AI_CARGO_GROUPED:onenterBoarding(CargoCarrier, Event, From, To) + +CARGO_GROUP:onenterBoarding(CargoCarrier, Event, From, To)
    @@ -1884,8 +1805,8 @@ The amount of seconds to delay the action.

    - -AI_CARGO_GROUPED:onenterLoaded(CargoCarrier, Event, From, To) + +CARGO_GROUP:onenterLoaded(CargoCarrier, Event, From, To)
    @@ -1920,8 +1841,8 @@ The amount of seconds to delay the action.

    - -AI_CARGO_GROUPED:onenterUnBoarding(ToPointVec2, Event, From, To) + +CARGO_GROUP:onenterUnBoarding(ToPointVec2, Event, From, To)
    @@ -1956,8 +1877,8 @@ The amount of seconds to delay the action.

    - -AI_CARGO_GROUPED:onenterUnLoaded(Core, Event, From, To, ToPointVec2) + +CARGO_GROUP:onenterUnLoaded(Core, Event, From, To, ToPointVec2)
    @@ -1998,8 +1919,8 @@ Point#POINT_VEC2

    - -AI_CARGO_GROUPED:onleaveBoarding(CargoCarrier, Event, From, To) + +CARGO_GROUP:onleaveBoarding(CargoCarrier, Event, From, To)
    @@ -2034,8 +1955,8 @@ Point#POINT_VEC2

    - -AI_CARGO_GROUPED:onleaveUnBoarding(ToPointVec2, Event, From, To) + +CARGO_GROUP:onleaveUnBoarding(ToPointVec2, Event, From, To)
    @@ -2068,14 +1989,14 @@ Point#POINT_VEC2

    -

    Type AI_CARGO_PACKAGE

    +

    Type CARGO_PACKAGE

    Field(s)

    - -AI_CARGO_PACKAGE.CargoCarrier + +CARGO_PACKAGE.CargoCarrier
    @@ -2088,8 +2009,8 @@ Point#POINT_VEC2

    - -AI_CARGO_PACKAGE.CargoInAir + +CARGO_PACKAGE.CargoInAir
    @@ -2102,8 +2023,8 @@ Point#POINT_VEC2

    #string - -AI_CARGO_PACKAGE.ClassName + +CARGO_PACKAGE.ClassName
    @@ -2115,8 +2036,8 @@ Point#POINT_VEC2

    - -AI_CARGO_PACKAGE:IsNear(CargoCarrier) + +CARGO_PACKAGE:IsNear(CargoCarrier)
    @@ -2141,13 +2062,13 @@ Point#POINT_VEC2

    - -AI_CARGO_PACKAGE:New(CargoCarrier, Type, Name, Weight, ReportRadius, NearRadius) + +CARGO_PACKAGE:New(CargoCarrier, Type, Name, Weight, ReportRadius, NearRadius)
    -

    AICARGOPACKAGE Constructor.

    +

    CARGO_PACKAGE Constructor.

    Parameters

      @@ -2187,7 +2108,7 @@ The UNIT carrying the package.

    Return value

    -

    #AICARGOPACKAGE:

    +

    #CARGO_PACKAGE:

    @@ -2195,8 +2116,8 @@ The UNIT carrying the package.

    - -AI_CARGO_PACKAGE:onafterLoad(Event, From, To, CargoCarrier, Speed, LoadDistance, Angle) + +CARGO_PACKAGE:onafterLoad(Event, From, To, CargoCarrier, Speed, LoadDistance, Angle)
    @@ -2246,8 +2167,8 @@ The UNIT carrying the package.

    - -AI_CARGO_PACKAGE:onafterOnBoard(Event, From, To, CargoCarrier, Speed, BoardDistance, Angle, LoadDistance) + +CARGO_PACKAGE:onafterOnBoard(Event, From, To, CargoCarrier, Speed, BoardDistance, Angle, LoadDistance)
    @@ -2302,8 +2223,8 @@ The UNIT carrying the package.

    - -AI_CARGO_PACKAGE:onafterOnBoarded(Event, From, To, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle) + +CARGO_PACKAGE:onafterOnBoarded(Event, From, To, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle)
    @@ -2358,8 +2279,8 @@ The UNIT carrying the package.

    - -AI_CARGO_PACKAGE:onafterUnBoard(Event, From, To, Speed, UnLoadDistance, UnBoardDistance, Radius, Angle, CargoCarrier) + +CARGO_PACKAGE:onafterUnBoard(Event, From, To, Speed, UnLoadDistance, UnBoardDistance, Radius, Angle, CargoCarrier)
    @@ -2419,8 +2340,8 @@ The UNIT carrying the package.

    - -AI_CARGO_PACKAGE:onafterUnBoarded(Event, From, To, CargoCarrier, Speed) + +CARGO_PACKAGE:onafterUnBoarded(Event, From, To, CargoCarrier, Speed)
    @@ -2460,8 +2381,8 @@ The UNIT carrying the package.

    - -AI_CARGO_PACKAGE:onafterUnLoad(Event, From, To, Distance, Angle, CargoCarrier, Speed) + +CARGO_PACKAGE:onafterUnLoad(Event, From, To, Distance, Angle, CargoCarrier, Speed)
    @@ -2509,14 +2430,14 @@ The UNIT carrying the package.

    -

    Type AI_CARGO_REPRESENTABLE

    +

    Type CARGO_REPORTABLE

    Field(s)

    #string - -AI_CARGO_REPRESENTABLE.ClassName + +CARGO_REPORTABLE.ClassName
    @@ -2528,13 +2449,39 @@ The UNIT carrying the package.

    - -AI_CARGO_REPRESENTABLE:New(CargoObject, Type, Name, Weight, ReportRadius, NearRadius) + +CARGO_REPORTABLE:IsInRadius(PointVec2)
    -

    AICARGOREPRESENTABLE Constructor.

    +

    Check if CargoCarrier is in the ReportRadius for the Cargo to be Loaded.

    + +

    Parameter

    + +

    Return value

    + +

    #boolean:

    + + +
    +
    +
    +
    + + +CARGO_REPORTABLE:New(CargoObject, Type, Name, Weight, ReportRadius, NearRadius) + +
    +
    + +

    CARGO_REPORTABLE Constructor.

    Parameters

      @@ -2573,7 +2520,7 @@ The UNIT carrying the package.

    Return value

    -

    #AICARGOREPRESENTABLE:

    +

    #CARGO_REPORTABLE:

    @@ -2581,8 +2528,91 @@ The UNIT carrying the package.

    - -AI_CARGO_REPRESENTABLE:RouteTo(ToPointVec2, Speed) + +CARGO_REPORTABLE.ReportRadius + +
    +
    + + + +
    +
    + +

    Type CARGO_REPRESENTABLE

    +

    Field(s)

    +
    +
    + + #string + +CARGO_REPRESENTABLE.ClassName + +
    +
    + + + +
    +
    +
    +
    + + +CARGO_REPRESENTABLE:New(CargoObject, Type, Name, Weight, ReportRadius, NearRadius) + +
    +
    + +

    CARGO_REPRESENTABLE Constructor.

    + +

    Parameters

    +
      +
    • + +

      Wrapper.Controllable#Controllable CargoObject :

      + +
    • +
    • + +

      #string Type :

      + +
    • +
    • + +

      #string Name :

      + +
    • +
    • + +

      #number Weight :

      + +
    • +
    • + +

      #number ReportRadius : +(optional)

      + +
    • +
    • + +

      #number NearRadius : +(optional)

      + +
    • +
    +

    Return value

    + +

    #CARGO_REPRESENTABLE:

    + + +
    +
    +
    +
    + + +CARGO_REPRESENTABLE:RouteTo(ToPointVec2, Speed)
    @@ -2604,19 +2634,19 @@ The UNIT carrying the package.

    Return value

    -

    #AICARGOREPRESENTABLE:

    +

    #CARGO_REPRESENTABLE:

    -

    Type AI_CARGO_UNIT

    +

    Type CARGO_UNIT

    Field(s)

    - -AI_CARGO_UNIT.CargoCarrier + +CARGO_UNIT.CargoCarrier
    @@ -2629,8 +2659,8 @@ The UNIT carrying the package.

    - -AI_CARGO_UNIT.CargoInAir + +CARGO_UNIT.CargoInAir
    @@ -2643,8 +2673,8 @@ The UNIT carrying the package.

    - -AI_CARGO_UNIT.CargoObject + +CARGO_UNIT.CargoObject
    @@ -2656,17 +2686,17 @@ The UNIT carrying the package.

    - -AI_CARGO_UNIT:Destroy() + +CARGO_UNIT:Destroy()
    -

    AICARGOUNIT Destructor.

    +

    CARGO_UNIT Destructor.

    Return value

    -

    #AICARGOUNIT:

    +

    #CARGO_UNIT:

    @@ -2674,13 +2704,13 @@ The UNIT carrying the package.

    - -AI_CARGO_UNIT:New(CargoUnit, Type, Name, Weight, ReportRadius, NearRadius) + +CARGO_UNIT:New(CargoUnit, Type, Name, Weight, ReportRadius, NearRadius)
    -

    AICARGOUNIT Constructor.

    +

    CARGO_UNIT Constructor.

    Parameters

      @@ -2719,7 +2749,7 @@ The UNIT carrying the package.

    Return value

    -

    #AICARGOUNIT:

    +

    #CARGO_UNIT:

    @@ -2727,8 +2757,8 @@ The UNIT carrying the package.

    - -AI_CARGO_UNIT.OnUnLoadedCallBack + +CARGO_UNIT.OnUnLoadedCallBack
    @@ -2740,8 +2770,8 @@ The UNIT carrying the package.

    - -AI_CARGO_UNIT:onafterBoard(Event, From, To, CargoCarrier, ...) + +CARGO_UNIT:onafterBoard(Event, From, To, CargoCarrier, NearRadius, ...)
    @@ -2772,6 +2802,11 @@ The UNIT carrying the package.

  • +

    NearRadius :

    + +
  • +
  • +

    ... :

  • @@ -2781,8 +2816,8 @@ The UNIT carrying the package.

    - -AI_CARGO_UNIT:onafterUnBoarding(Event, From, To, ToPointVec2) + +CARGO_UNIT:onafterUnBoarding(Event, From, To, ToPointVec2)
    @@ -2817,8 +2852,8 @@ The UNIT carrying the package.

    - -AI_CARGO_UNIT:onenterBoarding(Event, From, To, CargoCarrier, ...) + +CARGO_UNIT:onenterBoarding(Event, From, To, CargoCarrier, NearRadius, ...)
    @@ -2849,6 +2884,11 @@ The UNIT carrying the package.

  • +

    NearRadius :

    + +
  • +
  • +

    ... :

  • @@ -2858,8 +2898,8 @@ The UNIT carrying the package.

    - -AI_CARGO_UNIT:onenterLoaded(Event, From, To, CargoCarrier) + +CARGO_UNIT:onenterLoaded(Event, From, To, CargoCarrier)
    @@ -2894,8 +2934,8 @@ The UNIT carrying the package.

    - -AI_CARGO_UNIT:onenterUnBoarding(Event, From, To, ToPointVec2) + +CARGO_UNIT:onenterUnBoarding(Event, From, To, ToPointVec2)
    @@ -2930,8 +2970,8 @@ The UNIT carrying the package.

    - -AI_CARGO_UNIT:onenterUnLoaded(Event, From, To, Core, ToPointVec2) + +CARGO_UNIT:onenterUnLoaded(Event, From, To, Core, ToPointVec2)
    @@ -2972,8 +3012,8 @@ Point#POINT_VEC2

    - -AI_CARGO_UNIT:onleaveBoarding(Event, From, To, CargoCarrier, ...) + +CARGO_UNIT:onleaveBoarding(Event, From, To, CargoCarrier, NearRadius, ...)
    @@ -3004,6 +3044,11 @@ Point#POINT_VEC2

  • +

    NearRadius :

    + +
  • +
  • +

    ... :

  • @@ -3013,8 +3058,8 @@ Point#POINT_VEC2

    - -AI_CARGO_UNIT:onleaveUnBoarding(Event, From, To, ToPointVec2) + +CARGO_UNIT:onleaveUnBoarding(Event, From, To, ToPointVec2)
    diff --git a/docs/Documentation/Fsm.html b/docs/Documentation/Fsm.html index baf5bc616..9c7864cf4 100644 --- a/docs/Documentation/Fsm.html +++ b/docs/Documentation/Fsm.html @@ -1582,7 +1582,7 @@ A string defining the start state.

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

    + FSM.current diff --git a/docs/Documentation/MOVEMENT.html b/docs/Documentation/MOVEMENT.html index c8bfd7123..eb273ac47 100644 --- a/docs/Documentation/MOVEMENT.html +++ b/docs/Documentation/MOVEMENT.html @@ -211,7 +211,6 @@ on defined intervals (currently every minute).

    - #number MOVEMENT.AliveUnits @@ -220,9 +219,6 @@ on defined intervals (currently every minute).

    - -

    Contains the counter how many units are currently alive

    -
    diff --git a/docs/Documentation/Point.html b/docs/Documentation/Point.html index a1fb3c45e..d01447df5 100644 --- a/docs/Documentation/Point.html +++ b/docs/Documentation/Point.html @@ -1367,6 +1367,7 @@ The new calculated POINT_VEC2.

    + POINT_VEC2.z diff --git a/docs/Documentation/Spawn.html b/docs/Documentation/Spawn.html index 934853fcd..3b523abef 100644 --- a/docs/Documentation/Spawn.html +++ b/docs/Documentation/Spawn.html @@ -2117,9 +2117,6 @@ The group that was spawned. You can use this group for further actions.

    - -

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

    -
    @@ -2573,6 +2570,9 @@ when nothing was spawned.

    + +

    Overwrite unit names by default with group name.

    +
    @@ -2587,9 +2587,6 @@ when nothing was spawned.

    - -

    By default, no InitLimit

    -
    @@ -2625,7 +2622,7 @@ when nothing was spawned.

    - #number + SPAWN.SpawnMaxGroups @@ -2642,7 +2639,7 @@ when nothing was spawned.

    - #number + SPAWN.SpawnMaxUnitsAlive diff --git a/docs/Documentation/index.html b/docs/Documentation/index.html index 97b11e2e6..78c9b22f9 100644 --- a/docs/Documentation/index.html +++ b/docs/Documentation/index.html @@ -179,18 +179,7 @@ CLIENTS in a SET_CLIENT collection, which are not occupied by human players.

    From 0b59fb87f2da03928dc1f2fdf0728f3b6e3883d5 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Thu, 13 Apr 2017 09:53:27 +0200 Subject: [PATCH 11/14] Progress --- Moose Development/Moose/Tasking/Task_CARGO.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Moose Development/Moose/Tasking/Task_CARGO.lua b/Moose Development/Moose/Tasking/Task_CARGO.lua index f6361a777..f5705eb89 100644 --- a/Moose Development/Moose/Tasking/Task_CARGO.lua +++ b/Moose Development/Moose/Tasking/Task_CARGO.lua @@ -128,6 +128,8 @@ do -- TASK_CARGO TaskUnit.Menu = MENU_GROUP:New( TaskUnit:GetGroup(), Task:GetName() .. " @ " .. TaskUnit:GetName() ) + Task.SetCargo:Flush() + Task.SetCargo:ForEachCargo( --- @param Core.Cargo#CARGO Cargo From ff64255ea7fb5a1c369fbca2a5d3e9f0b6303d3a Mon Sep 17 00:00:00 2001 From: FlightControl Date: Fri, 14 Apr 2017 05:43:09 +0200 Subject: [PATCH 12/14] CARGO_GROUP working!!! --- Moose Development/Moose/Core/Base.lua | 3 +- Moose Development/Moose/Core/Cargo.lua | 88 ++++++++++++++----- Moose Development/Moose/Core/Set.lua | 8 +- .../Moose/Tasking/CommandCenter.lua | 19 +++- Moose Development/Moose/Tasking/Task.lua | 3 +- .../Moose/Tasking/Task_CARGO.lua | 6 +- 6 files changed, 93 insertions(+), 34 deletions(-) diff --git a/Moose Development/Moose/Core/Base.lua b/Moose Development/Moose/Core/Base.lua index 018f2eb02..64bcd30d2 100644 --- a/Moose Development/Moose/Core/Base.lua +++ b/Moose Development/Moose/Core/Base.lua @@ -228,7 +228,8 @@ BASE = { -- @type FORMATION -- @field Cone A cone formation. FORMATION = { - Cone = "Cone" + Cone = "Cone", + Vee = "Vee" } diff --git a/Moose Development/Moose/Core/Cargo.lua b/Moose Development/Moose/Core/Cargo.lua index c00740e68..e87f3bdfa 100644 --- a/Moose Development/Moose/Core/Cargo.lua +++ b/Moose Development/Moose/Core/Cargo.lua @@ -508,9 +508,11 @@ end -- @param #string From -- @param #string To -- @param Core.Point#POINT_VEC2 ToPointVec2 -function CARGO_UNIT:onenterUnBoarding( From, Event, To, ToPointVec2 ) +function CARGO_UNIT:onenterUnBoarding( From, Event, To, ToPointVec2, NearRadius ) self:F() + NearRadius = NearRadius or 25 + local Angle = 180 local Speed = 10 local DeployDistance = 5 @@ -541,7 +543,7 @@ function CARGO_UNIT:onenterUnBoarding( From, Event, To, ToPointVec2 ) local TaskRoute = self.CargoObject:TaskRoute( Points ) self.CargoObject:SetTask( TaskRoute, 1 ) - self:__UnBoarding( 1, ToPointVec2 ) + self:__UnBoarding( -1, ToPointVec2, NearRadius ) end end @@ -553,18 +555,20 @@ end -- @param #string From -- @param #string To -- @param Core.Point#POINT_VEC2 ToPointVec2 -function CARGO_UNIT:onleaveUnBoarding( From, Event, To, ToPointVec2 ) +function CARGO_UNIT:onleaveUnBoarding( From, Event, To, ToPointVec2, NearRadius ) self:F( { ToPointVec2, From, Event, To } ) + NearRadius = NearRadius or 25 + local Angle = 180 local Speed = 10 local Distance = 5 if From == "UnBoarding" then - if self:IsNear( ToPointVec2 ) then + if self:IsNear( ToPointVec2, NearRadius ) then return true else - self:__UnBoarding( 1, ToPointVec2 ) + self:__UnBoarding( 1, ToPointVec2, NearRadius ) end return false end @@ -577,9 +581,11 @@ end -- @param #string From -- @param #string To -- @param Core.Point#POINT_VEC2 ToPointVec2 -function CARGO_UNIT:onafterUnBoarding( From, Event, To, ToPointVec2 ) +function CARGO_UNIT:onafterUnBoarding( From, Event, To, ToPointVec2, NearRadius ) self:F( { ToPointVec2, From, Event, To } ) + NearRadius = NearRadius or 25 + self.CargoInAir = self.CargoObject:InAir() self:T( self.CargoInAir ) @@ -602,7 +608,7 @@ end -- @param #string From -- @param #string To -- @param Core.Point#POINT_VEC2 -function CARGO_UNIT:onenterUnLoaded( From, Event, To, ToPointVec2 ) +function CARGO_UNIT:onenterUnLoaded( From, Event, To, ToPointVec2 ) self:F( { ToPointVec2, From, Event, To } ) local Angle = 180 @@ -661,6 +667,25 @@ function CARGO_UNIT:onenterBoarding( From, Event, To, CargoCarrier, NearRadius, Points[#Points+1] = PointStartVec2:RoutePointGround( Speed ) Points[#Points+1] = CargoDeployPointVec2:RoutePointGround( Speed ) + + -- I need to do this mess, otherwise the units will stop boarding at a point in time... + -- This is a DCS bug that i am handling in this way. + do + local CargoBooardPointVec2 = CargoDeployPointVec2:GetRandomPointVec2InRadius(NearRadius,0) + Points[#Points+1] = CargoBooardPointVec2:RoutePointGround( Speed ) + end + do + local CargoBooardPointVec2 = CargoDeployPointVec2:GetRandomPointVec2InRadius(NearRadius,0) + Points[#Points+1] = CargoBooardPointVec2:RoutePointGround( Speed ) + end + do + local CargoBooardPointVec2 = CargoDeployPointVec2:GetRandomPointVec2InRadius(NearRadius,0) + Points[#Points+1] = CargoBooardPointVec2:RoutePointGround( Speed ) + end + do + local CargoBooardPointVec2 = CargoDeployPointVec2:GetRandomPointVec2InRadius(NearRadius,0) + Points[#Points+1] = CargoBooardPointVec2:RoutePointGround( Speed ) + end local TaskRoute = self.CargoObject:TaskRoute( Points ) self.CargoObject:SetTask( TaskRoute, 2 ) @@ -970,9 +995,9 @@ do -- CARGO_GROUP -- @param #number ReportRadius (optional) -- @param #number NearRadius (optional) -- @return #CARGO_GROUP -function CARGO_GROUP:New( CargoGroup, Type, Name, ReportRadius, NearRadius ) - local self = BASE:Inherit( self, CARGO_REPORTABLE:New( CargoGroup, Type, Name, 0, ReportRadius, NearRadius ) ) -- #CARGO_GROUP - self:F( { Type, Name, ReportRadius, NearRadius } ) +function CARGO_GROUP:New( CargoGroup, Type, Name, ReportRadius ) + local self = BASE:Inherit( self, CARGO_REPORTABLE:New( CargoGroup, Type, Name, 0, ReportRadius ) ) -- #CARGO_GROUP + self:F( { Type, Name, ReportRadius } ) self.CargoSet = SET_CARGO:New() @@ -1002,19 +1027,21 @@ end -- @param #string Event -- @param #string From -- @param #string To -function CARGO_GROUP:onenterBoarding( From, Event, To, CargoCarrier ) +function CARGO_GROUP:onenterBoarding( From, Event, To, CargoCarrier, NearRadius, ... ) self:F( { CargoCarrier.UnitName, From, Event, To } ) + NearRadius = NearRadius or 25 + if From == "UnLoaded" then -- For each Cargo object within the CARGO_GROUPED, route each object to the CargoLoadPointVec2 self.CargoSet:ForEach( function( Cargo ) - Cargo:__Board( 1, CargoCarrier ) + Cargo:__Board( 1, CargoCarrier, NearRadius ) end ) - self:__Boarding( 1, CargoCarrier ) + self:__Boarding( 1, CargoCarrier, NearRadius, ... ) end end @@ -1025,7 +1052,7 @@ end -- @param #string Event -- @param #string From -- @param #string To -function CARGO_GROUP:onenterLoaded( From, Event, To, CargoCarrier ) +function CARGO_GROUP:onenterLoaded( From, Event, To, CargoCarrier, ... ) self:F( { CargoCarrier.UnitName, From, Event, To } ) if From == "UnLoaded" then @@ -1034,6 +1061,9 @@ function CARGO_GROUP:onenterLoaded( From, Event, To, CargoCarrier ) Cargo:Load( CargoCarrier ) end end + + self.CargoCarrier = CargoCarrier + end --- Leave Boarding State. @@ -1042,23 +1072,27 @@ end -- @param #string Event -- @param #string From -- @param #string To -function CARGO_GROUP:onleaveBoarding( From, Event, To, CargoCarrier ) +function CARGO_GROUP:onleaveBoarding( From, Event, To, CargoCarrier, NearRadius, ... ) self:F( { CargoCarrier.UnitName, From, Event, To } ) + NearRadius = NearRadius or 25 + local Boarded = true + self.CargoSet:Flush() + -- For each Cargo object within the CARGO_GROUP, route each object to the CargoLoadPointVec2 for CargoID, Cargo in pairs( self.CargoSet:GetSet() ) do - self:T( Cargo.current ) + self:T( { Cargo:GetName(), Cargo.current } ) if not Cargo:is( "Loaded" ) then Boarded = false end end if not Boarded then - self:__Boarding( 1, CargoCarrier ) + self:__Boarding( 1, CargoCarrier, NearRadius, ... ) else - self:__Load( 1, CargoCarrier ) + self:__Load( 1, CargoCarrier, ... ) end return Boarded end @@ -1069,9 +1103,11 @@ end -- @param #string Event -- @param #string From -- @param #string To -function CARGO_GROUP:onenterUnBoarding( From, Event, To, ToPointVec2 ) +function CARGO_GROUP:onenterUnBoarding( From, Event, To, ToPointVec2, NearRadius ) self:F() + NearRadius = NearRadius or 25 + local Timer = 1 if From == "Loaded" then @@ -1079,12 +1115,12 @@ function CARGO_GROUP:onenterUnBoarding( From, Event, To, ToPointVec2 ) -- For each Cargo object within the CARGO_GROUP, route each object to the CargoLoadPointVec2 self.CargoSet:ForEach( function( Cargo ) - Cargo:__UnBoard( Timer, ToPointVec2 ) + Cargo:__UnBoard( Timer, ToPointVec2, NearRadius ) Timer = Timer + 10 end ) - self:__UnBoarding( 1, ToPointVec2 ) + self:__UnBoarding( 1, ToPointVec2, NearRadius ) end end @@ -1095,9 +1131,11 @@ end -- @param #string Event -- @param #string From -- @param #string To -function CARGO_GROUP:onleaveUnBoarding( From, Event, To, ToPointVec2 ) +function CARGO_GROUP:onleaveUnBoarding( From, Event, To, ToPointVec2, NearRadius ) self:F( { ToPointVec2, From, Event, To } ) + NearRadius = NearRadius or 25 + local Angle = 180 local Speed = 10 local Distance = 5 @@ -1116,7 +1154,7 @@ function CARGO_GROUP:onleaveUnBoarding( From, Event, To, ToPointVec2 ) if UnBoarded then return true else - self:__UnBoarding( 1, ToPointVec2 ) + self:__UnBoarding( 1, ToPointVec2, NearRadius ) end return false @@ -1130,9 +1168,11 @@ end -- @param #string Event -- @param #string From -- @param #string To -function CARGO_GROUP:onafterUnBoarding( From, Event, To, ToPointVec2 ) +function CARGO_GROUP:onafterUnBoarding( From, Event, To, ToPointVec2, NearRadius ) self:F( { ToPointVec2, From, Event, To } ) + NearRadius = NearRadius or 25 + self:__UnLoad( 1, ToPointVec2 ) end diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index d6e7c9caa..f83a2903a 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -124,7 +124,7 @@ end -- @param Core.Base#BASE Object -- @return Core.Base#BASE The added BASE Object. function SET_BASE:Add( ObjectName, Object ) - self:F2( ObjectName ) + self:F( ObjectName ) local t = { _ = Object } @@ -2467,9 +2467,6 @@ function SET_CARGO:New() -- Inherits from BASE local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.CARGOS ) ) - self:HandleEvent( EVENTS.NewCargo ) - self:HandleEvent( EVENTS.DeleteCargo ) - return self end @@ -2600,6 +2597,9 @@ function SET_CARGO:FilterStart() if _DATABASE then self:_FilterStart() end + + self:HandleEvent( EVENTS.NewCargo ) + self:HandleEvent( EVENTS.DeleteCargo ) return self end diff --git a/Moose Development/Moose/Tasking/CommandCenter.lua b/Moose Development/Moose/Tasking/CommandCenter.lua index 09ad2ab23..0bbb1208d 100644 --- a/Moose Development/Moose/Tasking/CommandCenter.lua +++ b/Moose Development/Moose/Tasking/CommandCenter.lua @@ -117,6 +117,21 @@ function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName ) end ) + -- Handle when a player leaves a slot and goes back to spectators ... + -- The PlayerUnit will be UnAssigned from the Task. + -- When there is no Unit left running the Task, the Task goes into Abort... + self:HandleEvent( EVENTS.MissionEnd, + --- @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:Stop() + 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... @@ -127,7 +142,9 @@ function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName ) local PlayerUnit = EventData.IniUnit for MissionID, Mission in pairs( self:GetMissions() ) do local Mission = Mission -- Tasking.Mission#MISSION - Mission:AbortUnit( PlayerUnit ) + if Mission:IsOngoing() then + Mission:AbortUnit( PlayerUnit ) + end end end ) diff --git a/Moose Development/Moose/Tasking/Task.lua b/Moose Development/Moose/Tasking/Task.lua index 2457077ef..30f93d0a4 100644 --- a/Moose Development/Moose/Tasking/Task.lua +++ b/Moose Development/Moose/Tasking/Task.lua @@ -275,8 +275,9 @@ function TASK:AbortUnit( PlayerUnit ) local IsAssignedToGroup = self:IsAssignedToGroup( PlayerGroup ) self:E( { IsAssignedToGroup = IsAssignedToGroup } ) if IsAssignedToGroup then + local PlayerName = PlayerUnit:GetPlayerName() self:UnAssignFromUnit( PlayerUnit ) - self:MessageToGroups( PlayerUnit:GetPlayerName() .. " aborted Task " .. self:GetName() ) + self:MessageToGroups( PlayerName .. " aborted Task " .. self:GetName() ) self:E( { TaskGroup = PlayerGroup:GetName(), GetUnits = PlayerGroup:GetUnits() } ) if #PlayerGroup:GetUnits() == 1 then self:UnAssignFromGroup( PlayerGroup ) diff --git a/Moose Development/Moose/Tasking/Task_CARGO.lua b/Moose Development/Moose/Tasking/Task_CARGO.lua index f5705eb89..b6d5b9ad9 100644 --- a/Moose Development/Moose/Tasking/Task_CARGO.lua +++ b/Moose Development/Moose/Tasking/Task_CARGO.lua @@ -347,7 +347,7 @@ do -- TASK_CARGO self:__Land( -0.1 ) else Task:GetMission():GetCommandCenter():MessageToGroup( "Boarding ...", TaskUnit:GetGroup(), "Boarding" ) - self.Cargo:Board( TaskUnit, self ) + self.Cargo:Board( TaskUnit, 20, self ) end else self:__ArriveAtCargo( -0.1 ) @@ -375,7 +375,7 @@ do -- TASK_CARGO self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) self.DeployZone = DeployZone - self.Cargo:__UnBoard( -0.1, DeployZone ) + self.Cargo:__UnBoard( -0.1, DeployZone, 20 ) end --- @@ -394,7 +394,7 @@ do -- TASK_CARGO end Task:GetMission():GetCommandCenter():MessageToGroup( "UnBoarding ...", TaskUnit:GetGroup(), "UnBoarding" ) - self.Cargo:__UnBoard( -0.1, self.DeployZone ) + self.Cargo:__UnBoard( -0.1, self.DeployZone, 20 ) end From 9a2b56fb9fb519bb65992c2e86ed94248069ed47 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Fri, 14 Apr 2017 08:26:53 +0200 Subject: [PATCH 13/14] Optimized landing, messaging, flow --- Moose Development/Moose/Core/Cargo.lua | 216 ++++++++++-------- .../Moose/Tasking/CommandCenter.lua | 3 +- .../Moose/Tasking/Task_CARGO.lua | 107 ++++----- 3 files changed, 171 insertions(+), 155 deletions(-) diff --git a/Moose Development/Moose/Core/Cargo.lua b/Moose Development/Moose/Core/Cargo.lua index e87f3bdfa..968b884b7 100644 --- a/Moose Development/Moose/Core/Cargo.lua +++ b/Moose Development/Moose/Core/Cargo.lua @@ -355,99 +355,115 @@ do -- CARGO_REPRESENTABLE ClassName = "CARGO_REPRESENTABLE" } ---- CARGO_REPRESENTABLE Constructor. --- @param #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 #CARGO_REPRESENTABLE -function CARGO_REPRESENTABLE:New( CargoObject, Type, Name, Weight, ReportRadius, NearRadius ) - local self = BASE:Inherit( self, CARGO:New( Type, Name, Weight, ReportRadius, NearRadius ) ) -- #CARGO - self:F( { Type, Name, Weight, ReportRadius, NearRadius } ) - - return self -end - ---- Route a cargo unit to a PointVec2. --- @param #CARGO_REPRESENTABLE self --- @param Core.Point#POINT_VEC2 ToPointVec2 --- @param #number Speed --- @return #CARGO_REPRESENTABLE -function CARGO_REPRESENTABLE:RouteTo( ToPointVec2, Speed ) - self:F2( ToPointVec2 ) - - local Points = {} - - local PointStartVec2 = self.CargoObject:GetPointVec2() - - Points[#Points+1] = PointStartVec2:RoutePointGround( Speed ) - Points[#Points+1] = ToPointVec2:RoutePointGround( Speed ) - - local TaskRoute = self.CargoObject:TaskRoute( Points ) - self.CargoObject:SetTask( TaskRoute, 2 ) - return self -end - + --- CARGO_REPRESENTABLE Constructor. + -- @param #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 #CARGO_REPRESENTABLE + function CARGO_REPRESENTABLE:New( CargoObject, Type, Name, Weight, ReportRadius, NearRadius ) + local self = BASE:Inherit( self, CARGO:New( Type, Name, Weight, ReportRadius, NearRadius ) ) -- #CARGO + self:F( { Type, Name, Weight, ReportRadius, NearRadius } ) + + return self + end + + --- Route a cargo unit to a PointVec2. + -- @param #CARGO_REPRESENTABLE self + -- @param Core.Point#POINT_VEC2 ToPointVec2 + -- @param #number Speed + -- @return #CARGO_REPRESENTABLE + function CARGO_REPRESENTABLE:RouteTo( ToPointVec2, Speed ) + self:F2( ToPointVec2 ) + + local Points = {} + + local PointStartVec2 = self.CargoObject:GetPointVec2() + + Points[#Points+1] = PointStartVec2:RoutePointGround( Speed ) + Points[#Points+1] = ToPointVec2:RoutePointGround( Speed ) + + local TaskRoute = self.CargoObject:TaskRoute( Points ) + self.CargoObject:SetTask( TaskRoute, 2 ) + return self + end + + end -- CARGO_REPRESENTABLE -do -- CARGO_REPORTABLE - - --- @type CARGO_REPORTABLE - -- @extends #CARGO - CARGO_REPORTABLE = { - ClassName = "CARGO_REPORTABLE" - } - ---- CARGO_REPORTABLE Constructor. --- @param #CARGO_REPORTABLE self --- @param Wrapper.Controllable#Controllable CargoObject --- @param #string Type --- @param #string Name --- @param #number Weight --- @param #number ReportRadius (optional) --- @param #number NearRadius (optional) --- @return #CARGO_REPORTABLE -function CARGO_REPORTABLE:New( CargoObject, Type, Name, Weight, ReportRadius ) - local self = BASE:Inherit( self, CARGO:New( Type, Name, Weight ) ) -- #CARGO_REPORTABLE - self:F( { Type, Name, Weight, ReportRadius } ) - - self.ReportRadius = ReportRadius or 1000 - self.CargoObject = CargoObject - - return self -end - ---- Check if CargoCarrier is in the ReportRadius for the Cargo to be Loaded. --- @param #CARGO_REPORTABLE self --- @param Core.Point#POINT_VEC2 PointVec2 --- @return #boolean -function CARGO_REPORTABLE:IsInRadius( PointVec2 ) - self:F( { PointVec2 } ) - - local Distance = 0 - if self:IsLoaded() then - Distance = PointVec2:DistanceFromPointVec2( self.CargoCarrier:GetPointVec2() ) - else - Distance = PointVec2:DistanceFromPointVec2( self.CargoObject:GetPointVec2() ) - end - self:T( Distance ) + do -- CARGO_REPORTABLE + + --- @type CARGO_REPORTABLE + -- @extends #CARGO + CARGO_REPORTABLE = { + ClassName = "CARGO_REPORTABLE" + } + + --- CARGO_REPORTABLE Constructor. + -- @param #CARGO_REPORTABLE self + -- @param Wrapper.Controllable#Controllable CargoObject + -- @param #string Type + -- @param #string Name + -- @param #number Weight + -- @param #number ReportRadius (optional) + -- @param #number NearRadius (optional) + -- @return #CARGO_REPORTABLE + function CARGO_REPORTABLE:New( CargoObject, Type, Name, Weight, ReportRadius ) + local self = BASE:Inherit( self, CARGO:New( Type, Name, Weight ) ) -- #CARGO_REPORTABLE + self:F( { Type, Name, Weight, ReportRadius } ) + + self.ReportRadius = ReportRadius or 1000 + self.CargoObject = CargoObject + + return self + end + + --- Check if CargoCarrier is in the ReportRadius for the Cargo to be Loaded. + -- @param #CARGO_REPORTABLE self + -- @param Core.Point#POINT_VEC2 PointVec2 + -- @return #boolean + function CARGO_REPORTABLE:IsInRadius( PointVec2 ) + self:F( { PointVec2 } ) + + local Distance = 0 + if self:IsLoaded() then + Distance = PointVec2:DistanceFromPointVec2( self.CargoCarrier:GetPointVec2() ) + else + Distance = PointVec2:DistanceFromPointVec2( self.CargoObject:GetPointVec2() ) + end + self:T( Distance ) + + if Distance <= self.ReportRadius then + return true + else + return false + end + - if Distance <= self.ReportRadius then - return true - else - return false end -end ---- Get the range till cargo will board. --- @param #CARGO self --- @return #number The range till cargo will board. -function CARGO:GetBoardingRange() - return self.ReportRadius -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 CARGO_REPORTABLE:MessageToGroup( Message, TaskGroup, Name ) + + local Prefix = Name and "@ " .. Name .. ": " or "@ " .. TaskGroup:GetCallsign() .. ": " + Message = Prefix .. Message + MESSAGE:New( Message, 20, "Cargo: " .. self:GetName() ):ToGroup( TaskGroup ) + + end + + --- Get the range till cargo will board. + -- @param #CARGO self + -- @return #number The range till cargo will board. + function CARGO_REPORTABLE:GetBoardingRange() + return self.ReportRadius + end end @@ -649,7 +665,7 @@ end function CARGO_UNIT:onenterBoarding( From, Event, To, CargoCarrier, NearRadius, ... ) self:F( { From, Event, To, CargoCarrier.UnitName, NearRadius } ) - local Speed = 10 + local Speed = 90 local Angle = 180 local Distance = 5 @@ -1103,8 +1119,8 @@ end -- @param #string Event -- @param #string From -- @param #string To -function CARGO_GROUP:onenterUnBoarding( From, Event, To, ToPointVec2, NearRadius ) - self:F() +function CARGO_GROUP:onenterUnBoarding( From, Event, To, ToPointVec2, NearRadius, ... ) + self:F({From, Event, To, ToPointVec2, NearRadius}) NearRadius = NearRadius or 25 @@ -1120,7 +1136,7 @@ function CARGO_GROUP:onenterUnBoarding( From, Event, To, ToPointVec2, NearRadius end ) - self:__UnBoarding( 1, ToPointVec2, NearRadius ) + self:__UnBoarding( 1, ToPointVec2, NearRadius, ... ) end end @@ -1131,8 +1147,8 @@ end -- @param #string Event -- @param #string From -- @param #string To -function CARGO_GROUP:onleaveUnBoarding( From, Event, To, ToPointVec2, NearRadius ) - self:F( { ToPointVec2, From, Event, To } ) +function CARGO_GROUP:onleaveUnBoarding( From, Event, To, ToPointVec2, NearRadius, ... ) + self:F( { From, Event, To, ToPointVec2, NearRadius } ) NearRadius = NearRadius or 25 @@ -1154,7 +1170,7 @@ function CARGO_GROUP:onleaveUnBoarding( From, Event, To, ToPointVec2, NearRadius if UnBoarded then return true else - self:__UnBoarding( 1, ToPointVec2, NearRadius ) + self:__UnBoarding( 1, ToPointVec2, NearRadius, ... ) end return false @@ -1168,12 +1184,12 @@ end -- @param #string Event -- @param #string From -- @param #string To -function CARGO_GROUP:onafterUnBoarding( From, Event, To, ToPointVec2, NearRadius ) - self:F( { ToPointVec2, From, Event, To } ) +function CARGO_GROUP:onafterUnBoarding( From, Event, To, ToPointVec2, NearRadius, ... ) + self:F( { From, Event, To, ToPointVec2, NearRadius } ) NearRadius = NearRadius or 25 - self:__UnLoad( 1, ToPointVec2 ) + self:__UnLoad( 1, ToPointVec2, ... ) end @@ -1184,8 +1200,8 @@ end -- @param #string Event -- @param #string From -- @param #string To -function CARGO_GROUP:onenterUnLoaded( From, Event, To, ToPointVec2 ) - self:F( { ToPointVec2, From, Event, To } ) +function CARGO_GROUP:onenterUnLoaded( From, Event, To, ToPointVec2, ... ) + self:F( { From, Event, To, ToPointVec2 } ) if From == "Loaded" then diff --git a/Moose Development/Moose/Tasking/CommandCenter.lua b/Moose Development/Moose/Tasking/CommandCenter.lua index 0bbb1208d..6589d43bd 100644 --- a/Moose Development/Moose/Tasking/CommandCenter.lua +++ b/Moose Development/Moose/Tasking/CommandCenter.lua @@ -274,8 +274,7 @@ end -- @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 = "@ Group" - Prefix = Prefix .. ( Name and " (" .. Name .. "): " or '' ) + local Prefix = Name and "@ " .. Name .. ": " or "@ " .. TaskGroup:GetCallsign() .. ": " Message = Prefix .. Message self:GetPositionable():MessageToGroup( Message , 20, TaskGroup, self:GetName() ) diff --git a/Moose Development/Moose/Tasking/Task_CARGO.lua b/Moose Development/Moose/Tasking/Task_CARGO.lua index b6d5b9ad9..cf7b3ea60 100644 --- a/Moose Development/Moose/Tasking/Task_CARGO.lua +++ b/Moose Development/Moose/Tasking/Task_CARGO.lua @@ -36,7 +36,7 @@ -- -- * **FlightControl**: Concept, Design & Programming. -- --- @module Task_CARGO +-- @module Task_Cargo do -- TASK_CARGO @@ -102,7 +102,7 @@ do -- TASK_CARGO Fsm:AddProcess ( "RoutingToDeploy", "RouteToDeployZone", ACT_ROUTE_ZONE:New(), { Arrived = "ArriveAtDeploy" } ) Fsm:AddTransition( "Arrived", "ArriveAtDeploy", "ArrivedAtDeploy" ) - Fsm:AddTransition( { "ArrivedAtPickup", "ArrivedAtDeploy" }, "Land", "Landing" ) + Fsm:AddTransition( { "ArrivedAtPickup", "ArrivedAtDeploy", "Landing" }, "Land", "Landing" ) Fsm:AddTransition( "Landing", "Landed", "Landed" ) Fsm:AddTransition( "WaitingForCommand", "PrepareBoarding", "AwaitBoarding" ) @@ -122,7 +122,7 @@ do -- TASK_CARGO --- -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_CARGO#TASK_CARGO Task + -- @param Tasking.Task_Cargo#TASK_CARGO Task function Fsm:OnEnterWaitingForCommand( TaskUnit, Task ) self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) @@ -165,7 +165,8 @@ do -- TASK_CARGO TaskUnit.Menu, self.MenuUnBoardCargo, self, - Cargo + Cargo, + DeployZone ) else MENU_GROUP_COMMAND:New( @@ -187,7 +188,7 @@ do -- TASK_CARGO --- -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_CARGO#TASK_CARGO Task + -- @param Tasking.Task_Cargo#TASK_CARGO Task function Fsm:OnLeaveWaitingForCommand( TaskUnit, Task ) self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) @@ -198,8 +199,8 @@ do -- TASK_CARGO self:__PrepareBoarding( 1.0, Cargo ) end - function Fsm:MenuUnBoardCargo( Cargo ) - self:__PrepareUnBoarding( 1.0, Cargo ) + function Fsm:MenuUnBoardCargo( Cargo, DeployZone ) + self:__PrepareUnBoarding( 1.0, Cargo, DeployZone ) end function Fsm:MenuRouteToPickup( Cargo ) @@ -213,26 +214,26 @@ do -- TASK_CARGO --- Route to Cargo -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_CARGO#TASK_CARGO Task + -- @param Tasking.Task_Cargo#TASK_CARGO Task function Fsm:onafterRouteToPickup( TaskUnit, Task, From, Event, To, Cargo ) self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) self.Cargo = Cargo Task:SetCargoPickup( self.Cargo, TaskUnit ) - self:__RouteToPickupPoint( 0.1 ) + self:__RouteToPickupPoint( -0.1 ) end --- -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_CARGO#TASK_CARGO Task + -- @param Tasking.Task_Cargo#TASK_CARGO Task function Fsm:onafterArriveAtPickup( TaskUnit, Task ) self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) if TaskUnit:IsAir() then - self:__Land( -0.1 ) + self:__Land( -0.1, "Pickup" ) else self:__SelectAction( -0.1 ) end @@ -248,19 +249,19 @@ do -- TASK_CARGO self.DeployZone = DeployZone Task:SetDeployZone( self.DeployZone, TaskUnit ) - self:__RouteToDeployZone( 0.1 ) + self:__RouteToDeployZone( -0.1 ) end --- -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_CARGO#TASK_CARGO Task + -- @param Tasking.Task_Cargo#TASK_CARGO Task function Fsm:onafterArriveAtDeploy( TaskUnit, Task ) self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) if TaskUnit:IsAir() then - self:__Land( -0.1 ) + self:__Land( -0.1, "Deploy" ) else self:__SelectAction( -0.1 ) end @@ -271,65 +272,64 @@ do -- TASK_CARGO --- -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_CARGO#TASK_CARGO Task - function Fsm:OnAfterLand( TaskUnit, Task, From, Event, To ) + -- @param Tasking.Task_Cargo#TASK_CARGO Task + function Fsm:OnAfterLand( TaskUnit, Task, From, Event, To, Action ) self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) if self.Cargo:IsInRadius( TaskUnit:GetPointVec2() ) then if TaskUnit:InAir() then - Task:GetMission():GetCommandCenter():MessageToGroup( "Land", TaskUnit:GetGroup(), "Land" ) - self:__Land( -10 ) + Task:GetMission():GetCommandCenter():MessageToGroup( "Land", TaskUnit:GetGroup() ) + self:__Land( -10, Action ) else - Task:GetMission():GetCommandCenter():MessageToGroup( "Landed ...", TaskUnit:GetGroup(), "Land" ) - self:__Landed( -0.1 ) + Task:GetMission():GetCommandCenter():MessageToGroup( "Landed ...", TaskUnit:GetGroup() ) + self:__Landed( -0.1, Action ) end else - self:__ArriveAtCargo( -0.1 ) + if Action == "Pickup" then + self:__RouteToPickupZone( -0.1 ) + else + self:__RouteToDeployZone( -0.1 ) + end end end --- -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_CARGO#TASK_CARGO Task - function Fsm:OnAfterLanded( TaskUnit, Task ) + -- @param Tasking.Task_Cargo#TASK_CARGO Task + function Fsm:OnAfterLanded( TaskUnit, Task, From, Event, To, Action ) self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) if self.Cargo:IsInRadius( TaskUnit:GetPointVec2() ) then if TaskUnit:InAir() then - self:__Land( -0.1 ) + self:__Land( -0.1, Action ) else - Task:GetMission():GetCommandCenter():MessageToGroup( "Preparing to board in 10 seconds ...", TaskUnit:GetGroup(), "Boarding" ) - self:__PrepareBoarding( -10 ) + self:__SelectAction( -0.1 ) end else - self:__ArriveAtCargo( -0.1 ) + if Action == "Pickup" then + self:__RouteToPickupZone( -0.1 ) + else + self:__RouteToDeployZone( -0.1 ) + end end end --- -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_CARGO#TASK_CARGO Task + -- @param Tasking.Task_Cargo#TASK_CARGO Task function Fsm:OnAfterPrepareBoarding( TaskUnit, Task, From, Event, To, Cargo ) self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - self.Cargo = Cargo - if self.Cargo:IsInRadius( TaskUnit:GetPointVec2() ) then - if TaskUnit:InAir() then - self:__Land( -0.1 ) - else - self:__Board( -0.1 ) - end - else - self:__ArriveAtCargo( -0.1 ) - end + self.Cargo = Cargo -- Core.Cargo#CARGO_GROUP + self:__Board( -0.1 ) end --- -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_CARGO#TASK_CARGO Task + -- @param Tasking.Task_Cargo#TASK_CARGO Task function Fsm:OnAfterBoard( TaskUnit, Task ) self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) @@ -344,13 +344,13 @@ do -- TASK_CARGO if self.Cargo:IsInRadius( TaskUnit:GetPointVec2() ) then if TaskUnit:InAir() then - self:__Land( -0.1 ) + --- ABORT the boarding. Split group if any and go back to select action. else - Task:GetMission():GetCommandCenter():MessageToGroup( "Boarding ...", TaskUnit:GetGroup(), "Boarding" ) + self.Cargo:MessageToGroup( "Boarding ...", TaskUnit:GetGroup() ) self.Cargo:Board( TaskUnit, 20, self ) end else - self:__ArriveAtCargo( -0.1 ) + --self:__ArriveAtCargo( -0.1 ) end end @@ -358,11 +358,11 @@ do -- TASK_CARGO --- -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_CARGO#TASK_CARGO Task + -- @param Tasking.Task_Cargo#TASK_CARGO Task function Fsm:OnAfterBoarded( TaskUnit, Task ) self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - Task:GetMission():GetCommandCenter():MessageToGroup( "Boarded ...", TaskUnit:GetGroup(), "Boarding" ) + self.Cargo:MessageToGroup( "Boarded ...", TaskUnit:GetGroup() ) self:__SelectAction( 1 ) end @@ -370,42 +370,43 @@ do -- TASK_CARGO --- -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_CARGO#TASK_CARGO Task + -- @param Tasking.Task_Cargo#TASK_CARGO Task function Fsm:OnAfterPrepareUnBoarding( TaskUnit, Task, From, Event, To, Cargo, DeployZone ) self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + self.Cargo = Cargo self.DeployZone = DeployZone - self.Cargo:__UnBoard( -0.1, DeployZone, 20 ) + self:__UnBoard( -0.1 ) end --- -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_CARGO#TASK_CARGO Task + -- @param Tasking.Task_Cargo#TASK_CARGO Task function Fsm:OnAfterUnBoard( TaskUnit, Task ) self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - function self.Cargo:OnEnterUnLoaded( From, Event, To, TaskUnit, TaskProcess ) + function self.Cargo:OnEnterUnLoaded( From, Event, To, DeployZone, TaskProcess ) self:E({From, Event, To, TaskUnit, TaskProcess }) - TaskProcess:__UnBoarded( 0.1 ) + TaskProcess:__UnBoarded( -0.1 ) end - Task:GetMission():GetCommandCenter():MessageToGroup( "UnBoarding ...", TaskUnit:GetGroup(), "UnBoarding" ) - self.Cargo:__UnBoard( -0.1, self.DeployZone, 20 ) + self.Cargo:MessageToGroup( "UnBoarding ...", TaskUnit:GetGroup() ) + self.Cargo:UnBoard( self.DeployZone:GetPointVec2(), 20, self ) end --- -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_CARGO#TASK_CARGO Task + -- @param Tasking.Task_Cargo#TASK_CARGO Task function Fsm:OnAfterUnBoarded( TaskUnit, Task ) self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - Task:GetMission():GetCommandCenter():MessageToGroup( "UnBoarded ...", TaskUnit:GetGroup(), "UnBoarding" ) + self.Cargo:MessageToGroup( "UnBoarded ...", TaskUnit:GetGroup() ) self:__SelectAction( 1 ) end From 798996efd204701d895f56dfc320c586f3d16dd3 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Fri, 14 Apr 2017 09:31:44 +0200 Subject: [PATCH 14/14] Documentation etc --- Moose Development/Moose/Core/Cargo.lua | 25 +- .../Moose/Tasking/Task_CARGO.lua | 127 ++++++-- docs/Documentation/AI_Balancer.html | 2 +- docs/Documentation/AI_Cap.html | 2 +- docs/Documentation/AI_Cas.html | 2 +- docs/Documentation/AI_Patrol.html | 5 +- docs/Documentation/Account.html | 2 +- docs/Documentation/Airbase.html | 2 +- docs/Documentation/AirbasePolice.html | 2 +- docs/Documentation/Assign.html | 2 +- docs/Documentation/Base.html | 22 +- docs/Documentation/Cargo.html | 273 ++++++++++++++---- docs/Documentation/CleanUp.html | 2 +- docs/Documentation/Client.html | 2 +- docs/Documentation/CommandCenter.html | 2 +- docs/Documentation/Controllable.html | 2 +- docs/Documentation/DCSAirbase.html | 2 +- docs/Documentation/DCSCoalitionObject.html | 2 +- docs/Documentation/DCSCommand.html | 2 +- docs/Documentation/DCSController.html | 2 +- docs/Documentation/DCSGroup.html | 2 +- docs/Documentation/DCSObject.html | 2 +- docs/Documentation/DCSTask.html | 2 +- docs/Documentation/DCSTypes.html | 2 +- docs/Documentation/DCSUnit.html | 2 +- docs/Documentation/DCSVec3.html | 2 +- docs/Documentation/DCSWorld.html | 2 +- docs/Documentation/DCSZone.html | 2 +- docs/Documentation/DCScountry.html | 2 +- docs/Documentation/DCStimer.html | 2 +- docs/Documentation/DCStrigger.html | 2 +- docs/Documentation/Database.html | 2 +- docs/Documentation/Detection.html | 4 +- docs/Documentation/DetectionManager.html | 2 +- docs/Documentation/Escort.html | 2 +- docs/Documentation/Event.html | 2 +- docs/Documentation/Fsm.html | 5 +- docs/Documentation/Group.html | 2 +- docs/Documentation/Identifiable.html | 2 +- docs/Documentation/MOVEMENT.html | 2 +- docs/Documentation/Menu.html | 2 +- docs/Documentation/Message.html | 2 +- docs/Documentation/MissileTrainer.html | 2 +- docs/Documentation/Mission.html | 2 +- docs/Documentation/Object.html | 2 +- docs/Documentation/Point.html | 3 +- docs/Documentation/Positionable.html | 2 +- docs/Documentation/Process_JTAC.html | 2 +- docs/Documentation/Process_Pickup.html | 2 +- docs/Documentation/Radio.html | 2 +- docs/Documentation/Route.html | 2 +- docs/Documentation/Scenery.html | 2 +- docs/Documentation/ScheduleDispatcher.html | 2 +- docs/Documentation/Scheduler.html | 2 +- docs/Documentation/Scoring.html | 2 +- docs/Documentation/Sead.html | 2 +- docs/Documentation/Set.html | 2 +- docs/Documentation/Smoke.html | 2 +- docs/Documentation/Spawn.html | 27 +- docs/Documentation/SpawnStatic.html | 2 +- docs/Documentation/Static.html | 2 +- docs/Documentation/StaticObject.html | 2 +- docs/Documentation/Task.html | 2 +- docs/Documentation/Task_A2G.html | 2 +- docs/Documentation/Task_A2G_Dispatcher.html | 2 +- docs/Documentation/Task_CARGO.html | 128 +++++++- docs/Documentation/Task_PICKUP.html | 2 +- docs/Documentation/Unit.html | 2 +- docs/Documentation/Utils.html | 2 +- docs/Documentation/Zone.html | 2 +- docs/Documentation/env.html | 2 +- docs/Documentation/index.html | 4 +- docs/Documentation/land.html | 2 +- docs/Documentation/routines.html | 2 +- docs/Presentations/CARGO/Dia1.JPG | Bin 265117 -> 197358 bytes docs/Presentations/CARGO/Dia2.JPG | Bin 185703 -> 189949 bytes docs/Presentations/CARGO/Dia3.JPG | Bin 211474 -> 198268 bytes docs/Presentations/CARGO/Dia4.JPG | Bin 263686 -> 199295 bytes docs/Presentations/CARGO/Dia5.JPG | Bin 263412 -> 200023 bytes docs/Presentations/CARGO/Dia6.JPG | Bin 264130 -> 200648 bytes docs/Presentations/CARGO/Dia7.JPG | Bin 267624 -> 201769 bytes docs/Presentations/CARGO/Dia8.JPG | Bin 264112 -> 202370 bytes 82 files changed, 559 insertions(+), 190 deletions(-) diff --git a/Moose Development/Moose/Core/Cargo.lua b/Moose Development/Moose/Core/Cargo.lua index 968b884b7..f8414b91f 100644 --- a/Moose Development/Moose/Core/Cargo.lua +++ b/Moose Development/Moose/Core/Cargo.lua @@ -6,13 +6,26 @@ -- -- Cargo can be of various forms, always are composed out of ONE object ( one unit or one static or one slingload crate ): -- --- * CARGO_UNIT, represented by a @{Unit} in a @{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. --- * 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. --- * 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. +-- * CARGO_UNIT, represented by a @{Unit} in a singleton @{Group}: Cargo can be represented by a Unit in a Group. a CARGO_UNIT is representable... +-- * CARGO_GROUP, represented by a @{Group}. A CARGO_GROUP is reportable... -- --- * CARGO_GROUP, represented by a Group of CARGO_UNITs. +-- ==== +-- +-- # Demo Missions +-- +-- ### [CARGO Demo Missions source code]() +-- +-- ### [CARGO Demo Missions, only for beta testers]() +-- +-- ### [ALL Demo Missions pack of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/releases) +-- +-- ==== +-- +-- # YouTube Channel +-- +-- ### [SPAWNSTATIC YouTube Channel]() +-- +-- ==== -- -- This module is still under construction, but is described above works already, and will keep working ... -- diff --git a/Moose Development/Moose/Tasking/Task_CARGO.lua b/Moose Development/Moose/Tasking/Task_CARGO.lua index cf7b3ea60..39a179600 100644 --- a/Moose Development/Moose/Tasking/Task_CARGO.lua +++ b/Moose Development/Moose/Tasking/Task_CARGO.lua @@ -4,15 +4,16 @@ -- -- ==== -- --- Cargo are units or cargo objects within DCS world that allow to be transported or sling loaded by other units. --- The CARGO class, as part of the moose core, is able to Board, Load, UnBoard and UnLoad from Carrier units. --- This collection of classes in this module define tasks for human players to handle cargo objects. +-- The Moose framework provides various CARGO classes that allow DCS phisical or logical objects to be transported or sling loaded by Carriers. +-- The CARGO_ classes, as part of the moose core, are able to Board, Load, UnBoard and UnLoad cargo between Carrier units. +-- +-- This collection of classes in this module define tasks for human players to handle these cargo objects. -- Cargo can be transported, picked-up, deployed and sling-loaded from and to other places. -- -- The following classes are important to consider: -- -- * @{#TASK_CARGO_TRANSPORT}: Defines a task for a human player to transport a set of cargo between various zones. --- +-- -- == -- -- # **API CHANGE HISTORY** @@ -46,9 +47,103 @@ do -- TASK_CARGO --- -- # TASK_CARGO class, extends @{Task#TASK} -- - -- The TASK_CARGO class defines @{Cargo} transport tasks, + -- ## A flexible tasking system + -- + -- The TASK_CARGO classes provide you with a flexible tasking sytem, + -- that allows you to transport cargo of various types between various locations + -- and various dedicated deployment zones. + -- + -- The cargo in scope of the TASK_CARGO classes must be explicitly given, and is of type SET_CARGO. + -- The SET_CARGO contains a collection of CARGO objects that must be handled by the players in the mission. + -- + -- + -- ## Task execution experience from the player perspective + -- + -- A human player can join the battle field in a client airborne slot or a ground vehicle within the CA module (ALT-J). + -- The player needs to accept the task from the task overview list within the mission, using the radio menus. + -- + -- Once the TASK_CARGO is assigned to the player and accepted by the player, the player will obtain + -- an extra **Cargo Handling Radio Menu** that contains the CARGO objects that need to be transported. + -- + -- Each CARGO object has a certain state: + -- + -- * **UnLoaded**: The CARGO is located within the battlefield. It may still need to be transported. + -- * **Loaded**: The CARGO is loaded within a Carrier. This can be your air unit, or another air unit, or even a vehicle. + -- * **Boarding**: The CARGO is running or moving towards your Carrier for loading. + -- * **UnBoarding**: The CARGO is driving or jumping out of your Carrier and moves to a location in the Deployment Zone. + -- + -- Cargo must be transported towards different **Deployment @{Zone}s**. + -- + -- The Cargo Handling Radio Menu system allows to execute **various actions** to handle the cargo. + -- In the menu, you'll find for each CARGO, that is part of the scope of the task, various actions that can be completed. + -- Depending on the location of your Carrier unit, the menu options will vary. + -- + -- + -- ## Cargo Pickup and Boarding + -- + -- For cargo boarding, a cargo can only execute the boarding actions if it is within the foreseen **Reporting Range**. + -- Therefore, it is important that you steer your Carrier within the Reporting Range, + -- so that boarding actions can be executed on the cargo. + -- To Pickup and Board cargo, the following menu items will be shown in your carrier radio menu: + -- + -- ### Board Cargo + -- + -- If your Carrier is within the Reporting Range of the cargo, it will allow to pickup the cargo by selecting this menu option. + -- Depending on the Cargo type, the cargo will either move to your Carrier or you will receive instructions how to handle the cargo + -- pickup. If the cargo moves to your carrier, it will indicate the boarding status. + -- Note that multiple units need to board your Carrier, so it is required to await the full boarding process. + -- Once the cargo is fully boarded within your Carrier, you will be notified of this. + -- + -- Note that for airborne Carriers, it is required to land first before the Boarding process can be initiated. + -- If during boarding the Carrier gets airborne, the boarding process will be cancelled. + -- + -- ## Pickup Cargo + -- + -- If your Carrier is not within the Reporting Range of the cargo, the HQ will guide you to its location. + -- Routing information is shown in flight that directs you to the cargo within Reporting Range. + -- Upon arrival, the Cargo will contact you and further instructions will be given. + -- When your Carrier is airborne, you will receive instructions to land your Carrier. + -- The action will not be completed until you've landed your Carrier. + -- + -- + -- ## Cargo Deploy and UnBoarding + -- + -- Various Deployment Zones can be foreseen in the scope of the Cargo transportation. Each deployment zone can be of varying @{Zone} type. + -- The Cargo Handling Radio Menu provides with menu options to execute an action to steer your Carrier to a specific Zone. + -- + -- ### UnBoard Cargo + -- + -- If your Carrier is already within a Deployment Zone, + -- then the Cargo Handling Radio Menu allows to **UnBoard** a specific cargo that is + -- loaded within your Carrier group into the Deployment Zone. + -- Note that the Unboarding process takes a while, as the cargo units (infantry or vehicles) must unload from your Carrier. + -- Ensure that you stay at the position or stay on the ground while Unboarding. + -- If any unforeseen manoeuvre is done by the Carrier, then the Unboarding will be cancelled. + -- + -- ### Deploy Cargo + -- + -- If your Carrier is not within a Deployment Zone, you'll need to fly towards one. + -- Fortunately, the Cargo Handling Radio Menu provides you with menu options to select a specific Deployment Zone to fly towards. + -- Once a Deployment Zone has been selected, your Carrier will receive routing information from HQ towards the Deployment Zone center. + -- Upon arrival, the HQ will provide you with further instructions. + -- When your Carrier is airborne, you will receive instructions to land your Carrier. + -- The action will not be completed until you've landed your Carrier! + -- + -- ## Handle TASK_CARGO Events ... + -- + -- The TASK_CARGO classes define @{Cargo} transport tasks, -- based on the tasking capabilities defined in @{Task#TASK}. - -- The TASK_CARGO is implemented using a @{Statemachine#FSM_TASK}, and has the following statuses: + -- + -- ### Specific TASK_CARGO Events + -- + -- Specific Cargo Handling event can be captured, that allow to trigger specific actions! + -- + -- * **Boarded**: Triggered when the Cargo has been Boarded into your Carrier. + -- * **UnBoarded**: Triggered when the cargo has been Unboarded from your Carrier and has arrived at the Deployment Zone. + -- + -- ### Standard TASK_CARGO Events + -- + -- The TASK_CARGO is implemented using a @{Statemachine#FSM_TASK}, and has the following standard statuses: -- -- * **None**: Start of the process. -- * **Planned**: The cargo task is planned. @@ -56,10 +151,6 @@ do -- TASK_CARGO -- * **Success**: The cargo task is successfully completed. -- * **Failed**: The cargo task has failed. This will happen if the player exists the task early, without communicating a possible cancellation to HQ. -- - -- # 1.1) Set the scoring of achievements in a cargo task. - -- - -- Scoring or penalties can be given in the following circumstances: - -- -- === -- -- @field #TASK_CARGO TASK_CARGO @@ -273,7 +364,7 @@ do -- TASK_CARGO -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit -- @param Tasking.Task_Cargo#TASK_CARGO Task - function Fsm:OnAfterLand( TaskUnit, Task, From, Event, To, Action ) + function Fsm:onafterLand( TaskUnit, Task, From, Event, To, Action ) self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) if self.Cargo:IsInRadius( TaskUnit:GetPointVec2() ) then @@ -297,7 +388,7 @@ do -- TASK_CARGO -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit -- @param Tasking.Task_Cargo#TASK_CARGO Task - function Fsm:OnAfterLanded( TaskUnit, Task, From, Event, To, Action ) + function Fsm:onafterLanded( TaskUnit, Task, From, Event, To, Action ) self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) if self.Cargo:IsInRadius( TaskUnit:GetPointVec2() ) then @@ -319,7 +410,7 @@ do -- TASK_CARGO -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit -- @param Tasking.Task_Cargo#TASK_CARGO Task - function Fsm:OnAfterPrepareBoarding( TaskUnit, Task, From, Event, To, Cargo ) + function Fsm:onafterPrepareBoarding( TaskUnit, Task, From, Event, To, Cargo ) self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) self.Cargo = Cargo -- Core.Cargo#CARGO_GROUP @@ -330,7 +421,7 @@ do -- TASK_CARGO -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit -- @param Tasking.Task_Cargo#TASK_CARGO Task - function Fsm:OnAfterBoard( TaskUnit, Task ) + function Fsm:onafterBoard( TaskUnit, Task ) self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) function self.Cargo:OnEnterLoaded( From, Event, To, TaskUnit, TaskProcess ) @@ -359,7 +450,7 @@ do -- TASK_CARGO -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit -- @param Tasking.Task_Cargo#TASK_CARGO Task - function Fsm:OnAfterBoarded( TaskUnit, Task ) + function Fsm:onafterBoarded( TaskUnit, Task ) self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) self.Cargo:MessageToGroup( "Boarded ...", TaskUnit:GetGroup() ) @@ -371,7 +462,7 @@ do -- TASK_CARGO -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit -- @param Tasking.Task_Cargo#TASK_CARGO Task - function Fsm:OnAfterPrepareUnBoarding( TaskUnit, Task, From, Event, To, Cargo, DeployZone ) + function Fsm:onafterPrepareUnBoarding( TaskUnit, Task, From, Event, To, Cargo, DeployZone ) self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) self.Cargo = Cargo @@ -383,7 +474,7 @@ do -- TASK_CARGO -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit -- @param Tasking.Task_Cargo#TASK_CARGO Task - function Fsm:OnAfterUnBoard( TaskUnit, Task ) + function Fsm:onafterUnBoard( TaskUnit, Task ) self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) function self.Cargo:OnEnterUnLoaded( From, Event, To, DeployZone, TaskProcess ) @@ -403,7 +494,7 @@ do -- TASK_CARGO -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit -- @param Tasking.Task_Cargo#TASK_CARGO Task - function Fsm:OnAfterUnBoarded( TaskUnit, Task ) + function Fsm:onafterUnBoarded( TaskUnit, Task ) self:E( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) self.Cargo:MessageToGroup( "UnBoarded ...", TaskUnit:GetGroup() ) diff --git a/docs/Documentation/AI_Balancer.html b/docs/Documentation/AI_Balancer.html index f8b0437fc..84f7b559e 100644 --- a/docs/Documentation/AI_Balancer.html +++ b/docs/Documentation/AI_Balancer.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/AI_Cap.html b/docs/Documentation/AI_Cap.html index 82c499fda..7b1a109bc 100644 --- a/docs/Documentation/AI_Cap.html +++ b/docs/Documentation/AI_Cap.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/AI_Cas.html b/docs/Documentation/AI_Cas.html index d4ebfc38b..4c0163c84 100644 --- a/docs/Documentation/AI_Cas.html +++ b/docs/Documentation/AI_Cas.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/AI_Patrol.html b/docs/Documentation/AI_Patrol.html index 74249623a..e21b9498f 100644 --- a/docs/Documentation/AI_Patrol.html +++ b/docs/Documentation/AI_Patrol.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • @@ -933,9 +933,6 @@ Use the method AIPATROLZONE.M - -

    This table contains the targets detected during patrol.

    -
    diff --git a/docs/Documentation/Account.html b/docs/Documentation/Account.html index 5d97ad3a9..134b635fa 100644 --- a/docs/Documentation/Account.html +++ b/docs/Documentation/Account.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Airbase.html b/docs/Documentation/Airbase.html index 3fce202ab..b08e91ce6 100644 --- a/docs/Documentation/Airbase.html +++ b/docs/Documentation/Airbase.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/AirbasePolice.html b/docs/Documentation/AirbasePolice.html index 697a7f431..698da3ed6 100644 --- a/docs/Documentation/AirbasePolice.html +++ b/docs/Documentation/AirbasePolice.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Assign.html b/docs/Documentation/Assign.html index 609590883..11f0041b7 100644 --- a/docs/Documentation/Assign.html +++ b/docs/Documentation/Assign.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Base.html b/docs/Documentation/Base.html index 7d2ff740c..7e2d36e6d 100644 --- a/docs/Documentation/Base.html +++ b/docs/Documentation/Base.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • @@ -523,6 +523,12 @@ When Moose is loaded statically, (as one file), tracing is switched off by defau
    + + + +
    AI_CARGO_UNIT.CargoCarrierCARGO_UNIT.CargoCarrier
    AI_CARGO_UNIT.CargoInAirCARGO_UNIT.CargoInAir
    AI_CARGO_UNIT.CargoObjectCARGO_UNIT.CargoObject
    AI_CARGO_UNIT:Destroy()CARGO_UNIT:Destroy() -

    AICARGOUNIT Destructor.

    +

    CARGO_UNIT Destructor.

    AI_CARGO_UNIT:New(CargoUnit, Type, Name, Weight, ReportRadius, NearRadius)CARGO_UNIT:New(CargoUnit, Type, Name, Weight, ReportRadius, NearRadius) -

    AICARGOUNIT Constructor.

    +

    CARGO_UNIT Constructor.

    AI_CARGO_UNIT.OnUnLoadedCallBackCARGO_UNIT.OnUnLoadedCallBack
    AI_CARGO_UNIT:onafterBoard(Event, From, To, CargoCarrier, ...)CARGO_UNIT:onafterBoard(Event, From, To, CargoCarrier, NearRadius, ...)

    Board Event.

    AI_CARGO_UNIT:onafterUnBoarding(Event, From, To, ToPointVec2)CARGO_UNIT:onafterUnBoarding(Event, From, To, ToPointVec2)

    UnBoard Event.

    AI_CARGO_UNIT:onenterBoarding(Event, From, To, CargoCarrier, ...)CARGO_UNIT:onenterBoarding(Event, From, To, CargoCarrier, NearRadius, ...)

    Enter Boarding State.

    AI_CARGO_UNIT:onenterLoaded(Event, From, To, CargoCarrier)CARGO_UNIT:onenterLoaded(Event, From, To, CargoCarrier)

    Loaded State.

    AI_CARGO_UNIT:onenterUnBoarding(Event, From, To, ToPointVec2)CARGO_UNIT:onenterUnBoarding(Event, From, To, ToPointVec2)

    Enter UnBoarding State.

    AI_CARGO_UNIT:onenterUnLoaded(Event, From, To, Core, ToPointVec2)CARGO_UNIT:onenterUnLoaded(Event, From, To, Core, ToPointVec2)

    Enter UnLoaded State.

    AI_CARGO_UNIT:onleaveBoarding(Event, From, To, CargoCarrier, ...)CARGO_UNIT:onleaveBoarding(Event, From, To, CargoCarrier, NearRadius, ...)

    Leave Boarding State.

    AI_CARGO_UNIT:onleaveUnBoarding(Event, From, To, ToPointVec2)CARGO_UNIT:onleaveUnBoarding(Event, From, To, ToPointVec2)

    Leave UnBoarding State.

    Cargo -

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

    - -

    Banner Image

    - -
    - -

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

    Core -- Management of CARGO logistics, that can be transported from and to transportation carriers.

    FORMATION.Cone

    A cone formation.

    +
    FORMATION.Vee +
    @@ -2262,6 +2268,20 @@ A #table or any field.

    A cone formation.

    + +
    +
    +
    + + #string + +FORMATION.Vee + +
    +
    + + +
    diff --git a/docs/Documentation/Cargo.html b/docs/Documentation/Cargo.html index 8feac7d7c..b6abf92f7 100644 --- a/docs/Documentation/Cargo.html +++ b/docs/Documentation/Cargo.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • @@ -104,14 +104,28 @@

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

      -
    • CARGO_UNIT, represented by a Unit in a 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.

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

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

    • -
    • CARGOGROUP, represented by a Group of CARGOUNITs.

    • +
    • CARGO_UNIT, represented by a Unit in a singleton Group: Cargo can be represented by a Unit in a Group. a CARGO_UNIT is representable...
    • +
    • CARGO_GROUP, represented by a Group. A CARGO_GROUP is reportable...
    +
    + +

    Demo Missions

    + +

    CARGO Demo Missions source code

    + +

    CARGO Demo Missions, only for beta testers

    + +

    ALL Demo Missions pack of the last release

    + +
    + +

    YouTube Channel

    + +

    SPAWNSTATIC YouTube Channel

    + +
    +

    This module is still under construction, but is described above works already, and will keep working ...

    @@ -190,12 +204,6 @@ CARGO.Containable

    This flag defines if the cargo can be contained within a DCS Unit.

    - - - - CARGO:GetBoardingRange() - -

    Get the range till cargo will board.

    @@ -395,6 +403,12 @@

    Type CARGO_GROUP

    + + + + - + - + - + - + - + - + - + @@ -523,15 +537,33 @@

    Type CARGO_REPORTABLE

    CARGO_GROUP.CargoCarrier + +
    CARGO_GROUP.CargoSet @@ -407,43 +421,43 @@
    CARGO_GROUP:onafterUnBoarding(ToPointVec2, Event, From, To)CARGO_GROUP:onafterUnBoarding(ToPointVec2, Event, From, To, NearRadius, ...)

    UnBoard Event.

    CARGO_GROUP:onenterBoarding(CargoCarrier, Event, From, To)CARGO_GROUP:onenterBoarding(CargoCarrier, Event, From, To, NearRadius, ...)

    Enter Boarding State.

    CARGO_GROUP:onenterLoaded(CargoCarrier, Event, From, To)CARGO_GROUP:onenterLoaded(CargoCarrier, Event, From, To, ...)

    Enter Loaded State.

    CARGO_GROUP:onenterUnBoarding(ToPointVec2, Event, From, To)CARGO_GROUP:onenterUnBoarding(ToPointVec2, Event, From, To, NearRadius, ...)

    Enter UnBoarding State.

    CARGO_GROUP:onenterUnLoaded(Core, Event, From, To, ToPointVec2)CARGO_GROUP:onenterUnLoaded(Core, Event, From, To, ToPointVec2, ...)

    Enter UnLoaded State.

    CARGO_GROUP:onleaveBoarding(CargoCarrier, Event, From, To)CARGO_GROUP:onleaveBoarding(CargoCarrier, Event, From, To, NearRadius, ...)

    Leave Boarding State.

    CARGO_GROUP:onleaveUnBoarding(ToPointVec2, Event, From, To)CARGO_GROUP:onleaveUnBoarding(ToPointVec2, Event, From, To, NearRadius, ...)

    Leave UnBoarding State.

    + + + + + + + + + + + + @@ -615,7 +647,7 @@ - + @@ -633,7 +665,7 @@ - + @@ -651,7 +683,7 @@ - + @@ -898,24 +930,6 @@ The radius when the cargo will board the Carrier (to avoid collision).

    - -CARGO:GetBoardingRange() - -
    -
    - -

    Get the range till cargo will board.

    - -

    Return value

    - -

    #number: -The range till cargo will board.

    - -
    -
    -
    -
    - CARGO:GetName() @@ -1671,6 +1685,20 @@ The amount of seconds to delay the action.

    + + +CARGO_GROUP.CargoCarrier + +
    +
    + + + +
    +
    +
    +
    + CARGO_GROUP.CargoSet @@ -1734,7 +1762,7 @@ The amount of seconds to delay the action.

    -CARGO_GROUP:onafterUnBoarding(ToPointVec2, Event, From, To) +CARGO_GROUP:onafterUnBoarding(ToPointVec2, Event, From, To, NearRadius, ...)
    @@ -1762,6 +1790,16 @@ The amount of seconds to delay the action.

    #string To :

    + +
  • + +

    NearRadius :

    + +
  • +
  • + +

    ... :

    +
  • @@ -1770,7 +1808,7 @@ The amount of seconds to delay the action.

    -CARGO_GROUP:onenterBoarding(CargoCarrier, Event, From, To) +CARGO_GROUP:onenterBoarding(CargoCarrier, Event, From, To, NearRadius, ...)
    @@ -1798,6 +1836,16 @@ The amount of seconds to delay the action.

    #string To :

    + +
  • + +

    NearRadius :

    + +
  • +
  • + +

    ... :

    +
  • @@ -1806,7 +1854,7 @@ The amount of seconds to delay the action.

    -CARGO_GROUP:onenterLoaded(CargoCarrier, Event, From, To) +CARGO_GROUP:onenterLoaded(CargoCarrier, Event, From, To, ...)
    @@ -1834,6 +1882,11 @@ The amount of seconds to delay the action.

    #string To :

    + +
  • + +

    ... :

    +
  • @@ -1842,7 +1895,7 @@ The amount of seconds to delay the action.

    -CARGO_GROUP:onenterUnBoarding(ToPointVec2, Event, From, To) +CARGO_GROUP:onenterUnBoarding(ToPointVec2, Event, From, To, NearRadius, ...)
    @@ -1870,6 +1923,16 @@ The amount of seconds to delay the action.

    #string To :

    + +
  • + +

    NearRadius :

    + +
  • +
  • + +

    ... :

    +
  • @@ -1878,7 +1941,7 @@ The amount of seconds to delay the action.

    -CARGO_GROUP:onenterUnLoaded(Core, Event, From, To, ToPointVec2) +CARGO_GROUP:onenterUnLoaded(Core, Event, From, To, ToPointVec2, ...)
    @@ -1912,6 +1975,11 @@ Point#POINT_VEC2

    ToPointVec2 :

    + +
  • + +

    ... :

    +
  • @@ -1920,7 +1988,7 @@ Point#POINT_VEC2

    -CARGO_GROUP:onleaveBoarding(CargoCarrier, Event, From, To) +CARGO_GROUP:onleaveBoarding(CargoCarrier, Event, From, To, NearRadius, ...)
    @@ -1948,6 +2016,16 @@ Point#POINT_VEC2

    #string To :

    + +
  • + +

    NearRadius :

    + +
  • +
  • + +

    ... :

    +
  • @@ -1956,7 +2034,7 @@ Point#POINT_VEC2

    -CARGO_GROUP:onleaveUnBoarding(ToPointVec2, Event, From, To) +CARGO_GROUP:onleaveUnBoarding(ToPointVec2, Event, From, To, NearRadius, ...)
    @@ -1984,6 +2062,16 @@ Point#POINT_VEC2

    #string To :

    + +
  • + +

    NearRadius :

    + +
  • +
  • + +

    ... :

    +
  • @@ -2435,6 +2523,20 @@ The UNIT carrying the package.

    + + +CARGO_REPORTABLE.CargoObject + +
    +
    + + + +
    +
    +
    +
    + #string CARGO_REPORTABLE.ClassName @@ -2444,6 +2546,24 @@ The UNIT carrying the package.

    + +
    +
    +
    + + +CARGO_REPORTABLE:GetBoardingRange() + +
    +
    + +

    Get the range till cargo will board.

    + +

    Return value

    + +

    #number: +The range till cargo will board.

    +
    @@ -2475,6 +2595,38 @@ The UNIT carrying the package.

    + +CARGO_REPORTABLE:MessageToGroup(Message, TaskGroup, Name) + +
    +
    + +

    Send a CC message to a GROUP.

    + +

    Parameters

    +
      +
    • + +

      #string Message :

      + +
    • +
    • + +

      Wrapper.Group#GROUP TaskGroup :

      + +
    • +
    • + +

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

      + +
    • +
    +
    +
    +
    +
    + CARGO_REPORTABLE:New(CargoObject, Type, Name, Weight, ReportRadius, NearRadius) @@ -2817,7 +2969,7 @@ The UNIT carrying the package.

    -CARGO_UNIT:onafterUnBoarding(Event, From, To, ToPointVec2) +CARGO_UNIT:onafterUnBoarding(Event, From, To, ToPointVec2, NearRadius)
    @@ -2845,6 +2997,11 @@ The UNIT carrying the package.

    Core.Point#POINT_VEC2 ToPointVec2 :

    + +
  • + +

    NearRadius :

    +
  • @@ -2935,7 +3092,7 @@ The UNIT carrying the package.

    -CARGO_UNIT:onenterUnBoarding(Event, From, To, ToPointVec2) +CARGO_UNIT:onenterUnBoarding(Event, From, To, ToPointVec2, NearRadius)
    @@ -2963,6 +3120,11 @@ The UNIT carrying the package.

    Core.Point#POINT_VEC2 ToPointVec2 :

    + +
  • + +

    NearRadius :

    +
  • @@ -3059,7 +3221,7 @@ Point#POINT_VEC2

    -CARGO_UNIT:onleaveUnBoarding(Event, From, To, ToPointVec2) +CARGO_UNIT:onleaveUnBoarding(Event, From, To, ToPointVec2, NearRadius)
    @@ -3087,11 +3249,20 @@ Point#POINT_VEC2

    Core.Point#POINT_VEC2 ToPointVec2 :

    + +
  • + +

    NearRadius :

    +
  • +

    Type COMMANDCENTER

    + +

    Type sring

    + diff --git a/docs/Documentation/CleanUp.html b/docs/Documentation/CleanUp.html index 56a491705..fa84131e1 100644 --- a/docs/Documentation/CleanUp.html +++ b/docs/Documentation/CleanUp.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Client.html b/docs/Documentation/Client.html index 136b4cd21..d4c925d9b 100644 --- a/docs/Documentation/Client.html +++ b/docs/Documentation/Client.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/CommandCenter.html b/docs/Documentation/CommandCenter.html index bcf129710..c3e048507 100644 --- a/docs/Documentation/CommandCenter.html +++ b/docs/Documentation/CommandCenter.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Controllable.html b/docs/Documentation/Controllable.html index be444f9a9..2aeac8f53 100644 --- a/docs/Documentation/Controllable.html +++ b/docs/Documentation/Controllable.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/DCSAirbase.html b/docs/Documentation/DCSAirbase.html index 5e7df20f9..bcd075f7d 100644 --- a/docs/Documentation/DCSAirbase.html +++ b/docs/Documentation/DCSAirbase.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/DCSCoalitionObject.html b/docs/Documentation/DCSCoalitionObject.html index 9adc3b0fb..ab1cd3fc9 100644 --- a/docs/Documentation/DCSCoalitionObject.html +++ b/docs/Documentation/DCSCoalitionObject.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/DCSCommand.html b/docs/Documentation/DCSCommand.html index 375f7891f..8f1be1fb1 100644 --- a/docs/Documentation/DCSCommand.html +++ b/docs/Documentation/DCSCommand.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/DCSController.html b/docs/Documentation/DCSController.html index 1a1714fcc..180e4d977 100644 --- a/docs/Documentation/DCSController.html +++ b/docs/Documentation/DCSController.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/DCSGroup.html b/docs/Documentation/DCSGroup.html index 4236a724c..958ed0031 100644 --- a/docs/Documentation/DCSGroup.html +++ b/docs/Documentation/DCSGroup.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/DCSObject.html b/docs/Documentation/DCSObject.html index b70eab2b0..88b67d3a5 100644 --- a/docs/Documentation/DCSObject.html +++ b/docs/Documentation/DCSObject.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/DCSTask.html b/docs/Documentation/DCSTask.html index d68a0619b..32a1bd5a7 100644 --- a/docs/Documentation/DCSTask.html +++ b/docs/Documentation/DCSTask.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/DCSTypes.html b/docs/Documentation/DCSTypes.html index 6d8ff9743..eac45e2ae 100644 --- a/docs/Documentation/DCSTypes.html +++ b/docs/Documentation/DCSTypes.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/DCSUnit.html b/docs/Documentation/DCSUnit.html index cbc0b5504..27f223811 100644 --- a/docs/Documentation/DCSUnit.html +++ b/docs/Documentation/DCSUnit.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/DCSVec3.html b/docs/Documentation/DCSVec3.html index b7d80f43a..16c845eb4 100644 --- a/docs/Documentation/DCSVec3.html +++ b/docs/Documentation/DCSVec3.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/DCSWorld.html b/docs/Documentation/DCSWorld.html index 20843d631..b4188357b 100644 --- a/docs/Documentation/DCSWorld.html +++ b/docs/Documentation/DCSWorld.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/DCSZone.html b/docs/Documentation/DCSZone.html index 8133929e9..97af5943c 100644 --- a/docs/Documentation/DCSZone.html +++ b/docs/Documentation/DCSZone.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/DCScountry.html b/docs/Documentation/DCScountry.html index 6a611ad74..e62671d74 100644 --- a/docs/Documentation/DCScountry.html +++ b/docs/Documentation/DCScountry.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/DCStimer.html b/docs/Documentation/DCStimer.html index 9a1b8b7ca..d4d84f593 100644 --- a/docs/Documentation/DCStimer.html +++ b/docs/Documentation/DCStimer.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/DCStrigger.html b/docs/Documentation/DCStrigger.html index 5a5901245..06e11aa76 100644 --- a/docs/Documentation/DCStrigger.html +++ b/docs/Documentation/DCStrigger.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Database.html b/docs/Documentation/Database.html index 498dd5ed5..dfc8a9bb5 100644 --- a/docs/Documentation/Database.html +++ b/docs/Documentation/Database.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Detection.html b/docs/Documentation/Detection.html index 8c2feee50..dc52c4036 100644 --- a/docs/Documentation/Detection.html +++ b/docs/Documentation/Detection.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • @@ -2172,7 +2172,6 @@ self

    - #number DETECTION_BASE.DetectedItemCount @@ -2186,7 +2185,6 @@ self

    - #number DETECTION_BASE.DetectedItemMax diff --git a/docs/Documentation/DetectionManager.html b/docs/Documentation/DetectionManager.html index 601843371..f44b7a2a7 100644 --- a/docs/Documentation/DetectionManager.html +++ b/docs/Documentation/DetectionManager.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Escort.html b/docs/Documentation/Escort.html index 2a5aadb20..52cc36d94 100644 --- a/docs/Documentation/Escort.html +++ b/docs/Documentation/Escort.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Event.html b/docs/Documentation/Event.html index ba837766d..5de725053 100644 --- a/docs/Documentation/Event.html +++ b/docs/Documentation/Event.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Fsm.html b/docs/Documentation/Fsm.html index 9c7864cf4..47a295893 100644 --- a/docs/Documentation/Fsm.html +++ b/docs/Documentation/Fsm.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • @@ -1582,7 +1582,7 @@ A string defining the start state.

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

    - FSM.current diff --git a/docs/Documentation/Group.html b/docs/Documentation/Group.html index 3c04c3719..2a7095a6d 100644 --- a/docs/Documentation/Group.html +++ b/docs/Documentation/Group.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Identifiable.html b/docs/Documentation/Identifiable.html index 98830226c..3d15ed716 100644 --- a/docs/Documentation/Identifiable.html +++ b/docs/Documentation/Identifiable.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/MOVEMENT.html b/docs/Documentation/MOVEMENT.html index eb273ac47..d3966610d 100644 --- a/docs/Documentation/MOVEMENT.html +++ b/docs/Documentation/MOVEMENT.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Menu.html b/docs/Documentation/Menu.html index fbea1030e..e6a65ed88 100644 --- a/docs/Documentation/Menu.html +++ b/docs/Documentation/Menu.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Message.html b/docs/Documentation/Message.html index 24d3bd5dc..355d0c6ab 100644 --- a/docs/Documentation/Message.html +++ b/docs/Documentation/Message.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/MissileTrainer.html b/docs/Documentation/MissileTrainer.html index b3e9f1343..3664bf7a8 100644 --- a/docs/Documentation/MissileTrainer.html +++ b/docs/Documentation/MissileTrainer.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Mission.html b/docs/Documentation/Mission.html index c40133f87..1dfbf5f58 100644 --- a/docs/Documentation/Mission.html +++ b/docs/Documentation/Mission.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Object.html b/docs/Documentation/Object.html index 67b078a51..a8198242c 100644 --- a/docs/Documentation/Object.html +++ b/docs/Documentation/Object.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Point.html b/docs/Documentation/Point.html index d01447df5..8eade306d 100644 --- a/docs/Documentation/Point.html +++ b/docs/Documentation/Point.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • @@ -1367,7 +1367,6 @@ The new calculated POINT_VEC2.

    - POINT_VEC2.z diff --git a/docs/Documentation/Positionable.html b/docs/Documentation/Positionable.html index 1073789e9..1e498e791 100644 --- a/docs/Documentation/Positionable.html +++ b/docs/Documentation/Positionable.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Process_JTAC.html b/docs/Documentation/Process_JTAC.html index 5520fb76d..f3e75bb9b 100644 --- a/docs/Documentation/Process_JTAC.html +++ b/docs/Documentation/Process_JTAC.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Process_Pickup.html b/docs/Documentation/Process_Pickup.html index 12ecdef87..6967c3780 100644 --- a/docs/Documentation/Process_Pickup.html +++ b/docs/Documentation/Process_Pickup.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Radio.html b/docs/Documentation/Radio.html index b52f23dfe..1254f064b 100644 --- a/docs/Documentation/Radio.html +++ b/docs/Documentation/Radio.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Route.html b/docs/Documentation/Route.html index 77e6ce903..837c6c28f 100644 --- a/docs/Documentation/Route.html +++ b/docs/Documentation/Route.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Scenery.html b/docs/Documentation/Scenery.html index 1ef03a861..f0530ef7a 100644 --- a/docs/Documentation/Scenery.html +++ b/docs/Documentation/Scenery.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/ScheduleDispatcher.html b/docs/Documentation/ScheduleDispatcher.html index 469faadf1..0175998ff 100644 --- a/docs/Documentation/ScheduleDispatcher.html +++ b/docs/Documentation/ScheduleDispatcher.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Scheduler.html b/docs/Documentation/Scheduler.html index d3e10ff41..78118365a 100644 --- a/docs/Documentation/Scheduler.html +++ b/docs/Documentation/Scheduler.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Scoring.html b/docs/Documentation/Scoring.html index 13927d8ea..7868ecda1 100644 --- a/docs/Documentation/Scoring.html +++ b/docs/Documentation/Scoring.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Sead.html b/docs/Documentation/Sead.html index 93b017bb7..06dcb8af4 100644 --- a/docs/Documentation/Sead.html +++ b/docs/Documentation/Sead.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Set.html b/docs/Documentation/Set.html index ce2588b24..dff2ef289 100644 --- a/docs/Documentation/Set.html +++ b/docs/Documentation/Set.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Smoke.html b/docs/Documentation/Smoke.html index 798744966..c4b4c2e9a 100644 --- a/docs/Documentation/Smoke.html +++ b/docs/Documentation/Smoke.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Spawn.html b/docs/Documentation/Spawn.html index 3b523abef..b8d4286eb 100644 --- a/docs/Documentation/Spawn.html +++ b/docs/Documentation/Spawn.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • @@ -810,12 +810,6 @@ and any spaces before and after the resulting name are removed.

    - - - -
    CARGO_REPORTABLE.CargoObject + +
    CARGO_REPORTABLE.ClassName +
    CARGO_REPORTABLE:GetBoardingRange() +

    Get the range till cargo will board.

    CARGO_REPORTABLE:IsInRadius(PointVec2)

    Check if CargoCarrier is in the ReportRadius for the Cargo to be Loaded.

    +
    CARGO_REPORTABLE:MessageToGroup(Message, TaskGroup, Name) +

    Send a CC message to a GROUP.

    CARGO_UNIT:onafterUnBoarding(Event, From, To, ToPointVec2)CARGO_UNIT:onafterUnBoarding(Event, From, To, ToPointVec2, NearRadius)

    UnBoard Event.

    CARGO_UNIT:onenterUnBoarding(Event, From, To, ToPointVec2)CARGO_UNIT:onenterUnBoarding(Event, From, To, ToPointVec2, NearRadius)

    Enter UnBoarding State.

    CARGO_UNIT:onleaveUnBoarding(Event, From, To, ToPointVec2)CARGO_UNIT:onleaveUnBoarding(Event, From, To, ToPointVec2, NearRadius)

    Leave UnBoarding State.

    SPAWN:_TranslateRotate(SpawnIndex, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle) -
    SPAWN.uncontrolled -
    @@ -2117,6 +2111,9 @@ The group that was spawned. You can use this group for further actions.

    + +

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

    +
    @@ -2991,7 +2988,7 @@ Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):Schedule( 600, 0.5 ) -

    When the first Spawn executes, all the Groups need to be made visible before start.

    +

    Flag that indicates if all the Groups of the SpawnGroup need to be visible when Spawned.

    @@ -3557,20 +3554,6 @@ True = Continue Scheduler

    - - -
    -
    - - - -SPAWN.uncontrolled - -
    -
    - - -
    diff --git a/docs/Documentation/SpawnStatic.html b/docs/Documentation/SpawnStatic.html index 6d4d01653..5413f9a2e 100644 --- a/docs/Documentation/SpawnStatic.html +++ b/docs/Documentation/SpawnStatic.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Static.html b/docs/Documentation/Static.html index 4f2fb8c63..82fa993f2 100644 --- a/docs/Documentation/Static.html +++ b/docs/Documentation/Static.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/StaticObject.html b/docs/Documentation/StaticObject.html index 05ee02014..1a46c6616 100644 --- a/docs/Documentation/StaticObject.html +++ b/docs/Documentation/StaticObject.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Task.html b/docs/Documentation/Task.html index c47821179..edde8df5d 100644 --- a/docs/Documentation/Task.html +++ b/docs/Documentation/Task.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Task_A2G.html b/docs/Documentation/Task_A2G.html index c3ff3617d..e497997f4 100644 --- a/docs/Documentation/Task_A2G.html +++ b/docs/Documentation/Task_A2G.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Task_A2G_Dispatcher.html b/docs/Documentation/Task_A2G_Dispatcher.html index 3ca561091..553474a4c 100644 --- a/docs/Documentation/Task_A2G_Dispatcher.html +++ b/docs/Documentation/Task_A2G_Dispatcher.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Task_CARGO.html b/docs/Documentation/Task_CARGO.html index 321ecaa1a..d6a95cfc8 100644 --- a/docs/Documentation/Task_CARGO.html +++ b/docs/Documentation/Task_CARGO.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • @@ -91,7 +91,7 @@
    -

    Module Task_CARGO

    +

    Module Task_Cargo

    Tasking (Release 2.1) -- The TASK_CARGO models tasks for players to transport Cargo.

    @@ -101,9 +101,10 @@
    -

    Cargo are units or cargo objects within DCS world that allow to be transported or sling loaded by other units. -The CARGO class, as part of the moose core, is able to Board, Load, UnBoard and UnLoad from Carrier units. -This collection of classes in this module define tasks for human players to handle cargo objects. +

    The Moose framework provides various CARGO classes that allow DCS phisical or logical objects to be transported or sling loaded by Carriers. +The CARGO_ classes, as part of the moose core, are able to Board, Load, UnBoard and UnLoad cargo between Carrier units.

    + +

    This collection of classes in this module define tasks for human players to handle these cargo objects. Cargo can be transported, picked-up, deployed and sling-loaded from and to other places.

    The following classes are important to consider:

    @@ -147,8 +148,11 @@ Cargo can be transported, picked-up, deployed and sling-loaded from and to other

    TASK_CARGO class, extends Task#TASK

    -

    The TASK_CARGO class defines Cargo transport tasks, -based on the tasking capabilities defined in Task#TASK.

    +

    A flexible tasking system

    + +

    The TASK_CARGO classes provide you with a flexible tasking sytem, +that allows you to transport cargo of various types between various locations +and various dedicated deployment zones.

    @@ -291,11 +295,109 @@ based on the tasking capabilities defined in Task#TA

    TASK_CARGO class, extends Task#TASK

    -

    The TASK_CARGO class defines Cargo transport tasks, -based on the tasking capabilities defined in Task#TASK.

    +

    A flexible tasking system

    + +

    The TASK_CARGO classes provide you with a flexible tasking sytem, +that allows you to transport cargo of various types between various locations +and various dedicated deployment zones.

    -

    The TASK_CARGO is implemented using a Statemachine#FSM_TASK, and has the following statuses:

    + +

    The cargo in scope of the TASKCARGO classes must be explicitly given, and is of type SETCARGO. +The SET_CARGO contains a collection of CARGO objects that must be handled by the players in the mission.

    + + +

    Task execution experience from the player perspective

    + +

    A human player can join the battle field in a client airborne slot or a ground vehicle within the CA module (ALT-J). +The player needs to accept the task from the task overview list within the mission, using the radio menus.

    + +

    Once the TASK_CARGO is assigned to the player and accepted by the player, the player will obtain +an extra Cargo Handling Radio Menu that contains the CARGO objects that need to be transported.

    + +

    Each CARGO object has a certain state:

    + +
      +
    • UnLoaded: The CARGO is located within the battlefield. It may still need to be transported.
    • +
    • Loaded: The CARGO is loaded within a Carrier. This can be your air unit, or another air unit, or even a vehicle.
    • +
    • Boarding: The CARGO is running or moving towards your Carrier for loading.
    • +
    • UnBoarding: The CARGO is driving or jumping out of your Carrier and moves to a location in the Deployment Zone.
    • +
    + +

    Cargo must be transported towards different **Deployment Zones**.

    + +

    The Cargo Handling Radio Menu system allows to execute various actions to handle the cargo. +In the menu, you'll find for each CARGO, that is part of the scope of the task, various actions that can be completed. +Depending on the location of your Carrier unit, the menu options will vary.

    + + +

    Cargo Pickup and Boarding

    + +

    For cargo boarding, a cargo can only execute the boarding actions if it is within the foreseen Reporting Range. +Therefore, it is important that you steer your Carrier within the Reporting Range, +so that boarding actions can be executed on the cargo. +To Pickup and Board cargo, the following menu items will be shown in your carrier radio menu:

    + +

    Board Cargo

    + +

    If your Carrier is within the Reporting Range of the cargo, it will allow to pickup the cargo by selecting this menu option. +Depending on the Cargo type, the cargo will either move to your Carrier or you will receive instructions how to handle the cargo +pickup. If the cargo moves to your carrier, it will indicate the boarding status. +Note that multiple units need to board your Carrier, so it is required to await the full boarding process. +Once the cargo is fully boarded within your Carrier, you will be notified of this.

    + +

    Note that for airborne Carriers, it is required to land first before the Boarding process can be initiated. +If during boarding the Carrier gets airborne, the boarding process will be cancelled.

    + +

    Pickup Cargo

    + +

    If your Carrier is not within the Reporting Range of the cargo, the HQ will guide you to its location. +Routing information is shown in flight that directs you to the cargo within Reporting Range. +Upon arrival, the Cargo will contact you and further instructions will be given. +When your Carrier is airborne, you will receive instructions to land your Carrier. +The action will not be completed until you've landed your Carrier.

    + + +

    Cargo Deploy and UnBoarding

    + +

    Various Deployment Zones can be foreseen in the scope of the Cargo transportation. Each deployment zone can be of varying Zone type. +The Cargo Handling Radio Menu provides with menu options to execute an action to steer your Carrier to a specific Zone.

    + +

    UnBoard Cargo

    + +

    If your Carrier is already within a Deployment Zone, +then the Cargo Handling Radio Menu allows to UnBoard a specific cargo that is +loaded within your Carrier group into the Deployment Zone. +Note that the Unboarding process takes a while, as the cargo units (infantry or vehicles) must unload from your Carrier. +Ensure that you stay at the position or stay on the ground while Unboarding. +If any unforeseen manoeuvre is done by the Carrier, then the Unboarding will be cancelled.

    + +

    Deploy Cargo

    + +

    If your Carrier is not within a Deployment Zone, you'll need to fly towards one. +Fortunately, the Cargo Handling Radio Menu provides you with menu options to select a specific Deployment Zone to fly towards. +Once a Deployment Zone has been selected, your Carrier will receive routing information from HQ towards the Deployment Zone center. +Upon arrival, the HQ will provide you with further instructions. +When your Carrier is airborne, you will receive instructions to land your Carrier. +The action will not be completed until you've landed your Carrier!

    + +

    Handle TASK_CARGO Events ...

    + +

    The TASK_CARGO classes define Cargo transport tasks, +based on the tasking capabilities defined in Task#TASK.

    + +

    Specific TASK_CARGO Events

    + +

    Specific Cargo Handling event can be captured, that allow to trigger specific actions!

    + +
      +
    • Boarded: Triggered when the Cargo has been Boarded into your Carrier.
    • +
    • UnBoarded: Triggered when the cargo has been Unboarded from your Carrier and has arrived at the Deployment Zone.
    • +
    + +

    Standard TASK_CARGO Events

    + +

    The TASK_CARGO is implemented using a Statemachine#FSM_TASK, and has the following standard statuses:

    -

    1.1) Set the scoring of achievements in a cargo task.

    - -

    Scoring or penalties can be given in the following circumstances:

    -
    @@ -328,7 +426,7 @@ based on the tasking capabilities defined in
    Task#TA -

    Type Task_CARGO

    +

    Type Task_Cargo

    Type FSM_PROCESS

    Field(s)

    diff --git a/docs/Documentation/Task_PICKUP.html b/docs/Documentation/Task_PICKUP.html index 07ad14a68..a26b39707 100644 --- a/docs/Documentation/Task_PICKUP.html +++ b/docs/Documentation/Task_PICKUP.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Unit.html b/docs/Documentation/Unit.html index cfb7e775a..d9cc8dfda 100644 --- a/docs/Documentation/Unit.html +++ b/docs/Documentation/Unit.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Utils.html b/docs/Documentation/Utils.html index 582c54d57..553a2be0e 100644 --- a/docs/Documentation/Utils.html +++ b/docs/Documentation/Utils.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/Zone.html b/docs/Documentation/Zone.html index 45cc41b92..57eba9a61 100644 --- a/docs/Documentation/Zone.html +++ b/docs/Documentation/Zone.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/env.html b/docs/Documentation/env.html index 2701111a8..03986352d 100644 --- a/docs/Documentation/env.html +++ b/docs/Documentation/env.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/index.html b/docs/Documentation/index.html index 78c9b22f9..91ea5f63f 100644 --- a/docs/Documentation/index.html +++ b/docs/Documentation/index.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • @@ -510,7 +510,7 @@ and creates a CSV file logging the scoring events and results for use at team or - Task_CARGO + Task_Cargo

    Tasking (Release 2.1) -- The TASK_CARGO models tasks for players to transport Cargo.

    diff --git a/docs/Documentation/land.html b/docs/Documentation/land.html index 012941041..f6b7ca91c 100644 --- a/docs/Documentation/land.html +++ b/docs/Documentation/land.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Documentation/routines.html b/docs/Documentation/routines.html index 0c85c750f..2503b891b 100644 --- a/docs/Documentation/routines.html +++ b/docs/Documentation/routines.html @@ -80,7 +80,7 @@
  • Task
  • Task_A2G
  • Task_A2G_Dispatcher
  • -
  • Task_CARGO
  • +
  • Task_Cargo
  • Task_PICKUP
  • Unit
  • Utils
  • diff --git a/docs/Presentations/CARGO/Dia1.JPG b/docs/Presentations/CARGO/Dia1.JPG index c38ff1e021cd41af221f5c7f133f36db554c133e..3096fad0517f6fd09af963e7d76e9196cf289c15 100644 GIT binary patch delta 139416 zcmZsiby$;ey!L0Jf+8wiDkxGSASE#i5s8U(PC;5gN^0z}03`(kl$uKC=w_2{kQ^PO zdjmGsygTPy=RNPau9yFJZ9KoY@B8z;pR}ZlJ0WZ(R>J^c49DGGBxYc;x9xlXdeU&< zJUqi=$#qS+K>RnGfTyyHi&~7OXhoo_d;eU({M#y58~@r@r3wPIiAio7m76or9k8}5 zYlo1?r)CuQh=OF;-Vo1^PrK2QI@%4E+Zw#zm*hU#oS?q)FD<0W+y9#CstO{5UV`hP z-<##~Zkz!Ms(({5SNS}>cYuySsl)JrBamJ( zfV&Q1NF+RdS2h1y)ByRyA9OAb{p)S*?b9J=UZnA8XBJNzx*d+wPH?8#6_})%nS3{N z@6ygpavOL>kE6K;C=iP>E6UXF%+#IVvV8A3g24bihng@|>a`vd|LC zc6Y*8Q`z>0_dyOQ2wp6*L1!_d%iTBX#p~!N)6y@!ID^r2<0Q|#84|*z2BW}h5mb

    CcY?6t}gye!E>+8GntsZ+m>Kz_>%WA@J|e*Ys7 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%0D%|6Qn+=i~ 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 m< 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 R9O8mK1|Jg%zmc{@8 delta 207952 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

    g*!ZJgDmZ&CgEsA1 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 diff --git a/docs/Presentations/CARGO/Dia2.JPG b/docs/Presentations/CARGO/Dia2.JPG index 626f6a55b842ae5b35612aca4fab54e455755ece..706c1c08ccf88c8558a5d260de4555df1af7df9b 100644 GIT binary patch delta 112024 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

    }QXxhghofNQuHoa*3B|C-~TPSA?) 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

    6e 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 Y!wtNSgq#j>MOW9M)Whp5ujGIK*+aiq7ytkO literal 211474 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

    H+4^@ZHpa`Dg?V%uF89%)n0_r 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 z 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

  • ?GOcvgEd=wo3n*~o5l_({w_RvSsNrZ&Z= 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

    K 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ðykl&kbJZnqdCb3i4gW_3%;aZusMkcby*f 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

    5jlV3oPYJGRVsHx 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

  • DL%BFyX^Q?ro1-D+{!ES7 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ëWw0dQU*N9F)H-8!7()*7rVKTa%me3#>2xONdS7)@y{$hFu+SvF&J&;hXXB8 zZOAu^iEc4pojwarg(msE3FTb>tdF@kcRT4To7-lrz3u_MbFRz3rp9f&Qg9Q}k zn)uLU&yG!MD$uaEu}g9fE(MJS53C|-|Vshd0E47x4wT;Xc2aHY6^V0S2S%kDhSw#ZE1kPyx5`CN$ zp|2)V07a7CHUuJ;^p0$(NX(NeQLRc~pc69!!zZy$il3VByod|GvCwSI8K$q&!@23a zn&CTkzdRK_8+BRau#_i!1B7sGQDXrW*bV@@6p%ghnX4|FV7K{lx}#S1@@_^z)5yLv zIAT}~4+Y+Ni64AAyS)aGrMrCg@QVUkx05M^t;|dr=`e)5O~L$qMmBeD`nf;{QnQ@` zPT^REYDfX4Bl6Is*`YyOwFiVoqDGV3mEYonqq0j1lV3=|<5yQBI?6TCo>#vDfGX*0 zvZWYvG$qp7$NDcu#wl>3-iLHuubx$c0T-vPe=%aU6atKc+Qf^lsfDQ4A^`0Q)bX+L zg^$=k`=-k(qEeU-93v|Gop4sivAR8CwFe2CdvuI$R?5Dbc1O!vV)HK#az_)a_3|nC zm)w~&oOQH?P5=3eL3bP9*8ub^WB>RGLg)+_x2{U^){nk;-D1A?eKFuLQwzHe* z{Fgo$OK1D%1s4&kqv%UotvpoZkqhfxj^sWKN=fuk|$zyM)Y zUBq4ym)c}nv)YeTG92a2`QODfc?|nTCoKm|v}4vFqY>Me;|yW9j*5RV6o0AiQ8ibM z(4qi*Sqz^;kuq4a=&uR~Px}VT9IA(8QmBaAgV+%fv9+r&mF$!yx4CbfbkUn+qQS&k9P2G#f3${gLO!CRGG?2-jQZH_axlJb%y2JnRf+pc z+Uh<1ddmg)3hb^sU0wZX-?U&na7834Hdq5}s8@+XF2&86Yn^{FI+krIl;?}qTG@w2 z)(RN}K7tCNp6SwPsm<9!n}zAo3fx-7vm$=%^GY_|se+pNxFBxcR3_1?zg8RcV*>b3|A{iBNZ9=7sCkrvQOM;0n{7Xha4DIojd5P9Z>6Rzr_Q;mY9IRx@qV6A+5s4 zY97#k+#MKc=73n8*`*jdu^>?#4f};x8`!A3QL#%O|JWYo$5v$fu0^{xe@OO>Jr^(y}+5IpM+J4p;RIYD!>;Sb)z-C= zNa>WPP@?sNXFCF$Q?f))^TJfM5uY7g1mKeyT0-vI2KtVEK~R3<1f~PYeEWfG#*>Zw zh`vHK!AkL)6IIPkU0ZK}tF&H<(6LJQJo>&xli3IHp(RkFa1B0bkoy$xz8C%KLW|gg zQI^8{`XQs7OqP{BDk5w>&JR=7OFsEh9Gem`q>6mrXl%pJ(?g0I*TT0C4-H=S$m7E$ z86N;-3K3@oO}kyXuQJkt7R}??dv^qiji(q|Q@!BL6->JctM<`=sJ^=s|E?B~P`W?s zh)MJ}QBlXYcnAT3@`iB=yI|}3^xwCeS?>;N6*p|Dg=Iq5&7NCEp~Yu8yicCYZ}Ub| z;E79xl|CXRF%%r*XaK^QKNBNj+98rU;6sAJ!?pFE!`jEB@qXYtW+ zfN9LE5@K5;bL&~aFG_BpXcPO!-}dX2u>MgE1oFYSdNxZ$#5RqhV6jD?`R99=E??kc z{}+k6W)aoChiE!@RLc+h9iyZ1n3kp80h6G+)=v^%T=D+vV1gz@D2Bj)zuT>YNT$Bf z#ZwduQIr_-7eklF@tJo71GZbmXRfhqAJ|-_sZqJ89{nOvn@#?z;&IO#h+UBKM_Vz2 zkl%POU-&c7GXT10iUg-pN#rs^+R?*_U7&b&N&VsouPiNVd|yUjVw~WNY8Z1x=luMAkMq9wyu?IK3x-N|lCV93pe=iQG9`2APKb<LK%H zR#0=Qywg_UEw(d7VzEr_+g7prNzMmmX$`La4B&QTy>_X@S-YG&U}%^t?-zIY-jk6J zp1-iw&ZnZn~TNqK~Dr88?i6+U{iKBmn*q7UT_Li7>!6G2157p4- zRP>fuh&aI$@&3kyw`pe3L8oqiPsFbNlJl*RrK8^9vm&)3Z(f9~i}jr!LFs8Pp838b zRk#QAd=H4%V3Lx4qV4+e=DJjJ-)WcZ2GGQddbgZ><@fz!>~@jMWA4DXkqp)v?V#;e zx{N8=l42LGKZVSWlaBY#E0D!VRO1uY`oF-ny`N$&I$RafE$_zrby$Ebc9s zoJiGtU}#-ZKOep!cJLmcv+^ysc)Z%K!7YB%5a>k&({C;5D`ab=>h_x)EUX(-bO52A z#t7FRuOi-e+Il80dJkiN7-z(70bS0wTT{wN#*HhEcFkxlYf}T@4?Ovn-Ji6FDV@)n zlrS+dQAz}x=GD9ZRj$@s)k_lpG4!Ldr<{3r<{Qi;P?9NQ@xrzA8EKNni)>!30E48= zrklE56J@=OMS_o_QabLjVTM5}_XIH3lt&i5w@d5LbE;NQ@@OrOs<2AY^-TIzmUd?h0BFH>1ix!?m2f}|1f5IP5=3A-uac|&2`|P0quLB z7ksb!)H)TzwWn2a{R46!BH#MS&za=%R-5|%OgmxCLDA<0=XW8@DM47#Rt z+PmLpCY+F!MSNZGFs%vB_vL#lVz|B|G3wwp*~MU(pyovgc*oAT$m@aqS7c1D+5P=i zWP~qTTD8x1&)CQ{`NqQ=%(a^ork>{v!O)Jl6Ydy$FH?vmu!$N_x=0?!gqxgRji?r(m3+y@6}I8^As67?t9(?O404TX2;Nk_7^>i=9tx3$z?#y&a|_)VsC1K#%@b$ z;GMX-0l_D;z$+mr z=-r+KY%p9p0RX!K2<5%eQa{yhKkF~HDPOGB{TB9~GPD8@z2fV<{-#9hSc8L@*71uz zoGyCWM(*;ry@`nNmFC8?-f)>6c8y^UYFw~zLQZ;X#_~XYVEI$U(b-gqDGSYI)9fUZ zv~j59!F|&1(BNLVAmufHZNGq{cA<)=s%rR&P%?Q6EAalV;^i!*nY(~L_=V|n5B#m` zjt)TE+s$?@|L-oX)tLnkrhhEVPzvcn_+w2i=NNC~g)_-mP@Ky$obsXb#k(>1!M!Vq zOtWB-AU2hVE0^{eB?@PZi#{%VXM^+;Q{RA8tgj)eBq=locPpF>j_11Berkk+@~p;t zNQ}J$PWw0%F(O5_JQQ_W!Cp46qr677dd~La#%X3wL`pJR-i2!51D@Moe|D#LfVkB_ zM6J&&t@KMOV){7mq&~BPqAS@aYr5^ZjiX&gHiZ&HLxeXW_X2)VFE3Ma^_fL=YdjmY z`RCqeBlteFvH@dM;aTm3-Df}#6#Com3zsefC1do-a?}|q19!M;NhLc3pQbu78&xq6 zTb!-p1R_;7AkktRsTQm79X843uwf;MUxcS&6W&)+Gh2q!5{q|zELEWn$E?I*V;apSagGP;QeEn|e8f*u`&n)GQ4lyn& zi@YerWpC*Fyq$pZxi90;NKUkI6JZ`q zGH~FESODWDHR3z@1A5g$In8jfaKL(3Tx7V)>NqmnGqKv3WU$^raajc3&hw6gO=DRM0va-tY; z%6Uc!_TIl%+NH6@9qp44S5fx8)`0P%Z+m|Jng0fF$c-ZNb{KwlI@HGlpaUUb)wlGf?KI3JopSof;D$A~Kyz z?IR-P`%?8*dAtmwZYTJY9u-u4W$*MCUO^nP)tmh*Pp(T9guYe{#)4Hkr=uM7bIo-t+P3M{VASnr zH-;}~#Ea#`|L8b--2YX@VV^w`0HX7+_$T6}!zepCBMdtbjp4xhVb!`AudUAFGwxJU zIH$5P_p6^>hqiB%6CwEzKX3>aieO zU6=+rk9EN~QoxZQf~qF4x#S221Yx};g4}B+`>?^EL6tjKjM@b8+GdjG@V{WQUvB4p z3B&2xR5qzkqxIzOg}Mam;wmt~%8}+KAy5o+6A_7a{7hc?tq&BrHDYtdg$k0IIzQ9o zi$0afcuj*c2^IyWD#DmGeLMTNfcjT;GZ&#*7A-o80OwDWnLioDBp-xRXm)PFU*OxY z;PbAWfC8uVXD%MZ`;GES^S9ydQ$sy7mz=^~B;gO%aV^}gRb?t!+6gO@<$tK5 z;ZLz{I`mMvXWbjMW%CER*y{r{TqH_Ukk;1NU0nB2Z&UxVAh!(I!}q5K6j-wbL=b4z z6Z)^K7>flhK22QJ8yk`6U#EOT8^RSJKY)OEgI1J+TGgK1`PJVFp2Jd~3%ZQgQ6x)g zALP7WFbBnjOy1oPXg=w?z6pZIBBWO5UmX8OG_?(~k&2BvdCc)`isUb*T@CV8wF_AP zGS#L^Q9v@@;=%zoT+irk>rT>@R@E-W-hV`IQNB()AJDRTub1GYWo$;H{$ zNzqG=r)WYLqJxaKj78vFtjbdN_4~hJAFi~HzeV6(2op0;0|3J~Fs2>$dg_oZ6VJXO zKuOW%b%*NjN;bufO(p+h01L$&#%#LXGQ-S%@(GYYlkM^~Atsmq1ZNvZj>w5jbHnt4 z-Z)5}7-kH{b+YYGS~$_+kX=mv0CS*CoB%pC#EYEw$>!dj?G70Bf36VgL{3%(felfb zy%k;*2-3mBnj~i$CfI9J3=U`>-Y^|u7``R%oi^FNt2U*hZpIO5`(0uIbw-WjPb^1b ztb4TI`NgXL$i9qMwge9HF?5=konF?}$WDP&n#$do$=z>1oum-g5tRVZ5VbHuONI0q z{Y5o7_7KB%zihxGdV!?!E?lD0ymF+(%I`6n_KVWakPS?QI_UI>Hh^)lU*Fu4J9XaK z?*LADUeCN88>3P!OMKtwFJ9A8U>fUp>H2M=N&bl%%J z{M>`)BgL<=`R!UdpaCZgt0wO0v6N^Ofhk8eM6GgOB;Lh_^)P2;I=Q5Nh?GmH`KD)P z$Z5lD{@+ecTvdfr*h_%^O07m@+`SG5YEKg9FUDPxkD)#B06DG)Z!+_((0H0Lbk(gh zqck;;T5o)*u%h(7cW(V(+R8gZ)n!d#V+1m5(h%uMx5Lduz?~Md@#@7jmEh0)OCTEJ z45eJ%^I($-%7|?B?LftlHBl(PF6ggq2<~{hcx8ZsiyldY^7Y>DY>|u`nEICOYIkyL z=Z$O78o_XIf91hs7>~tX0pn~#{0ODWbkFVSHH7c`_4Xex& zjlhv0{F8*tABsLH+tKe{foufCT!3+ZA?;P**T;>V3?a|o_zsSEHY&rgA12&es%*vDdR~HHA?V^U4(%BhbwQKG|%f;=M7H^5kG*rFVu+ny$x%rm` z#6V#Y&cg&l(x0G>E%({?mQF+Zjm_=maSqK|lE2+NdYai55R3nWM%v_h)Lp&76=d&v zyn7OKp4rZX{WNKh>Xf}Qv~#Of^>=5zrGTirakkx5_Up(OPP@0eUS-oqAjU|j05a#S zcSK6-3ZPkDw+-GFuyQFORIPDut8gMS;8x1Cr4w~1si7ruuJ~eW;_*T8owm=ZeK*FT z&x8b@_==Ieo||b%e{u!)-sPPCt_ayAJpj3^!tfb^Z<8MFiUB{xv2`-33UxcxgT@?1 zQ6XuVr&t4AEMERfvK_-)Ta60?cC@CpqaRY`ohnQPrarcLxbX@Z!11jHEz#lKBe!)q z5z($y5#W3o#w+KFrX53l*rQ^tSPK^{OhAanTGKQf9l+@V_DyCas`j}|p`Q4PJF=EP zzufL7O4uQ`Z?lQ$e_g)JSJbay&xnxtUiB)o>Gs~t-5V@#SmTma47TcnwQW(@oPa5G z>b=Ew1bYp26?g8$s5x~f4oHD|$(;|K8Yu21((UDrG#3N#A|gHCuQCm=ol!9(EO>*$wHtzGQDQuIcF&Y_wb@n_ z`1&l+lwt5?M9w7_*7Q36iUip3_?T6APwCO$+CARG;7Z(~2w$d{huxjU-z)*6UQ(fV z;JJVUb#zW4S^%S=H`xG_?k%7*-9omfQXuEtOeeC4?P5m?TU+>|>}3~}3#-(RbmA^c ze*Czg-88R_R!Ox|V@;wmxutKmC$`KVP^zV$$2@oca-^TYehn@H902nn1^JCi9Vs1q z&Ges5H5ShP=H{I3S?`(?Qij$8WM9XP;S%xeARHC)VRjHgEb_G;MIr3%LXtnCZBza+ z|F@ghOFhp98=@z@{9B)r#HgFM497Gjsh*8r>dk-L_gBZF>SM2nhlRQZn$B0F6jxTq1zBs?!V=o# z%K~N1(Xu7)4|(kKVEv?w-S#;!a8xqX12KS&7%r^9=l_Um zOM$<0l^d(1Lrl+`mZL1cq=sc+W6($t)B%;o=NJbCETE?Q4Z}Q!}q?rD7R$oq#)L=)H35P-R zcxWXWIY-l1cevz#`9j0w8r;v@K5>t{x0D>CA7F?Z8=bKlSAv? zQxE`EbBCMlV}fxrVLXiEm^7x7tsLi2L8;x7)#L@NsEoX~r;(3Z<}lx}coPCYr8qU6 zfun6)?!W5Avsmr?fp=?BW0{8!gJ#V!cbfu~4zOJ;S1w67LA(zn49NGjzQ;Qe>E5-r z=CE3ju{(&f<{HVubUMthBJgh4(eB_TFsAqv#I~aMw_qf=cpXc|1l-oEl1mZEzy3trg z9e$-)x(g3|@Ya>Pv8^G~i}ZOuwqEM?GvTy3;|%qty3Q=cAXI1-LgAf2le$#ndO`7} zVWE;FETrwV21bnF6XJ)1dgDcLQ&3I7!$T5sb6+hg@501Mn9u4ZRWF=b@V)rc>q)2~ zUNHnz%0!3bjpIV}Jw5H#f@KpJXZ#M|$))jz3^<9C4U~~*Y~^<`EV%r-91WC(N$*YV zY{?j47*n{zC6SGR-VFlD4!bi5H|+JFH_8fcj>9tcR@mCFQ?3Fx=B{p)I<*2m3z2oX z!NHM9+dR!#DRuHc!uwKA^5@>EPin!%alenhZDJj8u_b0E%M-vF>>eYYbwz5|jn~2> zDz~|tM5~*NE4Acvw1CzL!sf+iV}{ZEF=4rTo7`!GoSSCxobz1bQpA=Ni9K8RX}8-m z+KZoN9xyjj{|usf;`05Nv$q4_8{}lS{iUE|cU@fE7{xO?w_u#8@LS@U^ibChF~QgO zJ6_VqMm??kzX>t8meW!bdzO+QHtB=o)?w zA-P>D@J%PBtXn>_fPRN!3m@1$^aIqy{Ya+Oru=_ikKOI}GZd)UH=i+i%;FjSYIA;1&p}#8Uq~GxJKO(k(I-%Hyc=jF( zjV2MB2ZzKvasAiXhxd1|^^Jg(M#5izc_a1l-z_qiWPc%!qVw=)U?bKW4~s1%gL5}F z)gvs`gYQ)50KxKYPug-cFKAVXq75k_XUxJ9eU>leYEmqBrO-*Ul8&EQ|DpN#w)QI+xieqvO<)Ikfcdz+YY`Ec&BO zL82fq_vi?QyHmZ7jlNDgqa^L7yVArd%ZDZ{tfDXEo=dCm%^c3M=9v5Qe0u1$=%q^C z7EYw(f~VN~?E+~aB>LX{*$DH+g?z2Au#zlqb9d5>ET1u+^S`23Sryqn-GPmLgETxETh|&?NViWbzVNt+qTpqP2x?EI23%}*RANi68{6eE0De6J zr>cHD-d1KUdEGHl7*czuaR7V-waChz!3z|uIpFKE2=MOM|1DYgNg{Az=8vKYn>%T1 zN;vg<r)U#(j zLd|jBLzzP1-LXG&&*6FK-Cb>fx5+k@!KK{0jhK-wROmO-Zgu|f0-HWtz51%Xb@Ga- zgjl;R_Z4xIJ{!$Vv9*vEdsnub?IB$-Uz7-lPAeo2_z8{43bN1irEg|TTh?M9{oN}A zW~0vYISqF|Yz(K`5Hmv!@GF^AexB-X5pj~Y_$MMoD|+}xe~<)vGh+!5QF0ZX{erea z;;q6)rI7VMay;Tg*e)p7_VL~tY7;+jZF7f{^1UBmI@rW3tq`U- zcso)zPTcsqgS5X}Et~&GRMnE6@aY|>0+R)cF|uQSvfuB%tdDE=j6qX4{28XxmhRXi zo0@mA1I>2o*B49+?sG(}@fa~O(r(P2&A>5yJJYOhFapSr|v_46=TP`=qI#9!R2-9jEGgdSu10uH%bH z6uHaeEAGZJyp1$y@WZ*U)?bxDrFPaGusj3To&Fiug@2IwvlpM|k=NiBj|-bK4cISo zMtdTA1RiU|%@Alw$`+*qIi<4+O@9WLFRGoHW7Typb7y)7UK{;7o$@B8IZCxn)ZqEN zQR$p&=5nb9^yhah-S)T6q7v8K?o7ST0qlXBP2JPDCXBmbXG6jKQE_8)%H-tuv=-T6 z#c0wTHS)VKT{ctd{cH_BUuf-v+-?gyqD2;q>EDJhhY*i_4Z>!BTb$b~BsbgB4#3kEcRI4 ztUo>&Eg`?wJ6lc%qgrk5e0YLR?L~29tOMj%mDuUG4W6E5-NBFFYqbS4N7Se`pR+!@ zoHgG-)&%%UgsNPQpcf=Ni^%t+y0f3pEXI6p(WCOrv*{EnFnBvMnrIk!>LuPs17wt9#{-wcU>b+1x| zaN&Tj0sCEyob#S$&NRaRnwm2Wau(i3*>ntb0gowwu=21G!i`A+1XthRx*GQb3=WXc zk>eK@79OorF9`m#1gdw-UqR5NG*!$%&n|1L4S*X;;owb`|n<-g&DqU$4GvMoX z!M?V_qd@hJV`HHBJw`shOr@(ZME{lVadkdk;mZCve>KJps8^7zGSQ}J-1ZOX?-R~^ zXPG2k$M?S;$g;2-61j2@lhvzeyg>?+IBFFBGd6wZt>Z0t=kdnBAR(%!Y>7ajiV(HS z$Pt&dO(@IAUkKCMbwSvJfTmZd5J<(oU(l#jya#VO*L#Jc2X2d>fsio6jfI`065K}& z_&<|ee^j5D>!^VIKzVefAT#kz6CFN_0>v#*tJL1F~p4VCKA zjtyVX;7(1BQw@LisyH>s7!VVV>4g##<~T%;j}nSuP+YJLjY*YV>Uy!38MA4iigl`W zMR`J%h>e!o$w@vIb}Vw~F+Xs$VTL<3jXPu~+c(9NMTup`Nx#n?fA435feTKCJ+i=U z_Fyb5hQ0w0NM%{xk(%Kzbkf$h3U4|DKfQso^>K-K$l;NJr53&dR*>xjV+2CR@dTRs zd)NdcKYs8NShyc%;d4=)CkZ$R8yqb z?Xvjbg`lqLubVYn*m&W+Dh28FbnW>yQp%I|BB&MVrnL>Br$(zHY{c(ah(9wp7dKt_ zg;$-Z(A{O?j1wRU`w8CUnFMBxRB4$@C$Gjmt5xSG6Fy(x$x_gm@jv5Zg>ohKCF?lo zel3%gs&>-LLXf>oIw2O5_7!>^|%bSYGT0~>-$TI9tbd0;=7Xh=<0TMk))%lon)Ug zxxJ!J#+yLvmuw0pb+URdHX(@@z;X2goL7OH_L%C=IcJQ1jj3iOb?xt@ydQW++HIk& zIx~LD>Q`tD`^c`7rSJ29=3vKVdu&2sRw9^7GfO8k-LKe#WGgq?@+9KKI{K7p>X*mP zFj%`aKpK0c<*$esVWm|LmwL=VM8uh1fSc$54JLy+wA3V5Hn{9{Bz2$97@cl_-oiAu zy-CK`#7`u#3Ym)d*?E8l6tqF2F9)KEEuBUbe0_6mpi_T{p0ESsFl)eJwevkrW zm!&nGHO(Ju`~{c?gYac*I+KJ+WDTKsowf1wJXl9*v-c!0CZm9X(s@TIeoHD;86MMo zt5fK&D30$4hFoXBO;zKR0;T2yawgD5KbMuJZ<~BjG~-P!yxbOwU?K0<@{zAeZOF6M zug5*B`(FJapgV#uLBu=Z_5Fh&jMvIZxb6Ev@emj|MW9|%?<>WJuWeOY~M z3$&munXc%v`jYooNzScFS09ZNSf{n(Sf0J9`S~0pMGZ*Wb6x+ovdeD)CUp0%!o%hl z&XHZfJd^e@M+Se=k$&`IZ;k7E0{@34t)%q~vb23k2cTQ@%-j;Pt`UEjOx>tmof}9E zh{y-Th1F?UapFS*a91kVC6BM3dOQr3?(vH!Cy}*A=rn3+g7F&vw9i$IAhxS8Mo9f^ zpPQ-Ux;=kiF*aovsXbnSW5C`@i+WMm~=$@EE6KKn(L;1w+jK zhT(R@eeS2CT;woz>n3gcNuB(#UTT?dy{|C-%>9MRbLP zJ!U8dQw52)uGxv{lnD6g!5N@BPcQXpEcX7>XYzia-eeXh#+#cW78o3#`$=fKo z$`qn_{V|NJGuEYCq-aAxPdnF~d>HM}+Ji zM-0l9^{&RXiUR%YT0AI`uhX_jR0h3A2H#Q>iN-BYQ%?5!xN9=p*fFHLu(2@u3e+o6 z2K2~lk15F1&pfta;GbSD6olqhysPZ~?)4GPD$0L#B@u1-^ z8)Ao|)7iLV(M7OkA21F96{!B;@^O;f8K1k(*S$&t5c)eAFr=~VuQzaq)kiAVaFfmi zb9m;5j5z)XG45JEqD-5QNkXUo8?#BJxPA=hAcrDeeqmv6R=KJuopPUyBuRJ`?W#5| zzg?xhVCZE|!*j69-!btEvKT>lS>SOY0l#Ktcnfqt@xgK7^f&OwOz1S1&gU`6%uI}U z7z>aBO5~QWn?Qi@@b-U1-Y@Q?lveZ&}-V%iN?hw>&IRSs=9GA>Y5suJmZth38vmXb^^TbgNt|n?Wh= zJALpCYmg7!t>iX2yQYSEU2b6McT5CXV*n;XEUrHuggv}^j>N#CLf-yIbZ2O0$-;W! zI?`g`UlAwp{%0D)HXEchZuzw`8YE}VsC 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 z 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